#define _POSIX_C_SOURCE 200809L #include "mpd_common.h" #include #include #include #include #include #include #include #include #include #include #include #include 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; }