-
-
Save Noctem/5874205 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
''' | |
=============================================================================== | |
______ ___ _____ ___ ___ ___ _____ | |
/ __/ / / _ |/ ___/ |_ | / _ | / _ |/ ___/ | |
/ _// /__/ __ / /__ / __/ / __ |/ __ / /__ | |
/_/ /____/_/ |_\___/ /____/ /_/ |_/_/ |_\___/ | |
FLAC 2 AAC | |
=============================================================================== | |
Script: flac2aac.py | |
Author: Sebastian Weigand www.sw-dd.com | |
Email: sab@sw-dd.com | |
Current: July, 2012 | |
Copyright: 2012, Sebastian Weigand | |
License: BSD | |
Description: A script which converts FLAC files to MPEG4/AAC files and | |
copies over song metadata, utilizing Apple's CoreAudio | |
framework for better AAC support. | |
Version: 1.2 | |
Requirements: | |
OS: Mac OS X, v10.5+ [afconvert] | |
Platform: Python 2.6+ [multiprocessing] | |
Binaries: flac [decoding] | |
Python Lib: mutagen [metadata] | |
=============================================================================== | |
''' | |
import os, sys | |
# ============================================================================= | |
# Sanity Checking: | |
# ============================================================================= | |
try: | |
import mutagen, fnmatch, argparse | |
from subprocess import call, Popen, PIPE | |
from multiprocessing import Pool, cpu_count | |
except ImportError, e: | |
print >> sys.stderr, 'Error: Unable to import requisite modules:', e | |
exit(1) | |
def isProgramValid(program): | |
paths = os.environ["PATH"].split(os.pathsep) | |
for path in paths: | |
if os.access(os.path.join(path, program), os.X_OK): | |
return True | |
return False | |
for program in ['flac', 'afconvert']: | |
if not isProgramValid(program): | |
print >> sys.stderr, 'Error: Unable to execute/find "%s" from your PATH.' % program | |
exit(1) | |
afconvertFormatHelp = Popen('afconvert -hf', shell=True, stderr=PIPE).communicate()[1] | |
afconvertFormats = [format for format in ['aac', 'aach', 'aacp'] if format in afconvertFormatHelp] | |
# ============================================================================= | |
# Library Methods: | |
# ============================================================================= | |
def getRealPath(path): | |
return os.path.realpath(os.path.expanduser(path)) | |
def getWorkingDirectory(): | |
# curdir does not support ~ expansion: | |
return getRealPath(os.curdir) | |
def findMatchingFiles(pattern, root = getWorkingDirectory()): | |
root = getRealPath(root) | |
if not os.path.isdir(root): | |
raise IOError('"' + root + '" is not a directory to search within.') | |
elif not os.access(root, os.R_OK): | |
raise IOError('"' + root + '" is not readable by "' + getpass.getuser() + '".') | |
results = [] | |
try: | |
for path, dirs, files in os.walk(getRealPath(root)): | |
for filename in fnmatch.filter(files, pattern): | |
if '/.' not in path: | |
results.append(os.path.join(path, filename)) | |
except (KeyboardInterrupt, EOFError): | |
print >> sys.stderr, 'Session cancelled via break or EOF.' | |
if len(results) == 0: | |
print >> sys.stderr, 'No files were found which matched the pattern "' + pattern + '" within "' + root + '", or its subdirectories.' | |
return [] | |
else: | |
return results | |
# ============================================================================= | |
# Argument Parsing: | |
# ============================================================================= | |
parser = argparse.ArgumentParser(description = 'Converts FLAC to MPEG4/AAC via CoreAudio and transfers metadata using Mutagen.', epilog = 'Note: Mac OS X v10.5+ is required for HE AAC (aach), and 10.7 is required for HE AAC v2 (aacp).') | |
parser.add_argument('location', metavar = 'location', type = str, nargs = 1, help = 'the location to search for media files [.]') | |
parser.add_argument('-q', '--quality', type = int, default = 75, help = 'VBR quality, in percent (overrides bitrate)') | |
parser.add_argument('-v', '--no-vbr', action = "store_true", default = False, help = 'disable variable bitrate [no]') | |
parser.add_argument('-b', '--bitrate', type = int, default = 256, help = 'bitrate, in KB/s [256]') | |
parser.add_argument('-c', '--codec', choices = afconvertFormats, default = 'aac', help = 'codec to use, if available on your platform [aac]') | |
args = parser.parse_args() | |
# Reset the arguments: | |
bitrate = args.bitrate * 1000 | |
quality = str(int(args.quality / 100.0 * 127)) | |
args.location = args.location[0] | |
# See `afconvert -h`: | |
if args.no_vbr: | |
vbrMode = 0 | |
else: | |
vbrMode = 3 | |
# ============================================================================= | |
# Transcoding Methods: | |
# ============================================================================= | |
def convertFLACtoAAC(mediaFileLocation): | |
try: | |
metadata = mutagen.File(mediaFileLocation, easy = True) | |
validKeys = ['title', 'album', 'artist', 'albumartist', 'date', 'comment', 'description', 'grouping', 'genre', 'copyright', 'albumsort', 'albumartistsort', 'artistsort', 'titlesort', 'composersort', 'tracknumber', 'discnumber'] | |
# Sometimes extraneous data will be stored within FLAC's metadata, and that | |
# confuses the direct key updating methods of the Easy-branch of Mutagen. | |
for key in metadata.keys(): | |
if key not in validKeys: | |
del metadata[key] | |
print 'Parsed:', mediaFileLocation | |
call(['flac', '-s', '-d', mediaFileLocation]) | |
print 'Decoded:', mediaFileLocation | |
intermediateFile = mediaFileLocation.replace(mediaFileLocation.split('.')[-1], 'wav') | |
call(['afconvert', '-f', 'm4af', '-d', args.codec, '-b', str(bitrate), '-s', str(vbrMode), '-u', 'vbrq', quality, '--soundcheck-generate', intermediateFile]) | |
print 'Encoded:', intermediateFile | |
finalFile = mediaFileLocation.replace(mediaFileLocation.split('.')[-1], 'm4a') | |
m4aData = mutagen.File(finalFile, easy = True) | |
m4aData.update(metadata) | |
m4aData.save() | |
print 'Applied metadata:', finalFile | |
os.remove(intermediateFile) | |
print 'Removed intermediate file:', intermediateFile | |
print 'Converted', mediaFileLocation, 'to:', finalFile | |
except: | |
exit(2) | |
# ============================================================================= | |
# Main: | |
# ============================================================================= | |
# The path to a directory (and its subdirectories) which contain media files: | |
mediaLocation = getRealPath(args.location) | |
print 'Searching:', mediaLocation | |
mediaFileLocations = findMatchingFiles('*.flac', mediaLocation) | |
print 'Found', len(mediaFileLocations), 'files.' | |
if len(mediaFileLocations) < 1: | |
print >> sys.stderr, 'Could not find any files.' | |
exit(1) | |
print '=' * 80 | |
# Multi-core goodness (N-simultaneous processes, where N = core count): | |
if __name__ == '__main__': | |
try: | |
pool = Pool(cpu_count()) | |
p = pool.map_async(convertFLACtoAAC, mediaFileLocations) | |
p.get(0xFFFF) # needed for KeyboardInterrupt | |
except: | |
exit(2) | |
print 'Done.\n' | |
#EOF |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment