#!/usr/bin/python # Joe Gillotti - 2/15/2014 import mutagen.flac import mutagen.mp3 import mutagen.easyid3 import mutagen.oggvorbis import mutagen.m4a import mutagen.asf import mutagen.id3 import subprocess import time import sys import os import re class ParseFileException(Exception): pass class WriteFileException(Exception): pass class MetadataException(Exception): pass class swirly(): def __init__(self, title): self.title = title self.pos = 0 self.swirls = ('|', '/', '-', '\\') self.dead = False def next(self): char = self.iter().next() sys.stdout.write( '[%s] %s..\r' % (char, self.title)) sys.stdout.flush() def done(self): if not self.dead: sys.stdout.write('\r[+]\n') sys.stdout.flush() self.dead = True def iter(self): while True: try: char = self.swirls[self.pos] except IndexError: self.pos = 0 char = self.swirls[self.pos] self.pos += 1 yield char class SourceFile(): def __init__(self, path): self.path = path self.ext = os.path.basename(path).split('.')[-1] def getPCM (self): cmds = { 'flac': ('flac', '-d', self.path, '-c'), 'ogg': ('oggdec', '-o', '-', '-Q', self.path), 'mp3': ('mpg123', '-w', '-', self.path), 'm4a': ('ffmpeg', '-i', self.path, '-ac', '2', '-f', 'wav', '-'), 'wma': ('ffmpeg', '-i', self.path, '-ac', '2', '-f', 'wav', '-'), } if self.ext not in cmds: raise ParseFileException('Unsupported extension type') try: proc = subprocess.Popen(cmds[self.ext], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: raise ParseFileException('OSError: %s' % e) return proc def getMetadata (self): apps = { 'flac': mutagen.flac.FLAC, 'ogg': mutagen.oggvorbis.OggVorbis, 'm4a': mutagen.m4a.M4A, 'wma': mutagen.asf.ASF, 'mp3': mutagen.easyid3.EasyID3, } if self.ext in apps.keys(): try: info = apps[self.ext](self.path) except Exception as e: raise MetadataException('Failed reading from %s: %s' % (self.path, e)) else: raise ParseFileException('Unsupported extension type') # mutagen.m4a has a wacked naming schema if self.ext == 'm4a': try: info['title'] = (info['\xa9nam'], ) info['artist'] = (info['\xa9ART'], ) info['album'] = (info['\xa9alb'], ) if 'trkn' in info: info['tracknumber'] = (info['trkn'][0], ) if 'disk' in info: info['discnumber'] = (info['disk'][0], ) if '\xa9gen' in info: info['genre'] = (info['\xa9gen'], ) except KeyError as e: raise MetadataException('Failed m4a: %s' % e) # wma is worse elif self.ext == 'wma': try: for key in ('WM/AlbumArtist', 'Author'): if key in info: info['artist'] = info[key] break for key in ('WM/AlbumTitle', ): if key in info: info['album'] = info[key] break if 'WM/Genre' in info: info['genre'] = info['WM/Genre'] if 'WM/TrackNumber' in info: info['tracknumber'] = info['WM/TrackNumber'] if 'Title' in info: info['title'] = info['Title'] for key in ('title', 'artist'): if key not in info: raise MetadataException('Missing %s' % key) except KeyError as e: raise MetadataException('Failed wma: %s' % e) keys = ( 'title', 'album', 'tracknumber', 'artist', 'discnumber', 'genre' ) numeric = ( 'tracknumber', 'discnumber', 'tracktotal' ) ret = {} for key in keys: try: if key in numeric: ret[key] = int(info[key][0]) else: ret[key] = str(info[key][0]).strip() if ret[key] == '': ret[key] = 'Unknown' except: if key in numeric: ret[key] = 0 else: ret[key] = 'Unknown' if ret['tracknumber'] == 0: m = re.match('^\s*(\d+)', os.path.basename(self.path).strip()) if m: ret['tracknumber'] = int(m.group(1)) return ret def generateDestname(self): meta = self.getMetadata() for k, v in meta.iteritems(): if isinstance(v, basestring): meta[k] = v.replace('/', '_') if 'discnumber' in meta and meta['discnumber'] != 0: fileformat = '%(artist)s/%(album)s/%(discnumber)d - %(tracknumber)02d %(title)s.mp3' else: fileformat = '%(artist)s/%(album)s/%(tracknumber)02d %(title)s.mp3' return fileformat % meta class DestFile(): def __init__(self, path): self.path = path def write(self, src, expand=True, skipExisting=False): if os.path.exists(self.path) and skipExisting: raise WriteFileException('File already exists') if not os.path.isdir(os.path.dirname(self.path)) and expand: try: os.makedirs(os.path.dirname(self.path)) except OSError as e: raise WriteFileException('Failed making dir %s' % e) progress = swirly('Encoding %s' % os.path.basename(self.path)) cmd = ('lame', '-', self.path) readProc = src.getPCM() try: writeProc = subprocess.Popen(cmd, stdin=readProc.stdout, stderr=subprocess.PIPE) except OSError as e: raise WriteFileException('OSError: %s' % e) while readProc.poll() == None or writeProc.poll() == None: if readProc.poll() not in (None, 0): progress.done() raise WriteFileException('Reading from src failed: %s' % readProc.returncode) if writeProc.poll() not in (None, 0): progress.done() raise WriteFileException('Writing to proc failed: %s' % writeProc.returncode) progress.next() time.sleep(.1) progress.done() if readProc.returncode != 0 or writeProc.returncode != 0: raise WriteFileException('reading/writing failed') def saveMetadata(self, src): meta = src.getMetadata() try: mp3 = mutagen.easyid3.EasyID3(self.path) except mutagen.id3.ID3NoHeaderError: try: tags = mutagen.id3.ID3() tags.add(mutagen.id3.TIT2(encoding=3, text=meta['title'])) tags.save(self.path) except Exception as e: raise MetadataException('Couldnt write fake tag: %s' % e) try: mp3 = mutagen.easyid3.EasyID3(self.path) except Exception as e: raise MetadataException('Failed it again: %s' % e) try: for k, v in meta.iteritems(): mp3[k] = [v] mp3.save() except Exception as e: raise MetadataException('Failed saving: %s' % e) def getSrcFiles(): srcfolder = '/srv/media/public/Music/' for root, dirs, files in os.walk(srcfolder): for name in files: if name.split('.')[-1] in ('wma', 'm4a', 'flac', 'ogg', 'mp3'): yield os.path.join(root, name) def main(): outfolder = '/srv/media/public/tmp' times = [] count = 0 for srcpath in getSrcFiles(): start = time.time() try: try: sauce = SourceFile(srcpath) dest = os.path.join(outfolder, sauce.generateDestname()) except ParseFileException as e: print 'Skipping reading %s because %s' % (srcpath, e) except MetadataException as e: print 'Skipping %s because metadata %s' % (srcpath, e) continue try: transcode = DestFile(dest) transcode.write(sauce, True, True) transcode.saveMetadata(sauce) except WriteFileException as e: print 'Skipping writing %s because %s' % (dest, e) continue except MetadataException as e: print 'Skipping %s because metadata %s' % (srcpath, e) return except UnicodeError as e: print 'Skipping because of unicode funk: %s ' % e continue except KeyboardInterrupt: print '\r\nAborting..' try: os.remove(dest) print 'Removed %s' % dest except: pass return count += 1 times.append(float(time.time() - start)) if count % 10 == 0: print 'Average time to do last 10 songs: %f each' % round(sum(times) / float(len(times)), 2) times = [] if __name__ == '__main__': main()