gamdl 3.1__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.1 → gamdl-3.2}/PKG-INFO +1 -1
  2. gamdl-3.2/gamdl/__init__.py +1 -0
  3. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/cli.py +16 -5
  4. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/base.py +0 -2
  5. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/downloader.py +6 -7
  6. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/exceptions.py +0 -5
  7. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/exceptions.py +11 -2
  8. gamdl-3.2/gamdl/interface/interface.py +470 -0
  9. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/music_video.py +22 -19
  10. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/song.py +25 -25
  11. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/types.py +2 -2
  12. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/uploaded_video.py +18 -11
  13. {gamdl-3.1 → gamdl-3.2}/gamdl.egg-info/PKG-INFO +1 -1
  14. {gamdl-3.1 → gamdl-3.2}/pyproject.toml +1 -1
  15. gamdl-3.1/gamdl/__init__.py +0 -1
  16. gamdl-3.1/gamdl/interface/interface.py +0 -636
  17. {gamdl-3.1 → gamdl-3.2}/LICENSE +0 -0
  18. {gamdl-3.1 → gamdl-3.2}/README.md +0 -0
  19. {gamdl-3.1 → gamdl-3.2}/gamdl/__main__.py +0 -0
  20. {gamdl-3.1 → gamdl-3.2}/gamdl/api/__init__.py +0 -0
  21. {gamdl-3.1 → gamdl-3.2}/gamdl/api/apple_music.py +0 -0
  22. {gamdl-3.1 → gamdl-3.2}/gamdl/api/constants.py +0 -0
  23. {gamdl-3.1 → gamdl-3.2}/gamdl/api/exceptions.py +0 -0
  24. {gamdl-3.1 → gamdl-3.2}/gamdl/api/itunes.py +0 -0
  25. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/__init__.py +0 -0
  26. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/cli_config.py +0 -0
  27. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/config_file.py +0 -0
  28. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/constants.py +0 -0
  29. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/database.py +0 -0
  30. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/interactive_prompts.py +0 -0
  31. {gamdl-3.1 → gamdl-3.2}/gamdl/cli/utils.py +0 -0
  32. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/__init__.py +0 -0
  33. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/amdecrypt.py +0 -0
  34. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/constants.py +0 -0
  35. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/enums.py +0 -0
  36. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/music_video.py +0 -0
  37. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/song.py +0 -0
  38. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/types.py +0 -0
  39. {gamdl-3.1 → gamdl-3.2}/gamdl/downloader/uploaded_video.py +0 -0
  40. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/__init__.py +0 -0
  41. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/base.py +0 -0
  42. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/constants.py +0 -0
  43. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/enums.py +0 -0
  44. {gamdl-3.1 → gamdl-3.2}/gamdl/interface/wvd.py +0 -0
  45. {gamdl-3.1 → gamdl-3.2}/gamdl/utils.py +0 -0
  46. {gamdl-3.1 → gamdl-3.2}/gamdl.egg-info/SOURCES.txt +0 -0
  47. {gamdl-3.1 → gamdl-3.2}/gamdl.egg-info/dependency_links.txt +0 -0
  48. {gamdl-3.1 → gamdl-3.2}/gamdl.egg-info/entry_points.txt +0 -0
  49. {gamdl-3.1 → gamdl-3.2}/gamdl.egg-info/requires.txt +0 -0
  50. {gamdl-3.1 → gamdl-3.2}/gamdl.egg-info/top_level.txt +0 -0
  51. {gamdl-3.1 → gamdl-3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamdl
3
- Version: 3.1
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
@@ -0,0 +1 @@
1
+ __version__ = "3.2"
@@ -17,9 +17,7 @@ from ..downloader import (
17
17
  AppleMusicMusicVideoDownloader,
18
18
  AppleMusicSongDownloader,
19
19
  AppleMusicUploadedVideoDownloader,
20
- DownloadItem,
21
20
  GamdlDownloaderDependencyNotFoundError,
22
- GamdlDownloaderFlatFilterExcludedError,
23
21
  GamdlDownloaderMediaFileExistsError,
24
22
  GamdlDownloaderSyncedLyricsOnlyError,
25
23
  )
@@ -31,6 +29,7 @@ from ..interface import (
31
29
  AppleMusicUploadedVideoInterface,
32
30
  GamdlInterfaceArtistMediaTypeError,
33
31
  GamdlInterfaceDecryptionNotAvailableError,
32
+ GamdlInterfaceFlatFilterExcludedError,
34
33
  GamdlInterfaceFormatNotAvailableError,
35
34
  GamdlInterfaceMediaNotStreamableError,
36
35
  GamdlInterfaceUrlParseError,
@@ -152,7 +151,6 @@ async def main(config: CliConfig):
152
151
  synced_lyrics_format=config.synced_lyrics_format,
153
152
  codec_priority=config.song_codec_piority,
154
153
  use_album_date=config.use_album_date,
155
- skip_decryption_key_non_legacy=config.use_wrapper,
156
154
  skip_stream_info=config.synced_lyrics_only,
157
155
  ask_codec_function=interactive_prompts.ask_song_codec,
158
156
  )
@@ -262,8 +260,21 @@ async def main(config: CliConfig):
262
260
  )
263
261
  else "Unknown Title"
264
262
  )
263
+ media_type = (
264
+ download_item.media.media_metadata["type"]
265
+ if download_item.media.media_metadata
266
+ else None
267
+ )
265
268
 
266
- track_log.info(f'Downloading "{media_title}"')
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}"')
267
278
 
268
279
  try:
269
280
  await downloader.download(download_item)
@@ -275,7 +286,7 @@ async def main(config: CliConfig):
275
286
  GamdlDownloaderSyncedLyricsOnlyError,
276
287
  GamdlDownloaderMediaFileExistsError,
277
288
  GamdlDownloaderDependencyNotFoundError,
278
- GamdlDownloaderFlatFilterExcludedError,
289
+ GamdlInterfaceFlatFilterExcludedError,
279
290
  ) as e:
280
291
  track_log.warning(f'Skipping "{media_title}": {e}')
281
292
  continue
@@ -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}")
@@ -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
@@ -0,0 +1,470 @@
1
+ import asyncio
2
+ from typing import Any, AsyncGenerator, Callable
3
+
4
+ import structlog
5
+
6
+ from ..utils import safe_gather
7
+ from .constants import VALID_URL_PATTERN
8
+ from .enums import ArtistMediaType
9
+ from .exceptions import (
10
+ GamdlInterfaceMediaNotAllowedError,
11
+ GamdlInterfaceUrlParseError,
12
+ GamdlInterfaceArtistMediaTypeError,
13
+ GamdlInterfaceFlatFilterExcludedError,
14
+ )
15
+ from .music_video import AppleMusicMusicVideoInterface
16
+ from .song import AppleMusicSongInterface
17
+ from .types import AppleMusicMedia, AppleMusicUrlInfo
18
+ from .uploaded_video import AppleMusicUploadedVideoInterface
19
+
20
+ logger = structlog.get_logger(__name__)
21
+
22
+
23
+ class AppleMusicInterface:
24
+ def __init__(
25
+ self,
26
+ song: AppleMusicSongInterface,
27
+ music_video: AppleMusicMusicVideoInterface,
28
+ uploaded_video: AppleMusicUploadedVideoInterface,
29
+ artist_select_media_type_function: (
30
+ Callable[[list[ArtistMediaType], dict], ArtistMediaType | None] | None
31
+ ) = None,
32
+ artist_select_items_function: (
33
+ Callable[[ArtistMediaType, list[dict]], list[dict] | None] | None
34
+ ) = None,
35
+ flat_filter_function: Callable[[dict], Any] | None = None,
36
+ concurrency: int = 1,
37
+ disallowed_media_types: list[str] | None = None,
38
+ ) -> None:
39
+ self.song = song
40
+ self.music_video = music_video
41
+ self.uploaded_video = uploaded_video
42
+ self.artist_select_media_type_function = artist_select_media_type_function
43
+ self.artist_select_items_function = artist_select_items_function
44
+ self.flat_filter_function = flat_filter_function
45
+ self.concurrency = concurrency
46
+ self.disallowed_media_types = disallowed_media_types
47
+
48
+ self.base = song.base
49
+
50
+ @staticmethod
51
+ def get_url_info(url: str) -> AppleMusicUrlInfo | None:
52
+ log = logger.bind(action="get_url_info", url=url)
53
+
54
+ match = VALID_URL_PATTERN.match(url)
55
+ if not match:
56
+ log.debug("invalid_url_pattern")
57
+
58
+ return None
59
+
60
+ url_match = AppleMusicUrlInfo(
61
+ **match.groupdict(),
62
+ )
63
+
64
+ log.debug("success", url_info=url_match)
65
+
66
+ return url_match
67
+
68
+ async def _run_flat_filter(self, media: AppleMusicMedia) -> None:
69
+ if not self.flat_filter_function or not media.partial:
70
+ return
71
+
72
+ result = self.flat_filter_function(media.media_metadata)
73
+ if asyncio.iscoroutine(result):
74
+ result = await result
75
+
76
+ if result:
77
+ raise GamdlInterfaceFlatFilterExcludedError(media.media_id, result)
78
+
79
+ def _run_media_type_filter(self, media: AppleMusicMedia) -> None:
80
+ if not self.disallowed_media_types or not media.partial:
81
+ return
82
+
83
+ if media.media_metadata["type"] in self.disallowed_media_types:
84
+ raise GamdlInterfaceMediaNotAllowedError(
85
+ media.media_metadata["type"],
86
+ media.media_id,
87
+ )
88
+
89
+ async def _collect_generator(
90
+ self, generator_or_coroutine: AsyncGenerator[AppleMusicMedia, None]
91
+ ) -> list[AppleMusicMedia]:
92
+ results = []
93
+ async for result in generator_or_coroutine:
94
+ results.append(result)
95
+ return results
96
+
97
+ async def _get_song_media(
98
+ self,
99
+ media_id: str,
100
+ index: int | None = None,
101
+ total: int | None = None,
102
+ media_metadata: dict | None = None,
103
+ playlist_metadata: dict | None = None,
104
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
105
+ media = AppleMusicMedia(
106
+ media_id=media_id,
107
+ )
108
+
109
+ if index is not None:
110
+ media.index = index
111
+ if total is not None:
112
+ media.total = total
113
+
114
+ media.media_metadata = media_metadata
115
+ media.playlist_metadata = playlist_metadata
116
+
117
+ try:
118
+ async for media in self.song.get_media(media):
119
+ yield media
120
+
121
+ self._run_media_type_filter(media)
122
+ await self._run_flat_filter(media)
123
+ except Exception as e:
124
+ media.partial = False
125
+ media.error = e
126
+ yield media
127
+ return
128
+
129
+ async def _get_music_video_media(
130
+ self,
131
+ media_id: str,
132
+ index: int | None = None,
133
+ total: int | None = None,
134
+ media_metadata: dict | None = None,
135
+ playlist_metadata: dict | None = None,
136
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
137
+ media = AppleMusicMedia(
138
+ media_id=media_id,
139
+ )
140
+
141
+ if index is not None:
142
+ media.index = index
143
+ if total is not None:
144
+ media.total = total
145
+
146
+ media.media_metadata = media_metadata
147
+ media.playlist_metadata = playlist_metadata
148
+
149
+ try:
150
+ async for media in self.music_video.get_media(media):
151
+ yield media
152
+
153
+ self._run_media_type_filter(media)
154
+ await self._run_flat_filter(media)
155
+ except Exception as e:
156
+ media.partial = False
157
+ media.error = e
158
+ yield media
159
+ return
160
+
161
+ async def _get_uploaded_video_media(
162
+ self,
163
+ media_id: str,
164
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
165
+ media = AppleMusicMedia(
166
+ media_id=media_id,
167
+ )
168
+
169
+ try:
170
+ async for media in self.music_video.get_media(media):
171
+ yield
172
+
173
+ self._run_media_type_filter(media)
174
+ await self._run_flat_filter(media)
175
+ except Exception as e:
176
+ media.partial = False
177
+ media.error = e
178
+ yield media
179
+ return
180
+
181
+ async def _get_album_media(
182
+ self,
183
+ media_id: str,
184
+ is_library: bool = False,
185
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
186
+ base_media = AppleMusicMedia(media_id)
187
+
188
+ try:
189
+ base_media.media_metadata = (
190
+ await self.base.apple_music_api.get_library_album(
191
+ media_id,
192
+ )
193
+ if is_library
194
+ else await self.base.apple_music_api.get_album(
195
+ media_id,
196
+ )
197
+ )["data"][0]
198
+
199
+ self._run_media_type_filter(base_media)
200
+ await self._run_flat_filter(base_media)
201
+ except Exception as e:
202
+ base_media.partial = False
203
+ base_media.error = e
204
+ yield base_media
205
+ return
206
+
207
+ yield base_media
208
+
209
+ tracks = base_media.media_metadata["relationships"]["tracks"]["data"]
210
+ tasks = [
211
+ (
212
+ self._get_song_media(
213
+ media_id=track["id"],
214
+ index=index,
215
+ total=base_media.media_metadata["attributes"]["trackCount"],
216
+ media_metadata=track,
217
+ )
218
+ if track["type"] in {"songs", "library-songs"}
219
+ else self._get_music_video_media(
220
+ media_id=track["id"],
221
+ index=index,
222
+ total=base_media.media_metadata["attributes"]["trackCount"],
223
+ media_metadata=track,
224
+ )
225
+ )
226
+ for index, track in enumerate(tracks)
227
+ ]
228
+
229
+ if self.concurrency == 1:
230
+ for task in tasks:
231
+ async for media in task:
232
+ yield media
233
+ else:
234
+ collected_tasks = [self._collect_generator(task) for task in tasks]
235
+ batches = await safe_gather(*collected_tasks, limit=self.concurrency)
236
+ for batch in batches:
237
+ for media in batch:
238
+ yield media
239
+
240
+ async def _get_playlist_media(
241
+ self,
242
+ media_id: str,
243
+ is_library: bool = False,
244
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
245
+ base_media = AppleMusicMedia(media_id)
246
+
247
+ try:
248
+ base_media.media_metadata = (
249
+ await self.base.apple_music_api.get_library_playlist(
250
+ media_id,
251
+ )
252
+ if is_library
253
+ else await self.base.apple_music_api.get_playlist(
254
+ media_id,
255
+ )
256
+ )["data"][0]
257
+
258
+ self._run_media_type_filter(base_media)
259
+ await self._run_flat_filter(base_media)
260
+
261
+ tracks = base_media.media_metadata["relationships"]["tracks"]["data"]
262
+ next_uri = base_media.media_metadata["relationships"]["tracks"].get("next")
263
+ href_uri = base_media.media_metadata["relationships"]["tracks"].get("href")
264
+ while next_uri:
265
+ extended_data = await self.base.apple_music_api.get_extended_api_data(
266
+ next_uri,
267
+ href_uri,
268
+ )
269
+ tracks.extend(extended_data["data"])
270
+ next_uri = extended_data.get("next")
271
+ except Exception as e:
272
+ base_media.partial = False
273
+ base_media.error = e
274
+ yield base_media
275
+ return
276
+
277
+ yield base_media
278
+
279
+ tasks = [
280
+ (
281
+ self._get_song_media(
282
+ media_id=track["id"],
283
+ index=index,
284
+ total=base_media.media_metadata["attributes"]["trackCount"],
285
+ media_metadata=track,
286
+ playlist_metadata=base_media.media_metadata,
287
+ )
288
+ if track["type"] in {"songs", "library-songs"}
289
+ else self._get_music_video_media(
290
+ media_id=track["id"],
291
+ index=index,
292
+ total=base_media.media_metadata["attributes"]["trackCount"],
293
+ media_metadata=track,
294
+ playlist_metadata=base_media.media_metadata,
295
+ )
296
+ )
297
+ for index, track in enumerate(tracks)
298
+ ]
299
+
300
+ if self.concurrency == 1:
301
+ for task in tasks:
302
+ async for media in task:
303
+ yield media
304
+ else:
305
+ collected_tasks = [self._collect_generator(task) for task in tasks]
306
+ batches = await safe_gather(*collected_tasks, limit=self.concurrency)
307
+ for batch in batches:
308
+ for media in batch:
309
+ yield media
310
+
311
+ async def _get_artist_media(
312
+ self,
313
+ media_id: str,
314
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
315
+ base_media = AppleMusicMedia(media_id)
316
+
317
+ try:
318
+ base_media.media_metadata = (
319
+ await self.base.apple_music_api.get_artist(
320
+ media_id,
321
+ )
322
+ )["data"][0]
323
+
324
+ self._run_media_type_filter(base_media)
325
+ await self._run_flat_filter(base_media)
326
+
327
+ if self.artist_select_media_type_function:
328
+ artist_media_type = self.artist_select_media_type_function(
329
+ list(ArtistMediaType),
330
+ base_media.media_metadata,
331
+ )
332
+ if asyncio.iscoroutine(artist_media_type):
333
+ artist_media_type = await artist_media_type
334
+ else:
335
+ artist_media_type = list(ArtistMediaType)[0]
336
+
337
+ relation_key, type_key = artist_media_type.path_key
338
+
339
+ items_relation = base_media.media_metadata.get(relation_key, {}).get(
340
+ type_key, {}
341
+ )
342
+ items = items_relation.get("data", [])
343
+ if not items:
344
+ raise GamdlInterfaceArtistMediaTypeError(
345
+ base_media.media_id,
346
+ str(artist_media_type),
347
+ )
348
+
349
+ next_uri = items_relation.get("next")
350
+ href_uri = items_relation.get("href")
351
+ while next_uri:
352
+ extended_data = await self.base.apple_music_api.get_extended_api_data(
353
+ next_uri,
354
+ href_uri,
355
+ )
356
+ items.extend(extended_data.get("data", []))
357
+ next_uri = extended_data.get("next")
358
+ except Exception as e:
359
+ yield AppleMusicMedia(
360
+ media_id=media_id,
361
+ media_metadata=None,
362
+ error=e,
363
+ )
364
+ return
365
+
366
+ yield base_media
367
+
368
+ if self.artist_select_items_function:
369
+ selected_items = self.artist_select_items_function(
370
+ artist_media_type,
371
+ items,
372
+ )
373
+ if asyncio.iscoroutine(selected_items):
374
+ selected_items = await selected_items
375
+ else:
376
+ selected_items = items[:1]
377
+
378
+ tasks = []
379
+ for index, item in enumerate(selected_items):
380
+ if item["type"] in {"songs", "library-songs"}:
381
+ tasks.append(
382
+ self._get_song_media(
383
+ media_id=item["id"],
384
+ index=index,
385
+ total=len(selected_items),
386
+ media_metadata=item,
387
+ )
388
+ )
389
+ elif item["type"] in {"albums", "library-albums"}:
390
+ tasks.append(
391
+ self._get_album_media(
392
+ media_id=item["id"],
393
+ )
394
+ )
395
+ else:
396
+ tasks.append(
397
+ self._get_music_video_media(
398
+ media_id=item["id"],
399
+ index=index,
400
+ total=len(selected_items),
401
+ media_metadata=item,
402
+ )
403
+ )
404
+
405
+ if self.concurrency == 1:
406
+ for task in tasks:
407
+ async for media in task:
408
+ yield media
409
+ else:
410
+ collected_tasks = [self._collect_generator(task) for task in tasks]
411
+ batches = await safe_gather(*collected_tasks, limit=self.concurrency)
412
+ for batch in batches:
413
+ for media in batch:
414
+ yield media
415
+
416
+ async def get_media_from_url(
417
+ self,
418
+ url: str,
419
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
420
+ url_info = self.get_url_info(url)
421
+
422
+ if not url_info:
423
+ raise GamdlInterfaceUrlParseError(url)
424
+
425
+ if self.disallowed_media_types and url_info.type in self.disallowed_media_types:
426
+ raise GamdlInterfaceMediaNotAllowedError(
427
+ url_info.type,
428
+ )
429
+
430
+ if url_info.type == "song" or url_info.sub_id:
431
+ async for media in self._get_song_media(
432
+ media_id=url_info.sub_id or url_info.id,
433
+ index=0,
434
+ total=1,
435
+ ):
436
+ yield media
437
+
438
+ elif url_info.type == "music-video":
439
+ async for media in self._get_music_video_media(
440
+ media_id=url_info.id,
441
+ index=0,
442
+ total=1,
443
+ ):
444
+ yield media
445
+
446
+ elif url_info.type == "album" or url_info.library_type == "albums":
447
+ async for media in self._get_album_media(
448
+ media_id=url_info.library_id or url_info.id,
449
+ is_library=bool(url_info.library_type),
450
+ ):
451
+ yield media
452
+
453
+ elif url_info.type == "playlist" or url_info.library_type == "playlist":
454
+ async for media in self._get_playlist_media(
455
+ media_id=url_info.library_id or url_info.id,
456
+ is_library=bool(url_info.library_type),
457
+ ):
458
+ yield media
459
+
460
+ elif url_info.type == "post":
461
+ async for media in self._get_uploaded_video_media(
462
+ media_id=url_info.id,
463
+ ):
464
+ yield media
465
+
466
+ elif url_info.type == "artist":
467
+ async for media in self._get_artist_media(
468
+ media_id=url_info.id,
469
+ ):
470
+ yield media
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  import urllib.parse
3
- from typing import Callable
3
+ from typing import AsyncGenerator, Callable
4
4
 
5
5
  import m3u8
6
6
  import structlog
@@ -378,35 +378,36 @@ class AppleMusicMusicVideoInterface:
378
378
 
379
379
  async def get_media(
380
380
  self,
381
- music_video_metadata: dict,
382
- playlist_metadata: dict | None = None,
383
- playlist_track: dict | None = None,
384
- ) -> AppleMusicMedia:
385
- media = AppleMusicMedia(
386
- media_id=self.base.parse_catalog_media_id(music_video_metadata),
387
- media_metadata=music_video_metadata,
388
- )
381
+ media: AppleMusicMedia,
382
+ ) -> AsyncGenerator[AppleMusicMedia, None]:
383
+ if not media.media_metadata:
384
+ media.media_metadata = (
385
+ await self.base.apple_music_api.get_music_video(media.media_id)
386
+ )["data"][0]
387
+
388
+ media.media_id = self.base.parse_catalog_media_id(media.media_metadata)
389
389
 
390
- if not self.base.is_media_streamable(music_video_metadata):
390
+ yield media
391
+
392
+ if not self.base.is_media_streamable(media.media_metadata):
391
393
  raise GamdlInterfaceMediaNotStreamableError(media.media_id)
392
394
 
393
- if playlist_metadata and playlist_track:
394
- media.playlist_metadata = playlist_metadata
395
+ if media.playlist_metadata:
395
396
  media.playlist_tags = self.base.get_playlist_tags(
396
- playlist_metadata,
397
- playlist_track,
397
+ media.playlist_metadata,
398
+ media.index,
398
399
  )
399
400
 
400
- media.cover = await self.base.get_cover(music_video_metadata)
401
+ media.cover = await self.base.get_cover(media.media_metadata)
401
402
 
402
- itunes_page_metadata = await self.get_itunes_page_metadata(music_video_metadata)
403
+ itunes_page_metadata = await self.get_itunes_page_metadata(media.media_metadata)
403
404
  media.tags = await self.get_tags(
404
- music_video_metadata,
405
+ media.media_metadata,
405
406
  itunes_page_metadata,
406
407
  )
407
408
 
408
409
  media.stream_info = await self.get_stream_info(
409
- music_video_metadata,
410
+ media.media_metadata,
410
411
  itunes_page_metadata,
411
412
  )
412
413
  if not media.stream_info:
@@ -423,4 +424,6 @@ class AppleMusicMusicVideoInterface:
423
424
 
424
425
  media.decryption_key = await self.get_decryption_key(media.stream_info)
425
426
 
426
- return media
427
+ media.partial = False
428
+
429
+ yield media