diff --git a/README.md b/README.md index d3e78a6..6bdc1b9 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Config file values can be overridden using command line arguments. | `--language`, `-l` / `language` | Metadata language as an ISO-2A language code (don't always work for videos). | `en-US` | | `--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` | +| `--device-path` / `device_path` | Path to .wvd or .prd file. | `null` | | `--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` | @@ -121,7 +121,7 @@ Config file values can be overridden using command line arguments. | `--codec-music-video` / `codec_music_video` | Music video codec. | `h264` | | `--quality-post` / `quality_post` | Post video quality. | `best` | | `--no-config-file`, `-n` / - | Do not use a config file. | `false` | - +| `--playready`, `playready` / - | Use Playready DRM | `false` | ### Tags variables The following variables can be used in the template folders/files and/or in the `exclude_tags` list: @@ -178,6 +178,7 @@ 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` @@ -190,6 +191,7 @@ The following codecs are also available, **but are not guaranteed to work**, as * `alac` * `ask` * When using this option, Gamdl will ask you which codec from this list to use that is available for the song. +With PlayReady and the right CDM, binaural, atmos and aac should download. ### Music videos codecs The following codecs are available: diff --git a/gamdl/apple_music_api.py b/gamdl/apple_music_api.py index 647646c..c717575 100644 --- a/gamdl/apple_music_api.py +++ b/gamdl/apple_music_api.py @@ -1,11 +1,14 @@ from __future__ import annotations import functools +import json import re import time import typing from http.cookiejar import MozillaCookieJar from pathlib import Path +from .enums import DRM + import requests @@ -39,7 +42,7 @@ class AppleMusicApi: self.storefront = self.session.cookies.get_dict()["itua"] self.session.headers.update( { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", + "User-Agent": "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)", #"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.86", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", "Accept": "application/json", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", @@ -256,32 +259,41 @@ class AppleMusicApi: self._raise_response_exception(response) return webplayback[0] - def get_widevine_license( + def get_license( self, track_id: str, track_uri: str, challenge: str, + drm: DRM ) -> str: response = self.session.post( self.LICENSE_API_URL, json={ "challenge": challenge, - "key-system": "com.widevine.alpha", + "key-system": "com.widevine.alpha" if drm == DRM.Widevine else "com.microsoft.playready", "uri": track_uri, "adamId": track_id, "isLibrary": False, - "user-initiated": True, + "user-initiated": False, }, ) try: response.raise_for_status() response_dict = response.json() - widevine_license = response_dict.get("license") - assert widevine_license + license = response_dict.get("license") + assert license except ( requests.HTTPError, requests.exceptions.JSONDecodeError, AssertionError, ): + if response_dict.get("status") == -1002: + raise Exception("-1002: You do not own the title in the requested quality.") + elif response_dict.get("status") == -1021: + raise Exception("-1021: Device has insufficient security level or is blacklisted/revoked.") self._raise_response_exception(response) - return widevine_license + + return license + + + diff --git a/gamdl/cli.py b/gamdl/cli.py index 71c7e5f..2ba1a54 100644 --- a/gamdl/cli.py +++ b/gamdl/cli.py @@ -16,7 +16,7 @@ from .downloader_music_video import DownloaderMusicVideo from .downloader_post import DownloaderPost from .downloader_song import DownloaderSong from .downloader_song_legacy import DownloaderSongLegacy -from .enums import CoverFormat, DownloadMode, MusicVideoCodec, PostQuality, RemuxMode +from .enums import CoverFormat, DownloadMode, MusicVideoCodec, PostQuality, RemuxMode, DRM from .itunes_api import ItunesApi apple_music_api_sig = inspect.signature(AppleMusicApi.__init__) @@ -159,10 +159,15 @@ def load_config_file( help="Path to temporary directory.", ) @click.option( - "--wvd-path", + "--device-path", type=Path, - default=downloader_sig.parameters["wvd_path"].default, - help="Path to .wvd file.", + default=downloader_sig.parameters["device_path"].default, + help="Path to .wvd or .prd file.", +) +@click.option( + "--playready", + is_flag=True, + help="Use PlayReady DRM.", ) @click.option( "--nm3u8dlre-path", @@ -323,7 +328,8 @@ def main( language: str, output_path: Path, temp_path: Path, - wvd_path: Path, + device_path: Path, + playready: bool, nm3u8dlre_path: str, mp4decrypt_path: str, ffmpeg_path: str, @@ -371,7 +377,7 @@ def main( itunes_api, output_path, temp_path, - wvd_path, + device_path, nm3u8dlre_path, mp4decrypt_path, ffmpeg_path, @@ -390,6 +396,7 @@ def main( exclude_tags, cover_size, truncate, + DRM.Playready if playready else DRM.Widevine ) downloader_song = DownloaderSong( downloader, @@ -409,8 +416,8 @@ def main( quality_post, ) if not synced_lyrics_only: - if wvd_path and not wvd_path.exists(): - logger.critical(X_NOT_FOUND_STRING.format(".wvd file", wvd_path)) + if device_path and not device_path.exists(): + logger.critical(X_NOT_FOUND_STRING.format(".wvd file", device_path)) return logger.debug("Setting up CDM") downloader.set_cdm() @@ -509,6 +516,7 @@ def main( lyrics = downloader_song.get_lyrics(track_metadata) logger.debug("Getting webplayback") webplayback = apple_music_api.get_webplayback(track_metadata["id"]) + logger.debug(webplayback) tags = downloader_song.get_tags(webplayback, lyrics.unsynced) if playlist_track: tags = { @@ -548,10 +556,13 @@ def main( stream_info = downloader_song.get_stream_info( track_metadata ) + logger.debug(track_metadata) if not stream_info.stream_url or not stream_info.pssh: logger.warning( f"({queue_progress}) Song is not downloadable or is not" " available in the chosen codec, skipping" + f"\n{stream_info.pssh}, {stream_info.stream_url}" + ) continue logger.debug("Getting decryption key") diff --git a/gamdl/downloader.py b/gamdl/downloader.py index deda270..f0fd57d 100644 --- a/gamdl/downloader.py +++ b/gamdl/downloader.py @@ -15,13 +15,13 @@ from InquirerPy import inquirer from InquirerPy.base.control import Choice from mutagen.mp4 import MP4, MP4Cover from PIL import Image -from pywidevine import PSSH, Cdm, Device + from yt_dlp import YoutubeDL from .apple_music_api import AppleMusicApi from .constants import IMAGE_FILE_EXTENSION_MAP, MP4_TAGS_MAP -from .enums import CoverFormat, DownloadMode, RemuxMode -from .hardcoded_wvd import HARDCODED_WVD +from .enums import CoverFormat, DownloadMode, RemuxMode, DRM +from .hardcoded import HARDCODED_WVD, HARDCODED_PRD from .itunes_api import ItunesApi from .models import DownloadQueue, UrlInfo @@ -37,7 +37,7 @@ class Downloader: itunes_api: ItunesApi, output_path: Path = Path("./Apple Music"), temp_path: Path = Path("./temp"), - wvd_path: Path = None, + device_path: Path = None, nm3u8dlre_path: str = "N_m3u8DL-RE", mp4decrypt_path: str = "mp4decrypt", ffmpeg_path: str = "ffmpeg", @@ -56,13 +56,14 @@ class Downloader: exclude_tags: str = None, cover_size: int = 1200, truncate: int = None, - silent: bool = False, + drm: DRM = DRM.Widevine, + silent: bool = False ): self.apple_music_api = apple_music_api self.itunes_api = itunes_api self.output_path = output_path self.temp_path = temp_path - self.wvd_path = wvd_path + self.device_path = device_path self.nm3u8dlre_path = nm3u8dlre_path self.mp4decrypt_path = mp4decrypt_path self.ffmpeg_path = ffmpeg_path @@ -82,6 +83,7 @@ class Downloader: self.cover_size = cover_size self.truncate = truncate self.silent = silent + self.drm = drm self._set_binaries_path_full() self._set_exclude_tags_list() self._set_truncate() @@ -114,10 +116,14 @@ class Downloader: self.subprocess_additional_args = {} def set_cdm(self): - if self.wvd_path: - self.cdm = Cdm.from_device(Device.load(self.wvd_path)) + if self.drm == DRM.Widevine: + from pywidevine import Cdm, Device + elif self.drm == DRM.Playready: + from pyplayready import Cdm, Device + if self.device_path: + self.cdm = Cdm.from_device(Device.load(self.device_path)) else: - self.cdm = Cdm.from_device(Device.loads(HARDCODED_WVD)) + self.cdm = Cdm.from_device(Device.loads(HARDCODED_WVD if self.drm == DRM.Widevine else HARDCODED_PRD)) def get_url_info(self, url: str) -> UrlInfo: url_info = UrlInfo() @@ -314,24 +320,49 @@ class Downloader: return datetime.datetime.fromisoformat(date[:-1]).strftime(self.template_date) def get_decryption_key(self, pssh: str, track_id: str) -> str: - 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 + if self.drm == DRM.Widevine: + from pywidevine import PSSH + 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_license( + track_id, + pssh, + challenge, + self.drm, + ) + 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) + elif self.drm == DRM.Playready: + from pyplayready import PSSH + try: + pssh_obj = PSSH(pssh.split(",")[-1]).get_wrm_headers(downgrade_to_v4=True)[0] # Downgrade to v4 has to be set to True + cdm_session = self.cdm.open() + challenge = base64.b64encode( + self.cdm.get_license_challenge(cdm_session, pssh_obj).encode("utf-8") + ).decode() + license = self.apple_music_api.get_license( + track_id, + pssh, + challenge, + self.drm, + ) + self.cdm.parse_license(cdm_session, base64.b64decode(license.encode("utf-8")).decode("utf-8")) + decryption_keys = [i.key.hex() for i in self.cdm.get_keys(cdm_session)] + if len(decryption_keys) != 1: + raise ValueError(f"Expecting only one key to be returned, but {len(decryption_keys)} keys were returned") + elif len(decryption_keys) == 1 and "32b8ade1769e26b1ffb8986352793fc6" in decryption_keys: + raise ValueError("Only default key returned for track.") + finally: + self.cdm.close(cdm_session) + return decryption_keys[0] def download(self, path: Path, stream_url: str): if self.download_mode == DownloadMode.YTDLP: diff --git a/gamdl/downloader_song.py b/gamdl/downloader_song.py index 6402166..91a553d 100644 --- a/gamdl/downloader_song.py +++ b/gamdl/downloader_song.py @@ -15,7 +15,7 @@ from InquirerPy.base.control import Choice from .constants import SONG_CODEC_REGEX_MAP, SYNCED_LYRICS_FILE_EXTENSION_MAP from .downloader import Downloader -from .enums import RemuxMode, SongCodec, SyncedLyricsFormat +from .enums import RemuxMode, SongCodec, SyncedLyricsFormat, DRM from .models import Lyrics, StreamInfo @@ -90,20 +90,36 @@ class DownloaderSong: drm_infos: dict, drm_ids: list, ) -> 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" - ), - None, - ) - if not drm_info: - return None - return drm_info["urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]["URI"] + if self.downloader.drm == DRM.Widevine: + 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" + ), + None, + ) + if not drm_info: + return None + return drm_info["urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]["URI"] + else: + drm_info = next( + ( + drm_infos[drm_id] + for drm_id in drm_ids + if drm_infos[drm_id].get( + "com.microsoft.playready" + ) + and drm_id != "1" + ), + None, + ) + if not drm_info: + return None + return drm_info["com.microsoft.playready"]["URI"] def get_stream_info(self, track_metadata: dict) -> StreamInfo: m3u8_url = track_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls") diff --git a/gamdl/downloader_song_legacy.py b/gamdl/downloader_song_legacy.py index 97db1ef..cccd802 100644 --- a/gamdl/downloader_song_legacy.py +++ b/gamdl/downloader_song_legacy.py @@ -37,7 +37,7 @@ class DownloaderSongLegacy(DownloaderSong): challenge = base64.b64encode( self.downloader.cdm.get_license_challenge(cdm_session, pssh_obj) ).decode() - license = self.downloader.apple_music_api.get_widevine_license( + license = self.downloader.apple_music_api.get_license( track_id, pssh, challenge, diff --git a/gamdl/enums.py b/gamdl/enums.py index ff9dc63..699b4b2 100644 --- a/gamdl/enums.py +++ b/gamdl/enums.py @@ -47,3 +47,7 @@ class CoverFormat(Enum): JPG = "jpg" PNG = "png" RAW = "raw" + +class DRM(Enum): + Widevine: str = "WIDEVINE" + Playready: str = "PLAYREADY" \ No newline at end of file diff --git a/gamdl/hardcoded_wvd.py b/gamdl/hardcoded.py similarity index 56% rename from gamdl/hardcoded_wvd.py rename to gamdl/hardcoded.py index cb50946..0678540 100644 --- a/gamdl/hardcoded_wvd.py +++ b/gamdl/hardcoded.py @@ -1 +1,2 @@ HARDCODED_WVD = """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QrSyZcBqHO0QrdPcU35GBT7S7+IJr2AAXvnjqnb8yrtpPWN2ZW/IWUJN2z4vZ7/HV4aj3OZhkxC1DIMNyvsusUKoQQuf8gwKiEe8cFwbwFSicywlFk9la2IPe8oFShcxAzHLCCn/TIYUAvEL3/4LgaZvqWm80qCPYbgIP5HT8hPYkKWJ4WYknEWK+3InbnkzteFfGrQFCq4CCAESEGnj6Ji7LD+4o7MoHYT4jBQYjtW+kQUijgIwggEKAoIBAQDY9um1ifBRIOmkPtDZTqH+CZUBbb0eK0Cn3NHFf8MFUDzPEz+emK/OTub/hNxCJCao//pP5L8tRNUPFDrrvCBMo7Rn+iUb+mA/2yXiJ6ivqcN9Cu9i5qOU1ygon9SWZRsujFFB8nxVreY5Lzeq0283zn1Cg1stcX4tOHT7utPzFG/ReDFQt0O/GLlzVwB0d1sn3SKMO4XLjhZdncrtF9jljpg7xjMIlnWJUqxDo7TQkTytJmUl0kcM7bndBLerAdJFGaXc6oSY4eNy/IGDluLCQR3KZEQsy/mLeV1ggQ44MFr7XOM+rd+4/314q/deQbjHqjWFuVr8iIaKbq+R63ShAgMBAAEo8CISgAMii2Mw6z+Qs1bvvxGStie9tpcgoO2uAt5Zvv0CDXvrFlwnSbo+qR71Ru2IlZWVSbN5XYSIDwcwBzHjY8rNr3fgsXtSJty425djNQtF5+J2jrAhf3Q2m7EI5aohZGpD2E0cr+dVj9o8x0uJR2NWR8FVoVQSXZpad3M/4QzBLNto/tz+UKyZwa7Sc/eTQc2+ZcDS3ZEO3lGRsH864Kf/cEGvJRBBqcpJXKfG+ItqEW1AAPptjuggzmZEzRq5xTGf6or+bXrKjCpBS9G1SOyvCNF1k5z6lG8KsXhgQxL6ADHMoulxvUIihyPY5MpimdXfUdEQ5HA2EqNiNVNIO4qP007jW51yAeThOry4J22xs8RdkIClOGAauLIl0lLA4flMzW+VfQl5xYxP0E5tuhn0h+844DslU8ZF7U1dU2QprIApffXD9wgAACk26Rggy8e96z8i86/+YYyZQkc9hIdCAERrgEYCEbByzONrdRDs1MrS/ch1moV5pJv63BIKvQHGvLkaFwoMY29tcGFueV9uYW1lEgd1bmtub3duGioKCm1vZGVsX25hbWUSHEFuZHJvaWQgU0RLIGJ1aWx0IGZvciB4ODZfNjQaGwoRYXJjaGl0ZWN0dXJlX25hbWUSBng4Nl82NBodCgtkZXZpY2VfbmFtZRIOZ2VuZXJpY194ODZfNjQaIAoMcHJvZHVjdF9uYW1lEhBzZGtfcGhvbmVfeDg2XzY0GmMKCmJ1aWxkX2luZm8SVUFuZHJvaWQvc2RrX3Bob25lX3g4Nl82NC9nZW5lcmljX3g4Nl82NDo5L1BTUjEuMTgwNzIwLjAxMi80OTIzMjE0OnVzZXJkZWJ1Zy90ZXN0LWtleXMaHgoUd2lkZXZpbmVfY2RtX3ZlcnNpb24SBjE0LjAuMBokCh9vZW1fY3J5cHRvX3NlY3VyaXR5X3BhdGNoX2xldmVsEgEwMg4QASAAKA0wAEAASABQAA==""" +HARDCODED_PRD = """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""" diff --git a/gamdl/models.py b/gamdl/models.py index 9f8cec3..0375f57 100644 --- a/gamdl/models.py +++ b/gamdl/models.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass - +from enum import Enum @dataclass class UrlInfo: @@ -27,3 +27,4 @@ class StreamInfo: stream_url: str = None pssh: str = None codec: str = None +