crunchy hotfix

This commit is contained in:
stratumadev 2025-05-06 21:35:47 +02:00
parent a0a0bf827a
commit 41a83734b0
4 changed files with 4763 additions and 3830 deletions

View file

@ -382,7 +382,6 @@ export default class Crunchy implements ServiceClass {
const authData = new URLSearchParams({ const authData = new URLSearchParams({
'refresh_token': this.token.refresh_token, 'refresh_token': this.token.refresh_token,
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
//'grant_type': 'etp_rt_cookie',
'scope': 'offline_access', 'scope': 'offline_access',
'device_id': uuid, 'device_id': uuid,
'device_name': 'iPhone', 'device_name': 'iPhone',
@ -1587,10 +1586,10 @@ export default class Crunchy implements ServiceClass {
// Delete the stream if it's not needed // Delete the stream if it's not needed
if (options.novids && options.noaudio) { if (options.novids && options.noaudio) {
if (playStream) { // if (playStream) {
await this.refreshToken(true, true); // await this.refreshToken(true, true);
await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); // await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
} // }
} }
if(!dlFailed && curStream !== undefined && !(options.novids && options.noaudio)){ if(!dlFailed && curStream !== undefined && !(options.novids && options.noaudio)){
@ -1602,10 +1601,10 @@ export default class Crunchy implements ServiceClass {
const streamPlaylistBody = await streamPlaylistsReq.res.text(); const streamPlaylistBody = await streamPlaylistsReq.res.text();
if (streamPlaylistBody.match('MPD')) { if (streamPlaylistBody.match('MPD')) {
//We have the stream, so go ahead and delete the active stream //We have the stream, so go ahead and delete the active stream
if (playStream) { // if (playStream) {
await this.refreshToken(true, true); // await this.refreshToken(true, true);
await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); // await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
} // }
//Parse MPD Playlists //Parse MPD Playlists
const streamPlaylists = await parse(streamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), curStream.url.match(/.*\.urlset\//)[0]); const streamPlaylists = await parse(streamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), curStream.url.match(/.*\.urlset\//)[0]);
@ -1776,44 +1775,49 @@ export default class Crunchy implements ServiceClass {
//Handle Decryption if needed //Handle Decryption if needed
if ((chosenVideoSegments.pssh_wvd ||chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) && (videoDownloaded || audioDownloaded)) { if ((chosenVideoSegments.pssh_wvd ||chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) && (videoDownloaded || audioDownloaded)) {
const assetIdRegex = chosenVideoSegments.segments[0].uri.match(/\/assets\/(?:p\/)?([^_,]+)/); // const assetIdRegex = chosenVideoSegments.segments[0].uri.match(/\/assets\/(?:p\/)?([^_,]+)/);
const assetId = assetIdRegex ? assetIdRegex[1] : null; // const assetId = assetIdRegex ? assetIdRegex[1] : null;
const sessionId = new Date().getUTCMilliseconds().toString().padStart(3, '0') + process.hrtime.bigint().toString().slice(0, 13); // const sessionId = new Date().getUTCMilliseconds().toString().padStart(3, '0') + process.hrtime.bigint().toString().slice(0, 13);
console.info('Decryption Needed, attempting to decrypt'); console.info('Decryption Needed, attempting to decrypt');
const decReq = await this.req.getData(`${api.drm}`, { // const decReq = await this.req.getData(`${api.drm}`, {
'method': 'POST', // 'method': 'POST',
'body': JSON.stringify({ // 'body': JSON.stringify({
'accounting_id': 'crunchyroll', // 'accounting_id': 'crunchyroll',
'asset_id': assetId, // 'asset_id': assetId,
'session_id': sessionId, // 'session_id': sessionId,
'user_id': this.token.account_id // 'user_id': this.token.account_id
}), // }),
headers: { // headers: {
'User-Agent': api.defaultUserAgent // 'User-Agent': api.defaultUserAgent
} // }
}); // });
if(!decReq.ok || !decReq.res){ // if(!decReq.ok || !decReq.res){
console.error('Request to DRM Authentication failed:', decReq.error?.res.status, decReq.error?.message); // console.error('Request to DRM Authentication failed:', decReq.error?.res.status, decReq.error?.message);
return undefined; // return undefined;
} // }
const authData = await decReq.res.json() as {'custom_data': string, 'token': string}; // const authData = await decReq.res.json() as {'custom_data': string, 'token': string};
let encryptionKeys; let encryptionKeys;
if (cdm === 'widevine') { if (cdm === 'widevine') {
encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, 'https://lic.drmtoday.com/license-proxy-widevine/cenc/', { encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, api.drm_widevine, {
'dt-custom-data': authData.custom_data, Authorization: `Bearer ${this.token.access_token}`,
'x-dt-auth-token': authData.token 'User-Agent': api.defaultUserAgent,
Pragma: 'no-cache',
'Cache-Control': 'no-cache',
'content-type': 'application/octet-stream',
'x-cr-content-id': currentVersion!.guid,
'x-cr-video-token': playStream!.token
}); });
} }
if (cdm === 'playready') { // if (cdm === 'playready') {
encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, 'https://lic.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx', { // encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, 'https://lic.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx', {
'dt-custom-data': authData.custom_data, // 'dt-custom-data': authData.custom_data,
'x-dt-auth-token': authData.token // 'x-dt-auth-token': authData.token
}); // });
} // }
if (!encryptionKeys || encryptionKeys.length == 0) { if (!encryptionKeys || encryptionKeys.length == 0) {
console.error('Failed to get encryption keys'); console.error('Failed to get encryption keys');

View file

@ -99,12 +99,17 @@ export async function getKeysWVD(
//Generate license //Generate license
let response; let response;
try { console.log(licenseServer)
response = await got(licenseServer, { console.log({
method: 'POST', method: 'POST',
body: session.createLicenseRequest(), body: session.createLicenseRequest(),
headers: authData, headers: authData
responseType: 'text', } as any)
try {
response = await fetch(licenseServer, {
method: 'POST',
body: session.createLicenseRequest(),
headers: authData
}); });
} catch (_error) { } catch (_error) {
const error = _error as { const error = _error as {
@ -142,19 +147,21 @@ export async function getKeysWVD(
return []; return [];
} }
if (response.statusCode === 200) { if (response.status === 200) {
//Parse License and return keys //Parse License and return keys
const buffer = await response.arrayBuffer();
const text = new TextDecoder().decode(buffer);
try { try {
const json = JSON.parse(response.body); const json = JSON.parse(text);
return session.parseLicense(Buffer.from(json['license'], 'base64')); return session.parseLicense(Buffer.from(json['license'], 'base64'));
} catch { } catch {
return session.parseLicense(response.rawBody); return session.parseLicense(Buffer.from(new Uint8Array(buffer)));
} }
} else { } else {
console.info( console.info(
'License request failed:', 'License request failed:',
response.statusMessage, response.status,
response.body await response.text()
); );
return []; return [];
} }

View file

@ -59,13 +59,13 @@ const api: APIType = {
rss_gid: `${domain.www}/syndication/feed?type=episodes&group_id=`, // &lang=enUS rss_gid: `${domain.www}/syndication/feed?type=episodes&group_id=`, // &lang=enUS
media_page: `${domain.www}/media-`, media_page: `${domain.www}/media-`,
series_page: `${domain.www}/series-`, series_page: `${domain.www}/series-`,
auth: `${domain.api_beta}/auth/v1/token`, auth: `${domain.www}/auth/v1/token`,
// mobile api // mobile api
search3: `${domain.api}/autocomplete.0.json`, search3: `${domain.api}/autocomplete.0.json`,
session: `${domain.api}/start_session.0.json`, session: `${domain.api}/start_session.0.json`,
collections: `${domain.api}/list_collections.0.json`, collections: `${domain.api}/list_collections.0.json`,
// This User-Agent bypasses Cloudflare security of the newer Endpoint // This User-Agent bypasses Cloudflare security of the newer Endpoint
defaultUserAgent: 'Crunchyroll/4.75.0 (bundle_identifier:com.crunchyroll.iphone; build_number:4100608.433889621) iOS/18.3.2 Gravity/4.75.0', defaultUserAgent: 'Crunchyroll/4.77.2 (bundle_identifier:com.crunchyroll.iphone; build_number:4139672.438176041) iOS/18.3.2 Gravity/4.77.2',
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`, beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
beta_cmsToken: `${domain.api_beta}/index/v2`, beta_cmsToken: `${domain.api_beta}/index/v2`,
search: `${domain.api_beta}/content/v2/discover/search`, search: `${domain.api_beta}/content/v2/discover/search`,
@ -73,6 +73,7 @@ const api: APIType = {
beta_browse: `${domain.api_beta}/content/v1/browse`, beta_browse: `${domain.api_beta}/content/v1/browse`,
beta_cms: `${domain.api_beta}/cms/v2`, beta_cms: `${domain.api_beta}/cms/v2`,
// beta api // beta api
// broken - deprecated since 06.05.2025
drm: `${domain.api_beta}/drm/v1/auth`, drm: `${domain.api_beta}/drm/v1/auth`,
// new drm endpoints // new drm endpoints
drm_widevine: `${domain.www}/license/v1/license/widevine`, drm_widevine: `${domain.www}/license/v1/license/widevine`,

File diff suppressed because it is too large Load diff