mirror of
https://github.com/KevinMidboe/spotify-downloader.git
synced 2025-10-29 18:00:15 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2290167af4 | |||
|
|
252d945996 | ||
|
|
d53a6ea471 | ||
|
|
c1b3949edb | ||
|
|
6288e3c6e5 | ||
|
|
2aa7dce4a4 | ||
|
|
b13f12f1fe | ||
|
|
19ae8fd408 | ||
|
|
89735c2bbb | ||
|
|
debe7ee902 | ||
|
|
9c97f33aa2 | ||
|
|
046e7e9d3c | ||
|
|
29b1f31a26 | ||
|
|
64d54d7943 | ||
|
|
85c12a91ef | ||
|
|
9795d7e9b8 | ||
|
|
bbe43da191 | ||
|
|
8b7fd04321 | ||
|
|
cd5f224e37 | ||
|
|
675d1805ed |
41
CHANGES.md
41
CHANGES.md
@@ -8,6 +8,42 @@ The release dates mentioned follow the format `DD-MM-YYYY`.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.5] - 20-05-2020
|
||||||
|
### Fixed
|
||||||
|
- In some cases when using `-f` to create sub-directories from metadata, where the
|
||||||
|
full slugified download filename and the non-slugified download directory happen
|
||||||
|
to differ, the download would fail. The download directory will now be derived from
|
||||||
|
filename itself so that the sub-directory name always overlaps.
|
||||||
|
([@ritiek](https://github.com/ritiek/spotify-downloader))
|
||||||
|
(2aa7dce4a42feb5cd3ceb9324e58da524cdb4b6f)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Disable unneeded logs from `chardet`. ([@ritiek](https://github.com/ritiek))
|
||||||
|
(c1b3949edb943cc21a63c34d6a01ed59e9b6536d)
|
||||||
|
|
||||||
|
## [2.0.4] - 19-05-2020
|
||||||
|
### Fixed
|
||||||
|
- Do not remove the currently downloading track from file on `KeyboardInterrupt`
|
||||||
|
when `--list` is passed. ([@ritiek](https://github.com/ritiek/spotify-downloader)) (#722)
|
||||||
|
- Failure on invoking spotdl if FFmpeg isn't found. It should now warn about missing
|
||||||
|
FFmpeg and move ahead without encoding. ([@ritiek](https://github.com/ritiek))
|
||||||
|
(debe7ee9024e2ec65eed9935460c62f4eecd03ea)
|
||||||
|
|
||||||
|
## [2.0.3] (Hotfix Release) - 18-05-2020
|
||||||
|
### Fixed
|
||||||
|
- Genius would sometimes return invalid lyrics. Retry a few times in such a case.
|
||||||
|
([@ritiek](https://github.com/ritiek)) (29b1f31a2622f749df83c3072c4cbb22615bff95)
|
||||||
|
|
||||||
|
## [2.0.2] (Hotfix Release) - 18-05-2020
|
||||||
|
### Fixed
|
||||||
|
- Skipping tracks with `-m` would crash. ([@ritiek](https://github.com/ritiek))
|
||||||
|
(bbe43da191093302726ddc9a48f0fa0a55be6fb6)
|
||||||
|
|
||||||
|
## [2.0.1] (Hotfix Release) - 18-05-2020
|
||||||
|
### Fixed
|
||||||
|
- `-o m4a` would always fail. ([@ritiek](https://github.com/ritiek))
|
||||||
|
(cd5f224e379f3feefc95e338ec50674f976e2e89)
|
||||||
|
|
||||||
## [2.0.0] - 18-05-2020
|
## [2.0.0] - 18-05-2020
|
||||||
### Migrating from v1.2.6 to v2.0.0
|
### Migrating from v1.2.6 to v2.0.0
|
||||||
For v2.0.0 to work correctly, you need to remove your previous `config.yml` due to
|
For v2.0.0 to work correctly, you need to remove your previous `config.yml` due to
|
||||||
@@ -45,15 +81,14 @@ All the below changes were made as a part of #690.
|
|||||||
Such as `-o .mp3` is now written as `-o mp3`.
|
Such as `-o .mp3` is now written as `-o mp3`.
|
||||||
- **[Breaking]** Search format now uses hyphen for word break instead of underscore. Such as
|
- **[Breaking]** Search format now uses hyphen for word break instead of underscore. Such as
|
||||||
`-sf "{artist} - {track_name}"` is now written as `-sf "{artist} - {track-name}"`.
|
`-sf "{artist} - {track_name}"` is now written as `-sf "{artist} - {track-name}"`.
|
||||||
- **[Breaking]** `--write-sucessful` and `--skip` is renamed to `--write-succesful-file` and
|
- **[Breaking]** `--write-successful` and `--skip` is renamed to `--write-successful-file` and
|
||||||
`--skip-file` respectively.
|
`--skip-file` respectively.
|
||||||
Such as `-o .mp3` is now written as `-o mp3`.
|
|
||||||
- Partial re-write and internal API refactor.
|
- Partial re-write and internal API refactor.
|
||||||
- Enhance debug log output readability.
|
- Enhance debug log output readability.
|
||||||
- Internally adapt to latest changes made in Spotipy library.
|
- Internally adapt to latest changes made in Spotipy library.
|
||||||
- Switch to `logging` + `coloredlogs` instead of `logzero`. Our loggers weren't being
|
- Switch to `logging` + `coloredlogs` instead of `logzero`. Our loggers weren't being
|
||||||
setup properly with `logzero`.
|
setup properly with `logzero`.
|
||||||
- Simplify checking for an already track. Previously it also analyzed metadata
|
- Simplify checking for an downloaded already track. Previously it also analyzed metadata
|
||||||
for the already downloaded track to determine whether to overwrite the already downloaded
|
for the already downloaded track to determine whether to overwrite the already downloaded
|
||||||
track, which caused unexpected behvaiours at times.
|
track, which caused unexpected behvaiours at times.
|
||||||
- Codebase is now more modular making it easier to use spotdl in python scripts.
|
- Codebase is now more modular making it easier to use spotdl in python scripts.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from spotdl.command_line.exceptions import ArgumentError
|
|||||||
|
|
||||||
# hardcode loglevel for dependencies so that they do not spew generic
|
# hardcode loglevel for dependencies so that they do not spew generic
|
||||||
# log messages along with spotdl.
|
# log messages along with spotdl.
|
||||||
for module in ("urllib3", "spotipy", "pytube"):
|
for module in ("chardet", "urllib3", "spotipy", "pytube"):
|
||||||
logging.getLogger(module).setLevel(logging.CRITICAL)
|
logging.getLogger(module).setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
coloredlogs.DEFAULT_FIELD_STYLES = {
|
coloredlogs.DEFAULT_FIELD_STYLES = {
|
||||||
|
|||||||
@@ -224,6 +224,11 @@ class Spotdl:
|
|||||||
quality=self.arguments["quality"],
|
quality=self.arguments["quality"],
|
||||||
preftype=self.arguments["input_ext"],
|
preftype=self.arguments["input_ext"],
|
||||||
)
|
)
|
||||||
|
if stream is None:
|
||||||
|
logger.error('No matching streams found for given input format: "{}".'.format(
|
||||||
|
self.arguments["input_ext"]
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
if self.arguments["no_encode"]:
|
if self.arguments["no_encode"]:
|
||||||
output_extension = stream["encoding"]
|
output_extension = stream["encoding"]
|
||||||
@@ -256,16 +261,7 @@ class Spotdl:
|
|||||||
if not self.arguments["no_metadata"]:
|
if not self.arguments["no_metadata"]:
|
||||||
metadata["lyrics"].start()
|
metadata["lyrics"].start()
|
||||||
|
|
||||||
filter_space_chars = self.output_filename_filter(not self.arguments["no_spaces"])
|
os.makedirs(os.path.dirname(filename) or ".", exist_ok=True)
|
||||||
directory = os.path.dirname(
|
|
||||||
spotdl.metadata.format_string(
|
|
||||||
self.arguments["output_file"],
|
|
||||||
metadata,
|
|
||||||
output_extension=output_extension,
|
|
||||||
sanitizer=filter_space_chars
|
|
||||||
)
|
|
||||||
)
|
|
||||||
os.makedirs(directory or ".", exist_ok=True)
|
|
||||||
|
|
||||||
logger.info('Downloading to "{filename}"'.format(filename=filename))
|
logger.info('Downloading to "{filename}"'.format(filename=filename))
|
||||||
if self.arguments["no_encode"]:
|
if self.arguments["no_encode"]:
|
||||||
@@ -316,7 +312,7 @@ class Spotdl:
|
|||||||
|
|
||||||
def download_tracks_from_file(self, path):
|
def download_tracks_from_file(self, path):
|
||||||
logger.info(
|
logger.info(
|
||||||
"Checking and removing any duplicate tracks in {}.".format(path)
|
'Checking and removing any duplicate tracks in "{}".'.format(path)
|
||||||
)
|
)
|
||||||
tracks = spotdl.util.readlines_from_nonbinary_file(path)
|
tracks = spotdl.util.readlines_from_nonbinary_file(path)
|
||||||
tracks = self.strip_and_filter_duplicates(tracks)
|
tracks = self.strip_and_filter_duplicates(tracks)
|
||||||
@@ -341,12 +337,12 @@ class Spotdl:
|
|||||||
yt_search_format=self.arguments["search_format"],
|
yt_search_format=self.arguments["search_format"],
|
||||||
yt_manual=self.arguments["manual"]
|
yt_manual=self.arguments["manual"]
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
log_track_query = '{position}. Downloading "{track}"'.format(
|
log_track_query = '{position}. Downloading "{track}"'.format(
|
||||||
position=position,
|
position=position,
|
||||||
track=track
|
track=track
|
||||||
)
|
)
|
||||||
logger.info(log_track_query)
|
logger.info(log_track_query)
|
||||||
|
try:
|
||||||
metadata = search_metadata.on_youtube_and_spotify()
|
metadata = search_metadata.on_youtube_and_spotify()
|
||||||
self.download_track_from_metadata(metadata)
|
self.download_track_from_metadata(metadata)
|
||||||
except (urllib.request.URLError, TypeError, IOError) as e:
|
except (urllib.request.URLError, TypeError, IOError) as e:
|
||||||
@@ -358,6 +354,11 @@ class Spotdl:
|
|||||||
tracks.append(track)
|
tracks.append(track)
|
||||||
except (NoYouTubeVideoFoundError, NoYouTubeVideoMatchError) as e:
|
except (NoYouTubeVideoFoundError, NoYouTubeVideoMatchError) as e:
|
||||||
logger.error("{err}".format(err=e.args[0]))
|
logger.error("{err}".format(err=e.args[0]))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# The current track hasn't been downloaded completely.
|
||||||
|
# Make sure we continue from here the next the program runs.
|
||||||
|
tracks.insert(0, track)
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
if self.arguments["write_successful_file"]:
|
if self.arguments["write_successful_file"]:
|
||||||
with open(self.arguments["write_successful_file"], "a") as fout:
|
with open(self.arguments["write_successful_file"], "a") as fout:
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ from spotdl.encode.exceptions import EncoderNotFoundError
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_TARGET_FORMATS_FROM_ENCODING = {
|
||||||
|
"m4a": "mp4",
|
||||||
|
"mp3": "mp3",
|
||||||
|
"opus": "opus",
|
||||||
|
"flac": "flac"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EncoderBase(ABC):
|
class EncoderBase(ABC):
|
||||||
"""
|
"""
|
||||||
@@ -30,12 +37,12 @@ class EncoderBase(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __init__(self, encoder_path, loglevel, additional_arguments=[]):
|
def __init__(self, encoder_path, must_exist, loglevel, additional_arguments=[]):
|
||||||
"""
|
"""
|
||||||
This method must make sure whether specified encoder
|
This method must make sure whether specified encoder
|
||||||
is available under PATH.
|
is available under PATH.
|
||||||
"""
|
"""
|
||||||
if shutil.which(encoder_path) is None:
|
if must_exist and shutil.which(encoder_path) is None:
|
||||||
raise EncoderNotFoundError(
|
raise EncoderNotFoundError(
|
||||||
"{} executable does not exist or was not found in PATH.".format(
|
"{} executable does not exist or was not found in PATH.".format(
|
||||||
encoder_path
|
encoder_path
|
||||||
@@ -44,6 +51,7 @@ class EncoderBase(ABC):
|
|||||||
self.encoder_path = encoder_path
|
self.encoder_path = encoder_path
|
||||||
self._loglevel = loglevel
|
self._loglevel = loglevel
|
||||||
self._additional_arguments = additional_arguments
|
self._additional_arguments = additional_arguments
|
||||||
|
self._target_formats_from_encoding = _TARGET_FORMATS_FROM_ENCODING
|
||||||
|
|
||||||
def set_argument(self, argument):
|
def set_argument(self, argument):
|
||||||
"""
|
"""
|
||||||
@@ -94,6 +102,14 @@ class EncoderBase(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def target_format_from_encoding(self, encoding):
|
||||||
|
"""
|
||||||
|
This method generates the target stream format from given
|
||||||
|
input encoding.
|
||||||
|
"""
|
||||||
|
target_format = self._target_formats_from_encoding[encoding]
|
||||||
|
return target_format
|
||||||
|
|
||||||
def re_encode_from_stdin(self, input_encoding, target_path):
|
def re_encode_from_stdin(self, input_encoding, target_path):
|
||||||
"""
|
"""
|
||||||
This method must invoke the encoder to encode stdin to a
|
This method must invoke the encoder to encode stdin to a
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ RULES = {
|
|||||||
|
|
||||||
|
|
||||||
class EncoderFFmpeg(EncoderBase):
|
class EncoderFFmpeg(EncoderBase):
|
||||||
def __init__(self, encoder_path="ffmpeg"):
|
def __init__(self, encoder_path="ffmpeg", must_exist=True):
|
||||||
_loglevel = "-hide_banner -nostats -v panic"
|
_loglevel = "-hide_banner -nostats -v panic"
|
||||||
_additional_arguments = ["-b:a", "192k", "-vn"]
|
_additional_arguments = ["-b:a", "192k", "-vn"]
|
||||||
try:
|
try:
|
||||||
super().__init__(encoder_path, _loglevel, _additional_arguments)
|
super().__init__(encoder_path, must_exist, _loglevel, _additional_arguments)
|
||||||
except EncoderNotFoundError as e:
|
except EncoderNotFoundError as e:
|
||||||
raise FFmpegNotFoundError(e.args[0])
|
raise FFmpegNotFoundError(e.args[0])
|
||||||
self._rules = RULES
|
self._rules = RULES
|
||||||
@@ -76,7 +76,7 @@ class EncoderFFmpeg(EncoderBase):
|
|||||||
+ ["-i", input_path] \
|
+ ["-i", input_path] \
|
||||||
+ arguments.split() \
|
+ arguments.split() \
|
||||||
+ self._additional_arguments \
|
+ self._additional_arguments \
|
||||||
+ ["-f", target_encoding] \
|
+ ["-f", self.target_format_from_encoding(target_encoding)] \
|
||||||
+ [target_file]
|
+ [target_file]
|
||||||
|
|
||||||
return command
|
return command
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class TestEncodingDefaults:
|
|||||||
'-acodec', 'copy',
|
'-acodec', 'copy',
|
||||||
'-b:a', '192k',
|
'-b:a', '192k',
|
||||||
'-vn',
|
'-vn',
|
||||||
'-f', 'm4a',
|
'-f', 'mp4',
|
||||||
target_path
|
target_path
|
||||||
]
|
]
|
||||||
return command
|
return command
|
||||||
@@ -112,7 +112,7 @@ class TestEncodingInDebugMode:
|
|||||||
'-acodec', 'copy',
|
'-acodec', 'copy',
|
||||||
'-b:a', '192k',
|
'-b:a', '192k',
|
||||||
'-vn',
|
'-vn',
|
||||||
'-f', 'm4a',
|
'-f', 'mp4',
|
||||||
target_path
|
target_path
|
||||||
]
|
]
|
||||||
return command
|
return command
|
||||||
@@ -180,7 +180,7 @@ class TestEncodingAndTrimSilence:
|
|||||||
'-b:a', '192k',
|
'-b:a', '192k',
|
||||||
'-vn',
|
'-vn',
|
||||||
'-af', 'silenceremove=start_periods=1',
|
'-af', 'silenceremove=start_periods=1',
|
||||||
'-f', 'm4a',
|
'-f', 'mp4',
|
||||||
target_path
|
target_path
|
||||||
]
|
]
|
||||||
return command
|
return command
|
||||||
|
|||||||
@@ -86,3 +86,12 @@ class TestMethods:
|
|||||||
def test_encoder_not_found_error(self):
|
def test_encoder_not_found_error(self):
|
||||||
with pytest.raises(EncoderNotFoundError):
|
with pytest.raises(EncoderNotFoundError):
|
||||||
self.EncoderKid("/a/nonexistent/path", "0", [])
|
self.EncoderKid("/a/nonexistent/path", "0", [])
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("encoding, target_format", [
|
||||||
|
("m4a", "mp4"),
|
||||||
|
("mp3", "mp3"),
|
||||||
|
("opus", "opus"),
|
||||||
|
("flac", "flac"),
|
||||||
|
])
|
||||||
|
def test_target_format_from_encoding(self, encoderkid, encoding, target_format):
|
||||||
|
assert encoderkid.target_format_from_encoding(encoding) == target_format
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from spotdl.authorize.services import AuthorizeSpotify
|
|||||||
import spotdl.util
|
import spotdl.util
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import spotipy
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -48,14 +48,12 @@ class Genius(LyricBase):
|
|||||||
else:
|
else:
|
||||||
return response.read()
|
return response.read()
|
||||||
|
|
||||||
def _get_lyrics_text(self, html):
|
def _get_lyrics_text(self, paragraph):
|
||||||
"""
|
"""
|
||||||
Extracts and returns the lyric content from the provided HTML.
|
Extracts and returns the lyric content from the provided HTML.
|
||||||
"""
|
"""
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
if paragraph:
|
||||||
lyrics_paragraph = soup.find("p")
|
return paragraph.get_text()
|
||||||
if lyrics_paragraph:
|
|
||||||
return lyrics_paragraph.get_text()
|
|
||||||
else:
|
else:
|
||||||
raise LyricsNotFoundError(
|
raise LyricsNotFoundError(
|
||||||
"The lyrics for this track are yet to be released on Genius."
|
"The lyrics for this track are yet to be released on Genius."
|
||||||
@@ -81,6 +79,7 @@ class Genius(LyricBase):
|
|||||||
"""
|
"""
|
||||||
encoded_query = urllib.request.quote(query.replace(" ", "+"))
|
encoded_query = urllib.request.quote(query.replace(" ", "+"))
|
||||||
search_url = self.base_search_url + encoded_query
|
search_url = self.base_search_url + encoded_query
|
||||||
|
logger.debug('Fetching Genius search results from "{}".'.format(search_url))
|
||||||
metadata = self._fetch_search_page(search_url)
|
metadata = self._fetch_search_page(search_url)
|
||||||
|
|
||||||
lyric_url = None
|
lyric_url = None
|
||||||
@@ -105,6 +104,7 @@ class Genius(LyricBase):
|
|||||||
Returns the lyric string for the track best matching the
|
Returns the lyric string for the track best matching the
|
||||||
given query.
|
given query.
|
||||||
"""
|
"""
|
||||||
|
logger.debug('Fetching lyrics for the search query on "{}".'.format(query))
|
||||||
try:
|
try:
|
||||||
lyric_url = self.best_matching_lyric_url_from_query(query)
|
lyric_url = self.best_matching_lyric_url_from_query(query)
|
||||||
except LyricsNotFoundError:
|
except LyricsNotFoundError:
|
||||||
@@ -121,13 +121,31 @@ class Genius(LyricBase):
|
|||||||
result.
|
result.
|
||||||
"""
|
"""
|
||||||
lyric_url = self.guess_lyric_url_from_artist_and_track(artist, track)
|
lyric_url = self.guess_lyric_url_from_artist_and_track(artist, track)
|
||||||
return self.from_url(lyric_url, linesep, timeout)
|
return self.from_url(lyric_url, linesep, timeout=timeout)
|
||||||
|
|
||||||
def from_url(self, url, linesep="\n", timeout=None):
|
def from_url(self, url, linesep="\n", retries=5, timeout=None):
|
||||||
"""
|
"""
|
||||||
Returns the lyric string for the given URL.
|
Returns the lyric string for the given URL.
|
||||||
"""
|
"""
|
||||||
|
logger.debug('Fetching lyric text from "{}".'.format(url))
|
||||||
lyric_html_page = self._fetch_url_page(url, timeout=timeout)
|
lyric_html_page = self._fetch_url_page(url, timeout=timeout)
|
||||||
lyrics = self._get_lyrics_text(lyric_html_page)
|
soup = BeautifulSoup(lyric_html_page, "html.parser")
|
||||||
|
paragraph = soup.find("p")
|
||||||
|
# If <p> has a class (like <p class="bla">), then we got an invalid
|
||||||
|
# response. Retry in such a case.
|
||||||
|
invalid_response = paragraph.get("class") is not None
|
||||||
|
to_retry = retries > 0 and invalid_response
|
||||||
|
if to_retry:
|
||||||
|
logger.debug(
|
||||||
|
"Retrying since Genius returned invalid response for search "
|
||||||
|
"results. Retries left: {retries}.".format(retries=retries)
|
||||||
|
)
|
||||||
|
return self.from_url(url, linesep=linesep, retries=retries-1, timeout=timeout)
|
||||||
|
|
||||||
|
if invalid_response:
|
||||||
|
raise LyricsNotFoundError(
|
||||||
|
'Genius returned invalid response for the search URL "{}".'.format(url)
|
||||||
|
)
|
||||||
|
lyrics = self._get_lyrics_text(paragraph)
|
||||||
return lyrics.replace("\n", linesep)
|
return lyrics.replace("\n", linesep)
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class YouTubeSearch:
|
|||||||
quoted_query = urllib.request.quote(query)
|
quoted_query = urllib.request.quote(query)
|
||||||
return self.base_search_url.format(quoted_query)
|
return self.base_search_url.format(quoted_query)
|
||||||
|
|
||||||
def _fetch_response_html(self, url, retries=5):
|
def _fetch_response_html(self, url):
|
||||||
response = urllib.request.urlopen(url)
|
response = urllib.request.urlopen(url)
|
||||||
soup = BeautifulSoup(response.read(), "html.parser")
|
soup = BeautifulSoup(response.read(), "html.parser")
|
||||||
return soup
|
return soup
|
||||||
@@ -119,12 +119,11 @@ class YouTubeSearch:
|
|||||||
videos = self._fetch_search_results(html, limit=limit)
|
videos = self._fetch_search_results(html, limit=limit)
|
||||||
to_retry = retries > 0 and self._is_server_side_invalid_response(videos, html)
|
to_retry = retries > 0 and self._is_server_side_invalid_response(videos, html)
|
||||||
if to_retry:
|
if to_retry:
|
||||||
retries -= 1
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Retrying since YouTube returned invalid response for search "
|
"Retrying since YouTube returned invalid response for search "
|
||||||
"results. Retries left: {retries}.".format(retries=retries)
|
"results. Retries left: {retries}.".format(retries=retries)
|
||||||
)
|
)
|
||||||
return self.search(query, limit=limit, retries=retries)
|
return self.search(query, limit=limit, retries=retries-1)
|
||||||
return YouTubeVideos(videos)
|
return YouTubeVideos(videos)
|
||||||
|
|
||||||
|
|
||||||
@@ -138,6 +137,7 @@ class YouTubeStreams(StreamsBase):
|
|||||||
self.all = []
|
self.all = []
|
||||||
|
|
||||||
for stream in audiostreams:
|
for stream in audiostreams:
|
||||||
|
encoding = "m4a" if "mp4a" in stream.audio_codec else stream.audio_codec
|
||||||
standard_stream = {
|
standard_stream = {
|
||||||
# Store only the integer part for bitrate. For example
|
# Store only the integer part for bitrate. For example
|
||||||
# the given bitrate would be "192kbps", we store only
|
# the given bitrate would be "192kbps", we store only
|
||||||
@@ -145,7 +145,7 @@ class YouTubeStreams(StreamsBase):
|
|||||||
"bitrate": int(stream.abr[:-4]),
|
"bitrate": int(stream.abr[:-4]),
|
||||||
"connection": None,
|
"connection": None,
|
||||||
"download_url": stream.url,
|
"download_url": stream.url,
|
||||||
"encoding": stream.audio_codec,
|
"encoding": encoding,
|
||||||
"filesize": None,
|
"filesize": None,
|
||||||
}
|
}
|
||||||
establish_connection = threading.Thread(
|
establish_connection = threading.Thread(
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class MetadataSearch:
|
|||||||
if video is None:
|
if video is None:
|
||||||
raise NoYouTubeVideoMatchError(
|
raise NoYouTubeVideoMatchError(
|
||||||
'No matching videos found on YouTube for the search query "{}".'.format(
|
'No matching videos found on YouTube for the search query "{}".'.format(
|
||||||
search_query
|
query
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return video
|
return video
|
||||||
|
|||||||
@@ -13,6 +13,36 @@ def directory_fixture(tmpdir_factory):
|
|||||||
return dir_path
|
return dir_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", [
|
||||||
|
5,
|
||||||
|
"string",
|
||||||
|
{"a": 1, "b": 2},
|
||||||
|
(10, 20, 30, "string"),
|
||||||
|
[2, 4, "sample"]
|
||||||
|
])
|
||||||
|
def test_thread_with_return_value(value):
|
||||||
|
returner = lambda x: x
|
||||||
|
thread = spotdl.util.ThreadWithReturnValue(
|
||||||
|
target=returner,
|
||||||
|
args=(value,)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
assert value == thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("track, track_type", [
|
||||||
|
("https://open.spotify.com/track/3SipFlNddvL0XNZRLXvdZD", "spotify"),
|
||||||
|
("spotify:track:3SipFlNddvL0XNZRLXvdZD", "spotify"),
|
||||||
|
("3SipFlNddvL0XNZRLXvdZD", "spotify"),
|
||||||
|
("https://www.youtube.com/watch?v=oMiNsd176NM", "youtube"),
|
||||||
|
("oMiNsd176NM", "youtube"),
|
||||||
|
("kodaline - saving grace", "query"),
|
||||||
|
("or anything else", "query"),
|
||||||
|
])
|
||||||
|
def test_track_type(track, track_type):
|
||||||
|
assert spotdl.util.track_type(track) == track_type
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("str_duration, sec_duration", [
|
@pytest.mark.parametrize("str_duration, sec_duration", [
|
||||||
("0:23", 23),
|
("0:23", 23),
|
||||||
("0:45", 45),
|
("0:45", 45),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Track:
|
|||||||
return progress_bar
|
return progress_bar
|
||||||
|
|
||||||
def download_while_re_encoding(self, stream, target_path, target_encoding=None,
|
def download_while_re_encoding(self, stream, target_path, target_encoding=None,
|
||||||
encoder=EncoderFFmpeg(), show_progress=True):
|
encoder=EncoderFFmpeg(must_exist=False), show_progress=True):
|
||||||
total_chunks = self.calculate_total_chunks(stream["filesize"])
|
total_chunks = self.calculate_total_chunks(stream["filesize"])
|
||||||
process = encoder.re_encode_from_stdin(
|
process = encoder.re_encode_from_stdin(
|
||||||
stream["encoding"],
|
stream["encoding"],
|
||||||
@@ -80,7 +80,7 @@ class Track:
|
|||||||
writer(response, progress_bar, file_io)
|
writer(response, progress_bar, file_io)
|
||||||
|
|
||||||
def re_encode(self, input_path, target_path, target_encoding=None,
|
def re_encode(self, input_path, target_path, target_encoding=None,
|
||||||
encoder=EncoderFFmpeg(), show_progress=True):
|
encoder=EncoderFFmpeg(must_exist=False), show_progress=True):
|
||||||
stream = self.metadata["streams"].getbest()
|
stream = self.metadata["streams"].getbest()
|
||||||
total_chunks = self.calculate_total_chunks(stream["filesize"])
|
total_chunks = self.calculate_total_chunks(stream["filesize"])
|
||||||
process = encoder.re_encode_from_stdin(
|
process = encoder.re_encode_from_stdin(
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "2.0.0"
|
__version__ = "2.0.5"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user