diff --git a/.eslintrc.json b/.eslintrc.json index 41a8786..7b246c6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,7 +28,7 @@ 2 ], "linebreak-style": [ - "error", + "warn", "windows" ], "quotes": [ diff --git a/@types/funiSubtitleRequest.d.ts b/@types/funiSubtitleRequest.d.ts new file mode 100644 index 0000000..e4b6490 --- /dev/null +++ b/@types/funiSubtitleRequest.d.ts @@ -0,0 +1,75 @@ +// Generated by https://quicktype.io + +export interface SubtitleRequest { + primary: Primary; + fallback: Primary[]; +} + +export interface Primary { + venueVideoId: string; + alphaPackageId: string; + versionContentId: VersionContentID; + manifestPath: string; + fileExt: PrimaryFileEXT; + subtitles: Subtitle[]; + accessType: AccessType; + sessionId: string; + audioLanguage: AudioLanguage; + version: Version; + aips: Aip[]; + drmToken: string; + drmType: string; +} + +export enum AccessType { + Subscription = 'subscription', +} + +export interface Aip { + in: number; + out: number; +} + +export enum AudioLanguage { + En = 'en', + Ja = 'ja', +} + +export enum PrimaryFileEXT { + M3U8 = 'm3u8', + Mp4 = 'mp4', +} + +export interface Subtitle { + filePath: string; + fileExt: SubtitleFileEXT; + contentType: ContentType; + languageCode: LanguageCode; +} + +export enum ContentType { + Cc = 'cc', + Full = 'full', +} + +export enum SubtitleFileEXT { + Dfxp = 'dfxp', + Srt = 'srt', + Vtt = 'vtt', +} + +export enum LanguageCode { + En = 'en', + Es = 'es', + Pt = 'pt', +} + +export enum Version { + Simulcast = 'simulcast', + Uncut = 'uncut', +} + +export enum VersionContentID { + Akusim0012 = 'AKUSIM0012', + Akuunc0012 = 'AKUUNC0012', +} diff --git a/@types/messageHandler.d.ts b/@types/messageHandler.d.ts index 505d582..c0a4061 100644 --- a/@types/messageHandler.d.ts +++ b/@types/messageHandler.d.ts @@ -70,6 +70,11 @@ export type FuniEpisodeData = { episodeID: string, seasonTitle: string, seasonNumber: string, + ids: { + episode: string, + show: string, + season: string + } }; export type AuthData = { username: string, password: string }; diff --git a/TODO.md b/TODO.md index fed742e..2533343 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,6 @@ - [x] Window title - [x] Only open dev tools in test version - [x] Add help information (version, contributor, documentation...) -- [ ] App Icon with electron-forge make - [x] ContextMenu - [x] Better episode listing with selectio via left mouse button - [x] Use Child for Context Menu diff --git a/funi.ts b/funi.ts index b8073a5..5f0d404 100644 --- a/funi.ts +++ b/funi.ts @@ -39,6 +39,7 @@ import { TitleElement } from './@types/episode'; import { AvailableFilenameVars } from './modules/module.args'; import { AuthData, AuthResponse, CheckTokenResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, SearchData, FuniSearchReponse, FuniShowResponse, FuniStreamData, FuniSubsData, FuniEpisodeData, ResponseBase } from './@types/messageHandler'; import { ServiceClass } from './@types/serviceClassInterface'; +import { SubtitleRequest } from './@types/funiSubtitleRequest'; // check page // fn variables @@ -274,7 +275,18 @@ export default class Funi implements ServiceClass { // select is_selected = false; if (data.all || epSelList.isSelected(epStrId)) { - fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug, episodeID:epStrId, seasonTitle: eps[e].item.seasonTitle, seasonNumber: eps[e].item.seasonNum}); + fnSlug.push({ + title:eps[e].item.titleSlug, + episode:eps[e].item.episodeSlug, + episodeID:epStrId, + seasonTitle: eps[e].item.seasonTitle, + seasonNumber: eps[e].item.seasonNum, + ids: { + episode: eps[e].ids.externalEpisodeId, + season: eps[e].ids.externalSeasonId, + show: eps[e].ids.externalShowId + } + }); epSelEpsTxt.push(epStrId); is_selected = true; } @@ -347,7 +359,7 @@ export default class Funi implements ServiceClass { console.log('[INFO] Available streams (Non-Encrypted):'); } // map medias - const media = ep.media.map((m) =>{ + const media = await Promise.all(ep.media.map(async (m) =>{ if(m.mediaType == 'experience'){ if(m.version.match(/uncut/i) && m.language){ uncut[m.language] = true; @@ -357,13 +369,13 @@ export default class Funi implements ServiceClass { language: m.language, version: m.version, type: m.experienceType, - subtitles: this.getSubsUrl(m.mediaChildren, m.language, data.subs) + subtitles: await this.getSubsUrl(m.mediaChildren, m.language, data.subs, ep.ids.externalEpisodeId) }; } else{ return { id: 0, type: '' }; } - }); + })); // select stDlPath = []; @@ -389,10 +401,16 @@ export default class Funi implements ServiceClass { selected = true; } } - if (log) + if (log) { + const subsToDisplay: langsData.LanguageItem[] = []; + localSubs.forEach(a => { + if (!subsToDisplay.includes(a.lang)) + subsToDisplay.push(a.lang); + }); console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${ - localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.lang.name}'`).join(', ')} for subtitles)` : '' + localSubs && localSubs.length > 0 && selected ? ` (using ${subsToDisplay.map(a => `'${a.name}'`).join(', ')} for subtitles)` : '' }`); + } } } @@ -832,31 +850,60 @@ export default class Funi implements ServiceClass { return downloadStatus.ok; } - public getSubsUrl(m: MediaChild[], parentLanguage: TitleElement|undefined, data: FuniSubsData) : Subtitle[] { + + public async getSubsUrl(m: MediaChild[], parentLanguage: TitleElement|undefined, data: FuniSubsData, episodeID: string) : Promise { if((data.nosubs && !data.sub) || data.dlsubs.includes('none')){ return []; } - const found: Subtitle[] = []; - - const media = m.filter(a => a.filePath.split('.').pop() === 'vtt'); - for (const me of media) { - const lang = langsData.languages.find(a => me.language === (a.funi_name || a.name)); - if (!lang) { - continue; - } - const pLang = langsData.languages.find(a => (a.funi_name || a.name) === parentLanguage); - if (data.dlsubs.includes('all') || data.dlsubs.some(a => a === lang.locale)) { - found.push({ - url: me.filePath, - ext: `.${lang.code}${pLang?.code === lang.code ? '.cc' : ''}`, - lang, - closedCaption: pLang?.code === lang.code - }); - } + const subs = await getData({ + baseUrl: 'https://playback.prd.funimationsvc.com/v1/play', + url: `/${episodeID}`, + token: this.token, + useToken: true, + debug: this.debug, + querystring: { deviceType: 'web' } + }); + if (!subs.ok || !subs.res || !subs.res.body) { + console.log('[ERROR] Subtitle Request failed.'); + return []; } - - return found; + const parsed: SubtitleRequest = JSON.parse(subs.res.body); + + const found: { + isCC: boolean; + url: string; + lang: langsData.LanguageItem; + }[] = parsed.primary.subtitles.filter(a => a.fileExt === 'vtt').map(subtitle => { + return { + isCC: subtitle.contentType === 'cc', + url: subtitle.filePath, + lang: langsData.languages.find(a => a.funi_locale ?? a.locale === subtitle.languageCode) + }; + }).concat(m.filter(a => a.filePath.split('.').pop() === 'vtt').map(media => { + const lang = langsData.languages.find(a => media.language === (a.funi_name || a.name)); + const pLang = langsData.languages.find(a => (a.funi_name || a.name) === parentLanguage); + return { + isCC: pLang?.code === lang?.code, + url: media.filePath, + lang + }; + })).filter((a) => a.lang !== undefined) as { + isCC: boolean; + url: string; + lang: langsData.LanguageItem; + }[]; + + const ret = found.filter(item => { + return data.dlsubs.includes('all') || data.dlsubs.some(a => a === item.lang.locale); + }); + + return ret.map(a => ({ + ext: `.${a.lang.code}${a.isCC ? '.cc' : ''}`, + lang: a.lang, + url: a.url, + closedCaption: a.isCC + })); } } diff --git a/modules/module.getdata.ts b/modules/module.getdata.ts index db33385..d1a1667 100644 --- a/modules/module.getdata.ts +++ b/modules/module.getdata.ts @@ -36,6 +36,7 @@ const getData = async (options: Options) => { const gOptions = { url: options.url, + http2: true, headers: { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0', 'Accept-Encoding': 'gzip',