From c716e824dd955a69b47c39cae3efcb93fdd4a77d Mon Sep 17 00:00:00 2001 From: Connor Date: Sat, 26 Apr 2014 14:22:38 -0700 Subject: [PATCH 01/19] --- .dummy | 1 + 1 file changed, 1 insertion(+) create mode 100644 .dummy diff --git a/.dummy b/.dummy new file mode 100644 index 0000000..945c9b4 --- /dev/null +++ b/.dummy @@ -0,0 +1 @@ +. \ No newline at end of file From e5b036d9b874818d098dcbc07722321187c33668 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sat, 26 Apr 2014 14:34:13 -0700 Subject: [PATCH 02/19] some --- .dummy | 1 - unsync.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) delete mode 100644 .dummy create mode 100755 unsync.py 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) From 6a1562c5f55f98c368d310de9d0cef9b5e8dafbc Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 27 Apr 2014 06:46:24 -0700 Subject: [PATCH 03/19] BODY once told me --- unsync.py | 59 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/unsync.py b/unsync.py index 6c5bd86..d96508c 100755 --- a/unsync.py +++ b/unsync.py @@ -50,6 +50,17 @@ mutagen.easyid3.EasyID3.RegisterKey('rating', rating_get, rating_set, rating_del def safetydance(d, k): if k in d: return d[k] +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 = -1 sync = u'' @@ -90,41 +101,31 @@ def run(args): goodexts = ('.mp3', '.m4a', '.flac', '.ogg') tosync = [] indir = args[1] - walker = os.walk(indir) + paths = lambda dir: filterext(walkfiles(os.walk(dir)), goodexts) - # 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) + for p in paths(indir): + md = mutagen.File(p, easy=True) + if shouldsync(md): + if inonly: print(p) + 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) + for p in paths(outdir): + md = mutagen.File(p, 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", p) + else: + # TODO: just delete missing ones here + print("MISSING", p) try: run(sys.argv) From 1a2c9ae1e62fdcd10a4167de6b07225c6aff182a Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 27 Apr 2014 07:05:51 -0700 Subject: [PATCH 04/19] the world is gonna roll me --- unsync.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/unsync.py b/unsync.py index d96508c..a4efd7e 100755 --- a/unsync.py +++ b/unsync.py @@ -62,21 +62,20 @@ def filterext(paths, exts): yield p 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() + rating = safetydance(md, 'rating') + sync = safetydance(md, 'sync') + if rating != None: + rating = int(rating[0]) + 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 >= 3: + if rating != None and rating >= 3: return True return False @@ -93,7 +92,7 @@ def findmatching(haystack, needle): def run(args): if not len(args) in (2, 3): print("I need a path or two!", file=sys.stderr) - sys.exit(1) + return 1 inonly = len(args) == 2 @@ -109,7 +108,7 @@ def run(args): if inonly: print(p) else: tosync.append(md) - if inonly: return + if inonly: return 0 print("Beginning matching...", file=sys.stderr) outdir = args[2] @@ -127,7 +126,11 @@ def run(args): # TODO: just delete missing ones here print("MISSING", p) + return 0 + +ret = 0 try: - run(sys.argv) + ret = run(sys.argv) except KeyboardInterrupt: sys.exit(1) +sys.exit(ret) From d1ea33024334b482926c22faf02b34376bb5f36e Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 27 Apr 2014 07:17:32 -0700 Subject: [PATCH 05/19] but i ain't the sharpest tool in the shed --- unsync.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/unsync.py b/unsync.py index a4efd7e..eaa94dc 100755 --- a/unsync.py +++ b/unsync.py @@ -47,8 +47,11 @@ def rating_delete(id3, key): 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 G(d, k): + try: + return d[k] + except KeyError: + return None def walkfiles(walker): for root, _, files in walker: @@ -62,8 +65,8 @@ def filterext(paths, exts): yield p def shouldsync(md): - rating = safetydance(md, 'rating') - sync = safetydance(md, 'sync') + rating = G(md, 'rating') + sync = G(md, 'sync') if rating != None: rating = int(rating[0]) if sync: @@ -80,13 +83,13 @@ def shouldsync(md): return False def findmatching(haystack, needle): - artist = safetydance(needle, 'artist') - album = safetydance(needle, 'album') - title = safetydance(needle, 'title') + artist = G(needle, 'artist') + album = G(needle, 'album') + title = G(needle, 'title') for match in haystack: - if artist == safetydance(match, 'artist') \ - and album == safetydance(match, 'album') \ - and title == safetydance(match, 'title'): + if artist == G(match, 'artist') \ + and album == G(match, 'album') \ + and title == G(match, 'title'): return match def run(args): From 6ae05ac68d0c343bc23c36281cac29a64c49373b Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 27 Apr 2014 09:36:14 -0700 Subject: [PATCH 06/19] she was looking kinda dumb --- unsync.py | 67 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/unsync.py b/unsync.py index eaa94dc..72e986b 100755 --- a/unsync.py +++ b/unsync.py @@ -68,7 +68,10 @@ def shouldsync(md): rating = G(md, 'rating') sync = G(md, 'sync') if rating != None: - rating = int(rating[0]) + try: + rating = int(rating[0]) + except ValueError: + rating = None if sync: sync = sync[0].lower() else: @@ -82,15 +85,36 @@ def shouldsync(md): 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') - for match in haystack: - if artist == G(match, 'artist') \ - and album == G(match, 'album') \ - and title == G(match, 'title'): - return match + 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): @@ -108,26 +132,35 @@ def run(args): for p in paths(indir): md = mutagen.File(p, easy=True) if shouldsync(md): - if inonly: print(p) - else: tosync.append(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 + 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) - # 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", p) + print("UPD", p) + # update tags here if updatemetadata returns true else: - # TODO: just delete missing ones here - print("MISSING", p) + print("DEL", p) + # delete files here + + for md in tosync: + if md.seen: + continue + print("ADD", md.path) return 0 From 14014e2e93d2fcef9fd72aba33638229c77dcbb8 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 27 Apr 2014 12:03:52 -0700 Subject: [PATCH 07/19] with her finger and her thumb --- mutaext.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ unsync.py | 76 +++++++++++++++++++----------------------------- 2 files changed, 113 insertions(+), 47 deletions(-) create mode 100644 mutaext.py diff --git a/mutaext.py b/mutaext.py new file mode 100644 index 0000000..799580e --- /dev/null +++ b/mutaext.py @@ -0,0 +1,84 @@ +import mutagen +import mutagen.id3 +import mutagen.easyid3 + +# TODO: custom interface to tracknumber/tracktotal/disc... + +def popms(id3): + for k, v in id3.iteritems(): + if k.startswith('POPM'): + yield k, 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 rating2byte(r): + if r == 5: return 256 + if r == 4: return 192 + if r == 3: return 128 + if r == 2: return 64 + if r == 1: return 1 + return 0 + +def rating_get(id3, key): + try: + rating = id3['TXXX:RATING'] + except KeyError: + try: + _, popm = popms(id3).next() + except StopIteration: + return [] + else: + return [byte2rating(popm.rating)] + else: + return list(rating.text) + +def _canconv(r): + try: + if int(r) != str(int(r)): + return False + return int(r) >= 1 and int(r) <= 5 + except ValueError, TypeError: + return False + +def rating_set(id3, key, val): + rating_delete(id3, key) + if _canconv(val): + popm = mutagen.id3.POPM() + popm.email = "Windows Media Player 9 Series" + popm.count = 0 + popm.rating = rating2byte(int(val)) + id3.add(popm) + else: + if 'TXXX:RATING' in id3: + del(id3['TXXX:RATING']) + id3.add(mutagen.id3.TXXX(encoding=3, desc='RATING', text=unicode(val))) + +def rating_delete(id3, key): + for k, v in popms(id3): + del(id3[k]) + if 'TXXX:RATING' in id3: + del(id3['TXXX:RATING']) + +def tracknumber_get(id3, key): + # TODO: use for both track and disc + pass + +def tracknumber_set(id3, key, val): + pass + +def tracknumber_delete(id3, key): + pass + +replaygain_tags = ('replaygain_album_gain', 'replaygain_album_peak', \ + 'replaygain_track_gain', 'replaygain_track_peak') + +for tag in replaygain_tags: + mutagen.easyid3.EasyID3.RegisterTXXXKey(tag, tag) +mutagen.easyid3.EasyID3.RegisterTXXXKey('sync', 'SYNC') +mutagen.easyid3.EasyID3.RegisterKey('rating', rating_get, rating_set, rating_delete) diff --git a/unsync.py b/unsync.py index 72e986b..2cc1ffe 100755 --- a/unsync.py +++ b/unsync.py @@ -7,45 +7,7 @@ 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) +import mutaext def G(d, k): try: @@ -67,11 +29,8 @@ def filterext(paths, exts): def shouldsync(md): rating = G(md, 'rating') sync = G(md, 'sync') - if rating != None: - try: - rating = int(rating[0]) - except ValueError: - rating = None + if rating: + rating = rating[0] if sync: sync = sync[0].lower() else: @@ -93,7 +52,7 @@ def fixmetadata(md): 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 + # TODO: attempt to infer trackNum/discNum from fn md['title'] = fn def findmatching(haystack, needle): @@ -116,6 +75,28 @@ def findmatching(haystack, needle): break return match +def updatemetadata(mdold, mdnew): + modified = False + updatabletags = ('rating', 'sync', 'comment') + # TODO: + # composer : probably safe + # genre : might need deep equal? + # trackNum : TRCK with optional /[total] + # discNum : TPOS " " " + # total tracks : ? + # total discs : ? + # [replaygain] : TXXX:* in id3 + # albumArtist : TPE2 in id3 + for tag in updatabletags: + if tag in mdnew: + if not tag in mdold or mdnew[tag] != mdold[tag]: + mdold[tag] = mdnew[tag] + modified = True + elif tag in mdold: + del mdold[tag] + modified = True + return modified + def run(args): if not len(args) in (2, 3): print("I need a path or two!", file=sys.stderr) @@ -151,8 +132,9 @@ def run(args): md = mutagen.File(p, easy=True) match = findmatching(tosync, md) if match: - print("UPD", p) - # update tags here if updatemetadata returns true + if updatemetadata(md, match): + print("UPD", p) + # save here else: print("DEL", p) # delete files here From dbed8bb9266fbb4c996002e7b8c4519a1cdfa775 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 27 Apr 2014 13:47:08 -0700 Subject: [PATCH 08/19] in the shape of an L on her forehead --- mutaext.py | 28 ++++++++++------------------ unsync.py | 42 +++++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/mutaext.py b/mutaext.py index 799580e..1cb774b 100644 --- a/mutaext.py +++ b/mutaext.py @@ -1,8 +1,6 @@ import mutagen import mutagen.id3 -import mutagen.easyid3 - -# TODO: custom interface to tracknumber/tracktotal/disc... +from mutagen.easyid3 import EasyID3 def popms(id3): for k, v in id3.iteritems(): @@ -34,7 +32,7 @@ def rating_get(id3, key): except StopIteration: return [] else: - return [byte2rating(popm.rating)] + return [unicode(byte2rating(popm.rating))] else: return list(rating.text) @@ -65,20 +63,14 @@ def rating_delete(id3, key): if 'TXXX:RATING' in id3: del(id3['TXXX:RATING']) -def tracknumber_get(id3, key): - # TODO: use for both track and disc - pass - -def tracknumber_set(id3, key, val): - pass - -def tracknumber_delete(id3, key): - pass - replaygain_tags = ('replaygain_album_gain', 'replaygain_album_peak', \ 'replaygain_track_gain', 'replaygain_track_peak') - for tag in replaygain_tags: - mutagen.easyid3.EasyID3.RegisterTXXXKey(tag, tag) -mutagen.easyid3.EasyID3.RegisterTXXXKey('sync', 'SYNC') -mutagen.easyid3.EasyID3.RegisterKey('rating', rating_get, rating_set, rating_delete) + EasyID3.RegisterTXXXKey(tag, tag) + +extra_tags = ('sync', 'totaltracks', 'totaldiscs') +for tag in extra_tags: + EasyID3.RegisterTXXXKey(tag, tag.upper()) + +EasyID3.RegisterTextKey('albumartist', 'TPE2') +EasyID3.RegisterKey('rating', rating_get, rating_set, rating_delete) diff --git a/unsync.py b/unsync.py index 2cc1ffe..6acc894 100755 --- a/unsync.py +++ b/unsync.py @@ -9,6 +9,14 @@ import sys import mutagen import mutaext +updatabletags = [\ + 'albumartist', 'composer', 'comment' \ + 'tracknumber', 'discnumber', \ + 'genre', 'date', \ +] +updatabletags.extend(mutaext.replaygain_tags) +updatabletags.extend(mutaext.extra_tags) + def G(d, k): try: return d[k] @@ -29,8 +37,10 @@ def filterext(paths, exts): def shouldsync(md): rating = G(md, 'rating') sync = G(md, 'sync') - if rating: - rating = rating[0] + try: + rating = int(rating[0]) + except (IndexError, ValueError): + pass if sync: sync = sync[0].lower() else: @@ -40,7 +50,7 @@ def shouldsync(md): return False if sync == u'yes' or sync == u'share': return True - if rating != None and rating >= 3: + if type(rating) == int and rating >= 3: return True return False @@ -77,19 +87,14 @@ def findmatching(haystack, needle): def updatemetadata(mdold, mdnew): modified = False - updatabletags = ('rating', 'sync', 'comment') - # TODO: - # composer : probably safe - # genre : might need deep equal? - # trackNum : TRCK with optional /[total] - # discNum : TPOS " " " - # total tracks : ? - # total discs : ? - # [replaygain] : TXXX:* in id3 - # albumArtist : TPE2 in id3 + # TODO: ensure genre and date work properly (special cases in EasyID3) for tag in updatabletags: if tag in mdnew: - if not tag in mdold or mdnew[tag] != mdold[tag]: + if not tag in mdold: + #print("not in old:",tag) + mdold[tag] = mdnew[tag] + elif mdnew[tag][0] != mdold[tag][0]: + #print("in old:",tag) mdold[tag] = mdnew[tag] modified = True elif tag in mdold: @@ -134,15 +139,14 @@ def run(args): if match: if updatemetadata(md, match): print("UPD", p) - # save here + md.save() else: - print("DEL", p) + pass #print("DEL", p) # delete files here for md in tosync: - if md.seen: - continue - print("ADD", md.path) + if md.seen == False: + print("ADD", md.path) return 0 From a236ef5a51f52e89ed9a69bd1d0f2d8d231ded45 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 27 Apr 2014 15:34:09 -0700 Subject: [PATCH 09/19] it's workinggggg --- convert.py | 12 ++++++++++++ unsync.py | 34 +++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 convert.py diff --git a/convert.py b/convert.py new file mode 100644 index 0000000..96ff42c --- /dev/null +++ b/convert.py @@ -0,0 +1,12 @@ +import subprocess as sp +import tempfile + +def ogg(inf): + _, temppath = tempfile.mkstemp() + p1 = sp.Popen(["ffmpeg", "-loglevel", "warning", "-i", inf, "-f", "flac", "-"], stdout=sp.PIPE) + p2 = sp.Popen(["oggenc", "-Q", "-", "-o", temppath], stdin=p1.stdout, stdout=sp.PIPE) + p1.stdout.close() + _, err = p2.communicate() + if err: + raise Exception(err) + return temppath diff --git a/unsync.py b/unsync.py index 6acc894..9625c36 100755 --- a/unsync.py +++ b/unsync.py @@ -6,8 +6,11 @@ from __future__ import print_function import os import os.path import sys +import shutil import mutagen + import mutaext +import convert updatabletags = [\ 'albumartist', 'composer', 'comment' \ @@ -17,6 +20,9 @@ updatabletags = [\ updatabletags.extend(mutaext.replaygain_tags) updatabletags.extend(mutaext.extra_tags) +alltags = list(updatabletags) +alltags.extend(['artist', 'album', 'title']) + def G(d, k): try: return d[k] @@ -39,7 +45,7 @@ def shouldsync(md): sync = G(md, 'sync') try: rating = int(rating[0]) - except (IndexError, ValueError): + except: pass if sync: sync = sync[0].lower() @@ -102,6 +108,15 @@ def updatemetadata(mdold, mdnew): modified = True return modified +def makefilename(md): + fn = "" + title = md['title'][0] + artist = md['artist'][0] + album = md['album'][0] + fn = u"{1} - {2} - {0}".format(title,artist,album) + fn += ".ogg" + return fn + def run(args): if not len(args) in (2, 3): print("I need a path or two!", file=sys.stderr) @@ -129,8 +144,8 @@ def run(args): if inonly: return 0 - # TODO: don't print anything - print("Beginning matching...", file=sys.stderr) + + print("Matching...", file=sys.stderr) outdir = args[2] for p in paths(outdir): @@ -141,12 +156,21 @@ def run(args): print("UPD", p) md.save() else: - pass #print("DEL", p) - # delete files here + print("DEL", p) + os.remove(p) for md in tosync: if md.seen == False: print("ADD", md.path) + outf = os.path.join(outdir, makefilename(md)) + #print("TO", outf) + temppath = convert.ogg(md.path) + mdnew = mutagen.File(temppath, easy=True) + for tag in alltags: + if tag in md: + mdnew[tag] = md[tag] + mdnew.save() + shutil.move(temppath, outf) return 0 From b4eeededf89853a9eb725d1f294188081f2b8ba7 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Mon, 28 Apr 2014 19:31:24 -0700 Subject: [PATCH 10/19] it's workingggg better --- convert.py | 16 +++++++-------- mutaext.py | 2 +- unsync.py | 60 +++++++++++++++++++++++------------------------------- 3 files changed, 34 insertions(+), 44 deletions(-) diff --git a/convert.py b/convert.py index 96ff42c..aecb515 100644 --- a/convert.py +++ b/convert.py @@ -1,12 +1,10 @@ import subprocess as sp -import tempfile +import os -def ogg(inf): - _, temppath = tempfile.mkstemp() - p1 = sp.Popen(["ffmpeg", "-loglevel", "warning", "-i", inf, "-f", "flac", "-"], stdout=sp.PIPE) - p2 = sp.Popen(["oggenc", "-Q", "-", "-o", temppath], stdin=p1.stdout, stdout=sp.PIPE) +def ogg(fin, fout): + p1 = sp.Popen(["ffmpeg", "-loglevel", "error", "-i", fin, "-f", "flac", "-"], stdout=sp.PIPE) + p2 = sp.Popen(["oggenc", "-Q", "-q", "5", "-", "-o", fout], stdin=p1.stdout, stdout=sp.PIPE) p1.stdout.close() - _, err = p2.communicate() - if err: - raise Exception(err) - return temppath + p2.communicate() + ret = p1.poll() or p2.poll() + return ret diff --git a/mutaext.py b/mutaext.py index 1cb774b..bea3556 100644 --- a/mutaext.py +++ b/mutaext.py @@ -41,7 +41,7 @@ def _canconv(r): if int(r) != str(int(r)): return False return int(r) >= 1 and int(r) <= 5 - except ValueError, TypeError: + except (ValueError, TypeError): return False def rating_set(id3, key, val): diff --git a/unsync.py b/unsync.py index 9625c36..56ca89b 100755 --- a/unsync.py +++ b/unsync.py @@ -7,6 +7,7 @@ import os import os.path import sys import shutil +import tempfile import mutagen import mutaext @@ -23,12 +24,6 @@ updatabletags.extend(mutaext.extra_tags) alltags = list(updatabletags) alltags.extend(['artist', 'album', 'title']) -def G(d, k): - try: - return d[k] - except KeyError: - return None - def walkfiles(walker): for root, _, files in walker: for f in files: @@ -41,16 +36,14 @@ def filterext(paths, exts): yield p def shouldsync(md): - rating = G(md, 'rating') - sync = G(md, 'sync') + rating = md.get('rating') + sync = md.get('sync', u'') try: rating = int(rating[0]) except: pass if sync: sync = sync[0].lower() - else: - sync = u'' if sync == u'no' or sync == u'space': return False @@ -61,10 +54,8 @@ def shouldsync(md): return False def fixmetadata(md): - if 'artist' not in md: - md['artist'] = "Unknown Artist" - if 'album' not in md: - md['album'] = "Unknown Album" + md['artist'] = md.get('artist', "Unknown Artist") + md['album'] = md.get('album', "Unknown Album") if 'title' not in md: fn = os.path.basename(md.path) fn = os.path.splitext(fn)[0] @@ -73,14 +64,14 @@ def fixmetadata(md): def findmatching(haystack, needle): # TODO: don't match mismatched lengths (Xing?) - artist = G(needle, 'artist') - album = G(needle, 'album') - title = G(needle, 'title') + artist = needle.get('artist') + album = needle.get('album') + title = needle.get('title') match = None for hay in haystack: - if artist == G(hay, 'artist') \ - and album == G(hay, 'album') \ - and title == G(hay, 'title'): + if artist == hay.get('artist') \ + and album == hay.get('album') \ + and title == hay.get('title'): match = hay if match.seen: # TODO: check other tags and filename and such? @@ -93,18 +84,14 @@ def findmatching(haystack, needle): def updatemetadata(mdold, mdnew): modified = False - # TODO: ensure genre and date work properly (special cases in EasyID3) for tag in updatabletags: - if tag in mdnew: - if not tag in mdold: - #print("not in old:",tag) - mdold[tag] = mdnew[tag] - elif mdnew[tag][0] != mdold[tag][0]: - #print("in old:",tag) + if tag in mdnew and len(mdnew[tag]): + if not tag in mdold or mdnew[tag][0] != mdold[tag][0]: mdold[tag] = mdnew[tag] modified = True elif tag in mdold: del mdold[tag] + print('del', tag) modified = True return modified @@ -160,17 +147,22 @@ def run(args): os.remove(p) for md in tosync: - if md.seen == False: - print("ADD", md.path) - outf = os.path.join(outdir, makefilename(md)) - #print("TO", outf) - temppath = convert.ogg(md.path) - mdnew = mutagen.File(temppath, easy=True) + if md.seen: + continue + print("ADD", md.path) + + fout = os.path.join(outdir, makefilename(md)) + _, ftemp = tempfile.mkstemp() + try: + convert.ogg(md.path, ftemp) + mdnew = mutagen.File(ftemp, easy=True) for tag in alltags: if tag in md: mdnew[tag] = md[tag] mdnew.save() - shutil.move(temppath, outf) + shutil.copy2(ftemp, fout) + finally: + os.remove(ftemp) return 0 From f7ac8f0704f1267bffcf47eef254255582ef8025 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Tue, 29 Apr 2014 03:32:23 -0700 Subject: [PATCH 11/19] is this wrong, feels so wrong --- unsync.py | 87 ++++++++++++++++++++----------------------------------- 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/unsync.py b/unsync.py index 56ca89b..780062a 100755 --- a/unsync.py +++ b/unsync.py @@ -13,6 +13,10 @@ import mutagen import mutaext import convert +# BUG: doesn't work with my .m4a files? +goodexts = ('.mp3', '.m4a', '.flac', '.ogg') + +matchtags = ['artist', 'album', 'title'] updatabletags = [\ 'albumartist', 'composer', 'comment' \ 'tracknumber', 'discnumber', \ @@ -20,99 +24,71 @@ updatabletags = [\ ] updatabletags.extend(mutaext.replaygain_tags) updatabletags.extend(mutaext.extra_tags) - alltags = list(updatabletags) -alltags.extend(['artist', 'album', 'title']) +alltags.extend(matchtags) -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 +lament = lambda *args, **kwargs: print(*args, file=sys.stderr, **kwargs) +walkfiles = lambda w: (os.path.join(r, f) for r, _, fs in w for f in fs) +extof = lambda p: os.path.splitext(p)[1].lower() +filterext = lambda ps, es: (p for p in ps if extof(p) in es) def shouldsync(md): rating = md.get('rating') sync = md.get('sync', u'') try: rating = int(rating[0]) - except: + except (IndexError, ValueError): pass if sync: sync = sync[0].lower() - if sync == u'no' or sync == u'space': - return False - if sync == u'yes' or sync == u'share': - return True - if type(rating) == int and rating >= 3: - return True - return False + return sync == 'yes' or type(rating) == int and rating >= 3 and not sync is 'no' and not sync is 'space' def fixmetadata(md): - md['artist'] = md.get('artist', "Unknown Artist") - md['album'] = md.get('album', "Unknown Album") + md['artist'] = md.get('artist', u"Unknown Artist") + md['album'] = md.get('album', u"Unknown Album") if 'title' not in md: fn = os.path.basename(md.path) fn = os.path.splitext(fn)[0] # TODO: attempt to infer trackNum/discNum from fn - md['title'] = fn + md['title'] = unicode(fn) def findmatching(haystack, needle): - # TODO: don't match mismatched lengths (Xing?) - artist = needle.get('artist') - album = needle.get('album') - title = needle.get('title') - match = None - for hay in haystack: - if artist == hay.get('artist') \ - and album == hay.get('album') \ - and title == hay.get('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 + matchme = [needle[t] for t in matchtags] + ismatch = lambda hay: [hay[t] for t in matchtags] == matchme + for match in (hay for hay in haystack if ismatch(hay)): + if match.seen: + # TODO: check other tags and filename and such? + lament("Warning: duplicate match found:") + lament(u"%(title)s by %(artist)s from %(album)s" % locals()) + match.seen = True + return match def updatemetadata(mdold, mdnew): modified = False for tag in updatabletags: + # checking for length b/c sometimes (ID3 genre) exists but empty if tag in mdnew and len(mdnew[tag]): if not tag in mdold or mdnew[tag][0] != mdold[tag][0]: mdold[tag] = mdnew[tag] modified = True elif tag in mdold: del mdold[tag] - print('del', tag) modified = True return modified def makefilename(md): - fn = "" title = md['title'][0] artist = md['artist'][0] album = md['album'][0] - fn = u"{1} - {2} - {0}".format(title,artist,album) - fn += ".ogg" - return fn + return u"%(artist)s - %(album)s - %(title)s.ogg" % locals() def run(args): if not len(args) in (2, 3): - print("I need a path or two!", file=sys.stderr) + lament("I need a path or two!") 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) @@ -132,19 +108,18 @@ def run(args): if inonly: return 0 - print("Matching...", file=sys.stderr) + lament("Matching...") outdir = args[2] for p in paths(outdir): md = mutagen.File(p, easy=True) match = findmatching(tosync, md) - if match: - if updatemetadata(md, match): - print("UPD", p) - md.save() - else: + if not match: print("DEL", p) os.remove(p) + elif updatemetadata(md, match): + print("UPD", p) + md.save() for md in tosync: if md.seen: From 4bbf061085ba850730a5eb0e676241efc114aafc Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Tue, 29 Apr 2014 08:52:55 -0700 Subject: [PATCH 12/19] unbreak it --- unsync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsync.py b/unsync.py index 780062a..808d2bc 100755 --- a/unsync.py +++ b/unsync.py @@ -42,7 +42,7 @@ def shouldsync(md): if sync: sync = sync[0].lower() - return sync == 'yes' or type(rating) == int and rating >= 3 and not sync is 'no' and not sync is 'space' + return sync == 'yes' or type(rating) == int and rating >= 3 and sync != 'no' and sync != 'space' def fixmetadata(md): md['artist'] = md.get('artist', u"Unknown Artist") From 9d475fdba791080043d8b5a0aa33d563dceb8e39 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Wed, 30 Apr 2014 17:04:02 -0700 Subject: [PATCH 13/19] OOP bloat --- mutaext.py | 35 +++++++++++++++++++++++++++++++++++ unsync.py | 40 ++++++++++++++++++---------------------- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/mutaext.py b/mutaext.py index bea3556..149828d 100644 --- a/mutaext.py +++ b/mutaext.py @@ -1,3 +1,4 @@ +from collections import MutableMapping import mutagen import mutagen.id3 from mutagen.easyid3 import EasyID3 @@ -74,3 +75,37 @@ for tag in extra_tags: EasyID3.RegisterTextKey('albumartist', 'TPE2') EasyID3.RegisterKey('rating', rating_get, rating_set, rating_delete) + +class SyncFile(MutableMapping): + + def __init__(self, path): + self.md = mutagen.File(path, easy=True) + self.path = path + self.seen = False + + def __getitem__(self, key): + d = self.md[key] + try: + return d[0] + except IndexError: + raise KeyError(key) + + def __setitem__(self, key, value): + if type(value) != unicode: + raise ValueError + self.md[key] = [value] + + def __delitem__(self, key): + del(self.md[key]) + + def __iter__(self): + for k in self.md: + try: + self.__getitem__(k) + except KeyError: + pass + else: + yield k + + def __len__(self): + return len([k for k in self.__iter__()]) diff --git a/unsync.py b/unsync.py index 808d2bc..a5bfafb 100755 --- a/unsync.py +++ b/unsync.py @@ -8,10 +8,10 @@ import os.path import sys import shutil import tempfile -import mutagen import mutaext import convert +from mutaext import SyncFile # BUG: doesn't work with my .m4a files? goodexts = ('.mp3', '.m4a', '.flac', '.ogg') @@ -33,14 +33,14 @@ extof = lambda p: os.path.splitext(p)[1].lower() filterext = lambda ps, es: (p for p in ps if extof(p) in es) def shouldsync(md): - rating = md.get('rating') + rating = md.get('rating', u'') sync = md.get('sync', u'') try: - rating = int(rating[0]) - except (IndexError, ValueError): + rating = int(rating) + except ValueError: pass if sync: - sync = sync[0].lower() + sync = sync.lower() return sync == 'yes' or type(rating) == int and rating >= 3 and sync != 'no' and sync != 'space' @@ -50,7 +50,6 @@ def fixmetadata(md): if 'title' not in md: fn = os.path.basename(md.path) fn = os.path.splitext(fn)[0] - # TODO: attempt to infer trackNum/discNum from fn md['title'] = unicode(fn) def findmatching(haystack, needle): @@ -58,7 +57,7 @@ def findmatching(haystack, needle): ismatch = lambda hay: [hay[t] for t in matchtags] == matchme for match in (hay for hay in haystack if ismatch(hay)): if match.seen: - # TODO: check other tags and filename and such? + # TODO: check other tags too? lament("Warning: duplicate match found:") lament(u"%(title)s by %(artist)s from %(album)s" % locals()) match.seen = True @@ -67,9 +66,8 @@ def findmatching(haystack, needle): def updatemetadata(mdold, mdnew): modified = False for tag in updatabletags: - # checking for length b/c sometimes (ID3 genre) exists but empty - if tag in mdnew and len(mdnew[tag]): - if not tag in mdold or mdnew[tag][0] != mdold[tag][0]: + if tag in mdnew: + if mdnew[tag] != mdold[tag]: mdold[tag] = mdnew[tag] modified = True elif tag in mdold: @@ -78,9 +76,10 @@ def updatemetadata(mdold, mdnew): return modified def makefilename(md): - title = md['title'][0] - artist = md['artist'][0] - album = md['album'][0] + title = md['title'] + artist = md['artist'] + album = md['album'] + return u"%(artist)s - %(album)s - %(title)s.ogg" % locals() def run(args): @@ -94,14 +93,11 @@ def run(args): paths = lambda dir: filterext(walkfiles(os.walk(dir)), goodexts) for p in paths(indir): - md = mutagen.File(p, easy=True) + md = SyncFile(p) 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) @@ -112,14 +108,14 @@ def run(args): outdir = args[2] for p in paths(outdir): - md = mutagen.File(p, easy=True) + md = SyncFile(p) match = findmatching(tosync, md) - if not match: + if match == None: print("DEL", p) os.remove(p) elif updatemetadata(md, match): print("UPD", p) - md.save() + md.md.save() for md in tosync: if md.seen: @@ -130,11 +126,11 @@ def run(args): _, ftemp = tempfile.mkstemp() try: convert.ogg(md.path, ftemp) - mdnew = mutagen.File(ftemp, easy=True) + mdnew = SyncFile(ftemp) for tag in alltags: if tag in md: mdnew[tag] = md[tag] - mdnew.save() + mdnew.md.save() shutil.copy2(ftemp, fout) finally: os.remove(ftemp) From fef8ae685dfc83f90253e72ba64ec254a53bd523 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sat, 3 May 2014 19:33:38 -0700 Subject: [PATCH 14/19] beginning filesync syncing --- mutaext.py | 2 ++ unsync.py | 52 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/mutaext.py b/mutaext.py index 149828d..8cb42e0 100644 --- a/mutaext.py +++ b/mutaext.py @@ -77,6 +77,8 @@ EasyID3.RegisterTextKey('albumartist', 'TPE2') EasyID3.RegisterKey('rating', rating_get, rating_set, rating_delete) class SyncFile(MutableMapping): + """Dumb OOP crap that just adds more code + and a sense of self-importance""" def __init__(self, path): self.md = mutagen.File(path, easy=True) diff --git a/unsync.py b/unsync.py index a5bfafb..bfcb068 100755 --- a/unsync.py +++ b/unsync.py @@ -17,20 +17,20 @@ from mutaext import SyncFile goodexts = ('.mp3', '.m4a', '.flac', '.ogg') matchtags = ['artist', 'album', 'title'] -updatabletags = [\ +alltags = [\ 'albumartist', 'composer', 'comment' \ 'tracknumber', 'discnumber', \ 'genre', 'date', \ ] -updatabletags.extend(mutaext.replaygain_tags) -updatabletags.extend(mutaext.extra_tags) -alltags = list(updatabletags) +alltags.extend(mutaext.replaygain_tags) +alltags.extend(mutaext.extra_tags) alltags.extend(matchtags) lament = lambda *args, **kwargs: print(*args, file=sys.stderr, **kwargs) walkfiles = lambda w: (os.path.join(r, f) for r, _, fs in w for f in fs) extof = lambda p: os.path.splitext(p)[1].lower() filterext = lambda ps, es: (p for p in ps if extof(p) in es) +ansty = lambda u: str(u.decode('ascii', errors='replace').replace(u'\ufffd', '?')) def shouldsync(md): rating = md.get('rating', u'') @@ -53,21 +53,21 @@ def fixmetadata(md): md['title'] = unicode(fn) def findmatching(haystack, needle): - matchme = [needle[t] for t in matchtags] - ismatch = lambda hay: [hay[t] for t in matchtags] == matchme + matchme = [needle[t].lower() for t in matchtags] + ismatch = lambda hay: [hay[t].lower() for t in matchtags] == matchme for match in (hay for hay in haystack if ismatch(hay)): if match.seen: # TODO: check other tags too? - lament("Warning: duplicate match found:") - lament(u"%(title)s by %(artist)s from %(album)s" % locals()) - match.seen = True + lament("Duplicate") + return None + match.seen = needle.path return match def updatemetadata(mdold, mdnew): modified = False - for tag in updatabletags: + for tag in alltags: if tag in mdnew: - if mdnew[tag] != mdold[tag]: + if tag not in mdold or mdnew[tag] != mdold[tag]: mdold[tag] = mdnew[tag] modified = True elif tag in mdold: @@ -79,8 +79,12 @@ def makefilename(md): title = md['title'] artist = md['artist'] album = md['album'] + sync = md.get('sync') - return u"%(artist)s - %(album)s - %(title)s.ogg" % locals() + # TODO: strip /'s and other possible nasties + if sync: + return "!%(sync)s %(artist)s - %(album)s - %(title)s.ogg" % locals() + return "%(artist)s - %(album)s - %(title)s.ogg" % locals() def run(args): if not len(args) in (2, 3): @@ -90,7 +94,8 @@ def run(args): tosync = [] indir = args[1] - paths = lambda dir: filterext(walkfiles(os.walk(dir)), goodexts) + _paths = lambda dir: filterext(walkfiles(os.walk(dir)), goodexts) + paths = lambda dir: _paths(unicode(dir).encode('utf-8')) for p in paths(indir): md = SyncFile(p) @@ -118,11 +123,28 @@ def run(args): md.md.save() for md in tosync: + fn = makefilename(md) + fout = os.path.join(outdir, fn) + if md.seen: - continue + try: + _from = md.seen + _to = fout.encode('utf-8') + if type(_from) != type(_to): + raise TypeError + + if _from != _to: + print("MOV", _from) + #os.rename(_from, _to) + continue + except: + lament(type(_from), type(_to)) + lament("_from:", [_from]) + lament("_to:", [_to]) + raise + print("ADD", md.path) - fout = os.path.join(outdir, makefilename(md)) _, ftemp = tempfile.mkstemp() try: convert.ogg(md.path, ftemp) From c61c00dfd6bd2322068b0da7b119bff574502737 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 25 May 2014 16:34:58 -0700 Subject: [PATCH 15/19] i gave up --- unsync.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/unsync.py b/unsync.py index bfcb068..ee9ae7d 100755 --- a/unsync.py +++ b/unsync.py @@ -8,6 +8,7 @@ import os.path import sys import shutil import tempfile +from zlib import crc32 import mutaext import convert @@ -79,12 +80,13 @@ def makefilename(md): title = md['title'] artist = md['artist'] album = md['album'] - sync = md.get('sync') - # TODO: strip /'s and other possible nasties - if sync: - return "!%(sync)s %(artist)s - %(album)s - %(title)s.ogg" % locals() - return "%(artist)s - %(album)s - %(title)s.ogg" % locals() + fn = "%(artist)s - %(album)s - %(title)s" % locals() + crc = crc32(fn.encode('utf-8')) & 0xFFFFFFFF + # FAT is a pain to deal with so just use nondescript filenames + fn = '{:08X}.ogg'.format(crc) + + return fn def run(args): if not len(args) in (2, 3): @@ -129,13 +131,13 @@ def run(args): if md.seen: try: _from = md.seen - _to = fout.encode('utf-8') + _to = fout if type(_from) != type(_to): raise TypeError if _from != _to: print("MOV", _from) - #os.rename(_from, _to) + os.rename(_from, _to) continue except: lament(type(_from), type(_to)) From d4fbd7a829f40249b033a6e7933be5bf45efec81 Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 2 Nov 2014 10:12:52 -0800 Subject: [PATCH 16/19] cleanup --- mutaext.py | 2 ++ unsync.py | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/mutaext.py b/mutaext.py index 8cb42e0..ea3c0fe 100644 --- a/mutaext.py +++ b/mutaext.py @@ -86,6 +86,8 @@ class SyncFile(MutableMapping): self.seen = False def __getitem__(self, key): + if self.md == None: + print(self.path) d = self.md[key] try: return d[0] diff --git a/unsync.py b/unsync.py index ee9ae7d..7770115 100755 --- a/unsync.py +++ b/unsync.py @@ -15,7 +15,8 @@ import convert from mutaext import SyncFile # BUG: doesn't work with my .m4a files? -goodexts = ('.mp3', '.m4a', '.flac', '.ogg') +#goodexts = ('.mp3', '.mp2', '.m4a', '.flac', '.ogg') +goodexts = ('.mp3', '.flac', '.ogg') matchtags = ['artist', 'album', 'title'] alltags = [\ @@ -123,8 +124,12 @@ def run(args): elif updatemetadata(md, match): print("UPD", p) md.md.save() + else: + print("___", p) + lament("[beginning tosync]") for md in tosync: + #print("SYN", md.path) fn = makefilename(md) fout = os.path.join(outdir, fn) @@ -161,9 +166,10 @@ def run(args): return 0 -ret = 0 -try: - ret = run(sys.argv) -except KeyboardInterrupt: - sys.exit(1) -sys.exit(ret) +if __name__ == '__main__': + ret = 0 + try: + ret = run(sys.argv) + except KeyboardInterrupt: + sys.exit(1) + sys.exit(ret) From 2bbe301cdc7bcc869fde08cbda10667bac84c37e Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 2 Nov 2014 12:30:29 -0800 Subject: [PATCH 17/19] switch to mutagenx and upgrade to python 3 mutagenx is about 13 times faster than mutagen! --- mutaext.py | 20 ++++++++++---------- unsync.py | 22 ++++++++-------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/mutaext.py b/mutaext.py index ea3c0fe..504cabb 100644 --- a/mutaext.py +++ b/mutaext.py @@ -1,10 +1,10 @@ from collections import MutableMapping -import mutagen -import mutagen.id3 -from mutagen.easyid3 import EasyID3 +import mutagenx +import mutagenx.id3 +from mutagenx.easyid3 import EasyID3 def popms(id3): - for k, v in id3.iteritems(): + for k, v in id3.items(): if k.startswith('POPM'): yield k, v @@ -29,11 +29,11 @@ def rating_get(id3, key): rating = id3['TXXX:RATING'] except KeyError: try: - _, popm = popms(id3).next() + _, popm = next(popms(id3)) except StopIteration: return [] else: - return [unicode(byte2rating(popm.rating))] + return [str(byte2rating(popm.rating))] else: return list(rating.text) @@ -48,7 +48,7 @@ def _canconv(r): def rating_set(id3, key, val): rating_delete(id3, key) if _canconv(val): - popm = mutagen.id3.POPM() + popm = mutagenx.id3.POPM() popm.email = "Windows Media Player 9 Series" popm.count = 0 popm.rating = rating2byte(int(val)) @@ -56,7 +56,7 @@ def rating_set(id3, key, val): else: if 'TXXX:RATING' in id3: del(id3['TXXX:RATING']) - id3.add(mutagen.id3.TXXX(encoding=3, desc='RATING', text=unicode(val))) + id3.add(mutagenx.id3.TXXX(encoding=3, desc='RATING', text=str(val))) def rating_delete(id3, key): for k, v in popms(id3): @@ -81,7 +81,7 @@ class SyncFile(MutableMapping): and a sense of self-importance""" def __init__(self, path): - self.md = mutagen.File(path, easy=True) + self.md = mutagenx.File(path, easy=True) self.path = path self.seen = False @@ -95,7 +95,7 @@ class SyncFile(MutableMapping): raise KeyError(key) def __setitem__(self, key, value): - if type(value) != unicode: + if type(value) != str: raise ValueError self.md[key] = [value] diff --git a/unsync.py b/unsync.py index 7770115..1adfa36 100755 --- a/unsync.py +++ b/unsync.py @@ -1,7 +1,4 @@ -#!/bin/python2 - -# only using python2 because mutagen -from __future__ import print_function +#!/bin/python import os import os.path @@ -32,11 +29,11 @@ lament = lambda *args, **kwargs: print(*args, file=sys.stderr, **kwargs) walkfiles = lambda w: (os.path.join(r, f) for r, _, fs in w for f in fs) extof = lambda p: os.path.splitext(p)[1].lower() filterext = lambda ps, es: (p for p in ps if extof(p) in es) -ansty = lambda u: str(u.decode('ascii', errors='replace').replace(u'\ufffd', '?')) +ansty = lambda u: str(u.decode('ascii', errors='replace').replace('\ufffd', '?')) def shouldsync(md): - rating = md.get('rating', u'') - sync = md.get('sync', u'') + rating = md.get('rating', '') + sync = md.get('sync', '') try: rating = int(rating) except ValueError: @@ -47,12 +44,12 @@ def shouldsync(md): return sync == 'yes' or type(rating) == int and rating >= 3 and sync != 'no' and sync != 'space' def fixmetadata(md): - md['artist'] = md.get('artist', u"Unknown Artist") - md['album'] = md.get('album', u"Unknown Album") + md['artist'] = md.get('artist', "Unknown Artist") + md['album'] = md.get('album', "Unknown Album") if 'title' not in md: fn = os.path.basename(md.path) fn = os.path.splitext(fn)[0] - md['title'] = unicode(fn) + md['title'] = str(fn) def findmatching(haystack, needle): matchme = [needle[t].lower() for t in matchtags] @@ -97,8 +94,7 @@ def run(args): tosync = [] indir = args[1] - _paths = lambda dir: filterext(walkfiles(os.walk(dir)), goodexts) - paths = lambda dir: _paths(unicode(dir).encode('utf-8')) + paths = lambda dir: filterext(walkfiles(os.walk(dir)), goodexts) for p in paths(indir): md = SyncFile(p) @@ -124,8 +120,6 @@ def run(args): elif updatemetadata(md, match): print("UPD", p) md.md.save() - else: - print("___", p) lament("[beginning tosync]") for md in tosync: From 62f12d031a19c0773f789e2adb17d54abfcf055a Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Sun, 2 Nov 2014 13:29:44 -0800 Subject: [PATCH 18/19] more cleanup --- mutaext.py | 18 ++++++++--------- unsync.py | 59 ++++++++++++++++++++---------------------------------- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/mutaext.py b/mutaext.py index 504cabb..ce3a785 100644 --- a/mutaext.py +++ b/mutaext.py @@ -25,23 +25,23 @@ def rating2byte(r): return 0 def rating_get(id3, key): - try: + if 'TXXX:RATING' in id3: rating = id3['TXXX:RATING'] - except KeyError: + return list(rating.text) + else: try: _, popm = next(popms(id3)) except StopIteration: return [] else: return [str(byte2rating(popm.rating))] - else: - return list(rating.text) def _canconv(r): try: - if int(r) != str(int(r)): + ir = int(r) + if ir != str(ir): return False - return int(r) >= 1 and int(r) <= 5 + return ir >= 1 and ir <= 5 except (ValueError, TypeError): return False @@ -77,9 +77,6 @@ EasyID3.RegisterTextKey('albumartist', 'TPE2') EasyID3.RegisterKey('rating', rating_get, rating_set, rating_delete) class SyncFile(MutableMapping): - """Dumb OOP crap that just adds more code - and a sense of self-importance""" - def __init__(self, path): self.md = mutagenx.File(path, easy=True) self.path = path @@ -113,3 +110,6 @@ class SyncFile(MutableMapping): def __len__(self): return len([k for k in self.__iter__()]) + + def save(self): + return self.md.save() diff --git a/unsync.py b/unsync.py index 1adfa36..5c49a80 100755 --- a/unsync.py +++ b/unsync.py @@ -1,25 +1,22 @@ #!/bin/python -import os -import os.path +import os, os.path import sys -import shutil -import tempfile +from shutil import copy2 +from tempfile import mkstemp from zlib import crc32 import mutaext import convert from mutaext import SyncFile -# BUG: doesn't work with my .m4a files? -#goodexts = ('.mp3', '.mp2', '.m4a', '.flac', '.ogg') goodexts = ('.mp3', '.flac', '.ogg') matchtags = ['artist', 'album', 'title'] -alltags = [\ - 'albumartist', 'composer', 'comment' \ - 'tracknumber', 'discnumber', \ - 'genre', 'date', \ +alltags = [ + 'albumartist', 'composer', 'comment', + 'tracknumber', 'discnumber', + 'genre', 'date', ] alltags.extend(mutaext.replaygain_tags) alltags.extend(mutaext.extra_tags) @@ -29,15 +26,12 @@ lament = lambda *args, **kwargs: print(*args, file=sys.stderr, **kwargs) walkfiles = lambda w: (os.path.join(r, f) for r, _, fs in w for f in fs) extof = lambda p: os.path.splitext(p)[1].lower() filterext = lambda ps, es: (p for p in ps if extof(p) in es) -ansty = lambda u: str(u.decode('ascii', errors='replace').replace('\ufffd', '?')) def shouldsync(md): rating = md.get('rating', '') sync = md.get('sync', '') - try: + if rating.isnumeric(): rating = int(rating) - except ValueError: - pass if sync: sync = sync.lower() @@ -80,8 +74,8 @@ def makefilename(md): album = md['album'] fn = "%(artist)s - %(album)s - %(title)s" % locals() - crc = crc32(fn.encode('utf-8')) & 0xFFFFFFFF # FAT is a pain to deal with so just use nondescript filenames + crc = crc32(fn.encode('utf-8')) & 0xFFFFFFFF fn = '{:08X}.ogg'.format(crc) return fn @@ -108,7 +102,7 @@ def run(args): if inonly: return 0 - lament("Matching...") + lament("Matching tags...") outdir = args[2] for p in paths(outdir): @@ -119,42 +113,33 @@ def run(args): os.remove(p) elif updatemetadata(md, match): print("UPD", p) - md.md.save() + md.save() + + lament("Syncing files...") - lament("[beginning tosync]") for md in tosync: - #print("SYN", md.path) fn = makefilename(md) fout = os.path.join(outdir, fn) if md.seen: - try: - _from = md.seen - _to = fout - if type(_from) != type(_to): - raise TypeError - - if _from != _to: - print("MOV", _from) - os.rename(_from, _to) - continue - except: - lament(type(_from), type(_to)) - lament("_from:", [_from]) - lament("_to:", [_to]) - raise + _from = md.seen + _to = fout + if _from != _to: + print("MOV", _from) + os.rename(_from, _to) + continue print("ADD", md.path) - _, ftemp = tempfile.mkstemp() + _, ftemp = mkstemp() try: convert.ogg(md.path, ftemp) mdnew = SyncFile(ftemp) for tag in alltags: if tag in md: mdnew[tag] = md[tag] - mdnew.md.save() - shutil.copy2(ftemp, fout) + mdnew.save() + copy2(ftemp, fout) finally: os.remove(ftemp) From 11b0a802f33ef47fcd6e755b83e1ed5126acb53c Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Tue, 24 Mar 2015 20:25:59 -0700 Subject: [PATCH 19/19] sync by track and disc numbers too --- mutaext.py | 4 +++- unsync.py | 28 +++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/mutaext.py b/mutaext.py index ce3a785..18a73d6 100644 --- a/mutaext.py +++ b/mutaext.py @@ -92,7 +92,9 @@ class SyncFile(MutableMapping): raise KeyError(key) def __setitem__(self, key, value): - if type(value) != str: + #if type(value) != str: + #raise ValueError + if type(value) is type(None): raise ValueError self.md[key] = [value] diff --git a/unsync.py b/unsync.py index 5c49a80..d70cb78 100755 --- a/unsync.py +++ b/unsync.py @@ -12,10 +12,9 @@ from mutaext import SyncFile goodexts = ('.mp3', '.flac', '.ogg') -matchtags = ['artist', 'album', 'title'] +matchtags = ['artist', 'album', 'title', 'tracknumber', 'discnumber'] alltags = [ 'albumartist', 'composer', 'comment', - 'tracknumber', 'discnumber', 'genre', 'date', ] alltags.extend(mutaext.replaygain_tags) @@ -37,17 +36,31 @@ def shouldsync(md): return sync == 'yes' or type(rating) == int and rating >= 3 and sync != 'no' and sync != 'space' +import re +re_digits = re.compile(r'\d+') +def tonumber(crap): + if crap is None or len(crap) == 0: + return 0 + nums = re_digits.findall(crap) + if len(nums) == 0: + return 0 + return int(nums[0]) + def fixmetadata(md): md['artist'] = md.get('artist', "Unknown Artist") md['album'] = md.get('album', "Unknown Album") + md['discnumber'] = str(tonumber(md.get('discnumber', '0'))) + md['tracknumber'] = str(tonumber(md.get('tracknumber', '0'))) if 'title' not in md: fn = os.path.basename(md.path) fn = os.path.splitext(fn)[0] md['title'] = str(fn) def findmatching(haystack, needle): - matchme = [needle[t].lower() for t in matchtags] - ismatch = lambda hay: [hay[t].lower() for t in matchtags] == matchme + #matchme = [needle[t].lower() for t in matchtags] + #ismatch = lambda hay: [hay[t].lower() for t in matchtags] == matchme + matchme = [needle[t] for t in matchtags] + ismatch = lambda hay: [hay[t] for t in matchtags] == matchme for match in (hay for hay in haystack if ismatch(hay)): if match.seen: # TODO: check other tags too? @@ -72,8 +85,10 @@ def makefilename(md): title = md['title'] artist = md['artist'] album = md['album'] + track = md['tracknumber'] + disc = md['discnumber'] - fn = "%(artist)s - %(album)s - %(title)s" % locals() + fn = "%(disc)s-%(track)s - %(artist)s - %(album)s - %(title)s" % locals() # FAT is a pain to deal with so just use nondescript filenames crc = crc32(fn.encode('utf-8')) & 0xFFFFFFFF fn = '{:08X}.ogg'.format(crc) @@ -107,9 +122,11 @@ def run(args): outdir = args[2] for p in paths(outdir): md = SyncFile(p) + fixmetadata(md) match = findmatching(tosync, md) if match == None: print("DEL", p) + print('was', md['title'], 'by', md['artist']) os.remove(p) elif updatemetadata(md, match): print("UPD", p) @@ -138,6 +155,7 @@ def run(args): for tag in alltags: if tag in md: mdnew[tag] = md[tag] + fixmetadata(mdnew) # redundant? mdnew.save() copy2(ftemp, fout) finally: