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.
Files changed (47) hide show
  1. {gamdl-2.9 → gamdl-2.9.2}/PKG-INFO +14 -2
  2. {gamdl-2.9 → gamdl-2.9.2}/README.md +13 -1
  3. gamdl-2.9.2/gamdl/__init__.py +1 -0
  4. {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/cli.py +8 -3
  5. {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/cli_config.py +19 -6
  6. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/constants.py +20 -1
  7. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader.py +126 -120
  8. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_song.py +22 -24
  9. gamdl-2.9.2/gamdl/downloader/enums.py +38 -0
  10. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface_song.py +14 -3
  11. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/types.py +1 -0
  12. {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/PKG-INFO +14 -2
  13. {gamdl-2.9 → gamdl-2.9.2}/pyproject.toml +1 -1
  14. gamdl-2.9/gamdl/__init__.py +0 -1
  15. gamdl-2.9/gamdl/downloader/enums.py +0 -16
  16. {gamdl-2.9 → gamdl-2.9.2}/LICENSE +0 -0
  17. {gamdl-2.9 → gamdl-2.9.2}/gamdl/__main__.py +0 -0
  18. {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/__init__.py +0 -0
  19. {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/apple_music_api.py +0 -0
  20. {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/constants.py +0 -0
  21. {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/exceptions.py +0 -0
  22. {gamdl-2.9 → gamdl-2.9.2}/gamdl/api/itunes_api.py +0 -0
  23. {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/__init__.py +0 -0
  24. {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/config_file.py +0 -0
  25. {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/constants.py +0 -0
  26. {gamdl-2.9 → gamdl-2.9.2}/gamdl/cli/utils.py +0 -0
  27. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/__init__.py +0 -0
  28. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/amdecrypt.py +0 -0
  29. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_base.py +0 -0
  30. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_music_video.py +0 -0
  31. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/downloader_uploaded_video.py +0 -0
  32. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/exceptions.py +0 -0
  33. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/hardcoded_wvd.py +0 -0
  34. {gamdl-2.9 → gamdl-2.9.2}/gamdl/downloader/types.py +0 -0
  35. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/__init__.py +0 -0
  36. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/constants.py +0 -0
  37. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/enums.py +0 -0
  38. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface.py +0 -0
  39. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface_music_video.py +0 -0
  40. {gamdl-2.9 → gamdl-2.9.2}/gamdl/interface/interface_uploaded_video.py +0 -0
  41. {gamdl-2.9 → gamdl-2.9.2}/gamdl/utils.py +0 -0
  42. {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/SOURCES.txt +0 -0
  43. {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/dependency_links.txt +0 -0
  44. {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/entry_points.txt +0 -0
  45. {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/requires.txt +0 -0
  46. {gamdl-2.9 → gamdl-2.9.2}/gamdl.egg-info/top_level.txt +0 -0
  47. {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` | Song codec | `aac-legacy` |
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` | Song codec | `aac-legacy` |
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
- codec=config.song_codec,
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.song_codec not in (SongCodec.AAC_LEGACY, SongCodec.AAC_HE_LEGACY)
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 not config.song_codec.is_legacy() and not config.use_wrapper:
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
- song_codec: Annotated[
366
- SongCodec,
378
+ song_codec_piority: Annotated[
379
+ list[SongCodec],
367
380
  option(
368
- "--song-codec",
369
- help="Song codec",
370
- default=song_downloader_sig.parameters["codec"].default,
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
- media_type = await inquirer.select(
154
- message=f'Select which type to download for artist "{artist_metadata["attributes"]["name"]}":',
155
- choices=[
156
- Choice(
157
- name="Main Albums",
158
- value=["views", "full-albums"],
159
- ),
160
- Choice(
161
- name="Compilations Albums",
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
- selected_tracks = artist_metadata[media_type][media_type_key]["data"]
201
-
202
- if media_type_key in {
203
- "full-albums",
204
- "compilation-albums",
205
- "live-albums",
206
- "singles",
207
- "albums",
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(selected_tracks)
210
- elif media_type_key == "top-songs":
211
- return await self.get_artist_songs_download_items(selected_tracks)
212
- elif media_type_key == "music-videos":
213
- return await self.get_artist_music_videos_download_items(selected_tracks)
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
- choices = [
220
- Choice(
221
- name=" | ".join(
222
- [
223
- f'{album["attributes"]["trackCount"]:03d}',
224
- f'{album["attributes"]["releaseDate"]:<10}',
225
- f'{album["attributes"].get("contentRating", "None").title():<8}',
226
- f'{album["attributes"]["name"]}',
227
- ]
228
- ),
229
- value=album,
230
- )
231
- for album in albums_metadata
232
- if album.get("attributes")
233
- ]
234
- selected = await inquirer.select(
235
- message="Select which albums to download: (Track Count | Release Date | Rating | Title)",
236
- choices=choices,
237
- multiselect=True,
238
- ).execute_async()
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
- choices = [
264
- Choice(
265
- name=" | ".join(
266
- [
267
- self.millis_to_min_sec(
268
- music_video["attributes"]["durationInMillis"]
269
- ),
270
- f'{music_video["attributes"].get("contentRating", "None").title():<8}',
271
- music_video["attributes"]["name"],
272
- ],
273
- ),
274
- value=music_video,
275
- )
276
- for music_video in music_videos_metadata
277
- if music_video.get("attributes")
278
- ]
279
- selected = await inquirer.select(
280
- message="Select which music videos to download: (Duration | Rating | Title)",
281
- choices=choices,
282
- multiselect=True,
283
- ).execute_async()
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
- choices = [
300
- Choice(
301
- name=" | ".join(
302
- [
303
- self.millis_to_min_sec(song["attributes"]["durationInMillis"]),
304
- f'{song["attributes"].get("contentRating", "None").title():<8}',
305
- song["attributes"]["name"],
306
- ],
307
- ),
308
- value=song,
309
- )
310
- for song in songs_metadata
311
- if song.get("attributes")
312
- ]
313
- selected = await inquirer.select(
314
- message="Select which songs to download: (Duration | Rating | Title)",
315
- choices=choices,
316
- multiselect=True,
317
- ).execute_async()
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
- codec: SongCodec = SongCodec.AAC_LEGACY,
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.codec = codec
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
- if self.codec.is_legacy():
82
- download_item.stream_info = await self.interface.get_stream_info_legacy(
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
- else:
93
- download_item.stream_info = await self.interface.get_stream_info(
94
- song_metadata,
95
- self.codec,
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
- codec: SongCodec,
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=codec.is_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
- self.codec,
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 get_stream_info_legacy(
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"]
@@ -121,6 +121,7 @@ class StreamInfo:
121
121
  codec: str = None
122
122
  width: int = None
123
123
  height: int = None
124
+ legacy: bool = None
124
125
 
125
126
 
126
127
  @dataclass
@@ -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` | Song codec | `aac-legacy` |
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gamdl"
3
- version = "2.9"
3
+ version = "2.9.2"
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__ = "2.9"
@@ -1,16 +0,0 @@
1
- from enum import Enum
2
-
3
-
4
- class DownloadMode(Enum):
5
- YTDLP = "ytdlp"
6
- NM3U8DLRE = "nm3u8dlre"
7
-
8
-
9
- class RemuxMode(Enum):
10
- FFMPEG = "ffmpeg"
11
- MP4BOX = "mp4box"
12
-
13
-
14
- class RemuxFormatMusicVideo(Enum):
15
- M4V = "m4v"
16
- MP4 = "mp4"
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