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.
- {gamdl-3.6 → gamdl-3.7}/PKG-INFO +8 -5
- {gamdl-3.6 → gamdl-3.7}/README.md +7 -4
- gamdl-3.7/gamdl/__init__.py +1 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/api/apple_music.py +152 -5
- {gamdl-3.6 → gamdl-3.7}/gamdl/api/constants.py +8 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/cli.py +1 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/cli_config.py +8 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/base.py +39 -15
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/song.py +25 -19
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/base.py +16 -11
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/constants.py +2 -2
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/interface.py +44 -32
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/music_video.py +18 -4
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/song.py +120 -96
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/types.py +2 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/uploaded_video.py +2 -1
- {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/PKG-INFO +8 -5
- {gamdl-3.6 → gamdl-3.7}/pyproject.toml +1 -1
- gamdl-3.6/gamdl/__init__.py +0 -1
- {gamdl-3.6 → gamdl-3.7}/LICENSE +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/__main__.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/api/__init__.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/api/exceptions.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/api/itunes.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/api/wrapper.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/__init__.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/config_file.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/constants.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/database.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/interactive_prompts.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/cli/utils.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/__init__.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/amdecrypt.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/constants.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/downloader.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/enums.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/exceptions.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/music_video.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/types.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/downloader/uploaded_video.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/__init__.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/enums.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/exceptions.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/interface/wvd.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl/utils.py +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/SOURCES.txt +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/dependency_links.txt +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/entry_points.txt +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/requires.txt +0 -0
- {gamdl-3.6 → gamdl-3.7}/gamdl.egg-info/top_level.txt +0 -0
- {gamdl-3.6 → gamdl-3.7}/setup.cfg +0 -0
{gamdl-3.6 → gamdl-3.7}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gamdl
|
|
3
|
-
Version: 3.
|
|
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 (
|
|
110
|
-
- Playlists (
|
|
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 (
|
|
86
|
-
- Playlists (
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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=
|
|
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=
|
|
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>
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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=
|
|
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=
|
|
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
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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
|
|
198
|
+
async def get_m3u8_master_url(
|
|
197
199
|
self,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
) ->
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
+
song_metadata["id"],
|
|
256
225
|
)
|
|
257
226
|
)["data"][0]
|
|
258
227
|
|
|
259
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
|
551
|
-
|
|
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 =
|
|
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.
|
|
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 (
|
|
110
|
-
- Playlists (
|
|
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}` |
|
gamdl-3.6/gamdl/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.6"
|
{gamdl-3.6 → gamdl-3.7}/LICENSE
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|