Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4.2.9 #2230

Merged
merged 21 commits into from
Nov 15, 2024
Merged

v4.2.9 #2230

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/workflows/standard-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,9 @@ jobs:
poetry env use "3.11"
poetry install
- name: Check for docstring's
run: |
poetry run pylint --limit-inference-results 0 --enable missing-function-docstring missing-module-docstring missing-class-docstring empty-docstring --disable=all ./spotdl
- name: Run Pylint check
run: |
poetry run pylint --fail-under 10 --limit-inference-results 0 ./spotdl
poetry run pylint --fail-under 10 --limit-inference-results 0 --disable=R0917,W0511 ./spotdl
- name: Run MyPy check
run: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,6 @@ temp/
# VS Code
.vscode
*.txt

# Output Folder
output/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Refer to our [Installation Guide](https://spotdl.rtfd.io/en/latest/installation/
- On Termux
- `curl -L https://raw.githubusercontent.com/spotDL/spotify-downloader/master/scripts/termux.sh | sh`
- Arch
- There is an [Arch User Repository (AUR) package](https://aur.archlinux.org/packages/python-spotdl/) for
- There is an [Arch User Repository (AUR) package](https://aur.archlinux.org/packages/spotdl/) for
spotDL.
- Docker
- Build image:
Expand Down
3 changes: 3 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ If you don't want config to load automatically change `load_config` option in co
"scan_for_songs": false,
"m3u": null,
"output": "{artists} - {title}.{output-ext}",
"m3u_output": "#EXTINF:{duration}, {artists} - {title}.{output-ext}",
"overwrite": "skip",
"search_query": null,
"ffmpeg": "ffmpeg",
Expand Down Expand Up @@ -463,6 +464,8 @@ Output options:
--sponsor-block Use the sponsor block to download songs from yt/ytm.
--archive ARCHIVE Specify the file name for an archive of already downloaded songs
--playlist-numbering Sets each track in a playlist to have the playlist's name as its album, and album art as the playlist's icon
--playlist-retain-track-cover
Sets each track in a playlist to have the playlist's name as its album, while retaining album art of each track
--scan-for-songs Scan the output directory for existing files. This option should be combined with the --overwrite option to control how existing files are handled. (Output
directory is the last directory that is not a template variable in the output template)
--fetch-albums Fetch all albums from songs in query
Expand Down
1,965 changes: 1,227 additions & 738 deletions poetry.lock

Large diffs are not rendered by default.

56 changes: 37 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "spotdl"
version = "4.2.8"
version = "4.2.9"
description = "Download your Spotify playlists and songs along with album art and metadata"
license = "MIT"
authors = ["spotDL Team <[email protected]>"]
Expand All @@ -27,7 +27,7 @@ classifiers = [
]

[tool.poetry.dependencies]
python = ">=3.8,<3.13"
python = ">=3.8,<3.14"

spotipy = [
{version = "^2.23.0", python = "<=3.8"},
Expand All @@ -38,50 +38,68 @@ ytmusicapi = [
{version = "^1.4.1", python = ">=3.10"},
]
pytube = "^15.0.0"
yt-dlp = "^2024.8.6"
yt-dlp = [
{version = "^2024.10.7", python = "<3.9"},
{version = "^2024.11.4", python = ">=3.9"},
]
mutagen = "^1.47.0"
rich = "^13.8.0"
rich = "^13.9.4"
beautifulsoup4 = "^4.12.3"
requests = "^2.32.3"
rapidfuzz = "^3.9.7"
rapidfuzz = [
{version = "^3.9.7", python = "<3.9"},
{version = "^3.10.0", python = ">=3.9"},
]
python-slugify = {extras = ["unidecode"], version = "^8.0.4"}
uvicorn = "^0.23.2"
pydantic = "^2.9.0"
pydantic = "^2.9.2"
fastapi = "^0.103.0"
platformdirs = "^4.2.2"
platformdirs = "^4.3.6"
pykakasi = "^2.3.0"
syncedlyrics = "^1.0.1"
soundcloud-v2 = "^1.6.0"

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.2"
pytest = "^8.3.3"
pytest-mock = "^3.14.0"
pyfakefs = "^5.6.0"
pytest-cov = "^5.0.0"
pyfakefs = "^5.7.1"
pytest-cov = [
{version = "^5.0.0", python = "<3.9"},
{version = "^6.0.0", python = ">=3.9"},
]
pytest-subprocess = "^1.5.2"
pytest-asyncio = "^0.21.1"
mypy = "^1.11.2"
pylint = "^3.2.7"
black = "^24.8.0"
mypy = "^1.13.0"
pylint = [
{version = "^3.2.7", python = "<3.9"},
{version = "^3.3.1", python = ">=3.9"},
]
black = [
{version = "^24.8.0", python = "<3.9"},
{version = "^24.10.0", python = ">=3.9"},
]
mdformat-gfm = "^0.3.5"
types-orjson = "^3.6.2"
types-python-slugify = "^8.0.2.20240310"
types-requests = "==2.31.0.6"
types-setuptools = "^74.1.0.20240906"
types-setuptools = "^75.3.0.20241112"
types-toml = "^0.10.8.7"
types-ujson = "^5.10.0.20240515"
pyinstaller = "^6.10.0"
pyinstaller = "^6.11.1"
mkdocs = "^1.6.1"
isort = "^5.13.2"
dill = "^0.3.7"
mkdocs-material = "^9.5.34"
mkdocs-material = "^9.5.44"
mkdocstrings = "^0.26.0"
mkdocstrings-python = "^1.11.1"
pymdown-extensions = "^10.9"
mkdocstrings-python = [
{version = "^1.11.1", python = "<3.9"},
{version = "^1.12.0", python = ">=3.9"},
]
pymdown-extensions = "^10.12"
mkdocs-gen-files = "^0.5.0"
mkdocs-literate-nav = "^0.6.0"
mkdocs-section-index = "^0.3.5"
vcrpy = "^6.0.1"
vcrpy = "^6.0.2"
pytest-recording = "^0.13.1"

[tool.poetry.scripts]
Expand Down
3 changes: 3 additions & 0 deletions spotdl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ def search(self, query: List[str]) -> List[Song]:
use_ytm_data=self.downloader.settings["ytm_data"],
playlist_numbering=self.downloader.settings["playlist_numbering"],
album_type=self.downloader.settings["album_type"],
playlist_retain_track_cover=self.downloader.settings[
"playlist_retain_track_cover"
],
)

def get_download_urls(self, songs: List[Song]) -> List[Optional[str]]:
Expand Down
2 changes: 1 addition & 1 deletion spotdl/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Version module for spotdl.
"""

__version__ = "4.2.8"
__version__ = "4.2.9"
1 change: 1 addition & 0 deletions spotdl/console/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def download(
playlist_numbering=downloader.settings["playlist_numbering"],
albums_to_ignore=downloader.settings["ignore_albums"],
album_type=downloader.settings["album_type"],
playlist_retain_track_cover=downloader.settings["playlist_retain_track_cover"],
)

# Download the songs
Expand Down
3 changes: 3 additions & 0 deletions spotdl/console/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ async def pool_worker(file_path: Path) -> None:
use_ytm_data=downloader.settings["ytm_data"],
playlist_numbering=downloader.settings["playlist_numbering"],
album_type=downloader.settings["album_type"],
playlist_retain_track_cover=downloader.settings[
"playlist_retain_track_cover"
],
)

downloader.download_multiple_songs(songs_list)
46 changes: 31 additions & 15 deletions spotdl/console/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,41 @@ def save(
use_ytm_data=downloader.settings["ytm_data"],
playlist_numbering=downloader.settings["playlist_numbering"],
album_type=downloader.settings["album_type"],
playlist_retain_track_cover=downloader.settings["playlist_retain_track_cover"],
)
save_data = [song.json for song in songs]

def process_song(song: Song):
try:
data = downloader.search(song)
if data is None:
logger.error("Could not find a match for %s", song.display_name)

download_url = None
if downloader.settings["preload"]:
try:
download_url = downloader.search(song)
if download_url is None:
logger.error("Could not find a match for %s", song.display_name)
return None

logger.info("Found url for %s: %s", song.display_name, download_url)
except Exception as exception:
logger.error(
"%s generated an exception: %s", song.display_name, exception
)
return None

logger.info("Found url for %s: %s", song.display_name, data)

return {**song.json, "download_url": data}
lyrics = None
try:
lyrics = downloader.search_lyrics(song)
if lyrics is None:
logger.debug(
"No lyrics found for %s, lyrics providers: %s",
song.display_name,
", ".join(
[lprovider.name for lprovider in downloader.lyrics_providers]
),
)
except Exception as exception:
logger.error("%s generated an exception: %s", song.display_name, exception)
logger.debug("Could not search for lyrics: %s", exception)

return None
return {**song.json, "download_url": download_url, "lyrics": lyrics}

async def pool_worker(song: Song):
async with downloader.semaphore:
Expand All @@ -74,11 +91,10 @@ async def pool_worker(song: Song):
# hurt performance.
return await downloader.loop.run_in_executor(None, process_song, song)

if downloader.settings["preload"]:
tasks = [pool_worker(song) for song in songs]
tasks = [pool_worker(song) for song in songs]

# call all task asynchronously, and wait until all are finished
save_data = list(downloader.loop.run_until_complete(asyncio.gather(*tasks)))
# call all task asynchronously, and wait until all are finished
save_data = list(downloader.loop.run_until_complete(asyncio.gather(*tasks)))

if to_stdout:
# Print the songs to stdout
Expand All @@ -92,7 +108,7 @@ async def pool_worker(song: Song):
gen_m3u_files(
songs,
m3u_file,
downloader.settings["output"],
downloader.settings["m3u_output"],
downloader.settings["format"],
downloader.settings["restrict"],
False,
Expand Down
14 changes: 10 additions & 4 deletions spotdl/console/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

import json
import logging
from typing import List, Tuple
from pathlib import Path
from typing import List, Tuple

from spotdl.download.downloader import Downloader
from spotdl.types.song import Song
Expand Down Expand Up @@ -56,6 +56,9 @@ def sync(
use_ytm_data=downloader.settings["ytm_data"],
playlist_numbering=downloader.settings["playlist_numbering"],
album_type=downloader.settings["album_type"],
playlist_retain_track_cover=downloader.settings[
"playlist_retain_track_cover"
],
)

# Create sync file
Expand All @@ -79,7 +82,7 @@ def sync(
gen_m3u_files(
songs_list,
m3u_file,
downloader.settings["output"],
downloader.settings["m3u_output"],
downloader.settings["format"],
downloader.settings["restrict"],
False,
Expand Down Expand Up @@ -112,6 +115,9 @@ def sync(
use_ytm_data=downloader.settings["ytm_data"],
playlist_numbering=downloader.settings["playlist_numbering"],
album_type=downloader.settings["album_type"],
playlist_retain_track_cover=downloader.settings[
"playlist_retain_track_cover"
],
)

# Get the names and URLs of previously downloaded songs from the sync file
Expand Down Expand Up @@ -151,7 +157,7 @@ def sync(
if path != new_path:
to_rename.append((path, new_path))

# TODO: Downloading duplicate songs in the same playlist
# fix later Downloading duplicate songs in the same playlist
# will trigger a re-download of the song. To fix this we have to copy the song
# to the new location without removing the old one.
for old_path, new_path in to_rename:
Expand Down Expand Up @@ -226,7 +232,7 @@ def sync(
gen_m3u_files(
songs_playlist,
m3u_file,
downloader.settings["output"],
downloader.settings["m3u_output"],
downloader.settings["format"],
downloader.settings["restrict"],
False,
Expand Down
1 change: 1 addition & 0 deletions spotdl/console/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def url(
use_ytm_data=downloader.settings["ytm_data"],
playlist_numbering=downloader.settings["playlist_numbering"],
album_type=downloader.settings["album_type"],
playlist_retain_track_cover=downloader.settings["playlist_retain_track_cover"],
)

def process_song(song: Song):
Expand Down
19 changes: 12 additions & 7 deletions spotdl/download/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
AudioProvider,
BandCamp,
Piped,
SliderKZ,
SoundCloud,
YouTube,
YouTubeMusic,
Expand Down Expand Up @@ -57,7 +56,6 @@
AUDIO_PROVIDERS: Dict[str, Type[AudioProvider]] = {
"youtube": YouTube,
"youtube-music": YouTubeMusic,
"slider-kz": SliderKZ,
"soundcloud": SoundCloud,
"bandcamp": BandCamp,
"piped": Piped,
Expand Down Expand Up @@ -306,9 +304,14 @@ def download_multiple_songs(

if self.settings["save_errors"]:
with open(
self.settings["save_errors"], "w", encoding="utf-8"
self.settings["save_errors"], "a", encoding="utf-8"
) as error_file:
error_file.write("\n".join(self.errors))
if len(self.errors) > 0:
error_file.write(
f"{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}\n"
)
for error in self.errors:
error_file.write(f"{error}\n")

logger.info("Saved errors to %s", self.settings["save_errors"])

Expand Down Expand Up @@ -336,7 +339,7 @@ def download_multiple_songs(
gen_m3u_files(
song_list,
self.settings["m3u"],
self.settings["output"],
self.settings["m3u_output"],
self.settings["format"],
self.settings["restrict"],
False,
Expand Down Expand Up @@ -454,6 +457,7 @@ def search_and_download( # pylint: disable=R0911
restrict=self.settings["restrict"],
file_name_length=self.settings["max_filename_length"],
)

except Exception:
song = reinit_song(song)

Expand Down Expand Up @@ -490,7 +494,8 @@ def search_and_download( # pylint: disable=R0911
and dup_song_path.exists()
]

file_exists = output_file.exists() or dup_song_paths
# Checking if file already exists in all subfolders of output directory
file_exists = file_exists = output_file.exists() or dup_song_paths
if not self.settings["scan_for_songs"]:
for file_extension in self.scan_formats:
ext_path = output_file.with_suffix(f".{file_extension}")
Expand Down Expand Up @@ -566,7 +571,7 @@ def search_and_download( # pylint: disable=R0911
logger.info("Removing duplicate file: %s", dup_song_path)

dup_song_path.unlink()
except (PermissionError, OSError) as exc:
except (PermissionError, OSError, Exception) as exc:
logger.debug(
"Could not remove duplicate file: %s, error: %s",
dup_song_path,
Expand Down
Loading
Loading