mirror of
https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
synced 2025-07-05 08:54:45 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
06d1ac50ea
10
README.md
10
README.md
@ -34,14 +34,14 @@ Python packages:
|
||||
### Command line usage:
|
||||
|
||||
```
|
||||
Basic usage:
|
||||
python zspotify Loads search prompt to find then download a specific track, album or playlist
|
||||
python zspotify <track/album/playlist/episode url> Downloads the track, album, playlist or podcast episode specified as a command line argument
|
||||
python zspotify <artist url> Downloads all albums by specified artist
|
||||
Basic command line usage:
|
||||
python zspotify <track/album/playlist/episode/artist url> Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. Can take multiple urls.
|
||||
|
||||
Extra command line options:
|
||||
-p, --playlist Downloads a saved playlist from your account
|
||||
-ls, --liked-songs Downloads all the liked songs from your account
|
||||
-s, --search Loads search prompt to find then download a specific track, album or playlist
|
||||
-ns, --no-splash Suppress the splash screen when loading.
|
||||
|
||||
Options that can be configured in zs_config.json:
|
||||
ROOT_PATH Change this path if you don't like the default directory where ZSpotify saves the music
|
||||
@ -77,7 +77,7 @@ There is a community maintained repo for Google Colab at [Ori5000/zspotifycolab]
|
||||
|
||||
**There have been 2-3 reports from users who received account bans from Spotify for using this tool**.
|
||||
|
||||
We recommend using ZSpotify with a burner account.
|
||||
We recommend using ZSpotify with a burner account.
|
||||
Alternatively, there is a configuration option labled ```DOWNLOAD_REAL_TIME```, this limits the download speed to the duration of the song being downloaded thus not appearing suspicious to Spotify.
|
||||
This option is much slower and is only recommended for premium users who wish to download songs in 320kbps without buying premium on a burner account.
|
||||
|
||||
|
@ -1,4 +1,35 @@
|
||||
import argparse
|
||||
|
||||
from app import client
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
client()
|
||||
parser = argparse.ArgumentParser(prog='zspotify',
|
||||
description='A Spotify downloader needing only a python interpreter and ffmpeg.')
|
||||
parser.add_argument('-ns', '--no-splash',
|
||||
action='store_true',
|
||||
help='Suppress the splash screen when loading.')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('urls',
|
||||
type=str,
|
||||
# action='extend',
|
||||
default='',
|
||||
nargs='*',
|
||||
help='Downloads the track, album, playlist, podcast episode, or all albums by an artist from a url. Can take multiple urls.')
|
||||
group.add_argument('-ls', '--liked-songs',
|
||||
dest='liked_songs',
|
||||
action='store_true',
|
||||
help='Downloads all the liked songs from your account.')
|
||||
group.add_argument('-p', '--playlist',
|
||||
action='store_true',
|
||||
help='Downloads a saved playlist from your account.')
|
||||
group.add_argument('-s', '--search',
|
||||
dest='search_spotify',
|
||||
action='store_true',
|
||||
help='Loads search prompt to find then download a specific track, album or playlist')
|
||||
|
||||
parser.set_defaults(func=client)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
@ -2,7 +2,7 @@ from tqdm import tqdm
|
||||
|
||||
from const import ITEMS, ARTISTS, NAME, ID
|
||||
from track import download_track
|
||||
from utils import sanitize_data
|
||||
from utils import fix_filename
|
||||
from zspotify import ZSpotify
|
||||
|
||||
ALBUM_URL = 'https://api.spotify.com/v1/albums'
|
||||
@ -28,7 +28,7 @@ def get_album_tracks(album_id):
|
||||
def get_album_name(album_id):
|
||||
""" Returns album name """
|
||||
resp = ZSpotify.invoke_url(f'{ALBUM_URL}/{album_id}')
|
||||
return resp[ARTISTS][0][NAME], sanitize_data(resp[NAME])
|
||||
return resp[ARTISTS][0][NAME], fix_filename(resp[NAME])
|
||||
|
||||
|
||||
def get_artist_albums(artist_id):
|
||||
@ -47,9 +47,11 @@ def get_artist_albums(artist_id):
|
||||
def download_album(album):
|
||||
""" Downloads songs from an album """
|
||||
artist, album_name = get_album_name(album)
|
||||
artist_fixed = fix_filename(artist)
|
||||
album_name_fixed = fix_filename(album_name)
|
||||
tracks = get_album_tracks(album)
|
||||
for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)):
|
||||
download_track(track[ID], f'{artist}/{album_name}',
|
||||
download_track(track[ID], f'{artist_fixed}/{album_name_fixed}',
|
||||
prefix=True, prefix_value=str(n), disable_progressbar=True)
|
||||
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import sys
|
||||
|
||||
from librespot.audio.decoders import AudioQuality
|
||||
from tabulate import tabulate
|
||||
|
||||
@ -9,38 +7,32 @@ from const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALB
|
||||
from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist
|
||||
from podcast import download_episode, get_show_episodes
|
||||
from track import download_track, get_saved_tracks
|
||||
from utils import sanitize_data, splash, split_input, regex_input_for_urls
|
||||
from utils import fix_filename, splash, split_input, regex_input_for_urls
|
||||
from zspotify import ZSpotify
|
||||
|
||||
SEARCH_URL = 'https://api.spotify.com/v1/search'
|
||||
|
||||
|
||||
def client() -> None:
|
||||
def client(args) -> None:
|
||||
""" Connects to spotify to perform query's and get songs to download """
|
||||
ZSpotify()
|
||||
splash()
|
||||
|
||||
if not args.no_splash:
|
||||
splash()
|
||||
|
||||
if ZSpotify.check_premium():
|
||||
print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n')
|
||||
if not args.no_splash:
|
||||
print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n')
|
||||
ZSpotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH
|
||||
else:
|
||||
print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n')
|
||||
if not args.no_splash:
|
||||
print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n')
|
||||
ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == '-p' or sys.argv[1] == '--playlist':
|
||||
download_from_user_playlist()
|
||||
elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs':
|
||||
for song in get_saved_tracks():
|
||||
if not song[TRACK][NAME]:
|
||||
print(
|
||||
'### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###')
|
||||
else:
|
||||
download_track(song[TRACK][ID], 'Liked Songs/')
|
||||
print('\n')
|
||||
else:
|
||||
if args.urls:
|
||||
for spotify_url in args.urls:
|
||||
track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(
|
||||
sys.argv[1])
|
||||
spotify_url)
|
||||
|
||||
if track_id is not None:
|
||||
download_track(track_id)
|
||||
@ -53,7 +45,7 @@ def client() -> None:
|
||||
name, _ = get_playlist_info(playlist_id)
|
||||
for song in playlist_songs:
|
||||
download_track(song[TRACK][ID],
|
||||
sanitize_data(name) + '/')
|
||||
fix_filename(name) + '/')
|
||||
print('\n')
|
||||
elif episode_id is not None:
|
||||
download_episode(episode_id)
|
||||
@ -61,7 +53,19 @@ def client() -> None:
|
||||
for episode in get_show_episodes(show_id):
|
||||
download_episode(episode)
|
||||
|
||||
else:
|
||||
if args.playlist:
|
||||
download_from_user_playlist()
|
||||
|
||||
if args.liked_songs:
|
||||
for song in get_saved_tracks():
|
||||
if not song[TRACK][NAME]:
|
||||
print(
|
||||
'### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###')
|
||||
else:
|
||||
download_track(song[TRACK][ID], 'Liked Songs/')
|
||||
print('\n')
|
||||
|
||||
if args.search_spotify:
|
||||
search_text = ''
|
||||
while len(search_text) == 0:
|
||||
search_text = input('Enter search or URL: ')
|
||||
@ -79,7 +83,7 @@ def client() -> None:
|
||||
playlist_songs = get_playlist_songs(playlist_id)
|
||||
name, _ = get_playlist_info(playlist_id)
|
||||
for song in playlist_songs:
|
||||
download_track(song[TRACK][ID], sanitize_data(name) + '/')
|
||||
download_track(song[TRACK][ID], fix_filename(name) + '/')
|
||||
print('\n')
|
||||
elif episode_id is not None:
|
||||
download_episode(episode_id)
|
||||
|
@ -1,5 +1,3 @@
|
||||
SANITIZE = ('\\', '/', ':', '*', '?', '\'', '<', '>', '"')
|
||||
|
||||
SAVED_TRACKS_URL = 'https://api.spotify.com/v1/me/tracks'
|
||||
|
||||
TRACKS_URL = 'https://api.spotify.com/v1/tracks'
|
||||
|
@ -2,7 +2,7 @@ from tqdm import tqdm
|
||||
|
||||
from const import ITEMS, ID, TRACK, NAME
|
||||
from track import download_track
|
||||
from utils import sanitize_data
|
||||
from utils import fix_filename
|
||||
from zspotify import ZSpotify
|
||||
|
||||
MY_PLAYLISTS_URL = 'https://api.spotify.com/v1/me/playlists'
|
||||
@ -54,7 +54,7 @@ def download_playlist(playlist):
|
||||
p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True)
|
||||
enum = 1
|
||||
for song in p_bar:
|
||||
download_track(song[TRACK][ID], sanitize_data(playlist[NAME].strip()) + '/',
|
||||
download_track(song[TRACK][ID], fix_filename(playlist[NAME].strip()) + '/',
|
||||
prefix=True, prefix_value=str(enum) ,disable_progressbar=True)
|
||||
p_bar.set_description(song[TRACK][NAME])
|
||||
enum += 1
|
||||
@ -73,17 +73,14 @@ def download_from_user_playlist():
|
||||
print('> SELECT A RANGE BY ADDING A DASH BETWEEN BOTH ID\'s')
|
||||
print('> For example, typing 10 to get one playlist or 10-20 to get\nevery playlist from 10-20 (inclusive)\n')
|
||||
|
||||
playlist_choices = input('ID(s): ').split('-')
|
||||
playlist_choices = map(int, input('ID(s): ').split('-'))
|
||||
|
||||
if len(playlist_choices) == 1:
|
||||
download_playlist(playlists[0])
|
||||
else:
|
||||
start = int(playlist_choices[0])
|
||||
end = int(playlist_choices[1]) + 1
|
||||
start = next(playlist_choices) - 1
|
||||
end = next(playlist_choices, start + 1)
|
||||
|
||||
print(f'Downloading from {start} to {end}...')
|
||||
for playlist_number in range(start, end):
|
||||
playlist = playlists[playlist_number]
|
||||
print(f'Downloading {playlist[NAME].strip()}')
|
||||
download_playlist(playlist)
|
||||
|
||||
for playlist_number in range(start, end):
|
||||
download_playlist(playlists, playlist_number)
|
||||
|
||||
print('\n**All playlists have been downloaded**\n')
|
||||
print('\n**All playlists have been downloaded**\n')
|
||||
|
@ -7,7 +7,7 @@ from tqdm import tqdm
|
||||
|
||||
from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW,
|
||||
SKIP_EXISTING_FILES)
|
||||
from utils import create_download_directory, sanitize_data
|
||||
from utils import create_download_directory, fix_filename
|
||||
from zspotify import ZSpotify
|
||||
|
||||
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes'
|
||||
@ -18,7 +18,7 @@ def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]:
|
||||
info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}')
|
||||
if ERROR in info:
|
||||
return None, None
|
||||
return sanitize_data(info[SHOW][NAME]), sanitize_data(info[NAME])
|
||||
return fix_filename(info[SHOW][NAME]), fix_filename(info[NAME])
|
||||
|
||||
|
||||
def get_show_episodes(show_id_str) -> list:
|
||||
|
@ -11,7 +11,7 @@ from tqdm import tqdm
|
||||
from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \
|
||||
RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, CHUNK_SIZE, \
|
||||
SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT, BITRATE, CODEC_MAP, EXT_MAP, DOWNLOAD_REAL_TIME
|
||||
from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory
|
||||
from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory
|
||||
from zspotify import ZSpotify
|
||||
|
||||
|
||||
@ -38,9 +38,9 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any
|
||||
|
||||
artists = []
|
||||
for data in info[TRACKS][0][ARTISTS]:
|
||||
artists.append(sanitize_data(data[NAME]))
|
||||
album_name = sanitize_data(info[TRACKS][0][ALBUM][NAME])
|
||||
name = sanitize_data(info[TRACKS][0][NAME])
|
||||
artists.append(data[NAME])
|
||||
album_name = info[TRACKS][0][ALBUM][NAME]
|
||||
name = info[TRACKS][0][NAME]
|
||||
image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL]
|
||||
release_year = info[TRACKS][0][ALBUM][RELEASE_DATE].split('-')[0]
|
||||
disc_number = info[TRACKS][0][DISC_NUMBER]
|
||||
@ -66,7 +66,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
||||
download_directory = os.path.join(os.path.dirname(
|
||||
__file__), ZSpotify.get_config(ROOT_PATH), extra_paths)
|
||||
|
||||
song_name = artists[0] + ' - ' + name
|
||||
song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name)
|
||||
if prefix:
|
||||
song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit(
|
||||
) else f'{prefix_value} - {song_name}'
|
||||
|
@ -8,7 +8,7 @@ from typing import List, Tuple
|
||||
import music_tag
|
||||
import requests
|
||||
|
||||
from const import SANITIZE, ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \
|
||||
from const import ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \
|
||||
WINDOWS_SYSTEM
|
||||
|
||||
|
||||
@ -60,13 +60,6 @@ def clear() -> None:
|
||||
os.system('clear')
|
||||
|
||||
|
||||
def sanitize_data(value) -> str:
|
||||
""" Returns given string with problematic removed """
|
||||
for pattern in SANITIZE:
|
||||
value = value.replace(pattern, '')
|
||||
return value.replace('|', '-')
|
||||
|
||||
|
||||
def set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number) -> None:
|
||||
""" sets music_tag metadata """
|
||||
tags = music_tag.load_file(filename)
|
||||
@ -179,3 +172,22 @@ def regex_input_for_urls(search_input) -> Tuple[str, str, str, str, str, str]:
|
||||
artist_id_str = None
|
||||
|
||||
return track_id_str, album_id_str, playlist_id_str, episode_id_str, show_id_str, artist_id_str
|
||||
|
||||
|
||||
def fix_filename(name):
|
||||
"""
|
||||
Replace invalid characters on Linux/Windows/MacOS with underscores.
|
||||
List from https://stackoverflow.com/a/31976060/819417
|
||||
Trailing spaces & periods are ignored on Windows.
|
||||
>>> fix_filename(" COM1 ")
|
||||
'_ COM1 _'
|
||||
>>> fix_filename("COM10")
|
||||
'COM10'
|
||||
>>> fix_filename("COM1,")
|
||||
'COM1,'
|
||||
>>> fix_filename("COM1.txt")
|
||||
'_.txt'
|
||||
>>> all('_' == fix_filename(chr(i)) for i in list(range(32)))
|
||||
True
|
||||
"""
|
||||
return re.sub(r'[/\\:|<>"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$', "_", name, flags=re.IGNORECASE)
|
||||
|
Loading…
x
Reference in New Issue
Block a user