Something went wrong. Try again.
various tools I have been using throughout the years
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234#define _POSIX_C_SOURCE 200809L
#include "mpd_common.h"
#include <errno.h>#include <fcntl.h>#include <limits.h>#include <poll.h>#include <signal.h>#include <spawn.h>#include <stdint.h>#include <stdlib.h>#include <string.h>#include <sys/wait.h>#include <unistd.h>#include <wordexp.h>
extern char **environ;
enum helper_state { HELPER_UNRESOLVED, HELPER_READY, HELPER_FAILED,};
static enum helper_state profile_helper_state;
#define HELPER_DEFAULT_TIMEOUT_SECONDS 10U#define HELPER_MAX_TIMEOUT_SECONDS 3600U#define PLAYER_SNAPSHOT_RETRIES 3U
static bool safe_helper_value(const char *value) { if (*value == '\0') return false; for (const unsigned char *cursor = (const unsigned char *)value; *cursor != '\0'; cursor++) { if (*cursor < 0x20U || *cursor == 0x7fU) return false; } return true;}
static bool parse_helper_port(const char *value) { if (*value == '\0' || *value == '-') return false; errno = 0; char *end; unsigned long port = strtoul(value, &end, 10); return errno == 0 && *end == '\0' && port > 0UL && port <= 65535UL;}
static unsigned helper_timeout_seconds(void) { const char *configured = getenv("MPD_CONFIG_HELPER_TIMEOUT"); if (configured == NULL || *configured == '\0') return HELPER_DEFAULT_TIMEOUT_SECONDS;
size_t timeout; if (!mt_parse_size(configured, &timeout) || timeout == 0U || timeout > HELPER_MAX_TIMEOUT_SECONDS) { fprintf(stderr, "mpd: MPD_CONFIG_HELPER_TIMEOUT must be between 1 and %u " "seconds\n", HELPER_MAX_TIMEOUT_SECONDS); return 0U; } return (unsigned)timeout;}
static bool monotonic_now(struct timespec *value) { if (clock_gettime(CLOCK_MONOTONIC, value) == 0) return true; perror("mpd: read monotonic clock"); return false;}
static int milliseconds_until(const struct timespec *deadline) { struct timespec now; if (!monotonic_now(&now)) return -1; time_t seconds = deadline->tv_sec - now.tv_sec; long nanoseconds = deadline->tv_nsec - now.tv_nsec; if (nanoseconds < 0) { seconds--; nanoseconds += 1000000000L; } if (seconds < 0 || (seconds == 0 && nanoseconds == 0)) return 0; if (seconds > (time_t)(INT_MAX / 1000)) return INT_MAX; long milliseconds = nanoseconds / 1000000L; if (nanoseconds % 1000000L != 0) milliseconds++; return (int)(seconds * 1000 + milliseconds);}
static void terminate_helper(pid_t pid) { if (kill(-pid, SIGKILL) != 0 && errno != ESRCH) perror("mpd: terminate config helper"); int status; while (waitpid(pid, &status, 0) < 0 && errno == EINTR) { }}
static bool wait_for_helper(pid_t pid, int *status, const struct timespec *deadline) { for (;;) { pid_t result = waitpid(pid, status, WNOHANG); if (result == pid) return true; if (result < 0) { if (errno == EINTR) continue; perror("mpd: wait for config helper"); return false; } int remaining = milliseconds_until(deadline); if (remaining <= 0) return false; int pause = remaining < 20 ? remaining : 20; struct timespec delay = {.tv_sec = 0, .tv_nsec = (long)pause * 1000000L}; while (nanosleep(&delay, &delay) < 0 && errno == EINTR) { } }}
static bool parse_helper_output(char *output, size_t length, const char *helper) { char *host = NULL; char *port = NULL; bool valid = true;
if (memchr(output, '\0', length) != NULL) valid = false; output[length] = '\0'; char *cursor = output; while (valid && *cursor != '\0') { char *line = cursor; char *newline = strchr(cursor, '\n'); if (newline == NULL) { cursor += strlen(cursor); } else { *newline = '\0'; cursor = newline + 1; } size_t line_length = strlen(line); while (line_length > 0U && line[line_length - 1U] == '\r') line[--line_length] = '\0'; if (line_length == 0U) continue;
char **destination = NULL; const char *value = NULL; if (strncmp(line, "MPD_HOST=", 9U) == 0) { destination = &host; value = line + 9; } else if (strncmp(line, "MPD_PORT=", 9U) == 0) { destination = &port; value = line + 9; } else { valid = false; continue; } if (*destination != NULL || !safe_helper_value(value) || (destination == &port && !parse_helper_port(value))) { valid = false; continue; } *destination = strdup(value); if (*destination == NULL) { perror("mpd: store config helper output"); valid = false; } }
if (host == NULL || port == NULL) valid = false; if (!valid) { fprintf(stderr, "mpd: config helper '%s' must print exactly valid MPD_HOST and " "MPD_PORT assignments\n", helper); } else if (setenv("MPD_HOST", host, 1) != 0 || setenv("MPD_PORT", port, 1) != 0) { perror("mpd: apply config helper output"); valid = false; }
free(host); free(port); return valid;}
static bool collect_helper_output(int descriptor, pid_t pid, const char *helper, unsigned timeout_seconds) { int flags = fcntl(descriptor, F_GETFL); if (flags < 0 || fcntl(descriptor, F_SETFL, flags | O_NONBLOCK) != 0) { perror("mpd: configure config helper output"); close(descriptor); terminate_helper(pid); return false; }
struct timespec deadline; if (!monotonic_now(&deadline)) { close(descriptor); terminate_helper(pid); return false; } deadline.tv_sec += (time_t)timeout_seconds;
char output[4098U]; size_t length = 0U; bool overflow = false; bool complete = false; while (!complete) { char discard[1024U]; void *destination = overflow ? (void *)discard : (void *)(output + length); size_t available = overflow ? sizeof(discard) : 4097U - length; ssize_t count = read(descriptor, destination, available); if (count > 0) { if (!overflow) { length += (size_t)count; if (length > 4096U) overflow = true; } continue; } if (count == 0) { complete = true; break; } if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { perror("mpd: read config helper output"); close(descriptor); terminate_helper(pid); return false; } if (errno == EINTR) continue;
int remaining = milliseconds_until(&deadline); if (remaining <= 0) break; struct pollfd poll_descriptor = {.fd = descriptor, .events = POLLIN}; int result; do { result = poll(&poll_descriptor, 1U, remaining); } while (result < 0 && errno == EINTR); if (result < 0) { perror("mpd: poll config helper output"); close(descriptor); terminate_helper(pid); return false; } if (result == 0) break; } if (close(descriptor) != 0) perror("mpd: close config helper output");
int status = 0; if (!complete || !wait_for_helper(pid, &status, &deadline)) { fprintf(stderr, "mpd: config helper '%s' timed out after %u seconds\n", helper, timeout_seconds); terminate_helper(pid); return false; } if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { fprintf(stderr, "mpd: config helper '%s' exited unsuccessfully\n", helper); return false; } if (overflow) { fprintf(stderr, "mpd: config helper '%s' output exceeds 4096 bytes\n", helper); return false; } return parse_helper_output(output, length, helper);}
static bool run_profile_helper(const char *helper) { unsigned timeout_seconds = helper_timeout_seconds(); if (timeout_seconds == 0U) return false; int descriptors[2]; if (pipe(descriptors) != 0) { perror("mpd: create config helper pipe"); return false; }
posix_spawn_file_actions_t actions; posix_spawnattr_t attributes; int error = posix_spawn_file_actions_init(&actions); bool actions_initialized = error == 0; bool attributes_initialized = false; if (error == 0) error = posix_spawn_file_actions_addclose(&actions, descriptors[0]); if (error == 0) error = posix_spawn_file_actions_adddup2(&actions, descriptors[1], STDOUT_FILENO); if (error == 0) error = posix_spawn_file_actions_addclose(&actions, descriptors[1]); if (error == 0) { error = posix_spawnattr_init(&attributes); attributes_initialized = error == 0; } if (error == 0) error = posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETPGROUP); if (error == 0) error = posix_spawnattr_setpgroup(&attributes, 0); if (error != 0) { fprintf(stderr, "mpd: prepare config helper: %s\n", strerror(error)); if (actions_initialized) { int destroy_error = posix_spawn_file_actions_destroy(&actions); if (destroy_error != 0) fprintf(stderr, "mpd: release config helper actions: %s\n", strerror(destroy_error)); } if (attributes_initialized) { int destroy_error = posix_spawnattr_destroy(&attributes); if (destroy_error != 0) fprintf(stderr, "mpd: release config helper attributes: %s\n", strerror(destroy_error)); } close(descriptors[0]); close(descriptors[1]); return false; }
pid_t pid; char *arguments[] = {(char *)helper, NULL}; error = posix_spawn(&pid, helper, &actions, &attributes, arguments, environ); int destroy_error = posix_spawn_file_actions_destroy(&actions); if (destroy_error != 0) fprintf(stderr, "mpd: release config helper actions: %s\n", strerror(destroy_error)); destroy_error = posix_spawnattr_destroy(&attributes); if (destroy_error != 0) fprintf(stderr, "mpd: release config helper attributes: %s\n", strerror(destroy_error)); if (error != 0) { fprintf(stderr, "mpd: start config helper '%s': %s\n", helper, strerror(error)); close(descriptors[0]); close(descriptors[1]); return false; }
close(descriptors[1]); return collect_helper_output(descriptors[0], pid, helper, timeout_seconds);}
static bool resolve_profile_helper(void) { if (profile_helper_state != HELPER_UNRESOLVED) return profile_helper_state == HELPER_READY;
const char *configured = getenv("MPD_CONFIG_HELPER"); if (configured != NULL && (*configured == '\0' || strcmp(configured, "off") == 0)) { profile_helper_state = HELPER_READY; return true; }
char *default_helper = NULL; const char *helper = configured; if (helper == NULL) { const char *home = getenv("HOME"); if (home == NULL || *home == '\0') { profile_helper_state = HELPER_READY; return true; } size_t home_length = strlen(home); if (home_length > SIZE_MAX - sizeof("/bin/amen")) { fprintf(stderr, "mpd: HOME is too long\n"); profile_helper_state = HELPER_FAILED; return false; } size_t length = home_length + sizeof("/bin/amen"); default_helper = malloc(length); if (default_helper == NULL) { perror("mpd: config helper path"); profile_helper_state = HELPER_FAILED; return false; } snprintf(default_helper, length, "%s/bin/amen", home); if (access(default_helper, X_OK) != 0) { free(default_helper); profile_helper_state = HELPER_READY; return true; } helper = default_helper; }
bool valid = run_profile_helper(helper); free(default_helper); profile_helper_state = valid ? HELPER_READY : HELPER_FAILED; return valid;}
struct mpd_connection *mt_connect_to(const char *host, unsigned port) { if (!resolve_profile_helper()) return NULL; struct mpd_connection *connection = mpd_connection_new(host, port, 0); if (connection == NULL) { fprintf(stderr, "mpd: out of memory while creating connection\n"); return NULL; }
if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) { mt_report_error(connection, "connect"); mpd_connection_free(connection); return NULL; } return connection;}
struct mpd_connection *mt_connect(void) { return mt_connect_to(NULL, 0U); }
void mt_report_error(const struct mpd_connection *connection, const char *context) { const char *message = connection == NULL ? "unknown MPD error" : mpd_connection_get_error_message(connection); fprintf(stderr, "mpd: %s: %s\n", context, message);}
void mt_strvec_init(struct mt_strvec *vec) { vec->items = NULL; vec->len = 0; vec->cap = 0;}
void mt_strvec_clear(struct mt_strvec *vec) { for (size_t i = 0; i < vec->len; i++) free(vec->items[i]); free(vec->items); mt_strvec_init(vec);}
bool mt_strvec_push_n(struct mt_strvec *vec, const char *value, size_t length) { if (vec->len == vec->cap) { size_t cap = vec->cap == 0 ? 16U : vec->cap * 2U; if (cap < vec->cap || cap > SIZE_MAX / sizeof(*vec->items)) { errno = ENOMEM; return false; } char **items = realloc(vec->items, cap * sizeof(*items)); if (items == NULL) return false; vec->items = items; vec->cap = cap; }
if (length == SIZE_MAX) { errno = ENOMEM; return false; } char *copy = malloc(length + 1U); if (copy == NULL) return false; memcpy(copy, value, length); copy[length] = '\0'; vec->items[vec->len] = copy; vec->len++; return true;}
bool mt_strvec_push(struct mt_strvec *vec, const char *value) { return mt_strvec_push_n(vec, value, strlen(value));}
bool mt_read_lines(FILE *input, struct mt_strvec *lines) { char *line = NULL; size_t capacity = 0; ssize_t length;
while ((length = getline(&line, &capacity, input)) >= 0) { while (length > 0 && (line[(size_t)length - 1U] == '\n' || line[(size_t)length - 1U] == '\r')) line[--length] = '\0'; if (length == 0 || line[0] == '#') continue; if (!mt_strvec_push(lines, line)) { perror("mpd: storing input line"); free(line); return false; } }
free(line); if (ferror(input)) { perror("mpd: reading input"); return false; } return true;}
FILE *mt_open_input(const char *path, bool *must_close) { if (path == NULL || strcmp(path, "-") == 0) { *must_close = false; return stdin; }
FILE *input = fopen(path, "r"); if (input == NULL) perror(path); *must_close = input != NULL; return input;}
static bool finish_list(struct mpd_connection *connection, const char *context) { if (!mpd_command_list_end(connection) || !mpd_response_finish(connection)) { mt_report_error(connection, context); return false; } return true;}
bool mt_add_queue(struct mpd_connection *connection, const struct mt_strvec *tracks, bool verbose) { for (size_t start = 0; start < tracks->len; start += MPD_TOOLS_BATCH_SIZE) { size_t end = start + MPD_TOOLS_BATCH_SIZE; if (end > tracks->len) end = tracks->len; if (!mpd_command_list_begin(connection, false)) { mt_report_error(connection, "begin add command list"); return false; } for (size_t i = start; i < end; i++) { if (!mpd_send_add(connection, tracks->items[i])) { mt_report_error(connection, "queue add"); return false; } } if (!finish_list(connection, "add tracks")) { fprintf(stderr, "mpd: failed input batch %zu..%zu\n", start + 1U, end); return false; } if (verbose) { for (size_t i = start; i < end; i++) printf("Added: %s\n", tracks->items[i]); } } return true;}
bool mt_insert_queue(struct mpd_connection *connection, const struct mt_strvec *tracks, unsigned position, bool verbose) { size_t available = (size_t)UINT_MAX - (size_t)position + 1U; if (tracks->len > available) { fprintf(stderr, "mpd: too many tracks for the requested queue position\n"); return false; } size_t inserted = 0; while (inserted < tracks->len) { size_t end = inserted + MPD_TOOLS_BATCH_SIZE; if (end > tracks->len) end = tracks->len; if (!mpd_command_list_begin(connection, false)) { mt_report_error(connection, "begin insert command list"); return false; } for (size_t i = inserted; i < end; i++) { if (i - inserted > UINT_MAX - position || !mpd_send_add_id_to(connection, tracks->items[i], position + (unsigned)(i - inserted))) { mt_report_error(connection, "queue insert"); return false; } } if (!finish_list(connection, "insert tracks")) return false; if (end < tracks->len) position += (unsigned)(end - inserted); if (verbose) { for (size_t i = inserted; i < end; i++) printf("Inserted: %s\n", tracks->items[i]); } inserted = end; } return true;}
bool mt_add_playlist(struct mpd_connection *connection, const char *playlist, const struct mt_strvec *tracks, bool verbose) { for (size_t start = 0; start < tracks->len; start += MPD_TOOLS_BATCH_SIZE) { size_t end = start + MPD_TOOLS_BATCH_SIZE; if (end > tracks->len) end = tracks->len; if (!mpd_command_list_begin(connection, false)) { mt_report_error(connection, "begin playlist command list"); return false; } for (size_t i = start; i < end; i++) { if (!mpd_send_playlist_add(connection, playlist, tracks->items[i])) { mt_report_error(connection, "playlist add"); return false; } } if (!finish_list(connection, "add playlist tracks")) return false; if (verbose) { for (size_t i = start; i < end; i++) printf("Added to %s: %s\n", playlist, tracks->items[i]); } } return true;}
static bool add_stream(struct mpd_connection *connection, const char *playlist, FILE *input, bool verbose, size_t *count) { struct mt_strvec batch; mt_strvec_init(&batch); char *line = NULL; size_t capacity = 0U; bool ok = true; *count = 0U;
while (ok) { ssize_t length = getline(&line, &capacity, input); if (length < 0) break; while (length > 0 && (line[(size_t)length - 1U] == '\n' || line[(size_t)length - 1U] == '\r')) line[--length] = '\0'; if (length == 0 || line[0] == '#') continue; if (!mt_strvec_push(&batch, line)) { perror("mpd: storing input line"); ok = false; } if (ok && batch.len == MPD_TOOLS_BATCH_SIZE) { ok = playlist == NULL ? mt_add_queue(connection, &batch, verbose) : mt_add_playlist(connection, playlist, &batch, verbose); if (ok) *count += batch.len; mt_strvec_clear(&batch); mt_strvec_init(&batch); } } if (ok && ferror(input)) { perror("mpd: reading input"); ok = false; } if (ok && batch.len > 0U) { ok = playlist == NULL ? mt_add_queue(connection, &batch, verbose) : mt_add_playlist(connection, playlist, &batch, verbose); if (ok) *count += batch.len; } free(line); mt_strvec_clear(&batch); return ok;}
bool mt_add_queue_stream(struct mpd_connection *connection, FILE *input, bool verbose, size_t *count) { return add_stream(connection, NULL, input, verbose, count);}
bool mt_add_playlist_stream(struct mpd_connection *connection, const char *playlist, FILE *input, bool verbose, size_t *count) { return add_stream(connection, playlist, input, verbose, count);}
void mt_player_state_clear(struct mt_player_state *state) { free(state->current_uri); memset(state, 0, sizeof(*state)); state->song_pos = UINT_MAX; state->song_id = UINT_MAX;}
bool mt_get_player_state(struct mpd_connection *connection, struct mt_player_state *state) { for (unsigned attempt = 0U; attempt < PLAYER_SNAPSHOT_RETRIES; attempt++) { memset(state, 0, sizeof(*state)); state->song_pos = UINT_MAX; state->song_id = UINT_MAX;
struct mpd_status *status = mpd_run_status(connection); if (status == NULL) { mt_report_error(connection, "get status"); return false; } state->state = mpd_status_get_state(status); state->queue_version = mpd_status_get_queue_version(status); state->queue_length = mpd_status_get_queue_length(status); int song_pos = mpd_status_get_song_pos(status); int song_id = mpd_status_get_song_id(status); if (song_pos >= 0) state->song_pos = (unsigned)song_pos; if (song_id >= 0) state->song_id = (unsigned)song_id; mpd_status_free(status);
if (state->song_id == UINT_MAX) return true;
struct mpd_song *song = mpd_run_current_song(connection); if (song == NULL) { if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) { mt_report_error(connection, "get current song"); return false; } continue; } bool consistent = mpd_song_get_id(song) == state->song_id && mpd_song_get_pos(song) == state->song_pos; const char *uri = mpd_song_get_uri(song); bool has_uri = uri != NULL; if (consistent && has_uri) state->current_uri = strdup(uri); mpd_song_free(song); if (consistent && has_uri && state->current_uri == NULL) { perror("mpd: current song"); return false; } if (consistent) return true; } fprintf(stderr, "mpd: player changed repeatedly while reading its state\n"); return false;}
static bool receive_song_uris(struct mpd_connection *connection, struct mt_strvec *tracks) { struct mpd_entity *entity; while ((entity = mpd_recv_entity(connection)) != NULL) { if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) { const struct mpd_song *song = mpd_entity_get_song(entity); const char *uri = mpd_song_get_uri(song); if (uri != NULL && !mt_strvec_push(tracks, uri)) { mpd_entity_free(entity); perror("mpd: storing queue"); return false; } } mpd_entity_free(entity); } if (!mpd_response_finish(connection)) { mt_report_error(connection, "receive song list"); return false; } return true;}
bool mt_read_queue(struct mpd_connection *connection, struct mt_strvec *tracks) { if (!mpd_send_list_queue_meta(connection)) { mt_report_error(connection, "list queue"); return false; } return receive_song_uris(connection, tracks);}
bool mt_read_playlist(struct mpd_connection *connection, const char *playlist, struct mt_strvec *tracks) { if (!mpd_send_list_playlist(connection, playlist)) { mt_report_error(connection, "list stored playlist"); return false; } return receive_song_uris(connection, tracks);}
enum mt_state_match mt_player_state_is(struct mpd_connection *connection, unsigned expected_version, unsigned expected_song_id) { struct mpd_status *status = mpd_run_status(connection); if (status == NULL) { mt_report_error(connection, "check player state"); return MT_STATE_ERROR; } int song_id = mpd_status_get_song_id(status); bool song_matches = expected_song_id == UINT_MAX ? song_id < 0 : song_id >= 0 && (unsigned)song_id == expected_song_id; bool matches = mpd_status_get_queue_version(status) == expected_version && song_matches; mpd_status_free(status); return matches ? MT_STATE_MATCH : MT_STATE_CHANGED;}
static size_t first_uri(const struct mt_strvec *tracks, const char *uri) { if (uri == NULL) return SIZE_MAX; for (size_t i = 0; i < tracks->len; i++) { if (strcmp(tracks->items[i], uri) == 0) return i; } return SIZE_MAX;}
static bool add_queue_skipping(struct mpd_connection *connection, const struct mt_strvec *tracks, size_t skip) { size_t index = 0U; while (index < tracks->len) { while (index < tracks->len && index == skip) index++; if (index == tracks->len) break;
if (!mpd_command_list_begin(connection, false)) { mt_report_error(connection, "begin replacement add list"); return false; } size_t sent = 0U; while (index < tracks->len && sent < MPD_TOOLS_BATCH_SIZE) { if (index != skip) { if (!mpd_send_add(connection, tracks->items[index])) { mt_report_error(connection, "add replacement track"); return false; } sent++; } index++; } if (!finish_list(connection, "add replacement tracks")) return false; } return true;}
static bool restore_queue_snapshot(const struct mt_strvec *tracks, const struct mt_player_state *state) { struct mpd_connection *connection = mt_connect(); if (connection == NULL) return false; bool ok = mpd_run_clear(connection); if (!ok) mt_report_error(connection, "rollback: clear partial queue"); if (ok) ok = mt_add_queue(connection, tracks, false); if (ok && tracks->len > 0U && state->song_pos < tracks->len && (state->state == MPD_STATE_PLAY || state->state == MPD_STATE_PAUSE)) { ok = mpd_run_play_pos(connection, state->song_pos); if (!ok) { mt_report_error(connection, "rollback: restore playback position"); } else if (state->state == MPD_STATE_PAUSE) { ok = mpd_run_pause(connection, true); if (!ok) mt_report_error(connection, "rollback: restore paused state"); } } mpd_connection_free(connection); return ok;}
bool mt_replace_queue(struct mpd_connection *connection, const struct mt_strvec *tracks, unsigned expected_version, bool force) { struct mt_player_state state; struct mt_strvec original; mt_strvec_init(&original); bool snapshot_ready = false; bool state_changed = false; for (unsigned attempt = 0U; attempt < PLAYER_SNAPSHOT_RETRIES; attempt++) { if (!mt_get_player_state(connection, &state)) break; if (!force && expected_version != UINT_MAX && state.queue_version != expected_version) { fprintf(stderr, "mpd: queue changed while it was being edited; use " "--force to replace it anyway\n"); mt_player_state_clear(&state); break; } if (!mt_read_queue(connection, &original)) { mt_player_state_clear(&state); break; } enum mt_state_match match = mt_player_state_is(connection, state.queue_version, state.song_id); if (match == MT_STATE_MATCH) { snapshot_ready = true; break; } if (match == MT_STATE_CHANGED) state_changed = true; mt_player_state_clear(&state); mt_strvec_clear(&original); mt_strvec_init(&original); if (match == MT_STATE_ERROR) break; } if (!snapshot_ready) { if (state_changed) fprintf(stderr, "mpd: player changed repeatedly before replacement\n"); mt_strvec_clear(&original); return false; }
size_t keep = first_uri(tracks, state.current_uri); bool can_keep = keep != SIZE_MAX && state.song_pos != UINT_MAX && state.song_pos < state.queue_length; bool ok = true; bool mutation_started = false;
if (can_keep) { bool needs_trim = state.song_pos > 0U || state.song_pos + 1U < state.queue_length; if (needs_trim) { mutation_started = true; if (!mpd_command_list_begin(connection, false)) { mt_report_error(connection, "begin queue trim"); ok = false; } if (ok && state.song_pos + 1U < state.queue_length && !mpd_send_delete_range(connection, state.song_pos + 1U, state.queue_length)) { mt_report_error(connection, "delete queue suffix"); ok = false; } if (ok && state.song_pos > 0U && !mpd_send_delete_range(connection, 0U, state.song_pos)) { mt_report_error(connection, "delete queue prefix"); ok = false; } if (ok) ok = finish_list(connection, "trim queue around current song"); } if (ok) { mutation_started = true; ok = add_queue_skipping(connection, tracks, keep); } if (ok && keep > 0U && !mpd_run_move(connection, 0U, (unsigned)keep)) { mt_report_error(connection, "restore current queue position"); ok = false; } } else { mutation_started = true; if (!mpd_run_clear(connection)) { mt_report_error(connection, "clear queue"); ok = false; } if (ok) ok = mt_add_queue(connection, tracks, false); if (ok && tracks->len > 0U && (state.state == MPD_STATE_PLAY || state.state == MPD_STATE_PAUSE)) { if (!mpd_run_play_pos(connection, 0U)) { mt_report_error(connection, "restart playback"); ok = false; } else if (state.state == MPD_STATE_PAUSE && !mpd_run_pause(connection, true)) { mt_report_error(connection, "restore paused state"); ok = false; } } }
if (!ok && mutation_started) { fprintf(stderr, "mpd: replacement failed; restoring the previous queue\n"); if (!restore_queue_snapshot(&original, &state)) fprintf(stderr, "mpd: rollback failed; the queue may be partial\n"); }
mt_player_state_clear(&state); mt_strvec_clear(&original); return ok;}
static bool same_tracks(const struct mt_strvec *left, const struct mt_strvec *right) { if (left->len != right->len) return false; for (size_t i = 0U; i < left->len; i++) { if (strcmp(left->items[i], right->items[i]) != 0) return false; } return true;}
static unsigned long playlist_name_sequence;
static void temporary_playlist_name(char *buffer, size_t size, const char *purpose) { playlist_name_sequence++; struct timespec now = {0}; if (clock_gettime(CLOCK_REALTIME, &now) != 0) now.tv_nsec = (long)playlist_name_sequence; snprintf(buffer, size, "mpd-tools-%ld-%lld-%ld-%lu-%s", (long)getpid(), (long long)now.tv_sec, now.tv_nsec, playlist_name_sequence, purpose);}
static void remove_temporary_playlist(const char *playlist) { struct mpd_connection *cleanup = mt_connect(); if (cleanup == NULL) return; if (!mpd_run_rm(cleanup, playlist) && mpd_connection_get_error(cleanup) != MPD_ERROR_SERVER) mt_report_error(cleanup, "remove temporary playlist"); mpd_connection_free(cleanup);}
bool mt_replace_playlist(struct mpd_connection *connection, const char *playlist, const struct mt_strvec *tracks, const struct mt_strvec *expected, bool force) { if (tracks->len == 0U) { if (!force) { struct mt_strvec current; mt_strvec_init(¤t); bool read_ok = mt_read_playlist(connection, playlist, ¤t); bool unchanged = read_ok && same_tracks(expected, ¤t); mt_strvec_clear(¤t); if (!read_ok) return false; if (!unchanged) { fprintf(stderr, "mpd: stored playlist changed while it was being " "edited; use --force to replace it anyway\n"); return false; } } if (!mpd_run_playlist_clear(connection, playlist)) { mt_report_error(connection, "clear stored playlist"); return false; } return true; }
char staged[128U]; char backup[128U]; temporary_playlist_name(staged, sizeof(staged), "new"); temporary_playlist_name(backup, sizeof(backup), "old"); if (!mt_add_playlist(connection, staged, tracks, false)) { remove_temporary_playlist(staged); return false; }
if (!force) { struct mt_strvec current; mt_strvec_init(¤t); bool read_ok = mt_read_playlist(connection, playlist, ¤t); bool unchanged = read_ok && same_tracks(expected, ¤t); mt_strvec_clear(¤t); if (!read_ok) { remove_temporary_playlist(staged); return false; } if (!unchanged) { fprintf(stderr, "mpd: stored playlist changed while it was being " "edited; use --force to replace it anyway\n"); remove_temporary_playlist(staged); return false; } }
if (!mpd_run_rename(connection, playlist, backup)) { mt_report_error(connection, "move old stored playlist aside"); remove_temporary_playlist(staged); return false; } if (!mpd_run_rename(connection, staged, playlist)) { mt_report_error(connection, "install staged stored playlist"); struct mpd_connection *rollback = mt_connect(); if (rollback == NULL || !mpd_run_rename(rollback, backup, playlist)) fprintf(stderr, "mpd: playlist rollback failed; '%s' contains the " "backup\n", backup); if (rollback != NULL) mpd_connection_free(rollback); remove_temporary_playlist(staged); return false; } if (!mpd_run_rm(connection, backup)) { mt_report_error(connection, "remove stored playlist backup"); fprintf(stderr, "mpd: replacement succeeded, but backup '%s' remains\n", backup); return false; } return true;}
static bool run_editor(const char *path) { const char *editor = getenv("VISUAL"); if (editor == NULL || *editor == '\0') editor = getenv("EDITOR"); if (editor == NULL || *editor == '\0') editor = "vi";
wordexp_t words; int result = wordexp(editor, &words, WRDE_NOCMD); if (result != 0 || words.we_wordc == 0U) { fprintf(stderr, "mpd: invalid editor command\n"); if (result == 0) wordfree(&words); return false; }
char **args = calloc(words.we_wordc + 2U, sizeof(*args)); if (args == NULL) { perror("mpd: editor arguments"); wordfree(&words); return false; } for (size_t i = 0; i < words.we_wordc; i++) args[i] = words.we_wordv[i]; args[words.we_wordc] = (char *)path;
pid_t pid = fork(); if (pid == 0) { execvp(args[0], args); perror(args[0]); _exit(127); } if (pid < 0) { perror("mpd: fork editor"); free(args); wordfree(&words); return false; }
int status; while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { perror("mpd: wait for editor"); free(args); wordfree(&words); return false; } } free(args); wordfree(&words); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { fprintf(stderr, "mpd: editor exited unsuccessfully\n"); return false; } return true;}
static char *editor_file_template(void) { const char *directory = getenv("TMPDIR"); if (directory == NULL || *directory == '\0') directory = "/tmp"; size_t directory_length = strlen(directory); bool needs_slash = directory[directory_length - 1U] != '/'; const char suffix[] = "mpd_tools_XXXXXX"; if (directory_length > SIZE_MAX - sizeof(suffix) - (size_t)needs_slash) { errno = ENAMETOOLONG; return NULL; } size_t length = directory_length + (size_t)needs_slash + sizeof(suffix); char *path = malloc(length); if (path == NULL) return NULL; snprintf(path, length, "%s%s%s", directory, needs_slash ? "/" : "", suffix); return path;}
bool mt_edit_lines(struct mt_strvec *lines) { char *path = editor_file_template(); if (path == NULL) { perror("mpd: create editor file path"); return false; } int fd = mkstemp(path); if (fd < 0) { perror("mpd: create editor file"); free(path); return false; } FILE *file = fdopen(fd, "w"); if (file == NULL) { perror("mpd: open editor file"); close(fd); unlink(path); free(path); return false; } bool ok = true; for (size_t i = 0; i < lines->len; i++) { if (fprintf(file, "%s\n", lines->items[i]) < 0) { perror("mpd: write editor file"); ok = false; break; } } if (fclose(file) != 0) { perror("mpd: close editor file"); ok = false; }
if (ok) ok = run_editor(path); if (ok) { int read_fd = open(path, O_RDONLY | O_NOFOLLOW); if (read_fd < 0) { perror("mpd: reopen editor file"); ok = false; } else { file = fdopen(read_fd, "r"); if (file == NULL) { perror("mpd: read editor file"); close(read_fd); ok = false; } else { struct mt_strvec edited; mt_strvec_init(&edited); ok = mt_read_lines(file, &edited); if (fclose(file) != 0) { perror("mpd: close edited file"); ok = false; } if (ok) { mt_strvec_clear(lines); *lines = edited; } else { mt_strvec_clear(&edited); } } } }
if (unlink(path) != 0) perror("mpd: remove editor file"); free(path); return ok;}
bool mt_parse_size(const char *text, size_t *value) { if (text == NULL || *text == '\0' || *text == '-') return false; errno = 0; char *end; unsigned long long parsed = strtoull(text, &end, 10); if (errno != 0 || *end != '\0' || parsed > SIZE_MAX) return false; *value = (size_t)parsed; return true;}