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:
Ritiek Malhotra
2018-10-20 16:19:14 +05:30
committed by GitHub
parent 7d321d9616
commit b12ca8c785
7 changed files with 107 additions and 22 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
config.yml config.yml
Music/ Music/
*.txt *.txt
*.m3u
upload.sh upload.sh
.pytest_cache/ .pytest_cache/

View File

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

View File

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

View File

@@ -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"],
) )

View File

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

View File

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