Skip to content

Commit

Permalink
Merge pull request #1393 from spotDL/dev
Browse files Browse the repository at this point in the history
* bugfix: don't add cover if spotify does not provide album cover (#1384)


* 🪶: Refactor Code Expression (#1390)
Co-authored-by: Silverarmor <[email protected]>
Co-authored-by: Jakub Kot <[email protected]>
Co-authored-by: Peyton Creery <[email protected]>


* bugfix: replace rapidfuzz with thefuzz (#1391)


* gitignore update and update tests for multithread search (#1398)


* misc improvements (#1392)
This reverts commit 5787bea.
Co-authored-by: Silverarmor <[email protected]>


* Multiple Lyrics Providers (#1397)
Co-authored-by: Silverarmor <[email protected]>
Co-authored-by: Jakub Kot <[email protected]>


* misc: fix warnings (#1400)


* revert bea748b: revert to rapidfuzz (#1403)


* ci: cleanup (#1402)


* feat: added path template (#1401)
Co-authored-by: Silverarmor <[email protected]>


Co-authored-by: Jakub Kot <[email protected]>
Co-authored-by: Yasser Tahiri <[email protected]>
Co-authored-by: Peyton Creery <[email protected]>
Co-authored-by: Arbaaz Shafiq <[email protected]>
Co-authored-by: Jakub Kot <[email protected]>
  • Loading branch information
6 people authored Oct 24, 2021
2 parents 440f1ac + ef142ee commit 8474fee
Show file tree
Hide file tree
Showing 31 changed files with 28,415 additions and 58,983 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/spotify-downloader-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ jobs:
test:
runs-on: ${{ matrix.platform }}
strategy:
max-parallel: 4
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
python-version: [ 3.6.7, 3.7, 3.8, 3.9 ]
python-version: [ "3.6.7", "3.7", "3.8", "3.9", "3.10" ]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ dist
# Tox
.tox/

# Cache
.cache
# Test Coverage
.coverage
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
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[metadata]
version = 3.8.0
version = 3.9.0

name = spotdl
url = https://github.com/spotDL/spotify-downloader
Expand Down
1 change: 1 addition & 0 deletions spotdl/console/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def graceful_exit(signal, frame):
arguments.output_format,
arguments.use_youtube,
arguments.generate_m3u,
arguments.lyrics_provider,
arguments.search_threads,
)

Expand Down
30 changes: 20 additions & 10 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 All @@ -174,7 +187,7 @@ async def download_song(self, song_object: SongObject) -> None:
audio_handler = YoutubeDL(
{
"format": ytdl_format,
"outtmpl": f"{str(temp_folder)}/%(id)s.%(ext)s",
"outtmpl": f"{temp_folder}/%(id)s.%(ext)s",
"quiet": True,
"no_warnings": True,
"logger": YTDLLogger(),
Expand Down Expand Up @@ -270,14 +283,11 @@ def _perform_audio_download(
# ! The actual download, if there is any error, it'll be here,
try:
data = audio_handler.extract_info(youtube_link)
downloaded_file_path = Path(temp_folder / f"{data['id']}.{data['ext']}")

return downloaded_file_path
except Exception as e: # noqa:E722
# ! This is equivalent to a failed download, we do nothing, the song remains on
# ! download_trackers download queue and all is well...
return Path(temp_folder / f"{data['id']}.{data['ext']}")
except Exception as e:
temp_files = Path(temp_folder).glob(f"{converted_file_name}.*")
for temp_file in temp_files:
temp_file.unlink()

raise e
40 changes: 23 additions & 17 deletions spotdl/download/embed_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@
"explicit": "rtng",
}

TAG_PRESET = {}
for key in M4A_TAG_PRESET.keys():
TAG_PRESET[key] = key
TAG_PRESET = {key: key for key in M4A_TAG_PRESET}


def _set_id3_mp3(converted_file_path: str, song_object: SongObject):
Expand Down Expand Up @@ -143,17 +141,21 @@ def _embed_mp3_metadata(audio_file, song_object: SongObject):
def _embed_mp3_cover(audio_file, song_object, converted_file_path):
# ! setting the album art
audio_file = ID3(converted_file_path)
rawAlbumArt = urlopen(song_object.album_cover_url).read()
audio_file["APIC"] = AlbumCover(
encoding=3, mime="image/jpeg", type=3, desc="Cover", data=rawAlbumArt
)
if song_object.album_cover_url:
rawAlbumArt = urlopen(song_object.album_cover_url).read()
audio_file["APIC"] = AlbumCover(
encoding=3, mime="image/jpeg", type=3, desc="Cover", data=rawAlbumArt
)

return audio_file


def _embed_mp3_lyrics(audio_file, song_object):
# ! setting the lyrics
lyrics = song_object.lyrics
if not lyrics:
return audio_file

USLTOutput = USLT(encoding=3, lang=u"eng", desc=u"desc", text=lyrics)
audio_file["USLT::'eng'"] = USLTOutput

Expand All @@ -178,15 +180,16 @@ def _embed_m4a_metadata(audio_file, song_object: SongObject):

# Explicit values: Dirty: 4, Clean: 2, None: 0
audio_file[M4A_TAG_PRESET["explicit"]] = (0,)
try:
audio_file[M4A_TAG_PRESET["albumart"]] = [
MP4Cover(
urlopen(song_object.album_cover_url).read(),
imageformat=MP4Cover.FORMAT_JPEG,
)
]
except IndexError:
pass
if song_object.album_cover_url:
try:
audio_file[M4A_TAG_PRESET["albumart"]] = [
MP4Cover(
urlopen(song_object.album_cover_url).read(),
imageformat=MP4Cover.FORMAT_JPEG,
)
]
except IndexError:
pass

return audio_file

Expand Down Expand Up @@ -261,6 +264,9 @@ def _embed_ogg_metadata(audio_file, song_object: SongObject):


def _embed_cover(audio_file, song_object, encoding):
if song_object.album_cover_url is None:
return audio_file

image = Picture()
image.type = 3
image.desc = "Cover"
Expand All @@ -269,7 +275,7 @@ def _embed_cover(audio_file, song_object, encoding):

if encoding == "flac":
audio_file.add_picture(image)
elif encoding == "ogg" or encoding == "opus":
elif encoding in ["ogg", "opus"]:
# From the Mutagen docs (https://mutagen.readthedocs.io/en/latest/user/vcomment.html)
image_data = image.write()
encoded_data = base64.b64encode(image_data)
Expand Down
50 changes: 26 additions & 24 deletions spotdl/download/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,38 @@ def has_correct_version(

output = "".join(process.communicate())

if skip_version_check is False:
result = re.search(r"ffmpeg version \w?(\d+\.)?(\d+)", output)

if result is not None:
version = result.group(0).replace("ffmpeg version ", "")

# remove all non numeric characters from string example: n4.3
version = re.sub(r"[a-zA-Z]", "", version)
# remove all non numeric characters from string example: n4.3
if skip_version_check:
return True

if float(version) < 4.2:
print(
f"Your FFmpeg installation is too old ({version}), please update to 4.2+\n",
file=sys.stderr,
)
return False
result = re.search(r"ffmpeg version \w?(\d+\.)?(\d+)", output)

return True
else:
# fallback to copyright date check
date_result = re.search(r"Copyright \(c\) \d\d\d\d\-\d\d\d\d", output)
# fallback to copyright date check
if result is not None:
version = result.group(0).replace("ffmpeg version ", "")

if date_result is not None:
date = date_result.group(0)
if "2021" in date or "2020" in date:
return True
# remove all non numeric characters from string example: n4.3
version = re.sub(r"[a-zA-Z]", "", version)

print("Your FFmpeg version couldn't be detected", file=sys.stderr)
if float(version) < 4.2:
print(
f"Your FFmpeg installation is too old ({version}), please update to 4.2+\n",
file=sys.stderr,
)
return False
else:

return True
else:
# fallback to copyright date check
date_result = re.search(r"Copyright \(c\) \d\d\d\d\-\d\d\d\d", output)

if date_result is not None:
date = date_result.group(0)
if "2021" in date or "2020" in date:
return True

print("Your FFmpeg version couldn't be detected", file=sys.stderr)
return False


async def convert(
Expand Down
10 changes: 5 additions & 5 deletions spotdl/download/progress_ui_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,7 @@ def print(self, *text, color="green"):
if self.quiet:
return

line = ""
for item in text:
line += str(item) + " "

line = "".join(str(item) + " " for item in text)
if color:
self._rich_progress_bar.console.print(f"[{color}]{line}")
else:
Expand Down Expand Up @@ -271,7 +268,10 @@ def notify_error(self, e, tb):
"""
self.update(message="Error " + self.status)

message = f"Error: {e}\tWhile {self.status}: {self.song_object.display_name}\n {str(tb)}"
message = (
f"Error: {e}\tWhile {self.status}: {self.song_object.display_name}\n {tb}"
)

self.parent.print(message, color="red")

def update(self, message=""):
Expand Down
5 changes: 1 addition & 4 deletions spotdl/download/tracking_file_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,7 @@ def backup_to_disk(self):
return None

# prepare datadumps of all songObj's yet to be downloaded
song_data_dumps = []

for song in self.song_list:
song_data_dumps.append(song.data_dump)
song_data_dumps = [song.data_dump for song in self.song_list]

# ! the default naming of a tracking file is $nameOfFirstSOng.spotdlTrackingFile,
# ! it needs a little fixing because of disallowed characters in file naming
Expand Down
28 changes: 25 additions & 3 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 @@ -113,6 +117,24 @@ def parse_arguments():
"--use-youtube", help="Use youtube instead of YTM", action="store_true"
)

# Option to select a lyrics provider
parser.add_argument(
"--lyrics-provider",
help="Select a lyrics provider",
type=str,
choices=["genius", "musixmatch"],
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 All @@ -136,7 +158,7 @@ def parse_arguments():
"--st",
help="Number of threads used when searching for songs",
type=int,
default=1,
default=4,
)

# Option to generate .m3u
Expand Down
Loading

0 comments on commit 8474fee

Please sign in to comment.