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 }