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.
Files changed (51) hide show
  1. {gamdl-3.0 → gamdl-3.2}/PKG-INFO +2 -1
  2. {gamdl-3.0 → gamdl-3.2}/README.md +1 -0
  3. gamdl-3.2/gamdl/__init__.py +1 -0
  4. {gamdl-3.0 → gamdl-3.2}/gamdl/api/apple_music.py +17 -10
  5. {gamdl-3.0 → gamdl-3.2}/gamdl/api/exceptions.py +1 -1
  6. {gamdl-3.0 → gamdl-3.2}/gamdl/api/itunes.py +8 -5
  7. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/cli.py +65 -59
  8. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/cli_config.py +16 -8
  9. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/base.py +0 -2
  10. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/downloader.py +6 -7
  11. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/exceptions.py +0 -5
  12. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/song.py +1 -1
  13. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/base.py +24 -4
  14. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/exceptions.py +11 -2
  15. gamdl-3.2/gamdl/interface/interface.py +470 -0
  16. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/music_video.py +22 -19
  17. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/song.py +48 -26
  18. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/types.py +4 -2
  19. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/uploaded_video.py +18 -11
  20. {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/PKG-INFO +2 -1
  21. {gamdl-3.0 → gamdl-3.2}/pyproject.toml +1 -1
  22. gamdl-3.0/gamdl/__init__.py +0 -1
  23. gamdl-3.0/gamdl/interface/interface.py +0 -608
  24. {gamdl-3.0 → gamdl-3.2}/LICENSE +0 -0
  25. {gamdl-3.0 → gamdl-3.2}/gamdl/__main__.py +0 -0
  26. {gamdl-3.0 → gamdl-3.2}/gamdl/api/__init__.py +0 -0
  27. {gamdl-3.0 → gamdl-3.2}/gamdl/api/constants.py +0 -0
  28. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/__init__.py +0 -0
  29. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/config_file.py +0 -0
  30. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/constants.py +0 -0
  31. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/database.py +0 -0
  32. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/interactive_prompts.py +0 -0
  33. {gamdl-3.0 → gamdl-3.2}/gamdl/cli/utils.py +0 -0
  34. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/__init__.py +0 -0
  35. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/amdecrypt.py +0 -0
  36. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/constants.py +0 -0
  37. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/enums.py +0 -0
  38. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/music_video.py +0 -0
  39. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/types.py +0 -0
  40. {gamdl-3.0 → gamdl-3.2}/gamdl/downloader/uploaded_video.py +0 -0
  41. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/__init__.py +0 -0
  42. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/constants.py +0 -0
  43. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/enums.py +0 -0
  44. {gamdl-3.0 → gamdl-3.2}/gamdl/interface/wvd.py +0 -0
  45. {gamdl-3.0 → gamdl-3.2}/gamdl/utils.py +0 -0
  46. {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/SOURCES.txt +0 -0
  47. {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/dependency_links.txt +0 -0
  48. {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/entry_points.txt +0 -0
  49. {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/requires.txt +0 -0
  50. {gamdl-3.0 → gamdl-3.2}/gamdl.egg-info/top_level.txt +0 -0
  51. {gamdl-3.0 → gamdl-3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamdl
3
- Version: 3.0
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:
@@ -9,7 +9,7 @@ class GamdlApiResponseError(GamdlApiError):
9
9
  def __init__(
10
10
  self,
11
11
  message: str,
12
- content: str,
12
+ content: str | None = None,
13
13
  status_code: int | None = None,
14
14
  ):
15
15
  self.message = message
@@ -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
- download_queue: list[DownloadItem] = []
249
- async for media in downloader.get_download_item_from_url(url):
250
- download_queue.append(media)
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.warning(f"{e}")
307
+ url_log.error(f"{e}")
253
308
  continue
254
309
  except Exception as e:
255
- url_log.error(f'Error processing "{url}": {e}')
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 or media.flat_filter_result:
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.flat_filter_result:
84
- raise GamdlDownloaderFlatFilterExcludedError(
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}")
@@ -96,7 +96,7 @@ class AppleMusicSongDownloader:
96
96
  staged_path=staged_path,
97
97
  )
98
98
 
99
- if self.base.use_wrapper and not legacy:
99
+ if self.base.interface.base.use_wrapper and not legacy:
100
100
  await self._decrypt_amdecrypt(
101
101
  encrypted_path,
102
102
  staged_path,
@@ -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
- itunes_api: ItunesApi | None = None,
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 = re.sub(
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__(f"Artist has no media of type: {media_type}")
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