From 96ab547c5c33c5b59d8f3848f7df5f6b8608467e Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Mon, 2 Apr 2018 00:47:31 +0530 Subject: [PATCH] Support FLAC output format (#259) * Convert to .flac option * Embed metadata to FLAC * Update usage help * Write tests --- README.md | 16 +++--- core/const.py | 29 +++++++++- core/convert.py | 25 +++++---- core/handle.py | 5 +- core/metadata.py | 107 +++++++++++++++++++------------------ spotdl.py | 2 +- test/loader.py | 2 +- test/test_with_metadata.py | 16 +++++- 8 files changed, 123 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index f34d293..07deaff 100755 --- a/README.md +++ b/README.md @@ -90,9 +90,9 @@ but make sure `$ python -V` gives you a `Python 3.x.x`! ``` usage: spotdl.py [-h] (-s SONG | -l LIST | -p PLAYLIST | -b ALBUM | -u USERNAME) - [-m] [-nm] [-a] [-f FOLDER] [--overwrite {force,prompt,skip}] - [-i INPUT_EXT] [-o OUTPUT_EXT] [-ff] [-dm] [-d] [-mo] [-ns] - [-ll {INFO,WARNING,ERROR,DEBUG}] + [-m] [-nm] [-a] [-f FOLDER] [--overwrite {skip,force,prompt}] + [-i {.webm,.m4a}] [-o OUTPUT_EXT] [-ff] [-dm] [-d] [-mo] + [-ns] [-ll {INFO,WARNING,ERROR,DEBUG}] [-c CONFIG] Download and convert songs from Spotify, Youtube etc. @@ -116,14 +116,14 @@ optional arguments: -f FOLDER, --folder FOLDER path to folder where files will be stored in (default: Music) - --overwrite {force,prompt,skip} + --overwrite {skip,force,prompt} change the overwrite policy (default: prompt) - -i INPUT_EXT, --input-ext INPUT_EXT + -i {.webm,.m4a}, --input-ext {.webm,.m4a} prefered input format .m4a or .webm (Opus) (default: .m4a) -o OUTPUT_EXT, --output-ext OUTPUT_EXT - prefered output extension .mp3 or .m4a (AAC) (default: - .mp3) + prefered output format .mp3, .m4a (AAC), .flac, etc. + (default: .mp3) -ff, --file-format File format to save the downloaded song with, each tag is surrounded by curly braces. Possible formats: ['track_name', 'artist', 'album', 'album_artist', @@ -140,7 +140,7 @@ optional arguments: (default: False) -ll {INFO,WARNING,ERROR,DEBUG}, --log-level {INFO,WARNING,ERROR,DEBUG} set log verbosity (default: INFO) - -c CONFIG_FILE_PATH --config CONFIG_FILE_PATH + -c CONFIG, --config CONFIG Replace with custom config.yml file (default: None) ``` diff --git a/core/const.py b/core/const.py index 03f7f69..b895764 100644 --- a/core/const.py +++ b/core/const.py @@ -1,8 +1,33 @@ import logzero _log_format = ("%(color)s%(levelname)s:%(end_color)s %(message)s") -formatter = logzero.LogFormatter(fmt=_log_format) +_formatter = logzero.LogFormatter(fmt=_log_format) # options -log = logzero.setup_logger(formatter=formatter) +log = logzero.setup_logger(formatter=_formatter) args = None + +# Apple has specific tags - see mutagen docs - +# http://mutagen.readthedocs.io/en/latest/api/mp4.html +M4A_TAG_PRESET = { 'album' : '\xa9alb', + 'artist' : '\xa9ART', + 'date' : '\xa9day', + 'title' : '\xa9nam', + 'year' : '\xa9day', + 'originaldate' : 'purd', + 'comment' : '\xa9cmt', + 'group' : '\xa9grp', + 'writer' : '\xa9wrt', + 'genre' : '\xa9gen', + 'tracknumber' : 'trkn', + 'albumartist' : 'aART', + 'discnumber' : 'disk', + 'cpil' : 'cpil', + 'albumart' : 'covr', + 'copyright' : 'cprt', + 'tempo' : 'tmpo', + 'lyrics' : '\xa9lyr' } + +TAG_PRESET = {} +for key in M4A_TAG_PRESET.keys(): + TAG_PRESET[key] = key diff --git a/core/convert.py b/core/convert.py index cb18d2f..1e56c64 100644 --- a/core/convert.py +++ b/core/convert.py @@ -17,16 +17,16 @@ https://trac.ffmpeg.org/wiki/Encode/AAC def song(input_song, output_song, folder, avconv=False): """ Do the audio format conversion. """ - if not input_song == output_song: - convert = Converter(input_song, output_song, folder) - log.info('Converting {0} to {1}'.format( - input_song, output_song.split('.')[-1])) - if avconv: - exit_code = convert.with_avconv() - else: - exit_code = convert.with_ffmpeg() - return exit_code - return 0 + if input_song == output_song: + return 0 + convert = Converter(input_song, output_song, folder) + log.info('Converting {0} to {1}'.format( + input_song, output_song.split('.')[-1])) + if avconv: + exit_code = convert.with_avconv() + else: + exit_code = convert.with_ffmpeg() + return exit_code class Converter: @@ -56,6 +56,8 @@ class Converter: _, input_ext = os.path.splitext(self.input_file) _, output_ext = os.path.splitext(self.output_file) + ffmpeg_params = '' + if input_ext == '.m4a': if output_ext == '.mp3': ffmpeg_params = '-codec:v copy -codec:a libmp3lame -ar 44100 ' @@ -68,6 +70,9 @@ class Converter: elif output_ext == '.m4a': ffmpeg_params = '-cutoff 20000 -codec:a libfdk_aac -ar 44100 ' + if output_ext == '.flac': + ffmpeg_params = '-codec:a flac -ar 44100 ' + # add common params for any of the above combination ffmpeg_params += '-b:a 192k -vn ' ffmpeg_pre += ' -i' diff --git a/core/handle.py b/core/handle.py index 19e1bec..eb550f6 100644 --- a/core/handle.py +++ b/core/handle.py @@ -122,10 +122,11 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True): choices={'prompt', 'force', 'skip'}) parser.add_argument( '-i', '--input-ext', default=config['input-ext'], - help='prefered input format .m4a or .webm (Opus)') + help='prefered input format .m4a or .webm (Opus)', + choices={'.m4a', '.webm'}) parser.add_argument( '-o', '--output-ext', default=config['output-ext'], - help='prefered output extension .mp3 or .m4a (AAC)') + help='prefered output format .mp3, .m4a (AAC), .flac, etc.') parser.add_argument( '-ff', '--file-format', default=config['file-format'], help='File format to save the downloaded song with, each tag ' diff --git a/core/metadata.py b/core/metadata.py index 75a748d..eeda320 100755 --- a/core/metadata.py +++ b/core/metadata.py @@ -1,7 +1,8 @@ from mutagen.easyid3 import EasyID3 from mutagen.id3 import ID3, TORY, TYER, TPUB, APIC, USLT, COMM from mutagen.mp4 import MP4, MP4Cover -from core.const import log +from mutagen.flac import Picture, FLAC +from core.const import log, TAG_PRESET, M4A_TAG_PRESET import urllib.request @@ -31,6 +32,9 @@ def embed(music_file, meta_tags): elif music_file.endswith('.mp3'): log.info('Applying metadata') return embed.as_mp3() + elif music_file.endswith('.flac'): + log.info('Applying metadata') + return embed.as_flac() else: log.warning('Cannot embed metadata into given output extension') return False @@ -50,15 +54,7 @@ class EmbedMetadata: # https://github.com/quodlibet/mutagen/blob/master/mutagen/easyid3.py # Check out somewhere at end of above linked file audiofile = EasyID3(music_file) - audiofile['artist'] = meta_tags['artists'][0]['name'] - audiofile['albumartist'] = meta_tags['artists'][0]['name'] - audiofile['album'] = meta_tags['album']['name'] - audiofile['title'] = meta_tags['name'] - audiofile['tracknumber'] = [meta_tags['track_number'], - meta_tags['total_tracks']] - audiofile['discnumber'] = [meta_tags['disc_number'], 0] - audiofile['date'] = meta_tags['release_date'] - audiofile['originaldate'] = meta_tags['release_date'] + self._embed_basic_metadata(audiofile, preset=TAG_PRESET) audiofile['media'] = meta_tags['type'] audiofile['author'] = meta_tags['artists'][0]['name'] audiofile['lyricist'] = meta_tags['artists'][0]['name'] @@ -68,10 +64,6 @@ class EmbedMetadata: audiofile['length'] = str(meta_tags['duration']) if meta_tags['publisher']: audiofile['encodedby'] = meta_tags['publisher'] - if meta_tags['genre']: - audiofile['genre'] = meta_tags['genre'] - if meta_tags['copyright']: - audiofile['copyright'] = meta_tags['copyright'] if meta_tags['external_ids']['isrc']: audiofile['isrc'] = meta_tags['external_ids']['isrc'] audiofile.save(v2_version=3) @@ -101,48 +93,13 @@ class EmbedMetadata: """ Embed metadata to M4A files. """ music_file = self.music_file meta_tags = self.meta_tags - # Apple has specific tags - see mutagen docs - - # http://mutagen.readthedocs.io/en/latest/api/mp4.html - tags = { 'album' : '\xa9alb', - 'artist' : '\xa9ART', - 'date' : '\xa9day', - 'title' : '\xa9nam', - 'year' : '\xa9day', - 'originaldate' : 'purd', - 'comment' : '\xa9cmt', - 'group' : '\xa9grp', - 'writer' : '\xa9wrt', - 'genre' : '\xa9gen', - 'tracknumber' : 'trkn', - 'albumartist' : 'aART', - 'disknumber' : 'disk', - 'cpil' : 'cpil', - 'albumart' : 'covr', - 'copyright' : 'cprt', - 'tempo' : 'tmpo', - 'lyrics' : '\xa9lyr' } - audiofile = MP4(music_file) - audiofile[tags['artist']] = meta_tags['artists'][0]['name'] - audiofile[tags['albumartist']] = meta_tags['artists'][0]['name'] - audiofile[tags['album']] = meta_tags['album']['name'] - audiofile[tags['title']] = meta_tags['name'] - audiofile[tags['tracknumber']] = [(meta_tags['track_number'], - meta_tags['total_tracks'])] - audiofile[tags['disknumber']] = [(meta_tags['disc_number'], 0)] - audiofile[tags['date']] = meta_tags['release_date'] - audiofile[tags['year']] = meta_tags['year'] - audiofile[tags['originaldate']] = meta_tags['release_date'] - audiofile[tags['comment']] = meta_tags['external_urls']['spotify'] - if meta_tags['genre']: - audiofile[tags['genre']] = meta_tags['genre'] - if meta_tags['copyright']: - audiofile[tags['copyright']] = meta_tags['copyright'] - if meta_tags['lyrics']: - audiofile[tags['lyrics']] = meta_tags['lyrics'] + self._embed_basic_metadata(audiofile, preset=M4A_TAG_PRESET) + audiofile[M4A_TAG_PRESET['year']] = meta_tags['year'] + audiofile[M4A_TAG_PRESET['comment']] = meta_tags['external_urls']['spotify'] try: albumart = urllib.request.urlopen(meta_tags['album']['images'][0]['url']) - audiofile[tags['albumart']] = [MP4Cover( + audiofile[M4A_TAG_PRESET['albumart']] = [MP4Cover( albumart.read(), imageformat=MP4Cover.FORMAT_JPEG)] albumart.close() except IndexError: @@ -150,3 +107,47 @@ class EmbedMetadata: audiofile.save() return True + + def as_flac(self): + music_file = self.music_file + meta_tags = self.meta_tags + audiofile = FLAC(music_file) + self._embed_basic_metadata(audiofile) + audiofile['year'] = meta_tags['year'] + audiofile['comment'] = meta_tags['external_urls']['spotify'] + + image = Picture() + image.type = 3 + image.desc = 'Cover' + image.mime = 'image/jpeg' + albumart = urllib.request.urlopen(meta_tags['album']['images'][0]['url']) + image.data = albumart.read() + albumart.close() + audiofile.add_picture(image) + + audiofile.save() + return True + + def _embed_basic_metadata(self, audiofile, preset=TAG_PRESET): + meta_tags = self.meta_tags + audiofile[preset['artist']] = meta_tags['artists'][0]['name'] + audiofile[preset['albumartist']] = meta_tags['artists'][0]['name'] + audiofile[preset['album']] = meta_tags['album']['name'] + audiofile[preset['title']] = meta_tags['name'] + audiofile[preset['date']] = meta_tags['release_date'] + audiofile[preset['originaldate']] = meta_tags['release_date'] + if meta_tags['genre']: + audiofile[preset['genre']] = meta_tags['genre'] + if meta_tags['copyright']: + audiofile[preset['copyright']] = meta_tags['copyright'] + if meta_tags['lyrics']: + audiofile[preset['lyrics']] = meta_tags['lyrics'] + if self.music_file.endswith('.flac'): + audiofile[preset['discnumber']] = str(meta_tags['disc_number']) + else: + audiofile[preset['discnumber']] = [(meta_tags['disc_number'], 0)] + if self.music_file.endswith('.flac'): + audiofile[preset['tracknumber']] = str(meta_tags['track_number']) + else: + audiofile[preset['tracknumber']] = [(meta_tags['track_number'], + meta_tags['total_tracks'])] diff --git a/spotdl.py b/spotdl.py index 6f5380d..3772a98 100755 --- a/spotdl.py +++ b/spotdl.py @@ -172,7 +172,7 @@ if __name__ == '__main__': internals.filter_path(const.args.folder) youtube_tools.set_api_key() - const.log = const.logzero.setup_logger(formatter=const.formatter, + const.log = const.logzero.setup_logger(formatter=const._formatter, level=const.args.log_level) log = const.log log.debug('Python version: {}'.format(sys.version)) diff --git a/test/loader.py b/test/loader.py index 2c8d1fe..05bb0b7 100644 --- a/test/loader.py +++ b/test/loader.py @@ -10,5 +10,5 @@ def load_defaults(): const.args.log_level = 10 spotdl.args = const.args - spotdl.log = const.logzero.setup_logger(formatter=const.formatter, + spotdl.log = const.logzero.setup_logger(formatter=const._formatter, level=const.args.log_level) diff --git a/test/test_with_metadata.py b/test/test_with_metadata.py index ec3bb3d..4a56efd 100644 --- a/test/test_with_metadata.py +++ b/test/test_with_metadata.py @@ -16,7 +16,7 @@ raw_song = 'http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU' def test_metadata(): - expect_number = 22 + expect_number = 23 global meta_tags meta_tags = spotify_tools.generate_metadata(raw_song) assert len(meta_tags) == expect_number @@ -102,6 +102,13 @@ class TestFFmpeg(): const.args.folder) assert return_code == expect_return_code + def test_convert_from_m4a_to_flac(self): + expect_return_code = 0 + return_code = convert.song(file_name + '.m4a', + file_name + '.flac', + const.args.folder) + assert return_code == expect_return_code + class TestAvconv: def test_convert_from_m4a_to_mp3(self): @@ -133,10 +140,15 @@ class TestEmbedMetadata: os.remove(track_path + '.webm') assert embed == expect_embed + def test_embed_in_flac(self): + expect_embed = True + embed = metadata.embed(track_path + '.flac', meta_tags) + os.remove(track_path + '.flac') + assert embed == expect_embed + def test_check_track_exists_after_download(): expect_check = True - # prerequisites for determining filename check = spotdl.check_exists(file_name, raw_song, meta_tags) os.remove(track_path + '.mp3') assert check == expect_check