From 1120592f0fd643c5e05e00c49aa718cc2e33f5a0 Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 14:45:06 +0100 Subject: [PATCH 01/13] added genre retrieval --- zspotify/config.py | 14 +++++++++++++- zspotify/const.py | 4 ++++ zspotify/track.py | 20 +++++++++++++++----- zspotify/utils.py | 5 +++-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/zspotify/config.py b/zspotify/config.py index 0ba7d6d2..0c4e9cf0 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -26,6 +26,8 @@ PRINT_DOWNLOAD_PROGRESS = 'PRINT_DOWNLOAD_PROGRESS' PRINT_ERRORS = 'PRINT_ERRORS' PRINT_DOWNLOADS = 'PRINT_DOWNLOADS' TEMP_DOWNLOAD_DIR = 'TEMP_DOWNLOAD_DIR' +MD_ALLGENRES = 'MD_ALLGENRES' +MD_GENREDELIMITER = 'MD_GENREDELIMITER' CONFIG_VALUES = { ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' }, @@ -49,7 +51,9 @@ CONFIG_VALUES = { PRINT_DOWNLOAD_PROGRESS: { 'default': 'True', 'type': bool, 'arg': '--print-download-progress' }, PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' }, PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' }, - TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' }, + MD_ALLGENRES: { 'default': 'False', 'type': bool, 'arg': '--md-allgenres' }, + MD_GENREDELIMITER: { 'default': ';', 'type': str, 'arg': '--md-genredelimiter' }, + TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' } } OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}' @@ -192,7 +196,15 @@ class Config: if cls.get(TEMP_DOWNLOAD_DIR) == '': return '' return os.path.join(cls.get_root_path(), cls.get(TEMP_DOWNLOAD_DIR)) + + @classmethod + def get_allGenres(cls) -> bool: + return cls.get(MD_ALLGENRES) + @classmethod + def get_allGenresDelimiter(cls) -> bool: + return cls.get(MD_GENREDELIMITER) + @classmethod def get_output(cls, mode: str) -> str: v = cls.get(OUTPUT) diff --git a/zspotify/const.py b/zspotify/const.py index 057921c1..3e76f8a0 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -20,6 +20,10 @@ ARTISTS = 'artists' ALBUMARTIST = 'albumartist' +GENRES = 'genres' + +GENRE = 'genre' + ARTWORK = 'artwork' TRACKS = 'tracks' diff --git a/zspotify/track.py b/zspotify/track.py index 1278412f..faa630b7 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -8,7 +8,7 @@ from librespot.audio.decoders import AudioQuality from librespot.metadata import TrackId from ffmpy import FFmpeg -from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ +from const import TRACKS, ALBUM, GENRES, GENRE, 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, \ @@ -33,7 +33,7 @@ def get_saved_tracks() -> list: return songs -def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any, Any, int]: +def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, Any, Any, Any, Any, int]: """ Retrieves metadata for downloaded songs """ (raw, info) = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token') @@ -42,8 +42,18 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any try: artists = [] + genres = [] for data in info[TRACKS][0][ARTISTS]: artists.append(data[NAME]) + # query artist genres via href, which will be the api url + (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') + if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: + for genre in artistInfo[GENRES]: + genres.append(genre) + elif len(artistInfo[GENRES]) > 0: + genres.append(artistInfo[GENRES][0]) + else: + genres.append('') album_name = info[TRACKS][0][ALBUM][NAME] name = info[TRACKS][0][NAME] image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL] @@ -54,7 +64,7 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any is_playable = info[TRACKS][0][IS_PLAYABLE] duration_ms = info[TRACKS][0][DURATION_MS] - return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms + return artists, genres, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms except Exception as e: raise ValueError(f'Failed to parse TRACKS_URL response: {str(e)}\n{raw}') @@ -82,7 +92,7 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= try: output_template = ZSpotify.CONFIG.get_output(mode) - (artists, album_name, name, image_url, release_year, disc_number, + (artists, genres, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id) song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name) @@ -170,7 +180,7 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= time_downloaded = time.time() convert_audio_format(filename_temp) - set_audio_tags(filename_temp, artists, name, album_name, release_year, disc_number, track_number) + set_audio_tags(filename_temp, artists, genres, name, album_name, release_year, disc_number, track_number) set_music_thumbnail(filename_temp, image_url) if filename_temp != filename: diff --git a/zspotify/utils.py b/zspotify/utils.py index 36561458..f56ce5d0 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -10,7 +10,7 @@ from typing import List, Tuple import music_tag import requests -from const import ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ +from const import ARTIST, GENRE, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ WINDOWS_SYSTEM, ALBUMARTIST from zspotify import ZSpotify @@ -124,11 +124,12 @@ def clear() -> None: os.system('clear') -def set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number) -> None: +def set_audio_tags(filename, artists, genres, name, album_name, release_year, disc_number, track_number) -> None: """ sets music_tag metadata """ tags = music_tag.load_file(filename) tags[ALBUMARTIST] = artists[0] tags[ARTIST] = conv_artist_format(artists) + tags[GENRE] = genres[0] if not ZSpotify.CONFIG.get_allGenres() else ZSpotify.CONFIG.get_allGenresDelimiter().join(genres) tags[TRACKTITLE] = name tags[ALBUM] = album_name tags[YEAR] = release_year From 0d553243d2b4ec5a8c52f9ae591d9ca0d834ebed Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 17:01:34 +0100 Subject: [PATCH 02/13] added switch to not send too much requests --- zspotify/track.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index faa630b7..b58abbe9 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -43,17 +43,23 @@ def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, An try: artists = [] genres = [] + genreRetrieved = False for data in info[TRACKS][0][ARTISTS]: artists.append(data[NAME]) - # query artist genres via href, which will be the api url - (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') - if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: - for genre in artistInfo[GENRES]: - genres.append(genre) - elif len(artistInfo[GENRES]) > 0: - genres.append(artistInfo[GENRES][0]) - else: - genres.append('') + + if not genreRetrieved: + # query artist genres via href, which will be the api url + (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') + if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: + genreRetrieved = False + for genre in artistInfo[GENRES]: + genres.append(genre) + elif len(artistInfo[GENRES]) > 0: + genres.append(artistInfo[GENRES][0]) + genreRetrieved = True + else: + genres.append('') + genreRetrieved = True album_name = info[TRACKS][0][ALBUM][NAME] name = info[TRACKS][0][NAME] image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL] From 702ec22094e6707c508073e6788491f5e7fb020c Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 20:20:06 +0100 Subject: [PATCH 03/13] updated exception handling --- zspotify/track.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index b58abbe9..1deed403 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -1,5 +1,6 @@ import os import re +from threading import Thread import time import uuid from typing import Any, Tuple, List @@ -46,20 +47,24 @@ def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, An genreRetrieved = False for data in info[TRACKS][0][ARTISTS]: artists.append(data[NAME]) - - if not genreRetrieved: - # query artist genres via href, which will be the api url - (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') - if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: - genreRetrieved = False - for genre in artistInfo[GENRES]: - genres.append(genre) - elif len(artistInfo[GENRES]) > 0: - genres.append(artistInfo[GENRES][0]) - genreRetrieved = True - else: + try: + if not genreRetrieved: + # query artist genres via href, which will be the api url + (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') + if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: + genreRetrieved = False + for genre in artistInfo[GENRES]: + genres.append(genre) + elif len(artistInfo[GENRES]) > 0: + genres.append(artistInfo[GENRES][0]) + genreRetrieved = True + else: + genres.append('') + genreRetrieved = True + except Exception as genreError: + if len(genres) == 0: genres.append('') - genreRetrieved = True + album_name = info[TRACKS][0][ALBUM][NAME] name = info[TRACKS][0][NAME] image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL] From 16c7ee5d51531b90612254a86dfd102dada1350b Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 21:41:33 +0100 Subject: [PATCH 04/13] removed exception handling from track --- zspotify/track.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index 1deed403..6ebbf289 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -44,27 +44,20 @@ def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, An try: artists = [] genres = [] - genreRetrieved = False for data in info[TRACKS][0][ARTISTS]: artists.append(data[NAME]) - try: - if not genreRetrieved: - # query artist genres via href, which will be the api url - (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') - if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: - genreRetrieved = False - for genre in artistInfo[GENRES]: - genres.append(genre) - elif len(artistInfo[GENRES]) > 0: - genres.append(artistInfo[GENRES][0]) - genreRetrieved = True - else: - genres.append('') - genreRetrieved = True - except Exception as genreError: - if len(genres) == 0: - genres.append('') - + # query artist genres via href, which will be the api url + (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') + if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: + for genre in artistInfo[GENRES]: + genres.append(genre) + elif len(artistInfo[GENRES]) > 0: + genres.append(artistInfo[GENRES][0]) + + if len(genres) == 0: + Printer.print(PrintChannel.SKIPS, "No Genre found.") + genres.append('') + album_name = info[TRACKS][0][ALBUM][NAME] name = info[TRACKS][0][NAME] image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL] From 9a48746b790ea122a383cf7c47a64d2292741caa Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 21:41:48 +0100 Subject: [PATCH 05/13] added exception handling and retry-logic to zspotify --- zspotify/zspotify.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index f398d5d2..868e9e25 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -9,7 +9,7 @@ It's like youtube-dl, but for Spotify. import os import os.path from getpass import getpass - +import time import requests from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.core import Session @@ -19,7 +19,6 @@ from const import TYPE, \ PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ from config import Config - class ZSpotify: SESSION: Session = None DOWNLOAD_QUALITY = None @@ -82,10 +81,19 @@ class ZSpotify: return requests.get(url, headers=headers, params=params).json() @classmethod - def invoke_url(cls, url): + def invoke_url(cls, url, tryCount = 0): headers = cls.get_auth_header() response = requests.get(url, headers=headers) - return response.text, response.json() + responseText = response.text + responseJson = response.json() + + if 'error' in responseJson and tryCount < 20: + + print(f"Spotify API Error ({responseJson['error']['status']}): {responseJson['error']['message']}") + time.sleep(5) + return cls.invoke_url(url, tryCount + 1) + + return responseText, responseJson @classmethod def check_premium(cls) -> bool: From 401688a4581c995848f42589877abb6bae6ce7bd Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 22:11:59 +0100 Subject: [PATCH 06/13] added loader class for long running tasks --- zspotify/utils.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/zspotify/utils.py b/zspotify/utils.py index f56ce5d0..20d6d453 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -280,3 +280,84 @@ def fmt_seconds(secs: float) -> str: return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2) else: return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2) + + +# load symbol from: +# https://stackoverflow.com/questions/22029562/python-how-to-make-simple-animated-loading-while-process-is-running + +# imports +from itertools import cycle +from shutil import get_terminal_size +from threading import Thread +from time import sleep + +class Loader: + """Busy symbol. + + Can be called inside a context: + + with Loader("This take some Time..."): + # do something + pass + """ + def __init__(self, desc="Loading...", end='', timeout=0.3, mode='std2'): + """ + A loader-like context manager + + Args: + desc (str, optional): The loader's description. Defaults to "Loading...". + end (str, optional): Final print. Defaults to "". + timeout (float, optional): Sleep time between prints. Defaults to 0.1. + """ + self.desc = desc + self.end = end + self.timeout = timeout + + self._thread = Thread(target=self._animate, daemon=True) + if mode == 'std1': + self.steps = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"] + elif mode == 'std2': + self.steps = ["◜","◝","◞","◟"] + elif mode == 'std3': + self.steps = ["😐 ","😐 ","😮 ","😮 ","😦 ","😦 ","😧 ","😧 ","🤯 ","💥 ","✨ ","\u3000 ","\u3000 ","\u3000 "] + elif mode == 'prog': + self.steps = ["[∙∙∙]","[●∙∙]","[∙●∙]","[∙∙●]","[∙∙∙]"] + + self.done = False + + def start(self): + self._thread.start() + return self + + def _animate(self): + for c in cycle(self.steps): + if self.done: + break + print(f"\r\t☐ {c} {self.desc} ", flush=True, end="") + sleep(self.timeout) + + def __enter__(self): + self.start() + + def stop(self): + self.done = True + cols = get_terminal_size((80, 20)).columns + print("\r" + " " * cols, end="", flush=True) + + if self.end != "": + print(f"\r{self.end}", flush=True) + + def __exit__(self, exc_type, exc_value, tb): + # handle exceptions with those variables ^ + self.stop() + + +if __name__ == "__main__": + with Loader("Loading with context manager..."): + for i in range(10): + sleep(0.25) + + loader = Loader("Loading with object...", "That was fast!", 0.05).start() + for i in range(10): + sleep(0.25) + loader.stop() From a93fc0ee36234f9745303fd8ab2b71d86122f180 Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 22:12:47 +0100 Subject: [PATCH 07/13] implemented loaders in task - long running tasks like zspotify.invoke_url or convert_audio_format now use a Loader to show that they are busy --- zspotify/track.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index 6ebbf289..47c0e6a1 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -17,6 +17,8 @@ from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_down from zspotify import ZSpotify import traceback +from utils import Loader + def get_saved_tracks() -> list: """ Returns user's saved tracks """ songs = [] @@ -36,7 +38,8 @@ def get_saved_tracks() -> list: def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, Any, Any, Any, Any, int]: """ Retrieves metadata for downloaded songs """ - (raw, info) = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token') + with Loader("Fetching track information..."): + (raw, info) = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token') if not TRACKS in info: raise ValueError(f'Invalid response from TRACKS_URL:\n{raw}') @@ -47,7 +50,8 @@ def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, An for data in info[TRACKS][0][ARTISTS]: artists.append(data[NAME]) # query artist genres via href, which will be the api url - (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') + with Loader("Fetching artist information..."): + (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}') if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0: for genre in artistInfo[GENRES]: genres.append(genre) @@ -238,6 +242,9 @@ def convert_audio_format(filename) -> None: inputs={temp_filename: None}, outputs={filename: output_params} ) - ff_m.run() + + with Loader("Converting file..."): + ff_m.run() + if os.path.exists(temp_filename): os.remove(temp_filename) From 6877cca21b4983142c04564888e759b096e0bded Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 22:15:55 +0100 Subject: [PATCH 08/13] small design update to loader --- zspotify/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zspotify/utils.py b/zspotify/utils.py index 20d6d453..bc1f2795 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -333,7 +333,7 @@ class Loader: for c in cycle(self.steps): if self.done: break - print(f"\r\t☐ {c} {self.desc} ", flush=True, end="") + print(f"\r\t{c} {self.desc} ", flush=True, end="") sleep(self.timeout) def __enter__(self): From 055f7ad97d5bce294638e2a753f4623e407c892f Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Tue, 30 Nov 2021 22:33:50 +0100 Subject: [PATCH 09/13] added another loader for download preparation --- zspotify/track.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index 47c0e6a1..3fec49a8 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -102,7 +102,10 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= (artists, genres, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id) - + + prepareDownloadLoader = Loader("Preparing download..."); + prepareDownloadLoader.start() + song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name) for k in extra_keys: @@ -149,12 +152,15 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= else: try: if not is_playable: + prepareDownloadLoader.stop(); Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG IS UNAVAILABLE) ###' + "\n") else: if check_id and check_name and ZSpotify.CONFIG.get_skip_existing_files(): + prepareDownloadLoader.stop(); Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG ALREADY EXISTS) ###' + "\n") elif check_all_time and ZSpotify.CONFIG.get_skip_previously_downloaded(): + prepareDownloadLoader.stop(); Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG ALREADY DOWNLOADED ONCE) ###' + "\n") else: @@ -165,6 +171,8 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= create_download_directory(filedir) total_size = stream.input_stream.size + prepareDownloadLoader.stop(); + time_start = time.time() downloaded = 0 with open(filename_temp, 'wb') as file, Printer.progress( @@ -214,8 +222,8 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n") if os.path.exists(filename_temp): os.remove(filename_temp) - - + + prepareDownloadLoader.stop() def convert_audio_format(filename) -> None: """ Converts raw audio into playable file """ temp_filename = f'{os.path.splitext(filename)[0]}.tmp' From c9e8c0c45debae2084a27fb377c8539ec0c50698 Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Wed, 1 Dec 2021 13:48:53 +0100 Subject: [PATCH 10/13] use Printer class for output - added print-options for API_ERRORS - modified printing inside of invoke_url - "No Genre found." is printed using SKIPS --- zspotify/config.py | 2 ++ zspotify/termoutput.py | 3 ++- zspotify/track.py | 2 +- zspotify/zspotify.py | 6 ++++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/zspotify/config.py b/zspotify/config.py index 0c4e9cf0..be8a7349 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -25,6 +25,7 @@ PRINT_SKIPS = 'PRINT_SKIPS' PRINT_DOWNLOAD_PROGRESS = 'PRINT_DOWNLOAD_PROGRESS' PRINT_ERRORS = 'PRINT_ERRORS' PRINT_DOWNLOADS = 'PRINT_DOWNLOADS' +PRINT_API_ERRORS = 'PRINT_API_ERRORS' TEMP_DOWNLOAD_DIR = 'TEMP_DOWNLOAD_DIR' MD_ALLGENRES = 'MD_ALLGENRES' MD_GENREDELIMITER = 'MD_GENREDELIMITER' @@ -51,6 +52,7 @@ CONFIG_VALUES = { PRINT_DOWNLOAD_PROGRESS: { 'default': 'True', 'type': bool, 'arg': '--print-download-progress' }, PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' }, PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' }, + PRINT_API_ERRORS: { 'default': 'False', 'type': bool, 'arg': '--print-api-errors' }, MD_ALLGENRES: { 'default': 'False', 'type': bool, 'arg': '--md-allgenres' }, MD_GENREDELIMITER: { 'default': ';', 'type': str, 'arg': '--md-genredelimiter' }, TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' } diff --git a/zspotify/termoutput.py b/zspotify/termoutput.py index bf57e356..06eec980 100644 --- a/zspotify/termoutput.py +++ b/zspotify/termoutput.py @@ -1,7 +1,7 @@ from enum import Enum from tqdm import tqdm -from config import PRINT_SPLASH, PRINT_SKIPS, PRINT_DOWNLOAD_PROGRESS, PRINT_ERRORS, PRINT_DOWNLOADS +from config import PRINT_SPLASH, PRINT_SKIPS, PRINT_DOWNLOAD_PROGRESS, PRINT_ERRORS, PRINT_DOWNLOADS, PRINT_API_ERRORS from zspotify import ZSpotify @@ -11,6 +11,7 @@ class PrintChannel(Enum): DOWNLOAD_PROGRESS = PRINT_DOWNLOAD_PROGRESS ERRORS = PRINT_ERRORS DOWNLOADS = PRINT_DOWNLOADS + API_ERRORS = PRINT_API_ERRORS class Printer: diff --git a/zspotify/track.py b/zspotify/track.py index 3fec49a8..74704eaf 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -59,7 +59,7 @@ def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, An genres.append(artistInfo[GENRES][0]) if len(genres) == 0: - Printer.print(PrintChannel.SKIPS, "No Genre found.") + Printer.print(PrintChannel.SKIPS, '### No Genre found.') genres.append('') album_name = info[TRACKS][0][ALBUM][NAME] diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index 868e9e25..17385073 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -19,7 +19,7 @@ from const import TYPE, \ PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ from config import Config -class ZSpotify: +class ZSpotify: SESSION: Session = None DOWNLOAD_QUALITY = None CONFIG: Config = Config() @@ -82,6 +82,8 @@ class ZSpotify: @classmethod def invoke_url(cls, url, tryCount = 0): + # we need to import that here, otherwise we will get circular imports! + from termoutput import Printer, PrintChannel headers = cls.get_auth_header() response = requests.get(url, headers=headers) responseText = response.text @@ -89,7 +91,7 @@ class ZSpotify: if 'error' in responseJson and tryCount < 20: - print(f"Spotify API Error ({responseJson['error']['status']}): {responseJson['error']['message']}") + Printer.Print(PrintChannel.API_ERROR, f"Spotify API Error ({responseJson['error']['status']}): {responseJson['error']['message']}") time.sleep(5) return cls.invoke_url(url, tryCount + 1) From e00ad81c5ef999eb2b40ea6a6f7ed4369b92a7fc Mon Sep 17 00:00:00 2001 From: Leon Bohmann Date: Wed, 1 Dec 2021 13:49:24 +0100 Subject: [PATCH 11/13] use more fancy busySymbols --- zspotify/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zspotify/utils.py b/zspotify/utils.py index bc1f2795..ee20cec2 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -300,7 +300,7 @@ class Loader: # do something pass """ - def __init__(self, desc="Loading...", end='', timeout=0.3, mode='std2'): + def __init__(self, desc="Loading...", end='', timeout=0.1, mode='std1'): """ A loader-like context manager From 6f75353423b6e31387ed01c20931390739108bd1 Mon Sep 17 00:00:00 2001 From: Logykk <35679186+logykk@users.noreply.github.com> Date: Thu, 2 Dec 2021 15:49:08 +1300 Subject: [PATCH 12/13] Updated changelog to 0.5.2 --- CHANGELOG.md | 60 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7001d48..0cef13c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,37 @@ -## **Changelog:** -**v2.4 (27 Oct 20212):** +# Changelog: +### v0.5.2 - We're bad at counting (27 Nov 2021): +**General changes:** +- Fixed filenaming on Windows +- Fixed removal of special characters metadata +- Can now download different songs with the same name +- Real-time downloads now work correctly +- Removed some debug messages +- Added album_artist metadata +- Added global song archive +- Added SONG_ARCHIVE config value +- Added CREDENTIALS_LOCATION config value +- Added `--download` argument +- Added `--config-location` argument +- Added `--output` for output templating +- Save extra data in .song_ids +- Added options to regulate terminal output +- Direct download support for certain podcasts + +**Docker images:** +- Remember credentials between container starts +- Use same uid/gid in container as on host + +**Windows installer:** +- Now comes with full installer +- Dependencies are installed if not found + +### v0.2.4 (27 Oct 2021): - 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):** +### v0.2.3 (25 Oct 2021): - Moved changelog to seperate file. - Added argument parsing in search function (query results limit and query result types). - Fixed spelling errors. @@ -17,7 +43,7 @@ - Fixed issue where if you enabled splitting discs into seperate folders downloading would fail. - Added playlist file(m3u) creation for playlist download. -**v2.2 (24 Oct 2021):** +### v0.2.2 (24 Oct 2021): - Added basic support for downloading an entire podcast series. - Split code into multiple files for easier maintenance. - Changed initial launch script to app.py @@ -28,39 +54,39 @@ - Fixed artist names getting cutoff in metadata. - Removed data sanitization of metadata tags. -**v2.1 (23 Oct 2021):** +### v0.2.1 (23 Oct 2021): - Moved configuration from hard-coded values to separate zs_config.json file. - Add subfolders for each disc. - Can now search and download all songs by artist. - Show single progress bar for entire album. - Added song number at start of track name in albums. -**v2.0 (22 Oct 2021):** +### v0.2.0 (22 Oct 2021): - Added progress bar for downloads. - Added multi-select support for all results when searching. - Added GPLv3 Licence. - Changed welcome banner and removed unnecessary debug print statements. -**v1.9 (22 Oct 2021):** +### v0.1.9 (22 Oct 2021): - Added Gitea mirror for when the Spotify Glowies come to DMCA the shit out of this. - Changed the discord server invite to a matrix server so that won't get swatted either. - Added option to select multiple of our saved playlists to download at once. - Added support for downloading an entire show at once. -**v1.8 (21 Oct 2021):** +### v0.1.8 (21 Oct 2021): - Improved podcast downloading a bit. - Simplified the code that catches crashes while downloading. - Cleaned up code using linter again. - Added option to just paste a url in the search bar to download it. - Added a small delay between downloading each track when downloading in bulk to help with downloading issues and potential bans. -**v1.7 (21 Oct 2021):** +### v0.1.7 (21 Oct 2021): - Rewrote README.md to look a lot more professional. - Added patch to fix edge case crash when downloading liked songs. - Made premium account check a lot more reliable. - Added experimental podcast support for specific episodes! -**v1.6 (20 Oct 2021):** +### v0.1.6 (20 Oct 2021): - Added Pillow to requirements.txt. - Removed websocket-client from requirements.txt because librespot-python added it to their dependency list. - Made it hide your password when you type it in. @@ -69,34 +95,34 @@ - Added Shebang line so it runs smoother on Linux. - Made it download the entire track at once now so it is more efficient and fixed a bug users encountered. -**v1.5 (19 Oct 2021):** +### v0.1.5 (19 Oct 2021): - Made downloading a lot more efficient and probably faster. - Made the sanitizer more efficient. - Formatted and linted all the code. -**v1.4 (19 Oct 2021):** +### v0.1.4 (19 Oct 2021): - Added option to encode the downloaded tracks in the "ogg" format rather than "mp3". - Added small improvement to sanitation function so it catches another edge case. -**v1.3 (19 Oct 2021):** +### v0.1.3 (19 Oct 2021): - Added auto detection about if the current account is premium or not. If it is a premium account it automatically sets the quality to VERY_HIGH and otherwise HIGH if we are using a free account. - Fixed conversion function so it now exports to the correct bitrate. - Added sanitation to playlist names to help catch an edge case crash. - Added option to download all your liked songs into a sub-folder. -**v1.2 (18 Oct 2021):** +### v0.1.2 (18 Oct 2021): - Added .gitignore. - Replaced dependency list in README.md with a proper requirements.txt file. - Improved the readability of README.md. -**v1.1 (16 Oct 2021):** +### v0.1.1 (16 Oct 2021): - Added try/except to help catch crashes where a very few specific tracks would crash either the downloading or conversion part. -**v1.0 (14 Oct 2021):** +### v0.1.0 (14 Oct 2021): - Adjusted some functions so it runs again with the newer version of librespot-python. - Improved my sanitization function so it catches more edge cases. - Fixed an issue where sometimes spotify wouldn't provide a song id for a track we are trying to download. It will now detect and skip these invalid tracks. - Added additional check for tracks that cannot be "played" due to licence(and similar) issues. These tracks will be skipped. -**v0.9 (13 Oct 2021):** +### v0.0.9 (13 Oct 2021): - Initial upload, needs adjustments to get working again after backend rewrite. From 58fe9ae9e1962cf76335882cccdb7de273249ec9 Mon Sep 17 00:00:00 2001 From: Logykk <35679186+logykk@users.noreply.github.com> Date: Thu, 2 Dec 2021 17:20:32 +1300 Subject: [PATCH 13/13] Limit invoke_url retry's to 5 --- zspotify/zspotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index 17385073..e535a9f3 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -89,7 +89,7 @@ class ZSpotify: responseText = response.text responseJson = response.json() - if 'error' in responseJson and tryCount < 20: + if 'error' in responseJson and tryCount < 5: Printer.Print(PrintChannel.API_ERROR, f"Spotify API Error ({responseJson['error']['status']}): {responseJson['error']['message']}") time.sleep(5)