Use wrapper decrypt for music videos

This commit is contained in:
glomatico
2026-07-08 13:58:04 -03:00
parent b01939dc54
commit dda8bbb02b
3 changed files with 129 additions and 8 deletions
+32 -6
View File
@@ -2,7 +2,7 @@ from pathlib import Path
from ..interface.enums import CoverFormat
from ..interface.types import AppleMusicMedia, DecryptionKeyAv
from .amdecrypt import decrypt_and_mux_hex
from .amdecrypt import decrypt_and_mux_hex, decrypt_and_mux_wrapper
from .base import AppleMusicBaseDownloader
from .enums import RemuxFormatMusicVideo, RemuxMode
from .types import DownloadItem
@@ -22,15 +22,38 @@ class AppleMusicMusicVideoDownloader:
encrypted_path_video: str,
encrypted_path_audio: str,
staged_path: str,
decryption_key: DecryptionKeyAv,
media_id: str,
decryption_key: DecryptionKeyAv | None = None,
fairplay_key_video: str | None = None,
fairplay_key_audio: str | None = None,
is_m4v: bool = False,
):
await decrypt_and_mux_hex(
decryption_key.audio_track.key,
if decryption_key:
await decrypt_and_mux_hex(
decryption_key.audio_track.key,
encrypted_path_audio,
staged_path,
decryption_key.video_track.key,
encrypted_path_video,
m4v_brand=is_m4v,
)
return
wrapper_api = self.base.interface.base.wrapper_api
if wrapper_api is None:
raise ValueError("wrapper_api is required for FairPlay decrypt")
if not fairplay_key_audio or not fairplay_key_video:
raise ValueError("music video FairPlay keys are required for wrapper decrypt")
await decrypt_and_mux_wrapper(
wrapper_api,
media_id,
encrypted_path_audio,
staged_path,
decryption_key.video_track.key,
encrypted_path_video,
fairplay_key_audio=fairplay_key_audio,
input_video_path=encrypted_path_video,
fairplay_key_video=fairplay_key_video,
use_single_content_key=True,
m4v_brand=is_m4v,
)
@@ -102,7 +125,10 @@ class AppleMusicMusicVideoDownloader:
encrypted_path_video,
encrypted_path_audio,
download_item.staged_path,
download_item.media.media_id,
download_item.media.decryption_key,
download_item.media.stream_info.video_track.fairplay_key,
download_item.media.stream_info.audio_track.fairplay_key,
download_item.staged_path.endswith(".m4v"),
)
+10 -2
View File
@@ -214,6 +214,7 @@ class AppleMusicMusicVideoInterface:
file_format = MediaFileFormat.M4V
stream_info = StreamInfoAv(
media_id=media_id,
video_track=stream_info_video,
audio_track=stream_info_audio,
file_format=file_format,
@@ -491,13 +492,20 @@ class AppleMusicMusicVideoInterface:
m3u8_master_url,
)
if (
if self.base.wrapper_api:
if (
not media.stream_info.video_track.fairplay_key
or not media.stream_info.audio_track.fairplay_key
):
raise GamdlInterfaceDecryptionNotAvailableError(media.media_id)
elif (
not media.stream_info.video_track.widevine_pssh
or not media.stream_info.audio_track.widevine_pssh
):
raise GamdlInterfaceDecryptionNotAvailableError(media.media_id)
media.decryption_key = await self.get_decryption_key(media.stream_info)
if not self.base.wrapper_api:
media.decryption_key = await self.get_decryption_key(media.stream_info)
media.partial = False
+87
View File
@@ -0,0 +1,87 @@
from types import SimpleNamespace
import pytest
from gamdl.downloader.music_video import AppleMusicMusicVideoDownloader
from gamdl.interface.types import DecryptionKey, DecryptionKeyAv
@pytest.mark.asyncio
async def test_music_video_stage_uses_hex_when_decryption_key_present(monkeypatch):
calls = []
async def fake_hex(*args, **kwargs):
calls.append(("hex", args, kwargs))
async def fake_wrapper(*args, **kwargs):
calls.append(("wrapper", args, kwargs))
monkeypatch.setattr("gamdl.downloader.music_video.decrypt_and_mux_hex", fake_hex)
monkeypatch.setattr(
"gamdl.downloader.music_video.decrypt_and_mux_wrapper", fake_wrapper
)
downloader = AppleMusicMusicVideoDownloader(base=SimpleNamespace())
await downloader.stage(
"video.mp4",
"audio.m4a",
"out.m4v",
"12345",
DecryptionKeyAv(
video_track=DecryptionKey(key="video-key"),
audio_track=DecryptionKey(key="audio-key"),
),
"skd://video",
"skd://audio",
True,
)
assert len(calls) == 1
name, args, kwargs = calls[0]
assert name == "hex"
assert args[:5] == ("audio-key", "audio.m4a", "out.m4v", "video-key", "video.mp4")
assert kwargs == {"m4v_brand": True}
@pytest.mark.asyncio
async def test_music_video_stage_uses_wrapper_without_decryption_key(monkeypatch):
calls = []
wrapper_api = object()
base = SimpleNamespace(
interface=SimpleNamespace(base=SimpleNamespace(wrapper_api=wrapper_api))
)
async def fake_hex(*args, **kwargs):
calls.append(("hex", args, kwargs))
async def fake_wrapper(*args, **kwargs):
calls.append(("wrapper", args, kwargs))
monkeypatch.setattr("gamdl.downloader.music_video.decrypt_and_mux_hex", fake_hex)
monkeypatch.setattr(
"gamdl.downloader.music_video.decrypt_and_mux_wrapper", fake_wrapper
)
downloader = AppleMusicMusicVideoDownloader(base=base)
await downloader.stage(
"video.mp4",
"audio.m4a",
"out.m4v",
"12345",
None,
"skd://video",
"skd://audio",
True,
)
assert len(calls) == 1
name, args, kwargs = calls[0]
assert name == "wrapper"
assert args == (wrapper_api, "12345", "audio.m4a", "out.m4v")
assert kwargs == {
"fairplay_key_audio": "skd://audio",
"input_video_path": "video.mp4",
"fairplay_key_video": "skd://video",
"use_single_content_key": True,
"m4v_brand": True,
}