mirror of
https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
synced 2025-07-01 07:13:14 +00:00
Merge branch 'main' into Playlist-id-fix
This commit is contained in:
commit
9eabd55104
@ -1,4 +1,10 @@
|
|||||||
## **Changelog:**
|
## **Changelog:**
|
||||||
|
**v2.4 (27 Oct 20212):**
|
||||||
|
- Added realtime downloading support to avoid account suspensions.
|
||||||
|
- Fix for downloading by artist.
|
||||||
|
- Replace audio conversion method for better quality.
|
||||||
|
- Fix bug when automatically setting audio bitrate.
|
||||||
|
|
||||||
**v2.3 (25 Oct 2021):**
|
**v2.3 (25 Oct 2021):**
|
||||||
- Moved changelog to seperate file.
|
- Moved changelog to seperate file.
|
||||||
- Added argument parsing in search function (query results limit and query result types).
|
- Added argument parsing in search function (query results limit and query result types).
|
||||||
|
16
README.md
16
README.md
@ -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
|
||||||
@ -68,14 +68,18 @@ Create and run a container from the image:
|
|||||||
docker run --rm -v "$PWD/ZSpotify Music:/ZSpotify Music" -v "$PWD/ZSpotify Podcasts:/ZSpotify Podcasts" -it zspotify
|
docker run --rm -v "$PWD/ZSpotify Music:/ZSpotify Music" -v "$PWD/ZSpotify Podcasts:/ZSpotify Podcasts" -it zspotify
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Google Colab
|
||||||
|
There is a community maintained repo for Google Colab at [Ori5000/zspotifycolab](https://github.com/Ori5000/zspotifycolab) designed to make it easier to add songs to Google Drive or orther cloud services.
|
||||||
|
|
||||||
### Will my account get banned if I use this tool?
|
### Will my account get banned if I use this tool?
|
||||||
|
|
||||||
~~Currently no user has reported their account getting banned after using ZSpotify.~~
|
~~Currently no user has reported their account getting banned after using ZSpotify.~~
|
||||||
|
|
||||||
**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 are working on making it less likely for your account to get banned, please be patient.
|
We recommend using ZSpotify with a burner account.
|
||||||
We suggest you wait till this fix has been made to avoid ban, or if you are in a hurry, make sure to not use your primary 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.
|
||||||
|
|
||||||
**Use ZSpotify at your own risk**, the developers of ZSpotify are not responsible if your account gets banned.
|
**Use ZSpotify at your own risk**, the developers of ZSpotify are not responsible if your account gets banned.
|
||||||
|
|
||||||
|
@ -2,5 +2,7 @@ ffmpy
|
|||||||
git+https://github.com/kokarare1212/librespot-python
|
git+https://github.com/kokarare1212/librespot-python
|
||||||
music_tag
|
music_tag
|
||||||
Pillow
|
Pillow
|
||||||
|
protobuf
|
||||||
|
pydub
|
||||||
tabulate
|
tabulate
|
||||||
tqdm
|
tqdm
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -15,32 +13,26 @@ 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()
|
||||||
|
|
||||||
|
if not args.no_splash:
|
||||||
splash()
|
splash()
|
||||||
|
|
||||||
if ZSpotify.check_premium():
|
if ZSpotify.check_premium():
|
||||||
|
if not args.no_splash:
|
||||||
print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n')
|
print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n')
|
||||||
ZSpotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH
|
ZSpotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH
|
||||||
else:
|
else:
|
||||||
|
if not args.no_splash:
|
||||||
print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n')
|
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)
|
||||||
@ -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)
|
||||||
|
|
||||||
|
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:
|
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: ')
|
||||||
|
@ -72,6 +72,8 @@ USER_READ_EMAIL = 'user-read-email'
|
|||||||
|
|
||||||
PLAYLIST_READ_PRIVATE = 'playlist-read-private'
|
PLAYLIST_READ_PRIVATE = 'playlist-read-private'
|
||||||
|
|
||||||
|
USER_LIBRARY_READ = 'user-library-read'
|
||||||
|
|
||||||
WINDOWS_SYSTEM = 'Windows'
|
WINDOWS_SYSTEM = 'Windows'
|
||||||
|
|
||||||
CREDENTIALS_JSON = 'credentials.json'
|
CREDENTIALS_JSON = 'credentials.json'
|
||||||
@ -96,6 +98,8 @@ CHUNK_SIZE = 'CHUNK_SIZE'
|
|||||||
|
|
||||||
SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS'
|
SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS'
|
||||||
|
|
||||||
|
DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME'
|
||||||
|
|
||||||
BITRATE = 'BITRATE'
|
BITRATE = 'BITRATE'
|
||||||
|
|
||||||
CODEC_MAP = {
|
CODEC_MAP = {
|
||||||
@ -127,5 +131,6 @@ CONFIG_DEFAULT_SETTINGS = {
|
|||||||
'ANTI_BAN_WAIT_TIME': 1,
|
'ANTI_BAN_WAIT_TIME': 1,
|
||||||
'OVERRIDE_AUTO_WAIT': False,
|
'OVERRIDE_AUTO_WAIT': False,
|
||||||
'CHUNK_SIZE': 50000,
|
'CHUNK_SIZE': 50000,
|
||||||
'SPLIT_ALBUM_DISCS': False
|
'SPLIT_ALBUM_DISCS': False,
|
||||||
|
'DOWNLOAD_REAL_TIME': False
|
||||||
}
|
}
|
||||||
|
@ -52,10 +52,12 @@ def download_playlist(playlist):
|
|||||||
|
|
||||||
playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]]
|
playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]]
|
||||||
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
|
||||||
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], sanitize_data(playlist[NAME].strip()) + '/',
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
def download_from_user_playlist():
|
def download_from_user_playlist():
|
||||||
|
@ -5,11 +5,11 @@ from librespot.audio.decoders import VorbisOnlyAudioQuality
|
|||||||
from librespot.metadata import EpisodeId
|
from librespot.metadata import EpisodeId
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from const import NAME, ERROR, SHOW, ITEMS, ID, ROOT_PODCAST_PATH, CHUNK_SIZE
|
from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW,
|
||||||
from utils import sanitize_data, create_download_directory, MusicFormat
|
SKIP_EXISTING_FILES)
|
||||||
|
from utils import create_download_directory, sanitize_data
|
||||||
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'
|
||||||
SHOWS_URL = 'https://api.spotify.com/v1/shows'
|
SHOWS_URL = 'https://api.spotify.com/v1/shows'
|
||||||
|
|
||||||
@ -55,11 +55,27 @@ def download_episode(episode_id) -> None:
|
|||||||
ZSpotify.get_config(ROOT_PODCAST_PATH),
|
ZSpotify.get_config(ROOT_PODCAST_PATH),
|
||||||
extra_paths,
|
extra_paths,
|
||||||
)
|
)
|
||||||
|
download_directory = os.path.realpath(download_directory)
|
||||||
create_download_directory(download_directory)
|
create_download_directory(download_directory)
|
||||||
|
|
||||||
total_size = stream.input_stream.size
|
total_size = stream.input_stream.size
|
||||||
with open(os.path.join(download_directory, f"{filename}.ogg"),
|
|
||||||
'wb') as file, tqdm(
|
filepath = os.path.join(download_directory, f"{filename}.ogg")
|
||||||
|
if (
|
||||||
|
os.path.isfile(filepath)
|
||||||
|
and os.path.getsize(filepath) == total_size
|
||||||
|
and ZSpotify.get_config(SKIP_EXISTING_FILES)
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
"\n### SKIPPING:",
|
||||||
|
podcast_name,
|
||||||
|
"-",
|
||||||
|
episode_name,
|
||||||
|
"(EPISODE ALREADY EXISTS) ###",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(filepath, 'wb') as file, tqdm(
|
||||||
desc=filename,
|
desc=filename,
|
||||||
total=total_size,
|
total=total_size,
|
||||||
unit='B',
|
unit='B',
|
||||||
|
@ -5,11 +5,12 @@ from typing import Any, Tuple, List
|
|||||||
from librespot.audio.decoders import AudioQuality
|
from librespot.audio.decoders import AudioQuality
|
||||||
from librespot.metadata import TrackId
|
from librespot.metadata import TrackId
|
||||||
from ffmpy import FFmpeg
|
from ffmpy import FFmpeg
|
||||||
|
from pydub import AudioSegment
|
||||||
from tqdm import tqdm
|
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
|
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 sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory
|
||||||
from zspotify import ZSpotify
|
from zspotify import ZSpotify
|
||||||
|
|
||||||
@ -71,7 +72,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
|||||||
) else f'{prefix_value} - {song_name}'
|
) else f'{prefix_value} - {song_name}'
|
||||||
|
|
||||||
filename = os.path.join(
|
filename = os.path.join(
|
||||||
download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.get_config(DOWNLOAD_FORMAT))}')
|
download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.get_config(DOWNLOAD_FORMAT).lower())}')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('### SKIPPING SONG - FAILED TO QUERY METADATA ###')
|
print('### SKIPPING SONG - FAILED TO QUERY METADATA ###')
|
||||||
@ -102,9 +103,16 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
|||||||
unit_divisor=1024,
|
unit_divisor=1024,
|
||||||
disable=disable_progressbar
|
disable=disable_progressbar
|
||||||
) as p_bar:
|
) as p_bar:
|
||||||
for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1):
|
for chunk in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1):
|
||||||
p_bar.update(file.write(
|
data = stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE))
|
||||||
stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE))))
|
if data == b'':
|
||||||
|
break
|
||||||
|
p_bar.update(file.write(data))
|
||||||
|
if ZSpotify.get_config(DOWNLOAD_REAL_TIME):
|
||||||
|
if chunk == 0:
|
||||||
|
pause = get_segment_duration(p_bar)
|
||||||
|
if pause:
|
||||||
|
time.sleep(pause)
|
||||||
|
|
||||||
convert_audio_format(filename)
|
convert_audio_format(filename)
|
||||||
set_audio_tags(filename, artists, name, album_name,
|
set_audio_tags(filename, artists, name, album_name,
|
||||||
@ -121,12 +129,24 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
|||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def get_segment_duration(segment):
|
||||||
|
""" Returns playback duration of given audio segment """
|
||||||
|
sound = AudioSegment(
|
||||||
|
data = segment,
|
||||||
|
sample_width = 2,
|
||||||
|
frame_rate = 44100,
|
||||||
|
channels = 2
|
||||||
|
)
|
||||||
|
duration = len(sound) / 5000
|
||||||
|
return duration
|
||||||
|
|
||||||
|
|
||||||
def convert_audio_format(filename) -> None:
|
def convert_audio_format(filename) -> None:
|
||||||
""" Converts raw audio into playable file """
|
""" Converts raw audio into playable file """
|
||||||
temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
|
temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
|
||||||
os.replace(filename, temp_filename)
|
os.replace(filename, temp_filename)
|
||||||
|
|
||||||
download_format = ZSpotify.get_config(DOWNLOAD_FORMAT)
|
download_format = ZSpotify.get_config(DOWNLOAD_FORMAT).lower()
|
||||||
file_codec = CODEC_MAP.get(download_format, "copy")
|
file_codec = CODEC_MAP.get(download_format, "copy")
|
||||||
if file_codec != 'copy':
|
if file_codec != 'copy':
|
||||||
bitrate = ZSpotify.get_config(BITRATE)
|
bitrate = ZSpotify.get_config(BITRATE)
|
||||||
|
@ -18,7 +18,7 @@ from librespot.core import Session
|
|||||||
|
|
||||||
from const import CREDENTIALS_JSON, TYPE, \
|
from const import CREDENTIALS_JSON, TYPE, \
|
||||||
PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, CONFIG_FILE_PATH, FORCE_PREMIUM, \
|
PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, CONFIG_FILE_PATH, FORCE_PREMIUM, \
|
||||||
PLAYLIST_READ_PRIVATE, CONFIG_DEFAULT_SETTINGS
|
PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ, CONFIG_DEFAULT_SETTINGS
|
||||||
from utils import MusicFormat
|
from utils import MusicFormat
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ class ZSpotify:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __get_auth_token(cls):
|
def __get_auth_token(cls):
|
||||||
return cls.SESSION.tokens().get_token(USER_READ_EMAIL, PLAYLIST_READ_PRIVATE).access_token
|
return cls.SESSION.tokens().get_token(USER_READ_EMAIL, PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ).access_token
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_auth_header(cls):
|
def get_auth_header(cls):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user