1 /*
2 Copyright (C) 2005-2019 Marius L. Jøhndal
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Lesser General Public
6 License as published by the Free Software Foundation; either
7 version 2.1 of the License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Lesser General Public License for more details.
13
14 You should have received a copy of the GNU Lesser General Public
15 License along with this library; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17
18 */
19
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif /* HAVE_CONFIG_H */
23
24 #define _GNU_SOURCE
25 #include <getopt.h>
26 #include <glib.h>
27 #include <glib/gprintf.h>
28 #include <glib/gstdio.h>
29 #include <errno.h>
30 #include <string.h>
31 #include <unistd.h>
32 #include <libxml/parser.h>
33 #ifdef ENABLE_ID3LIB
34 #include <id3.h>
35 #endif /* ENABLE_ID3LIB */
36 #include "configuration.h"
37 #include "channel.h"
38
39 enum op {
40 OP_UPDATE,
41 OP_CATCHUP,
42 OP_LIST
43 };
44
45 static int _process_channel(const gchar *channel_directory, GKeyFile *kf, const char *identifier,
46 enum op op, struct channel_configuration *defaults,
47 enclosure_filter *filter);
48 static void version(void);
49 static GKeyFile *_configuration_file_open(const gchar *rcfile);
50 static void _configuration_file_close(GKeyFile *kf);
51 #ifdef ENABLE_ID3LIB
52 static int _id3_set(const gchar *filename, int clear, const gchar *lead_artist,
53 const gchar *content_group, const gchar *title,
54 const gchar *album, const gchar *content_type, const gchar *year,
55 const gchar *comment);
56 static int _id3_check_and_set(const gchar *filename,
57 const struct channel_configuration *cfg);
58 #endif /* ENABLE_ID3LIB */
59 static int playlist_add(const gchar *playlist_file,
60 const gchar *media_file);
61
62 static gboolean verbose = FALSE;
63 static gboolean quiet = FALSE;
64 static gboolean first_only = FALSE;
65 static gboolean resume = FALSE;
66 static gboolean debug = FALSE;
67 static gboolean show_progress_bar = FALSE;
68 static gboolean show_version = FALSE;
69 static gboolean show_debug_info = FALSE;
70 static gboolean new_only = FALSE;
71 static gboolean list = FALSE;
72 static gboolean catchup = FALSE;
73 static gchar *rcfile = NULL;
74 static gchar *filter_regex = NULL;
75
main(int argc,char ** argv)76 int main(int argc, char **argv)
77 {
78 enum op op = OP_UPDATE;
79 int i;
80 int ret = 0;
81 gchar **groups;
82 gchar *channeldir;
83 GKeyFile *kf;
84 struct channel_configuration *defaults;
85 enclosure_filter *filter = NULL;
86 GError *error = NULL;
87 GOptionContext *context;
88
89 static GOptionEntry options[] =
90 {
91 {"catchup", 'c', 0, G_OPTION_ARG_NONE, &catchup, "catch up with channels and exit"},
92 {"list", 'l', 0, G_OPTION_ARG_NONE, &list, "list available enclosures that have not yet been downloaded and exit"},
93 {"version", 'V', 0, G_OPTION_ARG_NONE, &show_version, "print version and exit"},
94
95 {"resume", 'r', 0, G_OPTION_ARG_NONE, &resume, "resume aborted downloads"},
96 {"rcfile", 'C', 0, G_OPTION_ARG_FILENAME, &rcfile, "override the default configuration file name"},
97
98 {"debug", 'd', 0, G_OPTION_ARG_NONE, &show_debug_info, "print connection debug information"},
99 {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "print detailed progress information"},
100 {"progress-bar", 'p', 0, G_OPTION_ARG_NONE, &show_progress_bar, "print progress bar"},
101
102 {"new-only", 'n', 0, G_OPTION_ARG_NONE, &new_only, "only process new channels"},
103 {"quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet, "only print error messages"},
104 {"first-only", '1', 0, G_OPTION_ARG_NONE, &first_only, "only process the most recent item from each channel"},
105 {"filter", 'f', 0, G_OPTION_ARG_STRING, &filter_regex, "only process items whose enclosure names match a regular expression"},
106
107 { NULL }
108 };
109
110 context = g_option_context_new("CHANNELS");
111 g_option_context_add_main_entries(context, options, NULL);
112
113 if (!g_option_context_parse(context, &argc, &argv, &error)) {
114 g_print("option parsing failed: %s\n", error->message);
115 exit(1);
116 }
117
118 /* Do some additional sanity checking of options. */
119 if ((verbose && quiet) || (show_progress_bar && quiet)) {
120 g_print("option parsing failed: options are incompatible.\n");
121 exit(1);
122 }
123
124 if ((catchup && list) || (catchup && show_version) || (list && show_version)) {
125 g_print("option parsing failed: --catchup, --list and --version options are incompatible.\n");
126 exit(1);
127 }
128
129 /* Decide on the action to take */
130 if (show_version) {
131 version();
132 exit(0);
133 }
134
135 if (catchup)
136 op = OP_CATCHUP;
137
138 if (list)
139 op = OP_LIST;
140
141 if (filter_regex) {
142 filter = enclosure_filter_new(filter_regex, FALSE);
143 g_free(filter_regex);
144 }
145
146 if (verbose && new_only)
147 g_print("Fetching new channels only...\n");
148
149 LIBXML_TEST_VERSION;
150
151 /* Build the channel directory path and ensure that it exists. */
152 channeldir = g_build_filename(g_get_home_dir(), ".castget", NULL);
153
154 if (!g_file_test(channeldir, G_FILE_TEST_IS_DIR)) {
155 if (g_mkdir(channeldir, 0755) < 0) {
156 perror("Error creating channel directory");
157 return 1;
158 }
159 }
160
161 /* Try opening configuration file. */
162 if (!rcfile)
163 /* Supply default path name. */
164 rcfile = g_build_filename(g_get_home_dir(), ".castgetrc", NULL);
165
166 kf = _configuration_file_open(rcfile);
167
168 if (kf) {
169 /* Read defaults. */
170 if (g_key_file_has_group(kf, "*")) {
171 /* Verify the keys in the global configuration. */
172 if (channel_configuration_verify_keys(kf, "*") < 0)
173 return -1;
174
175 defaults = channel_configuration_new(kf, "*", NULL);
176 } else
177 defaults = NULL;
178
179 /* Perform actions. */
180 if (optind < argc) {
181 while (optind < argc)
182 _process_channel(channeldir, kf, argv[optind++], op, defaults,
183 filter);
184 } else {
185 groups = g_key_file_get_groups(kf, NULL);
186
187 for (i = 0; groups[i]; i++)
188 if (strcmp(groups[i], "*"))
189 _process_channel(channeldir, kf, groups[i], op, defaults,
190 filter);
191
192 g_strfreev(groups);
193 }
194
195 /* Clean up defaults. */
196 if (defaults)
197 channel_configuration_free(defaults);
198 } else
199 ret = 1;
200
201 /* Clean-up. */
202 g_free(channeldir);
203
204 if (filter)
205 enclosure_filter_free(filter);
206
207 g_free(rcfile);
208
209 if (kf)
210 _configuration_file_close(kf);
211
212 xmlCleanupParser();
213
214 return ret;
215 }
216
version(void)217 static void version(void)
218 {
219 g_printf("%s %s", PACKAGE, VERSION);
220 #ifdef ENABLE_ID3LIB
221 g_printf(" with ID3 tag support\n");
222 #else
223 g_printf("\n");
224 #endif
225
226 g_printf("Copyright (C) 2005-2019 Marius L. Jøhndal <mariuslj at ifi.uio.no>\n");
227 }
228
_print_item_update(const enclosure * enclosure,const gchar * filename)229 static void _print_item_update(const enclosure *enclosure, const gchar *filename)
230 {
231 if (enclosure->length > 0) {
232 gchar *size = g_format_size(enclosure->length);
233 g_printf(" * %s (%s)\n", enclosure->url, size);
234 g_free(size);
235 } else
236 g_printf(" * %s (unknown size)\n", filename);
237 }
238
update_callback(void * user_data,channel_action action,channel_info * channel_info,enclosure * enclosure,const gchar * filename)239 static void update_callback(void *user_data, channel_action action,
240 channel_info *channel_info, enclosure *enclosure,
241 const gchar *filename)
242 {
243 struct channel_configuration *c = (struct channel_configuration *)user_data;
244
245 switch (action) {
246 case CCA_RSS_DOWNLOAD_START:
247 if (!quiet)
248 g_printf("Updating channel %s...\n", c->identifier);
249 break;
250
251 case CCA_RSS_DOWNLOAD_END:
252 break;
253
254 case CCA_ENCLOSURE_DOWNLOAD_START:
255 g_assert(channel_info);
256 g_assert(enclosure);
257
258 if (verbose)
259 _print_item_update(enclosure, filename);
260
261 break;
262
263 case CCA_ENCLOSURE_DOWNLOAD_END:
264 g_assert(channel_info);
265 g_assert(enclosure);
266 g_assert(filename);
267
268 /* Set media tags. */
269 if (enclosure->type && (!strcmp(enclosure->type, "audio/mpeg") || !strcmp(enclosure->type, "audio/mp3"))) {
270 #ifdef ENABLE_ID3LIB
271 if (_id3_check_and_set(filename, c))
272 fprintf(stderr, "Error setting ID3 tag for file %s.\n", filename);
273 #endif /* ENABLE_ID3LIB */
274 }
275
276 /* Update playlist. */
277 if (c->playlist) {
278 playlist_add(c->playlist, filename);
279
280 if (verbose)
281 printf(" * Added downloaded enclosure %s to playlist %s.\n",
282 filename, c->playlist);
283 }
284 break;
285 }
286 }
287
catchup_callback(void * user_data,channel_action action,channel_info * channel_info,enclosure * enclosure,const gchar * filename)288 static void catchup_callback(void *user_data, channel_action action, channel_info *channel_info,
289 enclosure *enclosure, const gchar *filename)
290 {
291 struct channel_configuration *c = (struct channel_configuration *)user_data;
292
293 switch (action) {
294 case CCA_RSS_DOWNLOAD_START:
295 if (!quiet)
296 g_printf("Catching up with channel %s...\n", c->identifier);
297 break;
298
299 case CCA_RSS_DOWNLOAD_END:
300 break;
301
302 case CCA_ENCLOSURE_DOWNLOAD_START:
303 g_assert(channel_info);
304 g_assert(enclosure);
305
306 if (verbose)
307 _print_item_update(enclosure, filename);
308
309 break;
310
311 case CCA_ENCLOSURE_DOWNLOAD_END:
312 break;
313 }
314 }
315
list_callback(void * user_data,channel_action action,channel_info * channel_info,enclosure * enclosure,const gchar * filename)316 static void list_callback(void *user_data, channel_action action, channel_info *channel_info,
317 enclosure *enclosure, const gchar *filename)
318 {
319 struct channel_configuration *c = (struct channel_configuration *)user_data;
320
321 switch (action) {
322 case CCA_RSS_DOWNLOAD_START:
323 g_printf("Listing channel %s...\n", c->identifier);
324 break;
325
326 case CCA_RSS_DOWNLOAD_END:
327 break;
328
329 case CCA_ENCLOSURE_DOWNLOAD_START:
330 g_assert(channel_info);
331 g_assert(enclosure);
332
333 if (verbose)
334 _print_item_update(enclosure, filename);
335
336 break;
337
338 case CCA_ENCLOSURE_DOWNLOAD_END:
339 break;
340 }
341 }
342
_process_channel(const gchar * channel_directory,GKeyFile * kf,const char * identifier,enum op op,struct channel_configuration * defaults,enclosure_filter * filter)343 static int _process_channel(const gchar *channel_directory, GKeyFile *kf, const char *identifier,
344 enum op op, struct channel_configuration *defaults,
345 enclosure_filter *filter)
346 {
347 channel *c;
348 gchar *channel_filename, *channel_file;
349 struct channel_configuration *channel_configuration;
350 enclosure_filter *per_channel_filter = NULL;
351
352 /* Check channel identifier and read channel configuration. */
353 if (!g_key_file_has_group(kf, identifier)) {
354 fprintf(stderr, "Unknown channel identifier %s.\n", identifier);
355
356 return -1;
357 }
358
359 /* Verify the keys in the channel configuration. */
360 if (channel_configuration_verify_keys(kf, identifier) < 0)
361 return -1;
362
363 channel_configuration = channel_configuration_new(kf, identifier, defaults);
364
365 /* Check that mandatory keys were set. */
366 if (!channel_configuration->url) {
367 fprintf(stderr, "No feed URL set for channel %s.\n", identifier);
368
369 channel_configuration_free(channel_configuration);
370 return -1;
371 }
372
373 if (!channel_configuration->spool_directory) {
374 fprintf(stderr, "No spool directory set for channel %s.\n", identifier);
375
376 channel_configuration_free(channel_configuration);
377 return -1;
378 }
379
380 /* Construct channel file name. */
381 channel_filename = g_strjoin(".", identifier, "xml", NULL);
382 channel_file = g_build_filename(channel_directory, channel_filename, NULL);
383 g_free(channel_filename);
384
385 if (new_only && access(channel_file, F_OK) == 0) {
386 /* If we are only fetching new channels, skip the channel if there is
387 already a channel file present. */
388
389 channel_configuration_free(channel_configuration);
390 return 0;
391 }
392
393 c = channel_new(channel_configuration->url, channel_file,
394 channel_configuration->spool_directory,
395 channel_configuration->filename_pattern,
396 resume);
397 g_free(channel_file);
398
399 if (!c) {
400 fprintf(stderr, "Error parsing channel file for channel %s.\n", identifier);
401
402 channel_configuration_free(channel_configuration);
403 return -1;
404 }
405
406 /* Set up per-channel filter unless overridden on the command
407 line. */
408 if (!filter && channel_configuration->regex_filter) {
409 per_channel_filter =
410 enclosure_filter_new(channel_configuration->regex_filter, FALSE);
411
412 filter = per_channel_filter;
413 }
414
415 switch (op) {
416 case OP_UPDATE:
417 channel_update(c, channel_configuration, update_callback, 0, 0,
418 first_only, resume, filter, debug, show_progress_bar);
419 break;
420
421 case OP_CATCHUP:
422 channel_update(c, channel_configuration, catchup_callback, 1, 0,
423 first_only, 0, filter, debug, show_progress_bar);
424 break;
425
426 case OP_LIST:
427 channel_update(c, channel_configuration, list_callback, 1, 1, first_only,
428 0, filter, debug, show_progress_bar);
429 break;
430 }
431
432 /* Clean-up. */
433 if (per_channel_filter)
434 enclosure_filter_free(per_channel_filter);
435
436 channel_free(c);
437 channel_configuration_free(channel_configuration);
438
439 return 0;
440 }
441
_configuration_file_open(const gchar * rcfile)442 static GKeyFile *_configuration_file_open(const gchar *rcfile)
443 {
444 GKeyFile *kf;
445 GError *error = NULL;
446
447 kf = g_key_file_new();
448
449 if (!g_key_file_load_from_file(kf, rcfile, G_KEY_FILE_NONE, &error)) {
450 fprintf(stderr, "Error reading configuration file %s: %s.\n", rcfile, error->message);
451 g_error_free(error);
452 g_key_file_free(kf);
453 kf = NULL;
454 }
455
456 return kf;
457 }
458
_configuration_file_close(GKeyFile * kf)459 static void _configuration_file_close(GKeyFile *kf)
460 {
461 g_key_file_free(kf);
462 }
463
464 #ifdef ENABLE_ID3LIB
_id3_find_and_set_frame(ID3Tag * tag,ID3_FrameID id,const char * value)465 static int _id3_find_and_set_frame(ID3Tag *tag, ID3_FrameID id, const char *value)
466 {
467 ID3Frame *frame;
468 ID3Field *field;
469
470 /* Remove existing tag to avoid issues with trashed frames. */
471 while ((frame = ID3Tag_FindFrameWithID(tag, id)))
472 ID3Tag_RemoveFrame(tag, frame);
473
474 if (value && strlen(value) > 0) {
475 frame = ID3Frame_NewID(id);
476 g_assert(frame);
477
478 ID3Tag_AttachFrame(tag, frame);
479
480 field = ID3Frame_GetField(frame, ID3FN_TEXT);
481
482 if (field)
483 ID3Field_SetASCII(field, value); //TODO: UTF8
484 else
485 return 1;
486 }
487
488 return 0;
489 }
490
_id3_set(const gchar * filename,int clear,const gchar * lead_artist,const gchar * content_group,const gchar * title,const gchar * album,const gchar * content_type,const gchar * year,const gchar * comment)491 static int _id3_set(const gchar *filename, int clear, const gchar *lead_artist,
492 const gchar *content_group, const gchar *title, const gchar *album,
493 const gchar *content_type, const gchar *year, const gchar *comment)
494 {
495 int errors = 0;
496 ID3Tag *tag;
497
498 tag = ID3Tag_New();
499
500 if (!tag)
501 return 1;
502
503 ID3Tag_Link(tag, filename);
504
505 if (clear)
506 ID3Tag_Clear(tag); // TODO
507
508 if (lead_artist) {
509 errors += _id3_find_and_set_frame(tag, ID3FID_LEADARTIST, lead_artist);
510
511 if (verbose)
512 printf(" * Set ID3 tag lead artist to %s.\n", lead_artist);
513 }
514
515 if (content_group) {
516 errors += _id3_find_and_set_frame(tag, ID3FID_CONTENTGROUP, content_group);
517
518 if (verbose)
519 printf(" * Set ID3 tag content group to %s.\n", content_group);
520 }
521
522 if (title) {
523 errors += _id3_find_and_set_frame(tag, ID3FID_TITLE, title);
524
525 if (verbose)
526 printf(" * Set ID3 tag title to %s.\n", title);
527 }
528
529 if (album) {
530 errors += _id3_find_and_set_frame(tag, ID3FID_ALBUM, album);
531
532 if (verbose)
533 printf(" * Set ID3 tag album to %s.\n", album);
534 }
535
536 if (content_type) {
537 errors += _id3_find_and_set_frame(tag, ID3FID_CONTENTTYPE, content_type);
538
539 if (verbose)
540 printf(" * Set ID3 tag content type to %s.\n", content_type);
541 }
542
543 if (year) {
544 errors += _id3_find_and_set_frame(tag, ID3FID_YEAR, year);
545
546 if (verbose)
547 printf(" * Set ID3 title year to %s.\n", year);
548 }
549
550 if (comment) {
551 errors += _id3_find_and_set_frame(tag, ID3FID_COMMENT, comment);
552
553 if (verbose)
554 printf(" * Set ID3 tag comment to %s.\n", comment);
555 }
556
557 if (!errors)
558 ID3Tag_Update(tag);
559
560 ID3Tag_Delete(tag);
561
562 return errors;
563 }
564
_id3_check_and_set(const gchar * filename,const struct channel_configuration * cfg)565 static int _id3_check_and_set(const gchar *filename,
566 const struct channel_configuration *cfg)
567 {
568 if (cfg->id3_lead_artist || cfg->id3_content_group || cfg->id3_title ||
569 cfg->id3_album || cfg->id3_content_type || cfg->id3_year || cfg->id3_comment)
570 return _id3_set(filename, 0, cfg->id3_lead_artist, cfg->id3_content_group,
571 cfg->id3_title, cfg->id3_album, cfg->id3_content_type,
572 cfg->id3_year, cfg->id3_comment);
573 else
574 return 0;
575 }
576
577 #endif /* ENABLE_ID3LIB */
578
playlist_add(const gchar * playlist_file,const gchar * media_file)579 static int playlist_add(const gchar *playlist_file,
580 const gchar *media_file)
581 {
582 FILE *f;
583
584 f = fopen(playlist_file, "a");
585
586 if (!f) {
587 fprintf(stderr, "Error opening playlist file %s: %s.\n",
588 playlist_file, strerror(errno));
589 return -1;
590 }
591
592 fprintf(f, "%s\n", media_file);
593 fclose(f);
594 return 0;
595 }
596