/flactraclib/command.py

https://github.com/kylebittinger/flactrac
Python | 178 lines | 148 code | 22 blank | 8 comment | 25 complexity | 3c59aa3205820df218983feb76f8254d MD5 | raw file
  1. #!/usr/bin/env python
  2. import copy
  3. import fnmatch
  4. import mutagen.flac
  5. import mutagen.easyid3
  6. import optparse
  7. import os
  8. import shutil
  9. import subprocess
  10. import sys
  11. import tempfile
  12. def maybe_mkdir(dir_path):
  13. if not os.path.exists(dir_path):
  14. os.mkdir(dir_path)
  15. def match_ext(filename, ext):
  16. _, observed_ext = os.path.splitext(filename)
  17. lowercase_ext = observed_ext.lower()
  18. return lowercase_ext == ext
  19. def replace_ext(filename, new_ext):
  20. basename, _ = os.path.splitext(filename)
  21. return basename + new_ext
  22. class Converter(object):
  23. def __init__(self, export_dir, bitrate):
  24. self.export_dir = export_dir
  25. self.bitrate = bitrate
  26. def convert_directory(self, input_dir):
  27. output_dir = self.init_output_dir(input_dir)
  28. flac_fps = self.get_track_filepaths(input_dir, ".flac")
  29. if flac_fps:
  30. sys.stderr.write("Converting FLAC files in %s\n" % input_dir)
  31. temp_dir = tempfile.mkdtemp()
  32. for flac_fp in flac_fps:
  33. wav_fp = self.flac_to_wav(flac_fp, temp_dir)
  34. converted_fp = self.get_converted_fp(wav_fp, output_dir)
  35. self.convert_wav(wav_fp, converted_fp)
  36. tags = self.get_flac_tags(flac_fp)
  37. self.set_converted_tags(converted_fp, tags)
  38. shutil.rmtree(temp_dir)
  39. return None
  40. else:
  41. wav_fps = self.get_track_filepaths(input_dir, ".wav")
  42. if wav_fps:
  43. sys.stderr.write("Converting WAV files in %s\n" % input_dir)
  44. for wav_fp in wav_fps:
  45. converted_fp = self.get_converted_fp(wav_fp, output_dir)
  46. self.convert_wav(wav_fp, converted_fp)
  47. return None
  48. sys.stderr.write("No FLAC or WAV files found in %s\n" % input_dir)
  49. def get_flac_tags(self, flac_fp):
  50. f = mutagen.flac.FLAC(flac_fp)
  51. tags = {}
  52. for key, val in f.tags:
  53. # standardize flac tags to all lowercase
  54. key = key.lower()
  55. tags[key] = val
  56. return tags
  57. def get_track_filepaths(self, input_dir, ext="flac"):
  58. track_filepaths = []
  59. for fname in os.listdir(input_dir):
  60. fpath = os.path.join(input_dir, fname)
  61. if os.path.isfile(fpath) and match_ext(fname, ext):
  62. track_filepaths.append(fpath)
  63. track_filepaths.sort()
  64. return track_filepaths
  65. def flac_to_wav(self, flac_filepath, output_dir):
  66. wav_filename = replace_ext(os.path.basename(flac_filepath), '.wav')
  67. wav_filepath = os.path.join(output_dir, wav_filename)
  68. args = ['flac', '-d', flac_filepath, '-o', wav_filepath]
  69. subprocess.check_call(args)
  70. return wav_filepath
  71. def init_output_dir(self, input_dir):
  72. """Initialize subdirectory structure in output_dir
  73. Creates a new directory in output_dir with the same name as
  74. the input directory.
  75. """
  76. input_dirname = os.path.basename(input_dir)
  77. output_dir = os.path.join(self.export_dir, input_dirname)
  78. maybe_mkdir(output_dir)
  79. return output_dir
  80. def get_converted_fp(self, wav_filepath, output_dir):
  81. converted_filename = replace_ext(
  82. os.path.basename(wav_filepath), self.output_ext)
  83. return os.path.join(output_dir, converted_filename)
  84. class Mp3Converter(Converter):
  85. output_ext = '.mp3'
  86. def format_tracknumber_str(self, tags):
  87. n = tags['tracknumber']
  88. total = tags.get('tracktotal')
  89. if total:
  90. return '%s/%s' % (n, total)
  91. else:
  92. return '%s' % n
  93. def format_discnumber_str(self, tags):
  94. n = tags['discnumber']
  95. total = tags.get('disctotal')
  96. if total:
  97. return '%s/%s' % (n, total)
  98. else:
  99. return '%s' % n
  100. def set_converted_tags(self, converted_fp, tags):
  101. tags = copy.deepcopy(tags)
  102. f = mutagen.easyid3.EasyID3(converted_fp)
  103. id3_standard_tags = [
  104. 'album', 'compilation', 'title', 'artist', 'date', 'genre',
  105. ]
  106. for tagname in id3_standard_tags:
  107. if tagname in tags:
  108. f[tagname] = tags[tagname]
  109. if 'tracknumber' in tags:
  110. f['tracknumber'] = self.format_tracknumber_str(tags)
  111. if 'discnumber' in tags:
  112. f['discnumber'] = self.format_discnumber_str(tags)
  113. # TODO: Fix multiple-choice genre tag
  114. f.save()
  115. def convert_wav(self, wav_filepath, converted_filepath):
  116. if self.bitrate.startswith("v") or self.bitrate.startswith("V"):
  117. variable_bitrate_num = self.bitrate[1:]
  118. bitrate_args = ['-V', variable_bitrate_num]
  119. else:
  120. bitrate_args = ['-b', self.bitrate]
  121. args = ['lame', '--add-id3v2'] + bitrate_args + \
  122. [wav_filepath, converted_filepath]
  123. retcode = subprocess.check_call(args)
  124. return True
  125. class FlacTracApp(object):
  126. converter_classes = {
  127. 'mp3': Mp3Converter,
  128. }
  129. def __init__(self, args=None):
  130. parser = self._build_parser()
  131. opts, args = parser.parse_args(args)
  132. self.flac_dirs = [os.path.realpath(d) for d in args]
  133. export_dir = os.path.expanduser(opts.export_dir)
  134. maybe_mkdir(export_dir)
  135. try:
  136. converter_class = self.converter_classes[opts.format]
  137. except KeyError:
  138. parser.error('Unknown output format: %s' % opts.format)
  139. self.converter = converter_class(export_dir, opts.bitrate)
  140. def _build_parser(self):
  141. p = optparse.OptionParser(usage='%prog [options] flac_dir')
  142. p.add_option('-f', '--format', default='mp3',
  143. help='output file format. Choices: ' + \
  144. ', '.join(self.converter_classes.keys()) + \
  145. '. [default: %default]')
  146. p.add_option('-b', '--bitrate', default="320",
  147. help='bitrate of output files [default: 320 kbps]')
  148. p.add_option('-o', '--export_dir', default='~/Desktop/Export',
  149. help='export directory [default: %default]')
  150. return p
  151. def run(self):
  152. for flac_dir in self.flac_dirs:
  153. self.converter.convert_directory(flac_dir)
  154. def main(argv=None):
  155. FlacTracApp(argv).run()