Merge branch 'develop' into 'main'

Zotify 0.6

See merge request team-zotify/zotify!3
This commit is contained in:
Not Logykk 2022-02-15 06:11:44 +00:00
commit 4dad0f87cf
17 changed files with 398 additions and 291 deletions

View File

@ -1,6 +1,46 @@
# Changelog:
### v0.5.2:
**General changes:**
# Changelog
## v0.6
**General changes**
- 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.
- 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 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,
- 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:
- Windows: `%AppData%\Roaming\Zotify\config.json`
- Linux: `~/.config/zotify/config.json`
- macOS: `~/Library/Application Support/Zotify/config.json`
- Other/Undetected: `.zotify/config.json`
- You can still use `--config-location` to specify a different location.
- New default credential 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
**Docker**
- Dockerfile is currently broken, it will be fixed soon. \
The Dockerhub image is now discontinued, we will try to switch to GitLab's container registry.
**Windows installer**
- The Windows installer is unavilable with this release.
- The current installation system will be replaced and a new version will be available with the next release.
## v0.5.2
**General changes**
- Fixed filenaming on Windows
- Fixed removal of special characters metadata
- Can now download different songs with the same name
@ -17,10 +57,10 @@
- Added options to regulate terminal output
- Direct download support for certain podcasts
**Docker images:**
**Docker images**
- Remember credentials between container starts
- Use same uid/gid in container as on host
**Windows installer:**
**Windows installer**
- Now comes with full installer
- Dependencies are installed if not found

View File

@ -1,9 +0,0 @@
# Introduction
Below will contain sets of errors that you might get running zotify. Below will also contain possible fixes to these errors. It is advisable that you read this before posting your error in any support channel.
## AttributeError: module 'google.protobuf.descriptor' has no attribute '\_internal_create_key
_Answer(s):_
`pip install --upgrade protobuf`

133
README.md
View File

@ -1,81 +1,91 @@
# Zotify
### A music and podcast downloader needing only a python interpreter and ffmpeg.
### A highly customizable music and podcast downloader.
<p align="center">
<img src="https://i.imgur.com/hGXQWSl.png">
<img src="https://i.imgur.com/hGXQWSl.png" width="50%">
</p>
[Discord Server](https://discord.gg/XDYsFRTUjE) - [NotABug Mirror](https://notabug.org/Zotify/zotify)
[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
```
Requirements:
Binaries
Dependencies:
- Python 3.9 or greater
- ffmpeg*
- Git**
- FFmpeg*
Python packages:
- pip install -r requirements.txt
Installation:
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`.
\*Zotify will work without FFmpeg but transcoding will be unavailable.
\*\*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:
python 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:
(nothing) Download the tracks/alumbs/playlists URLs from the parameter
-d, --download Download all tracks/alumbs/playlists URLs from the specified file
-p, --playlist Downloads a saved playlist from your account
-ls, --liked-songs Downloads all the liked songs from your account
-s, --search Loads search prompt to find then download a specific track, album or playlist
Extra command line options:
-ns, --no-splash Suppress the splash screen when loading.
--config-location Use a different zconfig.json, defaults to the one in the program directory
Basic options:
(nothing) Download the tracks/alumbs/playlists URLs from the parameter
-d, --download Download all tracks/alumbs/playlists URLs from the specified file
-p, --playlist Downloads a saved playlist from your account
-l, --liked Downloads all the liked songs from your account
-s, --search Searches for specified track, album, artist or playlist, loads search prompt if none are given.
```
### Options:
### Options
All these options can either be configured in the zconfig 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`
| Key (zconfig) | commandline parameter | Description
| Key (config) | commandline parameter | Description
|------------------------------|----------------------------------|---------------------------------------------------------------------|
| ROOT_PATH | --root-path | directory where Zotify saves the music
| ROOT_PODCAST_PATH | --root-podcast-path | directory where Zotify saves the podcasts
| 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
| 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 | split downloaded albums by disc
| DOWNLOAD_REAL_TIME | --download-real-time | only downloads songs as fast as they would be played, can 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
| OUTPUT | --output | The output location/format (see below)
| PRINT_SPLASH | --print-splash | Print the splash message
| PRINT_SKIPS | --print-skips | Print messages if a song is being skipped
| PRINT_DOWNLOAD_PROGRESS | --print-download-progress | Print the download/playlist progress bars
| PRINT_ERRORS | --print-errors | Print errors
| SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED
| ROOT_PATH | --root-path | Directory where Zotify saves music
| ROOT_PODCAST_PATH | --root-podcast-path | Directory where Zotify saves podcasts
| 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
| TEMP_DOWNLOAD_DIR | --temp-download-dir | Download tracks to a temporary directory first
### Output format:
*very-high is limited to premium only
### Output 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:
@ -100,33 +110,36 @@ Example values could be:
~~~~
{playlist}/{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}/{album}/{album_num} - {artist} - {song_name}.{ext}
/home/user/downloads/{artist} - {song_name} [{id}].{ext}
~~~~
### Docker Usage
### Docker Usage - CURRENTLY BROKEN
```
Build the docker image from the Dockerfile:
docker build -t zotify .
Create and run a container from the image:
docker run --rm -u $(id -u):$(id -g) -v "$PWD/zotify:/app" -v "$PWD/zconfig.json:/zconfig.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?
Currently no user has reported their account getting banned after using Zotify.
We highly recommend using 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 not appearing suspicious.
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.
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.
### 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.
### Disclaimer
Zotify is intended to be used in compliance with DMCA, Section 1201, for educational, private and fair use. \
Zotify contributors are not responsible for any misuse of the program or source code.
### Contributing
@ -135,7 +148,3 @@ Please refer to [CONTRIBUTING](CONTRIBUTING.md)
### Changelog
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
Pillow
protobuf
pwinput
tabulate
tqdm

View File

@ -1,11 +1,10 @@
import pathlib
from setuptools import setup
import setuptools
from pathlib import Path
from distutils.core import setup
from setuptools import setup, find_packages
# The directory containing this file
HERE = pathlib.Path(__file__).parent
HERE = Path(__file__).parent
# The text of the README file
README = (HERE / "README.md").read_text()
@ -13,17 +12,24 @@ README = (HERE / "README.md").read_text()
# This call to setup() does all the work
setup(
name="zotify",
version="0.5.3",
version="0.6.0",
author="Zotify Contributors",
description="A music and podcast downloader.",
long_description=README,
long_description_content_type="text/markdown",
url="https://gitlab.com/zotify/zotify.git",
author="zotify",
url="https://gitlab.com/team-zotify/zotify.git",
package_data={'': ['README.md', 'LICENSE']},
packages=['zotify'],
include_package_data=True,
entry_points={
'console_scripts': [
'zotify=zotify.__main__:main',
],
},
classifiers=[
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
packages=['zotify'],
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'],
include_package_data=True,
)

0
zotify/__init__.py Normal file
View File

View File

@ -7,12 +7,12 @@ It's like youtube-dl, but for that other music platform.
import argparse
from app import client
from config import CONFIG_VALUES
from zotify.app import client
from zotify.config import CONFIG_VALUES
if __name__ == '__main__':
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.')
@ -26,7 +26,7 @@ if __name__ == '__main__':
default='',
nargs='*',
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',
action='store_true',
help='Downloads all the liked songs from your account.')
@ -34,8 +34,9 @@ if __name__ == '__main__':
action='store_true',
help='Downloads a saved playlist from your account.')
group.add_argument('-s', '--search',
dest='search_spotify',
action='store_true',
type=str,
nargs='?',
const=' ',
help='Loads search prompt to find then download a specific track, album or playlist')
group.add_argument('-d', '--download',
type=str,
@ -51,3 +52,7 @@ if __name__ == '__main__':
args = parser.parse_args()
args.func(args)
if __name__ == '__main__':
main()

View File

@ -1,8 +1,8 @@
from const import ITEMS, ARTISTS, NAME, ID
from termoutput import Printer
from track import download_track
from utils import fix_filename
from zotify import Zotify
from zotify.const import ITEMS, ARTISTS, NAME, ID
from zotify.termoutput import Printer
from zotify.track import download_track
from zotify.utils import fix_filename
from zotify.zotify import Zotify
ALBUM_URL = 'https://api.spotify.com/v1/albums'
ARTIST_URL = 'https://api.spotify.com/v1/artists'

View File

@ -1,37 +1,42 @@
from librespot.audio.decoders import AudioQuality
from tabulate import tabulate
import os
from pathlib import Path
from album import download_album, download_artist_albums
from const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \
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 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 splash, split_input, regex_input_for_urls
from zotify import Zotify
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
from zotify.track import download_track, get_saved_tracks
from zotify.utils import splash, split_input, regex_input_for_urls
from zotify.zotify import Zotify
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.SPLASH, '[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n')
Zotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH
else:
Printer.print(PrintChannel.SPLASH, '[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\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 = []
filename = args.download
if os.path.exists(filename):
if Path(filename).exists():
with open(filename, 'r', encoding='utf-8') as file:
urls.extend([line.strip() for line in file.readlines()])
@ -41,7 +46,8 @@ def client(args) -> None:
Printer.print(PrintChannel.ERRORS, f'File {filename} not found.\n')
if args.urls:
download_from_urls(args.urls)
if len(args.urls) > 0:
download_from_urls(args.urls)
if args.playlist:
download_from_user_playlist()
@ -53,21 +59,21 @@ def client(args) -> None:
else:
download_track('liked', song[TRACK][ID])
if args.search_spotify:
search_text = ''
while len(search_text) == 0:
search_text = input('Enter search or URL: ')
if args.search:
if args.search == ' ':
search_text = ''
while len(search_text) == 0:
search_text = input('Enter search or URL: ')
if not download_from_urls([search_text]):
search(search_text)
if not download_from_urls([args.search]):
search(args.search)
def download_from_urls(urls: list[str]) -> bool:
""" Downloads from a list of urls """
download = False
for spotify_url in urls:
track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(
spotify_url)
track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(spotify_url)
if track_id is not None:
download = True

View File

@ -1,22 +1,22 @@
import json
import os
import sys
from pathlib import Path, PurePath
from typing import Any
CONFIG_FILE_PATH = '../zconfig.json'
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'
DOWNLOAD_QUALITY = 'DOWNLOAD_QUALITY'
TRANSCODE_BITRATE = 'TRANSCODE_BITRATE'
SONG_ARCHIVE = 'SONG_ARCHIVE'
CREDENTIALS_LOCATION = 'CREDENTIALS_LOCATION'
OUTPUT = 'OUTPUT'
@ -32,42 +32,43 @@ MD_GENREDELIMITER = 'MD_GENREDELIMITER'
PRINT_PROGRESS_INFO = 'PRINT_PROGRESS_INFO'
PRINT_WARNINGS = 'PRINT_WARNINGS'
RETRY_ATTEMPTS = 'RETRY_ATTEMPTS'
CONFIG_VERSION = 'CONFIG_VERSION'
CONFIG_VALUES = {
ROOT_PATH: { 'default': '../Zotify Music/', 'type': str, 'arg': '--root-path' },
ROOT_PODCAST_PATH: { 'default': '../Zotify 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' },
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' },
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': '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' },
PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' },
PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' },
PRINT_API_ERRORS: { 'default': 'False', 'type': bool, 'arg': '--print-api-errors' },
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' }
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' },
PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' },
PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' },
PRINT_API_ERRORS: { 'default': 'False', 'type': bool, 'arg': '--print-api-errors' },
PRINT_PROGRESS_INFO: { 'default': 'True', 'type': bool, 'arg': '--print-progress-info' },
PRINT_WARNINGS: { 'default': 'True', 'type': bool, 'arg': '--print-warnings' },
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}.{ext}'
OUTPUT_DEFAULT_SINGLE = '{artist}/{song_name}/{artist} - {song_name}.{ext}'
OUTPUT_DEFAULT_ALBUM = '{artist}/{album}/{album_num} - {artist} - {song_name}.{ext}'
@ -76,27 +77,31 @@ class Config:
@classmethod
def load(cls, args) -> None:
app_dir = os.path.dirname(__file__)
config_fp = CONFIG_FILE_PATH
system_paths = {
'win32': Path.home() / 'AppData/Roaming/Zotify',
'linux': Path.home() / '.config/zotify',
'darwin': Path.home() / 'Library/Application Support/Zotify'
}
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:
config_fp = args.config_location
true_config_file_path = os.path.join(app_dir, config_fp)
true_config_file_path = Path(config_fp).expanduser()
# Load config from zconfig.json
if not os.path.exists(true_config_file_path):
Path(PurePath(true_config_file_path).parent).mkdir(parents=True, exist_ok=True)
if not Path(true_config_file_path).exists():
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:
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])
with open(true_config_file_path, encoding='utf-8') as 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
@ -142,15 +147,25 @@ class Config:
@classmethod
def get_root_path(cls) -> str:
return os.path.join(os.path.dirname(__file__), cls.get(ROOT_PATH))
if cls.get(ROOT_PATH) == '':
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
def get_root_podcast_path(cls) -> str:
return os.path.join(os.path.dirname(__file__), cls.get(ROOT_PODCAST_PATH))
if cls.get(ROOT_PODCAST_PATH) == '':
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
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:
@ -168,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:
@ -189,22 +200,52 @@ 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_transcode_bitrate(cls) -> str:
return cls.get(TRANSCODE_BITRATE)
@classmethod
def get_song_archive(cls) -> str:
return os.path.join(cls.get_root_path(), cls.get(SONG_ARCHIVE))
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:
song_archive = PurePath(Path.cwd() / '.zotify/.song_archive')
else:
song_archive = PurePath(system_paths[sys.platform] / '.song_archive')
else:
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:
return os.path.join(os.getcwd(), 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
def get_temp_download_dir(cls) -> str:
if cls.get(TEMP_DOWNLOAD_DIR) == '':
return ''
return os.path.join(cls.get_root_path(), cls.get(TEMP_DOWNLOAD_DIR))
return PurePath(cls.get_root_path()).joinpath(cls.get(TEMP_DOWNLOAD_DIR))
@classmethod
def get_all_genres(cls) -> bool:
@ -221,28 +262,28 @@ class Config:
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])
split = PurePath(OUTPUT_DEFAULT_PLAYLIST).parent
return PurePath(split).joinpath('Disc {disc_number}').joinpath(split)
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])
split = PurePath(OUTPUT_DEFAULT_PLAYLIST_EXT).parent
return PurePath(split).joinpath('Disc {disc_number}').joinpath(split)
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])
split = PurePath(OUTPUT_DEFAULT_LIKED_SONGS).parent
return PurePath(split).joinpath('Disc {disc_number}').joinpath(split)
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])
split = PurePath(OUTPUT_DEFAULT_SINGLE).parent
return PurePath(split).joinpath('Disc {disc_number}').joinpath(split)
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])
split = PurePath(OUTPUT_DEFAULT_ALBUM).parent
return PurePath(split).joinpath('Disc {disc_number}').joinpath(split)
return OUTPUT_DEFAULT_ALBUM
raise ValueError()

View File

@ -7,7 +7,7 @@ from shutil import get_terminal_size
from threading import Thread
from time import sleep
from termoutput import Printer
from zotify.termoutput import Printer
class Loader:

View File

@ -1,8 +1,8 @@
from const import ITEMS, ID, TRACK, NAME
from termoutput import Printer
from track import download_track
from utils import split_input
from zotify import Zotify
from zotify.const import ITEMS, ID, TRACK, NAME
from zotify.termoutput import Printer
from zotify.track import download_track
from zotify.utils import split_input
from zotify.zotify import Zotify
MY_PLAYLISTS_URL = 'https://api.spotify.com/v1/me/playlists'
PLAYLISTS_URL = 'https://api.spotify.com/v1/playlists'

View File

@ -1,14 +1,15 @@
import os
# import os
from pathlib import PurePath, Path
import time
from typing import Optional, Tuple
from librespot.metadata import EpisodeId
from const import ERROR, ID, ITEMS, NAME, SHOW, DURATION_MS
from termoutput import PrintChannel, Printer
from utils import create_download_directory, fix_filename
from zotify import Zotify
from loader import Loader
from zotify.const import ERROR, ID, ITEMS, NAME, SHOW, DURATION_MS
from zotify.termoutput import PrintChannel, Printer
from zotify.utils import create_download_directory, fix_filename
from zotify.zotify import Zotify
from zotify.loader import Loader
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes'
@ -23,7 +24,7 @@ def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]:
duration_ms = info[DURATION_MS]
if ERROR in info:
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:
@ -46,7 +47,6 @@ def get_show_episodes(show_id_str) -> list:
def download_podcast_directly(url, filename):
import functools
import pathlib
import shutil
import requests
from tqdm.auto import tqdm
@ -58,7 +58,7 @@ def download_podcast_directly(url, filename):
f"Request to {url} returned status code {r.status_code}")
file_size = int(r.headers.get('Content-Length', 0))
path = pathlib.Path(filename).expanduser().resolve()
path = Path(filename).expanduser().resolve()
path.parent.mkdir(parents=True, exist_ok=True)
desc = "(Unknown total file size)" if file_size == 0 else ""
@ -86,8 +86,8 @@ def download_episode(episode_id) -> None:
direct_download_url = Zotify.invoke_url(
'https://api-partner.spotify.com/pathfinder/v1/query?operationName=getEpisode&variables={"uri":"spotify:episode:' + episode_id + '"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"224ba0fd89fcfdfb3a15fa2d82a6112d3f4e2ac88fba5c6713de04d1b72cf482"}}')[1]["data"]["episode"]["audio"]["items"][-1]["url"]
download_directory = os.path.join(Zotify.CONFIG.get_root_podcast_path(), extra_paths)
download_directory = os.path.realpath(download_directory)
download_directory = PurePath(Zotify.CONFIG.get_root_podcast_path()).joinpath(extra_paths)
# download_directory = os.path.realpath(download_directory)
create_download_directory(download_directory)
if "anon-podcast.scdn.co" in direct_download_url:
@ -97,11 +97,11 @@ def download_episode(episode_id) -> None:
total_size = stream.input_stream.size
filepath = os.path.join(download_directory, f"{filename}.ogg")
filepath = PurePath(download_directory).joinpath(f"{filename}.ogg")
if (
os.path.isfile(filepath)
and os.path.getsize(filepath) == total_size
and Zotify.CONFIG.get_skip_existing_files()
Path(filepath).isfile()
and Path(filepath).stat().st_size == total_size
and Zotify.CONFIG.get_skip_existing()
):
Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###")
prepare_download_loader.stop()
@ -128,7 +128,7 @@ def download_episode(episode_id) -> None:
if delta_want > delta_real:
time.sleep(delta_want - delta_real)
else:
filepath = os.path.join(download_directory, f"{filename}.mp3")
filepath = PurePath(download_directory).joinpath(f"{filename}.mp3")
download_podcast_directly(direct_download_url, filepath)
prepare_download_loader.stop()

View File

@ -2,8 +2,8 @@ import sys
from enum import Enum
from tqdm import tqdm
from config import *
from zotify import Zotify
from zotify.config import *
from zotify.zotify import Zotify
class PrintChannel(Enum):

View File

@ -1,4 +1,4 @@
import os
from pathlib import Path, PurePath
import re
import time
import uuid
@ -6,16 +6,16 @@ from typing import Any, Tuple, List
from librespot.audio.decoders import AudioQuality
from librespot.metadata import TrackId
from ffmpy import FFmpeg
import ffmpy
from 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
from termoutput import Printer, PrintChannel
from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory, \
from zotify.termoutput import Printer, PrintChannel
from zotify.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, fmt_seconds
from zotify import Zotify
from zotify.zotify import Zotify
import traceback
from loader import Loader
from zotify.loader import Loader
def get_saved_tracks() -> list:
@ -136,25 +136,25 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
output_template = output_template.replace("{track_id}", fix_filename(track_id))
output_template = output_template.replace("{ext}", ext)
filename = os.path.join(Zotify.CONFIG.get_root_path(), output_template)
filedir = os.path.dirname(filename)
filename = PurePath(Zotify.CONFIG.get_root_path()).joinpath(output_template)
filedir = PurePath(filename).parent
filename_temp = filename
if Zotify.CONFIG.get_temp_download_dir() != '':
filename_temp = os.path.join(Zotify.CONFIG.get_temp_download_dir(), f'zotify_{str(uuid.uuid4())}_{track_id}.{ext}')
filename_temp = PurePath(Zotify.CONFIG.get_temp_download_dir()).joinpath(f'zotify_{str(uuid.uuid4())}_{track_id}.{ext}')
check_name = os.path.isfile(filename) and os.path.getsize(filename)
check_name = Path(filename).is_file() and Path(filename).stat().st_size
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(filedir) 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
ext = PurePath(PurePath(filename).name).suffix
filename = os.path.join(filedir, f'{fname}_{c}{ext}')
filename = PurePath(filedir).joinpath(f'{fname}_{c}{ext}')
except Exception as e:
Printer.print(PrintChannel.ERRORS, '### SKIPPING SONG - FAILED TO QUERY METADATA ###')
@ -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")
@ -218,21 +218,21 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
set_music_thumbnail(filename_temp, image_url)
if filename_temp != filename:
os.rename(filename_temp, filename)
Path(filename_temp).rename(filename)
time_finished = time.time()
Printer.print(PrintChannel.DOWNLOADS, f'### Downloaded "{song_name}" to "{os.path.relpath(filename, Zotify.CONFIG.get_root_path())}" in {fmt_seconds(time_downloaded - time_start)} (plus {fmt_seconds(time_finished - time_downloaded)} converting) ###' + "\n")
Printer.print(PrintChannel.DOWNLOADS, f'### Downloaded "{song_name}" to "{Path(filename).relative_to(Zotify.CONFIG.get_root_path())}" in {fmt_seconds(time_downloaded - time_start)} (plus {fmt_seconds(time_finished - time_downloaded)} converting) ###' + "\n")
# add song id to archive file
if Zotify.CONFIG.get_skip_previously_downloaded():
add_to_archive(scraped_song_id, os.path.basename(filename), artists[0], name)
add_to_archive(scraped_song_id, PurePath(filename).name, 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, os.path.basename(filename), 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():
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))
@ -241,26 +241,28 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
Printer.print(PrintChannel.ERRORS, "\n")
Printer.print(PrintChannel.ERRORS, str(e) + "\n")
Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n")
if os.path.exists(filename_temp):
os.remove(filename_temp)
if Path(filename_temp).exists():
Path(filename_temp).unlink()
prepare_download_loader.stop()
def convert_audio_format(filename) -> None:
""" Converts raw audio into playable file """
temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
os.replace(filename, temp_filename)
temp_filename = f'{PurePath(filename).parent}.tmp'
Path(filename).replace(temp_filename)
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 os.path.exists(temp_filename):
os.remove(temp_filename)
except ffmpy.FFExecutableNotFoundError:
Printer.print(PrintChannel.ERRORS, f'### SKIPPING {file_codec.upper()} CONVERSION - FFMPEG NOT FOUND ###')

View File

@ -5,14 +5,15 @@ import platform
import re
import subprocess
from enum import Enum
from pathlib import Path, PurePath
from typing import List, Tuple
import music_tag
import requests
from const import ARTIST, GENRE, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \
from zotify.const import ARTIST, GENRE, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \
WINDOWS_SYSTEM, ALBUMARTIST
from zotify import Zotify
from zotify.zotify import Zotify
class MusicFormat(str, Enum):
@ -22,11 +23,11 @@ class MusicFormat(str, Enum):
def create_download_directory(download_path: str) -> None:
""" Create directory and add a hidden file with song ids """
os.makedirs(download_path, exist_ok=True)
Path(download_path).mkdir(parents=True, exist_ok=True)
# add hidden file with song ids
hidden_file_path = os.path.join(download_path, '.song_ids')
if not os.path.isfile(hidden_file_path):
hidden_file_path = PurePath(download_path).joinpath('.song_ids')
if not Path(hidden_file_path).is_file():
with open(hidden_file_path, 'w', encoding='utf-8') as f:
pass
@ -37,7 +38,7 @@ def get_previously_downloaded() -> List[str]:
ids = []
archive_path = Zotify.CONFIG.get_song_archive()
if os.path.exists(archive_path):
if Path(archive_path).exists():
with open(archive_path, 'r', encoding='utf-8') as f:
ids = [line.strip().split('\t')[0] for line in f.readlines()]
@ -49,7 +50,7 @@ def add_to_archive(song_id: str, filename: str, author_name: str, song_name: str
archive_path = Zotify.CONFIG.get_song_archive()
if os.path.exists(archive_path):
if Path(archive_path).exists():
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:
@ -62,8 +63,8 @@ def get_directory_song_ids(download_path: str) -> List[str]:
song_ids = []
hidden_file_path = os.path.join(download_path, '.song_ids')
if os.path.isfile(hidden_file_path):
hidden_file_path = PurePath(download_path).joinpath('.song_ids')
if Path(hidden_file_path).is_file():
with open(hidden_file_path, 'r', encoding='utf-8') as file:
song_ids.extend([line.strip().split('\t')[0] for line in file.readlines()])
@ -73,7 +74,7 @@ def get_directory_song_ids(download_path: str) -> List[str]:
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')
hidden_file_path = PurePath(download_path).joinpath('.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:
@ -108,11 +109,12 @@ def split_input(selection) -> List[str]:
def splash() -> str:
""" Displays splash screen """
return """
"""
@ -280,5 +282,3 @@ def fmt_seconds(secs: float) -> str:
return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
else:
return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)

View File

@ -1,15 +1,14 @@
import os
import os.path
from getpass import getpass
from pathlib import Path
from pwinput import pwinput
import time
import requests
from librespot.audio.decoders import VorbisOnlyAudioQuality
from librespot.core import Session
from const import TYPE, \
from zotify.const import TYPE, \
PREMIUM, USER_READ_EMAIL, OFFSET, LIMIT, \
PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ
from config import Config
from zotify.config import Config
class Zotify:
SESSION: Session = None
@ -26,9 +25,10 @@ class Zotify:
cred_location = Config.get_credentials_location()
if os.path.isfile(cred_location):
if Path(cred_location).is_file():
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
except RuntimeError:
pass
@ -36,7 +36,7 @@ class Zotify:
user_name = ''
while len(user_name) == 0:
user_name = input('Username: ')
password = getpass()
password = pwinput(prompt='Password: ', mask='*')
try:
conf = Session.Configuration.Builder().set_stored_credential_file(cred_location).build()
cls.SESSION = Session.Builder(conf).user_pass(user_name, password).create()
@ -75,11 +75,14 @@ class Zotify:
@classmethod
def invoke_url(cls, url, tryCount=0):
# we need to import that here, otherwise we will get circular imports!
from termoutput import Printer, PrintChannel
from zotify.termoutput import Printer, PrintChannel
headers = cls.get_auth_header()
response = requests.get(url, headers=headers)
responsetext = response.text
responsejson = response.json()
try:
responsejson = response.json()
except requests.exceptions.JSONDecodeError:
responsejson = {}
if 'error' in responsejson:
if tryCount < (cls.CONFIG.get_retry_attempts() - 1):
@ -94,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)