Almost finished. Cleanup and some functionality left to do.

This commit is contained in:
Eric Renfro 2016-07-04 18:50:52 -04:00
parent a0e50ceaf7
commit b1c05c352f
5 changed files with 268 additions and 70 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.pyc
*.pyo

View file

@ -10,6 +10,7 @@ import time
#from path import path
import musicman
import mutagen
#from musicman.utils.constants import (
# VERSION,
@ -101,13 +102,21 @@ def getEmptyDirs(path, excludeDirs=[]):
if not directories and not filenames:
yield root
def getSong(file):
def getSong(file, orgDir=None, tmpDir=None, dstDir=None):
from musicman.utils.metadata import MetaTag
from musicman.utils.constants import INTERNAL_FORMATS
global config
song = {}
global originDir
global workingDir
global targetDir
global targetFormat
song = {}
orgDir = originDir if orgDir is None else orgDir
tmpDir = workingDir if tmpDir is None else tmpDir
dstDir = targetDir if dstDir is None else dstDir
#if file.endswith(INTERNAL_FORMATS):
if (os.path.splitext(file)[1][1:] in INTERNAL_FORMATS):
metadata = MetaTag(file)
@ -150,6 +159,12 @@ def getSong(file):
song['outPath'] = os.path.join(song['artistName'], song['albumName'])
outFile = '{0}'.format(song['titleName'])
song['originFile'] = os.path.join(orgDir, song['outPath'], song['outFile'])
song['workingFile'] = os.path.join(tmpDir, song['outPath'], song['outFile'])
song['targetFile'] = os.path.join(dstDir, song['outPath'], song['outFile'])
song['originExt'] = os.path.splitext(file)[1]
song['targetExt'] = '.' + targetFormat
return song
else:
clearLine()
@ -157,31 +172,20 @@ def getSong(file):
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):
def cleanLibrary(rootDir, excludeDirs=[], act=False, verbose=0):
global config
if excludeDirs is None:
excludeDirs=[]
print("Clean Dirs")
print("Clean Dirs:", rootDir)
#while getEmptyDirs(originDir, excludeDirs) != [] and act == True and tries > 0:
#while getEmptyDirs(rootDir, excludeDirs) != [] and act == True and tries > 0:
if act:
try:
while getEmptyDirs(originDir, excludeDirs).__next__():
for path in getEmptyDirs(originDir, excludeDirs):
while getEmptyDirs(rootDir, excludeDirs).__next__():
for path in getEmptyDirs(rootDir, excludeDirs):
print("Removing:", path)
if act:
@ -193,64 +197,56 @@ def cleanLibrary(originDir, excludeDirs=[], act=False, verbose=0):
except StopIteration:
pass
else:
for path in getEmptyDirs(originDir, excludeDirs):
for path in getEmptyDirs(rootDir, excludeDirs):
print("Empty:", path)
print("Processing Complete!")
#print("Processing Complete!")
def renameLibrary(originDir, excludeDirs=[], act=False, verbose=0):
def renameLibrary(rootDir, excludeDirs=[], act=False, verbose=0):
global config
if excludeDirs is None:
excludeDirs=[]
print("Rename")
for file in getLibrary(originDir, excludeDirs):
#print("File:", file)
print("Renaming:", rootDir)
for file in getLibrary(rootDir, 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)
song = getSong(file, rootDir)
#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)):
if not os.path.isfile(song['originFile'] + song['originExt']):
clearLine()
print("Found:", file)
if verbose > 0:
print(" New:", os.path.join(originDir, song['outPath'], song['outFile'] + file_ext))
print(" New:", song['originFile'] + song['originExt'])
if (act):
if verbose > 1:
print("Renaming: \"{0}\" -> \"{1}\"".format(file, os.path.join(originDir, song['outPath'], song['outFile'] + file_ext)))
print("Renaming: \"{0}\" -> \"{1}\"".format(file, song['originFile'] + song['originExt']))
try:
os.renames(file,
os.path.join(originDir,
song['outPath'],
song['outFile'] + file_ext))
os.renames(file, song['originFile'] + song['originExt'])
except OSError as err:
print("ERROR: Failed to move:", err)
sys.exit(5)
clearLine()
print("Processing Complete!")
#print("Processing Complete!")
def findUntagged(originDir, excludeDirs=[], verbose=0):
def findUntagged(rootDir, excludeDirs=[], verbose=0):
global config
if excludeDirs is None:
excludeDirs=[]
print("Find Untagged")
for file in getLibrary(originDir, excludeDirs):
print("Find Untagged:", rootDir)
for file in getLibrary(rootDir, 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")
@ -260,49 +256,43 @@ def findUntagged(originDir, excludeDirs=[], verbose=0):
if song['metadata'] is None:
clearLine()
print("Untagged: {0}".format(file))
clearLine()
print("Processing Complete!")
def findNew(originDir, workingDir, targetDir, targetFormat, excludeDirs=[], verbose=0):
clearLine()
#print("Processing Complete!")
def findNew(rootDir, tmpDir, dstDir, dstFormat, excludeDirs=[], verbose=0):
global config
if excludeDirs is None:
excludeDirs=[]
print("Find New Media")
for file in getLibrary(originDir, excludeDirs):
for file in getLibrary(rootDir, 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']))):
if not (os.path.isfile(song['workingFile'] + song['targetExt']) or
os.path.isfile(song['targetFile'] + song['targetExt'])):
clearLine()
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)
if not os.path.isfile(os.path.isfile(song['workingFile'] + song['targetExt'])):
print(" No:", song['workingFile'] + song['targetExt'])
elif not os.path.isfile(os.path.isfile(song['targetFile'] + song['targetExt'])):
print(" No:", song['targetFile'] + song['targetExt'])
clearLine()
print("Processing Complete!")
#clearLine()
#print("Processing Complete!")
def syncWorking(workingDir, targetDir, excludeDirs=[], act=False, verbose=0):
def syncWorking(tmpDir, excludeDirs=[], act=False, verbose=0):
global config
if excludeDirs is None:
@ -310,10 +300,9 @@ def syncWorking(workingDir, targetDir, excludeDirs=[], act=False, verbose=0):
print("Sync Target Media")
for file in getLibrary(workingDir, excludeDirs):
for file in getLibrary(tmpDir, 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)
@ -326,17 +315,84 @@ def syncWorking(workingDir, targetDir, excludeDirs=[], act=False, verbose=0):
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)):
if not os.path.isfile(song['targetDir'] + song['originExt']):
print("Sync:", file)
if verbose > 0:
print(" To:", os.path.join(targetDir, song['outPath'], song['outFile'] + file_ext))
print(" To:", song['targetFile'], song['originExt'])
if act:
print("would've acted")
clearLine()
print("Processing Complete!")
#if not os.path.isfile(os.path.join(destDir, song['outPath'], song['outFile'] + file_ext)):
# print("Sync:", file)
# if verbose > 0:
# print(" To:", os.path.join(destDir, song['outPath'], song['outFile'] + file_ext))
#
# if act:
# print("would've acted")
#clearLine()
#print("Processing Complete!")
def convertMedia(rootDir, tmpDir, dstDir, dstFormat, excludeDirs=[], act=False, verbose=0):
import subprocess
from musicman.utils.copytags import (
copy_tags
)
if excludeDirs is None:
excludeDirs=[]
print("Convert Library Media")
for file in getLibrary(rootDir, 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, rootDir, tmpDir, targetDir)
if song['metadata'] is None:
if verbose > 2:
clearLine()
print("Skipping: {0} due to lack of metadata".format(file))
else:
if not (os.path.isfile(song['workingFile'] + song['targetExt']) or
os.path.isfile(song['targetFile'] + song['targetExt'])):
clearLine()
print("New:", file)
if verbose > 2:
if not os.path.isfile(os.path.isfile(song['workingFile'] + song['targetExt'])):
print(" No:", song['workingFile'] + song['targetExt'])
elif not os.path.isfile(os.path.isfile(song['targetFile'] + song['targetExt'])):
print(" No:", song['targetFile'] + song['targetExt'])
if act:
#print("would've acted")
if not os.path.isdir(os.path.dirname(song['workingFile'])):
os.makedirs(os.path.dirname(song['workingFile']))
#subprocess.call("ffmpeg", "-loglevel", "quiet", "-stats", "-i", file, "-vn", "-c:a", "libfdk_aac", "-vbr", "5", "-nostdin", "-y", song['workingFile'] + song['targetExt'])
try:
subprocess.call('/usr/bin/ffmpeg -loglevel quiet -stats -i "{0}" -vn -c:a libfdk_aac -vbr 5 -nostdin -y "{1}"'.format(file, song['workingFile'] + song['targetExt']), shell=True)
#new = MetaData(song['workingFile'] + song['targetExt'])
#new.update(song['metadata'])
#new.save()
#old = mutagen.File(file)
#new = mutagen.File(song['workingFile'] + song['targetExt'])
#new.update(old)
#new.save()
copy_tags(file, song['workingFile'] + song['targetExt'])
except KeyboardInterrupt:
if os.path.isfile(song['workingFile'] + song['targetExt']):
os.remove(song['workingFile'] + song['targetExt'])
clearLine()
print(end='\r')
print("Aborted by user")
sys.exit(0)
clearLine()
if __name__ == '__main__':
global opt
@ -377,9 +433,13 @@ if __name__ == '__main__':
try:
if opt.mode == 'clean':
cleanLibrary(originDir, opt.excludeDirs, opt.act, opt.verbose)
cleanLibrary(targetDir, opt.excludeDirs, opt.act, opt.verbose)
cleanLibrary(workingDir, opt.excludeDirs, opt.act, opt.verbose)
elif opt.mode == 'rename':
renameLibrary(originDir, opt.excludeDirs, opt.act, opt.verbose)
renameLibrary(targetDir, opt.excludeDirs, opt.act, opt.verbose)
renameLibrary(workingDir, opt.excludeDirs, opt.act, opt.verbose)
elif opt.mode == 'scan':
if opt.scanMode is None:
@ -387,15 +447,23 @@ if __name__ == '__main__':
sys.exit(1)
elif opt.scanMode == 'untagged':
findUntagged(originDir, opt.excludeDirs, opt.verbose)
findUntagged(targetDir, opt.excludeDirs, opt.verbose)
findUntagged(workingDir, 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)
elif opt.mode == 'convert':
convertMedia(originDir, workingDir, targetDir, targetFormat, opt.excludeDirs, opt.act, opt.verbose)
except KeyboardInterrupt:
clearLine()
print(end='\r')
print("Aborted by user")
sys.exit(0)
clearLine()
print("Processing Complete!")
#for file in getLibrary(config['origin']['path']):

View file

@ -23,6 +23,8 @@ def parse_args():
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('-t', '--target', help="Target Directory for Library (overrides config)", metavar="DIR", type=str, dest="targetDir")
clean_lib.add_argument('-w', '--work', help="Working Directory for new processed files (overrides config)", metavar="DIR", type=str, dest="workingDir")
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')
@ -32,8 +34,8 @@ def parse_args():
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('-w', '--work', help="Working Directory for new processed files (overrides config)", metavar="DIR", type=str, dest="workingDir")
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')
@ -54,8 +56,8 @@ def parse_args():
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('-w', '--work', help="Working Directory for new processed files (overrides config)", metavar="DIR", type=str, dest="workingDir")
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.')
@ -63,7 +65,7 @@ def parse_args():
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('-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')

125
musicman/utils/copytags.py Normal file
View file

@ -0,0 +1,125 @@
import os
import os.path
import sys
import re
#import logging
from mutagen import File as MusicFile
from mutagen.aac import AACError
try:
from collections import MutableMapping
except ImportError:
from UserDict import DictMixin as MutableMapping
try:
from tqdm import tqdm
except ImportError:
def tqdm(iterable, *args, **kwargs):
return iterable
# Set up logging
#logFormatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
#logger = logging.getLogger(__name__)
#logger.setLevel(logging.INFO)
#logger.handlers = []
#logger.addHandler(logging.StreamHandler())
#for handler in logger.handlers:
# handler.setFormatter(logFormatter)
class AudioFile(MutableMapping):
"""A simple class just for tag editing.
No internal mutagen tags are exposed, or filenames or anything. So
calling clear() won't destroy the filename field or things like
that. Use it like a dict, then .write() it to commit the changes.
When saving, tags that cannot be saved by the file format will be
skipped with a debug message, since this is a common occurrance
with MP3/M4A.
Optional argument blacklist is a list of regexps matching
non-transferrable tags. They will effectively be hidden, nether
settable nor gettable.
Or grab the actual underlying mutagen format object from the
.data field and get your hands dirty.
"""
def __init__(self, filename, blacklist=[], easy=True):
self.filename = filename
self.data = MusicFile(self.filename, easy=easy)
if self.data is None:
raise ValueError("Unable to identify %s as a music file" % (repr(filename)))
# Also exclude mutagen's internal tags
self.blacklist = [ re.compile("^~") ] + blacklist
def __getitem__(self, item):
if self.blacklisted(item):
#logger.debug("Attempted to get blacklisted key: %s." % repr(item))
return
else:
return self.data.__getitem__(item)
def __setitem__(self, item, value):
if self.blacklisted(item):
#logger.debug("Attempted to set blacklisted key: %s." % repr(item))
#print("DEBUG: Attempted to set blacklisted key: %s." % repr(item))
return
else:
try:
return self.data.__setitem__(item, value)
except KeyError:
#print("Skipping unsupported tag {0} for file type {0}".format(item, type(self.data)))
pass
#logger.debug("Skipping unsupported tag %s for file type %s",
# item, type(self.data))
def __delitem__(self, item):
if self.blacklisted(item):
#logger.debug("Attempted to del blacklisted key: %s." % repr(item))
return
else:
return self.data.__delitem__(item)
def __len__(self):
return len(self.keys())
def __iter__(self):
return iter(self.keys())
def blacklisted(self, item):
"""Return True if tag is blacklisted.
Blacklist automatically includes internal mutagen tags (those
beginning with a tilde)."""
for regex in self.blacklist:
if re.search(regex, item):
return True
else:
return False
def keys(self):
return [ key for key in self.data.keys() if not self.blacklisted(key) ]
def write(self):
return self.data.save()
def copy_tags (src, dest):
"""Replace tags of dest file with those of src.
Excludes format-specific tags and replaygain info, which does not
carry across formats."""
# A list of regexps matching non-transferrable tags, like file format
# info and replaygain info. This will not be transferred from source,
# nor deleted from destination.
blacklist_regexes = [ re.compile(s) for s in (
'encoded',
'replaygain',
) ]
try:
m_src = AudioFile(src, blacklist = blacklist_regexes, easy=True)
m_dest = AudioFile(dest, blacklist = m_src.blacklist, easy=True)
m_dest.clear()
#logging.debug("Adding tags from source file:\n%s",
# "\n".join("%s: %s" % (k, repr(m_src[k])) for k in sorted(m_src.keys())))
m_dest.update(m_src)
#logger.debug("Added tags to dest file:\n%s",
# "\n".join("%s: %s" % (k, repr(m_dest[k])) for k in sorted(m_dest.keys())))
m_dest.write()
except AACError:
#logger.warn("No tags copied because output format does not support tags: %s", repr(type(m_dest.data)))
print("WARN: no tags copied because output format does not support tags: {0}".format(repr(type(m_dest.data))))