From e8831433c00c70b8f92644edee81f45855c750e8 Mon Sep 17 00:00:00 2001 From: icepic0 Date: Sat, 1 Aug 2015 14:21:20 -0400 Subject: [PATCH 1/3] Command line options and Plex file naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added command line switches to control man hardcoded files, paths, and options. The defaults are the same except M3U files are off by default. I’m thinking about making some of these configurations in the database. - Added an option to save the file with the format Name.Season/Year Episode/Month+Day.Title of Episode format which looks better and allows Plex to read it as a TV series. --- PodGrab.py | 120 ++++++++++++++++++++++++++++++++++++++++++----------- README | 12 +++++- 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/PodGrab.py b/PodGrab.py index a81d25e..121de55 100755 --- a/PodGrab.py +++ b/PodGrab.py @@ -6,9 +6,16 @@ # Jonathan Baker # jon@the-node.org (http://the-node.org) -# Werner Avenant - added small changes to write M3U file of podcasts downloaded today +# Version: 1.1.3 - +# - added small changes to write M3U file of podcasts downloaded today +# Werner Avenant # werner.avenant@gmail.com (http://www.collectiveminds.co.za) +# Version: 1.1.4 - 07/31/2015 +# - added command line switches for db location, download location, plex configuration, M3U creation +# - changed mkdir to mkdirs +# David Smith + # Do with this code what you will, it's "open source". As a courtesy, # I would appreciate credit if you base your code on mine. If you find # a bug or think the code sucks balls, please let me know :-) @@ -35,7 +42,7 @@ import platform import traceback import unicodedata - +from subprocess import Popen, PIPE MODE_NONE = 70 @@ -51,6 +58,8 @@ MODE_IMPORT = 80 NUM_MAX_DOWNLOADS = 4 +PLEX_NAMING = 0 +CREATE_M3U = 0 DOWNLOAD_DIRECTORY = "podcasts" #DOWNLOAD_DIRECTORY = os.path.realpath("/home/hrehfeld/host/d/download/podcasts_podgrab") @@ -59,7 +68,6 @@ current_directory = '' m3u_file = '' - total_item = 0 total_size = 0 has_error = 0 @@ -81,14 +89,22 @@ def main(argv): now = datetime.datetime.now(); m3u_file = str(now)[:10] + '.m3u' current_directory = os.path.realpath(os.path.dirname(sys.argv[0])) - download_directory = DOWNLOAD_DIRECTORY + global db_name + global db_path + db_name = "PodGrab.db" + db_path=current_directory + global DOWNLOAD_DIRECTORY + global NUM_MAX_DOWNLOADS + global PLEX_NAMING + global CREATE_M3U global total_items global total_size total_items = 0 total_size = 0 data = "" + parser = argparse.ArgumentParser(description='A command line Podcast downloader for RSS XML feeds') parser.add_argument('-s', '--subscribe', action="store", dest="sub_feed_url", help='Subscribe to the following XML feed and download latest podcast') parser.add_argument('-d', '--download', action="store", dest="dl_feed_url", help='Bulk download all podcasts in the following XML feed or file') @@ -102,9 +118,38 @@ def main(argv): parser.add_argument('-io', '--import', action="store", dest="opml_import", help='Import subscriptions from OPML file') parser.add_argument('-eo', '--export', action="store_const", const="OPML_EXPORT", dest="opml_export", help='Export subscriptions to OPML file') + + parser.add_argument('-pn', '--plex-naming', action="store_true", dest="plex_naming", help='Name files with Season=Year and Epsiode=Month+Day') + parser.add_argument('-max', '--max-downloads', action="store", dest="max_downloads", help='Max number of podcasts to download') + parser.add_argument('-dir', '--download-directory', action="store", dest="download_directory", help='Directory to store podcasts in') + parser.add_argument('-db', '--db_path', action="store", dest="db_path", help='Location of the PodGrab.db file') + parser.add_argument('-m3u', '--create-m3u', action="store_true", dest="create_m3u", help='Create m3u files for playlists') + arguments = parser.parse_args() + if arguments.download_directory: + DOWNLOAD_DIRECTORY = arguments.download_directory + + if arguments.db_path: + db_path = arguments.db_path + + if arguments.max_downloads: + NUM_MAX_DOWNLOADS = arguments.max_downloads + print("Max items per podcast is " + str(NUM_MAX_DOWNLOADS)) + + if arguments.plex_naming: + print("PLEX naming is on") + PLEX_NAMING = 1 + else: + print("PLEX naming is off") + + if arguments.create_m3u: + print("M3U files will be created") + CREATE_M3U = 1 + else: + print("M3U files will not created") + if arguments.sub_feed_url: feed_url = arguments.sub_feed_url data = open_datasource(feed_url) @@ -160,6 +205,7 @@ def main(argv): print("Default encoding: " + sys.getdefaultencoding()) todays_date = strftime("%a, %d %b %Y %H:%M:%S", gmtime()) print("Current Directory: " + current_directory) + if does_database_exist(current_directory): connection = connect_database(current_directory) if not connection: @@ -179,16 +225,16 @@ def main(argv): setup_database(cursor, connection) print("Database setup complete") - if not os.path.exists(download_directory): + if not os.path.exists(DOWNLOAD_DIRECTORY): print("Podcast download directory is missing. Creating...") try: - os.mkdir(download_directory) - print("Download directory '" + download_directory + "' created") + os.makedirs(DOWNLOAD_DIRECTORY) + print("Download directory '" + DOWNLOAD_DIRECTORY + "' created") except OSError: error_string = "Could not create podcast download sub-directory!" has_error = 1 else: - print("Download directory exists: '" + download_directory + "'" ) + print("Download directory exists: '" + DOWNLOAD_DIRECTORY + "'" ) if not has_error: if mode == MODE_UNSUBSCRIBE: feed_name = get_name_from_feed(cursor, connection, feed_url) @@ -196,7 +242,7 @@ def main(argv): print("Feed does not exist in the database! Skipping...") else: feed_name = clean_string(feed_name) - channel_directory = download_directory + os.sep + feed_name + channel_directory = DOWNLOAD_DIRECTORY + os.sep + feed_name print("Deleting '" + channel_directory + "'...") delete_subscription(cursor, connection, feed_url) try : @@ -218,7 +264,7 @@ def main(argv): if not data: print("'" + feed_url + "' for '" + feed_name + "' is not a valid feed URL!") else: - message = iterate_feed(data, mode, download_directory, todays_date, cursor, connection, feed_url) + message = iterate_feed(data, mode, DOWNLOAD_DIRECTORY, todays_date, cursor, connection, feed_url) print(message) mail += message mail = mail + "\n\n" + str(total_items) + " podcasts totalling " + str(total_size) + " bytes have been downloaded." @@ -226,7 +272,7 @@ def main(argv): print("Have e-mail address(es) - attempting e-mail...") mail_updates(cursor, connection, mail, str(total_items)) elif mode == MODE_DOWNLOAD or mode == MODE_SUBSCRIBE: - print(iterate_feed(data, mode, download_directory, todays_date, cursor, connection, feed_url)) + print(iterate_feed(data, mode, DOWNLOAD_DIRECTORY, todays_date, cursor, connection, feed_url)) elif mode == MODE_MAIL_ADD: add_mail_user(cursor, connection, mail_address) print("E-Mail address: " + mail_address + " has been added") @@ -238,7 +284,7 @@ def main(argv): elif mode == MODE_EXPORT: export_opml_file(cursor, connection, current_directory) elif mode == MODE_IMPORT: - import_opml_file(cursor, connection, current_directory, download_directory, import_file_name) + import_opml_file(cursor, connection, current_directory, DOWNLOAD_DIRECTORY, import_file_name) else: print("Sorry, there was some sort of error: '" + error_string + "'\nExiting...\n") if connection: @@ -381,9 +427,9 @@ def clean_string(str): new_string = new_string.rstrip("-") new_string_final = '' for c in new_string: - if c.isalnum() or c == "-" or c == "." or c.isspace(): + if c.isalnum() or c == "-" or c == "_" or c == "." or c.isspace(): new_string_final = new_string_final + ''.join(c) - new_string_final = new_string_final.replace(' ','-') + new_string_final = new_string_final.replace(' ','_') new_string_final = new_string_final.replace('---','-') new_string_final = new_string_final.replace('--','-') new_string_final = new_string_final.strip() @@ -392,13 +438,21 @@ def clean_string(str): # Change 2011-10-06 - Changed chan_loc to channel_title to help with relative path names # in the m3u file -def write_podcast(item, channel_title, date, type): +def write_podcast(item, channel_title, date, type, title, desc): (item_path, item_file_name) = os.path.split(item) + plex_info = "" + item_save_name = item_file_name + + # Added name and season to the saved file name based on the date released. This is compatible with Plex TV inputs. + if PLEX_NAMING: + struct_time_item = datetime.datetime.strptime(fix_date(date), "%a, %d %b %Y %H:%M:%S") + plex_info = channel_title + "." + struct_time_item.strftime("S%YE%m%d") + "." + item_save_name = plex_info + title if len(item_file_name) > 50: item_file_name = item_file_name[:50] - local_file = DOWNLOAD_DIRECTORY + os.sep + channel_title + os.sep + clean_string(item_file_name) + local_file = DOWNLOAD_DIRECTORY + os.sep + channel_title + os.sep + clean_string(item_save_name) if type == "video/quicktime" or type == "audio/mp4" or type == "video/mp4": if not local_file.endswith(".mp4"): local_file = local_file + ".mp4" @@ -436,7 +490,7 @@ def write_podcast(item, channel_title, date, type): if os.path.exists(local_file) and os.path.getsize(local_file) != 0: return 'File Exists' else: - print("\nDownloading " + item_file_name + " which was published on " + date) + print("\nDownloading " + item_file_name + " as \"" + clean_string(item_save_name) + "\"" + " which was published on " + date) try: req = urllib2.urlopen(item) CHUNK = 16 * 1024 @@ -457,9 +511,11 @@ def write_podcast(item, channel_title, date, type): #output.close() print("Podcast: " + item + " downloaded to: " + local_file) # 2011-11-06 Append to m3u file - output = open(current_directory + os.sep + m3u_file, 'a') - output.write(DOWNLOAD_DIRECTORY + os.sep + channel_title + os.sep + item_file_name + "\n") - output.close() + if CREATE_M3U: + output = open(DOWNLOAD_DIRECTORY + os.sep + m3u_file, 'a') + output.write(DOWNLOAD_DIRECTORY + os.sep + channel_title + os.sep + item_file_name + "\n") + output.close() + return 'Successful Write' except urllib2.URLError as e: print("ERROR - Could not write item to file: " + e) @@ -467,8 +523,9 @@ def write_podcast(item, channel_title, date, type): def does_database_exist(curr_loc): - db_name = "PodGrab.db" - if os.path.exists(curr_loc + os.sep + db_name): + #db_name = "PodGrab.db" + #if os.path.exists(curr_loc + os.sep + db_name): + if os.path.exists(db_path + os.sep + db_name): return 1 else: return 0 @@ -532,9 +589,19 @@ def mail(server_url=None, sender='', to='', subject='', text=''): def connect_database(curr_loc): - conn = sqlite3.connect(curr_loc + os.sep + "PodGrab.db") + #conn = sqlite3.connect(curr_loc + os.sep + "PodGrab.db") + if not os.path.exists(db_path): + try: + print("Creating dir " + db_path) + os.makedirs(db_path) + except OSError: + error_string = "Could not create podcast database directory!" + return 0 + + conn = sqlite3.connect(db_path + os.sep + db_name) return conn + def setup_database(cur, conn): cur.execute("CREATE TABLE subscriptions (channel text, feed text, last_ep text)") cur.execute("CREATE TABLE email (address text)") @@ -574,12 +641,16 @@ def iterate_channel(chan, today, mode, cur, conn, feed, channel_title): for item in chan.getElementsByTagName('item'): try: item_title = item.getElementsByTagName('title')[0].firstChild.data + item_desc = item.getElementsByTagName('description')[0].firstChild.data item_date = item.getElementsByTagName('pubDate')[0].firstChild.data item_file = item.getElementsByTagName('enclosure')[0].getAttribute('url') item_size = item.getElementsByTagName('enclosure')[0].getAttribute('length') item_type = item.getElementsByTagName('enclosure')[0].getAttribute('type') struct_time_today = strptime(today, "%a, %d %b %Y %H:%M:%S") + #item_title = item_title.strip() + #item_desc = item_desc.strip() + has_error = 0 try: struct_time_item = strptime(fix_date(item_date), "%a, %d %b %Y %H:%M:%S") @@ -599,7 +670,7 @@ def iterate_channel(chan, today, mode, cur, conn, feed, channel_title): if not has_error: if mktime(struct_time_item) > mktime(struct_last_ep) or mode == MODE_DOWNLOAD: - saved = write_podcast(item_file, channel_title, item_date, item_type) + saved = write_podcast(item_file, channel_title, item_date, item_type, item_title, item_desc) if saved == 'File Exists': print("File Existed - updating local database's Last Episode") @@ -607,6 +678,7 @@ def iterate_channel(chan, today, mode, cur, conn, feed, channel_title): if saved == 'Successful Write': print("\nTitle: " + item_title) + print("Description: " + item_desc) print("Date: " + item_date) print("File: " + item_file) print("Size: " + item_size + " bytes") diff --git a/README b/README index 067d3af..4bb88bf 100644 --- a/README +++ b/README @@ -24,7 +24,7 @@ Author: Werner Avenant werner.avenant@gmail.com (http://www.collectiveminds.co.z Changes after fork: - Added support for M3U files listing all files downloaded that day - - Last Episode Detection wasn't always right. It wasn't noticable + - Last Episode Detection wasn't always right. It wasn't noticeable because if the file existed it wouldn't download the episode. Rewrote last_ep logic - Changed write_podcast to return if a file existed. This in turn @@ -33,3 +33,13 @@ Changes after fork: - Function update_subscription will check to see if the last_ep is older than the existing last_ep - Moved NUM_MAX_DOWNLOAD to the front of the file for easy configuration + +==== CHANGES MADE AFTER FORK ==== + +Author: David Smith + +Changes after fork: + + - Added option to output file names in an Season/Year Episode/Month+Day Title of Episode format + - Added command line switches for db location, download location, plex configuration, M3U creation + - Changed mkdir to mkdirs to deal with creating multiple levels of directories From 64b96a1aaa5811b655499c58ce4242ef12ddadef Mon Sep 17 00:00:00 2001 From: icepic0 Date: Sun, 2 Aug 2015 19:51:11 -0400 Subject: [PATCH 2/3] PodGrab, now with metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The major update is to add Title and Description to the metadata fields in the podcasts if the do not exist. I used ffmpeg so if you don’t have it won’t work, but it shouldn’t break anything else. I also in included a new executable that just does metadata updates. --- .gitignore | 2 + PodGrab.py | 191 ++++++++++++++++++++++++++++++++++----------- update_metadata.py | 136 ++++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+), 44 deletions(-) create mode 100755 update_metadata.py diff --git a/.gitignore b/.gitignore index e65c467..85a53a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ PodGrab.db *.m3u + +test diff --git a/PodGrab.py b/PodGrab.py index 121de55..8f576af 100755 --- a/PodGrab.py +++ b/PodGrab.py @@ -42,7 +42,7 @@ import platform import traceback import unicodedata -from subprocess import Popen, PIPE +from subprocess import Popen, PIPE, call MODE_NONE = 70 @@ -60,6 +60,7 @@ NUM_MAX_DOWNLOADS = 4 PLEX_NAMING = 0 CREATE_M3U = 0 +UPDATE_METADATA = 0 DOWNLOAD_DIRECTORY = "podcasts" #DOWNLOAD_DIRECTORY = os.path.realpath("/home/hrehfeld/host/d/download/podcasts_podgrab") @@ -94,6 +95,7 @@ def main(argv): db_name = "PodGrab.db" db_path=current_directory + global UPDATE_METADATA global DOWNLOAD_DIRECTORY global NUM_MAX_DOWNLOADS global PLEX_NAMING @@ -124,10 +126,17 @@ def main(argv): parser.add_argument('-dir', '--download-directory', action="store", dest="download_directory", help='Directory to store podcasts in') parser.add_argument('-db', '--db_path', action="store", dest="db_path", help='Location of the PodGrab.db file') parser.add_argument('-m3u', '--create-m3u', action="store_true", dest="create_m3u", help='Create m3u files for playlists') + parser.add_argument('-um', '--update_metadata', action="store_true", dest="update_metadata", help='Use ffmpeg to update metadata with the title and description from the feed') arguments = parser.parse_args() + if arguments.update_metadata: + print("Metadata will be updated") + UPDATE_METADATA = 1 + else: + print("Metadata will be left alone") + if arguments.download_directory: DOWNLOAD_DIRECTORY = arguments.download_directory @@ -438,7 +447,7 @@ def clean_string(str): # Change 2011-10-06 - Changed chan_loc to channel_title to help with relative path names # in the m3u file -def write_podcast(item, channel_title, date, type, title, desc): +def write_podcast(item, channel_title, date, type, title, metadata_feed): (item_path, item_file_name) = os.path.split(item) plex_info = "" item_save_name = item_file_name @@ -449,40 +458,12 @@ def write_podcast(item, channel_title, date, type, title, desc): plex_info = channel_title + "." + struct_time_item.strftime("S%YE%m%d") + "." item_save_name = plex_info + title - if len(item_file_name) > 50: - item_file_name = item_file_name[:50] + if len(item_save_name) > 50: + item_save_name = item_save_name[:50] local_file = DOWNLOAD_DIRECTORY + os.sep + channel_title + os.sep + clean_string(item_save_name) - if type == "video/quicktime" or type == "audio/mp4" or type == "video/mp4": - if not local_file.endswith(".mp4"): - local_file = local_file + ".mp4" - - elif type == "video/mpeg": - if not local_file.endswith(".mpg"): - local_file = local_file + ".mpg" - elif type == "video/x-flv": - if not local_file.endswith(".flv"): - local_file = local_file + ".flv" - - elif type == "video/x-ms-wmv": - if not local_file.endswith(".wmv"): - local_file = local_file + ".wmv" - - elif type == "video/webm" or type == "audio/webm": - if not local_file.endswith(".webm"): - local_file = local_file + ".webm" - - elif type == "audio/mpeg": - if not local_file.endswith(".mp3"): - local_file = local_file + ".mp3" - - elif type == "audio/ogg" or type == "video/ogg" or type == "audio/vorbis": - if not local_file.endswith(".ogg"): - local_file = local_file + ".ogg" - elif type == "audio/x-ms-wma" or type == "audio/x-ms-wax": - if not local_file.endswith(".wma"): - local_file = local_file + ".wma" + local_file = fix_file_extention(type, local_file) # Check if file exists, but if the file size is zero (which happens when the user # presses Crtl-C during a download) - the the code should go ahead and download @@ -501,30 +482,145 @@ def write_podcast(item, channel_title, date, type, title, desc): fp.write(chunk) item_file_name = os.path.basename(fp.name) - - #item_file = urllib2.urlopen(item) - #output = open(local_file, 'wb') - # 2011-10-06 Werner Avenant - For some reason the file name changes when - # saved to disk - probably a python feature (sorry, only wrote my first line of python today) - #item_file_name = os.path.basename(output.name) - #output.write(item_file.read()) - #output.close() print("Podcast: " + item + " downloaded to: " + local_file) + # 2011-11-06 Append to m3u file if CREATE_M3U: + print("Creating M3U file in " + DOWNLOAD_DIRECTORY + os.sep + m3u_file) output = open(DOWNLOAD_DIRECTORY + os.sep + m3u_file, 'a') output.write(DOWNLOAD_DIRECTORY + os.sep + channel_title + os.sep + item_file_name + "\n") output.close() + # add missing metadata in the file to match metadata in the feed + if UPDATE_METADATA: + metadata_file = read_metadata(local_file) + if metadata_file: + for key in sorted(iter(metadata_file)): + print("Existing Metadata: " + key + "=" + metadata_file[key]) + metadata_write = write_metadata(local_file, metadata_feed, metadata_file) return 'Successful Write' except urllib2.URLError as e: print("ERROR - Could not write item to file: " + e) return 'Write Error' +# Fix any odd file endings +def fix_file_extention(type, local_file): + if type == "video/quicktime" or type == "audio/mp4" or type == "video/mp4": + if not local_file.endswith(".mp4"): + local_file = local_file + ".mp4" + elif type == "video/mpeg": + if not local_file.endswith(".mpg"): + local_file = local_file + ".mpg" + elif type == "video/x-flv": + if not local_file.endswith(".flv"): + local_file = local_file + ".flv" + elif type == "video/x-ms-wmv": + if not local_file.endswith(".wmv"): + local_file = local_file + ".wmv" + elif type == "video/webm" or type == "audio/webm": + if not local_file.endswith(".webm"): + local_file = local_file + ".webm" + elif type == "audio/mpeg": + if not local_file.endswith(".mp3"): + local_file = local_file + ".mp3" + elif type == "audio/ogg" or type == "video/ogg" or type == "audio/vorbis": + if not local_file.endswith(".ogg"): + local_file = local_file + ".ogg" + elif type == "audio/x-ms-wma" or type == "audio/x-ms-wax": + if not local_file.endswith(".wma"): + local_file = local_file + ".wma" + return(local_file) + + +# read metadata from an audio or video file. Assumes that it can call ffmpg in the path. This dependency should be fixed. +# I've only tested with mp4 video files and mp3 audio files. +def read_metadata(local_file): + metadata = metadata_feed = dict() + #print("\nReading file: " + local_file) + if not os.path.exists(local_file): + print("File not found for metadata update") + return 1 + + cmd_line = ['ffmpeg', '-loglevel', 'quiet', '-i', local_file, '-f', 'ffmetadata', '-'] + + try: + process = Popen(cmd_line, stdout=PIPE, stderr=PIPE) # I'm not sure if I want to do anything with stderr yet + stdout, stderr = process.communicate() + except OSError as e: + print >>sys.stderr, "FFMPEG Failed, aborting metadata updates:", e + return 0 + for line in stdout.splitlines(): + line.rstrip() + tokens = line.partition('=') + if tokens[2]: + #print("DATA: " + tokens[0] + " = " + tokens[2]) + if tokens[0] == 'title': + metadata['TITLE_MATCH'] = tokens[2] + elif tokens[0] == 'description' or tokens[0] == 'TDES': + metadata['DESCRIPTION_MATCH'] = tokens[2] + #elif tokens[0] == 'album': + # metadata['ALBUM_MATCH'] = tokens[2] + #elif tokens[0] == 'minor_version': + # metadata['EPISODE_MATCH'] = tokens[2] + + metadata[tokens[0]] = tokens[2] + #else: + # print("Not valid metadata: ", line) + + return(metadata) + + +# write metadata to an audio or video file. Assumes that it can call ffmpg in the path. This dependency should be fixed. +def write_metadata(local_file, metadata_feed, metadata_file): + update_needed = 0 + cmd_line = ['ffmpeg', '-y', '-loglevel', 'quiet', '-i', local_file] + (item_path, item_file_name) = os.path.split(local_file) + tmp_file = item_path + os.sep + "TMP_" + item_file_name # note, for ffmpeg this needs to be the same extention + + # Which metadata do we have? + if not 'TITLE_MATCH' in metadata_file: + #print("Adding Title: " + metadata_feed['title']) + update_needed = 1 + cmd_line.extend(['-metadata', "title=" + metadata_feed['title']]) + + if not 'DESCRIPTION_MATCH' in metadata_file: + #print("Adding Description: " + metadata_feed['description']) + update_needed = 1 + cmd_line.extend(['-metadata', "description=" + metadata_feed['description']]) + + if update_needed: + print("Updating Metadata on " + local_file) + + cmd_line_mapping = ['-map', '0', '-codec', 'copy'] + cmd_line_end = [tmp_file] + + try: + rtn = call(cmd_line + cmd_line_mapping + cmd_line_end) + if rtn == 0: + os.rename(tmp_file, local_file) + else: + # I have some podcasts that seem to have extra streams in them. I found this on Apple Byte podcast which has RTP hit streams. + #print >>sys.stderr, "Child returned", rtn + print("Unknown streams found, Trying to copy just one stream of audio and video for metadata") + cmd_line_mapping = ['-codec', 'copy'] + rtn = call(cmd_line + cmd_line_mapping + cmd_line_end) + if rtn != 0: + print("Copy Failed") + if os.path.exists(tmp_file): + os.remove(tmp_file) + return rtn + else: + os.rename(tmp_file, local_file) + except OSError as e: + print >>sys.stderr, "Execution failed:", e + return 1 + else: + print("File already has embedded title and description, no need to update the file") + return 0 + + def does_database_exist(curr_loc): - #db_name = "PodGrab.db" - #if os.path.exists(curr_loc + os.sep + db_name): if os.path.exists(db_path + os.sep + db_name): return 1 else: @@ -650,6 +746,13 @@ def iterate_channel(chan, today, mode, cur, conn, feed, channel_title): #item_title = item_title.strip() #item_desc = item_desc.strip() + metadata_feed = dict() + metadata_feed['title'] = item_title + metadata_feed['description'] = item_desc + metadata_feed['date'] = item_date + metadata_feed['file'] = item_file + metadata_feed['size'] = item_size + metadata_feed['type'] = item_type has_error = 0 try: @@ -670,7 +773,7 @@ def iterate_channel(chan, today, mode, cur, conn, feed, channel_title): if not has_error: if mktime(struct_time_item) > mktime(struct_last_ep) or mode == MODE_DOWNLOAD: - saved = write_podcast(item_file, channel_title, item_date, item_type, item_title, item_desc) + saved = write_podcast(item_file, channel_title, item_date, item_type, item_title, metadata_feed) if saved == 'File Exists': print("File Existed - updating local database's Last Episode") diff --git a/update_metadata.py b/update_metadata.py new file mode 100755 index 0000000..f59b9ce --- /dev/null +++ b/update_metadata.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python + +import sys +import os +import argparse +from subprocess import Popen, PIPE, call + + +def main(argv): + + parser = argparse.ArgumentParser(description='A command line way to edit video and audio metadata.') + parser.add_argument('-f', '--file', action="store", dest="file", help='File to process') + parser.add_argument('-t', '--title', action="store", dest="title", help='Title to apply') + parser.add_argument('-d', '--description', action="store", dest="description", help='Description to apply') + parser.add_argument('-ro', '--read_only', action="store_true", dest="read_only", help='Only print metadata, do not write') + + arguments = parser.parse_args() + + metadata_feed = dict() + metadata_feed['title'] = arguments.title + metadata_feed['description'] = arguments.description + + #print("command line: " + ', '.join(argv)) + if arguments.file: + local_file = arguments.file + else: + print("no file given") + return 1 + + # read the file for any existing metadata + #print("Calling Read Metadata") + metadata_file = read_metadata(local_file) + if metadata_file: + for key in sorted(iter(metadata_file)): + print("KEY: " + key + "=" + metadata_file[key]) + + #print("Calling Write Metadata") + if not arguments.read_only: + print("Writing Metadata") + metadata_write = write_metadata(local_file, metadata_feed, metadata_file) + return 0 + +# read metadata from an audio or video file. Assumes that it can call ffmpg in the path. This dependency should be fixed. +# I've only tested with mp4 video files and mp3 audio files. +def read_metadata(local_file): + metadata = metadata_feed = dict() + print("\nReading file: " + local_file) + if not os.path.exists(local_file): + print("File not found for metadata update") + return 1 + + cmd_line = ['ffmpeg', '-loglevel', 'quiet', '-i', local_file, '-f', 'ffmetadata', '-'] + + try: + process = Popen(cmd_line, stdout=PIPE, stderr=PIPE) # I'm not sure if I want to do anything with stderr yet + stdout, stderr = process.communicate() + except OSError as e: + print >>sys.stderr, "FFMPEG Failed, aborting metadata updates:", e + return 0 + + for line in stdout.splitlines(): + line.rstrip() + tokens = line.partition('=') + if tokens[2]: + #print("DATA: " + tokens[0] + " = " + tokens[2]) + if tokens[0] == 'title': + metadata['TITLE_MATCH'] = tokens[2] + elif tokens[0] == 'description' or tokens[0] == 'TDES': + metadata['DESCRIPTION_MATCH'] = tokens[2] + #elif tokens[0] == 'album': + # metadata['ALBUM_MATCH'] = tokens[2] + #elif tokens[0] == 'minor_version': + # metadata['EPISODE_MATCH'] = tokens[2] + + metadata[tokens[0]] = tokens[2] + #else: + # print("Not valid metadata: ", line) + return(metadata) + + +# write metadata to an audio or video file. Assumes that it can call ffmpg in the path. This dependency should be fixed. +def write_metadata(local_file, metadata_feed, metadata_file): + update_needed = 0 + cmd_line = ['ffmpeg', '-y', '-loglevel', 'quiet', '-i', local_file] + tmp_file = "TMP_" + local_file # note, for ffmpeg this needs to be the same extention + + # Which metadata do we have? + if not 'TITLE_MATCH' in metadata_file: + print("Adding Title: " + metadata_feed['title']) + update_needed = 1 + cmd_line.extend(['-metadata', "title=" + metadata_feed['title']]) + else: + print("Title already exists") + + if not 'DESCRIPTION_MATCH' in metadata_file: + print("Adding Description: " + metadata_feed['description']) + update_needed = 1 + cmd_line.extend(['-metadata', "description=" + metadata_feed['description']]) + else: + print("Description already exists") + + if update_needed: + print("Updating Metadata on " + local_file) + + cmd_line_mapping = ['-map', '0', '-codec', 'copy'] + cmd_line_end = [tmp_file] + + print("Command line: " + ' '.join(cmd_line + cmd_line_mapping + cmd_line_end)) + try: + rtn = call(cmd_line + cmd_line_mapping + cmd_line_end) + if rtn == 0: + os.rename(tmp_file, local_file) + else: + # I have some podcasts that seem to have extra streams in them. I found this on Apple Byte podcast which has RTP hit streams. + #print >>sys.stderr, "Child returned", rtn + print("Trying to copy just one stream of audio and video") + cmd_line_mapping = ['-codec', 'copy'] + rtn = call(cmd_line + cmd_line_mapping + cmd_line_end) + if rtn != 0: + print("Copy Failed") + if os.path.exists(tmp_file): + os.remove(tmp_file) + return rtn + else: + os.rename(tmp_file, local_file) + except OSError as e: + print >>sys.stderr, "Execution failed:", e + return 0 + else: + print("File already has title and description, no need to update the file") + return 1 + + +if __name__ == "__main__": + main(sys.argv[1:]) + From 4a6ad7cb445b9b00ad9da5a78afd0d145e8ba8fd Mon Sep 17 00:00:00 2001 From: icepic0 Date: Sun, 2 Aug 2015 19:57:34 -0400 Subject: [PATCH 3/3] updated readme updated readme file and change history in PodGrab.py --- PodGrab.py | 2 ++ README | 2 ++ 2 files changed, 4 insertions(+) diff --git a/PodGrab.py b/PodGrab.py index 8f576af..4a331f2 100755 --- a/PodGrab.py +++ b/PodGrab.py @@ -14,6 +14,8 @@ # Version: 1.1.4 - 07/31/2015 # - added command line switches for db location, download location, plex configuration, M3U creation # - changed mkdir to mkdirs +# Version 1.1.5 - 8/2/2015 +# - added option to populate missing metadata in the mp3/mp4 file from the information in the feed. # David Smith # Do with this code what you will, it's "open source". As a courtesy, diff --git a/README b/README index 4bb88bf..6591c6b 100644 --- a/README +++ b/README @@ -43,3 +43,5 @@ Changes after fork: - Added option to output file names in an Season/Year Episode/Month+Day Title of Episode format - Added command line switches for db location, download location, plex configuration, M3U creation - Changed mkdir to mkdirs to deal with creating multiple levels of directories + - added option to populate missing metadata in the mp3/mp4 file from the information in the feed. +