diff --git a/.dummy b/.dummy deleted file mode 100644 index 945c9b4..0000000 --- a/.dummy +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/unsync.py b/unsync.py new file mode 100755 index 0000000..6c5bd86 --- /dev/null +++ b/unsync.py @@ -0,0 +1,132 @@ +#!/bin/python2 + +# only using python2 because mutagen +from __future__ import print_function + +import os +import os.path +import sys +import mutagen +import mutagen.id3 +import mutagen.easyid3 + +def popm_any(id3): + for k, v in id3.iteritems(): + if k.startswith('POPM'): + return v + +def byte2rating(b): + if b >= 224: return 5 + if b >= 160: return 4 + if b >= 96: return 3 + if b >= 32: return 2 + if b >= 1: return 1 + return 0 + +def rating_get(id3, key): + try: + return list(id3['TXXX:RATING']) + except KeyError: + try: + return list(unicode(byte2rating(popm_any(id3).rating))) + except AttributeError: + return +def rating_set(id3, key, val): + # TODO: test + try: + frame = id3["TXXX:RATING"] + except KeyError: + id3.add(mutagen.id3.TXXX(encoding=3, desc=u'RATING', text=val)) + else: + frame.encoding = 3 + frame.text = val +def rating_delete(id3, key): + # TODO: delete POPM too? + del(id3["TXXX:RATING"]) + +mutagen.easyid3.EasyID3.RegisterTXXXKey('sync', 'SYNC') +mutagen.easyid3.EasyID3.RegisterKey('rating', rating_get, rating_set, rating_delete) + +def safetydance(d, k): + if k in d: return d[k] + +def shouldsync(md): + rating = -1 + sync = u'' + if safetydance(md, 'rating'): + try: + rating = int(md['rating'][0]) + except ValueError: + pass + if safetydance(md, 'sync'): + sync = md['sync'][0].lower() + + if sync == u'no' or sync == u'space': + return False + if sync == u'yes' or sync == u'share': + return True + if rating >= 3: + return True + return False + +def findmatching(haystack, needle): + artist = safetydance(needle, 'artist') + album = safetydance(needle, 'album') + title = safetydance(needle, 'title') + for match in haystack: + if artist == safetydance(match, 'artist') \ + and album == safetydance(match, 'album') \ + and title == safetydance(match, 'title'): + return match + +def run(args): + if not len(args) in (2, 3): + print("I need a path or two!", file=sys.stderr) + sys.exit(1) + + inonly = len(args) == 2 + + # BUG: doesn't work with my .m4a files? + goodexts = ('.mp3', '.m4a', '.flac', '.ogg') + tosync = [] + indir = args[1] + walker = os.walk(indir) + + # TODO: abstract to mdloop(callback) + for root, _, files in walker: + for f in files: + ext = os.path.splitext(f)[1].lower() + if not ext in goodexts: continue + path = os.path.join(root, f) + md = mutagen.File(path, easy=True) + if shouldsync(md): + if inonly: print(path) + else: tosync.append(md) + + if inonly: return + print("Beginning matching...", file=sys.stderr) + + outdir = args[2] + walker = os.walk(outdir) + for root, _, files in walker: + for f in files: + ext = os.path.splitext(f)[1].lower() + if not ext in goodexts: continue + path = os.path.join(root, f) + md = mutagen.File(path, easy=True) + match = findmatching(tosync, md) + # TODO: don't print anything + # TODO: don't match mismatched lengths (Xing?) + # TODO: update important tags on mostly matching files + # TODO: don't sync files that wouldn't match! + # TODO: convert files in loop that works on altered tosync + if match: + print("MATCHING", path) + else: + # TODO: just delete missing ones here + print("MISSING", path) + +try: + run(sys.argv) +except KeyboardInterrupt: + sys.exit(1)