From 26ed5bef39b1856e6f656af1f1284d6d00426c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Mon, 22 Nov 2021 10:31:19 +0100 Subject: [PATCH 1/5] Update README --- README.md | 84 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index aed534e9..0d10a019 100644 --- a/README.md +++ b/README.md @@ -37,26 +37,78 @@ Python packages: Basic command line usage: python zspotify 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 ``` From c42b8131d16b513033dcaf2355dd5f2a352d2d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Mon, 22 Nov 2021 14:33:10 +0100 Subject: [PATCH 2/5] Fix credentials.json not being saved in CREDENTIALS_LOCATION path --- zspotify/zspotify.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index 51ef6c64..4e918426 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -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 From 0c6ec8a108c6841729cbba9b2a9d2b9d360dfb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Mon, 22 Nov 2021 11:41:38 +0100 Subject: [PATCH 3/5] Fix delaytime with DOWNLOAD_REAL_TIME ( fixes #175 ) --- zspotify/track.py | 18 ++++++++++++++---- zspotify/utils.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index 8f4ef9de..e40c0a8d 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -1,3 +1,4 @@ +import math import os import re import time @@ -12,7 +13,7 @@ from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAY 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 @@ -133,6 +134,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 +144,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(): diff --git a/zspotify/utils.py b/zspotify/utils.py index c7da4c39..e421b339 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -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) From c5886e340114a09c181ca6b1052885176d312be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Mon, 22 Nov 2021 14:45:20 +0100 Subject: [PATCH 4/5] Exception traceback in GENERAL DOWNLOAD ERROR --- zspotify/track.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index 8f4ef9de..73197ae4 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -14,7 +14,7 @@ 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 from zspotify import ZSpotify - +import traceback def get_saved_tracks() -> list: """ Returns user's saved tracks """ @@ -131,7 +131,7 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= stream = ZSpotify.get_content_stream( track_id, ZSpotify.DOWNLOAD_QUALITY) create_download_directory(filedir) - total_size = stream.input_stream.size + total_size = stream.input_stream.size / "" with open(filename, 'wb') as file, Printer.progress( desc=song_name, @@ -166,6 +166,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) From cb42b4a87833d64b004042327d62ffefe7554fce Mon Sep 17 00:00:00 2001 From: logykk Date: Wed, 24 Nov 2021 21:22:54 +1300 Subject: [PATCH 5/5] removed pydub dependency --- requirements.txt | 1 - zspotify/app.py | 4 +--- zspotify/playlist.py | 4 +--- zspotify/track.py | 3 +-- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index b1877ecf..87072683 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,5 @@ git+https://github.com/kokarare1212/librespot-python music_tag Pillow protobuf -pydub tabulate tqdm \ No newline at end of file diff --git a/zspotify/app.py b/zspotify/app.py index 75be3156..3bf32fa1 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -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) diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 67ddb336..36cac397 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -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)) diff --git a/zspotify/track.py b/zspotify/track.py index d7dde3d5..fb91b10a 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -7,7 +7,6 @@ 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 @@ -132,7 +131,7 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= stream = ZSpotify.get_content_stream( track_id, ZSpotify.DOWNLOAD_QUALITY) create_download_directory(filedir) - total_size = stream.input_stream.size / "" + total_size = stream.input_stream.size time_start = time.time() downloaded = 0