commit a0e50ceaf7672ef25a36958ba7aca8c41395d917 Author: root Date: Sun Jul 3 23:43:32 2016 -0400 Initial commit diff --git a/.project b/.project new file mode 100644 index 0000000..7401a8e --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + mediamanager_media4 + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/musicman.ini b/musicman.ini new file mode 100644 index 0000000..2d86b17 --- /dev/null +++ b/musicman.ini @@ -0,0 +1,11 @@ +[origin] +Path = /srv/public/Music-Lossless +format = flac + +[target] +Path = /srv/public/Music-iTunes +format = m4a + +[working] +Path = /srv/public/Music-iTunes.new + diff --git a/musicman.py b/musicman.py new file mode 100755 index 0000000..5f7cb0f --- /dev/null +++ b/musicman.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 + +from __future__ import division, absolute_import, print_function, unicode_literals + +import os +import signal +import sys +#import mutagen +import time +#from path import path + +import musicman + +#from musicman.utils.constants import ( +# VERSION, +# NO_TAGS +#) +#from musicman.utils import ( +# parse_args, +# load_config +#) +#from musicman.utils.metadata import MetaTag + +def spinning_cursor(): + while True: + for cursor in '|/-\\': + yield cursor + +spinner = spinning_cursor() + +def supports_color(): + """ + Returns True if the running system's terminal supports color, and False + otherwise. + """ + plat = sys.platform + supported_platform = plat != 'Pocket PC' and (plat != 'win32' or + 'ANSICON' in os.environ) + # isatty is not always implemented, #6223. + is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() + if not supported_platform or not is_a_tty: + return False + return True + +def clearLine(): + if supports_color(): + sys.stdout.write('\033[K') + sys.stdout.flush() + +def sanitize(text): + newtext = text + newtext = newtext.replace('<', '').replace('>', '').replace(':', '').replace('"', '').replace('|', '').replace('?', '').replace('*', '') + newtext = newtext.replace('/', '-') + newtext = newtext.strip() + #if newtext != text: + # clearLine() + # print("text:", text) + # print(" new:", newtext) + return newtext + +def getLibrary(path, excludeDirs=[]): + if os.path.isdir(path): + for root, directories, filenames in sorted(os.walk(path)): + if root not in excludeDirs: + for filename in filenames: + yield os.path.join(root, filename) + +# files=[] +# +# if os.path.isdir(path): +# for libraryDir, artists, dummy in os.walk(path): +# for artist in sorted(artists): +# for artistDir, albums, dummy in os.walk(os.path.join(libraryDir, artist)): +# if artistDir in excludeDirs: +# #clearLine() +# #print("Excluding:", artistDir) +# #print() +# continue +# for album in sorted(albums): +# for albumDir, dummy, songs in os.walk(os.path.join(artistDir, album)): +# if albumDir in excludeDirs: +# #clearLine() +# #print("Excluding:", albumDir) +# #print() +# continue +# #print("AlbumDir:", albumDir) +# #print(" Artist:", artist) +# #print(" Album:", album) +# for song in songs: +# print("Scanning", path, next(spinner), end="\r") +# files.append(os.path.join(albumDir, song)) +# #print("Path:", os.path.join(albumDir, song)) +# clearLine() +# print("Scan complete!") +# return files + +def getEmptyDirs(path, excludeDirs=[]): + if os.path.isdir(path): + for root, directories, filenames in sorted(os.walk(path)): + if root not in excludeDirs: + if not directories and not filenames: + yield root + +def getSong(file): + from musicman.utils.metadata import MetaTag + from musicman.utils.constants import INTERNAL_FORMATS + global config + song = {} + + #if file.endswith(INTERNAL_FORMATS): + if (os.path.splitext(file)[1][1:] in INTERNAL_FORMATS): + metadata = MetaTag(file) + + if metadata.tags.get("artist") is None or len(metadata.tags.get("artist")) < 1: + return {'metadata': None} + elif metadata.tags.get("albumartist") is None or len(metadata.tags.get("albumartist")) < 1: + return {'metadata': None} + elif metadata.tags.get('album') is None or len(metadata.tags.get("album")) < 1: + return {'metadata': None} + elif metadata.tags.get("musicbrainz_albumid") is None or len(metadata.tags.get("musicbrainz_albumid")) < 5: + return {'metadata': None} + else: + song['metadata'] = metadata + song['artistName'] = sanitize(metadata.tags["albumartist"]) + song['albumName'] = sanitize(metadata.tags["album"]) + song['titleName'] = sanitize(metadata.tags["title"]) + if isinstance(metadata.tags.get("totaldiscs", 0), tuple): + song['discnumber'] = int(metadata.tags.get('totaldiscs', 0)[0]) + song['totaldiscs'] = int(metadata.tags.get("totaldiscs", 0)[1]) + else: + song['discnumber'] = int(metadata.tags.get('discnumber', 0)) + song['totaldiscs'] = int(metadata.tags.get("totaldiscs", 0)) + if isinstance(metadata.tags.get('totaltracks', 0), tuple): + song['tracknumber'] = int(metadata.tags.get('totaltracks', 0)[0]) + song['totaltracks'] = int(metadata.tags.get('totaltracks', 0)[1]) + else: + song['tracknumber'] = int(metadata.tags.get('tracknumber', 0)) + song['totaltracks'] = int(metadata.tags.get('totaltracks', 0)) + + if song['totaldiscs'] > 1: + song['outPath'] = os.path.join(song['artistName'], song['albumName']) + song['outFile'] = '{0:d}-{1:02d}-{2}'.format(song['discnumber'], + song['tracknumber'], + song['titleName']) + else: + if metadata.tags.get("tracknumber") is not None: + song['outPath'] = os.path.join(song['artistName'], song['albumName']) + song['outFile'] = '{0:02d}-{1}'.format(song['tracknumber'], song['titleName']) + else: + song['outPath'] = os.path.join(song['artistName'], song['albumName']) + outFile = '{0}'.format(song['titleName']) + + return song + else: + clearLine() + print("FATAL: File extension \"{0}\" is not supported.".format(os.path.splitext(file)[1])) + print(" Supported:", ", ".join(INTERNAL_FORMATS)) + sys.exit(2) + +def getSongPaths(song): + global originDir + global workingDir + global targetDir + + print("DEBUG:", originDir) + print("DEBUG:", workingDir) + print("DEBUG:", targetDir) + + sys.exit(0) + +def cleanLibrary(originDir, excludeDirs=[], act=False, verbose=0): + global config + + if excludeDirs is None: + excludeDirs=[] + + print("Clean Dirs") + + #while getEmptyDirs(originDir, excludeDirs) != [] and act == True and tries > 0: + + if act: + try: + while getEmptyDirs(originDir, excludeDirs).__next__(): + for path in getEmptyDirs(originDir, excludeDirs): + print("Removing:", path) + + if act: + try: + os.rmdir(path) + except OSError as err: + print("ERROR: Failed to remove directory:", err) + sys.exit(5) + except StopIteration: + pass + else: + for path in getEmptyDirs(originDir, excludeDirs): + print("Empty:", path) + + print("Processing Complete!") + +def renameLibrary(originDir, excludeDirs=[], act=False, verbose=0): + global config + + if excludeDirs is None: + excludeDirs=[] + + print("Rename") + for file in getLibrary(originDir, excludeDirs): + #print("File:", file) + if (os.path.isdir(os.path.dirname(file)) and os.path.isfile(file)): + clearLine() + print("Processing:", os.path.dirname(os.path.dirname(file)), next(spinner), end="\r") + + song = getSong(file) + + #print("song", song) + if song['metadata'] is None: + if verbose > 2: + clearLine() + print("Skipping: {0} due to lack of metadata".format(file)) + else: + filename = os.path.basename(file) + file_ext = os.path.splitext(filename)[1] + + if not os.path.isfile(os.path.join(originDir, song['outPath'], song['outFile'] + file_ext)): + clearLine() + print("Found:", file) + if verbose > 0: + print(" New:", os.path.join(originDir, song['outPath'], song['outFile'] + file_ext)) + + if (act): + if verbose > 1: + print("Renaming: \"{0}\" -> \"{1}\"".format(file, os.path.join(originDir, song['outPath'], song['outFile'] + file_ext))) + try: + os.renames(file, + os.path.join(originDir, + song['outPath'], + song['outFile'] + file_ext)) + except OSError as err: + print("ERROR: Failed to move:", err) + sys.exit(5) + + clearLine() + print("Processing Complete!") + +def findUntagged(originDir, excludeDirs=[], verbose=0): + global config + + if excludeDirs is None: + excludeDirs=[] + + print("Find Untagged") + for file in getLibrary(originDir, excludeDirs): + if (os.path.isdir(os.path.dirname(file)) and os.path.isfile(file)): + clearLine() + print("Processing:", os.path.dirname(os.path.dirname(file)), next(spinner), end="\r") + + song = getSong(file) + + if song['metadata'] is None: + clearLine() + print("Untagged: {0}".format(file)) + clearLine() + print("Processing Complete!") + +def findNew(originDir, workingDir, targetDir, targetFormat, excludeDirs=[], verbose=0): + global config + + if excludeDirs is None: + excludeDirs=[] + + print("Find New Media") + for file in getLibrary(originDir, excludeDirs): + if (os.path.isdir(os.path.dirname(file)) and os.path.isfile(file)): + clearLine() + print("Processing:", os.path.dirname(os.path.dirname(file)), next(spinner), end="\r") + + song = getSong(file) + + getSongPaths(song) + + if song['metadata'] is None: + if verbose > 2: + clearLine() + print("Skipping: {0} due to lack of metadata".format(file)) + else: + filename = os.path.basename(file) + file_ext = os.path.splitext(filename)[1] + + if not (os.path.isfile(os.path.join(workingDir, song['outPath'], song['outFile'] + '.' + config['target']['format'])) or + os.path.isfile(os.path.join(targetDir, song['outPath'], song['outFile'] + '.' + config['target']['format']))): + print("New:", file) + if verbose > 0: + if not os.path.isfile(os.path.join(targetDir, song['outPath'], song['outFile'] + '.' + config['target']['format'])): + print(" No:", os.path.join(targetDir, song['outPath'], song['outFile'] + '.' + config['target']['format'])) + elif not os.path.isfile(os.path.join(workingDir, song['outPath'], song['outFile'] + '.' + config['target']['format'])): + print(" No:", os.path.join(workingDir, song['outPath'], song['outFile'] + '.' + config['target']['format'])) + #if not os.path.isfile(os.path.join(originDir, song['outPath'], song['outFile'] + file_ext)): + # clearLine() + # print("New:", file) + + clearLine() + print("Processing Complete!") + +def syncWorking(workingDir, targetDir, excludeDirs=[], act=False, verbose=0): + global config + + if excludeDirs is None: + excludeDirs=[] + + print("Sync Target Media") + + for file in getLibrary(workingDir, excludeDirs): + if (os.path.isdir(os.path.dirname(file)) and os.path.isfile(file)): + clearLine() + #print("Checking:", file) + print("Processing:", os.path.dirname(os.path.dirname(file)), next(spinner), end="\r") + + song = getSong(file) + + if song['metadata'] is None: + if verbose > 2: + clearLine() + print("Skipping: {0} due to lack of metadata".format(file)) + else: + filename = os.path.basename(file) + file_ext = os.path.splitext(filename)[1] + + if not os.path.isfile(os.path.join(targetDir, song['outPath'], song['outFile'] + file_ext)): + print("Sync:", file) + if verbose > 0: + print(" To:", os.path.join(targetDir, song['outPath'], song['outFile'] + file_ext)) + + if act: + print("would've acted") + + clearLine() + print("Processing Complete!") + + +if __name__ == '__main__': + global opt + global config + global originPath, targetPath, targetFormat, workPath + import configparser + #opt, files = parse_args() + opt = musicman.utils.parse_args() + config = musicman.utils.load_config() + + print("opt:", opt) + + try: + originDir = config['origin']['path'] if opt.originDir is None else opt.originDir + except AttributeError: + originDir = config['origin']['path'] + + try: + targetDir = config['target']['path'] if opt.targetDir is None else opt.targetDir + except AttributeError: + targetDir = config['target']['path'] + + try: + targetFormat = config['target']['format'] if opt.targetFormat is None else opt.targetFormat + except AttributeError: + targetFormat = config['target']['format'] + + try: + workingDir = config['working']['path'] if opt.workingDir is None else opt.workingDir + except AttributeError: + workingDir = config['working']['path'] + + if opt.mode is None: + print("ERROR: No command provided.") + sys.exit(1) + + + try: + if opt.mode == 'clean': + cleanLibrary(originDir, opt.excludeDirs, opt.act, opt.verbose) + + elif opt.mode == 'rename': + renameLibrary(originDir, opt.excludeDirs, opt.act, opt.verbose) + + elif opt.mode == 'scan': + if opt.scanMode is None: + print("ERROR: Subcommand for scan not provided.") + sys.exit(1) + elif opt.scanMode == 'untagged': + findUntagged(originDir, opt.excludeDirs, opt.verbose) + elif opt.scanMode == 'new': + findNew(originDir, workingDir, targetDir, targetFormat, opt.excludeDirs, opt.verbose) + elif opt.mode == 'sync': + syncWorking(workingDir, targetDir, opt.excludeDirs, opt.act, opt.verbose) + except KeyboardInterrupt: + clearLine() + print(end='\r') + print("Aborted by user") + + + + #for file in getLibrary(config['origin']['path']): + # #print("File:", file) + # if (os.path.isdir(os.path.dirname(file)) and os.path.isfile(file)): + # clearLine() + # print("Processing:", os.path.dirname(os.path.dirname(file)), next(spinner), end="\r") + # #time.sleep(0.01) + # + # #print("Path:", os.path.dirname(file)) + # #print("File:", os.path.basename(file)) + #clearLine() + #print("Processing Complete!") + + sys.exit(0) + #config = configparser.ConfigParser() + #config.read('library.ini') + + #print(config.sections()) + #print(config["lossless"]["Path"]) + + #try: + # #print("Test1:", config.get('lossless', 'test')) + # config.get('lossless', 'path') + # config.get('lossless', 'format') + # config.get('converted', 'path') + # config.get('converted', 'format') + #except configparser.NoOptionError as err: + # print("ERROR: Configuration of required settings are missing:", err) + # sys.exit(1) + + LosslessLibraryRoot = '/srv/public/Music-Lossless' + #LosslessLibraryRoot = '/srv/public/Music-iTunes' + iTunesLibraryRoot = '/srv/public/Music-iTunes' + + #artistWalker = os.walk(LosslessLibraryRoot) + #dest_dir, artists, files = artistWalker.next() + + #print "dest_dir: %s" % dest_dir + #print "artists: %s" % artists + #print "files: %s" % files + + #for artist in artists: + # print "Artist: %s" % artist + + # albumWalker = os.walk(os.path.join(dest_dir, artist)) + # artist_dir, albums, artist_files = albumWalker.next() + + # print "Albums: %s" % albums + # print "Album Dir: %s" % artist_dir + + # for album in albums: + # songWalker = os.walk(os.path.join(artist_dir, album) + # album_dir, dummy, songs = songWalker.next() + + for libraryDir, artists, dummy in os.walk(config['origin']['path']): + for artist in sorted(artists): + for artistDir, albums, dummy in os.walk(os.path.join(libraryDir, artist)): + for album in sorted(albums): + for albumDir, dummy, songs in os.walk(os.path.join(artistDir, album)): + #print("AlbumDir:", albumDir) + #print(" Artist:", artist) + #print(" Album:", album) + for song in songs: + if song.endswith('.flac'): + #print " Song: %s" % song + metadata = MetaTag(os.path.join(albumDir, song)) + #if os.path.isfile(os.path.join() + #print "MetaData: %s" % metadata.tags + #os.path.join(iTunesLibraryRoot, [metadata.tags["artist"], metadata.tags["album"]]) + #print("\033[KArtist:", metadata.tags["artist"], end="\r") + + clearLine() + print("Scanning", artistDir, next(spinner), end="\r") + + #if int(metadata.tags["totaldiscs"]) > 1: + if metadata.tags.get("artist") is None: + continue + if metadata.tags.get("albumartist") is None: + continue + if metadata.tags.get('album') is None: + continue + if metadata.tags.get("musicbrainz_albumid") is None or len(metadata.tags.get("musicbrainz_albumid")) < 5: + clearLine() + print("Skipping:", os.path.join(albumDir, song)) + continue + + #if 'Centennial' in song: + # print + # print + # print("DEBUG") + # print("Path:", artistDir) + # print("Song:", song) + # print("musicbrainz_albumid:", metadata.tags.get("musicbrainz_albumid")) + # print(type(metadata.tags.get("musicbrainz_albumid"))) + # sys.exit(0) + + artistName = sanitize(metadata.tags["albumartist"]) + albumName = sanitize(metadata.tags["album"]) + titleName = sanitize(metadata.tags["title"]) + outPath = '' + outFile = '' + + if int(metadata.tags.get("totaldiscs", 0)) > 1: + outPath = os.path.join(config['converted']['path'], + artistName, + albumName) + outFile = '{0:d}-{1:02d}-{2}.{3}'.format(int(metadata.tags["discnumber"]), + int(metadata.tags["tracknumber"]), + titleName, + 'm4a') + #print("iTunes:", os.path.join(iTunesLibraryRoot, + # metadata.tags["artist"], + # metadata.tags["album"], + # '{0:d}-{1:02d}-{2}.{3}'.format(int(metadata.tags["discnumber"]), + # int(metadata.tags["tracknumber"]), + # metadata.tags["title"], + # 'm4a'))) + # #int(metadata.tags["discnumber"]) + '-' + '{0:02d}'.format(int(metadata.tags["tracknumber"])) + '-' + metadata.tags["title"] + ".m4a") + else: + if metadata.tags.get("tracknumber") is not None: + outPath = os.path.join(config['converted']['path'], + artistName, + albumName) + outFile = '{0:02d}-{1}.{2}'.format(int(metadata.tags["tracknumber"]), titleName, 'm4a') + #print("iTunes:", os.path.join(iTunesLibraryRoot, + # metadata.tags["artist"], + # metadata.tags["album"], + # '{0:02d}'.format(int(metadata.tags["tracknumber"])) + '-' + metadata.tags["title"] + ".m4a")) + else: + outPath = os.path.join(config['converted']['path'], + artistName, + albumName) + outFile = '{0}.{1}'.format(titleName, 'm4a') + #print("iTunes:", os.path.join(iTunesLibraryRoot, + # metadata.tags["artist"], + # metadata.tags["album"], + # metadata.tags["title"] + ".m4a")) + #print "iTunes: %s" % os.path.join(iTunesLibraryRoot, metadata.tags["artist"], metadata.tags["album"]) + if not os.path.isfile(os.path.join(outPath, outFile)): + print("NEW:", os.path.join(outPath, outFile)) + if song.endswith('.m4a'): + #print " Song: %s" % song + metadata = MetaTag(os.path.join(albumDir, song)) + #print "MetaData: %s" % metadata.tags + clearLine() + print() + + +#LosslessLibraryRoot = '/srv/public/Music-Lossless' +# +#for subdir, dirs, files in os.walk(LosslessLibraryRoot): +# print subdir +# +# for file in files: +# if file.endswith('.flac'): +# audio = mutagen.File(os.path.join(subdir, file)) +# print file +# #print audio.tags.pprint() +# print "Artist: %s" % audio['ARTIST'][0] +# print "Album: %s" % audio['ALBUM'][0] +# print "Title: %s" % audio['TITLE'][0] +# +# #for file in files: +# # print os.path.join(subdir, file) + diff --git a/musicman/__init__.py b/musicman/__init__.py new file mode 100644 index 0000000..d52db2d --- /dev/null +++ b/musicman/__init__.py @@ -0,0 +1,2 @@ +import musicman.utils + diff --git a/musicman/__init__.pyc b/musicman/__init__.pyc new file mode 100644 index 0000000..0f9e57d Binary files /dev/null and b/musicman/__init__.pyc differ diff --git a/musicman/__pycache__/__init__.cpython-33.pyc b/musicman/__pycache__/__init__.cpython-33.pyc new file mode 100644 index 0000000..4c430d5 Binary files /dev/null and b/musicman/__pycache__/__init__.cpython-33.pyc differ diff --git a/musicman/utils/__init__.py b/musicman/utils/__init__.py new file mode 100644 index 0000000..0370c73 --- /dev/null +++ b/musicman/utils/__init__.py @@ -0,0 +1,93 @@ +import os +#import tempfile + +from musicman.utils.constants import SUPPORTED_FORMATS, VERSION + +def parse_args(): + import argparse + #parser = optparse.OptionParser(usage="%prog [options] [files]", + # version=VERSION) + #parser = argparse.ArgumentParser(description="Media Library Management Tools", + # usage="%(prog)s [command] [options]") + parser = argparse.ArgumentParser(description="Media Library Management Tools") + #subparsers = parser.add_subparsers(help="sub-command-help") + + #parser.add_argument('-b', '--base', help="Origin Directory for Library (overrides config)", type=str, dest="originDir") + #parser.add_argument('-t', '--target', help="Target Directory for Library (overrides config)", type=str, dest="targetDir") + + parser.add_argument('--version', help="show version information and exit", action="version", version='%(prog)s '+VERSION) + parser.add_argument('-v', help='Increase verbosity of processing', dest='verbose', action='count') + + subparsers = parser.add_subparsers(title="Command Modes", dest="mode") + + clean = subparsers.add_parser('clean', help='Clean Mode', description='Library cleanup operations. Allows you to remove empty directories.') + clean_lib = clean.add_argument_group('Library Options') + clean_lib.add_argument('-o', '--origin', help="Origin Directory for Library (overrides config)", metavar="DIR", type=str, dest="originDir") + clean_lib.add_argument('-e', '--exclude', help="Exclude Directory from origin (can be used multiple times)", metavar="DIR", dest='excludeDirs', action='append') + clean_act = clean.add_argument_group('Action Options') + clean_act.add_argument('-g', '--go', help="Clean up library (default just shows what would be done)", dest="act", action='store_true') + clean.set_defaults(act=False) + + convert = subparsers.add_parser('convert', help='Convert Mode', description='Conversion mode scans for media in the origin library, and converts them into the target format. Conversion depends on a fully tagged library, including MusicBrainz metadata. This insures that the data provide is accurate as it uses that metadata for the destination artist/album/song.') + convert_lib = convert.add_argument_group('Library Options') + convert_lib.add_argument('-o', '--origin', help="Origin Directory for Library (overrides config)", metavar="DIR", type=str, dest="originDir") + convert_lib.add_argument('-t', '--target', help="Target Directory for Library (overrides config)", metavar="DIR", type=str, dest="targetDir") + convert_lib.add_argument('--format', help="Target directory library format", metavar="FORMAT", type=str, dest="targetFormat") + convert_lib.add_argument('-w', '--work', help="Working Directory for new processed files (overrides config)", metavar="DIR", type=str, dest="workDir") + convert_lib.add_argument('-e', '--exclude', help="Exclude Directory from origin (can be used multiple times)", metavar="DIR", dest='excludeDirs', action='append') + convert_act = convert.add_argument_group('Action Options') + convert_act.add_argument('-g', '--go', help="Convert media (default just shows new items)", dest="act", action='store_true') + convert.set_defaults(act=False) + + info = subparsers.add_parser('info', help='Info Mode', description='Displays file and metadata information about specified files and files within specified directories.') + info.add_argument('paths', help="Show information about files, or all files in directory.", metavar="PATH", nargs='+') + + rename = subparsers.add_parser('rename', help='Rename Mode', description='Library rename tool renames media into their respective Artist/Album/[Disc-][Track-]Title in relation to their metadata.') + rename_lib = rename.add_argument_group('Library Options') + rename_lib.add_argument('-o', '--origin', help="Origin Directory for Library (overrides config)", metavar="DIR", type=str, dest="originDir") + rename_lib.add_argument('-e', '--exclude', help="Exclude Directory from origin (can be used multiple times)", metavar="DIR", dest='excludeDirs', action='append') + rename_act = rename.add_argument_group('Action Options') + rename_act.add_argument('-g', '--go', help="Process renaming (default just shows what would be done", dest="act", action='store_true') + rename.set_defaults(act=False) + + scan = subparsers.add_parser('scan', help='Scan Mode', description='Scan library for various different operational purposes.') + scan_lib = scan.add_argument_group('Library Options') + scan_lib.add_argument('-o', '--origin', help="Origin Directory for Library (overrides config)", metavar="DIR", type=str, dest="originDir") + scan_lib.add_argument('-t', '--target', help="Target Directory for Library (overrides config)", metavar="DIR", type=str, dest="targetDir") + scan_lib.add_argument('--format', help="Target directory library format", metavar="FORMAT", type=str, dest="targetFormat") + scan_lib.add_argument('-w', '--work', help="Working Directory for new processed files (overrides config)", metavar="DIR", type=str, dest="workDir") + scan_lib.add_argument('-e', '--exclude', help="Exclude Directory from origin (can be used multiple times)", metavar="DIR", dest='excludeDirs', action='append') + scan_subparsers = scan.add_subparsers(title='Scan Modes', dest='scanMode') + scan_untagged = scan_subparsers.add_parser('untagged', help='Find untagged media', description='Scans for untagged or insufficiently tagged media in the library.') + scan_new = scan_subparsers.add_parser('new', help='Find new unconverted media', description='Scans for new media that is not in the target library for conversion.') + + sync = subparsers.add_parser('sync', help='Sync Mode', description='Moves media from working DIR into target DIR') + sync_lib = sync.add_argument_group('Library Options') + sync_lib.add_argument('-o', '--origin', help="Origin Directory for Library (overrides config)", metavar="DIR", type=str, dest="originDir") + sync_lib.add_argument('-t', '--target', help="Target Directory for Library (overrides config)", metavar="DIR", type=str, dest="targetDir") + sync_lib.add_argument('-w', '--work', help="Working Directory for new processed files (overrides config)", metavar="DIR", type=str, dest="workingDir") + sync_lib.add_argument('-e', '--exclude', help="Exclude Directory from origin (can be used multiple times)", metavar="DIR", dest='excludeDirs', action='append') + sync_act = sync.add_argument_group('Action Options') + sync_act.add_argument('-g', '--go', help="Move media (default just what would be done)", dest="act", action='store_true') + + parser.set_defaults(verbose=0) + + return parser.parse_args() + +def load_config(): + import configparser + config = configparser.ConfigParser() + config.read('musicman.ini') + + try: + #print("Test1:", config.get('lossless', 'test')) + config.get('origin', 'path') + config.get('origin', 'format') + config.get('target', 'path') + config.get('target', 'format') + config.get('working', 'path') + except configparser.NoOptionError as err: + print("ERROR: Configuration of required settings are missing:", err) + sys.exit(1) + + return config diff --git a/musicman/utils/__init__.pyc b/musicman/utils/__init__.pyc new file mode 100644 index 0000000..0c58977 Binary files /dev/null and b/musicman/utils/__init__.pyc differ diff --git a/musicman/utils/__pycache__/__init__.cpython-33.pyc b/musicman/utils/__pycache__/__init__.cpython-33.pyc new file mode 100644 index 0000000..ba5ef8c Binary files /dev/null and b/musicman/utils/__pycache__/__init__.cpython-33.pyc differ diff --git a/musicman/utils/__pycache__/constants.cpython-33.pyc b/musicman/utils/__pycache__/constants.cpython-33.pyc new file mode 100644 index 0000000..853ec55 Binary files /dev/null and b/musicman/utils/__pycache__/constants.cpython-33.pyc differ diff --git a/musicman/utils/__pycache__/metadata.cpython-33.pyc b/musicman/utils/__pycache__/metadata.cpython-33.pyc new file mode 100644 index 0000000..de8e9f5 Binary files /dev/null and b/musicman/utils/__pycache__/metadata.cpython-33.pyc differ diff --git a/musicman/utils/__pycache__/tagmap.cpython-33.pyc b/musicman/utils/__pycache__/tagmap.cpython-33.pyc new file mode 100644 index 0000000..830f5b6 Binary files /dev/null and b/musicman/utils/__pycache__/tagmap.cpython-33.pyc differ diff --git a/musicman/utils/constants.py b/musicman/utils/constants.py new file mode 100644 index 0000000..cf696bc --- /dev/null +++ b/musicman/utils/constants.py @@ -0,0 +1,7 @@ +NO_TAGS = False +SILENT = False +VERSION = "0.0.1" +SUPPORTED_FORMATS = {'mp3', 'wma', 'wav', 'ogg', 'flac', 'm4a', 'mpc', 'wv', 'avi'} +EXTERNAL_FORMATS = {'mpc', 'wv'} +INTERNAL_FORMATS = SUPPORTED_FORMATS - EXTERNAL_FORMATS + diff --git a/musicman/utils/constants.pyc b/musicman/utils/constants.pyc new file mode 100644 index 0000000..52f0ba9 Binary files /dev/null and b/musicman/utils/constants.pyc differ diff --git a/musicman/utils/metadata.py b/musicman/utils/metadata.py new file mode 100644 index 0000000..ec2d985 --- /dev/null +++ b/musicman/utils/metadata.py @@ -0,0 +1,103 @@ +import os +import base64 + +try: + import mutagen + import mutagen.id3 + import mutagen.mp3 + import mutagen.mp4 + import mutagen.asf + import mutagen.flac + import mutagen.apev2 + import mutagen.musepack + import mutagen.oggvorbis + NO_TAGS = False +except ImportError: + NO_TAGS = True + +from musicman.utils.tagmap import tags as tagmap + +class MetaTag(object): + """ + handles tag extraction and insertion into and/or from audio files + """ + + __tag_mapping = tagmap.copy() + exts = __tag_mapping.keys() + + if not NO_TAGS: + __id3_mapping = { + 'artist' : mutagen.id3.TPE1, + 'album' : mutagen.id3.TALB, + 'title' : mutagen.id3.TIT2, + 'genre' : mutagen.id3.TCON, + 'year' : mutagen.id3.TDRC, + 'tracknumber' : mutagen.id3.TRCK, + 'totaltracks' : mutagen.id3.TRCK, + 'composer' : mutagen.id3.TCOM, + 'lyrics' : mutagen.id3.USLT, + 'disc' : mutagen.id3.TPOS, + 'discnumber' : mutagen.id3.TPOS, + } + __opener = { + '.mp3' : mutagen.mp3.Open, + '.wma' : mutagen.asf.Open, + '.m4a' : mutagen.mp4.Open, + '.flac' : mutagen.flac.Open, + '.wv' : mutagen.apev2.APEv2, + '.mpc' : mutagen.musepack.Open, + '.ogg' : mutagen.oggvorbis.Open, + } + else: + __id3_mapping = {} + __opener = {} + + def __init__(self, input_file): + self.input_file = input_file + self.tags = {key: None for key in self.__id3_mapping} + self.extract() + + def extract(self): + """ + extracts metadata tags from the audio file + """ + ext = os.path.splitext(self.input_file)[1].lower() + if ext in self.exts: + tags = mutagen.File(self.input_file) + #for tag, key in self.__tag_mapping[ext].iteritems(): + for tag, key in self.__tag_mapping[ext].items(): + if tag == 'albumart': + try: + self._extract_album_art(ext, tags) + except: + continue + elif key in tags: + #print "tag: %s, key: %s" % (tag, key) + self.tags[tag] = tags[key][0] + elif tag == 'lyrics' and key == 'USLT': + self.tags.update({tag: tags[id3tag].text for id3tag in tags if id3tag.startswith(key)}) + + def _extract_album_art(self, ext, tags): + tag = self.__tag_mapping[ext].get('albumart') + if tag is None: + return + if tag in tags: + self.coverart['ext'] = ext + if ext == '.mp3': + apic = tags[tag] + self.coverart['mime'] = apic.mime + self.coverart['data'] = apic.data + elif ext == '.m4a': + self.coverart['data'] = tags[tag][0] + elif ext in ('.ogg', '.flac'): + encoded_image = tags[tag][0] + image = mutagen.flac.Picture(base64.b64decode(encoded_image)) + self.coverart['data'] = image.data + self.coverart['mime'] = image.mime + elif ext == '.mp3': + for key in tags: + if key.startswith(tag): + apic = tags[key] + self.coverart['mime'] = apic.mime + self.coverart['data'] = apic.data + diff --git a/musicman/utils/metadata.pyc b/musicman/utils/metadata.pyc new file mode 100644 index 0000000..52d0822 Binary files /dev/null and b/musicman/utils/metadata.pyc differ diff --git a/musicman/utils/tagmap.py b/musicman/utils/tagmap.py new file mode 100644 index 0000000..2356d3b --- /dev/null +++ b/musicman/utils/tagmap.py @@ -0,0 +1,289 @@ +tags = { + '.mp3': { + 'album' : 'TALB', + 'albumsort' : 'TSOA', + 'title' : 'TIT2', + 'titlesort' : 'TSOT', + 'work' : 'TOAL', + 'artist' : 'TPE1', + 'artistsort' : 'TSOP', + 'albumartist' : 'TPE2', + 'albumartistsort' : 'TSO2', + 'date' : 'TDRC', + 'originaldate' : 'TDOR', + 'composer' : 'TCOM', + 'composersort' : 'TSOC', + 'lyricist' : 'TEXT', + 'writer' : 'TXXX:Writer', + 'conductor' : 'TPE3', + 'performer:instrument': 'TMCL:instrument', + 'remixer' : 'TPE4', + 'arranger' : 'TIPL:arranger', + 'engineer' : 'TIPL:engineer', + 'producer' : 'TIPL:producer', + 'djmixer' : 'TIPL:DJ-mix', + 'mixer' : 'TIPL:mix', + 'label' : 'TPUB', + 'grouping' : 'TIT1', + 'subtitle' : 'TIT3', + 'discsubtitle' : 'TSST', + 'tracknumber' : 'TRCK', + 'totaltracks' : 'TRCK', + 'discnumber' : 'TPOS', + 'totaldiscs' : 'TPOS', + 'compilation' : 'TCMP', + 'comment:description' : 'COMM:description', + 'genre' : 'TCON', + '_rating' : 'POPM', + 'bpm' : 'TBPM', + 'mood' : 'TMOO', + 'lyrics:description' : 'USLT:description', + 'media' : 'TMED', + 'catalognumber' : 'TXXX:CATALOGNUMBER', + 'releasestatus' : 'TXXX:MusicBrainz Album Status', + 'releasetype' : 'TXXX:MusicBrainz Album Type', + 'releasecountry' : 'TXXX:MusicBrainz Album Release Country', + 'script' : 'TXXX:SCRIPT', + 'language' : 'TLAN', + 'copyright' : 'TCOP', + 'license' : 'WCOP', + 'encodedby' : 'TENC', + 'encodersettings' : 'TSSE', + 'barcode' : 'TXXX:BARCODE', + 'isrc' : 'TSRC', + 'asin' : 'TXXX:ASIN', + 'albumart' : 'APIC:', + 'musicbrainz_recordingid' : 'UFID:http://musicbrainz.org', + 'musicbrainz_releasetrackid' : 'TXXX:MusicBrainz Release Track Id', + 'musicbrainz_albumid' : 'TXXX:MusicBrainz Album Id', + 'musicbrainz_artistid' : 'TXXX:MusicBrainz Artist Id', + 'musicbrainz_albumartistid' : 'TXXX:MusicBrainz Album Artist Id', + 'musicbrainz_releasegroupid' : 'TXXX:MusicBrainz Release Group Id', + 'musicbrainz_workid' : 'TXXX:MusicBrainz Work Id', + 'musicbrainz_trmid' : 'TXXX:MusicBrainz TRM Id', + 'musicbrainz_discid' : 'TXXX:MusicBrainz Disc Id', + 'acoustic_id' : 'TXXX:Acoustic Id', + 'acousticid_fingerprint' : 'TXXX:Acousticid Fingerprint', + 'musicip_puid' : 'TXXX:MusicIP PUID', + 'website' : 'WOAR', + }, + '.wma': { + 'artist' : 'Author', + 'album' : 'WM/AlbumTitle', + 'title' : 'Title', + 'genre' : 'WM/Genre', + 'year' : 'WM/Year', + 'tracknumber' : 'WM/TrackNumber', + 'composer' : 'WM/Composer', + 'publisher' : 'WM/Publisher', + 'lyrics' : 'WM/Lyrics', + 'albumartist' : 'WM/AlbumArtist', + }, + '.wmv': { + 'artist' : 'Author', + 'album' : 'WM/AlbumTitle', + 'title' : 'Title', + 'genre' : 'WM/Genre', + 'year' : 'WM/Year', + 'tracknumber' : 'WM/TrackNumber', + 'composer' : 'WM/Composer', + 'publisher' : 'WM/Publisher', + 'lyrics' : 'WM/Lyrics', + }, + '.m4a': { + 'album' : '\xa9alb', + 'albumsort' : 'soal', + 'title' : '\xa9nam', + 'titlesort' : 'sonm', + 'artist' : '\xa9ART', + 'artistsort' : 'soar', + 'albumartist' : 'aART', + 'albumartistsort' : 'soaa', + 'date' : '\xa9day', + 'composer' : '\xa9wrt', + 'composersort' : 'soco', + 'lyricist' : '----:com.apple.iTunes:LYRICIST', + 'conductor' : '----:com.apple.iTunes:CONDUCTOR', + 'remixer' : '----:com.apple.iTunes:REMIXER', + 'engineer' : '----:com.apple.iTunes:ENGINEER', + 'producer' : '----:com.apple.iTunes:PRODUCER', + 'djmixer' : '----:com.apple.iTunes:DJMIXER', + 'mixer' : '----:com.apple.iTunes:MIXER', + 'label' : '----:com.apple.iTunes:LABEL', + 'grouping' : '\xa9grp', + 'subtitle' : '----:com.apple.iTunes:SUBTITLE', + 'discsubtitle' : '----:com.apple.iTunes:DISCSUBTITLE', + 'tracknumber' : 'trkn', + 'totaltracks' : 'trkn', + 'discnumber' : 'disk', + 'totaldiscs' : 'disk', + #'compilation' : 'cpil', + 'comment' : '\xa9cmt', + 'genre' : '\xa9gen', + 'bpm' : 'tmpo', + 'mood' : '----:com.apple.iTunes:MOOD', + 'lyrics' : '\xa9lyr', + 'media' : '----:com.apple.iTunes:MEDIA', + 'catalognumber' : '----:com.apple.iTunes:CATALOGNUMBER', + 'show' : 'tvsh', + 'showsort' : 'sosn', + 'podcast' : 'pcst', + 'podcasturl' : 'purl', + 'releasestatus' : '----:com.apple.iTunes:MusicBrainz Album Status', + 'releasetype' : '----:com.apple.iTunes:MusicBrainz Album Type', + 'releasecountry' : '----:com.apple.iTunes:MusicBrainz Album Release Country', + 'script' : '----:com.apple.iTunes:SCRIPT', + 'language' : '----:com.apple.iTunes:LANGUAGE', + 'copyright' : 'cprt', + 'license' : '----:com.apple.iTunes:LICENSE', + 'encodedby' : '\xa9too', + 'barcode' : '----:com.apple.iTunes:BARCODE', + 'isrc' : '----:com.apple.iTunes:ISRC', + 'asin' : '----:com.apple.iTunes:ASIN', + 'albumart' : 'covr', + 'musicbrainz_recordingid' : '----:com.apple.iTunes:MusicBrainz Track Id', + 'musicbrainz_releasetrackid' : '----:com.apple.iTunes:MusicBrainz Release Track Id', + 'musicbrainz_albumid' : '----:com.apple.iTunes:MusicBrainz Album Id', + 'musicbrainz_artistid' : '----:com.apple.iTunes:MusicBrainz Artist Id', + 'musicbrainz_albumartistid' : '----:com.apple.iTunes:MusicBrainz Album Artist Id', + 'musicbrainz_releasegroupid' : '----:com.apple.iTunes:MusicBrainz Release Group Id', + 'musicbrainz_workid' : '----:com.apple.iTunes:MusicBrainz Work Id', + 'musicbrainz_trmid' : '----:com.apple.iTunes:MusicBrainz TRM Id', + 'musicbrainz_discid' : '----:com.apple.iTunes:MusicBrainz Disc Id', + 'acoustic_id' : '----:com.apple.iTunes:Acoustid Id', + 'acousticid_fingerprint' : '----:com.apple.iTunes:Acoustid Fingerprint', + 'musicip_puid' : '----:com.apple.iTunes:MusicIP PUID', + }, + '.mp4': { + 'artist' : '\xa9ART', + 'album' : '\xa9alb', + 'title' : '\xa9nam', + 'genre' : '\xa9gen', + 'year' : '\xa9day', + 'tracknumber' : 'trkn', + 'composer' : '\xa9wrt', + 'lyrics' : '\xa9lyr', + 'albumartist' : 'aART', + 'disk' : 'disk', + 'comment' : '\xa9cmt', + 'albumart' : 'covr', + }, + '.aac': { + 'artist' : '\xa9ART', + 'album' : '\xa9alb', + 'title' : '\xa9nam', + 'genre' : '\xa9gen', + 'year' : '\xa9day', + 'tracknumber' : 'trkn', + 'composer' : '\xa9wrt', + 'lyrics' : '\xa9lyr', + 'albumartist' : 'aART', + 'disk' : 'disk', + 'comment' : '\xa9cmt', + }, + '.ogg': { + 'artist' : 'artist', + 'album' : 'album', + 'title' : 'title', + 'genre' : 'genre', + 'year' : 'date', + 'tracknumber' : 'tracknumber', + 'composer' : 'composer', + 'albumart' : 'metadata_block_picture', + 'lyrics' : 'lyrics', + }, + '.flac': { + 'artist' : 'artist', + 'album' : 'album', + 'title' : 'title', + 'genre' : 'genre', + 'year' : 'date', + 'originaldate' : 'originaldate', + 'originalyear' : 'originalyear', + 'tracknumber' : 'tracknumber', + 'totaltracks' : 'totaltracks', + 'discnumber' : 'discnumber', + 'totaldiscs' : 'totaldiscs', + 'bpm' : 'bpm', + 'mood' : 'mood', + 'composer' : 'composer', + 'albumartist' : 'albumartist', + 'lyricist' : 'lyricist', + 'writer' : 'writer', + 'conductor' : 'conductor', + 'remixer' : 'remixer', + 'arranger' : 'arranger', + 'engineer' : 'engineer', + 'producer' : 'producer', + 'djmixer' : 'djmixer', + 'mixer' : 'mixer', + 'label' : 'label', + 'catalognumber' : 'catalognumber', + 'grouping' : 'grouping', + 'subtitle' : 'subtitle', + 'discsubtitle' : 'discsubtitle', + 'compilation' : 'compilation', + 'comment' : 'comment', + 'albumart' : 'metadata_block_picture', + 'lyrics' : 'lyrics', + 'albumsort' : 'albumsort', + 'titlesort' : 'titlesort', + 'artistsort' : 'artistsort', + 'albumartistsort' : 'albumartistsort', + 'composersort' : 'composersort', + 'work' : 'work', + 'releasestatus' : 'releasestatus', + 'releasetype' : 'releasetype', + 'releasecountry' : 'releasecountry', + 'script' : 'script', + 'language' : 'language', + 'copyright' : 'copyright', + 'license' : 'license', + 'barcode' : 'barcode', + 'isrc' : 'isrc', + 'asin' : 'asin', + 'musicbrainz_trackid' : 'musicbrainz_trackid', + 'musicbrainz_releasetrackid' : 'musicbrainz_releasetrackid', + 'musicbrainz_albumid' : 'musicbrainz_albumid', + 'musicbrainz_artistid' : 'musicbrainz_artistid', + 'musicbrainz_albumartistid' : 'musicbrainz_albumartistid', + 'musicbrainz_releasegroupid' : 'musicbrainz_releasegroupid', + 'musicbrainz_workid' : 'musicbrainz_workid', + 'musicbrainz_trmid' : 'musicbrainz_trmid', + 'musicbrainz_discid' : 'musicbrainz_discid', + 'acoustic_id' : 'acoustic_id', + 'acousticid_fingerprint' : 'acousticid_fingerprint', + 'musicip_puid' : 'musicp_puid', + }, + '.mpc': { + 'artist' : 'Artist', + 'album' : 'Album', + 'title' : 'Title', + 'genre' : 'Genre', + 'year' : 'Year', + 'tracknumber' : 'Track', + 'composer' : 'Composer', + }, + '.3gp': { + 'artist' : '\xa9ART', + 'album' : '\xa9alb', + 'title' : '\xa9nam', + 'genre' : '\xa9gen', + 'year' : '\xa9day', + 'tracknumber' : 'trkn', + 'composer' : '\xa9wrt', + 'lyrics' : '\xa9lyr', + 'albumartist' : 'aART', + 'disk' : 'disk', + 'comment' : '\xa9cmt', + 'albumart' : 'covr', + }, + '.wv': { + 'artist' : 'artist', + 'album' : 'album', + 'title' : 'title', + 'genre' : 'genre', + 'year' : 'year', + 'tracknumber' : 'track', + } + } diff --git a/musicman/utils/tagmap.pyc b/musicman/utils/tagmap.pyc new file mode 100644 index 0000000..7ab9de9 Binary files /dev/null and b/musicman/utils/tagmap.pyc differ