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:
|
||||
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
|
||||
-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
|
||||
|
||||
Extra command line options:
|
||||
-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
|
||||
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
|
||||
--config-location Use a different zs_config.json, defaults to the one in the program directory
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
|
@ -3,6 +3,5 @@ git+https://github.com/kokarare1212/librespot-python
|
||||
music_tag
|
||||
Pillow
|
||||
protobuf
|
||||
pydub
|
||||
tabulate
|
||||
tqdm
|
@ -255,9 +255,7 @@ def search(search_term):
|
||||
selection = ''
|
||||
print('\n> SELECT A DOWNLOAD OPTION BY ID')
|
||||
print('> SELECT A RANGE BY ADDING A DASH BETWEEN BOTH ID\'s')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s')
|
||||
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')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s\n')
|
||||
while len(selection) == 0:
|
||||
selection = str(input('ID(s): '))
|
||||
inputs = split_input(selection)
|
||||
|
@ -70,9 +70,7 @@ def download_from_user_playlist():
|
||||
selection = ''
|
||||
print('\n> SELECT A PLAYLIST BY ID')
|
||||
print('> SELECT A RANGE BY ADDING A DASH BETWEEN BOTH ID\'s')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s')
|
||||
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')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s\n')
|
||||
while len(selection) == 0:
|
||||
selection = str(input('ID(s): '))
|
||||
playlist_choices = map(int, split_input(selection))
|
||||
|
@ -1,3 +1,4 @@
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
@ -6,15 +7,14 @@ from typing import Any, Tuple, List
|
||||
from librespot.audio.decoders import AudioQuality
|
||||
from librespot.metadata import TrackId
|
||||
from ffmpy import FFmpeg
|
||||
from pydub import AudioSegment
|
||||
|
||||
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
|
||||
from termoutput import Printer, PrintChannel
|
||||
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
|
||||
|
||||
import traceback
|
||||
|
||||
def get_saved_tracks() -> list:
|
||||
""" 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)
|
||||
total_size = stream.input_stream.size
|
||||
|
||||
time_start = time.time()
|
||||
downloaded = 0
|
||||
with open(filename, 'wb') as file, Printer.progress(
|
||||
desc=song_name,
|
||||
total=total_size,
|
||||
@ -141,18 +143,25 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
||||
unit_divisor=1024,
|
||||
disable=disable_progressbar
|
||||
) as p_bar:
|
||||
pause = duration_ms / ZSpotify.CONFIG.get_chunk_size()
|
||||
for chunk in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
||||
data = stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())
|
||||
p_bar.update(file.write(data))
|
||||
downloaded += len(data)
|
||||
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)
|
||||
set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number)
|
||||
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
|
||||
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:
|
||||
Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###')
|
||||
Printer.print(PrintChannel.ERRORS, str(e) + "\n")
|
||||
Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n")
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
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:
|
||||
pass
|
||||
|
||||
|
||||
def get_previously_downloaded() -> List[str]:
|
||||
""" Returns list of all time downloaded songs """
|
||||
|
||||
@ -42,6 +44,7 @@ def get_previously_downloaded() -> List[str]:
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
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 """
|
||||
|
||||
@ -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:
|
||||
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]:
|
||||
""" Gets song ids of songs in directory """
|
||||
|
||||
@ -66,6 +70,7 @@ def get_directory_song_ids(download_path: str) -> List[str]:
|
||||
|
||||
return song_ids
|
||||
|
||||
|
||||
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 """
|
||||
|
||||
@ -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:
|
||||
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:
|
||||
""" Returns the downloaded file's duration in seconds """
|
||||
|
||||
@ -251,3 +257,26 @@ def fix_filename(name):
|
||||
True
|
||||
"""
|
||||
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: ')
|
||||
password = getpass()
|
||||
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
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
Loading…
x
Reference in New Issue
Block a user