reverted crunchyroll to legacy api

This commit is contained in:
stratumadev 2025-11-26 17:08:47 +01:00
parent ea53e9071f
commit 160c8a1cb4
2 changed files with 75 additions and 35 deletions

View file

@ -390,15 +390,20 @@ export default class Crunchy implements ServiceClass {
// } // }
public async doAuth(data: AuthData): Promise<AuthResponse> { public async doAuth(data: AuthData): Promise<AuthResponse> {
const basic = atob(api.basic_auth_token);
const client = basic.split(':');
const uuid = randomUUID(); const uuid = randomUUID();
const authData = new URLSearchParams({ const authData = new URLSearchParams({
username: data.username, username: data.username,
password: data.password, password: data.password,
grant_type: 'password', grant_type: 'password',
scope: 'offline_access', scope: 'offline_access',
client_id: client[0],
client_secret: client[1],
device_id: uuid, device_id: uuid,
device_name: 'iPhone', device_name: 'emu64xa',
device_type: 'iPhone 13' device_type: 'ANDROIDTV'
}).toString(); }).toString();
const authReqOpts: FetchParams = { const authReqOpts: FetchParams = {
method: 'POST', method: 'POST',
@ -411,6 +416,7 @@ export default class Crunchy implements ServiceClass {
console.error('Authentication failed!'); console.error('Authentication failed!');
return { isOk: false, reason: new Error('Authentication failed') }; return { isOk: false, reason: new Error('Authentication failed') };
} }
// To prevent any Cloudflare errors in the future // To prevent any Cloudflare errors in the future
if (authReq.res.headers.get('Set-Cookie')) { if (authReq.res.headers.get('Set-Cookie')) {
api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string;
@ -422,6 +428,7 @@ export default class Crunchy implements ServiceClass {
api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent'];
api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent'];
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid; this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in * 1000); this.token.expires = new Date(Date.now() + this.token.expires_in * 1000);
@ -432,13 +439,18 @@ export default class Crunchy implements ServiceClass {
} }
public async doAnonymousAuth() { public async doAnonymousAuth() {
const basic = atob(api.basic_auth_token);
const client = basic.split(':');
const uuid = randomUUID(); const uuid = randomUUID();
const authData = new URLSearchParams({ const authData = new URLSearchParams({
grant_type: 'client_id', grant_type: 'client_id',
scope: 'offline_access', scope: 'offline_access',
client_id: client[0],
client_secret: client[1],
device_id: uuid, device_id: uuid,
device_name: 'iPhone', device_name: 'emu64xa',
device_type: 'iPhone 13' device_type: 'ANDROIDTV'
}).toString(); }).toString();
const authReqOpts: FetchParams = { const authReqOpts: FetchParams = {
method: 'POST', method: 'POST',
@ -462,6 +474,7 @@ export default class Crunchy implements ServiceClass {
api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent'];
api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent'];
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid; this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in * 1000); this.token.expires = new Date(Date.now() + this.token.expires_in * 1000);
@ -473,6 +486,7 @@ export default class Crunchy implements ServiceClass {
console.error('No access token!'); console.error('No access token!');
return false; return false;
} }
const profileReqOptions = { const profileReqOptions = {
headers: { headers: {
...api.crunchyDefHeader, ...api.crunchyDefHeader,
@ -498,15 +512,20 @@ export default class Crunchy implements ServiceClass {
} }
public async loginWithToken(refreshToken: string) { public async loginWithToken(refreshToken: string) {
const basic = atob(api.basic_auth_token);
const client = basic.split(':');
const uuid = randomUUID(); const uuid = randomUUID();
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', //'grant_type': 'etp_rt_cookie',
scope: 'offline_access', scope: 'offline_access',
client_id: client[0],
client_secret: client[1],
device_id: uuid, device_id: uuid,
device_name: 'iPhone', device_name: 'emu64xa',
device_type: 'iPhone 13' device_type: 'ANDROIDTV'
}).toString(); }).toString();
const authReqOpts: FetchParams = { const authReqOpts: FetchParams = {
method: 'POST', method: 'POST',
@ -533,6 +552,7 @@ export default class Crunchy implements ServiceClass {
api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent'];
api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent'];
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid; this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in * 1000); this.token.expires = new Date(Date.now() + this.token.expires_in * 1000);
@ -552,17 +572,24 @@ export default class Crunchy implements ServiceClass {
} else { } else {
//console.info('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.'); //console.info('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.');
} }
const basic = atob(api.basic_auth_token);
const client = basic.split(':');
const uuid = this.token.device_id || randomUUID(); const uuid = this.token.device_id || randomUUID();
const authData = new URLSearchParams({ const authData = new URLSearchParams({
refresh_token: this.token.refresh_token,
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: this.token.refresh_token,
scope: 'offline_access',
client_id: client[0],
client_secret: client[1],
device_id: uuid, device_id: uuid,
device_name: 'iPhone', device_name: 'emu64xa',
device_type: 'iPhone 13' device_type: 'ANDROIDTV'
}).toString(); }).toString();
const authReqOpts: FetchParams = { const authReqOpts: FetchParams = {
method: 'POST', method: 'POST',
headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, headers: { ...api.crunchyAuthHeader },
body: authData, body: authData,
useProxy: true useProxy: true
}; };
@ -585,6 +612,7 @@ export default class Crunchy implements ServiceClass {
api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent'];
api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent'];
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid; this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in * 1000); this.token.expires = new Date(Date.now() + this.token.expires_in * 1000);
@ -1719,7 +1747,7 @@ export default class Crunchy implements ServiceClass {
const me = await this.req.getData(api.me, AuthHeaders); const me = await this.req.getData(api.me, AuthHeaders);
if (me.ok && me.res) { if (me.ok && me.res) {
const data_me = await me.res.json(); const data_me = await me.res.json();
const benefits = await this.req.getData(`https://www.crunchyroll.com/subs/v1/subscriptions/${data_me.external_id}/benefits`, AuthHeaders); const benefits = await this.req.getData(`https://beta-api.crunchyroll.com/subs/v1/subscriptions/${data_me.external_id}/benefits`, AuthHeaders);
if (benefits.ok && benefits.res) { if (benefits.ok && benefits.res) {
const data_benefits = (await benefits.res.json()) as { items: { benefit: string }[] }; const data_benefits = (await benefits.res.json()) as { items: { benefit: string }[] };
if (data_benefits?.items && !data_benefits.items.find((i) => i.benefit === 'offline_viewing')) { if (data_benefits?.items && !data_benefits.items.find((i) => i.benefit === 'offline_viewing')) {
@ -1763,14 +1791,14 @@ export default class Crunchy implements ServiceClass {
if (activeStreamsReq.ok && activeStreamsReq.res) { if (activeStreamsReq.ok && activeStreamsReq.res) {
const data = await activeStreamsReq.res.json(); const data = await activeStreamsReq.res.json();
for (const s of data.items) { for (const s of data.items) {
await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${s.contentId}/${s.token}`, { ...{ method: 'DELETE' }, ...AuthHeaders }); await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${s.contentId}/${s.token}`, { ...{ method: 'DELETE' }, ...AuthHeaders });
} }
console.warn(`Killed ${data.items?.length ?? 0} Sessions`); console.warn(`Killed ${data.items?.length ?? 0} Sessions`);
} }
} }
const videoPlaybackReq = await this.req.getData( const videoPlaybackReq = await this.req.getData(
`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams['androidtv']}/play?queue=0`, `https://cr-play-service.prd.crunchyrollsvc.com/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams['androidtv']}/play?queue=0`,
AuthHeaders AuthHeaders
); );
if (!videoPlaybackReq.ok || !videoPlaybackReq.res) { if (!videoPlaybackReq.ok || !videoPlaybackReq.res) {
@ -1787,7 +1815,7 @@ export default class Crunchy implements ServiceClass {
} }
if (isDLVideoBypass) { if (isDLVideoBypass) {
const videoDLReq = await this.req.getData( const videoDLReq = await this.req.getData(
`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams[options.vstream]}/download`, `https://cr-play-service.prd.crunchyrollsvc.com/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams[options.vstream]}/download`,
AuthHeaders AuthHeaders
); );
if (videoDLReq.ok && videoDLReq.res) { if (videoDLReq.ok && videoDLReq.res) {
@ -1824,7 +1852,7 @@ export default class Crunchy implements ServiceClass {
if (!options.cstream && options.vstream !== options.astream && videoStream) { if (!options.cstream && options.vstream !== options.astream && videoStream) {
const audioPlaybackReq = await this.req.getData( const audioPlaybackReq = await this.req.getData(
`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyAudioPlayStreams[options.astream]}/${isDLAudioBypass ? 'download' : 'play?queue=1'}`, `https://cr-play-service.prd.crunchyrollsvc.com/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyAudioPlayStreams[options.astream]}/${isDLAudioBypass ? 'download' : 'play?queue=1'}`,
AuthHeaders AuthHeaders
); );
if (!audioPlaybackReq.ok || !audioPlaybackReq.res) { if (!audioPlaybackReq.ok || !audioPlaybackReq.res) {

View file

@ -1,7 +1,9 @@
// api domains // api domains
const domain = { const domain = {
cr_www: 'https://www.crunchyroll.com', cr_www: 'https://www.crunchyroll.com',
cr_api: 'https://api.crunchyroll.com', cr_api: 'https://beta-api.crunchyroll.com',
cr_playback: 'https://cr-play-service.prd.crunchyrollsvc.com',
cr_license: 'https://cr-license-proxy.prd.crunchyrollsvc.com',
hd_www: 'https://www.hidive.com', hd_www: 'https://www.hidive.com',
hd_api: 'https://api.hidive.com', hd_api: 'https://api.hidive.com',
hd_new: 'https://dce-frontoffice.imggaming.com' hd_new: 'https://dce-frontoffice.imggaming.com'
@ -28,8 +30,9 @@ export type APIType = {
cms_auth: string; cms_auth: string;
// Crunchyroll Headers // Crunchyroll Headers
crunchyDefUserAgent: string; crunchyDefUserAgent: string;
crunchyDefHeader: Record<string, string>; crunchyDefHeader: Record<string, any>;
crunchyAuthHeader: Record<string, string>; crunchyAuthHeader: Record<string, string>;
crunchyAuthRefreshHeader: Record<string, string>;
// Hidive // Hidive
hd_apikey: string; hd_apikey: string;
hd_devName: string; hd_devName: string;
@ -50,27 +53,28 @@ const api: APIType = {
bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js', bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js',
// //
// Crunchyroll API // Crunchyroll API
basic_auth_token: 'bGtlc2k3c25zeTlvb2ptaTJyOWg6LWFHRFhGRk5UbHVaTUxZWEVSbmdOWW5FanZnSDVvZHY=', basic_auth_token: 'bmR0aTZicXlqcm9wNXZnZjF0dnU6elpIcS00SEJJVDlDb2FMcnBPREJjRVRCTUNHai1QNlg=',
auth: `${domain.cr_www}/auth/v1/token`, auth: `${domain.cr_api}/auth/v1/token`,
me: `${domain.cr_www}/accounts/v1/me`, me: `${domain.cr_api}/accounts/v1/me`,
profile: `${domain.cr_www}/accounts/v1/me/profile`, profile: `${domain.cr_api}/accounts/v1/me/profile`,
search: `${domain.cr_www}/content/v2/discover/search`, search: `${domain.cr_api}/content/v2/discover/search`,
content_cms: `${domain.cr_www}/content/v2/cms`, content_cms: `${domain.cr_api}/content/v2/cms`,
content_music: `${domain.cr_www}/content/v2/music`, content_music: `${domain.cr_api}/content/v2/music`,
browse: `${domain.cr_www}/content/v1/browse`, browse: `${domain.cr_api}/content/v1/browse`,
browse_all_series: `${domain.cr_www}/content/v2/discover/browse`, browse_all_series: `${domain.cr_api}/content/v2/discover/browse`,
streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`, streaming_sessions: `${domain.cr_playback}/v1/sessions/streaming`,
drm_widevine: `${domain.cr_www}/license/v1/license/widevine`, drm_widevine: `https://cr-license-proxy.prd.crunchyrollsvc.com/v1/license/widevine`,
drm_playready: `${domain.cr_www}/license/v1/license/playReady`, drm_playready: `https://cr-license-proxy.prd.crunchyrollsvc.com/v1/license/playReady`,
// //
// Crunchyroll Bucket // Crunchyroll Bucket
cms_bucket: `${domain.cr_www}/cms/v2`, cms_bucket: `${domain.cr_api}/cms/v2`,
cms_auth: `${domain.cr_www}/index/v2`, cms_auth: `${domain.cr_api}/index/v2`,
// //
// Crunchyroll Headers // Crunchyroll Headers
crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.49.1_22281 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)', crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.50.0_22282 (Android 16; en-US; sdk_gphone64_x86_64)',
crunchyDefHeader: {}, crunchyDefHeader: {},
crunchyAuthHeader: {}, crunchyAuthHeader: {},
crunchyAuthRefreshHeader: {},
// //
// //
// Hidive // Hidive
@ -89,7 +93,6 @@ const api: APIType = {
api.crunchyDefHeader = { api.crunchyDefHeader = {
'User-Agent': api.crunchyDefUserAgent, 'User-Agent': api.crunchyDefUserAgent,
Accept: '*/*',
'Accept-Encoding': 'gzip', 'Accept-Encoding': 'gzip',
Connection: 'Keep-Alive', Connection: 'Keep-Alive',
Host: 'www.crunchyroll.com' Host: 'www.crunchyroll.com'
@ -97,10 +100,19 @@ api.crunchyDefHeader = {
// set header // set header
api.crunchyAuthHeader = { api.crunchyAuthHeader = {
Authorization: `Basic ${api.basic_auth_token}`, Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Accept-Charset': 'UTF-8',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Request-Type': 'SignIn', 'Request-Type': 'SignIn',
...api.crunchyDefHeader ...api.crunchyDefHeader
}; };
// set header
api.crunchyAuthRefreshHeader = {
Accept: 'application/json',
'Accept-Charset': 'UTF-8',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
...api.crunchyDefHeader
};
export { domain, api }; export { domain, api };