mirror of
https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
synced 2025-07-03 00:03:14 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
0f2e57aa21
80
README.md
80
README.md
@ -37,26 +37,78 @@ Python packages:
|
|||||||
Basic command line usage:
|
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.
|
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:
|
Different usage modes:
|
||||||
|
(nothing) Download the tracks/alumbs/playlists URLs from the parameter
|
||||||
|
-d, --download Download all tracks/alumbs/playlists URLs from the specified file
|
||||||
-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
|
-s, --search Loads search prompt to find then download a specific track, album or playlist
|
||||||
|
|
||||||
|
Extra command line options:
|
||||||
-ns, --no-splash Suppress the splash screen when loading.
|
-ns, --no-splash Suppress the splash screen when loading.
|
||||||
|
--config-location Use a different zs_config.json, defaults to the one in the program directory
|
||||||
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_PODCAST_PATH Change this path if you don't like the default directory where ZSpotify saves the podcasts
|
|
||||||
|
|
||||||
SKIP_EXISTING_FILES Set this to false if you want ZSpotify to overwrite files with the same name rather than skipping the song
|
|
||||||
|
|
||||||
MUSIC_FORMAT Can be "mp3" or "ogg", mp3 is required for track metadata however ogg is slightly higher quality as it is not transcoded.
|
|
||||||
|
|
||||||
FORCE_PREMIUM Set this to true if ZSpotify isn't automatically detecting that you are using a premium account
|
|
||||||
|
|
||||||
ANTI_BAN_WAIT_TIME Change this setting if the time waited between bulk downloads is too high or low
|
|
||||||
OVERRIDE_AUTO_WAIT Change this to true if you want to completely disable the wait between songs for faster downloads with the risk of instability
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Options:
|
||||||
|
|
||||||
|
All these options can either be configured in the zs_config or via the commandline, in case of both the commandline-option has higher priority.
|
||||||
|
Be aware you have to set boolean values in the commandline like this: `--download-real-time=True`
|
||||||
|
|
||||||
|
| Key (zs-config) | commandline parameter | Description
|
||||||
|
|------------------------------|----------------------------------|---------------------------------------------------------------------|
|
||||||
|
| ROOT_PATH | --root-path | directory where ZSpotify saves the music
|
||||||
|
| ROOT_PODCAST_PATH | --root-podcast-path | directory where ZSpotify saves the podcasts
|
||||||
|
| SKIP_EXISTING_FILES | --skip-existing-files | Skip songs with the same name
|
||||||
|
| SKIP_PREVIOUSLY_DOWNLOADED | --skip-previously-downloaded | Create a .song_archive file and skip previously downloaded songs
|
||||||
|
| DOWNLOAD_FORMAT | --download-format | The download audio format (aac, fdk_aac, m4a, mp3, ogg, opus, vorbis)
|
||||||
|
| FORCE_PREMIUM | --force-premium | Force the use of high quality downloads (only with premium accounts)
|
||||||
|
| ANTI_BAN_WAIT_TIME | --anti-ban-wait-time | The wait time between bulk downloads
|
||||||
|
| OVERRIDE_AUTO_WAIT | --override-auto-wait | Totally disable wait time between songs with the risk of instability
|
||||||
|
| CHUNK_SIZE | --chunk-size | chunk size for downloading
|
||||||
|
| SPLIT_ALBUM_DISCS | --split-album-discs | split downloaded albums by disc
|
||||||
|
| DOWNLOAD_REAL_TIME | --download-real-time | only downloads songs as fast as they would be played, can prevent account bans
|
||||||
|
| LANGUAGE | --language | Language for spotify metadata
|
||||||
|
| BITRATE | --bitrate | Overwrite the bitrate for ffmpeg encoding
|
||||||
|
| SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED
|
||||||
|
| CREDENTIALS_LOCATION | --credentials-location | The location of the credentials.json
|
||||||
|
| OUTPUT | --output | The output location/format (see below)
|
||||||
|
| PRINT_SPLASH | --print-splash | Print the splash message
|
||||||
|
| PRINT_SKIPS | --print-skips | Print messages if a song is being skipped
|
||||||
|
| PRINT_DOWNLOAD_PROGRESS | --print-download-progress | Print the download/playlist progress bars
|
||||||
|
| PRINT_ERRORS | --print-errors | Print errors
|
||||||
|
| PRINT_DOWNLOADS | --print-downloads | Print messages when a song is finished downloading
|
||||||
|
|
||||||
|
### Output format:
|
||||||
|
|
||||||
|
With the option `OUTPUT` (or the commandline parameter `--output`) you can specify the output location and format.
|
||||||
|
The value is relative to the `ROOT_PATH`/`ROOT_PODCAST_PATH` directory and can contain the following placeholder:
|
||||||
|
|
||||||
|
| Placeholder | Description
|
||||||
|
|-----------------|--------------------------------
|
||||||
|
| {artist} | The song artist
|
||||||
|
| {album} | The song album
|
||||||
|
| {song_name} | The song name
|
||||||
|
| {release_year} | The song release year
|
||||||
|
| {disc_number} | The disc number
|
||||||
|
| {track_number} | The track_number
|
||||||
|
| {id} | The song id
|
||||||
|
| {track_id} | The track id
|
||||||
|
| {ext} | The file extension
|
||||||
|
| {album_id} | (only when downloading albums) ID of the album
|
||||||
|
| {album_num} | (only when downloading albums) Incrementing track number
|
||||||
|
| {playlist} | (only when downloading playlists) Name of the playlist
|
||||||
|
| {playlist_num} | (only when downloading playlists) Incrementing track number
|
||||||
|
|
||||||
|
Example values could be:
|
||||||
|
~~~~
|
||||||
|
{playlist}/{artist} - {song_name}.{ext}
|
||||||
|
{playlist}/{playlist_num} - {artist} - {song_name}.{ext}
|
||||||
|
Liked Songs/{artist} - {song_name}.{ext}
|
||||||
|
{artist} - {song_name}.{ext}
|
||||||
|
{artist}/{album}/{album_num} - {artist} - {song_name}.{ext}
|
||||||
|
/home/user/downloads/{artist} - {song_name} [{id}].{ext}
|
||||||
|
~~~~
|
||||||
|
|
||||||
### Docker Usage
|
### Docker Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -3,6 +3,5 @@ git+https://github.com/kokarare1212/librespot-python
|
|||||||
music_tag
|
music_tag
|
||||||
Pillow
|
Pillow
|
||||||
protobuf
|
protobuf
|
||||||
pydub
|
|
||||||
tabulate
|
tabulate
|
||||||
tqdm
|
tqdm
|
@ -255,9 +255,7 @@ def search(search_term):
|
|||||||
selection = ''
|
selection = ''
|
||||||
print('\n> SELECT A DOWNLOAD OPTION BY ID')
|
print('\n> SELECT A DOWNLOAD OPTION BY ID')
|
||||||
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('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s')
|
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s\n')
|
||||||
print('> For example, typing 5 to get option 5 or 10-20 to get\nevery option from 10-20 (inclusive)\n')
|
|
||||||
print('> Or type 10,12,15,18 to get those options in particular')
|
|
||||||
while len(selection) == 0:
|
while len(selection) == 0:
|
||||||
selection = str(input('ID(s): '))
|
selection = str(input('ID(s): '))
|
||||||
inputs = split_input(selection)
|
inputs = split_input(selection)
|
||||||
|
@ -70,9 +70,7 @@ def download_from_user_playlist():
|
|||||||
selection = ''
|
selection = ''
|
||||||
print('\n> SELECT A PLAYLIST BY ID')
|
print('\n> SELECT A PLAYLIST BY ID')
|
||||||
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('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s')
|
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s\n')
|
||||||
print('> For example, typing 10 to get one playlist or 10-20 to get\nevery playlist from 10-20 (inclusive)\n')
|
|
||||||
print('> Or type 10,12,15,18 to get those playlists in particular')
|
|
||||||
while len(selection) == 0:
|
while len(selection) == 0:
|
||||||
selection = str(input('ID(s): '))
|
selection = str(input('ID(s): '))
|
||||||
playlist_choices = map(int, split_input(selection))
|
playlist_choices = map(int, split_input(selection))
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@ -6,15 +7,14 @@ 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 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, TRACK_STATS_URL, CODEC_MAP, EXT_MAP, DURATION_MS
|
RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, TRACK_STATS_URL, CODEC_MAP, EXT_MAP, DURATION_MS
|
||||||
from termoutput import Printer, PrintChannel
|
from termoutput import Printer, PrintChannel
|
||||||
from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory, \
|
from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory, \
|
||||||
get_directory_song_ids, add_to_directory_song_ids, get_previously_downloaded, add_to_archive
|
get_directory_song_ids, add_to_directory_song_ids, get_previously_downloaded, add_to_archive, fmt_seconds
|
||||||
from zspotify import ZSpotify
|
from zspotify import ZSpotify
|
||||||
|
import traceback
|
||||||
|
|
||||||
def get_saved_tracks() -> list:
|
def get_saved_tracks() -> list:
|
||||||
""" Returns user's saved tracks """
|
""" Returns user's saved tracks """
|
||||||
@ -133,6 +133,8 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
|||||||
create_download_directory(filedir)
|
create_download_directory(filedir)
|
||||||
total_size = stream.input_stream.size
|
total_size = stream.input_stream.size
|
||||||
|
|
||||||
|
time_start = time.time()
|
||||||
|
downloaded = 0
|
||||||
with open(filename, 'wb') as file, Printer.progress(
|
with open(filename, 'wb') as file, Printer.progress(
|
||||||
desc=song_name,
|
desc=song_name,
|
||||||
total=total_size,
|
total=total_size,
|
||||||
@ -141,18 +143,25 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
|||||||
unit_divisor=1024,
|
unit_divisor=1024,
|
||||||
disable=disable_progressbar
|
disable=disable_progressbar
|
||||||
) as p_bar:
|
) as p_bar:
|
||||||
pause = duration_ms / ZSpotify.CONFIG.get_chunk_size()
|
|
||||||
for chunk in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
for chunk in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
||||||
data = stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())
|
data = stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())
|
||||||
p_bar.update(file.write(data))
|
p_bar.update(file.write(data))
|
||||||
|
downloaded += len(data)
|
||||||
if ZSpotify.CONFIG.get_download_real_time():
|
if ZSpotify.CONFIG.get_download_real_time():
|
||||||
time.sleep(pause)
|
delta_real = time.time() - time_start
|
||||||
|
delta_want = (downloaded / total_size) * (duration_ms/1000)
|
||||||
|
if delta_want > delta_real:
|
||||||
|
time.sleep(delta_want - delta_real)
|
||||||
|
|
||||||
|
time_downloaded = time.time()
|
||||||
|
|
||||||
convert_audio_format(filename)
|
convert_audio_format(filename)
|
||||||
set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number)
|
set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number)
|
||||||
set_music_thumbnail(filename, image_url)
|
set_music_thumbnail(filename, image_url)
|
||||||
|
|
||||||
Printer.print(PrintChannel.DOWNLOADS, f'### Downloaded "{song_name}" to "{os.path.relpath(filename, os.path.dirname(__file__))}" ###' + "\n")
|
time_finished = time.time()
|
||||||
|
|
||||||
|
Printer.print(PrintChannel.DOWNLOADS, f'### Downloaded "{song_name}" to "{os.path.relpath(filename, os.path.dirname(__file__))}" in {fmt_seconds(time_downloaded - time_start)} (plus {fmt_seconds(time_finished - time_downloaded)} converting) ###' + "\n")
|
||||||
|
|
||||||
# add song id to archive file
|
# add song id to archive file
|
||||||
if ZSpotify.CONFIG.get_skip_previously_downloaded():
|
if ZSpotify.CONFIG.get_skip_previously_downloaded():
|
||||||
@ -166,6 +175,7 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###')
|
Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###')
|
||||||
Printer.print(PrintChannel.ERRORS, str(e) + "\n")
|
Printer.print(PrintChannel.ERRORS, str(e) + "\n")
|
||||||
|
Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n")
|
||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
@ -30,6 +31,7 @@ def create_download_directory(download_path: str) -> None:
|
|||||||
with open(hidden_file_path, 'w', encoding='utf-8') as f:
|
with open(hidden_file_path, 'w', encoding='utf-8') as f:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_previously_downloaded() -> List[str]:
|
def get_previously_downloaded() -> List[str]:
|
||||||
""" Returns list of all time downloaded songs """
|
""" Returns list of all time downloaded songs """
|
||||||
|
|
||||||
@ -42,6 +44,7 @@ def get_previously_downloaded() -> List[str]:
|
|||||||
|
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
|
|
||||||
def add_to_archive(song_id: str, filename: str, author_name: str, song_name: str) -> None:
|
def add_to_archive(song_id: str, filename: str, author_name: str, song_name: str) -> None:
|
||||||
""" Adds song id to all time installed songs archive """
|
""" Adds song id to all time installed songs archive """
|
||||||
|
|
||||||
@ -54,6 +57,7 @@ def add_to_archive(song_id: str, filename: str, author_name: str, song_name: str
|
|||||||
with open(archive_path, 'w', encoding='utf-8') as file:
|
with open(archive_path, 'w', encoding='utf-8') as file:
|
||||||
file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
|
file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
|
||||||
|
|
||||||
|
|
||||||
def get_directory_song_ids(download_path: str) -> List[str]:
|
def get_directory_song_ids(download_path: str) -> List[str]:
|
||||||
""" Gets song ids of songs in directory """
|
""" Gets song ids of songs in directory """
|
||||||
|
|
||||||
@ -66,6 +70,7 @@ def get_directory_song_ids(download_path: str) -> List[str]:
|
|||||||
|
|
||||||
return song_ids
|
return song_ids
|
||||||
|
|
||||||
|
|
||||||
def add_to_directory_song_ids(download_path: str, song_id: str, filename: str, author_name: str, song_name: str) -> None:
|
def add_to_directory_song_ids(download_path: str, song_id: str, filename: str, author_name: str, song_name: str) -> None:
|
||||||
""" Appends song_id to .song_ids file in directory """
|
""" Appends song_id to .song_ids file in directory """
|
||||||
|
|
||||||
@ -75,6 +80,7 @@ def add_to_directory_song_ids(download_path: str, song_id: str, filename: str, a
|
|||||||
with open(hidden_file_path, 'a', encoding='utf-8') as file:
|
with open(hidden_file_path, 'a', encoding='utf-8') as file:
|
||||||
file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
|
file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
|
||||||
|
|
||||||
|
|
||||||
def get_downloaded_song_duration(filename: str) -> float:
|
def get_downloaded_song_duration(filename: str) -> float:
|
||||||
""" Returns the downloaded file's duration in seconds """
|
""" Returns the downloaded file's duration in seconds """
|
||||||
|
|
||||||
@ -251,3 +257,26 @@ def fix_filename(name):
|
|||||||
True
|
True
|
||||||
"""
|
"""
|
||||||
return re.sub(r'[/\\:|<>"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$', "_", str(name), flags=re.IGNORECASE)
|
return re.sub(r'[/\\:|<>"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$', "_", str(name), flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_seconds(secs: float) -> str:
|
||||||
|
val = math.floor(secs)
|
||||||
|
|
||||||
|
s = math.floor(val % 60)
|
||||||
|
val -= s
|
||||||
|
val /= 60
|
||||||
|
|
||||||
|
m = math.floor(val % 60)
|
||||||
|
val -= m
|
||||||
|
val /= 60
|
||||||
|
|
||||||
|
h = math.floor(val)
|
||||||
|
|
||||||
|
if h == 0 and m == 0 and s == 0:
|
||||||
|
return "0"
|
||||||
|
elif h == 0 and m == 0:
|
||||||
|
return f'{s}'.zfill(2)
|
||||||
|
elif h == 0:
|
||||||
|
return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
||||||
|
else:
|
||||||
|
return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
||||||
|
@ -49,7 +49,8 @@ class ZSpotify:
|
|||||||
user_name = input('Username: ')
|
user_name = input('Username: ')
|
||||||
password = getpass()
|
password = getpass()
|
||||||
try:
|
try:
|
||||||
cls.SESSION = Session.Builder().user_pass(user_name, password).create()
|
conf = Session.Configuration.Builder().set_stored_credential_file(cred_location).build()
|
||||||
|
cls.SESSION = Session.Builder(conf).user_pass(user_name, password).create()
|
||||||
return
|
return
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
Loading…
x
Reference in New Issue
Block a user