mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-13 20:25:13 +03:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2001b19d8f | |||
| 14814dd2da | |||
| 6fad41467f | |||
| 0868f1c28c | |||
| a964011507 | |||
| 3a943d0154 | |||
| 84bf0a3c2b | |||
| 93dda6889c | |||
| d62a1377f8 | |||
| 3a2d521352 | |||
| c8f45110bd | |||
| 36925025b7 | |||
| d8937d9805 | |||
| 513db83645 | |||
| 11f9b5a75c | |||
| 1dd01368c3 | |||
| 4fc8887101 | |||
| 9169665579 | |||
| d053db96e8 |
@@ -1,37 +1,29 @@
|
||||
# Glomatico's Apple Music Downloader
|
||||
A Python CLI app for downloading Apple Music songs/music videos/posts.
|
||||
# Glomatico’s Apple Music Downloader
|
||||
A Python CLI app for downloading Apple Music songs, music videos and post videos.
|
||||
|
||||
**Discord Server:** https://discord.gg/aBjMEZ9tnq
|
||||
**Join our Discord Server:** https://discord.gg/aBjMEZ9tnq
|
||||
|
||||
## Features
|
||||
* Download songs in AAC 256kbps and other codecs
|
||||
* Download music videos up to 4K
|
||||
* Download synced lyrics in LRC, SRT or TTML
|
||||
* Choose between FFmpeg and MP4Box for remuxing
|
||||
* Choose between yt-dlp and N_m3u8DL-RE for downloading
|
||||
* Highly customizable
|
||||
* Use artist links to download all of their albums or music videos
|
||||
* **High-Quality Songs**: Download songs in AAC 256kbps and other codecs.
|
||||
* **High-Quality Music Videos**: Download music videos in resolutions up to 4K.
|
||||
* **Synced Lyrics**: Download synced lyrics in LRC, SRT, or TTML formats.
|
||||
* **Artist Support**: Download all albums or music videos from an artist using their link.
|
||||
* **Highly Customizable**: Extensive configuration options for advanced users.
|
||||
|
||||
## Prerequisites
|
||||
* Python 3.8 or higher
|
||||
* The cookies file of your Apple Music browser session in Netscape format (requires an active subscription)
|
||||
* To export your cookies, use one of the following browser extensions while signed in to Apple Music:
|
||||
* Firefox: https://addons.mozilla.org/addon/export-cookies-txt
|
||||
* Chromium based browsers: https://chrome.google.com/webstore/detail/gdocmgbfkjnnpapoeobnolbbkoibbcif
|
||||
* FFmpeg on your system PATH
|
||||
* Older versions of FFmpeg may not work.
|
||||
* Up to date binaries can be obtained from the links below:
|
||||
* Windows: https://github.com/AnimMouse/ffmpeg-stable-autobuild/releases
|
||||
* Linux: https://johnvansickle.com/ffmpeg/
|
||||
* **Python 3.9 or higher** installed on your system.
|
||||
* The **cookies file** of your Apple Music browser session in Netscape format (requires an active subscription).
|
||||
* **Firefox**: Use the [Export Cookies](https://addons.mozilla.org/addon/export-cookies-txt) extension.
|
||||
* **Chromium-based Browsers**: Use the [Open Cookies.txt](https://chromewebstore.google.com/detail/open-cookiestxt/gdocmgbfkjnnpapoeobnolbbkoibbcif) extension.
|
||||
* **FFmpeg** on your system PATH.
|
||||
* **Windows**: Download from [AnimMouse’s FFmpeg Builds](https://github.com/AnimMouse/ffmpeg-stable-autobuild/releases).
|
||||
* **Linux**: Download from [John Van Sickle’s FFmpeg Builds](https://johnvansickle.com/ffmpeg/).
|
||||
|
||||
### Optional dependencies
|
||||
The following tools are optional but required for specific features. Add them to your system’s PATH or specify their paths using command-line arguments or the config file.
|
||||
* [mp4decrypt](https://www.bento4.com/downloads/)
|
||||
* Required when setting `mp4box` as remux mode, for downloading music videos and for downloading songs in non-legacy formats.
|
||||
* [MP4Box](https://gpac.io/downloads/gpac-nightly-builds/)
|
||||
* Required when setting `mp4box` as remux mode.
|
||||
* [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE/releases/latest)
|
||||
* Required when setting `nm3u8dlre` as download mode.
|
||||
* [mp4decrypt](https://www.bento4.com/downloads/): Required for `mp4box` remux mode, music video downloads, and experimental song codecs.
|
||||
* [MP4Box](https://gpac.io/downloads/gpac-nightly-builds/): Required for `mp4box` remux mode.
|
||||
* [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE/releases/latest): Required for `nm3u8dlre` download mode.
|
||||
|
||||
## Installation
|
||||
1. Install the package `gamdl` using pip
|
||||
@@ -39,49 +31,50 @@ The following tools are optional but required for specific features. Add them to
|
||||
pip install gamdl
|
||||
```
|
||||
2. Set up the cookies file.
|
||||
* You can either move to the current directory from which you will be running Gamdl as `cookies.txt` or specify its path using the command-line arguments/config file.
|
||||
* Move the cookies file to the directory where you’ll run Gamdl and rename it to `cookies.txt`.
|
||||
* Alternatively, specify the path to the cookies file using command-line arguments or the config file.
|
||||
|
||||
## Usage
|
||||
Run Gamdl with the following command:
|
||||
```bash
|
||||
gamdl [OPTIONS] URLS...
|
||||
```
|
||||
|
||||
### Supported URL types
|
||||
Gamdl supports the following types of URLs:
|
||||
* Song
|
||||
* Album
|
||||
* Playlist
|
||||
* Music video
|
||||
* Artist
|
||||
* Post video/extra video
|
||||
* Post video
|
||||
|
||||
### Examples
|
||||
* Download a song
|
||||
* Download a Song:
|
||||
```bash
|
||||
gamdl "https://music.apple.com/us/album/never-gonna-give-you-up-2022-remaster/1624945511?i=1624945512"
|
||||
```
|
||||
* Download an album
|
||||
* Download an Album:
|
||||
```bash
|
||||
gamdl "https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511"
|
||||
```
|
||||
* Choose which albums or music videos to download from an artist
|
||||
* Download from an Artist:
|
||||
```bash
|
||||
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
|
||||
* **Arrow keys**: Move selection
|
||||
* **Space**: Toggle selection
|
||||
* **Ctrl + A**: Select all
|
||||
* **Enter**: Confirm selection
|
||||
|
||||
## Configuration
|
||||
Gamdl can be configured by using the command line arguments or the config file.
|
||||
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 |
|
||||
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` |
|
||||
| `--save-cover`, `-s` / `save_cover` | Save cover as a separate file. | `false` |
|
||||
@@ -160,64 +153,45 @@ The following variables can be used in the template folders/files and/or in the
|
||||
* `track_total`
|
||||
* `xid`
|
||||
|
||||
### Remux modes
|
||||
The following remux modes are available:
|
||||
* `ffmpeg`
|
||||
* `mp4box`
|
||||
* Doesn't convert closed captions in music videos that have them
|
||||
### Remux Modes
|
||||
* `ffmpeg`: Default remuxing mode.
|
||||
* `mp4box`: Alternative remuxing mode (doesn’t convert closed captions in music videos).
|
||||
|
||||
### Download modes
|
||||
The following download modes are available:
|
||||
* `ytdlp`
|
||||
* `nm3u8dlre`
|
||||
* Faster than `ytdlp`
|
||||
* `ytdlp`: Default download mode.
|
||||
* `nm3u8dlre`: Faster than `ytdlp`.
|
||||
|
||||
### Song Codecs
|
||||
* Supported Codecs:
|
||||
* `aac-legacy`: AAC 256kbps 44.1kHz.
|
||||
* `aac-he-legacy`: AAC-HE 64kbps 44.1kHz.
|
||||
* Experimental Codecs (not guaranteed to work due to API limitations):
|
||||
* `aac`: AAC 256kbps up to 48kHz.
|
||||
* `aac-he`: AAC-HE 64kbps up to 48kHz.
|
||||
* `aac-binaural`: AAC 256kbps binaural.
|
||||
* `aac-downmix`: AAC 256kbps downmix.
|
||||
* `aac-he-binaural`: AAC-HE 64kbps binaural.
|
||||
* `aac-he-downmix`: AAC-HE 64kbps downmix.
|
||||
* `atmos`: Dolby Atmos 768kbps.
|
||||
* `ac3`: AC3 640kbps.
|
||||
* `alac`: ALAC up to 24-bit/192 kHz.
|
||||
* `ask`: Prompt to choose available audio codec.
|
||||
|
||||
### Song codecs
|
||||
The following codecs are available:
|
||||
* `aac-legacy`
|
||||
* `aac-he-legacy`
|
||||
|
||||
The following codecs are also available, **but are not guaranteed to work**, as currently most (or all) of the songs fails to be downloaded when using them:
|
||||
* `aac`
|
||||
* `aac-he`
|
||||
* `aac-binaural`
|
||||
* `aac-downmix`
|
||||
* `aac-he-binaural`
|
||||
* `aac-he-downmix`
|
||||
* `atmos`
|
||||
* `ac3`
|
||||
* `alac`
|
||||
* `ask`
|
||||
* When using this option, Gamdl will ask you which codec from this list to use that is available for the song.
|
||||
|
||||
### Music videos codecs
|
||||
The following codecs are available:
|
||||
* `h264` (up to 1080p, with AAC 256kbps)
|
||||
* `h265` (up to 2160p, with AAC 256kpbs)
|
||||
* `ask`
|
||||
* When using this option, Gamdl will ask you which audio and video codec to use that is available for the music video.
|
||||
### Music Videos Codecs
|
||||
* `h264`: Up to 1080p with AAC 256kbps.
|
||||
* `h265`: Up to 2160p with AAC 256kpbs.
|
||||
* `ask`: Prompt to choose available video and audio codecs.
|
||||
|
||||
### Post videos/extra videos qualities
|
||||
The following qualities are available:
|
||||
* `best` (up to 1080p, with AAC 256kbps)
|
||||
* `ask`
|
||||
* When using this option, Gamdl will ask you which video quality to use that is available for the video.
|
||||
|
||||
Post videos doesn't require remuxing and are limited to `ytdlp` download mode.
|
||||
* `best`: Up to 1080p with AAC 256kbps.
|
||||
* `ask`: Prompt to choose available video quality.
|
||||
|
||||
### Synced lyrics formats
|
||||
The following synced lyrics formats are available:
|
||||
* `lrc`
|
||||
* `srt`
|
||||
* `ttml`
|
||||
* Native format for Apple Music synced lyrics.
|
||||
* Highly unsupported by most media players.
|
||||
* `lrc`: Lightweight and widely supported.
|
||||
* `srt`: SubRip format (has more accurate timestamps).
|
||||
* `ttml`: Native Apple Music format (unsupported by most media players).
|
||||
|
||||
### Cover formats
|
||||
The following cover formats are available:
|
||||
* `jpg`
|
||||
* `png`
|
||||
* `raw`
|
||||
* This format gets the raw cover without any processing.
|
||||
* Note that when using this format, the cover image will not be embedded within the files. To address this, you can enable `save_cover` option to save the cover as a separate file.
|
||||
* `jpg`: Default format.
|
||||
* `png`: Lossless format.
|
||||
* `raw`: Raw cover without processing (requires `save_cover` to save separately).
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "2.3.8"
|
||||
__version__ = "2.4"
|
||||
|
||||
@@ -37,6 +37,9 @@ class AppleMusicApi:
|
||||
cookies.load(ignore_discard=True, ignore_expires=True)
|
||||
self.session.cookies.update(cookies)
|
||||
self.storefront = self.session.cookies.get_dict()["itua"]
|
||||
media_user_token = self.session.cookies.get_dict()["media-user-token"]
|
||||
else:
|
||||
media_user_token = ""
|
||||
self.session.headers.update(
|
||||
{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0",
|
||||
@@ -44,9 +47,7 @@ class AppleMusicApi:
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"content-type": "application/json",
|
||||
"Media-User-Token": self.session.cookies.get_dict().get(
|
||||
"media-user-token", ""
|
||||
),
|
||||
"Media-User-Token": media_user_token,
|
||||
"x-apple-renewal": "true",
|
||||
"DNT": "1",
|
||||
"Connection": "keep-alive",
|
||||
|
||||
+30
-18
@@ -7,10 +7,12 @@ from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from termcolor import colored
|
||||
|
||||
from . import __version__
|
||||
from .apple_music_api import AppleMusicApi
|
||||
from .constants import *
|
||||
from .custom_formatter import CustomFormatter
|
||||
from .downloader import Downloader
|
||||
from .downloader_music_video import DownloaderMusicVideo
|
||||
from .downloader_post import DownloaderPost
|
||||
@@ -348,16 +350,20 @@ def main(
|
||||
quality_post: PostQuality,
|
||||
no_config_file: bool,
|
||||
):
|
||||
logging.basicConfig(
|
||||
format="[%(levelname)-8s %(asctime)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(log_level)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(CustomFormatter())
|
||||
logger.addHandler(stream_handler)
|
||||
logger.info("Starting Gamdl")
|
||||
if not cookies_path.exists():
|
||||
logger.critical(X_NOT_FOUND_STRING.format("Cookies file", cookies_path))
|
||||
return
|
||||
while not cookies_path.exists():
|
||||
cookies_path_str = click.prompt(
|
||||
X_NOT_FOUND_STRING.format("Cookies file", cookies_path.absolute())
|
||||
+ ". Move it to that location or drag and drop it here. Then, press enter to continue",
|
||||
default=str(cookies_path),
|
||||
show_default=False,
|
||||
)
|
||||
cookies_path = Path(cookies_path_str.strip('"'))
|
||||
apple_music_api = AppleMusicApi(
|
||||
cookies_path,
|
||||
language=language,
|
||||
@@ -440,7 +446,7 @@ def main(
|
||||
logger.critical(X_NOT_FOUND_STRING.format("N_m3u8DL-RE", nm3u8dlre_path))
|
||||
return
|
||||
if not downloader.mp4decrypt_path_full:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
X_NOT_FOUND_STRING.format("mp4decrypt", mp4decrypt_path)
|
||||
+ ", music videos will not be downloaded"
|
||||
)
|
||||
@@ -448,9 +454,9 @@ def main(
|
||||
else:
|
||||
skip_mv = False
|
||||
if codec_song not in LEGACY_CODECS:
|
||||
logger.warn(
|
||||
"You have chosen a non-legacy codec. Support for non-legacy codecs are not guaranteed, "
|
||||
"as most of the songs cannot be downloaded when using non-legacy codecs."
|
||||
logger.warning(
|
||||
"You have chosen an experimental codec. "
|
||||
"They're not guaranteed to work due to API limitations."
|
||||
)
|
||||
error_count = 0
|
||||
if read_urls_as_txt:
|
||||
@@ -460,7 +466,7 @@ def main(
|
||||
_urls.extend(Path(url).read_text(encoding="utf-8").splitlines())
|
||||
urls = _urls
|
||||
for url_index, url in enumerate(urls, start=1):
|
||||
url_progress = f"URL {url_index}/{len(urls)}"
|
||||
url_progress = colored(f"URL {url_index}/{len(urls)}", "grey")
|
||||
try:
|
||||
logger.info(f'({url_progress}) Checking "{url}"')
|
||||
url_info = downloader.get_url_info(url)
|
||||
@@ -476,7 +482,10 @@ def main(
|
||||
for download_index, track_metadata in enumerate(
|
||||
download_queue_tracks_metadata, start=1
|
||||
):
|
||||
queue_progress = f"Track {download_index}/{len(download_queue_tracks_metadata)} from URL {url_index}/{len(urls)}"
|
||||
queue_progress = colored(
|
||||
f"Track {download_index}/{len(download_queue_tracks_metadata)} from URL {url_index}/{len(urls)}",
|
||||
"grey",
|
||||
)
|
||||
try:
|
||||
remuxed_path = None
|
||||
if download_queue.playlist_attributes:
|
||||
@@ -542,13 +551,16 @@ def main(
|
||||
)
|
||||
logger.debug("Getting decryption key")
|
||||
decryption_key = downloader_song_legacy.get_decryption_key(
|
||||
stream_info.pssh, track_metadata["id"]
|
||||
stream_info.widevine_pssh, track_metadata["id"]
|
||||
)
|
||||
else:
|
||||
stream_info = downloader_song.get_stream_info(
|
||||
track_metadata
|
||||
)
|
||||
if not stream_info.stream_url or not stream_info.pssh:
|
||||
if (
|
||||
not stream_info.stream_url
|
||||
or not stream_info.widevine_pssh
|
||||
):
|
||||
logger.warning(
|
||||
f"({queue_progress}) Song is not downloadable or is not"
|
||||
" available in the chosen codec, skipping"
|
||||
@@ -556,7 +568,7 @@ def main(
|
||||
continue
|
||||
logger.debug("Getting decryption key")
|
||||
decryption_key = downloader.get_decryption_key(
|
||||
stream_info.pssh, track_metadata["id"]
|
||||
stream_info.widevine_pssh, track_metadata["id"]
|
||||
)
|
||||
encrypted_path = downloader_song.get_encrypted_path(
|
||||
track_metadata["id"]
|
||||
@@ -658,10 +670,10 @@ def main(
|
||||
downloader_music_video.get_stream_info_audio(m3u8_data),
|
||||
)
|
||||
decryption_key_video = downloader.get_decryption_key(
|
||||
stream_info_video.pssh, track_metadata["id"]
|
||||
stream_info_video.widevine_pssh, track_metadata["id"]
|
||||
)
|
||||
decryption_key_audio = downloader.get_decryption_key(
|
||||
stream_info_audio.pssh, track_metadata["id"]
|
||||
stream_info_audio.widevine_pssh, track_metadata["id"]
|
||||
)
|
||||
encrypted_path_video = (
|
||||
downloader_music_video.get_encrypted_path_video(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import logging
|
||||
from termcolor import colored
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
basic_format = "[%(levelname)-8s %(asctime)s]"
|
||||
formats = {
|
||||
logging.DEBUG: colored(basic_format, "grey"),
|
||||
logging.INFO: colored(basic_format, "green"),
|
||||
logging.WARNING: colored(basic_format, "yellow"),
|
||||
logging.ERROR: colored(basic_format, "red"),
|
||||
logging.CRITICAL: colored(basic_format, "red"),
|
||||
}
|
||||
date_format = "%H:%M:%S"
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
return logging.Formatter(
|
||||
self.formats.get(record.levelno) + " %(message)s",
|
||||
datefmt=self.date_format,
|
||||
).format(record)
|
||||
+1
-1
@@ -255,7 +255,7 @@ class Downloader:
|
||||
playlist_track: int,
|
||||
) -> dict:
|
||||
tags = {
|
||||
"playlist_artist": playlist_attributes["curatorName"],
|
||||
"playlist_artist": playlist_attributes.get("curatorName", "Apple Music"),
|
||||
"playlist_id": playlist_attributes["playParams"]["id"],
|
||||
"playlist_title": playlist_attributes["name"],
|
||||
"playlist_track": playlist_track,
|
||||
|
||||
@@ -136,7 +136,7 @@ class DownloaderMusicVideo:
|
||||
stream_info.stream_url = playlist["uri"]
|
||||
stream_info.codec = playlist["stream_info"]["codecs"]
|
||||
m3u8_data = m3u8.load(stream_info.stream_url).data
|
||||
stream_info.pssh = self.get_pssh(m3u8_data)
|
||||
stream_info.widevine_pssh = self.get_pssh(m3u8_data)
|
||||
return stream_info
|
||||
|
||||
def get_stream_info_audio(self, m3u8_master_data: dict) -> StreamInfo:
|
||||
@@ -148,7 +148,7 @@ class DownloaderMusicVideo:
|
||||
stream_info.stream_url = playlist["uri"]
|
||||
stream_info.codec = playlist["group_id"]
|
||||
m3u8_data = m3u8.load(stream_info.stream_url).data
|
||||
stream_info.pssh = self.get_pssh(m3u8_data)
|
||||
stream_info.widevine_pssh = self.get_pssh(m3u8_data)
|
||||
return stream_info
|
||||
|
||||
def get_music_video_id_alt(self, metadata: dict) -> str:
|
||||
|
||||
@@ -85,25 +85,48 @@ class DownloaderSong:
|
||||
).execute()
|
||||
return selected
|
||||
|
||||
def get_pssh(
|
||||
def _get_drm_data(
|
||||
self,
|
||||
drm_infos: dict,
|
||||
drm_ids: list,
|
||||
drm_key: str,
|
||||
) -> str | None:
|
||||
drm_info = next(
|
||||
(
|
||||
drm_infos[drm_id]
|
||||
for drm_id in drm_ids
|
||||
if drm_infos[drm_id].get(
|
||||
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"
|
||||
)
|
||||
and drm_id != "1"
|
||||
if drm_infos[drm_id].get(drm_key) and drm_id != "1"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not drm_info:
|
||||
return None
|
||||
return drm_info["urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]["URI"]
|
||||
return drm_info[drm_key]["URI"]
|
||||
|
||||
def get_widevine_pssh(
|
||||
self,
|
||||
drm_infos: dict,
|
||||
drm_ids: list,
|
||||
) -> str | None:
|
||||
return self._get_drm_data(
|
||||
drm_infos,
|
||||
drm_ids,
|
||||
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",
|
||||
)
|
||||
|
||||
def get_playready_pssh(self, drm_infos: dict, drm_ids: list) -> str | None:
|
||||
return self._get_drm_data(
|
||||
drm_infos,
|
||||
drm_ids,
|
||||
"com.microsoft.playready",
|
||||
)
|
||||
|
||||
def get_fairplay_key(self, drm_infos: dict, drm_ids: list) -> str | None:
|
||||
return self._get_drm_data(
|
||||
drm_infos,
|
||||
drm_ids,
|
||||
"com.apple.streamingkeydelivery",
|
||||
)
|
||||
|
||||
def get_stream_info(self, track_metadata: dict) -> StreamInfo:
|
||||
m3u8_url = track_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")
|
||||
@@ -128,8 +151,14 @@ class DownloaderSong:
|
||||
stream_info.stream_url = m3u8_obj.base_uri + playlist["uri"]
|
||||
variant_id = playlist["stream_info"]["stable_variant_id"]
|
||||
drm_ids = asset_infos[variant_id]["AUDIO-SESSION-KEY-IDS"]
|
||||
pssh = self.get_pssh(drm_infos, drm_ids)
|
||||
stream_info.pssh = pssh
|
||||
widevine_pssh, playready_pssh, fairplay_key = (
|
||||
self.get_widevine_pssh(drm_infos, drm_ids),
|
||||
self.get_playready_pssh(drm_infos, drm_ids),
|
||||
self.get_fairplay_key(drm_infos, drm_ids),
|
||||
)
|
||||
stream_info.widevine_pssh = widevine_pssh
|
||||
stream_info.playready_pssh = playready_pssh
|
||||
stream_info.fairplay_key = fairplay_key
|
||||
stream_info.codec = playlist["stream_info"]["codecs"]
|
||||
return stream_info
|
||||
|
||||
@@ -147,7 +176,10 @@ class DownloaderSong:
|
||||
secs = float(f"{mins_secs_ms[-2]}.{mins_secs_ms[-1]}")
|
||||
if len(mins_secs_ms) > 2:
|
||||
mins = int(mins_secs_ms[-3])
|
||||
return datetime.datetime.fromtimestamp((mins * 60) + secs + (ms / 1000))
|
||||
return datetime.datetime.fromtimestamp(
|
||||
(mins * 60) + secs + (ms / 1000),
|
||||
tz=datetime.timezone.utc,
|
||||
)
|
||||
|
||||
def get_lyrics_synced_timestamp_lrc(self, timestamp_ttml: str) -> str:
|
||||
datetime_obj = self.parse_datetime_obj_from_timestamp_ttml(timestamp_ttml)
|
||||
|
||||
@@ -24,7 +24,7 @@ class DownloaderSongLegacy(DownloaderSong):
|
||||
i for i in webplayback["assets"] if i["flavor"] == flavor
|
||||
)["URL"]
|
||||
m3u8_obj = m3u8.load(stream_info.stream_url)
|
||||
stream_info.pssh = m3u8_obj.keys[0].uri
|
||||
stream_info.widevine_pssh = m3u8_obj.keys[0].uri
|
||||
return stream_info
|
||||
|
||||
def get_decryption_key(self, pssh: str, track_id: str) -> str:
|
||||
|
||||
+3
-1
@@ -25,5 +25,7 @@ class Lyrics:
|
||||
@dataclass
|
||||
class StreamInfo:
|
||||
stream_url: str = None
|
||||
pssh: str = None
|
||||
widevine_pssh: str = None
|
||||
playready_pssh: str = None
|
||||
fairplay_key: str = None
|
||||
codec: str = None
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "gamdl"
|
||||
description = "A Python CLI app for downloading Apple Music songs/music videos/posts."
|
||||
requires-python = ">=3.8"
|
||||
description = "A Python CLI app for downloading Apple Music songs, music videos and post videos."
|
||||
requires-python = ">=3.9"
|
||||
authors = [{ name = "glomatico" }]
|
||||
dependencies = [
|
||||
"click",
|
||||
@@ -11,6 +11,7 @@ dependencies = [
|
||||
"pillow",
|
||||
"pywidevine",
|
||||
"pyyaml",
|
||||
"termcolor",
|
||||
"yt-dlp",
|
||||
]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -5,4 +5,5 @@ mutagen
|
||||
pillow
|
||||
pywidevine
|
||||
pyyaml
|
||||
termcolor
|
||||
yt-dlp
|
||||
|
||||
Reference in New Issue
Block a user