Zotify 0.6 RC1

This commit is contained in:
logykk 2022-02-12 20:48:27 +13:00
parent 3d50d8f141
commit d8c17e2ce9
11 changed files with 161 additions and 117 deletions

View File

@ -2,14 +2,31 @@
## v0.6 ## v0.6
**General changes** **General changes**
- Switched from os.path to pathlib - Switched from os.path to pathlib
- Zotify can now be installed with pip - - Renamed .song_archive to track_archive
`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 -ls/--liked-songs option has been shrotened to -l/--liked,
- 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`
- You can still use `--config-location` to specify a local config file. - Other/Undetected: `.zotify/config.json`
- You can still use `--config-location` to specify a different location.
- New default config locations:
- Windows: `%AppData%\Roaming\Zotify\credentials.json`
- Linux: `~/.local/share/zotify/credentials.json`
- macOS: `~/Library/Application Support/Zotify/credentials.json`
- Other/Undetected: `.zotify/credentials.json`
- You can still use `--credentials-location` to specify a different file.
- New default music and podcast locations:
- Windows: `C:\Users\<user>\Music\Zotify Music\` & `C:\Users\<user>\Music\Zotify Podcasts\`
- Linux & macOS: `~/Music/Zotify Music/` & `~/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
- 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

@ -3,66 +3,58 @@
### A music and podcast downloader needing only a python interpreter and ffmpeg. ### A music and podcast downloader needing only a python interpreter and ffmpeg.
<p align="center"> <p align="center">
<img src="https://i.imgur.com/hGXQWSl.png"> <img src="https://i.imgur.com/hGXQWSl.png" width="50%">
</p> </p>
[Discord Server](https://discord.gg/XDYsFRTUjE) - [NotABug Mirror](https://notabug.org/Zotify/zotify) [Discord Server](https://discord.gg/XDYsFRTUjE)
### Install
``` ```
Requirements: Dependencies:
Binaries
- Python 3.9 or greater - Python 3.9 or greater
- ffmpeg* - ffmpeg*
- Git**
Python packages: Installation:
- pip install -r requirements.txt
python -m pip install https://gitlab.com/team-zotify/zotify/-/archive/main/zotify-main.zip
``` ```
\*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can install it with [Homebrew](https://brew.sh) by running `brew install ffmpeg`. \*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.
\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ### Command line usage
### Command line usage:
``` ```
Basic command line usage: Basic command line usage:
zotify <track/album/playlist/episode/artist url> Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. Can take multiple urls. zotify <track/album/playlist/episode/artist url> Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. Can take multiple urls.
Different usage modes: Basic options:
(nothing) Download the tracks/alumbs/playlists URLs from the parameter (nothing) Download the tracks/alumbs/playlists URLs from the parameter
-d, --download Download all tracks/alumbs/playlists URLs from the specified file -d, --download Download all tracks/alumbs/playlists URLs from the specified file
-p, --playlist Downloads a saved playlist from your account -p, --playlist Downloads a saved playlist from your account
-ls, --liked-songs Downloads all the liked songs from your account -l, --liked Downloads all the liked songs from your account
-s, --search Loads search prompt to find then download a specific track, album or playlist -s, --search Searches for specified track, album, artist or playlist, loads search prompt if none are given.
Extra command line options:
-ns, --no-splash Suppress the splash screen when loading.
--config-location Use a different config.json.
``` ```
### Options: ### Options
All these options can either be configured in the config or via the commandline, in case of both the commandline-option has higher priority. All these options can either be configured in the config or via the commandline, in case of both the commandline-option has higher priority.
Be aware you have to set boolean values in the commandline like this: `--download-real-time=True` Be aware you have to set boolean values in the commandline like this: `--download-real-time=True`
| Key (config) | commandline parameter | Description | Key (config) | commandline parameter | Description
|------------------------------|----------------------------------|---------------------------------------------------------------------| |------------------------------|----------------------------------|---------------------------------------------------------------------|
| ROOT_PATH | --root-path | directory where Zotify saves the music | ROOT_PATH | --root-path | directory where Zotify saves music
| ROOT_PODCAST_PATH | --root-podcast-path | directory where Zotify saves the podcasts | ROOT_PODCAST_PATH | --root-podcast-path | directory where Zotify saves podcasts
| SKIP_EXISTING_FILES | --skip-existing-files | Skip songs with the same name | SKIP_EXISTING_FILES | --skip-existing-files | Skip songs with the same name
| SKIP_PREVIOUSLY_DOWNLOADED | --skip-previously-downloaded | Create a .song_archive file and skip previously downloaded songs | 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) | 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) | 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 | 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 | 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 | CHUNK_SIZE | --chunk-size | Chunk size for downloading
| SPLIT_ALBUM_DISCS | --split-album-discs | split downloaded albums by disc | SPLIT_ALBUM_DISCS | --split-album-discs | Saves each disk in its own folder
| DOWNLOAD_REAL_TIME | --download-real-time | only downloads songs as fast as they would be played, can prevent account bans | 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 | LANGUAGE | --language | Language for spotify metadata
| BITRATE | --bitrate | Overwrite the bitrate for ffmpeg encoding | BITRATE | --bitrate | Overwrite the bitrate for ffmpeg encoding
| SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED | SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED
@ -75,7 +67,7 @@ Be aware you have to set boolean values in the commandline like this: `--downloa
| 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
### 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.
The value is relative to the `ROOT_PATH`/`ROOT_PODCAST_PATH` directory and can contain the following placeholder: The value is relative to the `ROOT_PATH`/`ROOT_PODCAST_PATH` directory and can contain the following placeholder:
@ -100,7 +92,7 @@ Example values could be:
~~~~ ~~~~
{playlist}/{artist} - {song_name}.{ext} {playlist}/{artist} - {song_name}.{ext}
{playlist}/{playlist_num} - {artist} - {song_name}.{ext} {playlist}/{playlist_num} - {artist} - {song_name}.{ext}
Liked Songs/{artist} - {song_name}.{ext} Bangers/{artist} - {song_name}.{ext}
{artist} - {song_name}.{ext} {artist} - {song_name}.{ext}
{artist}/{album}/{album_num} - {artist} - {song_name}.{ext} {artist}/{album}/{album_num} - {artist} - {song_name}.{ext}
/home/user/downloads/{artist} - {song_name} [{id}].{ext} /home/user/downloads/{artist} - {song_name} [{id}].{ext}
@ -135,7 +127,3 @@ Please refer to [CONTRIBUTING](CONTRIBUTING.md)
### Changelog ### Changelog
Please refer to [CHANGELOG](CHANGELOG.md) Please refer to [CHANGELOG](CHANGELOG.md)
### Common Errors
Please refer to [COMMON_ERRORS](COMMON_ERRORS.md)

View File

@ -3,5 +3,6 @@ https://github.com/kokarare1212/librespot-python/archive/refs/heads/rewrite.zip
music_tag music_tag
Pillow Pillow
protobuf protobuf
pwinput
tabulate tabulate
tqdm tqdm

View File

@ -1,10 +1,10 @@
import pathlib from pathlib import Path
from distutils.core import setup from distutils.core import setup
from setuptools import setup, find_packages from setuptools import setup, find_packages
# The directory containing this file # The directory containing this file
HERE = pathlib.Path(__file__).parent HERE = Path(__file__).parent
# The text of the README file # The text of the README file
README = (HERE / "README.md").read_text() README = (HERE / "README.md").read_text()
@ -13,7 +13,7 @@ README = (HERE / "README.md").read_text()
setup( setup(
name="zotify", name="zotify",
version="0.6.0", version="0.6.0",
author="Zotify", author="Zotify Contributors",
description="A music and podcast downloader.", description="A music and podcast downloader.",
long_description=README, long_description=README,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@ -30,6 +30,6 @@ setup(
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
], ],
install_requires=['ffmpy', 'music_tag', 'Pillow', 'protobuf', 'tabulate', 'tqdm', install_requires=['ffmpy', 'music_tag', 'Pillow', 'protobuf', 'pwinput', 'tabulate', 'tqdm',
'librespot @ https://github.com/kokarare1212/librespot-python/archive/refs/heads/rewrite.zip'], 'librespot @ https://github.com/kokarare1212/librespot-python/archive/refs/heads/rewrite.zip'],
) )

View File

@ -18,7 +18,7 @@ def main():
help='Suppress the splash screen when loading.') help='Suppress the splash screen when loading.')
parser.add_argument('--config-location', parser.add_argument('--config-location',
type=str, type=str,
help='Specify the json config location') help='Specify the zconfig.json location')
group = parser.add_mutually_exclusive_group(required=True) group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('urls', group.add_argument('urls',
type=str, type=str,
@ -26,7 +26,7 @@ def main():
default='', default='',
nargs='*', nargs='*',
help='Downloads the track, album, playlist, podcast episode, or all albums by an artist from a url. Can take multiple urls.') help='Downloads the track, album, playlist, podcast episode, or all albums by an artist from a url. Can take multiple urls.')
group.add_argument('-ls', '--liked-songs', group.add_argument('-l', '--liked',
dest='liked_songs', dest='liked_songs',
action='store_true', action='store_true',
help='Downloads all the liked songs from your account.') help='Downloads all the liked songs from your account.')
@ -34,8 +34,9 @@ def main():
action='store_true', action='store_true',
help='Downloads a saved playlist from your account.') help='Downloads a saved playlist from your account.')
group.add_argument('-s', '--search', group.add_argument('-s', '--search',
dest='search_spotify', type=str,
action='store_true', nargs='?',
const=' ',
help='Loads search prompt to find then download a specific track, album or playlist') help='Loads search prompt to find then download a specific track, album or playlist')
group.add_argument('-d', '--download', group.add_argument('-d', '--download',
type=str, type=str,
@ -52,5 +53,6 @@ def main():
args = parser.parse_args() args = parser.parse_args()
args.func(args) args.func(args)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,6 +1,5 @@
from librespot.audio.decoders import AudioQuality from librespot.audio.decoders import AudioQuality
from tabulate import tabulate from tabulate import tabulate
#import os
from pathlib import Path from pathlib import Path
from zotify.album import download_album, download_artist_albums from zotify.album import download_album, download_artist_albums
@ -23,10 +22,10 @@ def client(args) -> None:
Printer.print(PrintChannel.SPLASH, splash()) Printer.print(PrintChannel.SPLASH, splash())
if Zotify.check_premium(): if Zotify.check_premium():
Printer.print(PrintChannel.WARNINGS, '[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n') Printer.print(PrintChannel.WARNINGS, '[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n')
Zotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH Zotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH
else: else:
Printer.print(PrintChannel.WARNINGS, '[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') Printer.print(PrintChannel.WARNINGS, '[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n')
Zotify.DOWNLOAD_QUALITY = AudioQuality.HIGH Zotify.DOWNLOAD_QUALITY = AudioQuality.HIGH
if args.download: if args.download:
@ -42,7 +41,8 @@ def client(args) -> None:
Printer.print(PrintChannel.ERRORS, f'File {filename} not found.\n') Printer.print(PrintChannel.ERRORS, f'File {filename} not found.\n')
if args.urls: if args.urls:
download_from_urls(args.urls) if len(args.urls) > 0:
download_from_urls(args.urls)
if args.playlist: if args.playlist:
download_from_user_playlist() download_from_user_playlist()
@ -54,13 +54,14 @@ def client(args) -> None:
else: else:
download_track('liked', song[TRACK][ID]) download_track('liked', song[TRACK][ID])
if args.search_spotify: if args.search:
search_text = '' if args.search == ' ':
while len(search_text) == 0: search_text = ''
search_text = input('Enter search or URL: ') while len(search_text) == 0:
search_text = input('Enter search or URL: ')
if not download_from_urls([search_text]): if not download_from_urls([args.search]):
search(search_text) search(args.search)
def download_from_urls(urls: list[str]) -> bool: def download_from_urls(urls: list[str]) -> bool:
""" Downloads from a list of urls """ """ Downloads from a list of urls """

View File

@ -17,7 +17,7 @@ SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS'
DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME'
LANGUAGE = 'LANGUAGE' LANGUAGE = 'LANGUAGE'
BITRATE = 'BITRATE' BITRATE = 'BITRATE'
SONG_ARCHIVE = 'SONG_ARCHIVE' TRACK_ARCHIVE = 'TRACK_ARCHIVE'
CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION' CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION'
OUTPUT = 'OUTPUT' OUTPUT = 'OUTPUT'
PRINT_SPLASH = 'PRINT_SPLASH' PRINT_SPLASH = 'PRINT_SPLASH'
@ -32,42 +32,43 @@ MD_GENREDELIMITER = 'MD_GENREDELIMITER'
PRINT_PROGRESS_INFO = 'PRINT_PROGRESS_INFO' PRINT_PROGRESS_INFO = 'PRINT_PROGRESS_INFO'
PRINT_WARNINGS = 'PRINT_WARNINGS' PRINT_WARNINGS = 'PRINT_WARNINGS'
RETRY_ATTEMPTS = 'RETRY_ATTEMPTS' RETRY_ATTEMPTS = 'RETRY_ATTEMPTS'
CONFIG_VERSION = 'CONFIG_VERSION'
CONFIG_VALUES = { CONFIG_VALUES = {
ROOT_PATH: { 'default': './Zotify Music/', 'type': str, 'arg': '--root-path' }, ROOT_PATH: { 'default': '', 'type': str, 'arg': '--root-path' },
ROOT_PODCAST_PATH: { 'default': './Zotify Podcasts/', 'type': str, 'arg': '--root-podcast-path' }, ROOT_PODCAST_PATH: { 'default': '', 'type': str, 'arg': '--root-podcast-path' },
SKIP_EXISTING_FILES: { 'default': 'True', 'type': bool, 'arg': '--skip-existing-files' }, SKIP_EXISTING_FILES: { 'default': 'True', 'type': bool, 'arg': '--skip-existing-files' },
SKIP_PREVIOUSLY_DOWNLOADED: { 'default': 'False', 'type': bool, 'arg': '--skip-previously-downloaded' }, SKIP_PREVIOUSLY_DOWNLOADED: { 'default': 'False', 'type': bool, 'arg': '--skip-previously-downloaded' },
RETRY_ATTEMPTS: { 'default': '5', 'type': int, 'arg': '--retry-attemps' }, RETRY_ATTEMPTS: { 'default': '5', 'type': int, 'arg': '--retry-attemps' },
DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': str, 'arg': '--download-format' }, DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': str, 'arg': '--download-format' },
FORCE_PREMIUM: { 'default': 'False', 'type': bool, 'arg': '--force-premium' }, FORCE_PREMIUM: { 'default': 'False', 'type': bool, 'arg': '--force-premium' },
ANTI_BAN_WAIT_TIME: { 'default': '1', 'type': int, 'arg': '--anti-ban-wait-time' }, ANTI_BAN_WAIT_TIME: { 'default': '1', 'type': int, 'arg': '--anti-ban-wait-time' },
OVERRIDE_AUTO_WAIT: { 'default': 'False', 'type': bool, 'arg': '--override-auto-wait' }, OVERRIDE_AUTO_WAIT: { 'default': 'False', 'type': bool, 'arg': '--override-auto-wait' },
CHUNK_SIZE: { 'default': '50000', 'type': int, 'arg': '--chunk-size' }, CHUNK_SIZE: { 'default': '50000', 'type': int, 'arg': '--chunk-size' },
SPLIT_ALBUM_DISCS: { 'default': 'False', 'type': bool, 'arg': '--split-album-discs' }, SPLIT_ALBUM_DISCS: { 'default': 'False', 'type': bool, 'arg': '--split-album-discs' },
DOWNLOAD_REAL_TIME: { 'default': 'False', 'type': bool, 'arg': '--download-real-time' }, DOWNLOAD_REAL_TIME: { 'default': 'False', 'type': bool, 'arg': '--download-real-time' },
LANGUAGE: { 'default': 'en', 'type': str, 'arg': '--language' }, LANGUAGE: { 'default': 'en', 'type': str, 'arg': '--language' },
BITRATE: { 'default': '', 'type': str, 'arg': '--bitrate' }, BITRATE: { 'default': '', 'type': str, 'arg': '--bitrate' },
SONG_ARCHIVE: { 'default': '.song_archive', 'type': str, 'arg': '--song-archive' }, TRACK_ARCHIVE: { 'default': '', 'type': str, 'arg': '--track-archive' },
CREDENTIALS_LOCATION: { 'default': 'credentials.json', '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' },
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' },
PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' }, PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' },
PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' }, PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' },
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_ALLGENRES: { 'default': 'False', 'type': bool, 'arg': '--md-allgenres' },
MD_GENREDELIMITER: { 'default': ';', 'type': str, 'arg': '--md-genredelimiter' }, 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}.{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}'
@ -81,7 +82,10 @@ class Config:
'linux': Path.home() / '.config/zotify', 'linux': Path.home() / '.config/zotify',
'darwin': Path.home() / 'Library/Application Support/Zotify' 'darwin': Path.home() / 'Library/Application Support/Zotify'
} }
config_fp = system_paths[sys.platform] / 'config.json' if sys.platform not in system_paths:
config_fp = Path.cwd() / '.zotify/config.json'
else:
config_fp = system_paths[sys.platform] / 'config.json'
if args.config_location: if args.config_location:
config_fp = args.config_location config_fp = args.config_location
@ -143,13 +147,21 @@ class Config:
@classmethod @classmethod
def get_root_path(cls) -> str: def get_root_path(cls) -> str:
# return PurePath(Path.cwd()).joinpath(cls.get(ROOT_PATH)) if cls.get(ROOT_PATH) == '':
return PurePath(Path(cls.get(ROOT_PATH)).expanduser()) root_path = PurePath(Path.home() / 'Music/Zotify Music/')
else:
root_path = PurePath(Path(cls.get(ROOT_PATH)).expanduser())
Path(root_path).mkdir(parents=True, exist_ok=True)
return root_path
@classmethod @classmethod
def get_root_podcast_path(cls) -> str: def get_root_podcast_path(cls) -> str:
# return PurePath(Path.cwd()).joinpath(cls.get(ROOT_PODCAST_PATH)) if cls.get(ROOT_PODCAST_PATH) == '':
return PurePath(Path(cls.get(ROOT_PODCAST_PATH)).expanduser()) root_podcast_path = PurePath(Path.home() / 'Music/Zotify Podcasts/')
else:
root_podcast_path = PurePath(Path(cls.get(ROOT_PODCAST_PATH)).expanduser())
Path(root_podcast_path).mkdir(parents=True, exist_ok=True)
return root_podcast_path
@classmethod @classmethod
def get_skip_existing_files(cls) -> bool: def get_skip_existing_files(cls) -> bool:
@ -196,12 +208,38 @@ class Config:
return cls.get(BITRATE) return cls.get(BITRATE)
@classmethod @classmethod
def get_song_archive(cls) -> str: def get_track_archive(cls) -> str:
return PurePath(cls.get_root_path()).joinpath(cls.get(SONG_ARCHIVE)) if cls.get(TRACK_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')
else:
track_archive = PurePath(system_paths[sys.platform] / 'track_archive')
else:
track_archive = PurePath(Path(cls.get(TRACK_ARCHIVE)).expanduser())
Path(track_archive.parent).mkdir(parents=True, exist_ok=True)
return track_archive
@classmethod @classmethod
def get_credentials_location(cls) -> str: def get_credentials_location(cls) -> str:
return PurePath(Path.cwd()).joinpath(cls.get(CREDENTIALS_LOCATION)) if cls.get(CREDENTIALS_LOCATION) == '':
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:
credentials_location = PurePath(Path.cwd() / '.zotify/credentials.json')
else:
credentials_location = PurePath(system_paths[sys.platform] / 'credentials.json')
else:
credentials_location = PurePath(Path.cwd()).joinpath(cls.get(CREDENTIALS_LOCATION))
Path(credentials_location.parent).mkdir(parents=True, exist_ok=True)
return credentials_location
@classmethod @classmethod
def get_temp_download_dir(cls) -> str: def get_temp_download_dir(cls) -> str:

View File

@ -7,7 +7,7 @@ from librespot.metadata import EpisodeId
from zotify.const import ERROR, ID, ITEMS, NAME, SHOW, DURATION_MS from zotify.const import ERROR, ID, ITEMS, NAME, SHOW, DURATION_MS
from zotify.termoutput import PrintChannel, Printer from zotify.termoutput import PrintChannel, Printer
from zotify.utils import create_download_directory, fix_filename, convert_audio_format from zotify.utils import create_download_directory, fix_filename
from zotify.zotify import Zotify from zotify.zotify import Zotify
from zotify.loader import Loader from zotify.loader import Loader
@ -24,7 +24,7 @@ def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]:
duration_ms = info[DURATION_MS] duration_ms = info[DURATION_MS]
if ERROR in info: if ERROR in info:
return None, None return None, None
return fix_filename(info[SHOW][NAME]), duration_ms, fix_filename(info[NAME]) return fix_filename(info[SHOW][NAME]), duration_ms, fix_filename(info[NAME])
def get_show_episodes(show_id_str) -> list: def get_show_episodes(show_id_str) -> list:
@ -47,7 +47,6 @@ def get_show_episodes(show_id_str) -> list:
def download_podcast_directly(url, filename): def download_podcast_directly(url, filename):
import functools import functools
# import pathlib
import shutil import shutil
import requests import requests
from tqdm.auto import tqdm from tqdm.auto import tqdm
@ -59,7 +58,6 @@ def download_podcast_directly(url, filename):
f"Request to {url} returned status code {r.status_code}") f"Request to {url} returned status code {r.status_code}")
file_size = int(r.headers.get('Content-Length', 0)) file_size = int(r.headers.get('Content-Length', 0))
# path = pathlib.Path(filename).expanduser().resolve()
path = Path(filename).expanduser().resolve() path = Path(filename).expanduser().resolve()
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)

View File

@ -1,4 +1,3 @@
# import os
from pathlib import Path, PurePath from pathlib import Path, PurePath
import re import re
import time import time
@ -152,8 +151,6 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
if not check_id and check_name: if not check_id and check_name:
c = len([file for file in Path(filedir).iterdir() if re.search(f'^{filename}_', str(file))]) + 1 c = len([file for file in Path(filedir).iterdir() if re.search(f'^{filename}_', str(file))]) + 1
# fname = os.path.splitext(os.path.basename(filename))[0]
# ext = os.path.splitext(os.path.basename(filename))[1]
fname = PurePath(PurePath(filename).name).parent fname = PurePath(PurePath(filename).name).parent
ext = PurePath(PurePath(filename).name).suffix ext = PurePath(PurePath(filename).name).suffix
@ -252,7 +249,6 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
def convert_audio_format(filename) -> None: def convert_audio_format(filename) -> None:
""" Converts raw audio into playable file """ """ Converts raw audio into playable file """
# temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
temp_filename = f'{PurePath(filename).parent}.tmp' temp_filename = f'{PurePath(filename).parent}.tmp'
Path(filename).replace(temp_filename) Path(filename).replace(temp_filename)

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_song_archive() archive_path = Zotify.CONFIG.get_track_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_song_archive() archive_path = Zotify.CONFIG.get_track_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:
@ -109,11 +109,12 @@ def split_input(selection) -> List[str]:
def splash() -> str: def splash() -> str:
""" Displays splash screen """ """ Displays splash screen """
return """ return """
""" """

View File

@ -1,7 +1,5 @@
import os
import os.path
from pathlib import Path from pathlib import Path
from getpass import getpass from pwinput import pwinput
import time import time
import requests import requests
from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.audio.decoders import VorbisOnlyAudioQuality
@ -29,7 +27,8 @@ class Zotify:
if Path(cred_location).is_file(): if Path(cred_location).is_file():
try: try:
cls.SESSION = Session.Builder().stored_file(cred_location).create() conf = Session.Configuration.Builder().set_store_credentials(False).build()
cls.SESSION = Session.Builder(conf).stored_file(cred_location).create()
return return
except RuntimeError: except RuntimeError:
pass pass
@ -37,7 +36,7 @@ class Zotify:
user_name = '' user_name = ''
while len(user_name) == 0: while len(user_name) == 0:
user_name = input('Username: ') user_name = input('Username: ')
password = getpass() password = pwinput(prompt='Password: ', mask='*')
try: try:
conf = Session.Configuration.Builder().set_stored_credential_file(cred_location).build() conf = Session.Configuration.Builder().set_stored_credential_file(cred_location).build()
cls.SESSION = Session.Builder(conf).user_pass(user_name, password).create() cls.SESSION = Session.Builder(conf).user_pass(user_name, password).create()
@ -80,7 +79,10 @@ class Zotify:
headers = cls.get_auth_header() headers = cls.get_auth_header()
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
responsetext = response.text responsetext = response.text
responsejson = response.json() try:
responsejson = response.json()
except requests.exceptions.JSONDecodeError:
responsejson = {}
if 'error' in responsejson: if 'error' in responsejson:
if tryCount < (cls.CONFIG.get_retry_attempts() - 1): if tryCount < (cls.CONFIG.get_retry_attempts() - 1):