commit
20229a4667
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -159,6 +159,7 @@ export const streamsbScraper = makeEmbed({
|
||||||
type: 'file',
|
type: 'file',
|
||||||
flags: [flags.NO_CORS],
|
flags: [flags.NO_CORS],
|
||||||
qualities,
|
qualities,
|
||||||
|
captions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -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: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue