Skip to content

Commit ef142ee

Browse files
xnetcatSilverarmor
andauthored
feat: added path template (spotDL#1401)
* 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]>
1 parent 7be4f43 commit ef142ee

16 files changed

+23991
-39261
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,25 @@ There is an Arch User Repository (AUR) package for [spotDL](https://aur.archlinu
263263
spotdl [songUrl] --ignore-ffmpeg-version
264264
```
265265

266+
- #### To use path template
267+
268+
```bash
269+
spotdl [songUrl] --path-template 'template'
270+
```
271+
272+
example:
273+
```bash
274+
spotdl https://open.spotify.com/track/0VjIjW4GlUZAMYd2vXMi3b --path-template '{artist}/{album}/{title} - {artist}.{ext}'
275+
```
276+
277+
possible values:
278+
- {artist}
279+
- {artists}
280+
- {title}
281+
- {album}
282+
- {ext}
283+
- {playlist}
284+
266285
## `pipx` Isolated Environment Alternative
267286

268287
For users who are not familiar with `pipx`, it can be used to run scripts **without**

spotdl/download/downloader.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from spotdl.search import SongObject
1212
from spotdl.download.progress_ui_handler import YTDLLogger
1313
from spotdl.download import ffmpeg, set_id3_data, DisplayManager, DownloadTracker
14-
from spotdl.providers.provider_utils import _get_converted_file_path
14+
from spotdl.providers.provider_utils import (
15+
_get_converted_file_path,
16+
_parse_path_template,
17+
)
1518

1619

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

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

151-
converted_file_path = _get_converted_file_path(
152-
song_object, self.arguments["output_format"]
153-
)
155+
if self.arguments["path_template"] is not None:
156+
converted_file_path = _parse_path_template(
157+
self.arguments["path_template"],
158+
song_object,
159+
self.arguments["output_format"],
160+
)
161+
else:
162+
converted_file_path = _get_converted_file_path(
163+
song_object, self.arguments["output_format"]
164+
)
165+
166+
converted_file_path.parent.mkdir(parents=True, exist_ok=True)
154167

155168
# if a song is already downloaded skip it
156169
if converted_file_path.is_file():

spotdl/parsers/argument_parser.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,19 @@
2929
spotdl [songUrl] --output-format mp3/m4a/flac/opus/ogg/wav
3030
ex. spotdl [songUrl] --output-format opus
3131
32-
To use ffmpeg binary that is not on PATH run:
32+
To specifiy path template run:
33+
spotdl [songUrl] -p 'template'
34+
ex. spotdl [songUrl] -p "{playlist}/{artists}/{album} - {title} {artist}.{ext}"
35+
36+
To use FFmpeg binary that is not on PATH run:
3337
spotdl [songUrl] --ffmpeg path/to/your/ffmpeg.exe
3438
ex. spotdl [songUrl] --ffmpeg C:\ffmpeg\bin\ffmpeg.exe
3539
3640
To generate .m3u file for each playlist run:
3741
spotdl [playlistUrl] --m3u
3842
ex. spotdl https://open.spotify.com/playlist/37i9dQZF1E8UXBoz02kGID --m3u
3943
40-
To use youtube instead of youtube music run:
44+
To use Youtube instead of YouTube Music run:
4145
spotdl [songUrl] --use-youtube
4246
ex. spotdl https://open.spotify.com/track/4fzsfWzRhPawzqhX8Qt9F3 --use-youtube
4347
@@ -122,6 +126,15 @@ def parse_arguments():
122126
default="musixmatch",
123127
)
124128

129+
# Option to provide path template for downloaded files
130+
parser.add_argument(
131+
"-p",
132+
"--path-template",
133+
help="Path template for downloaded files",
134+
type=str,
135+
default=None,
136+
)
137+
125138
# Option to specify path to local ffmpeg
126139
parser.add_argument("-f", "--ffmpeg", help="Path to ffmpeg", dest="ffmpeg")
127140

spotdl/parsers/query_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,5 @@ def get_youtube_meta_track(
151151
lyrics = lyrics_providers.get_lyrics_musixmatch(song_name, contributing_artist)
152152

153153
return SongObject(
154-
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_url, lyrics
154+
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_url, lyrics, None
155155
)

spotdl/providers/provider_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
from pathlib import Path
24
from typing import List
35

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

126128
return converted_file_path
129+
130+
131+
def _parse_path_template(path_template, song_object, output_format, short=False):
132+
converted_file_name = path_template
133+
134+
converted_file_name = converted_file_name.format(
135+
artist=_sanitize_filename(song_object.contributing_artists[0]),
136+
title=_sanitize_filename(song_object.song_name),
137+
album=_sanitize_filename(song_object.album_name),
138+
playlist=_sanitize_filename(song_object.playlist_name) if song_object.playlist_name else "",
139+
artists=_sanitize_filename(
140+
", ".join(song_object.contributing_artists)
141+
if short is False
142+
else song_object.contributing_artists[0]
143+
),
144+
ext=_sanitize_filename(output_format),
145+
)
146+
147+
if len(converted_file_name) > 250:
148+
return _parse_path_template(
149+
path_template, song_object, output_format, short=True
150+
)
151+
152+
converted_file_path = Path(converted_file_name)
153+
154+
return converted_file_path

spotdl/search/song_gatherer.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def from_spotify_url(http://github.com/%3C/div%3E%3C/code%3E%3C/div%3E%3C/td%3E%3C/tr%3E%3Ctr%20class=%22diff-line-row%22%3E%3Ctd%20data-grid-cell-id=%22diff-06e72073ce9e871a1fa2b825b4566e2489e26b4aee4603863f9dc82c777b4c95-19-19-0%22%20data-selected=%22false%22%20role=%22gridcell%22%20style=%22background-color:var(--bgColor-default);text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative diff-line-number-neutral left-side">19
19
output_format: str = None,
2020
use_youtube: bool = False,
2121
lyrics_provider: str = None,
22+
playlist: dict = None,
2223
) -> SongObject:
2324
"""
2425
Creates song object using spotfy url
@@ -94,7 +95,7 @@ def from_spotify_url(http://github.com/%3C/div%3E%3C/code%3E%3C/div%3E%3C/td%3E%3C/tr%3E%3Ctr%20class=%22diff-line-row%22%3E%3Ctd%20data-grid-cell-id=%22diff-06e72073ce9e871a1fa2b825b4566e2489e26b4aee4603863f9dc82c777b4c95-94-95-0%22%20data-selected=%22false%22%20role=%22gridcell%22%20style=%22background-color:var(--bgColor-default);text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative diff-line-number-neutral left-side">94
95
lyrics = lyrics_providers.get_lyrics_musixmatch(song_name, contributing_artists)
9596

9697
return SongObject(
97-
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics
98+
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics, playlist
9899
)
99100

100101

@@ -124,7 +125,9 @@ def from_search_term(
124125
raise Exception("No song matches found on Spotify")
125126
song_url = "http://open.spotify.com/track/" + result["tracks"]["items"][0]["id"]
126127
try:
127-
song = from_spotify_url(song_url, output_format, use_youtube, lyrics_provider)
128+
song = from_spotify_url(
129+
song_url, output_format, use_youtube, lyrics_provider, None
130+
)
128131
return [song] if song.youtube_link is not None else []
129132
except (LookupError, OSError, ValueError):
130133
return []
@@ -180,6 +183,7 @@ def get_tracks(track):
180183
output_format,
181184
use_youtube,
182185
lyrics_provider,
186+
None,
183187
)
184188

185189
if generate_m3u:
@@ -271,7 +275,8 @@ def from_playlist(
271275
spotify_client = SpotifyClient()
272276
tracks = []
273277

274-
playlist_response = spotify_client.playlist_items(playlist_url)
278+
playlist_response = spotify_client.playlist_tracks(playlist_url)
279+
playlist = spotify_client.playlist(playlist_url)
275280
if playlist_response is None:
276281
raise ValueError("Wrong playlist id")
277282

@@ -310,6 +315,7 @@ def get_song(track):
310315
output_format,
311316
use_youtube,
312317
lyrics_provider,
318+
playlist,
313319
)
314320

315321
if generate_m3u:
@@ -491,6 +497,7 @@ def get_song(track_uri):
491497
output_format,
492498
use_youtube,
493499
lyrics_provider,
500+
None,
494501
)
495502
except (LookupError, ValueError, OSError):
496503
return None
@@ -554,6 +561,7 @@ def get_song(track):
554561
output_format,
555562
use_youtube,
556563
lyrics_provider,
564+
None,
557565
)
558566
except (LookupError, ValueError, OSError):
559567
return None
@@ -584,5 +592,5 @@ def from_dump(data_dump: dict) -> SongObject:
584592
lyrics = data_dump["lyrics"]
585593

586594
return SongObject(
587-
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics
595+
raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics, None
588596
)

spotdl/search/song_object.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ class SongObject:
55

66
# Constructor
77
def __init__(
8-
self, raw_track_meta, raw_album_meta, raw_artist_meta, youtube_link, lyrics
8+
self,
9+
raw_track_meta,
10+
raw_album_meta,
11+
raw_artist_meta,
12+
youtube_link,
13+
lyrics,
14+
playlist,
915
):
1016
self._raw_track_meta = raw_track_meta
1117
self._raw_album_meta = raw_album_meta
1218
self._raw_artist_meta = raw_artist_meta
1319
self._youtube_link = youtube_link
1420
self._lyrics = lyrics
21+
self._playlist = playlist
1522

1623
# Equals method
1724
# for example song_obj1 == song_obj2
@@ -139,6 +146,17 @@ def album_cover_url(http://github.com/self) -> Optional[str]:
139146

140147
return None
141148

149+
@property
150+
def playlist_name(self) -> Optional[str]:
151+
"""
152+
returns name of the playlist that the song belongs to.
153+
"""
154+
155+
if self._playlist is None:
156+
return None
157+
158+
return self._playlist["name"]
159+
142160
@property
143161
def data_dump(self) -> dict:
144162
"""
@@ -161,6 +179,7 @@ def data_dump(self) -> dict:
161179
"raw_album_meta": self._raw_album_meta,
162180
"raw_artist_meta": self._raw_artist_meta,
163181
"lyrics": self._lyrics,
182+
"playlist": self._playlist,
164183
}
165184

166185
@property

0 commit comments

Comments
 (0)