Introduce usage of black (code formatter) (#393)

This commit is contained in:
Linus Groh
2018-10-09 09:57:11 +02:00
committed by Ritiek Malhotra
parent 71ee6ad5e2
commit 13c83bd225
19 changed files with 755 additions and 606 deletions

View File

@@ -22,6 +22,7 @@ don't feel bad. Open an issue any way!
[good-first-issue](https://github.com/ritiek/spotify-downloader/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). [good-first-issue](https://github.com/ritiek/spotify-downloader/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
- When making a PR, point it to the [master branch](https://github.com/ritiek/spotify-downloader/tree/master) - When making a PR, point it to the [master branch](https://github.com/ritiek/spotify-downloader/tree/master)
unless mentioned otherwise. unless mentioned otherwise.
- Code should be formatted using [black](https://github.com/ambv/black).
- All tests are placed in the [test directory](https://github.com/ritiek/spotify-downloader/tree/master/test). We use [pytest](https://github.com/pytest-dev/pytest) - All tests are placed in the [test directory](https://github.com/ritiek/spotify-downloader/tree/master/test). We use [pytest](https://github.com/pytest-dev/pytest)
to run the test suite: `$ python3 -m pytest test`. to run the test suite: `$ python3 -m pytest test`.
If you don't have pytest, you can install it with `$ pip3 install pytest`. If you don't have pytest, you can install it with `$ pip3 install pytest`.

View File

@@ -1,6 +1,7 @@
# Spotify-Downloader # Spotify-Downloader
[![PyPi](https://img.shields.io/pypi/v/spotdl.svg)](https://pypi.org/project/spotdl) [![PyPi](https://img.shields.io/pypi/v/spotdl.svg)](https://pypi.org/project/spotdl)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
[![Build Status](https://travis-ci.org/ritiek/spotify-downloader.svg?branch=master)](https://travis-ci.org/ritiek/spotify-downloader) [![Build Status](https://travis-ci.org/ritiek/spotify-downloader.svg?branch=master)](https://travis-ci.org/ritiek/spotify-downloader)
[![Coverage Status](https://codecov.io/gh/ritiek/spotify-downloader/branch/master/graph/badge.svg)](https://codecov.io/gh/ritiek/spotify-downloader) [![Coverage Status](https://codecov.io/gh/ritiek/spotify-downloader/branch/master/graph/badge.svg)](https://codecov.io/gh/ritiek/spotify-downloader)
[![Docker Build Status](https://img.shields.io/docker/build/ritiek/spotify-downloader.svg)](https://hub.docker.com/r/ritiek/spotify-downloader) [![Docker Build Status](https://img.shields.io/docker/build/ritiek/spotify-downloader.svg)](https://hub.docker.com/r/ritiek/spotify-downloader)

View File

@@ -1,59 +1,64 @@
from setuptools import setup from setuptools import setup
with open('README.md', 'r') as f: with open("README.md", "r") as f:
long_description = f.read() long_description = f.read()
import spotdl import spotdl
setup( setup(
# 'spotify-downloader' was already taken :/ # 'spotify-downloader' was already taken :/
name='spotdl', name="spotdl",
# Tests are included automatically: # Tests are included automatically:
# https://docs.python.org/3.6/distutils/sourcedist.html#specifying-the-files-to-distribute # https://docs.python.org/3.6/distutils/sourcedist.html#specifying-the-files-to-distribute
packages=['spotdl'], packages=["spotdl"],
version=spotdl.__version__, version=spotdl.__version__,
install_requires=[ install_requires=[
'pathlib >= 1.0.1', "pathlib >= 1.0.1",
'youtube_dl >= 2017.9.26', "youtube_dl >= 2017.9.26",
'pafy >= 0.5.3.1', "pafy >= 0.5.3.1",
'spotipy >= 2.4.4', "spotipy >= 2.4.4",
'mutagen >= 1.41.1', "mutagen >= 1.41.1",
'beautifulsoup4 >= 4.6.3', "beautifulsoup4 >= 4.6.3",
'unicode-slugify >= 0.1.3', "unicode-slugify >= 0.1.3",
'titlecase >= 0.10.0', "titlecase >= 0.10.0",
'logzero >= 1.3.1', "logzero >= 1.3.1",
'lyricwikia >= 0.1.8', "lyricwikia >= 0.1.8",
'PyYAML >= 3.13', "PyYAML >= 3.13",
'appdirs >= 1.4.3' "appdirs >= 1.4.3",
], ],
description='Download songs from YouTube using Spotify song URLs or playlists with albumart and meta-tags.', description="Download songs from YouTube using Spotify song URLs or playlists with albumart and meta-tags.",
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type="text/markdown",
author='Ritiek Malhotra and the spotify-downloader contributors', author="Ritiek Malhotra and the spotify-downloader contributors",
author_email='ritiekmalhotra123@gmail.com', author_email="ritiekmalhotra123@gmail.com",
license='MIT', license="MIT",
python_requires='>=3.4', python_requires=">=3.4",
url='https://github.com/ritiek/spotify-downloader', url="https://github.com/ritiek/spotify-downloader",
download_url='https://pypi.org/project/spotdl/', download_url="https://pypi.org/project/spotdl/",
keywords=['spotify', 'downloader', 'download', 'music', 'youtube', 'mp3', 'album', 'metadata'], keywords=[
classifiers=[ "spotify",
'Development Status :: 4 - Beta', "downloader",
'Intended Audience :: End Users/Desktop', "download",
'License :: OSI Approved :: MIT License', "music",
'Programming Language :: Python', "youtube",
'Programming Language :: Python :: 3', "mp3",
'Programming Language :: Python :: 3.4', "album",
'Programming Language :: Python :: 3.5', "metadata",
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio',
'Topic :: Utilities'
], ],
entry_points={ classifiers=[
'console_scripts': [ "Development Status :: 4 - Beta",
'spotdl = spotdl.spotdl:main', "Intended Audience :: End Users/Desktop",
], "License :: OSI Approved :: MIT License",
} "Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Multimedia",
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Utilities",
],
entry_points={"console_scripts": ["spotdl = spotdl.spotdl:main"]},
) )

View File

@@ -1 +1 @@
__version__ = '1.0.0' __version__ = "1.0.0"

View File

@@ -1,6 +1,6 @@
import logzero import logzero
_log_format = ("%(color)s%(levelname)s:%(end_color)s %(message)s") _log_format = "%(color)s%(levelname)s:%(end_color)s %(message)s"
_formatter = logzero.LogFormatter(fmt=_log_format) _formatter = logzero.LogFormatter(fmt=_log_format)
# Set up a temporary logger with default log level so that # Set up a temporary logger with default log level so that
@@ -12,24 +12,26 @@ args = None
# Apple has specific tags - see mutagen docs - # Apple has specific tags - see mutagen docs -
# http://mutagen.readthedocs.io/en/latest/api/mp4.html # http://mutagen.readthedocs.io/en/latest/api/mp4.html
M4A_TAG_PRESET = { 'album' : '\xa9alb', M4A_TAG_PRESET = {
'artist' : '\xa9ART', "album": "\xa9alb",
'date' : '\xa9day', "artist": "\xa9ART",
'title' : '\xa9nam', "date": "\xa9day",
'year' : '\xa9day', "title": "\xa9nam",
'originaldate' : 'purd', "year": "\xa9day",
'comment' : '\xa9cmt', "originaldate": "purd",
'group' : '\xa9grp', "comment": "\xa9cmt",
'writer' : '\xa9wrt', "group": "\xa9grp",
'genre' : '\xa9gen', "writer": "\xa9wrt",
'tracknumber' : 'trkn', "genre": "\xa9gen",
'albumartist' : 'aART', "tracknumber": "trkn",
'discnumber' : 'disk', "albumartist": "aART",
'cpil' : 'cpil', "discnumber": "disk",
'albumart' : 'covr', "cpil": "cpil",
'copyright' : 'cprt', "albumart": "covr",
'tempo' : 'tmpo', "copyright": "cprt",
'lyrics' : '\xa9lyr' } "tempo": "tmpo",
"lyrics": "\xa9lyr",
}
TAG_PRESET = {} TAG_PRESET = {}
for key in M4A_TAG_PRESET.keys(): for key in M4A_TAG_PRESET.keys():

View File

@@ -20,8 +20,7 @@ def song(input_song, output_song, folder, avconv=False, trim_silence=False):
if input_song == output_song: if input_song == output_song:
return 0 return 0
convert = Converter(input_song, output_song, folder, trim_silence) convert = Converter(input_song, output_song, folder, trim_silence)
log.info('Converting {0} to {1}'.format( log.info("Converting {0} to {1}".format(input_song, output_song.split(".")[-1]))
input_song, output_song.split('.')[-1]))
if avconv: if avconv:
exit_code = convert.with_avconv() exit_code = convert.with_avconv()
else: else:
@@ -37,54 +36,67 @@ class Converter:
def with_avconv(self): def with_avconv(self):
if log.level == 10: if log.level == 10:
level = 'debug' level = "debug"
else: else:
level = '0' level = "0"
command = ['avconv', '-loglevel', level, '-i', command = [
self.input_file, '-ab', '192k', "avconv",
self.output_file, '-y'] "-loglevel",
level,
"-i",
self.input_file,
"-ab",
"192k",
self.output_file,
"-y",
]
if self.trim_silence: if self.trim_silence:
log.warning('--trim-silence not supported with avconv') log.warning("--trim-silence not supported with avconv")
log.debug(command) log.debug(command)
return subprocess.call(command) return subprocess.call(command)
def with_ffmpeg(self): def with_ffmpeg(self):
ffmpeg_pre = 'ffmpeg -y ' ffmpeg_pre = "ffmpeg -y "
if not log.level == 10: if not log.level == 10:
ffmpeg_pre += '-hide_banner -nostats -v panic ' ffmpeg_pre += "-hide_banner -nostats -v panic "
_, input_ext = os.path.splitext(self.input_file) _, input_ext = os.path.splitext(self.input_file)
_, output_ext = os.path.splitext(self.output_file) _, output_ext = os.path.splitext(self.output_file)
ffmpeg_params = '' ffmpeg_params = ""
if input_ext == '.m4a': if input_ext == ".m4a":
if output_ext == '.mp3': if output_ext == ".mp3":
ffmpeg_params = '-codec:v copy -codec:a libmp3lame -ar 44100 ' ffmpeg_params = "-codec:v copy -codec:a libmp3lame -ar 44100 "
elif output_ext == '.webm': elif output_ext == ".webm":
ffmpeg_params = '-codec:a libopus -vbr on ' ffmpeg_params = "-codec:a libopus -vbr on "
elif input_ext == '.webm': elif input_ext == ".webm":
if output_ext == '.mp3': if output_ext == ".mp3":
ffmpeg_params = '-codec:a libmp3lame -ar 44100 ' ffmpeg_params = "-codec:a libmp3lame -ar 44100 "
elif output_ext == '.m4a': elif output_ext == ".m4a":
ffmpeg_params = '-cutoff 20000 -codec:a libfdk_aac -ar 44100 ' ffmpeg_params = "-cutoff 20000 -codec:a libfdk_aac -ar 44100 "
if output_ext == '.flac': if output_ext == ".flac":
ffmpeg_params = '-codec:a flac -ar 44100 ' ffmpeg_params = "-codec:a flac -ar 44100 "
# add common params for any of the above combination # add common params for any of the above combination
ffmpeg_params += '-b:a 192k -vn ' ffmpeg_params += "-b:a 192k -vn "
ffmpeg_pre += ' -i' ffmpeg_pre += " -i"
if self.trim_silence: if self.trim_silence:
ffmpeg_params += '-af silenceremove=start_periods=1 ' ffmpeg_params += "-af silenceremove=start_periods=1 "
command = ffmpeg_pre.split() + [self.input_file] + ffmpeg_params.split() + [self.output_file] command = (
ffmpeg_pre.split()
+ [self.input_file]
+ ffmpeg_params.split()
+ [self.output_file]
)
log.debug(command) log.debug(command)
return subprocess.call(command) return subprocess.call(command)

View File

@@ -10,28 +10,30 @@ import os
import sys import sys
_LOG_LEVELS_STR = ['INFO', 'WARNING', 'ERROR', 'DEBUG'] _LOG_LEVELS_STR = ["INFO", "WARNING", "ERROR", "DEBUG"]
default_conf = { 'spotify-downloader': default_conf = {
{ 'manual' : False, "spotify-downloader": {
'no-metadata' : False, "manual": False,
'avconv' : False, "no-metadata": False,
'folder' : internals.get_music_dir(), "avconv": False,
'overwrite' : 'prompt', "folder": internals.get_music_dir(),
'input-ext' : '.m4a', "overwrite": "prompt",
'output-ext' : '.mp3', "input-ext": ".m4a",
'trim-silence' : False, "output-ext": ".mp3",
'download-only-metadata' : False, "trim-silence": False,
'dry-run' : False, "download-only-metadata": False,
'music-videos-only' : False, "dry-run": False,
'no-spaces' : False, "music-videos-only": False,
'file-format' : '{artist} - {track_name}', "no-spaces": False,
'search-format' : '{artist} - {track_name} lyrics', "file-format": "{artist} - {track_name}",
'youtube-api-key' : None, "search-format": "{artist} - {track_name} lyrics",
'skip' : None, "youtube-api-key": None,
'write-successful' : None, "skip": None,
'log-level' : 'INFO' } "write-successful": None,
} "log-level": "INFO",
}
}
def log_leveller(log_level_str): def log_leveller(log_level_str):
@@ -50,142 +52,200 @@ def merge(default, config):
def get_config(config_file): def get_config(config_file):
try: try:
with open(config_file, 'r') as ymlfile: with open(config_file, "r") as ymlfile:
cfg = yaml.load(ymlfile) cfg = yaml.load(ymlfile)
except FileNotFoundError: except FileNotFoundError:
log.info('Writing default configuration to {0}:'.format(config_file)) log.info("Writing default configuration to {0}:".format(config_file))
with open(config_file, 'w') as ymlfile: with open(config_file, "w") as ymlfile:
yaml.dump(default_conf, ymlfile, default_flow_style=False) yaml.dump(default_conf, ymlfile, default_flow_style=False)
cfg = default_conf cfg = default_conf
for line in yaml.dump(default_conf['spotify-downloader'], default_flow_style=False).split('\n'): for line in yaml.dump(
default_conf["spotify-downloader"], default_flow_style=False
).split("\n"):
if line.strip(): if line.strip():
log.info(line.strip()) log.info(line.strip())
log.info('Please note that command line arguments have higher priority ' log.info(
'than their equivalents in the configuration file') "Please note that command line arguments have higher priority "
"than their equivalents in the configuration file"
)
return cfg['spotify-downloader'] return cfg["spotify-downloader"]
def override_config(config_file, parser, raw_args=None): def override_config(config_file, parser, raw_args=None):
""" Override default dict with config dict passed as comamnd line argument. """ """ Override default dict with config dict passed as comamnd line argument. """
config_file = os.path.realpath(config_file) config_file = os.path.realpath(config_file)
config = merge(default_conf['spotify-downloader'], get_config(config_file)) config = merge(default_conf["spotify-downloader"], get_config(config_file))
parser.set_defaults(**config) parser.set_defaults(**config)
return parser.parse_args(raw_args) return parser.parse_args(raw_args)
def get_arguments(raw_args=None, to_group=True, to_merge=True): def get_arguments(raw_args=None, to_group=True, to_merge=True):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Download and convert tracks from Spotify, Youtube etc.', description="Download and convert tracks from Spotify, Youtube etc.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
if to_merge: if to_merge:
config_dir = os.path.join(appdirs.user_config_dir(), 'spotdl') config_dir = os.path.join(appdirs.user_config_dir(), "spotdl")
os.makedirs(config_dir, exist_ok=True) os.makedirs(config_dir, exist_ok=True)
config_file = os.path.join(config_dir, 'config.yml') config_file = os.path.join(config_dir, "config.yml")
config = merge(default_conf['spotify-downloader'], get_config(config_file)) config = merge(default_conf["spotify-downloader"], get_config(config_file))
else: else:
config = default_conf['spotify-downloader'] config = default_conf["spotify-downloader"]
if to_group: if to_group:
group = parser.add_mutually_exclusive_group(required=True) group = parser.add_mutually_exclusive_group(required=True)
group.add_argument( group.add_argument(
'-s', '--song', "-s", "--song", help="download track by spotify link or name"
help='download track by spotify link or name') )
group.add_argument("-l", "--list", help="download tracks from a file")
group.add_argument( group.add_argument(
'-l', '--list', "-p",
help='download tracks from a file') "--playlist",
help="load tracks from playlist URL into <playlist_name>.txt",
)
group.add_argument( group.add_argument(
'-p', '--playlist', "-b", "--album", help="load tracks from album URL into <album_name>.txt"
help='load tracks from playlist URL into <playlist_name>.txt') )
group.add_argument( group.add_argument(
'-b', '--album', "-u",
help='load tracks from album URL into <album_name>.txt') "--username",
help="load tracks from user's playlist into <playlist_name>.txt",
)
group.add_argument( group.add_argument(
'-u', '--username', "-V", "--version", help="show version and exit", action="store_true"
help="load tracks from user's playlist into <playlist_name>.txt") )
group.add_argument(
'-V', '--version',
help="show version and exit",
action='store_true')
parser.add_argument( parser.add_argument(
'-m', '--manual', default=config['manual'], "-m",
help='choose the track to download manually from a list ' "--manual",
'of matching tracks', default=config["manual"],
action='store_true') help="choose the track to download manually from a list " "of matching tracks",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-nm', '--no-metadata', default=config['no-metadata'], "-nm",
help='do not embed metadata in tracks', action='store_true') "--no-metadata",
default=config["no-metadata"],
help="do not embed metadata in tracks",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-a', '--avconv', default=config['avconv'], "-a",
help='use avconv for conversion (otherwise defaults to ffmpeg)', "--avconv",
action='store_true') default=config["avconv"],
help="use avconv for conversion (otherwise defaults to ffmpeg)",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-f', '--folder', default=os.path.abspath(config['folder']), "-f",
help='path to folder where downloaded tracks will be stored in') "--folder",
default=os.path.abspath(config["folder"]),
help="path to folder where downloaded tracks will be stored in",
)
parser.add_argument( parser.add_argument(
'--overwrite', default=config['overwrite'], "--overwrite",
help='change the overwrite policy', default=config["overwrite"],
choices={'prompt', 'force', 'skip'}) help="change the overwrite policy",
choices={"prompt", "force", "skip"},
)
parser.add_argument( parser.add_argument(
'-i', '--input-ext', default=config['input-ext'], "-i",
help='preferred input format .m4a or .webm (Opus)', "--input-ext",
choices={'.m4a', '.webm'}) default=config["input-ext"],
help="preferred input format .m4a or .webm (Opus)",
choices={".m4a", ".webm"},
)
parser.add_argument( parser.add_argument(
'-o', '--output-ext', default=config['output-ext'], "-o",
help='preferred output format .mp3, .m4a (AAC), .flac, etc.') "--output-ext",
default=config["output-ext"],
help="preferred output format .mp3, .m4a (AAC), .flac, etc.",
)
parser.add_argument( parser.add_argument(
'-ff', '--file-format', default=config['file-format'], "-ff",
help='file format to save the downloaded track with, each tag ' "--file-format",
'is surrounded by curly braces. Possible formats: ' default=config["file-format"],
'{}'.format([internals.formats[x] for x in internals.formats])) help="file format to save the downloaded track with, each tag "
"is surrounded by curly braces. Possible formats: "
"{}".format([internals.formats[x] for x in internals.formats]),
)
parser.add_argument( parser.add_argument(
'--trim-silence', default=config['trim-silence'], "--trim-silence",
help='remove silence from the start of the audio', default=config["trim-silence"],
action='store_true') help="remove silence from the start of the audio",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-sf', '--search-format', default=config['search-format'], "-sf",
help='search format to search for on YouTube, each tag ' "--search-format",
'is surrounded by curly braces. Possible formats: ' default=config["search-format"],
'{}'.format([internals.formats[x] for x in internals.formats])) help="search format to search for on YouTube, each tag "
"is surrounded by curly braces. Possible formats: "
"{}".format([internals.formats[x] for x in internals.formats]),
)
parser.add_argument( parser.add_argument(
'-dm', '--download-only-metadata', default=config['download-only-metadata'], "-dm",
help='download tracks only whose metadata is found', "--download-only-metadata",
action='store_true') default=config["download-only-metadata"],
help="download tracks only whose metadata is found",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-d', '--dry-run', default=config['dry-run'], "-d",
help='show only track title and YouTube URL, and then skip ' "--dry-run",
'to the next track (if any)', default=config["dry-run"],
action='store_true') help="show only track title and YouTube URL, and then skip "
"to the next track (if any)",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-mo', '--music-videos-only', default=config['music-videos-only'], "-mo",
help='search only for music videos on Youtube (works only ' "--music-videos-only",
'when YouTube API key is set', default=config["music-videos-only"],
action='store_true') help="search only for music videos on Youtube (works only "
"when YouTube API key is set",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-ns', '--no-spaces', default=config['no-spaces'], "-ns",
help='replace spaces with underscores in file names', "--no-spaces",
action='store_true') default=config["no-spaces"],
help="replace spaces with underscores in file names",
action="store_true",
)
parser.add_argument( parser.add_argument(
'-ll', '--log-level', default=config['log-level'], "-ll",
"--log-level",
default=config["log-level"],
choices=_LOG_LEVELS_STR, choices=_LOG_LEVELS_STR,
type=str.upper, type=str.upper,
help='set log verbosity') help="set log verbosity",
)
parser.add_argument( parser.add_argument(
'-yk', '--youtube-api-key', default=config['youtube-api-key'], "-yk",
help=argparse.SUPPRESS) "--youtube-api-key",
default=config["youtube-api-key"],
help=argparse.SUPPRESS,
)
parser.add_argument( parser.add_argument(
'-sk', '--skip', default=config['skip'], "-sk",
help='path to file containing tracks to skip') "--skip",
default=config["skip"],
help="path to file containing tracks to skip",
)
parser.add_argument( parser.add_argument(
'-w', '--write-successful', default=config['write-successful'], "-w",
help='path to file to write successful tracks to') "--write-successful",
default=config["write-successful"],
help="path to file to write successful tracks to",
)
parser.add_argument( parser.add_argument(
'-c', '--config', default=None, "-c", "--config", default=None, help="path to custom config.yml file"
help='path to custom config.yml file') )
parsed = parser.parse_args(raw_args) parsed = parser.parse_args(raw_args)

View File

@@ -12,45 +12,47 @@ except ImportError:
try: try:
from slugify import SLUG_OK, slugify from slugify import SLUG_OK, slugify
except ImportError: except ImportError:
log.error('Oops! `unicode-slugify` was not found.') log.error("Oops! `unicode-slugify` was not found.")
log.info('Please remove any other slugify library and install `unicode-slugify`') log.info("Please remove any other slugify library and install `unicode-slugify`")
sys.exit(5) sys.exit(5)
formats = { 0 : 'track_name', formats = {
1 : 'artist', 0: "track_name",
2 : 'album', 1: "artist",
3 : 'album_artist', 2: "album",
4 : 'genre', 3: "album_artist",
5 : 'disc_number', 4: "genre",
6 : 'duration', 5: "disc_number",
7 : 'year', 6: "duration",
8 : 'original_date', 7: "year",
9 : 'track_number', 8: "original_date",
10 : 'total_tracks', 9: "track_number",
11 : 'isrc' } 10: "total_tracks",
11: "isrc",
}
def input_link(links): def input_link(links):
""" Let the user input a choice. """ """ Let the user input a choice. """
while True: while True:
try: try:
log.info('Choose your number:') log.info("Choose your number:")
the_chosen_one = int(input('> ')) the_chosen_one = int(input("> "))
if 1 <= the_chosen_one <= len(links): if 1 <= the_chosen_one <= len(links):
return links[the_chosen_one - 1] return links[the_chosen_one - 1]
elif the_chosen_one == 0: elif the_chosen_one == 0:
return None return None
else: else:
log.warning('Choose a valid number!') log.warning("Choose a valid number!")
except ValueError: except ValueError:
log.warning('Choose a valid number!') log.warning("Choose a valid number!")
def trim_song(text_file): def trim_song(text_file):
""" Remove the first song from file. """ """ Remove the first song from file. """
with open(text_file, 'r') as file_in: with open(text_file, "r") as file_in:
data = file_in.read().splitlines(True) data = file_in.read().splitlines(True)
with open(text_file, 'w') as file_out: with open(text_file, "w") as file_out:
file_out.writelines(data[1:]) file_out.writelines(data[1:])
return data[0] return data[0]
@@ -58,7 +60,7 @@ def trim_song(text_file):
def is_spotify(raw_song): def is_spotify(raw_song):
""" Check if the input song is a Spotify link. """ """ Check if the input song is a Spotify link. """
status = len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song status = len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song
status = status or raw_song.find('spotify') > -1 status = status or raw_song.find("spotify") > -1
return status return status
@@ -66,49 +68,47 @@ def is_youtube(raw_song):
""" Check if the input song is a YouTube link. """ """ Check if the input song is a YouTube link. """
status = len(raw_song) == 11 and raw_song.replace(" ", "%20") == raw_song status = len(raw_song) == 11 and raw_song.replace(" ", "%20") == raw_song
status = status and not raw_song.lower() == raw_song status = status and not raw_song.lower() == raw_song
status = status or 'youtube.com/watch?v=' in raw_song status = status or "youtube.com/watch?v=" in raw_song
return status return status
def format_string(string_format, tags, slugification=False, force_spaces=False): def format_string(string_format, tags, slugification=False, force_spaces=False):
""" Generate a string of the format '[artist] - [song]' for the given spotify song. """ """ Generate a string of the format '[artist] - [song]' for the given spotify song. """
format_tags = dict(formats) format_tags = dict(formats)
format_tags[0] = tags['name'] format_tags[0] = tags["name"]
format_tags[1] = tags['artists'][0]['name'] format_tags[1] = tags["artists"][0]["name"]
format_tags[2] = tags['album']['name'] format_tags[2] = tags["album"]["name"]
format_tags[3] = tags['artists'][0]['name'] format_tags[3] = tags["artists"][0]["name"]
format_tags[4] = tags['genre'] format_tags[4] = tags["genre"]
format_tags[5] = tags['disc_number'] format_tags[5] = tags["disc_number"]
format_tags[6] = tags['duration'] format_tags[6] = tags["duration"]
format_tags[7] = tags['year'] format_tags[7] = tags["year"]
format_tags[8] = tags['release_date'] format_tags[8] = tags["release_date"]
format_tags[9] = tags['track_number'] format_tags[9] = tags["track_number"]
format_tags[10] = tags['total_tracks'] format_tags[10] = tags["total_tracks"]
format_tags[11] = tags['external_ids']['isrc'] format_tags[11] = tags["external_ids"]["isrc"]
for tag in format_tags: for tag in format_tags:
if slugification: if slugification:
format_tags[tag] = sanitize_title(format_tags[tag], format_tags[tag] = sanitize_title(format_tags[tag], ok="'-_()[]{}")
ok="'-_()[]{}")
else: else:
format_tags[tag] = str(format_tags[tag]) format_tags[tag] = str(format_tags[tag])
for x in formats: for x in formats:
format_tag = '{' + formats[x] + '}' format_tag = "{" + formats[x] + "}"
string_format = string_format.replace(format_tag, string_format = string_format.replace(format_tag, format_tags[x])
format_tags[x])
if const.args.no_spaces and not force_spaces: if const.args.no_spaces and not force_spaces:
string_format = string_format.replace(' ', '_') string_format = string_format.replace(" ", "_")
return string_format return string_format
def sanitize_title(title, ok='-_()[]{}\/'): def sanitize_title(title, ok="-_()[]{}\/"):
""" Generate filename of the song to be downloaded. """ """ Generate filename of the song to be downloaded. """
if const.args.no_spaces: if const.args.no_spaces:
title = title.replace(' ', '_') title = title.replace(" ", "_")
# slugify removes any special characters # slugify removes any special characters
title = slugify(title, ok=ok, lower=False, spaces=True) title = slugify(title, ok=ok, lower=False, spaces=True)
@@ -119,7 +119,7 @@ def filter_path(path):
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
for temp in os.listdir(path): for temp in os.listdir(path):
if temp.endswith('.temp'): if temp.endswith(".temp"):
os.remove(os.path.join(path, temp)) os.remove(os.path.join(path, temp))
@@ -127,19 +127,20 @@ def videotime_from_seconds(time):
if time < 60: if time < 60:
return str(time) return str(time)
if time < 3600: if time < 3600:
return '{0}:{1:02}'.format(time//60, time % 60) return "{0}:{1:02}".format(time // 60, time % 60)
return '{0}:{1:02}:{2:02}'.format((time//60)//60, (time//60) % 60, time % 60) return "{0}:{1:02}:{2:02}".format((time // 60) // 60, (time // 60) % 60, time % 60)
def get_sec(time_str): def get_sec(time_str):
if ':' in time_str: if ":" in time_str:
splitter = ':' splitter = ":"
elif '.' in time_str: elif "." in time_str:
splitter = '.' splitter = "."
else: else:
raise ValueError("No expected character found in {} to split" raise ValueError(
"time values.".format(time_str)) "No expected character found in {} to split" "time values.".format(time_str)
)
v = time_str.split(splitter, 3) v = time_str.split(splitter, 3)
v.reverse() v.reverse()
sec = 0 sec = 0
@@ -153,12 +154,12 @@ def get_sec(time_str):
def get_splits(url): def get_splits(url):
if '/' in url: if "/" in url:
if url.endswith('/'): if url.endswith("/"):
url = url[:-1] url = url[:-1]
splits = url.split('/') splits = url.split("/")
else: else:
splits = url.split(':') splits = url.split(":")
return splits return splits
@@ -168,7 +169,7 @@ def get_unique_tracks(text_file):
file containing tracks. file containing tracks.
""" """
with open(text_file, 'r') as listed: with open(text_file, "r") as listed:
# Read tracks into a list and remove any duplicates # Read tracks into a list and remove any duplicates
lines = listed.read().splitlines() lines = listed.read().splitlines()
@@ -181,18 +182,20 @@ def get_unique_tracks(text_file):
# a hacky way to get user's localized music directory # a hacky way to get user's localized music directory
# (thanks @linusg, issue #203) # (thanks @linusg, issue #203)
def get_music_dir(): def get_music_dir():
home = os.path.expanduser('~') home = os.path.expanduser("~")
# On Linux, the localized folder names are the actual ones. # On Linux, the localized folder names are the actual ones.
# It's a freedesktop standard though. # It's a freedesktop standard though.
if sys.platform.startswith('linux'): if sys.platform.startswith("linux"):
for file_item in ('.config/user-dirs.dirs', 'user-dirs.dirs'): for file_item in (".config/user-dirs.dirs", "user-dirs.dirs"):
path = os.path.join(home, file_item) path = os.path.join(home, file_item)
if os.path.isfile(path): if os.path.isfile(path):
with open(path, 'r') as f: with open(path, "r") as f:
for line in f: for line in f:
if line.startswith('XDG_MUSIC_DIR'): if line.startswith("XDG_MUSIC_DIR"):
return os.path.expandvars(line.strip().split('=')[1].strip('"')) return os.path.expandvars(
line.strip().split("=")[1].strip('"')
)
# Windows / Cygwin # Windows / Cygwin
# Queries registry for 'My Music' folder path (as this can be changed) # Queries registry for 'My Music' folder path (as this can be changed)
@@ -208,7 +211,7 @@ def get_music_dir():
# So, defaulting to C:\Users\<user>\Music or /Users/<user>/Music # So, defaulting to C:\Users\<user>\Music or /Users/<user>/Music
# respectively is sufficient. # respectively is sufficient.
# On Linux, default to /home/<user>/Music if the above method failed. # On Linux, default to /home/<user>/Music if the above method failed.
return os.path.join(home, 'Music') return os.path.join(home, "Music")
def remove_duplicates(tracks): def remove_duplicates(tracks):
@@ -221,5 +224,4 @@ def remove_duplicates(tracks):
local_set = set() local_set = set()
local_set_add = local_set.add local_set_add = local_set.add
return [x for x in tracks return [x for x in tracks if not (x in local_set or local_set_add(x))]
if not (x in local_set or local_set_add(x))]

View File

@@ -12,12 +12,12 @@ def compare(music_file, metadata):
"""Check if the input music file title matches the expected title.""" """Check if the input music file title matches the expected title."""
already_tagged = False already_tagged = False
try: try:
if music_file.endswith('.mp3'): if music_file.endswith(".mp3"):
audiofile = EasyID3(music_file) audiofile = EasyID3(music_file)
already_tagged = audiofile['title'][0] == metadata['name'] already_tagged = audiofile["title"][0] == metadata["name"]
elif music_file.endswith('.m4a'): elif music_file.endswith(".m4a"):
audiofile = MP4(music_file) audiofile = MP4(music_file)
already_tagged = audiofile['\xa9nam'][0] == metadata['name'] already_tagged = audiofile["\xa9nam"][0] == metadata["name"]
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
@@ -27,17 +27,17 @@ def compare(music_file, metadata):
def embed(music_file, meta_tags): def embed(music_file, meta_tags):
""" Embed metadata. """ """ Embed metadata. """
embed = EmbedMetadata(music_file, meta_tags) embed = EmbedMetadata(music_file, meta_tags)
if music_file.endswith('.m4a'): if music_file.endswith(".m4a"):
log.info('Applying metadata') log.info("Applying metadata")
return embed.as_m4a() return embed.as_m4a()
elif music_file.endswith('.mp3'): elif music_file.endswith(".mp3"):
log.info('Applying metadata') log.info("Applying metadata")
return embed.as_mp3() return embed.as_mp3()
elif music_file.endswith('.flac'): elif music_file.endswith(".flac"):
log.info('Applying metadata') log.info("Applying metadata")
return embed.as_flac() return embed.as_flac()
else: else:
log.warning('Cannot embed metadata into given output extension') log.warning("Cannot embed metadata into given output extension")
return False return False
@@ -56,34 +56,41 @@ class EmbedMetadata:
# Check out somewhere at end of above linked file # Check out somewhere at end of above linked file
audiofile = EasyID3(music_file) audiofile = EasyID3(music_file)
self._embed_basic_metadata(audiofile, preset=TAG_PRESET) self._embed_basic_metadata(audiofile, preset=TAG_PRESET)
audiofile['media'] = meta_tags['type'] audiofile["media"] = meta_tags["type"]
audiofile['author'] = meta_tags['artists'][0]['name'] audiofile["author"] = meta_tags["artists"][0]["name"]
audiofile['lyricist'] = meta_tags['artists'][0]['name'] audiofile["lyricist"] = meta_tags["artists"][0]["name"]
audiofile['arranger'] = meta_tags['artists'][0]['name'] audiofile["arranger"] = meta_tags["artists"][0]["name"]
audiofile['performer'] = meta_tags['artists'][0]['name'] audiofile["performer"] = meta_tags["artists"][0]["name"]
audiofile['website'] = meta_tags['external_urls']['spotify'] audiofile["website"] = meta_tags["external_urls"]["spotify"]
audiofile['length'] = str(meta_tags['duration']) audiofile["length"] = str(meta_tags["duration"])
if meta_tags['publisher']: if meta_tags["publisher"]:
audiofile['encodedby'] = meta_tags['publisher'] audiofile["encodedby"] = meta_tags["publisher"]
if meta_tags['external_ids']['isrc']: if meta_tags["external_ids"]["isrc"]:
audiofile['isrc'] = meta_tags['external_ids']['isrc'] audiofile["isrc"] = meta_tags["external_ids"]["isrc"]
audiofile.save(v2_version=3) audiofile.save(v2_version=3)
# For supported id3 tags: # For supported id3 tags:
# https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py # https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py
# Each class represents an id3 tag # Each class represents an id3 tag
audiofile = ID3(music_file) audiofile = ID3(music_file)
audiofile['TORY'] = TORY(encoding=3, text=meta_tags['year']) audiofile["TORY"] = TORY(encoding=3, text=meta_tags["year"])
audiofile['TYER'] = TYER(encoding=3, text=meta_tags['year']) audiofile["TYER"] = TYER(encoding=3, text=meta_tags["year"])
if meta_tags['publisher']: if meta_tags["publisher"]:
audiofile['TPUB'] = TPUB(encoding=3, text=meta_tags['publisher']) audiofile["TPUB"] = TPUB(encoding=3, text=meta_tags["publisher"])
audiofile['COMM'] = COMM(encoding=3, text=meta_tags['external_urls']['spotify']) audiofile["COMM"] = COMM(encoding=3, text=meta_tags["external_urls"]["spotify"])
if meta_tags['lyrics']: if meta_tags["lyrics"]:
audiofile['USLT'] = USLT(encoding=3, desc=u'Lyrics', text=meta_tags['lyrics']) audiofile["USLT"] = USLT(
encoding=3, desc=u"Lyrics", text=meta_tags["lyrics"]
)
try: try:
albumart = urllib.request.urlopen(meta_tags['album']['images'][0]['url']) albumart = urllib.request.urlopen(meta_tags["album"]["images"][0]["url"])
audiofile['APIC'] = APIC(encoding=3, mime='image/jpeg', type=3, audiofile["APIC"] = APIC(
desc=u'Cover', data=albumart.read()) encoding=3,
mime="image/jpeg",
type=3,
desc=u"Cover",
data=albumart.read(),
)
albumart.close() albumart.close()
except IndexError: except IndexError:
pass pass
@@ -97,13 +104,14 @@ class EmbedMetadata:
meta_tags = self.meta_tags meta_tags = self.meta_tags
audiofile = MP4(music_file) audiofile = MP4(music_file)
self._embed_basic_metadata(audiofile, preset=M4A_TAG_PRESET) self._embed_basic_metadata(audiofile, preset=M4A_TAG_PRESET)
audiofile[M4A_TAG_PRESET['year']] = meta_tags['year'] audiofile[M4A_TAG_PRESET["year"]] = meta_tags["year"]
if meta_tags['lyrics']: if meta_tags["lyrics"]:
audiofile[M4A_TAG_PRESET['lyrics']] = meta_tags['lyrics'] audiofile[M4A_TAG_PRESET["lyrics"]] = meta_tags["lyrics"]
try: try:
albumart = urllib.request.urlopen(meta_tags['album']['images'][0]['url']) albumart = urllib.request.urlopen(meta_tags["album"]["images"][0]["url"])
audiofile[M4A_TAG_PRESET['albumart']] = [MP4Cover( audiofile[M4A_TAG_PRESET["albumart"]] = [
albumart.read(), imageformat=MP4Cover.FORMAT_JPEG)] MP4Cover(albumart.read(), imageformat=MP4Cover.FORMAT_JPEG)
]
albumart.close() albumart.close()
except IndexError: except IndexError:
pass pass
@@ -116,16 +124,16 @@ class EmbedMetadata:
meta_tags = self.meta_tags meta_tags = self.meta_tags
audiofile = FLAC(music_file) audiofile = FLAC(music_file)
self._embed_basic_metadata(audiofile) self._embed_basic_metadata(audiofile)
audiofile['year'] = meta_tags['year'] audiofile["year"] = meta_tags["year"]
audiofile['comment'] = meta_tags['external_urls']['spotify'] audiofile["comment"] = meta_tags["external_urls"]["spotify"]
if meta_tags['lyrics']: if meta_tags["lyrics"]:
audiofile['lyrics'] = meta_tags['lyrics'] audiofile["lyrics"] = meta_tags["lyrics"]
image = Picture() image = Picture()
image.type = 3 image.type = 3
image.desc = 'Cover' image.desc = "Cover"
image.mime = 'image/jpeg' image.mime = "image/jpeg"
albumart = urllib.request.urlopen(meta_tags['album']['images'][0]['url']) albumart = urllib.request.urlopen(meta_tags["album"]["images"][0]["url"])
image.data = albumart.read() image.data = albumart.read()
albumart.close() albumart.close()
audiofile.add_picture(image) audiofile.add_picture(image)
@@ -135,27 +143,28 @@ class EmbedMetadata:
def _embed_basic_metadata(self, audiofile, preset=TAG_PRESET): def _embed_basic_metadata(self, audiofile, preset=TAG_PRESET):
meta_tags = self.meta_tags meta_tags = self.meta_tags
audiofile[preset['artist']] = meta_tags['artists'][0]['name'] audiofile[preset["artist"]] = meta_tags["artists"][0]["name"]
audiofile[preset['albumartist']] = meta_tags['artists'][0]['name'] audiofile[preset["albumartist"]] = meta_tags["artists"][0]["name"]
audiofile[preset['album']] = meta_tags['album']['name'] audiofile[preset["album"]] = meta_tags["album"]["name"]
audiofile[preset['title']] = meta_tags['name'] audiofile[preset["title"]] = meta_tags["name"]
audiofile[preset['date']] = meta_tags['release_date'] audiofile[preset["date"]] = meta_tags["release_date"]
audiofile[preset['originaldate']] = meta_tags['release_date'] audiofile[preset["originaldate"]] = meta_tags["release_date"]
if meta_tags['genre']: if meta_tags["genre"]:
audiofile[preset['genre']] = meta_tags['genre'] audiofile[preset["genre"]] = meta_tags["genre"]
if meta_tags['copyright']: if meta_tags["copyright"]:
audiofile[preset['copyright']] = meta_tags['copyright'] audiofile[preset["copyright"]] = meta_tags["copyright"]
if self.music_file.endswith('.flac'): if self.music_file.endswith(".flac"):
audiofile[preset['discnumber']] = str(meta_tags['disc_number']) audiofile[preset["discnumber"]] = str(meta_tags["disc_number"])
else: else:
audiofile[preset['discnumber']] = [(meta_tags['disc_number'], 0)] audiofile[preset["discnumber"]] = [(meta_tags["disc_number"], 0)]
if self.music_file.endswith('.flac'): if self.music_file.endswith(".flac"):
audiofile[preset['tracknumber']] = str(meta_tags['track_number']) audiofile[preset["tracknumber"]] = str(meta_tags["track_number"])
else: else:
if preset['tracknumber'] == TAG_PRESET['tracknumber']: if preset["tracknumber"] == TAG_PRESET["tracknumber"]:
audiofile[preset['tracknumber']] = '{}/{}'.format(meta_tags['track_number'], audiofile[preset["tracknumber"]] = "{}/{}".format(
meta_tags['total_tracks']) meta_tags["track_number"], meta_tags["total_tracks"]
)
else: else:
audiofile[preset['tracknumber']] = [ audiofile[preset["tracknumber"]] = [
(meta_tags['track_number'], meta_tags['total_tracks']) (meta_tags["track_number"], meta_tags["total_tracks"])
] ]

View File

@@ -22,11 +22,13 @@ import pprint
def check_exists(music_file, raw_song, meta_tags): def check_exists(music_file, raw_song, meta_tags):
""" Check if the input song already exists in the given folder. """ """ Check if the input song already exists in the given folder. """
log.debug('Cleaning any temp files and checking ' log.debug(
'if "{}" already exists'.format(music_file)) "Cleaning any temp files and checking "
'if "{}" already exists'.format(music_file)
)
songs = os.listdir(const.args.folder) songs = os.listdir(const.args.folder)
for song in songs: for song in songs:
if song.endswith('.temp'): if song.endswith(".temp"):
os.remove(os.path.join(const.args.folder, song)) os.remove(os.path.join(const.args.folder, song))
continue continue
# check if a song with the same name is already present in the given folder # check if a song with the same name is already present in the given folder
@@ -35,29 +37,33 @@ def check_exists(music_file, raw_song, meta_tags):
if internals.is_spotify(raw_song): if internals.is_spotify(raw_song):
# check if the already downloaded song has correct metadata # check if the already downloaded song has correct metadata
# if not, remove it and download again without prompt # if not, remove it and download again without prompt
already_tagged = metadata.compare(os.path.join(const.args.folder, song), already_tagged = metadata.compare(
meta_tags) os.path.join(const.args.folder, song), meta_tags
log.debug('Checking if it is already tagged correctly? {}', )
already_tagged) log.debug(
"Checking if it is already tagged correctly? {}", already_tagged
)
if not already_tagged: if not already_tagged:
os.remove(os.path.join(const.args.folder, song)) os.remove(os.path.join(const.args.folder, song))
return False return False
log.warning('"{}" already exists'.format(song)) log.warning('"{}" already exists'.format(song))
if const.args.overwrite == 'prompt': if const.args.overwrite == "prompt":
log.info('"{}" has already been downloaded. ' log.info(
'Re-download? (y/N): '.format(song)) '"{}" has already been downloaded. '
prompt = input('> ') "Re-download? (y/N): ".format(song)
if prompt.lower() == 'y': )
prompt = input("> ")
if prompt.lower() == "y":
os.remove(os.path.join(const.args.folder, song)) os.remove(os.path.join(const.args.folder, song))
return False return False
else: else:
return True return True
elif const.args.overwrite == 'force': elif const.args.overwrite == "force":
os.remove(os.path.join(const.args.folder, song)) os.remove(os.path.join(const.args.folder, song))
log.info('Overwriting "{}"'.format(song)) log.info('Overwriting "{}"'.format(song))
return False return False
elif const.args.overwrite == 'skip': elif const.args.overwrite == "skip":
log.info('Skipping "{}"'.format(song)) log.info('Skipping "{}"'.format(song))
return True return True
return False return False
@@ -66,35 +72,31 @@ def check_exists(music_file, raw_song, meta_tags):
def download_list(tracks_file, skip_file=None, write_successful_file=None): def download_list(tracks_file, skip_file=None, write_successful_file=None):
""" Download all songs from the list. """ """ Download all songs from the list. """
log.info('Checking and removing any duplicate tracks') log.info("Checking and removing any duplicate tracks")
tracks = internals.get_unique_tracks(tracks_file) tracks = internals.get_unique_tracks(tracks_file)
# override file with unique tracks # override file with unique tracks
with open(tracks_file, 'w') as f: with open(tracks_file, "w") as f:
f.write('\n'.join(tracks)) f.write("\n".join(tracks))
# Remove tracks to skip from tracks list # Remove tracks to skip from tracks list
if skip_file is not None: if skip_file is not None:
skip_tracks = internals.get_unique_tracks(skip_file) skip_tracks = internals.get_unique_tracks(skip_file)
len_before = len(tracks) len_before = len(tracks)
tracks = [ tracks = [track for track in tracks if track not in skip_tracks]
track for track in tracks log.info("Skipping {} tracks".format(len_before - len(tracks)))
if track not in skip_tracks
]
log.info('Skipping {} tracks'.format(len_before - len(tracks)))
log.info(u"Preparing to download {} songs".format(len(tracks)))
log.info(u'Preparing to download {} songs'.format(len(tracks)))
downloaded_songs = [] downloaded_songs = []
for number, raw_song in enumerate(tracks, 1): for number, raw_song in enumerate(tracks, 1):
print('') print("")
try: try:
download_single(raw_song, number=number) download_single(raw_song, number=number)
# token expires after 1 hour # token expires after 1 hour
except spotipy.client.SpotifyException: except spotipy.client.SpotifyException:
# refresh token when it expires # refresh token when it expires
log.debug('Token expired, generating new one and authorizing') log.debug("Token expired, generating new one and authorizing")
new_token = spotify_tools.generate_token() new_token = spotify_tools.generate_token()
spotify_tools.spotify = spotipy.Spotify(auth=new_token) spotify_tools.spotify = spotipy.Spotify(auth=new_token)
download_single(raw_song, number=number) download_single(raw_song, number=number)
@@ -104,20 +106,20 @@ def download_list(tracks_file, skip_file=None, write_successful_file=None):
# remove the downloaded song from file # remove the downloaded song from file
internals.trim_song(tracks_file) internals.trim_song(tracks_file)
# and append it at the end of file # and append it at the end of file
with open(tracks_file, 'a') as f: with open(tracks_file, "a") as f:
f.write('\n' + raw_song) f.write("\n" + raw_song)
log.warning('Failed to download song. Will retry after other songs\n') log.warning("Failed to download song. Will retry after other songs\n")
# wait 0.5 sec to avoid infinite looping # wait 0.5 sec to avoid infinite looping
time.sleep(0.5) time.sleep(0.5)
continue continue
downloaded_songs.append(raw_song) downloaded_songs.append(raw_song)
# Add track to file of successful downloads # Add track to file of successful downloads
log.debug('Adding downloaded song to write successful file') log.debug("Adding downloaded song to write successful file")
if write_successful_file is not None: if write_successful_file is not None:
with open(write_successful_file, 'a') as f: with open(write_successful_file, "a") as f:
f.write('\n' + raw_song) f.write("\n" + raw_song)
log.debug('Removing downloaded song from tracks file') log.debug("Removing downloaded song from tracks file")
internals.trim_song(tracks_file) internals.trim_song(tracks_file)
return downloaded_songs return downloaded_songs
@@ -127,39 +129,41 @@ def download_single(raw_song, number=None):
""" Logic behind downloading a song. """ """ Logic behind downloading a song. """
if internals.is_youtube(raw_song): if internals.is_youtube(raw_song):
log.debug('Input song is a YouTube URL') log.debug("Input song is a YouTube URL")
content = youtube_tools.go_pafy(raw_song, meta_tags=None) content = youtube_tools.go_pafy(raw_song, meta_tags=None)
raw_song = slugify(content.title).replace('-', ' ') raw_song = slugify(content.title).replace("-", " ")
meta_tags = spotify_tools.generate_metadata(raw_song) meta_tags = spotify_tools.generate_metadata(raw_song)
else: else:
meta_tags = spotify_tools.generate_metadata(raw_song) meta_tags = spotify_tools.generate_metadata(raw_song)
content = youtube_tools.go_pafy(raw_song, meta_tags) content = youtube_tools.go_pafy(raw_song, meta_tags)
if content is None: if content is None:
log.debug('Found no matching video') log.debug("Found no matching video")
return return
if const.args.download_only_metadata and meta_tags is None: if const.args.download_only_metadata and meta_tags is None:
log.info('Found no metadata. Skipping the download') log.info("Found no metadata. Skipping the download")
return return
# "[number]. [artist] - [song]" if downloading from list # "[number]. [artist] - [song]" if downloading from list
# otherwise "[artist] - [song]" # otherwise "[artist] - [song]"
youtube_title = youtube_tools.get_youtube_title(content, number) youtube_title = youtube_tools.get_youtube_title(content, number)
log.info('{} ({})'.format(youtube_title, content.watchv_url)) log.info("{} ({})".format(youtube_title, content.watchv_url))
# generate file name of the song to download # generate file name of the song to download
songname = content.title songname = content.title
if meta_tags is not None: if meta_tags is not None:
refined_songname = internals.format_string(const.args.file_format, refined_songname = internals.format_string(
meta_tags, const.args.file_format, meta_tags, slugification=True
slugification=True) )
log.debug('Refining songname from "{0}" to "{1}"'.format(songname, refined_songname)) log.debug(
if not refined_songname == ' - ': 'Refining songname from "{0}" to "{1}"'.format(songname, refined_songname)
)
if not refined_songname == " - ":
songname = refined_songname songname = refined_songname
else: else:
log.warning('Could not find metadata') log.warning("Could not find metadata")
songname = internals.sanitize_title(songname) songname = internals.sanitize_title(songname)
if const.args.dry_run: if const.args.dry_run:
@@ -172,13 +176,18 @@ def download_single(raw_song, number=None):
input_song = songname + const.args.input_ext input_song = songname + const.args.input_ext
output_song = songname + const.args.output_ext output_song = songname + const.args.output_ext
if youtube_tools.download_song(input_song, content): if youtube_tools.download_song(input_song, content):
print('') print("")
try: try:
convert.song(input_song, output_song, const.args.folder, convert.song(
avconv=const.args.avconv, trim_silence=const.args.trim_silence) input_song,
output_song,
const.args.folder,
avconv=const.args.avconv,
trim_silence=const.args.trim_silence,
)
except FileNotFoundError: except FileNotFoundError:
encoder = 'avconv' if const.args.avconv else 'ffmpeg' encoder = "avconv" if const.args.avconv else "ffmpeg"
log.warning('Could not find {0}, skipping conversion'.format(encoder)) log.warning("Could not find {0}, skipping conversion".format(encoder))
const.args.output_ext = const.args.input_ext const.args.output_ext = const.args.input_ext
output_song = songname + const.args.output_ext output_song = songname + const.args.output_ext
@@ -193,7 +202,7 @@ def main():
const.args = handle.get_arguments() const.args = handle.get_arguments()
if const.args.version: if const.args.version:
print('spotdl {version}'.format(version=__version__)) print("spotdl {version}".format(version=__version__))
sys.exit() sys.exit()
internals.filter_path(const.args.folder) internals.filter_path(const.args.folder)
@@ -201,8 +210,8 @@ def main():
logzero.setup_default_logger(formatter=const._formatter, level=const.args.log_level) logzero.setup_default_logger(formatter=const._formatter, level=const.args.log_level)
log.debug('Python version: {}'.format(sys.version)) log.debug("Python version: {}".format(sys.version))
log.debug('Platform: {}'.format(platform.platform())) log.debug("Platform: {}".format(platform.platform()))
log.debug(pprint.pformat(const.args.__dict__)) log.debug(pprint.pformat(const.args.__dict__))
try: try:
@@ -212,7 +221,7 @@ def main():
download_list( download_list(
tracks_file=const.args.list, tracks_file=const.args.list,
skip_file=const.args.skip, skip_file=const.args.skip,
write_successful_file=const.args.write_successful write_successful_file=const.args.write_successful,
) )
elif const.args.playlist: elif const.args.playlist:
spotify_tools.write_playlist(playlist_url=const.args.playlist) spotify_tools.write_playlist(playlist_url=const.args.playlist)
@@ -230,5 +239,5 @@ def main():
sys.exit(3) sys.exit(3)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -14,11 +14,13 @@ import sys
def generate_token(): def generate_token():
""" Generate the token. Please respect these credentials :) """ """ Generate the token. Please respect these credentials :) """
credentials = oauth2.SpotifyClientCredentials( credentials = oauth2.SpotifyClientCredentials(
client_id='4fe3fecfe5334023a1472516cc99d805', client_id="4fe3fecfe5334023a1472516cc99d805",
client_secret='0f02b7c483c04257984695007a4a8d5c') client_secret="0f02b7c483c04257984695007a4a8d5c",
)
token = credentials.get_access_token() token = credentials.get_access_token()
return token return token
# token is mandatory when using Spotify's API # token is mandatory when using Spotify's API
# https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/ # https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/
token = generate_token() token = generate_token()
@@ -29,51 +31,51 @@ def generate_metadata(raw_song):
""" Fetch a song's metadata from Spotify. """ """ Fetch a song's metadata from Spotify. """
if internals.is_spotify(raw_song): if internals.is_spotify(raw_song):
# fetch track information directly if it is spotify link # fetch track information directly if it is spotify link
log.debug('Fetching metadata for given track URL') log.debug("Fetching metadata for given track URL")
meta_tags = spotify.track(raw_song) meta_tags = spotify.track(raw_song)
else: else:
# otherwise search on spotify and fetch information from first result # otherwise search on spotify and fetch information from first result
log.debug('Searching for "{}" on Spotify'.format(raw_song)) log.debug('Searching for "{}" on Spotify'.format(raw_song))
try: try:
meta_tags = spotify.search(raw_song, limit=1)['tracks']['items'][0] meta_tags = spotify.search(raw_song, limit=1)["tracks"]["items"][0]
except IndexError: except IndexError:
return None return None
artist = spotify.artist(meta_tags['artists'][0]['id']) artist = spotify.artist(meta_tags["artists"][0]["id"])
album = spotify.album(meta_tags['album']['id']) album = spotify.album(meta_tags["album"]["id"])
try: try:
meta_tags[u'genre'] = titlecase(artist['genres'][0]) meta_tags[u"genre"] = titlecase(artist["genres"][0])
except IndexError: except IndexError:
meta_tags[u'genre'] = None meta_tags[u"genre"] = None
try: try:
meta_tags[u'copyright'] = album['copyrights'][0]['text'] meta_tags[u"copyright"] = album["copyrights"][0]["text"]
except IndexError: except IndexError:
meta_tags[u'copyright'] = None meta_tags[u"copyright"] = None
try: try:
meta_tags[u'external_ids'][u'isrc'] meta_tags[u"external_ids"][u"isrc"]
except KeyError: except KeyError:
meta_tags[u'external_ids'][u'isrc'] = None meta_tags[u"external_ids"][u"isrc"] = None
meta_tags[u'release_date'] = album['release_date'] meta_tags[u"release_date"] = album["release_date"]
meta_tags[u'publisher'] = album['label'] meta_tags[u"publisher"] = album["label"]
meta_tags[u'total_tracks'] = album['tracks']['total'] meta_tags[u"total_tracks"] = album["tracks"]["total"]
log.debug('Fetching lyrics') log.debug("Fetching lyrics")
try: try:
meta_tags['lyrics'] = lyricwikia.get_lyrics( meta_tags["lyrics"] = lyricwikia.get_lyrics(
meta_tags['artists'][0]['name'], meta_tags["artists"][0]["name"], meta_tags["name"]
meta_tags['name']) )
except lyricwikia.LyricsNotFound: except lyricwikia.LyricsNotFound:
meta_tags['lyrics'] = None meta_tags["lyrics"] = None
# Some sugar # Some sugar
meta_tags['year'], *_ = meta_tags['release_date'].split('-') meta_tags["year"], *_ = meta_tags["release_date"].split("-")
meta_tags['duration'] = meta_tags['duration_ms'] / 1000.0 meta_tags["duration"] = meta_tags["duration_ms"] / 1000.0
# Remove unwanted parameters # Remove unwanted parameters
del meta_tags['duration_ms'] del meta_tags["duration_ms"]
del meta_tags['available_markets'] del meta_tags["available_markets"]
del meta_tags['album']['available_markets'] del meta_tags["album"]["available_markets"]
log.debug(pprint.pformat(meta_tags)) log.debug(pprint.pformat(meta_tags))
return meta_tags return meta_tags
@@ -92,18 +94,20 @@ def get_playlists(username):
check = 1 check = 1
while True: while True:
for playlist in playlists['items']: for playlist in playlists["items"]:
# in rare cases, playlists may not be found, so playlists['next'] # in rare cases, playlists may not be found, so playlists['next']
# is None. Skip these. Also see Issue #91. # is None. Skip these. Also see Issue #91.
if playlist['name'] is not None: if playlist["name"] is not None:
log.info(u'{0:>5}. {1:<30} ({2} tracks)'.format( log.info(
check, playlist['name'], u"{0:>5}. {1:<30} ({2} tracks)".format(
playlist['tracks']['total'])) check, playlist["name"], playlist["tracks"]["total"]
playlist_url = playlist['external_urls']['spotify'] )
)
playlist_url = playlist["external_urls"]["spotify"]
log.debug(playlist_url) log.debug(playlist_url)
links.append(playlist_url) links.append(playlist_url)
check += 1 check += 1
if playlists['next']: if playlists["next"]:
playlists = spotify.next(playlists) playlists = spotify.next(playlists)
else: else:
break break
@@ -117,15 +121,16 @@ def fetch_playlist(playlist):
username = splits[-3] username = splits[-3]
except IndexError: except IndexError:
# Wrong format, in either case # Wrong format, in either case
log.error('The provided playlist URL is not in a recognized format!') log.error("The provided playlist URL is not in a recognized format!")
sys.exit(10) sys.exit(10)
playlist_id = splits[-1] playlist_id = splits[-1]
try: try:
results = spotify.user_playlist(username, playlist_id, results = spotify.user_playlist(
fields='tracks,next,name') username, playlist_id, fields="tracks,next,name"
)
except spotipy.client.SpotifyException: except spotipy.client.SpotifyException:
log.error('Unable to find playlist') log.error("Unable to find playlist")
log.info('Make sure the playlist is set to publicly visible and then try again') log.info("Make sure the playlist is set to publicly visible and then try again")
sys.exit(11) sys.exit(11)
return results return results
@@ -133,9 +138,9 @@ def fetch_playlist(playlist):
def write_playlist(playlist_url, text_file=None): def write_playlist(playlist_url, text_file=None):
playlist = fetch_playlist(playlist_url) playlist = fetch_playlist(playlist_url)
tracks = playlist['tracks'] tracks = playlist["tracks"]
if not text_file: if not text_file:
text_file = u'{0}.txt'.format(slugify(playlist['name'], ok='-_()[]{}')) text_file = u"{0}.txt".format(slugify(playlist["name"], ok="-_()[]{}"))
return write_tracks(tracks, text_file) return write_tracks(tracks, text_file)
@@ -148,34 +153,36 @@ def fetch_album(album):
def write_album(album_url, text_file=None): def write_album(album_url, text_file=None):
album = fetch_album(album_url) album = fetch_album(album_url)
tracks = spotify.album_tracks(album['id']) tracks = spotify.album_tracks(album["id"])
if not text_file: if not text_file:
text_file = u'{0}.txt'.format(slugify(album['name'], ok='-_()[]{}')) text_file = u"{0}.txt".format(slugify(album["name"], ok="-_()[]{}"))
return write_tracks(tracks, text_file) return write_tracks(tracks, text_file)
def write_tracks(tracks, text_file): def write_tracks(tracks, text_file):
log.info(u'Writing {0} tracks to {1}'.format( log.info(u"Writing {0} tracks to {1}".format(tracks["total"], text_file))
tracks['total'], text_file))
track_urls = [] track_urls = []
with open(text_file, 'a') as file_out: with open(text_file, "a") as file_out:
while True: while True:
for item in tracks['items']: for item in tracks["items"]:
if 'track' in item: if "track" in item:
track = item['track'] track = item["track"]
else: else:
track = item track = item
try: try:
track_url = track['external_urls']['spotify'] track_url = track["external_urls"]["spotify"]
log.debug(track_url) log.debug(track_url)
file_out.write(track_url + '\n') file_out.write(track_url + "\n")
track_urls.append(track_url) track_urls.append(track_url)
except KeyError: except KeyError:
log.warning(u'Skipping track {0} by {1} (local only?)'.format( log.warning(
track['name'], track['artists'][0]['name'])) u"Skipping track {0} by {1} (local only?)".format(
track["name"], track["artists"][0]["name"]
)
)
# 1 page = 50 results # 1 page = 50 results
# check if there are more pages # check if there are more pages
if tracks['next']: if tracks["next"]:
tracks = spotify.next(tracks) tracks = spotify.next(tracks)
else: else:
break break

View File

@@ -11,7 +11,7 @@ import pprint
# Fix download speed throttle on short duration tracks # Fix download speed throttle on short duration tracks
# Read more on mps-youtube/pafy#199 # Read more on mps-youtube/pafy#199
pafy.g.opener.addheaders.append(('Range', 'bytes=0-')) pafy.g.opener.addheaders.append(("Range", "bytes=0-"))
def set_api_key(): def set_api_key():
@@ -19,7 +19,7 @@ def set_api_key():
key = const.args.youtube_api_key key = const.args.youtube_api_key
else: else:
# Please respect this YouTube token :) # Please respect this YouTube token :)
key = 'AIzaSyC6cEeKlxtOPybk9sEe5ksFN5sB-7wzYp0' key = "AIzaSyC6cEeKlxtOPybk9sEe5ksFN5sB-7wzYp0"
pafy.set_api_key(key) pafy.set_api_key(key)
@@ -42,7 +42,7 @@ def get_youtube_title(content, number=None):
""" Get the YouTube video's title. """ """ Get the YouTube video's title. """
title = content.title title = content.title
if number: if number:
return '{0}. {1}'.format(number, title) return "{0}. {1}".format(number, title)
else: else:
return title return title
@@ -50,20 +50,20 @@ def get_youtube_title(content, number=None):
def download_song(file_name, content): def download_song(file_name, content):
""" Download the audio file from YouTube. """ """ Download the audio file from YouTube. """
_, extension = os.path.splitext(file_name) _, extension = os.path.splitext(file_name)
if extension in ('.webm', '.m4a'): if extension in (".webm", ".m4a"):
link = content.getbestaudio(preftype=extension[1:]) link = content.getbestaudio(preftype=extension[1:])
else: else:
log.debug('No audio streams available for {} type'.format(extension)) log.debug("No audio streams available for {} type".format(extension))
return False return False
if link: if link:
log.debug('Downloading from URL: ' + link.url) log.debug("Downloading from URL: " + link.url)
filepath = os.path.join(const.args.folder, file_name) filepath = os.path.join(const.args.folder, file_name)
log.debug('Saving to: ' + filepath) log.debug("Saving to: " + filepath)
link.download(filepath=filepath) link.download(filepath=filepath)
return True return True
else: else:
log.debug('No audio streams available') log.debug("No audio streams available")
return False return False
@@ -72,23 +72,25 @@ def generate_search_url(query):
# urllib.request.quote() encodes string with special characters # urllib.request.quote() encodes string with special characters
quoted_query = urllib.request.quote(query) quoted_query = urllib.request.quote(query)
# Special YouTube URL filter to search only for videos # Special YouTube URL filter to search only for videos
url = 'https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={0}'.format(quoted_query) url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={0}".format(
quoted_query
)
return url return url
def is_video(result): def is_video(result):
# ensure result is not a channel # ensure result is not a channel
not_video = result.find('channel') is not None or \ not_video = (
'yt-lockup-channel' in result.parent.attrs['class'] or \ result.find("channel") is not None
'yt-lockup-channel' in result.attrs['class'] or "yt-lockup-channel" in result.parent.attrs["class"]
or "yt-lockup-channel" in result.attrs["class"]
)
# ensure result is not a mix/playlist # ensure result is not a mix/playlist
not_video = not_video or \ not_video = not_video or "yt-lockup-playlist" in result.parent.attrs["class"]
'yt-lockup-playlist' in result.parent.attrs['class']
# ensure video result is not an advertisement # ensure video result is not an advertisement
not_video = not_video or \ not_video = not_video or result.find("googleads") is not None
result.find('googleads') is not None
video = not not_video video = not not_video
return video return video
@@ -111,18 +113,25 @@ class GenerateYouTubeURL:
if meta_tags is None: if meta_tags is None:
self.search_query = raw_song self.search_query = raw_song
else: else:
self.search_query = internals.format_string(const.args.search_format, self.search_query = internals.format_string(
meta_tags, force_spaces=True) const.args.search_format, meta_tags, force_spaces=True
)
def _best_match(self, videos): def _best_match(self, videos):
""" Select the best matching video from a list of videos. """ """ Select the best matching video from a list of videos. """
if const.args.manual: if const.args.manual:
log.info(self.raw_song) log.info(self.raw_song)
log.info('0. Skip downloading this song.\n') log.info("0. Skip downloading this song.\n")
# fetch all video links on first page on YouTube # fetch all video links on first page on YouTube
for i, v in enumerate(videos): for i, v in enumerate(videos):
log.info(u'{0}. {1} {2} {3}'.format(i+1, v['title'], v['videotime'], log.info(
"http://youtube.com/watch?v="+v['link'])) u"{0}. {1} {2} {3}".format(
i + 1,
v["title"],
v["videotime"],
"http://youtube.com/watch?v=" + v["link"],
)
)
# let user select the song to download # let user select the song to download
result = internals.input_link(videos) result = internals.input_link(videos)
if result is None: if result is None:
@@ -132,7 +141,9 @@ class GenerateYouTubeURL:
# if the metadata could not be acquired, take the first result # if the metadata could not be acquired, take the first result
# from Youtube because the proper song length is unknown # from Youtube because the proper song length is unknown
result = videos[0] result = videos[0]
log.debug('Since no metadata found on Spotify, going with the first result') log.debug(
"Since no metadata found on Spotify, going with the first result"
)
else: else:
# filter out videos that do not have a similar length to the Spotify song # filter out videos that do not have a similar length to the Spotify song
duration_tolerance = 10 duration_tolerance = 10
@@ -143,16 +154,27 @@ class GenerateYouTubeURL:
# until one of the Youtube results falls within the correct duration or # until one of the Youtube results falls within the correct duration or
# the duration_tolerance has reached the max_duration_tolerance # the duration_tolerance has reached the max_duration_tolerance
while len(possible_videos_by_duration) == 0: while len(possible_videos_by_duration) == 0:
possible_videos_by_duration = list(filter(lambda x: abs(x['seconds'] - self.meta_tags['duration']) <= duration_tolerance, videos)) possible_videos_by_duration = list(
filter(
lambda x: abs(x["seconds"] - self.meta_tags["duration"])
<= duration_tolerance,
videos,
)
)
duration_tolerance += 1 duration_tolerance += 1
if duration_tolerance > max_duration_tolerance: if duration_tolerance > max_duration_tolerance:
log.error("{0} by {1} was not found.\n".format(self.meta_tags['name'], self.meta_tags['artists'][0]['name'])) log.error(
"{0} by {1} was not found.\n".format(
self.meta_tags["name"],
self.meta_tags["artists"][0]["name"],
)
)
return None return None
result = possible_videos_by_duration[0] result = possible_videos_by_duration[0]
if result: if result:
url = "http://youtube.com/watch?v={0}".format(result['link']) url = "http://youtube.com/watch?v={0}".format(result["link"])
else: else:
url = None url = None
@@ -163,33 +185,41 @@ class GenerateYouTubeURL:
# prevents an infinite loop but allows for a few retries # prevents an infinite loop but allows for a few retries
if tries_remaining == 0: if tries_remaining == 0:
log.debug('No tries left. I quit.') log.debug("No tries left. I quit.")
return return
search_url = generate_search_url(self.search_query) search_url = generate_search_url(self.search_query)
log.debug('Opening URL: {0}'.format(search_url)) log.debug("Opening URL: {0}".format(search_url))
item = urllib.request.urlopen(search_url).read() item = urllib.request.urlopen(search_url).read()
items_parse = BeautifulSoup(item, "html.parser") items_parse = BeautifulSoup(item, "html.parser")
videos = [] videos = []
for x in items_parse.find_all('div', {'class': 'yt-lockup-dismissable yt-uix-tile'}): for x in items_parse.find_all(
"div", {"class": "yt-lockup-dismissable yt-uix-tile"}
):
if not is_video(x): if not is_video(x):
continue continue
y = x.find('div', class_='yt-lockup-content') y = x.find("div", class_="yt-lockup-content")
link = y.find('a')['href'][-11:] link = y.find("a")["href"][-11:]
title = y.find('a')['title'] title = y.find("a")["title"]
try: try:
videotime = x.find('span', class_="video-time").get_text() videotime = x.find("span", class_="video-time").get_text()
except AttributeError: except AttributeError:
log.debug('Could not find video duration on YouTube, retrying..') log.debug("Could not find video duration on YouTube, retrying..")
return self.scrape(bestmatch=bestmatch, tries_remaining=tries_remaining-1) return self.scrape(
bestmatch=bestmatch, tries_remaining=tries_remaining - 1
)
youtubedetails = {'link': link, 'title': title, 'videotime': videotime, youtubedetails = {
'seconds': internals.get_sec(videotime)} "link": link,
"title": title,
"videotime": videotime,
"seconds": internals.get_sec(videotime),
}
videos.append(youtubedetails) videos.append(youtubedetails)
if bestmatch: if bestmatch:
@@ -197,40 +227,43 @@ class GenerateYouTubeURL:
return videos return videos
def api(self, bestmatch=True): def api(self, bestmatch=True):
""" Use YouTube API to search and return a list of matching videos. """ """ Use YouTube API to search and return a list of matching videos. """
query = { 'part' : 'snippet', query = {"part": "snippet", "maxResults": 50, "type": "video"}
'maxResults' : 50,
'type' : 'video' }
if const.args.music_videos_only: if const.args.music_videos_only:
query['videoCategoryId'] = '10' query["videoCategoryId"] = "10"
if not self.meta_tags: if not self.meta_tags:
song = self.raw_song song = self.raw_song
query['q'] = song query["q"] = song
else: else:
query['q'] = self.search_query query["q"] = self.search_query
log.debug('query: {0}'.format(query)) log.debug("query: {0}".format(query))
data = pafy.call_gdata('search', query) data = pafy.call_gdata("search", query)
data['items'] = list(filter(lambda x: x['id'].get('videoId') is not None, data["items"] = list(
data['items'])) filter(lambda x: x["id"].get("videoId") is not None, data["items"])
query_results = {'part': 'contentDetails,snippet,statistics', )
'maxResults': 50, query_results = {
'id': ','.join(i['id']['videoId'] for i in data['items'])} "part": "contentDetails,snippet,statistics",
log.debug('query_results: {0}'.format(query_results)) "maxResults": 50,
"id": ",".join(i["id"]["videoId"] for i in data["items"]),
}
log.debug("query_results: {0}".format(query_results))
vdata = pafy.call_gdata('videos', query_results) vdata = pafy.call_gdata("videos", query_results)
videos = [] videos = []
for x in vdata['items']: for x in vdata["items"]:
duration_s = pafy.playlist.parseISO8591(x['contentDetails']['duration']) duration_s = pafy.playlist.parseISO8591(x["contentDetails"]["duration"])
youtubedetails = {'link': x['id'], 'title': x['snippet']['title'], youtubedetails = {
'videotime':internals.videotime_from_seconds(duration_s), "link": x["id"],
'seconds': duration_s} "title": x["snippet"]["title"],
"videotime": internals.videotime_from_seconds(duration_s),
"seconds": duration_s,
}
videos.append(youtubedetails) videos.append(youtubedetails)
if bestmatch: if bestmatch:

View File

@@ -6,8 +6,8 @@ import pytest
def load_defaults(): def load_defaults():
const.args = handle.get_arguments(raw_args='', to_group=False, to_merge=False) const.args = handle.get_arguments(raw_args="", to_group=False, to_merge=False)
const.args.overwrite = 'skip' const.args.overwrite = "skip"
const.args.log_level = 10 const.args.log_level = 10
spotdl.args = const.args spotdl.args = const.args

View File

@@ -7,14 +7,14 @@ import loader
loader.load_defaults() loader.load_defaults()
TRACK_URL = 'http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU' TRACK_URL = "http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU"
def test_dry_download_list(tmpdir): def test_dry_download_list(tmpdir):
const.args.folder = str(tmpdir) const.args.folder = str(tmpdir)
const.args.dry_run = True const.args.dry_run = True
file_path = os.path.join(const.args.folder, 'test_list.txt') file_path = os.path.join(const.args.folder, "test_list.txt")
with open(file_path, 'w') as f: with open(file_path, "w") as f:
f.write(TRACK_URL) f.write(TRACK_URL)
downloaded_song, *_ = spotdl.download_list(file_path) downloaded_song, *_ = spotdl.download_list(file_path)
assert downloaded_song == TRACK_URL assert downloaded_song == TRACK_URL

View File

@@ -11,36 +11,37 @@ import yaml
def test_log_str_to_int(): def test_log_str_to_int():
expect_levels = [20, 30, 40, 10] expect_levels = [20, 30, 40, 10]
levels = [handle.log_leveller(level) levels = [handle.log_leveller(level) for level in handle._LOG_LEVELS_STR]
for level in handle._LOG_LEVELS_STR]
assert levels == expect_levels assert levels == expect_levels
class TestConfig: class TestConfig:
def test_default_config(self, tmpdir): def test_default_config(self, tmpdir):
expect_config = handle.default_conf['spotify-downloader'] expect_config = handle.default_conf["spotify-downloader"]
global config_path global config_path
config_path = os.path.join(str(tmpdir), 'config.yml') config_path = os.path.join(str(tmpdir), "config.yml")
config = handle.get_config(config_path) config = handle.get_config(config_path)
assert config == expect_config assert config == expect_config
def test_modified_config(self): def test_modified_config(self):
global modified_config global modified_config
modified_config = dict(handle.default_conf) modified_config = dict(handle.default_conf)
modified_config['spotify-downloader']['file-format'] = 'just_a_test' modified_config["spotify-downloader"]["file-format"] = "just_a_test"
merged_config = handle.merge(handle.default_conf, modified_config) merged_config = handle.merge(handle.default_conf, modified_config)
assert merged_config == modified_config assert merged_config == modified_config
def test_custom_config_path(self, tmpdir): def test_custom_config_path(self, tmpdir):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
with open(config_path, 'w') as config_file: with open(config_path, "w") as config_file:
yaml.dump(modified_config, config_file, default_flow_style=False) yaml.dump(modified_config, config_file, default_flow_style=False)
overridden_config = handle.override_config(config_path, overridden_config = handle.override_config(config_path, parser, raw_args="")
parser, modified_values = [
raw_args='') str(value) for value in modified_config["spotify-downloader"].values()
modified_values = [ str(value) for value in modified_config['spotify-downloader'].values() ] ]
overridden_config.folder = os.path.realpath(overridden_config.folder) overridden_config.folder = os.path.realpath(overridden_config.folder)
overridden_values = [ str(value) for value in overridden_config.__dict__.values() ] overridden_values = [
str(value) for value in overridden_config.__dict__.values()
]
assert sorted(overridden_values) == sorted(modified_values) assert sorted(overridden_values) == sorted(modified_values)

View File

@@ -8,38 +8,47 @@ import pytest
DUPLICATE_TRACKS_TEST_TABLE = [ DUPLICATE_TRACKS_TEST_TABLE = [
(('https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ', (
'https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ'), (
('https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ',)), "https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ",
"https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ",
(('https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ', ),
'', ("https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ",),
'https://open.spotify.com/track/3SipFlNddvL0XNZRLXvdZD'), ),
('https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ', (
'https://open.spotify.com/track/3SipFlNddvL0XNZRLXvdZD')), (
"https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ",
(('ncs fade', "",
'https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ', "https://open.spotify.com/track/3SipFlNddvL0XNZRLXvdZD",
'', ),
'ncs fade'), (
('ncs fade', "https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ",
'https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ')), "https://open.spotify.com/track/3SipFlNddvL0XNZRLXvdZD",
),
(('ncs spectre ', ),
' https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ', (
''), (
('ncs spectre', "ncs fade",
'https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ')) "https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ",
"",
"ncs fade",
),
("ncs fade", "https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ"),
),
(
("ncs spectre ", " https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ", ""),
("ncs spectre", "https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ"),
),
] ]
def test_default_music_directory(): def test_default_music_directory():
if sys.platform.startswith('linux'): if sys.platform.startswith("linux"):
output = subprocess.check_output(['xdg-user-dir', 'MUSIC']) output = subprocess.check_output(["xdg-user-dir", "MUSIC"])
expect_directory = output.decode('utf-8').rstrip() expect_directory = output.decode("utf-8").rstrip()
else: else:
home = os.path.expanduser('~') home = os.path.expanduser("~")
expect_directory = os.path.join(home, 'Music') expect_directory = os.path.join(home, "Music")
directory = internals.get_music_dir() directory = internals.get_music_dir()
assert directory == expect_directory assert directory == expect_directory
@@ -49,15 +58,15 @@ class TestPathFilterer:
def test_create_directory(self, tmpdir): def test_create_directory(self, tmpdir):
expect_path = True expect_path = True
global folder_path global folder_path
folder_path = os.path.join(str(tmpdir), 'filter_this_folder') folder_path = os.path.join(str(tmpdir), "filter_this_folder")
internals.filter_path(folder_path) internals.filter_path(folder_path)
is_path = os.path.isdir(folder_path) is_path = os.path.isdir(folder_path)
assert is_path == expect_path assert is_path == expect_path
def test_remove_temp_files(self, tmpdir): def test_remove_temp_files(self, tmpdir):
expect_file = False expect_file = False
file_path = os.path.join(folder_path, 'pesky_file.temp') file_path = os.path.join(folder_path, "pesky_file.temp")
open(file_path, 'a') open(file_path, "a")
internals.filter_path(folder_path) internals.filter_path(folder_path)
is_file = os.path.isfile(file_path) is_file = os.path.isfile(file_path)
assert is_file == expect_file assert is_file == expect_file
@@ -65,17 +74,17 @@ class TestPathFilterer:
class TestVideoTimeFromSeconds: class TestVideoTimeFromSeconds:
def test_from_seconds(self): def test_from_seconds(self):
expect_duration = '35' expect_duration = "35"
duration = internals.videotime_from_seconds(35) duration = internals.videotime_from_seconds(35)
assert duration == expect_duration assert duration == expect_duration
def test_from_minutes(self): def test_from_minutes(self):
expect_duration = '2:38' expect_duration = "2:38"
duration = internals.videotime_from_seconds(158) duration = internals.videotime_from_seconds(158)
assert duration == expect_duration assert duration == expect_duration
def test_from_hours(self): def test_from_hours(self):
expect_duration = '1:16:02' expect_duration = "1:16:02"
duration = internals.videotime_from_seconds(4562) duration = internals.videotime_from_seconds(4562)
assert duration == expect_duration assert duration == expect_duration
@@ -83,37 +92,37 @@ class TestVideoTimeFromSeconds:
class TestGetSeconds: class TestGetSeconds:
def test_from_seconds(self): def test_from_seconds(self):
expect_secs = 45 expect_secs = 45
secs = internals.get_sec('0:45') secs = internals.get_sec("0:45")
assert secs == expect_secs assert secs == expect_secs
secs = internals.get_sec('0.45') secs = internals.get_sec("0.45")
assert secs == expect_secs assert secs == expect_secs
def test_from_minutes(self): def test_from_minutes(self):
expect_secs = 213 expect_secs = 213
secs = internals.get_sec('3.33') secs = internals.get_sec("3.33")
assert secs == expect_secs assert secs == expect_secs
secs = internals.get_sec('3:33') secs = internals.get_sec("3:33")
assert secs == expect_secs assert secs == expect_secs
def test_from_hours(self): def test_from_hours(self):
expect_secs = 5405 expect_secs = 5405
secs = internals.get_sec('1.30.05') secs = internals.get_sec("1.30.05")
assert secs == expect_secs assert secs == expect_secs
secs = internals.get_sec('1:30:05') secs = internals.get_sec("1:30:05")
assert secs == expect_secs assert secs == expect_secs
def test_raise_error(self): def test_raise_error(self):
with pytest.raises(ValueError): with pytest.raises(ValueError):
internals.get_sec('10*05') internals.get_sec("10*05")
with pytest.raises(ValueError): with pytest.raises(ValueError):
internals.get_sec('02,28,46') internals.get_sec("02,28,46")
@pytest.mark.parametrize("duplicates, expected", DUPLICATE_TRACKS_TEST_TABLE) @pytest.mark.parametrize("duplicates, expected", DUPLICATE_TRACKS_TEST_TABLE)
def test_get_unique_tracks(tmpdir, duplicates, expected): def test_get_unique_tracks(tmpdir, duplicates, expected):
file_path = os.path.join(str(tmpdir), 'test_duplicates.txt') file_path = os.path.join(str(tmpdir), "test_duplicates.txt")
with open(file_path, 'w') as f: with open(file_path, "w") as f:
f.write('\n'.join(duplicates)) f.write("\n".join(duplicates))
unique_tracks = internals.get_unique_tracks(file_path) unique_tracks = internals.get_unique_tracks(file_path)
assert tuple(unique_tracks) == expected assert tuple(unique_tracks) == expected

View File

@@ -5,25 +5,25 @@ from spotdl import spotify_tools
from spotdl import const from spotdl import const
from spotdl import spotdl from spotdl import spotdl
PLAYLIST_URL = 'https://open.spotify.com/user/alex/playlist/0iWOVoumWlkXIrrBTSJmN8' PLAYLIST_URL = "https://open.spotify.com/user/alex/playlist/0iWOVoumWlkXIrrBTSJmN8"
ALBUM_URL = 'https://open.spotify.com/album/499J8bIsEnU7DSrosFDJJg' ALBUM_URL = "https://open.spotify.com/album/499J8bIsEnU7DSrosFDJJg"
def test_user_playlists(tmpdir, monkeypatch): def test_user_playlists(tmpdir, monkeypatch):
expect_tracks = 14 expect_tracks = 14
text_file = os.path.join(str(tmpdir), 'test_us.txt') text_file = os.path.join(str(tmpdir), "test_us.txt")
monkeypatch.setattr('builtins.input', lambda x: 1) monkeypatch.setattr("builtins.input", lambda x: 1)
spotify_tools.write_user_playlist('alex', text_file) spotify_tools.write_user_playlist("alex", text_file)
with open(text_file, 'r') as f: with open(text_file, "r") as f:
tracks = len(f.readlines()) tracks = len(f.readlines())
assert tracks == expect_tracks assert tracks == expect_tracks
def test_playlist(tmpdir): def test_playlist(tmpdir):
expect_tracks = 14 expect_tracks = 14
text_file = os.path.join(str(tmpdir), 'test_pl.txt') text_file = os.path.join(str(tmpdir), "test_pl.txt")
spotify_tools.write_playlist(PLAYLIST_URL, text_file) spotify_tools.write_playlist(PLAYLIST_URL, text_file)
with open(text_file, 'r') as f: with open(text_file, "r") as f:
tracks = len(f.readlines()) tracks = len(f.readlines())
assert tracks == expect_tracks assert tracks == expect_tracks
@@ -31,22 +31,22 @@ def test_playlist(tmpdir):
def test_album(tmpdir): def test_album(tmpdir):
expect_tracks = 15 expect_tracks = 15
global text_file global text_file
text_file = os.path.join(str(tmpdir), 'test_al.txt') text_file = os.path.join(str(tmpdir), "test_al.txt")
spotify_tools.write_album(ALBUM_URL, text_file) spotify_tools.write_album(ALBUM_URL, text_file)
with open(text_file, 'r') as f: with open(text_file, "r") as f:
tracks = len(f.readlines()) tracks = len(f.readlines())
assert tracks == expect_tracks assert tracks == expect_tracks
def test_trim(): def test_trim():
with open(text_file, 'r') as track_file: with open(text_file, "r") as track_file:
tracks = track_file.readlines() tracks = track_file.readlines()
expect_number = len(tracks) - 1 expect_number = len(tracks) - 1
expect_track = tracks[0] expect_track = tracks[0]
track = spotdl.internals.trim_song(text_file) track = spotdl.internals.trim_song(text_file)
with open(text_file, 'r') as track_file: with open(text_file, "r") as track_file:
number = len(track_file.readlines()) number = len(track_file.readlines())
assert expect_number == number and expect_track == track assert expect_number == number and expect_track == track

View File

@@ -12,10 +12,10 @@ import loader
loader.load_defaults() loader.load_defaults()
TRACK_URL = 'http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU' TRACK_URL = "http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU"
EXPECTED_TITLE = 'David André Østby - Intro' EXPECTED_TITLE = "David André Østby - Intro"
EXPECTED_YT_TITLE = 'Intro - David André Østby' EXPECTED_YT_TITLE = "Intro - David André Østby"
EXPECTED_YT_URL = 'http://youtube.com/watch?v=rg1wfcty0BA' EXPECTED_YT_URL = "http://youtube.com/watch?v=rg1wfcty0BA"
def test_metadata(): def test_metadata():
@@ -33,7 +33,7 @@ class TestFileFormat:
def test_without_spaces(self): def test_without_spaces(self):
const.args.no_spaces = True const.args.no_spaces = True
title = internals.format_string(const.args.file_format, meta_tags) title = internals.format_string(const.args.file_format, meta_tags)
assert title == EXPECTED_TITLE.replace(' ', '_') assert title == EXPECTED_TITLE.replace(" ", "_")
def test_youtube_url(): def test_youtube_url():
@@ -62,60 +62,58 @@ def test_check_track_exists_before_download(tmpdir):
class TestDownload: class TestDownload:
def test_m4a(self): def test_m4a(self):
expect_download = True expect_download = True
download = youtube_tools.download_song(file_name + '.m4a', content) download = youtube_tools.download_song(file_name + ".m4a", content)
assert download == expect_download assert download == expect_download
def test_webm(self): def test_webm(self):
expect_download = True expect_download = True
download = youtube_tools.download_song(file_name + '.webm', content) download = youtube_tools.download_song(file_name + ".webm", content)
assert download == expect_download assert download == expect_download
class TestFFmpeg: class TestFFmpeg:
def test_convert_from_webm_to_mp3(self): def test_convert_from_webm_to_mp3(self):
expect_return_code = 0 expect_return_code = 0
return_code = convert.song(file_name + '.webm', return_code = convert.song(
file_name + '.mp3', file_name + ".webm", file_name + ".mp3", const.args.folder
const.args.folder) )
assert return_code == expect_return_code assert return_code == expect_return_code
def test_convert_from_webm_to_m4a(self): def test_convert_from_webm_to_m4a(self):
expect_return_code = 0 expect_return_code = 0
return_code = convert.song(file_name + '.webm', return_code = convert.song(
file_name + '.m4a', file_name + ".webm", file_name + ".m4a", const.args.folder
const.args.folder) )
assert return_code == expect_return_code assert return_code == expect_return_code
def test_convert_from_m4a_to_mp3(self): def test_convert_from_m4a_to_mp3(self):
expect_return_code = 0 expect_return_code = 0
return_code = convert.song(file_name + '.m4a', return_code = convert.song(
file_name + '.mp3', file_name + ".m4a", file_name + ".mp3", const.args.folder
const.args.folder) )
assert return_code == expect_return_code assert return_code == expect_return_code
def test_convert_from_m4a_to_webm(self): def test_convert_from_m4a_to_webm(self):
expect_return_code = 0 expect_return_code = 0
return_code = convert.song(file_name + '.m4a', return_code = convert.song(
file_name + '.webm', file_name + ".m4a", file_name + ".webm", const.args.folder
const.args.folder) )
assert return_code == expect_return_code assert return_code == expect_return_code
def test_convert_from_m4a_to_flac(self): def test_convert_from_m4a_to_flac(self):
expect_return_code = 0 expect_return_code = 0
return_code = convert.song(file_name + '.m4a', return_code = convert.song(
file_name + '.flac', file_name + ".m4a", file_name + ".flac", const.args.folder
const.args.folder) )
assert return_code == expect_return_code assert return_code == expect_return_code
class TestAvconv: class TestAvconv:
def test_convert_from_m4a_to_mp3(self): def test_convert_from_m4a_to_mp3(self):
expect_return_code = 0 expect_return_code = 0
return_code = convert.song(file_name + '.m4a', return_code = convert.song(
file_name + '.mp3', file_name + ".m4a", file_name + ".mp3", const.args.folder, avconv=True
const.args.folder, )
avconv=True)
assert return_code == expect_return_code assert return_code == expect_return_code
@@ -124,30 +122,30 @@ class TestEmbedMetadata:
expect_embed = True expect_embed = True
global track_path global track_path
track_path = os.path.join(const.args.folder, file_name) track_path = os.path.join(const.args.folder, file_name)
embed = metadata.embed(track_path + '.mp3', meta_tags) embed = metadata.embed(track_path + ".mp3", meta_tags)
assert embed == expect_embed assert embed == expect_embed
def test_embed_in_m4a(self): def test_embed_in_m4a(self):
expect_embed = True expect_embed = True
embed = metadata.embed(track_path + '.m4a', meta_tags) embed = metadata.embed(track_path + ".m4a", meta_tags)
os.remove(track_path + '.m4a') os.remove(track_path + ".m4a")
assert embed == expect_embed assert embed == expect_embed
def test_embed_in_webm(self): def test_embed_in_webm(self):
expect_embed = False expect_embed = False
embed = metadata.embed(track_path + '.webm', meta_tags) embed = metadata.embed(track_path + ".webm", meta_tags)
os.remove(track_path + '.webm') os.remove(track_path + ".webm")
assert embed == expect_embed assert embed == expect_embed
def test_embed_in_flac(self): def test_embed_in_flac(self):
expect_embed = True expect_embed = True
embed = metadata.embed(track_path + '.flac', meta_tags) embed = metadata.embed(track_path + ".flac", meta_tags)
os.remove(track_path + '.flac') os.remove(track_path + ".flac")
assert embed == expect_embed assert embed == expect_embed
def test_check_track_exists_after_download(): def test_check_track_exists_after_download():
expect_check = True expect_check = True
check = spotdl.check_exists(file_name, TRACK_URL, meta_tags) check = spotdl.check_exists(file_name, TRACK_URL, meta_tags)
os.remove(track_path + '.mp3') os.remove(track_path + ".mp3")
assert check == expect_check assert check == expect_check

View File

@@ -11,17 +11,17 @@ import loader
loader.load_defaults() loader.load_defaults()
YT_API_KEY = 'AIzaSyAnItl3udec-Q1d5bkjKJGL-RgrKO_vU90' YT_API_KEY = "AIzaSyAnItl3udec-Q1d5bkjKJGL-RgrKO_vU90"
TRACK_SEARCH = "Tony's Videos VERY SHORT VIDEO 28.10.2016" TRACK_SEARCH = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
EXPECTED_TITLE = TRACK_SEARCH EXPECTED_TITLE = TRACK_SEARCH
EXPECTED_YT_URL = 'http://youtube.com/watch?v=qOOcy2-tmbk' EXPECTED_YT_URL = "http://youtube.com/watch?v=qOOcy2-tmbk"
EXPECTED_YT_URLS = (EXPECTED_YT_URL, 'http://youtube.com/watch?v=5USR1Omo7f0') EXPECTED_YT_URLS = (EXPECTED_YT_URL, "http://youtube.com/watch?v=5USR1Omo7f0")
RESULT_COUNT_SEARCH = "she is still sleeping SAO" RESULT_COUNT_SEARCH = "she is still sleeping SAO"
EXPECTED_YT_API_KEY = 'AIzaSyC6cEeKlxtOPybk9sEe5ksFN5sB-7wzYp0' EXPECTED_YT_API_KEY = "AIzaSyC6cEeKlxtOPybk9sEe5ksFN5sB-7wzYp0"
EXPECTED_YT_API_KEY_CUSTOM = 'some_api_key' EXPECTED_YT_API_KEY_CUSTOM = "some_api_key"
class TestYouTubeAPIKeys: class TestYouTubeAPIKeys:
@@ -76,13 +76,13 @@ class TestYouTubeURL:
def test_args_manual(self, monkeypatch): def test_args_manual(self, monkeypatch):
const.args.manual = True const.args.manual = True
monkeypatch.setattr('builtins.input', lambda x: '1') monkeypatch.setattr("builtins.input", lambda x: "1")
url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata) url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata)
assert url == EXPECTED_YT_URL assert url == EXPECTED_YT_URL
def test_args_manual_none(self, monkeypatch): def test_args_manual_none(self, monkeypatch):
expect_url = None expect_url = None
monkeypatch.setattr('builtins.input', lambda x: '0') monkeypatch.setattr("builtins.input", lambda x: "0")
url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata) url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata)
const.args.manual = False const.args.manual = False
assert url == expect_url assert url == expect_url
@@ -120,10 +120,10 @@ class TestDownload:
def test_webm(self): def test_webm(self):
# content does not have any .webm audiostream # content does not have any .webm audiostream
expect_download = False expect_download = False
download = youtube_tools.download_song(file_name + '.webm', content) download = youtube_tools.download_song(file_name + ".webm", content)
assert download == expect_download assert download == expect_download
def test_other(self): def test_other(self):
expect_download = False expect_download = False
download = youtube_tools.download_song(file_name + '.fake_extension', content) download = youtube_tools.download_song(file_name + ".fake_extension", content)
assert download == expect_download assert download == expect_download