Merge pull request #155 from chu23465/main

Reworked slightly to add PlayReady DRM support.
This commit is contained in:
Rafael Moraes 2024-12-14 12:51:49 -03:00 committed by GitHub
commit f219dd1deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 139 additions and 61 deletions

View File

@ -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:

View File

@ -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

View File

@ -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")

View File

@ -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,16 +320,19 @@ class Downloader:
return datetime.datetime.fromisoformat(date[:-1]).strftime(self.template_date)
def get_decryption_key(self, pssh: str, track_id: str) -> str:
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_widevine_license(
license = self.apple_music_api.get_license(
track_id,
pssh,
challenge,
self.drm,
)
self.cdm.parse_license(cdm_session, license)
decryption_key = next(
@ -331,7 +340,29 @@ class Downloader:
).key.hex()
finally:
self.cdm.close(cdm_session)
return decryption_key
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:

View File

@ -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,6 +90,7 @@ class DownloaderSong:
drm_infos: dict,
drm_ids: list,
) -> str | None:
if self.downloader.drm == DRM.Widevine:
drm_info = next(
(
drm_infos[drm_id]
@ -104,6 +105,21 @@ class DownloaderSong:
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")

View File

@ -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,

View File

@ -47,3 +47,7 @@ class CoverFormat(Enum):
JPG = "jpg"
PNG = "png"
RAW = "raw"
class DRM(Enum):
Widevine: str = "WIDEVINE"
Playready: str = "PLAYREADY"

View File

@ -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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"""

View File

@ -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