Skip to content

Commit

Permalink
feat: added path template (#1401)
Browse files Browse the repository at this point in the history
* feat: added path template

added playlist

added some more checks

simplified code

black

default to None

Update provider_utils.py

Update downloader.py

cassettes

cassettes again

Update query_parser.py

* feat: Add short flag -p  and edit help message and readme

Co-authored-by: Silverarmor <[email protected]>
  • Loading branch information
xnetcat and Silverarmor authored Oct 23, 2021
1 parent 7be4f43 commit ef142ee
Show file tree
Hide file tree
Showing 16 changed files with 23,991 additions and 39,261 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,25 @@ There is an Arch User Repository (AUR) package for [spotDL](https://aur.archlinu
spotdl [songUrl] --ignore-ffmpeg-version
```

- #### To use path template

```bash
spotdl [songUrl] --path-template 'template'
```

example:
```bash
spotdl https://open.spotify.com/track/0VjIjW4GlUZAMYd2vXMi3b --path-template '{artist}/{album}/{title} - {artist}.{ext}'
```

possible values:
- {artist}
- {artists}
- {title}
- {album}
- {ext}
- {playlist}

## `pipx` Isolated Environment Alternative

For users who are not familiar with `pipx`, it can be used to run scripts **without**
Expand Down
21 changes: 17 additions & 4 deletions spotdl/download/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
from spotdl.search import SongObject
from spotdl.download.progress_ui_handler import YTDLLogger
from spotdl.download import ffmpeg, set_id3_data, DisplayManager, DownloadTracker
from spotdl.providers.provider_utils import _get_converted_file_path
from spotdl.providers.provider_utils import (
_get_converted_file_path,
_parse_path_template,
)


class DownloadManager:
Expand All @@ -26,6 +29,7 @@ def __init__(self, arguments: Optional[dict] = None):
arguments.setdefault("ffmpeg", "ffmpeg")
arguments.setdefault("output_format", "mp3")
arguments.setdefault("download_threads", 4)
arguments.setdefault("path_template", None)

if sys.platform == "win32":
# ! ProactorEventLoop is required on Windows to run subprocess asynchronously
Expand Down Expand Up @@ -148,9 +152,18 @@ async def download_song(self, song_object: SongObject) -> None:
if not temp_folder.exists():
temp_folder.mkdir()

converted_file_path = _get_converted_file_path(
song_object, self.arguments["output_format"]
)
if self.arguments["path_template"] is not None:
converted_file_path = _parse_path_template(
self.arguments["path_template"],
song_object,
self.arguments["output_format"],
)
else:
converted_file_path = _get_converted_file_path(
song_object, self.arguments["output_format"]
)

converted_file_path.parent.mkdir(parents=True, exist_ok=True)

# if a song is already downloaded skip it
if converted_file_path.is_file():
Expand Down
17 changes: 15 additions & 2 deletions spotdl/parsers/argument_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,19 @@
spotdl [songUrl] --output-format mp3/m4a/flac/opus/ogg/wav
ex. spotdl [songUrl] --output-format opus
To use ffmpeg binary that is not on PATH run:
To specifiy path template run:
spotdl [songUrl] -p 'template'
ex. spotdl [songUrl] -p "{playlist}/{artists}/{album} - {title} {artist}.{ext}"
To use FFmpeg binary that is not on PATH run:
spotdl [songUrl] --ffmpeg path/to/your/ffmpeg.exe
ex. spotdl [songUrl] --ffmpeg C:\ffmpeg\bin\ffmpeg.exe
To generate .m3u file for each playlist run:
spotdl [playlistUrl] --m3u
ex. spotdl https://open.spotify.com/playlist/37i9dQZF1E8UXBoz02kGID --m3u
To use youtube instead of youtube music run:
To use Youtube instead of YouTube Music run:
spotdl [songUrl] --use-youtube
ex. spotdl https://open.spotify.com/track/4fzsfWzRhPawzqhX8Qt9F3 --use-youtube
Expand Down Expand Up @@ -122,6 +126,15 @@ def parse_arguments():
default="musixmatch",
)

# Option to provide path template for downloaded files
parser.add_argument(
"-p",
"--path-template",
help="Path template for downloaded files",
type=str,
default=None,
)

# Option to specify path to local ffmpeg
parser.add_argument("-f", "--ffmpeg", help="Path to ffmpeg", dest="ffmpeg")

Expand Down
2 changes: 1 addition & 1 deletion spotdl/parsers/query_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,5 @@ def get_youtube_meta_track(
lyrics = lyrics_providers.get_lyrics_musixmatch(song_name, contributing_artist)

return SongObject(
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_url, lyrics
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_url, lyrics, None
)
28 changes: 28 additions & 0 deletions spotdl/providers/provider_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from pathlib import Path
from typing import List

Expand Down Expand Up @@ -124,3 +126,29 @@ def _get_converted_file_path(song_obj, output_format: str = None) -> Path:
return _get_smaller_file_path(song_obj, output_format)

return converted_file_path


def _parse_path_template(path_template, song_object, output_format, short=False):
converted_file_name = path_template

converted_file_name = converted_file_name.format(
artist=_sanitize_filename(song_object.contributing_artists[0]),
title=_sanitize_filename(song_object.song_name),
album=_sanitize_filename(song_object.album_name),
playlist=_sanitize_filename(song_object.playlist_name) if song_object.playlist_name else "",
artists=_sanitize_filename(
", ".join(song_object.contributing_artists)
if short is False
else song_object.contributing_artists[0]
),
ext=_sanitize_filename(output_format),
)

if len(converted_file_name) > 250:
return _parse_path_template(
path_template, song_object, output_format, short=True
)

converted_file_path = Path(converted_file_name)

return converted_file_path
16 changes: 12 additions & 4 deletions spotdl/search/song_gatherer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def from_spotify_url(
output_format: str = None,
use_youtube: bool = False,
lyrics_provider: str = None,
playlist: dict = None,
) -> SongObject:
"""
Creates song object using spotfy url
Expand Down Expand Up @@ -94,7 +95,7 @@ def from_spotify_url(
lyrics = lyrics_providers.get_lyrics_musixmatch(song_name, contributing_artists)

return SongObject(
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics, playlist
)


Expand Down Expand Up @@ -124,7 +125,9 @@ def from_search_term(
raise Exception("No song matches found on Spotify")
song_url = "http://open.spotify.com/track/" + result["tracks"]["items"][0]["id"]
try:
song = from_spotify_url(song_url, output_format, use_youtube, lyrics_provider)
song = from_spotify_url(
song_url, output_format, use_youtube, lyrics_provider, None
)
return [song] if song.youtube_link is not None else []
except (LookupError, OSError, ValueError):
return []
Expand Down Expand Up @@ -180,6 +183,7 @@ def get_tracks(track):
output_format,
use_youtube,
lyrics_provider,
None,
)

if generate_m3u:
Expand Down Expand Up @@ -271,7 +275,8 @@ def from_playlist(
spotify_client = SpotifyClient()
tracks = []

playlist_response = spotify_client.playlist_items(playlist_url)
playlist_response = spotify_client.playlist_tracks(playlist_url)
playlist = spotify_client.playlist(playlist_url)
if playlist_response is None:
raise ValueError("Wrong playlist id")

Expand Down Expand Up @@ -310,6 +315,7 @@ def get_song(track):
output_format,
use_youtube,
lyrics_provider,
playlist,
)

if generate_m3u:
Expand Down Expand Up @@ -491,6 +497,7 @@ def get_song(track_uri):
output_format,
use_youtube,
lyrics_provider,
None,
)
except (LookupError, ValueError, OSError):
return None
Expand Down Expand Up @@ -554,6 +561,7 @@ def get_song(track):
output_format,
use_youtube,
lyrics_provider,
None,
)
except (LookupError, ValueError, OSError):
return None
Expand Down Expand Up @@ -584,5 +592,5 @@ def from_dump(data_dump: dict) -> SongObject:
lyrics = data_dump["lyrics"]

return SongObject(
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics, None
)
21 changes: 20 additions & 1 deletion spotdl/search/song_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ class SongObject:

# Constructor
def __init__(
self, raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics
self,
raw_track_meta,
raw_album_meta,
raw_artist_meta,
youtube_link,
lyrics,
playlist,
):
self._raw_track_meta = raw_track_meta
self._raw_album_meta = raw_album_meta
self._raw_artist_meta = raw_artist_meta
self._youtube_link = youtube_link
self._lyrics = lyrics
self._playlist = playlist

# Equals method
# for example song_obj1 == song_obj2
Expand Down Expand Up @@ -139,6 +146,17 @@ def album_cover_url(self) -> Optional[str]:

return None

@property
def playlist_name(self) -> Optional[str]:
"""
returns name of the playlist that the song belongs to.
"""

if self._playlist is None:
return None

return self._playlist["name"]

@property
def data_dump(self) -> dict:
"""
Expand All @@ -161,6 +179,7 @@ def data_dump(self) -> dict:
"raw_album_meta": self._raw_album_meta,
"raw_artist_meta": self._raw_artist_meta,
"lyrics": self._lyrics,
"playlist": self._playlist,
}

@property
Expand Down
Loading

0 comments on commit ef142ee

Please sign in to comment.