diff --git a/CHANGES.md b/CHANGES.md index 5c7eccb..2de1c3f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Ability to pass multiple tracks with `-s` option ([@ritiek](https://github.com/ritiek)) (#442) ### Changed +- Change FFmpeg to use the built-in encoder `aac` instead of 3rd party `libfdk-aac` which does not + ship with the apt package ([@ritiek](https://github.com/ritiek)) (#448) +- Monkeypatch ever-changing network-relying tests ([@ritiek](https://github.com/ritiek)) (#448) - Correct `.m4a` container before writing metadata so metadata fields shows up properly in media players (especially iTunes) ([@ritiek](https://github.com/ritiek) with thanks to [@Amit-L](https://github.com/Amit-L)!) (#453) - Refactored core downloading module ([@ritiek](https://github.com/ritiek)) (#410) diff --git a/spotdl/convert.py b/spotdl/convert.py index 8af876e..e6e688e 100644 --- a/spotdl/convert.py +++ b/spotdl/convert.py @@ -3,7 +3,8 @@ import os from logzero import logger as log -"""What are the differences and similarities between ffmpeg, libav, and avconv? +""" +What are the differences and similarities between ffmpeg, libav, and avconv? https://stackoverflow.com/questions/9477115 ffmeg encoders high to lower quality @@ -25,10 +26,10 @@ def song(input_song, output_song, folder, avconv=False, trim_silence=False): else: return 0 if avconv: - exit_code = convert.with_avconv() + exit_code, command = convert.with_avconv() else: - exit_code = convert.with_ffmpeg() - return exit_code + exit_code, command = convert.with_ffmpeg() + return exit_code, command class Converter: @@ -59,7 +60,7 @@ class Converter: log.warning("--trim-silence not supported with avconv") log.debug(command) - return subprocess.call(command) + return subprocess.call(command), command def with_ffmpeg(self): ffmpeg_pre = "ffmpeg -y " @@ -84,7 +85,7 @@ class Converter: if output_ext == ".mp3": ffmpeg_params = "-codec:a libmp3lame -ar 44100 " elif output_ext == ".m4a": - ffmpeg_params = "-cutoff 20000 -codec:a libfdk_aac -ar 44100 " + ffmpeg_params = "-cutoff 20000 -codec:a aac -ar 44100 " if output_ext == ".flac": ffmpeg_params = "-codec:a flac -ar 44100 " @@ -104,4 +105,4 @@ class Converter: ) log.debug(command) - return subprocess.call(command) + return subprocess.call(command), command diff --git a/spotdl/downloader.py b/spotdl/downloader.py index 9dc19e2..608a5c3 100644 --- a/spotdl/downloader.py +++ b/spotdl/downloader.py @@ -1,3 +1,8 @@ +import spotipy +import urllib +import os +from logzero import logger as log + from spotdl import const from spotdl import metadata from spotdl import convert @@ -5,10 +10,6 @@ from spotdl import internals from spotdl import spotify_tools from spotdl import youtube_tools -import spotipy -from logzero import logger as log -import os - class CheckExists: def __init__(self, music_file, meta_tags=None): diff --git a/spotdl/handle.py b/spotdl/handle.py index 041f912..e158703 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -1,14 +1,14 @@ -import appdirs -from spotdl import internals from logzero import logger as log +import appdirs import logging import yaml import argparse import mimetypes - import os +from spotdl import internals + _LOG_LEVELS_STR = ["INFO", "WARNING", "ERROR", "DEBUG"] diff --git a/spotdl/internals.py b/spotdl/internals.py index 12b4e87..375cc3a 100644 --- a/spotdl/internals.py +++ b/spotdl/internals.py @@ -1,6 +1,6 @@ +from logzero import logger as log import os import sys -from logzero import logger as log from spotdl import const diff --git a/spotdl/metadata.py b/spotdl/metadata.py index 2dd3bd3..eec6171 100644 --- a/spotdl/metadata.py +++ b/spotdl/metadata.py @@ -2,10 +2,11 @@ from mutagen.easyid3 import EasyID3 from mutagen.id3 import ID3, TORY, TYER, TPUB, APIC, USLT, COMM from mutagen.mp4 import MP4, MP4Cover from mutagen.flac import Picture, FLAC -from logzero import logger as log -from spotdl.const import TAG_PRESET, M4A_TAG_PRESET import urllib.request +from logzero import logger as log + +from spotdl.const import TAG_PRESET, M4A_TAG_PRESET def compare(music_file, metadata): diff --git a/spotdl/spotdl.py b/spotdl/spotdl.py index e8aca5d..090e6f3 100644 --- a/spotdl/spotdl.py +++ b/spotdl/spotdl.py @@ -1,5 +1,11 @@ #!/usr/bin/env python3 +import sys +import platform +import pprint +import logzero +from logzero import logger as log + from spotdl import __version__ from spotdl import const from spotdl import handle @@ -7,11 +13,6 @@ from spotdl import internals from spotdl import spotify_tools from spotdl import youtube_tools from spotdl import downloader -from logzero import logger as log -import logzero -import sys -import platform -import pprint def debug_sys_info(): diff --git a/spotdl/spotify_tools.py b/spotdl/spotify_tools.py index acdac3d..354588d 100644 --- a/spotdl/spotify_tools.py +++ b/spotdl/spotify_tools.py @@ -1,15 +1,15 @@ import spotipy import spotipy.oauth2 as oauth2 import lyricwikia -from logzero import logger as log - -from spotdl import internals from slugify import slugify from titlecase import titlecase +from logzero import logger as log import pprint import sys +from spotdl import internals + def generate_token(): """ Generate the token. Please respect these credentials :) """ @@ -29,8 +29,8 @@ def refresh_token(): # token is mandatory when using Spotify's API # https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/ -token = generate_token() -spotify = spotipy.Spotify(auth=token) +_token = generate_token() +spotify = spotipy.Spotify(auth=_token) def generate_metadata(raw_song): @@ -87,12 +87,6 @@ def generate_metadata(raw_song): return meta_tags -def write_user_playlist(username, text_file=None): - links = get_playlists(username=username) - playlist = internals.input_link(links) - return write_playlist(playlist, text_file) - - def get_playlists(username): """ Fetch user playlists when using the -u option. """ playlists = spotify.user_playlists(username) @@ -121,6 +115,12 @@ def get_playlists(username): return links +def write_user_playlist(username, text_file=None): + links = get_playlists(username=username) + playlist = internals.input_link(links) + return write_playlist(playlist, text_file) + + def fetch_playlist(playlist): try: playlist_id = internals.extract_spotify_id(playlist) @@ -154,7 +154,7 @@ def fetch_album(album): return album -def fetch_album_from_artist(artist_url, album_type="album"): +def fetch_albums_from_artist(artist_url, album_type="album"): """ This funcction returns all the albums from a give artist_url using the US market @@ -191,7 +191,7 @@ def write_all_albums_from_artist(artist_url, text_file=None): album_base_url = "https://open.spotify.com/album/" # fetching all default albums - albums = fetch_album_from_artist(artist_url) + albums = fetch_albums_from_artist(artist_url) # if no file if given, the default save file is in the current working # directory with the name of the artist @@ -204,7 +204,7 @@ def write_all_albums_from_artist(artist_url, text_file=None): write_album(album_base_url + album["id"], text_file=text_file) # fetching all single albums - singles = fetch_album_from_artist(artist_url, album_type="single") + singles = fetch_albums_from_artist(artist_url, album_type="single") for single in singles: log.info("Fetching single: " + single["name"]) diff --git a/spotdl/youtube_tools.py b/spotdl/youtube_tools.py index b7798c4..7fb8476 100644 --- a/spotdl/youtube_tools.py +++ b/spotdl/youtube_tools.py @@ -1,15 +1,15 @@ from bs4 import BeautifulSoup import urllib import pafy + from slugify import slugify from logzero import logger as log +import os from spotdl import spotify_tools from spotdl import internals from spotdl import const -import os - # Fix download speed throttle on short duration tracks # Read more on mps-youtube/pafy#199 pafy.g.opener.addheaders.append(("Range", "bytes=0-")) @@ -75,6 +75,8 @@ def generate_m3u(track_file): log.info("Generating {0} from {1} YouTube URLs".format(target_file, total_tracks)) with open(target_file, "w") as output_file: output_file.write("#EXTM3U\n\n") + + videos = [] for n, track in enumerate(tracks, 1): content, _ = match_video_and_metadata(track) if content is None: @@ -94,6 +96,9 @@ def generate_m3u(track_file): log.debug(m3u_key) with open(target_file, "a") as output_file: output_file.write(m3u_key) + videos.append(content.watchv_url) + + return videos def download_song(file_name, content): @@ -240,7 +245,7 @@ class GenerateYouTubeURL: search_url = generate_search_url(self.search_query) log.debug("Opening URL: {0}".format(search_url)) - item = urllib.request.urlopen(search_url).read() + item = self._fetch_response(search_url).read() items_parse = BeautifulSoup(item, "html.parser") videos = [] @@ -319,3 +324,11 @@ class GenerateYouTubeURL: return self._best_match(videos) return videos + + @staticmethod + def _fetch_response(url): + # XXX: This method exists only because it helps us indirectly + # monkey patch `urllib.request.open`, directly monkey patching + # `urllib.request.open` causes us to end up in an infinite recursion + # during the test since `urllib.request.open` would monkeypatch itself. + return urllib.request.urlopen(url) diff --git a/test/test_download_with_metadata.py b/test/test_download_with_metadata.py new file mode 100644 index 0000000..a5ab54e --- /dev/null +++ b/test/test_download_with_metadata.py @@ -0,0 +1,195 @@ +import urllib +import subprocess +import os + +from spotdl import const +from spotdl import internals +from spotdl import spotify_tools +from spotdl import youtube_tools +from spotdl import convert +from spotdl import metadata +from spotdl import downloader + +import pytest +import loader + +loader.load_defaults() + +SPOTIFY_TRACK_URL = "https://open.spotify.com/track/3SipFlNddvL0XNZRLXvdZD" +EXPECTED_YOUTUBE_TITLE = "Janji - Heroes Tonight (feat. Johnning) [NCS Release]" +EXPECTED_SPOTIFY_TITLE = "Janji - Heroes Tonight" +EXPECTED_YOUTUBE_URL = "http://youtube.com/watch?v=3nQNiWdeH2Q" +# GIST_URL is the monkeypatched version of: https://www.youtube.com/results?search_query=janji+-+heroes +# so that we get same results even if YouTube changes the list/order of videos on their page. +GIST_URL = "https://gist.githubusercontent.com/ritiek/e731338e9810e31c2f00f13c249a45f5/raw/c11a27f3b5d11a8d082976f1cdd237bd605ec2c2/search_results.html" + + +def pytest_namespace(): + # XXX: We override the value of `content_fixture` later in the tests. + # We do not use an acutal @pytest.fixture because it does not accept + # the monkeypatch parameter and we need to monkeypatch the network + # request before creating the Pafy object. + return {"content_fixture": None} + + +@pytest.fixture(scope="module") +def metadata_fixture(): + meta_tags = spotify_tools.generate_metadata(SPOTIFY_TRACK_URL) + return meta_tags + + +def test_metadata(metadata_fixture): + expect_number = 23 + assert len(metadata_fixture) == expect_number + + +class TestFileFormat: + def test_with_spaces(self, metadata_fixture): + title = internals.format_string(const.args.file_format, metadata_fixture) + assert title == EXPECTED_SPOTIFY_TITLE + + def test_without_spaces(self, metadata_fixture): + const.args.no_spaces = True + title = internals.format_string(const.args.file_format, metadata_fixture) + assert title == EXPECTED_SPOTIFY_TITLE.replace(" ", "_") + + +def monkeypatch_youtube_search_page(*args, **kwargs): + fake_urlopen = urllib.request.urlopen(GIST_URL) + return fake_urlopen + + +def test_youtube_url(metadata_fixture, monkeypatch): + monkeypatch.setattr(youtube_tools.GenerateYouTubeURL, "_fetch_response", monkeypatch_youtube_search_page) + url = youtube_tools.generate_youtube_url(SPOTIFY_TRACK_URL, metadata_fixture) + assert url == EXPECTED_YOUTUBE_URL + + +def test_youtube_title(metadata_fixture, monkeypatch): + monkeypatch.setattr(youtube_tools.GenerateYouTubeURL, "_fetch_response", monkeypatch_youtube_search_page) + content = youtube_tools.go_pafy(SPOTIFY_TRACK_URL, metadata_fixture) + pytest.content_fixture = content + title = youtube_tools.get_youtube_title(content) + assert title == EXPECTED_YOUTUBE_TITLE + + +@pytest.fixture(scope="module") +def filename_fixture(metadata_fixture): + songname = internals.format_string(const.args.file_format, metadata_fixture) + filename = internals.sanitize_title(songname) + return filename + + +def test_check_track_exists_before_download(tmpdir, metadata_fixture, filename_fixture): + expect_check = False + const.args.folder = str(tmpdir) + # prerequisites for determining filename + track_existence = downloader.CheckExists(filename_fixture, metadata_fixture) + check = track_existence.already_exists(SPOTIFY_TRACK_URL) + assert check == expect_check + + +class TestDownload: + def blank_audio_generator(self, filepath): + if filepath.endswith(".m4a"): + cmd = "ffmpeg -f lavfi -i anullsrc -t 1 -c:a aac {}".format(filepath) + elif filepath.endswith(".webm"): + cmd = "ffmpeg -f lavfi -i anullsrc -t 1 -c:a libopus {}".format(filepath) + subprocess.call(cmd.split(" ")) + + def test_m4a(self, monkeypatch, filename_fixture): + expect_download = True + monkeypatch.setattr("pafy.backend_shared.BaseStream.download", self.blank_audio_generator) + download = youtube_tools.download_song(filename_fixture + ".m4a", pytest.content_fixture) + assert download == expect_download + + def test_webm(self, monkeypatch, filename_fixture): + expect_download = True + monkeypatch.setattr("pafy.backend_shared.BaseStream.download", self.blank_audio_generator) + download = youtube_tools.download_song(filename_fixture + ".webm", pytest.content_fixture) + assert download == expect_download + + +class TestFFmpeg: + def test_convert_from_webm_to_mp3(self, filename_fixture, monkeypatch): + expect_command = "ffmpeg -y -i {0}.webm -codec:a libmp3lame -ar 44100 -b:a 192k -vn {0}.mp3".format(os.path.join(const.args.folder, filename_fixture)) + _, command = convert.song( + filename_fixture + ".webm", filename_fixture + ".mp3", const.args.folder + ) + assert ' '.join(command) == expect_command + + def test_convert_from_webm_to_m4a(self, filename_fixture, monkeypatch): + expect_command = "ffmpeg -y -i {0}.webm -cutoff 20000 -codec:a aac -ar 44100 -b:a 192k -vn {0}.m4a".format(os.path.join(const.args.folder, filename_fixture)) + _, command = convert.song( + filename_fixture + ".webm", filename_fixture + ".m4a", const.args.folder + ) + assert ' '.join(command) == expect_command + + def test_convert_from_m4a_to_mp3(self, filename_fixture, monkeypatch): + expect_command = "ffmpeg -y -i {0}.m4a -codec:v copy -codec:a libmp3lame -ar 44100 -b:a 192k -vn {0}.mp3".format(os.path.join(const.args.folder, filename_fixture)) + _, command = convert.song( + filename_fixture + ".m4a", filename_fixture + ".mp3", const.args.folder + ) + assert ' '.join(command) == expect_command + + def test_convert_from_m4a_to_webm(self, filename_fixture, monkeypatch): + expect_command = "ffmpeg -y -i {0}.m4a -codec:a libopus -vbr on -b:a 192k -vn {0}.webm".format(os.path.join(const.args.folder, filename_fixture)) + _, command = convert.song( + filename_fixture + ".m4a", filename_fixture + ".webm", const.args.folder + ) + assert ' '.join(command) == expect_command + + def test_convert_from_m4a_to_flac(self, filename_fixture, monkeypatch): + expect_command = "ffmpeg -y -i {0}.m4a -codec:a flac -ar 44100 -b:a 192k -vn {0}.flac".format(os.path.join(const.args.folder, filename_fixture)) + _, command = convert.song( + filename_fixture + ".m4a", filename_fixture + ".flac", const.args.folder + ) + assert ' '.join(command) == expect_command + + +class TestAvconv: + def test_convert_from_m4a_to_mp3(self, filename_fixture): + expect_command = "avconv -loglevel debug -i {0}.m4a -ab 192k {0}.mp3 -y".format(os.path.join(const.args.folder, filename_fixture)) + _, command = convert.song( + filename_fixture + ".m4a", filename_fixture + ".mp3", const.args.folder, avconv=True + ) + assert ' '.join(command) == expect_command + + +@pytest.fixture(scope="module") +def trackpath_fixture(filename_fixture): + trackpath = os.path.join(const.args.folder, filename_fixture) + return trackpath + + +class TestEmbedMetadata: + def test_embed_in_mp3(self, metadata_fixture, trackpath_fixture): + expect_embed = True + embed = metadata.embed(trackpath_fixture + ".mp3", metadata_fixture) + assert embed == expect_embed + + def test_embed_in_m4a(self, metadata_fixture, trackpath_fixture): + expect_embed = True + embed = metadata.embed(trackpath_fixture + ".m4a", metadata_fixture) + os.remove(trackpath_fixture + ".m4a") + assert embed == expect_embed + + def test_embed_in_webm(self, metadata_fixture, trackpath_fixture): + expect_embed = False + embed = metadata.embed(trackpath_fixture + ".webm", metadata_fixture) + os.remove(trackpath_fixture + ".webm") + assert embed == expect_embed + + def test_embed_in_flac(self, metadata_fixture, trackpath_fixture): + expect_embed = True + embed = metadata.embed(trackpath_fixture + ".flac", metadata_fixture) + os.remove(trackpath_fixture + ".flac") + assert embed == expect_embed + + +def test_check_track_exists_after_download(metadata_fixture, filename_fixture, trackpath_fixture): + expect_check = True + track_existence = downloader.CheckExists(filename_fixture, metadata_fixture) + check = track_existence.already_exists(SPOTIFY_TRACK_URL) + os.remove(trackpath_fixture + ".mp3") + assert check == expect_check diff --git a/test/test_handle.py b/test/test_handle.py index b7647f4..4cef0a5 100644 --- a/test/test_handle.py +++ b/test/test_handle.py @@ -25,28 +25,37 @@ def test_log_str_to_int(): assert levels == expect_levels +@pytest.fixture(scope="module") +def config_path_fixture(tmpdir_factory): + config_path = os.path.join(str(tmpdir_factory.mktemp("config")), + "config.yml") + return config_path + + +@pytest.fixture(scope="module") +def modified_config_fixture(): + modified_config = dict(handle.default_conf) + return modified_config + + class TestConfig: - def test_default_config(self, tmpdir): + def test_default_config(self, config_path_fixture): expect_config = handle.default_conf["spotify-downloader"] - global config_path - config_path = os.path.join(str(tmpdir), "config.yml") - config = handle.get_config(config_path) + config = handle.get_config(config_path_fixture) assert config == expect_config - def test_modified_config(self): - global modified_config - modified_config = dict(handle.default_conf) - modified_config["spotify-downloader"]["file-format"] = "just_a_test" - merged_config = handle.merge(handle.default_conf, modified_config) - assert merged_config == modified_config + def test_modified_config(self, modified_config_fixture): + modified_config_fixture["spotify-downloader"]["file-format"] = "just_a_test" + merged_config = handle.merge(handle.default_conf, modified_config_fixture) + assert merged_config == modified_config_fixture - def test_custom_config_path(self, tmpdir): + def test_custom_config_path(self, config_path_fixture, modified_config_fixture): parser = argparse.ArgumentParser() - with open(config_path, "w") as config_file: - yaml.dump(modified_config, config_file, default_flow_style=False) - overridden_config = handle.override_config(config_path, parser, raw_args="") + with open(config_path_fixture, "w") as config_file: + yaml.dump(modified_config_fixture, config_file, default_flow_style=False) + overridden_config = handle.override_config(config_path_fixture, parser, raw_args="") modified_values = [ - str(value) for value in modified_config["spotify-downloader"].values() + str(value) for value in modified_config_fixture["spotify-downloader"].values() ] overridden_config.folder = os.path.realpath(overridden_config.folder) overridden_values = [ diff --git a/test/test_internals.py b/test/test_internals.py index 93cb16b..e2a8875 100644 --- a/test/test_internals.py +++ b/test/test_internals.py @@ -80,6 +80,26 @@ STRING_IDS_TEST_TABLE = [ ] +FROM_SECONDS_TEST_TABLE = [ + (35, "35"), + (23, "23"), + (158, "2:38"), + (263, "4:23"), + (4562, "1:16:02"), + (26762, "7:26:02") +] + + +TO_SECONDS_TEST_TABLE = [ + ("0:23", 23), + ("0:45", 45), + ("2:19", 139), + ("3:33", 213), + ("7:38", 458), + ("1:30:05", 5405), +] + + def test_default_music_directory(): if sys.platform.startswith("linux"): output = subprocess.check_output(["xdg-user-dir", "MUSIC"]) @@ -92,68 +112,39 @@ def test_default_music_directory(): assert directory == expect_directory +@pytest.fixture(scope="module") +def directory_fixture(tmpdir_factory): + dir_path = os.path.join(str(tmpdir_factory.mktemp("tmpdir")), + "filter_this_folder") + return dir_path + + class TestPathFilterer: - def test_create_directory(self, tmpdir): + def test_create_directory(self, directory_fixture): expect_path = True - global folder_path - folder_path = os.path.join(str(tmpdir), "filter_this_folder") - internals.filter_path(folder_path) - is_path = os.path.isdir(folder_path) + internals.filter_path(directory_fixture) + is_path = os.path.isdir(directory_fixture) assert is_path == expect_path - def test_remove_temp_files(self, tmpdir): + def test_remove_temp_files(self, directory_fixture): expect_file = False - file_path = os.path.join(folder_path, "pesky_file.temp") + file_path = os.path.join(directory_fixture, "pesky_file.temp") open(file_path, "a") - internals.filter_path(folder_path) + internals.filter_path(directory_fixture) is_file = os.path.isfile(file_path) assert is_file == expect_file -class TestVideoTimeFromSeconds: - def test_from_seconds(self): - expect_duration = "35" - duration = internals.videotime_from_seconds(35) - assert duration == expect_duration - - def test_from_minutes(self): - expect_duration = "2:38" - duration = internals.videotime_from_seconds(158) - assert duration == expect_duration - - def test_from_hours(self): - expect_duration = "1:16:02" - duration = internals.videotime_from_seconds(4562) - assert duration == expect_duration +@pytest.mark.parametrize("sec_duration, str_duration", FROM_SECONDS_TEST_TABLE) +def test_video_time_from_seconds(sec_duration, str_duration): + duration = internals.videotime_from_seconds(sec_duration) + assert duration == str_duration -class TestGetSeconds: - def test_from_seconds(self): - expect_secs = 45 - secs = internals.get_sec("0:45") - assert secs == expect_secs - secs = internals.get_sec("0.45") - assert secs == expect_secs - - def test_from_minutes(self): - expect_secs = 213 - secs = internals.get_sec("3.33") - assert secs == expect_secs - secs = internals.get_sec("3:33") - assert secs == expect_secs - - def test_from_hours(self): - expect_secs = 5405 - secs = internals.get_sec("1.30.05") - assert secs == expect_secs - secs = internals.get_sec("1:30:05") - assert secs == expect_secs - - def test_raise_error(self): - with pytest.raises(ValueError): - internals.get_sec("10*05") - with pytest.raises(ValueError): - internals.get_sec("02,28,46") +@pytest.mark.parametrize("str_duration, sec_duration", TO_SECONDS_TEST_TABLE) +def test_get_seconds_from_video_time(str_duration, sec_duration): + secs = internals.get_sec(str_duration) + assert secs == sec_duration @pytest.mark.parametrize("duplicates, expected", DUPLICATE_TRACKS_TEST_TABLE) @@ -170,3 +161,21 @@ def test_get_unique_tracks(tmpdir, duplicates, expected): def test_extract_spotify_id(input_str, expected_spotify_id): spotify_id = internals.extract_spotify_id(input_str) assert spotify_id == expected_spotify_id + + +def test_trim(tmpdir): + text_file = os.path.join(str(tmpdir), "test_trim.txt") + with open(text_file, "w") as track_file: + track_file.write("ncs - spectre\nncs - heroes\nncs - hope") + + with open(text_file, "r") as track_file: + tracks = track_file.readlines() + + expect_number = len(tracks) - 1 + expect_track = tracks[0] + track = internals.trim_song(text_file) + + with open(text_file, "r") as track_file: + number = len(track_file.readlines()) + + assert expect_number == number and expect_track == track diff --git a/test/test_list.py b/test/test_list.py deleted file mode 100644 index d037053..0000000 --- a/test/test_list.py +++ /dev/null @@ -1,90 +0,0 @@ -import builtins -import os - -from spotdl import spotify_tools -from spotdl import youtube_tools -from spotdl import const -from spotdl import spotdl - -import loader - - -USERNAME = "uqlakumu7wslkoen46s5bulq0" -PLAYLIST_URL = "https://open.spotify.com/playlist/0fWBMhGh38y0wsYWwmM9Kt" -ALBUM_URL = "https://open.spotify.com/album/499J8bIsEnU7DSrosFDJJg" -ARTIST_URL = "https://open.spotify.com/artist/4dpARuHxo51G3z768sgnrY" - -loader.load_defaults() - - -def test_user_playlists(tmpdir, monkeypatch): - expect_tracks = 17 - text_file = os.path.join(str(tmpdir), "test_us.txt") - monkeypatch.setattr("builtins.input", lambda x: 1) - spotify_tools.write_user_playlist(USERNAME, text_file) - with open(text_file, "r") as f: - tracks = len(f.readlines()) - assert tracks == expect_tracks - - -def test_playlist(tmpdir): - expect_tracks = 14 - text_file = os.path.join(str(tmpdir), "test_pl.txt") - spotify_tools.write_playlist(PLAYLIST_URL, text_file) - with open(text_file, "r") as f: - tracks = len(f.readlines()) - assert tracks == expect_tracks - - -def test_album(tmpdir): - expect_tracks = 15 - text_file = os.path.join(str(tmpdir), "test_al.txt") - spotify_tools.write_album(ALBUM_URL, text_file) - with open(text_file, "r") as f: - tracks = len(f.readlines()) - assert tracks == expect_tracks - - -def test_m3u(tmpdir): - expect_m3u = ( - "#EXTM3U\n\n" - "#EXTINF:31,Eminem - 01 - Eminem - Curtains Up (Skit)\n" - "http://www.youtube.com/watch?v=qk13SFlwG9A\n" - "#EXTINF:226,Alan Walker - Spectre [NCS Release]\n" - "http://www.youtube.com/watch?v=AOeY-nDp7hI\n" - ) - m3u_track_file = os.path.join(str(tmpdir), "m3u_test.txt") - with open(m3u_track_file, "w") as track_file: - track_file.write("\nhttps://open.spotify.com/track/2nT5m433s95hvYJH4S7ont") - track_file.write("\nhttp://www.youtube.com/watch?v=AOeY-nDp7hI") - youtube_tools.generate_m3u(m3u_track_file) - m3u_file = "{}.m3u".format(m3u_track_file.split(".")[0]) - with open(m3u_file, "r") as m3u_in: - m3u = m3u_in.readlines() - assert "".join(m3u) == expect_m3u - - -def test_all_albums(tmpdir): - # current number of tracks on spotify since as of 10/10/2018 - # in US market only - expect_tracks = 49 - global text_file - text_file = os.path.join(str(tmpdir), "test_ab.txt") - spotify_tools.write_all_albums_from_artist(ARTIST_URL, text_file) - with open(text_file, "r") as f: - tracks = len(f.readlines()) - assert tracks == expect_tracks - - -def test_trim(): - with open(text_file, "r") as track_file: - tracks = track_file.readlines() - - expect_number = len(tracks) - 1 - expect_track = tracks[0] - track = spotdl.internals.trim_song(text_file) - - with open(text_file, "r") as track_file: - number = len(track_file.readlines()) - - assert expect_number == number and expect_track == track diff --git a/test/test_spotify_tools.py b/test/test_spotify_tools.py new file mode 100644 index 0000000..5a26dd9 --- /dev/null +++ b/test/test_spotify_tools.py @@ -0,0 +1,137 @@ +from spotdl import spotify_tools + +import os +import pytest + + +def test_generate_token(): + token = spotify_tools.generate_token() + assert len(token) == 83 + + +def test_refresh_token(): + old_instance = spotify_tools.spotify + spotify_tools.refresh_token() + new_instance = spotify_tools.spotify + assert not old_instance == new_instance + + +class TestGenerateMetadata: + @pytest.fixture(scope="module") + def metadata_fixture(self): + metadata = spotify_tools.generate_metadata("ncs - spectre") + return metadata + + def test_len(self, metadata_fixture): + assert len(metadata_fixture) == 23 + + def test_trackname(self, metadata_fixture): + assert metadata_fixture["name"] == "Spectre" + + def test_artist(self, metadata_fixture): + assert metadata_fixture["artists"][0]["name"] == "Alan Walker" + + def test_duration(self, metadata_fixture): + assert metadata_fixture["duration"] == 230.634 + + +def test_get_playlists(): + expect_playlist_ids = [ "34gWCK8gVeYDPKcctB6BQJ", + "04wTU2c2WNQG9XE5oSLYfj", + "0fWBMhGh38y0wsYWwmM9Kt" ] + + expect_playlists = [ "https://open.spotify.com/playlist/" + playlist_id + for playlist_id in expect_playlist_ids ] + + playlists = spotify_tools.get_playlists("uqlakumu7wslkoen46s5bulq0") + assert playlists == expect_playlists + + +def test_write_user_playlist(tmpdir, monkeypatch): + expect_tracks = 17 + text_file = os.path.join(str(tmpdir), "test_us.txt") + monkeypatch.setattr("builtins.input", lambda x: 1) + spotify_tools.write_user_playlist("uqlakumu7wslkoen46s5bulq0", text_file) + with open(text_file, "r") as f: + tracks = len(f.readlines()) + assert tracks == expect_tracks + + +class TestFetchPlaylist: + @pytest.fixture(scope="module") + def playlist_fixture(self): + playlist = spotify_tools.fetch_playlist("https://open.spotify.com/playlist/0fWBMhGh38y0wsYWwmM9Kt") + return playlist + + def test_name(self, playlist_fixture): + assert playlist_fixture["name"] == "special_test_playlist" + + def test_tracks(self, playlist_fixture): + assert playlist_fixture["tracks"]["total"] == 14 + + +def test_write_playlist(tmpdir): + expect_tracks = 14 + text_file = os.path.join(str(tmpdir), "test_pl.txt") + spotify_tools.write_playlist("https://open.spotify.com/playlist/0fWBMhGh38y0wsYWwmM9Kt", text_file) + with open(text_file, "r") as f: + tracks = len(f.readlines()) + assert tracks == expect_tracks + + +# XXX: Mock this test off if it fails in future +class TestFetchAlbum: + @pytest.fixture(scope="module") + def album_fixture(self): + album = spotify_tools.fetch_album("https://open.spotify.com/album/499J8bIsEnU7DSrosFDJJg") + return album + + def test_name(self, album_fixture): + assert album_fixture["name"] == "NCS: Infinity" + + def test_tracks(self, album_fixture): + assert album_fixture["tracks"]["total"] == 15 + + +# XXX: Mock this test off if it fails in future +class TestFetchAlbumsFromArtist: + @pytest.fixture(scope="module") + def albums_from_artist_fixture(self): + albums = spotify_tools.fetch_albums_from_artist("https://open.spotify.com/artist/7oPftvlwr6VrsViSDV7fJY") + return albums + + def test_len(self, albums_from_artist_fixture): + assert len(albums_from_artist_fixture) == 18 + + def test_zeroth_album_name(self, albums_from_artist_fixture): + assert albums_from_artist_fixture[0]["name"] == "Revolution Radio" + + def test_zeroth_album_tracks(self, albums_from_artist_fixture): + assert albums_from_artist_fixture[0]["total_tracks"] == 12 + + def test_fist_album_name(self, albums_from_artist_fixture): + assert albums_from_artist_fixture[1]["name"] == "Demolicious" + + def test_first_album_tracks(self, albums_from_artist_fixture): + assert albums_from_artist_fixture[0]["total_tracks"] == 12 + + +# XXX: Mock this test off if it fails in future +def test_write_all_albums_from_artist(tmpdir): + # current number of tracks on spotify since as of 10/10/2018 + # in US market only + expect_tracks = 49 + text_file = os.path.join(str(tmpdir), "test_ab.txt") + spotify_tools.write_all_albums_from_artist("https://open.spotify.com/artist/4dpARuHxo51G3z768sgnrY", text_file) + with open(text_file, "r") as f: + tracks = len(f.readlines()) + assert tracks == expect_tracks + + +def test_write_album(tmpdir): + expect_tracks = 15 + text_file = os.path.join(str(tmpdir), "test_al.txt") + spotify_tools.write_album("https://open.spotify.com/album/499J8bIsEnU7DSrosFDJJg", text_file) + with open(text_file, "r") as f: + tracks = len(f.readlines()) + assert tracks == expect_tracks diff --git a/test/test_with_metadata.py b/test/test_with_metadata.py deleted file mode 100644 index b007d72..0000000 --- a/test/test_with_metadata.py +++ /dev/null @@ -1,153 +0,0 @@ -import os - -from spotdl import const -from spotdl import internals -from spotdl import spotify_tools -from spotdl import youtube_tools -from spotdl import convert -from spotdl import metadata -from spotdl import downloader - -import loader - -loader.load_defaults() - -TRACK_URL = "https://open.spotify.com/track/2nT5m433s95hvYJH4S7ont" -EXPECTED_TITLE = "Eminem - Curtains Up" -EXPECTED_YT_TITLE = "Eminem - 01 - Eminem - Curtains Up (Skit)" -EXPECTED_YT_URL = "http://youtube.com/watch?v=qk13SFlwG9A" - - -def test_metadata(): - expect_number = 23 - global meta_tags - meta_tags = spotify_tools.generate_metadata(TRACK_URL) - assert len(meta_tags) == expect_number - - -class TestFileFormat: - def test_with_spaces(self): - title = internals.format_string(const.args.file_format, meta_tags) - assert title == EXPECTED_TITLE - - def test_without_spaces(self): - const.args.no_spaces = True - title = internals.format_string(const.args.file_format, meta_tags) - assert title == EXPECTED_TITLE.replace(" ", "_") - - -def test_youtube_url(): - url = youtube_tools.generate_youtube_url(TRACK_URL, meta_tags) - assert url == EXPECTED_YT_URL - - -def test_youtube_title(): - global content - content = youtube_tools.go_pafy(TRACK_URL, meta_tags) - title = youtube_tools.get_youtube_title(content) - assert title == EXPECTED_YT_TITLE - - -def test_check_track_exists_before_download(tmpdir): - expect_check = False - const.args.folder = str(tmpdir) - # prerequisites for determining filename - songname = internals.format_string(const.args.file_format, meta_tags) - global file_name - file_name = internals.sanitize_title(songname) - track_existence = downloader.CheckExists(file_name, meta_tags) - check = track_existence.already_exists(TRACK_URL) - assert check == expect_check - - -class TestDownload: - def test_m4a(self): - expect_download = True - download = youtube_tools.download_song(file_name + ".m4a", content) - assert download == expect_download - - def test_webm(self): - expect_download = True - download = youtube_tools.download_song(file_name + ".webm", content) - assert download == expect_download - - -class TestFFmpeg: - def test_convert_from_webm_to_mp3(self): - expect_return_code = 0 - return_code = convert.song( - file_name + ".webm", file_name + ".mp3", const.args.folder - ) - assert return_code == expect_return_code - - def test_convert_from_webm_to_m4a(self): - expect_return_code = 0 - return_code = convert.song( - file_name + ".webm", file_name + ".m4a", const.args.folder - ) - assert return_code == expect_return_code - - def test_convert_from_m4a_to_mp3(self): - expect_return_code = 0 - return_code = convert.song( - file_name + ".m4a", file_name + ".mp3", const.args.folder - ) - assert return_code == expect_return_code - - def test_convert_from_m4a_to_webm(self): - expect_return_code = 0 - return_code = convert.song( - file_name + ".m4a", file_name + ".webm", const.args.folder - ) - assert return_code == expect_return_code - - def test_convert_from_m4a_to_flac(self): - expect_return_code = 0 - return_code = convert.song( - file_name + ".m4a", file_name + ".flac", const.args.folder - ) - assert return_code == expect_return_code - - -class TestAvconv: - def test_convert_from_m4a_to_mp3(self): - expect_return_code = 0 - return_code = convert.song( - file_name + ".m4a", file_name + ".mp3", const.args.folder, avconv=True - ) - assert return_code == expect_return_code - - -class TestEmbedMetadata: - def test_embed_in_mp3(self): - expect_embed = True - global track_path - track_path = os.path.join(const.args.folder, file_name) - embed = metadata.embed(track_path + ".mp3", meta_tags) - assert embed == expect_embed - - def test_embed_in_m4a(self): - expect_embed = True - embed = metadata.embed(track_path + ".m4a", meta_tags) - os.remove(track_path + ".m4a") - assert embed == expect_embed - - def test_embed_in_webm(self): - expect_embed = False - embed = metadata.embed(track_path + ".webm", meta_tags) - os.remove(track_path + ".webm") - assert embed == expect_embed - - def test_embed_in_flac(self): - expect_embed = True - embed = metadata.embed(track_path + ".flac", meta_tags) - os.remove(track_path + ".flac") - assert embed == expect_embed - - -def test_check_track_exists_after_download(): - expect_check = True - track_existence = downloader.CheckExists(file_name, meta_tags) - check = track_existence.already_exists(TRACK_URL) - os.remove(track_path + ".mp3") - assert check == expect_check diff --git a/test/test_without_metadata.py b/test/test_youtube_tools.py similarity index 61% rename from test/test_without_metadata.py rename to test/test_youtube_tools.py index 8584556..45867c4 100644 --- a/test/test_without_metadata.py +++ b/test/test_youtube_tools.py @@ -8,6 +8,7 @@ from spotdl import youtube_tools from spotdl import downloader import loader +import pytest loader.load_defaults() @@ -38,11 +39,15 @@ class TestYouTubeAPIKeys: assert key == EXPECTED_YT_API_KEY -def test_metadata(): - expect_metadata = None - global metadata +@pytest.fixture(scope="module") +def metadata_fixture(): metadata = spotify_tools.generate_metadata(TRACK_SEARCH) - assert metadata == expect_metadata + return metadata + + +def test_metadata(metadata_fixture): + expect_metadata = None + assert metadata_fixture == expect_metadata class TestArgsManualResultCount: @@ -63,68 +68,82 @@ class TestArgsManualResultCount: class TestYouTubeURL: - def test_only_music_category(self): + def test_only_music_category(self, metadata_fixture): const.args.music_videos_only = True - url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata) + url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata_fixture) # YouTube keeps changing its results assert url in EXPECTED_YT_URLS - def test_all_categories(self): + def test_all_categories(self, metadata_fixture): const.args.music_videos_only = False - url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata) + url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata_fixture) assert url == EXPECTED_YT_URL - def test_args_manual(self, monkeypatch): + def test_args_manual(self, metadata_fixture, monkeypatch): const.args.manual = True 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_fixture) assert url == EXPECTED_YT_URL - def test_args_manual_none(self, monkeypatch): + def test_args_manual_none(self, metadata_fixture, monkeypatch): expect_url = None 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_fixture) const.args.manual = False assert url == expect_url +@pytest.fixture(scope="module") +def content_fixture(metadata_fixture): + content = youtube_tools.go_pafy(TRACK_SEARCH, metadata_fixture) + return content + + +@pytest.fixture(scope="module") +def title_fixture(content_fixture): + title = youtube_tools.get_youtube_title(content_fixture) + return title + + class TestYouTubeTitle: - def test_single_download_with_youtube_api(self): - global content - global title + def test_single_download_with_youtube_api(self, title_fixture): const.args.youtube_api_key = YT_API_KEY youtube_tools.set_api_key() - content = youtube_tools.go_pafy(TRACK_SEARCH, metadata) - title = youtube_tools.get_youtube_title(content) - assert title == EXPECTED_TITLE + assert title_fixture == EXPECTED_TITLE - def test_download_from_list_without_youtube_api(self): + def test_download_from_list_without_youtube_api(self, metadata_fixture, content_fixture): const.args.youtube_api_key = None youtube_tools.set_api_key() - content = youtube_tools.go_pafy(TRACK_SEARCH, metadata) - title = youtube_tools.get_youtube_title(content, 1) + content_fixture = youtube_tools.go_pafy(TRACK_SEARCH, metadata_fixture) + title = youtube_tools.get_youtube_title(content_fixture, 1) assert title == "1. {0}".format(EXPECTED_TITLE) -def test_check_exists(tmpdir): +@pytest.fixture(scope="module") +def filename_fixture(title_fixture): + filename = internals.sanitize_title(title_fixture) + return filename + + +def test_check_exists(metadata_fixture, filename_fixture, tmpdir): expect_check = False const.args.folder = str(tmpdir) # prerequisites for determining filename - global file_name - file_name = internals.sanitize_title(title) - track_existence = downloader.CheckExists(file_name, metadata) + track_existence = downloader.CheckExists(filename_fixture, metadata_fixture) check = track_existence.already_exists(TRACK_SEARCH) assert check == expect_check class TestDownload: - def test_webm(self): - # content does not have any .webm audiostream + def test_webm(self, content_fixture, filename_fixture, monkeypatch): + # content_fixture does not have any .webm audiostream expect_download = False - download = youtube_tools.download_song(file_name + ".webm", content) + monkeypatch.setattr("pafy.backend_shared.BaseStream.download", lambda x: None) + download = youtube_tools.download_song(filename_fixture + ".webm", content_fixture) assert download == expect_download - def test_other(self): + def test_other(self, content_fixture, filename_fixture, monkeypatch): expect_download = False - download = youtube_tools.download_song(file_name + ".fake_extension", content) + monkeypatch.setattr("pafy.backend_shared.BaseStream.download", lambda x: None) + download = youtube_tools.download_song(filename_fixture + ".fake_extension", content_fixture) assert download == expect_download