/* Copyright (C) 2005-2019 Marius L. Jøhndal This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* HAVE_CONFIG_H */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #ifdef ENABLE_ID3LIB #include #endif /* ENABLE_ID3LIB */ #include "configuration.h" #include "channel.h" enum op { OP_UPDATE, OP_CATCHUP, OP_LIST }; static int _process_channel(const gchar *channel_directory, GKeyFile *kf, const char *identifier, enum op op, struct channel_configuration *defaults, enclosure_filter *filter); static void version(void); static GKeyFile *_configuration_file_open(const gchar *rcfile); static void _configuration_file_close(GKeyFile *kf); #ifdef ENABLE_ID3LIB static int _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); static int _id3_check_and_set(const gchar *filename, const struct channel_configuration *cfg); #endif /* ENABLE_ID3LIB */ static int playlist_add(const gchar *playlist_file, const gchar *media_file); static gboolean verbose = FALSE; static gboolean quiet = FALSE; static gboolean first_only = FALSE; static gboolean resume = FALSE; static gboolean debug = FALSE; static gboolean show_progress_bar = FALSE; static gboolean show_version = FALSE; static gboolean show_debug_info = FALSE; static gboolean new_only = FALSE; static gboolean list = FALSE; static gboolean catchup = FALSE; static gchar *rcfile = NULL; static gchar *filter_regex = NULL; int main(int argc, char **argv) { enum op op = OP_UPDATE; int i; int ret = 0; gchar **groups; gchar *channeldir; GKeyFile *kf; struct channel_configuration *defaults; enclosure_filter *filter = NULL; GError *error = NULL; GOptionContext *context; static GOptionEntry options[] = { {"catchup", 'c', 0, G_OPTION_ARG_NONE, &catchup, "catch up with channels and exit"}, {"list", 'l', 0, G_OPTION_ARG_NONE, &list, "list available enclosures that have not yet been downloaded and exit"}, {"version", 'V', 0, G_OPTION_ARG_NONE, &show_version, "print version and exit"}, {"resume", 'r', 0, G_OPTION_ARG_NONE, &resume, "resume aborted downloads"}, {"rcfile", 'C', 0, G_OPTION_ARG_FILENAME, &rcfile, "override the default configuration file name"}, {"debug", 'd', 0, G_OPTION_ARG_NONE, &show_debug_info, "print connection debug information"}, {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "print detailed progress information"}, {"progress-bar", 'p', 0, G_OPTION_ARG_NONE, &show_progress_bar, "print progress bar"}, {"new-only", 'n', 0, G_OPTION_ARG_NONE, &new_only, "only process new channels"}, {"quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet, "only print error messages"}, {"first-only", '1', 0, G_OPTION_ARG_NONE, &first_only, "only process the most recent item from each channel"}, {"filter", 'f', 0, G_OPTION_ARG_STRING, &filter_regex, "only process items whose enclosure names match a regular expression"}, { NULL } }; context = g_option_context_new("CHANNELS"); g_option_context_add_main_entries(context, options, NULL); if (!g_option_context_parse(context, &argc, &argv, &error)) { g_print("option parsing failed: %s\n", error->message); exit(1); } /* Do some additional sanity checking of options. */ if ((verbose && quiet) || (show_progress_bar && quiet)) { g_print("option parsing failed: options are incompatible.\n"); exit(1); } if ((catchup && list) || (catchup && show_version) || (list && show_version)) { g_print("option parsing failed: --catchup, --list and --version options are incompatible.\n"); exit(1); } /* Decide on the action to take */ if (show_version) { version(); exit(0); } if (catchup) op = OP_CATCHUP; if (list) op = OP_LIST; if (filter_regex) { filter = enclosure_filter_new(filter_regex, FALSE); g_free(filter_regex); } if (verbose && new_only) g_print("Fetching new channels only...\n"); LIBXML_TEST_VERSION; /* Build the channel directory path and ensure that it exists. */ channeldir = g_build_filename(g_get_home_dir(), ".castget", NULL); if (!g_file_test(channeldir, G_FILE_TEST_IS_DIR)) { if (g_mkdir(channeldir, 0755) < 0) { perror("Error creating channel directory"); return 1; } } /* Try opening configuration file. */ if (!rcfile) /* Supply default path name. */ rcfile = g_build_filename(g_get_home_dir(), ".castgetrc", NULL); kf = _configuration_file_open(rcfile); if (kf) { /* Read defaults. */ if (g_key_file_has_group(kf, "*")) { /* Verify the keys in the global configuration. */ if (channel_configuration_verify_keys(kf, "*") < 0) return -1; defaults = channel_configuration_new(kf, "*", NULL); } else defaults = NULL; /* Perform actions. */ if (optind < argc) { while (optind < argc) _process_channel(channeldir, kf, argv[optind++], op, defaults, filter); } else { groups = g_key_file_get_groups(kf, NULL); for (i = 0; groups[i]; i++) if (strcmp(groups[i], "*")) _process_channel(channeldir, kf, groups[i], op, defaults, filter); g_strfreev(groups); } /* Clean up defaults. */ if (defaults) channel_configuration_free(defaults); } else ret = 1; /* Clean-up. */ g_free(channeldir); if (filter) enclosure_filter_free(filter); g_free(rcfile); if (kf) _configuration_file_close(kf); xmlCleanupParser(); return ret; } static void version(void) { g_printf("%s %s", PACKAGE, VERSION); #ifdef ENABLE_ID3LIB g_printf(" with ID3 tag support\n"); #else g_printf("\n"); #endif g_printf("Copyright (C) 2005-2019 Marius L. Jøhndal \n"); } static void _print_item_update(const enclosure *enclosure, const gchar *filename) { if (enclosure->length > 0) { gchar *size = g_format_size(enclosure->length); g_printf(" * %s (%s)\n", enclosure->url, size); g_free(size); } else g_printf(" * %s (unknown size)\n", filename); } static void update_callback(void *user_data, channel_action action, channel_info *channel_info, enclosure *enclosure, const gchar *filename) { struct channel_configuration *c = (struct channel_configuration *)user_data; switch (action) { case CCA_RSS_DOWNLOAD_START: if (!quiet) g_printf("Updating channel %s...\n", c->identifier); break; case CCA_RSS_DOWNLOAD_END: break; case CCA_ENCLOSURE_DOWNLOAD_START: g_assert(channel_info); g_assert(enclosure); if (verbose) _print_item_update(enclosure, filename); break; case CCA_ENCLOSURE_DOWNLOAD_END: g_assert(channel_info); g_assert(enclosure); g_assert(filename); /* Set media tags. */ if (enclosure->type && (!strcmp(enclosure->type, "audio/mpeg") || !strcmp(enclosure->type, "audio/mp3"))) { #ifdef ENABLE_ID3LIB if (_id3_check_and_set(filename, c)) fprintf(stderr, "Error setting ID3 tag for file %s.\n", filename); #endif /* ENABLE_ID3LIB */ } /* Update playlist. */ if (c->playlist) { playlist_add(c->playlist, filename); if (verbose) printf(" * Added downloaded enclosure %s to playlist %s.\n", filename, c->playlist); } break; } } static void catchup_callback(void *user_data, channel_action action, channel_info *channel_info, enclosure *enclosure, const gchar *filename) { struct channel_configuration *c = (struct channel_configuration *)user_data; switch (action) { case CCA_RSS_DOWNLOAD_START: if (!quiet) g_printf("Catching up with channel %s...\n", c->identifier); break; case CCA_RSS_DOWNLOAD_END: break; case CCA_ENCLOSURE_DOWNLOAD_START: g_assert(channel_info); g_assert(enclosure); if (verbose) _print_item_update(enclosure, filename); break; case CCA_ENCLOSURE_DOWNLOAD_END: break; } } static void list_callback(void *user_data, channel_action action, channel_info *channel_info, enclosure *enclosure, const gchar *filename) { struct channel_configuration *c = (struct channel_configuration *)user_data; switch (action) { case CCA_RSS_DOWNLOAD_START: g_printf("Listing channel %s...\n", c->identifier); break; case CCA_RSS_DOWNLOAD_END: break; case CCA_ENCLOSURE_DOWNLOAD_START: g_assert(channel_info); g_assert(enclosure); if (verbose) _print_item_update(enclosure, filename); break; case CCA_ENCLOSURE_DOWNLOAD_END: break; } } static int _process_channel(const gchar *channel_directory, GKeyFile *kf, const char *identifier, enum op op, struct channel_configuration *defaults, enclosure_filter *filter) { channel *c; gchar *channel_filename, *channel_file; struct channel_configuration *channel_configuration; enclosure_filter *per_channel_filter = NULL; /* Check channel identifier and read channel configuration. */ if (!g_key_file_has_group(kf, identifier)) { fprintf(stderr, "Unknown channel identifier %s.\n", identifier); return -1; } /* Verify the keys in the channel configuration. */ if (channel_configuration_verify_keys(kf, identifier) < 0) return -1; channel_configuration = channel_configuration_new(kf, identifier, defaults); /* Check that mandatory keys were set. */ if (!channel_configuration->url) { fprintf(stderr, "No feed URL set for channel %s.\n", identifier); channel_configuration_free(channel_configuration); return -1; } if (!channel_configuration->spool_directory) { fprintf(stderr, "No spool directory set for channel %s.\n", identifier); channel_configuration_free(channel_configuration); return -1; } /* Construct channel file name. */ channel_filename = g_strjoin(".", identifier, "xml", NULL); channel_file = g_build_filename(channel_directory, channel_filename, NULL); g_free(channel_filename); if (new_only && access(channel_file, F_OK) == 0) { /* If we are only fetching new channels, skip the channel if there is already a channel file present. */ channel_configuration_free(channel_configuration); return 0; } c = channel_new(channel_configuration->url, channel_file, channel_configuration->spool_directory, channel_configuration->filename_pattern, resume); g_free(channel_file); if (!c) { fprintf(stderr, "Error parsing channel file for channel %s.\n", identifier); channel_configuration_free(channel_configuration); return -1; } /* Set up per-channel filter unless overridden on the command line. */ if (!filter && channel_configuration->regex_filter) { per_channel_filter = enclosure_filter_new(channel_configuration->regex_filter, FALSE); filter = per_channel_filter; } switch (op) { case OP_UPDATE: channel_update(c, channel_configuration, update_callback, 0, 0, first_only, resume, filter, debug, show_progress_bar); break; case OP_CATCHUP: channel_update(c, channel_configuration, catchup_callback, 1, 0, first_only, 0, filter, debug, show_progress_bar); break; case OP_LIST: channel_update(c, channel_configuration, list_callback, 1, 1, first_only, 0, filter, debug, show_progress_bar); break; } /* Clean-up. */ if (per_channel_filter) enclosure_filter_free(per_channel_filter); channel_free(c); channel_configuration_free(channel_configuration); return 0; } static GKeyFile *_configuration_file_open(const gchar *rcfile) { GKeyFile *kf; GError *error = NULL; kf = g_key_file_new(); if (!g_key_file_load_from_file(kf, rcfile, G_KEY_FILE_NONE, &error)) { fprintf(stderr, "Error reading configuration file %s: %s.\n", rcfile, error->message); g_error_free(error); g_key_file_free(kf); kf = NULL; } return kf; } static void _configuration_file_close(GKeyFile *kf) { g_key_file_free(kf); } #ifdef ENABLE_ID3LIB static int _id3_find_and_set_frame(ID3Tag *tag, ID3_FrameID id, const char *value) { ID3Frame *frame; ID3Field *field; /* Remove existing tag to avoid issues with trashed frames. */ while ((frame = ID3Tag_FindFrameWithID(tag, id))) ID3Tag_RemoveFrame(tag, frame); if (value && strlen(value) > 0) { frame = ID3Frame_NewID(id); g_assert(frame); ID3Tag_AttachFrame(tag, frame); field = ID3Frame_GetField(frame, ID3FN_TEXT); if (field) ID3Field_SetASCII(field, value); //TODO: UTF8 else return 1; } return 0; } static int _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) { int errors = 0; ID3Tag *tag; tag = ID3Tag_New(); if (!tag) return 1; ID3Tag_Link(tag, filename); if (clear) ID3Tag_Clear(tag); // TODO if (lead_artist) { errors += _id3_find_and_set_frame(tag, ID3FID_LEADARTIST, lead_artist); if (verbose) printf(" * Set ID3 tag lead artist to %s.\n", lead_artist); } if (content_group) { errors += _id3_find_and_set_frame(tag, ID3FID_CONTENTGROUP, content_group); if (verbose) printf(" * Set ID3 tag content group to %s.\n", content_group); } if (title) { errors += _id3_find_and_set_frame(tag, ID3FID_TITLE, title); if (verbose) printf(" * Set ID3 tag title to %s.\n", title); } if (album) { errors += _id3_find_and_set_frame(tag, ID3FID_ALBUM, album); if (verbose) printf(" * Set ID3 tag album to %s.\n", album); } if (content_type) { errors += _id3_find_and_set_frame(tag, ID3FID_CONTENTTYPE, content_type); if (verbose) printf(" * Set ID3 tag content type to %s.\n", content_type); } if (year) { errors += _id3_find_and_set_frame(tag, ID3FID_YEAR, year); if (verbose) printf(" * Set ID3 title year to %s.\n", year); } if (comment) { errors += _id3_find_and_set_frame(tag, ID3FID_COMMENT, comment); if (verbose) printf(" * Set ID3 tag comment to %s.\n", comment); } if (!errors) ID3Tag_Update(tag); ID3Tag_Delete(tag); return errors; } static int _id3_check_and_set(const gchar *filename, const struct channel_configuration *cfg) { if (cfg->id3_lead_artist || cfg->id3_content_group || cfg->id3_title || cfg->id3_album || cfg->id3_content_type || cfg->id3_year || cfg->id3_comment) return _id3_set(filename, 0, cfg->id3_lead_artist, cfg->id3_content_group, cfg->id3_title, cfg->id3_album, cfg->id3_content_type, cfg->id3_year, cfg->id3_comment); else return 0; } #endif /* ENABLE_ID3LIB */ static int playlist_add(const gchar *playlist_file, const gchar *media_file) { FILE *f; f = fopen(playlist_file, "a"); if (!f) { fprintf(stderr, "Error opening playlist file %s: %s.\n", playlist_file, strerror(errno)); return -1; } fprintf(f, "%s\n", media_file); fclose(f); return 0; }