zotify/zotify/config.py

361 lines
11 KiB
Python

from argparse import Namespace
from json import dump, load
from os import environ
from pathlib import Path
from sys import platform as PLATFORM
from typing import Any
from zotify.utils import AudioFormat, ImageSize, Quality
ALBUM_LIBRARY = "album_library"
ALL_ARTISTS = "all_artists"
ARTWORK_SIZE = "artwork_size"
AUDIO_FORMAT = "audio_format"
CREATE_PLAYLIST_FILE = "create_playlist_file"
CREDENTIALS_PATH = "credentials_path"
DOWNLOAD_QUALITY = "download_quality"
DOWNLOAD_REAL_TIME = "download_real_time"
FFMPEG_ARGS = "ffmpeg_args"
FFMPEG_PATH = "ffmpeg_path"
LANGUAGE = "language"
LYRICS_FILE = "lyrics_file"
LYRICS_ONLY = "lyrics_only"
OUTPUT = "output"
OUTPUT_ALBUM = "output_album"
OUTPUT_PLAYLIST_TRACK = "output_playlist_track"
OUTPUT_PLAYLIST_EPISODE = "output_playlist_episode"
OUTPUT_PODCAST = "output_podcast"
OUTPUT_SINGLE = "output_single"
PATH_ARCHIVE = "path_archive"
PLAYLIST_LIBRARY = "playlist_library"
PODCAST_LIBRARY = "podcast_library"
PRINT_DOWNLOADS = "print_downloads"
PRINT_ERRORS = "print_errors"
PRINT_PROGRESS = "print_progress"
PRINT_SKIPS = "print_skips"
PRINT_WARNINGS = "print_warnings"
REPLACE_EXISTING = "replace_existing"
SAVE_METADATA = "save_metadata"
SAVE_SUBTITLES = "save_subtitles"
SKIP_DUPLICATES = "skip_duplicates"
SKIP_PREVIOUS = "skip_previous"
TRANSCODE_BITRATE = "transcode_bitrate"
SYSTEM_PATHS = {
"win32": Path.home().joinpath("AppData/Roaming/Zotify"),
"darwin": Path.home().joinpath("Library/Application Support/Zotify"),
"linux": Path(environ.get("XDG_CONFIG_HOME") or "~/.config")
.expanduser()
.joinpath("zotify"),
}
LIBRARY_PATHS = {
"album": Path.home().joinpath("Music/Zotify Albums"),
"podcast": Path.home().joinpath("Music/Zotify Podcasts"),
"playlist": Path.home().joinpath("Music/Zotify Playlists"),
}
CONFIG_PATHS = {
"conf": SYSTEM_PATHS[PLATFORM].joinpath("config.json"),
"creds": SYSTEM_PATHS[PLATFORM].joinpath("credentials.json"),
"archive": SYSTEM_PATHS[PLATFORM].joinpath("track_archive"),
}
OUTPUT_PATHS = {
"album": "{album_artist}/{album}/{track_number}. {artists} - {title}",
"podcast": "{podcast}/{episode_number} - {title}",
"playlist_track": "{playlist}/{artists} - {title}",
"playlist_episode": "{playlist}/{episode_number} - {title}",
}
CONFIG_VALUES = {
CREDENTIALS_PATH: {
"default": CONFIG_PATHS["creds"],
"type": Path,
"args": ["--credentials"],
"help": "Path to credentials file",
},
PATH_ARCHIVE: {
"default": CONFIG_PATHS["archive"],
"type": Path,
"args": ["--archive"],
"help": "Path to track archive file",
},
ALBUM_LIBRARY: {
"default": LIBRARY_PATHS["album"],
"type": Path,
"args": ["--album-library"],
"help": "Path to root of album library",
},
PODCAST_LIBRARY: {
"default": LIBRARY_PATHS["podcast"],
"type": Path,
"args": ["--podcast-library"],
"help": "Path to root of podcast library",
},
PLAYLIST_LIBRARY: {
"default": LIBRARY_PATHS["playlist"],
"type": Path,
"args": ["--playlist-library"],
"help": "Path to root of playlist library",
},
OUTPUT_ALBUM: {
"default": OUTPUT_PATHS["album"],
"type": str,
"args": ["--output-album", "-oa"],
"help": "File layout for saved albums",
},
OUTPUT_PLAYLIST_TRACK: {
"default": OUTPUT_PATHS["playlist_track"],
"type": str,
"args": ["--output-playlist-track", "-opt"],
"help": "File layout for tracks in a playlist",
},
OUTPUT_PLAYLIST_EPISODE: {
"default": OUTPUT_PATHS["playlist_episode"],
"type": str,
"args": ["--output-playlist-episode", "-ope"],
"help": "File layout for episodes in a playlist",
},
OUTPUT_PODCAST: {
"default": OUTPUT_PATHS["podcast"],
"type": str,
"args": ["--output-podcast", "-op"],
"help": "File layout for saved podcasts",
},
DOWNLOAD_QUALITY: {
"default": "auto",
"type": Quality.from_string,
"choices": list(Quality),
"args": ["--download-quality"],
"help": "Audio download quality (auto for highest available)",
},
DOWNLOAD_REAL_TIME: {
"default": False,
"type": bool,
"args": ["--download-real-time"],
"help": "Download at the same rate as the track being played",
},
ARTWORK_SIZE: {
"default": "large",
"type": ImageSize.from_string,
"choices": list(ImageSize),
"args": ["--artwork-size"],
"help": "Image size of track's cover art",
},
AUDIO_FORMAT: {
"default": "vorbis",
"type": AudioFormat.from_string,
"choices": list(AudioFormat),
"args": ["--audio-format"],
"help": "Audio format of final track output",
},
TRANSCODE_BITRATE: {
"default": -1,
"type": int,
"args": ["--bitrate"],
"help": "Transcoding bitrate (-1 to use download rate)",
},
FFMPEG_PATH: {
"default": "",
"type": str,
"args": ["--ffmpeg-path"],
"help": "Path to ffmpeg binary",
},
FFMPEG_ARGS: {
"default": "",
"type": str,
"args": ["--ffmpeg-args"],
"help": "Additional ffmpeg arguments when transcoding",
},
SAVE_SUBTITLES: {
"default": False,
"type": bool,
"args": ["--save-subtitles"],
"help": "Save subtitles from podcasts to a .srt file",
},
LANGUAGE: {
"default": "en",
"type": str,
"args": ["--language"],
"help": "Language for metadata",
},
LYRICS_FILE: {
"default": False,
"type": bool,
"args": ["--lyrics-file"],
"help": "Save lyrics to a file",
},
LYRICS_ONLY: {
"default": False,
"type": bool,
"args": ["--lyrics-only"],
"help": "Only download lyrics and not actual audio",
},
CREATE_PLAYLIST_FILE: {
"default": True,
"type": bool,
"args": ["--playlist-file"],
"help": "Save playlist information to an m3u8 file",
},
SAVE_METADATA: {
"default": True,
"type": bool,
"args": ["--save-metadata"],
"help": "Save metadata, required for other metadata options",
},
ALL_ARTISTS: {
"default": True,
"type": bool,
"args": ["--all-artists"],
"help": "Add all track artists to artist tag in metadata",
},
REPLACE_EXISTING: {
"default": False,
"type": bool,
"args": ["--replace-existing"],
"help": "Overwrite existing files with the same name",
},
SKIP_PREVIOUS: {
"default": True,
"type": bool,
"args": ["--skip-previous"],
"help": "Skip previously downloaded songs",
},
SKIP_DUPLICATES: {
"default": True,
"type": bool,
"args": ["--skip-duplicates"],
"help": "Skip downloading existing track to different album",
},
PRINT_DOWNLOADS: {
"default": False,
"type": bool,
"args": ["--print-downloads"],
"help": "Print messages when a song is finished downloading",
},
PRINT_PROGRESS: {
"default": True,
"type": bool,
"args": ["--print-progress"],
"help": "Show progress bars",
},
PRINT_SKIPS: {
"default": False,
"type": bool,
"args": ["--print-skips"],
"help": "Show messages if a song is being skipped",
},
PRINT_WARNINGS: {
"default": True,
"type": bool,
"args": ["--print-warnings"],
"help": "Show warnings",
},
PRINT_ERRORS: {
"default": True,
"type": bool,
"args": ["--print-errors"],
"help": "Show errors",
},
}
class Config:
__config_file: Path | None
album_library: Path
artwork_size: ImageSize
audio_format: AudioFormat
credentials_path: Path
download_quality: Quality
download_real_time: bool
ffmpeg_args: str
ffmpeg_path: str
language: str
lyrics_file: bool
output_album: str
output_podcast: str
output_playlist_track: str
output_playlist_episode: str
playlist_library: Path
podcast_library: Path
print_progress: bool
replace_existing: bool
save_metadata: bool
transcode_bitrate: int
def __init__(self, args: Namespace | None = None):
jsonvalues = {}
if args is not None and args.config:
self.__config_file = Path(args.config)
# Valid config file found
if self.__config_file.exists():
with open(self.__config_file, "r", encoding="utf-8") as conf:
jsonvalues = load(conf)
# Remove config file and make a new one
else:
self.__config_file.parent.mkdir(parents=True, exist_ok=True)
jsonvalues = {}
for key in CONFIG_VALUES:
if CONFIG_VALUES[key]["type"] in [str, int, bool]:
jsonvalues[key] = CONFIG_VALUES[key]["default"]
else:
jsonvalues[key] = str(CONFIG_VALUES[key]["default"])
with open(self.__config_file, "w+", encoding="utf-8") as conf:
dump(jsonvalues, conf, indent=4)
else:
self.__config_file = None
for key in CONFIG_VALUES:
# Override config with commandline arguments
if args is not None and key in vars(args) and vars(args)[key] is not None:
setattr(self, key, self.__parse_arg_value(key, vars(args)[key]))
# If no command option specified use config
elif key in jsonvalues:
setattr(self, key, self.__parse_arg_value(key, jsonvalues[key]))
# Use default values for missing keys
else:
setattr(
self,
key,
self.__parse_arg_value(key, CONFIG_VALUES[key]["default"]),
)
# "library" arg overrides all *_library options
if args is not None and args.library:
self.album_library = Path(args.library).expanduser().resolve()
self.playlist_library = Path(args.library).expanduser().resolve()
self.podcast_library = Path(args.library).expanduser().resolve()
# "output" arg overrides all output_* options
if args is not None and args.output:
self.output_album = args.output
self.output_podcast = args.output
self.output_playlist_track = args.output
self.output_playlist_episode = args.output
@staticmethod
def __parse_arg_value(key: str, value: Any) -> Any:
config_type = CONFIG_VALUES[key]["type"]
if type(value) is config_type:
return value
elif config_type == Path:
return Path(value).expanduser().resolve()
elif config_type == AudioFormat.from_string:
return AudioFormat.from_string(value)
elif config_type == ImageSize.from_string:
return ImageSize.from_string(value)
elif config_type == Quality.from_string:
return Quality.from_string(value)
else:
raise TypeError("Invalid Type: " + value)
def get(self, key: str) -> Any:
"""
Gets a value from config
Args:
key: config attribute to return value of
Returns:
Value of key
"""
return getattr(self, key)