gamdl 2.9__tar.gz → 2.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gamdl-2.9 → gamdl-2.9.2}/PKG-INFO +14 -2
- {gamdl-2.9 → gamdl-2.9.2}/README.md +13 -1
- gamdl-2.9.2/gamdl/__init__.py +1 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/cli.py +8 -3
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/cli_config.py +19 -6
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/constants.py +20 -1
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader.py +126 -120
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_song.py +22 -24
- gamdl-2.9.2/gamdl/downloader/enums.py +38 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface_song.py +14 -3
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/types.py +1 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/PKG-INFO +14 -2
- {gamdl-2.9 → gamdl-2.9.2}/pyproject.toml +1 -1
- gamdl-2.9/gamdl/__init__.py +0 -1
- gamdl-2.9/gamdl/downloader/enums.py +0 -16
- {gamdl-2.9 → gamdl-2.9.2}/LICENSE +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/__main__.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/__init__.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/apple_music_api.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/constants.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/exceptions.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/itunes_api.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/__init__.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/config_file.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/constants.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/utils.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/__init__.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/amdecrypt.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_base.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_music_video.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_uploaded_video.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/exceptions.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/hardcoded_wvd.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/types.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/__init__.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/constants.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/enums.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface_music_video.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface_uploaded_video.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl/utils.py +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/SOURCES.txt +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/dependency_links.txt +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/entry_points.txt +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/requires.txt +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/top_level.txt +0 -0
- {gamdl-2.9 → gamdl-2.9.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gamdl
|
|
3
|
-
Version: 2.9
|
|
3
|
+
Version: 2.9.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
|
|
@@ -86,6 +86,7 @@ gamdl [OPTIONS] URLS...
|
|
|
86
86
|
- Music Videos
|
|
87
87
|
- Artists
|
|
88
88
|
- Post Videos
|
|
89
|
+
- Apple Music Classical
|
|
89
90
|
|
|
90
91
|
### Examples
|
|
91
92
|
|
|
@@ -150,6 +151,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
150
151
|
| `--save-cover`, `-s` | Save cover as separate file | `false` |
|
|
151
152
|
| `--save-playlist` | Save M3U8 playlist file | `false` |
|
|
152
153
|
| **Download Options** | | |
|
|
154
|
+
| `--artist-auto-select` | Automatically select artist content to download (artist URLs) | - |
|
|
153
155
|
| `--nm3u8dlre-path` | N_m3u8DL-RE executable path | `N_m3u8DL-RE` |
|
|
154
156
|
| `--mp4decrypt-path` | mp4decrypt executable path | `mp4decrypt` |
|
|
155
157
|
| `--ffmpeg-path` | FFmpeg executable path | `ffmpeg` |
|
|
@@ -171,7 +173,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
171
173
|
| `--cover-size` | Cover size in pixels | `1200` |
|
|
172
174
|
| `--truncate` | Max filename length | - |
|
|
173
175
|
| **Song Options** | | |
|
|
174
|
-
| `--song-codec`
|
|
176
|
+
| `--song-codec-priority` | Comma-separated codec priority | `aac-legacy` |
|
|
175
177
|
| `--synced-lyrics-format` | Synced lyrics format | `lrc` |
|
|
176
178
|
| `--no-synced-lyrics` | Don't download synced lyrics | `false` |
|
|
177
179
|
| `--synced-lyrics-only` | Download only synced lyrics | `false` |
|
|
@@ -275,6 +277,16 @@ Use ISO 639-1 language codes (e.g., `en-US`, `es-ES`, `ja-JP`, `pt-BR`). Don't a
|
|
|
275
277
|
- `best` - Up to 1080p with AAC 256kbps
|
|
276
278
|
- `ask` - Interactive quality selection
|
|
277
279
|
|
|
280
|
+
### Artist Auto-Select Options
|
|
281
|
+
|
|
282
|
+
- `main-albums`
|
|
283
|
+
- `compilation-albums`
|
|
284
|
+
- `live-albums`
|
|
285
|
+
- `singles-eps`
|
|
286
|
+
- `all-albums`
|
|
287
|
+
- `top-songs`
|
|
288
|
+
- `music-videos`
|
|
289
|
+
|
|
278
290
|
## ⚙️ Wrapper
|
|
279
291
|
|
|
280
292
|
Use the [wrapper](https://github.com/WorldObservationLog/wrapper) to download songs in ALAC and other experimental codecs without API limitations. Cookies are not required when using the wrapper.
|
|
@@ -64,6 +64,7 @@ gamdl [OPTIONS] URLS...
|
|
|
64
64
|
- Music Videos
|
|
65
65
|
- Artists
|
|
66
66
|
- Post Videos
|
|
67
|
+
- Apple Music Classical
|
|
67
68
|
|
|
68
69
|
### Examples
|
|
69
70
|
|
|
@@ -128,6 +129,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
128
129
|
| `--save-cover`, `-s` | Save cover as separate file | `false` |
|
|
129
130
|
| `--save-playlist` | Save M3U8 playlist file | `false` |
|
|
130
131
|
| **Download Options** | | |
|
|
132
|
+
| `--artist-auto-select` | Automatically select artist content to download (artist URLs) | - |
|
|
131
133
|
| `--nm3u8dlre-path` | N_m3u8DL-RE executable path | `N_m3u8DL-RE` |
|
|
132
134
|
| `--mp4decrypt-path` | mp4decrypt executable path | `mp4decrypt` |
|
|
133
135
|
| `--ffmpeg-path` | FFmpeg executable path | `ffmpeg` |
|
|
@@ -149,7 +151,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
149
151
|
| `--cover-size` | Cover size in pixels | `1200` |
|
|
150
152
|
| `--truncate` | Max filename length | - |
|
|
151
153
|
| **Song Options** | | |
|
|
152
|
-
| `--song-codec`
|
|
154
|
+
| `--song-codec-priority` | Comma-separated codec priority | `aac-legacy` |
|
|
153
155
|
| `--synced-lyrics-format` | Synced lyrics format | `lrc` |
|
|
154
156
|
| `--no-synced-lyrics` | Don't download synced lyrics | `false` |
|
|
155
157
|
| `--synced-lyrics-only` | Download only synced lyrics | `false` |
|
|
@@ -253,6 +255,16 @@ Use ISO 639-1 language codes (e.g., `en-US`, `es-ES`, `ja-JP`, `pt-BR`). Don't a
|
|
|
253
255
|
- `best` - Up to 1080p with AAC 256kbps
|
|
254
256
|
- `ask` - Interactive quality selection
|
|
255
257
|
|
|
258
|
+
### Artist Auto-Select Options
|
|
259
|
+
|
|
260
|
+
- `main-albums`
|
|
261
|
+
- `compilation-albums`
|
|
262
|
+
- `live-albums`
|
|
263
|
+
- `singles-eps`
|
|
264
|
+
- `all-albums`
|
|
265
|
+
- `top-songs`
|
|
266
|
+
- `music-videos`
|
|
267
|
+
|
|
256
268
|
## ⚙️ Wrapper
|
|
257
269
|
|
|
258
270
|
Use the [wrapper](https://github.com/WorldObservationLog/wrapper) to download songs in ALAC and other experimental codecs without API limitations. Cookies are not required when using the wrapper.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.9.2"
|
|
@@ -142,7 +142,7 @@ async def main(config: CliConfig):
|
|
|
142
142
|
song_downloader = AppleMusicSongDownloader(
|
|
143
143
|
base_downloader=base_downloader,
|
|
144
144
|
interface=song_interface,
|
|
145
|
-
|
|
145
|
+
codec_priority=config.song_codec_piority,
|
|
146
146
|
synced_lyrics_format=config.synced_lyrics_format,
|
|
147
147
|
no_synced_lyrics=config.no_synced_lyrics,
|
|
148
148
|
synced_lyrics_only=config.synced_lyrics_only,
|
|
@@ -168,6 +168,7 @@ async def main(config: CliConfig):
|
|
|
168
168
|
song_downloader=song_downloader,
|
|
169
169
|
music_video_downloader=music_video_downloader,
|
|
170
170
|
uploaded_video_downloader=uploaded_video_downloader,
|
|
171
|
+
artist_auto_select=config.artist_auto_select,
|
|
171
172
|
)
|
|
172
173
|
|
|
173
174
|
if not config.synced_lyrics_only:
|
|
@@ -197,7 +198,8 @@ async def main(config: CliConfig):
|
|
|
197
198
|
)
|
|
198
199
|
|
|
199
200
|
if not base_downloader.full_mp4decrypt_path and (
|
|
200
|
-
config.
|
|
201
|
+
config.song_codec_piority
|
|
202
|
+
not in (SongCodec.AAC_LEGACY, SongCodec.AAC_HE_LEGACY)
|
|
201
203
|
or config.music_video_remux_mode == RemuxMode.MP4BOX
|
|
202
204
|
):
|
|
203
205
|
missing_music_video_paths.append(
|
|
@@ -210,7 +212,10 @@ async def main(config: CliConfig):
|
|
|
210
212
|
+ "\n".join(missing_music_video_paths)
|
|
211
213
|
)
|
|
212
214
|
|
|
213
|
-
if
|
|
215
|
+
if (
|
|
216
|
+
any(not codec.is_legacy() for codec in config.song_codec_piority)
|
|
217
|
+
and not config.use_wrapper
|
|
218
|
+
):
|
|
214
219
|
logger.warning(
|
|
215
220
|
"You have chosen an experimental song codec "
|
|
216
221
|
"without enabling wrapper. "
|
|
@@ -9,9 +9,11 @@ from dataclass_click import argument, option
|
|
|
9
9
|
from ..api import AppleMusicApi
|
|
10
10
|
from ..downloader import (
|
|
11
11
|
AppleMusicBaseDownloader,
|
|
12
|
+
AppleMusicDownloader,
|
|
12
13
|
AppleMusicMusicVideoDownloader,
|
|
13
14
|
AppleMusicSongDownloader,
|
|
14
15
|
AppleMusicUploadedVideoDownloader,
|
|
16
|
+
ArtistAutoSelect,
|
|
15
17
|
DownloadMode,
|
|
16
18
|
RemuxFormatMusicVideo,
|
|
17
19
|
RemuxMode,
|
|
@@ -35,6 +37,7 @@ song_downloader_sig = inspect.signature(AppleMusicSongDownloader.__init__)
|
|
|
35
37
|
uploaded_video_downloader_sig = inspect.signature(
|
|
36
38
|
AppleMusicUploadedVideoDownloader.__init__
|
|
37
39
|
)
|
|
40
|
+
downloader_sig = inspect.signature(AppleMusicDownloader.__init__)
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
@dataclass
|
|
@@ -135,6 +138,16 @@ class CliConfig:
|
|
|
135
138
|
default=api_sig.parameters["language"].default,
|
|
136
139
|
),
|
|
137
140
|
]
|
|
141
|
+
# Downloader specific options
|
|
142
|
+
artist_auto_select: Annotated[
|
|
143
|
+
ArtistAutoSelect | None,
|
|
144
|
+
option(
|
|
145
|
+
"--artist-auto-select",
|
|
146
|
+
help="Automatically select artist content to download (only for artist URLs)",
|
|
147
|
+
default=downloader_sig.parameters["artist_auto_select"].default,
|
|
148
|
+
type=ArtistAutoSelect,
|
|
149
|
+
),
|
|
150
|
+
]
|
|
138
151
|
# Base Downloader specific options
|
|
139
152
|
output_path: Annotated[
|
|
140
153
|
str,
|
|
@@ -362,13 +375,13 @@ class CliConfig:
|
|
|
362
375
|
),
|
|
363
376
|
]
|
|
364
377
|
# DownloaderSong specific options
|
|
365
|
-
|
|
366
|
-
SongCodec,
|
|
378
|
+
song_codec_piority: Annotated[
|
|
379
|
+
list[SongCodec],
|
|
367
380
|
option(
|
|
368
|
-
"--song-codec",
|
|
369
|
-
help="
|
|
370
|
-
default=song_downloader_sig.parameters["
|
|
371
|
-
type=SongCodec,
|
|
381
|
+
"--song-codec-priority",
|
|
382
|
+
help="Comma-separated codec priority",
|
|
383
|
+
default=song_downloader_sig.parameters["codec_priority"].default,
|
|
384
|
+
type=Csv(SongCodec),
|
|
372
385
|
),
|
|
373
386
|
]
|
|
374
387
|
synced_lyrics_format: Annotated[
|
|
@@ -11,8 +11,27 @@ ARTIST_MEDIA_TYPE = {"artist", "artists", "library-artists"}
|
|
|
11
11
|
UPLOADED_VIDEO_MEDIA_TYPE = {"post", "uploaded-videos"}
|
|
12
12
|
PLAYLIST_MEDIA_TYPE = {"playlist", "playlists", "library-playlists"}
|
|
13
13
|
|
|
14
|
+
ARTIST_AUTO_SELECT_KEY_MAP = {
|
|
15
|
+
"main-albums": ("views", "full-albums"),
|
|
16
|
+
"compilation-albums": ("views", "compilation-albums"),
|
|
17
|
+
"live-albums": ("views", "live-albums"),
|
|
18
|
+
"singles-eps": ("views", "singles"),
|
|
19
|
+
"all-albums": ("relationships", "albums"),
|
|
20
|
+
"top-songs": ("views", "top-songs"),
|
|
21
|
+
"music-videos": ("relationships", "music-videos"),
|
|
22
|
+
}
|
|
23
|
+
ARTIST_AUTO_SELECT_STR_MAP = {
|
|
24
|
+
"main-albums": "Main Albums",
|
|
25
|
+
"compilation-albums": "Compilation Albums",
|
|
26
|
+
"live-albums": "Live Albums",
|
|
27
|
+
"singles-eps": "Singles & EPs",
|
|
28
|
+
"all-albums": "All Albums",
|
|
29
|
+
"top-songs": "Top Songs",
|
|
30
|
+
"music-videos": "Music Videos",
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
VALID_URL_PATTERN = re.compile(
|
|
15
|
-
r"https://music\.apple\.com"
|
|
34
|
+
r"https://(?:classical\.)?music\.apple\.com"
|
|
16
35
|
r"(?:"
|
|
17
36
|
r"/(?P<storefront>[a-z]{2})"
|
|
18
37
|
r"/(?P<type>artist|album|playlist|song|music-video|post)"
|
|
@@ -21,7 +21,7 @@ from .downloader_base import AppleMusicBaseDownloader
|
|
|
21
21
|
from .downloader_music_video import AppleMusicMusicVideoDownloader
|
|
22
22
|
from .downloader_song import AppleMusicSongDownloader
|
|
23
23
|
from .downloader_uploaded_video import AppleMusicUploadedVideoDownloader
|
|
24
|
-
from .enums import DownloadMode, RemuxMode
|
|
24
|
+
from .enums import ArtistAutoSelect, DownloadMode, RemuxMode
|
|
25
25
|
from .exceptions import (
|
|
26
26
|
ExecutableNotFound,
|
|
27
27
|
FormatNotAvailable,
|
|
@@ -41,6 +41,7 @@ class AppleMusicDownloader:
|
|
|
41
41
|
song_downloader: AppleMusicSongDownloader,
|
|
42
42
|
music_video_downloader: AppleMusicMusicVideoDownloader,
|
|
43
43
|
uploaded_video_downloader: AppleMusicUploadedVideoDownloader,
|
|
44
|
+
artist_auto_select: ArtistAutoSelect | None = None,
|
|
44
45
|
skip_music_videos: bool = False,
|
|
45
46
|
skip_processing: bool = False,
|
|
46
47
|
flat_filter: typing.Callable = None,
|
|
@@ -50,6 +51,7 @@ class AppleMusicDownloader:
|
|
|
50
51
|
self.song_downloader = song_downloader
|
|
51
52
|
self.music_video_downloader = music_video_downloader
|
|
52
53
|
self.uploaded_video_downloader = uploaded_video_downloader
|
|
54
|
+
self.artist_auto_select = artist_auto_select
|
|
53
55
|
self.skip_music_videos = skip_music_videos
|
|
54
56
|
self.skip_processing = skip_processing
|
|
55
57
|
self.flat_filter = flat_filter
|
|
@@ -150,92 +152,86 @@ class AppleMusicDownloader:
|
|
|
150
152
|
self,
|
|
151
153
|
artist_metadata: dict,
|
|
152
154
|
) -> list[DownloadItem]:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
value=["views", "compilation-albums"],
|
|
163
|
-
),
|
|
164
|
-
Choice(
|
|
165
|
-
name="Live Albums",
|
|
166
|
-
value=["views", "live-albums"],
|
|
167
|
-
),
|
|
168
|
-
Choice(
|
|
169
|
-
name="Singles & EPs",
|
|
170
|
-
value=["views", "singles"],
|
|
171
|
-
),
|
|
172
|
-
Choice(
|
|
173
|
-
name="All Albums",
|
|
174
|
-
value=["relationships", "albums"],
|
|
175
|
-
),
|
|
176
|
-
Choice(
|
|
177
|
-
name="Top Songs",
|
|
178
|
-
value=["views", "top-songs"],
|
|
179
|
-
),
|
|
180
|
-
Choice(
|
|
181
|
-
name="Music Videos",
|
|
182
|
-
value=["relationships", "music-videos"],
|
|
183
|
-
),
|
|
184
|
-
],
|
|
185
|
-
validate=lambda result: artist_metadata.get(result[0], {})
|
|
186
|
-
.get(result[1], {})
|
|
187
|
-
.get("data"),
|
|
188
|
-
invalid_message="The artist doesn't have any items of this type",
|
|
189
|
-
).execute_async()
|
|
190
|
-
|
|
191
|
-
media_type, media_type_key = media_type
|
|
192
|
-
artist_metadata[media_type][media_type_key]["data"].extend(
|
|
193
|
-
[
|
|
194
|
-
extended_data
|
|
195
|
-
async for extended_data in self.interface.apple_music_api.extend_api_data(
|
|
196
|
-
artist_metadata[media_type][media_type_key],
|
|
155
|
+
if not self.artist_auto_select:
|
|
156
|
+
available_choices = []
|
|
157
|
+
for artist_auto_select_option in list(ArtistAutoSelect):
|
|
158
|
+
relation_key, type_key = artist_auto_select_option.path_key
|
|
159
|
+
available_choices.append(
|
|
160
|
+
Choice(
|
|
161
|
+
name=str(artist_auto_select_option),
|
|
162
|
+
value=(artist_auto_select_option,),
|
|
163
|
+
),
|
|
197
164
|
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
165
|
+
|
|
166
|
+
(artist_auto_select,) = await inquirer.select(
|
|
167
|
+
message=f'Select which type to download for artist "{artist_metadata["attributes"]["name"]}":',
|
|
168
|
+
choices=available_choices,
|
|
169
|
+
validate=lambda result: artist_metadata.get(result[0].path_key[0], {})
|
|
170
|
+
.get(result[0].path_key[1], {})
|
|
171
|
+
.get("data"),
|
|
172
|
+
).execute_async()
|
|
173
|
+
else:
|
|
174
|
+
artist_auto_select = self.artist_auto_select
|
|
175
|
+
|
|
176
|
+
relation_key, type_key = artist_auto_select.path_key
|
|
177
|
+
async for extended_data in self.interface.apple_music_api.extend_api_data(
|
|
178
|
+
artist_metadata[relation_key][type_key],
|
|
179
|
+
):
|
|
180
|
+
artist_metadata[relation_key][type_key]["data"].extend(extended_data["data"])
|
|
181
|
+
|
|
182
|
+
selected_items = artist_metadata[relation_key][type_key]["data"]
|
|
183
|
+
select_all = self.artist_auto_select is not None
|
|
184
|
+
|
|
185
|
+
if artist_auto_select in {
|
|
186
|
+
ArtistAutoSelect.MAIN_ALBUMS,
|
|
187
|
+
ArtistAutoSelect.COMPILATION_ALBUMS,
|
|
188
|
+
ArtistAutoSelect.LIVE_ALBUMS,
|
|
189
|
+
ArtistAutoSelect.SINGLES_EPS,
|
|
190
|
+
ArtistAutoSelect.ALL_ALBUMS,
|
|
208
191
|
}:
|
|
209
|
-
return await self.get_artist_albums_download_items(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
192
|
+
return await self.get_artist_albums_download_items(
|
|
193
|
+
selected_items,
|
|
194
|
+
select_all,
|
|
195
|
+
)
|
|
196
|
+
elif artist_auto_select == ArtistAutoSelect.TOP_SONGS:
|
|
197
|
+
return await self.get_artist_songs_download_items(
|
|
198
|
+
selected_items,
|
|
199
|
+
select_all,
|
|
200
|
+
)
|
|
201
|
+
elif artist_auto_select == ArtistAutoSelect.MUSIC_VIDEOS:
|
|
202
|
+
return await self.get_artist_music_videos_download_items(
|
|
203
|
+
selected_items,
|
|
204
|
+
select_all,
|
|
205
|
+
)
|
|
214
206
|
|
|
215
207
|
async def get_artist_albums_download_items(
|
|
216
208
|
self,
|
|
217
209
|
albums_metadata: list[dict],
|
|
210
|
+
select_all: bool = False,
|
|
218
211
|
) -> list[DownloadItem]:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
212
|
+
if not select_all:
|
|
213
|
+
choices = [
|
|
214
|
+
Choice(
|
|
215
|
+
name=" | ".join(
|
|
216
|
+
[
|
|
217
|
+
f'{album["attributes"]["trackCount"]:03d}',
|
|
218
|
+
f'{album["attributes"]["releaseDate"]:<10}',
|
|
219
|
+
f'{album["attributes"].get("contentRating", "None").title():<8}',
|
|
220
|
+
f'{album["attributes"]["name"]}',
|
|
221
|
+
]
|
|
222
|
+
),
|
|
223
|
+
value=album,
|
|
224
|
+
)
|
|
225
|
+
for album in albums_metadata
|
|
226
|
+
if album.get("attributes")
|
|
227
|
+
]
|
|
228
|
+
selected = await inquirer.select(
|
|
229
|
+
message="Select which albums to download: (Track Count | Release Date | Rating | Title)",
|
|
230
|
+
choices=choices,
|
|
231
|
+
multiselect=True,
|
|
232
|
+
).execute_async()
|
|
233
|
+
else:
|
|
234
|
+
selected = albums_metadata
|
|
239
235
|
|
|
240
236
|
download_items = []
|
|
241
237
|
|
|
@@ -259,28 +255,32 @@ class AppleMusicDownloader:
|
|
|
259
255
|
async def get_artist_music_videos_download_items(
|
|
260
256
|
self,
|
|
261
257
|
music_videos_metadata: list[dict],
|
|
258
|
+
select_all: bool = False,
|
|
262
259
|
) -> list[DownloadItem]:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
260
|
+
if not select_all:
|
|
261
|
+
choices = [
|
|
262
|
+
Choice(
|
|
263
|
+
name=" | ".join(
|
|
264
|
+
[
|
|
265
|
+
self.millis_to_min_sec(
|
|
266
|
+
music_video["attributes"]["durationInMillis"]
|
|
267
|
+
),
|
|
268
|
+
f'{music_video["attributes"].get("contentRating", "None").title():<8}',
|
|
269
|
+
music_video["attributes"]["name"],
|
|
270
|
+
],
|
|
271
|
+
),
|
|
272
|
+
value=music_video,
|
|
273
|
+
)
|
|
274
|
+
for music_video in music_videos_metadata
|
|
275
|
+
if music_video.get("attributes")
|
|
276
|
+
]
|
|
277
|
+
selected = await inquirer.select(
|
|
278
|
+
message="Select which music videos to download: (Duration | Rating | Title)",
|
|
279
|
+
choices=choices,
|
|
280
|
+
multiselect=True,
|
|
281
|
+
).execute_async()
|
|
282
|
+
else:
|
|
283
|
+
selected = music_videos_metadata
|
|
284
284
|
|
|
285
285
|
music_video_tasks = [
|
|
286
286
|
self.get_single_download_item(
|
|
@@ -295,26 +295,32 @@ class AppleMusicDownloader:
|
|
|
295
295
|
async def get_artist_songs_download_items(
|
|
296
296
|
self,
|
|
297
297
|
songs_metadata: list[dict],
|
|
298
|
+
select_all: bool = False,
|
|
298
299
|
) -> list[DownloadItem]:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
300
|
+
if not select_all:
|
|
301
|
+
choices = [
|
|
302
|
+
Choice(
|
|
303
|
+
name=" | ".join(
|
|
304
|
+
[
|
|
305
|
+
self.millis_to_min_sec(
|
|
306
|
+
song["attributes"]["durationInMillis"]
|
|
307
|
+
),
|
|
308
|
+
f'{song["attributes"].get("contentRating", "None").title():<8}',
|
|
309
|
+
song["attributes"]["name"],
|
|
310
|
+
],
|
|
311
|
+
),
|
|
312
|
+
value=song,
|
|
313
|
+
)
|
|
314
|
+
for song in songs_metadata
|
|
315
|
+
if song.get("attributes")
|
|
316
|
+
]
|
|
317
|
+
selected = await inquirer.select(
|
|
318
|
+
message="Select which songs to download: (Duration | Rating | Title)",
|
|
319
|
+
choices=choices,
|
|
320
|
+
multiselect=True,
|
|
321
|
+
).execute_async()
|
|
322
|
+
else:
|
|
323
|
+
selected = songs_metadata
|
|
318
324
|
|
|
319
325
|
song_tasks = [
|
|
320
326
|
self.get_single_download_item(
|
|
@@ -13,7 +13,7 @@ class AppleMusicSongDownloader(AppleMusicBaseDownloader):
|
|
|
13
13
|
self,
|
|
14
14
|
base_downloader: AppleMusicBaseDownloader,
|
|
15
15
|
interface: AppleMusicSongInterface,
|
|
16
|
-
|
|
16
|
+
codec_priority: SongCodec = [SongCodec.AAC_LEGACY],
|
|
17
17
|
synced_lyrics_format: SyncedLyricsFormat = SyncedLyricsFormat.LRC,
|
|
18
18
|
no_synced_lyrics: bool = False,
|
|
19
19
|
synced_lyrics_only: bool = False,
|
|
@@ -22,7 +22,7 @@ class AppleMusicSongDownloader(AppleMusicBaseDownloader):
|
|
|
22
22
|
):
|
|
23
23
|
self.__dict__.update(base_downloader.__dict__)
|
|
24
24
|
self.interface = interface
|
|
25
|
-
self.
|
|
25
|
+
self.codec_priority = codec_priority
|
|
26
26
|
self.synced_lyrics_format = synced_lyrics_format
|
|
27
27
|
self.no_synced_lyrics = no_synced_lyrics
|
|
28
28
|
self.synced_lyrics_only = synced_lyrics_only
|
|
@@ -78,33 +78,31 @@ class AppleMusicSongDownloader(AppleMusicBaseDownloader):
|
|
|
78
78
|
if self.synced_lyrics_only:
|
|
79
79
|
return download_item
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
download_item.stream_info = await self.interface.
|
|
81
|
+
for codec in self.codec_priority:
|
|
82
|
+
download_item.stream_info = await self.interface.get_stream_info(
|
|
83
|
+
codec,
|
|
84
|
+
song_metadata,
|
|
83
85
|
webplayback,
|
|
84
|
-
self.codec,
|
|
85
86
|
)
|
|
87
|
+
if download_item.stream_info:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
if download_item.stream_info.audio_track.legacy:
|
|
86
91
|
download_item.decryption_key = (
|
|
87
92
|
await self.interface.get_decryption_key_legacy(
|
|
88
93
|
download_item.stream_info,
|
|
89
94
|
self.cdm,
|
|
90
95
|
)
|
|
91
96
|
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
elif (
|
|
98
|
+
not self.use_wrapper
|
|
99
|
+
and download_item.stream_info
|
|
100
|
+
and download_item.stream_info.audio_track.widevine_pssh
|
|
101
|
+
):
|
|
102
|
+
download_item.decryption_key = await self.interface.get_decryption_key(
|
|
103
|
+
download_item.stream_info,
|
|
104
|
+
self.cdm,
|
|
96
105
|
)
|
|
97
|
-
if (
|
|
98
|
-
not self.use_wrapper
|
|
99
|
-
and download_item.stream_info
|
|
100
|
-
and download_item.stream_info.audio_track.widevine_pssh
|
|
101
|
-
):
|
|
102
|
-
download_item.decryption_key = await self.interface.get_decryption_key(
|
|
103
|
-
download_item.stream_info,
|
|
104
|
-
self.cdm,
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
download_item.decryption_key = None
|
|
108
106
|
|
|
109
107
|
download_item.cover_url_template = self.interface.get_cover_url_template(
|
|
110
108
|
song_metadata,
|
|
@@ -173,11 +171,11 @@ class AppleMusicSongDownloader(AppleMusicBaseDownloader):
|
|
|
173
171
|
encrypted_path: str,
|
|
174
172
|
staged_path: str,
|
|
175
173
|
decryption_key: DecryptionKeyAv,
|
|
176
|
-
|
|
174
|
+
legacy: bool,
|
|
177
175
|
media_id: str,
|
|
178
176
|
fairplay_key: str,
|
|
179
177
|
):
|
|
180
|
-
if self.use_wrapper:
|
|
178
|
+
if self.use_wrapper and not legacy:
|
|
181
179
|
await self.decrypt_amdecrypt(
|
|
182
180
|
encrypted_path,
|
|
183
181
|
staged_path,
|
|
@@ -189,7 +187,7 @@ class AppleMusicSongDownloader(AppleMusicBaseDownloader):
|
|
|
189
187
|
encrypted_path,
|
|
190
188
|
staged_path,
|
|
191
189
|
decryption_key.audio_track.key,
|
|
192
|
-
legacy
|
|
190
|
+
legacy,
|
|
193
191
|
)
|
|
194
192
|
|
|
195
193
|
def get_lyrics_synced_path(self, final_path: str) -> str:
|
|
@@ -232,7 +230,7 @@ class AppleMusicSongDownloader(AppleMusicBaseDownloader):
|
|
|
232
230
|
encrypted_path,
|
|
233
231
|
download_item.staged_path,
|
|
234
232
|
download_item.decryption_key,
|
|
235
|
-
|
|
233
|
+
download_item.stream_info.audio_track.legacy,
|
|
236
234
|
download_item.media_metadata["id"],
|
|
237
235
|
download_item.stream_info.audio_track.fairplay_key,
|
|
238
236
|
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from .constants import (
|
|
4
|
+
ARTIST_AUTO_SELECT_KEY_MAP,
|
|
5
|
+
ARTIST_AUTO_SELECT_STR_MAP,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DownloadMode(Enum):
|
|
10
|
+
YTDLP = "ytdlp"
|
|
11
|
+
NM3U8DLRE = "nm3u8dlre"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RemuxMode(Enum):
|
|
15
|
+
FFMPEG = "ffmpeg"
|
|
16
|
+
MP4BOX = "mp4box"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RemuxFormatMusicVideo(Enum):
|
|
20
|
+
M4V = "m4v"
|
|
21
|
+
MP4 = "mp4"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArtistAutoSelect(Enum):
|
|
25
|
+
MAIN_ALBUMS = "main-albums"
|
|
26
|
+
COMPILATION_ALBUMS = "compilation-albums"
|
|
27
|
+
LIVE_ALBUMS = "live-albums"
|
|
28
|
+
SINGLES_EPS = "singles-eps"
|
|
29
|
+
ALL_ALBUMS = "all-albums"
|
|
30
|
+
TOP_SONGS = "top-songs"
|
|
31
|
+
MUSIC_VIDEOS = "music-videos"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def path_key(self) -> tuple[str, str]:
|
|
35
|
+
return ARTIST_AUTO_SELECT_KEY_MAP[self.value]
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
return ARTIST_AUTO_SELECT_STR_MAP[self.value]
|
|
@@ -226,6 +226,17 @@ class AppleMusicSongInterface(AppleMusicInterface):
|
|
|
226
226
|
return tags
|
|
227
227
|
|
|
228
228
|
async def get_stream_info(
|
|
229
|
+
self,
|
|
230
|
+
codec: SongCodec,
|
|
231
|
+
song_metadata: dict | None = None,
|
|
232
|
+
webplayback: dict | None = None,
|
|
233
|
+
) -> StreamInfoAv | None:
|
|
234
|
+
if codec.is_legacy():
|
|
235
|
+
return await self._get_stream_info_legacy(webplayback, codec)
|
|
236
|
+
else:
|
|
237
|
+
return await self._get_stream_info(song_metadata, codec)
|
|
238
|
+
|
|
239
|
+
async def _get_stream_info(
|
|
229
240
|
self,
|
|
230
241
|
song_metadata: dict,
|
|
231
242
|
codec: SongCodec,
|
|
@@ -257,7 +268,7 @@ class AppleMusicSongInterface(AppleMusicInterface):
|
|
|
257
268
|
if playlist is None:
|
|
258
269
|
return None
|
|
259
270
|
|
|
260
|
-
stream_info = StreamInfo()
|
|
271
|
+
stream_info = StreamInfo(legacy=False)
|
|
261
272
|
stream_info.stream_url = (
|
|
262
273
|
f"{m3u8_master_url.rpartition('/')[0]}/{playlist['uri']}"
|
|
263
274
|
)
|
|
@@ -386,14 +397,14 @@ class AppleMusicSongInterface(AppleMusicInterface):
|
|
|
386
397
|
return key.uri
|
|
387
398
|
return None
|
|
388
399
|
|
|
389
|
-
async def
|
|
400
|
+
async def _get_stream_info_legacy(
|
|
390
401
|
self,
|
|
391
402
|
webplayback: dict,
|
|
392
403
|
codec: SongCodec,
|
|
393
404
|
) -> StreamInfoAv:
|
|
394
405
|
flavor = "32:ctrp64" if codec == SongCodec.AAC_HE_LEGACY else "28:ctrp256"
|
|
395
406
|
|
|
396
|
-
stream_info = StreamInfo()
|
|
407
|
+
stream_info = StreamInfo(legacy=True)
|
|
397
408
|
stream_info.stream_url = next(
|
|
398
409
|
i for i in webplayback["songList"][0]["assets"] if i["flavor"] == flavor
|
|
399
410
|
)["URL"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gamdl
|
|
3
|
-
Version: 2.9
|
|
3
|
+
Version: 2.9.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
|
|
@@ -86,6 +86,7 @@ gamdl [OPTIONS] URLS...
|
|
|
86
86
|
- Music Videos
|
|
87
87
|
- Artists
|
|
88
88
|
- Post Videos
|
|
89
|
+
- Apple Music Classical
|
|
89
90
|
|
|
90
91
|
### Examples
|
|
91
92
|
|
|
@@ -150,6 +151,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
150
151
|
| `--save-cover`, `-s` | Save cover as separate file | `false` |
|
|
151
152
|
| `--save-playlist` | Save M3U8 playlist file | `false` |
|
|
152
153
|
| **Download Options** | | |
|
|
154
|
+
| `--artist-auto-select` | Automatically select artist content to download (artist URLs) | - |
|
|
153
155
|
| `--nm3u8dlre-path` | N_m3u8DL-RE executable path | `N_m3u8DL-RE` |
|
|
154
156
|
| `--mp4decrypt-path` | mp4decrypt executable path | `mp4decrypt` |
|
|
155
157
|
| `--ffmpeg-path` | FFmpeg executable path | `ffmpeg` |
|
|
@@ -171,7 +173,7 @@ The file is created automatically on first run. Command-line arguments override
|
|
|
171
173
|
| `--cover-size` | Cover size in pixels | `1200` |
|
|
172
174
|
| `--truncate` | Max filename length | - |
|
|
173
175
|
| **Song Options** | | |
|
|
174
|
-
| `--song-codec`
|
|
176
|
+
| `--song-codec-priority` | Comma-separated codec priority | `aac-legacy` |
|
|
175
177
|
| `--synced-lyrics-format` | Synced lyrics format | `lrc` |
|
|
176
178
|
| `--no-synced-lyrics` | Don't download synced lyrics | `false` |
|
|
177
179
|
| `--synced-lyrics-only` | Download only synced lyrics | `false` |
|
|
@@ -275,6 +277,16 @@ Use ISO 639-1 language codes (e.g., `en-US`, `es-ES`, `ja-JP`, `pt-BR`). Don't a
|
|
|
275
277
|
- `best` - Up to 1080p with AAC 256kbps
|
|
276
278
|
- `ask` - Interactive quality selection
|
|
277
279
|
|
|
280
|
+
### Artist Auto-Select Options
|
|
281
|
+
|
|
282
|
+
- `main-albums`
|
|
283
|
+
- `compilation-albums`
|
|
284
|
+
- `live-albums`
|
|
285
|
+
- `singles-eps`
|
|
286
|
+
- `all-albums`
|
|
287
|
+
- `top-songs`
|
|
288
|
+
- `music-videos`
|
|
289
|
+
|
|
278
290
|
## ⚙️ Wrapper
|
|
279
291
|
|
|
280
292
|
Use the [wrapper](https://github.com/WorldObservationLog/wrapper) to download songs in ALAC and other experimental codecs without API limitations. Cookies are not required when using the wrapper.
|
gamdl-2.9/gamdl/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.9"
|
|
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
|