diff --git a/.gitignore b/.gitignore index e2a49b3..df54316 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ config.yml Music/ *.txt +*.m3u upload.sh .pytest_cache/ diff --git a/spotdl/handle.py b/spotdl/handle.py index 0d62a54..81464de 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -124,6 +124,12 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True): "-V", "--version", help="show version and exit", action="store_true" ) + parser.add_argument( + '--write-m3u', + help="generate an .m3u playlist file with youtube links given " + "a text file containing tracks", + action='store_true' + ) parser.add_argument( "-m", "--manual", @@ -257,6 +263,9 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True): if parsed.config is not None and to_merge: parsed = override_config(parsed.config, parser) + if parsed.write_m3u and not parsed.list: + parser.error('--write-m3u can only be used with --list') + parsed.log_level = log_leveller(parsed.log_level) return parsed diff --git a/spotdl/internals.py b/spotdl/internals.py index 64d2ac0..980fb96 100755 --- a/spotdl/internals.py +++ b/spotdl/internals.py @@ -205,7 +205,7 @@ def get_music_dir(): return winreg.QueryValueEx(key, "My Music")[0] except (FileNotFoundError, NameError): pass - + # On both Windows and macOS, the localized folder names you see in # Explorer and Finder are actually in English on the file system. # So, defaulting to C:\Users\\Music or /Users//Music diff --git a/spotdl/spotdl.py b/spotdl/spotdl.py index 1560032..2aa9264 100755 --- a/spotdl/spotdl.py +++ b/spotdl/spotdl.py @@ -127,15 +127,7 @@ def download_list(tracks_file, skip_file=None, write_successful_file=None): def download_single(raw_song, number=None): """ Logic behind downloading a song. """ - - if internals.is_youtube(raw_song): - log.debug("Input song is a YouTube URL") - content = youtube_tools.go_pafy(raw_song, meta_tags=None) - raw_song = slugify(content.title).replace("-", " ") - meta_tags = spotify_tools.generate_metadata(raw_song) - else: - meta_tags = spotify_tools.generate_metadata(raw_song) - content = youtube_tools.go_pafy(raw_song, meta_tags) + content, meta_tags = youtube_tools.match_video_and_metadata(raw_song) if content is None: log.debug("Found no matching video") @@ -219,11 +211,14 @@ def main(): if const.args.song: download_single(raw_song=const.args.song) elif const.args.list: - download_list( - tracks_file=const.args.list, - skip_file=const.args.skip, - write_successful_file=const.args.write_successful, - ) + if const.args.write_m3u: + youtube_tools.generate_m3u(track_file=const.args.list) + else: + download_list( + tracks_file=const.args.list, + skip_file=const.args.skip, + write_successful_file=const.args.write_successful, + ) elif const.args.playlist: spotify_tools.write_playlist(playlist_url=const.args.playlist) elif const.args.album: diff --git a/spotdl/youtube_tools.py b/spotdl/youtube_tools.py index da7379d..6ae5713 100644 --- a/spotdl/youtube_tools.py +++ b/spotdl/youtube_tools.py @@ -1,8 +1,10 @@ from bs4 import BeautifulSoup import urllib import pafy +from slugify import slugify from logzero import logger as log +from spotdl import spotify_tools from spotdl import internals from spotdl import const @@ -38,6 +40,21 @@ def go_pafy(raw_song, meta_tags=None): return track_info +def match_video_and_metadata(track, force_pafy=True): + if internals.is_youtube(track): + log.debug("Input song is a YouTube URL") + content = go_pafy(track, meta_tags=None) + track = slugify(content.title).replace("-", " ") + meta_tags = spotify_tools.generate_metadata(track) + else: + meta_tags = spotify_tools.generate_metadata(track) + if force_pafy: + content = go_pafy(track, meta_tags) + else: + content = None + return content, meta_tags + + def get_youtube_title(content, number=None): """ Get the YouTube video's title. """ title = content.title @@ -47,6 +64,34 @@ def get_youtube_title(content, number=None): return title +def generate_m3u(track_file): + tracks = internals.get_unique_tracks(track_file) + target_file = "{}.m3u".format(track_file.split(".")[0]) + total_tracks = len(tracks) + 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") + for n, track in enumerate(tracks, 1): + content, _ = match_video_and_metadata(track) + if content is None: + log.warning("Skipping {}".format(track)) + else: + log.info( + "Matched track {0}/{1} ({2})".format( + n, total_tracks, content.watchv_url + ) + ) + log.debug(track) + m3u_key = "#EXTINF:{duration},{title}\n{youtube_url}\n".format( + duration=internals.get_sec(content.duration), + title=content.title, + youtube_url=content.watchv_url, + ) + log.debug(m3u_key) + with open(target_file, "a") as output_file: + output_file.write(m3u_key) + + def download_song(file_name, content): """ Download the audio file from YouTube. """ _, extension = os.path.splitext(file_name) @@ -164,7 +209,7 @@ class GenerateYouTubeURL: duration_tolerance += 1 if duration_tolerance > max_duration_tolerance: log.error( - "{0} by {1} was not found.\n".format( + "{0} by {1} was not found.".format( self.meta_tags["name"], self.meta_tags["artists"][0]["name"], ) diff --git a/test/test_handle.py b/test/test_handle.py index 0cbfdfa..b7647f4 100644 --- a/test/test_handle.py +++ b/test/test_handle.py @@ -3,12 +3,22 @@ import sys import argparse from spotdl import handle -from spotdl import const import pytest import yaml +def test_error_m3u_without_list(): + with pytest.raises(SystemExit): + handle.get_arguments(raw_args=('-s cool song', '--write-m3u',), + to_group=True) + + +def test_m3u_with_list(): + handle.get_arguments(raw_args=('-l cool_list.txt', '--write-m3u',), + to_group=True) + + def test_log_str_to_int(): expect_levels = [20, 30, 40, 10] levels = [handle.log_leveller(level) for level in handle._LOG_LEVELS_STR] diff --git a/test/test_list.py b/test/test_list.py index 118fc8e..6c53382 100644 --- a/test/test_list.py +++ b/test/test_list.py @@ -2,13 +2,19 @@ import builtins import os from spotdl import spotify_tools +from spotdl import youtube_tools from spotdl import const from spotdl import spotdl +import loader + + PLAYLIST_URL = "https://open.spotify.com/user/alex/playlist/0iWOVoumWlkXIrrBTSJmN8" 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 = 21 @@ -31,21 +37,40 @@ def test_playlist(tmpdir): def test_album(tmpdir): expect_tracks = 15 - global text_file 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:47,Eminem - Encore - Curtains Up\n" + "http://www.youtube.com/watch?v=0BZ6JYwrl2Y\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 + # 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') + 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: + with open(text_file, "r") as f: tracks = len(f.readlines()) assert tracks == expect_tracks