updates 5.1.0

This commit is contained in:
VineFeeder
2026-06-04 13:18:27 +01:00
parent 7e10493609
commit 8e8a1cc49c
7 changed files with 0 additions and 2011 deletions
@@ -1,76 +0,0 @@
# DisneyPlus(디즈니플러스)
```
uv run envied.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, 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 `envied.yaml`.
웹 리프레시 토큰 기반 로그인 방식을 활성화하려면 `envied.yaml`의 credentials에서 DSNP부분을 주석 처리하거나 삭제하세요.
```
credentials:
...
# DSNP: example@example.com:example
```
- Configure user settings within the `envied.yaml` file.
사용자 설정은 `envied.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 `envied.yaml`.
tier_unlimits 명령을 기본값으로 활성화하려면 `envied.yaml`에 다음을 추가하세요.
```
dl:
...
DSNP:
tier_unlimits: True
```
File diff suppressed because it is too large Load Diff
@@ -1,84 +0,0 @@
## DO NOT EDIT THIS FILE
# 해당 config 파일은 개발자 외에는 수정하지 마세요.
# This configuration file should not be modified by anyone other than developers.
# 사용자 환경설정은 반드시 "envied.yaml"에서만 사용하세요.
# User configuration must be performed only in "envied.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.13"
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"
@@ -1,14 +0,0 @@
# 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}}"""
@@ -1,19 +0,0 @@
# U-NEXT(유넥스트)
```
uv run envied.dl --list -vl all -al orig -sl all -q 2160 -v h.265 -r HDR10 UNXT SID0087607
```
## Information
- Authorization: Credentials
- Security: UHD@L3/SL2000, FHD@L3/SL2000
- Working Client Agent: AndroidTV
- Support Codec
- Video: H264, H265
- Audio: AAC, AC3, ATMOS
- Range: SDR, HDR10, DV
## Support Args
- `-t`, `--type`: Prefer subtitle or dubbing version for content type if available.
@@ -1,662 +0,0 @@
from __future__ import annotations
import click
import re
import sys
import time
import uuid
from click import Context
from collections.abc import Generator
from datetime import datetime
from http.cookiejar import CookieJar
from langcodes import Language
from pathlib import Path
from requests import Request
from typing import Any, Optional, Union, List, Literal
from urllib.parse import urlparse, parse_qs, urlencode
from envied.core.cdm.detect import is_playready_cdm
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests import DASH, ISM
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Title_T, Titles_T, Episode, Movie, Movies, Series
from envied.core.tracks import Chapters, Tracks, Video, Audio, Subtitle, Attachment, Chapter
from envied.core.utilities import get_ip_info
class UNXT(Service):
"""
Service code for U-NEXT Streaming Service (https://video.unext.jp).\n
Version: 26.03.01
Author: Made by CodeName393 with Special Thanks to narakama\n
Authorization: Credentials\n
Security: UHD@L3/SL2000, FHD@L3/SL2000
"""
ALIASES = ("UNXT", "UNEXT", "U-NEXT")
GEOFENCE = ("JP",)
TITLE_RE = (r"https?://(?:[\w-]+\.)?unext\.jp/.*?(?:/|td=)(?P<id>SID[0-9]+)",)
@staticmethod
@click.command(name="U-NEXT", short_help="https://video.unext.jp", help=__doc__)
@click.argument("title", type=str)
@click.option(
"-t",
"--type",
type=click.Choice(["sub", "dub"], case_sensitive=False),
default="sub",
help="Prefer subtitle or dubbing version for content type if available.",
)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> UNXT:
return UNXT(ctx, **kwargs)
def __init__(self, ctx: Context, title: str, type: Literal["sub", "dub"]):
self.title = title
super().__init__(ctx)
self.title_id = self.title
for pattern in self.TITLE_RE:
match = re.match(pattern, self.title)
if match:
self.title_id = match.group("id")
break
self.dub_type = type
self.cdm = ctx.obj.cdm
self.device_id = None
self.account_tokens = {}
self.active_session = {}
self.playback_data = {}
self.log.info("Preparing...")
ip_info = get_ip_info(self.session)
country_key = None
possible_keys = ["countryCode", "country", "country_code", "country-code"]
for key in possible_keys:
if key in ip_info:
country_key = key
break
if country_key:
region = str(ip_info[country_key]).upper()
self.log.info(f" + IP Region: {region}")
else:
self.log.warning(f" - The region could not be determined from IP information: {ip_info}")
region = "US"
self.log.info(f" + IP Region: {region} (By Default)")
if region != "JP":
self.log.error(" - It is not currently available in the country.", exc_info=False)
sys.exit(1)
self.session.headers.update(
{
"User-Agent": self.config["device"]["user_agent"],
"Content-Type": "application/json; charset=utf-8",
"Accept-Encoding": "gzip",
}
)
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
self.credentials = credential
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
self.log.info("Logging into U-NEXT...")
self._login()
self.log.debug(self.active_session)
self.log.info(f" + Account ID: {self.active_session['cuid']}")
self.log.info(f" + Platform ID: {self.active_session['pfid']}")
self.log.info(f" + Subscribed: {self.active_session['contractStatusCode'] == 'ACTIVE'}")
def _login(self) -> None:
cache = self.cache.get(f"tokens_{self.credentials.sha1}")
if cache:
try:
self.log.info(" + Using cached tokens...")
self.account_tokens = cache.data.get("tokens", {})
self.device_id = cache.data.get("device_id")
required_keys = ["userToken", "userTokenExpire", "securityToken"]
if not self.account_tokens or not all(k in self.account_tokens for k in required_keys) or not self.device_id:
raise ValueError("Invalid token data in cache")
except (KeyError, ValueError, TypeError) as e:
self.log.warning(f" - Cached token data is invalid or corrupted ({e}). Getting new tokens...")
self._perform_full_login()
try:
self._refresh()
except Exception as e:
self.log.warning(f" - Failed to refresh token ({e}). Getting new tokens...")
self._perform_full_login()
else:
self.log.info(" + Getting new tokens...")
self._perform_full_login()
self.log.info(" + Fetching session data...")
self.active_session = self._get_account_info()
self.log.info("Session data setup successfully.")
def _perform_full_login(self) -> None:
if not self.device_id:
self.device_id = str(uuid.uuid4())
user_info = self._login_with_password(self.credentials.username, self.credentials.password)
token_data = {
"userToken": user_info["userToken"],
"userTokenExpire": user_info["userTokenExpire"],
"securityToken": user_info["securityToken"],
}
self._apply_new_tokens(token_data)
def _refresh(self) -> None:
cache = self.cache.get(f"tokens_{self.credentials.sha1}")
if not cache.expired:
self.log.debug(f" + Token is valid until: {datetime.fromtimestamp(cache.expiration.timestamp()).strftime('%Y-%m-%d %H:%M:%S')}")
return
self.log.warning(" + Token expired. Refreshing...")
try:
user_info = self._refresh_token()
token_data = {
"userToken": user_info["userToken"],
"userTokenExpire": user_info["userTokenExpire"],
"securityToken": user_info["securityToken"],
}
self._apply_new_tokens(token_data)
except Exception as e:
self.log.error(f"Refresh failed: {e}.", exc_info=False)
raise e
def _apply_new_tokens(self, token_data: dict) -> None:
self.account_tokens = token_data
expire_time = token_data["userTokenExpire"]
valid_duration = max(0, expire_time - int(time.time()))
cache_data = {
"tokens": self.account_tokens,
"device_id": self.device_id,
}
cache = self.cache.get(f"tokens_{self.credentials.sha1}")
cache.set(cache_data, valid_duration - 60)
self.log.debug(f" + New Token is valid until: {datetime.fromtimestamp(cache.expiration.timestamp()).strftime('%Y-%m-%d %H:%M:%S')}")
def search(self) -> Generator[SearchResult, None, None]:
data = self._get_search(self.title)
if not data["title_list"]:
return
for result in data["title_list"]:
year = result["production_year"]
if not year:
year = result["since_year"]
yield SearchResult(
id_=result["title_code"],
title=result["title_name"],
description=result["catchphrase"],
label=year,
url=f"https://video.unext.jp/title/{result['title_code']}",
)
def get_titles(self) -> Titles_T:
stage_data = self._get_stage_data(self.title_id)
content_type = stage_data["media_type_code"]
self.log.debug(f" + Content Type: {content_type.upper()}")
if content_type in ["MOVIE", "VIDEO", "OV"]:
year = stage_data["production_year"]
if not year:
year = stage_data["since_year"]
movie_data = self._get_movie_data(self.title_id)
return Movies(
[
Movie(
id_=movie_data["episode"]["episode_code"],
service=self.__class__,
name=stage_data["title_name"],
description=stage_data["story"],
year=year,
language=Language.get("en"),
data=movie_data["episode"],
)
]
)
elif content_type == "TV":
return Series(self._get_series(stage_data))
else:
self.log.error(f" - Unsupported content type: {content_type}", exc_info=False)
sys.exit(1)
def _get_series(self, stage_data: dict) -> Series:
title_relation = self._get_title_relation(self.title_id)
all_seasons = {}
special_keywords = ["メイキング", "making", "special", "特典", "映像"]
current_year = stage_data["production_year"]
if not current_year:
current_year = stage_data["since_year"]
title_name_main = stage_data["title_name"]
is_special_main = any(k in title_name_main.lower() for k in special_keywords)
raw_order_main = int(stage_data["series_in_order"])
if is_special_main:
order_main = 0
else:
order_main = raw_order_main if raw_order_main > 0 else 1
series_name = stage_data["series_name"]
if not series_name:
series_name = title_name_main
all_seasons[self.title_id] = {
"order": order_main,
"name": series_name,
"year": current_year,
}
if "relation" in title_relation:
for rel_group in title_relation["relation"]:
if rel_group["groupcode"] != stage_data["series_code"]:
continue
for item in rel_group["grouplist"]:
if item["media_type_code"] != "TV":
continue
item_year = item["production_year"]
if not item_year:
item_year = item["since_year"]
title_name_sub = item["title_name"]
is_special_sub = any(k in title_name_sub.lower() for k in special_keywords)
raw_order_sub = int(item["series_in_order"])
if is_special_sub:
order_sub = 0
else:
order_sub = raw_order_sub if raw_order_sub > 0 else 1
all_seasons[item["title_code"]] = {
"order": order_sub,
"name": item["series_name"],
"year": item_year,
}
sorted_season_ids = sorted(all_seasons.keys(), key=lambda x: all_seasons[x]["order"])
episodes: List[Episode] = []
regular_season_idx = 1
special_episode_idx = 1
for season_id in sorted_season_ids:
season_info = all_seasons[season_id]
if season_info["order"] == 0:
current_season_number = 0
else:
current_season_number = regular_season_idx
regular_season_idx += 1
episode_list_data = self._get_episode_list(season_id)
for idx, episode in enumerate(episode_list_data["items"], start=1):
if current_season_number == 0:
episode_number = special_episode_idx
special_episode_idx += 1
else:
episode_number = idx
episodes.append(
Episode(
id_=episode["episode_code"],
service=self.__class__,
title=season_info["name"],
season=current_season_number,
number=episode_number,
name=episode["episode_name"],
description=episode["introduction"],
year=season_info["year"],
language=Language.get("en"),
data=episode,
)
)
return episodes
def get_tracks(self, title: Title_T) -> Tracks:
self._refresh() # Safe Access
play_mode = "caption"
if title.data["has_dub"] and self.dub_type == "dub":
play_mode = "dub"
title.language = Language.get("ja")
fetched_ranges = set()
def _fetch_variant(title: Title_T, codec: Optional[Video.Codec], range_: Video.Range) -> Tracks:
if codec == Video.Codec.AVC and range_ in (Video.Range.DV, Video.Range.HDR10, Video.Range.HDR10P):
return Tracks()
nonlocal fetched_ranges
video_ranges = ["SDR"]
if range_ == Video.Range.DV:
video_ranges = ["VISION"]
elif range_ in (Video.Range.HDR10, Video.Range.HDR10P):
video_ranges = ["HDR10"]
range_key = video_ranges[0]
if range_key in fetched_ranges:
return Tracks()
fetched_ranges.add(range_key)
self.log.debug(f"Fetching {range_.name} manifest...")
return self._fetch_manifest_tracks(title, video_ranges, play_mode)
tracks = self._get_tracks_for_variants(title, _fetch_variant)
if thumb := title.data.get("thumbnail"):
url = f"https://{thumb['standard']}"
description = Path(url).stem
tracks.add(Attachment.from_url(url=url, name="thumbnail", description=description, session=self.session))
for track in tracks:
if isinstance(track, (Audio, Subtitle)):
track.name = track.language.display_name()
track.name += " [Original]" if track.is_original_lang else ""
for audio in tracks.audio:
if audio.codec == Audio.Codec.EC3 and audio.bitrate > 448_000:
audio.joc = 16 # U-NEXT Bug...
return tracks
def _fetch_manifest_tracks(self, title: Title_T, video_ranges: List[str], play_mode: str) -> Tracks:
self.playback_data[title.id] = self._get_playlist(title.id, video_ranges, play_mode)
if (res_code := self.playback_data[title.id]["result_status"]) != 200:
self.log.debug(f" - Get stream error({res_code})")
return Tracks()
movie_profile = self.playback_data[title.id]["url_info"][0]["movie_profile"]
target_drm_key = "playready" if is_playready_cdm(self.cdm) else "widevine"
priority_protocols = ["dash", "smooth"]
selected_protocol = next(
(proto for proto in priority_protocols if proto in movie_profile and target_drm_key in movie_profile[proto].get("license_url_list", {})),
None,
)
if not selected_protocol:
self.log.debug(f" - Unable to handle with invalid DRM type. {target_drm_key} info not found or license_url is empty in response.")
return Tracks()
self.playback_data[title.id]["protocol"] = selected_protocol
base_url = movie_profile[selected_protocol].get("playlist_url")
u = urlparse(base_url)
query = parse_qs(u.query)
query["play_token"] = self.playback_data[title.id]["play_token"]
new_query = urlencode(query, doseq=True)
manifest_url = u._replace(query=new_query).geturl()
self.log.debug(f" + Manifest URL: {manifest_url}")
if ".mpd/" in manifest_url:
tracks = DASH.from_url(url=manifest_url, session=self.session).to_tracks(title.language)
elif ".ism/manifest" in manifest_url:
tracks = ISM.from_url(url=manifest_url, session=self.session).to_tracks(title.language)
else:
self.log.debug(" - Manifest type cannot be handled.")
return Tracks()
for video in tracks.videos:
if video.codec == Video.Codec.HEVC and "HDR10" in video_ranges:
video.range = Video.Range.HDR10 # U-NEXT Bug...
return tracks
def get_chapters(self, title: Titles_T) -> Chapters:
url_info = self.playback_data[title.id]["url_info"][0]
endroll_start_position = float(url_info["endroll_start_position"])
movie_parts_list = url_info["movie_parts_position_list"]
pre_chapter = []
has_intro = False
has_credits = False
if movie_parts_list:
for part in movie_parts_list:
part_type = part.get("movie_parts_type")
start = float(part.get("from", 0))
end = float(part.get("to", 0))
chapter_name = "Scene"
if part_type == "Opening":
if not has_intro:
chapter_name = "Intro"
has_intro = True
elif part_type == "Ending":
if not has_credits:
chapter_name = "Credits"
has_credits = True
pre_chapter.append((chapter_name, start))
if end > start:
pre_chapter.append(("Scene", end))
if endroll_start_position > 0:
is_duplicate = any(abs(t - endroll_start_position) < 1.0 for _, t in pre_chapter)
if not is_duplicate:
if has_credits:
pre_chapter.append(("Scene", endroll_start_position))
else:
pre_chapter.append(("Credits", endroll_start_position))
pre_chapter.sort(key=lambda x: x[1])
if not pre_chapter or pre_chapter[0][1] > 0:
pre_chapter.insert(0, ("Scene", 0.0))
if len(pre_chapter) == 1 and pre_chapter[0] == ("Scene", 0):
return []
chapters: List[Chapter] = []
for i, (chapter_title, time_sec) in enumerate(pre_chapter):
chapters.append(
Chapter(
timestamp=float(time_sec),
name=chapter_title if chapter_title != "Scene" else None,
)
)
return chapters
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
protocol = self.playback_data[title.id]["protocol"]
license_url = self.playback_data[title.id]["url_info"][0]["movie_profile"][protocol]["license_url_list"]["widevine"]
params = {"play_token": self.playback_data[title.id]["play_token"]}
headers = {"Content-Type": "application/octet-stream"}
res = self.session.post(license_url, params=params, headers=headers, data=challenge)
res.raise_for_status()
return res.content
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
protocol = self.playback_data[title.id]["protocol"]
license_url = self.playback_data[title.id]["url_info"][0]["movie_profile"][protocol]["license_url_list"]["playready"]
params = {"play_token": self.playback_data[title.id]["play_token"]}
headers = {
"Accept": "application/xml",
"Content-Type": "text/xml; charset=utf-8",
}
res = self.session.post(license_url, params=params, headers=headers, data=challenge)
res.raise_for_status()
return res.content
def _get_search(self, title: str) -> dict:
endpoint = self.config["endpoints"]["search"]
data_payload = {
"searchString": title,
"numParPage": 32,
"pageNo": 1,
"order": "recommend",
"film_rating_code": "R15",
"need_fulldata": 1,
}
payload = self._build_payload(
data_payload=data_payload,
user_token=self.account_tokens["userToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["data"]
def _get_stage_data(self, title_id: str) -> dict:
endpoint = self.config["endpoints"]["stage"]
data_payload = {"title_code": title_id}
payload = self._build_payload(
data_payload=data_payload,
user_token=self.account_tokens["userToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["data"]
def _get_movie_data(self, title_id: str) -> dict:
endpoint = self.config["endpoints"]["movie"]
data_payload = {"title_code": title_id}
payload = self._build_payload(
data_payload=data_payload,
user_token=self.account_tokens["userToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["data"]
def _get_title_relation(self, title_id: str) -> dict:
endpoint = self.config["endpoints"]["relation"]
data_payload = {"title_code": title_id}
payload = self._build_payload(
data_payload=data_payload,
user_token=self.account_tokens["userToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["data"]
def _get_episode_list(self, season_id: str) -> dict:
endpoint = self.config["endpoints"]["episode"]
data_payload = {
"title_code": season_id,
"page_size": 1000,
"page_number": 1,
}
payload = self._build_payload(
data_payload=data_payload,
user_token=self.account_tokens["userToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["data"]
def _get_playlist(self, title_id: str, video_ranges: List[str], play_mode: str) -> dict:
data_payload = {
"code": title_id,
"bitrate_low": 192,
"play_type": 2,
"play_mode": play_mode,
"keyonly_flg": 0,
"validation_flg": 0,
"codec": ["H264", "H265"],
"dynamic_range_list": video_ranges,
"audio_type_list": ["ac-3", "ec-3", "mp4a"],
}
endpoint = self.config["endpoints"]["playlist"]
payload = self._build_payload(
data_payload=data_payload,
user_token=self.account_tokens["userToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["data"]
def _login_with_password(self, username: str, password: str) -> dict:
endpoint = self.config["endpoints"]["auth"]
data_payload = {
"loginId": username,
"password": password,
}
payload = self._build_payload(data_payload=data_payload)
data = self._request("POST", endpoint, payload=payload)
return data["common"]["userInfo"]
def _get_account_info(self) -> dict:
endpoint = self.config["endpoints"]["account"]
payload = self._build_payload(
data_payload={},
user_token=self.account_tokens["userToken"],
security_token=self.account_tokens["securityToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["common"]["userInfo"]
def _refresh_token(self) -> dict:
endpoint = self.config["endpoints"]["auth"]
data_payload = {"securityToken": self.account_tokens["securityToken"]}
payload = self._build_payload(
data_payload=data_payload,
user_token=self.account_tokens["userToken"],
)
data = self._request("POST", endpoint, payload=payload)
return data["common"]["userInfo"]
def _build_payload(self, data_payload: dict, user_token: str = "", security_token: str = None) -> dict:
user_info = {
"userToken": user_token,
"service_name": "unext",
}
if security_token:
user_info["securityToken"] = security_token
device_info = {
"deviceType": "980",
"appVersion": "1",
"deviceUuid": self.device_id,
}
payload = {
"common": {
"userInfo": user_info,
"deviceInfo": device_info,
},
"data": data_payload,
}
return payload
def _request(self, method: str, endpoint: str, params: dict = None, headers: dict = None, payload: dict = None) -> Any[dict | str]:
_headers = self.session.headers.copy()
if headers:
_headers.update(headers)
req = Request(method, endpoint, headers=_headers, params=params, json=payload)
prepped = self.session.prepare_request(req)
try:
res = self.session.send(prepped)
res.raise_for_status()
data = res.json()
if error_code := data["common"]["result"]["errorCode"]:
raise ConnectionError(f"{error_code}: {data['common']['result']['errorMessage']}")
return data
except Exception as e:
self.log.error(f"API Request failed: {e}", exc_info=False)
sys.exit(1)
@@ -1,12 +0,0 @@
endpoints:
auth: "https://napi.unext.jp/1/auth/login"
account: "https://napi.unext.jp/2/user/account/get"
search: "https://napi.unext.jp/1/cmscommon/freeword/video"
stage: "https://napi.unext.jp/1/cmsuser/stage"
movie: "https://napi.unext.jp/1/cmsuser/episode/current"
relation: "https://napi.unext.jp/1/cmsuser/titledetail/relation"
episode: "https://napi.unext.jp/1/cmsuser/episode"
playlist: "https://napi.unext.jp/3/cmsuser/playlisturl/file"
device:
user_agent: "U-NEXT TV App Android16 5.61.0 AOSP TV"