#!/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 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 "  " + missing_item,
                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 "    " + sugg
                        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):
    print
    print pl
    print
    print entry
    print new
    new_pl = []
    for item in pl:
        if item == REPLACE_MARKER + entry:
            new_pl.append(new)
        else:
            new_pl.append(item)
    print new_pl
    return new_pl

def print_version(_option, _opt_str, _value, _parser):
    print "playlist_checker %s" % VERSION
    sys.exit()

if __name__ == '__main__':
    main()
ViewGit