Commit Diff


commit - 31e05974545e3f509c95cb9df3729822f9d3e3d6
commit + 173ea72d580c5c3ddf834d0dae1b5e460e7aca83
blob - 581075845b1c4c214c56fed9f07a5d7fbb525804
blob + 0237e323f7eab7456591a8e0022e4f9c9e5f533b
--- .gitignore
+++ .gitignore
@@ -4,6 +4,8 @@
 bin/*
 # python
 .venv/
+__pycache__/
+*.py[cod]
 # zig
 .zig-cache
 zig-out
blob - 288cf09f95427e4f4072efcc28759f99eee863f6
blob + 0896a45076225f2350f9b0f4c72ff37cc072dc77
--- README.md
+++ README.md
@@ -54,7 +54,7 @@ Builds and copies binaries to `$HOME/.local/bin`:
 just deploy <target>
 ```
 
-`deploy mpd` installs all five `mpd_*` binaries. Deployed Python tools
+`deploy mpd` installs the configured `mpd_*` binaries. Deployed Python tools
 (diggah, lazymaster, nts, shuffle) are thin shims that run out of this
 repository's `.venv`, and deployed Ruby tools (hue, reflink-snap) run
 straight from the checkout; both need the checkout to stay in place.
@@ -62,7 +62,7 @@ straight from the checkout; both need the checkout to 
 ## Development
 
 ```sh
-just test                    # run the Rust, Zig, and Go test suites
+just test                    # run the Rust, Zig, Go, and MPD test suites
 just clippy                  # run Clippy over the Cargo workspace
 just rustfmt                 # format the Cargo workspace with rustfmt
 ```
blob - c431e972f2bea551cee2dd3b6b9584e3a2dc1ce0
blob + 16155eeab5531a0310685212cc3a4cccdfe5b6b8
--- justfile
+++ justfile
@@ -3,8 +3,8 @@ cargo := "cargo"
 
 deploy_dir := home_directory() / ".local/bin"
 
-# The five C binaries deployed under the umbrella name "mpd"
-mpd_bins := "mpd_add_to_playlist mpd_add_to_queue mpd_edit_queue mpd_update_library mpd_update_queue"
+# The MPD C binaries deployed under the umbrella name "mpd"
+mpd_bins := "mpd_add_to_playlist mpd_add_to_queue mpd_edit_queue mpd_update_library mpd_update_queue mpd_insert_next mpd_edit_playlist mpd_now_playing mpd_trim_queue mpd_report mpd_report_monthly"
 
 # Show this help message
 default:
@@ -39,6 +39,7 @@ test:
     {{ cargo }} test --workspace
     cd pracomer && zig build test
     cd wrd && go test ./...
+    python3 -m unittest discover -s mpd/tests -v
 
 # Run `cargo clippy` over the workspace
 clippy:
blob - 470a9d2fbe8cb274400e2916aa7169d44a728d57
blob + 9e9f7ddb5489025ed5c1191a8f4d23d36fc7ed24
--- knitfile
+++ knitfile
@@ -2,7 +2,7 @@ knit = require("knit")
 
 root = knit.abs(".")
 
-cflags := -Wall -Wextra -O2 -march=native -mtune=native -fanalyzer -Wshadow -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Werror=format-security -Wnull-dereference -Wdangling-pointer -Wtrampolines -Walloca -Wcast-align=strict -Wdate-time -Wfloat-equal -Wpointer-arith -Wredundant-decls -Wswitch-default -Wvla -Wwrite-strings
+cflags := -std=c17 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Wconversion -Wstrict-prototypes -O2 -march=native -mtune=native -fanalyzer -Wshadow -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Werror=format-security -Wnull-dereference -Wdangling-pointer -Wtrampolines -Walloca -Wcast-align=strict -Wdate-time -Wfloat-equal -Wpointer-arith -Wredundant-decls -Wswitch-default -Wvla -Wwrite-strings
 ldflags := -lmpdclient -Wl,-z,relro -Wl,-z,now
 cc := /usr/bin/cc
 
@@ -36,21 +36,40 @@ $ bin/speediness: speediness/build.zig speediness/buil
 $ bin/wrd: wrd/go.mod $(knit.glob("wrd/*.go"))
     cd wrd && go build -o $(root)/$output .
 
-$ bin/mpd_add_to_playlist: mpd/mpd_add_to_playlist.c
-    $cc $cflags -o $output $input $ldflags
+$ bin/mpd_add_to_playlist: mpd/mpd_add_to_playlist.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_add_to_playlist.c mpd/mpd_common.c $ldflags
 
-$ bin/mpd_add_to_queue: mpd/mpd_add_to_queue.c
-    $cc $cflags -o $output $input $ldflags
+$ bin/mpd_add_to_queue: mpd/mpd_add_to_queue.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_add_to_queue.c mpd/mpd_common.c $ldflags
 
-$ bin/mpd_edit_queue: mpd/mpd_edit_queue.c
-    $cc $cflags -o $output $input $ldflags
+$ bin/mpd_edit_queue: mpd/mpd_edit_queue.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_edit_queue.c mpd/mpd_common.c $ldflags
 
-$ bin/mpd_update_library: mpd/mpd_update_library.c
-    $cc $cflags -o $output $input $ldflags
+$ bin/mpd_update_library: mpd/mpd_update_library.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_update_library.c mpd/mpd_common.c $ldflags
 
-$ bin/mpd_update_queue: mpd/mpd_update_queue.c
-    $cc $cflags -o $output $input $ldflags
+$ bin/mpd_update_queue: mpd/mpd_update_queue.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_update_queue.c mpd/mpd_common.c $ldflags
 
+$ bin/mpd_insert_next: mpd/mpd_insert_next.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_insert_next.c mpd/mpd_common.c $ldflags
+
+$ bin/mpd_edit_playlist: mpd/mpd_edit_playlist.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_edit_playlist.c mpd/mpd_common.c $ldflags
+
+$ bin/mpd_now_playing: mpd/mpd_now_playing.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_now_playing.c mpd/mpd_common.c $ldflags
+
+$ bin/mpd_trim_queue: mpd/mpd_trim_queue.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_trim_queue.c mpd/mpd_common.c $ldflags
+
+$ bin/mpd_report: mpd/mpd_report.c mpd/mpd_common.c mpd/mpd_common.h
+    $cc $cflags -o $output mpd/mpd_report.c mpd/mpd_common.c $ldflags
+
+$ bin/mpd_report_monthly: mpd/mpd_report_monthly
+    cp mpd/mpd_report_monthly $output
+    chmod +x $output
+
 $ .venv-stamp: requirements.txt
     python3 -m venv .venv
     .venv/bin/pip install --quiet --require-hashes -r requirements.txt
@@ -88,7 +107,9 @@ $ all:VB: bin/dam bin/lastfm_rs bin/plants \
           bin/pmn bin/pracomer bin/speediness \
           bin/wrd \
           bin/mpd_add_to_playlist bin/mpd_add_to_queue bin/mpd_edit_queue \
-          bin/mpd_update_library bin/mpd_update_queue \
+          bin/mpd_update_library bin/mpd_update_queue bin/mpd_insert_next \
+          bin/mpd_edit_playlist bin/mpd_now_playing bin/mpd_trim_queue \
+          bin/mpd_report bin/mpd_report_monthly \
           bin/diggah bin/lazymaster bin/nts bin/shuffle bin/kundali \
           bin/hue bin/reflink-snap
 }
blob - 808c0e012b2192340043829e1ab568ca23eee246
blob + 4bec2cccc2a20e7291cb0ec0409101f37c6d65c5
--- mpd/README.md
+++ mpd/README.md
@@ -1,54 +1,63 @@
 # mpd
 
-A collection of C utilities for controlling [MPD](https://www.musicpd.org/) via libmpdclient.
+Small C utilities for controlling [MPD](https://www.musicpd.org/) through
+libmpdclient. List inputs contain one MPD URI per line; empty lines and lines
+starting with `#` are ignored. Use `-` for stdin.
 
-## Tools
+## Commands
 
-### mpd_add_to_queue
+| Command | Usage | Purpose |
+|---------|-------|---------|
+| `mpd_add_to_queue` | `[--verbose] [FILE\|-]` | Append tracks to the queue. |
+| `mpd_add_to_playlist` | `[--verbose] PLAYLIST [FILE\|-]` | Add tracks to a stored playlist; with terminal stdin, add the current song. |
+| `mpd_edit_queue` | `[--force]` | Edit the queue with `$VISUAL`, `$EDITOR`, or `vi`. |
+| `mpd_edit_playlist` | `[--force] PLAYLIST` | Edit a stored playlist. |
+| `mpd_insert_next` | `[--verbose] [FILE\|-]` | Insert tracks after the current song, or at the front. |
+| `mpd_now_playing` | `[--watch] [--json]` | Print the current song; watch mode uses MPD idle events. |
+| `mpd_trim_queue` | `[--keep N]` | Remove played entries, optionally retaining the previous `N`. |
+| `mpd_update_queue` | `[--force] [FILE\|-]` | Replace the queue while preserving playback where possible. |
+| `mpd_update_library` | `[--no-wait] [--rescan] [FILE\|-]` | Update unique top-level directories, or the whole database with terminal stdin. |
+| `mpd_report` | `[OPTIONS]` | Find database entries by `Last-Modified` time window. |
+| `mpd_report_monthly` | `MONTH [YEAR] [MPD_REPORT_OPTION ...]` | Write the first four weekly reports for a month. |
 
-Adds tracks to the MPD play queue. Reads a list of paths from a file or stdin.
+Queue editing detects concurrent changes and attempts to restore the previous
+queue after a replacement failure. Playlist edits stage non-empty replacements
+before moving the old playlist aside. `--force` bypasses concurrency checks.
 
-```sh
-mpd_add_to_queue [file]
-```
+`mpd_report` accepts repeatable `--window START,END` epoch-second ranges; `-`
+means unbounded. Directories are emitted by default; `--files` includes songs,
+`--path URI` limits the search, and `--relative` strips that prefix. Multiple
+windows produce `INDEX<TAB>URI`, including every matching overlapping window.
+`--tracks-output FILE` atomically writes all song URIs during the same scan.
 
-### mpd_add_to_playlist
+`mpd_report_monthly` is a Ruby helper for the four weekly reports created by
+`diggah -m MM -w`. Run `mpd_report_monthly 07 2026` to write
+`1_07_2026.txt` through `4_07_2026.txt`; append `--files`, `--path URI`, or
+other `mpd_report` options after `--`. It uses the current year when omitted,
+accepts `-o DIRECTORY`, and replaces reports atomically after a successful MPD
+query.
 
-Adds tracks to a named MPD playlist. Reads a list of paths from a file or stdin.
-
-```sh
-mpd_add_to_playlist [file]
-```
-
-### mpd_edit_queue
-
-Opens the current MPD queue in `$EDITOR` as a list of file paths. Saves and replaces the queue with the edited result, preserving the currently playing track's position where possible.
-
-```sh
-mpd_edit_queue
-```
-
-### mpd_update_library
-
-Triggers an MPD library update and waits for it to complete, retrying on connection errors with exponential backoff.
-
-```sh
-mpd_update_library
-```
-
-### mpd_update_queue
-
-Replaces the current MPD queue with a new list of tracks read from a file or stdin.
-
-```sh
-mpd_update_queue [file]
-```
-
 ## Configuration
 
-All tools read connection settings from environment variables.
+| Variable | Default | Purpose |
+|----------|---------|---------|
+| `MPD_HOST` | `localhost` | Hostname, Unix socket, or `password@host`. |
+| `MPD_PORT` | `6600` | Server port. |
+| `MPD_TIMEOUT` | libmpdclient default | Connection timeout in seconds. |
+| `MPD_CONFIG_HELPER` | unset | Helper program to run. Set to `off` to skip `$HOME/bin/amen`. |
+| `MPD_CONFIG_HELPER_TIMEOUT` | `10` | Helper timeout in seconds (1–3600). |
 
-| Variable | Default | Description |
-|----------|---------|-------------|
-| `MPD_HOST` | `localhost` | MPD server hostname |
-| `MPD_PORT` | `6600` | MPD server port |
+If `MPD_CONFIG_HELPER` is unset, the tools try `$HOME/bin/amen`. If it is not
+executable, they connect using `MPD_HOST` and `MPD_PORT` as usual. The helper
+must print one `MPD_HOST=value` line and one `MPD_PORT=value` line. It is run
+without a shell and only once per process.
+
+Exit status is `0` for success, `1` for runtime errors, and `2` for invalid
+arguments.
+
+## Development
+
+```sh
+just build mpd_report mpd_report_monthly
+python3 -m unittest discover -s mpd/tests -v
+```
blob - cb46f773814fde2901c943921d1cc3e2e245306d
blob + 3e4bcfbcf7aa58375f53089ef30b23832d521174
--- mpd/mpd_add_to_playlist.c
+++ mpd/mpd_add_to_playlist.c
@@ -1,96 +1,96 @@
-#include <mpd/client.h>
-#include <stdio.h>
-#include <stdlib.h>
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
 #include <string.h>
-// #define DEBUG
-#ifdef DEBUG
-#define D(x)                                                                   \
-  do {                                                                         \
-    x;                                                                         \
-  } while (0)
-#else
-#define D(x)                                                                   \
-  do {                                                                         \
-  } while (0)
-#endif
-#define DEFAULT_HOST "localhost"
-#define DEFAULT_PORT 6600
-struct mpd_connection *conn() {
-  // Read host from environment variable, use default if not set
-  const char *host = getenv("MPD_HOST");
-  if (host == NULL || strlen(host) == 0) {
-    host = DEFAULT_HOST;
-  }
-  D(printf("Using host: %s\n", host));
+#include <unistd.h>
 
-  // Read port from environment variable, use default if not set
-  unsigned port = DEFAULT_PORT;
-  const char *port_str = getenv("MPD_PORT");
-  if (port_str != NULL && strlen(port_str) > 0) {
-    port = (unsigned)atoi(port_str);
-  }
-  D(printf("Using port: %u\n", port));
-
-  D(printf("%s %s:%u\n", "Connecting to", host, port));
-
-  struct mpd_connection *c = mpd_connection_new(host, port, 0);
-  if (c == NULL) {
-    fprintf(stderr, "Failed to create MPD connection object\n");
-    return NULL;
-  }
-
-  if (mpd_connection_get_error(c) != MPD_ERROR_SUCCESS) {
-    fprintf(stderr, "Error connecting to MPD (%s:%u): %s\n", host, port,
-            mpd_connection_get_error_message(c));
-    mpd_connection_free(c);
-    return NULL;
-  }
-#ifdef PASS
-  const char *pass = PASS;
-  if (mpd_run_password(c, pass) == false) {
-    fprintf(stderr, "Bad password\n");
-    mpd_connection_free(c);
-    return NULL;
-  }
-#endif
-  D(printf("%s %s:%u\n", "Connected to", host, port));
-  return c;
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--verbose] PLAYLIST [FILE|-]\n", program);
 }
-int main(int argc, char *argv[]) {
-  // Check for playlist name argument
-  if (argc < 2) {
-    printf("Usage: %s PLAYLIST_NAME\n", argv[0]);
-    return 1;
-  }
 
-  const char *playlist = argv[1];
-  D(printf("Using playlist: %s\n", playlist));
-
-  struct mpd_connection *c = conn();
-  if (c == NULL)
-    return -1;
-
-  struct mpd_song *curr = mpd_run_current_song(c);
-  if (curr == NULL) {
-    printf("No song is currently playing\n");
-    mpd_connection_free(c);
-    return -1;
+int main(int argc, char **argv) {
+  bool verbose = false;
+  const char *playlist = NULL;
+  const char *path = NULL;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
+    }
+    if (strcmp(argv[i], "--verbose") == 0) {
+      verbose = true;
+    } else if (argv[i][0] == '-' && strcmp(argv[i], "-") != 0) {
+      usage(stderr, argv[0]);
+      return 2;
+    } else if (playlist == NULL) {
+      playlist = argv[i];
+    } else if (path == NULL) {
+      path = argv[i];
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
+    }
   }
+  if (playlist == NULL) {
+    usage(stderr, argv[0]);
+    return 2;
+  }
 
-  const char *curr_uri = mpd_song_get_uri(curr);
-  D(printf("Currently playing: %s\n", curr_uri));
+  struct mt_strvec tracks;
+  mt_strvec_init(&tracks);
+  struct mpd_connection *connection = NULL;
+  bool ok = true;
+  size_t count = 0U;
 
-  if (mpd_run_playlist_add(c, playlist, curr_uri)) {
-    printf("%s %s %s %s\n", "Added", curr_uri, "to playlist", playlist);
+  if (path == NULL && isatty(STDIN_FILENO)) {
+    connection = mt_connect();
+    if (connection == NULL)
+      return 1;
+    struct mpd_song *song = mpd_run_current_song(connection);
+    if (song == NULL) {
+      if (mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS)
+        fprintf(stderr, "mpd: no current song\n");
+      else
+        mt_report_error(connection, "get current song");
+      ok = false;
+    } else {
+      const char *uri = mpd_song_get_uri(song);
+      ok = uri != NULL && mt_strvec_push(&tracks, uri);
+      if (!ok)
+        perror("mpd: store current song");
+      mpd_song_free(song);
+      if (ok)
+        count = 1U;
+    }
   } else {
-    printf("%s\n", "Some error");
-    mpd_song_free(curr);
-    mpd_connection_free(c);
-    return -1;
+    bool close_input;
+    FILE *input = mt_open_input(path, &close_input);
+    if (input == NULL)
+      return 1;
+    connection = mt_connect();
+    if (connection == NULL)
+      ok = false;
+    if (ok)
+      ok = mt_add_playlist_stream(connection, playlist, input, verbose, &count);
+    if (close_input && fclose(input) != 0) {
+      perror("mpd: close input");
+      ok = false;
+    }
   }
-  // Free resources
-  mpd_song_free(curr);
-  mpd_connection_free(c);
 
-  return 0;
+  if (ok && connection == NULL)
+    connection = mt_connect();
+  if (ok && connection == NULL)
+    ok = false;
+  if (ok && tracks.len > 0U)
+    ok = mt_add_playlist(connection, playlist, &tracks, verbose);
+  if (ok)
+    printf("Added %zu track%s to %s.\n", count, count == 1U ? "" : "s",
+           playlist);
+
+  if (connection != NULL)
+    mpd_connection_free(connection);
+  mt_strvec_clear(&tracks);
+  return ok ? 0 : 1;
 }
blob - dd788e7b53f502ff8375fe03b9db76b5ff75cf74
blob + cdd91a1c0402b854f74df10df2a334c1ac5359a3
--- mpd/mpd_add_to_queue.c
+++ mpd/mpd_add_to_queue.c
@@ -1,123 +1,56 @@
-#include <mpd/client.h>

-#include <stdbool.h>

-#include <stdio.h>

-#include <stdlib.h>

-#include <string.h>

-#include <unistd.h>

-

-// #define DEBUG

-#ifdef DEBUG

-#define D(x)                                                                   \

-  do {                                                                         \

-    x;                                                                         \

-  } while (0)

-#else

-#define D(x)                                                                   \

-  do {                                                                         \

-  } while (0)

-#endif

-

-#define DEFAULT_HOST "localhost"

-#define DEFAULT_PORT 6600

-

-struct mpd_connection *conn() {

-  const char *host = getenv("MPD_HOST");

-  if (host == NULL || strlen(host) == 0) {

-    host = DEFAULT_HOST;

-  }

-

-  unsigned port = DEFAULT_PORT;

-  const char *port_str = getenv("MPD_PORT");

-  if (port_str != NULL && strlen(port_str) > 0) {

-    port = (unsigned)atoi(port_str);

-  }

-

-  struct mpd_connection *c = mpd_connection_new(host, port, 0);

-  if (c == NULL) {

-    fprintf(stderr, "Failed to create MPD connection object\n");

-    return NULL;

-  }

-

-  enum mpd_error err = mpd_connection_get_error(c);

-  if (err != 0) {

-    fprintf(stderr, "Error connecting to MPD (%s:%u): %s (code: %u)\n", host,

-            port, mpd_connection_get_error_message(c), err);

-    mpd_connection_free(c);

-    return NULL;

-  }

-#ifdef PASS

-  const char *pass = PASS;

-  if (mpd_run_password(c, pass) == false) {

-    fprintf(stderr, "MPD authentication failed: %s\n",

-            mpd_connection_get_error_message(c));

-    mpd_connection_free(c);

-    return NULL;

-  }

-#endif

-  return c;

-}

-

-int main(int argc, char *argv[]) {

-  FILE *input = stdin;

-

-  if (argc > 1) {

-    input = fopen(argv[1], "r");

-    if (input == NULL) {

-      perror("Error opening file");

-      return 1;

-    }

-  } else if (isatty(STDIN_FILENO)) {

-    fprintf(stderr, "Reading from stdin... (Press Ctrl+D to finish, or provide "

-                    "a filename as argument)\n");

-  }

-

-  struct mpd_connection *c = conn();

-  if (c == NULL) {

-    if (input != stdin)

-      fclose(input);

-    return -1;

-  }

-

-  char *line = NULL;

-  size_t len = 0;

-  ssize_t read_bytes;

-  int added_count = 0;

-  int failed_count = 0;

-

-  while ((read_bytes = getline(&line, &len, input)) != -1) {

-    if (read_bytes > 0 && line[read_bytes - 1] == '\n') {

-      line[read_bytes - 1] = '\0';

-      read_bytes--;

-    }

-    if (read_bytes > 0 && line[read_bytes - 1] == '\r') {

-      line[read_bytes - 1] = '\0';

-      read_bytes--;

-    }

-

-    if (read_bytes == 0)

-      continue;

-    if (line[0] == '#')

-      continue;

-

-    if (!mpd_run_add(c, line)) {

-      fprintf(stderr, "Failed to add track: %s (Error: %s)\n", line,

-              mpd_connection_get_error_message(c));

-      failed_count++;

-      if (!mpd_connection_clear_error(c))

-        break;

-    } else {

-      printf("Added: %s\n", line);

-      added_count++;

-    }

-  }

-

-  printf("\nSummary: Added %d tracks, failed to add %d tracks.\n", added_count,

-         failed_count);

-

-  free(line);

-  if (input != stdin)

-    fclose(input);

-  mpd_connection_free(c);

-

-  return 0;

-}

+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <string.h>
+#include <unistd.h>
+
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--verbose] [FILE|-]\n", program);
+}
+
+int main(int argc, char **argv) {
+  bool verbose = false;
+  const char *path = NULL;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
+    }
+    if (strcmp(argv[i], "--verbose") == 0) {
+      verbose = true;
+    } else if (argv[i][0] == '-' && strcmp(argv[i], "-") != 0) {
+      usage(stderr, argv[0]);
+      return 2;
+    } else if (path == NULL) {
+      path = argv[i];
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
+    }
+  }
+
+  bool close_input;
+  FILE *input = mt_open_input(path, &close_input);
+  if (input == NULL)
+    return 1;
+  if (path == NULL && isatty(STDIN_FILENO))
+    fprintf(stderr, "Reading tracks from stdin; press Ctrl-D to finish.\n");
+
+  struct mpd_connection *connection = mt_connect();
+  if (connection == NULL) {
+    if (close_input)
+      fclose(input);
+    return 1;
+  }
+  size_t count = 0U;
+  bool ok = mt_add_queue_stream(connection, input, verbose, &count);
+  if (close_input && fclose(input) != 0) {
+    perror("mpd: close input");
+    ok = false;
+  }
+  if (ok)
+    printf("Added %zu track%s.\n", count, count == 1U ? "" : "s");
+  mpd_connection_free(connection);
+  return ok ? 0 : 1;
+}
blob - 26577918c531049c42e790c0120324fe2ea6ecb5
blob + 05b500433b6cda1d354555f5d36f88338dbf72f3
--- mpd/mpd_edit_queue.c
+++ mpd/mpd_edit_queue.c
@@ -1,220 +1,58 @@
-#include <mpd/client.h>
-#include <stdio.h>
-#include <stdlib.h>
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <limits.h>
 #include <string.h>
-#include <stdbool.h>
-#include <unistd.h>
-#include <sys/wait.h>
-#include <ctype.h>
-#include <fcntl.h>
 
-#define DEFAULT_HOST "localhost"
-#define DEFAULT_PORT 6600
-
-struct mpd_connection *conn() {
-    const char *host = getenv("MPD_HOST");
-    if (host == NULL || strlen(host) == 0) host = DEFAULT_HOST;
-    
-    unsigned port = DEFAULT_PORT;
-    const char *port_str = getenv("MPD_PORT");
-    if (port_str != NULL && strlen(port_str) > 0) port = (unsigned)atoi(port_str);
-
-    struct mpd_connection *c = mpd_connection_new(host, port, 0);
-    if (c == NULL) {
-        fprintf(stderr, "Failed to create MPD connection object\n");
-        return NULL;
-    }
-
-    if (mpd_connection_get_error(c) != MPD_ERROR_SUCCESS) {
-        fprintf(stderr, "Error connecting to MPD (%s:%u): %s\n", host, port, mpd_connection_get_error_message(c));
-        mpd_connection_free(c);
-        return NULL;
-    }
-    return c;
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--force]\n", program);
 }
 
-char* trim_whitespace(char* str) {
-    char *end;
-    while(isspace((unsigned char)*str)) str++;
-    if(*str == 0) return str;
-    end = str + strlen(str) - 1;
-    while(end > str && isspace((unsigned char)*end)) end--;
-    end[1] = '\0';
-    return str;
-}
-
-int main() {
-    struct mpd_connection *c = conn();
-    if (c == NULL) return 1;
-
-    char tmp_template[] = "/tmp/mpd_queue_XXXXXX";
-    int fd = mkstemp(tmp_template);
-    if (fd == -1) {
-        perror("mkstemp");
-        mpd_connection_free(c);
-        return 1;
+int main(int argc, char **argv) {
+  bool force = false;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
     }
-
-    FILE *f = fdopen(fd, "w");
-    if (f == NULL) {
-        perror("fdopen");
-        close(fd);
-        unlink(tmp_template);
-        mpd_connection_free(c);
-        return 1;
+    if (strcmp(argv[i], "--force") == 0) {
+      force = true;
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
     }
+  }
 
-    if (!mpd_send_list_queue_meta(c)) {
-        fprintf(stderr, "Error sending queue request\n");
-        fclose(f);
-        unlink(tmp_template);
-        mpd_connection_free(c);
-        return 1;
-    }
+  struct mt_strvec tracks;
+  mt_strvec_init(&tracks);
+  struct mpd_connection *connection = mt_connect();
+  if (connection == NULL)
+    return 1;
 
-    struct mpd_entity *entity;
-    while ((entity = mpd_recv_entity(c)) != NULL) {
-        if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) {
-            const struct mpd_song *song = mpd_entity_get_song(entity);
-            fprintf(f, "%s\n", mpd_song_get_uri(song));
-        }
-        mpd_entity_free(entity);
-    }
-    fclose(f);
+  struct mt_player_state state;
+  bool ok = mt_get_player_state(connection, &state);
+  unsigned version = UINT_MAX;
+  if (ok) {
+    version = state.queue_version;
+    mt_player_state_clear(&state);
+    ok = mt_read_queue(connection, &tracks);
+  }
+  mpd_connection_free(connection);
 
-    if (mpd_connection_get_error(c) != MPD_ERROR_SUCCESS) {
-        fprintf(stderr, "Error reading queue: %s\n", mpd_connection_get_error_message(c));
-        unlink(tmp_template);
-        mpd_connection_free(c);
-        return 1;
+  if (ok)
+    ok = mt_edit_lines(&tracks);
+  if (ok) {
+    connection = mt_connect();
+    if (connection == NULL)
+      ok = false;
+    else {
+      ok = mt_replace_queue(connection, &tracks, version, force);
+      mpd_connection_free(connection);
     }
-    mpd_response_finish(c);
-
-    const char *editor = getenv("EDITOR");
-    if (editor == NULL || strlen(editor) == 0) editor = "vi";
-
-    pid_t pid = fork();
-    if (pid == -1) {
-        perror("fork");
-        unlink(tmp_template);
-        mpd_connection_free(c);
-        return 1;
-    } else if (pid == 0) {
-        execlp(editor, editor, tmp_template, NULL);
-        perror("execlp");
-        exit(1);
-    }
-
-    int wstatus;
-    waitpid(pid, &wstatus, 0);
-
-    /* Re-open temp file with O_NOFOLLOW to prevent symlink attacks */
-    int fd2 = open(tmp_template, O_RDONLY | O_NOFOLLOW);
-    if (fd2 == -1) {
-        perror("open");
-        unlink(tmp_template);
-        mpd_connection_free(c);
-        return 1;
-    }
-    f = fdopen(fd2, "r");
-    if (f == NULL) {
-        perror("fdopen");
-        close(fd2);
-        unlink(tmp_template);
-        mpd_connection_free(c);
-        return 1;
-    }
-
-    char **new_tracks = NULL;
-    int track_count = 0;
-    char *line = NULL;
-    size_t len = 0;
-    ssize_t read_bytes;
-    char *current_file = NULL;
-    enum mpd_state state;
-    int current_pos;
-    int total_queue_len;
-    int new_pos = -1;
-
-    while ((read_bytes = getline(&line, &len, f)) != -1) {
-        char *trimmed = trim_whitespace(line);
-        if (strlen(trimmed) == 0 || trimmed[0] == '#') continue;
-
-        char **temp_tracks = realloc(new_tracks, sizeof(char *) * (track_count + 1));
-        if (temp_tracks == NULL) {
-            perror("realloc");
-            goto cleanup;
-        }
-        new_tracks = temp_tracks;
-        
-        new_tracks[track_count] = strdup(trimmed);
-        if (new_tracks[track_count] == NULL) {
-            perror("strdup");
-            goto cleanup;
-        }
-        track_count++;
-    }
-
-    /* Re-query MPD state after editor to avoid stale data */
-    struct mpd_status *status = mpd_run_status(c);
-    if (status == NULL) {
-        fprintf(stderr, "Error getting status: %s\n", mpd_connection_get_error_message(c));
-        goto cleanup;
-    }
-    state = mpd_status_get_state(status);
-    current_pos = mpd_status_get_song_pos(status);
-    total_queue_len = mpd_status_get_queue_length(status);
-    mpd_status_free(status);
-
-    if (state == MPD_STATE_PLAY || state == MPD_STATE_PAUSE) {
-        struct mpd_song *song = mpd_run_current_song(c);
-        if (song != NULL) {
-            current_file = strdup(mpd_song_get_uri(song));
-            mpd_song_free(song);
-        }
-    }
-
-    /* Compute new_pos from fresh current_file */
-    for (int i = 0; i < track_count; i++) {
-        if (current_file && strcmp(new_tracks[i], current_file) == 0 && new_pos == -1) {
-            new_pos = i;
-        }
-    }
-
-    if (!mpd_command_list_begin(c, false)) {
-        fprintf(stderr, "Failed to start command list: %s\n", mpd_connection_get_error_message(c));
-        goto cleanup;
-    }
-
-    if (state == MPD_STATE_STOP || current_file == NULL || new_pos == -1) {
-        mpd_send_clear(c);
-        for (int i = 0; i < track_count; i++) {
-            mpd_send_add(c, new_tracks[i]);
-        }
-        if (state != MPD_STATE_STOP) mpd_send_play(c);
-    } else {
-        for (int i = total_queue_len - 1; i >= 0; i--) {
-            if (i != current_pos) mpd_send_delete(c, i);
-        }
-        for (int i = 0; i < track_count; i++) {
-            if (i != new_pos) mpd_send_add(c, new_tracks[i]);
-        }
-        if (new_pos > 0) mpd_send_move(c, 0, new_pos);
-    }
-
-    if (!mpd_command_list_end(c) || !mpd_response_finish(c)) {
-        fprintf(stderr, "Failed to execute command list: %s\n", mpd_connection_get_error_message(c));
-    } else {
-        printf("Queue updated successfully.\n");
-    }
-
-cleanup:
-    free(line);
-    if (f) fclose(f);
-    unlink(tmp_template);
-    if (current_file) free(current_file);
-    for (int i = 0; i < track_count; i++) free(new_tracks[i]);
-    free(new_tracks);
-    mpd_connection_free(c);
-    return 0;
+  }
+  if (ok)
+    printf("Queue updated successfully.\n");
+  mt_strvec_clear(&tracks);
+  return ok ? 0 : 1;
 }
blob - /dev/null
blob + 9f3eefa2ad358537cd4a2e021c57b69acde83ed7 (mode 644)
--- /dev/null
+++ mpd/mpd_common.c
@@ -0,0 +1,1233 @@
+#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(&current);
+      bool read_ok = mt_read_playlist(connection, playlist, &current);
+      bool unchanged = read_ok && same_tracks(expected, &current);
+      mt_strvec_clear(&current);
+      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(&current);
+    bool read_ok = mt_read_playlist(connection, playlist, &current);
+    bool unchanged = read_ok && same_tracks(expected, &current);
+    mt_strvec_clear(&current);
+    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;
+}
blob - 9e8b94e562dee855eda37d4c848e2d1c428d70bd
blob + 0e843e665b489bd463cf0bfcedb78382033d1f66
--- mpd/mpd_update_library.c
+++ mpd/mpd_update_library.c
@@ -1,169 +1,207 @@
-#include <ctype.h>
-#include <mpd/client.h>
-#include <stdbool.h>
-#include <stdio.h>
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <errno.h>
 #include <stdlib.h>
 #include <string.h>
+#include <time.h>
 #include <unistd.h>
 
-#define DEFAULT_HOST "localhost"
-#define DEFAULT_PORT 6600
-#define DEFAULT_BACKOFF 5
+#define UPDATE_RETRIES 5U
 
-struct mpd_connection *conn() {
-  const char *host = getenv("MPD_HOST");
-  if (host == NULL || strlen(host) == 0)
-    host = DEFAULT_HOST;
-
-  unsigned port = DEFAULT_PORT;
-  const char *port_str = getenv("MPD_PORT");
-  if (port_str != NULL && strlen(port_str) > 0)
-    port = (unsigned)atoi(port_str);
-
-  struct mpd_connection *c = mpd_connection_new(host, port, 0);
-  if (c == NULL) {
-    fprintf(stderr, "Failed to create MPD connection object\n");
-    return NULL;
-  }
-
-  if (mpd_connection_get_error(c) != MPD_ERROR_SUCCESS) {
-    fprintf(stderr, "Error connecting to MPD (%s:%u): %s\n", host, port,
-            mpd_connection_get_error_message(c));
-    mpd_connection_free(c);
-    return NULL;
-  }
-  return c;
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--no-wait] [--rescan] [FILE|-]\n", program);
 }
 
-char *trim_whitespace(char *str) {
-  char *end;
-  while (isspace((unsigned char)*str))
-    str++;
-  if (*str == 0)
-    return str;
-  end = str + strlen(str) - 1;
-  while (end > str && isspace((unsigned char)*end))
-    end--;
-  end[1] = '\0';
-  return str;
+static int compare_strings(const void *left, const void *right) {
+  const char *const *a = left;
+  const char *const *b = right;
+  return strcmp(*a, *b);
 }
 
-int main(int argc, char *argv[]) {
-  if (argc > 1 &&
-      (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
-    printf("Usage: %s [input_file]\n", argv[0]);
-    return 0;
-  }
-
-  FILE *input = stdin;
-  if (argc > 1) {
-    input = fopen(argv[1], "r");
-    if (input == NULL) {
-      perror("Error opening file");
-      return 1;
+static bool top_directories(const struct mt_strvec *paths,
+                            struct mt_strvec *directories) {
+  for (size_t i = 0; i < paths->len; i++) {
+    const char *slash = strchr(paths->items[i], '/');
+    size_t length = slash == NULL ? strlen(paths->items[i])
+                                  : (size_t)(slash - paths->items[i]);
+    if (!mt_strvec_push_n(directories, paths->items[i], length)) {
+      perror("mpd: store update directory");
+      return false;
     }
-  } else if (isatty(STDIN_FILENO)) {
-    fprintf(stderr, "Reading from stdin... (Press Ctrl+D to finish)\n");
   }
 
-  char **top_dirs = NULL;
-  int dir_count = 0;
-  char *line = NULL;
-  size_t len = 0;
-  ssize_t read_bytes;
-
-  while ((read_bytes = getline(&line, &len, input)) != -1) {
-    char *trimmed = trim_whitespace(line);
-    if (strlen(trimmed) == 0 || trimmed[0] == '#')
-      continue;
-
-    char *slash = strchr(trimmed, '/');
-    if (slash != NULL)
-      *slash = '\0';
-
-    bool found = false;
-    for (int i = 0; i < dir_count; i++) {
-      if (strcmp(top_dirs[i], trimmed) == 0) {
-        found = true;
-        break;
-      }
-    }
-
-    if (!found) {
-      char **temp_dirs = realloc(top_dirs, sizeof(char *) * (dir_count + 1));
-      if (temp_dirs == NULL) {
-        perror("realloc");
-        break;
-      }
-      top_dirs = temp_dirs;
-      top_dirs[dir_count] = strdup(trimmed);
-      if (top_dirs[dir_count] == NULL) {
-        perror("strdup");
-        break;
-      }
-      dir_count++;
-    }
-  }
-  free(line);
-  if (input != stdin)
-    fclose(input);
-
-  if (dir_count == 0) {
-    printf("No directories found in input.\n");
-    for (int i = 0; i < dir_count; i++)
-      free(top_dirs[i]);
-    free(top_dirs);
-    return 0;
-  }
-
-  printf("Found %d unique top-level directories to update.\n", dir_count);
-
-  struct mpd_connection *c = conn();
-  if (c == NULL) {
-    for (int i = 0; i < dir_count; i++)
-      free(top_dirs[i]);
-    free(top_dirs);
-    return 1;
-  }
-
-  int backoff = DEFAULT_BACKOFF;
-  const char *backoff_str = getenv("MPD_UPDATE_BACKOFF");
-  if (backoff_str != NULL && strlen(backoff_str) > 0)
-    backoff = atoi(backoff_str);
-
-  int updated_count = 0;
-  int error_count = 0;
-
-  for (int i = 0; i < dir_count; i++) {
-    printf("Updating: '%s'\n", top_dirs[i]);
-    unsigned job_id = mpd_run_update(c, top_dirs[i]);
-    if (job_id == 0) {
-      fprintf(stderr, "  ✗ Failed: %s\n", mpd_connection_get_error_message(c));
-      error_count++;
-      mpd_connection_clear_error(c);
+  qsort(directories->items, directories->len, sizeof(*directories->items),
+        compare_strings);
+  size_t output = 0;
+  for (size_t i = 0; i < directories->len; i++) {
+    if (output > 0U &&
+        strcmp(directories->items[output - 1U], directories->items[i]) == 0) {
+      free(directories->items[i]);
     } else {
-      printf("  ✓ Success (Job ID: %u)\n", job_id);
-      updated_count++;
+      directories->items[output++] = directories->items[i];
     }
-    if (i < dir_count - 1 && backoff > 0)
-      sleep(backoff);
   }
+  directories->len = output;
+  return true;
+}
 
-  printf("Update completed!\n");
-  printf("Total directories processed: %d\n", dir_count);
-  printf("Successfully updated: %d\n", updated_count);
-  printf("Errors: %d\n", error_count);
+static void retry_delay(unsigned attempt) {
+  unsigned milliseconds = 250U << attempt;
+  if (milliseconds > 4000U)
+    milliseconds = 4000U;
+  struct timespec delay = {.tv_sec = (time_t)(milliseconds / 1000U),
+                           .tv_nsec = (long)(milliseconds % 1000U) * 1000000L};
+  while (nanosleep(&delay, &delay) < 0 && errno == EINTR) {
+  }
+}
 
-  struct mpd_status *status = mpd_run_status(c);
-  if (status != NULL) {
-    if (mpd_status_get_update_id(status) > 0)
-      printf("MPD is currently updating...\n");
+static struct mpd_connection *reconnect(unsigned attempt) {
+  if (attempt > 0U)
+    retry_delay(attempt - 1U);
+  return mt_connect();
+}
+
+static bool submit_directory(struct mpd_connection **connection,
+                             const char *directory, bool rescan) {
+  for (unsigned attempt = 0; attempt < UPDATE_RETRIES; attempt++) {
+    if (*connection == NULL)
+      *connection = reconnect(attempt);
+    if (*connection == NULL)
+      continue;
+
+    unsigned id = rescan ? mpd_run_rescan(*connection, directory)
+                         : mpd_run_update(*connection, directory);
+    if (id != 0U) {
+      printf("Submitted update for '%s' (job %u).\n", directory, id);
+      return true;
+    }
+
+    enum mpd_error error = mpd_connection_get_error(*connection);
+    mt_report_error(*connection, rescan ? "submit rescan" : "submit update");
+    if (error == MPD_ERROR_SERVER || error == MPD_ERROR_ARGUMENT)
+      return false;
+    mpd_connection_free(*connection);
+    *connection = NULL;
+  }
+  fprintf(stderr, "mpd: exhausted retries for '%s'\n", directory);
+  return false;
+}
+
+static bool wait_for_updates(struct mpd_connection **connection) {
+  for (unsigned failures = 0; failures < UPDATE_RETRIES;) {
+    if (*connection == NULL)
+      *connection = reconnect(failures);
+    if (*connection == NULL) {
+      failures++;
+      continue;
+    }
+
+    struct mpd_status *status = mpd_run_status(*connection);
+    if (status == NULL) {
+      mt_report_error(*connection, "check update status");
+      mpd_connection_free(*connection);
+      *connection = NULL;
+      failures++;
+      continue;
+    }
+    unsigned update_id = mpd_status_get_update_id(status);
     mpd_status_free(status);
+    if (update_id == 0U)
+      return true;
+
+    enum mpd_idle event = mpd_run_idle_mask(*connection, MPD_IDLE_UPDATE);
+    if ((event & MPD_IDLE_UPDATE) != 0) {
+      failures = 0;
+      continue;
+    }
+    mt_report_error(*connection, "wait for database update");
+    mpd_connection_free(*connection);
+    *connection = NULL;
+    failures++;
   }
+  fprintf(stderr, "mpd: exhausted retries while waiting for updates\n");
+  return false;
+}
 
-  for (int i = 0; i < dir_count; i++)
-    free(top_dirs[i]);
-  free(top_dirs);
-  mpd_connection_free(c);
-  return 0;
+int main(int argc, char **argv) {
+  bool wait = true;
+  bool rescan = false;
+  const char *path = NULL;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
+    }
+    if (strcmp(argv[i], "--no-wait") == 0) {
+      wait = false;
+    } else if (strcmp(argv[i], "--rescan") == 0) {
+      rescan = true;
+    } else if (argv[i][0] == '-' && strcmp(argv[i], "-") != 0) {
+      usage(stderr, argv[0]);
+      return 2;
+    } else if (path == NULL) {
+      path = argv[i];
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
+    }
+  }
+
+  struct mt_strvec paths;
+  struct mt_strvec directories;
+  mt_strvec_init(&paths);
+  mt_strvec_init(&directories);
+  bool ok = true;
+
+  if (path == NULL && isatty(STDIN_FILENO)) {
+    ok = mt_strvec_push(&directories, "");
+  } else {
+    bool close_input;
+    FILE *input = mt_open_input(path, &close_input);
+    if (input == NULL)
+      ok = false;
+    else {
+      ok = mt_read_lines(input, &paths);
+      if (close_input && fclose(input) != 0) {
+        perror("mpd: close input");
+        ok = false;
+      }
+      if (ok)
+        ok = top_directories(&paths, &directories);
+    }
+  }
+  if (!ok) {
+    mt_strvec_clear(&paths);
+    mt_strvec_clear(&directories);
+    return 1;
+  }
+  if (directories.len == 0U) {
+    printf("No directories found in input.\n");
+    mt_strvec_clear(&paths);
+    mt_strvec_clear(&directories);
+    return 0;
+  }
+
+  struct mpd_connection *connection = NULL;
+  for (size_t i = 0; i < directories.len; i++) {
+    if (!submit_directory(&connection, directories.items[i], rescan)) {
+      ok = false;
+      break;
+    }
+  }
+  if (ok && wait) {
+    printf("Waiting for database updates to finish...\n");
+    ok = wait_for_updates(&connection);
+  }
+  if (ok)
+    printf("Processed %zu director%s successfully.\n", directories.len,
+           directories.len == 1U ? "y" : "ies");
+
+  if (connection != NULL)
+    mpd_connection_free(connection);
+  mt_strvec_clear(&paths);
+  mt_strvec_clear(&directories);
+  return ok ? 0 : 1;
 }
blob - /dev/null
blob + de099c28158f491c076d26d30f3b16747d5a21e8 (mode 644)
--- /dev/null
+++ mpd/mpd_common.h
@@ -0,0 +1,78 @@
+#ifndef MPD_COMMON_H
+#define MPD_COMMON_H
+
+#include <mpd/client.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+
+#define MPD_TOOLS_BATCH_SIZE 512U
+
+struct mt_strvec {
+  char **items;
+  size_t len;
+  size_t cap;
+};
+
+struct mt_player_state {
+  enum mpd_state state;
+  unsigned queue_version;
+  unsigned queue_length;
+  unsigned song_pos;
+  unsigned song_id;
+  char *current_uri;
+};
+
+enum mt_state_match {
+  MT_STATE_ERROR,
+  MT_STATE_CHANGED,
+  MT_STATE_MATCH,
+};
+
+struct mpd_connection *mt_connect(void);
+struct mpd_connection *mt_connect_to(const char *host, unsigned port);
+void mt_report_error(const struct mpd_connection *connection,
+                     const char *context);
+
+void mt_strvec_init(struct mt_strvec *vec);
+void mt_strvec_clear(struct mt_strvec *vec);
+bool mt_strvec_push(struct mt_strvec *vec, const char *value);
+bool mt_strvec_push_n(struct mt_strvec *vec, const char *value, size_t length);
+bool mt_read_lines(FILE *input, struct mt_strvec *lines);
+FILE *mt_open_input(const char *path, bool *must_close);
+
+bool mt_add_queue(struct mpd_connection *connection,
+                  const struct mt_strvec *tracks, bool verbose);
+bool mt_insert_queue(struct mpd_connection *connection,
+                     const struct mt_strvec *tracks, unsigned position,
+                     bool verbose);
+bool mt_add_playlist(struct mpd_connection *connection, const char *playlist,
+                     const struct mt_strvec *tracks, bool verbose);
+bool mt_add_queue_stream(struct mpd_connection *connection, FILE *input,
+                         bool verbose, size_t *count);
+bool mt_add_playlist_stream(struct mpd_connection *connection,
+                            const char *playlist, FILE *input, bool verbose,
+                            size_t *count);
+
+bool mt_get_player_state(struct mpd_connection *connection,
+                         struct mt_player_state *state);
+void mt_player_state_clear(struct mt_player_state *state);
+bool mt_read_queue(struct mpd_connection *connection,
+                   struct mt_strvec *tracks);
+bool mt_read_playlist(struct mpd_connection *connection, const char *playlist,
+                      struct mt_strvec *tracks);
+enum mt_state_match mt_player_state_is(struct mpd_connection *connection,
+                                       unsigned expected_version,
+                                       unsigned expected_song_id);
+bool mt_replace_queue(struct mpd_connection *connection,
+                      const struct mt_strvec *tracks,
+                      unsigned expected_version, bool force);
+bool mt_replace_playlist(struct mpd_connection *connection,
+                         const char *playlist,
+                         const struct mt_strvec *tracks,
+                         const struct mt_strvec *expected, bool force);
+
+bool mt_edit_lines(struct mt_strvec *lines);
+bool mt_parse_size(const char *text, size_t *value);
+
+#endif
blob - 59bc41caf9a3305710d95dfc5b0a0df7c5d3cbb4
blob + 1df4ca3f93719a9bc0602c1c03e3a078e8a70aff
--- mpd/mpd_update_queue.c
+++ mpd/mpd_update_queue.c
@@ -1,139 +1,66 @@
-#include <mpd/client.h>
-#include <stdio.h>
-#include <stdlib.h>
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <limits.h>
 #include <string.h>
-#include <stdbool.h>
 #include <unistd.h>
 
-#define DEFAULT_HOST "localhost"
-#define DEFAULT_PORT 6600
-
-struct mpd_connection *conn() {
-    const char *host = getenv("MPD_HOST");
-    if (host == NULL || strlen(host) == 0) host = DEFAULT_HOST;
-    
-    unsigned port = DEFAULT_PORT;
-    const char *port_str = getenv("MPD_PORT");
-    if (port_str != NULL && strlen(port_str) > 0) port = (unsigned)atoi(port_str);
-
-    struct mpd_connection *c = mpd_connection_new(host, port, 0);
-    if (c == NULL) {
-        fprintf(stderr, "Failed to create MPD connection object\n");
-        return NULL;
-    }
-
-    if (mpd_connection_get_error(c) != MPD_ERROR_SUCCESS) {
-        fprintf(stderr, "Error connecting to MPD (%s:%u): %s\n", host, port, mpd_connection_get_error_message(c));
-        mpd_connection_free(c);
-        return NULL;
-    }
-    return c;
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--force] [FILE|-]\n", program);
 }
 
-int main(int argc, char *argv[]) {
-    if (argc < 2) {
-        fprintf(stderr, "Usage: %s <new_queue_file>\n", argv[0]);
-        return 1;
+int main(int argc, char **argv) {
+  bool force = false;
+  const char *path = NULL;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
     }
-
-    FILE *input = fopen(argv[1], "r");
-    if (input == NULL) {
-        perror("Error opening file");
-        return 1;
-    }
-
-    struct mpd_connection *c = conn();
-    if (c == NULL) {
-        fclose(input);
-        return 1;
-    }
-
-    char **new_tracks = NULL;
-    int track_count = 0;
-    char *line = NULL;
-    size_t len = 0;
-    ssize_t read_bytes;
-    char *current_file = NULL;
-
-    /* Read input file first, before querying MPD state */
-    while ((read_bytes = getline(&line, &len, input)) != -1) {
-        if (read_bytes > 0 && line[read_bytes - 1] == '\n') line[read_bytes - 1] = '\0';
-        if (read_bytes > 1 && line[read_bytes - 2] == '\r') line[read_bytes - 2] = '\0';
-        if (strlen(line) == 0 || line[0] == '#') continue;
-
-        char **temp_tracks = realloc(new_tracks, sizeof(char *) * (track_count + 1));
-        if (temp_tracks == NULL) {
-            perror("realloc");
-            goto cleanup;
-        }
-        new_tracks = temp_tracks;
-
-        new_tracks[track_count] = strdup(line);
-        if (new_tracks[track_count] == NULL) {
-            perror("strdup");
-            goto cleanup;
-        }
-        track_count++;
-    }
-
-    /* Re-query MPD state before building command list */
-    struct mpd_status *status = mpd_run_status(c);
-    if (status == NULL) {
-        fprintf(stderr, "Error getting status: %s\n", mpd_connection_get_error_message(c));
-        goto cleanup;
-    }
-    enum mpd_state state = mpd_status_get_state(status);
-    int current_pos = mpd_status_get_song_pos(status);
-    int total_queue_len = mpd_status_get_queue_length(status);
-    mpd_status_free(status);
-
-    if (state == MPD_STATE_PLAY || state == MPD_STATE_PAUSE) {
-        struct mpd_song *song = mpd_run_current_song(c);
-        if (song != NULL) {
-            current_file = strdup(mpd_song_get_uri(song));
-            mpd_song_free(song);
-        }
-    }
-
-    /* Compute new_pos from fresh current_file */
-    int new_pos = -1;
-    for (int i = 0; i < track_count; i++) {
-        if (current_file && strcmp(new_tracks[i], current_file) == 0 && new_pos == -1) {
-            new_pos = i;
-        }
-    }
-
-    if (!mpd_command_list_begin(c, false)) {
-        fprintf(stderr, "Failed to start command list: %s\n", mpd_connection_get_error_message(c));
-        goto cleanup;
-    }
-
-    if (state == MPD_STATE_STOP || current_file == NULL || new_pos == -1) {
-        mpd_send_clear(c);
-        for (int i = 0; i < track_count; i++) mpd_send_add(c, new_tracks[i]);
-        if (state != MPD_STATE_STOP) mpd_send_play(c);
+    if (strcmp(argv[i], "--force") == 0) {
+      force = true;
+    } else if (argv[i][0] == '-' && strcmp(argv[i], "-") != 0) {
+      usage(stderr, argv[0]);
+      return 2;
+    } else if (path == NULL) {
+      path = argv[i];
     } else {
-        for (int i = total_queue_len - 1; i >= 0; i--) {
-            if (i != current_pos) mpd_send_delete(c, i);
-        }
-        for (int i = 0; i < track_count; i++) {
-            if (i != new_pos) mpd_send_add(c, new_tracks[i]);
-        }
-        if (new_pos > 0) mpd_send_move(c, 0, new_pos);
+      usage(stderr, argv[0]);
+      return 2;
     }
+  }
 
-    if (!mpd_command_list_end(c) || !mpd_response_finish(c)) {
-        fprintf(stderr, "Failed to execute command list: %s\n", mpd_connection_get_error_message(c));
-    } else {
-        printf("Queue updated successfully.\n");
-    }
+  bool close_input;
+  FILE *input = mt_open_input(path, &close_input);
+  if (input == NULL)
+    return 1;
+  if (path == NULL && isatty(STDIN_FILENO))
+    fprintf(stderr,
+            "Reading the new queue from stdin; press Ctrl-D to finish.\n");
 
-cleanup:
-    free(line);
-    fclose(input);
-    if (current_file) free(current_file);
-    for (int i = 0; i < track_count; i++) free(new_tracks[i]);
-    free(new_tracks);
-    mpd_connection_free(c);
-    return 0;
+  struct mt_strvec tracks;
+  mt_strvec_init(&tracks);
+  bool ok = mt_read_lines(input, &tracks);
+  if (close_input && fclose(input) != 0) {
+    perror("mpd: close input");
+    ok = false;
+  }
+  if (!ok) {
+    mt_strvec_clear(&tracks);
+    return 1;
+  }
+
+  struct mpd_connection *connection = mt_connect();
+  if (connection == NULL) {
+    mt_strvec_clear(&tracks);
+    return 1;
+  }
+  ok = mt_replace_queue(connection, &tracks, UINT_MAX, force);
+  if (ok)
+    printf("Queue replaced with %zu track%s.\n", tracks.len,
+           tracks.len == 1U ? "" : "s");
+  mpd_connection_free(connection);
+  mt_strvec_clear(&tracks);
+  return ok ? 0 : 1;
 }
blob - /dev/null
blob + 3d6f2494fd3febf153765f1c122707de6e402103 (mode 644)
--- /dev/null
+++ mpd/mpd_edit_playlist.c
@@ -0,0 +1,69 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <string.h>
+
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--force] PLAYLIST\n", program);
+}
+
+int main(int argc, char **argv) {
+  bool force = false;
+  const char *playlist = NULL;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
+    }
+    if (strcmp(argv[i], "--force") == 0) {
+      force = true;
+    } else if (argv[i][0] == '-') {
+      usage(stderr, argv[0]);
+      return 2;
+    } else if (playlist == NULL) {
+      playlist = argv[i];
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
+    }
+  }
+  if (playlist == NULL) {
+    usage(stderr, argv[0]);
+    return 2;
+  }
+
+  struct mt_strvec original;
+  struct mt_strvec edited;
+  mt_strvec_init(&original);
+  mt_strvec_init(&edited);
+
+  struct mpd_connection *connection = mt_connect();
+  if (connection == NULL)
+    return 1;
+  bool ok = mt_read_playlist(connection, playlist, &original);
+  mpd_connection_free(connection);
+  for (size_t i = 0; ok && i < original.len; i++)
+    ok = mt_strvec_push(&edited, original.items[i]);
+  if (!ok && edited.len != original.len)
+    perror("mpd: copy stored playlist");
+
+  if (ok)
+    ok = mt_edit_lines(&edited);
+  if (ok) {
+    connection = mt_connect();
+    if (connection == NULL) {
+      ok = false;
+    } else {
+      ok = mt_replace_playlist(connection, playlist, &edited, &original, force);
+      mpd_connection_free(connection);
+    }
+  }
+  if (ok)
+    printf("Playlist %s replaced with %zu track%s.\n", playlist, edited.len,
+           edited.len == 1U ? "" : "s");
+
+  mt_strvec_clear(&original);
+  mt_strvec_clear(&edited);
+  return ok ? 0 : 1;
+}
blob - /dev/null
blob + 1de49c69c865e2418222cc96412f8b6a2242553d (mode 644)
--- /dev/null
+++ mpd/mpd_insert_next.c
@@ -0,0 +1,91 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <limits.h>
+#include <string.h>
+#include <unistd.h>
+
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--verbose] [FILE|-]\n", program);
+}
+
+int main(int argc, char **argv) {
+  bool verbose = false;
+  const char *path = NULL;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
+    }
+    if (strcmp(argv[i], "--verbose") == 0) {
+      verbose = true;
+    } else if (argv[i][0] == '-' && strcmp(argv[i], "-") != 0) {
+      usage(stderr, argv[0]);
+      return 2;
+    } else if (path == NULL) {
+      path = argv[i];
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
+    }
+  }
+
+  bool close_input;
+  FILE *input = mt_open_input(path, &close_input);
+  if (input == NULL)
+    return 1;
+  if (path == NULL && isatty(STDIN_FILENO))
+    fprintf(stderr, "Reading tracks from stdin; press Ctrl-D to finish.\n");
+
+  struct mt_strvec tracks;
+  mt_strvec_init(&tracks);
+  bool ok = mt_read_lines(input, &tracks);
+  if (close_input && fclose(input) != 0) {
+    perror("mpd: close input");
+    ok = false;
+  }
+  if (!ok) {
+    mt_strvec_clear(&tracks);
+    return 1;
+  }
+
+  struct mpd_connection *connection = mt_connect();
+  if (connection == NULL) {
+    mt_strvec_clear(&tracks);
+    return 1;
+  }
+  struct mt_player_state state;
+  ok = mt_get_player_state(connection, &state);
+  unsigned position = 0U;
+  if (ok && state.song_pos != UINT_MAX) {
+    if (state.song_pos == UINT_MAX - 1U) {
+      fprintf(stderr, "mpd: current queue position is too large\n");
+      ok = false;
+    } else {
+      position = state.song_pos + 1U;
+    }
+  }
+  if (ok) {
+    enum mt_state_match match =
+        mt_player_state_is(connection, state.queue_version, state.song_id);
+    if (match == MT_STATE_CHANGED) {
+      fprintf(stderr,
+              "mpd: player or queue changed before insertion; retry the "
+              "command\n");
+      ok = false;
+    } else if (match == MT_STATE_ERROR) {
+      ok = false;
+    }
+  }
+  if (ok)
+    ok = mt_insert_queue(connection, &tracks, position, verbose);
+  if (ok)
+    printf("Inserted %zu track%s at queue position %u.\n", tracks.len,
+           tracks.len == 1U ? "" : "s", position);
+
+  mt_player_state_clear(&state);
+  mpd_connection_free(connection);
+  mt_strvec_clear(&tracks);
+  return ok ? 0 : 1;
+}
blob - /dev/null
blob + 724f19a4fbf4447f416030a79b91d823ceccdfa6 (mode 644)
--- /dev/null
+++ mpd/mpd_now_playing.c
@@ -0,0 +1,234 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <errno.h>
+#include <signal.h>
+#include <string.h>
+#include <time.h>
+
+static volatile sig_atomic_t received_signal;
+
+static void handle_signal(int signal_number) {
+  received_signal = signal_number;
+}
+
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--watch] [--json]\n", program);
+}
+
+static const char *state_name(enum mpd_state state) {
+  switch (state) {
+  case MPD_STATE_STOP:
+    return "stopped";
+  case MPD_STATE_PLAY:
+    return "playing";
+  case MPD_STATE_PAUSE:
+    return "paused";
+  case MPD_STATE_UNKNOWN:
+  default:
+    return "unknown";
+  }
+}
+
+static void print_json_string(const char *value) {
+  if (value == NULL) {
+    fputs("null", stdout);
+    return;
+  }
+  putchar('"');
+  for (const unsigned char *cursor = (const unsigned char *)value;
+       *cursor != '\0'; cursor++) {
+    switch (*cursor) {
+    case '"':
+      fputs("\\\"", stdout);
+      break;
+    case '\\':
+      fputs("\\\\", stdout);
+      break;
+    case '\b':
+      fputs("\\b", stdout);
+      break;
+    case '\f':
+      fputs("\\f", stdout);
+      break;
+    case '\n':
+      fputs("\\n", stdout);
+      break;
+    case '\r':
+      fputs("\\r", stdout);
+      break;
+    case '\t':
+      fputs("\\t", stdout);
+      break;
+    default:
+      if (*cursor < 0x20U)
+        printf("\\u%04x", (unsigned)*cursor);
+      else
+        putchar((int)*cursor);
+    }
+  }
+  putchar('"');
+}
+
+static bool emit_snapshot(struct mpd_connection *connection, bool json) {
+  struct mpd_status *status = mpd_run_status(connection);
+  if (status == NULL) {
+    mt_report_error(connection, "get now-playing status");
+    return false;
+  }
+  enum mpd_state state = mpd_status_get_state(status);
+  int position = mpd_status_get_song_pos(status);
+  int song_id = mpd_status_get_song_id(status);
+  unsigned elapsed_ms = mpd_status_get_elapsed_ms(status);
+  unsigned duration = mpd_status_get_total_time(status);
+  mpd_status_free(status);
+
+  struct mpd_song *song = mpd_run_current_song(connection);
+  if (song == NULL &&
+      mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) {
+    mt_report_error(connection, "get now-playing song");
+    return false;
+  }
+
+  const char *artist =
+      song == NULL ? NULL : mpd_song_get_tag(song, MPD_TAG_ARTIST, 0U);
+  const char *album_artist =
+      song == NULL ? NULL : mpd_song_get_tag(song, MPD_TAG_ALBUM_ARTIST, 0U);
+  const char *album =
+      song == NULL ? NULL : mpd_song_get_tag(song, MPD_TAG_ALBUM, 0U);
+  const char *title =
+      song == NULL ? NULL : mpd_song_get_tag(song, MPD_TAG_TITLE, 0U);
+  const char *uri = song == NULL ? NULL : mpd_song_get_uri(song);
+
+  if (json) {
+    fputs("{\"state\":", stdout);
+    print_json_string(state_name(state));
+    fputs(",\"artist\":", stdout);
+    print_json_string(artist);
+    fputs(",\"album_artist\":", stdout);
+    print_json_string(album_artist);
+    fputs(",\"album\":", stdout);
+    print_json_string(album);
+    fputs(",\"title\":", stdout);
+    print_json_string(title);
+    fputs(",\"uri\":", stdout);
+    print_json_string(uri);
+    fputs(",\"elapsed_seconds\":", stdout);
+    if (song_id < 0)
+      fputs("null", stdout);
+    else
+      printf("%.3f", (double)elapsed_ms / 1000.0);
+    fputs(",\"duration_seconds\":", stdout);
+    if (song_id < 0 || duration == 0U)
+      fputs("null", stdout);
+    else
+      printf("%u", duration);
+    fputs(",\"queue_position\":", stdout);
+    if (position < 0)
+      fputs("null", stdout);
+    else
+      printf("%d", position);
+    fputs(",\"song_id\":", stdout);
+    if (song_id < 0)
+      fputs("null", stdout);
+    else
+      printf("%d", song_id);
+    fputs("}\n", stdout);
+  } else if (artist != NULL && title != NULL) {
+    printf("%s - %s\n", artist, title);
+  } else if (title != NULL) {
+    printf("%s\n", title);
+  } else if (uri != NULL) {
+    printf("%s\n", uri);
+  } else {
+    printf("%s\n", state_name(state));
+  }
+
+  if (song != NULL)
+    mpd_song_free(song);
+  if (fflush(stdout) != 0) {
+    perror("mpd: flush now-playing output");
+    return false;
+  }
+  return true;
+}
+
+static void reconnect_delay(unsigned failures) {
+  unsigned milliseconds = failures < 5U ? 250U << failures : 4000U;
+  if (milliseconds > 4000U)
+    milliseconds = 4000U;
+  struct timespec delay = {.tv_sec = (time_t)(milliseconds / 1000U),
+                           .tv_nsec = (long)(milliseconds % 1000U) * 1000000L};
+  while (received_signal == 0 && nanosleep(&delay, &delay) < 0 &&
+         errno == EINTR) {
+  }
+}
+
+int main(int argc, char **argv) {
+  bool watch = false;
+  bool json = false;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
+    }
+    if (strcmp(argv[i], "--watch") == 0) {
+      watch = true;
+    } else if (strcmp(argv[i], "--json") == 0) {
+      json = true;
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
+    }
+  }
+
+  if (!watch) {
+    struct mpd_connection *connection = mt_connect();
+    if (connection == NULL)
+      return 1;
+    bool ok = emit_snapshot(connection, json);
+    mpd_connection_free(connection);
+    return ok ? 0 : 1;
+  }
+
+  struct sigaction action;
+  memset(&action, 0, sizeof(action));
+  action.sa_handler = handle_signal;
+  sigemptyset(&action.sa_mask);
+  if (sigaction(SIGINT, &action, NULL) != 0 ||
+      sigaction(SIGTERM, &action, NULL) != 0) {
+    perror("mpd: install signal handlers");
+    return 1;
+  }
+
+  unsigned failures = 0U;
+  while (received_signal == 0) {
+    struct mpd_connection *connection = mt_connect();
+    if (connection == NULL) {
+      reconnect_delay(failures++);
+      continue;
+    }
+    if (!emit_snapshot(connection, json)) {
+      mpd_connection_free(connection);
+      reconnect_delay(failures++);
+      continue;
+    }
+    failures = 0U;
+
+    while (received_signal == 0) {
+      enum mpd_idle events =
+          mpd_run_idle_mask(connection, MPD_IDLE_PLAYER | MPD_IDLE_QUEUE);
+      if (received_signal != 0)
+        break;
+      if ((events & (MPD_IDLE_PLAYER | MPD_IDLE_QUEUE)) == 0)
+        break;
+      if (!emit_snapshot(connection, json))
+        break;
+    }
+    mpd_connection_free(connection);
+    if (received_signal == 0)
+      reconnect_delay(failures++);
+  }
+  return 128 + (int)received_signal;
+}
blob - /dev/null
blob + 21ea049c9fd2c480673e6ec649db75d6efa1aab3 (mode 644)
--- /dev/null
+++ mpd/mpd_report.c
@@ -0,0 +1,398 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <mpd/client.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+struct window {
+  time_t start;
+  time_t end;
+  bool has_start;
+  bool has_end;
+};
+
+struct options {
+  const char *host;
+  const char *path;
+  const char *tracks_output;
+  unsigned port;
+  bool include_files;
+  bool indexed;
+  bool relative;
+  struct window *windows;
+  size_t window_count;
+};
+
+struct atomic_file {
+  FILE *stream;
+  char *temporary_path;
+  const char *final_path;
+};
+
+static void usage(FILE *stream, const char *program) {
+  fprintf(
+      stream,
+      "Usage: %s [OPTIONS]\n"
+      "Query MPD's database for directories modified in one or more time "
+      "windows.\n\n"
+      "  -w, --window START,END   epoch-second bounds; '-' means unbounded\n"
+      "  -f, --files              include songs as well as directories\n"
+      "  -i, --indexed            prefix results with their window index\n"
+      "  -r, --relative           strip the requested MPD path prefix\n"
+      "  -p, --path URI           restrict the lookup to an MPD URI\n"
+      "      --tracks-output FILE atomically write every song URI to FILE\n"
+      "  -H, --host HOST          MPD host (default: $MPD_HOST or localhost)\n"
+      "  -P, --port PORT          MPD port (default: $MPD_PORT or 6600)\n"
+      "  -h, --help               show this help\n\n"
+      "One window writes plain URIs. Multiple windows (or --indexed) write\n"
+      "WINDOW_INDEX<TAB>URI. With no --window, the default is all time.\n"
+      "Date windows use exclusive bounds, like diggah's filesystem backend.\n",
+      program);
+}
+
+static bool parse_unsigned(const char *text, unsigned *value) {
+  char *end = NULL;
+  errno = 0;
+  unsigned long parsed = strtoul(text, &end, 10);
+  if (errno != 0 || end == text || *end != '\0' || parsed > UINT_MAX)
+    return false;
+  *value = (unsigned)parsed;
+  return true;
+}
+
+static bool parse_timestamp(const char *text, time_t *value) {
+  char *end = NULL;
+  errno = 0;
+  intmax_t parsed = strtoimax(text, &end, 10);
+  if (errno != 0 || end == text || *end != '\0')
+    return false;
+
+  time_t timestamp = (time_t)parsed;
+  if ((intmax_t)timestamp != parsed)
+    return false;
+  *value = timestamp;
+  return true;
+}
+
+static bool append_window(struct options *options, const char *argument) {
+  const char *comma = strchr(argument, ',');
+  if (comma == NULL || strchr(comma + 1, ',') != NULL) {
+    fprintf(stderr, "invalid window '%s' (expected START,END)\n", argument);
+    return false;
+  }
+
+  size_t start_length = (size_t)(comma - argument);
+  char *start = strndup(argument, start_length);
+  char *end = strdup(comma + 1);
+  if (start == NULL || end == NULL) {
+    perror("allocating window");
+    free(start);
+    free(end);
+    return false;
+  }
+
+  struct window window = {0};
+  window.has_start = strcmp(start, "-") != 0;
+  window.has_end = strcmp(end, "-") != 0;
+  bool valid = true;
+  if (window.has_start && !parse_timestamp(start, &window.start))
+    valid = false;
+  if (window.has_end && !parse_timestamp(end, &window.end))
+    valid = false;
+  if (window.has_start && window.has_end && window.start >= window.end)
+    valid = false;
+  free(start);
+  free(end);
+
+  if (!valid) {
+    fprintf(stderr, "invalid window '%s'\n", argument);
+    return false;
+  }
+
+  struct window *windows =
+      realloc(options->windows,
+              (options->window_count + 1U) * sizeof(*options->windows));
+  if (windows == NULL) {
+    perror("allocating windows");
+    return false;
+  }
+  options->windows = windows;
+  options->windows[options->window_count++] = window;
+  return true;
+}
+
+static bool parse_options(int argc, char **argv, struct options *options) {
+  *options = (struct options){.path = ""};
+
+  enum { OPT_TRACKS_OUTPUT = 256 };
+  static const struct option long_options[] = {
+      {"window", required_argument, NULL, 'w'},
+      {"files", no_argument, NULL, 'f'},
+      {"indexed", no_argument, NULL, 'i'},
+      {"relative", no_argument, NULL, 'r'},
+      {"path", required_argument, NULL, 'p'},
+      {"tracks-output", required_argument, NULL, OPT_TRACKS_OUTPUT},
+      {"host", required_argument, NULL, 'H'},
+      {"port", required_argument, NULL, 'P'},
+      {"help", no_argument, NULL, 'h'},
+      {NULL, 0, NULL, 0},
+  };
+
+  int option = 0;
+  while ((option = getopt_long(argc, argv, "w:firp:H:P:h", long_options,
+                               NULL)) != -1) {
+    switch (option) {
+    case 'w':
+      if (!append_window(options, optarg))
+        return false;
+      break;
+    case 'f':
+      options->include_files = true;
+      break;
+    case 'i':
+      options->indexed = true;
+      break;
+    case 'r':
+      options->relative = true;
+      break;
+    case 'p':
+      options->path = optarg;
+      break;
+    case 'H':
+      options->host = optarg;
+      break;
+    case 'P':
+      if (!parse_unsigned(optarg, &options->port) || options->port == 0U ||
+          options->port > 65535U) {
+        fprintf(stderr, "invalid MPD port '%s'\n", optarg);
+        return false;
+      }
+      break;
+    case OPT_TRACKS_OUTPUT:
+      options->tracks_output = optarg;
+      break;
+    case 'h':
+      usage(stdout, argv[0]);
+      exit(0);
+    default:
+      usage(stderr, argv[0]);
+      return false;
+    }
+  }
+
+  if (optind != argc) {
+    fprintf(stderr, "unexpected argument '%s'\n", argv[optind]);
+    return false;
+  }
+  if (options->window_count == 0U && !append_window(options, "-,-"))
+    return false;
+  return true;
+}
+
+static struct mpd_connection *connect_mpd(const struct options *options) {
+  return mt_connect_to(options->host, options->port);
+}
+
+static bool atomic_file_open(struct atomic_file *file, const char *path) {
+  *file = (struct atomic_file){.final_path = path};
+  size_t length = strlen(path) + sizeof(".tmp.XXXXXX");
+  file->temporary_path = malloc(length);
+  if (file->temporary_path == NULL) {
+    perror("allocating temporary path");
+    return false;
+  }
+  snprintf(file->temporary_path, length, "%s.tmp.XXXXXX", path);
+
+  int descriptor = mkstemp(file->temporary_path);
+  if (descriptor < 0) {
+    fprintf(stderr, "opening temporary track list for '%s': %s\n", path,
+            strerror(errno));
+    free(file->temporary_path);
+    file->temporary_path = NULL;
+    return false;
+  }
+  file->stream = fdopen(descriptor, "w");
+  if (file->stream == NULL) {
+    int error = errno;
+    close(descriptor);
+    unlink(file->temporary_path);
+    errno = error;
+    perror("opening temporary track list stream");
+    free(file->temporary_path);
+    file->temporary_path = NULL;
+    return false;
+  }
+  return true;
+}
+
+static void atomic_file_abort(struct atomic_file *file) {
+  if (file->stream != NULL)
+    fclose(file->stream);
+  if (file->temporary_path != NULL) {
+    unlink(file->temporary_path);
+    free(file->temporary_path);
+  }
+  *file = (struct atomic_file){0};
+}
+
+static bool atomic_file_commit(struct atomic_file *file) {
+  bool success = true;
+  int write_error = 0;
+  if (fflush(file->stream) != 0)
+    write_error = errno;
+  if (fsync(fileno(file->stream)) != 0 && write_error == 0)
+    write_error = errno;
+  if (fclose(file->stream) != 0 && write_error == 0)
+    write_error = errno;
+  if (write_error != 0) {
+    fprintf(stderr, "writing track list '%s': %s\n", file->final_path,
+            strerror(write_error));
+    success = false;
+  }
+  file->stream = NULL;
+  if (success && rename(file->temporary_path, file->final_path) != 0) {
+    fprintf(stderr, "installing track list '%s': %s\n", file->final_path,
+            strerror(errno));
+    success = false;
+  }
+  if (!success)
+    unlink(file->temporary_path);
+  free(file->temporary_path);
+  file->temporary_path = NULL;
+  return success;
+}
+
+static bool in_window(time_t modified, const struct window *window) {
+  if ((window->has_start || window->has_end) && modified == (time_t)0)
+    return false;
+  if (window->has_start && modified <= window->start)
+    return false;
+  if (window->has_end && modified >= window->end)
+    return false;
+  return true;
+}
+
+static const char *relative_uri(const char *uri,
+                                const struct options *options) {
+  if (!options->relative || options->path[0] == '\0')
+    return uri;
+
+  size_t prefix_length = strlen(options->path);
+  while (prefix_length > 0U && options->path[prefix_length - 1U] == '/')
+    prefix_length--;
+  if (strncmp(uri, options->path, prefix_length) != 0)
+    return uri;
+  if (uri[prefix_length] == '/')
+    return uri + prefix_length + 1U;
+  if (uri[prefix_length] == '\0')
+    return ".";
+  return uri;
+}
+
+static bool emit_matches(const struct options *options, const char *uri,
+                         time_t modified) {
+  const char *output_uri = relative_uri(uri, options);
+  for (size_t index = 0; index < options->window_count; index++) {
+    if (!in_window(modified, &options->windows[index]))
+      continue;
+    int written;
+    if (options->indexed || options->window_count > 1U)
+      written = printf("%zu\t%s\n", index, output_uri);
+    else
+      written = printf("%s\n", output_uri);
+    if (written < 0)
+      return false;
+  }
+  return true;
+}
+
+static bool query(const struct options *options) {
+  struct mpd_connection *connection = connect_mpd(options);
+  if (connection == NULL)
+    return false;
+
+  struct atomic_file tracks = {0};
+  if (options->tracks_output != NULL &&
+      !atomic_file_open(&tracks, options->tracks_output)) {
+    mpd_connection_free(connection);
+    return false;
+  }
+
+  bool success = mpd_send_list_all_meta(connection, options->path);
+  if (!success) {
+    fprintf(stderr, "requesting MPD database: %s\n",
+            mpd_connection_get_error_message(connection));
+  }
+
+  struct mpd_entity *entity = NULL;
+  while (success && (entity = mpd_recv_entity(connection)) != NULL) {
+    enum mpd_entity_type type = mpd_entity_get_type(entity);
+    const char *uri = NULL;
+    time_t modified = 0;
+
+    if (type == MPD_ENTITY_TYPE_DIRECTORY) {
+      const struct mpd_directory *directory = mpd_entity_get_directory(entity);
+      uri = mpd_directory_get_path(directory);
+      modified = mpd_directory_get_last_modified(directory);
+    } else if (type == MPD_ENTITY_TYPE_SONG) {
+      const struct mpd_song *song = mpd_entity_get_song(entity);
+      uri = mpd_song_get_uri(song);
+      modified = mpd_song_get_last_modified(song);
+      if (tracks.stream != NULL && fprintf(tracks.stream, "%s\n", uri) < 0) {
+        fprintf(stderr, "writing track list '%s': %s\n", tracks.final_path,
+                strerror(errno));
+        success = false;
+      }
+      if (!options->include_files)
+        uri = NULL;
+    }
+
+    if (success && uri != NULL && !emit_matches(options, uri, modified)) {
+      perror("writing results");
+      success = false;
+    }
+    mpd_entity_free(entity);
+  }
+
+  if (success && !mpd_response_finish(connection)) {
+    fprintf(stderr, "reading MPD database: %s\n",
+            mpd_connection_get_error_message(connection));
+    success = false;
+  }
+  if (success && fflush(stdout) != 0) {
+    perror("writing results");
+    success = false;
+  }
+
+  if (tracks.stream != NULL) {
+    if (success)
+      success = atomic_file_commit(&tracks);
+    else
+      atomic_file_abort(&tracks);
+  }
+  mpd_connection_free(connection);
+  return success;
+}
+
+int main(int argc, char **argv) {
+  struct options options;
+  if (!parse_options(argc, argv, &options)) {
+    free(options.windows);
+    return 2;
+  }
+
+  bool success = query(&options);
+  free(options.windows);
+  return success ? 0 : 1;
+}
blob - /dev/null
blob + c0f67d5514b70bf566ae9d403c65451e1d4e6ec7 (mode 755)
--- /dev/null
+++ mpd/mpd_report_monthly
@@ -0,0 +1,93 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "date"
+require "open3"
+require "optparse"
+require "tempfile"
+
+REPORT_COUNT = 4
+MANAGED_MPD_OPTIONS = %w[-i --indexed -w --window].freeze
+
+def main(arguments)
+  options = {output_directory: "."}
+  parser = OptionParser.new do |opts|
+    opts.banner = "Usage: #{opts.program_name} [-o DIRECTORY] MONTH [YEAR] [-- MPD_OPTION ...]"
+    opts.on("-o", "--output-dir DIRECTORY", "write reports beneath DIRECTORY") do |directory|
+      options[:output_directory] = directory
+    end
+    opts.on("-h", "--help", "show this help") do
+      puts opts
+      return 0
+    end
+  end
+
+  separator = arguments.index("--")
+  mpd_options = separator ? arguments.slice!((separator + 1)..) : []
+  arguments.pop if arguments.last == "--"
+  parser.parse!(arguments)
+
+  month = Integer(arguments.shift || raise(OptionParser::MissingArgument, "MONTH"), 10)
+  year = Integer(arguments.shift || Date.today.year, 10)
+  raise OptionParser::InvalidArgument, arguments.first unless arguments.empty?
+
+  first = Date.new(year, month, 1)
+  output_directory = options[:output_directory]
+  raise OptionParser::InvalidArgument, "not a directory: #{output_directory}" unless Dir.exist?(output_directory)
+
+  reserved = mpd_options.find do |option|
+    MANAGED_MPD_OPTIONS.include?(option) || option.start_with?("--window=")
+  end
+  raise OptionParser::InvalidArgument, "#{reserved} is managed by this helper" if reserved
+
+  command = ENV["MPD_REPORT"] || begin
+    sibling = File.join(__dir__, "mpd_report")
+    File.executable?(sibling) ? sibling : "mpd_report"
+  end
+
+  windows = REPORT_COUNT.times.flat_map do |week|
+    dates = [first + week * 7, first + (week + 1) * 7]
+    epochs = dates.map { |date| Time.local(date.year, date.month, date.day).to_i }
+    ["--window", epochs.join(",")]
+  end
+
+  stdout, stderr, status = Open3.capture3(command, *mpd_options, *windows)
+  warn stderr unless stderr.empty?
+  return status.exitstatus || 1 unless status.success?
+
+  reports = Array.new(REPORT_COUNT) { [] }
+  stdout.each_line do |line|
+    index, uri = line.chomp.split("\t", 2)
+    unless index&.match?(/\A[0-3]\z/) && uri
+      warn "#{parser.program_name}: malformed indexed output: #{line.chomp}"
+      return 1
+    end
+    reports[index.to_i] << uri
+  end
+
+  reports.each_with_index do |uris, index|
+    name = format("%d_%02d_%04d.txt", index + 1, month, year)
+    target = File.join(output_directory, name)
+    Tempfile.create([".#{name}.", ".tmp"], output_directory) do |file|
+      uris.each { |uri| file.puts(uri) }
+      file.flush
+      file.fsync
+      file.close
+      File.rename(file.path, target)
+    end
+    puts target
+  end
+  0
+rescue OptionParser::ParseError, ArgumentError => error
+  warn "#{parser.program_name}: #{error.message}"
+  warn parser
+  2
+rescue Errno::ENOENT => error
+  warn "#{parser.program_name}: #{error.message}"
+  127
+rescue SystemCallError => error
+  warn "#{parser.program_name}: #{error.message}"
+  1
+end
+
+exit main(ARGV)
blob - /dev/null
blob + c174881c601c9bf7943eb787c8359f9d15648e11 (mode 644)
--- /dev/null
+++ mpd/mpd_trim_queue.c
@@ -0,0 +1,59 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpd_common.h"
+
+#include <limits.h>
+#include <string.h>
+
+static void usage(FILE *file, const char *program) {
+  fprintf(file, "Usage: %s [--keep N]\n", program);
+}
+
+int main(int argc, char **argv) {
+  size_t keep = 0U;
+  for (int i = 1; i < argc; i++) {
+    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+      usage(stdout, argv[0]);
+      return 0;
+    }
+    if (strcmp(argv[i], "--keep") == 0 && i + 1 < argc) {
+      if (!mt_parse_size(argv[++i], &keep) || keep > UINT_MAX) {
+        fprintf(stderr, "mpd: invalid --keep value\n");
+        return 2;
+      }
+    } else {
+      usage(stderr, argv[0]);
+      return 2;
+    }
+  }
+
+  struct mpd_connection *connection = mt_connect();
+  if (connection == NULL)
+    return 1;
+  struct mt_player_state state;
+  bool ok = mt_get_player_state(connection, &state);
+  unsigned remove = 0U;
+  if (ok && state.song_pos != UINT_MAX && state.song_pos > (unsigned)keep)
+    remove = state.song_pos - (unsigned)keep;
+  if (ok && remove > 0U) {
+    enum mt_state_match match =
+        mt_player_state_is(connection, state.queue_version, state.song_id);
+    if (match == MT_STATE_CHANGED) {
+      fprintf(stderr, "mpd: player or queue changed before trimming; retry the "
+                      "command\n");
+      ok = false;
+    } else if (match == MT_STATE_ERROR) {
+      ok = false;
+    }
+  }
+  if (ok && remove > 0U && !mpd_run_delete_range(connection, 0U, remove)) {
+    mt_report_error(connection, "trim played tracks");
+    ok = false;
+  }
+  if (ok)
+    printf("Removed %u played track%s.\n", remove, remove == 1U ? "" : "s");
+
+  mt_player_state_clear(&state);
+  mpd_connection_free(connection);
+  return ok ? 0 : 1;
+}
blob - /dev/null
blob + 227a6851c9c4f9a6419b0ccc3bb7e821882d1799 (mode 644)
--- /dev/null
+++ mpd/tests/test_mpd_report.py
@@ -0,0 +1,149 @@
+import datetime
+import os
+import socketserver
+import subprocess
+import tempfile
+import threading
+import unittest
+from pathlib import Path
+
+BIN_OVERRIDE = os.environ.get("MPD_TEST_BIN")
+
+MPD_RESPONSE = (
+    "directory: Artist\n"
+    "Last-Modified: 2026-08-07T10:00:00Z\n"
+    "directory: Artist/Album\n"
+    "Last-Modified: 2026-08-08T10:00:00Z\n"
+    "file: Artist/Album/track.flac\n"
+    "Last-Modified: 2026-08-08T11:00:00Z\n"
+    "Time: 60\n"
+    "duration: 60.0\n"
+)
+
+
+class MpdHandler(socketserver.StreamRequestHandler):
+    def handle(self):
+        self.wfile.write(b"OK MPD 0.23.0\n")
+        self.wfile.flush()
+        while raw := self.rfile.readline():
+            command = raw.decode().rstrip("\r\n")
+            self.server.commands.append(command)
+            if command.startswith("listallinfo "):
+                self.wfile.write(MPD_RESPONSE.encode())
+                self.wfile.write(b"OK\n")
+            else:
+                name = command.split(maxsplit=1)[0]
+                self.wfile.write(
+                    f"ACK [5@0] {{{name}}} unsupported test command\n".encode()
+                )
+            self.wfile.flush()
+
+
+class MpdServer(socketserver.ThreadingTCPServer):
+    allow_reuse_address = True
+    daemon_threads = True
+
+    def __init__(self):
+        self.commands = []
+        super().__init__(("127.0.0.1", 0), MpdHandler)
+
+
+def epoch(day):
+    value = datetime.datetime.fromisoformat(day).replace(tzinfo=datetime.UTC)
+    return int(value.timestamp())
+
+
+class MpdReportTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        if BIN_OVERRIDE is not None:
+            cls.temporary_directory = None
+            cls.binary = Path(BIN_OVERRIDE) / "mpd_report"
+            return
+        cls.temporary_directory = tempfile.TemporaryDirectory()
+        cls.binary = Path(cls.temporary_directory.name) / "mpd_report"
+        source = Path(__file__).resolve().parents[1] / "mpd_report.c"
+        common = Path(__file__).resolve().parents[1] / "mpd_common.c"
+        subprocess.run(
+            [
+                "cc",
+                "-std=c17",
+                "-D_POSIX_C_SOURCE=200809L",
+                "-Wall",
+                "-Wextra",
+                "-Wconversion",
+                "-Wstrict-prototypes",
+                "-o",
+                str(cls.binary),
+                str(source),
+                str(common),
+                "-lmpdclient",
+            ],
+            check=True,
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.temporary_directory is not None:
+            cls.temporary_directory.cleanup()
+
+    def setUp(self):
+        self.server = MpdServer()
+        self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
+        self.thread.start()
+
+    def tearDown(self):
+        self.server.shutdown()
+        self.server.server_close()
+        self.thread.join(timeout=2)
+
+    def run_tool(self, *arguments):
+        environment = os.environ.copy()
+        environment.update(
+            MPD_HOST="127.0.0.1",
+            MPD_PORT=str(self.server.server_address[1]),
+            MPD_CONFIG_HELPER="off",
+        )
+        return subprocess.run(
+            [str(self.binary), *arguments],
+            text=True,
+            capture_output=True,
+            env=environment,
+            timeout=5,
+        )
+
+    def test_all_time_writes_plain_directories_and_tracks(self):
+        with tempfile.TemporaryDirectory() as directory:
+            tracks = Path(directory) / "all_tracks.txt"
+            result = self.run_tool("--tracks-output", str(tracks))
+
+            self.assertEqual(result.returncode, 0, result.stderr)
+            self.assertEqual(result.stdout, "Artist\nArtist/Album\n")
+            self.assertEqual(tracks.read_text(), "Artist/Album/track.flac\n")
+            self.assertEqual(self.server.commands, ['listallinfo ""'])
+
+    def test_multiple_windows_are_indexed_and_can_include_files(self):
+        first = f"{epoch('2026-08-07')},{epoch('2026-08-08')}"
+        second = f"{epoch('2026-08-08')},{epoch('2026-08-09')}"
+        result = self.run_tool("--files", "--window", first, "--window", second)
+
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(
+            result.stdout,
+            "0\tArtist\n" "1\tArtist/Album\n" "1\tArtist/Album/track.flac\n",
+        )
+
+    def test_overlapping_windows_emit_each_matching_index(self):
+        broad = f"{epoch('2026-08-07')},{epoch('2026-08-09')}"
+        narrow = f"{epoch('2026-08-08')},{epoch('2026-08-09')}"
+        result = self.run_tool("--window", broad, "--window", narrow)
+
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(
+            result.stdout,
+            "0\tArtist\n" "0\tArtist/Album\n" "1\tArtist/Album\n",
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
blob - /dev/null
blob + a3faac462834a8fb55c3f613628d24e441484578 (mode 644)
--- /dev/null
+++ mpd/tests/test_mpd_report_monthly.py
@@ -0,0 +1,107 @@
+import os
+import subprocess
+import tempfile
+import unittest
+from pathlib import Path
+
+
+HELPER = Path(__file__).resolve().parents[1] / "mpd_report_monthly"
+
+
+class MpdReportMonthlyTest(unittest.TestCase):
+    def make_fake(self, directory: Path, body: str) -> Path:
+        fake = directory / "mpd_report"
+        fake.write_text("#!/bin/sh\nset -eu\n" + body)
+        fake.chmod(0o755)
+        return fake
+
+    def run_helper(self, directory: Path, fake: Path, *arguments: str):
+        environment = os.environ.copy()
+        environment.update(MPD_REPORT=str(fake), TZ="UTC")
+        return subprocess.run(
+            [str(HELPER), "--output-dir", str(directory), *arguments],
+            text=True,
+            capture_output=True,
+            env=environment,
+            timeout=5,
+        )
+
+    def test_splits_indexed_results_and_builds_expected_windows(self):
+        with tempfile.TemporaryDirectory() as name:
+            directory = Path(name)
+            arguments_file = directory / "arguments"
+            fake = self.make_fake(
+                directory,
+                'printf "%s\\n" "$@" > "$ARGUMENTS_FILE"\n'
+                "printf '0\\tArtist\\n1\\tArtist/Album\\n3\\ttrack.flac\\n'\n",
+            )
+            environment = os.environ.copy()
+            environment.update(
+                MPD_REPORT=str(fake), ARGUMENTS_FILE=str(arguments_file), TZ="UTC"
+            )
+            result = subprocess.run(
+                [
+                    str(HELPER),
+                    "--output-dir",
+                    str(directory),
+                    "7",
+                    "2026",
+                    "--",
+                    "--files",
+                ],
+                text=True,
+                capture_output=True,
+                env=environment,
+                timeout=5,
+            )
+
+            self.assertEqual(result.returncode, 0, result.stderr)
+            self.assertEqual((directory / "1_07_2026.txt").read_text(), "Artist\n")
+            self.assertEqual(
+                (directory / "2_07_2026.txt").read_text(), "Artist/Album\n"
+            )
+            self.assertEqual((directory / "3_07_2026.txt").read_text(), "")
+            self.assertEqual((directory / "4_07_2026.txt").read_text(), "track.flac\n")
+            self.assertEqual(
+                arguments_file.read_text().splitlines(),
+                [
+                    "--files",
+                    "--window",
+                    "1782864000,1783468800",
+                    "--window",
+                    "1783468800,1784073600",
+                    "--window",
+                    "1784073600,1784678400",
+                    "--window",
+                    "1784678400,1785283200",
+                ],
+            )
+
+    def test_failed_query_does_not_replace_existing_reports(self):
+        with tempfile.TemporaryDirectory() as name:
+            directory = Path(name)
+            report = directory / "1_07_2026.txt"
+            report.write_text("keep me\n")
+            fake = self.make_fake(directory, "printf '0\\tpartial\\n'\nexit 1\n")
+
+            result = self.run_helper(directory, fake, "07", "2026")
+
+            self.assertEqual(result.returncode, 1)
+            self.assertEqual(report.read_text(), "keep me\n")
+            self.assertFalse((directory / "2_07_2026.txt").exists())
+
+    def test_rejects_windows_owned_by_helper(self):
+        with tempfile.TemporaryDirectory() as name:
+            directory = Path(name)
+            fake = self.make_fake(directory, "exit 0\n")
+
+            result = self.run_helper(
+                directory, fake, "07", "2026", "--", "--window", "1,2"
+            )
+
+            self.assertEqual(result.returncode, 2)
+            self.assertIn("managed by this helper", result.stderr)
+
+
+if __name__ == "__main__":
+    unittest.main()
blob - /dev/null
blob + c2fbb26e67987d182d86ff1049ef76170caaab8d (mode 644)
--- /dev/null
+++ mpd/tests/test_mpd_tools.py
@@ -0,0 +1,620 @@
+import json
+import os
+import shlex
+import socketserver
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+BIN_OVERRIDE = os.environ.get("MPD_TEST_BIN")
+BIN = Path(BIN_OVERRIDE) if BIN_OVERRIDE else ROOT / "bin"
+TOOLS = (
+    "mpd_add_to_playlist",
+    "mpd_add_to_queue",
+    "mpd_edit_queue",
+    "mpd_update_library",
+    "mpd_update_queue",
+    "mpd_insert_next",
+    "mpd_edit_playlist",
+    "mpd_now_playing",
+    "mpd_trim_queue",
+    "mpd_report",
+)
+
+
+class MPDState:
+    def __init__(self):
+        self.lock = threading.Lock()
+        self.queue = []
+        self.playlists = {}
+        self.next_id = 1
+        self.version = 1
+        self.state = "stop"
+        self.current = None
+        self.commands = []
+        self.updating = False
+        self.advance_on_current_song = False
+
+    def add_song(self, uri, position=None):
+        song = {"uri": uri, "id": self.next_id}
+        self.next_id += 1
+        if position is None or position >= len(self.queue):
+            self.queue.append(song)
+        else:
+            self.queue.insert(position, song)
+            if self.current is not None and position <= self.current:
+                self.current += 1
+        self.version += 1
+        return song
+
+    def response(self, raw):
+        args = shlex.split(raw)
+        if not args:
+            return []
+        command = args[0]
+        self.commands.append((command, args[1:]))
+
+        if command == "status":
+            lines = [
+                "volume: -1",
+                "repeat: 0",
+                "random: 0",
+                "single: 0",
+                "consume: 0",
+                f"playlist: {self.version}",
+                f"playlistlength: {len(self.queue)}",
+                f"state: {self.state}",
+            ]
+            if self.current is not None and self.current < len(self.queue):
+                song = self.queue[self.current]
+                lines += [
+                    f"song: {self.current}",
+                    f"songid: {song['id']}",
+                    "elapsed: 12.500",
+                    "duration: 180.000",
+                ]
+            if self.updating:
+                lines.append("updating_db: 1")
+            return lines
+
+        if command == "currentsong":
+            if self.advance_on_current_song:
+                self.advance_on_current_song = False
+                if self.current is not None and self.current + 1 < len(self.queue):
+                    self.current += 1
+            if self.current is None or self.current >= len(self.queue):
+                return []
+            song = self.queue[self.current]
+            return [
+                f"file: {song['uri']}",
+                'Artist: Artist "quoted"',
+                "AlbumArtist: Album Artist",
+                "Album: Test Album",
+                "Title: Test Title",
+                "Time: 180",
+                "duration: 180.000",
+                f"Pos: {self.current}",
+                f"Id: {song['id']}",
+            ]
+
+        if command == "playlistinfo":
+            lines = []
+            for position, song in enumerate(self.queue):
+                lines += [
+                    f"file: {song['uri']}",
+                    "Time: 180",
+                    f"Pos: {position}",
+                    f"Id: {song['id']}",
+                ]
+            return lines
+
+        if command == "listplaylist":
+            return [f"file: {uri}" for uri in self.playlists.get(args[1], [])]
+
+        if command in ("add", "addid"):
+            uri = args[1]
+            if uri == "FAIL":
+                raise ValueError("No such song")
+            position = int(args[2]) if command == "addid" and len(args) > 2 else None
+            song = self.add_song(uri, position)
+            return [f"Id: {song['id']}"] if command == "addid" else []
+
+        if command == "delete":
+            spec = args[1]
+            if ":" in spec:
+                start_text, end_text = spec.split(":", 1)
+                start = int(start_text)
+                end = len(self.queue) if not end_text else int(end_text)
+            else:
+                start = int(spec)
+                end = start + 1
+            removed = end - start
+            del self.queue[start:end]
+            if self.current is not None:
+                if start <= self.current < end:
+                    self.current = None
+                    self.state = "stop"
+                elif self.current >= end:
+                    self.current -= removed
+            self.version += 1
+            return []
+
+        if command == "move":
+            source, destination = int(args[1]), int(args[2])
+            song = self.queue.pop(source)
+            self.queue.insert(destination, song)
+            if self.current == source:
+                self.current = destination
+            elif self.current is not None and source < self.current <= destination:
+                self.current -= 1
+            elif self.current is not None and destination <= self.current < source:
+                self.current += 1
+            self.version += 1
+            return []
+
+        if command == "clear":
+            self.queue.clear()
+            self.current = None
+            self.state = "stop"
+            self.version += 1
+            return []
+
+        if command == "play":
+            if self.queue:
+                self.current = int(args[1]) if len(args) > 1 else 0
+                self.state = "play"
+            return []
+
+        if command == "pause":
+            self.state = "pause" if args[1] == "1" else "play"
+            return []
+
+        if command == "playlistclear":
+            self.playlists[args[1]] = []
+            return []
+
+        if command == "playlistadd":
+            if args[2] == "FAIL":
+                raise ValueError("No such song")
+            self.playlists.setdefault(args[1], []).append(args[2])
+            return []
+
+        if command == "rename":
+            source, destination = args[1], args[2]
+            if source not in self.playlists:
+                raise ValueError("No such playlist")
+            if destination in self.playlists:
+                raise ValueError("Playlist already exists")
+            self.playlists[destination] = self.playlists.pop(source)
+            return []
+
+        if command == "rm":
+            if args[1] not in self.playlists:
+                raise ValueError("No such playlist")
+            del self.playlists[args[1]]
+            return []
+
+        if command in ("update", "rescan"):
+            self.updating = True
+            return ["updating_db: 1"]
+
+        if command == "idle":
+            self.updating = False
+            return ["changed: update"]
+
+        raise ValueError(f"unsupported command: {raw}")
+
+
+class MPDHandler(socketserver.StreamRequestHandler):
+    def handle(self):
+        self.wfile.write(b"OK MPD 0.23.0\n")
+        self.wfile.flush()
+        command_list = None
+        responses = []
+        while True:
+            raw = self.rfile.readline()
+            if not raw:
+                return
+            line = raw.decode().rstrip("\r\n")
+            if line in ("command_list_begin", "command_list_ok_begin"):
+                command_list = line
+                responses = []
+                with self.server.state.lock:
+                    self.server.state.commands.append((line, []))
+                continue
+            if line == "command_list_end":
+                with self.server.state.lock:
+                    self.server.state.commands.append((line, []))
+                for response in responses:
+                    for item in response:
+                        self.wfile.write(f"{item}\n".encode())
+                    if command_list == "command_list_ok_begin":
+                        self.wfile.write(b"list_OK\n")
+                self.wfile.write(b"OK\n")
+                self.wfile.flush()
+                command_list = None
+                responses = []
+                continue
+            try:
+                with self.server.state.lock:
+                    response = self.server.state.response(line)
+            except (ValueError, IndexError) as error:
+                self.wfile.write(f"ACK [50@0] {{{line.split()[0]}}} {error}\n".encode())
+                self.wfile.flush()
+                command_list = None
+                responses = []
+                continue
+            if command_list is not None:
+                responses.append(response)
+            else:
+                for item in response:
+                    self.wfile.write(f"{item}\n".encode())
+                self.wfile.write(b"OK\n")
+                self.wfile.flush()
+
+
+class MPDServer(socketserver.ThreadingTCPServer):
+    allow_reuse_address = True
+    daemon_threads = True
+
+    def __init__(self):
+        self.state = MPDState()
+        super().__init__(("127.0.0.1", 0), MPDHandler)
+
+    def handle_error(self, request, client_address):
+        if isinstance(sys.exception(), BrokenPipeError):
+            return
+        super().handle_error(request, client_address)
+
+
+class MPDToolsTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        if BIN_OVERRIDE is None:
+            subprocess.run(
+                ["knit", *(f"bin/{tool}" for tool in TOOLS)],
+                cwd=ROOT,
+                check=True,
+            )
+
+    def setUp(self):
+        self.server = MPDServer()
+        self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
+        self.thread.start()
+
+    def tearDown(self):
+        self.server.shutdown()
+        self.server.server_close()
+        self.thread.join(timeout=2)
+
+    def tool_environment(self, extra_env=None):
+        env = os.environ.copy()
+        env.update(
+            MPD_HOST="127.0.0.1",
+            MPD_PORT=str(self.server.server_address[1]),
+            MPD_TIMEOUT="2",
+            MPD_CONFIG_HELPER="off",
+        )
+        if extra_env:
+            for key, value in extra_env.items():
+                if value is None:
+                    env.pop(key, None)
+                else:
+                    env[key] = value
+        return env
+
+    def run_tool(self, tool, *args, input_text=None, extra_env=None):
+        return subprocess.run(
+            [str(BIN / tool), *args],
+            input=input_text,
+            text=True,
+            capture_output=True,
+            env=self.tool_environment(extra_env),
+            timeout=10,
+        )
+
+    def seed_queue(self, uris, current=None, state="stop"):
+        for uri in uris:
+            self.server.state.add_song(uri)
+        self.server.state.current = current
+        self.server.state.state = state
+
+    def test_add_batches_and_preserves_line_content(self):
+        tracks = [f"https://example.test/{index}" for index in range(1100)]
+        tracks[5] = " leading and trailing "
+        text = "# ignored\r\n" + "\r\n".join(tracks) + "\r\n"
+        result = self.run_tool("mpd_add_to_queue", input_text=text)
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertIn("Added 1100 tracks", result.stdout)
+        self.assertEqual([song["uri"] for song in self.server.state.queue], tracks)
+        begins = [c for c, _ in self.server.state.commands if c == "command_list_begin"]
+        self.assertEqual(len(begins), 3)
+
+    def test_add_failure_is_nonzero(self):
+        result = self.run_tool("mpd_add_to_queue", input_text="ok\nFAIL\nlater\n")
+        self.assertEqual(result.returncode, 1)
+        self.assertIn("failed input batch", result.stderr)
+
+    def test_replace_uses_ranges_and_preserves_paused_current(self):
+        old = [f"old/{index}" for index in range(100)]
+        self.seed_queue(old, current=50, state="pause")
+        wanted = ["new/a", "new/b", old[50], "new/c", "new/d"]
+        result = self.run_tool("mpd_update_queue", input_text="\n".join(wanted))
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual([song["uri"] for song in self.server.state.queue], wanted)
+        self.assertEqual(self.server.state.current, 2)
+        self.assertEqual(self.server.state.state, "pause")
+        deletes = [
+            args for command, args in self.server.state.commands if command == "delete"
+        ]
+        self.assertEqual(len(deletes), 2)
+        self.assertTrue(all(":" in args[0] for args in deletes))
+
+    def test_failed_queue_replacement_rolls_back(self):
+        self.seed_queue(["old/a", "old/b"], current=1, state="pause")
+        result = self.run_tool("mpd_update_queue", input_text="new/a\nFAIL\n")
+        self.assertEqual(result.returncode, 1)
+        self.assertIn("restoring the previous queue", result.stderr)
+        self.assertNotIn("rollback failed", result.stderr)
+        self.assertEqual(
+            [song["uri"] for song in self.server.state.queue],
+            ["old/a", "old/b"],
+        )
+        self.assertEqual(self.server.state.current, 1)
+        self.assertEqual(self.server.state.state, "pause")
+
+    def test_player_snapshot_retries_if_song_advances(self):
+        self.seed_queue(["old/a", "old/b", "old/c"], current=0, state="play")
+        self.server.state.advance_on_current_song = True
+        result = self.run_tool("mpd_update_queue", input_text="new/a\nold/b\nnew/b\n")
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(
+            [song["uri"] for song in self.server.state.queue],
+            ["new/a", "old/b", "new/b"],
+        )
+        self.assertEqual(self.server.state.current, 1)
+
+    def test_insert_next_and_trim(self):
+        self.seed_queue(["a", "b", "c", "d", "e"], current=1, state="play")
+        result = self.run_tool("mpd_insert_next", input_text="x\ny\n")
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(
+            [song["uri"] for song in self.server.state.queue],
+            ["a", "b", "x", "y", "c", "d", "e"],
+        )
+        self.server.state.current = 5
+        result = self.run_tool("mpd_trim_queue", "--keep", "1")
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(
+            [song["uri"] for song in self.server.state.queue], ["c", "d", "e"]
+        )
+        self.assertEqual(self.server.state.current, 1)
+
+    def test_playlist_add_and_edit(self):
+        result = self.run_tool(
+            "mpd_add_to_playlist", "favorites", input_text="one\ntwo\n"
+        )
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(self.server.state.playlists["favorites"], ["one", "two"])
+
+        with tempfile.TemporaryDirectory() as directory:
+            editor = Path(directory) / "editor.sh"
+            editor.write_text("#!/bin/sh\nprintf 'three\\nfour\\n' > \"$1\"\n")
+            editor.chmod(0o755)
+            result = self.run_tool(
+                "mpd_edit_playlist",
+                "favorites",
+                extra_env={"VISUAL": str(editor)},
+            )
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(self.server.state.playlists["favorites"], ["three", "four"])
+
+    def test_failed_playlist_staging_preserves_original(self):
+        self.server.state.playlists["favorites"] = ["one", "two"]
+        with tempfile.TemporaryDirectory() as directory:
+            editor = Path(directory) / "editor.sh"
+            editor.write_text("#!/bin/sh\nprintf 'new\\nFAIL\\n' > \"$1\"\n")
+            editor.chmod(0o755)
+            result = self.run_tool(
+                "mpd_edit_playlist",
+                "favorites",
+                extra_env={"VISUAL": str(editor)},
+            )
+        self.assertEqual(result.returncode, 1)
+        self.assertEqual(self.server.state.playlists["favorites"], ["one", "two"])
+        self.assertEqual(
+            [
+                name
+                for name in self.server.state.playlists
+                if name.startswith("mpd-tools-")
+            ],
+            [],
+        )
+
+    def test_queue_editor_preserves_current_and_aborts_on_editor_failure(self):
+        self.seed_queue(["one", "two"], current=0, state="play")
+        before = list(self.server.state.queue)
+        result = self.run_tool("mpd_edit_queue", extra_env={"VISUAL": "/bin/false"})
+        self.assertEqual(result.returncode, 1)
+        self.assertEqual(self.server.state.queue, before)
+
+        with tempfile.TemporaryDirectory() as directory:
+            editor = Path(directory) / "editor.sh"
+            editor.write_text("#!/bin/sh\nprintf 'two\\none\\n' > \"$1\"\n")
+            editor.chmod(0o755)
+            result = self.run_tool("mpd_edit_queue", extra_env={"VISUAL": str(editor)})
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(
+            [song["uri"] for song in self.server.state.queue], ["two", "one"]
+        )
+        self.assertEqual(self.server.state.current, 1)
+        self.assertEqual(self.server.state.state, "play")
+
+    def test_now_playing_json(self):
+        self.seed_queue(["track.ogg"], current=0, state="play")
+        result = self.run_tool("mpd_now_playing", "--json")
+        self.assertEqual(result.returncode, 0, result.stderr)
+        value = json.loads(result.stdout)
+        self.assertEqual(value["state"], "playing")
+        self.assertEqual(value["artist"], 'Artist "quoted"')
+        self.assertEqual(value["uri"], "track.ogg")
+        self.assertEqual(value["elapsed_seconds"], 12.5)
+        self.assertEqual(value["song_id"], 1)
+
+    def test_explicit_config_helper_overrides_connection_environment(self):
+        self.seed_queue(["helper.ogg"], current=0, state="play")
+        with tempfile.TemporaryDirectory() as directory:
+            helper = Path(directory) / "helper"
+            helper.write_text(
+                "#!/bin/sh\n"
+                "printf 'MPD_HOST=127.0.0.1\\n'\n"
+                f"printf 'MPD_PORT={self.server.server_address[1]}\\n'\n"
+            )
+            helper.chmod(0o755)
+            result = self.run_tool(
+                "mpd_now_playing",
+                "--json",
+                extra_env={
+                    "MPD_CONFIG_HELPER": str(helper),
+                    "MPD_HOST": "invalid.example",
+                    "MPD_PORT": "1",
+                },
+            )
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(json.loads(result.stdout)["uri"], "helper.ogg")
+
+    def test_amen_is_auto_discovered_and_runs_once_across_reconnects(self):
+        self.seed_queue(["one"], current=0, state="pause")
+        with tempfile.TemporaryDirectory() as directory:
+            home = Path(directory)
+            (home / "bin").mkdir()
+            count = home / "calls"
+            helper = home / "bin" / "amen"
+            helper.write_text(
+                "#!/bin/sh\n"
+                f"printf x >> {shlex.quote(str(count))}\n"
+                "printf 'MPD_HOST=127.0.0.1\\n'\n"
+                f"printf 'MPD_PORT={self.server.server_address[1]}\\n'\n"
+            )
+            helper.chmod(0o755)
+            result = self.run_tool(
+                "mpd_edit_queue",
+                extra_env={
+                    "HOME": str(home),
+                    "MPD_CONFIG_HELPER": None,
+                    "MPD_HOST": "invalid.example",
+                    "MPD_PORT": "1",
+                    "VISUAL": "/bin/true",
+                },
+            )
+            calls = count.read_text()
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(calls, "x")
+
+    def test_invalid_config_helper_output_fails_without_fallback(self):
+        with tempfile.TemporaryDirectory() as directory:
+            helper = Path(directory) / "invalid-helper"
+            helper.write_text("#!/bin/sh\nprintf 'MPD_HOST=127.0.0.1\\n'\n")
+            helper.chmod(0o755)
+            result = self.run_tool(
+                "mpd_now_playing",
+                extra_env={"MPD_CONFIG_HELPER": str(helper)},
+            )
+        self.assertEqual(result.returncode, 1)
+        self.assertIn("must print exactly valid MPD_HOST", result.stderr)
+
+    def test_config_helper_timeout_is_bounded(self):
+        with tempfile.TemporaryDirectory() as directory:
+            helper = Path(directory) / "slow-helper"
+            helper.write_text("#!/bin/sh\nexec sleep 30\n")
+            helper.chmod(0o755)
+            started = time.monotonic()
+            result = self.run_tool(
+                "mpd_now_playing",
+                extra_env={
+                    "MPD_CONFIG_HELPER": str(helper),
+                    "MPD_CONFIG_HELPER_TIMEOUT": "1",
+                },
+            )
+            elapsed = time.monotonic() - started
+        self.assertEqual(result.returncode, 1)
+        self.assertIn("timed out after 1 seconds", result.stderr)
+        self.assertLess(elapsed, 3)
+
+    def test_editor_uses_tmpdir(self):
+        self.server.state.playlists["favorites"] = ["one"]
+        with tempfile.TemporaryDirectory() as directory:
+            editor = Path(directory) / "editor.sh"
+            editor.write_text(
+                "#!/bin/sh\n"
+                f'case "$1" in {shlex.quote(directory)}/*) exit 0;; *) exit 1;; esac\n'
+            )
+            editor.chmod(0o755)
+            result = self.run_tool(
+                "mpd_edit_playlist",
+                "favorites",
+                extra_env={"VISUAL": str(editor), "TMPDIR": directory},
+            )
+        self.assertEqual(result.returncode, 0, result.stderr)
+
+    def test_watch_returns_signal_specific_status(self):
+        process = subprocess.Popen(
+            [str(BIN / "mpd_now_playing"), "--watch"],
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.PIPE,
+            text=True,
+            env=self.tool_environment(),
+        )
+        try:
+            time.sleep(0.1)
+            process.terminate()
+            _, stderr = process.communicate(timeout=3)
+        finally:
+            if process.poll() is None:
+                process.kill()
+                process.wait()
+        self.assertEqual(process.returncode, 128 + 15, stderr)
+
+    def test_empty_replacement_clears_queue(self):
+        self.seed_queue(["one", "two"], current=0, state="pause")
+        result = self.run_tool("mpd_update_queue", input_text="")
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertEqual(self.server.state.queue, [])
+        self.assertEqual(self.server.state.state, "stop")
+
+    def test_library_update_deduplicates_without_fixed_delay(self):
+        result = self.run_tool(
+            "mpd_update_library",
+            "--no-wait",
+            input_text="rock/a.ogg\nrock/b.ogg\njazz/c.ogg\n",
+        )
+        self.assertEqual(result.returncode, 0, result.stderr)
+        updates = [
+            args for command, args in self.server.state.commands if command == "update"
+        ]
+        self.assertEqual(updates, [["jazz"], ["rock"]])
+
+    def test_library_update_waits_on_idle_event(self):
+        result = self.run_tool("mpd_update_library", input_text="rock/a.ogg\n")
+        self.assertEqual(result.returncode, 0, result.stderr)
+        commands = [command for command, _ in self.server.state.commands]
+        self.assertIn("idle", commands)
+
+    def test_help_and_invalid_usage(self):
+        for tool in TOOLS:
+            result = self.run_tool(tool, "--help")
+            self.assertEqual(result.returncode, 0, f"{tool}: {result.stderr}")
+            self.assertIn("Usage:", result.stdout)
+        result = self.run_tool("mpd_trim_queue", "--keep", "nope")
+        self.assertEqual(result.returncode, 2)
+        result = self.run_tool("mpd_add_to_queue", "--unknown")
+        self.assertEqual(result.returncode, 2)
+
+
+if __name__ == "__main__":
+    unittest.main()