gamdl 3.0__tar.gz → 3.2__tar.gz
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.
- {gamdl-3.0 → gamdl-3.2}/PKG-INFO +2 -1
- {gamdl-3.0 → gamdl-3.2}/README.md +1 -0
- gamdl-3.2/gamdl/__init__.py +1 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/api/apple_music.py +17 -10
- {gamdl-3.0 → gamdl-3.2}/gamdl/api/exceptions.py +1 -1
- {gamdl-3.0 → gamdl-3.2}/gamdl/api/itunes.py +8 -5
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/cli.py +65 -59
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/cli_config.py +16 -8
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/base.py +0 -2
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/downloader.py +6 -7
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/exceptions.py +0 -5
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/song.py +1 -1
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/base.py +24 -4
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/exceptions.py +11 -2
- gamdl-3.2/gamdl/interface/interface.py +470 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/music_video.py +22 -19
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/song.py +48 -26
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/types.py +4 -2
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/uploaded_video.py +18 -11
- {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/PKG-INFO +2 -1
- {gamdl-3.0 → gamdl-3.2}/pyproject.toml +1 -1
- gamdl-3.0/gamdl/__init__.py +0 -1
- gamdl-3.0/gamdl/interface/interface.py +0 -608
- {gamdl-3.0 → gamdl-3.2}/LICENSE +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/__main__.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/api/__init__.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/api/constants.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/__init__.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/config_file.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/constants.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/database.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/interactive_prompts.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/cli/utils.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/__init__.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/amdecrypt.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/constants.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/enums.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/music_video.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/types.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/uploaded_video.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/__init__.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/constants.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/enums.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/interface/wvd.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl/utils.py +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/SOURCES.txt +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/dependency_links.txt +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/entry_points.txt +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/requires.txt +0 -0
- {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/top_level.txt +0 -0
- {gamdl-3.0 → gamdl-3.2}/setup.cfg +0 -0
{gamdl-3.0 → gamdl-3.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gamdl
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2
|
|
4
4
|
Summary: A command-line app for downloading Apple Music songs, music videos and post videos.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/glomatico/gamdl
|
|
@@ -166,6 +166,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
166
166
|
| `--cover-format` | Cover format | `jpg` |
|
|
167
167
|
| `--cover-size` | Cover size in pixels | `1200` |
|
|
168
168
|
| `--wvd-path` | .wvd file path | - |
|
|
169
|
+
| `--wrapper-m3u8-ip` | Wrapper m3u8 IP address and port | - |
|
|
169
170
|
| **Song Options** | | |
|
|
170
171
|
| `--synced-lyrics-format` | Synced lyrics format | `lrc` |
|
|
171
172
|
| `--song-codec-priority` | Comma-separated codec priority | `aac-legacy` |
|
|
@@ -142,6 +142,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
142
142
|
| `--cover-format` | Cover format | `jpg` |
|
|
143
143
|
| `--cover-size` | Cover size in pixels | `1200` |
|
|
144
144
|
| `--wvd-path` | .wvd file path | - |
|
|
145
|
+
| `--wrapper-m3u8-ip` | Wrapper m3u8 IP address and port | - |
|
|
145
146
|
| **Song Options** | | |
|
|
146
147
|
| `--synced-lyrics-format` | Synced lyrics format | `lrc` |
|
|
147
148
|
| `--song-codec-priority` | Comma-separated codec priority | `aac-legacy` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.2"
|
|
@@ -70,6 +70,7 @@ class AppleMusicApi:
|
|
|
70
70
|
async def get_token() -> str:
|
|
71
71
|
log = logger.bind(action="get_token")
|
|
72
72
|
|
|
73
|
+
response = None
|
|
73
74
|
async with httpx.AsyncClient() as client:
|
|
74
75
|
try:
|
|
75
76
|
response = await client.get(
|
|
@@ -81,7 +82,7 @@ class AppleMusicApi:
|
|
|
81
82
|
except httpx.HTTPError:
|
|
82
83
|
raise GamdlApiResponseError(
|
|
83
84
|
"Error fetching Apple Music homepage",
|
|
84
|
-
status_code=response.status_code,
|
|
85
|
+
status_code=response.status_code if response is not None else None,
|
|
85
86
|
)
|
|
86
87
|
|
|
87
88
|
index_js_uri_match = re.search(
|
|
@@ -94,6 +95,7 @@ class AppleMusicApi:
|
|
|
94
95
|
)
|
|
95
96
|
index_js_uri = index_js_uri_match.group(1)
|
|
96
97
|
|
|
98
|
+
response = None
|
|
97
99
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
98
100
|
try:
|
|
99
101
|
response = await client.get(
|
|
@@ -104,7 +106,7 @@ class AppleMusicApi:
|
|
|
104
106
|
except httpx.HTTPError:
|
|
105
107
|
raise GamdlApiResponseError(
|
|
106
108
|
"Error fetching index.js page",
|
|
107
|
-
status_code=response.status_code,
|
|
109
|
+
status_code=response.status_code if response is not None else None,
|
|
108
110
|
)
|
|
109
111
|
|
|
110
112
|
token_match = re.search('(?=eyJh)(.*?)(?=")', index_js_page)
|
|
@@ -124,6 +126,7 @@ class AppleMusicApi:
|
|
|
124
126
|
) -> dict:
|
|
125
127
|
log = logger.bind(action="get_account_info", meta=meta)
|
|
126
128
|
|
|
129
|
+
response = None
|
|
127
130
|
async with httpx.AsyncClient() as client:
|
|
128
131
|
try:
|
|
129
132
|
response = await client.get(
|
|
@@ -142,7 +145,7 @@ class AppleMusicApi:
|
|
|
142
145
|
except httpx.HTTPError:
|
|
143
146
|
raise GamdlApiResponseError(
|
|
144
147
|
"Error fetching account info",
|
|
145
|
-
status_code=response.status_code,
|
|
148
|
+
status_code=response.status_code if response is not None else None,
|
|
146
149
|
)
|
|
147
150
|
|
|
148
151
|
log.debug("success", account_info=account_info)
|
|
@@ -243,6 +246,7 @@ class AppleMusicApi:
|
|
|
243
246
|
*args,
|
|
244
247
|
**kwargs,
|
|
245
248
|
) -> "AppleMusicApi":
|
|
249
|
+
response = None
|
|
246
250
|
async with httpx.AsyncClient() as client:
|
|
247
251
|
try:
|
|
248
252
|
response = await client.get(wrapper_account_url)
|
|
@@ -251,7 +255,7 @@ class AppleMusicApi:
|
|
|
251
255
|
except httpx.HTTPError:
|
|
252
256
|
raise GamdlApiResponseError(
|
|
253
257
|
"Error fetching wrapper account info",
|
|
254
|
-
status_code=response.status_code,
|
|
258
|
+
status_code=response.status_code if response is not None else None,
|
|
255
259
|
)
|
|
256
260
|
|
|
257
261
|
return await cls.create(
|
|
@@ -266,6 +270,7 @@ class AppleMusicApi:
|
|
|
266
270
|
uri: str,
|
|
267
271
|
params: dict | None = None,
|
|
268
272
|
) -> dict:
|
|
273
|
+
response = None
|
|
269
274
|
try:
|
|
270
275
|
response = await self.client.get(
|
|
271
276
|
APPLE_MUSIC_AMP_API_URL + uri,
|
|
@@ -276,8 +281,8 @@ class AppleMusicApi:
|
|
|
276
281
|
except httpx.HTTPError:
|
|
277
282
|
raise GamdlApiResponseError(
|
|
278
283
|
"Error fetching from AMP API",
|
|
279
|
-
content=response.text,
|
|
280
|
-
status_code=response.status_code,
|
|
284
|
+
content=response.text if response is not None else None,
|
|
285
|
+
status_code=response.status_code if response is not None else None,
|
|
281
286
|
)
|
|
282
287
|
|
|
283
288
|
if "errors" in response_json:
|
|
@@ -533,6 +538,7 @@ class AppleMusicApi:
|
|
|
533
538
|
) -> dict:
|
|
534
539
|
log = logger.bind(action="get_webplayback", track_id=track_id)
|
|
535
540
|
|
|
541
|
+
response = None
|
|
536
542
|
try:
|
|
537
543
|
response = await self.client.post(
|
|
538
544
|
APPLE_MUSIC_WEBPLAYBACK_API_URL,
|
|
@@ -546,8 +552,8 @@ class AppleMusicApi:
|
|
|
546
552
|
except httpx.HTTPError:
|
|
547
553
|
raise GamdlApiResponseError(
|
|
548
554
|
"Error fetching webplayback data",
|
|
549
|
-
content=response.text,
|
|
550
|
-
status_code=response.status_code,
|
|
555
|
+
content=response.text if response is not None else None,
|
|
556
|
+
status_code=response.status_code if response is not None else None,
|
|
551
557
|
)
|
|
552
558
|
|
|
553
559
|
if "dialog" in webplayback:
|
|
@@ -570,6 +576,7 @@ class AppleMusicApi:
|
|
|
570
576
|
) -> dict:
|
|
571
577
|
log = logger.bind(action="get_license_exchange", track_id=track_id)
|
|
572
578
|
|
|
579
|
+
response = None
|
|
573
580
|
try:
|
|
574
581
|
response = await self.client.post(
|
|
575
582
|
APPLE_MUSIC_LICENSE_API_URL,
|
|
@@ -587,8 +594,8 @@ class AppleMusicApi:
|
|
|
587
594
|
except httpx.HTTPError:
|
|
588
595
|
raise GamdlApiResponseError(
|
|
589
596
|
"Error fetching license exchange data",
|
|
590
|
-
content=response.text,
|
|
591
|
-
status_code=response.status_code,
|
|
597
|
+
content=response.text if response is not None else None,
|
|
598
|
+
status_code=response.status_code if response is not None else None,
|
|
592
599
|
)
|
|
593
600
|
|
|
594
601
|
if license_exchange.get("status") != 0:
|
|
@@ -30,6 +30,7 @@ class ItunesApi:
|
|
|
30
30
|
async def get_storefront_id(storefront: str) -> int:
|
|
31
31
|
log = logger.bind(action="get_storefront_id", storefront=storefront)
|
|
32
32
|
|
|
33
|
+
response = None
|
|
33
34
|
async with httpx.AsyncClient() as client:
|
|
34
35
|
try:
|
|
35
36
|
response = await client.get(APPLE_MUSIC_MUSIC_KIT_URL)
|
|
@@ -38,7 +39,7 @@ class ItunesApi:
|
|
|
38
39
|
except httpx.HTTPError:
|
|
39
40
|
raise GamdlApiResponseError(
|
|
40
41
|
"Error fetching MusicKit content",
|
|
41
|
-
status_code=response.status_code,
|
|
42
|
+
status_code=response.status_code if response is not None else None,
|
|
42
43
|
)
|
|
43
44
|
|
|
44
45
|
normalized_storefront = storefront.upper()
|
|
@@ -92,6 +93,7 @@ class ItunesApi:
|
|
|
92
93
|
) -> dict:
|
|
93
94
|
log = logger.bind(action="get_lookup_result", media_id=media_id, entity=entity)
|
|
94
95
|
|
|
96
|
+
response = None
|
|
95
97
|
try:
|
|
96
98
|
response = await self.client.get(
|
|
97
99
|
ITUNES_LOOKUP_API_URL,
|
|
@@ -107,8 +109,8 @@ class ItunesApi:
|
|
|
107
109
|
except httpx.HTTPError:
|
|
108
110
|
raise GamdlApiResponseError(
|
|
109
111
|
"Error fetching iTunes lookup result",
|
|
110
|
-
content=response.text,
|
|
111
|
-
status_code=response.status_code,
|
|
112
|
+
content=response.text if response is not None else None,
|
|
113
|
+
status_code=response.status_code if response is not None else None,
|
|
112
114
|
)
|
|
113
115
|
|
|
114
116
|
log.debug("success", lookup_result=lookup_result)
|
|
@@ -126,6 +128,7 @@ class ItunesApi:
|
|
|
126
128
|
media_id=media_id,
|
|
127
129
|
)
|
|
128
130
|
|
|
131
|
+
response = None
|
|
129
132
|
try:
|
|
130
133
|
response = await self.client.get(
|
|
131
134
|
ITUNES_PAGE_API_URL.format(media_type=media_type, media_id=media_id),
|
|
@@ -138,8 +141,8 @@ class ItunesApi:
|
|
|
138
141
|
except httpx.HTTPError:
|
|
139
142
|
raise GamdlApiResponseError(
|
|
140
143
|
"Error fetching iTunes page",
|
|
141
|
-
content=response.text,
|
|
142
|
-
status_code=response.status_code,
|
|
144
|
+
content=response.text if response is not None else None,
|
|
145
|
+
status_code=response.status_code if response is not None else None,
|
|
143
146
|
)
|
|
144
147
|
|
|
145
148
|
log.debug("success", itunes_page=itunes_page)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
import traceback
|
|
4
3
|
from functools import wraps
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
|
|
@@ -18,9 +17,7 @@ from ..downloader import (
|
|
|
18
17
|
AppleMusicMusicVideoDownloader,
|
|
19
18
|
AppleMusicSongDownloader,
|
|
20
19
|
AppleMusicUploadedVideoDownloader,
|
|
21
|
-
DownloadItem,
|
|
22
20
|
GamdlDownloaderDependencyNotFoundError,
|
|
23
|
-
GamdlDownloaderFlatFilterExcludedError,
|
|
24
21
|
GamdlDownloaderMediaFileExistsError,
|
|
25
22
|
GamdlDownloaderSyncedLyricsOnlyError,
|
|
26
23
|
)
|
|
@@ -32,6 +29,7 @@ from ..interface import (
|
|
|
32
29
|
AppleMusicUploadedVideoInterface,
|
|
33
30
|
GamdlInterfaceArtistMediaTypeError,
|
|
34
31
|
GamdlInterfaceDecryptionNotAvailableError,
|
|
32
|
+
GamdlInterfaceFlatFilterExcludedError,
|
|
35
33
|
GamdlInterfaceFormatNotAvailableError,
|
|
36
34
|
GamdlInterfaceMediaNotStreamableError,
|
|
37
35
|
GamdlInterfaceUrlParseError,
|
|
@@ -78,6 +76,7 @@ async def main(config: CliConfig):
|
|
|
78
76
|
structlog.configure(
|
|
79
77
|
processors=[
|
|
80
78
|
structlog.processors.add_log_level,
|
|
79
|
+
structlog.processors.ExceptionPrettyPrinter(),
|
|
81
80
|
custom_structlog_formatter,
|
|
82
81
|
],
|
|
83
82
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
@@ -142,6 +141,8 @@ async def main(config: CliConfig):
|
|
|
142
141
|
apple_music_api=apple_music_api,
|
|
143
142
|
cover_format=config.cover_format,
|
|
144
143
|
cover_size=config.cover_size,
|
|
144
|
+
use_wrapper=config.use_wrapper,
|
|
145
|
+
wrapper_m3u8_ip=config.wrapper_m3u8_ip,
|
|
145
146
|
wvd_path=config.wvd_path,
|
|
146
147
|
)
|
|
147
148
|
|
|
@@ -150,7 +151,6 @@ async def main(config: CliConfig):
|
|
|
150
151
|
synced_lyrics_format=config.synced_lyrics_format,
|
|
151
152
|
codec_priority=config.song_codec_piority,
|
|
152
153
|
use_album_date=config.use_album_date,
|
|
153
|
-
skip_decryption_key_non_legacy=config.use_wrapper,
|
|
154
154
|
skip_stream_info=config.synced_lyrics_only,
|
|
155
155
|
ask_codec_function=interactive_prompts.ask_song_codec,
|
|
156
156
|
)
|
|
@@ -184,7 +184,6 @@ async def main(config: CliConfig):
|
|
|
184
184
|
mp4decrypt_path=config.mp4decrypt_path,
|
|
185
185
|
ffmpeg_path=config.ffmpeg_path,
|
|
186
186
|
mp4box_path=config.mp4box_path,
|
|
187
|
-
use_wrapper=config.use_wrapper,
|
|
188
187
|
wrapper_decrypt_ip=config.wrapper_decrypt_ip,
|
|
189
188
|
download_mode=config.download_mode,
|
|
190
189
|
album_folder_template=config.album_folder_template,
|
|
@@ -245,64 +244,71 @@ async def main(config: CliConfig):
|
|
|
245
244
|
url_log.info(f'Processing "{url}"')
|
|
246
245
|
|
|
247
246
|
try:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
async for download_item in downloader.get_download_item_from_url(url):
|
|
248
|
+
media_index = download_item.media.index + 1
|
|
249
|
+
media_total = download_item.media.total or "-"
|
|
250
|
+
|
|
251
|
+
track_log = logger.bind(
|
|
252
|
+
action=f"Track {media_index:>3}/{media_total:<3}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
media_title = (
|
|
256
|
+
download_item.media.media_metadata["attributes"]["name"]
|
|
257
|
+
if download_item.media.media_metadata
|
|
258
|
+
and download_item.media.media_metadata.get("attributes", {}).get(
|
|
259
|
+
"name"
|
|
260
|
+
)
|
|
261
|
+
else "Unknown Title"
|
|
262
|
+
)
|
|
263
|
+
media_type = (
|
|
264
|
+
download_item.media.media_metadata["type"]
|
|
265
|
+
if download_item.media.media_metadata
|
|
266
|
+
else None
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if download_item.media.partial and media_type in {
|
|
270
|
+
None,
|
|
271
|
+
"songs",
|
|
272
|
+
"library-songs",
|
|
273
|
+
"music-videos",
|
|
274
|
+
"library-music-videos",
|
|
275
|
+
"uploaded-videos",
|
|
276
|
+
}:
|
|
277
|
+
track_log.info(f'Downloading "{media_title}"')
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
await downloader.download(download_item)
|
|
281
|
+
except (
|
|
282
|
+
GamdlInterfaceMediaNotStreamableError,
|
|
283
|
+
GamdlInterfaceFormatNotAvailableError,
|
|
284
|
+
GamdlInterfaceDecryptionNotAvailableError,
|
|
285
|
+
GamdlInterfaceArtistMediaTypeError,
|
|
286
|
+
GamdlDownloaderSyncedLyricsOnlyError,
|
|
287
|
+
GamdlDownloaderMediaFileExistsError,
|
|
288
|
+
GamdlDownloaderDependencyNotFoundError,
|
|
289
|
+
GamdlInterfaceFlatFilterExcludedError,
|
|
290
|
+
) as e:
|
|
291
|
+
track_log.warning(f'Skipping "{media_title}": {e}')
|
|
292
|
+
continue
|
|
293
|
+
except Exception as e:
|
|
294
|
+
error_count += 1
|
|
295
|
+
track_log.exception(f'Error downloading "{media_title}"')
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
database
|
|
299
|
+
and download_item.media.media_metadata
|
|
300
|
+
and download_item.final_path
|
|
301
|
+
):
|
|
302
|
+
database.add(
|
|
303
|
+
download_item.media.media_metadata["id"],
|
|
304
|
+
download_item.final_path,
|
|
305
|
+
)
|
|
251
306
|
except GamdlInterfaceUrlParseError as e:
|
|
252
|
-
url_log.
|
|
307
|
+
url_log.error(f"{e}")
|
|
253
308
|
continue
|
|
254
309
|
except Exception as e:
|
|
255
|
-
url_log.
|
|
310
|
+
url_log.exception(f'Error processing "{url}": {e}')
|
|
256
311
|
error_count += 1
|
|
257
|
-
if not config.no_exceptions:
|
|
258
|
-
traceback.print_exc()
|
|
259
312
|
continue
|
|
260
313
|
|
|
261
|
-
for download_index, download_item in enumerate(
|
|
262
|
-
download_queue,
|
|
263
|
-
1,
|
|
264
|
-
):
|
|
265
|
-
track_log = logger.bind(
|
|
266
|
-
action=f"Track {download_index:>3}/{len(download_queue):<3}"
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
media_title = (
|
|
270
|
-
download_item.media.media_metadata["attributes"]["name"]
|
|
271
|
-
if download_item.media.media_metadata
|
|
272
|
-
and download_item.media.media_metadata.get("attributes", {}).get("name")
|
|
273
|
-
else "Unknown Title"
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
track_log.info(f'Downloading "{media_title}"')
|
|
277
|
-
|
|
278
|
-
try:
|
|
279
|
-
await downloader.download(download_item)
|
|
280
|
-
except (
|
|
281
|
-
GamdlInterfaceMediaNotStreamableError,
|
|
282
|
-
GamdlInterfaceFormatNotAvailableError,
|
|
283
|
-
GamdlInterfaceDecryptionNotAvailableError,
|
|
284
|
-
GamdlInterfaceArtistMediaTypeError,
|
|
285
|
-
GamdlDownloaderSyncedLyricsOnlyError,
|
|
286
|
-
GamdlDownloaderMediaFileExistsError,
|
|
287
|
-
GamdlDownloaderDependencyNotFoundError,
|
|
288
|
-
GamdlDownloaderFlatFilterExcludedError,
|
|
289
|
-
) as e:
|
|
290
|
-
track_log.warning(f'Skipping "{media_title}": {e}')
|
|
291
|
-
continue
|
|
292
|
-
except Exception as e:
|
|
293
|
-
error_count += 1
|
|
294
|
-
track_log.error(f'Error downloading "{media_title}"')
|
|
295
|
-
if not config.no_exceptions:
|
|
296
|
-
traceback.print_exc()
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
database
|
|
300
|
-
and download_item.media.media_metadata
|
|
301
|
-
and download_item.final_path
|
|
302
|
-
):
|
|
303
|
-
database.add(
|
|
304
|
-
download_item.media.media_metadata["id"],
|
|
305
|
-
download_item.final_path,
|
|
306
|
-
)
|
|
307
|
-
|
|
308
314
|
logger.info(f"Finished with {error_count} error(s)")
|
|
@@ -210,6 +210,22 @@ class CliConfig:
|
|
|
210
210
|
),
|
|
211
211
|
),
|
|
212
212
|
]
|
|
213
|
+
use_wrapper: Annotated[
|
|
214
|
+
bool,
|
|
215
|
+
option(
|
|
216
|
+
"--use-wrapper",
|
|
217
|
+
help="Use wrapper for decrypting songs",
|
|
218
|
+
is_flag=True,
|
|
219
|
+
),
|
|
220
|
+
]
|
|
221
|
+
wrapper_m3u8_ip: Annotated[
|
|
222
|
+
str,
|
|
223
|
+
option(
|
|
224
|
+
"--wrapper-m3u8-ip",
|
|
225
|
+
help="Wrapper m3u8 IP address and port",
|
|
226
|
+
default=base_interface_create_sig.parameters["wrapper_m3u8_ip"].default,
|
|
227
|
+
),
|
|
228
|
+
]
|
|
213
229
|
# Song Interface Options
|
|
214
230
|
synced_lyrics_format: Annotated[
|
|
215
231
|
SyncedLyricsFormat,
|
|
@@ -328,14 +344,6 @@ class CliConfig:
|
|
|
328
344
|
default=base_downloader_sig.parameters["mp4box_path"].default,
|
|
329
345
|
),
|
|
330
346
|
]
|
|
331
|
-
use_wrapper: Annotated[
|
|
332
|
-
bool,
|
|
333
|
-
option(
|
|
334
|
-
"--use-wrapper",
|
|
335
|
-
help="Use wrapper for decrypting songs",
|
|
336
|
-
is_flag=True,
|
|
337
|
-
),
|
|
338
|
-
]
|
|
339
347
|
wrapper_decrypt_ip: Annotated[
|
|
340
348
|
str,
|
|
341
349
|
option(
|
|
@@ -27,7 +27,6 @@ class AppleMusicBaseDownloader:
|
|
|
27
27
|
mp4decrypt_path: str = "mp4decrypt",
|
|
28
28
|
ffmpeg_path: str = "ffmpeg",
|
|
29
29
|
mp4box_path: str = "MP4Box",
|
|
30
|
-
use_wrapper: bool = False,
|
|
31
30
|
wrapper_decrypt_ip: str = "127.0.0.1:10020",
|
|
32
31
|
download_mode: DownloadMode = DownloadMode.YTDLP,
|
|
33
32
|
album_folder_template: str = "{album_artist}/{album}",
|
|
@@ -50,7 +49,6 @@ class AppleMusicBaseDownloader:
|
|
|
50
49
|
self.mp4decrypt_path = mp4decrypt_path
|
|
51
50
|
self.ffmpeg_path = ffmpeg_path
|
|
52
51
|
self.mp4box_path = mp4box_path
|
|
53
|
-
self.use_wrapper = use_wrapper
|
|
54
52
|
self.wrapper_decrypt_ip = wrapper_decrypt_ip
|
|
55
53
|
self.download_mode = download_mode
|
|
56
54
|
self.album_folder_template = album_folder_template
|
|
@@ -5,12 +5,10 @@ from typing import AsyncGenerator
|
|
|
5
5
|
import structlog
|
|
6
6
|
|
|
7
7
|
from ..interface.types import AppleMusicMedia
|
|
8
|
-
|
|
9
8
|
from .constants import TEMP_PATH_TEMPLATE
|
|
10
9
|
from .enums import DownloadMode, RemuxMode
|
|
11
10
|
from .exceptions import (
|
|
12
11
|
GamdlDownloaderDependencyNotFoundError,
|
|
13
|
-
GamdlDownloaderFlatFilterExcludedError,
|
|
14
12
|
GamdlDownloaderMediaFileExistsError,
|
|
15
13
|
GamdlDownloaderSyncedLyricsOnlyError,
|
|
16
14
|
)
|
|
@@ -60,7 +58,10 @@ class AppleMusicDownloader:
|
|
|
60
58
|
self,
|
|
61
59
|
media: AppleMusicMedia,
|
|
62
60
|
) -> DownloadItem:
|
|
63
|
-
if media.error
|
|
61
|
+
if media.error:
|
|
62
|
+
return DownloadItem(media)
|
|
63
|
+
|
|
64
|
+
if media.partial:
|
|
64
65
|
return DownloadItem(media)
|
|
65
66
|
|
|
66
67
|
elif media.media_metadata["type"] in {"songs", "library-songs"}:
|
|
@@ -80,10 +81,8 @@ class AppleMusicDownloader:
|
|
|
80
81
|
if item.media.error:
|
|
81
82
|
raise item.media.error
|
|
82
83
|
|
|
83
|
-
if item.media.
|
|
84
|
-
|
|
85
|
-
item.media.media_metadata["id"]
|
|
86
|
-
)
|
|
84
|
+
if item.media.partial:
|
|
85
|
+
return
|
|
87
86
|
|
|
88
87
|
await self._initial_processing(item)
|
|
89
88
|
await self._download(item)
|
|
@@ -18,8 +18,3 @@ class GamdlDownloaderMediaFileExistsError(GamdlDownloaderError):
|
|
|
18
18
|
class GamdlDownloaderDependencyNotFoundError(GamdlDownloaderError):
|
|
19
19
|
def __init__(self, dependency_name: str) -> None:
|
|
20
20
|
super().__init__(f"Required dependency not found: {dependency_name}")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class GamdlDownloaderFlatFilterExcludedError(GamdlDownloaderError):
|
|
24
|
-
def __init__(self, media_id: str) -> None:
|
|
25
|
-
super().__init__(f"Media is excluded by flat filter: {media_id}")
|
|
@@ -29,12 +29,16 @@ class AppleMusicBaseInterface:
|
|
|
29
29
|
itunes_api: ItunesApi,
|
|
30
30
|
cover_format: CoverFormat,
|
|
31
31
|
cover_size: int,
|
|
32
|
+
use_wrapper: bool,
|
|
33
|
+
wrapper_m3u8_ip: str,
|
|
32
34
|
cdm: Cdm,
|
|
33
35
|
) -> None:
|
|
34
36
|
self.apple_music_api = apple_music_api
|
|
35
37
|
self.itunes_api = itunes_api
|
|
36
38
|
self.cover_format = cover_format
|
|
37
39
|
self.cover_size = cover_size
|
|
40
|
+
self.use_wrapper = use_wrapper
|
|
41
|
+
self.wrapper_m3u8_ip = wrapper_m3u8_ip
|
|
38
42
|
self.cdm = cdm
|
|
39
43
|
|
|
40
44
|
@staticmethod
|
|
@@ -103,14 +107,28 @@ class AppleMusicBaseInterface:
|
|
|
103
107
|
|
|
104
108
|
return response
|
|
105
109
|
|
|
110
|
+
@staticmethod
|
|
111
|
+
def format_cover(
|
|
112
|
+
template_cover_url: str,
|
|
113
|
+
cover_size: int,
|
|
114
|
+
cover_format: CoverFormat,
|
|
115
|
+
) -> str:
|
|
116
|
+
return re.sub(
|
|
117
|
+
r"/\{w\}x\{h\}([a-z]{2})\.jpg",
|
|
118
|
+
f"/{cover_size}x{cover_size}bb.{cover_format.value}",
|
|
119
|
+
template_cover_url,
|
|
120
|
+
)
|
|
121
|
+
|
|
106
122
|
@classmethod
|
|
107
123
|
async def create(
|
|
108
124
|
cls,
|
|
109
125
|
apple_music_api: AppleMusicApi,
|
|
110
126
|
cover_format: CoverFormat = CoverFormat.JPG,
|
|
111
127
|
cover_size: int = 1200,
|
|
112
|
-
|
|
128
|
+
use_wrapper: bool = False,
|
|
129
|
+
wrapper_m3u8_ip: str = "127.0.0.1:20020",
|
|
113
130
|
wvd_path: str | None = None,
|
|
131
|
+
itunes_api: ItunesApi | None = None,
|
|
114
132
|
):
|
|
115
133
|
itunes_api = itunes_api or await ItunesApi.create(
|
|
116
134
|
storefront=apple_music_api.storefront,
|
|
@@ -123,6 +141,8 @@ class AppleMusicBaseInterface:
|
|
|
123
141
|
itunes_api=itunes_api,
|
|
124
142
|
cover_format=cover_format,
|
|
125
143
|
cover_size=cover_size,
|
|
144
|
+
use_wrapper=use_wrapper,
|
|
145
|
+
wrapper_m3u8_ip=wrapper_m3u8_ip,
|
|
126
146
|
cdm=cdm,
|
|
127
147
|
)
|
|
128
148
|
return base
|
|
@@ -246,10 +266,10 @@ class AppleMusicBaseInterface:
|
|
|
246
266
|
if self.cover_format == CoverFormat.RAW:
|
|
247
267
|
cover_url = template_url
|
|
248
268
|
else:
|
|
249
|
-
cover_url =
|
|
250
|
-
r"/\{w\}x\{h\}([a-z]{2})\.jpg",
|
|
251
|
-
f"/{self.cover_size}x{self.cover_size}bb.{self.cover_format.value}",
|
|
269
|
+
cover_url = self.format_cover(
|
|
252
270
|
template_url,
|
|
271
|
+
self.cover_size,
|
|
272
|
+
self.cover_format,
|
|
253
273
|
)
|
|
254
274
|
|
|
255
275
|
cover_file_extension = await self._get_cover_file_extension(cover_url)
|
|
@@ -38,5 +38,14 @@ class GamdlInterfaceUrlParseError(GamdlInterfaceError):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class GamdlInterfaceArtistMediaTypeError(GamdlInterfaceError):
|
|
41
|
-
def __init__(self, media_type: str):
|
|
42
|
-
super().__init__(
|
|
41
|
+
def __init__(self, media_id: str, media_type: str):
|
|
42
|
+
super().__init__(
|
|
43
|
+
f"Artist has no media of type (media ID: {media_id}): {media_type}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GamdlInterfaceFlatFilterExcludedError(GamdlInterfaceError):
|
|
48
|
+
def __init__(self, media_id: str, result: Any):
|
|
49
|
+
super().__init__(f"Media excluded by flat filter: {media_id}")
|
|
50
|
+
|
|
51
|
+
self.result = result
|