Merge pull request #29 from movie-web/linked-captions

Linked captions
This commit is contained in:
William Oldham 2023-11-18 23:03:11 +00:00 committed by GitHub
commit 20229a4667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 140 additions and 13 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@movie-web/providers", "name": "@movie-web/providers",
"version": "1.0.5", "version": "1.1.0",
"description": "Package that contains all the providers of movie-web", "description": "Package that contains all the providers of movie-web",
"main": "./lib/index.umd.js", "main": "./lib/index.umd.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -79,6 +79,7 @@
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"iso-639-1": "^3.1.0",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"unpacker": "^1.0.1" "unpacker": "^1.0.1"

32
src/providers/captions.ts Normal file
View File

@ -0,0 +1,32 @@
import ISO6391 from 'iso-639-1';
export const captionTypes = {
srt: 'srt',
vtt: 'vtt',
};
export type CaptionType = keyof typeof captionTypes;
export type Caption = {
type: CaptionType;
url: string;
hasCorsRestrictions: boolean;
language: string;
};
export function getCaptionTypeFromUrl(url: string): CaptionType | null {
const extensions = Object.keys(captionTypes) as CaptionType[];
const type = extensions.find((v) => url.endsWith(`.${v}`));
if (!type) return null;
return type;
}
export function labelToLanguageCode(label: string): string | null {
const code = ISO6391.getCode(label);
if (code.length === 0) return null;
return code;
}
export function isValidLanguageCode(code: string | null): boolean {
if (!code) return false;
return ISO6391.validate(code);
}

View File

@ -65,6 +65,7 @@ export const febBoxScraper = makeEmbed({
return { return {
stream: { stream: {
type: 'file', type: 'file',
captions: [],
flags: [flags.NO_CORS], flags: [flags.NO_CORS],
qualities: embedQualities, qualities: embedQualities,
}, },

View File

@ -36,6 +36,7 @@ export const mixdropScraper = makeEmbed({
stream: { stream: {
type: 'file', type: 'file',
flags: [], flags: [],
captions: [],
qualities: { qualities: {
unknown: { unknown: {
type: 'mp4', type: 'mp4',

View File

@ -18,6 +18,7 @@ export const mp4uploadScraper = makeEmbed({
stream: { stream: {
type: 'file', type: 'file',
flags: [flags.NO_CORS], flags: [flags.NO_CORS],
captions: [],
qualities: { qualities: {
'1080': { '1080': {
type: 'mp4', type: 'mp4',

View File

@ -159,6 +159,7 @@ export const streamsbScraper = makeEmbed({
type: 'file', type: 'file',
flags: [flags.NO_CORS], flags: [flags.NO_CORS],
qualities, qualities,
captions: [],
}, },
}; };
}, },

View File

@ -2,6 +2,7 @@ import crypto from 'crypto-js';
import { flags } from '@/main/targets'; import { flags } from '@/main/targets';
import { makeEmbed } from '@/providers/base'; import { makeEmbed } from '@/providers/base';
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
const { AES, enc } = crypto; const { AES, enc } = crypto;
@ -96,11 +97,27 @@ export const upcloudScraper = makeEmbed({
if (!sources) throw new Error('upcloud source not found'); if (!sources) throw new Error('upcloud source not found');
const captions: Caption[] = [];
streamRes.tracks.forEach((track) => {
if (track.kind !== 'captions') return;
const type = getCaptionTypeFromUrl(track.file);
if (!type) return;
const language = labelToLanguageCode(track.label);
if (!language) return;
captions.push({
language,
hasCorsRestrictions: false,
type,
url: track.file,
});
});
return { return {
stream: { stream: {
type: 'hls', type: 'hls',
playlist: sources.file, playlist: sources.file,
flags: [flags.NO_CORS], flags: [flags.NO_CORS],
captions,
}, },
}; };
}, },

View File

@ -25,6 +25,7 @@ export const upstreamScraper = makeEmbed({
type: 'hls', type: 'hls',
playlist: link[1], playlist: link[1],
flags: [flags.NO_CORS], flags: [flags.NO_CORS],
captions: [],
}, },
}; };
} }

View File

@ -17,9 +17,7 @@ export const goMoviesScraper = makeSourcerer({
async scrapeShow(ctx) { async scrapeShow(ctx) {
const search = await ctx.proxiedFetcher<string>(`/ajax/search`, { const search = await ctx.proxiedFetcher<string>(`/ajax/search`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: new URLSearchParams({ keyword: ctx.media.title }),
keyword: ctx.media.title,
}),
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
}, },
@ -104,9 +102,7 @@ export const goMoviesScraper = makeSourcerer({
async scrapeMovie(ctx) { async scrapeMovie(ctx) {
const search = await ctx.proxiedFetcher<string>(`ajax/search`, { const search = await ctx.proxiedFetcher<string>(`ajax/search`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: new URLSearchParams({ keyword: ctx.media.title }),
keyword: ctx.media.title,
}),
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
}, },

View File

@ -23,6 +23,7 @@ export const remotestreamScraper = makeSourcerer({
return { return {
embeds: [], embeds: [],
stream: { stream: {
captions: [],
playlist: playlistLink, playlist: playlistLink,
type: 'hls', type: 'hls',
flags: [flags.NO_CORS], flags: [flags.NO_CORS],
@ -40,6 +41,7 @@ export const remotestreamScraper = makeSourcerer({
return { return {
embeds: [], embeds: [],
stream: { stream: {
captions: [],
playlist: playlistLink, playlist: playlistLink,
type: 'hls', type: 'hls',
flags: [flags.NO_CORS], flags: [flags.NO_CORS],

View File

@ -6,14 +6,16 @@ import { sendRequest } from './sendRequest';
const allowedQualities = ['360', '480', '720', '1080']; const allowedQualities = ['360', '480', '720', '1080'];
export async function getStreamQualities(ctx: ScrapeContext, apiQuery: object) { export async function getStreamQualities(ctx: ScrapeContext, apiQuery: object) {
const mediaRes: { list: { path: string; real_quality: string }[] } = (await sendRequest(ctx, apiQuery)).data; const mediaRes: { list: { path: string; quality: string; fid?: number }[] } = (await sendRequest(ctx, apiQuery)).data;
ctx.progress(66); ctx.progress(66);
console.log(mediaRes);
const qualityMap = mediaRes.list const qualityMap = mediaRes.list
.filter((file) => allowedQualities.includes(file.real_quality.replace('p', ''))) .filter((file) => allowedQualities.includes(file.quality.replace('p', '')))
.map((file) => ({ .map((file) => ({
url: file.path, url: file.path,
quality: file.real_quality.replace('p', ''), quality: file.quality.replace('p', ''),
})); }));
const qualities: Record<string, StreamFile> = {}; const qualities: Record<string, StreamFile> = {};
@ -28,5 +30,8 @@ export async function getStreamQualities(ctx: ScrapeContext, apiQuery: object) {
} }
}); });
return qualities; return {
qualities,
fid: mediaRes.list[0]?.fid,
};
} }

View File

@ -1,5 +1,6 @@
import { flags } from '@/main/targets'; import { flags } from '@/main/targets';
import { makeSourcerer } from '@/providers/base'; import { makeSourcerer } from '@/providers/base';
import { getSubtitles } from '@/providers/sources/superstream/subtitles';
import { compareTitle } from '@/utils/compare'; import { compareTitle } from '@/utils/compare';
import { NotFoundError } from '@/utils/errors'; import { NotFoundError } from '@/utils/errors';
@ -41,11 +42,19 @@ export const superStreamScraper = makeSourcerer({
group: '', group: '',
}; };
const qualities = await getStreamQualities(ctx, apiQuery); const { qualities, fid } = await getStreamQualities(ctx, apiQuery);
return { return {
embeds: [], embeds: [],
stream: { stream: {
captions: await getSubtitles(
ctx,
superstreamId,
fid,
'show',
ctx.media.episode.number,
ctx.media.season.number,
),
qualities, qualities,
type: 'file', type: 'file',
flags: [flags.NO_CORS], flags: [flags.NO_CORS],
@ -80,11 +89,12 @@ export const superStreamScraper = makeSourcerer({
group: '', group: '',
}; };
const qualities = await getStreamQualities(ctx, apiQuery); const { qualities, fid } = await getStreamQualities(ctx, apiQuery);
return { return {
embeds: [], embeds: [],
stream: { stream: {
captions: await getSubtitles(ctx, superstreamId, fid, 'movie'),
qualities, qualities,
type: 'file', type: 'file',
flags: [flags.NO_CORS], flags: [flags.NO_CORS],

View File

@ -0,0 +1,57 @@
import { Caption, getCaptionTypeFromUrl, isValidLanguageCode } from '@/providers/captions';
import { sendRequest } from '@/providers/sources/superstream/sendRequest';
import { ScrapeContext } from '@/utils/context';
interface CaptionApiResponse {
data: {
list: {
subtitles: {
order: number;
lang: string;
file_path: string;
}[];
}[];
};
}
export async function getSubtitles(
ctx: ScrapeContext,
id: string,
fid: number | undefined,
type: 'show' | 'movie',
episodeId?: number,
seasonId?: number,
): Promise<Caption[]> {
const module = type === 'movie' ? 'Movie_srt_list_v2' : 'TV_srt_list_v2';
const subtitleApiQuery = {
fid,
uid: '',
module,
mid: id,
episode: episodeId?.toString(),
season: seasonId?.toString(),
group: episodeId ? '' : undefined,
};
const subtitleList = ((await sendRequest(ctx, subtitleApiQuery)) as CaptionApiResponse).data.list;
const output: Caption[] = [];
subtitleList.forEach((sub) => {
const subtitle = sub.subtitles.sort((a, b) => a.order - b.order)[0];
if (!subtitle) return;
const subtitleType = getCaptionTypeFromUrl(subtitle.file_path);
if (!subtitleType) return;
const validCode = isValidLanguageCode(subtitle.lang);
if (!validCode) return;
output.push({
language: subtitle.lang,
hasCorsRestrictions: true,
type: subtitleType,
url: subtitle.file_path,
});
});
return output;
}

View File

@ -1,4 +1,5 @@
import { Flags } from '@/main/targets'; import { Flags } from '@/main/targets';
import { Caption } from '@/providers/captions';
export type StreamFile = { export type StreamFile = {
type: 'mp4'; type: 'mp4';
@ -18,6 +19,7 @@ export type HlsBasedStream = {
type: 'hls'; type: 'hls';
flags: Flags[]; flags: Flags[];
playlist: string; playlist: string;
captions: Caption[];
}; };
export type Stream = FileBasedStream | HlsBasedStream; export type Stream = FileBasedStream | HlsBasedStream;