Merge branch 'dev' into fix-showbox-useragent
This commit is contained in:
commit
e728f8c211
|
@ -2,7 +2,7 @@ import { getConfig } from '@/dev-cli/config';
|
||||||
|
|
||||||
import { MovieMedia, ShowMedia } from '..';
|
import { MovieMedia, ShowMedia } from '..';
|
||||||
|
|
||||||
export async function makeTMDBRequest(url: string): Promise<Response> {
|
export async function makeTMDBRequest(url: string, appendToResponse?: string): Promise<Response> {
|
||||||
const headers: {
|
const headers: {
|
||||||
accept: 'application/json';
|
accept: 'application/json';
|
||||||
authorization?: string;
|
authorization?: string;
|
||||||
|
@ -10,7 +10,7 @@ export async function makeTMDBRequest(url: string): Promise<Response> {
|
||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
let requestURL = url;
|
const requestURL = new URL(url);
|
||||||
const key = getConfig().tmdbApiKey;
|
const key = getConfig().tmdbApiKey;
|
||||||
|
|
||||||
// * JWT keys always start with ey and are ONLY valid as a header.
|
// * JWT keys always start with ey and are ONLY valid as a header.
|
||||||
|
@ -19,7 +19,11 @@ export async function makeTMDBRequest(url: string): Promise<Response> {
|
||||||
if (key.startsWith('ey')) {
|
if (key.startsWith('ey')) {
|
||||||
headers.authorization = `Bearer ${key}`;
|
headers.authorization = `Bearer ${key}`;
|
||||||
} else {
|
} else {
|
||||||
requestURL += `?api_key=${key}`;
|
requestURL.searchParams.append('api_key', key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appendToResponse) {
|
||||||
|
requestURL.searchParams.append('append_to_response', appendToResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(requestURL, {
|
return fetch(requestURL, {
|
||||||
|
@ -29,7 +33,7 @@ export async function makeTMDBRequest(url: string): Promise<Response> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMovieMediaDetails(id: string): Promise<MovieMedia> {
|
export async function getMovieMediaDetails(id: string): Promise<MovieMedia> {
|
||||||
const response = await makeTMDBRequest(`https://api.themoviedb.org/3/movie/${id}`);
|
const response = await makeTMDBRequest(`https://api.themoviedb.org/3/movie/${id}`, 'external_ids');
|
||||||
const movie = await response.json();
|
const movie = await response.json();
|
||||||
|
|
||||||
if (movie.success === false) {
|
if (movie.success === false) {
|
||||||
|
@ -45,13 +49,14 @@ export async function getMovieMediaDetails(id: string): Promise<MovieMedia> {
|
||||||
title: movie.title,
|
title: movie.title,
|
||||||
releaseYear: Number(movie.release_date.split('-')[0]),
|
releaseYear: Number(movie.release_date.split('-')[0]),
|
||||||
tmdbId: id,
|
tmdbId: id,
|
||||||
|
imdbId: movie.imdb_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getShowMediaDetails(id: string, seasonNumber: string, episodeNumber: string): Promise<ShowMedia> {
|
export async function getShowMediaDetails(id: string, seasonNumber: string, episodeNumber: string): Promise<ShowMedia> {
|
||||||
// * TV shows require the TMDB ID for the series, season, and episode
|
// * TV shows require the TMDB ID for the series, season, and episode
|
||||||
// * and the name of the series. Needs multiple requests
|
// * and the name of the series. Needs multiple requests
|
||||||
let response = await makeTMDBRequest(`https://api.themoviedb.org/3/tv/${id}`);
|
let response = await makeTMDBRequest(`https://api.themoviedb.org/3/tv/${id}`, 'external_ids');
|
||||||
const series = await response.json();
|
const series = await response.json();
|
||||||
|
|
||||||
if (series.success === false) {
|
if (series.success === false) {
|
||||||
|
@ -91,5 +96,6 @@ export async function getShowMediaDetails(id: string, seasonNumber: string, epis
|
||||||
number: season.season_number,
|
number: season.season_number,
|
||||||
tmdbId: season.id,
|
tmdbId: season.id,
|
||||||
},
|
},
|
||||||
|
imdbId: series.external_ids.imdb_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,12 @@ import { showboxScraper } from '@/providers/sources/showbox/index';
|
||||||
import { vidsrcScraper } from '@/providers/sources/vidsrc/index';
|
import { vidsrcScraper } from '@/providers/sources/vidsrc/index';
|
||||||
import { zoechipScraper } from '@/providers/sources/zoechip';
|
import { zoechipScraper } from '@/providers/sources/zoechip';
|
||||||
|
|
||||||
|
import { fileMoonScraper } from './embeds/filemoon';
|
||||||
import { smashyStreamDScraper } from './embeds/smashystream/dued';
|
import { smashyStreamDScraper } from './embeds/smashystream/dued';
|
||||||
import { smashyStreamFScraper } from './embeds/smashystream/video1';
|
import { smashyStreamFScraper } from './embeds/smashystream/video1';
|
||||||
|
import { vidplayScraper } from './embeds/vidplay';
|
||||||
import { smashyStreamScraper } from './sources/smashystream';
|
import { smashyStreamScraper } from './sources/smashystream';
|
||||||
|
import { vidSrcToScraper } from './sources/vidsrcto';
|
||||||
|
|
||||||
export function gatherAllSources(): Array<Sourcerer> {
|
export function gatherAllSources(): Array<Sourcerer> {
|
||||||
// all sources are gathered here
|
// all sources are gathered here
|
||||||
|
@ -33,6 +36,7 @@ export function gatherAllSources(): Array<Sourcerer> {
|
||||||
vidsrcScraper,
|
vidsrcScraper,
|
||||||
lookmovieScraper,
|
lookmovieScraper,
|
||||||
smashyStreamScraper,
|
smashyStreamScraper,
|
||||||
|
vidSrcToScraper,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,5 +54,7 @@ export function gatherAllEmbeds(): Array<Embed> {
|
||||||
streambucketScraper,
|
streambucketScraper,
|
||||||
smashyStreamFScraper,
|
smashyStreamFScraper,
|
||||||
smashyStreamDScraper,
|
smashyStreamDScraper,
|
||||||
|
fileMoonScraper,
|
||||||
|
vidplayScraper,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { unpack } from 'unpacker';
|
||||||
|
|
||||||
|
import { SubtitleResult } from './types';
|
||||||
|
import { makeEmbed } from '../../base';
|
||||||
|
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '../../captions';
|
||||||
|
|
||||||
|
const evalCodeRegex = /eval\((.*)\)/g;
|
||||||
|
const fileRegex = /file:"(.*?)"/g;
|
||||||
|
|
||||||
|
export const fileMoonScraper = makeEmbed({
|
||||||
|
id: 'filemoon',
|
||||||
|
name: 'Filemoon',
|
||||||
|
rank: 400,
|
||||||
|
scrape: async (ctx) => {
|
||||||
|
const embedRes = await ctx.proxiedFetcher<string>(ctx.url, {
|
||||||
|
headers: {
|
||||||
|
referer: ctx.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const evalCode = evalCodeRegex.exec(embedRes);
|
||||||
|
if (!evalCode) throw new Error('Failed to find eval code');
|
||||||
|
const unpacked = unpack(evalCode[1]);
|
||||||
|
const file = fileRegex.exec(unpacked);
|
||||||
|
if (!file?.[1]) throw new Error('Failed to find file');
|
||||||
|
|
||||||
|
const url = new URL(ctx.url);
|
||||||
|
const subtitlesLink = url.searchParams.get('sub.info');
|
||||||
|
const captions: Caption[] = [];
|
||||||
|
if (subtitlesLink) {
|
||||||
|
const captionsResult = await ctx.proxiedFetcher<SubtitleResult>(subtitlesLink);
|
||||||
|
|
||||||
|
for (const caption of captionsResult) {
|
||||||
|
const language = labelToLanguageCode(caption.label);
|
||||||
|
const captionType = getCaptionTypeFromUrl(caption.file);
|
||||||
|
if (!language || !captionType) continue;
|
||||||
|
captions.push({
|
||||||
|
id: caption.file,
|
||||||
|
url: caption.file,
|
||||||
|
type: captionType,
|
||||||
|
language,
|
||||||
|
hasCorsRestrictions: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: [
|
||||||
|
{
|
||||||
|
id: 'primary',
|
||||||
|
type: 'hls',
|
||||||
|
playlist: file[1],
|
||||||
|
flags: [],
|
||||||
|
captions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type SubtitleResult = {
|
||||||
|
file: string;
|
||||||
|
label: string;
|
||||||
|
kind: string;
|
||||||
|
}[];
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { makeFullUrl } from '@/fetchers/common';
|
||||||
|
import { decodeData } from '@/providers/sources/vidsrcto/common';
|
||||||
|
import { EmbedScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
export const vidplayBase = 'https://vidplay.site';
|
||||||
|
|
||||||
|
// This file is based on https://github.com/Ciarands/vidsrc-to-resolver/blob/dffa45e726a4b944cb9af0c9e7630476c93c0213/vidsrc.py#L16
|
||||||
|
// Full credits to @Ciarands!
|
||||||
|
|
||||||
|
export const getDecryptionKeys = async (ctx: EmbedScrapeContext): Promise<string[]> => {
|
||||||
|
const res = await ctx.fetcher<string>(
|
||||||
|
'https://raw.githubusercontent.com/Claudemirovsky/worstsource-keys/keys/keys.json',
|
||||||
|
);
|
||||||
|
return JSON.parse(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEncodedId = async (ctx: EmbedScrapeContext) => {
|
||||||
|
const url = new URL(ctx.url);
|
||||||
|
const id = url.pathname.replace('/e/', '');
|
||||||
|
const keyList = await getDecryptionKeys(ctx);
|
||||||
|
|
||||||
|
const decodedId = decodeData(keyList[0], id);
|
||||||
|
const encodedResult = decodeData(keyList[1], decodedId);
|
||||||
|
const b64encoded = btoa(encodedResult);
|
||||||
|
return b64encoded.replace('/', '_');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFuTokenKey = async (ctx: EmbedScrapeContext) => {
|
||||||
|
const id = await getEncodedId(ctx);
|
||||||
|
const fuTokenRes = await ctx.proxiedFetcher<string>('/futoken', {
|
||||||
|
baseUrl: vidplayBase,
|
||||||
|
headers: {
|
||||||
|
referer: ctx.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const fuKey = fuTokenRes.match(/var\s+k\s*=\s*'([^']+)'/)?.[1];
|
||||||
|
if (!fuKey) throw new Error('No fuKey found');
|
||||||
|
const tokens = [];
|
||||||
|
for (let i = 0; i < id.length; i += 1) {
|
||||||
|
tokens.push(fuKey.charCodeAt(i % fuKey.length) + id.charCodeAt(i));
|
||||||
|
}
|
||||||
|
return `${fuKey},${tokens.join(',')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFileUrl = async (ctx: EmbedScrapeContext) => {
|
||||||
|
const fuToken = await getFuTokenKey(ctx);
|
||||||
|
return makeFullUrl(`/mediainfo/${fuToken}`, {
|
||||||
|
baseUrl: vidplayBase,
|
||||||
|
query: {
|
||||||
|
...Object.fromEntries(new URL(ctx.url).searchParams.entries()),
|
||||||
|
autostart: 'true',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { makeEmbed } from '@/providers/base';
|
||||||
|
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
|
||||||
|
|
||||||
|
import { getFileUrl } from './common';
|
||||||
|
import { SubtitleResult, VidplaySourceResponse } from './types';
|
||||||
|
|
||||||
|
export const vidplayScraper = makeEmbed({
|
||||||
|
id: 'vidplay',
|
||||||
|
name: 'VidPlay',
|
||||||
|
rank: 401,
|
||||||
|
scrape: async (ctx) => {
|
||||||
|
const fileUrl = await getFileUrl(ctx);
|
||||||
|
const fileUrlRes = await ctx.proxiedFetcher<VidplaySourceResponse>(fileUrl, {
|
||||||
|
headers: {
|
||||||
|
referer: ctx.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (typeof fileUrlRes.result === 'number') throw new Error('File not found');
|
||||||
|
const source = fileUrlRes.result.sources[0].file;
|
||||||
|
|
||||||
|
const url = new URL(ctx.url);
|
||||||
|
const subtitlesLink = url.searchParams.get('sub.info');
|
||||||
|
const captions: Caption[] = [];
|
||||||
|
if (subtitlesLink) {
|
||||||
|
const captionsResult = await ctx.proxiedFetcher<SubtitleResult>(subtitlesLink);
|
||||||
|
|
||||||
|
for (const caption of captionsResult) {
|
||||||
|
const language = labelToLanguageCode(caption.label);
|
||||||
|
const captionType = getCaptionTypeFromUrl(caption.file);
|
||||||
|
if (!language || !captionType) continue;
|
||||||
|
captions.push({
|
||||||
|
id: caption.file,
|
||||||
|
url: caption.file,
|
||||||
|
type: captionType,
|
||||||
|
language,
|
||||||
|
hasCorsRestrictions: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: [
|
||||||
|
{
|
||||||
|
id: 'primary',
|
||||||
|
type: 'hls',
|
||||||
|
playlist: source,
|
||||||
|
flags: [],
|
||||||
|
captions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
export type VidplaySourceResponse = {
|
||||||
|
result:
|
||||||
|
| {
|
||||||
|
sources: {
|
||||||
|
file: string;
|
||||||
|
tracks: {
|
||||||
|
file: string;
|
||||||
|
kind: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
| number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubtitleResult = {
|
||||||
|
file: string;
|
||||||
|
label: string;
|
||||||
|
kind: string;
|
||||||
|
}[];
|
|
@ -0,0 +1,49 @@
|
||||||
|
// This file is based on https://github.com/Ciarands/vidsrc-to-resolver/blob/dffa45e726a4b944cb9af0c9e7630476c93c0213/vidsrc.py#L16
|
||||||
|
// Full credits to @Ciarands!
|
||||||
|
|
||||||
|
const DECRYPTION_KEY = '8z5Ag5wgagfsOuhz';
|
||||||
|
|
||||||
|
export const decodeBase64UrlSafe = (str: string) => {
|
||||||
|
const standardizedInput = str.replace(/_/g, '/').replace(/-/g, '+');
|
||||||
|
const decodedData = atob(standardizedInput);
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(decodedData.length);
|
||||||
|
for (let i = 0; i < bytes.length; i += 1) {
|
||||||
|
bytes[i] = decodedData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeData = (key: string, data: any) => {
|
||||||
|
const state = Array.from(Array(256).keys());
|
||||||
|
let index1 = 0;
|
||||||
|
for (let i = 0; i < 256; i += 1) {
|
||||||
|
index1 = (index1 + state[i] + key.charCodeAt(i % key.length)) % 256;
|
||||||
|
const temp = state[i];
|
||||||
|
state[i] = state[index1];
|
||||||
|
state[index1] = temp;
|
||||||
|
}
|
||||||
|
index1 = 0;
|
||||||
|
let index2 = 0;
|
||||||
|
let finalKey = '';
|
||||||
|
for (let char = 0; char < data.length; char += 1) {
|
||||||
|
index1 = (index1 + 1) % 256;
|
||||||
|
index2 = (index2 + state[index1]) % 256;
|
||||||
|
const temp = state[index1];
|
||||||
|
state[index1] = state[index2];
|
||||||
|
state[index2] = temp;
|
||||||
|
if (typeof data[char] === 'string') {
|
||||||
|
finalKey += String.fromCharCode(data[char].charCodeAt(0) ^ state[(state[index1] + state[index2]) % 256]);
|
||||||
|
} else if (typeof data[char] === 'number') {
|
||||||
|
finalKey += String.fromCharCode(data[char] ^ state[(state[index1] + state[index2]) % 256]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finalKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSourceUrl = (sourceUrl: string) => {
|
||||||
|
const encoded = decodeBase64UrlSafe(sourceUrl);
|
||||||
|
const decoded = decodeData(DECRYPTION_KEY, encoded);
|
||||||
|
return decodeURIComponent(decodeURIComponent(decoded));
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
|
import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
import { decryptSourceUrl } from './common';
|
||||||
|
import { SourceResult, SourcesResult } from './types';
|
||||||
|
|
||||||
|
const vidSrcToBase = 'https://vidsrc.to';
|
||||||
|
const referer = `${vidSrcToBase}/`;
|
||||||
|
|
||||||
|
const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> => {
|
||||||
|
const imdbId = ctx.media.imdbId;
|
||||||
|
const url =
|
||||||
|
ctx.media.type === 'movie'
|
||||||
|
? `/embed/movie/${imdbId}`
|
||||||
|
: `/embed/tv/${imdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`;
|
||||||
|
const mainPage = await ctx.proxiedFetcher<string>(url, {
|
||||||
|
baseUrl: vidSrcToBase,
|
||||||
|
headers: {
|
||||||
|
referer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mainPage$ = load(mainPage);
|
||||||
|
const dataId = mainPage$('a[data-id]').attr('data-id');
|
||||||
|
if (!dataId) throw new Error('No data-id found');
|
||||||
|
const sources = await ctx.proxiedFetcher<SourcesResult>(`/ajax/embed/episode/${dataId}/sources`, {
|
||||||
|
baseUrl: vidSrcToBase,
|
||||||
|
headers: {
|
||||||
|
referer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (sources.status !== 200) throw new Error('No sources found');
|
||||||
|
|
||||||
|
const embeds: SourcererEmbed[] = [];
|
||||||
|
const embedUrls = [];
|
||||||
|
for (const source of sources.result) {
|
||||||
|
const sourceRes = await ctx.proxiedFetcher<SourceResult>(`/ajax/embed/source/${source.id}`, {
|
||||||
|
baseUrl: vidSrcToBase,
|
||||||
|
headers: {
|
||||||
|
referer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const decryptedUrl = decryptSourceUrl(sourceRes.result.url);
|
||||||
|
embedUrls.push(decryptedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Originally Filemoon does not have subtitles. But we can use the ones from Vidplay.
|
||||||
|
const subtitleUrl = new URL(embedUrls.find((v) => v.includes('sub.info')) ?? '').searchParams.get('sub.info');
|
||||||
|
for (const source of sources.result) {
|
||||||
|
if (source.title === 'Vidplay') {
|
||||||
|
const embedUrl = embedUrls.find((v) => v.includes('vidplay'));
|
||||||
|
if (!embedUrl) continue;
|
||||||
|
embeds.push({
|
||||||
|
embedId: 'vidplay',
|
||||||
|
url: embedUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.title === 'Filemoon') {
|
||||||
|
const embedUrl = embedUrls.find((v) => v.includes('filemoon'));
|
||||||
|
if (!embedUrl) continue;
|
||||||
|
const fullUrl = new URL(embedUrl);
|
||||||
|
if (subtitleUrl) fullUrl.searchParams.set('sub.info', subtitleUrl);
|
||||||
|
embeds.push({
|
||||||
|
embedId: 'filemoon',
|
||||||
|
url: fullUrl.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const vidSrcToScraper = makeSourcerer({
|
||||||
|
id: 'vidsrcto',
|
||||||
|
name: 'VidSrcTo',
|
||||||
|
scrapeMovie: universalScraper,
|
||||||
|
scrapeShow: universalScraper,
|
||||||
|
flags: [],
|
||||||
|
rank: 400,
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
export type VidSrcToResponse<T> = {
|
||||||
|
status: number;
|
||||||
|
result: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SourcesResult = VidSrcToResponse<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
title: 'Filemoon' | 'Vidplay';
|
||||||
|
}[]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type SourceResult = VidSrcToResponse<{
|
||||||
|
url: string;
|
||||||
|
}>;
|
Loading…
Reference in New Issue