mirror of
https://zotify.xyz/zotify/zotify.git
synced 2025-06-22 18:06:44 +00:00
Zotify 0.6
This commit is contained in:
parent
d8c17e2ce9
commit
1e48861df3
9 changed files with 136 additions and 102 deletions
|
@ -12,7 +12,7 @@ from zotify.config import CONFIG_VALUES
|
|||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='zotify',
|
||||
description='A music and podcast downloader needing only a python interpreter and ffmpeg.')
|
||||
description='A music and podcast downloader needing only python and ffmpeg.')
|
||||
parser.add_argument('-ns', '--no-splash',
|
||||
action='store_true',
|
||||
help='Suppress the splash screen when loading.')
|
||||
|
|
|
@ -5,6 +5,7 @@ from pathlib import Path
|
|||
from zotify.album import download_album, download_artist_albums
|
||||
from zotify.const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \
|
||||
OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME, TYPE
|
||||
from zotify.loader import Loader
|
||||
from zotify.playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist
|
||||
from zotify.podcast import download_episode, get_show_episodes
|
||||
from zotify.termoutput import Printer, PrintChannel
|
||||
|
@ -17,16 +18,20 @@ SEARCH_URL = 'https://api.spotify.com/v1/search'
|
|||
|
||||
def client(args) -> None:
|
||||
""" Connects to download server to perform query's and get songs to download """
|
||||
prepare_download_loader = Loader(PrintChannel.PROGRESS_INFO, "Signing in...")
|
||||
prepare_download_loader.start()
|
||||
Zotify(args)
|
||||
prepare_download_loader.stop()
|
||||
|
||||
Printer.print(PrintChannel.SPLASH, splash())
|
||||
|
||||
if Zotify.check_premium():
|
||||
Printer.print(PrintChannel.WARNINGS, '[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n')
|
||||
Zotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH
|
||||
else:
|
||||
Printer.print(PrintChannel.WARNINGS, '[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n')
|
||||
Zotify.DOWNLOAD_QUALITY = AudioQuality.HIGH
|
||||
quality_options = {
|
||||
'auto': AudioQuality.VERY_HIGH if Zotify.check_premium() else AudioQuality.HIGH,
|
||||
'normal': AudioQuality.NORMAL,
|
||||
'high': AudioQuality.HIGH,
|
||||
'very_high': AudioQuality.VERY_HIGH
|
||||
}
|
||||
Zotify.DOWNLOAD_QUALITY = quality_options[Zotify.CONFIG.get_download_quality()]
|
||||
|
||||
if args.download:
|
||||
urls = []
|
||||
|
|
|
@ -6,18 +6,18 @@ from typing import Any
|
|||
|
||||
ROOT_PATH = 'ROOT_PATH'
|
||||
ROOT_PODCAST_PATH = 'ROOT_PODCAST_PATH'
|
||||
SKIP_EXISTING_FILES = 'SKIP_EXISTING_FILES'
|
||||
SKIP_EXISTING = 'SKIP_EXISTING'
|
||||
SKIP_PREVIOUSLY_DOWNLOADED = 'SKIP_PREVIOUSLY_DOWNLOADED'
|
||||
DOWNLOAD_FORMAT = 'DOWNLOAD_FORMAT'
|
||||
FORCE_PREMIUM = 'FORCE_PREMIUM'
|
||||
ANTI_BAN_WAIT_TIME = 'ANTI_BAN_WAIT_TIME'
|
||||
BULK_WAIT_TIME = 'BULK_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'
|
||||
TRACK_ARCHIVE = 'TRACK_ARCHIVE'
|
||||
DOWNLOAD_QUALITY = 'DOWNLOAD_QUALITY'
|
||||
TRANSCODE_BITRATE = 'TRANSCODE_BITRATE'
|
||||
SONG_ARCHIVE = 'SONG_ARCHIVE'
|
||||
CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION'
|
||||
OUTPUT = 'OUTPUT'
|
||||
PRINT_SPLASH = 'PRINT_SPLASH'
|
||||
|
@ -35,23 +35,25 @@ RETRY_ATTEMPTS = 'RETRY_ATTEMPTS'
|
|||
CONFIG_VERSION = 'CONFIG_VERSION'
|
||||
|
||||
CONFIG_VALUES = {
|
||||
ROOT_PATH: { 'default': '', 'type': str, 'arg': '--root-path' },
|
||||
ROOT_PODCAST_PATH: { 'default': '', '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' },
|
||||
RETRY_ATTEMPTS: { 'default': '5', 'type': int, 'arg': '--retry-attemps' },
|
||||
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' },
|
||||
TRACK_ARCHIVE: { 'default': '', 'type': str, 'arg': '--track-archive' },
|
||||
CREDENTIALS_LOCATION: { 'default': '', 'type': str, 'arg': '--credentials-location' },
|
||||
OUTPUT: { 'default': '', 'type': str, 'arg': '--output' },
|
||||
SONG_ARCHIVE: { 'default': '', 'type': str, 'arg': '--song-archive' },
|
||||
ROOT_PATH: { 'default': '', 'type': str, 'arg': '--root-path' },
|
||||
ROOT_PODCAST_PATH: { 'default': '', 'type': str, 'arg': '--root-podcast-path' },
|
||||
SPLIT_ALBUM_DISCS: { 'default': 'False', 'type': bool, 'arg': '--split-album-discs' },
|
||||
MD_ALLGENRES: { 'default': 'False', 'type': bool, 'arg': '--md-allgenres' },
|
||||
MD_GENREDELIMITER: { 'default': ',', 'type': str, 'arg': '--md-genredelimiter' },
|
||||
DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': str, 'arg': '--download-format' },
|
||||
DOWNLOAD_QUALITY: { 'default': 'auto', 'type': str, 'arg': '--download-quality' },
|
||||
TRANSCODE_BITRATE: { 'default': 'auto', 'type': str, 'arg': '--transcode-bitrate' },
|
||||
SKIP_EXISTING: { 'default': 'True', 'type': bool, 'arg': '--skip-existing' },
|
||||
SKIP_PREVIOUSLY_DOWNLOADED: { 'default': 'False', 'type': bool, 'arg': '--skip-previously-downloaded' },
|
||||
RETRY_ATTEMPTS: { 'default': '5', 'type': int, 'arg': '--retry-attemps' },
|
||||
BULK_WAIT_TIME: { 'default': '1', 'type': int, 'arg': '--bulk-wait-time' },
|
||||
OVERRIDE_AUTO_WAIT: { 'default': 'False', 'type': bool, 'arg': '--override-auto-wait' },
|
||||
CHUNK_SIZE: { 'default': '50000', 'type': int, 'arg': '--chunk-size' },
|
||||
DOWNLOAD_REAL_TIME: { 'default': 'False', 'type': bool, 'arg': '--download-real-time' },
|
||||
LANGUAGE: { 'default': 'en', 'type': str, 'arg': '--language' },
|
||||
PRINT_SPLASH: { 'default': 'False', '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' },
|
||||
|
@ -60,15 +62,13 @@ CONFIG_VALUES = {
|
|||
PRINT_API_ERRORS: { 'default': 'False', 'type': bool, 'arg': '--print-api-errors' },
|
||||
PRINT_PROGRESS_INFO: { 'default': 'True', 'type': bool, 'arg': '--print-progress-info' },
|
||||
PRINT_WARNINGS: { 'default': 'True', 'type': bool, 'arg': '--print-warnings' },
|
||||
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}'
|
||||
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}/{artist} - {song_name}.{ext}'
|
||||
OUTPUT_DEFAULT_SINGLE = '{artist}/{song_name}/{artist} - {song_name}.{ext}'
|
||||
OUTPUT_DEFAULT_ALBUM = '{artist}/{album}/{album_num} - {artist} - {song_name}.{ext}'
|
||||
|
||||
|
||||
|
@ -164,8 +164,8 @@ class Config:
|
|||
return root_podcast_path
|
||||
|
||||
@classmethod
|
||||
def get_skip_existing_files(cls) -> bool:
|
||||
return cls.get(SKIP_EXISTING_FILES)
|
||||
def get_skip_existing(cls) -> bool:
|
||||
return cls.get(SKIP_EXISTING)
|
||||
|
||||
@classmethod
|
||||
def get_skip_previously_downloaded(cls) -> bool:
|
||||
|
@ -183,17 +183,13 @@ class Config:
|
|||
def get_override_auto_wait(cls) -> bool:
|
||||
return cls.get(OVERRIDE_AUTO_WAIT)
|
||||
|
||||
@classmethod
|
||||
def get_force_premium(cls) -> bool:
|
||||
return cls.get(FORCE_PREMIUM)
|
||||
|
||||
@classmethod
|
||||
def get_download_format(cls) -> str:
|
||||
return cls.get(DOWNLOAD_FORMAT)
|
||||
|
||||
@classmethod
|
||||
def get_anti_ban_wait_time(cls) -> int:
|
||||
return cls.get(ANTI_BAN_WAIT_TIME)
|
||||
def get_bulk_wait_time(cls) -> int:
|
||||
return cls.get(BULK_WAIT_TIME)
|
||||
|
||||
@classmethod
|
||||
def get_language(cls) -> str:
|
||||
|
@ -204,25 +200,29 @@ class Config:
|
|||
return cls.get(DOWNLOAD_REAL_TIME)
|
||||
|
||||
@classmethod
|
||||
def get_bitrate(cls) -> str:
|
||||
return cls.get(BITRATE)
|
||||
def get_download_quality(cls) -> str:
|
||||
return cls.get(DOWNLOAD_QUALITY)
|
||||
|
||||
@classmethod
|
||||
def get_track_archive(cls) -> str:
|
||||
if cls.get(TRACK_ARCHIVE) == '':
|
||||
def get_transcode_bitrate(cls) -> str:
|
||||
return cls.get(TRANSCODE_BITRATE)
|
||||
|
||||
@classmethod
|
||||
def get_song_archive(cls) -> str:
|
||||
if cls.get(SONG_ARCHIVE) == '':
|
||||
system_paths = {
|
||||
'win32': Path.home() / 'AppData/Roaming/Zotify',
|
||||
'linux': Path.home() / '.local/share/zotify',
|
||||
'darwin': Path.home() / 'Library/Application Support/Zotify'
|
||||
}
|
||||
if sys.platform not in system_paths:
|
||||
track_archive = PurePath(Path.cwd() / '.zotify/track_archive')
|
||||
song_archive = PurePath(Path.cwd() / '.zotify/.song_archive')
|
||||
else:
|
||||
track_archive = PurePath(system_paths[sys.platform] / 'track_archive')
|
||||
song_archive = PurePath(system_paths[sys.platform] / '.song_archive')
|
||||
else:
|
||||
track_archive = PurePath(Path(cls.get(TRACK_ARCHIVE)).expanduser())
|
||||
Path(track_archive.parent).mkdir(parents=True, exist_ok=True)
|
||||
return track_archive
|
||||
song_archive = PurePath(Path(cls.get(SONG_ARCHIVE)).expanduser())
|
||||
Path(song_archive.parent).mkdir(parents=True, exist_ok=True)
|
||||
return song_archive
|
||||
|
||||
@classmethod
|
||||
def get_credentials_location(cls) -> str:
|
||||
|
|
|
@ -101,7 +101,7 @@ def download_episode(episode_id) -> None:
|
|||
if (
|
||||
Path(filepath).isfile()
|
||||
and Path(filepath).stat().st_size == total_size
|
||||
and Zotify.CONFIG.get_skip_existing_files()
|
||||
and Zotify.CONFIG.get_skip_existing()
|
||||
):
|
||||
Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###")
|
||||
prepare_download_loader.stop()
|
||||
|
|
|
@ -6,7 +6,7 @@ from typing import Any, Tuple, List
|
|||
|
||||
from librespot.audio.decoders import AudioQuality
|
||||
from librespot.metadata import TrackId
|
||||
from ffmpy import FFmpeg
|
||||
import ffmpy
|
||||
|
||||
from zotify.const import TRACKS, ALBUM, GENRES, 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, HREF
|
||||
|
@ -171,7 +171,7 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
|
|||
prepare_download_loader.stop()
|
||||
Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG IS UNAVAILABLE) ###' + "\n")
|
||||
else:
|
||||
if check_id and check_name and Zotify.CONFIG.get_skip_existing_files():
|
||||
if check_id and check_name and Zotify.CONFIG.get_skip_existing():
|
||||
prepare_download_loader.stop()
|
||||
Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG ALREADY EXISTS) ###' + "\n")
|
||||
|
||||
|
@ -231,8 +231,8 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
|
|||
if not check_id:
|
||||
add_to_directory_song_ids(filedir, scraped_song_id, PurePath(filename).name, artists[0], name)
|
||||
|
||||
if not Zotify.CONFIG.get_anti_ban_wait_time():
|
||||
time.sleep(Zotify.CONFIG.get_anti_ban_wait_time())
|
||||
if not Zotify.CONFIG.get_bulk_wait_time():
|
||||
time.sleep(Zotify.CONFIG.get_bulk_wait_time())
|
||||
except Exception as e:
|
||||
Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###')
|
||||
Printer.print(PrintChannel.ERRORS, 'Track_ID: ' + str(track_id))
|
||||
|
@ -255,12 +255,14 @@ def convert_audio_format(filename) -> None:
|
|||
download_format = Zotify.CONFIG.get_download_format().lower()
|
||||
file_codec = CODEC_MAP.get(download_format, 'copy')
|
||||
if file_codec != 'copy':
|
||||
bitrate = Zotify.CONFIG.get_bitrate()
|
||||
if not bitrate:
|
||||
if Zotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH:
|
||||
bitrate = '320k'
|
||||
else:
|
||||
bitrate = '160k'
|
||||
bitrate = Zotify.CONFIG.get_transcode_bitrate()
|
||||
bitrates = {
|
||||
'auto': '320k' if Zotify.check_premium() else '160k',
|
||||
'normal': '96k',
|
||||
'high': '160k',
|
||||
'very_high': '320k'
|
||||
}
|
||||
bitrate = bitrates[Zotify.CONFIG.get_download_quality()]
|
||||
else:
|
||||
bitrate = None
|
||||
|
||||
|
@ -268,14 +270,17 @@ def convert_audio_format(filename) -> None:
|
|||
if bitrate:
|
||||
output_params += ['-b:a', bitrate]
|
||||
|
||||
ff_m = FFmpeg(
|
||||
global_options=['-y', '-hide_banner', '-loglevel error'],
|
||||
inputs={temp_filename: None},
|
||||
outputs={filename: output_params}
|
||||
)
|
||||
try:
|
||||
ff_m = ffmpy.FFmpeg(
|
||||
global_options=['-y', '-hide_banner', '-loglevel error'],
|
||||
inputs={temp_filename: None},
|
||||
outputs={filename: output_params}
|
||||
)
|
||||
with Loader(PrintChannel.PROGRESS_INFO, "Converting file..."):
|
||||
ff_m.run()
|
||||
|
||||
with Loader(PrintChannel.PROGRESS_INFO, "Converting file..."):
|
||||
ff_m.run()
|
||||
if Path(temp_filename).exists():
|
||||
Path(temp_filename).unlink()
|
||||
|
||||
if Path(temp_filename).exists():
|
||||
Path(temp_filename).unlink()
|
||||
except ffmpy.FFExecutableNotFoundError:
|
||||
Printer.print(PrintChannel.ERRORS, f'### SKIPPING {file_codec.upper()} CONVERSION - FFMPEG NOT FOUND ###')
|
|
@ -36,7 +36,7 @@ def get_previously_downloaded() -> List[str]:
|
|||
""" Returns list of all time downloaded songs """
|
||||
|
||||
ids = []
|
||||
archive_path = Zotify.CONFIG.get_track_archive()
|
||||
archive_path = Zotify.CONFIG.get_song_archive()
|
||||
|
||||
if Path(archive_path).exists():
|
||||
with open(archive_path, 'r', encoding='utf-8') as f:
|
||||
|
@ -48,7 +48,7 @@ def get_previously_downloaded() -> List[str]:
|
|||
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 = Zotify.CONFIG.get_track_archive()
|
||||
archive_path = Zotify.CONFIG.get_song_archive()
|
||||
|
||||
if Path(archive_path).exists():
|
||||
with open(archive_path, 'a', encoding='utf-8') as file:
|
||||
|
|
|
@ -97,4 +97,4 @@ class Zotify:
|
|||
@classmethod
|
||||
def check_premium(cls) -> bool:
|
||||
""" If user has spotify premium return true """
|
||||
return (cls.SESSION.get_user_attribute(TYPE) == PREMIUM) or cls.CONFIG.get_force_premium()
|
||||
return (cls.SESSION.get_user_attribute(TYPE) == PREMIUM)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue