Skip to content

Commit

Permalink
Merge branch 'dev' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Shajal-Kumar authored Nov 3, 2024
2 parents ad080be + 602fe87 commit 43bd4d0
Show file tree
Hide file tree
Showing 16 changed files with 1,183 additions and 717 deletions.
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: 2 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,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,720 changes: 1,044 additions & 676 deletions poetry.lock

Large diffs are not rendered by default.

44 changes: 28 additions & 16 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,62 @@ ytmusicapi = [
{version = "^1.4.1", python = ">=3.10"},
]
pytube = "^15.0.0"
yt-dlp = "^2024.8.6"
yt-dlp = "^2024.10.7"
mutagen = "^1.47.0"
rich = "^13.8.0"
rich = "^13.9.2"
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"
pyfakefs = "^5.7.0"
pytest-cov = "^5.0.0"
pytest-subprocess = "^1.5.2"
pytest-asyncio = "^0.21.1"
mypy = "^1.11.2"
pylint = "^3.2.7"
black = "^24.8.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.1.0.20240917"
types-toml = "^0.10.8.7"
types-ujson = "^5.10.0.20240515"
pyinstaller = "^6.10.0"
mkdocs = "^1.6.1"
isort = "^5.13.2"
dill = "^0.3.7"
mkdocs-material = "^9.5.34"
mkdocs-material = "^9.5.40"
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.11.2"
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)
44 changes: 30 additions & 14 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 Down
10 changes: 8 additions & 2 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 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
# fixme 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
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
39 changes: 31 additions & 8 deletions spotdl/download/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,18 @@ def search_and_download( # pylint: disable=R0911
restrict=self.settings["restrict"],
file_name_length=self.settings["max_filename_length"],
)

# Update output path using song.album_name if valid; otherwise, use song.artist.
output_file = (
Path("output")
/ (
Path(song.album_name)
if Path(song.album_name).is_dir()
else Path(song.artist)
)
/ output_file
)

except Exception:
song = reinit_song(song)

Expand All @@ -471,6 +483,17 @@ def search_and_download( # pylint: disable=R0911
file_name_length=self.settings["max_filename_length"],
)

# Update output path using song.album_name if valid; otherwise, use song.artist.
output_file = (
Path("output")
/ (
Path(song.album_name)
if Path(song.album_name).is_dir()
else Path(song.artist)
)
/ output_file
)

reinitialized = True

if song.explicit is True and self.settings["skip_explicit"] is True:
Expand All @@ -489,14 +512,14 @@ def search_and_download( # pylint: disable=R0911
dup_song_paths: List[Path] = self.known_songs.get(song.url, [])

# Remove files from the list that have the same path as the output file
dup_song_paths = [
dup_song_path
for dup_song_path in dup_song_paths
if (dup_song_path.absolute() != output_file.absolute())
and dup_song_path.exists()
]
dup_song_paths = list(Path(output_file.parts[0]).rglob(output_file.name))

# Checking if file already exists in all subfolders of output directory
file_exists = (
next(Path(output_file.parts[0]).rglob(output_file.name), None)
or dup_song_paths
)

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 @@ -572,7 +595,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
2 changes: 2 additions & 0 deletions spotdl/types/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class DownloaderOptions(TypedDict):
lyrics_providers: List[str]
genius_token: str
playlist_numbering: bool
playlist_retain_track_cover: bool
scan_for_songs: bool
m3u: Optional[str]
output: str
Expand Down Expand Up @@ -141,6 +142,7 @@ class DownloaderOptionalOptions(TypedDict, total=False):
lyrics_providers: List[str]
genius_token: str
playlist_numbering: bool
playlist_retain_track_cover: bool
scan_for_songs: bool
m3u: Optional[str]
output: str
Expand Down
11 changes: 11 additions & 0 deletions spotdl/utils/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,17 @@ def parse_output_options(parser: _ArgumentGroup):
and album art as the playlist's icon",
)

# Option to set the track number & album of tracks, while retaining album art of each track, in
# a playlist to their index in the playlist & the name of playlist respectively.
parser.add_argument(
"--playlist-retain-track-cover",
action="store_const",
dest="playlist_retain_track_cover",
const=True,
help="Sets each track in a playlist to have the playlist's name as its album,\
while retaining album art of each track",
)

# Option to scan the output directory for existing files
parser.add_argument(
"--scan-for-songs",
Expand Down
1 change: 1 addition & 0 deletions spotdl/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def get_parameter(cls, key):
"lyrics_providers": ["genius", "azlyrics", "musixmatch"],
"genius_token": "alXXDbPZtK1m2RrZ8I4k2Hn8Ahsd0Gh_o076HYvcdlBvmc0ULL1H8Z8xRlew5qaG",
"playlist_numbering": False,
"playlist_retain_track_cover": False,
"scan_for_songs": False,
"m3u": None,
"output": "{artists} - {title}.{output-ext}",
Expand Down
Loading

0 comments on commit 43bd4d0

Please sign in to comment.