diff --git a/CHANGELOG.md b/CHANGELOG.md index 0acb2e9..d2b6055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ -# Changelog: -### v0.5.2: -**General changes:** +# Changelog +## v0.6 +**General changes** +- Switched from os.path to pathlib +- Zotify can now be installed with pip \ +`pip install https://gitlab.com/team-zotify/zotify/-/archive/main/zotify-main.zip` +- Zotify can be ran from any directory with `zotify `, you no longer need to prefix `python` in the command. + +**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 +32,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 diff --git a/COMMON_ERRORS.md b/COMMON_ERRORS.md deleted file mode 100644 index 52bce29..0000000 --- a/COMMON_ERRORS.md +++ /dev/null @@ -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` diff --git a/setup.py b/setup.py index 3b730af..e3abc2e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ import pathlib -from setuptools import setup -import setuptools - +from distutils.core import setup +from setuptools import setup, find_packages # The directory containing this file @@ -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", 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', 'librespot @ https://github.com/kokarare1212/librespot-python/archive/refs/heads/rewrite.zip'], - include_package_data=True, ) diff --git a/zotify/__init__.py b/zotify/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zotify/__main__.py b/zotify/__main__.py index 091a395..de8e0c8 100644 --- a/zotify/__main__.py +++ b/zotify/__main__.py @@ -7,10 +7,10 @@ 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.') parser.add_argument('-ns', '--no-splash', @@ -51,3 +51,6 @@ if __name__ == '__main__': args = parser.parse_args() args.func(args) + +if __name__ == '__main__': + main() diff --git a/zotify/album.py b/zotify/album.py index 8920af3..53c7992 100644 --- a/zotify/album.py +++ b/zotify/album.py @@ -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' diff --git a/zotify/app.py b/zotify/app.py index fca6d42..7482e43 100644 --- a/zotify/app.py +++ b/zotify/app.py @@ -1,16 +1,17 @@ from librespot.audio.decoders import AudioQuality from tabulate import tabulate -import os +#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, \ - OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME -from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist -from podcast import download_episode, get_show_episodes -from termoutput import Printer, PrintChannel -from track import download_track, get_saved_tracks -from utils import splash, split_input, regex_input_for_urls -from zotify import Zotify +from zotify.album import download_album, download_artist_albums +from zotify.const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \ + OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME, TYPE +from zotify.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' @@ -31,7 +32,7 @@ def client(args) -> None: 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()]) @@ -88,14 +89,17 @@ def download_from_urls(urls: list[str]) -> bool: if not song[TRACK][NAME] or not song[TRACK][ID]: Printer.print(PrintChannel.SKIPS, '### SKIPPING: SONG DOES NOT EXIST ANYMORE ###' + "\n") else: - download_track('playlist', song[TRACK][ID], extra_keys= - { - 'playlist_song_name': song[TRACK][NAME], - 'playlist': name, - 'playlist_num': str(enum).zfill(char_num), - 'playlist_id': playlist_id, - 'playlist_track_id': song[TRACK][ID] - }) + if song[TRACK][TYPE] == "episode": # Playlist item is a podcast episode + download_episode(song[TRACK][ID]) + else: + download_track('playlist', song[TRACK][ID], extra_keys= + { + 'playlist_song_name': song[TRACK][NAME], + 'playlist': name, + 'playlist_num': str(enum).zfill(char_num), + 'playlist_id': playlist_id, + 'playlist_track_id': song[TRACK][ID] + }) enum += 1 elif episode_id is not None: download = True diff --git a/zotify/config.py b/zotify/config.py index adee713..fe290ee 100644 --- a/zotify/config.py +++ b/zotify/config.py @@ -1,8 +1,9 @@ import json -import os +# import os +from pathlib import Path, PurePath from typing import Any -CONFIG_FILE_PATH = '../zconfig.json' +CONFIG_FILE_PATH = './zconfig.json' ROOT_PATH = 'ROOT_PATH' ROOT_PODCAST_PATH = 'ROOT_PODCAST_PATH' @@ -34,34 +35,34 @@ PRINT_WARNINGS = 'PRINT_WARNINGS' RETRY_ATTEMPTS = 'RETRY_ATTEMPTS' 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' } + 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' } } OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}' @@ -76,17 +77,18 @@ class Config: @classmethod def load(cls, args) -> None: - app_dir = os.path.dirname(__file__) + #app_dir = PurePath(__file__).parent + app_dir = Path.cwd() config_fp = CONFIG_FILE_PATH if args.config_location: config_fp = args.config_location - true_config_file_path = os.path.join(app_dir, config_fp) + true_config_file_path = PurePath(app_dir).joinpath(config_fp) # Load config from zconfig.json - if not os.path.exists(true_config_file_path): + 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() @@ -142,11 +144,11 @@ class Config: @classmethod def get_root_path(cls) -> str: - return os.path.join(os.path.dirname(__file__), cls.get(ROOT_PATH)) + return PurePath(Path.cwd()).joinpath(cls.get(ROOT_PATH)) @classmethod def get_root_podcast_path(cls) -> str: - return os.path.join(os.path.dirname(__file__), cls.get(ROOT_PODCAST_PATH)) + return PurePath(Path.cwd()).joinpath(cls.get(ROOT_PODCAST_PATH)) @classmethod def get_skip_existing_files(cls) -> bool: @@ -194,17 +196,17 @@ class Config: @classmethod def get_song_archive(cls) -> str: - return os.path.join(cls.get_root_path(), cls.get(SONG_ARCHIVE)) + return PurePath(cls.get_root_path()).joinpath(cls.get(SONG_ARCHIVE)) @classmethod def get_credentials_location(cls) -> str: - return os.path.join(os.getcwd(), cls.get(CREDENTIALS_LOCATION)) + return PurePath(Path.cwd()).joinpath(cls.get(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 +223,38 @@ 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 = 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 = 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 = 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 = 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 = 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() diff --git a/zotify/loader.py b/zotify/loader.py index faa3cb6..ca894fe 100644 --- a/zotify/loader.py +++ b/zotify/loader.py @@ -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: diff --git a/zotify/playlist.py b/zotify/playlist.py index e00c745..53c1941 100644 --- a/zotify/playlist.py +++ b/zotify/playlist.py @@ -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' diff --git a/zotify/podcast.py b/zotify/podcast.py index 1c5b493..39e778c 100644 --- a/zotify/podcast.py +++ b/zotify/podcast.py @@ -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' @@ -46,7 +47,7 @@ def get_show_episodes(show_id_str) -> list: def download_podcast_directly(url, filename): import functools - import pathlib + # import pathlib import shutil import requests from tqdm.auto import tqdm @@ -58,7 +59,8 @@ 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 = 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 +88,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,10 +99,10 @@ 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 + Path(filepath).isfile() + and Path(filepath).stat().st_size == total_size and Zotify.CONFIG.get_skip_existing_files() ): Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###") @@ -128,7 +130,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() diff --git a/zotify/termoutput.py b/zotify/termoutput.py index 7a7ad96..2539d1f 100644 --- a/zotify/termoutput.py +++ b/zotify/termoutput.py @@ -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): diff --git a/zotify/track.py b/zotify/track.py index af4ef38..b6b0019 100644 --- a/zotify/track.py +++ b/zotify/track.py @@ -1,4 +1,5 @@ -import os +# import os +from pathlib import Path, PurePath import re import time import uuid @@ -8,14 +9,14 @@ from librespot.audio.decoders import AudioQuality from librespot.metadata import TrackId from ffmpy import FFmpeg -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 +137,27 @@ 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 = 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 ###') @@ -218,18 +221,18 @@ 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()) @@ -241,16 +244,17 @@ 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'{os.path.splitext(filename)[0]}.tmp' + 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') @@ -277,5 +281,5 @@ def convert_audio_format(filename) -> None: with Loader(PrintChannel.PROGRESS_INFO, "Converting file..."): ff_m.run() - if os.path.exists(temp_filename): - os.remove(temp_filename) + if Path(temp_filename).exists(): + Path(temp_filename).unlink() diff --git a/zotify/utils.py b/zotify/utils.py index 31c6298..359e500 100644 --- a/zotify/utils.py +++ b/zotify/utils.py @@ -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,12 @@ 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) + # 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 +39,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 +51,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 +64,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 +75,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: diff --git a/zotify/zotify.py b/zotify/zotify.py index f0b3329..d963359 100644 --- a/zotify/zotify.py +++ b/zotify/zotify.py @@ -1,15 +1,16 @@ import os import os.path +from pathlib import Path from getpass import getpass 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,7 +27,7 @@ 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() return @@ -75,7 +76,7 @@ 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