gamdl 3.6__tar.gz → 3.7__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.6 → gamdl-3.7}/PKG-INFO +8 -5
  2. {gamdl-3.6 → gamdl-3.7}/README.md +7 -4
  3. gamdl-3.7/gamdl/__init__.py +1 -0
  4. {gamdl-3.6 → gamdl-3.7}/gamdl/api/apple_music.py +152 -5
  5. {gamdl-3.6 → gamdl-3.7}/gamdl/api/constants.py +8 -0
  6. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/cli.py +1 -0
  7. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/cli_config.py +8 -0
  8. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/base.py +39 -15
  9. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/song.py +25 -19
  10. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/base.py +16 -11
  11. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/constants.py +2 -2
  12. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/interface.py +44 -32
  13. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/music_video.py +18 -4
  14. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/song.py +120 -96
  15. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/types.py +2 -0
  16. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/uploaded_video.py +2 -1
  17. {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/PKG-INFO +8 -5
  18. {gamdl-3.6 → gamdl-3.7}/pyproject.toml +1 -1
  19. gamdl-3.6/gamdl/__init__.py +0 -1
  20. {gamdl-3.6 → gamdl-3.7}/LICENSE +0 -0
  21. {gamdl-3.6 → gamdl-3.7}/gamdl/__main__.py +0 -0
  22. {gamdl-3.6 → gamdl-3.7}/gamdl/api/__init__.py +0 -0
  23. {gamdl-3.6 → gamdl-3.7}/gamdl/api/exceptions.py +0 -0
  24. {gamdl-3.6 → gamdl-3.7}/gamdl/api/itunes.py +0 -0
  25. {gamdl-3.6 → gamdl-3.7}/gamdl/api/wrapper.py +0 -0
  26. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/__init__.py +0 -0
  27. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/config_file.py +0 -0
  28. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/constants.py +0 -0
  29. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/database.py +0 -0
  30. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/interactive_prompts.py +0 -0
  31. {gamdl-3.6 → gamdl-3.7}/gamdl/cli/utils.py +0 -0
  32. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/__init__.py +0 -0
  33. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/amdecrypt.py +0 -0
  34. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/constants.py +0 -0
  35. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/downloader.py +0 -0
  36. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/enums.py +0 -0
  37. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/exceptions.py +0 -0
  38. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/music_video.py +0 -0
  39. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/types.py +0 -0
  40. {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/uploaded_video.py +0 -0
  41. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/__init__.py +0 -0
  42. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/enums.py +0 -0
  43. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/exceptions.py +0 -0
  44. {gamdl-3.6 → gamdl-3.7}/gamdl/interface/wvd.py +0 -0
  45. {gamdl-3.6 → gamdl-3.7}/gamdl/utils.py +0 -0
  46. {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/SOURCES.txt +0 -0
  47. {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/dependency_links.txt +0 -0
  48. {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/entry_points.txt +0 -0
  49. {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/requires.txt +0 -0
  50. {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/top_level.txt +0 -0
  51. {gamdl-3.6 → gamdl-3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamdl
3
- Version: 3.6
3
+ Version: 3.7
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
@@ -82,6 +82,8 @@ Use [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE/releases/latest) as a f
82
82
 
83
83
  If the executable is not available in your system PATH, set its location with `--nm3u8dlre-path` or `nm3u8dlre_path`.
84
84
 
85
+ N_m3u8DL-RE also needs FFmpeg. If the FFmpeg executable is not available in your system PATH, set its location with `--ffmpeg-path` or `ffmpeg_path`.
86
+
85
87
  ## 📦 Installation
86
88
 
87
89
  1. **Install Gamdl via pip:**
@@ -105,10 +107,10 @@ gamdl [OPTIONS] URLS...
105
107
 
106
108
  ### Supported URL Types
107
109
 
108
- - Songs
109
- - Albums (Public/Library)
110
- - Playlists (Public/Library)
111
- - Music Videos
110
+ - Songs (Catalog/Library)
111
+ - Albums (Catalog/Library)
112
+ - Playlists (Catalog/Library)
113
+ - Music Videos (Catalog/Library)
112
114
  - Artists
113
115
  - Post Videos
114
116
  - Apple Music Classical
@@ -191,6 +193,7 @@ The file is created automatically on first run. Command-line arguments override
191
193
  | `--output-path`, `-o` | Output directory path | `./Apple Music` |
192
194
  | `--temp-path` | Temporary directory path | `.` |
193
195
  | `--nm3u8dlre-path` | N_m3u8DL-RE executable path | `N_m3u8DL-RE` |
196
+ | `--ffmpeg-path` | FFmpeg executable path | `ffmpeg` |
194
197
  | `--download-mode` | Download mode | `ytdlp` |
195
198
  | **Template Options** | | |
196
199
  | `--album-folder-template` | Album folder template | `{album_artist}/{album}` |
@@ -58,6 +58,8 @@ Use [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE/releases/latest) as a f
58
58
 
59
59
  If the executable is not available in your system PATH, set its location with `--nm3u8dlre-path` or `nm3u8dlre_path`.
60
60
 
61
+ N_m3u8DL-RE also needs FFmpeg. If the FFmpeg executable is not available in your system PATH, set its location with `--ffmpeg-path` or `ffmpeg_path`.
62
+
61
63
  ## 📦 Installation
62
64
 
63
65
  1. **Install Gamdl via pip:**
@@ -81,10 +83,10 @@ gamdl [OPTIONS] URLS...
81
83
 
82
84
  ### Supported URL Types
83
85
 
84
- - Songs
85
- - Albums (Public/Library)
86
- - Playlists (Public/Library)
87
- - Music Videos
86
+ - Songs (Catalog/Library)
87
+ - Albums (Catalog/Library)
88
+ - Playlists (Catalog/Library)
89
+ - Music Videos (Catalog/Library)
88
90
  - Artists
89
91
  - Post Videos
90
92
  - Apple Music Classical
@@ -167,6 +169,7 @@ The file is created automatically on first run. Command-line arguments override
167
169
  | `--output-path`, `-o` | Output directory path | `./Apple Music` |
168
170
  | `--temp-path` | Temporary directory path | `.` |
169
171
  | `--nm3u8dlre-path` | N_m3u8DL-RE executable path | `N_m3u8DL-RE` |
172
+ | `--ffmpeg-path` | FFmpeg executable path | `ffmpeg` |
170
173
  | `--download-mode` | Download mode | `ytdlp` |
171
174
  | **Template Options** | | |
172
175
  | `--album-folder-template` | Album folder template | `{album_artist}/{album}` |
@@ -0,0 +1 @@
1
+ __version__ = "3.7"
@@ -15,10 +15,16 @@ from .constants import (
15
15
  APPLE_MUSIC_HOMEPAGE_URL,
16
16
  APPLE_MUSIC_LIBRARY_ALBUM_API_URI,
17
17
  APPLE_MUSIC_LIBRARY_PLAYLIST_API_URI,
18
+ APPLE_MUSIC_LIBRARY_PLAYLISTS_API_URI,
18
19
  APPLE_MUSIC_LICENSE_API_URL,
20
+ APPLE_MUSIC_LIBRARY_MUSIC_VIDEO_API_URI,
19
21
  APPLE_MUSIC_MUSIC_VIDEO_API_URI,
22
+ APPLE_MUSIC_LIBRARY_ALBUMS_API_URI,
20
23
  APPLE_MUSIC_PLAYLIST_API_URI,
21
24
  APPLE_MUSIC_SEARCH_API_URI,
25
+ APPLE_MUSIC_LIBRARY_MUSIC_VIDEOS_API_URI,
26
+ APPLE_MUSIC_LIBRARY_SONG_API_URI,
27
+ APPLE_MUSIC_LIBRARY_SONGS_API_URI,
22
28
  APPLE_MUSIC_SONG_API_URI,
23
29
  APPLE_MUSIC_UPLOADED_VIDEO_API_URL,
24
30
  APPLE_MUSIC_WEBPLAYBACK_API_URL,
@@ -426,9 +432,54 @@ class AppleMusicApi:
426
432
 
427
433
  return artist
428
434
 
435
+ async def get_library_song(
436
+ self,
437
+ song_id: str,
438
+ include: str = "catalog",
439
+ extend: str = "extendedAssetUrls",
440
+ ) -> dict:
441
+ log = logger.bind(action="get_library_song", song_id=song_id)
442
+
443
+ song = await self._amp_request(
444
+ APPLE_MUSIC_LIBRARY_SONG_API_URI.format(
445
+ song_id=song_id,
446
+ ),
447
+ {
448
+ "include": include,
449
+ "extend": extend,
450
+ },
451
+ )
452
+
453
+ log.debug("success", song=song)
454
+
455
+ return song
456
+
457
+ async def get_library_music_video(
458
+ self,
459
+ music_video_id: str,
460
+ include: str = "catalog",
461
+ ) -> dict:
462
+ log = logger.bind(
463
+ action="get_library_music_video", music_video_id=music_video_id
464
+ )
465
+
466
+ music_video = await self._amp_request(
467
+ APPLE_MUSIC_LIBRARY_MUSIC_VIDEO_API_URI.format(
468
+ music_video_id=music_video_id,
469
+ ),
470
+ {
471
+ "include": include,
472
+ },
473
+ )
474
+
475
+ log.debug("success", music_video=music_video)
476
+
477
+ return music_video
478
+
429
479
  async def get_library_album(
430
480
  self,
431
481
  album_id: str,
482
+ include: str = "catalog",
432
483
  extend: str = "extendedAssetUrls",
433
484
  ) -> dict:
434
485
  log = logger.bind(action="get_library_album", album_id=album_id)
@@ -438,6 +489,7 @@ class AppleMusicApi:
438
489
  album_id=album_id,
439
490
  ),
440
491
  {
492
+ "include": include,
441
493
  "extend": extend,
442
494
  },
443
495
  )
@@ -449,7 +501,7 @@ class AppleMusicApi:
449
501
  async def get_library_playlist(
450
502
  self,
451
503
  playlist_id: str,
452
- include: str = "tracks",
504
+ include: str = "catalog,tracks",
453
505
  limit: int = 100,
454
506
  extend: str = "extendedAssetUrls",
455
507
  ) -> dict:
@@ -470,6 +522,92 @@ class AppleMusicApi:
470
522
 
471
523
  return playlist
472
524
 
525
+ async def get_library_songs(
526
+ self,
527
+ limit: int = 100,
528
+ offset: int = 0,
529
+ include: str = "catalog",
530
+ extend: str = "extendedAssetUrls",
531
+ ) -> dict:
532
+ log = logger.bind(action="get_library_songs")
533
+
534
+ library_songs = await self._amp_request(
535
+ APPLE_MUSIC_LIBRARY_SONGS_API_URI,
536
+ {
537
+ "limit": limit,
538
+ "offset": offset,
539
+ "include": include,
540
+ "extend": extend,
541
+ },
542
+ )
543
+
544
+ log.debug("success", library_songs=library_songs)
545
+
546
+ return library_songs
547
+
548
+ async def get_library_music_videos(
549
+ self,
550
+ limit: int = 100,
551
+ offset: int = 0,
552
+ include: str = "catalog",
553
+ ) -> dict:
554
+ log = logger.bind(action="get_library_music_videos")
555
+
556
+ library_music_videos = await self._amp_request(
557
+ APPLE_MUSIC_LIBRARY_MUSIC_VIDEOS_API_URI,
558
+ {
559
+ "limit": limit,
560
+ "offset": offset,
561
+ "include": include,
562
+ },
563
+ )
564
+
565
+ log.debug("success", library_music_videos=library_music_videos)
566
+
567
+ return library_music_videos
568
+
569
+ async def get_library_albums(
570
+ self,
571
+ limit: int = 100,
572
+ offset: int = 0,
573
+ include: str = "catalog",
574
+ ) -> dict:
575
+ log = logger.bind(action="get_library_albums")
576
+
577
+ library_albums = await self._amp_request(
578
+ APPLE_MUSIC_LIBRARY_ALBUMS_API_URI,
579
+ {
580
+ "limit": limit,
581
+ "offset": offset,
582
+ "include": include,
583
+ },
584
+ )
585
+
586
+ log.debug("success", library_albums=library_albums)
587
+
588
+ return library_albums
589
+
590
+ async def get_library_playlists(
591
+ self,
592
+ limit: int = 100,
593
+ offset: int = 0,
594
+ include: str = "catalog",
595
+ ) -> dict:
596
+ log = logger.bind(action="get_library_playlists")
597
+
598
+ library_playlists = await self._amp_request(
599
+ APPLE_MUSIC_LIBRARY_PLAYLISTS_API_URI,
600
+ {
601
+ "limit": limit,
602
+ "offset": offset,
603
+ "include": include,
604
+ },
605
+ )
606
+
607
+ log.debug("success", library_playlists=library_playlists)
608
+
609
+ return library_playlists
610
+
473
611
  async def get_search_results(
474
612
  self,
475
613
  term: str,
@@ -531,17 +669,26 @@ class AppleMusicApi:
531
669
  async def get_webplayback(
532
670
  self,
533
671
  track_id: str,
672
+ is_library: bool = False,
534
673
  ) -> dict:
535
674
  log = logger.bind(action="get_webplayback", track_id=track_id)
536
675
 
537
676
  response = None
677
+
678
+ if is_library:
679
+ request_body = {
680
+ "universalLibraryId": track_id,
681
+ }
682
+ else:
683
+ request_body = {
684
+ "salableAdamId": track_id,
685
+ }
686
+ request_body["language"] = self.language
687
+
538
688
  try:
539
689
  response = await self.client.post(
540
690
  APPLE_MUSIC_WEBPLAYBACK_API_URL,
541
- json={
542
- "salableAdamId": track_id,
543
- "language": self.language,
544
- },
691
+ json=request_body,
545
692
  )
546
693
  response.raise_for_status()
547
694
  webplayback = response.json()
@@ -14,9 +14,17 @@ APPLE_MUSIC_UPLOADED_VIDEO_API_URL = (
14
14
  APPLE_MUSIC_ALBUM_API_URI = "/v1/catalog/{storefront}/albums/{album_id}"
15
15
  APPLE_MUSIC_PLAYLIST_API_URI = "/v1/catalog/{storefront}/playlists/{playlist_id}"
16
16
  APPLE_MUSIC_ARTIST_API_URI = "/v1/catalog/{storefront}/artists/{artist_id}"
17
+ APPLE_MUSIC_LIBRARY_SONG_API_URI = "/v1/me/library/songs/{song_id}"
18
+ APPLE_MUSIC_LIBRARY_MUSIC_VIDEO_API_URI = (
19
+ "/v1/me/library/music-videos/{music_video_id}"
20
+ )
17
21
  APPLE_MUSIC_LIBRARY_ALBUM_API_URI = "/v1/me/library/albums/{album_id}"
18
22
  APPLE_MUSIC_LIBRARY_PLAYLIST_API_URI = "/v1/me/library/playlists/{playlist_id}"
19
23
  APPLE_MUSIC_SEARCH_API_URI = "/v1/catalog/{storefront}/search"
24
+ APPLE_MUSIC_LIBRARY_SONGS_API_URI = "/v1/me/library/songs"
25
+ APPLE_MUSIC_LIBRARY_MUSIC_VIDEOS_API_URI = "/v1/me/library/music-videos"
26
+ APPLE_MUSIC_LIBRARY_ALBUMS_API_URI = "/v1/me/library/albums"
27
+ APPLE_MUSIC_LIBRARY_PLAYLISTS_API_URI = "/v1/me/library/playlists"
20
28
 
21
29
  APPLE_MUSIC_WEBPLAYBACK_API_URL = (
22
30
  "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/webPlayback"
@@ -176,6 +176,7 @@ async def main(config: CliConfig):
176
176
  output_path=config.output_path,
177
177
  temp_path=config.temp_path,
178
178
  nm3u8dlre_path=config.nm3u8dlre_path,
179
+ ffmpeg_path=config.ffmpeg_path,
179
180
  download_mode=config.download_mode,
180
181
  album_folder_template=config.album_folder_template,
181
182
  compilation_folder_template=config.compilation_folder_template,
@@ -313,6 +313,14 @@ class CliConfig:
313
313
  default=base_downloader_sig.parameters["nm3u8dlre_path"].default,
314
314
  ),
315
315
  ]
316
+ ffmpeg_path: Annotated[
317
+ str,
318
+ option(
319
+ "--ffmpeg-path",
320
+ help="FFmpeg executable path",
321
+ default=base_downloader_sig.parameters["ffmpeg_path"].default,
322
+ ),
323
+ ]
316
324
  download_mode: Annotated[
317
325
  DownloadMode,
318
326
  option(
@@ -11,6 +11,7 @@ from mutagen.mp4 import MP4, MP4Cover
11
11
  from yt_dlp import YoutubeDL
12
12
 
13
13
  from ..interface.enums import CoverFormat
14
+ from yt_dlp.downloader.http import HttpFD
14
15
  from ..interface.interface import AppleMusicInterface
15
16
  from ..interface.types import MediaTags, PlaylistTags
16
17
  from ..utils import CustomStringFormatter, async_subprocess
@@ -27,19 +28,34 @@ def _download_ytdlp_process(
27
28
  result_queue,
28
29
  ) -> None:
29
30
  try:
30
- with YoutubeDL(
31
- {
32
- "quiet": True,
33
- "no_warnings": True,
34
- "outtmpl": download_path,
35
- "allow_unplayable_formats": True,
36
- "overwrites": True,
37
- "fixup": "never",
38
- "noprogress": silent,
39
- "allowed_extractors": ["generic"],
40
- }
41
- ) as ydl:
42
- ydl.download(stream_url)
31
+ common_args = {
32
+ "quiet": True,
33
+ "no_warnings": True,
34
+ "noprogress": silent,
35
+ }
36
+
37
+ if stream_url.split("?")[0].endswith(".m3u8"):
38
+ with YoutubeDL(
39
+ {
40
+ **common_args,
41
+ "outtmpl": download_path,
42
+ "allow_unplayable_formats": True,
43
+ "overwrites": True,
44
+ "fixup": "never",
45
+ "allowed_extractors": ["generic"],
46
+ }
47
+ ) as ydl:
48
+ ydl.download(stream_url)
49
+ else:
50
+ Path(download_path).parent.mkdir(parents=True, exist_ok=True)
51
+ with YoutubeDL(common_args) as ydl:
52
+ http_downloader = HttpFD(ydl, ydl.params)
53
+ http_downloader.download(
54
+ download_path,
55
+ {
56
+ "url": stream_url,
57
+ },
58
+ )
43
59
  except Exception as e:
44
60
  result_queue.put(("error", repr(e), traceback.format_exc()))
45
61
 
@@ -51,6 +67,7 @@ class AppleMusicBaseDownloader:
51
67
  output_path: str = "./Apple Music",
52
68
  temp_path: str = ".",
53
69
  nm3u8dlre_path: str = "N_m3u8DL-RE",
70
+ ffmpeg_path: str = "ffmpeg",
54
71
  download_mode: DownloadMode = DownloadMode.YTDLP,
55
72
  album_folder_template: str = "{album_artist}/{album}",
56
73
  compilation_folder_template: str = "Compilations/{album}",
@@ -69,6 +86,7 @@ class AppleMusicBaseDownloader:
69
86
  self.output_path = output_path
70
87
  self.temp_path = temp_path
71
88
  self.nm3u8dlre_path = nm3u8dlre_path
89
+ self.ffmpeg_path = ffmpeg_path
72
90
  self.download_mode = download_mode
73
91
  self.album_folder_template = album_folder_template
74
92
  self.compilation_folder_template = compilation_folder_template
@@ -89,10 +107,12 @@ class AppleMusicBaseDownloader:
89
107
  log = logger.bind(action="initialize_binary_paths")
90
108
 
91
109
  self.full_nm3u8dlre_path = shutil.which(self.nm3u8dlre_path)
110
+ self.full_ffmpeg_path = shutil.which(self.ffmpeg_path)
92
111
 
93
112
  log = log.debug(
94
113
  "success",
95
114
  full_nm3u8dlre_path=self.full_nm3u8dlre_path,
115
+ full_ffmpeg_path=self.full_ffmpeg_path,
96
116
  )
97
117
 
98
118
  def get_temp_path(
@@ -218,10 +238,12 @@ class AppleMusicBaseDownloader:
218
238
  action="download_stream", stream_url=stream_url, download_path=download_path
219
239
  )
220
240
 
221
- if self.download_mode == DownloadMode.YTDLP:
241
+ if self.download_mode == DownloadMode.YTDLP or not stream_url.split("?")[
242
+ 0
243
+ ].endswith(".m3u8"):
222
244
  await self._download_ytdlp_async(stream_url, download_path)
223
245
 
224
- if self.download_mode == DownloadMode.NM3U8DLRE:
246
+ elif self.download_mode == DownloadMode.NM3U8DLRE:
225
247
  await self._download_nm3u8dlre(stream_url, download_path)
226
248
 
227
249
  log.debug("success")
@@ -273,6 +295,8 @@ class AppleMusicBaseDownloader:
273
295
  "--no-log",
274
296
  "--log-level",
275
297
  "off",
298
+ "--ffmpeg-binary-path",
299
+ self.full_ffmpeg_path,
276
300
  "--save-name",
277
301
  download_path_obj.stem,
278
302
  "--save-dir",
@@ -159,26 +159,32 @@ class AppleMusicSongDownloader:
159
159
  self,
160
160
  download_item: DownloadItem,
161
161
  ) -> None:
162
- encrypted_path = self.base.get_temp_path(
163
- download_item.media.media_metadata["id"],
164
- download_item.uuid_,
165
- "encrypted",
166
- ".m4a",
167
- )
168
- await self.base.download_stream(
169
- download_item.media.stream_info.audio_track.stream_url,
170
- encrypted_path,
171
- )
162
+ if download_item.media.stream_info.audio_track.drm_free:
163
+ await self.base.download_stream(
164
+ download_item.media.stream_info.audio_track.stream_url,
165
+ download_item.staged_path,
166
+ )
167
+ else:
168
+ encrypted_path = self.base.get_temp_path(
169
+ download_item.media.media_metadata["id"],
170
+ download_item.uuid_,
171
+ "encrypted",
172
+ ".m4a",
173
+ )
174
+ await self.base.download_stream(
175
+ download_item.media.stream_info.audio_track.stream_url,
176
+ encrypted_path,
177
+ )
172
178
 
173
- await self.stage(
174
- encrypted_path,
175
- download_item.staged_path,
176
- download_item.media.media_id,
177
- download_item.media.decryption_key,
178
- download_item.media.stream_info.audio_track.fairplay_key,
179
- download_item.media.stream_info.audio_track.use_cenc,
180
- download_item.media.stream_info.audio_track.use_single_content_key,
181
- )
179
+ await self.stage(
180
+ encrypted_path,
181
+ download_item.staged_path,
182
+ download_item.media.media_id,
183
+ download_item.media.decryption_key,
184
+ download_item.media.stream_info.audio_track.fairplay_key,
185
+ download_item.media.stream_info.audio_track.use_cenc,
186
+ download_item.media.stream_info.audio_track.use_single_content_key,
187
+ )
182
188
 
183
189
  cover_bytes = (
184
190
  await self.base.interface.base.get_cover_bytes(
@@ -56,11 +56,6 @@ class AppleMusicBaseInterface:
56
56
  ) -> bool:
57
57
  return bool(media_metadata["attributes"].get("playParams"))
58
58
 
59
- @staticmethod
60
- def parse_catalog_media_id(media_metadata: dict) -> str:
61
- play_params = media_metadata["attributes"].get("playParams", {})
62
- return play_params.get("catalogId", media_metadata["id"])
63
-
64
59
  @staticmethod
65
60
  def parse_media_id_from_url(media_metadata: dict) -> str | None:
66
61
  media_url = media_metadata["attributes"].get("url")
@@ -118,6 +113,14 @@ class AppleMusicBaseInterface:
118
113
  template_cover_url,
119
114
  )
120
115
 
116
+ @staticmethod
117
+ def get_catalog_metadata_from_library(library_metadata: dict) -> dict | None:
118
+ data = library_metadata.get("relationships", {}).get("catalog", {}).get("data")
119
+ if not data:
120
+ return None
121
+
122
+ return data[0]
123
+
121
124
  @classmethod
122
125
  async def create(
123
126
  cls,
@@ -263,9 +266,7 @@ class AppleMusicBaseInterface:
263
266
  self,
264
267
  metadata: dict,
265
268
  ) -> str:
266
- log = logger.bind(
267
- action="get_cover", media_id=self.parse_catalog_media_id(metadata)
268
- )
269
+ log = logger.bind(action="get_cover", media_id=metadata["id"])
269
270
 
270
271
  template_url = self._get_cover_template_url(metadata)
271
272
 
@@ -352,7 +353,9 @@ class AppleMusicBaseInterface:
352
353
  ),
353
354
  album_sort=asset_data.get("sort-album"),
354
355
  artist=asset_data["artistName"],
355
- artist_id=int(asset_data["artistId"]),
356
+ artist_id=(
357
+ int(asset_data["artistId"]) if asset_data.get("artistId") else None
358
+ ),
356
359
  artist_sort=asset_data["sort-artist"],
357
360
  comment=asset_data.get("comments"),
358
361
  compilation=asset_data.get("compilation"),
@@ -377,7 +380,9 @@ class AppleMusicBaseInterface:
377
380
  disc_total=asset_data.get("discCount"),
378
381
  gapless=asset_data.get("gapless"),
379
382
  genre=asset_data.get("genre"),
380
- genre_id=int(asset_data["genreId"]),
383
+ genre_id=(
384
+ int(asset_data["genreId"]) if asset_data.get("genreId") else None
385
+ ),
381
386
  lyrics=lyrics if lyrics else None,
382
387
  media_type=(
383
388
  MediaType.SONG
@@ -385,7 +390,7 @@ class AppleMusicBaseInterface:
385
390
  else MediaType.MUSIC_VIDEO
386
391
  ),
387
392
  rating=MediaRating(asset_data["explicit"]),
388
- storefront=asset_data["s"],
393
+ storefront=(int(asset_data["s"]) if asset_data.get("s") else None),
389
394
  title=asset_data["itemName"],
390
395
  title_id=int(asset_data["itemId"]),
391
396
  title_sort=asset_data["sort-name"],
@@ -71,8 +71,8 @@ VALID_URL_PATTERN = re.compile(
71
71
  r"(?:\?i=(?P<sub_id>[0-9]+))?"
72
72
  r"|"
73
73
  r"(?:/(?P<library_storefront>[a-z]{2}))?"
74
- r"/library/(?P<library_type>playlist|albums)"
75
- r"/(?P<library_id>p\.[a-zA-Z0-9]+|l\.[a-zA-Z0-9]+)"
74
+ r"/library/(?P<library_type>playlist|albums|songs|music-videos)"
75
+ r"/(?P<library_id>[pli]\.[a-zA-Z0-9]+)"
76
76
  r")"
77
77
  )
78
78
 
@@ -101,19 +101,16 @@ class AppleMusicInterface:
101
101
  total: int | None = None,
102
102
  media_metadata: dict | None = None,
103
103
  playlist_metadata: dict | None = None,
104
+ is_library: bool = False,
104
105
  ) -> AsyncGenerator[AppleMusicMedia, None]:
105
106
  media = AppleMusicMedia(
106
107
  media_id=media_id,
108
+ is_library=is_library,
109
+ index=index,
110
+ total=total,
111
+ media_metadata=media_metadata,
112
+ playlist_metadata=playlist_metadata,
107
113
  )
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
114
  try:
118
115
  async for media in self.song.get_media(media):
119
116
  yield media
@@ -133,16 +130,17 @@ class AppleMusicInterface:
133
130
  total: int | None = None,
134
131
  media_metadata: dict | None = None,
135
132
  playlist_metadata: dict | None = None,
133
+ is_library: bool = False,
136
134
  ) -> AsyncGenerator[AppleMusicMedia, None]:
137
135
  media = AppleMusicMedia(
138
136
  media_id=media_id,
137
+ is_library=is_library,
138
+ index=index,
139
+ total=total,
140
+ media_metadata=media_metadata,
141
+ playlist_metadata=playlist_metadata,
139
142
  )
140
143
 
141
- if index is not None:
142
- media.index = index
143
- if total is not None:
144
- media.total = total
145
-
146
144
  media.media_metadata = media_metadata
147
145
  media.playlist_metadata = playlist_metadata
148
146
 
@@ -187,12 +185,14 @@ class AppleMusicInterface:
187
185
 
188
186
  try:
189
187
  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,
188
+ await (
189
+ self.base.apple_music_api.get_library_album(
190
+ media_id,
191
+ )
192
+ if is_library
193
+ else self.base.apple_music_api.get_album(
194
+ media_id,
195
+ )
196
196
  )
197
197
  )["data"][0]
198
198
 
@@ -214,6 +214,7 @@ class AppleMusicInterface:
214
214
  index=index,
215
215
  total=base_media.media_metadata["attributes"]["trackCount"],
216
216
  media_metadata=track,
217
+ is_library=is_library,
217
218
  )
218
219
  if track["type"] in {"songs", "library-songs"}
219
220
  else self._get_music_video_media(
@@ -221,6 +222,7 @@ class AppleMusicInterface:
221
222
  index=index,
222
223
  total=base_media.media_metadata["attributes"]["trackCount"],
223
224
  media_metadata=track,
225
+ is_library=is_library,
224
226
  )
225
227
  )
226
228
  for index, track in enumerate(tracks)
@@ -246,12 +248,14 @@ class AppleMusicInterface:
246
248
 
247
249
  try:
248
250
  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,
251
+ await (
252
+ self.base.apple_music_api.get_library_playlist(
253
+ media_id,
254
+ )
255
+ if is_library
256
+ else self.base.apple_music_api.get_playlist(
257
+ media_id,
258
+ )
255
259
  )
256
260
  )["data"][0]
257
261
 
@@ -283,6 +287,7 @@ class AppleMusicInterface:
283
287
  index=index,
284
288
  media_metadata=track,
285
289
  playlist_metadata=base_media.media_metadata,
290
+ is_library=is_library,
286
291
  )
287
292
  if track["type"] in {"songs", "library-songs"}
288
293
  else self._get_music_video_media(
@@ -290,6 +295,7 @@ class AppleMusicInterface:
290
295
  index=index,
291
296
  media_metadata=track,
292
297
  playlist_metadata=base_media.media_metadata,
298
+ is_library=is_library,
293
299
  )
294
300
  )
295
301
  for index, track in enumerate(tracks)
@@ -425,32 +431,38 @@ class AppleMusicInterface:
425
431
  url_info.type,
426
432
  )
427
433
 
428
- if url_info.type == "song" or url_info.sub_id:
434
+ if (
435
+ url_info.type == "song"
436
+ or url_info.library_type == "songs"
437
+ or url_info.sub_id
438
+ ):
429
439
  async for media in self._get_song_media(
430
- media_id=url_info.sub_id or url_info.id,
440
+ media_id=url_info.sub_id or url_info.id or url_info.library_id,
431
441
  index=0,
432
442
  total=1,
443
+ is_library=bool(url_info.library_type),
433
444
  ):
434
445
  yield media
435
446
 
436
- elif url_info.type == "music-video":
447
+ elif url_info.type == "music-video" or url_info.library_type == "music-videos":
437
448
  async for media in self._get_music_video_media(
438
- media_id=url_info.id,
449
+ media_id=url_info.id or url_info.library_id,
439
450
  index=0,
440
451
  total=1,
452
+ is_library=bool(url_info.library_type),
441
453
  ):
442
454
  yield media
443
455
 
444
456
  elif url_info.type == "album" or url_info.library_type == "albums":
445
457
  async for media in self._get_album_media(
446
- media_id=url_info.library_id or url_info.id,
458
+ media_id=url_info.id or url_info.library_id,
447
459
  is_library=bool(url_info.library_type),
448
460
  ):
449
461
  yield media
450
462
 
451
463
  elif url_info.type == "playlist" or url_info.library_type == "playlist":
452
464
  async for media in self._get_playlist_media(
453
- media_id=url_info.library_id or url_info.id,
465
+ media_id=url_info.id or url_info.library_id,
454
466
  is_library=bool(url_info.library_type),
455
467
  ):
456
468
  yield media
@@ -97,7 +97,7 @@ class AppleMusicMusicVideoInterface:
97
97
  ) -> MediaTags:
98
98
  log = logger.bind(
99
99
  action="get_music_video_tags",
100
- media_id=self.base.parse_catalog_media_id(metadata),
100
+ media_id=metadata["id"],
101
101
  )
102
102
 
103
103
  url_media_id = self.base.parse_media_id_from_url(metadata)
@@ -154,7 +154,7 @@ class AppleMusicMusicVideoInterface:
154
154
  ) -> StreamInfoAv | None:
155
155
  log = logger.bind(
156
156
  action="get_music_video_stream_info",
157
- media_id=self.base.parse_catalog_media_id(metadata),
157
+ media_id=metadata["id"],
158
158
  )
159
159
 
160
160
  url_media_id = self.base.parse_media_id_from_url(metadata)
@@ -394,10 +394,24 @@ class AppleMusicMusicVideoInterface:
394
394
  ) -> AsyncGenerator[AppleMusicMedia, None]:
395
395
  if not media.media_metadata:
396
396
  media.media_metadata = (
397
- await self.base.apple_music_api.get_music_video(media.media_id)
397
+ await (
398
+ self.base.apple_music_api.get_library_music_video(media.media_id)
399
+ if media.is_library
400
+ else self.base.apple_music_api.get_music_video(media.media_id)
401
+ )
398
402
  )["data"][0]
399
403
 
400
- media.media_id = self.base.parse_catalog_media_id(media.media_metadata)
404
+ if media.media_metadata["attributes"]["playParams"].get("isLibrary"):
405
+ catalog_metadata = self.base.get_catalog_metadata_from_library(
406
+ media.media_metadata
407
+ )
408
+ if catalog_metadata:
409
+ media.media_id = catalog_metadata["id"]
410
+ media.is_library = False
411
+ media.media_metadata = catalog_metadata
412
+
413
+ if media.is_library:
414
+ raise GamdlInterfaceMediaNotStreamableError(media.media_id)
401
415
 
402
416
  yield media
403
417
 
@@ -3,7 +3,6 @@ import base64
3
3
  import datetime
4
4
  import json
5
5
  import re
6
- import struct
7
6
  from typing import AsyncGenerator, Callable
8
7
  from xml.dom import minidom
9
8
  from xml.etree import ElementTree
@@ -28,7 +27,6 @@ from .types import (
28
27
  StreamInfo,
29
28
  StreamInfoAv,
30
29
  )
31
- import httpx
32
30
 
33
31
  logger = structlog.get_logger(__name__)
34
32
 
@@ -56,9 +54,13 @@ class AppleMusicSongInterface:
56
54
  ) -> Lyrics | None:
57
55
  log = logger.bind(
58
56
  action="get_lyrics",
59
- song_id=self.base.parse_catalog_media_id(song_metadata),
57
+ song_id=song_metadata["id"],
60
58
  )
61
59
 
60
+ if song_metadata["attributes"]["playParams"].get("isLibrary"):
61
+ log.debug("library_song_no_lyrics")
62
+ return None
63
+
62
64
  if not song_metadata["attributes"]["hasLyrics"]:
63
65
  log.debug("no_lyrics")
64
66
  return None
@@ -69,7 +71,7 @@ class AppleMusicSongInterface:
69
71
  ):
70
72
  song_metadata = (
71
73
  await self.base.apple_music_api.get_song(
72
- self.base.parse_catalog_media_id(song_metadata)
74
+ song_metadata["id"],
73
75
  )
74
76
  )["data"][0]
75
77
 
@@ -193,87 +195,66 @@ class AppleMusicSongInterface:
193
195
  def _get_m3u8_from_playback(self, playback: dict) -> str | None:
194
196
  return playback["songList"][0].get("hls-playlist-url")
195
197
 
196
- async def get_tags(
198
+ async def get_m3u8_master_url(
197
199
  self,
198
- asset_data: dict,
199
- lyrics: str | None = None,
200
- ) -> MediaTags:
201
- log = logger.bind(action="get_song_tags")
202
-
203
- tags = MediaTags(
204
- album=asset_data["playlistName"],
205
- album_artist=asset_data["playlistArtistName"],
206
- album_id=int(asset_data["playlistId"]),
207
- album_sort=asset_data["sort-album"],
208
- artist=asset_data["artistName"],
209
- artist_id=int(asset_data["artistId"]),
210
- artist_sort=asset_data["sort-artist"],
211
- comment=asset_data.get("comments"),
212
- compilation=asset_data["compilation"],
213
- composer=asset_data.get("composerName"),
214
- composer_id=(
215
- int(asset_data.get("composerId"))
216
- if asset_data.get("composerId")
217
- else None
218
- ),
219
- composer_sort=asset_data.get("sort-composer"),
220
- copyright=asset_data.get("copyright"),
221
- date=(
222
- await self.base.get_media_date(asset_data["playlistId"])
223
- if self.use_album_date
224
- else (
225
- self.base.parse_date(asset_data["releaseDate"])
226
- if asset_data.get("releaseDate")
227
- else None
228
- )
229
- ),
230
- disc=asset_data["discNumber"],
231
- disc_total=asset_data["discCount"],
232
- gapless=asset_data["gapless"],
233
- genre=asset_data.get("genre"),
234
- genre_id=int(asset_data["genreId"]),
235
- lyrics=lyrics if lyrics else None,
236
- media_type=MediaType.SONG,
237
- rating=MediaRating(asset_data["explicit"]),
238
- storefront=asset_data["s"],
239
- title=asset_data["itemName"],
240
- title_id=int(asset_data["itemId"]),
241
- title_sort=asset_data["sort-name"],
242
- track=asset_data["trackNumber"],
243
- track_total=asset_data["trackCount"],
244
- xid=asset_data.get("xid"),
245
- )
200
+ playback: dict | None,
201
+ song_metadata: dict | None,
202
+ ) -> str | None:
203
+ if playback:
204
+ return self._get_m3u8_from_playback(playback)
205
+ else:
206
+ return await self._get_m3u8_master_url_from_metadata(song_metadata)
246
207
 
247
- log.debug("success", tags=tags)
208
+ async def _get_m3u8_master_url_from_metadata(
209
+ self,
210
+ song_metadata: dict,
211
+ ) -> str | None:
212
+ log = logger.bind(
213
+ action="get_m3u8_master_url_from_metadata",
214
+ song_id=song_metadata["id"],
215
+ )
248
216
 
249
- return tags
217
+ if song_metadata["attributes"]["playParams"].get("isLibrary"):
218
+ log.debug("library_song_no_m3u8_master_url")
219
+ return None
250
220
 
251
- async def _get_m3u8_from_metadata(self, song_metadata: dict) -> str | None:
252
221
  if "extendedAssetUrls" not in song_metadata["attributes"]:
253
222
  song_metadata = (
254
223
  await self.base.apple_music_api.get_song(
255
- self.base.parse_catalog_media_id(song_metadata),
224
+ song_metadata["id"],
256
225
  )
257
226
  )["data"][0]
258
227
 
259
- return song_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")
228
+ enhanced = song_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")
229
+
230
+ if enhanced:
231
+ log.debug("success", m3u8_master_url=enhanced)
232
+ return enhanced
233
+
234
+ log.debug("no_m3u8_master_url")
235
+
236
+ return None
260
237
 
261
238
  async def get_stream_info(
262
239
  self,
263
240
  media_id: str,
241
+ is_library: bool,
264
242
  m3u8_master_url: str | None = None,
265
243
  webplayback: dict | None = None,
266
244
  ) -> StreamInfoAv:
267
245
  stream_info = None
268
246
 
269
- for codec in self.codec_priority:
270
- if codec.is_web:
271
- stream_info = await self._get_web_stream_info(webplayback, codec)
272
- else:
273
- stream_info = await self._get_stream_info(m3u8_master_url, codec)
247
+ if is_library and webplayback:
248
+ stream_info = await self._get_library_stream_info(webplayback)
249
+ elif webplayback or m3u8_master_url:
250
+ for codec in self.codec_priority:
251
+ if codec.is_web:
252
+ stream_info = await self._get_web_stream_info(webplayback, codec)
253
+ else:
254
+ stream_info = await self._get_stream_info(m3u8_master_url, codec)
274
255
 
275
- if stream_info:
276
- break
256
+ if stream_info:
257
+ break
277
258
 
278
259
  if not stream_info:
279
260
  raise GamdlInterfaceFormatNotAvailableError(
@@ -481,16 +462,51 @@ class AppleMusicSongInterface:
481
462
 
482
463
  return stream_info_av
483
464
 
465
+ async def _get_library_stream_info(
466
+ self,
467
+ webplayback: dict,
468
+ ) -> StreamInfoAv | None:
469
+ log = logger.bind(action="get_library_song_stream_info")
470
+
471
+ stream_info = StreamInfo(drm_free=True)
472
+
473
+ if len(webplayback["songList"][0]["assets"]) == 0:
474
+ log.debug("no_matching_asset")
475
+ return None
476
+ asset = webplayback["songList"][0]["assets"][0]
477
+
478
+ stream_info.stream_url = asset["URL"]
479
+
480
+ stream_info_av = StreamInfoAv(
481
+ media_id=webplayback["songList"][0]["songId"],
482
+ audio_track=stream_info,
483
+ file_format=MediaFileFormat.M4A,
484
+ )
485
+ log.debug("success", stream_info=stream_info_av)
486
+
487
+ return stream_info_av
488
+
484
489
  async def get_media(
485
490
  self,
486
491
  media: AppleMusicMedia,
487
492
  ) -> AsyncGenerator[AppleMusicMedia, None]:
488
493
  if not media.media_metadata:
489
494
  media.media_metadata = (
490
- await self.base.apple_music_api.get_song(media.media_id)
495
+ await (
496
+ self.base.apple_music_api.get_library_song(media.media_id)
497
+ if media.is_library
498
+ else self.base.apple_music_api.get_song(media.media_id)
499
+ )
491
500
  )["data"][0]
492
501
 
493
- media.media_id = self.base.parse_catalog_media_id(media.media_metadata)
502
+ if media.media_metadata["attributes"]["playParams"].get("isLibrary"):
503
+ catalog_metadata = self.base.get_catalog_metadata_from_library(
504
+ media.media_metadata
505
+ )
506
+ if catalog_metadata:
507
+ media.media_id = catalog_metadata["id"]
508
+ media.is_library = False
509
+ media.media_metadata = catalog_metadata
494
510
 
495
511
  yield media
496
512
 
@@ -510,45 +526,54 @@ class AppleMusicSongInterface:
510
526
  media.lyrics = await self.get_lyrics(media.media_metadata)
511
527
 
512
528
  if self.base.wrapper_api:
513
- playback = await self.base.wrapper_api.get_playback(media.media_id)
514
- media.tags = await self.base.get_tags_from_asset_info(
515
- playback["songList"][0]["assets"][0]["metadata"],
516
- media.lyrics.unsynced if media.lyrics else None,
517
- self.use_album_date,
529
+ playback = (
530
+ await self.base.wrapper_api.get_playback(media.media_id)
531
+ if not media.is_library
532
+ else None
518
533
  )
519
- if not self.skip_stream_info:
520
- m3u8_master_url = self._get_m3u8_from_playback(playback)
521
- webplayback = (
522
- await self.base.apple_music_api.get_webplayback(media.media_id)
523
- if any(codec.is_web for codec in self.codec_priority)
524
- else None
525
- )
526
- media.stream_info = await self.get_stream_info(
534
+ webplayback = (
535
+ await self.base.apple_music_api.get_webplayback(
527
536
  media.media_id,
528
- m3u8_master_url,
529
- webplayback,
537
+ media.is_library,
530
538
  )
539
+ if media.is_library
540
+ or any(codec.is_web for codec in self.codec_priority)
541
+ else None
542
+ )
531
543
  else:
544
+ playback = None
532
545
  webplayback = await self.base.apple_music_api.get_webplayback(
533
- media.media_id
546
+ media.media_id,
547
+ media.is_library,
534
548
  )
549
+
550
+ if playback:
551
+ media.tags = self.base.get_tags_from_asset_info(
552
+ playback["songList"][0]["assets"][0]["metadata"],
553
+ media.lyrics.unsynced if media.lyrics else None,
554
+ )
555
+ else:
535
556
  media.tags = await self.base.get_tags_from_asset_info(
536
557
  webplayback["songList"][0]["assets"][0]["metadata"],
537
558
  media.lyrics.unsynced if media.lyrics else None,
538
- self.use_album_date,
539
559
  )
540
- if not self.skip_stream_info:
541
- m3u8_master_url = await self._get_m3u8_from_metadata(
542
- media.media_metadata
543
- )
544
- media.stream_info = await self.get_stream_info(
545
- media.media_id,
546
- m3u8_master_url,
547
- webplayback,
548
- )
549
560
 
550
- if media.stream_info:
551
- if (
561
+ if not self.skip_stream_info:
562
+ m3u8_master_url = await self.get_m3u8_master_url(
563
+ playback,
564
+ media.media_metadata,
565
+ )
566
+
567
+ media.stream_info = await self.get_stream_info(
568
+ media.media_id,
569
+ media.is_library,
570
+ m3u8_master_url,
571
+ webplayback,
572
+ )
573
+
574
+ if media.stream_info.audio_track.drm_free:
575
+ pass
576
+ elif (
552
577
  not self.base.wrapper_api
553
578
  and not media.stream_info.audio_track.widevine_pssh
554
579
  ) or (
@@ -557,7 +582,6 @@ class AppleMusicSongInterface:
557
582
  and not media.stream_info.audio_track.use_cenc
558
583
  ):
559
584
  raise GamdlInterfaceDecryptionNotAvailableError(media_id=media.media_id)
560
-
561
585
  elif media.stream_info.audio_track.widevine_pssh:
562
586
  media.decryption_key = DecryptionKeyAv(
563
587
  audio_track=await self.base.get_decryption_key(
@@ -122,6 +122,7 @@ class StreamInfo:
122
122
  codec: str = None
123
123
  width: int = None
124
124
  height: int = None
125
+ drm_free: bool = False
125
126
  use_cenc: bool = False
126
127
  use_single_content_key: bool = True
127
128
 
@@ -156,6 +157,7 @@ class Cover:
156
157
  @dataclass
157
158
  class AppleMusicMedia:
158
159
  media_id: str
160
+ is_library: bool = False
159
161
  index: int = 0
160
162
  total: int = 0
161
163
  partial: bool = True
@@ -79,6 +79,7 @@ class AppleMusicUploadedVideoInterface:
79
79
  file_format=MediaFileFormat.M4V,
80
80
  video_track=StreamInfo(
81
81
  stream_url=stream_url,
82
+ drm_free=True,
82
83
  ),
83
84
  )
84
85
 
@@ -113,7 +114,7 @@ class AppleMusicUploadedVideoInterface:
113
114
  await self.base.apple_music_api.get_uploaded_video(media.media_id)
114
115
  )["data"][0]
115
116
 
116
- media.media_id = self.base.parse_catalog_media_id(media.media_metadata)
117
+ media.media_id = media["id"]
117
118
 
118
119
  yield media
119
120
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamdl
3
- Version: 3.6
3
+ Version: 3.7
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
@@ -82,6 +82,8 @@ Use [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE/releases/latest) as a f
82
82
 
83
83
  If the executable is not available in your system PATH, set its location with `--nm3u8dlre-path` or `nm3u8dlre_path`.
84
84
 
85
+ N_m3u8DL-RE also needs FFmpeg. If the FFmpeg executable is not available in your system PATH, set its location with `--ffmpeg-path` or `ffmpeg_path`.
86
+
85
87
  ## 📦 Installation
86
88
 
87
89
  1. **Install Gamdl via pip:**
@@ -105,10 +107,10 @@ gamdl [OPTIONS] URLS...
105
107
 
106
108
  ### Supported URL Types
107
109
 
108
- - Songs
109
- - Albums (Public/Library)
110
- - Playlists (Public/Library)
111
- - Music Videos
110
+ - Songs (Catalog/Library)
111
+ - Albums (Catalog/Library)
112
+ - Playlists (Catalog/Library)
113
+ - Music Videos (Catalog/Library)
112
114
  - Artists
113
115
  - Post Videos
114
116
  - Apple Music Classical
@@ -191,6 +193,7 @@ The file is created automatically on first run. Command-line arguments override
191
193
  | `--output-path`, `-o` | Output directory path | `./Apple Music` |
192
194
  | `--temp-path` | Temporary directory path | `.` |
193
195
  | `--nm3u8dlre-path` | N_m3u8DL-RE executable path | `N_m3u8DL-RE` |
196
+ | `--ffmpeg-path` | FFmpeg executable path | `ffmpeg` |
194
197
  | `--download-mode` | Download mode | `ytdlp` |
195
198
  | **Template Options** | | |
196
199
  | `--album-folder-template` | Album folder template | `{album_artist}/{album}` |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gamdl"
3
- version = "3.6"
3
+ version = "3.7"
4
4
  description = "A command-line app for downloading Apple Music songs, music videos and post videos."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "3.6"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes