musicman/musicman.py

466 lines
15 KiB
Python
Executable file

#!/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
import mutagen
#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, orgDir=None, tmpDir=None, dstDir=None):
from musicman.utils.metadata import MetaTag
from musicman.utils.constants import INTERNAL_FORMATS
global config
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 (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'])
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()
print("FATAL: File extension \"{0}\" is not supported.".format(os.path.splitext(file)[1]))
print(" Supported:", ", ".join(INTERNAL_FORMATS))
sys.exit(2)
def cleanLibrary(rootDir, excludeDirs=[], act=False, verbose=0):
global config
if excludeDirs is None:
excludeDirs=[]
print("Clean Dirs:", rootDir)
#while getEmptyDirs(rootDir, excludeDirs) != [] and act == True and tries > 0:
if act:
try:
while getEmptyDirs(rootDir, excludeDirs).__next__():
for path in getEmptyDirs(rootDir, 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(rootDir, excludeDirs):
print("Empty:", path)
#print("Processing Complete!")
def renameLibrary(rootDir, excludeDirs=[], act=False, verbose=0):
global config
if excludeDirs is None:
excludeDirs=[]
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, rootDir)
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['originFile'] + song['originExt']):
clearLine()
print("Found:", file)
if verbose > 0:
print(" New:", song['originFile'] + song['originExt'])
if (act):
if verbose > 1:
print("Renaming: \"{0}\" -> \"{1}\"".format(file, song['originFile'] + song['originExt']))
try:
os.renames(file, song['originFile'] + song['originExt'])
except OSError as err:
print("ERROR: Failed to move:", err)
sys.exit(5)
clearLine()
#print("Processing Complete!")
def findUntagged(rootDir, excludeDirs=[], verbose=0):
global config
if excludeDirs is None:
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")
song = getSong(file)
if song['metadata'] is None:
clearLine()
print("Untagged: {0}".format(file))
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(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)
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 > 0:
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!")
def syncWorking(tmpDir, excludeDirs=[], act=False, verbose=0):
global config
if excludeDirs is None:
excludeDirs=[]
print("Sync Target Media")
for file in getLibrary(tmpDir, 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:
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(song['targetDir'] + song['originExt']):
print("Sync:", file)
if verbose > 0:
print(" To:", song['targetFile'], song['originExt'])
if act:
print("would've acted")
#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
global config
global originPath, targetPath, targetFormat, workPath
import configparser
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)
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:
print("ERROR: Subcommand for scan not provided.")
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!")