Zotify 0.6

This commit is contained in:
logykk 2022-02-15 19:07:45 +13:00
parent d8c17e2ce9
commit 1e48861df3
9 changed files with 136 additions and 102 deletions

View File

@ -1,19 +1,25 @@
# Changelog # Changelog
## v0.6 ## v0.6
**General changes** **General changes**
- Switched from os.path to pathlib - Added "DOWNLOAD_QUALITY" config option. This can be "normal" (96kbks), "high" (160kpbs), "very-high" (320kpbs, premium only) or "auto" which selects the highest format available for your account automatically.
- Renamed .song_archive to track_archive - The "FORCE_PREMIUM" option has been removed, the same result can be achieved with `--download-quality="very-high"`.
- The "BITRATE" option has been renamed "TRANSCODE_BITRATE" as it now only effects transcodes
- FFmpeg is now semi-optional, not having it installed means you are limited to saving music as ogg vorbis.
- Zotify can now be installed with `pip install https://gitlab.com/team-zotify/zotify/-/archive/main/zotify-main.zip` - Zotify can now be installed with `pip install https://gitlab.com/team-zotify/zotify/-/archive/main/zotify-main.zip`
- Zotify can be ran from any directory with `zotify [args]`, you no longer need to prefix "python" in the command. - Zotify can be ran from any directory with `zotify [args]`, you no longer need to prefix "python" in the command.
- The -s option now takes search input as a command argument, it will still promt you if no search is given. - The -s option now takes search input as a command argument, it will still promt you if no search is given.
- The -ls/--liked-songs option has been shrotened to -l/--liked, - The -ls/--liked-songs option has been shrotened to -l/--liked,
- Singles are now stored in their own folders under the artist folder
- Fixed default config not loading on first run
- Now shows asterisks when entering password
- Switched from os.path to pathlib
- New default config locations: - New default config locations:
- Windows: `%AppData%\Roaming\Zotify\config.json` - Windows: `%AppData%\Roaming\Zotify\config.json`
- Linux: `~/.config/zotify/config.json` - Linux: `~/.config/zotify/config.json`
- macOS: `~/Library/Application Support/Zotify/config.json` - macOS: `~/Library/Application Support/Zotify/config.json`
- Other/Undetected: `.zotify/config.json` - Other/Undetected: `.zotify/config.json`
- You can still use `--config-location` to specify a different location. - You can still use `--config-location` to specify a different location.
- New default config locations: - New default credential locations:
- Windows: `%AppData%\Roaming\Zotify\credentials.json` - Windows: `%AppData%\Roaming\Zotify\credentials.json`
- Linux: `~/.local/share/zotify/credentials.json` - Linux: `~/.local/share/zotify/credentials.json`
- macOS: `~/Library/Application Support/Zotify/credentials.json` - macOS: `~/Library/Application Support/Zotify/credentials.json`
@ -24,9 +30,6 @@
- Linux & macOS: `~/Music/Zotify Music/` & `~/Music/Zotify Podcasts/` - Linux & macOS: `~/Music/Zotify Music/` & `~/Music/Zotify Podcasts/`
- Other/Undetected: `./Zotify Music/` & `./Zotify Podcasts/` - Other/Undetected: `./Zotify Music/` & `./Zotify Podcasts/`
- You can still use `--root-path` and `--root-podcast-path` respectively to specify a differnt location - You can still use `--root-path` and `--root-podcast-path` respectively to specify a differnt location
- Singles are now stored in their own folders
- Fixed default config not loading on first run
- Now shows asterisks when entering password
**Docker** **Docker**
- Dockerfile is currently broken, it will be fixed soon. \ - Dockerfile is currently broken, it will be fixed soon. \

View File

@ -1,6 +1,6 @@
# Zotify # Zotify
### A music and podcast downloader needing only a python interpreter and ffmpeg. ### A highly customizable music and podcast downloader.
<p align="center"> <p align="center">
<img src="https://i.imgur.com/hGXQWSl.png" width="50%"> <img src="https://i.imgur.com/hGXQWSl.png" width="50%">
@ -8,20 +8,33 @@
[Discord Server](https://discord.gg/XDYsFRTUjE) [Discord Server](https://discord.gg/XDYsFRTUjE)
### Featues
- Downloads at up to 320kbps*
- Downloads directly from the source**
- Downloads podcasts, playlists, liked songs, albums, artists, singles.
- Option to download in real time to appear more legitimate***
- Supports multiple audio formats
- Download directly from URL or use built-in in search
- Bulk downloads from a list of URLs in a text file or parsed directly as arguments
*Free accounts are limited to 160kbps. \
**Audio files are NOT substituted with ones from other sources such as YouTube or Deezer, they are sourced directly. \
***'real time' refers to downloading at the speed it would normally be streamed at (the duration of the track).
### Install ### Install
``` ```
Dependencies: Dependencies:
- Python 3.9 or greater - Python 3.9 or greater
- ffmpeg* - FFmpeg*
Installation: Installation:
python -m pip install https://gitlab.com/team-zotify/zotify/-/archive/main/zotify-main.zip python -m pip install https://gitlab.com/team-zotify/zotify/-/archive/main/zotify-main.zip
``` ```
\*Windows users can download the binaries from [ffmpeg.org](https://ffmpeg.org) and add them to %PATH%. Mac users can install it via [Homebrew](https://brew.sh) by running `brew install ffmpeg`. Linux users should already know how to install ffmpeg, I don't want to add instructions for every package manager. \*Zotify will work without FFmpeg but transcoding will be unavailable.
### Command line usage ### Command line usage
@ -44,29 +57,34 @@ Be aware you have to set boolean values in the commandline like this: `--downloa
| Key (config) | commandline parameter | Description | Key (config) | commandline parameter | Description
|------------------------------|----------------------------------|---------------------------------------------------------------------| |------------------------------|----------------------------------|---------------------------------------------------------------------|
| ROOT_PATH | --root-path | directory where Zotify saves music
| ROOT_PODCAST_PATH | --root-podcast-path | directory where Zotify saves podcasts
| SKIP_EXISTING_FILES | --skip-existing-files | Skip songs with the same name
| SKIP_PREVIOUSLY_DOWNLOADED | --skip-previously-downloaded | Use a song_archive file to skip previously downloaded songs
| DOWNLOAD_FORMAT | --download-format | The download audio format (aac, fdk_aac, m4a, mp3, ogg, opus, vorbis)
| FORCE_PREMIUM | --force-premium | Force the use of high quality downloads (only with premium accounts)
| ANTI_BAN_WAIT_TIME | --anti-ban-wait-time | The wait time between bulk downloads
| OVERRIDE_AUTO_WAIT | --override-auto-wait | Totally disable wait time between songs with the risk of instability
| CHUNK_SIZE | --chunk-size | Chunk size for downloading
| SPLIT_ALBUM_DISCS | --split-album-discs | Saves each disk in its own folder
| DOWNLOAD_REAL_TIME | --download-real-time | Downloads songs as fast as they would be played, should prevent account bans.
| LANGUAGE | --language | Language for spotify metadata
| BITRATE | --bitrate | Overwrite the bitrate for ffmpeg encoding
| SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED
| CREDENTIALS_LOCATION | --credentials-location | The location of the credentials.json | CREDENTIALS_LOCATION | --credentials-location | The location of the credentials.json
| OUTPUT | --output | The output location/format (see below) | OUTPUT | --output | The output location/format (see below)
| PRINT_SPLASH | --print-splash | Print the splash message | SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED
| PRINT_SKIPS | --print-skips | Print messages if a song is being skipped | ROOT_PATH | --root-path | Directory where Zotify saves music
| PRINT_DOWNLOAD_PROGRESS | --print-download-progress | Print the download/playlist progress bars | ROOT_PODCAST_PATH | --root-podcast-path | Directory where Zotify saves podcasts
| PRINT_ERRORS | --print-errors | Print errors | SPLIT_ALBUM_DISCS | --split-album-discs | Saves each disk in its own folder
| MD_ALLGENRES | --md-allgenres | Save all relevant genres in metadata
| MD_GENREDELIMITER | --md-genredelimiter | Delimiter character used to split genres in metadata
| DOWNLOAD_FORMAT | --download-format | The download audio format (aac, fdk_aac, m4a, mp3, ogg, opus, vorbis)
| DOWNLOAD_QUALITY | --download-quality | Audio quality of downloaded songs (normal, high, very-high*)
| TRANSCODE_BITRATE | --transcode-bitrate | Overwrite the bitrate for ffmpeg encoding
| SKIP_EXISTING_FILES | --skip-existing-files | Skip songs with the same name
| SKIP_PREVIOUSLY_DOWNLOADED | --skip-previously-downloaded | Use a song_archive file to skip previously downloaded songs
| RETRY_ATTEMPTS | --retry-attempts | Number of times Zotify will retry a failed request
| BULK_WAIT_TIME | --bulk-wait-time | The wait time between bulk downloads
| OVERRIDE_AUTO_WAIT | --override-auto-wait | Totally disable wait time between songs with the risk of instability
| CHUNK_SIZE | --chunk-size | Chunk size for downloading
| DOWNLOAD_REAL_TIME | --download-real-time | Downloads songs as fast as they would be played, should prevent account bans.
| LANGUAGE | --language | Language for spotify metadata
| PRINT_SPLASH | --print-splash | Show the Zotify logo at startup
| PRINT_SKIPS | --print-skips | Show messages if a song is being skipped
| PRINT_DOWNLOAD_PROGRESS | --print-download-progress | Show download/playlist progress bars
| PRINT_ERRORS | --print-errors | Show errors
| PRINT_DOWNLOADS | --print-downloads | Print messages when a song is finished downloading | PRINT_DOWNLOADS | --print-downloads | Print messages when a song is finished downloading
| TEMP_DOWNLOAD_DIR | --temp-download-dir | Download tracks to a temporary directory first | TEMP_DOWNLOAD_DIR | --temp-download-dir | Download tracks to a temporary directory first
*very-high is limited to premium only
### Output format ### Output format
With the option `OUTPUT` (or the commandline parameter `--output`) you can specify the output location and format. With the option `OUTPUT` (or the commandline parameter `--output`) you can specify the output location and format.
@ -106,6 +124,11 @@ Create and run a container from the image:
docker run --rm -u $(id -u):$(id -g) -v "$PWD/zotify:/app" -v "$PWD/config.json:/config.json" -v "$PWD/Zotify Music:/Zotify Music" -v "$PWD/Zotify Podcasts:/Zotify Podcasts" -it zotify docker run --rm -u $(id -u):$(id -g) -v "$PWD/zotify:/app" -v "$PWD/config.json:/config.json" -v "$PWD/Zotify Music:/Zotify Music" -v "$PWD/Zotify Podcasts:/Zotify Podcasts" -it zotify
``` ```
### What do I do if I see "Your session has been terminated"?
If you see this, don't worry! Just try logging back in. If you see the incorrect username or password error, reset your password and you should be able to log back in.
### Will my account get banned if I use this tool? ### Will my account get banned if I use this tool?
Currently no user has reported their account getting banned after using Zotify. Currently no user has reported their account getting banned after using Zotify.
@ -114,11 +137,9 @@ It is recommended you use Zotify with a burner account.
Alternatively, there is a configuration option labled ```DOWNLOAD_REAL_TIME```, this limits the download speed to the duration of the song being downloaded thus appearing less suspicious. Alternatively, there is a configuration option labled ```DOWNLOAD_REAL_TIME```, this limits the download speed to the duration of the song being downloaded thus appearing less suspicious.
This option is much slower and is only recommended for premium users who wish to download songs in 320kbps without buying premium on a burner account. This option is much slower and is only recommended for premium users who wish to download songs in 320kbps without buying premium on a burner account.
**Use Zotify at your own risk**, the developers of Zotify are not responsible if your account gets banned. ### Disclaimer
Zotify is intended to be used in compliance with DMCA, Section 1201, for educational, private and fair use. \
### What do I do if I see "Your session has been terminated"? Zotify contributors are not responsible for any misuse of the program or source code.
If you see this, don't worry! Just try logging back in. If you see the incorrect username or password error, reset your password and you should be able to log back in.
### Contributing ### Contributing

View File

@ -12,7 +12,7 @@ from zotify.config import CONFIG_VALUES
def main(): def main():
parser = argparse.ArgumentParser(prog='zotify', 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', parser.add_argument('-ns', '--no-splash',
action='store_true', action='store_true',
help='Suppress the splash screen when loading.') help='Suppress the splash screen when loading.')

View File

@ -5,6 +5,7 @@ from pathlib import Path
from zotify.album import download_album, download_artist_albums from zotify.album import download_album, download_artist_albums
from zotify.const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \ from zotify.const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \
OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME, TYPE 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.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.podcast import download_episode, get_show_episodes
from zotify.termoutput import Printer, PrintChannel from zotify.termoutput import Printer, PrintChannel
@ -17,16 +18,20 @@ SEARCH_URL = 'https://api.spotify.com/v1/search'
def client(args) -> None: def client(args) -> None:
""" Connects to download server to perform query's and get songs to download """ """ 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) Zotify(args)
prepare_download_loader.stop()
Printer.print(PrintChannel.SPLASH, splash()) Printer.print(PrintChannel.SPLASH, splash())
if Zotify.check_premium(): quality_options = {
Printer.print(PrintChannel.WARNINGS, '[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n') 'auto': AudioQuality.VERY_HIGH if Zotify.check_premium() else AudioQuality.HIGH,
Zotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH 'normal': AudioQuality.NORMAL,
else: 'high': AudioQuality.HIGH,
Printer.print(PrintChannel.WARNINGS, '[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n') 'very_high': AudioQuality.VERY_HIGH
Zotify.DOWNLOAD_QUALITY = AudioQuality.HIGH }
Zotify.DOWNLOAD_QUALITY = quality_options[Zotify.CONFIG.get_download_quality()]
if args.download: if args.download:
urls = [] urls = []

View File

@ -6,18 +6,18 @@ from typing import Any
ROOT_PATH = 'ROOT_PATH' ROOT_PATH = 'ROOT_PATH'
ROOT_PODCAST_PATH = 'ROOT_PODCAST_PATH' ROOT_PODCAST_PATH = 'ROOT_PODCAST_PATH'
SKIP_EXISTING_FILES = 'SKIP_EXISTING_FILES' SKIP_EXISTING = 'SKIP_EXISTING'
SKIP_PREVIOUSLY_DOWNLOADED = 'SKIP_PREVIOUSLY_DOWNLOADED' SKIP_PREVIOUSLY_DOWNLOADED = 'SKIP_PREVIOUSLY_DOWNLOADED'
DOWNLOAD_FORMAT = 'DOWNLOAD_FORMAT' DOWNLOAD_FORMAT = 'DOWNLOAD_FORMAT'
FORCE_PREMIUM = 'FORCE_PREMIUM' BULK_WAIT_TIME = 'BULK_WAIT_TIME'
ANTI_BAN_WAIT_TIME = 'ANTI_BAN_WAIT_TIME'
OVERRIDE_AUTO_WAIT = 'OVERRIDE_AUTO_WAIT' OVERRIDE_AUTO_WAIT = 'OVERRIDE_AUTO_WAIT'
CHUNK_SIZE = 'CHUNK_SIZE' CHUNK_SIZE = 'CHUNK_SIZE'
SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS'
DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME'
LANGUAGE = 'LANGUAGE' LANGUAGE = 'LANGUAGE'
BITRATE = 'BITRATE' DOWNLOAD_QUALITY = 'DOWNLOAD_QUALITY'
TRACK_ARCHIVE = 'TRACK_ARCHIVE' TRANSCODE_BITRATE = 'TRANSCODE_BITRATE'
SONG_ARCHIVE = 'SONG_ARCHIVE'
CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION' CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION'
OUTPUT = 'OUTPUT' OUTPUT = 'OUTPUT'
PRINT_SPLASH = 'PRINT_SPLASH' PRINT_SPLASH = 'PRINT_SPLASH'
@ -35,23 +35,25 @@ RETRY_ATTEMPTS = 'RETRY_ATTEMPTS'
CONFIG_VERSION = 'CONFIG_VERSION' CONFIG_VERSION = 'CONFIG_VERSION'
CONFIG_VALUES = { 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' }, CREDENTIALS_LOCATION: { 'default': '', 'type': str, 'arg': '--credentials-location' },
OUTPUT: { 'default': '', 'type': str, 'arg': '--output' }, 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_SPLASH: { 'default': 'False', 'type': bool, 'arg': '--print-splash' },
PRINT_SKIPS: { 'default': 'True', 'type': bool, 'arg': '--print-skips' }, PRINT_SKIPS: { 'default': 'True', 'type': bool, 'arg': '--print-skips' },
PRINT_DOWNLOAD_PROGRESS: { 'default': 'True', 'type': bool, 'arg': '--print-download-progress' }, 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_API_ERRORS: { 'default': 'False', 'type': bool, 'arg': '--print-api-errors' },
PRINT_PROGRESS_INFO: { 'default': 'True', 'type': bool, 'arg': '--print-progress-info' }, PRINT_PROGRESS_INFO: { 'default': 'True', 'type': bool, 'arg': '--print-progress-info' },
PRINT_WARNINGS: { 'default': 'True', 'type': bool, 'arg': '--print-warnings' }, 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' } TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' }
} }
OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}' OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}'
OUTPUT_DEFAULT_PLAYLIST_EXT = '{playlist}/{playlist_num} - {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_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}' OUTPUT_DEFAULT_ALBUM = '{artist}/{album}/{album_num} - {artist} - {song_name}.{ext}'
@ -164,8 +164,8 @@ class Config:
return root_podcast_path return root_podcast_path
@classmethod @classmethod
def get_skip_existing_files(cls) -> bool: def get_skip_existing(cls) -> bool:
return cls.get(SKIP_EXISTING_FILES) return cls.get(SKIP_EXISTING)
@classmethod @classmethod
def get_skip_previously_downloaded(cls) -> bool: def get_skip_previously_downloaded(cls) -> bool:
@ -183,17 +183,13 @@ class Config:
def get_override_auto_wait(cls) -> bool: def get_override_auto_wait(cls) -> bool:
return cls.get(OVERRIDE_AUTO_WAIT) return cls.get(OVERRIDE_AUTO_WAIT)
@classmethod
def get_force_premium(cls) -> bool:
return cls.get(FORCE_PREMIUM)
@classmethod @classmethod
def get_download_format(cls) -> str: def get_download_format(cls) -> str:
return cls.get(DOWNLOAD_FORMAT) return cls.get(DOWNLOAD_FORMAT)
@classmethod @classmethod
def get_anti_ban_wait_time(cls) -> int: def get_bulk_wait_time(cls) -> int:
return cls.get(ANTI_BAN_WAIT_TIME) return cls.get(BULK_WAIT_TIME)
@classmethod @classmethod
def get_language(cls) -> str: def get_language(cls) -> str:
@ -204,25 +200,29 @@ class Config:
return cls.get(DOWNLOAD_REAL_TIME) return cls.get(DOWNLOAD_REAL_TIME)
@classmethod @classmethod
def get_bitrate(cls) -> str: def get_download_quality(cls) -> str:
return cls.get(BITRATE) return cls.get(DOWNLOAD_QUALITY)
@classmethod @classmethod
def get_track_archive(cls) -> str: def get_transcode_bitrate(cls) -> str:
if cls.get(TRACK_ARCHIVE) == '': return cls.get(TRANSCODE_BITRATE)
@classmethod
def get_song_archive(cls) -> str:
if cls.get(SONG_ARCHIVE) == '':
system_paths = { system_paths = {
'win32': Path.home() / 'AppData/Roaming/Zotify', 'win32': Path.home() / 'AppData/Roaming/Zotify',
'linux': Path.home() / '.local/share/zotify', 'linux': Path.home() / '.local/share/zotify',
'darwin': Path.home() / 'Library/Application Support/Zotify' 'darwin': Path.home() / 'Library/Application Support/Zotify'
} }
if sys.platform not in system_paths: if sys.platform not in system_paths:
track_archive = PurePath(Path.cwd() / '.zotify/track_archive') song_archive = PurePath(Path.cwd() / '.zotify/.song_archive')
else: else:
track_archive = PurePath(system_paths[sys.platform] / 'track_archive') song_archive = PurePath(system_paths[sys.platform] / '.song_archive')
else: else:
track_archive = PurePath(Path(cls.get(TRACK_ARCHIVE)).expanduser()) song_archive = PurePath(Path(cls.get(SONG_ARCHIVE)).expanduser())
Path(track_archive.parent).mkdir(parents=True, exist_ok=True) Path(song_archive.parent).mkdir(parents=True, exist_ok=True)
return track_archive return song_archive
@classmethod @classmethod
def get_credentials_location(cls) -> str: def get_credentials_location(cls) -> str:

View File

@ -101,7 +101,7 @@ def download_episode(episode_id) -> None:
if ( if (
Path(filepath).isfile() Path(filepath).isfile()
and Path(filepath).stat().st_size == total_size 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) ###") Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###")
prepare_download_loader.stop() prepare_download_loader.stop()

View File

@ -6,7 +6,7 @@ from typing import Any, Tuple, List
from librespot.audio.decoders import AudioQuality from librespot.audio.decoders import AudioQuality
from librespot.metadata import TrackId 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, \ 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 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() prepare_download_loader.stop()
Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG IS UNAVAILABLE) ###' + "\n") Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG IS UNAVAILABLE) ###' + "\n")
else: 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() prepare_download_loader.stop()
Printer.print(PrintChannel.SKIPS, '\n### SKIPPING: ' + song_name + ' (SONG ALREADY EXISTS) ###' + "\n") 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: if not check_id:
add_to_directory_song_ids(filedir, scraped_song_id, PurePath(filename).name, artists[0], name) add_to_directory_song_ids(filedir, scraped_song_id, PurePath(filename).name, artists[0], name)
if not Zotify.CONFIG.get_anti_ban_wait_time(): if not Zotify.CONFIG.get_bulk_wait_time():
time.sleep(Zotify.CONFIG.get_anti_ban_wait_time()) time.sleep(Zotify.CONFIG.get_bulk_wait_time())
except Exception as e: except Exception as e:
Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###') Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###')
Printer.print(PrintChannel.ERRORS, 'Track_ID: ' + str(track_id)) 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() download_format = Zotify.CONFIG.get_download_format().lower()
file_codec = CODEC_MAP.get(download_format, 'copy') file_codec = CODEC_MAP.get(download_format, 'copy')
if file_codec != 'copy': if file_codec != 'copy':
bitrate = Zotify.CONFIG.get_bitrate() bitrate = Zotify.CONFIG.get_transcode_bitrate()
if not bitrate: bitrates = {
if Zotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH: 'auto': '320k' if Zotify.check_premium() else '160k',
bitrate = '320k' 'normal': '96k',
else: 'high': '160k',
bitrate = '160k' 'very_high': '320k'
}
bitrate = bitrates[Zotify.CONFIG.get_download_quality()]
else: else:
bitrate = None bitrate = None
@ -268,14 +270,17 @@ def convert_audio_format(filename) -> None:
if bitrate: if bitrate:
output_params += ['-b:a', bitrate] output_params += ['-b:a', bitrate]
ff_m = FFmpeg( try:
global_options=['-y', '-hide_banner', '-loglevel error'], ff_m = ffmpy.FFmpeg(
inputs={temp_filename: None}, global_options=['-y', '-hide_banner', '-loglevel error'],
outputs={filename: output_params} 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..."): if Path(temp_filename).exists():
ff_m.run() Path(temp_filename).unlink()
if Path(temp_filename).exists(): except ffmpy.FFExecutableNotFoundError:
Path(temp_filename).unlink() Printer.print(PrintChannel.ERRORS, f'### SKIPPING {file_codec.upper()} CONVERSION - FFMPEG NOT FOUND ###')

View File

@ -36,7 +36,7 @@ def get_previously_downloaded() -> List[str]:
""" Returns list of all time downloaded songs """ """ Returns list of all time downloaded songs """
ids = [] ids = []
archive_path = Zotify.CONFIG.get_track_archive() archive_path = Zotify.CONFIG.get_song_archive()
if Path(archive_path).exists(): if Path(archive_path).exists():
with open(archive_path, 'r', encoding='utf-8') as f: 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: 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 """ """ 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(): if Path(archive_path).exists():
with open(archive_path, 'a', encoding='utf-8') as file: with open(archive_path, 'a', encoding='utf-8') as file:

View File

@ -97,4 +97,4 @@ class Zotify:
@classmethod @classmethod
def check_premium(cls) -> bool: def check_premium(cls) -> bool:
""" If user has spotify premium return true """ """ 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)