Merge remote-tracking branch 'upstream/main'

This commit is contained in:
dabreadman 2021-10-30 18:06:41 +00:00
commit 06d1ac50ea
9 changed files with 106 additions and 62 deletions

View File

@ -34,14 +34,14 @@ Python packages:
### Command line usage: ### Command line usage:
``` ```
Basic usage: Basic command line usage:
python zspotify Loads search prompt to find then download a specific track, album or playlist 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.
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
Extra command line options: Extra command line options:
-p, --playlist Downloads a saved playlist from your account -p, --playlist Downloads a saved playlist from your account
-ls, --liked-songs Downloads all the liked songs 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: 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 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**. **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. 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. 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.

View File

@ -1,4 +1,35 @@
import argparse
from app import client from app import client
if __name__ == '__main__': 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)

View File

@ -2,7 +2,7 @@ from tqdm import tqdm
from const import ITEMS, ARTISTS, NAME, ID from const import ITEMS, ARTISTS, NAME, ID
from track import download_track from track import download_track
from utils import sanitize_data from utils import fix_filename
from zspotify import ZSpotify from zspotify import ZSpotify
ALBUM_URL = 'https://api.spotify.com/v1/albums' ALBUM_URL = 'https://api.spotify.com/v1/albums'
@ -28,7 +28,7 @@ def get_album_tracks(album_id):
def get_album_name(album_id): def get_album_name(album_id):
""" Returns album name """ """ Returns album name """
resp = ZSpotify.invoke_url(f'{ALBUM_URL}/{album_id}') 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): def get_artist_albums(artist_id):
@ -47,9 +47,11 @@ def get_artist_albums(artist_id):
def download_album(album): def download_album(album):
""" Downloads songs from an album """ """ Downloads songs from an album """
artist, album_name = get_album_name(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) tracks = get_album_tracks(album)
for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): 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) prefix=True, prefix_value=str(n), disable_progressbar=True)

View File

@ -1,5 +1,3 @@
import sys
from librespot.audio.decoders import AudioQuality from librespot.audio.decoders import AudioQuality
from tabulate import tabulate 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 playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist
from podcast import download_episode, get_show_episodes from podcast import download_episode, get_show_episodes
from track import download_track, get_saved_tracks 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 from zspotify import ZSpotify
SEARCH_URL = 'https://api.spotify.com/v1/search' 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 """ """ Connects to spotify to perform query's and get songs to download """
ZSpotify() ZSpotify()
splash()
if not args.no_splash:
splash()
if ZSpotify.check_premium(): 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 ZSpotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH
else: 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 ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH
if len(sys.argv) > 1: if args.urls:
if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': for spotify_url in args.urls:
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:
track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_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: if track_id is not None:
download_track(track_id) download_track(track_id)
@ -53,7 +45,7 @@ def client() -> None:
name, _ = get_playlist_info(playlist_id) name, _ = get_playlist_info(playlist_id)
for song in playlist_songs: for song in playlist_songs:
download_track(song[TRACK][ID], download_track(song[TRACK][ID],
sanitize_data(name) + '/') fix_filename(name) + '/')
print('\n') print('\n')
elif episode_id is not None: elif episode_id is not None:
download_episode(episode_id) download_episode(episode_id)
@ -61,7 +53,19 @@ def client() -> None:
for episode in get_show_episodes(show_id): for episode in get_show_episodes(show_id):
download_episode(episode) 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 = '' search_text = ''
while len(search_text) == 0: while len(search_text) == 0:
search_text = input('Enter search or URL: ') search_text = input('Enter search or URL: ')
@ -79,7 +83,7 @@ def client() -> None:
playlist_songs = get_playlist_songs(playlist_id) playlist_songs = get_playlist_songs(playlist_id)
name, _ = get_playlist_info(playlist_id) name, _ = get_playlist_info(playlist_id)
for song in playlist_songs: for song in playlist_songs:
download_track(song[TRACK][ID], sanitize_data(name) + '/') download_track(song[TRACK][ID], fix_filename(name) + '/')
print('\n') print('\n')
elif episode_id is not None: elif episode_id is not None:
download_episode(episode_id) download_episode(episode_id)

View File

@ -1,5 +1,3 @@
SANITIZE = ('\\', '/', ':', '*', '?', '\'', '<', '>', '"')
SAVED_TRACKS_URL = 'https://api.spotify.com/v1/me/tracks' SAVED_TRACKS_URL = 'https://api.spotify.com/v1/me/tracks'
TRACKS_URL = 'https://api.spotify.com/v1/tracks' TRACKS_URL = 'https://api.spotify.com/v1/tracks'

View File

@ -2,7 +2,7 @@ from tqdm import tqdm
from const import ITEMS, ID, TRACK, NAME from const import ITEMS, ID, TRACK, NAME
from track import download_track from track import download_track
from utils import sanitize_data from utils import fix_filename
from zspotify import ZSpotify from zspotify import ZSpotify
MY_PLAYLISTS_URL = 'https://api.spotify.com/v1/me/playlists' 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) p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True)
enum = 1 enum = 1
for song in p_bar: 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) prefix=True, prefix_value=str(enum) ,disable_progressbar=True)
p_bar.set_description(song[TRACK][NAME]) p_bar.set_description(song[TRACK][NAME])
enum += 1 enum += 1
@ -73,17 +73,14 @@ def download_from_user_playlist():
print('> SELECT A RANGE BY ADDING A DASH BETWEEN BOTH ID\'s') 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') 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: start = next(playlist_choices) - 1
download_playlist(playlists[0]) end = next(playlist_choices, start + 1)
else:
start = int(playlist_choices[0])
end = int(playlist_choices[1]) + 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): print('\n**All playlists have been downloaded**\n')
download_playlist(playlists, playlist_number)
print('\n**All playlists have been downloaded**\n')

View File

@ -7,7 +7,7 @@ from tqdm import tqdm
from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW, from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW,
SKIP_EXISTING_FILES) SKIP_EXISTING_FILES)
from utils import create_download_directory, sanitize_data from utils import create_download_directory, fix_filename
from zspotify import ZSpotify from zspotify import ZSpotify
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' 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}') info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}')
if ERROR in info: if ERROR in info:
return None, None 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: def get_show_episodes(show_id_str) -> list:

View File

@ -11,7 +11,7 @@ from tqdm import tqdm
from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ 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, \ 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 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 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 = [] artists = []
for data in info[TRACKS][0][ARTISTS]: for data in info[TRACKS][0][ARTISTS]:
artists.append(sanitize_data(data[NAME])) artists.append(data[NAME])
album_name = sanitize_data(info[TRACKS][0][ALBUM][NAME]) album_name = info[TRACKS][0][ALBUM][NAME]
name = sanitize_data(info[TRACKS][0][NAME]) name = info[TRACKS][0][NAME]
image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL] image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL]
release_year = info[TRACKS][0][ALBUM][RELEASE_DATE].split('-')[0] release_year = info[TRACKS][0][ALBUM][RELEASE_DATE].split('-')[0]
disc_number = info[TRACKS][0][DISC_NUMBER] 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( download_directory = os.path.join(os.path.dirname(
__file__), ZSpotify.get_config(ROOT_PATH), extra_paths) __file__), ZSpotify.get_config(ROOT_PATH), extra_paths)
song_name = artists[0] + ' - ' + name song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name)
if prefix: if prefix:
song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit(
) else f'{prefix_value} - {song_name}' ) else f'{prefix_value} - {song_name}'

View File

@ -8,7 +8,7 @@ from typing import List, Tuple
import music_tag import music_tag
import requests 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 WINDOWS_SYSTEM
@ -60,13 +60,6 @@ def clear() -> None:
os.system('clear') 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: def set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number) -> None:
""" sets music_tag metadata """ """ sets music_tag metadata """
tags = music_tag.load_file(filename) 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 artist_id_str = None
return track_id_str, album_id_str, playlist_id_str, episode_id_str, show_id_str, artist_id_str 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)