added crunchyroll music video support

This commit is contained in:
stratumadev 2025-10-10 00:41:07 +02:00
parent 79acb28f3b
commit e30a9b9c9e
6 changed files with 171 additions and 40 deletions

View file

@ -1,5 +1,64 @@
import { ImageType, Images, Image } from './objectInfo';
export interface CrunchyMVObject {
id: string;
images: {
thumbnail: {
height: number;
source: string;
type: string;
width: number;
}[];
};
licensor: string;
originalRelease: string;
durationMs: number;
genres: {
displayValue: string;
id: string;
}[];
isPremiumOnly: boolean;
availability: {
endDate: string;
startDate: string;
};
matureBlocked: boolean;
displayArtistName: string;
sequenceNumber: number;
createdAt: string;
isPublic: boolean;
publishDate: string;
title: string;
artists: {
MainArtist: {
connector: string;
id: string;
name: string;
roles: string[];
sequenceNumber: number;
slug: string;
}[];
};
artist: {
id: string;
name: string;
slug: string;
};
isMature: boolean;
slug: string;
type: string;
streams_link: string;
playback?: string;
animeIds: string[];
displayArtistNameRequired: boolean;
readyToPublish: boolean;
updatedAt: string;
description: string;
hash: string;
copyright: string;
isSelected?: boolean;
}
export interface CrunchyAndroidObject {
__class__: string;
__href__: string;

View file

@ -1,8 +1,9 @@
// Generated by https://quicktype.io
import { CrunchyMVObject } from './crunchyAndroidObject';
export interface ObjectInfo {
total: number;
data: CrunchyObject[];
data: (CrunchyObject | CrunchyMVObject)[];
meta: Record<unknown>;
}

View file

@ -35,7 +35,7 @@ import { AuthData, AuthResponse, Episode, ResponseBase, SearchData, SearchRespon
import { ServiceClass } from './@types/serviceClassInterface';
import { CrunchyAndroidEpisodes } from './@types/crunchyAndroidEpisodes';
import { parse } from './modules/module.transform-mpd';
import { CrunchyAndroidObject } from './@types/crunchyAndroidObject';
import { AndroidObject, CrunchyAndroidObject, CrunchyMVObject } from './@types/crunchyAndroidObject';
import { CrunchyChapters, CrunchyChapter, CrunchyOldChapter } from './@types/crunchyChapters';
import vtt2ass from './modules/module.vtt2ass';
import { CrunchyPlayStream } from './@types/crunchyPlayStreams';
@ -760,6 +760,8 @@ export default class Crunchy implements ServiceClass {
iType = 'movie';
} else if (item.movie_release_year) {
iType = 'movie_listing';
} else if (item.type && item.type === 'musicVideo') {
iType = 'music_video';
} else {
if (item.identifier !== '') {
const iTypeCheck = item.identifier?.split('|');
@ -786,7 +788,8 @@ export default class Crunchy implements ServiceClass {
season: 'S', // VOL
episode: 'E', // EPI
movie_listing: 'F', // FLM
movie: 'M' // MED
movie: 'M', // MED
musicVideo: 'MV' // MVD
};
// check title
item.title = item.title != '' ? item.title : 'NO_TITLE';
@ -1306,46 +1309,83 @@ export default class Crunchy implements ServiceClass {
// reqs
let objectInfo: ObjectInfo = { total: 0, data: [], meta: {} };
const objectReqOpts = [
api.cms_bucket,
this.cmsToken.cms_web.bucket,
'/objects/',
doEpsFilter.values.join(','),
'?',
new URLSearchParams({
force_locale: '',
preferred_audio_language: 'ja-JP',
locale: this.locale,
Policy: this.cmsToken.cms_web.policy,
Signature: this.cmsToken.cms_web.signature,
'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id
})
].join('');
const objectReq = await this.req.getData(objectReqOpts, AuthHeaders);
if (!objectReq.ok || !objectReq.res) {
console.error('Objects Request FAILED!');
if (objectReq.error && objectReq.error.res && objectReq.error.res.body) {
const objectInfo = await objectReq.error.res.json();
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
objectInfo.error = true;
return objectInfo;
// Music Videos handling
if (doEpsFilter.values.filter((e) => e.startsWith('MV')).length > 0) {
const toFetch = doEpsFilter.values.filter((e) => e.startsWith('MV'));
const AuthHeaders = {
headers: {
Authorization: `Bearer ${this.token.access_token}`,
...api.crunchyDefHeader
},
useProxy: true
};
const mvInfoReq = await this.req.getData(
`${api.content_music}/music_videos/${toFetch.join(',')}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`,
AuthHeaders
);
if (!mvInfoReq.ok || !mvInfoReq.res) {
console.error('Music Video Request FAILED!');
return [];
}
return [];
const mvInfo = (await mvInfoReq.res.json()) as { data: CrunchyMVObject[]; total: number };
if (mvInfo.data.length === 0) return [];
objectInfo = {
total: objectInfo.total + mvInfo.total,
data: [...objectInfo.data, ...mvInfo.data],
meta: {}
};
doEpsFilter.values = doEpsFilter.values.filter((e) => !e.startsWith('MV'));
}
// Media ID handling
if (doEpsFilter.values.length > 0) {
const objectReqOpts = [
api.cms_bucket,
this.cmsToken.cms_web.bucket,
'/objects/',
doEpsFilter.values.join(','),
'?',
new URLSearchParams({
force_locale: '',
preferred_audio_language: 'ja-JP',
locale: this.locale,
Policy: this.cmsToken.cms_web.policy,
Signature: this.cmsToken.cms_web.signature,
'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id
})
].join('');
const objectReq = await this.req.getData(objectReqOpts, AuthHeaders);
if (!objectReq.ok || !objectReq.res) {
console.error('Objects Request FAILED!');
if (objectReq.error && objectReq.error.res && objectReq.error.res.body) {
const objectInfo = await objectReq.error.res.json();
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
objectInfo.error = true;
return objectInfo;
}
return [];
}
const objectInfoAndroid = (await objectReq.res.json()) as CrunchyAndroidObject;
objectInfo = {
total: objectInfo.total + objectInfoAndroid.total,
data: [...objectInfo.data, ...objectInfoAndroid.items],
meta: {}
};
}
const objectInfoAndroid = (await objectReq.res.json()) as CrunchyAndroidObject;
objectInfo = {
total: objectInfoAndroid.total,
data: objectInfoAndroid.items,
meta: {}
};
if (earlyReturn) {
return objectInfo;
}
const selectedMedia: Partial<CrunchyEpMeta>[] = [];
for (const item of objectInfo.data) {
// Non MV handling
for (const item of objectInfo.data.filter((i) => i.type !== 'musicVideo') as AndroidObject[]) {
if (item.type != 'episode' && item.type != 'movie') {
await this.logObject(item, 2, true, false);
continue;
@ -1413,6 +1453,35 @@ export default class Crunchy implements ServiceClass {
}
await this.logObject(item, 2);
}
// MV handling
for (const item of objectInfo.data.filter((i) => i.type === 'musicVideo') as CrunchyMVObject[]) {
const epMeta: Partial<CrunchyEpMeta> = {};
epMeta.data = [
{
mediaId: 'V:' + item.id,
isSubbed: false,
isDubbed: false
}
];
epMeta.season = 0;
epMeta.seriesTitle = item.title;
epMeta.seasonTitle = item.title;
epMeta.episodeNumber = 'Music Video';
epMeta.episodeTitle = item.title;
if (item.streams_link) {
epMeta.data[0].playback = item.streams_link;
if (!item.playback) {
item.playback = item.streams_link;
}
selectedMedia.push(epMeta);
item.isSelected = true;
}
await this.logObject(item, 2);
}
console.info('');
return selectedMedia;
}
@ -2263,8 +2332,8 @@ export default class Crunchy implements ServiceClass {
) {
console.info('Decryption Needed, attempting to decrypt');
if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) {
let commandBaseVideo = `--show-progress --key ${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].key} `;
let commandBaseAudio = `--show-progress --key ${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].key} `;
let commandBaseVideo = `--show-progress --key ${encryptionKeysVideo?.[0].kid}:${encryptionKeysVideo?.[0].key} `;
let commandBaseAudio = `--show-progress --key ${encryptionKeysAudio?.[0].kid}:${encryptionKeysAudio?.[0].key} `;
let commandVideo = commandBaseVideo + `"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`;
let commandAudio = commandBaseAudio + `"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`;

View file

@ -904,7 +904,7 @@ export default class Hidive implements ServiceClass {
return undefined;
}
if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) {
let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `;
let commandBase = `--show-progress --key ${encryptionKeys[0].kid}:${encryptionKeys[0].key} `;
let commandVideo = commandBase + `"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`;
if (this.cfg.bin.shaka) {
@ -999,7 +999,7 @@ export default class Hidive implements ServiceClass {
return undefined;
}
if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) {
let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `;
let commandBase = `--show-progress --key ${encryptionKeys[0].kid}:${encryptionKeys[0].key} `;
let commandAudio = commandBase + `"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`;
if (this.cfg.bin.shaka) {

View file

@ -17,6 +17,7 @@ export type APIType = {
profile: string;
search: string;
content_cms: string;
content_music: string;
browse: string;
browse_all_series: string;
streaming_sessions: string;
@ -55,6 +56,7 @@ const api: APIType = {
profile: `${domain.cr_www}/accounts/v1/me/profile`,
search: `${domain.cr_www}/content/v2/discover/search`,
content_cms: `${domain.cr_www}/content/v2/cms`,
content_music: `${domain.cr_www}/content/v2/music`,
browse: `${domain.cr_www}/content/v1/browse`,
browse_all_series: `${domain.cr_www}/content/v2/discover/browse`,
streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`,

View file

@ -12,7 +12,7 @@ const parseSelect = (
values: [],
isSelected: () => but
};
const parts = selectString.split(',');
const parts = selectString.includes(',') ? selectString.split(',') : selectString.split(' ');
const select: string[] = [];
parts.forEach((part) => {