#!/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 G(d, k): try: return d[k] except KeyError: return None def walkfiles(walker): for root, _, files in walker: for f in files: yield os.path.join(root, f) def filterext(paths, exts): for p in paths: ext = os.path.splitext(p)[1].lower() if ext in exts: yield p def shouldsync(md): rating = G(md, 'rating') sync = G(md, 'sync') if rating != None: try: rating = int(rating[0]) except ValueError: rating = None if sync: sync = sync[0].lower() else: sync = u'' if sync == u'no' or sync == u'space': return False if sync == u'yes' or sync == u'share': return True if rating != None and rating >= 3: return True return False def fixmetadata(md): if 'artist' not in md: md['artist'] = "Unknown Artist" if 'album' not in md: md['album'] = "Unknown Album" if 'title' not in md: fn = os.path.basename(md.path) fn = os.path.splitext(fn)[0] # TODO: attempt to infer trackNum/diskNum from fn md['title'] = fn def findmatching(haystack, needle): # TODO: don't match mismatched lengths (Xing?) artist = G(needle, 'artist') album = G(needle, 'album') title = G(needle, 'title') match = None for hay in haystack: if artist == G(hay, 'artist') \ and album == G(hay, 'album') \ and title == G(hay, 'title'): match = hay if match.seen: # TODO: check other tags and filename and such? print("Warning: duplicate match found:", file=sys.stderr) print("{0} by {1} from {2}".format(artist,album,title), file=sys.stderr) else: match.seen = True break return match def run(args): if not len(args) in (2, 3): print("I need a path or two!", file=sys.stderr) return 1 inonly = len(args) == 2 # BUG: doesn't work with my .m4a files? goodexts = ('.mp3', '.m4a', '.flac', '.ogg') tosync = [] indir = args[1] paths = lambda dir: filterext(walkfiles(os.walk(dir)), goodexts) for p in paths(indir): md = mutagen.File(p, easy=True) if shouldsync(md): if inonly: print(p) else: # TODO: don't use custom members on external metadata class md.path = p md.seen = False fixmetadata(md) tosync.append(md) if inonly: return 0 # TODO: don't print anything print("Beginning matching...", file=sys.stderr) outdir = args[2] for p in paths(outdir): md = mutagen.File(p, easy=True) match = findmatching(tosync, md) if match: print("UPD", p) # update tags here if updatemetadata returns true else: print("DEL", p) # delete files here for md in tosync: if md.seen: continue print("ADD", md.path) return 0 ret = 0 try: ret = run(sys.argv) except KeyboardInterrupt: sys.exit(1) sys.exit(ret)