More tweaks/fixes

This commit is contained in:
zotify 2023-09-08 17:22:55 +12:00
parent 911c29820a
commit a10b32b5b7
10 changed files with 140 additions and 87 deletions

1
.gitignore vendored
View file

@ -160,4 +160,5 @@ cython_debug/
#.idea/
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json

15
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"matangover.mypy",
"ms-python.black-formatter",
"ms-python.flake8"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [
]
}

View file

@ -1,11 +1,7 @@
{
"python.linting.flake8Enabled": true,
"python.linting.mypyEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"isort.args": ["--profile", "black"]
}
}

View file

@ -2,7 +2,7 @@
## v1.0.0
An unexpected reboot
An unexpected reboot.
### BREAKING CHANGES AHEAD
@ -29,9 +29,14 @@ An unexpected reboot
### Additions
- Added new command line arguments
- `--library`/`-l` overrides both `music_library` and `podcast_library` options similar to `--output`
- `--library`/`-l` overrides both `music_library` and `podcast_library` options similar to `--output`/`-o`
- `--category`/`-c` will limit search results to a certain type, accepted values are "album", "artist", "playlist", "track", "show", "episode". Accepts multiple choices.
- `--debug` shows full tracebacks on crash instead of just the final error message
- Added new shorthand aliases to some options:
- `-oa` = `--output-album`
- `-opt` = `--output-playlist-track`
- `-ope` = `--output-playlist-episode`
- `-op` = `--output-podcast`
- Search results can be narrowed down using search filters
- Available filters are 'album', 'artist', 'track', 'year', 'upc', 'tag:hipster', 'tag:new', 'isrc', and 'genre'.
- The 'artist' and 'year' filters only shows results from the given year or a range (e.g. 1970-1982).

View file

@ -32,6 +32,19 @@ console_scripts =
zotify = zotify.__main__:main
[flake8]
# Conflicts with black
ignore = E203
max-line-length = 160
[mypy]
warn_unused_configs = True
[mypy-librespot.*]
ignore_missing_imports = True
[mypy-music_tag]
ignore_missing_imports = True
[mypy-pwinput]
ignore_missing_imports = True
[mypy-tqdm]
ignore_missing_imports = True

View file

@ -5,15 +5,16 @@ from pathlib import Path
from zotify.app import App
from zotify.config import CONFIG_PATHS, CONFIG_VALUES
from zotify.utils import OptionalOrFalse
from zotify.utils import OptionalOrFalse, SimpleHelpFormatter
VERSION = "0.9.2"
VERSION = "0.9.3"
def main():
parser = ArgumentParser(
prog="zotify",
description="A fast and customizable music and podcast downloader",
formatter_class=SimpleHelpFormatter,
)
parser.add_argument(
"-v",
@ -39,7 +40,7 @@ def main():
help="Specify a path to the root of a music/playlist/podcast library",
)
parser.add_argument(
"-o", "--output", type=str, help="Specify the output location/format"
"-o", "--output", type=str, help="Specify the output file structure/format"
)
parser.add_argument(
"-c",
@ -101,7 +102,7 @@ def main():
for k, v in CONFIG_VALUES.items():
if v["type"] == bool:
parser.add_argument(
v["arg"],
*v["args"],
action=OptionalOrFalse,
default=v["default"],
help=v["help"],
@ -109,7 +110,7 @@ def main():
else:
try:
parser.add_argument(
v["arg"],
*v["args"],
type=v["type"],
choices=v["choices"],
default=None,
@ -117,7 +118,7 @@ def main():
)
except KeyError:
parser.add_argument(
v["arg"],
*v["args"],
type=v["type"],
default=None,
help=v["help"],

View file

@ -19,7 +19,7 @@ from zotify.config import Config
from zotify.file import TranscodingError
from zotify.loader import Loader
from zotify.printer import PrintChannel, Printer
from zotify.utils import API_URL, AudioFormat, b62_to_hex
from zotify.utils import API_URL, AudioFormat, MetadataEntry, b62_to_hex
class ParseError(ValueError):
@ -36,7 +36,7 @@ class PlayableData(NamedTuple):
id: PlayableId
library: Path
output: str
metadata: dict[str, Any] = {}
metadata: list[MetadataEntry] = []
class Selection:
@ -385,7 +385,7 @@ class App:
self.__config.chunk_size,
)
if self.__config.save_lyrics_file and playable.type == PlayableType.TRACK:
if playable.type == PlayableType.TRACK and self.__config.lyrics_file:
with Loader("Fetching lyrics..."):
try:
track.get_lyrics().save(output)

View file

@ -17,6 +17,7 @@ DOWNLOAD_QUALITY = "download_quality"
FFMPEG_ARGS = "ffmpeg_args"
FFMPEG_PATH = "ffmpeg_path"
LANGUAGE = "language"
LYRICS_FILE = "lyrics_file"
LYRICS_ONLY = "lyrics_only"
MUSIC_LIBRARY = "music_library"
OUTPUT = "output"
@ -34,7 +35,6 @@ PRINT_PROGRESS = "print_progress"
PRINT_SKIPS = "print_skips"
PRINT_WARNINGS = "print_warnings"
REPLACE_EXISTING = "replace_existing"
SAVE_LYRICS_FILE = "save_lyrics_file"
SAVE_METADATA = "save_metadata"
SAVE_SUBTITLES = "save_subtitles"
SKIP_DUPLICATES = "skip_duplicates"
@ -72,190 +72,190 @@ CONFIG_VALUES = {
CREDENTIALS: {
"default": CONFIG_PATHS["creds"],
"type": Path,
"arg": "--credentials",
"args": ["--credentials"],
"help": "Path to credentials file",
},
PATH_ARCHIVE: {
"default": CONFIG_PATHS["archive"],
"type": Path,
"arg": "--archive",
"args": ["--archive"],
"help": "Path to track archive file",
},
MUSIC_LIBRARY: {
"default": LIBRARY_PATHS["music"],
"type": Path,
"arg": "--music-library",
"args": ["--music-library"],
"help": "Path to root of music library",
},
PODCAST_LIBRARY: {
"default": LIBRARY_PATHS["podcast"],
"type": Path,
"arg": "--podcast-library",
"args": ["--podcast-library"],
"help": "Path to root of podcast library",
},
PLAYLIST_LIBRARY: {
"default": LIBRARY_PATHS["playlist"],
"type": Path,
"arg": "--playlist-library",
"args": ["--playlist-library"],
"help": "Path to root of playlist library",
},
OUTPUT_ALBUM: {
"default": OUTPUT_PATHS["album"],
"type": str,
"arg": "--output-album",
"args": ["--output-album", "-oa"],
"help": "File layout for saved albums",
},
OUTPUT_PLAYLIST_TRACK: {
"default": OUTPUT_PATHS["playlist_track"],
"type": str,
"arg": "--output-playlist-track",
"args": ["--output-playlist-track", "-opt"],
"help": "File layout for tracks in a playlist",
},
OUTPUT_PLAYLIST_EPISODE: {
"default": OUTPUT_PATHS["playlist_episode"],
"type": str,
"arg": "--output-playlist-episode",
"args": ["--output-playlist-episode", "-ope"],
"help": "File layout for episodes in a playlist",
},
OUTPUT_PODCAST: {
"default": OUTPUT_PATHS["podcast"],
"type": str,
"arg": "--output-podcast",
"args": ["--output-podcast", "-op"],
"help": "File layout for saved podcasts",
},
DOWNLOAD_QUALITY: {
"default": "auto",
"type": Quality.from_string,
"choices": list(Quality),
"arg": "--download-quality",
"args": ["--download-quality"],
"help": "Audio download quality (auto for highest available)",
},
ARTWORK_SIZE: {
"default": "large",
"type": ImageSize.from_string,
"choices": list(ImageSize),
"arg": "--artwork-size",
"args": ["--artwork-size"],
"help": "Image size of track's cover art",
},
AUDIO_FORMAT: {
"default": "vorbis",
"type": AudioFormat,
"choices": [n.value.name for n in AudioFormat],
"arg": "--audio-format",
"args": ["--audio-format"],
"help": "Audio format of final track output",
},
TRANSCODE_BITRATE: {
"default": -1,
"type": int,
"arg": "--bitrate",
"args": ["--bitrate"],
"help": "Transcoding bitrate (-1 to use download rate)",
},
FFMPEG_PATH: {
"default": "",
"type": str,
"arg": "--ffmpeg-path",
"args": ["--ffmpeg-path"],
"help": "Path to ffmpeg binary",
},
FFMPEG_ARGS: {
"default": "",
"type": str,
"arg": "--ffmpeg-args",
"args": ["--ffmpeg-args"],
"help": "Additional ffmpeg arguments when transcoding",
},
SAVE_SUBTITLES: {
"default": False,
"type": bool,
"arg": "--save-subtitles",
"args": ["--save-subtitles"],
"help": "Save subtitles from podcasts to a .srt file",
},
LANGUAGE: {
"default": "en",
"type": str,
"arg": "--language",
"args": ["--language"],
"help": "Language for metadata",
},
SAVE_LYRICS_FILE: {
"default": True,
LYRICS_FILE: {
"default": False,
"type": bool,
"arg": "--save-lyrics-file",
"args": ["--lyrics-file"],
"help": "Save lyrics to a file",
},
LYRICS_ONLY: {
"default": False,
"type": bool,
"arg": "--lyrics-only",
"args": ["--lyrics-only"],
"help": "Only download lyrics and not actual audio",
},
CREATE_PLAYLIST_FILE: {
"default": True,
"type": bool,
"arg": "--playlist-file",
"args": ["--playlist-file"],
"help": "Save playlist information to an m3u8 file",
},
SAVE_METADATA: {
"default": True,
"type": bool,
"arg": "--save-metadata",
"args": ["--save-metadata"],
"help": "Save metadata, required for other metadata options",
},
ALL_ARTISTS: {
"default": True,
"type": bool,
"arg": "--all-artists",
"args": ["--all-artists"],
"help": "Add all track artists to artist tag in metadata",
},
REPLACE_EXISTING: {
"default": False,
"type": bool,
"arg": "--replace-existing",
"args": ["--replace-existing"],
"help": "Overwrite existing files with the same name",
},
SKIP_PREVIOUS: {
"default": True,
"type": bool,
"arg": "--skip-previous",
"args": ["--skip-previous"],
"help": "Skip previously downloaded songs",
},
SKIP_DUPLICATES: {
"default": True,
"type": bool,
"arg": "--skip-duplicates",
"args": ["--skip-duplicates"],
"help": "Skip downloading existing track to different album",
},
CHUNK_SIZE: {
"default": 131072,
"default": 16384,
"type": int,
"arg": "--chunk-size",
"args": ["--chunk-size"],
"help": "Number of bytes read at a time during download",
},
PRINT_DOWNLOADS: {
"default": False,
"type": bool,
"arg": "--print-downloads",
"args": ["--print-downloads"],
"help": "Print messages when a song is finished downloading",
},
PRINT_PROGRESS: {
"default": True,
"type": bool,
"arg": "--print-progress",
"args": ["--print-progress"],
"help": "Show progress bars",
},
PRINT_SKIPS: {
"default": False,
"type": bool,
"arg": "--print-skips",
"args": ["--print-skips"],
"help": "Show messages if a song is being skipped",
},
PRINT_WARNINGS: {
"default": True,
"type": bool,
"arg": "--print-warnings",
"args": ["--print-warnings"],
"help": "Show warnings",
},
PRINT_ERRORS: {
"default": True,
"type": bool,
"arg": "--print-errors",
"args": ["--print-errors"],
"help": "Show errors",
},
}
@ -272,6 +272,7 @@ class Config:
ffmpeg_path: str
music_library: Path
language: str
lyrics_file: bool
output_album: str
output_liked: str
output_podcast: str
@ -280,7 +281,6 @@ class Config:
playlist_library: Path
podcast_library: Path
print_progress: bool
save_lyrics_file: bool
save_metadata: bool
transcode_bitrate: int
@ -303,6 +303,8 @@ class Config:
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
@ -318,10 +320,14 @@ class Config:
key,
self.__parse_arg_value(key, CONFIG_VALUES[key]["default"]),
)
else:
self.__config_file = None
# Make "output" arg override all output_* options
# "library" arg overrides all *_library options
if args.library:
self.music_library = args.library
self.playlist_library = args.library
self.podcast_library = args.library
# "output" arg overrides all output_* options
if args.output:
self.output_album = args.output
self.output_liked = args.output

View file

@ -63,7 +63,7 @@ class Printer:
iterable=iterable,
desc=desc,
total=total,
disable=False, # cls.__config.print_progress,
disable=not cls.__config.print_progress,
leave=leave,
position=position,
unit=unit,

View file

@ -1,7 +1,9 @@
from argparse import Action, ArgumentError
from argparse import Action, ArgumentError, HelpFormatter
from enum import Enum, IntEnum
from re import IGNORECASE, sub
from sys import exit
from sys import platform as PLATFORM
from sys import stderr
from typing import Any, NamedTuple
from librespot.audio.decoders import AudioQuality
@ -15,8 +17,8 @@ BASE62 = Base62.create_instance_with_inverted_character_set()
class AudioCodec(NamedTuple):
ext: str
name: str
ext: str
class AudioFormat(Enum):
@ -69,6 +71,43 @@ class ImageSize(IntEnum):
return s
class MetadataEntry:
name: str
value: Any
output: str
def __init__(self, name: str, value: Any, output_value: str | None = None):
"""
Holds metadata entries
args:
name: name of metadata key
value: Value to use in metadata tags
output_value: Value when used in output formatting, if none is provided
will use value from previous argument.
"""
self.name = name
if type(value) == list:
value = "\0".join(value)
self.value = value
if output_value is None:
output_value = self.value
elif output_value == "":
output_value = None
if type(output_value) == list:
output_value = ", ".join(output_value)
self.output = str(output_value)
class SimpleHelpFormatter(HelpFormatter):
def _format_usage(self, usage, actions, groups, prefix):
if usage is not None:
super()._format_usage(usage, actions, groups, prefix)
stderr.write('zotify: error: unrecognized arguments - try "zotify -h"\n')
exit(2)
class OptionalOrFalse(Action):
def __init__(
self,
@ -103,38 +142,15 @@ class OptionalOrFalse(Action):
)
def __call__(self, parser, namespace, values, option_string=None):
if values is None and not option_string.startswith("--no-"):
raise ArgumentError(self, "expected 1 argument")
if values is not None:
raise ArgumentError(self, "expected 0 arguments")
setattr(
namespace,
self.dest,
values if not option_string.startswith("--no-") else False,
True if not option_string.startswith("--no-") else False,
)
class MetadataEntry:
def __init__(self, name: str, value: Any, output_value: str | None = None):
"""
Holds metadata entries
args:
name: name of metadata key
tag_val: Value to use in metadata tags
output_value: Value when used in output formatting
"""
self.name = name
if type(value) == list:
value = "\0".join(value)
self.value = value
if output_value is None:
output_value = value
if output_value == "":
output_value = None
if type(output_value) == list:
output_value = ", ".join(output_value)
self.output = str(output_value)
def fix_filename(filename: str, substitute: str = "_", platform: str = PLATFORM) -> str:
"""
Replace invalid characters on Linux/Windows/MacOS with underscores.