Skip to content

Instantly share code, notes, and snippets.

@Noctem
Forked from ThatDevopsGuy/flac2aac.py
Created June 27, 2013 05:37
Show Gist options
  • Save Noctem/5874205 to your computer and use it in GitHub Desktop.
Save Noctem/5874205 to your computer and use it in GitHub Desktop.
#!/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