mirror of
https://github.com/vinefeeder/TwinVine.git
synced 2026-07-15 18:10:04 +02:00
service-option
This commit is contained in:
@@ -606,11 +606,11 @@ class DownloadQueueManager:
|
||||
"parameters": job.parameters,
|
||||
}
|
||||
|
||||
payload_fd, payload_path = tempfile.mkstemp(prefix=f"unshackle_job_{job.job_id}_", suffix="_payload.json")
|
||||
payload_fd, payload_path = tempfile.mkstemp(prefix=f"envied.job_{job.job_id}_", suffix="_payload.json")
|
||||
os.close(payload_fd)
|
||||
result_fd, result_path = tempfile.mkstemp(prefix=f"unshackle_job_{job.job_id}_", suffix="_result.json")
|
||||
result_fd, result_path = tempfile.mkstemp(prefix=f"envied.job_{job.job_id}_", suffix="_result.json")
|
||||
os.close(result_fd)
|
||||
progress_fd, progress_path = tempfile.mkstemp(prefix=f"unshackle_job_{job.job_id}_", suffix="_progress.json")
|
||||
progress_fd, progress_path = tempfile.mkstemp(prefix=f"envied.job_{job.job_id}_", suffix="_progress.json")
|
||||
os.close(progress_fd)
|
||||
|
||||
with open(payload_path, "w", encoding="utf-8") as handle:
|
||||
@@ -741,7 +741,7 @@ def get_download_manager() -> DownloadQueueManager:
|
||||
"""Get the global download manager instance."""
|
||||
global download_manager
|
||||
if download_manager is None:
|
||||
# Load configuration from unshackle config
|
||||
# Load configuration from envied.config
|
||||
from envied.core.config import config
|
||||
|
||||
max_concurrent = getattr(config, "max_concurrent_downloads", 2)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
API Error Handling System
|
||||
|
||||
Provides structured error responses with error codes, categorization,
|
||||
and optional debug information for the unshackle REST API.
|
||||
and optional debug information for the envied.REST API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1328,9 +1328,9 @@ def setup_swagger(app: web.Application) -> None:
|
||||
app,
|
||||
swagger_ui_settings=SwaggerUiSettings(path="/api/docs/"),
|
||||
info=SwaggerInfo(
|
||||
title="Unshackle REST API",
|
||||
title="envied.REST API",
|
||||
version=__version__,
|
||||
description="REST API for Unshackle - Modular Movie, TV, and Music Archival Software",
|
||||
description="REST API for envied.- Modular Movie, TV, and Music Archival Software",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class UpdateChecker:
|
||||
DEFAULT_CHECK_INTERVAL: Default time between checks in seconds (24 hours)
|
||||
"""
|
||||
|
||||
REPO_URL = "https://api.github.com/repos/unshackle-dl/unshackle/releases/latest"
|
||||
REPO_URL = "https://api.github.com/repos/envied.dl/envied.releases/latest"
|
||||
TIMEOUT = 5
|
||||
DEFAULT_CHECK_INTERVAL = 24 * 60 * 60
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from envied.core.config import config
|
||||
from envied.core.constants import LANGUAGE_EXACT_DISTANCE, LANGUAGE_MAX_DISTANCE
|
||||
|
||||
"""
|
||||
Utility functions for the unshackle media archival tool.
|
||||
Utility functions for the envied.media archival tool.
|
||||
|
||||
This module provides various utility functions including:
|
||||
- Font discovery and fallback system for subtitle rendering
|
||||
@@ -784,7 +784,7 @@ class DebugLogger:
|
||||
operation="session_start",
|
||||
message="Debug logging session started",
|
||||
context={
|
||||
"unshackle_version": __version__,
|
||||
"envied.version": __version__,
|
||||
"python_version": sys.version,
|
||||
"platform": platform.platform(),
|
||||
"platform_system": platform.system(),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# DisneyPlus(디즈니플러스)
|
||||
|
||||
```
|
||||
uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r SDR,HDR10,DV DSNP entity-4d12671a-f0ad-4c3f-8526-09ae6772390b
|
||||
```
|
||||
|
||||
## Information(정보)
|
||||
|
||||
- Authorization: Credentials, Web Token
|
||||
- Security: UHD@L1/SL3000, FHD@L1/SL3000, HD@L3/SL2000
|
||||
- Working Client Agent: AndroidTV
|
||||
- Support Codec
|
||||
- Video: H264, H265
|
||||
- Audio: AAC, AC3, ATMOS, DTS:X(P2:IMAX)
|
||||
- Range: SDR, HDR10, HDR10, DV
|
||||
|
||||
## Support Args(지원하는 명령어 인자)
|
||||
|
||||
- `-i`, `--imax`: Prefer IMAX Enhanced version if available.
|
||||
- `-r`, `--remastered-ar`: Prefer Remastered Aspect Ratio if available.
|
||||
- `-e`, `--extras`: Select a extras video if available.
|
||||
- `-tu`, `--tier-unlimits`: Remove stream quality restrictions for a specific account.
|
||||
|
||||
## Tips
|
||||
|
||||
- To enable the web refresh token-based login method, please comment out or delete the DSNP section under credentials in `unshackle.yaml`.
|
||||
웹 리프레시 토큰 기반 로그인 방식을 활성화하려면 `unshackle.yaml`의 credentials에서 DSNP부분을 주석 처리하거나 삭제하세요.
|
||||
|
||||
```
|
||||
credentials:
|
||||
...
|
||||
# DSNP: example@example.com:example
|
||||
```
|
||||
|
||||
- Configure user settings within the `unshackle.yaml` file.
|
||||
사용자 설정은 `unshackle.yaml`에서 다음과 같이 사용하세요.
|
||||
|
||||
```
|
||||
services:
|
||||
DSNP:
|
||||
## 사용자 환경설정
|
||||
## User configuration
|
||||
# 해당 설정값이 주석처리 되어 있는 경우에는 설정값들이 자동으로 선택됩니다.
|
||||
# If these settings are commented out, values will be selected automatically.
|
||||
preferences:
|
||||
# 사용할 프로필의 인덱스 번호를 지정합니다. (0 = 첫 번째 프로필, 1 = 두 번째 프로필 등)
|
||||
# Specifies the index of the profile to use. (0 = first profile, 1 = second profile, etc.)
|
||||
# 값이 설정되지 않은 경우에는 자동으로 PIN이 안 걸려 있고 키즈 모드가 아닌 프로필로 자동 선택됩니다.
|
||||
# If no value is set, a profile without a PIN and not in Kids Mode will be automatically selected.
|
||||
profile: 0
|
||||
|
||||
# 서비스 내에서 표시되는 메타데이터 언어를 선택합니다.
|
||||
# Selects the metadata language displayed within the service.
|
||||
# 언어 설정은 Disney+에서 지원하는 언어 코드(예: "ko", "en")만 사용 가능합니다.
|
||||
# Language settings are only available for language codes supported by Disney+ (e.g., "ko", "en").
|
||||
# 값이 설정되지 않은 경우에는 현재 프로필에 설정된 언어 설정을 사용합니다.
|
||||
# If no value is set, the language settings of the current profile will be used.
|
||||
# language: "ko"
|
||||
|
||||
# 매니페스트 로그 출력 레벨을 설정합니다.
|
||||
# Sets the manifest log output level.
|
||||
# 로그를 항상 표시해야 하는 경우 "info"를 사용하고, 그 외의 모든 경우에는 가급적 "debug"를 사용하십시오.
|
||||
# Use "info" if the log must always be displayed; otherwise, use "debug" whenever possible.
|
||||
# 값이 설정되지 않은 경우 기본값은 "debug"로 적용됩니다.
|
||||
# If no value is set, the default level is "debug".
|
||||
# manifest_log: "info"
|
||||
```
|
||||
|
||||
- To enable the tier_unlimits command by default, add the following to `unshackle.yaml`.
|
||||
tier_unlimits 명령을 기본값으로 활성화하려면 `unshackle.yaml`에 다음을 추가하세요.
|
||||
```
|
||||
dl:
|
||||
...
|
||||
DSNP:
|
||||
tier_unlimits: True
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .dsnp import DSNP
|
||||
|
||||
"""
|
||||
Service code for Disney+ Streaming Service (https://disneyplus.com).\n
|
||||
Version: 26.05.06
|
||||
|
||||
Author: Made by CodeName393 and Improvement by Hugov, sp4rk.y with Special Thanks to narakama, Sam\n
|
||||
Authorization: Credentials, Web Token\n
|
||||
Security: UHD@L1/SL3000 FHD@L1/SL3000 HD@L3/SL2000
|
||||
"""
|
||||
__all__ = ("DSNP",)
|
||||
@@ -0,0 +1,84 @@
|
||||
## DO NOT EDIT THIS FILE
|
||||
# 해당 config 파일은 개발자 외에는 수정하지 마세요.
|
||||
# This configuration file should not be modified by anyone other than developers.
|
||||
# 사용자 환경설정은 반드시 "unshackle.yaml"에서만 사용하세요.
|
||||
# User configuration must be performed only in "unshackle.yaml".
|
||||
|
||||
certificate: |
|
||||
CAUSugUKtAIIAxIQbj3s4jO5oUyWjDWqjfr9WRjA2afZBSKOAjCCAQoCggEBALhKWfnyA+FGn5P3tl6ffDjoGq2Oq86hKGl6aZIaGaF7XHPO5mIk7Q35ml
|
||||
ZIgg1A458Udb4eXRws1n+kJFqtZXCY5S1yElLP0Om1WQsoEY2stpl+PZTGnVv/CsOJGKQ8K4KMr7rKjZem9lA9BrBoxgfXY3tbwlnSf3wTEohyANb5Qfpa
|
||||
xsU4v8tQDA8PcjzzV9ICodl6crcFZhAy4QMNXfbWOv/ZrGFx5blSXrzP1sMQ64IY8bjUYw4coZM34NDhu8aCA692g8k2mTz2494x7u3Is8v7RKC9ZNiETE
|
||||
K5/4oeVclXPpelNQokR4uvggnCD1L2EULG/pp6wnk1yWNNLxcCAwEAAToHYmFtdGVjaBKAA2FqHlqkE7EUmdOLiCi0hy5jRgBDJrU1CWNHfH6r2i6s5T5k
|
||||
6LK7ZfD65Tv6uyqq1k82PsDz4++kxbpfJDZaypFbae4XPc6lZxRCc5X0toX/x9TftOQQ4N82l5Hxoha569EPRkrnNy7rO7xrRILa3ZVj1alttEnEEjxEuw
|
||||
SV8usdlUg8/LvLA2C59T/HA2I77k7yVbTrVdy0f81r2l+E2SslivCy1JD3xKlgoaKl4xBnRxItWt8+DCw1Xm2lemYl2LGoh1Wk9gvlXQvr2Jv2+dFX3RNs
|
||||
i5sd00KS9sePszfjoTkQ6fmpRd7ZgFCGFWYB9JZ92aGUFQRE14OTST2uwSf32YCfsoATDNs4V6dB8YDoTGKFGrcoc4gtHPKySGNt7z/fOW4/01ZGzKqoVY
|
||||
Fp3jPq7R0qyt5P6fU5NshbLh5VKcnQvwg62BuKsdwV9u4NV36b2a546hGRl/GBneQ+QDA7NRrgITR33Sz02Oq8yJr3sy24GfZRTbtLJ4qiWkjtw==
|
||||
|
||||
## config ( {configVersion}/{clientId}/{deviceFamily}/{sdkVersion}/{applicationRuntime}/{deviceProfile}/{environment} ) ##
|
||||
# Browser (windows, chrome) : /browser/v34.4/windows/chrome/prod.json
|
||||
# Android Phone : /android/v18.0.0/google/handset/prod.json
|
||||
# Android TV : /android/v18.0.0/google/tv/prod.json
|
||||
# Amazon Fire TV : /android/v18.0.0/amazon/tv/prod.json
|
||||
# Apple Iphone(old) : https://bam-sdk-configs.bamgrid.com/bam-sdk/v2.0/disney-svod-3d9324fc/apple/v9.10.0/ios/iphone/prod.json
|
||||
# Apple Ipad(old) : https://bam-sdk-configs.bamgrid.com/bam-sdk/v2.0/disney-svod-3d9324fc/apple/v9.10.0/ios/ipad/prod.json
|
||||
|
||||
endpoints:
|
||||
config: "https://client-sdk-configs.bamgrid.com/bam-sdk/v7.0/disney-svod-3d9324fc/android/v18.0.0/google/tv/prod.json"
|
||||
|
||||
## user_agent (okhttp/5.0.0-alpha.14) ##
|
||||
# android-phone : BAMSDK/v18.0.0 (disney-svod-3d9324fc 26.0.2+rc1-2026.01.29.0; v7.0/v18.0.0; android; phone)
|
||||
# android-tv : BAMSDK/v18.0.0 (disney-svod-3d9324fc 26.0.2+rc1-2026.01.29.0; v7.0/v18.0.0; android; tv)
|
||||
|
||||
## api_key ##
|
||||
# browser : ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84
|
||||
# android : ZGlzbmV5JmFuZHJvaWQmMS4wLjA.bkeb0m230uUhv8qrAXuNu39tbE_mD5EEhM_NAcohjyA
|
||||
# apple : ZGlzbmV5JmFwcGxlJjEuMC4w.H9L7eJvc2oPYwDgmkoar6HzhBJRuUUzt_PcaC3utBI4
|
||||
|
||||
## yp_service_id ##
|
||||
# browser : 63626081279ebe65eb50fb54
|
||||
# android : 624b805dafc5c73635b1a216
|
||||
|
||||
bamsdk:
|
||||
sdk_version: "18.0.1"
|
||||
application_version: "26.3.0+rc4-2026.03.12.0"
|
||||
explore_version: "v1.14"
|
||||
client: "disney-svod-3d9324fc"
|
||||
user_agent: "BAMSDK/v18.0.1 (disney-svod-3d9324fc 26.3.0+rc4-2026.03.12.0; v7.0/v18.0.0; android; tv)"
|
||||
api_key: "ZGlzbmV5JmFuZHJvaWQmMS4wLjA.bkeb0m230uUhv8qrAXuNu39tbE_mD5EEhM_NAcohjyA"
|
||||
yp_service_id: "624b805dafc5c73635b1a216"
|
||||
|
||||
device:
|
||||
family: "android"
|
||||
profile: "tv"
|
||||
platform: "android/google/tv" # {deviceFamily}/{applicationRuntime}/{deviceProfile}
|
||||
platform_id: "android-tv"
|
||||
applicationRuntime: "android"
|
||||
operatingSystem: "Android"
|
||||
operatingSystemVersion: "16"
|
||||
|
||||
# ## 사용자 환경설정
|
||||
# ## User configuration
|
||||
# # 해당 설정값이 주석처리 되어 있는 경우에는 설정값들이 자동으로 선택됩니다.
|
||||
# # If these settings are commented out, values will be selected automatically.
|
||||
# preferences:
|
||||
# # 사용할 프로필의 인덱스 번호를 지정합니다. (0 = 첫 번째 프로필, 1 = 두 번째 프로필 등)
|
||||
# # Specifies the index of the profile to use. (0 = first profile, 1 = second profile, etc.)
|
||||
# # 값이 설정되지 않은 경우에는 자동으로 PIN이 안 걸려 있고 키즈 모드가 아닌 프로필로 자동 선택됩니다.
|
||||
# # If no value is set, a profile without a PIN and not in Kids Mode will be automatically selected.
|
||||
# profile: 0
|
||||
|
||||
# # 서비스 내에서 표시되는 메타데이터 언어를 선택합니다.
|
||||
# # Selects the metadata language displayed within the service.
|
||||
# # 언어 설정은 Disney+에서 지원하는 언어 코드(예: "ko", "en")만 사용 가능합니다.
|
||||
# # Language settings are only available for language codes supported by Disney+ (e.g., "ko", "en").
|
||||
# # 값이 설정되지 않은 경우에는 현재 프로필에 설정된 언어 설정을 사용합니다.
|
||||
# # If no value is set, the language settings of the current profile will be used.
|
||||
# language: "ko"
|
||||
|
||||
# # 매니페스트 로그 출력 레벨을 설정합니다.
|
||||
# # Sets the manifest log output level.
|
||||
# # 로그를 항상 표시해야 하는 경우 "info"를 사용하고, 그 외의 모든 경우에는 가급적 "debug"를 사용하십시오.
|
||||
# # Use "info" if the log must always be displayed; otherwise, use "debug" whenever possible.
|
||||
# # 값이 설정되지 않은 경우 기본값은 "debug"로 적용됩니다.
|
||||
# # If no value is set, the default level is "debug".
|
||||
# manifest_log: "debug"
|
||||
@@ -0,0 +1,14 @@
|
||||
# REQUEST_DEVICE_CODE = """mutation requestLicensePlate($input: RequestLicensePlateInput!) { requestLicensePlate(requestLicensePlate: $input) { licensePlate expirationTime expiresInSeconds } }"""
|
||||
CHECK_EMAIL = """query check($email: String!) { check(email: $email) { operations nextOperation } }"""
|
||||
LOGIN = """mutation login($input: LoginInput!, $includeIdentity: Boolean!, $includeAccountConsentToken: Boolean!) { login(login: $input) { account { __typename ...accountGraphFragment } actionGrant activeSession { __typename ...sessionGraphFragment } identity @include(if: $includeIdentity) { __typename ...identityGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
LOGIN_ACTION_GRANT = """mutation loginWithActionGrant($input: LoginWithActionGrantInput!, $includeAccountConsentToken: Boolean!) { loginWithActionGrant(login: $input) { account { __typename ...accountGraphFragment } activeSession { __typename ...sessionGraphFragment } identity { __typename ...identityGraphFragment } actionGrant } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
LOGIN_OTP = """mutation authenticateWithOtp($input: AuthenticateWithOtpInput!) { authenticateWithOtp(authenticateWithOtp: $input) { actionGrant securityAction passwordRules { __typename ...passwordRulesFragment } } } fragment passwordRulesFragment on PasswordRules { minLength charTypes }"""
|
||||
ME = """query me($includeAccountConsentToken: Boolean!) { me { account { __typename ...accountGraphFragment } activeSession { __typename ...sessionGraphFragment } identity { __typename ...identityGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
REFRESH_TOKEN = """mutation refreshToken($refreshToken: RefreshTokenInput!) { refreshToken(refreshToken: $refreshToken) { activeSession { sessionId } } }"""
|
||||
REGISTER_DEVICE = """mutation ($registerDevice: RegisterDeviceInput!) { registerDevice(registerDevice: $registerDevice) { __typename } }"""
|
||||
REQUESET_OTP = """mutation requestOtp($input: RequestOtpInput!) { requestOtp(requestOtp: $input) { accepted } }"""
|
||||
SET_IMAX = """mutation updateProfileImaxEnhancedVersion($input: UpdateProfileImaxEnhancedVersionInput!, $includeProfile: Boolean!) { updateProfileImaxEnhancedVersion(updateProfileImaxEnhancedVersion: $input) { accepted profile @include(if: $includeProfile) { __typename ...profileGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } }"""
|
||||
SET_REMASTERED_AR = """mutation updateProfileRemasteredAspectRatio($input: UpdateProfileRemasteredAspectRatioInput!, $includeProfile: Boolean!) { updateProfileRemasteredAspectRatio(updateProfileRemasteredAspectRatio: $input) { accepted profile @include(if: $includeProfile) { __typename ...profileGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } }"""
|
||||
SET_APP_LANGUAGE = """mutation updateProfileAppLanguage($input: UpdateProfileAppLanguageInput!, $includeProfile: Boolean!) { updateProfileAppLanguage(updateProfileAppLanguage: $input) { accepted profile @include(if: $includeProfile) { __typename ...profileGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled isGeminiOnboarded profileLinked languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } linkedProfile { pinProtected } } }"""
|
||||
SWITCH_PROFILE = """mutation switchProfile($input: SwitchProfileInput!, $includeIdentity: Boolean!, $includeAccountConsentToken: Boolean!) { switchProfile(switchProfile: $input) { account { __typename ...accountGraphFragment } activeSession { __typename ...sessionGraphFragment } identity @include(if: $includeIdentity) { __typename ...identityGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
UPDATE_DEVICE = """mutation updateDeviceOperatingSystem($updateDeviceOperatingSystem: UpdateDeviceOperatingSystemInput!) {updateDeviceOperatingSystem(updateDeviceOperatingSystem: $updateDeviceOperatingSystem) {accepted}}"""
|
||||
@@ -0,0 +1,76 @@
|
||||
# DisneyPlus(디즈니플러스)
|
||||
|
||||
```
|
||||
uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r SDR,HDR10,DV DSNP entity-4d12671a-f0ad-4c3f-8526-09ae6772390b
|
||||
```
|
||||
|
||||
## Information(정보)
|
||||
|
||||
- Authorization: Credentials, Web Token
|
||||
- Security: UHD@L1/SL3000, FHD@L1/SL3000, HD@L3/SL2000
|
||||
- Working Client Agent: AndroidTV
|
||||
- Support Codec
|
||||
- Video: H264, H265
|
||||
- Audio: AAC, AC3, ATMOS, DTS:X(P2:IMAX)
|
||||
- Range: SDR, HDR10, HDR10, DV
|
||||
|
||||
## Support Args(지원하는 명령어 인자)
|
||||
|
||||
- `-i`, `--imax`: Prefer IMAX Enhanced version if available.
|
||||
- `-r`, `--remastered-ar`: Prefer Remastered Aspect Ratio if available.
|
||||
- `-e`, `--extras`: Select a extras video if available.
|
||||
- `-tu`, `--tier-unlimits`: Remove stream quality restrictions for a specific account.
|
||||
|
||||
## Tips
|
||||
|
||||
- To enable the web refresh token-based login method, please comment out or delete the DSNP section under credentials in `unshackle.yaml`.
|
||||
웹 리프레시 토큰 기반 로그인 방식을 활성화하려면 `unshackle.yaml`의 credentials에서 DSNP부분을 주석 처리하거나 삭제하세요.
|
||||
|
||||
```
|
||||
credentials:
|
||||
...
|
||||
# DSNP: example@example.com:example
|
||||
```
|
||||
|
||||
- Configure user settings within the `unshackle.yaml` file.
|
||||
사용자 설정은 `unshackle.yaml`에서 다음과 같이 사용하세요.
|
||||
|
||||
```
|
||||
services:
|
||||
DSNP:
|
||||
## 사용자 환경설정
|
||||
## User configuration
|
||||
# 해당 설정값이 주석처리 되어 있는 경우에는 설정값들이 자동으로 선택됩니다.
|
||||
# If these settings are commented out, values will be selected automatically.
|
||||
preferences:
|
||||
# 사용할 프로필의 인덱스 번호를 지정합니다. (0 = 첫 번째 프로필, 1 = 두 번째 프로필 등)
|
||||
# Specifies the index of the profile to use. (0 = first profile, 1 = second profile, etc.)
|
||||
# 값이 설정되지 않은 경우에는 자동으로 PIN이 안 걸려 있고 키즈 모드가 아닌 프로필로 자동 선택됩니다.
|
||||
# If no value is set, a profile without a PIN and not in Kids Mode will be automatically selected.
|
||||
profile: 0
|
||||
|
||||
# 서비스 내에서 표시되는 메타데이터 언어를 선택합니다.
|
||||
# Selects the metadata language displayed within the service.
|
||||
# 언어 설정은 Disney+에서 지원하는 언어 코드(예: "ko", "en")만 사용 가능합니다.
|
||||
# Language settings are only available for language codes supported by Disney+ (e.g., "ko", "en").
|
||||
# 값이 설정되지 않은 경우에는 현재 프로필에 설정된 언어 설정을 사용합니다.
|
||||
# If no value is set, the language settings of the current profile will be used.
|
||||
# language: "ko"
|
||||
|
||||
# 매니페스트 로그 출력 레벨을 설정합니다.
|
||||
# Sets the manifest log output level.
|
||||
# 로그를 항상 표시해야 하는 경우 "info"를 사용하고, 그 외의 모든 경우에는 가급적 "debug"를 사용하십시오.
|
||||
# Use "info" if the log must always be displayed; otherwise, use "debug" whenever possible.
|
||||
# 값이 설정되지 않은 경우 기본값은 "debug"로 적용됩니다.
|
||||
# If no value is set, the default level is "debug".
|
||||
# manifest_log: "info"
|
||||
```
|
||||
|
||||
- To enable the tier_unlimits command by default, add the following to `unshackle.yaml`.
|
||||
tier_unlimits 명령을 기본값으로 활성화하려면 `unshackle.yaml`에 다음을 추가하세요.
|
||||
```
|
||||
dl:
|
||||
...
|
||||
DSNP:
|
||||
tier_unlimits: True
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .dsnp import DSNP
|
||||
|
||||
"""
|
||||
Service code for Disney+ Streaming Service (https://disneyplus.com).\n
|
||||
Version: 26.05.06
|
||||
|
||||
Author: Made by CodeName393 and Improvement by Hugov, sp4rk.y with Special Thanks to narakama, Sam\n
|
||||
Authorization: Credentials, Web Token\n
|
||||
Security: UHD@L1/SL3000 FHD@L1/SL3000 HD@L3/SL2000
|
||||
"""
|
||||
__all__ = ("DSNP",)
|
||||
@@ -0,0 +1,84 @@
|
||||
## DO NOT EDIT THIS FILE
|
||||
# 해당 config 파일은 개발자 외에는 수정하지 마세요.
|
||||
# This configuration file should not be modified by anyone other than developers.
|
||||
# 사용자 환경설정은 반드시 "unshackle.yaml"에서만 사용하세요.
|
||||
# User configuration must be performed only in "unshackle.yaml".
|
||||
|
||||
certificate: |
|
||||
CAUSugUKtAIIAxIQbj3s4jO5oUyWjDWqjfr9WRjA2afZBSKOAjCCAQoCggEBALhKWfnyA+FGn5P3tl6ffDjoGq2Oq86hKGl6aZIaGaF7XHPO5mIk7Q35ml
|
||||
ZIgg1A458Udb4eXRws1n+kJFqtZXCY5S1yElLP0Om1WQsoEY2stpl+PZTGnVv/CsOJGKQ8K4KMr7rKjZem9lA9BrBoxgfXY3tbwlnSf3wTEohyANb5Qfpa
|
||||
xsU4v8tQDA8PcjzzV9ICodl6crcFZhAy4QMNXfbWOv/ZrGFx5blSXrzP1sMQ64IY8bjUYw4coZM34NDhu8aCA692g8k2mTz2494x7u3Is8v7RKC9ZNiETE
|
||||
K5/4oeVclXPpelNQokR4uvggnCD1L2EULG/pp6wnk1yWNNLxcCAwEAAToHYmFtdGVjaBKAA2FqHlqkE7EUmdOLiCi0hy5jRgBDJrU1CWNHfH6r2i6s5T5k
|
||||
6LK7ZfD65Tv6uyqq1k82PsDz4++kxbpfJDZaypFbae4XPc6lZxRCc5X0toX/x9TftOQQ4N82l5Hxoha569EPRkrnNy7rO7xrRILa3ZVj1alttEnEEjxEuw
|
||||
SV8usdlUg8/LvLA2C59T/HA2I77k7yVbTrVdy0f81r2l+E2SslivCy1JD3xKlgoaKl4xBnRxItWt8+DCw1Xm2lemYl2LGoh1Wk9gvlXQvr2Jv2+dFX3RNs
|
||||
i5sd00KS9sePszfjoTkQ6fmpRd7ZgFCGFWYB9JZ92aGUFQRE14OTST2uwSf32YCfsoATDNs4V6dB8YDoTGKFGrcoc4gtHPKySGNt7z/fOW4/01ZGzKqoVY
|
||||
Fp3jPq7R0qyt5P6fU5NshbLh5VKcnQvwg62BuKsdwV9u4NV36b2a546hGRl/GBneQ+QDA7NRrgITR33Sz02Oq8yJr3sy24GfZRTbtLJ4qiWkjtw==
|
||||
|
||||
## config ( {configVersion}/{clientId}/{deviceFamily}/{sdkVersion}/{applicationRuntime}/{deviceProfile}/{environment} ) ##
|
||||
# Browser (windows, chrome) : /browser/v34.4/windows/chrome/prod.json
|
||||
# Android Phone : /android/v18.0.0/google/handset/prod.json
|
||||
# Android TV : /android/v18.0.0/google/tv/prod.json
|
||||
# Amazon Fire TV : /android/v18.0.0/amazon/tv/prod.json
|
||||
# Apple Iphone(old) : https://bam-sdk-configs.bamgrid.com/bam-sdk/v2.0/disney-svod-3d9324fc/apple/v9.10.0/ios/iphone/prod.json
|
||||
# Apple Ipad(old) : https://bam-sdk-configs.bamgrid.com/bam-sdk/v2.0/disney-svod-3d9324fc/apple/v9.10.0/ios/ipad/prod.json
|
||||
|
||||
endpoints:
|
||||
config: "https://client-sdk-configs.bamgrid.com/bam-sdk/v7.0/disney-svod-3d9324fc/android/v18.0.0/google/tv/prod.json"
|
||||
|
||||
## user_agent (okhttp/5.0.0-alpha.14) ##
|
||||
# android-phone : BAMSDK/v18.0.0 (disney-svod-3d9324fc 26.0.2+rc1-2026.01.29.0; v7.0/v18.0.0; android; phone)
|
||||
# android-tv : BAMSDK/v18.0.0 (disney-svod-3d9324fc 26.0.2+rc1-2026.01.29.0; v7.0/v18.0.0; android; tv)
|
||||
|
||||
## api_key ##
|
||||
# browser : ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84
|
||||
# android : ZGlzbmV5JmFuZHJvaWQmMS4wLjA.bkeb0m230uUhv8qrAXuNu39tbE_mD5EEhM_NAcohjyA
|
||||
# apple : ZGlzbmV5JmFwcGxlJjEuMC4w.H9L7eJvc2oPYwDgmkoar6HzhBJRuUUzt_PcaC3utBI4
|
||||
|
||||
## yp_service_id ##
|
||||
# browser : 63626081279ebe65eb50fb54
|
||||
# android : 624b805dafc5c73635b1a216
|
||||
|
||||
bamsdk:
|
||||
sdk_version: "18.0.1"
|
||||
application_version: "26.3.0+rc4-2026.03.12.0"
|
||||
explore_version: "v1.14"
|
||||
client: "disney-svod-3d9324fc"
|
||||
user_agent: "BAMSDK/v18.0.1 (disney-svod-3d9324fc 26.3.0+rc4-2026.03.12.0; v7.0/v18.0.0; android; tv)"
|
||||
api_key: "ZGlzbmV5JmFuZHJvaWQmMS4wLjA.bkeb0m230uUhv8qrAXuNu39tbE_mD5EEhM_NAcohjyA"
|
||||
yp_service_id: "624b805dafc5c73635b1a216"
|
||||
|
||||
device:
|
||||
family: "android"
|
||||
profile: "tv"
|
||||
platform: "android/google/tv" # {deviceFamily}/{applicationRuntime}/{deviceProfile}
|
||||
platform_id: "android-tv"
|
||||
applicationRuntime: "android"
|
||||
operatingSystem: "Android"
|
||||
operatingSystemVersion: "16"
|
||||
|
||||
# ## 사용자 환경설정
|
||||
# ## User configuration
|
||||
# # 해당 설정값이 주석처리 되어 있는 경우에는 설정값들이 자동으로 선택됩니다.
|
||||
# # If these settings are commented out, values will be selected automatically.
|
||||
# preferences:
|
||||
# # 사용할 프로필의 인덱스 번호를 지정합니다. (0 = 첫 번째 프로필, 1 = 두 번째 프로필 등)
|
||||
# # Specifies the index of the profile to use. (0 = first profile, 1 = second profile, etc.)
|
||||
# # 값이 설정되지 않은 경우에는 자동으로 PIN이 안 걸려 있고 키즈 모드가 아닌 프로필로 자동 선택됩니다.
|
||||
# # If no value is set, a profile without a PIN and not in Kids Mode will be automatically selected.
|
||||
# profile: 0
|
||||
|
||||
# # 서비스 내에서 표시되는 메타데이터 언어를 선택합니다.
|
||||
# # Selects the metadata language displayed within the service.
|
||||
# # 언어 설정은 Disney+에서 지원하는 언어 코드(예: "ko", "en")만 사용 가능합니다.
|
||||
# # Language settings are only available for language codes supported by Disney+ (e.g., "ko", "en").
|
||||
# # 값이 설정되지 않은 경우에는 현재 프로필에 설정된 언어 설정을 사용합니다.
|
||||
# # If no value is set, the language settings of the current profile will be used.
|
||||
# language: "ko"
|
||||
|
||||
# # 매니페스트 로그 출력 레벨을 설정합니다.
|
||||
# # Sets the manifest log output level.
|
||||
# # 로그를 항상 표시해야 하는 경우 "info"를 사용하고, 그 외의 모든 경우에는 가급적 "debug"를 사용하십시오.
|
||||
# # Use "info" if the log must always be displayed; otherwise, use "debug" whenever possible.
|
||||
# # 값이 설정되지 않은 경우 기본값은 "debug"로 적용됩니다.
|
||||
# # If no value is set, the default level is "debug".
|
||||
# manifest_log: "debug"
|
||||
@@ -0,0 +1,14 @@
|
||||
# REQUEST_DEVICE_CODE = """mutation requestLicensePlate($input: RequestLicensePlateInput!) { requestLicensePlate(requestLicensePlate: $input) { licensePlate expirationTime expiresInSeconds } }"""
|
||||
CHECK_EMAIL = """query check($email: String!) { check(email: $email) { operations nextOperation } }"""
|
||||
LOGIN = """mutation login($input: LoginInput!, $includeIdentity: Boolean!, $includeAccountConsentToken: Boolean!) { login(login: $input) { account { __typename ...accountGraphFragment } actionGrant activeSession { __typename ...sessionGraphFragment } identity @include(if: $includeIdentity) { __typename ...identityGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
LOGIN_ACTION_GRANT = """mutation loginWithActionGrant($input: LoginWithActionGrantInput!, $includeAccountConsentToken: Boolean!) { loginWithActionGrant(login: $input) { account { __typename ...accountGraphFragment } activeSession { __typename ...sessionGraphFragment } identity { __typename ...identityGraphFragment } actionGrant } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
LOGIN_OTP = """mutation authenticateWithOtp($input: AuthenticateWithOtpInput!) { authenticateWithOtp(authenticateWithOtp: $input) { actionGrant securityAction passwordRules { __typename ...passwordRulesFragment } } } fragment passwordRulesFragment on PasswordRules { minLength charTypes }"""
|
||||
ME = """query me($includeAccountConsentToken: Boolean!) { me { account { __typename ...accountGraphFragment } activeSession { __typename ...sessionGraphFragment } identity { __typename ...identityGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
REFRESH_TOKEN = """mutation refreshToken($refreshToken: RefreshTokenInput!) { refreshToken(refreshToken: $refreshToken) { activeSession { sessionId } } }"""
|
||||
REGISTER_DEVICE = """mutation ($registerDevice: RegisterDeviceInput!) { registerDevice(registerDevice: $registerDevice) { __typename } }"""
|
||||
REQUESET_OTP = """mutation requestOtp($input: RequestOtpInput!) { requestOtp(requestOtp: $input) { accepted } }"""
|
||||
SET_IMAX = """mutation updateProfileImaxEnhancedVersion($input: UpdateProfileImaxEnhancedVersionInput!, $includeProfile: Boolean!) { updateProfileImaxEnhancedVersion(updateProfileImaxEnhancedVersion: $input) { accepted profile @include(if: $includeProfile) { __typename ...profileGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } }"""
|
||||
SET_REMASTERED_AR = """mutation updateProfileRemasteredAspectRatio($input: UpdateProfileRemasteredAspectRatioInput!, $includeProfile: Boolean!) { updateProfileRemasteredAspectRatio(updateProfileRemasteredAspectRatio: $input) { accepted profile @include(if: $includeProfile) { __typename ...profileGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } }"""
|
||||
SET_APP_LANGUAGE = """mutation updateProfileAppLanguage($input: UpdateProfileAppLanguageInput!, $includeProfile: Boolean!) { updateProfileAppLanguage(updateProfileAppLanguage: $input) { accepted profile @include(if: $includeProfile) { __typename ...profileGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled isGeminiOnboarded profileLinked languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } linkedProfile { pinProtected } } }"""
|
||||
SWITCH_PROFILE = """mutation switchProfile($input: SwitchProfileInput!, $includeIdentity: Boolean!, $includeAccountConsentToken: Boolean!) { switchProfile(switchProfile: $input) { account { __typename ...accountGraphFragment } activeSession { __typename ...sessionGraphFragment } identity @include(if: $includeIdentity) { __typename ...identityGraphFragment } } } fragment profileGraphFragment on Profile { id name personalInfo { dateOfBirth gender } maturityRating { ratingSystem ratingSystemValues contentMaturityRating maxRatingSystemValue isMaxContentMaturityRating suggestedMaturityRatings { minimumAge maximumAge ratingSystemValue } } isAge21Verified flows { star { eligibleForOnboarding isOnboarded } personalInfo { eligibleForCollection requiresCollection } } attributes { isDefault kidsModeEnabled languagePreferences { appLanguage playbackLanguage preferAudioDescription preferSDH subtitleLanguage subtitlesEnabled } parentalControls { isPinProtected kidProofExitEnabled liveAndUnratedContent { enabled available } } playbackSettings { autoplay backgroundVideo backgroundAudio prefer133 preferImaxEnhancedVersion } avatar { id userSelected } privacySettings { consents { consentType value } } } } fragment accountGraphFragment on Account { id umpMessages { data { messages { messageId messageSource displayLocations content } } } accountConsentToken @include(if: $includeAccountConsentToken) activeProfile { id umpMessages { data { messages { messageId content } } } } profiles { __typename ...profileGraphFragment } profileRequirements { primaryProfiles { personalInfo { requiresCollection } } secondaryProfiles { personalInfo { requiresCollection } personalInfoJrMode { requiresCollection } } } parentalControls { isProfileCreationProtected } flows { star { isOnboarded } } attributes { email emailVerified userVerified maxNumberOfProfilesAllowed locations { manual { country } purchase { country } registration { geoIp { country } } } } } fragment sessionGraphFragment on Session { sessionId device { id } entitlements experiments { featureId variantId version } features { coPlay download noAds } homeLocation { countryCode adsSupported } inSupportedLocation isSubscriber location { countryCode adsSupported } portabilityLocation { countryCode } preferredMaturityRating { impliedMaturityRating ratingSystem } } fragment identityGraphFragment on Identity { id email repromptSubscriberAgreement attributes { passwordResetRequired } commerce { notifications { subscriptionId type showNotification offerData { productType expectedTransition { date price { amount currency } } cypherKeys { key value type } } currentOffer { offerId price { amount currency frequency } } } } flows { marketingPreferences { isOnboarded eligibleForOnboarding } personalInfo { eligibleForCollection requiresCollection } } personalInfo { dateOfBirth gender } locations { purchase { country } } subscriber { subscriberStatus subscriptionAtRisk overlappingSubscription doubleBilled doubleBilledProviders subscriptions { id groupId state partner isEntitled source { sourceProvider sourceType subType sourceRef } product { id sku name entitlements { id name partner } bundle subscriptionPeriod earlyAccess trial { duration } categoryCodes } stacking { status overlappingSubscriptionProviders previouslyStacked previouslyStackedByProvider } term { purchaseDate startDate expiryDate nextRenewalDate pausedDate churnedDate isFreeTrial } } } consent { id idType token } }"""
|
||||
UPDATE_DEVICE = """mutation updateDeviceOperatingSystem($updateDeviceOperatingSystem: UpdateDeviceOperatingSystemInput!) {updateDeviceOperatingSystem(updateDeviceOperatingSystem: $updateDeviceOperatingSystem) {accepted}}"""
|
||||
@@ -0,0 +1,2 @@
|
||||
The sevices in this folder have constraints onthe python version they use.
|
||||
Only copy them into your services folder if they have the same python version that you have.
|
||||
@@ -0,0 +1,501 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Generator
|
||||
from http.cookiejar import MozillaCookieJar
|
||||
from typing import Any, Optional, Union
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import click
|
||||
from click import Context
|
||||
from lxml import etree
|
||||
from pywidevine.cdm import Cdm as WidevineCdm
|
||||
from requests import Request
|
||||
|
||||
from envied.core.cdm.detect import is_playready_cdm
|
||||
from envied.core.console import console
|
||||
from envied.core.constants import AnyTrack
|
||||
from envied.core.credential import Credential
|
||||
from envied.core.manifests.dash import DASH
|
||||
from envied.core.manifests.ism import ISM
|
||||
from envied.core.search_result import SearchResult
|
||||
from envied.core.service import Service
|
||||
from envied.core.titles import Episode, Movie, Movies, Series, Title_T
|
||||
from envied.core.tracks import Audio, Chapter, Chapters, Tracks, Video
|
||||
|
||||
|
||||
class ThreeNow(Service):
|
||||
"""
|
||||
envied.support for the ThreeNow streaming service (https://www.threenow.co.nz).
|
||||
Supports TV Series / Episodes. Movies and sports are yet to be supported.
|
||||
Note - ThreeNow is still stuck in the past with 720p content.
|
||||
|
||||
Version: 1.0.0
|
||||
Author: jungyein
|
||||
Authorization: None
|
||||
CDM Support:
|
||||
WV L3: 720p, AAC2.0
|
||||
PR SL2000: 720p, AAC2.0
|
||||
|
||||
Tips:
|
||||
- ISM ONLY works with n_m3u8dl_re and not aria2c or requests
|
||||
"""
|
||||
|
||||
GEOFENCE = ("nz", "au")
|
||||
TITLE_RE = (
|
||||
r"^(?:(?:https?://)?(?:www\.)?threenow\.co\.nz/)?"
|
||||
r"(?:shows/(?:[^/?#]+/)*?)?"
|
||||
r"(?P<show_id>[Ss]?\d+(?:-\d+)*)"
|
||||
r"(?:/(?P<episode_id>[Mm]?\d+(?:-\d+)*))?/?(?:[?#].*)?$"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@click.command(name="ThreeNow", short_help="https://www.threenow.co.nz", help=__doc__)
|
||||
@click.argument("title", type=str)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--playlist",
|
||||
type=click.Choice(["dash", "ism"], case_sensitive=False),
|
||||
default="dash",
|
||||
help="playlist type, dash or ism",
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx: Context, **kwargs: Any) -> ThreeNow:
|
||||
return ThreeNow(ctx, **kwargs)
|
||||
|
||||
def __init__(self, ctx: Context, title: str, playlist: str):
|
||||
self.title = title
|
||||
self.playlist = playlist.lower()
|
||||
|
||||
if self.playlist == "ism" and not is_playready_cdm(getattr(ctx.obj, "cdm", None)):
|
||||
raise click.UsageError(
|
||||
"ThreeNow ISM streams require a PlayReady CDM. Configure/select a PlayReady CDM or use -p dash.",
|
||||
ctx,
|
||||
)
|
||||
|
||||
super().__init__(ctx)
|
||||
self.license = {}
|
||||
|
||||
self.session.headers.update(self.config["headers"])
|
||||
|
||||
def search(self) -> Generator[SearchResult, None, None]:
|
||||
self.log.error("ThreeNow search is not implemented yet")
|
||||
yield from ()
|
||||
|
||||
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
|
||||
self.session.verify = False
|
||||
|
||||
def get_titles(self) -> Union[Movies, Series]:
|
||||
match = re.match(self.TITLE_RE, self.title)
|
||||
if not match:
|
||||
raise ValueError(f"Could not parse ID from title: {self.title}")
|
||||
|
||||
self.show_id = match.group("show_id")
|
||||
self.episode_id = match.group("episode_id")
|
||||
|
||||
page = self._request("GET", "shows/{}".format(self.show_id))
|
||||
# with open(f"debug/ThreeNow_{self.show_id.replace('/', '_')}.json", "w") as f:
|
||||
# f.write(json.dumps(page, indent=4))
|
||||
|
||||
seasons = page.get("seasons") or []
|
||||
if not seasons:
|
||||
raise ValueError(f"Could not find show from url: {self.title}")
|
||||
|
||||
title = page.get("name", "")
|
||||
episodes = []
|
||||
|
||||
for season in seasons:
|
||||
for episode in season.get("episodes", []):
|
||||
if self.episode_id and episode.get("videoId") != self.episode_id:
|
||||
continue
|
||||
|
||||
episodeData = episode.copy()
|
||||
episodeData["seasonNumber"] = season.get("seasonNumber", "1")
|
||||
episodeData["episodeNumber"] = episode.get("episode", "1")
|
||||
episodes.extend(self._episode(episodeData, title))
|
||||
self.log.info(f"Parsed episode: {title} - {episode.get('name')} (S{episodeData['seasonNumber']:02}E{episodeData['episodeNumber']:02})")
|
||||
|
||||
if not episodes:
|
||||
raise ValueError(f"Could not find episode from url: {self.title}")
|
||||
|
||||
return Series(episodes)
|
||||
|
||||
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
|
||||
brightcodeEpisodeId = title.data.get("externalMediaId")
|
||||
if not brightcodeEpisodeId:
|
||||
raise ValueError("Unable to find Brightcove video ID for this episode")
|
||||
|
||||
account_id = self.config["endpoints"]["brightcove_account"]
|
||||
self.drm_token = None
|
||||
data = self._request(
|
||||
"GET", self.config["endpoints"]["brightcove"].format(account_id, brightcodeEpisodeId),
|
||||
headers={"BCOV-POLICY": self.config["policy"]},
|
||||
)
|
||||
|
||||
sources = data["sources"]
|
||||
dash_source = next((
|
||||
source for source in sources
|
||||
if source.get("type") == "application/dash+xml"
|
||||
and (source.get("key_systems") or {}).get("com.widevine.alpha")),
|
||||
None,
|
||||
) or next((
|
||||
source for source in sources
|
||||
if (source.get("key_systems") or {}).get("com.widevine.alpha")),
|
||||
None,
|
||||
)
|
||||
if not dash_source:
|
||||
raise ValueError("Could not find a DASH source")
|
||||
|
||||
dash_playready_source = next((
|
||||
source for source in sources
|
||||
if source.get("type") == "application/dash+xml"
|
||||
and (source.get("key_systems") or {}).get("com.microsoft.playready")),
|
||||
dash_source,
|
||||
)
|
||||
dash_key_systems = dash_source.get("key_systems") or {}
|
||||
dash_playready_key_systems = dash_playready_source.get("key_systems") or {}
|
||||
self.license[Video.Descriptor.DASH] = {
|
||||
"widevine": dash_key_systems.get("com.widevine.alpha", {}).get("license_url"),
|
||||
"playready": dash_playready_key_systems.get("com.microsoft.playready", {}).get("license_url"),
|
||||
}
|
||||
source_manifest = dash_source.get("src")
|
||||
|
||||
# manifest = self.trim_duration(source_manifest)
|
||||
tmpDashTracks = DASH.from_url(source_manifest, self.session).to_tracks(title.language)
|
||||
|
||||
dashTracks = Tracks()
|
||||
|
||||
highestDashAudioBitrate = 0
|
||||
for tmpTrack in tmpDashTracks:
|
||||
# some rare cases have a 1080p video which cannot be decrypted
|
||||
# if isinstance(tmpTrack, Video):
|
||||
# if tmpTrack.height == 1080:
|
||||
# continue
|
||||
if isinstance(tmpTrack, Audio):
|
||||
highestDashAudioBitrate = max(highestDashAudioBitrate, tmpTrack.bitrate or 0)
|
||||
dashTracks.add(tmpTrack)
|
||||
|
||||
if self.playlist == "ism":
|
||||
ism_source = next((
|
||||
source for source in sources
|
||||
if source.get("type") == "application/vnd.ms-sstr+xml"),
|
||||
None,
|
||||
)
|
||||
if not ism_source:
|
||||
raise ValueError("Could not find an ISM source")
|
||||
|
||||
ism_key_systems = ism_source.get("key_systems") or {}
|
||||
self.license[Video.Descriptor.ISM] = {
|
||||
"playready": ism_key_systems.get("com.microsoft.playready", {}).get("license_url"),
|
||||
}
|
||||
source_manifest = ism_source.get("src")
|
||||
|
||||
# manifest = self.trim_duration(source_manifest, "ism")
|
||||
ismTracks = ISM.from_url(source_manifest, self.session).to_tracks(title.language)
|
||||
tracks = dashTracks
|
||||
|
||||
for tmpTrack in ismTracks:
|
||||
if isinstance(tmpTrack, Audio):
|
||||
# Filter out ISM audio tracks with bitrate <= highest DASH audio track
|
||||
# to avoid duplicate audio tracks. To keep all ISM audio tracks,
|
||||
# comment out this if block.
|
||||
if (tmpTrack.bitrate or 0) <= highestDashAudioBitrate:
|
||||
continue
|
||||
tracks.add(tmpTrack)
|
||||
else:
|
||||
tracks = dashTracks
|
||||
|
||||
# sort the videos by quality
|
||||
tracks.sort_videos()
|
||||
tracks.sort_audio()
|
||||
|
||||
# return the list of tracks
|
||||
console.log(tracks)
|
||||
console.log()
|
||||
|
||||
return tracks
|
||||
|
||||
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
|
||||
chapters = {}
|
||||
|
||||
for cue_point in title.data.get("cuePoints") or []:
|
||||
timestamp = cue_point.get("time") if isinstance(cue_point, dict) else cue_point
|
||||
if timestamp is not None:
|
||||
chapters[float(timestamp)] = None
|
||||
|
||||
credits_timestamp = title.data.get("creditsCuePoint")
|
||||
if credits_timestamp is not None:
|
||||
chapters[float(credits_timestamp)] = "Credits"
|
||||
|
||||
return Chapters(Chapter(timestamp=timestamp, name=name) for timestamp, name in sorted(chapters.items()))
|
||||
|
||||
def get_widevine_service_certificate(self, **_: Any) -> str:
|
||||
return WidevineCdm.common_privacy_cert
|
||||
|
||||
def _get_license_url(self, descriptor: Video.Descriptor, drm: str) -> str:
|
||||
try:
|
||||
selectedLicense = self.license[descriptor][drm]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"No {drm} license endpoint for {descriptor.name}") from e
|
||||
|
||||
if not selectedLicense:
|
||||
raise ValueError(f"No {drm} license endpoint for {descriptor.name}")
|
||||
|
||||
return selectedLicense
|
||||
|
||||
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[bytes]:
|
||||
selectedLicense = self._get_license_url(track.descriptor, "playready")
|
||||
|
||||
headers = {"Authorization": f"Bearer {self.drm_token}"} if self.drm_token else self.session.headers
|
||||
|
||||
r = self.session.post(selectedLicense, headers=headers, data=challenge)
|
||||
r.raise_for_status()
|
||||
|
||||
return r.content
|
||||
|
||||
def get_widevine_license(self, challenge: bytes, title: Title_T, track: AnyTrack, **_: Any) -> str:
|
||||
selectedLicense = self._get_license_url(track.descriptor, "widevine")
|
||||
|
||||
headers = {"Authorization": f"Bearer {self.drm_token}"} if self.drm_token else self.session.headers
|
||||
r = self.session.post(selectedLicense, headers=headers, data=challenge)
|
||||
r.raise_for_status()
|
||||
|
||||
return r.content
|
||||
|
||||
# Service specific
|
||||
|
||||
def _show(self, episodes: list, title: str) -> Episode:
|
||||
return [
|
||||
Episode(
|
||||
id_=episode.get("videoId"),
|
||||
service=self.__class__,
|
||||
title=title,
|
||||
season=int(episode.get("seasonNumber")) if episode.get("seasonNumber") else 0,
|
||||
number=int(episode.get("episodeNumber")) if episode.get("episodeNumber") else 0,
|
||||
name=episode.get("title"),
|
||||
language="en",
|
||||
data=episode,
|
||||
description=episode.get("synopsis") if episode.get("synopsis") else None,
|
||||
)
|
||||
for episode in episodes
|
||||
]
|
||||
|
||||
def _movie(self, movies: list, title: str) -> Movie:
|
||||
return [
|
||||
Movie(
|
||||
id_=movie.get("videoId"),
|
||||
service=self.__class__,
|
||||
name=title,
|
||||
year=None,
|
||||
language="en",
|
||||
data=movie,
|
||||
)
|
||||
for movie in movies
|
||||
]
|
||||
|
||||
def _episode(self, video: dict, title: str) -> list[Episode]:
|
||||
kind = video.get("type")
|
||||
name = video.get("title") or video.get("name")
|
||||
|
||||
if kind == "sportVideo" and video.get("_embedded"):
|
||||
_type = next((x for x in video["_embedded"].values() if x.get("type") == "competition"), None)
|
||||
title = _type.get("title") if _type else title
|
||||
name = video.get("title", "") + " " + video.get("phase", "")
|
||||
|
||||
return [
|
||||
Episode(
|
||||
id_=video.get("videoId"),
|
||||
service=self.__class__,
|
||||
title=title,
|
||||
season=int(video.get("seasonNumber")) if video.get("seasonNumber") else 0,
|
||||
number=int(video.get("episodeNumber")) if video.get("episodeNumber") else 0,
|
||||
name=name if name[:6] != "Season" else None,
|
||||
language="en",
|
||||
data=video,
|
||||
description=video.get("synopsis") if video.get("synopsis") else None,
|
||||
)
|
||||
]
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
api: str,
|
||||
params: dict = None,
|
||||
headers: dict = None,
|
||||
payload: dict = None,
|
||||
) -> Any[dict | str]:
|
||||
url = urljoin(self.config["endpoints"]["base_api"], api)
|
||||
if headers:
|
||||
self.session.headers.update(headers)
|
||||
|
||||
prep = self.session.prepare_request(Request(method, url, params=params, json=payload))
|
||||
response = self.session.send(prep)
|
||||
|
||||
try:
|
||||
# with open(f"debug/ThreeNow_{api.replace('/', '_')}.json", "w") as f:
|
||||
# f.write(response.text)
|
||||
data = json.loads(response.content)
|
||||
|
||||
if data.get("message"):
|
||||
raise ConnectionError(f"{response.status_code} - {data.get('message')}")
|
||||
|
||||
return data
|
||||
|
||||
except Exception:
|
||||
raise ConnectionError("Request failed: {} - {}".format(response.status_code, response.text))
|
||||
|
||||
def trim_duration(self, source_manifest: str, playlist: str = "dash") -> str:
|
||||
"""
|
||||
The last segment on all tracks may return a 404 for some reason, causing a failed download.
|
||||
Trim the manifest by exactly one segment to account for that.
|
||||
"""
|
||||
|
||||
if playlist == "dash":
|
||||
manifest = DASH.from_url(source_manifest, self.session).manifest
|
||||
trimmed = self._trim_dash_manifest(manifest)
|
||||
else:
|
||||
manifest = ISM.from_url(source_manifest, self.session).manifest
|
||||
trimmed = self._trim_ism_manifest(manifest)
|
||||
|
||||
if not trimmed:
|
||||
self.log.warning("Unable to trim the final %s manifest segment", playlist.upper())
|
||||
|
||||
# with open("debug/ThreeNow_trimmed.mpd", "w") as f:
|
||||
# f.write(etree.tostring(manifest, encoding="unicode"))
|
||||
|
||||
return etree.tostring(manifest, encoding="unicode")
|
||||
|
||||
def _trim_dash_manifest(self, manifest: Any) -> bool:
|
||||
trimmed = False
|
||||
|
||||
for segment_timeline in manifest.findall(".//SegmentTimeline"):
|
||||
if self._trim_dash_segment_timeline(segment_timeline):
|
||||
segment_template = segment_timeline.getparent()
|
||||
if segment_template is not None:
|
||||
self._trim_dash_end_number(segment_template)
|
||||
trimmed = True
|
||||
|
||||
for segment_template in manifest.findall(".//SegmentTemplate"):
|
||||
if segment_template.find("SegmentTimeline") is not None:
|
||||
continue
|
||||
if self._trim_dash_end_number(segment_template):
|
||||
trimmed = True
|
||||
continue
|
||||
|
||||
period = self._find_parent(segment_template, "Period")
|
||||
period_duration = period.get("duration") if period is not None else None
|
||||
period_duration = period_duration or manifest.get("mediaPresentationDuration")
|
||||
segment_duration = float(segment_template.get("duration") or 0)
|
||||
timescale = float(segment_template.get("timescale") or 1)
|
||||
if not period_duration or not segment_duration:
|
||||
continue
|
||||
|
||||
segment_count = math.ceil(DASH.pt_to_sec(period_duration) / (segment_duration / timescale))
|
||||
if segment_count > 1:
|
||||
start_number = int(segment_template.get("startNumber") or 1)
|
||||
segment_template.set("endNumber", str(start_number + segment_count - 2))
|
||||
trimmed = True
|
||||
|
||||
for segment_list in manifest.findall(".//SegmentList"):
|
||||
segment_urls = segment_list.findall("SegmentURL")
|
||||
if len(segment_urls) > 1:
|
||||
segment_list.remove(segment_urls[-1])
|
||||
trimmed = True
|
||||
|
||||
return trimmed
|
||||
|
||||
@staticmethod
|
||||
def _trim_dash_segment_timeline(segment_timeline: Any) -> bool:
|
||||
segments = segment_timeline.findall("S")
|
||||
if not segments:
|
||||
return False
|
||||
|
||||
last_segment = segments[-1]
|
||||
repeat = int(last_segment.get("r") or 0)
|
||||
if repeat > 0:
|
||||
last_segment.set("r", str(repeat - 1))
|
||||
return True
|
||||
|
||||
if len(segments) == 1:
|
||||
return False
|
||||
|
||||
segment_timeline.remove(last_segment)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _trim_dash_end_number(segment_template: Any) -> bool:
|
||||
end_number = segment_template.get("endNumber")
|
||||
if not end_number:
|
||||
return False
|
||||
|
||||
start_number = int(segment_template.get("startNumber") or 1)
|
||||
end_number = int(end_number)
|
||||
if end_number <= start_number:
|
||||
return False
|
||||
|
||||
segment_template.set("endNumber", str(end_number - 1))
|
||||
return True
|
||||
|
||||
def _trim_ism_manifest(self, manifest: Any) -> bool:
|
||||
trimmed = False
|
||||
end_times = []
|
||||
manifest_duration = int(manifest.get("Duration") or 0)
|
||||
|
||||
for stream_index in manifest.findall("StreamIndex"):
|
||||
segments = self._get_ism_segments(stream_index, manifest_duration)
|
||||
if len(segments) <= 1:
|
||||
continue
|
||||
|
||||
trimmed_segments = segments[:-1]
|
||||
self._replace_ism_segments(stream_index, trimmed_segments)
|
||||
end_times.append(trimmed_segments[-1][0] + trimmed_segments[-1][1])
|
||||
trimmed = True
|
||||
|
||||
if trimmed and end_times:
|
||||
manifest.set("Duration", str(max(end_times)))
|
||||
|
||||
return trimmed
|
||||
|
||||
@staticmethod
|
||||
def _get_ism_segments(stream_index: Any, manifest_duration: int) -> list[tuple[int, int]]:
|
||||
fragments = stream_index.findall("c")
|
||||
fragment_time = 0
|
||||
segments = []
|
||||
|
||||
for idx, fragment in enumerate(fragments):
|
||||
fragment_time = int(fragment.get("t", fragment_time))
|
||||
repeat = int(fragment.get("r") or 1)
|
||||
duration = int(fragment.get("d") or 0)
|
||||
if not duration:
|
||||
try:
|
||||
next_time = int(fragments[idx + 1].get("t"))
|
||||
except (IndexError, TypeError):
|
||||
next_time = manifest_duration
|
||||
duration = int((next_time - fragment_time) / repeat) if repeat else 0
|
||||
|
||||
for _ in range(repeat):
|
||||
segments.append((fragment_time, duration))
|
||||
fragment_time += duration
|
||||
|
||||
return segments
|
||||
|
||||
@staticmethod
|
||||
def _replace_ism_segments(stream_index: Any, segments: list[tuple[int, int]]) -> None:
|
||||
for fragment in stream_index.findall("c"):
|
||||
stream_index.remove(fragment)
|
||||
|
||||
for start_time, duration in segments:
|
||||
fragment = etree.SubElement(stream_index, "c")
|
||||
fragment.set("t", str(start_time))
|
||||
fragment.set("d", str(duration))
|
||||
|
||||
@staticmethod
|
||||
def _find_parent(element: Any, tag: str) -> Optional[Any]:
|
||||
parent = element.getparent()
|
||||
while parent is not None:
|
||||
if parent.tag == tag:
|
||||
return parent
|
||||
parent = parent.getparent()
|
||||
return None
|
||||
@@ -0,0 +1,9 @@
|
||||
headers:
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36
|
||||
|
||||
endpoints:
|
||||
base_api: https://now-api.fullscreen.nz/v5/
|
||||
brightcove: https://edge.api.brightcove.com/playback/v1/accounts/{}/videos/{}
|
||||
brightcove_account: 3812193411001
|
||||
|
||||
policy: BCpkADawqM2NDYVFYXV66rIDrq6i9YpFSTom-hlJ_pdoGkeWuItRDsn1Bhm7QVyQvFIF0OExqoywBvX5-aAFaxYHPlq9st-1mQ73ZONxFHTx0N7opvkHJYpbd_Hi1gJuPP5qCFxyxB8oevg-
|
||||
@@ -16,7 +16,7 @@ from envied.core.console import console
|
||||
from envied.core.constants import AnyTrack
|
||||
from envied.core.credential import Credential
|
||||
from envied.core.tracks import Chapters, Tracks, Subtitle, Chapter
|
||||
#from envied.core.utilities import get_ip_info
|
||||
from envied.core.utils import ip_info
|
||||
from envied.core.manifests import HLS, DASH
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
Reference in New Issue
Block a user