|
#!/bin/bash |
|
|
|
from __future__ import annotations |
|
|
|
from typing import Any, List |
|
|
|
import argparse |
|
import objectrest |
|
from pydantic import BaseModel |
|
|
|
|
|
class Track(BaseModel): |
|
id: str |
|
title: str |
|
artists: str |
|
cover: str |
|
album: str |
|
releaseDate: str |
|
|
|
|
|
class TrackListData(BaseModel): |
|
success: bool |
|
nextOffset: Any |
|
trackList: List[Track] |
|
|
|
|
|
class Metadata(BaseModel): |
|
id: str |
|
artists: str |
|
title: str |
|
cover: str |
|
album: str |
|
releaseDate: str |
|
|
|
|
|
class TrackDownloadSummary(BaseModel): |
|
success: bool |
|
metadata: Metadata |
|
link: str |
|
|
|
|
|
BASE_URL = "https://api.spotifydown.com" |
|
HEADERS = { |
|
'authority': 'api.spotifydown.com', |
|
'accept': '*/*', |
|
'accept-language': 'en-US,en;q=0.9', |
|
'dnt': '1', |
|
'origin': 'https://spotifydown.com', |
|
'referer': 'https://spotifydown.com/', |
|
'sec-ch-ua-mobile': '?0', |
|
'sec-fetch-dest': 'empty', |
|
'sec-fetch-mode': 'cors', |
|
'sec-fetch-site': 'same-site' |
|
} |
|
ALLOWED_FILENAME_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!?" |
|
FILENAME_TEMPLATE = "{artist} - {album} - {title}.mp3" |
|
|
|
def extract_playlist_id(playlist_url: str) -> str: |
|
# remove any query parameters |
|
playlist_url = playlist_url.split("?")[0] |
|
# playlist id is the last part of the url |
|
return playlist_url.split("/")[-1] |
|
|
|
def get_track_list_url(playlist_id: str) -> str: |
|
return f"{BASE_URL}/trackList/playlist/{playlist_id}" |
|
|
|
def get_track_download_url(track_id: str) -> str: |
|
return f"{BASE_URL}/download/{track_id}" |
|
|
|
def format_track_filename(track: TrackDownloadSummary) -> str: |
|
artist = clean_filename_element(track.metadata.artists) |
|
album = clean_filename_element(track.metadata.album) |
|
title = clean_filename_element(track.metadata.title) |
|
return FILENAME_TEMPLATE.format(artist=artist, album=album, title=title) |
|
|
|
def clean_filename_element(filename_element: str) -> str: |
|
# remove any special characters |
|
filename_element = "".join([c for c in filename_element if c == " " or c in ALLOWED_FILENAME_CHARACTERS]).rstrip() |
|
# replace slashes with dashes |
|
filename_element = filename_element.replace("/", "-") |
|
|
|
return filename_element |
|
|
|
# noinspection PyTypeChecker |
|
def main(args: argparse.Namespace): |
|
playlist_url = args.playlist_url |
|
|
|
playlist_id = extract_playlist_id(playlist_url=playlist_url) |
|
|
|
track_list_url = get_track_list_url(playlist_id=playlist_id) |
|
|
|
track_list_data: TrackListData = objectrest.get_object(url=track_list_url, model=TrackListData, headers=HEADERS) |
|
|
|
for track in track_list_data.trackList: |
|
track_download_url = get_track_download_url(track_id=track.id) |
|
|
|
track_download_summary: TrackDownloadSummary = objectrest.get_object(url=track_download_url, |
|
model=TrackDownloadSummary, |
|
headers=HEADERS) |
|
|
|
track_name = format_track_filename(track=track_download_summary) |
|
track_link = track_download_summary.link |
|
|
|
print(f"Downloading {track_name}...") |
|
with open(track_name, "wb") as f: |
|
res = objectrest.get(url=track_link, headers=HEADERS) |
|
f.write(res.content) |
|
|
|
|
|
if __name__ == "__main__": |
|
args_parser = argparse.ArgumentParser(description="Download Spotify playlist") |
|
args_parser.add_argument("playlist_url", type=str, help="Spotify playlist URL") |
|
args = args_parser.parse_args() |
|
|
|
main(args=args) |