tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

mpd_report.c (12941B)


      1 #define _POSIX_C_SOURCE 200809L
      2 
      3 #include "mpd_common.h"
      4 
      5 #include <errno.h>
      6 #include <getopt.h>
      7 #include <inttypes.h>
      8 #include <limits.h>
      9 #include <mpd/client.h>
     10 #include <stdbool.h>
     11 #include <stdint.h>
     12 #include <stdio.h>
     13 #include <stdlib.h>
     14 #include <string.h>
     15 #include <time.h>
     16 #include <unistd.h>
     17 
     18 struct window {
     19   time_t start;
     20   time_t end;
     21   bool has_start;
     22   bool has_end;
     23 };
     24 
     25 struct options {
     26   const char *host;
     27   const char *path;
     28   const char *output;
     29   const char *tracks_output;
     30   unsigned port;
     31   bool include_files;
     32   bool indexed;
     33   bool relative;
     34   struct window *windows;
     35   size_t window_count;
     36 };
     37 
     38 struct atomic_file {
     39   FILE *stream;
     40   char *temporary_path;
     41   const char *final_path;
     42 };
     43 
     44 static void usage(FILE *stream, const char *program) {
     45   fprintf(
     46       stream,
     47       "Usage: %s [OPTIONS]\n"
     48       "Query MPD's database for directories modified in one or more time "
     49       "windows.\n\n"
     50       "  -w, --window START,END   epoch-second bounds; '-' means unbounded\n"
     51       "  -f, --files              include songs as well as directories\n"
     52       "  -i, --indexed            prefix results with their window index\n"
     53       "  -r, --relative           strip the requested MPD path prefix\n"
     54       "  -p, --path URI           restrict the lookup to an MPD URI\n"
     55       "  -o, --output FILE        atomically write results to FILE (default: "
     56       "all.txt); '-' uses stdout\n"
     57       "      --tracks-output FILE atomically write every song URI to FILE\n"
     58       "  -H, --host HOST          MPD host (default: $MPD_HOST or localhost)\n"
     59       "  -P, --port PORT          MPD port (default: $MPD_PORT or 6600)\n"
     60       "  -h, --help               show this help\n\n"
     61       "One window writes plain URIs. Multiple windows (or --indexed) write\n"
     62       "WINDOW_INDEX<TAB>URI. With no --window, the default is all time.\n"
     63       "Date windows use exclusive bounds, like diggah's filesystem backend.\n",
     64       program);
     65 }
     66 
     67 static bool parse_unsigned(const char *text, unsigned *value) {
     68   char *end = NULL;
     69   errno = 0;
     70   unsigned long parsed = strtoul(text, &end, 10);
     71   if (errno != 0 || end == text || *end != '\0' || parsed > UINT_MAX)
     72     return false;
     73   *value = (unsigned)parsed;
     74   return true;
     75 }
     76 
     77 static bool parse_timestamp(const char *text, time_t *value) {
     78   char *end = NULL;
     79   errno = 0;
     80   intmax_t parsed = strtoimax(text, &end, 10);
     81   if (errno != 0 || end == text || *end != '\0')
     82     return false;
     83 
     84   time_t timestamp = (time_t)parsed;
     85   if ((intmax_t)timestamp != parsed)
     86     return false;
     87   *value = timestamp;
     88   return true;
     89 }
     90 
     91 static bool append_window(struct options *options, const char *argument) {
     92   const char *comma = strchr(argument, ',');
     93   if (comma == NULL || strchr(comma + 1, ',') != NULL) {
     94     fprintf(stderr, "invalid window '%s' (expected START,END)\n", argument);
     95     return false;
     96   }
     97 
     98   size_t start_length = (size_t)(comma - argument);
     99   char *start = strndup(argument, start_length);
    100   char *end = strdup(comma + 1);
    101   if (start == NULL || end == NULL) {
    102     perror("allocating window");
    103     free(start);
    104     free(end);
    105     return false;
    106   }
    107 
    108   struct window window = {0};
    109   window.has_start = strcmp(start, "-") != 0;
    110   window.has_end = strcmp(end, "-") != 0;
    111   bool valid = true;
    112   if (window.has_start && !parse_timestamp(start, &window.start))
    113     valid = false;
    114   if (window.has_end && !parse_timestamp(end, &window.end))
    115     valid = false;
    116   if (window.has_start && window.has_end && window.start >= window.end)
    117     valid = false;
    118   free(start);
    119   free(end);
    120 
    121   if (!valid) {
    122     fprintf(stderr, "invalid window '%s'\n", argument);
    123     return false;
    124   }
    125 
    126   struct window *windows =
    127       realloc(options->windows,
    128               (options->window_count + 1U) * sizeof(*options->windows));
    129   if (windows == NULL) {
    130     perror("allocating windows");
    131     return false;
    132   }
    133   options->windows = windows;
    134   options->windows[options->window_count++] = window;
    135   return true;
    136 }
    137 
    138 static bool parse_options(int argc, char **argv, struct options *options) {
    139   *options = (struct options){.path = "", .output = "all.txt"};
    140 
    141   enum { OPT_TRACKS_OUTPUT = 256 };
    142   static const struct option long_options[] = {
    143       {"window", required_argument, NULL, 'w'},
    144       {"files", no_argument, NULL, 'f'},
    145       {"indexed", no_argument, NULL, 'i'},
    146       {"relative", no_argument, NULL, 'r'},
    147       {"path", required_argument, NULL, 'p'},
    148       {"output", required_argument, NULL, 'o'},
    149       {"tracks-output", required_argument, NULL, OPT_TRACKS_OUTPUT},
    150       {"host", required_argument, NULL, 'H'},
    151       {"port", required_argument, NULL, 'P'},
    152       {"help", no_argument, NULL, 'h'},
    153       {NULL, 0, NULL, 0},
    154   };
    155 
    156   int option = 0;
    157   while ((option = getopt_long(argc, argv, "w:firp:o:H:P:h", long_options,
    158                                NULL)) != -1) {
    159     switch (option) {
    160     case 'w':
    161       if (!append_window(options, optarg))
    162         return false;
    163       break;
    164     case 'f':
    165       options->include_files = true;
    166       break;
    167     case 'i':
    168       options->indexed = true;
    169       break;
    170     case 'r':
    171       options->relative = true;
    172       break;
    173     case 'p':
    174       options->path = optarg;
    175       break;
    176     case 'o':
    177       options->output = optarg;
    178       break;
    179     case 'H':
    180       options->host = optarg;
    181       break;
    182     case 'P':
    183       if (!parse_unsigned(optarg, &options->port) || options->port == 0U ||
    184           options->port > 65535U) {
    185         fprintf(stderr, "invalid MPD port '%s'\n", optarg);
    186         return false;
    187       }
    188       break;
    189     case OPT_TRACKS_OUTPUT:
    190       options->tracks_output = optarg;
    191       break;
    192     case 'h':
    193       usage(stdout, argv[0]);
    194       exit(0);
    195     default:
    196       usage(stderr, argv[0]);
    197       return false;
    198     }
    199   }
    200 
    201   if (optind != argc) {
    202     fprintf(stderr, "unexpected argument '%s'\n", argv[optind]);
    203     return false;
    204   }
    205   if (options->output[0] == '\0') {
    206     fprintf(stderr, "output path must not be empty\n");
    207     return false;
    208   }
    209   if (options->tracks_output != NULL &&
    210       strcmp(options->tracks_output, options->output) == 0) {
    211     fprintf(stderr, "--output and --tracks-output must name different files\n");
    212     return false;
    213   }
    214   if (options->window_count == 0U && !append_window(options, "-,-"))
    215     return false;
    216   return true;
    217 }
    218 
    219 static struct mpd_connection *connect_mpd(const struct options *options) {
    220   return mt_connect_to(options->host, options->port);
    221 }
    222 
    223 static bool atomic_file_open(struct atomic_file *file, const char *path) {
    224   *file = (struct atomic_file){.final_path = path};
    225   size_t length = strlen(path) + sizeof(".tmp.XXXXXX");
    226   file->temporary_path = malloc(length);
    227   if (file->temporary_path == NULL) {
    228     perror("allocating temporary path");
    229     return false;
    230   }
    231   snprintf(file->temporary_path, length, "%s.tmp.XXXXXX", path);
    232 
    233   int descriptor = mkstemp(file->temporary_path);
    234   if (descriptor < 0) {
    235     fprintf(stderr, "opening temporary output for '%s': %s\n", path,
    236             strerror(errno));
    237     free(file->temporary_path);
    238     file->temporary_path = NULL;
    239     return false;
    240   }
    241   file->stream = fdopen(descriptor, "w");
    242   if (file->stream == NULL) {
    243     int error = errno;
    244     close(descriptor);
    245     unlink(file->temporary_path);
    246     errno = error;
    247     perror("opening temporary output stream");
    248     free(file->temporary_path);
    249     file->temporary_path = NULL;
    250     return false;
    251   }
    252   return true;
    253 }
    254 
    255 static void atomic_file_abort(struct atomic_file *file) {
    256   if (file->stream != NULL)
    257     fclose(file->stream);
    258   if (file->temporary_path != NULL) {
    259     unlink(file->temporary_path);
    260     free(file->temporary_path);
    261   }
    262   *file = (struct atomic_file){0};
    263 }
    264 
    265 static bool atomic_file_commit(struct atomic_file *file) {
    266   bool success = true;
    267   int write_error = 0;
    268   if (fflush(file->stream) != 0)
    269     write_error = errno;
    270   if (fsync(fileno(file->stream)) != 0 && write_error == 0)
    271     write_error = errno;
    272   if (fclose(file->stream) != 0 && write_error == 0)
    273     write_error = errno;
    274   if (write_error != 0) {
    275     fprintf(stderr, "writing output file '%s': %s\n", file->final_path,
    276             strerror(write_error));
    277     success = false;
    278   }
    279   file->stream = NULL;
    280   if (success && rename(file->temporary_path, file->final_path) != 0) {
    281     fprintf(stderr, "installing output file '%s': %s\n", file->final_path,
    282             strerror(errno));
    283     success = false;
    284   }
    285   if (!success)
    286     unlink(file->temporary_path);
    287   free(file->temporary_path);
    288   file->temporary_path = NULL;
    289   return success;
    290 }
    291 
    292 static bool in_window(time_t modified, const struct window *window) {
    293   if ((window->has_start || window->has_end) && modified == (time_t)0)
    294     return false;
    295   if (window->has_start && modified <= window->start)
    296     return false;
    297   if (window->has_end && modified >= window->end)
    298     return false;
    299   return true;
    300 }
    301 
    302 static const char *relative_uri(const char *uri,
    303                                 const struct options *options) {
    304   if (!options->relative || options->path[0] == '\0')
    305     return uri;
    306 
    307   size_t prefix_length = strlen(options->path);
    308   while (prefix_length > 0U && options->path[prefix_length - 1U] == '/')
    309     prefix_length--;
    310   if (strncmp(uri, options->path, prefix_length) != 0)
    311     return uri;
    312   if (uri[prefix_length] == '/')
    313     return uri + prefix_length + 1U;
    314   if (uri[prefix_length] == '\0')
    315     return ".";
    316   return uri;
    317 }
    318 
    319 static bool emit_matches(const struct options *options, FILE *stream,
    320                          const char *uri, time_t modified) {
    321   const char *output_uri = relative_uri(uri, options);
    322   for (size_t index = 0; index < options->window_count; index++) {
    323     if (!in_window(modified, &options->windows[index]))
    324       continue;
    325     int written;
    326     if (options->indexed || options->window_count > 1U)
    327       written = fprintf(stream, "%zu\t%s\n", index, output_uri);
    328     else
    329       written = fprintf(stream, "%s\n", output_uri);
    330     if (written < 0)
    331       return false;
    332   }
    333   return true;
    334 }
    335 
    336 static bool query(const struct options *options) {
    337   struct mpd_connection *connection = connect_mpd(options);
    338   if (connection == NULL)
    339     return false;
    340 
    341   struct atomic_file results = {0};
    342   FILE *result_stream = stdout;
    343   bool results_to_stdout = strcmp(options->output, "-") == 0;
    344   if (!results_to_stdout) {
    345     if (!atomic_file_open(&results, options->output)) {
    346       mpd_connection_free(connection);
    347       return false;
    348     }
    349     result_stream = results.stream;
    350   }
    351 
    352   struct atomic_file tracks = {0};
    353   if (options->tracks_output != NULL &&
    354       !atomic_file_open(&tracks, options->tracks_output)) {
    355     if (results.stream != NULL)
    356       atomic_file_abort(&results);
    357     mpd_connection_free(connection);
    358     return false;
    359   }
    360 
    361   bool success = mpd_send_list_all_meta(connection, options->path);
    362   if (!success) {
    363     fprintf(stderr, "requesting MPD database: %s\n",
    364             mpd_connection_get_error_message(connection));
    365   }
    366 
    367   struct mpd_entity *entity = NULL;
    368   while (success && (entity = mpd_recv_entity(connection)) != NULL) {
    369     enum mpd_entity_type type = mpd_entity_get_type(entity);
    370     const char *uri = NULL;
    371     time_t modified = 0;
    372 
    373     if (type == MPD_ENTITY_TYPE_DIRECTORY) {
    374       const struct mpd_directory *directory = mpd_entity_get_directory(entity);
    375       uri = mpd_directory_get_path(directory);
    376       modified = mpd_directory_get_last_modified(directory);
    377     } else if (type == MPD_ENTITY_TYPE_SONG) {
    378       const struct mpd_song *song = mpd_entity_get_song(entity);
    379       uri = mpd_song_get_uri(song);
    380       modified = mpd_song_get_last_modified(song);
    381       if (tracks.stream != NULL && fprintf(tracks.stream, "%s\n", uri) < 0) {
    382         fprintf(stderr, "writing track list '%s': %s\n", tracks.final_path,
    383                 strerror(errno));
    384         success = false;
    385       }
    386       if (!options->include_files)
    387         uri = NULL;
    388     }
    389 
    390     if (success && uri != NULL &&
    391         !emit_matches(options, result_stream, uri, modified)) {
    392       fprintf(stderr, "writing results to '%s': %s\n", options->output,
    393               strerror(errno));
    394       success = false;
    395     }
    396     mpd_entity_free(entity);
    397   }
    398 
    399   if (success && !mpd_response_finish(connection)) {
    400     fprintf(stderr, "reading MPD database: %s\n",
    401             mpd_connection_get_error_message(connection));
    402     success = false;
    403   }
    404   if (results_to_stdout) {
    405     if (success && fflush(stdout) != 0) {
    406       perror("writing results");
    407       success = false;
    408     }
    409   } else if (results.stream != NULL) {
    410     if (success)
    411       success = atomic_file_commit(&results);
    412     else
    413       atomic_file_abort(&results);
    414   }
    415 
    416   if (tracks.stream != NULL) {
    417     if (success)
    418       success = atomic_file_commit(&tracks);
    419     else
    420       atomic_file_abort(&tracks);
    421   }
    422   mpd_connection_free(connection);
    423   return success;
    424 }
    425 
    426 int main(int argc, char **argv) {
    427   struct options options;
    428   if (!parse_options(argc, argv, &options)) {
    429     free(options.windows);
    430     return 2;
    431   }
    432 
    433   bool success = query(&options);
    434   free(options.windows);
    435   return success ? 0 : 1;
    436 }