mirror of
https://github.com/KevinMidboe/spotify-downloader.git
synced 2025-10-29 18:00:15 +00:00
Add support for .m3u playlists (#401)
* Add support for .m3u playlists * Run black code formatter on changes * Stay consistent with Spotify test track
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
config.yml
|
config.yml
|
||||||
Music/
|
Music/
|
||||||
*.txt
|
*.txt
|
||||||
|
*.m3u
|
||||||
upload.sh
|
upload.sh
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
"-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(
|
parser.add_argument(
|
||||||
"-m",
|
"-m",
|
||||||
"--manual",
|
"--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:
|
if parsed.config is not None and to_merge:
|
||||||
parsed = override_config(parsed.config, parser)
|
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)
|
parsed.log_level = log_leveller(parsed.log_level)
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
|
|||||||
@@ -127,15 +127,7 @@ def download_list(tracks_file, skip_file=None, write_successful_file=None):
|
|||||||
|
|
||||||
def download_single(raw_song, number=None):
|
def download_single(raw_song, number=None):
|
||||||
""" Logic behind downloading a song. """
|
""" Logic behind downloading a song. """
|
||||||
|
content, meta_tags = youtube_tools.match_video_and_metadata(raw_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)
|
|
||||||
|
|
||||||
if content is None:
|
if content is None:
|
||||||
log.debug("Found no matching video")
|
log.debug("Found no matching video")
|
||||||
@@ -219,6 +211,9 @@ def main():
|
|||||||
if const.args.song:
|
if const.args.song:
|
||||||
download_single(raw_song=const.args.song)
|
download_single(raw_song=const.args.song)
|
||||||
elif const.args.list:
|
elif const.args.list:
|
||||||
|
if const.args.write_m3u:
|
||||||
|
youtube_tools.generate_m3u(track_file=const.args.list)
|
||||||
|
else:
|
||||||
download_list(
|
download_list(
|
||||||
tracks_file=const.args.list,
|
tracks_file=const.args.list,
|
||||||
skip_file=const.args.skip,
|
skip_file=const.args.skip,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import urllib
|
import urllib
|
||||||
import pafy
|
import pafy
|
||||||
|
from slugify import slugify
|
||||||
from logzero import logger as log
|
from logzero import logger as log
|
||||||
|
|
||||||
|
from spotdl import spotify_tools
|
||||||
from spotdl import internals
|
from spotdl import internals
|
||||||
from spotdl import const
|
from spotdl import const
|
||||||
|
|
||||||
@@ -38,6 +40,21 @@ def go_pafy(raw_song, meta_tags=None):
|
|||||||
return track_info
|
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):
|
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
|
||||||
@@ -47,6 +64,34 @@ def get_youtube_title(content, number=None):
|
|||||||
return title
|
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):
|
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)
|
||||||
@@ -164,7 +209,7 @@ class GenerateYouTubeURL:
|
|||||||
duration_tolerance += 1
|
duration_tolerance += 1
|
||||||
if duration_tolerance > max_duration_tolerance:
|
if duration_tolerance > max_duration_tolerance:
|
||||||
log.error(
|
log.error(
|
||||||
"{0} by {1} was not found.\n".format(
|
"{0} by {1} was not found.".format(
|
||||||
self.meta_tags["name"],
|
self.meta_tags["name"],
|
||||||
self.meta_tags["artists"][0]["name"],
|
self.meta_tags["artists"][0]["name"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,12 +3,22 @@ import sys
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from spotdl import handle
|
from spotdl import handle
|
||||||
from spotdl import const
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
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():
|
def test_log_str_to_int():
|
||||||
expect_levels = [20, 30, 40, 10]
|
expect_levels = [20, 30, 40, 10]
|
||||||
levels = [handle.log_leveller(level) for level in handle._LOG_LEVELS_STR]
|
levels = [handle.log_leveller(level) for level in handle._LOG_LEVELS_STR]
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ import builtins
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from spotdl import spotify_tools
|
from spotdl import spotify_tools
|
||||||
|
from spotdl import youtube_tools
|
||||||
from spotdl import const
|
from spotdl import const
|
||||||
from spotdl import spotdl
|
from spotdl import spotdl
|
||||||
|
|
||||||
|
import loader
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
ARTIST_URL = "https://open.spotify.com/artist/4dpARuHxo51G3z768sgnrY"
|
ARTIST_URL = "https://open.spotify.com/artist/4dpARuHxo51G3z768sgnrY"
|
||||||
|
|
||||||
|
loader.load_defaults()
|
||||||
|
|
||||||
|
|
||||||
def test_user_playlists(tmpdir, monkeypatch):
|
def test_user_playlists(tmpdir, monkeypatch):
|
||||||
expect_tracks = 21
|
expect_tracks = 21
|
||||||
@@ -31,21 +37,40 @@ def test_playlist(tmpdir):
|
|||||||
|
|
||||||
def test_album(tmpdir):
|
def test_album(tmpdir):
|
||||||
expect_tracks = 15
|
expect_tracks = 15
|
||||||
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_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):
|
def test_all_albums(tmpdir):
|
||||||
#current number of tracks on spotify since as of 10/10/2018
|
# current number of tracks on spotify since as of 10/10/2018
|
||||||
#in US market only
|
# in US market only
|
||||||
expect_tracks = 49
|
expect_tracks = 49
|
||||||
global text_file
|
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)
|
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())
|
tracks = len(f.readlines())
|
||||||
assert tracks == expect_tracks
|
assert tracks == expect_tracks
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user