Compare commits

..

22 Commits

Author SHA1 Message Date
glomatico ecc7979d7e adjust log message 2024-05-20 12:52:21 -03:00
glomatico d129551b55 adjust log message 2024-05-20 12:51:47 -03:00
glomatico 08a5ac00d8 adjust get_playlist 2024-05-20 12:50:15 -03:00
glomatico 628c9786d5 remove unused import 2024-05-20 04:36:53 -03:00
glomatico 7de12c3da7 add storefront tag for post videos 2024-05-20 04:21:17 -03:00
glomatico 39d724c488 Update cli.py 2024-05-20 04:17:25 -03:00
glomatico 79e00e5e19 adjust some log messages 2024-05-20 04:15:54 -03:00
glomatico e90fd24af0 adjust temp_path for posts 2024-05-20 04:13:32 -03:00
glomatico d68edd5393 Update README.md 2024-05-20 04:09:31 -03:00
glomatico 5acefd9a06 Update README.md 2024-05-20 04:08:22 -03:00
glomatico 93b62cdde9 update description 2024-05-20 04:08:19 -03:00
glomatico fc61a51da2 Update README.md 2024-05-20 04:06:37 -03:00
glomatico 81b44a808d Add missing storefront id tag for music videos 2024-05-20 04:02:07 -03:00
glomatico 24f3af1a5e Update README.md 2024-05-16 23:11:26 -03:00
glomatico 4a469d74d3 Update README.md 2024-05-10 19:33:11 -03:00
glomatico 6122835caa Update README.md 2024-05-10 19:24:01 -03:00
glomatico be597f0de4 bump version 2024-05-10 12:42:41 -03:00
glomatico b10ab5332d adjust default nm3u8dlre path 2024-05-10 12:42:22 -03:00
glomatico 080413b183 bump version 2024-05-09 20:17:15 -03:00
glomatico f6443081ae add try-finally block in get_decryption_key 2024-05-09 20:16:56 -03:00
glomatico 8dcf10c221 whoops 2024-05-05 13:33:35 -03:00
glomatico 6f5efd1779 Update README.md 2024-05-05 13:32:21 -03:00
9 changed files with 76 additions and 63 deletions
+11 -6
View File
@@ -1,5 +1,5 @@
# Glomatico's Apple Music Downloader
A Python CLI app for downloading Apple Music songs/music videos/albums/playlists/posts.
A Python CLI app for downloading Apple Music songs/music videos/posts.
**Discord Server:** https://discord.gg/aBjMEZ9tnq
@@ -14,7 +14,7 @@ A Python CLI app for downloading Apple Music songs/music videos/albums/playlists
## Prerequisites
* Python 3.8 or higher
* The cookies file of your Apple Music account (requires an active subscription)
* The cookies file of your Apple Music browser session (requires an active subscription)
* You can get your cookies by using one of the following extensions on your browser of choice at the Apple Music website with your account signed in:
* Firefox: https://addons.mozilla.org/addon/export-cookies-txt
* Chromium based browsers: https://chrome.google.com/webstore/detail/gdocmgbfkjnnpapoeobnolbbkoibbcif
@@ -53,8 +53,14 @@ gamdl [OPTIONS] URLS...
gamdl "https://music.apple.com/us/artist/rick-astley/669771"
```
### Interactive prompt controls
* Arrow keys - Move selection
* Space - Toggle selection
* Ctrl + A - Select all
* Enter - Confirm selection
## Configuration
You can configure gamdl by using the command line arguments or the config file. The config file is created automatically when you run gamdl for the first time at `~/.gamdl/config.json` on Linux and `%USERPROFILE%\.gamdl\config.json` on Windows. Config file values can be overridden using command line arguments.
gamdl can be configured by using the command line arguments or the config file. The config file is created automatically when you run gamdl for the first time at `~/.gamdl/config.json` on Linux and `%USERPROFILE%\.gamdl\config.json` on Windows. Config file values can be overridden using command line arguments.
| Command line argument / Config file key | Description | Default value |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------- |
| `--disable-music-video-skip` / `disable_music_video_skip` | Don't skip downloading music videos in albums/playlists. | `false` |
@@ -71,7 +77,7 @@ You can configure gamdl by using the command line arguments or the config file.
| `--output-path`, `-o` / `output_path` | Path to output directory. | `./Apple Music` |
| `--temp-path` / `temp_path` | Path to temporary directory. | `./temp` |
| `--wvd-path` / `wvd_path` | Path to .wvd file. | `null` |
| `--nm3u8dlre-path` / `nm3u8dlre_path` | Path to N_m3u8DL-RE binary. | `N_m3u8dl-RE` |
| `--nm3u8dlre-path` / `nm3u8dlre_path` | Path to N_m3u8DL-RE binary. | `N_m3u8DL-RE` |
| `--mp4decrypt-path` / `mp4decrypt_path` | Path to mp4decrypt binary. | `mp4decrypt` |
| `--ffmpeg-path` / `ffmpeg_path` | Path to FFmpeg binary. | `ffmpeg` |
| `--mp4box-path` / `mp4box_path` | Path to MP4Box binary. | `MP4Box` |
@@ -185,10 +191,9 @@ The following synced lyrics formats are available:
* `srt`
* `ttml`
* Native format for Apple Music synced lyrics.
* Highly unsupported by media players.
* Highly unsupported by most media players.
### Cover formats
The following cover formats are available:
* `jpg`
* `png`
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.2.2"
__version__ = "2.2.4"
+1 -2
View File
@@ -169,13 +169,12 @@ class AppleMusicApi:
def get_playlist(
self,
playlist_id: str,
is_library: bool = False,
limit_tracks: int = 300,
extend: str = "extendedAssetUrls",
fetch_all: bool = True,
) -> dict:
response = self.session.get(
f"{self.AMP_API_URL}/v1/{'me' if is_library else 'catalog'}/{self.storefront}/playlists/{playlist_id}",
f"{self.AMP_API_URL}/v1/catalog/{self.storefront}/playlists/{playlist_id}",
params={
"extend": extend,
"limit[tracks]": limit_tracks,
+19 -17
View File
@@ -525,10 +525,12 @@ def main(
encrypted_path = downloader_song.get_encrypted_path(track["id"])
decrypted_path = downloader_song.get_decrypted_path(track["id"])
remuxed_path = downloader_song.get_remuxed_path(track["id"])
logger.debug(f"Downloading to {encrypted_path}")
logger.debug(f'Downloading to "{encrypted_path}"')
downloader.download(encrypted_path, stream_info.stream_url)
if codec_song in LEGACY_CODECS:
logger.debug(f"Remuxing/Decrypting to {remuxed_path}")
logger.debug(
f'Decrypting/Remuxing to "{decrypted_path}"/"{remuxed_path}"'
)
downloader_song_legacy.remux(
encrypted_path,
decrypted_path,
@@ -536,11 +538,11 @@ def main(
decryption_key,
)
else:
logger.debug(f"Decrypting to {decrypted_path}")
logger.debug(f'Decrypting to "{decrypted_path}"')
downloader_song.decrypt(
encrypted_path, decrypted_path, decryption_key
)
logger.debug(f"Remuxing to {final_path}")
logger.debug(f'Remuxing to "{final_path}"')
downloader_song.remux(
decrypted_path,
remuxed_path,
@@ -548,7 +550,7 @@ def main(
)
logger.debug("Applying tags")
downloader.apply_tags(remuxed_path, tags, cover_url)
logger.debug(f"Moving to {final_path}")
logger.debug(f'Moving to "{final_path}"')
downloader.move_to_output_path(remuxed_path, final_path)
if no_synced_lyrics or not lyrics.synced:
pass
@@ -628,27 +630,27 @@ def main(
remuxed_path = downloader_music_video.get_remuxed_path(
track["id"]
)
logger.debug(f"Downloading video to {encrypted_path_video}")
logger.debug(f'Downloading video to "{encrypted_path_video}"')
downloader.download(
encrypted_path_video, stream_info_video.stream_url
)
logger.debug(f"Downloading audio to {encrypted_path_audio}")
logger.debug(f'Downloading audio to "{encrypted_path_audio}"')
downloader.download(
encrypted_path_audio, stream_info_audio.stream_url
)
logger.debug(f"Decrypting video to {decrypted_path_video}")
logger.debug(f'Decrypting video to "{decrypted_path_video}"')
downloader_music_video.decrypt(
encrypted_path_video,
decryption_key_video,
decrypted_path_video,
)
logger.debug(f"Decrypting audio to {decrypted_path_audio}")
logger.debug(f'Decrypting audio to "{decrypted_path_audio}"')
downloader_music_video.decrypt(
encrypted_path_audio,
decryption_key_audio,
decrypted_path_audio,
)
logger.debug(f"Remuxing to {remuxed_path}")
logger.debug(f'Remuxing to "{remuxed_path}"')
downloader_music_video.remux(
decrypted_path_video,
decrypted_path_audio,
@@ -658,7 +660,7 @@ def main(
)
logger.debug("Applying tags")
downloader.apply_tags(remuxed_path, tags, cover_url)
logger.debug(f"Moving to {final_path}")
logger.debug(f'Moving to "{final_path}"')
downloader.move_to_output_path(remuxed_path, final_path)
if not save_cover:
pass
@@ -672,7 +674,7 @@ def main(
elif track["type"] == "uploaded-videos":
stream_url = downloader_post.get_stream_url(track)
tags = downloader_post.get_tags(track)
temp_path = downloader_post.get_temp_path(track["id"])
post_temp_path = downloader_post.get_post_temp_path(track["id"])
final_path = downloader.get_final_path(tags, ".m4v")
cover_path = downloader_music_video.get_cover_path(final_path)
cover_url = downloader.get_cover_url(track)
@@ -681,12 +683,12 @@ def main(
f'({queue_progress}) Post video already exists at "{final_path}", skipping'
)
else:
logger.debug(f"Downloading to {final_path}")
downloader.download_ytdlp(temp_path, stream_url)
logger.debug(f'Downloading to "{post_temp_path}"')
downloader.download_ytdlp(post_temp_path, stream_url)
logger.debug("Applying tags")
downloader.apply_tags(temp_path, tags, cover_url)
logger.debug(f"Moving to {final_path}")
downloader.move_to_output_path(temp_path, final_path)
downloader.apply_tags(post_temp_path, tags, cover_url)
logger.debug(f'Moving to "{final_path}"')
downloader.move_to_output_path(post_temp_path, final_path)
if not save_cover:
pass
elif cover_path.exists() and not overwrite:
+18 -16
View File
@@ -33,7 +33,7 @@ class Downloader:
output_path: Path = Path("./Apple Music"),
temp_path: Path = Path("./temp"),
wvd_path: Path = None,
nm3u8dlre_path: str = "N_m3u8dl-RE",
nm3u8dlre_path: str = "N_m3u8DL-RE",
mp4decrypt_path: str = "mp4decrypt",
ffmpeg_path: str = "ffmpeg",
mp4box_path: str = "MP4Box",
@@ -252,21 +252,23 @@ class Downloader:
return datetime_obj.strftime(self.template_date)
def get_decryption_key(self, pssh: str, track_id: str) -> str:
pssh_obj = PSSH(pssh.split(",")[-1])
cdm_session = self.cdm.open()
challenge = base64.b64encode(
self.cdm.get_license_challenge(cdm_session, pssh_obj)
).decode()
license = self.apple_music_api.get_widevine_license(
track_id,
pssh,
challenge,
)
self.cdm.parse_license(cdm_session, license)
decryption_key = next(
i for i in self.cdm.get_keys(cdm_session) if i.type == "CONTENT"
).key.hex()
self.cdm.close(cdm_session)
try:
pssh_obj = PSSH(pssh.split(",")[-1])
cdm_session = self.cdm.open()
challenge = base64.b64encode(
self.cdm.get_license_challenge(cdm_session, pssh_obj)
).decode()
license = self.apple_music_api.get_widevine_license(
track_id,
pssh,
challenge,
)
self.cdm.parse_license(cdm_session, license)
decryption_key = next(
i for i in self.cdm.get_keys(cdm_session) if i.type == "CONTENT"
).key.hex()
finally:
self.cdm.close(cdm_session)
return decryption_key
def download(self, path: Path, stream_url: str):
+1 -1
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
import re
import subprocess
import urllib.parse
from pathlib import Path
@@ -167,6 +166,7 @@ class DownloaderMusicVideo:
"genre": metadata_itunes[0]["primaryGenreName"],
"genre_id": int(itunes_page["genres"][0]["genreId"]),
"media_type": 6,
"storefront": int(self.downloader.itunes_api.storefront_id.split("-")[0]),
"title": metadata_itunes[0]["trackCensoredName"],
"title_id": int(metadata["id"]),
}
+2 -1
View File
@@ -67,7 +67,8 @@ class DownloaderPost:
"date": self.downloader.sanitize_date(attributes["uploadDate"]),
"title": attributes["name"],
"title_id": int(metadata["id"]),
"storefront": int(self.downloader.itunes_api.storefront_id.split("-")[0]),
}
def get_temp_path(self, track_id: str) -> Path:
def get_post_temp_path(self, track_id: str) -> Path:
return self.downloader.temp_path / f"{track_id}_temp.m4v"
+22 -18
View File
@@ -28,24 +28,28 @@ class DownloaderSongLegacy(DownloaderSong):
return stream_info
def get_decryption_key(self, pssh: str, track_id: str) -> str:
widevine_pssh_data = WidevinePsshData()
widevine_pssh_data.algorithm = 1
widevine_pssh_data.key_ids.append(base64.b64decode(pssh.split(",")[1]))
pssh_obj = PSSH(widevine_pssh_data.SerializeToString())
cdm_session = self.downloader.cdm.open()
challenge = base64.b64encode(
self.downloader.cdm.get_license_challenge(cdm_session, pssh_obj)
).decode()
license = self.downloader.apple_music_api.get_widevine_license(
track_id,
pssh,
challenge,
)
self.downloader.cdm.parse_license(cdm_session, license)
decryption_key = next(
i for i in self.downloader.cdm.get_keys(cdm_session) if i.type == "CONTENT"
).key.hex()
self.downloader.cdm.close(cdm_session)
try:
widevine_pssh_data = WidevinePsshData()
widevine_pssh_data.algorithm = 1
widevine_pssh_data.key_ids.append(base64.b64decode(pssh.split(",")[1]))
pssh_obj = PSSH(widevine_pssh_data.SerializeToString())
cdm_session = self.downloader.cdm.open()
challenge = base64.b64encode(
self.downloader.cdm.get_license_challenge(cdm_session, pssh_obj)
).decode()
license = self.downloader.apple_music_api.get_widevine_license(
track_id,
pssh,
challenge,
)
self.downloader.cdm.parse_license(cdm_session, license)
decryption_key = next(
i
for i in self.downloader.cdm.get_keys(cdm_session)
if i.type == "CONTENT"
).key.hex()
finally:
self.downloader.cdm.close(cdm_session)
return decryption_key
def decrypt(
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gamdl"
description = "A Python CLI app for downloading Apple Music songs/music videos/albums/playlists/posts."
description = "A Python CLI app for downloading Apple Music songs/music videos/posts."
requires-python = ">=3.8"
authors = [{ name = "glomatico" }]
dependencies = [