Support FLAC output format (#259)

* Convert to .flac option

* Embed metadata to FLAC

* Update usage help

* Write tests
This commit is contained in:
Ritiek Malhotra
2018-04-02 00:47:31 +05:30
committed by GitHub
parent 7f7c3d6f58
commit 96ab547c5c
8 changed files with 123 additions and 79 deletions

View File

@@ -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)
```

View File

@@ -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

View File

@@ -17,7 +17,8 @@ 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:
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]))
@@ -26,7 +27,6 @@ def song(input_song, output_song, folder, avconv=False):
else:
exit_code = convert.with_ffmpeg()
return exit_code
return 0
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'

View File

@@ -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 '

View File

@@ -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'])]

View File

@@ -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))

View File

@@ -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)

View File

@@ -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