#!/usr/bin/env python """Checks playlists. TODO: * grep -> ignore case and match the end of the string find -type f | grep -Ei "^.+\.(txt|mp3)\$" * strategie de sugg: basename ou pas ? May suggest similar files when some are not found (-s option). """ # pl -t /media/FRED check /media/FRED/Playlists/*.m3u8 # pl -t "/media/FRED/music/" -p "/music/" create /media/PLAYER/Playlists/_all_todo.m3u8 # pl -t "/media/FRED/music/Amon Tobin" -p "/music/Amon Tobin/" create /media/PLAYER/Playlists/_all_todo.m3u8 import sys import os import optparse import difflib VERSION = '0.1' MY_MUSIC_EXT = [ 'flac', 'm4a', 'm4p', 'mp3', 'ogg', 'wav', 'wma', ] # http://download.rockbox.org/manual/rockbox-ipodvideo/rockbox-buildap1.html MUSIC_EXT = [ 'mp1', 'mpa', 'mp2', 'mp3', 'ogg', 'oga', 'mpc', 'm4a', 'm4b', 'p4', 'wma', 'wmv', 'sf', 'a52', 'ac3', 'adx', 'spx', 'wav', 'aif', 'aiff', 'fla', 'm4a', 'mp4', 'wv', 'shn', 'ape', 'mac', 'sid', 'mod', 'nsf', 'nsfe', 'spc', 'sap' ] REPLACE_MARKER = '** please try to replace ** ' def main(): valid_actions = ['check', 'create'] parser = optparse.OptionParser() parser.usage = "%prog [OPTIONS...] ACTION PLAYLIST [OTHER_PLAYLIST]\n" + \ " [-defhtV] [--delete] [--error-stop] [--fix]\n" + \ " [-p PREFIX] [-s SCORE] [-t TARGET]\n" + \ " [--prefix PREFIX] [--score SCORE] [--target TARGET]\n" + \ " [--help] [--version]\n" + \ " AVAILABLE ACTIONS:\n" + \ "check:\n" + \ "create: create (or append to) the playlist the files found\n" + \ " under TARGET, paths are relative to TARGET, but you can\n" + \ " add a PREFIX\n" + \ " EXAMPLES:\n" + \ "plck -t /media/PLAYER check /media/PLAYER/Playlists/*.m3u8 [-s 0.7]\n" + \ "plck -t /dir/to/be/scanned/recursively create target_playlist.m3u8 -p what_will_replace_TARGET\n" + \ "plck -t /dir/to/be/scanned/recursively replace target_playlist.m3u8 -p what_will_replace_TARGET\n" + \ " NOTES:\n" + \ "SCORE must be between 0 and 1, 1 is the perfect match (useless):\n" + \ "" parser.usage = parser.usage[:-1] parser.add_option('-d', '--delete', default=False, action='store_true', dest='delete', help="check - delete entries that were not found") parser.add_option('-e', '--error-stop', default=False, action='store_true', dest='error_stop', help="check - don't process any other playlist if one fails") parser.add_option('-f', '--fix', default=False, action='store_true', dest='fix', help="check - prompt for fixes among suggestions") parser.add_option('-p', '--prefix', default='', action='store', dest='prefix', type='string', help="the string that will replace TARGET in the created playlist, " "typically TARGET without the path of your player") parser.add_option('-q', '--quiet', default=False, action='store_true', dest='quiet', help="check - don't report clean playlists") parser.add_option('-s', '--score', default=1, action='store', dest='score', type='float', help="minimum matching score for suggestions, " "the more the faster but there may be no file to suggest") parser.add_option('-t', '--target', action='store', dest='target', type='string', default='', help="the directory to search the files in") parser.add_option('-V', '--version', action='callback', callback=print_version, help="print program version") options, args = parser.parse_args(sys.argv[1:]) if not args: parser.error("You must provide an action.") action, playlists = args[0], args[1:] if not action in valid_actions: parser.error("You must provide a valid action (%s)." % \ ', '.join(valid_actions)) # playlist args check and options check if action == 'check': if not playlists: parser.error("You must provide at least one playlist.") if not 0 < options.score <= 1: parser.error("SCORE must be between 0 and 1.") # default auto-fix score if options.fix and options.score == 1: options.score = 0.8 if options.delete: user_in = raw_input("Type 'delete' to confirm.\n") if user_in.strip().lower() != 'delete': print "Aborted." exit() if options.delete and options.fix: parser.error("'delete' and 'auto-fix' are incompatible otions.") elif action == 'create': if len(playlists) != 1: parser.error("You must provide one and only one playlist.") if action == 'check': bad_playlists = [] for playlist in playlists: if not playlist_is_ok(playlist, options): bad_playlists.append(playlist) if options.error_stop: break if len(playlists) > 1: if bad_playlists: sep = '\n ' print "Problems occured in%s%s" % (sep, sep.join(bad_playlists)) else: print "Everything is OK." elif action == 'create': command = "find %(find_dir)s -type f | grep -e '%(regex)s' | " + \ "sed -e 's#^%(replace_dir)s/#%(prefix)s#g' | sort >> %(pl)s" command = command % \ dict(find_dir=options.target, replace_dir=options.target, pl=playlists[0], prefix=options.prefix, regex=r"\.\(%s\)$" % r'\|'.join(MUSIC_EXT)) # print command os.system(command) else: print "This unknown action should have been caught as an error." print "Please report this bug precising '%s not implemented'." % action def playlist_is_ok(playlist, options): items = [] try: pl_file = open(playlist, 'r') except IOError: print "Couldn't read this playlist: %s." % playlist return False items = pl_file.readlines() pl_file.close() if not items: if not options.quiet: print "%s is empty." % playlist # this is not a problem to be reported globally, we return True return True else: missing_items = set() if options.delete or options.fix: new_playlist = [] for item in items: item = item.strip() # ignore empty and commented lines if item and not item.startswith('#'): path_to_check = options.target + item if not os.path.isfile(path_to_check): missing_items.add(path_to_check) if options.fix: new_playlist.append(REPLACE_MARKER + item) else: if options.delete or options.fix: new_playlist.append(item) else: if options.delete or options.fix: # copy this line new_playlist.append(item) if not missing_items: if not options.quiet: print "%s is OK." % playlist return True else: missing_items = sorted(missing_items) if len(missing_items) == 1: tpl = "%d file was not found in %s:" else: tpl = "%d files were not found in %s:" print tpl % (len(missing_items), playlist) # maybe we have to find suggestions (display to user or auto-fix) # so we initialize the score dict and the seq matcher if options.score < 1: scores = {} for filename in music_files_list(options.target): scores[filename] = 0 seqmatcher = difflib.SequenceMatcher() # this is the report for missing_item in missing_items: print " " + format_item(missing_item, options=options), if options.score < 1: seqmatcher.set_seq1(os.path.basename(missing_item)) for filename in scores: seqmatcher.set_seq2(os.path.basename(filename)) scores[filename] = seqmatcher.ratio() # top matches at the top of the list thanks to -scores[x] suggs = [item for item in sorted(scores.keys(), key=lambda x: (-scores[x], x)) if scores[item] >= options.score] if not suggs: print "has no similar file, try to decrease " + \ "the minimum score." if options.fix: # clean the marker new_playlist = replace_in_pl( pl=new_playlist, entry=missing_item[len(options.target):], new=missing_item[len(options.target):]) else: print "could be" for sugg in suggs[:5]: print " " + format_item(sugg, options=options) if options.fix: replace = raw_input('Which one to use? [12345, nothing to ignore] ') if replace.strip(): try: number = int(replace) - 1 except: number = -1 else: if 0 <= number <= 4: new_playlist = replace_in_pl( pl=new_playlist, entry=missing_item[len(options.target):], new=suggs[number][len(options.target):]) else: # removes the marker new_playlist = replace_in_pl( pl=new_playlist, entry=missing_item[len(options.target):], new=missing_item[len(options.target):]) else: # new line if no suggestions needed print # maybe we have to write a new playlist, # to delete or fix entries that were not found if options.delete or options.fix: try: pl_file = open(playlist, 'w') except IOError: print "Couldn't write this playlist: %s." % playlist else: pl_file.write("\n".join(new_playlist)) pl_file.close() if options.delete: print "Those entries were deleted from the playlist." if options.fix: print "Those entries were fixed." def music_files_list(path): filenames = [] for root, dirs, files in os.walk(path): for filename in files: # [1:] because we must remove the dot from the extension if os.path.splitext(filename)[1][1:] in MUSIC_EXT: filenames.append(os.path.join(root, filename)) filenames.sort() return filenames def replace_in_pl(pl, entry, new): new_pl = [] for item in pl: if item == REPLACE_MARKER + entry: new_pl.append(new) else: new_pl.append(item) return new_pl def format_item(item, options): return item[len(options.target):] def print_version(_option, _opt_str, _value, _parser): print "playlist_checker %s" % VERSION sys.exit() if __name__ == '__main__': main()