From 8cd868f31778104727e7f417d05f39d2d1535f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Thu, 18 Nov 2021 22:16:07 +0100 Subject: [PATCH 01/11] Move config to config.py --- zspotify/__main__.py | 3 ++ zspotify/app.py | 2 +- zspotify/config.py | 110 +++++++++++++++++++++++++++++++++++++++++++ zspotify/const.py | 41 ---------------- zspotify/podcast.py | 11 ++--- zspotify/track.py | 38 +++++++-------- zspotify/zspotify.py | 33 ++++--------- 7 files changed, 146 insertions(+), 92 deletions(-) create mode 100644 zspotify/config.py diff --git a/zspotify/__main__.py b/zspotify/__main__.py index 0311fc2f..9d7cef98 100644 --- a/zspotify/__main__.py +++ b/zspotify/__main__.py @@ -10,6 +10,9 @@ if __name__ == '__main__': parser.add_argument('-ns', '--no-splash', action='store_true', help='Suppress the splash screen when loading.') + parser.add_argument('--config-location', + type=str, + help='Specify the zs_config.json location') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('urls', type=str, diff --git a/zspotify/app.py b/zspotify/app.py index 2d315094..7b6bbffb 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -16,7 +16,7 @@ SEARCH_URL = 'https://api.spotify.com/v1/search' def client(args) -> None: """ Connects to spotify to perform query's and get songs to download """ - ZSpotify() + ZSpotify(args) if not args.no_splash: splash() diff --git a/zspotify/config.py b/zspotify/config.py new file mode 100644 index 00000000..f6e5ae97 --- /dev/null +++ b/zspotify/config.py @@ -0,0 +1,110 @@ +import json +import os +import sys +from typing import Any + +CONFIG_FILE_PATH = '../zs_config.json' + +ROOT_PATH = 'ROOT_PATH' +ROOT_PODCAST_PATH = 'ROOT_PODCAST_PATH' +SKIP_EXISTING_FILES = 'SKIP_EXISTING_FILES' +SKIP_PREVIOUSLY_DOWNLOADED = 'SKIP_PREVIOUSLY_DOWNLOADED' +DOWNLOAD_FORMAT = 'DOWNLOAD_FORMAT' +FORCE_PREMIUM = 'FORCE_PREMIUM' +ANTI_BAN_WAIT_TIME = 'ANTI_BAN_WAIT_TIME' +OVERRIDE_AUTO_WAIT = 'OVERRIDE_AUTO_WAIT' +CHUNK_SIZE = 'CHUNK_SIZE' +SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' +DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' +LANGUAGE = 'LANGUAGE' +BITRATE = 'BITRATE' + +CONFIG_DEFAULT_SETTINGS = { + 'ROOT_PATH': '../ZSpotify Music/', + 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', + 'SKIP_EXISTING_FILES': True, + 'SKIP_PREVIOUSLY_DOWNLOADED': False, + 'DOWNLOAD_FORMAT': 'ogg', + 'FORCE_PREMIUM': False, + 'ANTI_BAN_WAIT_TIME': 1, + 'OVERRIDE_AUTO_WAIT': False, + 'CHUNK_SIZE': 50000, + 'SPLIT_ALBUM_DISCS': False, + 'DOWNLOAD_REAL_TIME': False, + 'LANGUAGE': 'en', + 'BITRATE': '', +} + +class Config: + + Values = {} + + @classmethod + def load(cls, args) -> None: + app_dir = os.path.dirname(__file__) + true_config_file_path = os.path.join(app_dir, CONFIG_FILE_PATH) + + if not os.path.exists(true_config_file_path): + with open(true_config_file_path, 'w', encoding='utf-8') as config_file: + json.dump(CONFIG_DEFAULT_SETTINGS, config_file, indent=4) + cls.Values = CONFIG_DEFAULT_SETTINGS + else: + with open(true_config_file_path, encoding='utf-8') as config_file: + cls.Values = json.load(config_file) + + @classmethod + def get(cls, key) -> Any: + return cls.Values.get(key) + + @classmethod + def getRootPath(cls) -> str: + return cls.get(ROOT_PATH) + + @classmethod + def getRootPodcastPath(cls) -> str: + return cls.get(ROOT_PODCAST_PATH) + + @classmethod + def getSkipExistingFiles(cls) -> bool: + return cls.get(SKIP_EXISTING_FILES) + + @classmethod + def getSkipPreviouslyDownloaded(cls) -> bool: + return cls.get(SKIP_PREVIOUSLY_DOWNLOADED) + + @classmethod + def getSplitAlbumDiscs(cls) -> bool: + return cls.get(SPLIT_ALBUM_DISCS) + + @classmethod + def getChunkSize(cls) -> int(): + return cls.get(CHUNK_SIZE) + + @classmethod + def getOverrideAutoWait(cls) -> bool: + return cls.get(OVERRIDE_AUTO_WAIT) + + @classmethod + def getForcePremium(cls) -> bool: + return cls.get(FORCE_PREMIUM) + + @classmethod + def getDownloadFormat(cls) -> str: + return cls.get(DOWNLOAD_FORMAT) + + @classmethod + def getAntiBanWaitTime(cls) -> int: + return cls.get(ANTI_BAN_WAIT_TIME) + + @classmethod + def getLanguage(cls) -> str: + return cls.get(LANGUAGE) + + @classmethod + def getDownloadRealTime(cls) -> bool: + return cls.get(DOWNLOAD_REAL_TIME) + + @classmethod + def getBitrate(cls) -> str: + return cls.get(BITRATE) + \ No newline at end of file diff --git a/zspotify/const.py b/zspotify/const.py index 06e800f6..828bdd3d 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -82,32 +82,6 @@ WINDOWS_SYSTEM = 'Windows' CREDENTIALS_JSON = 'credentials.json' -CONFIG_FILE_PATH = '../zs_config.json' - -ROOT_PATH = 'ROOT_PATH' - -ROOT_PODCAST_PATH = 'ROOT_PODCAST_PATH' - -SKIP_EXISTING_FILES = 'SKIP_EXISTING_FILES' - -SKIP_PREVIOUSLY_DOWNLOADED = 'SKIP_PREVIOUSLY_DOWNLOADED' - -DOWNLOAD_FORMAT = 'DOWNLOAD_FORMAT' - -FORCE_PREMIUM = 'FORCE_PREMIUM' - -ANTI_BAN_WAIT_TIME = 'ANTI_BAN_WAIT_TIME' - -OVERRIDE_AUTO_WAIT = 'OVERRIDE_AUTO_WAIT' - -CHUNK_SIZE = 'CHUNK_SIZE' - -SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' - -DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' - -BITRATE = 'BITRATE' - CODEC_MAP = { 'aac': 'aac', 'fdk_aac': 'libfdk_aac', @@ -127,18 +101,3 @@ EXT_MAP = { 'opus': 'ogg', 'vorbis': 'ogg', } - -CONFIG_DEFAULT_SETTINGS = { - 'ROOT_PATH': '../ZSpotify Music/', - 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', - 'SKIP_EXISTING_FILES': True, - 'SKIP_PREVIOUSLY_DOWNLOADED': False, - 'DOWNLOAD_FORMAT': 'ogg', - 'FORCE_PREMIUM': False, - 'ANTI_BAN_WAIT_TIME': 1, - 'OVERRIDE_AUTO_WAIT': False, - 'CHUNK_SIZE': 50000, - 'SPLIT_ALBUM_DISCS': False, - 'DOWNLOAD_REAL_TIME': False, - 'LANGUAGE': 'en' -} diff --git a/zspotify/podcast.py b/zspotify/podcast.py index 265bff7c..c99797f7 100644 --- a/zspotify/podcast.py +++ b/zspotify/podcast.py @@ -5,8 +5,7 @@ from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.metadata import EpisodeId from tqdm import tqdm -from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW, - SKIP_EXISTING_FILES) +from const import (ERROR, ID, ITEMS, NAME, SHOW) from utils import create_download_directory, fix_filename from zspotify import ZSpotify @@ -80,7 +79,7 @@ def download_episode(episode_id) -> None: download_directory = os.path.join( os.path.dirname(__file__), - ZSpotify.get_config(ROOT_PODCAST_PATH), + ZSpotify.CONFIG.getRootPodcastPath(), extra_paths, ) download_directory = os.path.realpath(download_directory) @@ -97,7 +96,7 @@ def download_episode(episode_id) -> None: if ( os.path.isfile(filepath) and os.path.getsize(filepath) == total_size - and ZSpotify.get_config(SKIP_EXISTING_FILES) + and ZSpotify.CONFIG.getSkipExistingFiles() ): print( "\n### SKIPPING:", @@ -115,9 +114,9 @@ def download_episode(episode_id) -> None: unit_scale=True, unit_divisor=1024 ) as bar: - for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): + for _ in range(int(total_size / ZSpotify.CONFIG.getChunkSize()) + 1): bar.update(file.write( - stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) + stream.input_stream.stream().read(ZSpotify.CONFIG.getChunkSize()))) else: filepath = os.path.join(download_directory, f"{filename}.mp3") download_podcast_directly(direct_download_url, filepath) diff --git a/zspotify/track.py b/zspotify/track.py index a80ad92e..f8b440c7 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -10,9 +10,7 @@ from pydub import AudioSegment from tqdm import tqdm 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, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, \ - CHUNK_SIZE, SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT, BITRATE, CODEC_MAP, EXT_MAP, DOWNLOAD_REAL_TIME, \ - SKIP_PREVIOUSLY_DOWNLOADED, DURATION_MS + RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, TRACK_STATS_URL, CODEC_MAP, EXT_MAP, DURATION_MS 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 @@ -78,12 +76,12 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', (artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id) - if ZSpotify.get_config(SPLIT_ALBUM_DISCS): + if ZSpotify.CONFIG.getSplitAlbumDiscs(): download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.get_config(ROOT_PATH), extra_paths, f'Disc {disc_number}') + __file__), ZSpotify.CONFIG.getRootPath(), extra_paths, f'Disc {disc_number}') else: download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.get_config(ROOT_PATH), extra_paths) + __file__), ZSpotify.CONFIG.getRootPath(), extra_paths) song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name) if prefix: @@ -91,9 +89,9 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', ) else f'{prefix_value} - {song_name}' filename = os.path.join( - download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.get_config(DOWNLOAD_FORMAT).lower())}') + download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.CONFIG.getDownloadFormat().lower())}') - archive_directory = os.path.join(os.path.dirname(__file__), ZSpotify.get_config(ROOT_PATH)) + archive_directory = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.getRootPath()) check_name = os.path.isfile(filename) and os.path.getsize(filename) check_id = scraped_song_id in get_directory_song_ids(download_directory) check_all_time = scraped_song_id in get_previously_downloaded(scraped_song_id, archive_directory) @@ -104,7 +102,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', if re.search(f'^{song_name}_', file)]) + 1 filename = os.path.join( - download_directory, f'{song_name}_{c}.{EXT_MAP.get(ZSpotify.get_config(DOWNLOAD_FORMAT))}') + download_directory, f'{song_name}_{c}.{EXT_MAP.get(ZSpotify.CONFIG.getDownloadFormat())}') except Exception as e: @@ -116,11 +114,11 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', print('\n### SKIPPING:', song_name, '(SONG IS UNAVAILABLE) ###') else: - if check_id and check_name and ZSpotify.get_config(SKIP_EXISTING_FILES): + if check_id and check_name and ZSpotify.CONFIG.getSkipExistingFiles(): print('\n### SKIPPING:', song_name, '(SONG ALREADY EXISTS) ###') - elif check_all_time and ZSpotify.get_config(SKIP_PREVIOUSLY_DOWNLOADED): + elif check_all_time and ZSpotify.CONFIG.getSkipPreviouslyDownloaded(): print('\n### SKIPPING:', song_name, '(SONG ALREADY DOWNLOADED ONCE) ###') @@ -141,11 +139,11 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', unit_divisor=1024, disable=disable_progressbar ) as p_bar: - pause = duration_ms / ZSpotify.get_config(CHUNK_SIZE) - for chunk in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): - data = stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)) + pause = duration_ms / ZSpotify.CONFIG.getChunkSize() + for chunk in range(int(total_size / ZSpotify.CONFIG.getChunkSize()) + 1): + data = stream.input_stream.stream().read(ZSpotify.CONFIG.getChunkSize()) p_bar.update(file.write(data)) - if ZSpotify.get_config(DOWNLOAD_REAL_TIME): + if ZSpotify.CONFIG.getDownloadRealTime(): time.sleep(pause) convert_audio_format(filename) @@ -154,14 +152,14 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', set_music_thumbnail(filename, image_url) # add song id to archive file - if ZSpotify.get_config(SKIP_PREVIOUSLY_DOWNLOADED): + if ZSpotify.CONFIG.getSkipPreviouslyDownloaded(): add_to_archive(scraped_song_id, archive_directory) # add song id to download directory's .song_ids file if not check_id: add_to_directory_song_ids(download_directory, scraped_song_id) - if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT): - time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME)) + if not ZSpotify.CONFIG.getAntiBanWaitTime(): + time.sleep(ZSpotify.CONFIG.getAntiBanWaitTime()) except Exception as e: print('### SKIPPING:', song_name, '(GENERAL DOWNLOAD ERROR) ###') @@ -175,10 +173,10 @@ def convert_audio_format(filename) -> None: temp_filename = f'{os.path.splitext(filename)[0]}.tmp' os.replace(filename, temp_filename) - download_format = ZSpotify.get_config(DOWNLOAD_FORMAT).lower() + download_format = ZSpotify.CONFIG.getDownloadFormat().lower() file_codec = CODEC_MAP.get(download_format, 'copy') if file_codec != 'copy': - bitrate = ZSpotify.get_config(BITRATE) + bitrate = ZSpotify.CONFIG.getBitrate() if not bitrate: if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH: bitrate = '320k' diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index a3f5e600..f40ac122 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -17,18 +17,19 @@ from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.core import Session from const import CREDENTIALS_JSON, TYPE, \ - PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, CONFIG_FILE_PATH, FORCE_PREMIUM, \ - PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ, CONFIG_DEFAULT_SETTINGS + PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, \ + PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ from utils import MusicFormat +from config import Config class ZSpotify: SESSION: Session = None DOWNLOAD_QUALITY = None - CONFIG = {} + CONFIG = Config() - def __init__(self): - ZSpotify.load_config() + def __init__(self, args): + ZSpotify.CONFIG.load(args) ZSpotify.login() @classmethod @@ -52,22 +53,6 @@ class ZSpotify: except RuntimeError: pass - @classmethod - def load_config(cls) -> None: - app_dir = os.path.dirname(__file__) - true_config_file_path = os.path.join(app_dir, CONFIG_FILE_PATH) - if not os.path.exists(true_config_file_path): - with open(true_config_file_path, 'w', encoding='utf-8') as config_file: - json.dump(CONFIG_DEFAULT_SETTINGS, config_file, indent=4) - cls.CONFIG = CONFIG_DEFAULT_SETTINGS - else: - with open(true_config_file_path, encoding='utf-8') as config_file: - cls.CONFIG = json.load(config_file) - - @classmethod - def get_config(cls, key) -> Any: - return cls.CONFIG.get(key) - @classmethod def get_content_stream(cls, content_id, quality): return cls.SESSION.content_feeder().load(content_id, VorbisOnlyAudioQuality(quality), False, None) @@ -80,14 +65,14 @@ class ZSpotify: def get_auth_header(cls): return { 'Authorization': f'Bearer {cls.__get_auth_token()}', - 'Accept-Language': f'{cls.CONFIG.get("LANGUAGE")}' + 'Accept-Language': f'{cls.CONFIG.getLanguage()}' } @classmethod def get_auth_header_and_params(cls, limit, offset): return { 'Authorization': f'Bearer {cls.__get_auth_token()}', - 'Accept-Language': f'{cls.CONFIG.get("LANGUAGE")}' + 'Accept-Language': f'{cls.CONFIG.getLanguage()}' }, {LIMIT: limit, OFFSET: offset} @classmethod @@ -104,4 +89,4 @@ class ZSpotify: @classmethod def check_premium(cls) -> bool: """ If user has spotify premium return true """ - return (cls.SESSION.get_user_attribute(TYPE) == PREMIUM) or cls.get_config(FORCE_PREMIUM) + return (cls.SESSION.get_user_attribute(TYPE) == PREMIUM) or cls.CONFIG.getForcePremium() From 50187c5aeba5246cd4ea552eb6111ec95f4db3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Thu, 18 Nov 2021 22:24:45 +0100 Subject: [PATCH 02/11] Added --config-location argument --- zspotify/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zspotify/config.py b/zspotify/config.py index f6e5ae97..75b437ed 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -42,7 +42,12 @@ class Config: @classmethod def load(cls, args) -> None: app_dir = os.path.dirname(__file__) - true_config_file_path = os.path.join(app_dir, CONFIG_FILE_PATH) + + config_fp = CONFIG_FILE_PATH + if args.config_location: + config_fp = args.config_location + + true_config_file_path = os.path.join(app_dir, config_fp) if not os.path.exists(true_config_file_path): with open(true_config_file_path, 'w', encoding='utf-8') as config_file: From 9082938dfdf1e8cb20cb5e177f486ad98a09e3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Thu, 18 Nov 2021 22:39:17 +0100 Subject: [PATCH 03/11] Use default values on missing configs --- zspotify/config.py | 81 ++++++++++++++++++++++++-------------------- zspotify/podcast.py | 8 ++--- zspotify/track.py | 34 +++++++++---------- zspotify/zspotify.py | 8 ++--- 4 files changed, 70 insertions(+), 61 deletions(-) diff --git a/zspotify/config.py b/zspotify/config.py index 75b437ed..463b482c 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -19,24 +19,24 @@ DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' LANGUAGE = 'LANGUAGE' BITRATE = 'BITRATE' -CONFIG_DEFAULT_SETTINGS = { - 'ROOT_PATH': '../ZSpotify Music/', - 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', - 'SKIP_EXISTING_FILES': True, - 'SKIP_PREVIOUSLY_DOWNLOADED': False, - 'DOWNLOAD_FORMAT': 'ogg', - 'FORCE_PREMIUM': False, - 'ANTI_BAN_WAIT_TIME': 1, - 'OVERRIDE_AUTO_WAIT': False, - 'CHUNK_SIZE': 50000, - 'SPLIT_ALBUM_DISCS': False, - 'DOWNLOAD_REAL_TIME': False, - 'LANGUAGE': 'en', - 'BITRATE': '', +CONFIG_VALUES = { + ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': 'str', 'arg': '--root-path' }, + ROOT_PODCAST_PATH: { 'default': '../ZSpotify Podcasts/', 'type': 'str', 'arg': '--root-podcast-path' }, + SKIP_EXISTING_FILES: { 'default': True, 'type': 'bool', 'arg': '--skip-existing-files' }, + SKIP_PREVIOUSLY_DOWNLOADED: { 'default': False, 'type': 'bool', 'arg': '--skip-previously-downloaded' }, + DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': 'str', 'arg': '--download-format' }, + FORCE_PREMIUM: { 'default': False, 'type': 'bool', 'arg': '--force-premium' }, + ANTI_BAN_WAIT_TIME: { 'default': 1, 'type': 'int', 'arg': '--anti-ban-wait-time' }, + OVERRIDE_AUTO_WAIT: { 'default': False, 'type': 'bool', 'arg': '--override-auto-wait' }, + CHUNK_SIZE: { 'default': 50000, 'type': 'int', 'arg': '--chunk-size' }, + SPLIT_ALBUM_DISCS: { 'default': False, 'type': 'bool', 'arg': '--split-album-discs' }, + DOWNLOAD_REAL_TIME: { 'default': False, 'type': 'bool', 'arg': '--download-real-time' }, + LANGUAGE: { 'default': 'en', 'type': 'str', 'arg': '--language' }, + BITRATE: { 'default': '', 'type': 'str', 'arg': '--bitrate' }, } -class Config: +class Config: Values = {} @classmethod @@ -51,65 +51,74 @@ class Config: if not os.path.exists(true_config_file_path): with open(true_config_file_path, 'w', encoding='utf-8') as config_file: - json.dump(CONFIG_DEFAULT_SETTINGS, config_file, indent=4) - cls.Values = CONFIG_DEFAULT_SETTINGS + json.dump(cls.get_default_json(), config_file, indent=4) + cls.Values = cls.get_default_json() else: with open(true_config_file_path, encoding='utf-8') as config_file: cls.Values = json.load(config_file) + for key in CONFIG_VALUES: + if key not in cls.Values: + cls.Values[key] = CONFIG_VALUES[key].default + + @classmethod + def get_default_json(cls) -> Any: + r = {} + for key in CONFIG_VALUES: + r[key] = CONFIG_VALUES[key].default + return r @classmethod def get(cls, key) -> Any: return cls.Values.get(key) - + @classmethod - def getRootPath(cls) -> str: + def get_root_path(cls) -> str: return cls.get(ROOT_PATH) @classmethod - def getRootPodcastPath(cls) -> str: + def get_root_podcast_path(cls) -> str: return cls.get(ROOT_PODCAST_PATH) @classmethod - def getSkipExistingFiles(cls) -> bool: + def get_skip_existing_files(cls) -> bool: return cls.get(SKIP_EXISTING_FILES) @classmethod - def getSkipPreviouslyDownloaded(cls) -> bool: + def get_skip_previously_downloaded(cls) -> bool: return cls.get(SKIP_PREVIOUSLY_DOWNLOADED) @classmethod - def getSplitAlbumDiscs(cls) -> bool: + def get_split_album_discs(cls) -> bool: return cls.get(SPLIT_ALBUM_DISCS) @classmethod - def getChunkSize(cls) -> int(): + def get_chunk_size(cls) -> int(): return cls.get(CHUNK_SIZE) @classmethod - def getOverrideAutoWait(cls) -> bool: + def get_override_auto_wait(cls) -> bool: return cls.get(OVERRIDE_AUTO_WAIT) @classmethod - def getForcePremium(cls) -> bool: + def get_force_premium(cls) -> bool: return cls.get(FORCE_PREMIUM) - + @classmethod - def getDownloadFormat(cls) -> str: + def get_download_format(cls) -> str: return cls.get(DOWNLOAD_FORMAT) - + @classmethod - def getAntiBanWaitTime(cls) -> int: + def get_anti_ban_wait_time(cls) -> int: return cls.get(ANTI_BAN_WAIT_TIME) - + @classmethod - def getLanguage(cls) -> str: + def get_language(cls) -> str: return cls.get(LANGUAGE) @classmethod - def getDownloadRealTime(cls) -> bool: + def get_download_real_time(cls) -> bool: return cls.get(DOWNLOAD_REAL_TIME) - + @classmethod - def getBitrate(cls) -> str: + def get_bitrate(cls) -> str: return cls.get(BITRATE) - \ No newline at end of file diff --git a/zspotify/podcast.py b/zspotify/podcast.py index c99797f7..e6107838 100644 --- a/zspotify/podcast.py +++ b/zspotify/podcast.py @@ -79,7 +79,7 @@ def download_episode(episode_id) -> None: download_directory = os.path.join( os.path.dirname(__file__), - ZSpotify.CONFIG.getRootPodcastPath(), + ZSpotify.CONFIG.get_root_podcast_path(), extra_paths, ) download_directory = os.path.realpath(download_directory) @@ -96,7 +96,7 @@ def download_episode(episode_id) -> None: if ( os.path.isfile(filepath) and os.path.getsize(filepath) == total_size - and ZSpotify.CONFIG.getSkipExistingFiles() + and ZSpotify.CONFIG.get_skip_existing_files() ): print( "\n### SKIPPING:", @@ -114,9 +114,9 @@ def download_episode(episode_id) -> None: unit_scale=True, unit_divisor=1024 ) as bar: - for _ in range(int(total_size / ZSpotify.CONFIG.getChunkSize()) + 1): + for _ in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1): bar.update(file.write( - stream.input_stream.stream().read(ZSpotify.CONFIG.getChunkSize()))) + stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size()))) else: filepath = os.path.join(download_directory, f"{filename}.mp3") download_podcast_directly(direct_download_url, filepath) diff --git a/zspotify/track.py b/zspotify/track.py index f8b440c7..037134d8 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -76,12 +76,12 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', (artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id) - if ZSpotify.CONFIG.getSplitAlbumDiscs(): + if ZSpotify.CONFIG.get_split_album_discs(): download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.CONFIG.getRootPath(), extra_paths, f'Disc {disc_number}') + __file__), ZSpotify.CONFIG.get_root_path(), extra_paths, f'Disc {disc_number}') else: download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.CONFIG.getRootPath(), extra_paths) + __file__), ZSpotify.CONFIG.get_root_path(), extra_paths) song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name) if prefix: @@ -89,9 +89,9 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', ) else f'{prefix_value} - {song_name}' filename = os.path.join( - download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.CONFIG.getDownloadFormat().lower())}') + download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.CONFIG.get_download_format().lower())}') - archive_directory = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.getRootPath()) + archive_directory = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path()) check_name = os.path.isfile(filename) and os.path.getsize(filename) check_id = scraped_song_id in get_directory_song_ids(download_directory) check_all_time = scraped_song_id in get_previously_downloaded(scraped_song_id, archive_directory) @@ -102,7 +102,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', if re.search(f'^{song_name}_', file)]) + 1 filename = os.path.join( - download_directory, f'{song_name}_{c}.{EXT_MAP.get(ZSpotify.CONFIG.getDownloadFormat())}') + download_directory, f'{song_name}_{c}.{EXT_MAP.get(ZSpotify.CONFIG.get_download_format())}') except Exception as e: @@ -114,11 +114,11 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', print('\n### SKIPPING:', song_name, '(SONG IS UNAVAILABLE) ###') else: - if check_id and check_name and ZSpotify.CONFIG.getSkipExistingFiles(): + if check_id and check_name and ZSpotify.CONFIG.get_skip_existing_files(): print('\n### SKIPPING:', song_name, '(SONG ALREADY EXISTS) ###') - elif check_all_time and ZSpotify.CONFIG.getSkipPreviouslyDownloaded(): + elif check_all_time and ZSpotify.CONFIG.get_skip_previously_downloaded(): print('\n### SKIPPING:', song_name, '(SONG ALREADY DOWNLOADED ONCE) ###') @@ -139,11 +139,11 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', unit_divisor=1024, disable=disable_progressbar ) as p_bar: - pause = duration_ms / ZSpotify.CONFIG.getChunkSize() - for chunk in range(int(total_size / ZSpotify.CONFIG.getChunkSize()) + 1): - data = stream.input_stream.stream().read(ZSpotify.CONFIG.getChunkSize()) + 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)) - if ZSpotify.CONFIG.getDownloadRealTime(): + if ZSpotify.CONFIG.get_download_real_time(): time.sleep(pause) convert_audio_format(filename) @@ -152,14 +152,14 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', set_music_thumbnail(filename, image_url) # add song id to archive file - if ZSpotify.CONFIG.getSkipPreviouslyDownloaded(): + if ZSpotify.CONFIG.get_skip_previously_downloaded(): add_to_archive(scraped_song_id, archive_directory) # add song id to download directory's .song_ids file if not check_id: add_to_directory_song_ids(download_directory, scraped_song_id) - if not ZSpotify.CONFIG.getAntiBanWaitTime(): - time.sleep(ZSpotify.CONFIG.getAntiBanWaitTime()) + if not ZSpotify.CONFIG.get_anti_ban_wait_time(): + time.sleep(ZSpotify.CONFIG.get_anti_ban_wait_time()) except Exception as e: print('### SKIPPING:', song_name, '(GENERAL DOWNLOAD ERROR) ###') @@ -173,10 +173,10 @@ def convert_audio_format(filename) -> None: temp_filename = f'{os.path.splitext(filename)[0]}.tmp' os.replace(filename, temp_filename) - download_format = ZSpotify.CONFIG.getDownloadFormat().lower() + download_format = ZSpotify.CONFIG.get_download_format().lower() file_codec = CODEC_MAP.get(download_format, 'copy') if file_codec != 'copy': - bitrate = ZSpotify.CONFIG.getBitrate() + bitrate = ZSpotify.CONFIG.get_bitrate() if not bitrate: if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH: bitrate = '320k' diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index f40ac122..1fe7a648 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -26,7 +26,7 @@ from config import Config class ZSpotify: SESSION: Session = None DOWNLOAD_QUALITY = None - CONFIG = Config() + CONFIG: Config = Config() def __init__(self, args): ZSpotify.CONFIG.load(args) @@ -65,14 +65,14 @@ class ZSpotify: def get_auth_header(cls): return { 'Authorization': f'Bearer {cls.__get_auth_token()}', - 'Accept-Language': f'{cls.CONFIG.getLanguage()}' + 'Accept-Language': f'{cls.CONFIG.get_language()}' } @classmethod def get_auth_header_and_params(cls, limit, offset): return { 'Authorization': f'Bearer {cls.__get_auth_token()}', - 'Accept-Language': f'{cls.CONFIG.getLanguage()}' + 'Accept-Language': f'{cls.CONFIG.get_language()}' }, {LIMIT: limit, OFFSET: offset} @classmethod @@ -89,4 +89,4 @@ class ZSpotify: @classmethod def check_premium(cls) -> bool: """ If user has spotify premium return true """ - return (cls.SESSION.get_user_attribute(TYPE) == PREMIUM) or cls.CONFIG.getForcePremium() + return (cls.SESSION.get_user_attribute(TYPE) == PREMIUM) or cls.CONFIG.get_force_premium() From a35d46e6ae4e3ccaf0e1ec3d27e38fa4bbce5fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Thu, 18 Nov 2021 23:02:05 +0100 Subject: [PATCH 04/11] Supply config values per commandline args --- zspotify/__main__.py | 9 ++++-- zspotify/config.py | 65 +++++++++++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/zspotify/__main__.py b/zspotify/__main__.py index 9d7cef98..b06ade3c 100644 --- a/zspotify/__main__.py +++ b/zspotify/__main__.py @@ -1,8 +1,7 @@ import argparse from app import client - - +from config import CONFIG_VALUES if __name__ == '__main__': parser = argparse.ArgumentParser(prog='zspotify', @@ -35,6 +34,12 @@ if __name__ == '__main__': type=str, help='Downloads tracks, playlists and albums from the URLs written in the file passed.') + for configkey in CONFIG_VALUES: + parser.add_argument(CONFIG_VALUES[configkey]['arg'], + type=str, + default=None, + help='Specify the value of the ['+configkey+'] config value') + parser.set_defaults(func=client) args = parser.parse_args() diff --git a/zspotify/config.py b/zspotify/config.py index 463b482c..771073aa 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -20,19 +20,19 @@ LANGUAGE = 'LANGUAGE' BITRATE = 'BITRATE' CONFIG_VALUES = { - ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': 'str', 'arg': '--root-path' }, - ROOT_PODCAST_PATH: { 'default': '../ZSpotify Podcasts/', 'type': 'str', 'arg': '--root-podcast-path' }, - SKIP_EXISTING_FILES: { 'default': True, 'type': 'bool', 'arg': '--skip-existing-files' }, - SKIP_PREVIOUSLY_DOWNLOADED: { 'default': False, 'type': 'bool', 'arg': '--skip-previously-downloaded' }, - DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': 'str', 'arg': '--download-format' }, - FORCE_PREMIUM: { 'default': False, 'type': 'bool', 'arg': '--force-premium' }, - ANTI_BAN_WAIT_TIME: { 'default': 1, 'type': 'int', 'arg': '--anti-ban-wait-time' }, - OVERRIDE_AUTO_WAIT: { 'default': False, 'type': 'bool', 'arg': '--override-auto-wait' }, - CHUNK_SIZE: { 'default': 50000, 'type': 'int', 'arg': '--chunk-size' }, - SPLIT_ALBUM_DISCS: { 'default': False, 'type': 'bool', 'arg': '--split-album-discs' }, - DOWNLOAD_REAL_TIME: { 'default': False, 'type': 'bool', 'arg': '--download-real-time' }, - LANGUAGE: { 'default': 'en', 'type': 'str', 'arg': '--language' }, - BITRATE: { 'default': '', 'type': 'str', 'arg': '--bitrate' }, + ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' }, + ROOT_PODCAST_PATH: { 'default': '../ZSpotify Podcasts/', 'type': str, 'arg': '--root-podcast-path' }, + SKIP_EXISTING_FILES: { 'default': 'True', 'type': bool, 'arg': '--skip-existing-files' }, + SKIP_PREVIOUSLY_DOWNLOADED: { 'default': 'False', 'type': bool, 'arg': '--skip-previously-downloaded' }, + DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': str, 'arg': '--download-format' }, + FORCE_PREMIUM: { 'default': 'False', 'type': bool, 'arg': '--force-premium' }, + ANTI_BAN_WAIT_TIME: { 'default': '1', 'type': int, 'arg': '--anti-ban-wait-time' }, + OVERRIDE_AUTO_WAIT: { 'default': 'False', 'type': bool, 'arg': '--override-auto-wait' }, + CHUNK_SIZE: { 'default': '50000', 'type': int, 'arg': '--chunk-size' }, + SPLIT_ALBUM_DISCS: { 'default': 'False', 'type': bool, 'arg': '--split-album-discs' }, + DOWNLOAD_REAL_TIME: { 'default': 'False', 'type': bool, 'arg': '--download-real-time' }, + LANGUAGE: { 'default': 'en', 'type': str, 'arg': '--language' }, + BITRATE: { 'default': '', 'type': str, 'arg': '--bitrate' }, } @@ -49,24 +49,57 @@ class Config: true_config_file_path = os.path.join(app_dir, config_fp) + # Load config from zs_config.json + if not os.path.exists(true_config_file_path): with open(true_config_file_path, 'w', encoding='utf-8') as config_file: json.dump(cls.get_default_json(), config_file, indent=4) cls.Values = cls.get_default_json() else: with open(true_config_file_path, encoding='utf-8') as config_file: - cls.Values = json.load(config_file) + jsonvalues = json.load(config_file) + cls.Values = {} + for key in CONFIG_VALUES: + if key in jsonvalues: + cls.Values[key] = cls.parse_arg_value(key, jsonvalues[key]) + + # Add default values for missing keys + for key in CONFIG_VALUES: if key not in cls.Values: - cls.Values[key] = CONFIG_VALUES[key].default + cls.Values[key] = cls.parse_arg_value(key, CONFIG_VALUES[key]['default']) + + # Override config from commandline arguments + + for key in CONFIG_VALUES: + if key.lower() in vars(args) and vars(args)[key.lower()] is not None: + cls.Values[key] = cls.parse_arg_value(key, vars(args)[key.lower()]) + + print(cls.Values, ' ', '\n') @classmethod def get_default_json(cls) -> Any: r = {} for key in CONFIG_VALUES: - r[key] = CONFIG_VALUES[key].default + r[key] = CONFIG_VALUES[key]['default'] return r + @classmethod + def parse_arg_value(cls, key, value) -> Any: + if type(value) == CONFIG_VALUES[key]['type']: + return value + if CONFIG_VALUES[key]['type'] == str: + return str(value) + if CONFIG_VALUES[key]['type'] == int: + return int(value) + if CONFIG_VALUES[key]['type'] == bool: + if str(value).lower() in ['yes', 'true', '1']: + return True + if str(value).lower() in ['no', 'false', '0']: + return False + raise ValueError("Not a boolean: " + value) + raise ValueError("Unknown Type: " + value) + @classmethod def get(cls, key) -> Any: return cls.Values.get(key) From 0adaa20d5956594505f5197bd584ac72bbcdc273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Thu, 18 Nov 2021 23:24:08 +0100 Subject: [PATCH 05/11] Added SONG_ARCHIVE config value --- zspotify/config.py | 10 ++++++++-- zspotify/track.py | 5 ++--- zspotify/utils.py | 17 ++++++++++------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/zspotify/config.py b/zspotify/config.py index 771073aa..a4320008 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -18,6 +18,7 @@ SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' LANGUAGE = 'LANGUAGE' BITRATE = 'BITRATE' +SONG_ARCHIVE = 'SONG_ARCHIVE' CONFIG_VALUES = { ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' }, @@ -33,6 +34,7 @@ CONFIG_VALUES = { DOWNLOAD_REAL_TIME: { 'default': 'False', 'type': bool, 'arg': '--download-real-time' }, LANGUAGE: { 'default': 'en', 'type': str, 'arg': '--language' }, BITRATE: { 'default': '', 'type': str, 'arg': '--bitrate' }, + SONG_ARCHIVE: { 'default': '.song_archive', 'type': str, 'arg': '--song-archive' }, } @@ -85,7 +87,7 @@ class Config: return r @classmethod - def parse_arg_value(cls, key, value) -> Any: + def parse_arg_value(cls, key: str, value: Any) -> Any: if type(value) == CONFIG_VALUES[key]['type']: return value if CONFIG_VALUES[key]['type'] == str: @@ -101,7 +103,7 @@ class Config: raise ValueError("Unknown Type: " + value) @classmethod - def get(cls, key) -> Any: + def get(cls, key: str) -> Any: return cls.Values.get(key) @classmethod @@ -155,3 +157,7 @@ class Config: @classmethod def get_bitrate(cls) -> str: return cls.get(BITRATE) + + @classmethod + def get_song_archive(cls) -> str: + return cls.get(SONG_ARCHIVE) diff --git a/zspotify/track.py b/zspotify/track.py index 037134d8..884d1329 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -91,10 +91,9 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', filename = os.path.join( download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.CONFIG.get_download_format().lower())}') - archive_directory = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path()) check_name = os.path.isfile(filename) and os.path.getsize(filename) check_id = scraped_song_id in get_directory_song_ids(download_directory) - check_all_time = scraped_song_id in get_previously_downloaded(scraped_song_id, archive_directory) + check_all_time = scraped_song_id in get_previously_downloaded() # a song with the same name is installed if not check_id and check_name: @@ -153,7 +152,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', # add song id to archive file if ZSpotify.CONFIG.get_skip_previously_downloaded(): - add_to_archive(scraped_song_id, archive_directory) + add_to_archive(scraped_song_id, artists[0], name) # add song id to download directory's .song_ids file if not check_id: add_to_directory_song_ids(download_directory, scraped_song_id) diff --git a/zspotify/utils.py b/zspotify/utils.py index 6c67c6b9..84bd9354 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -1,3 +1,4 @@ +import datetime import os import platform import re @@ -11,6 +12,8 @@ import requests from const import ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ WINDOWS_SYSTEM, ALBUMARTIST +from zspotify import ZSpotify + class MusicFormat(str, Enum): MP3 = 'mp3', @@ -27,29 +30,29 @@ def create_download_directory(download_path: str) -> None: with open(hidden_file_path, 'w', encoding='utf-8') as f: pass -def get_previously_downloaded(song_id: str, archive_directory: str) -> List[str]: +def get_previously_downloaded() -> List[str]: """ Returns list of all time downloaded songs """ ids = [] - archive_path = os.path.join(archive_directory, '.song_archive') + archive_path = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path(), ZSpotify.CONFIG.get_song_archive()) if os.path.exists(archive_path): with open(archive_path, 'r', encoding='utf-8') as f: - ids = [line.strip() for line in f.readlines()] + ids = [line.strip().split('\t')[0] for line in f.readlines()] return ids -def add_to_archive(song_id: str, archive_directory: str) -> None: +def add_to_archive(song_id: str, author_name: str, song_name: str) -> None: """ Adds song id to all time installed songs archive """ - archive_path = os.path.join(archive_directory, '.song_archive') + archive_path = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path(), ZSpotify.CONFIG.get_song_archive()) if os.path.exists(archive_path): with open(archive_path, 'a', encoding='utf-8') as f: - f.write(f'{song_id}\n') + f.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\n') else: with open(archive_path, 'w', encoding='utf-8') as f: - f.write(f'{song_id}\n') + f.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\n') def get_directory_song_ids(download_path: str) -> List[str]: """ Gets song ids of songs in directory """ From ad43153d4c980e86d326153f5bca42ea04e2bd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Thu, 18 Nov 2021 23:31:48 +0100 Subject: [PATCH 06/11] Added CREDENTIALS_LOCATION config value --- zspotify/config.py | 8 ++++++-- zspotify/const.py | 2 -- zspotify/zspotify.py | 9 +++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/zspotify/config.py b/zspotify/config.py index a4320008..ca10910d 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -19,6 +19,7 @@ DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' LANGUAGE = 'LANGUAGE' BITRATE = 'BITRATE' SONG_ARCHIVE = 'SONG_ARCHIVE' +CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION' CONFIG_VALUES = { ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' }, @@ -35,6 +36,7 @@ CONFIG_VALUES = { LANGUAGE: { 'default': 'en', 'type': str, 'arg': '--language' }, BITRATE: { 'default': '', 'type': str, 'arg': '--bitrate' }, SONG_ARCHIVE: { 'default': '.song_archive', 'type': str, 'arg': '--song-archive' }, + CREDENTIALS_LOCATION: { 'default': 'credentials.json', 'type': str, 'arg': '--credentials-location' }, } @@ -77,8 +79,6 @@ class Config: if key.lower() in vars(args) and vars(args)[key.lower()] is not None: cls.Values[key] = cls.parse_arg_value(key, vars(args)[key.lower()]) - print(cls.Values, ' ', '\n') - @classmethod def get_default_json(cls) -> Any: r = {} @@ -161,3 +161,7 @@ class Config: @classmethod def get_song_archive(cls) -> str: return cls.get(SONG_ARCHIVE) + + @classmethod + def get_credentials_location(cls) -> str: + return cls.get(CREDENTIALS_LOCATION) diff --git a/zspotify/const.py b/zspotify/const.py index 828bdd3d..057921c1 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -80,8 +80,6 @@ USER_LIBRARY_READ = 'user-library-read' WINDOWS_SYSTEM = 'Windows' -CREDENTIALS_JSON = 'credentials.json' - CODEC_MAP = { 'aac': 'aac', 'fdk_aac': 'libfdk_aac', diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index 1fe7a648..51ef6c64 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -16,10 +16,9 @@ import requests from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.core import Session -from const import CREDENTIALS_JSON, TYPE, \ +from const import TYPE, \ PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, \ PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ -from utils import MusicFormat from config import Config @@ -36,9 +35,11 @@ class ZSpotify: def login(cls): """ Authenticates with Spotify and saves credentials to a file """ - if os.path.isfile(CREDENTIALS_JSON): + cred_location = os.path.join(os.getcwd(), Config.get_credentials_location()) + + if os.path.isfile(cred_location): try: - cls.SESSION = Session.Builder().stored_file().create() + cls.SESSION = Session.Builder().stored_file(cred_location).create() return except RuntimeError: pass From 543567079ba09f04920cfd0267b1074f97c6fcb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 19 Nov 2021 00:20:17 +0100 Subject: [PATCH 07/11] Added --output argument for output templating --- zspotify/album.py | 5 +---- zspotify/app.py | 9 ++++----- zspotify/config.py | 24 +++++++++++++++++++++++ zspotify/playlist.py | 3 +-- zspotify/track.py | 46 +++++++++++++++++++++++++------------------- zspotify/utils.py | 2 +- 6 files changed, 57 insertions(+), 32 deletions(-) diff --git a/zspotify/album.py b/zspotify/album.py index 5325caa9..4392f5f5 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -47,12 +47,9 @@ def get_artist_albums(artist_id): def download_album(album): """ Downloads songs from an album """ artist, album_name = get_album_name(album) - artist_fixed = fix_filename(artist) - album_name_fixed = fix_filename(album_name) tracks = get_album_tracks(album) for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): - download_track(track[ID], f'{artist_fixed}/{album_name_fixed}', - prefix=True, prefix_value=str(n), disable_progressbar=True) + download_track('album', track[ID], extra_keys={'album_num': str(n).zfill(2), 'artist': artist, 'album': album}, disable_progressbar=True) def download_artist_albums(artist): diff --git a/zspotify/app.py b/zspotify/app.py index 7b6bbffb..6cc67194 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -54,7 +54,7 @@ def client(args) -> None: print( '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') else: - download_track(song[TRACK][ID], 'Liked Songs/') + download_track('liked', song[TRACK][ID]) print('\n') if args.search_spotify: @@ -75,7 +75,7 @@ def download_from_urls(urls: list[str]) -> bool: if track_id is not None: download = True - download_track(track_id) + download_track('single', track_id) elif artist_id is not None: download = True download_artist_albums(artist_id) @@ -87,8 +87,7 @@ def download_from_urls(urls: list[str]) -> bool: playlist_songs = get_playlist_songs(playlist_id) name, _ = get_playlist_info(playlist_id) for song in playlist_songs: - download_track(song[TRACK][ID], - fix_filename(name) + '/') + download_track('playlist', song[TRACK][ID], extra_keys={'playlist': name}) print('\n') elif episode_id is not None: download = True @@ -273,7 +272,7 @@ def search(search_term): print_pos = dics.index(dic) + 1 if print_pos == position: if dic['type'] == TRACK: - download_track(dic[ID]) + download_track('single', dic[ID]) elif dic['type'] == ALBUM: download_album(dic[ID]) elif dic['type'] == ARTIST: diff --git a/zspotify/config.py b/zspotify/config.py index ca10910d..4e2e081c 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -20,6 +20,7 @@ LANGUAGE = 'LANGUAGE' BITRATE = 'BITRATE' SONG_ARCHIVE = 'SONG_ARCHIVE' CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION' +OUTPUT = 'OUTPUT' CONFIG_VALUES = { ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' }, @@ -37,8 +38,14 @@ CONFIG_VALUES = { BITRATE: { 'default': '', 'type': str, 'arg': '--bitrate' }, SONG_ARCHIVE: { 'default': '.song_archive', 'type': str, 'arg': '--song-archive' }, CREDENTIALS_LOCATION: { 'default': 'credentials.json', 'type': str, 'arg': '--credentials-location' }, + OUTPUT: { 'default': '', 'type': str, 'arg': '--output' }, } +OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}' +OUTPUT_DEFAULT_PLAYLIST_EXT = '{playlist}/{playlist_num} - {artist} - {song_name}.{ext}' +OUTPUT_DEFAULT_LIKED_SONGS = 'Liked Songs/{artist} - {song_name}.{ext}' +OUTPUT_DEFAULT_SINGLE = '{artist} - {song_name}.{ext}' +OUTPUT_DEFAULT_ALBUM = '{artist}/{album}/{album_num} - {artist} - {song_name}.{ext}' class Config: Values = {} @@ -165,3 +172,20 @@ class Config: @classmethod def get_credentials_location(cls) -> str: return cls.get(CREDENTIALS_LOCATION) + + @classmethod + def get_output(cls, mode: str) -> str: + v = cls.get(OUTPUT) + if v: + return v + if mode == 'playlist': + return OUTPUT_DEFAULT_PLAYLIST + if mode == 'extplaylist': + return OUTPUT_DEFAULT_PLAYLIST_EXT + if mode == 'liked': + return OUTPUT_DEFAULT_LIKED_SONGS + if mode == 'single': + return OUTPUT_DEFAULT_SINGLE + if mode == 'album': + return OUTPUT_DEFAULT_ALBUM + raise ValueError() diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 4a2b8916..3f169340 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -54,8 +54,7 @@ def download_playlist(playlist): p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) enum = 1 for song in p_bar: - download_track(song[TRACK][ID], fix_filename(playlist[NAME].strip()) + '/', - prefix=True, prefix_value=str(enum) ,disable_progressbar=True) + download_track('extplaylist', song[TRACK][ID], extra_keys={'playlist': playlist, 'playlist_num': str(enum).zfill(2)}, disable_progressbar=True) p_bar.set_description(song[TRACK][NAME]) enum += 1 diff --git a/zspotify/track.py b/zspotify/track.py index 884d1329..ae5e54b3 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -69,39 +69,45 @@ def get_song_duration(song_id: str) -> float: return duration # noinspection PyBroadException -def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', disable_progressbar=False) -> None: +def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=False) -> None: """ Downloads raw song audio from Spotify """ try: + output_template = ZSpotify.CONFIG.get_output(mode) + (artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id) - if ZSpotify.CONFIG.get_split_album_discs(): - download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.CONFIG.get_root_path(), extra_paths, f'Disc {disc_number}') - else: - download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.CONFIG.get_root_path(), extra_paths) - song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name) - if prefix: - song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( - ) else f'{prefix_value} - {song_name}' - filename = os.path.join( - download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.CONFIG.get_download_format().lower())}') + for k in extra_keys: + output_template = output_template.replace("{"+k+"}", fix_filename(extra_keys[k])) + + output_template = output_template.replace("{artist}", fix_filename(artists[0])) + output_template = output_template.replace("{album}", fix_filename(album_name)) + output_template = output_template.replace("{song_name}", fix_filename(name)) + output_template = output_template.replace("{release_year}", fix_filename(release_year)) + output_template = output_template.replace("{disc_number}", fix_filename(disc_number)) + output_template = output_template.replace("{track_number}", fix_filename(track_number)) + output_template = output_template.replace("{id}", fix_filename(scraped_song_id)) + output_template = output_template.replace("{track_id}", fix_filename(track_id)) + output_template = output_template.replace("{ext}", EXT_MAP.get(ZSpotify.CONFIG.get_download_format().lower())) + + filename = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path(), output_template) + filedir = os.path.dirname(filename) check_name = os.path.isfile(filename) and os.path.getsize(filename) - check_id = scraped_song_id in get_directory_song_ids(download_directory) + check_id = scraped_song_id in get_directory_song_ids(filedir) check_all_time = scraped_song_id in get_previously_downloaded() # a song with the same name is installed if not check_id and check_name: - c = len([file for file in os.listdir(download_directory) - if re.search(f'^{song_name}_', file)]) + 1 + c = len([file for file in os.listdir(filedir) if re.search(f'^{filename}_', str(file))]) + 1 - filename = os.path.join( - download_directory, f'{song_name}_{c}.{EXT_MAP.get(ZSpotify.CONFIG.get_download_format())}') + fname = os.path.splitext(os.path.basename(filename))[0] + ext = os.path.splitext(os.path.basename(filename))[1] + + filename = os.path.join(filedir, f'{fname}_{c}{ext}') except Exception as e: @@ -127,7 +133,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', track_id = TrackId.from_base62(track_id) stream = ZSpotify.get_content_stream( track_id, ZSpotify.DOWNLOAD_QUALITY) - create_download_directory(download_directory) + create_download_directory(filedir) total_size = stream.input_stream.size with open(filename, 'wb') as file, tqdm( @@ -155,7 +161,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', add_to_archive(scraped_song_id, artists[0], name) # add song id to download directory's .song_ids file if not check_id: - add_to_directory_song_ids(download_directory, scraped_song_id) + add_to_directory_song_ids(filedir, scraped_song_id) if not ZSpotify.CONFIG.get_anti_ban_wait_time(): time.sleep(ZSpotify.CONFIG.get_anti_ban_wait_time()) diff --git a/zspotify/utils.py b/zspotify/utils.py index 84bd9354..d7627d95 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -256,4 +256,4 @@ def fix_filename(name): >>> all('_' == fix_filename(chr(i)) for i in list(range(32))) True """ - return re.sub(r'[/\\:|<>"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$', "_", 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) From c836240b3076818de4011d7c2fe18c304cc39654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 19 Nov 2021 00:37:13 +0100 Subject: [PATCH 08/11] reintroduce split_album_discs config --- zspotify/config.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/zspotify/config.py b/zspotify/config.py index 4e2e081c..2293ee78 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -179,13 +179,28 @@ class Config: if v: return v if mode == 'playlist': + if cls.get_split_album_discs(): + split = os.path.split(OUTPUT_DEFAULT_PLAYLIST) + return os.path.join(split[0], 'Disc {disc_number}', split[0]) return OUTPUT_DEFAULT_PLAYLIST if mode == 'extplaylist': + if cls.get_split_album_discs(): + split = os.path.split(OUTPUT_DEFAULT_PLAYLIST_EXT) + return os.path.join(split[0], 'Disc {disc_number}', split[0]) return OUTPUT_DEFAULT_PLAYLIST_EXT if mode == 'liked': + if cls.get_split_album_discs(): + split = os.path.split(OUTPUT_DEFAULT_LIKED_SONGS) + return os.path.join(split[0], 'Disc {disc_number}', split[0]) return OUTPUT_DEFAULT_LIKED_SONGS if mode == 'single': + if cls.get_split_album_discs(): + split = os.path.split(OUTPUT_DEFAULT_SINGLE) + return os.path.join(split[0], 'Disc {disc_number}', split[0]) return OUTPUT_DEFAULT_SINGLE if mode == 'album': + if cls.get_split_album_discs(): + split = os.path.split(OUTPUT_DEFAULT_ALBUM) + return os.path.join(split[0], 'Disc {disc_number}', split[0]) return OUTPUT_DEFAULT_ALBUM raise ValueError() From cdd013b09ed8028408498182057cf6c4b2e7982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 19 Nov 2021 17:46:32 +0100 Subject: [PATCH 09/11] Also save ext data in .song_ids file (filename, date, etc) --- zspotify/track.py | 4 ++-- zspotify/utils.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/zspotify/track.py b/zspotify/track.py index ae5e54b3..4863ca21 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -158,10 +158,10 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= # add song id to archive file if ZSpotify.CONFIG.get_skip_previously_downloaded(): - add_to_archive(scraped_song_id, artists[0], name) + add_to_archive(scraped_song_id, filename, artists[0], name) # add song id to download directory's .song_ids file if not check_id: - add_to_directory_song_ids(filedir, scraped_song_id) + add_to_directory_song_ids(filedir, scraped_song_id, filename, artists[0], name) if not ZSpotify.CONFIG.get_anti_ban_wait_time(): time.sleep(ZSpotify.CONFIG.get_anti_ban_wait_time()) diff --git a/zspotify/utils.py b/zspotify/utils.py index d7627d95..bb233c76 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -42,17 +42,17 @@ def get_previously_downloaded() -> List[str]: return ids -def add_to_archive(song_id: 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 """ archive_path = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path(), ZSpotify.CONFIG.get_song_archive()) if os.path.exists(archive_path): - with open(archive_path, 'a', encoding='utf-8') as f: - f.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\n') + with open(archive_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') else: - with open(archive_path, 'w', encoding='utf-8') as f: - f.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\n') + 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 """ @@ -62,18 +62,18 @@ def get_directory_song_ids(download_path: str) -> List[str]: hidden_file_path = os.path.join(download_path, '.song_ids') if os.path.isfile(hidden_file_path): with open(hidden_file_path, 'r', encoding='utf-8') as file: - song_ids.extend([line.strip() for line in file.readlines()]) + song_ids.extend([line.strip().split('\t')[0] for line in file.readlines()]) return song_ids -def add_to_directory_song_ids(download_path: str, song_id: 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 """ hidden_file_path = os.path.join(download_path, '.song_ids') # not checking if file exists because we need an exception # to be raised if something is wrong with open(hidden_file_path, 'a', encoding='utf-8') as file: - file.write(f'{song_id}\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: """ Returns the downloaded file's duration in seconds """ From c8c6c7d8cc09331cc8ba76714636397eb6fa2844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 19 Nov 2021 17:54:07 +0100 Subject: [PATCH 10/11] A few fixes --- zspotify/album.py | 2 +- zspotify/playlist.py | 2 +- zspotify/track.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zspotify/album.py b/zspotify/album.py index 4392f5f5..c4769f69 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -49,7 +49,7 @@ def download_album(album): artist, album_name = get_album_name(album) tracks = get_album_tracks(album) for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): - download_track('album', track[ID], extra_keys={'album_num': str(n).zfill(2), 'artist': artist, 'album': album}, disable_progressbar=True) + download_track('album', track[ID], extra_keys={'album_num': str(n).zfill(2), 'artist': artist, 'album': album_name, 'album_id': album}, disable_progressbar=True) def download_artist_albums(artist): diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 3f169340..7ecccd63 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -54,7 +54,7 @@ def download_playlist(playlist): p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) enum = 1 for song in p_bar: - download_track('extplaylist', song[TRACK][ID], extra_keys={'playlist': playlist, 'playlist_num': str(enum).zfill(2)}, disable_progressbar=True) + download_track('extplaylist', song[TRACK][ID], extra_keys={'playlist': playlist[NAME], 'playlist_num': str(enum).zfill(2)}, disable_progressbar=True) p_bar.set_description(song[TRACK][NAME]) enum += 1 diff --git a/zspotify/track.py b/zspotify/track.py index 4863ca21..c37aac24 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -158,10 +158,10 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= # add song id to archive file if ZSpotify.CONFIG.get_skip_previously_downloaded(): - add_to_archive(scraped_song_id, filename, artists[0], name) + add_to_archive(scraped_song_id, os.path.basename(filename), artists[0], name) # add song id to download directory's .song_ids file if not check_id: - add_to_directory_song_ids(filedir, scraped_song_id, filename, artists[0], name) + add_to_directory_song_ids(filedir, scraped_song_id, os.path.basename(filename), artists[0], name) if not ZSpotify.CONFIG.get_anti_ban_wait_time(): time.sleep(ZSpotify.CONFIG.get_anti_ban_wait_time()) From de0f8b30b11984f4270a773fe1775704a452101d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 19 Nov 2021 18:45:04 +0100 Subject: [PATCH 11/11] Added options to regulate terminal output (splash, progress, errors, skips, ...) --- zspotify/album.py | 5 ++--- zspotify/app.py | 17 ++++++----------- zspotify/config.py | 14 ++++++++++++++ zspotify/playlist.py | 5 ++--- zspotify/podcast.py | 14 ++++---------- zspotify/termoutput.py | 26 ++++++++++++++++++++++++++ zspotify/track.py | 27 ++++++++++++--------------- zspotify/utils.py | 12 +++--------- 8 files changed, 69 insertions(+), 51 deletions(-) create mode 100644 zspotify/termoutput.py diff --git a/zspotify/album.py b/zspotify/album.py index c4769f69..8aea1199 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -1,6 +1,5 @@ -from tqdm import tqdm - from const import ITEMS, ARTISTS, NAME, ID +from termoutput import Printer from track import download_track from utils import fix_filename from zspotify import ZSpotify @@ -48,7 +47,7 @@ def download_album(album): """ Downloads songs from an album """ artist, album_name = get_album_name(album) tracks = get_album_tracks(album) - for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): + for n, track in Printer.progress(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): download_track('album', track[ID], extra_keys={'album_num': str(n).zfill(2), 'artist': artist, 'album': album_name, 'album_id': album}, disable_progressbar=True) diff --git a/zspotify/app.py b/zspotify/app.py index 6cc67194..75be3156 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -7,6 +7,7 @@ from const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALB OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist from podcast import download_episode, get_show_episodes +from termoutput import Printer, PrintChannel from track import download_track, get_saved_tracks from utils import fix_filename, splash, split_input, regex_input_for_urls from zspotify import ZSpotify @@ -18,16 +19,13 @@ def client(args) -> None: """ Connects to spotify to perform query's and get songs to download """ ZSpotify(args) - if not args.no_splash: - splash() + Printer.print(PrintChannel.SPLASH, splash()) if ZSpotify.check_premium(): - if not args.no_splash: - print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n') + Printer.print(PrintChannel.SPLASH, '[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH else: - if not args.no_splash: - print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') + Printer.print(PrintChannel.SPLASH, '[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH if args.download: @@ -40,7 +38,7 @@ def client(args) -> None: download_from_urls(urls) else: - print(f'File {filename} not found.\n') + Printer.print(PrintChannel.ERRORS, f'File {filename} not found.\n') if args.urls: download_from_urls(args.urls) @@ -51,11 +49,9 @@ def client(args) -> None: if args.liked_songs: for song in get_saved_tracks(): if not song[TRACK][NAME]: - print( - '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') + Printer.print(PrintChannel.ERRORS, '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###' + "\n") else: download_track('liked', song[TRACK][ID]) - print('\n') if args.search_spotify: search_text = '' @@ -88,7 +84,6 @@ def download_from_urls(urls: list[str]) -> bool: name, _ = get_playlist_info(playlist_id) for song in playlist_songs: download_track('playlist', song[TRACK][ID], extra_keys={'playlist': name}) - print('\n') elif episode_id is not None: download = True download_episode(episode_id) diff --git a/zspotify/config.py b/zspotify/config.py index 2293ee78..79f79638 100644 --- a/zspotify/config.py +++ b/zspotify/config.py @@ -2,6 +2,7 @@ import json import os import sys from typing import Any +from enum import Enum CONFIG_FILE_PATH = '../zs_config.json' @@ -21,6 +22,11 @@ BITRATE = 'BITRATE' SONG_ARCHIVE = 'SONG_ARCHIVE' CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION' OUTPUT = 'OUTPUT' +PRINT_SPLASH = 'PRINT_SPLASH' +PRINT_SKIPS = 'PRINT_SKIPS' +PRINT_DOWNLOAD_PROGRESS = 'PRINT_DOWNLOAD_PROGRESS' +PRINT_ERRORS = 'PRINT_ERRORS' +PRINT_DOWNLOADS = 'PRINT_DOWNLOADS' CONFIG_VALUES = { ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' }, @@ -39,6 +45,11 @@ CONFIG_VALUES = { SONG_ARCHIVE: { 'default': '.song_archive', 'type': str, 'arg': '--song-archive' }, CREDENTIALS_LOCATION: { 'default': 'credentials.json', 'type': str, 'arg': '--credentials-location' }, OUTPUT: { 'default': '', 'type': str, 'arg': '--output' }, + PRINT_SPLASH: { 'default': 'True', 'type': bool, 'arg': '--print-splash' }, + PRINT_SKIPS: { 'default': 'True', 'type': bool, 'arg': '--print-skips' }, + 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' }, } OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}' @@ -86,6 +97,9 @@ class Config: if key.lower() in vars(args) and vars(args)[key.lower()] is not None: cls.Values[key] = cls.parse_arg_value(key, vars(args)[key.lower()]) + if args.no_splash: + cls.Values[PRINT_SPLASH] = False + @classmethod def get_default_json(cls) -> Any: r = {} diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 7ecccd63..67ddb336 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -1,6 +1,5 @@ -from tqdm import tqdm - from const import ITEMS, ID, TRACK, NAME +from termoutput import Printer from track import download_track from utils import fix_filename, split_input from zspotify import ZSpotify @@ -51,7 +50,7 @@ def download_playlist(playlist): """Downloads all the songs from a playlist""" playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]] - p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) + p_bar = Printer.progress(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) enum = 1 for song in p_bar: download_track('extplaylist', song[TRACK][ID], extra_keys={'playlist': playlist[NAME], 'playlist_num': str(enum).zfill(2)}, disable_progressbar=True) diff --git a/zspotify/podcast.py b/zspotify/podcast.py index e6107838..88cd4318 100644 --- a/zspotify/podcast.py +++ b/zspotify/podcast.py @@ -3,9 +3,9 @@ from typing import Optional, Tuple from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.metadata import EpisodeId -from tqdm import tqdm from const import (ERROR, ID, ITEMS, NAME, SHOW) +from termoutput import PrintChannel, Printer from utils import create_download_directory, fix_filename from zspotify import ZSpotify @@ -70,7 +70,7 @@ def download_episode(episode_id) -> None: extra_paths = podcast_name + '/' if podcast_name is None: - print('### SKIPPING: (EPISODE NOT FOUND) ###') + Printer.print(PrintChannel.ERRORS, '### SKIPPING: (EPISODE NOT FOUND) ###') else: filename = podcast_name + ' - ' + episode_name @@ -98,16 +98,10 @@ def download_episode(episode_id) -> None: and os.path.getsize(filepath) == total_size and ZSpotify.CONFIG.get_skip_existing_files() ): - print( - "\n### SKIPPING:", - podcast_name, - "-", - episode_name, - "(EPISODE ALREADY EXISTS) ###", - ) + Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###") return - with open(filepath, 'wb') as file, tqdm( + with open(filepath, 'wb') as file, Printer.progress( desc=filename, total=total_size, unit='B', diff --git a/zspotify/termoutput.py b/zspotify/termoutput.py new file mode 100644 index 00000000..bf57e356 --- /dev/null +++ b/zspotify/termoutput.py @@ -0,0 +1,26 @@ +from enum import Enum +from tqdm import tqdm + +from config import PRINT_SPLASH, PRINT_SKIPS, PRINT_DOWNLOAD_PROGRESS, PRINT_ERRORS, PRINT_DOWNLOADS +from zspotify import ZSpotify + + +class PrintChannel(Enum): + SPLASH = PRINT_SPLASH + SKIPS = PRINT_SKIPS + DOWNLOAD_PROGRESS = PRINT_DOWNLOAD_PROGRESS + ERRORS = PRINT_ERRORS + DOWNLOADS = PRINT_DOWNLOADS + + +class Printer: + @staticmethod + def print(channel: PrintChannel, msg: str) -> None: + if ZSpotify.CONFIG.get(channel.value): + print(msg) + + @staticmethod + def progress(iterable=None, desc=None, total=None, unit='it', disable=False, unit_scale=False, unit_divisor=1000): + if not ZSpotify.CONFIG.get(PrintChannel.DOWNLOAD_PROGRESS.value): + disable = True + return tqdm(iterable=iterable, desc=desc, total=total, disable=disable, unit=unit, unit_scale=unit_scale, unit_divisor=unit_divisor) diff --git a/zspotify/track.py b/zspotify/track.py index c37aac24..8f4ef9de 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -7,10 +7,10 @@ from librespot.audio.decoders import AudioQuality from librespot.metadata import TrackId from ffmpy import FFmpeg from pydub import AudioSegment -from tqdm import tqdm 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 from zspotify import ZSpotify @@ -111,21 +111,18 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= except Exception as e: - print('### SKIPPING SONG - FAILED TO QUERY METADATA ###') - print(e) + Printer.print(PrintChannel.ERRORS, '### SKIPPING SONG - FAILED TO QUERY METADATA ###') + Printer.print(PrintChannel.ERRORS, str(e) + "\n") else: try: if not is_playable: - print('\n### SKIPPING:', song_name, - '(SONG IS UNAVAILABLE) ###') + 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(): - print('\n### SKIPPING:', song_name, - '(SONG ALREADY EXISTS) ###') + Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG ALREADY EXISTS) ###' + "\n") elif check_all_time and ZSpotify.CONFIG.get_skip_previously_downloaded(): - print('\n### SKIPPING:', song_name, - '(SONG ALREADY DOWNLOADED ONCE) ###') + Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG ALREADY DOWNLOADED ONCE) ###' + "\n") else: if track_id != scraped_song_id: @@ -136,7 +133,7 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= create_download_directory(filedir) total_size = stream.input_stream.size - with open(filename, 'wb') as file, tqdm( + with open(filename, 'wb') as file, Printer.progress( desc=song_name, total=total_size, unit='B', @@ -152,10 +149,11 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= time.sleep(pause) 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) + Printer.print(PrintChannel.DOWNLOADS, f'### Downloaded "{song_name}" to "{os.path.relpath(filename, os.path.dirname(__file__))}" ###' + "\n") + # add song id to archive file if ZSpotify.CONFIG.get_skip_previously_downloaded(): add_to_archive(scraped_song_id, os.path.basename(filename), artists[0], name) @@ -166,9 +164,8 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar= if not ZSpotify.CONFIG.get_anti_ban_wait_time(): time.sleep(ZSpotify.CONFIG.get_anti_ban_wait_time()) except Exception as e: - print('### SKIPPING:', song_name, - '(GENERAL DOWNLOAD ERROR) ###') - print(e) + Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###') + Printer.print(PrintChannel.ERRORS, str(e) + "\n") if os.path.exists(filename): os.remove(filename) diff --git a/zspotify/utils.py b/zspotify/utils.py index bb233c76..c7da4c39 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -86,12 +86,6 @@ def get_downloaded_song_duration(filename: str) -> float: return duration -def wait(seconds: int = 3) -> None: - """ Pause for a set number of seconds """ - for second in range(seconds)[::-1]: - print(f'\rWait for {second + 1} second(s)...', end='') - time.sleep(1) - def split_input(selection) -> List[str]: """ Returns a list of inputted strings """ @@ -106,15 +100,15 @@ def split_input(selection) -> List[str]: return inputs -def splash() -> None: +def splash() -> str: """ Displays splash screen """ - print(""" + return """ ███████ ███████ ██████ ██████ ████████ ██ ███████ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ███████ ██████ ██ ██ ██ ██ █████ ████ ███ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ███████ ██ ██████ ██ ██ ██ ██ - """) + """ def clear() -> None: