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