mirror of
				https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
				synced 2025-11-04 05:20:34 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/main'
This commit is contained in:
		
						commit
						0f2e57aa21
					
				
							
								
								
									
										84
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								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:
 | 
			
		||||
  -p, --playlist       Downloads a saved playlist from your account
 | 
			
		||||
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
 | 
			
		||||
  -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