votify 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- votify/__init__.py +1 -0
- votify/__main__.py +3 -0
- votify/cli.py +516 -0
- votify/constants.py +41 -0
- votify/downloader.py +463 -0
- votify/downloader_episode.py +47 -0
- votify/downloader_song.py +126 -0
- votify/enums.py +12 -0
- votify/models.py +32 -0
- votify/playplay_pb2.py +33 -0
- votify/playplay_pb2.pyi +46 -0
- votify/spotify_api.py +251 -0
- votify/utils.py +16 -0
- votify-1.0.dist-info/METADATA +141 -0
- votify-1.0.dist-info/RECORD +17 -0
- votify-1.0.dist-info/WHEEL +4 -0
- votify-1.0.dist-info/entry_points.txt +3 -0
votify/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0"
|
votify/__main__.py
ADDED
votify/cli.py
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .constants import EXCLUDED_CONFIG_FILE_PARAMS, X_NOT_FOUND_STRING
|
|
14
|
+
from .downloader import Downloader
|
|
15
|
+
from .downloader_episode import DownloaderEpisode
|
|
16
|
+
from .downloader_song import DownloaderSong
|
|
17
|
+
from .enums import DownloadMode, Quality
|
|
18
|
+
from .spotify_api import SpotifyApi
|
|
19
|
+
|
|
20
|
+
spotify_api_sig = inspect.signature(SpotifyApi.__init__)
|
|
21
|
+
downloader_sig = inspect.signature(Downloader.__init__)
|
|
22
|
+
downloader_song_sig = inspect.signature(DownloaderSong.__init__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_param_string(param: click.Parameter) -> str:
|
|
26
|
+
if isinstance(param.default, Enum):
|
|
27
|
+
return param.default.value
|
|
28
|
+
elif isinstance(param.default, Path):
|
|
29
|
+
return str(param.default)
|
|
30
|
+
else:
|
|
31
|
+
return param.default
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_default_config_file(ctx: click.Context) -> None:
|
|
35
|
+
ctx.params["config_path"].parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
config_file = {
|
|
37
|
+
param.name: get_param_string(param)
|
|
38
|
+
for param in ctx.command.params
|
|
39
|
+
if param.name not in EXCLUDED_CONFIG_FILE_PARAMS
|
|
40
|
+
}
|
|
41
|
+
ctx.params["config_path"].write_text(json.dumps(config_file, indent=4))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_config_file(
|
|
45
|
+
ctx: click.Context,
|
|
46
|
+
param: click.Parameter,
|
|
47
|
+
no_config_file: bool,
|
|
48
|
+
) -> click.Context:
|
|
49
|
+
if no_config_file:
|
|
50
|
+
return ctx
|
|
51
|
+
if not ctx.params["config_path"].exists():
|
|
52
|
+
write_default_config_file(ctx)
|
|
53
|
+
config_file = dict(json.loads(ctx.params["config_path"].read_text()))
|
|
54
|
+
for param in ctx.command.params:
|
|
55
|
+
if (
|
|
56
|
+
config_file.get(param.name) is not None
|
|
57
|
+
and not ctx.get_parameter_source(param.name)
|
|
58
|
+
== click.core.ParameterSource.COMMANDLINE
|
|
59
|
+
):
|
|
60
|
+
ctx.params[param.name] = param.type_cast_value(ctx, config_file[param.name])
|
|
61
|
+
return ctx
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.command()
|
|
65
|
+
@click.help_option("-h", "--help")
|
|
66
|
+
@click.version_option(__version__, "-v", "--version")
|
|
67
|
+
# CLI specific options
|
|
68
|
+
@click.argument(
|
|
69
|
+
"urls",
|
|
70
|
+
nargs=-1,
|
|
71
|
+
type=str,
|
|
72
|
+
required=True,
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--wait-interval",
|
|
76
|
+
"-w",
|
|
77
|
+
type=float,
|
|
78
|
+
default=10,
|
|
79
|
+
help="Wait interval between downloads in seconds.",
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--force-premium",
|
|
83
|
+
"-f",
|
|
84
|
+
is_flag=True,
|
|
85
|
+
help="Force to detect the account as premium.",
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--save-cover",
|
|
89
|
+
"-s",
|
|
90
|
+
is_flag=True,
|
|
91
|
+
help="Save cover as a separate file.",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--overwrite",
|
|
95
|
+
is_flag=True,
|
|
96
|
+
help="Overwrite existing files.",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--read-urls-as-txt",
|
|
100
|
+
"-r",
|
|
101
|
+
is_flag=True,
|
|
102
|
+
help="Interpret URLs as paths to text files containing URLs.",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--save-playlist",
|
|
106
|
+
is_flag=True,
|
|
107
|
+
help="Save a M3U8 playlist file when downloading a playlist.",
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--lrc-only",
|
|
111
|
+
"-l",
|
|
112
|
+
is_flag=True,
|
|
113
|
+
help="Download only the synced lyrics.",
|
|
114
|
+
)
|
|
115
|
+
@click.option(
|
|
116
|
+
"--no-lrc",
|
|
117
|
+
is_flag=True,
|
|
118
|
+
help="Don't download the synced lyrics.",
|
|
119
|
+
)
|
|
120
|
+
@click.option(
|
|
121
|
+
"--config-path",
|
|
122
|
+
type=Path,
|
|
123
|
+
default=Path.home() / ".spotify-web-downloader" / "config.json",
|
|
124
|
+
help="Path to config file.",
|
|
125
|
+
)
|
|
126
|
+
@click.option(
|
|
127
|
+
"--log-level",
|
|
128
|
+
type=str,
|
|
129
|
+
default="INFO",
|
|
130
|
+
help="Log level.",
|
|
131
|
+
)
|
|
132
|
+
@click.option(
|
|
133
|
+
"--print-exceptions",
|
|
134
|
+
is_flag=True,
|
|
135
|
+
help="Print exceptions.",
|
|
136
|
+
)
|
|
137
|
+
# SpotifyApi specific options
|
|
138
|
+
@click.option(
|
|
139
|
+
"--cookies-path",
|
|
140
|
+
type=Path,
|
|
141
|
+
default=spotify_api_sig.parameters["cookies_path"].default,
|
|
142
|
+
help="Path to cookies file.",
|
|
143
|
+
)
|
|
144
|
+
# Downloader specific options
|
|
145
|
+
@click.option(
|
|
146
|
+
"--quality",
|
|
147
|
+
"-q",
|
|
148
|
+
type=Quality,
|
|
149
|
+
default=downloader_sig.parameters["quality"].default,
|
|
150
|
+
help="Audio quality.",
|
|
151
|
+
)
|
|
152
|
+
@click.option(
|
|
153
|
+
"--output-path",
|
|
154
|
+
"-o",
|
|
155
|
+
type=Path,
|
|
156
|
+
default=downloader_sig.parameters["output_path"].default,
|
|
157
|
+
help="Path to output directory.",
|
|
158
|
+
)
|
|
159
|
+
@click.option(
|
|
160
|
+
"--temp-path",
|
|
161
|
+
type=Path,
|
|
162
|
+
default=downloader_sig.parameters["temp_path"].default,
|
|
163
|
+
help="Path to temporary directory.",
|
|
164
|
+
)
|
|
165
|
+
@click.option(
|
|
166
|
+
"--download-mode",
|
|
167
|
+
"-d",
|
|
168
|
+
type=DownloadMode,
|
|
169
|
+
default=downloader_sig.parameters["download_mode"].default,
|
|
170
|
+
help="Download mode.",
|
|
171
|
+
)
|
|
172
|
+
@click.option(
|
|
173
|
+
"--aria2c-path",
|
|
174
|
+
type=str,
|
|
175
|
+
default=downloader_sig.parameters["aria2c_path"].default,
|
|
176
|
+
help="Path to aria2c binary.",
|
|
177
|
+
)
|
|
178
|
+
@click.option(
|
|
179
|
+
"--unplayplay-path",
|
|
180
|
+
type=str,
|
|
181
|
+
default=downloader_sig.parameters["unplayplay_path"].default,
|
|
182
|
+
help="Path to unplayplay binary.",
|
|
183
|
+
)
|
|
184
|
+
@click.option(
|
|
185
|
+
"--template-folder-album",
|
|
186
|
+
type=str,
|
|
187
|
+
default=downloader_sig.parameters["template_folder_album"].default,
|
|
188
|
+
help="Template folder for tracks that are part of an album.",
|
|
189
|
+
)
|
|
190
|
+
@click.option(
|
|
191
|
+
"--template-folder-compilation",
|
|
192
|
+
type=str,
|
|
193
|
+
default=downloader_sig.parameters["template_folder_compilation"].default,
|
|
194
|
+
help="Template folder for tracks that are part of a compilation album.",
|
|
195
|
+
)
|
|
196
|
+
@click.option(
|
|
197
|
+
"--template-file-single-disc",
|
|
198
|
+
type=str,
|
|
199
|
+
default=downloader_sig.parameters["template_file_single_disc"].default,
|
|
200
|
+
help="Template file for the tracks that are part of a single-disc album.",
|
|
201
|
+
)
|
|
202
|
+
@click.option(
|
|
203
|
+
"--template-file-multi-disc",
|
|
204
|
+
type=str,
|
|
205
|
+
default=downloader_sig.parameters["template_file_multi_disc"].default,
|
|
206
|
+
help="Template file for the tracks that are part of a multi-disc album.",
|
|
207
|
+
)
|
|
208
|
+
@click.option(
|
|
209
|
+
"--template-folder-episode",
|
|
210
|
+
type=str,
|
|
211
|
+
default=downloader_sig.parameters["template_folder_episode"].default,
|
|
212
|
+
help="Template folder for episodes (podcasts).",
|
|
213
|
+
)
|
|
214
|
+
@click.option(
|
|
215
|
+
"--template-file-episode",
|
|
216
|
+
type=str,
|
|
217
|
+
default=downloader_sig.parameters["template_file_episode"].default,
|
|
218
|
+
help="Template file for episodes (podcasts).",
|
|
219
|
+
)
|
|
220
|
+
@click.option(
|
|
221
|
+
"--template-file-playlist",
|
|
222
|
+
type=str,
|
|
223
|
+
default=downloader_sig.parameters["template_file_playlist"].default,
|
|
224
|
+
help="Template file for the M3U8 playlist.",
|
|
225
|
+
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--date-tag-template",
|
|
228
|
+
type=str,
|
|
229
|
+
default=downloader_sig.parameters["date_tag_template"].default,
|
|
230
|
+
help="Date tag template.",
|
|
231
|
+
)
|
|
232
|
+
@click.option(
|
|
233
|
+
"--exclude-tags",
|
|
234
|
+
type=str,
|
|
235
|
+
default=downloader_sig.parameters["exclude_tags"].default,
|
|
236
|
+
help="Comma-separated tags to exclude.",
|
|
237
|
+
)
|
|
238
|
+
@click.option(
|
|
239
|
+
"--truncate",
|
|
240
|
+
type=int,
|
|
241
|
+
default=downloader_sig.parameters["truncate"].default,
|
|
242
|
+
help="Maximum length of the file/folder names.",
|
|
243
|
+
)
|
|
244
|
+
# This option should always be last
|
|
245
|
+
@click.option(
|
|
246
|
+
"--no-config-file",
|
|
247
|
+
"-n",
|
|
248
|
+
is_flag=True,
|
|
249
|
+
callback=load_config_file,
|
|
250
|
+
help="Do not use a config file.",
|
|
251
|
+
)
|
|
252
|
+
def main(
|
|
253
|
+
urls: list[str],
|
|
254
|
+
wait_interval: float,
|
|
255
|
+
force_premium: bool,
|
|
256
|
+
save_cover: bool,
|
|
257
|
+
overwrite: bool,
|
|
258
|
+
read_urls_as_txt: bool,
|
|
259
|
+
save_playlist: bool,
|
|
260
|
+
lrc_only: bool,
|
|
261
|
+
no_lrc: bool,
|
|
262
|
+
config_path: Path,
|
|
263
|
+
log_level: str,
|
|
264
|
+
print_exceptions: bool,
|
|
265
|
+
cookies_path: Path,
|
|
266
|
+
quality: Quality,
|
|
267
|
+
output_path: Path,
|
|
268
|
+
temp_path: Path,
|
|
269
|
+
download_mode: DownloadMode,
|
|
270
|
+
aria2c_path: str,
|
|
271
|
+
unplayplay_path: str,
|
|
272
|
+
template_folder_album: str,
|
|
273
|
+
template_folder_compilation: str,
|
|
274
|
+
template_file_single_disc: str,
|
|
275
|
+
template_file_multi_disc: str,
|
|
276
|
+
template_folder_episode: str,
|
|
277
|
+
template_file_episode: str,
|
|
278
|
+
template_file_playlist: str,
|
|
279
|
+
date_tag_template: str,
|
|
280
|
+
exclude_tags: str,
|
|
281
|
+
truncate: int,
|
|
282
|
+
no_config_file: bool,
|
|
283
|
+
) -> None:
|
|
284
|
+
logging.basicConfig(
|
|
285
|
+
format="[%(levelname)-8s %(asctime)s] %(message)s",
|
|
286
|
+
datefmt="%H:%M:%S",
|
|
287
|
+
)
|
|
288
|
+
logger = logging.getLogger(__name__)
|
|
289
|
+
logger.setLevel(log_level)
|
|
290
|
+
logger.debug("Starting downloader")
|
|
291
|
+
spotify_api = SpotifyApi(cookies_path)
|
|
292
|
+
downloader = Downloader(
|
|
293
|
+
spotify_api,
|
|
294
|
+
quality,
|
|
295
|
+
output_path,
|
|
296
|
+
temp_path,
|
|
297
|
+
download_mode,
|
|
298
|
+
aria2c_path,
|
|
299
|
+
unplayplay_path,
|
|
300
|
+
template_folder_album,
|
|
301
|
+
template_folder_compilation,
|
|
302
|
+
template_file_single_disc,
|
|
303
|
+
template_file_multi_disc,
|
|
304
|
+
template_folder_episode,
|
|
305
|
+
template_file_episode,
|
|
306
|
+
template_file_playlist,
|
|
307
|
+
date_tag_template,
|
|
308
|
+
exclude_tags,
|
|
309
|
+
truncate,
|
|
310
|
+
)
|
|
311
|
+
downloader_song = DownloaderSong(
|
|
312
|
+
downloader,
|
|
313
|
+
)
|
|
314
|
+
downloader_episode = DownloaderEpisode(
|
|
315
|
+
downloader,
|
|
316
|
+
)
|
|
317
|
+
if not lrc_only:
|
|
318
|
+
if not downloader.unplayplay_path_full:
|
|
319
|
+
logger.critical(X_NOT_FOUND_STRING.format("playplay", unplayplay_path))
|
|
320
|
+
return
|
|
321
|
+
if download_mode == DownloadMode.ARIA2C and not downloader.aria2c_path_full:
|
|
322
|
+
logger.critical(X_NOT_FOUND_STRING.format("aria2c", aria2c_path))
|
|
323
|
+
return
|
|
324
|
+
spotify_api.config_info["isPremium"] = (
|
|
325
|
+
True if force_premium else spotify_api.config_info["isPremium"]
|
|
326
|
+
)
|
|
327
|
+
if not spotify_api.config_info["isPremium"] and quality == Quality.HIGH:
|
|
328
|
+
logger.critical("Cannot download at chosen quality with a free account")
|
|
329
|
+
return
|
|
330
|
+
error_count = 0
|
|
331
|
+
if read_urls_as_txt:
|
|
332
|
+
_urls = []
|
|
333
|
+
for url in urls:
|
|
334
|
+
if Path(url).exists():
|
|
335
|
+
_urls.extend(Path(url).read_text(encoding="utf-8").splitlines())
|
|
336
|
+
urls = _urls
|
|
337
|
+
for url_index, url in enumerate(urls, start=1):
|
|
338
|
+
url_progress = f"URL {url_index}/{len(urls)}"
|
|
339
|
+
logger.info(f'({url_progress}) Checking "{url}"')
|
|
340
|
+
try:
|
|
341
|
+
url_info = downloader.get_url_info(url)
|
|
342
|
+
download_queue = downloader.get_download_queue(url_info)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
error_count += 1
|
|
345
|
+
logger.error(
|
|
346
|
+
f'({url_progress}) Failed to check "{url}"',
|
|
347
|
+
exc_info=print_exceptions,
|
|
348
|
+
)
|
|
349
|
+
continue
|
|
350
|
+
medias_metadata = download_queue.medias_metadata
|
|
351
|
+
playlist_metadata = download_queue.playlist_metadata
|
|
352
|
+
for index, media_metadata in enumerate(medias_metadata, start=1):
|
|
353
|
+
queue_progress = (
|
|
354
|
+
f"Track {index}/{len(medias_metadata)} from URL {url_index}/{len(urls)}"
|
|
355
|
+
)
|
|
356
|
+
try:
|
|
357
|
+
logger.info(
|
|
358
|
+
f'({queue_progress}) Downloading "{media_metadata["name"]}"'
|
|
359
|
+
)
|
|
360
|
+
decrypted_path = None
|
|
361
|
+
media_id = downloader.get_media_id(media_metadata)
|
|
362
|
+
media_type = media_metadata["type"]
|
|
363
|
+
logger.debug("Getting stream info")
|
|
364
|
+
stream_info = downloader.get_stream_info(
|
|
365
|
+
**{f"{media_type}_id": media_id},
|
|
366
|
+
)
|
|
367
|
+
if not stream_info.file_id:
|
|
368
|
+
logger.warning(
|
|
369
|
+
f"({queue_progress}) Media is not available on Spotify's "
|
|
370
|
+
"servers and no alternative found, skipping"
|
|
371
|
+
)
|
|
372
|
+
continue
|
|
373
|
+
if stream_info.quality != quality:
|
|
374
|
+
logger.warning(
|
|
375
|
+
f"({queue_progress}) Quality has been changed to {stream_info.quality.value}"
|
|
376
|
+
)
|
|
377
|
+
logger.debug("Getting decryption key")
|
|
378
|
+
decryption_key = downloader.get_decryption_key(stream_info.file_id)
|
|
379
|
+
if media_type == "track":
|
|
380
|
+
logger.debug("Getting lyrics")
|
|
381
|
+
lyrics = downloader_song.get_lyrics(media_id)
|
|
382
|
+
if not download_queue.album_metadata:
|
|
383
|
+
logger.debug("Getting album metadata")
|
|
384
|
+
album_metadata = spotify_api.get_album(
|
|
385
|
+
media_metadata["album"]["id"]
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
album_metadata = download_queue.album_metadata
|
|
389
|
+
logger.debug("Getting track credits")
|
|
390
|
+
track_credits = spotify_api.get_track_credits(media_id)
|
|
391
|
+
tags = downloader_song.get_tags(
|
|
392
|
+
media_metadata,
|
|
393
|
+
album_metadata,
|
|
394
|
+
track_credits,
|
|
395
|
+
lyrics.unsynced,
|
|
396
|
+
)
|
|
397
|
+
if playlist_metadata:
|
|
398
|
+
tags = {
|
|
399
|
+
**tags,
|
|
400
|
+
**downloader.get_playlist_tags(
|
|
401
|
+
playlist_metadata,
|
|
402
|
+
index,
|
|
403
|
+
),
|
|
404
|
+
}
|
|
405
|
+
final_path = downloader.get_final_path(
|
|
406
|
+
media_type,
|
|
407
|
+
tags,
|
|
408
|
+
".ogg",
|
|
409
|
+
)
|
|
410
|
+
lrc_path = downloader.get_lrc_path(final_path)
|
|
411
|
+
cover_path = downloader_song.get_cover_path(final_path)
|
|
412
|
+
cover_url = downloader_song.get_cover_url(album_metadata)
|
|
413
|
+
if lrc_only:
|
|
414
|
+
pass
|
|
415
|
+
elif final_path.exists() and not overwrite:
|
|
416
|
+
logger.warning(
|
|
417
|
+
f'({queue_progress}) Track already exists at "{final_path}", skipping'
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
encrypted_path = downloader.get_encrypted_path(media_id)
|
|
421
|
+
decrypted_path = downloader.get_decrypted_path(media_id)
|
|
422
|
+
logger.debug(f'Downloading to "{encrypted_path}"')
|
|
423
|
+
downloader.download(encrypted_path, stream_info.stream_url)
|
|
424
|
+
logger.debug(f'Decrypting to "{decrypted_path}"')
|
|
425
|
+
downloader.decrypt(
|
|
426
|
+
decryption_key,
|
|
427
|
+
encrypted_path,
|
|
428
|
+
decrypted_path,
|
|
429
|
+
)
|
|
430
|
+
if no_lrc or not lyrics.synced:
|
|
431
|
+
pass
|
|
432
|
+
elif lrc_path.exists() and not overwrite:
|
|
433
|
+
logger.debug(
|
|
434
|
+
f'Synced lyrics already exists at "{lrc_path}", skipping'
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
logger.debug(f'Saving synced lyrics to "{lrc_path}"')
|
|
438
|
+
downloader.save_lrc(lrc_path, lyrics.synced)
|
|
439
|
+
elif media_type == "episode" and not lrc_only:
|
|
440
|
+
if not download_queue.show_metadata:
|
|
441
|
+
logger.debug("Getting show metadata")
|
|
442
|
+
show_metadata = spotify_api.get_show(
|
|
443
|
+
media_metadata["show"]["id"]
|
|
444
|
+
)
|
|
445
|
+
else:
|
|
446
|
+
show_metadata = download_queue.show_metadata
|
|
447
|
+
tags = downloader_episode.get_tags(
|
|
448
|
+
media_metadata,
|
|
449
|
+
show_metadata,
|
|
450
|
+
)
|
|
451
|
+
if playlist_metadata:
|
|
452
|
+
tags = {
|
|
453
|
+
**tags,
|
|
454
|
+
**downloader.get_playlist_tags(
|
|
455
|
+
playlist_metadata,
|
|
456
|
+
index,
|
|
457
|
+
),
|
|
458
|
+
}
|
|
459
|
+
final_path = downloader.get_final_path(
|
|
460
|
+
media_type,
|
|
461
|
+
tags,
|
|
462
|
+
".ogg",
|
|
463
|
+
)
|
|
464
|
+
cover_path = downloader_episode.get_cover_path(final_path)
|
|
465
|
+
cover_url = downloader_song.get_cover_url(media_metadata)
|
|
466
|
+
if final_path.exists() and not overwrite:
|
|
467
|
+
logger.warning(
|
|
468
|
+
f'({queue_progress}) Track already exists at "{final_path}", skipping'
|
|
469
|
+
)
|
|
470
|
+
else:
|
|
471
|
+
encrypted_path = downloader.get_encrypted_path(media_id)
|
|
472
|
+
decrypted_path = downloader.get_decrypted_path(media_id)
|
|
473
|
+
logger.debug(f'Downloading to "{encrypted_path}"')
|
|
474
|
+
downloader.download(encrypted_path, stream_info.stream_url)
|
|
475
|
+
logger.debug(f'Decrypting to "{decrypted_path}"')
|
|
476
|
+
downloader.decrypt(
|
|
477
|
+
decryption_key,
|
|
478
|
+
encrypted_path,
|
|
479
|
+
decrypted_path,
|
|
480
|
+
)
|
|
481
|
+
if lrc_only or not save_cover:
|
|
482
|
+
pass
|
|
483
|
+
elif cover_path.exists() and not overwrite:
|
|
484
|
+
logger.debug(f'Cover already exists at "{cover_path}", skipping')
|
|
485
|
+
elif cover_url is not None:
|
|
486
|
+
logger.debug(f'Saving cover to "{cover_path}"')
|
|
487
|
+
downloader.save_cover(cover_path, cover_url)
|
|
488
|
+
if decrypted_path:
|
|
489
|
+
logger.debug("Applying tags")
|
|
490
|
+
downloader.apply_tags(decrypted_path, tags, cover_url)
|
|
491
|
+
logger.debug(f'Moving to "{final_path}"')
|
|
492
|
+
downloader.move_to_final_path(decrypted_path, final_path)
|
|
493
|
+
if not lrc_only and save_playlist and playlist_metadata:
|
|
494
|
+
playlist_file_path = downloader.get_playlist_file_path(tags)
|
|
495
|
+
logger.debug(f'Updating M3U8 playlist from "{playlist_file_path}"')
|
|
496
|
+
downloader.update_playlist_file(
|
|
497
|
+
playlist_file_path,
|
|
498
|
+
final_path,
|
|
499
|
+
index,
|
|
500
|
+
)
|
|
501
|
+
except Exception as e:
|
|
502
|
+
error_count += 1
|
|
503
|
+
logger.error(
|
|
504
|
+
f'({queue_progress}) Failed to download "{media_metadata["name"]}"',
|
|
505
|
+
exc_info=print_exceptions,
|
|
506
|
+
)
|
|
507
|
+
finally:
|
|
508
|
+
if temp_path.exists():
|
|
509
|
+
logger.debug(f'Cleaning up "{temp_path}"')
|
|
510
|
+
downloader.cleanup_temp_path()
|
|
511
|
+
if wait_interval > 0 and index != len(medias_metadata):
|
|
512
|
+
logger.debug(
|
|
513
|
+
f"Waiting for {wait_interval} second(s) before continuing"
|
|
514
|
+
)
|
|
515
|
+
time.sleep(wait_interval)
|
|
516
|
+
logger.info(f"Done ({error_count} error(s))")
|
votify/constants.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .enums import Quality
|
|
4
|
+
|
|
5
|
+
EXCLUDED_CONFIG_FILE_PARAMS = (
|
|
6
|
+
"urls",
|
|
7
|
+
"config_path",
|
|
8
|
+
"read_urls_as_txt",
|
|
9
|
+
"no_config_file",
|
|
10
|
+
"version",
|
|
11
|
+
"help",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
VORBIS_TAGS_MAPPING = {
|
|
15
|
+
"album": "ALBUM",
|
|
16
|
+
"album_artist": "ALBUMARTIST",
|
|
17
|
+
"artist": "ARTIST",
|
|
18
|
+
"composer": "COMPOSER",
|
|
19
|
+
"copyright": "COPYRIGHT",
|
|
20
|
+
"description": "DESCRIPTION",
|
|
21
|
+
"disc": "DISC",
|
|
22
|
+
"disc_total": "DISCTOTAL",
|
|
23
|
+
"isrc": "ISRC",
|
|
24
|
+
"label": "LABEL",
|
|
25
|
+
"lyrics": "LYRICS",
|
|
26
|
+
"publisher": "PUBLISHER",
|
|
27
|
+
"producer": "PRODUCER",
|
|
28
|
+
"release_date": "YEAR",
|
|
29
|
+
"title": "TITLE",
|
|
30
|
+
"track": "TRACKNUMBER",
|
|
31
|
+
"track_total": "TRACKTOTAL",
|
|
32
|
+
"url": "URL",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
QUALITY_X_FORMAT_ID_MAPPING = {
|
|
36
|
+
Quality.HIGH: "OGG_VORBIS_320",
|
|
37
|
+
Quality.MEDIUM: "OGG_VORBIS_160",
|
|
38
|
+
Quality.LOW: "OGG_VORBIS_96",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
X_NOT_FOUND_STRING = "{} not found at {}"
|