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` | | `--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` | | `--output-path`, `-o` / `output_path` | Path to output directory. | `./Apple Music` |
| `--temp-path` / `temp_path` | Path to temporary directory. | `./temp` | | `--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` | | `--nm3u8dlre-path` / `nm3u8dlre_path` | Path to N_m3u8DL-RE binary. | `N_m3u8DL-RE` |
| `--mp4decrypt-path` / `mp4decrypt_path` | Path to mp4decrypt binary. | `mp4decrypt` | | `--mp4decrypt-path` / `mp4decrypt_path` | Path to mp4decrypt binary. | `mp4decrypt` |
| `--ffmpeg-path` / `ffmpeg_path` | Path to FFmpeg binary. | `ffmpeg` | | `--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` | | `--codec-music-video` / `codec_music_video` | Music video codec. | `h264` |
| `--quality-post` / `quality_post` | Post video quality. | `best` | | `--quality-post` / `quality_post` | Post video quality. | `best` |
| `--no-config-file`, `-n` / - | Do not use a config file. | `false` | | `--no-config-file`, `-n` / - | Do not use a config file. | `false` |
| `--playready`, `playready` / - | Use Playready DRM | `false` |
### Tags variables ### Tags variables
The following variables can be used in the template folders/files and/or in the `exclude_tags` list: 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-legacy`
* `aac-he-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: 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`
* `aac-he` * `aac-he`
@ -190,6 +191,7 @@ The following codecs are also available, **but are not guaranteed to work**, as
* `alac` * `alac`
* `ask` * `ask`
* When using this option, Gamdl will ask you which codec from this list to use that is available for the song. * 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 ### Music videos codecs
The following codecs are available: The following codecs are available:

View File

@ -1,11 +1,14 @@
from __future__ import annotations from __future__ import annotations
import functools import functools
import json
import re import re
import time import time
import typing import typing
from http.cookiejar import MozillaCookieJar from http.cookiejar import MozillaCookieJar
from pathlib import Path from pathlib import Path
from .enums import DRM
import requests import requests
@ -39,7 +42,7 @@ class AppleMusicApi:
self.storefront = self.session.cookies.get_dict()["itua"] self.storefront = self.session.cookies.get_dict()["itua"]
self.session.headers.update( 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": "application/json",
"Accept-Language": "en-US,en;q=0.5", "Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
@ -256,32 +259,41 @@ class AppleMusicApi:
self._raise_response_exception(response) self._raise_response_exception(response)
return webplayback[0] return webplayback[0]
def get_widevine_license( def get_license(
self, self,
track_id: str, track_id: str,
track_uri: str, track_uri: str,
challenge: str, challenge: str,
drm: DRM
) -> str: ) -> str:
response = self.session.post( response = self.session.post(
self.LICENSE_API_URL, self.LICENSE_API_URL,
json={ json={
"challenge": challenge, "challenge": challenge,
"key-system": "com.widevine.alpha", "key-system": "com.widevine.alpha" if drm == DRM.Widevine else "com.microsoft.playready",
"uri": track_uri, "uri": track_uri,
"adamId": track_id, "adamId": track_id,
"isLibrary": False, "isLibrary": False,
"user-initiated": True, "user-initiated": False,
}, },
) )
try: try:
response.raise_for_status() response.raise_for_status()
response_dict = response.json() response_dict = response.json()
widevine_license = response_dict.get("license") license = response_dict.get("license")
assert widevine_license assert license
except ( except (
requests.HTTPError, requests.HTTPError,
requests.exceptions.JSONDecodeError, requests.exceptions.JSONDecodeError,
AssertionError, 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) 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_post import DownloaderPost
from .downloader_song import DownloaderSong from .downloader_song import DownloaderSong
from .downloader_song_legacy import DownloaderSongLegacy 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 from .itunes_api import ItunesApi
apple_music_api_sig = inspect.signature(AppleMusicApi.__init__) apple_music_api_sig = inspect.signature(AppleMusicApi.__init__)
@ -159,10 +159,15 @@ def load_config_file(
help="Path to temporary directory.", help="Path to temporary directory.",
) )
@click.option( @click.option(
"--wvd-path", "--device-path",
type=Path, type=Path,
default=downloader_sig.parameters["wvd_path"].default, default=downloader_sig.parameters["device_path"].default,
help="Path to .wvd file.", help="Path to .wvd or .prd file.",
)
@click.option(
"--playready",
is_flag=True,
help="Use PlayReady DRM.",
) )
@click.option( @click.option(
"--nm3u8dlre-path", "--nm3u8dlre-path",
@ -323,7 +328,8 @@ def main(
language: str, language: str,
output_path: Path, output_path: Path,
temp_path: Path, temp_path: Path,
wvd_path: Path, device_path: Path,
playready: bool,
nm3u8dlre_path: str, nm3u8dlre_path: str,
mp4decrypt_path: str, mp4decrypt_path: str,
ffmpeg_path: str, ffmpeg_path: str,
@ -371,7 +377,7 @@ def main(
itunes_api, itunes_api,
output_path, output_path,
temp_path, temp_path,
wvd_path, device_path,
nm3u8dlre_path, nm3u8dlre_path,
mp4decrypt_path, mp4decrypt_path,
ffmpeg_path, ffmpeg_path,
@ -390,6 +396,7 @@ def main(
exclude_tags, exclude_tags,
cover_size, cover_size,
truncate, truncate,
DRM.Playready if playready else DRM.Widevine
) )
downloader_song = DownloaderSong( downloader_song = DownloaderSong(
downloader, downloader,
@ -409,8 +416,8 @@ def main(
quality_post, quality_post,
) )
if not synced_lyrics_only: if not synced_lyrics_only:
if wvd_path and not wvd_path.exists(): if device_path and not device_path.exists():
logger.critical(X_NOT_FOUND_STRING.format(".wvd file", wvd_path)) logger.critical(X_NOT_FOUND_STRING.format(".wvd file", device_path))
return return
logger.debug("Setting up CDM") logger.debug("Setting up CDM")
downloader.set_cdm() downloader.set_cdm()
@ -509,6 +516,7 @@ def main(
lyrics = downloader_song.get_lyrics(track_metadata) lyrics = downloader_song.get_lyrics(track_metadata)
logger.debug("Getting webplayback") logger.debug("Getting webplayback")
webplayback = apple_music_api.get_webplayback(track_metadata["id"]) webplayback = apple_music_api.get_webplayback(track_metadata["id"])
logger.debug(webplayback)
tags = downloader_song.get_tags(webplayback, lyrics.unsynced) tags = downloader_song.get_tags(webplayback, lyrics.unsynced)
if playlist_track: if playlist_track:
tags = { tags = {
@ -548,10 +556,13 @@ def main(
stream_info = downloader_song.get_stream_info( stream_info = downloader_song.get_stream_info(
track_metadata track_metadata
) )
logger.debug(track_metadata)
if not stream_info.stream_url or not stream_info.pssh: if not stream_info.stream_url or not stream_info.pssh:
logger.warning( logger.warning(
f"({queue_progress}) Song is not downloadable or is not" f"({queue_progress}) Song is not downloadable or is not"
" available in the chosen codec, skipping" " available in the chosen codec, skipping"
f"\n{stream_info.pssh}, {stream_info.stream_url}"
) )
continue continue
logger.debug("Getting decryption key") logger.debug("Getting decryption key")

View File

@ -15,13 +15,13 @@ from InquirerPy import inquirer
from InquirerPy.base.control import Choice from InquirerPy.base.control import Choice
from mutagen.mp4 import MP4, MP4Cover from mutagen.mp4 import MP4, MP4Cover
from PIL import Image from PIL import Image
from pywidevine import PSSH, Cdm, Device
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from .apple_music_api import AppleMusicApi from .apple_music_api import AppleMusicApi
from .constants import IMAGE_FILE_EXTENSION_MAP, MP4_TAGS_MAP from .constants import IMAGE_FILE_EXTENSION_MAP, MP4_TAGS_MAP
from .enums import CoverFormat, DownloadMode, RemuxMode from .enums import CoverFormat, DownloadMode, RemuxMode, DRM
from .hardcoded_wvd import HARDCODED_WVD from .hardcoded import HARDCODED_WVD, HARDCODED_PRD
from .itunes_api import ItunesApi from .itunes_api import ItunesApi
from .models import DownloadQueue, UrlInfo from .models import DownloadQueue, UrlInfo
@ -37,7 +37,7 @@ class Downloader:
itunes_api: ItunesApi, itunes_api: ItunesApi,
output_path: Path = Path("./Apple Music"), output_path: Path = Path("./Apple Music"),
temp_path: Path = Path("./temp"), temp_path: Path = Path("./temp"),
wvd_path: Path = None, device_path: Path = None,
nm3u8dlre_path: str = "N_m3u8DL-RE", nm3u8dlre_path: str = "N_m3u8DL-RE",
mp4decrypt_path: str = "mp4decrypt", mp4decrypt_path: str = "mp4decrypt",
ffmpeg_path: str = "ffmpeg", ffmpeg_path: str = "ffmpeg",
@ -56,13 +56,14 @@ class Downloader:
exclude_tags: str = None, exclude_tags: str = None,
cover_size: int = 1200, cover_size: int = 1200,
truncate: int = None, truncate: int = None,
silent: bool = False, drm: DRM = DRM.Widevine,
silent: bool = False
): ):
self.apple_music_api = apple_music_api self.apple_music_api = apple_music_api
self.itunes_api = itunes_api self.itunes_api = itunes_api
self.output_path = output_path self.output_path = output_path
self.temp_path = temp_path self.temp_path = temp_path
self.wvd_path = wvd_path self.device_path = device_path
self.nm3u8dlre_path = nm3u8dlre_path self.nm3u8dlre_path = nm3u8dlre_path
self.mp4decrypt_path = mp4decrypt_path self.mp4decrypt_path = mp4decrypt_path
self.ffmpeg_path = ffmpeg_path self.ffmpeg_path = ffmpeg_path
@ -82,6 +83,7 @@ class Downloader:
self.cover_size = cover_size self.cover_size = cover_size
self.truncate = truncate self.truncate = truncate
self.silent = silent self.silent = silent
self.drm = drm
self._set_binaries_path_full() self._set_binaries_path_full()
self._set_exclude_tags_list() self._set_exclude_tags_list()
self._set_truncate() self._set_truncate()
@ -114,10 +116,14 @@ class Downloader:
self.subprocess_additional_args = {} self.subprocess_additional_args = {}
def set_cdm(self): def set_cdm(self):
if self.wvd_path: if self.drm == DRM.Widevine:
self.cdm = Cdm.from_device(Device.load(self.wvd_path)) 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: 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: def get_url_info(self, url: str) -> UrlInfo:
url_info = UrlInfo() url_info = UrlInfo()
@ -314,24 +320,49 @@ class Downloader:
return datetime.datetime.fromisoformat(date[:-1]).strftime(self.template_date) return datetime.datetime.fromisoformat(date[:-1]).strftime(self.template_date)
def get_decryption_key(self, pssh: str, track_id: str) -> str: def get_decryption_key(self, pssh: str, track_id: str) -> str:
try: if self.drm == DRM.Widevine:
pssh_obj = PSSH(pssh.split(",")[-1]) from pywidevine import PSSH
cdm_session = self.cdm.open() try:
challenge = base64.b64encode( pssh_obj = PSSH(pssh.split(",")[-1])
self.cdm.get_license_challenge(cdm_session, pssh_obj) cdm_session = self.cdm.open()
).decode() challenge = base64.b64encode(
license = self.apple_music_api.get_widevine_license( self.cdm.get_license_challenge(cdm_session, pssh_obj)
track_id, ).decode()
pssh, license = self.apple_music_api.get_license(
challenge, track_id,
) pssh,
self.cdm.parse_license(cdm_session, license) challenge,
decryption_key = next( self.drm,
i for i in self.cdm.get_keys(cdm_session) if i.type == "CONTENT" )
).key.hex() self.cdm.parse_license(cdm_session, license)
finally: decryption_key = next(
self.cdm.close(cdm_session) i for i in self.cdm.get_keys(cdm_session) if i.type == "CONTENT"
return decryption_key ).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): def download(self, path: Path, stream_url: str):
if self.download_mode == DownloadMode.YTDLP: 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 .constants import SONG_CODEC_REGEX_MAP, SYNCED_LYRICS_FILE_EXTENSION_MAP
from .downloader import Downloader from .downloader import Downloader
from .enums import RemuxMode, SongCodec, SyncedLyricsFormat from .enums import RemuxMode, SongCodec, SyncedLyricsFormat, DRM
from .models import Lyrics, StreamInfo from .models import Lyrics, StreamInfo
@ -90,20 +90,36 @@ class DownloaderSong:
drm_infos: dict, drm_infos: dict,
drm_ids: list, drm_ids: list,
) -> str | None: ) -> str | None:
drm_info = next( if self.downloader.drm == DRM.Widevine:
( drm_info = next(
drm_infos[drm_id] (
for drm_id in drm_ids drm_infos[drm_id]
if drm_infos[drm_id].get( for drm_id in drm_ids
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" if drm_infos[drm_id].get(
) "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"
and drm_id != "1" )
), and drm_id != "1"
None, ),
) None,
if not drm_info: )
return None if not drm_info:
return drm_info["urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]["URI"] 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: def get_stream_info(self, track_metadata: dict) -> StreamInfo:
m3u8_url = track_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls") m3u8_url = track_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")

View File

@ -37,7 +37,7 @@ class DownloaderSongLegacy(DownloaderSong):
challenge = base64.b64encode( challenge = base64.b64encode(
self.downloader.cdm.get_license_challenge(cdm_session, pssh_obj) self.downloader.cdm.get_license_challenge(cdm_session, pssh_obj)
).decode() ).decode()
license = self.downloader.apple_music_api.get_widevine_license( license = self.downloader.apple_music_api.get_license(
track_id, track_id,
pssh, pssh,
challenge, challenge,

View File

@ -47,3 +47,7 @@ class CoverFormat(Enum):
JPG = "jpg" JPG = "jpg"
PNG = "png" PNG = "png"
RAW = "raw" 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_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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
@dataclass @dataclass
class UrlInfo: class UrlInfo:
@ -27,3 +27,4 @@ class StreamInfo:
stream_url: str = None stream_url: str = None
pssh: str = None pssh: str = None
codec: str = None codec: str = None