mirror of
https://github.com/movie-web/providers.git
synced 2025-09-13 12:43:25 +00:00
Merge branch 'dev' into pr/14
This commit is contained in:
3895
.docs/package-lock.json
generated
3895
.docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2
.docs/package.json
Executable file → Normal file
2
.docs/package.json
Executable file → Normal file
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt-themes/docus": "^1.13.1",
|
"@nuxt-themes/docus": "^1.13.1",
|
||||||
"@nuxt/devtools": "^0.6.7",
|
"@nuxt/devtools": "^1.0.1",
|
||||||
"@nuxt/eslint-config": "^0.1.1",
|
"@nuxt/eslint-config": "^0.1.1",
|
||||||
"@nuxtjs/plausible": "^0.2.1",
|
"@nuxtjs/plausible": "^0.2.1",
|
||||||
"@types/node": "^20.4.0",
|
"@types/node": "^20.4.0",
|
||||||
|
4708
package-lock.json
generated
4708
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@movie-web/providers",
|
"name": "@movie-web/providers",
|
||||||
"version": "1.0.0",
|
"version": "1.1.5",
|
||||||
"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,8 +79,9 @@
|
|||||||
"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",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"randombytes": "^2.1.0",
|
|
||||||
"unpacker": "^1.0.1"
|
"unpacker": "^1.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
55
src/__test__/utils/valid.test.ts
Normal file
55
src/__test__/utils/valid.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { makeStandardFetcher } from "@/fetchers/standardFetch";
|
||||||
|
import { makeProviders } from "@/main/builder";
|
||||||
|
import { targets } from "@/main/targets";
|
||||||
|
import { isValidStream } from "@/utils/valid";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
describe('isValidStream()', () => {
|
||||||
|
it('should pass valid streams', () => {
|
||||||
|
expect(isValidStream({
|
||||||
|
type: "file",
|
||||||
|
flags: [],
|
||||||
|
qualities: {
|
||||||
|
"1080": {
|
||||||
|
type: "mp4",
|
||||||
|
url: "hello-world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})).toBe(true);
|
||||||
|
expect(isValidStream({
|
||||||
|
type: "hls",
|
||||||
|
flags: [],
|
||||||
|
playlist: "hello-world"
|
||||||
|
})).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect empty qualities', () => {
|
||||||
|
expect(isValidStream({
|
||||||
|
type: "file",
|
||||||
|
flags: [],
|
||||||
|
qualities: {}
|
||||||
|
})).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect empty stream urls', () => {
|
||||||
|
expect(isValidStream({
|
||||||
|
type: "file",
|
||||||
|
flags: [],
|
||||||
|
qualities: {
|
||||||
|
"1080": {
|
||||||
|
type: "mp4",
|
||||||
|
url: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect emtpy HLS playlists', () => {
|
||||||
|
expect(isValidStream({
|
||||||
|
type: "hls",
|
||||||
|
flags: [],
|
||||||
|
playlist: "",
|
||||||
|
})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,5 +1,7 @@
|
|||||||
/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
|
/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
|
||||||
|
|
||||||
|
import util from 'node:util';
|
||||||
|
|
||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { prompt } from 'enquirer';
|
import { prompt } from 'enquirer';
|
||||||
@@ -46,6 +48,10 @@ if (!TMDB_API_KEY?.trim()) {
|
|||||||
throw new Error('Missing MOVIE_WEB_TMDB_API_KEY environment variable');
|
throw new Error('Missing MOVIE_WEB_TMDB_API_KEY environment variable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logDeepObject(object: Record<any, any>) {
|
||||||
|
console.log(util.inspect(object, { showHidden: false, depth: null, colors: true }));
|
||||||
|
}
|
||||||
|
|
||||||
function getAllSources() {
|
function getAllSources() {
|
||||||
// * The only way to get a list of all sources is to
|
// * The only way to get a list of all sources is to
|
||||||
// * create all these things. Maybe this should change
|
// * create all these things. Maybe this should change
|
||||||
@@ -183,7 +189,7 @@ async function runScraper(providers: ProviderControls, source: MetaOutput, optio
|
|||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
});
|
});
|
||||||
spinnies.succeed('scrape', { text: 'Done!' });
|
spinnies.succeed('scrape', { text: 'Done!' });
|
||||||
console.log(result);
|
logDeepObject(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Unknown error';
|
let message = 'Unknown error';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -208,7 +214,7 @@ async function runScraper(providers: ProviderControls, source: MetaOutput, optio
|
|||||||
id: source.id,
|
id: source.id,
|
||||||
});
|
});
|
||||||
spinnies.succeed('scrape', { text: 'Done!' });
|
spinnies.succeed('scrape', { text: 'Done!' });
|
||||||
console.log(result);
|
logDeepObject(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Unknown error';
|
let message = 'Unknown error';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
@@ -6,6 +6,7 @@ import { EmbedOutput, SourcererOutput } from '@/providers/base';
|
|||||||
import { ProviderList } from '@/providers/get';
|
import { ProviderList } from '@/providers/get';
|
||||||
import { ScrapeContext } from '@/utils/context';
|
import { ScrapeContext } from '@/utils/context';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
import { isValidStream } from '@/utils/valid';
|
||||||
|
|
||||||
export type IndividualSourceRunnerOptions = {
|
export type IndividualSourceRunnerOptions = {
|
||||||
features: FeatureMap;
|
features: FeatureMap;
|
||||||
@@ -50,7 +51,7 @@ export async function scrapeInvidualSource(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// stream doesn't satisfy the feature flags, so gets removed in output
|
// stream doesn't satisfy the feature flags, so gets removed in output
|
||||||
if (output?.stream && !flagsAllowedInFeatures(ops.features, output.stream.flags)) {
|
if (output?.stream && (!isValidStream(output.stream) || !flagsAllowedInFeatures(ops.features, output.stream.flags))) {
|
||||||
output.stream = undefined;
|
output.stream = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +90,9 @@ export async function scrapeIndividualEmbed(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isValidStream(output.stream)) throw new NotFoundError('stream is incomplete');
|
||||||
if (!flagsAllowedInFeatures(ops.features, output.stream.flags))
|
if (!flagsAllowedInFeatures(ops.features, output.stream.flags))
|
||||||
throw new NotFoundError("stream doesn't satisfy target feature flags");
|
throw new NotFoundError("stream doesn't satisfy target feature flags");
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import { Stream } from '@/providers/streams';
|
|||||||
import { ScrapeContext } from '@/utils/context';
|
import { ScrapeContext } from '@/utils/context';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
import { reorderOnIdList } from '@/utils/list';
|
import { reorderOnIdList } from '@/utils/list';
|
||||||
|
import { isValidStream } from '@/utils/valid';
|
||||||
|
|
||||||
export type RunOutput = {
|
export type RunOutput = {
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
@@ -79,6 +80,9 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt
|
|||||||
...contextBase,
|
...contextBase,
|
||||||
media: ops.media,
|
media: ops.media,
|
||||||
});
|
});
|
||||||
|
if (output?.stream && !isValidStream(output?.stream)) {
|
||||||
|
throw new NotFoundError('stream is incomplete');
|
||||||
|
}
|
||||||
if (output?.stream && !flagsAllowedInFeatures(ops.features, output.stream.flags)) {
|
if (output?.stream && !flagsAllowedInFeatures(ops.features, output.stream.flags)) {
|
||||||
throw new NotFoundError("stream doesn't satisfy target feature flags");
|
throw new NotFoundError("stream doesn't satisfy target feature flags");
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
export const flags = {
|
export const flags = {
|
||||||
NO_CORS: 'no-cors',
|
NO_CORS: 'no-cors',
|
||||||
|
IP_LOCKED: 'ip-locked',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type Flags = (typeof flags)[keyof typeof flags];
|
export type Flags = (typeof flags)[keyof typeof flags];
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Embed, Sourcerer } from '@/providers/base';
|
import { Embed, Sourcerer } from '@/providers/base';
|
||||||
|
import { febBoxScraper } from '@/providers/embeds/febBox';
|
||||||
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
||||||
import { mp4uploadScraper } from '@/providers/embeds/mp4upload';
|
import { mp4uploadScraper } from '@/providers/embeds/mp4upload';
|
||||||
import { streambucketScraper } from '@/providers/embeds/streambucket';
|
import { streambucketScraper } from '@/providers/embeds/streambucket';
|
||||||
@@ -9,11 +10,17 @@ import { vidsrcembedScraper } from '@/providers/embeds/vidsrc';
|
|||||||
import { flixhqScraper } from '@/providers/sources/flixhq/index';
|
import { flixhqScraper } from '@/providers/sources/flixhq/index';
|
||||||
import { goMoviesScraper } from '@/providers/sources/gomovies/index';
|
import { goMoviesScraper } from '@/providers/sources/gomovies/index';
|
||||||
import { kissAsianScraper } from '@/providers/sources/kissasian/index';
|
import { kissAsianScraper } from '@/providers/sources/kissasian/index';
|
||||||
|
import { lookmovieScraper } from '@/providers/sources/lookmovie';
|
||||||
import { remotestreamScraper } from '@/providers/sources/remotestream';
|
import { remotestreamScraper } from '@/providers/sources/remotestream';
|
||||||
import { superStreamScraper } from '@/providers/sources/superstream/index';
|
import { superStreamScraper } from '@/providers/sources/superstream/index';
|
||||||
import { vidsrcScraper } from '@/providers/sources/vidsrc';
|
import { vidsrcScraper } from '@/providers/sources/vidsrc';
|
||||||
import { zoechipScraper } from '@/providers/sources/zoechip';
|
import { zoechipScraper } from '@/providers/sources/zoechip';
|
||||||
|
|
||||||
|
import { smashyStreamDScraper } from './embeds/smashystream/dued';
|
||||||
|
import { smashyStreamFScraper } from './embeds/smashystream/video1';
|
||||||
|
import { showBoxScraper } from './sources/showbox';
|
||||||
|
import { smashyStreamScraper } from './sources/smashystream';
|
||||||
|
|
||||||
export function gatherAllSources(): Array<Sourcerer> {
|
export function gatherAllSources(): Array<Sourcerer> {
|
||||||
// all sources are gathered here
|
// all sources are gathered here
|
||||||
return [
|
return [
|
||||||
@@ -24,6 +31,9 @@ export function gatherAllSources(): Array<Sourcerer> {
|
|||||||
goMoviesScraper,
|
goMoviesScraper,
|
||||||
zoechipScraper,
|
zoechipScraper,
|
||||||
vidsrcScraper,
|
vidsrcScraper,
|
||||||
|
lookmovieScraper,
|
||||||
|
showBoxScraper,
|
||||||
|
smashyStreamScraper,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,5 +47,8 @@ export function gatherAllEmbeds(): Array<Embed> {
|
|||||||
mixdropScraper,
|
mixdropScraper,
|
||||||
vidsrcembedScraper,
|
vidsrcembedScraper,
|
||||||
streambucketScraper,
|
streambucketScraper,
|
||||||
|
febBoxScraper,
|
||||||
|
smashyStreamFScraper,
|
||||||
|
smashyStreamDScraper,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { MovieMedia, ShowMedia } from '@/main/media';
|
|
||||||
import { Flags } from '@/main/targets';
|
import { Flags } from '@/main/targets';
|
||||||
import { Stream } from '@/providers/streams';
|
import { Stream } from '@/providers/streams';
|
||||||
import { EmbedScrapeContext, ScrapeContext } from '@/utils/context';
|
import { EmbedScrapeContext, MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
export type SourcererEmbed = {
|
||||||
|
embedId: string;
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
export type SourcererOutput = {
|
export type SourcererOutput = {
|
||||||
embeds: {
|
embeds: SourcererEmbed[];
|
||||||
embedId: string;
|
|
||||||
url: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
}[];
|
|
||||||
stream?: Stream;
|
stream?: Stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,8 +19,8 @@ export type Sourcerer = {
|
|||||||
rank: number; // the higher the number, the earlier it gets put on the queue
|
rank: number; // the higher the number, the earlier it gets put on the queue
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
flags: Flags[];
|
flags: Flags[];
|
||||||
scrapeMovie?: (input: ScrapeContext & { media: MovieMedia }) => Promise<SourcererOutput>;
|
scrapeMovie?: (input: MovieScrapeContext) => Promise<SourcererOutput>;
|
||||||
scrapeShow?: (input: ScrapeContext & { media: ShowMedia }) => Promise<SourcererOutput>;
|
scrapeShow?: (input: ShowScrapeContext) => Promise<SourcererOutput>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function makeSourcerer(state: Sourcerer): Sourcerer {
|
export function makeSourcerer(state: Sourcerer): Sourcerer {
|
||||||
|
32
src/providers/captions.ts
Normal file
32
src/providers/captions.ts
Normal 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);
|
||||||
|
}
|
74
src/providers/embeds/febBox.ts
Normal file
74
src/providers/embeds/febBox.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { flags } from '@/main/targets';
|
||||||
|
import { makeEmbed } from '@/providers/base';
|
||||||
|
import { StreamFile } from '@/providers/streams';
|
||||||
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
const febBoxBase = `https://www.febbox.com`;
|
||||||
|
|
||||||
|
const allowedQualities = ['360', '480', '720', '1080'];
|
||||||
|
|
||||||
|
export const febBoxScraper = makeEmbed({
|
||||||
|
id: 'febbox',
|
||||||
|
name: 'FebBox',
|
||||||
|
rank: 160,
|
||||||
|
async scrape(ctx) {
|
||||||
|
const shareKey = ctx.url.split('/')[4];
|
||||||
|
const streams = await ctx.proxiedFetcher<{
|
||||||
|
data?: {
|
||||||
|
file_list?: {
|
||||||
|
fid?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}>('/file/file_share_list', {
|
||||||
|
headers: {
|
||||||
|
'accept-language': 'en', // without this header, the request is marked as a webscraper
|
||||||
|
},
|
||||||
|
baseUrl: febBoxBase,
|
||||||
|
query: {
|
||||||
|
share_key: shareKey,
|
||||||
|
pwd: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fid = streams?.data?.file_list?.[0]?.fid;
|
||||||
|
if (!fid) throw new NotFoundError('no result found');
|
||||||
|
|
||||||
|
const formParams = new URLSearchParams();
|
||||||
|
formParams.append('fid', fid);
|
||||||
|
formParams.append('share_key', shareKey);
|
||||||
|
|
||||||
|
const player = await ctx.proxiedFetcher<string>('/file/player', {
|
||||||
|
baseUrl: febBoxBase,
|
||||||
|
body: formParams,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'accept-language': 'en', // without this header, the request is marked as a webscraper
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourcesMatch = player?.match(/var sources = (\[[^\]]+\]);/);
|
||||||
|
const qualities = sourcesMatch ? JSON.parse(sourcesMatch[0].replace('var sources = ', '').replace(';', '')) : null;
|
||||||
|
|
||||||
|
const embedQualities: Record<string, StreamFile> = {};
|
||||||
|
|
||||||
|
qualities.forEach((quality: { file: string; label: string }) => {
|
||||||
|
const normalizedLabel = quality.label.toLowerCase().replace('p', '');
|
||||||
|
if (allowedQualities.includes(normalizedLabel)) {
|
||||||
|
if (!quality.file) return;
|
||||||
|
embedQualities[normalizedLabel] = {
|
||||||
|
type: 'mp4',
|
||||||
|
url: quality.file,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: {
|
||||||
|
type: 'file',
|
||||||
|
captions: [],
|
||||||
|
flags: [flags.NO_CORS],
|
||||||
|
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',
|
||||||
|
68
src/providers/embeds/smashystream/dued.ts
Normal file
68
src/providers/embeds/smashystream/dued.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
|
import { flags } from '@/main/targets';
|
||||||
|
import { makeEmbed } from '@/providers/base';
|
||||||
|
|
||||||
|
type DPlayerSourcesResponse = {
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
file: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export const smashyStreamDScraper = makeEmbed({
|
||||||
|
id: 'smashystream-d',
|
||||||
|
name: 'SmashyStream (D)',
|
||||||
|
rank: 71,
|
||||||
|
async scrape(ctx) {
|
||||||
|
const mainPageRes = await ctx.proxiedFetcher<string>(ctx.url, {
|
||||||
|
headers: {
|
||||||
|
Referer: ctx.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mainPageRes$ = load(mainPageRes);
|
||||||
|
const iframeUrl = mainPageRes$('iframe').attr('src');
|
||||||
|
if (!iframeUrl) throw new Error(`[${this.name}] failed to find iframe url`);
|
||||||
|
const mainUrl = new URL(iframeUrl);
|
||||||
|
const iframeRes = await ctx.proxiedFetcher<string>(iframeUrl, {
|
||||||
|
headers: {
|
||||||
|
Referer: ctx.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const textFilePath = iframeRes.match(/"file":"([^"]+)"/)?.[1];
|
||||||
|
const csrfToken = iframeRes.match(/"key":"([^"]+)"/)?.[1];
|
||||||
|
if (!textFilePath || !csrfToken) throw new Error(`[${this.name}] failed to find text file url or token`);
|
||||||
|
const textFileUrl = `${mainUrl.origin}${textFilePath}`;
|
||||||
|
const textFileRes = await ctx.proxiedFetcher<DPlayerSourcesResponse>(textFileUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-TOKEN': csrfToken,
|
||||||
|
Referer: iframeUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Playlists in Hindi, English, Tamil and Telugu are available. We only get the english one.
|
||||||
|
const textFilePlaylist = textFileRes.find((x) => x.title === 'English')?.file;
|
||||||
|
if (!textFilePlaylist) throw new Error(`[${this.name}] failed to find an english playlist`);
|
||||||
|
|
||||||
|
const playlistRes = await ctx.proxiedFetcher<string>(
|
||||||
|
`${mainUrl.origin}/playlist/${textFilePlaylist.slice(1)}.txt`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-TOKEN': csrfToken,
|
||||||
|
Referer: iframeUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: {
|
||||||
|
playlist: playlistRes,
|
||||||
|
type: 'hls',
|
||||||
|
flags: [flags.NO_CORS],
|
||||||
|
captions: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
53
src/providers/embeds/smashystream/video1.ts
Normal file
53
src/providers/embeds/smashystream/video1.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { flags } from '@/main/targets';
|
||||||
|
import { makeEmbed } from '@/providers/base';
|
||||||
|
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
|
||||||
|
|
||||||
|
type FPlayerResponse = {
|
||||||
|
sourceUrls: string[];
|
||||||
|
subtitleUrls: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const smashyStreamFScraper = makeEmbed({
|
||||||
|
id: 'smashystream-f',
|
||||||
|
name: 'SmashyStream (F)',
|
||||||
|
rank: 70,
|
||||||
|
async scrape(ctx) {
|
||||||
|
const res = await ctx.proxiedFetcher<FPlayerResponse>(ctx.url, {
|
||||||
|
headers: {
|
||||||
|
Referer: ctx.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const captions: Caption[] =
|
||||||
|
res.subtitleUrls
|
||||||
|
.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g)
|
||||||
|
?.map<Caption | null>((entry: string) => {
|
||||||
|
const match = entry.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/);
|
||||||
|
if (match) {
|
||||||
|
const [, language, url] = match;
|
||||||
|
if (language && url) {
|
||||||
|
const languageCode = labelToLanguageCode(language);
|
||||||
|
const captionType = getCaptionTypeFromUrl(url);
|
||||||
|
if (!languageCode || !captionType) return null;
|
||||||
|
return {
|
||||||
|
url: url.replace(',', ''),
|
||||||
|
language: languageCode,
|
||||||
|
type: captionType,
|
||||||
|
hasCorsRestrictions: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((x): x is Caption => x !== null) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: {
|
||||||
|
playlist: res.sourceUrls[0],
|
||||||
|
type: 'hls',
|
||||||
|
flags: [flags.NO_CORS],
|
||||||
|
captions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
@@ -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;
|
||||||
|
|
||||||
@@ -24,6 +25,34 @@ function isJSON(json: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
example script segment:
|
||||||
|
switch(I9){case 0x0:II=X,IM=t;break;case 0x1:II=b,IM=D;break;case 0x2:II=x,IM=f;break;case 0x3:II=S,IM=j;break;case 0x4:II=U,IM=G;break;case 0x5:II=partKeyStartPosition_5,IM=partKeyLength_5;}
|
||||||
|
*/
|
||||||
|
function extractKey(script: string): [number, number][] | null {
|
||||||
|
const startOfSwitch = script.lastIndexOf('switch');
|
||||||
|
const endOfCases = script.indexOf('partKeyStartPosition');
|
||||||
|
const switchBody = script.slice(startOfSwitch, endOfCases);
|
||||||
|
|
||||||
|
const nums: [number, number][] = [];
|
||||||
|
const matches = switchBody.matchAll(/:[a-zA-Z0-9]+=([a-zA-Z0-9]+),[a-zA-Z0-9]+=([a-zA-Z0-9]+);/g);
|
||||||
|
for (const match of matches) {
|
||||||
|
const innerNumbers: number[] = [];
|
||||||
|
for (const varMatch of [match[1], match[2]]) {
|
||||||
|
const regex = new RegExp(`${varMatch}=0x([a-zA-Z0-9]+)`, 'g');
|
||||||
|
const varMatches = [...script.matchAll(regex)];
|
||||||
|
const lastMatch = varMatches[varMatches.length - 1];
|
||||||
|
if (!lastMatch) return null;
|
||||||
|
const number = parseInt(lastMatch[1], 16);
|
||||||
|
innerNumbers.push(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
nums.push([innerNumbers[0], innerNumbers[1]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nums;
|
||||||
|
}
|
||||||
|
|
||||||
export const upcloudScraper = makeEmbed({
|
export const upcloudScraper = makeEmbed({
|
||||||
id: 'upcloud',
|
id: 'upcloud',
|
||||||
name: 'UpCloud',
|
name: 'UpCloud',
|
||||||
@@ -45,20 +74,27 @@ export const upcloudScraper = makeEmbed({
|
|||||||
let sources: { file: string; type: string } | null = null;
|
let sources: { file: string; type: string } | null = null;
|
||||||
|
|
||||||
if (!isJSON(streamRes.sources)) {
|
if (!isJSON(streamRes.sources)) {
|
||||||
const decryptionKey = JSON.parse(
|
const scriptJs = await ctx.proxiedFetcher<string>(`https://rabbitstream.net/js/player/prod/e4-player.min.js`, {
|
||||||
await ctx.proxiedFetcher<string>(`https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt`),
|
query: {
|
||||||
) as [number, number][];
|
// browser side caching on this endpoint is quite extreme. Add version query paramter to circumvent any caching
|
||||||
|
v: Date.now().toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const decryptionKey = extractKey(scriptJs);
|
||||||
|
if (!decryptionKey) throw new Error('Key extraction failed');
|
||||||
|
|
||||||
let extractedKey = '';
|
let extractedKey = '';
|
||||||
const sourcesArray = streamRes.sources.split('');
|
let strippedSources = streamRes.sources;
|
||||||
for (const index of decryptionKey) {
|
let totalledOffset = 0;
|
||||||
for (let i: number = index[0]; i < index[1]; i += 1) {
|
decryptionKey.forEach(([a, b]) => {
|
||||||
extractedKey += streamRes.sources[i];
|
const start = a + totalledOffset;
|
||||||
sourcesArray[i] = '';
|
const end = start + b;
|
||||||
}
|
extractedKey += streamRes.sources.slice(start, end);
|
||||||
}
|
strippedSources = strippedSources.replace(streamRes.sources.substring(start, end), '');
|
||||||
|
totalledOffset += b;
|
||||||
|
});
|
||||||
|
|
||||||
const decryptedStream = AES.decrypt(sourcesArray.join(''), extractedKey).toString(enc.Utf8);
|
const decryptedStream = AES.decrypt(strippedSources, extractedKey).toString(enc.Utf8);
|
||||||
const parsedStream = JSON.parse(decryptedStream)[0];
|
const parsedStream = JSON.parse(decryptedStream)[0];
|
||||||
if (!parsedStream) throw new Error('No stream found');
|
if (!parsedStream) throw new Error('No stream found');
|
||||||
sources = parsedStream;
|
sources = parsedStream;
|
||||||
@@ -66,11 +102,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: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
import { flags } from '@/main/targets';
|
import { flags } from '@/main/targets';
|
||||||
import { makeSourcerer } from '@/providers/base';
|
import { makeSourcerer } from '@/providers/base';
|
||||||
import { upcloudScraper } from '@/providers/embeds/upcloud';
|
import { upcloudScraper } from '@/providers/embeds/upcloud';
|
||||||
import { getFlixhqSourceDetails, getFlixhqSources } from '@/providers/sources/flixhq/scrape';
|
import { getFlixhqMovieSources, getFlixhqShowSources, getFlixhqSourceDetails } from '@/providers/sources/flixhq/scrape';
|
||||||
import { getFlixhqId } from '@/providers/sources/flixhq/search';
|
import { getFlixhqId } from '@/providers/sources/flixhq/search';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
// TODO tv shows are available in flixHQ, just no scraper yet
|
|
||||||
export const flixhqScraper = makeSourcerer({
|
export const flixhqScraper = makeSourcerer({
|
||||||
id: 'flixhq',
|
id: 'flixhq',
|
||||||
name: 'FlixHQ',
|
name: 'FlixHQ',
|
||||||
@@ -15,10 +14,27 @@ export const flixhqScraper = makeSourcerer({
|
|||||||
const id = await getFlixhqId(ctx, ctx.media);
|
const id = await getFlixhqId(ctx, ctx.media);
|
||||||
if (!id) throw new NotFoundError('no search results match');
|
if (!id) throw new NotFoundError('no search results match');
|
||||||
|
|
||||||
const sources = await getFlixhqSources(ctx, id);
|
const sources = await getFlixhqMovieSources(ctx, ctx.media, id);
|
||||||
const upcloudStream = sources.find((v) => v.embed.toLowerCase() === 'upcloud');
|
const upcloudStream = sources.find((v) => v.embed.toLowerCase() === 'upcloud');
|
||||||
if (!upcloudStream) throw new NotFoundError('upcloud stream not found for flixhq');
|
if (!upcloudStream) throw new NotFoundError('upcloud stream not found for flixhq');
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
embedId: upcloudScraper.id,
|
||||||
|
url: await getFlixhqSourceDetails(ctx, upcloudStream.episodeId),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async scrapeShow(ctx) {
|
||||||
|
const id = await getFlixhqId(ctx, ctx.media);
|
||||||
|
if (!id) throw new NotFoundError('no search results match');
|
||||||
|
|
||||||
|
const sources = await getFlixhqShowSources(ctx, ctx.media, id);
|
||||||
|
const upcloudStream = sources.find((v) => v.embed.toLowerCase() === 'server upcloud');
|
||||||
|
if (!upcloudStream) throw new NotFoundError('upcloud stream not found for flixhq');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
|
@@ -1,16 +1,26 @@
|
|||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
|
import { MovieMedia, ShowMedia } from '@/main/media';
|
||||||
import { flixHqBase } from '@/providers/sources/flixhq/common';
|
import { flixHqBase } from '@/providers/sources/flixhq/common';
|
||||||
import { ScrapeContext } from '@/utils/context';
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
export async function getFlixhqSources(ctx: ScrapeContext, id: string) {
|
export async function getFlixhqSourceDetails(ctx: ScrapeContext, sourceId: string): Promise<string> {
|
||||||
const type = id.split('/')[0];
|
const jsonData = await ctx.proxiedFetcher<Record<string, any>>(`/ajax/sources/${sourceId}`, {
|
||||||
|
baseUrl: flixHqBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsonData.link;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFlixhqMovieSources(ctx: ScrapeContext, media: MovieMedia, id: string) {
|
||||||
const episodeParts = id.split('-');
|
const episodeParts = id.split('-');
|
||||||
const episodeId = episodeParts[episodeParts.length - 1];
|
const episodeId = episodeParts[episodeParts.length - 1];
|
||||||
|
|
||||||
const data = await ctx.proxiedFetcher<string>(`/ajax/${type}/episodes/${episodeId}`, {
|
const data = await ctx.proxiedFetcher<string>(`/ajax/movie/episodes/${episodeId}`, {
|
||||||
baseUrl: flixHqBase,
|
baseUrl: flixHqBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
const doc = load(data);
|
const doc = load(data);
|
||||||
const sourceLinks = doc('.nav-item > a')
|
const sourceLinks = doc('.nav-item > a')
|
||||||
.toArray()
|
.toArray()
|
||||||
@@ -28,10 +38,55 @@ export async function getFlixhqSources(ctx: ScrapeContext, id: string) {
|
|||||||
return sourceLinks;
|
return sourceLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFlixhqSourceDetails(ctx: ScrapeContext, sourceId: string): Promise<string> {
|
export async function getFlixhqShowSources(ctx: ScrapeContext, media: ShowMedia, id: string) {
|
||||||
const jsonData = await ctx.proxiedFetcher<Record<string, any>>(`/ajax/sources/${sourceId}`, {
|
const episodeParts = id.split('-');
|
||||||
|
const episodeId = episodeParts[episodeParts.length - 1];
|
||||||
|
|
||||||
|
const seasonsListData = await ctx.proxiedFetcher<string>(`/ajax/season/list/${episodeId}`, {
|
||||||
baseUrl: flixHqBase,
|
baseUrl: flixHqBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonData.link;
|
const seasonsDoc = load(seasonsListData);
|
||||||
|
const season = seasonsDoc('.dropdown-item')
|
||||||
|
.toArray()
|
||||||
|
.find((el) => seasonsDoc(el).text() === `Season ${media.season.number}`)?.attribs['data-id'];
|
||||||
|
|
||||||
|
if (!season) throw new NotFoundError('season not found');
|
||||||
|
|
||||||
|
const seasonData = await ctx.proxiedFetcher<string>(`/ajax/season/episodes/${season}`, {
|
||||||
|
baseUrl: flixHqBase,
|
||||||
|
});
|
||||||
|
const seasonDoc = load(seasonData);
|
||||||
|
const episode = seasonDoc('.nav-item > a')
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
return {
|
||||||
|
id: seasonDoc(el).attr('data-id'),
|
||||||
|
title: seasonDoc(el).attr('title'),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.find((e) => e.title?.startsWith(`Eps ${media.episode.number}`))?.id;
|
||||||
|
|
||||||
|
if (!episode) throw new NotFoundError('episode not found');
|
||||||
|
|
||||||
|
const data = await ctx.proxiedFetcher<string>(`/ajax/episode/servers/${episode}`, {
|
||||||
|
baseUrl: flixHqBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
const doc = load(data);
|
||||||
|
|
||||||
|
const sourceLinks = doc('.nav-item > a')
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
const query = doc(el);
|
||||||
|
const embedTitle = query.attr('title');
|
||||||
|
const linkId = query.attr('data-id');
|
||||||
|
if (!embedTitle || !linkId) throw new Error('invalid sources');
|
||||||
|
return {
|
||||||
|
embed: embedTitle,
|
||||||
|
episodeId: linkId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return sourceLinks;
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
import { MovieMedia } from '@/main/media';
|
import { MovieMedia, ShowMedia } from '@/main/media';
|
||||||
import { flixHqBase } from '@/providers/sources/flixhq/common';
|
import { flixHqBase } from '@/providers/sources/flixhq/common';
|
||||||
import { compareMedia } from '@/utils/compare';
|
import { compareMedia, compareTitle } from '@/utils/compare';
|
||||||
import { ScrapeContext } from '@/utils/context';
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
export async function getFlixhqId(ctx: ScrapeContext, media: MovieMedia): Promise<string | null> {
|
export async function getFlixhqId(ctx: ScrapeContext, media: MovieMedia | ShowMedia): Promise<string | null> {
|
||||||
const searchResults = await ctx.proxiedFetcher<string>(`/search/${media.title.replaceAll(/[^a-z0-9A-Z]/g, '-')}`, {
|
const searchResults = await ctx.proxiedFetcher<string>(`/search/${media.title.replaceAll(/[^a-z0-9A-Z]/g, '-')}`, {
|
||||||
baseUrl: flixHqBase,
|
baseUrl: flixHqBase,
|
||||||
});
|
});
|
||||||
@@ -18,16 +18,26 @@ export async function getFlixhqId(ctx: ScrapeContext, media: MovieMedia): Promis
|
|||||||
const id = query.find('div.film-poster > a').attr('href')?.slice(1);
|
const id = query.find('div.film-poster > a').attr('href')?.slice(1);
|
||||||
const title = query.find('div.film-detail > h2 > a').attr('title');
|
const title = query.find('div.film-detail > h2 > a').attr('title');
|
||||||
const year = query.find('div.film-detail > div.fd-infor > span:nth-child(1)').text();
|
const year = query.find('div.film-detail > div.fd-infor > span:nth-child(1)').text();
|
||||||
|
const seasons = year.includes('SS') ? year.split('SS')[1] : '0';
|
||||||
|
|
||||||
if (!id || !title || !year) return null;
|
if (!id || !title || !year) return null;
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
year: +year,
|
year: parseInt(year, 10),
|
||||||
|
seasons: parseInt(seasons, 10),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const matchingItem = items.find((v) => v && compareMedia(media, v.title, v.year));
|
const matchingItem = items.find((v) => {
|
||||||
|
if (!v) return false;
|
||||||
|
|
||||||
|
if (media.type === 'movie') {
|
||||||
|
return compareMedia(media, v.title, v.year);
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareTitle(media.title, v.title) && media.season.number < v.seasons + 1;
|
||||||
|
});
|
||||||
|
|
||||||
if (!matchingItem) return null;
|
if (!matchingItem) return null;
|
||||||
return matchingItem.id;
|
return matchingItem.id;
|
||||||
|
@@ -12,14 +12,12 @@ export const gomoviesBase = `https://gomovies.sx`;
|
|||||||
export const goMoviesScraper = makeSourcerer({
|
export const goMoviesScraper = makeSourcerer({
|
||||||
id: 'gomovies',
|
id: 'gomovies',
|
||||||
name: 'GOmovies',
|
name: 'GOmovies',
|
||||||
rank: 200,
|
rank: 110,
|
||||||
flags: [flags.NO_CORS],
|
flags: [flags.NO_CORS],
|
||||||
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',
|
||||||
},
|
},
|
||||||
|
36
src/providers/sources/lookmovie/index.ts
Normal file
36
src/providers/sources/lookmovie/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { flags } from '@/main/targets';
|
||||||
|
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
import { scrape, searchAndFindMedia } from './util';
|
||||||
|
|
||||||
|
async function universalScraper(ctx: MovieScrapeContext | ShowScrapeContext): Promise<SourcererOutput> {
|
||||||
|
const lookmovieData = await searchAndFindMedia(ctx, ctx.media);
|
||||||
|
if (!lookmovieData) throw new NotFoundError('Media not found');
|
||||||
|
|
||||||
|
ctx.progress(30);
|
||||||
|
const videoUrl = await scrape(ctx, ctx.media, lookmovieData);
|
||||||
|
if (!videoUrl) throw new NotFoundError('No video found');
|
||||||
|
|
||||||
|
ctx.progress(60);
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds: [],
|
||||||
|
stream: {
|
||||||
|
playlist: videoUrl,
|
||||||
|
type: 'hls',
|
||||||
|
flags: [flags.IP_LOCKED],
|
||||||
|
captions: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lookmovieScraper = makeSourcerer({
|
||||||
|
id: 'lookmovie',
|
||||||
|
name: 'LookMovie',
|
||||||
|
rank: 1,
|
||||||
|
flags: [flags.IP_LOCKED],
|
||||||
|
scrapeShow: universalScraper,
|
||||||
|
scrapeMovie: universalScraper,
|
||||||
|
});
|
60
src/providers/sources/lookmovie/type.ts
Normal file
60
src/providers/sources/lookmovie/type.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { MovieMedia } from '@/main/media';
|
||||||
|
|
||||||
|
// ! Types
|
||||||
|
interface BaseConfig {
|
||||||
|
/** The website's slug. Formatted as `1839578-person-of-interest-2011` */
|
||||||
|
slug: string;
|
||||||
|
/** Type of request */
|
||||||
|
type: 'show' | 'movie';
|
||||||
|
/** Hash */
|
||||||
|
hash: string;
|
||||||
|
/** Hash expiry */
|
||||||
|
expires: number;
|
||||||
|
}
|
||||||
|
interface TvConfig extends BaseConfig {
|
||||||
|
/** Type of request */
|
||||||
|
type: 'show';
|
||||||
|
/** The episode ID for a TV show. Given in search and URL */
|
||||||
|
episodeId: string;
|
||||||
|
}
|
||||||
|
interface MovieConfig extends BaseConfig {
|
||||||
|
/** Type of request */
|
||||||
|
type: 'movie';
|
||||||
|
/** Movie's id */
|
||||||
|
id_movie: string;
|
||||||
|
}
|
||||||
|
export type Config = MovieConfig | TvConfig;
|
||||||
|
|
||||||
|
export interface episodeObj {
|
||||||
|
season: string;
|
||||||
|
episode: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShowDataResult {
|
||||||
|
episodes: episodeObj[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoSources {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamsDataResult {
|
||||||
|
streams: VideoSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResultItem {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
year: string;
|
||||||
|
id_movie: string;
|
||||||
|
id_show: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
title(media: MovieMedia, title: any, arg2: number): boolean;
|
||||||
|
year(year: any): number | undefined;
|
||||||
|
id_movie: any;
|
||||||
|
id_show: string;
|
||||||
|
items: ResultItem[];
|
||||||
|
}
|
59
src/providers/sources/lookmovie/util.ts
Normal file
59
src/providers/sources/lookmovie/util.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { MovieMedia, ShowMedia } from '@/main/media';
|
||||||
|
import { compareMedia } from '@/utils/compare';
|
||||||
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
import { Result, ResultItem, ShowDataResult, episodeObj } from './type';
|
||||||
|
import { getVideoUrl } from './video';
|
||||||
|
|
||||||
|
export async function searchAndFindMedia(
|
||||||
|
ctx: ScrapeContext,
|
||||||
|
media: MovieMedia | ShowMedia,
|
||||||
|
): Promise<ResultItem | undefined> {
|
||||||
|
if (media.type === 'show') {
|
||||||
|
const searchRes = await ctx.fetcher<Result>(`/v1/shows`, {
|
||||||
|
baseUrl: 'https://lmscript.xyz',
|
||||||
|
query: { 'filters[q]': media.title },
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = searchRes.items;
|
||||||
|
|
||||||
|
const result = results.find((res: ResultItem) => compareMedia(media, res.title, Number(res.year)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (media.type === 'movie') {
|
||||||
|
const searchRes = await ctx.fetcher<Result>(`/v1/movies`, {
|
||||||
|
baseUrl: 'https://lmscript.xyz',
|
||||||
|
query: { 'filters[q]': media.title },
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = searchRes.items;
|
||||||
|
const result = results.find((res: ResultItem) => compareMedia(media, res.title, Number(res.year)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrape(ctx: ScrapeContext, media: MovieMedia | ShowMedia, result: ResultItem) {
|
||||||
|
// Find the relevant id
|
||||||
|
let id = null;
|
||||||
|
if (media.type === 'movie') {
|
||||||
|
id = result.id_movie;
|
||||||
|
} else if (media.type === 'show') {
|
||||||
|
const data = await ctx.fetcher<ShowDataResult>(`/v1/shows`, {
|
||||||
|
baseUrl: 'https://lmscript.xyz',
|
||||||
|
query: { expand: 'episodes', id: result.id_show },
|
||||||
|
});
|
||||||
|
|
||||||
|
const episode = data.episodes?.find((v: episodeObj) => {
|
||||||
|
return Number(v.season) === Number(media.season.number) && Number(v.episode) === Number(media.episode.number);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (episode) id = episode.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ID
|
||||||
|
if (id === null) throw new NotFoundError('Not found');
|
||||||
|
|
||||||
|
const videoUrl = await getVideoUrl(ctx, id, media);
|
||||||
|
return videoUrl;
|
||||||
|
}
|
46
src/providers/sources/lookmovie/video.ts
Normal file
46
src/providers/sources/lookmovie/video.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { MovieMedia, ShowMedia } from '@/main/media';
|
||||||
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
import { StreamsDataResult } from './type';
|
||||||
|
|
||||||
|
export async function getVideoSources(
|
||||||
|
ctx: ScrapeContext,
|
||||||
|
id: string,
|
||||||
|
media: MovieMedia | ShowMedia,
|
||||||
|
): Promise<StreamsDataResult> {
|
||||||
|
// Fetch video sources
|
||||||
|
|
||||||
|
let path = '';
|
||||||
|
if (media.type === 'show') {
|
||||||
|
path = `/v1/episodes/view`;
|
||||||
|
} else if (media.type === 'movie') {
|
||||||
|
path = `/v1/movies/view`;
|
||||||
|
}
|
||||||
|
const data = await ctx.fetcher<StreamsDataResult>(path, {
|
||||||
|
baseUrl: 'https://lmscript.xyz',
|
||||||
|
query: { expand: 'streams', id },
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVideoUrl(
|
||||||
|
ctx: ScrapeContext,
|
||||||
|
id: string,
|
||||||
|
media: MovieMedia | ShowMedia,
|
||||||
|
): Promise<string | null> {
|
||||||
|
// Get sources
|
||||||
|
const data = await getVideoSources(ctx, id, media);
|
||||||
|
const videoSources = data.streams;
|
||||||
|
|
||||||
|
// Find video URL and return it
|
||||||
|
const opts = ['auto', '1080p', '1080', '720p', '720', '480p', '480', '240p', '240', '360p', '360', '144', '144p'];
|
||||||
|
|
||||||
|
let videoUrl: string | null = null;
|
||||||
|
for (const res of opts) {
|
||||||
|
if (videoSources[res] && !videoUrl) {
|
||||||
|
videoUrl = videoSources[res];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoUrl;
|
||||||
|
}
|
@@ -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],
|
||||||
|
64
src/providers/sources/showbox/index.ts
Normal file
64
src/providers/sources/showbox/index.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
|
import { flags } from '@/main/targets';
|
||||||
|
import { makeSourcerer } from '@/providers/base';
|
||||||
|
import { febBoxScraper } from '@/providers/embeds/febBox';
|
||||||
|
import { compareMedia } from '@/utils/compare';
|
||||||
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
const showboxBase = `https://www.showbox.media`;
|
||||||
|
|
||||||
|
export const showBoxScraper = makeSourcerer({
|
||||||
|
id: 'show_box',
|
||||||
|
name: 'ShowBox',
|
||||||
|
rank: 20,
|
||||||
|
disabled: true,
|
||||||
|
flags: [flags.NO_CORS],
|
||||||
|
async scrapeMovie(ctx) {
|
||||||
|
const search = await ctx.proxiedFetcher<string>('/search', {
|
||||||
|
baseUrl: showboxBase,
|
||||||
|
query: {
|
||||||
|
keyword: ctx.media.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchPage = load(search);
|
||||||
|
const result = searchPage('.film-name > a')
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
const titleContainer = el.parent?.parent;
|
||||||
|
if (!titleContainer) return;
|
||||||
|
const year = searchPage(titleContainer).find('.fdi-item').first().text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: el.attribs.title,
|
||||||
|
path: el.attribs.href,
|
||||||
|
year: !year.includes('SS') ? parseInt(year, 10) : undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.find((v) => v && compareMedia(ctx.media, v.title, v.year ? v.year : undefined));
|
||||||
|
|
||||||
|
if (!result?.path) throw new NotFoundError('no result found');
|
||||||
|
|
||||||
|
const febboxResult = await ctx.proxiedFetcher<{
|
||||||
|
data?: { link?: string };
|
||||||
|
}>('/index/share_link', {
|
||||||
|
baseUrl: showboxBase,
|
||||||
|
query: {
|
||||||
|
id: result.path.split('/')[3],
|
||||||
|
type: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!febboxResult?.data?.link) throw new NotFoundError('no result found');
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
embedId: febBoxScraper.id,
|
||||||
|
url: febboxResult.data.link,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
64
src/providers/sources/smashystream/index.ts
Normal file
64
src/providers/sources/smashystream/index.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
|
import { flags } from '@/main/targets';
|
||||||
|
import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
|
import { smashyStreamDScraper } from '@/providers/embeds/smashystream/dued';
|
||||||
|
import { smashyStreamFScraper } from '@/providers/embeds/smashystream/video1';
|
||||||
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
const smashyStreamBase = 'https://embed.smashystream.com';
|
||||||
|
const referer = 'https://smashystream.com/';
|
||||||
|
|
||||||
|
const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> => {
|
||||||
|
const mainPage = await ctx.proxiedFetcher<string>('/playere.php', {
|
||||||
|
query: {
|
||||||
|
tmdb: ctx.media.tmdbId,
|
||||||
|
...(ctx.media.type === 'show' && {
|
||||||
|
season: ctx.media.season.number.toString(),
|
||||||
|
episode: ctx.media.episode.number.toString(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Referer: referer,
|
||||||
|
},
|
||||||
|
baseUrl: smashyStreamBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.progress(30);
|
||||||
|
|
||||||
|
const mainPage$ = load(mainPage);
|
||||||
|
const sourceUrls = mainPage$('.dropdown-menu a[data-url]')
|
||||||
|
.map((_, el) => mainPage$(el).attr('data-url'))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const embeds: SourcererEmbed[] = [];
|
||||||
|
for (const sourceUrl of sourceUrls) {
|
||||||
|
if (sourceUrl.includes('video1d.php')) {
|
||||||
|
embeds.push({
|
||||||
|
embedId: smashyStreamFScraper.id,
|
||||||
|
url: sourceUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sourceUrl.includes('dued.php')) {
|
||||||
|
embeds.push({
|
||||||
|
embedId: smashyStreamDScraper.id,
|
||||||
|
url: sourceUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.progress(60);
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const smashyStreamScraper = makeSourcerer({
|
||||||
|
id: 'smashystream',
|
||||||
|
name: 'SmashyStream',
|
||||||
|
rank: 70,
|
||||||
|
flags: [flags.NO_CORS],
|
||||||
|
scrapeMovie: universalScraper,
|
||||||
|
scrapeShow: universalScraper,
|
||||||
|
});
|
@@ -11,3 +11,4 @@ export const apiUrls = [
|
|||||||
];
|
];
|
||||||
export const appKey = atob('bW92aWVib3g=');
|
export const appKey = atob('bW92aWVib3g=');
|
||||||
export const appId = atob('Y29tLnRkby5zaG93Ym94');
|
export const appId = atob('Y29tLnRkby5zaG93Ym94');
|
||||||
|
export const captionsDomains = [atob('bWJwaW1hZ2VzLmNodWF4aW4uY29t'), atob('aW1hZ2VzLnNoZWd1Lm5ldA==')];
|
||||||
|
@@ -3,24 +3,24 @@ import { ScrapeContext } from '@/utils/context';
|
|||||||
|
|
||||||
import { sendRequest } from './sendRequest';
|
import { sendRequest } from './sendRequest';
|
||||||
|
|
||||||
import { allowedQualities } from '.';
|
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);
|
||||||
|
|
||||||
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> = {};
|
||||||
|
|
||||||
allowedQualities.forEach((quality) => {
|
allowedQualities.forEach((quality) => {
|
||||||
const foundQuality = qualityMap.find((q) => q.quality === quality);
|
const foundQuality = qualityMap.find((q) => q.quality === quality);
|
||||||
if (foundQuality) {
|
if (foundQuality && foundQuality.url) {
|
||||||
qualities[quality] = {
|
qualities[quality] = {
|
||||||
type: 'mp4',
|
type: 'mp4',
|
||||||
url: foundQuality.url,
|
url: foundQuality.url,
|
||||||
@@ -28,5 +28,8 @@ export async function getStreamQualities(ctx: ScrapeContext, apiQuery: object) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return qualities;
|
return {
|
||||||
|
qualities,
|
||||||
|
fid: mediaRes.list[0]?.fid,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,12 @@
|
|||||||
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';
|
||||||
|
|
||||||
import { getStreamQualities } from './getStreamQualities';
|
import { getStreamQualities } from './getStreamQualities';
|
||||||
import { sendRequest } from './sendRequest';
|
import { sendRequest } from './sendRequest';
|
||||||
|
|
||||||
export const allowedQualities = ['360', '480', '720', '1080'];
|
|
||||||
|
|
||||||
export const superStreamScraper = makeSourcerer({
|
export const superStreamScraper = makeSourcerer({
|
||||||
id: 'superstream',
|
id: 'superstream',
|
||||||
name: 'Superstream',
|
name: 'Superstream',
|
||||||
@@ -15,14 +14,14 @@ export const superStreamScraper = makeSourcerer({
|
|||||||
flags: [flags.NO_CORS],
|
flags: [flags.NO_CORS],
|
||||||
async scrapeShow(ctx) {
|
async scrapeShow(ctx) {
|
||||||
const searchQuery = {
|
const searchQuery = {
|
||||||
module: 'Search3',
|
module: 'Search4',
|
||||||
page: '1',
|
page: '1',
|
||||||
type: 'all',
|
type: 'all',
|
||||||
keyword: ctx.media.title,
|
keyword: ctx.media.title,
|
||||||
pagelimit: '20',
|
pagelimit: '20',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchRes = (await sendRequest(ctx, searchQuery, true)).data;
|
const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list;
|
||||||
ctx.progress(33);
|
ctx.progress(33);
|
||||||
|
|
||||||
const superstreamEntry = searchRes.find(
|
const superstreamEntry = searchRes.find(
|
||||||
@@ -43,11 +42,20 @@ export const superStreamScraper = makeSourcerer({
|
|||||||
group: '',
|
group: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const qualities = await getStreamQualities(ctx, apiQuery);
|
const { qualities, fid } = await getStreamQualities(ctx, apiQuery);
|
||||||
|
if (fid === undefined) throw new NotFoundError('No streamable file found');
|
||||||
|
|
||||||
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],
|
||||||
@@ -56,14 +64,14 @@ export const superStreamScraper = makeSourcerer({
|
|||||||
},
|
},
|
||||||
async scrapeMovie(ctx) {
|
async scrapeMovie(ctx) {
|
||||||
const searchQuery = {
|
const searchQuery = {
|
||||||
module: 'Search3',
|
module: 'Search4',
|
||||||
page: '1',
|
page: '1',
|
||||||
type: 'all',
|
type: 'all',
|
||||||
keyword: ctx.media.title,
|
keyword: ctx.media.title,
|
||||||
pagelimit: '20',
|
pagelimit: '20',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchRes = (await sendRequest(ctx, searchQuery, true)).data;
|
const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list;
|
||||||
ctx.progress(33);
|
ctx.progress(33);
|
||||||
|
|
||||||
const superstreamEntry = searchRes.find(
|
const superstreamEntry = searchRes.find(
|
||||||
@@ -82,11 +90,13 @@ export const superStreamScraper = makeSourcerer({
|
|||||||
group: '',
|
group: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const qualities = await getStreamQualities(ctx, apiQuery);
|
const { qualities, fid } = await getStreamQualities(ctx, apiQuery);
|
||||||
|
if (fid === undefined) throw new NotFoundError('No streamable file found');
|
||||||
|
|
||||||
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],
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import randomBytes from 'randombytes';
|
import { customAlphabet } from 'nanoid';
|
||||||
|
|
||||||
import type { ScrapeContext } from '@/utils/context';
|
import type { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
import { apiUrls, appId, appKey, key } from './common';
|
import { apiUrls, appId, appKey, key } from './common';
|
||||||
import { encrypt, getVerify } from './crypto';
|
import { encrypt, getVerify } from './crypto';
|
||||||
|
|
||||||
|
const randomId = customAlphabet('1234567890abcdef');
|
||||||
const expiry = () => Math.floor(Date.now() / 1000 + 60 * 60 * 12);
|
const expiry = () => Math.floor(Date.now() / 1000 + 60 * 60 * 12);
|
||||||
|
|
||||||
export const sendRequest = async (ctx: ScrapeContext, data: object, altApi = false) => {
|
export const sendRequest = async (ctx: ScrapeContext, data: object, altApi = false) => {
|
||||||
@@ -39,7 +40,7 @@ export const sendRequest = async (ctx: ScrapeContext, data: object, altApi = fal
|
|||||||
formatted.append('platform', 'android');
|
formatted.append('platform', 'android');
|
||||||
formatted.append('version', '129');
|
formatted.append('version', '129');
|
||||||
formatted.append('medium', 'Website');
|
formatted.append('medium', 'Website');
|
||||||
formatted.append('token', randomBytes(16).toString('hex'));
|
formatted.append('token', randomId(32));
|
||||||
|
|
||||||
const requestUrl = altApi ? apiUrls[1] : apiUrls[0];
|
const requestUrl = altApi ? apiUrls[1] : apiUrls[0];
|
||||||
|
|
||||||
|
66
src/providers/sources/superstream/subtitles.ts
Normal file
66
src/providers/sources/superstream/subtitles.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Caption, getCaptionTypeFromUrl, isValidLanguageCode } from '@/providers/captions';
|
||||||
|
import { sendRequest } from '@/providers/sources/superstream/sendRequest';
|
||||||
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
import { captionsDomains } from './common';
|
||||||
|
|
||||||
|
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: type === 'movie' ? id : undefined,
|
||||||
|
tid: type !== 'movie' ? id : undefined,
|
||||||
|
episode: episodeId?.toString(),
|
||||||
|
season: seasonId?.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const subResult = (await sendRequest(ctx, subtitleApiQuery)) as CaptionApiResponse;
|
||||||
|
const subtitleList = subResult.data.list;
|
||||||
|
const output: Caption[] = [];
|
||||||
|
|
||||||
|
subtitleList.forEach((sub) => {
|
||||||
|
const subtitle = sub.subtitles.sort((a, b) => b.order - a.order)[0];
|
||||||
|
if (!subtitle) return;
|
||||||
|
const subtitleFilePath = subtitle.file_path
|
||||||
|
.replace(captionsDomains[0], captionsDomains[1])
|
||||||
|
.replace(/\s/g, '+')
|
||||||
|
.replace(/[()]/g, (c) => {
|
||||||
|
return `%${c.charCodeAt(0).toString(16)}`;
|
||||||
|
});
|
||||||
|
const subtitleType = getCaptionTypeFromUrl(subtitleFilePath);
|
||||||
|
if (!subtitleType) return;
|
||||||
|
|
||||||
|
const validCode = isValidLanguageCode(subtitle.lang);
|
||||||
|
if (!validCode) return;
|
||||||
|
|
||||||
|
output.push({
|
||||||
|
language: subtitle.lang,
|
||||||
|
hasCorsRestrictions: true,
|
||||||
|
type: subtitleType,
|
||||||
|
url: subtitleFilePath,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
@@ -1,20 +1,11 @@
|
|||||||
import { MovieMedia, ShowMedia } from '@/main/media';
|
|
||||||
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
||||||
import { upcloudScraper } from '@/providers/embeds/upcloud';
|
import { upcloudScraper } from '@/providers/embeds/upcloud';
|
||||||
import { upstreamScraper } from '@/providers/embeds/upstream';
|
import { upstreamScraper } from '@/providers/embeds/upstream';
|
||||||
import { getZoeChipSourceURL, getZoeChipSources } from '@/providers/sources/zoechip/scrape';
|
import { getZoeChipSourceURL, getZoeChipSources } from '@/providers/sources/zoechip/scrape';
|
||||||
import { ScrapeContext } from '@/utils/context';
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
export const zoeBase = 'https://zoechip.cc';
|
export const zoeBase = 'https://zoechip.cc';
|
||||||
|
|
||||||
export type MovieContext = ScrapeContext & {
|
|
||||||
media: MovieMedia;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShowContext = ScrapeContext & {
|
|
||||||
media: ShowMedia;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ZoeChipSourceDetails = {
|
export type ZoeChipSourceDetails = {
|
||||||
type: string; // Only seen "iframe" so far
|
type: string; // Only seen "iframe" so far
|
||||||
link: string;
|
link: string;
|
||||||
@@ -23,7 +14,10 @@ export type ZoeChipSourceDetails = {
|
|||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function formatSource(ctx: MovieContext | ShowContext, source: { embed: string; episodeId: string }) {
|
export async function formatSource(
|
||||||
|
ctx: MovieScrapeContext | ShowScrapeContext,
|
||||||
|
source: { embed: string; episodeId: string },
|
||||||
|
) {
|
||||||
const link = await getZoeChipSourceURL(ctx, source.episodeId);
|
const link = await getZoeChipSourceURL(ctx, source.episodeId);
|
||||||
if (link) {
|
if (link) {
|
||||||
const embed = {
|
const embed = {
|
||||||
@@ -51,7 +45,7 @@ export async function formatSource(ctx: MovieContext | ShowContext, source: { em
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createZoeChipStreamData(ctx: MovieContext | ShowContext, id: string) {
|
export async function createZoeChipStreamData(ctx: MovieScrapeContext | ShowScrapeContext, id: string) {
|
||||||
const sources = await getZoeChipSources(ctx, id);
|
const sources = await getZoeChipSources(ctx, id);
|
||||||
const embeds: {
|
const embeds: {
|
||||||
embedId: string;
|
embedId: string;
|
||||||
|
@@ -6,7 +6,7 @@ import { scrapeShow } from '@/providers/sources/zoechip/scrape-show';
|
|||||||
export const zoechipScraper = makeSourcerer({
|
export const zoechipScraper = makeSourcerer({
|
||||||
id: 'zoechip',
|
id: 'zoechip',
|
||||||
name: 'ZoeChip',
|
name: 'ZoeChip',
|
||||||
rank: 110,
|
rank: 200,
|
||||||
flags: [flags.NO_CORS],
|
flags: [flags.NO_CORS],
|
||||||
scrapeMovie,
|
scrapeMovie,
|
||||||
scrapeShow,
|
scrapeShow,
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { MovieContext, createZoeChipStreamData } from '@/providers/sources/zoechip/common';
|
import { createZoeChipStreamData } from '@/providers/sources/zoechip/common';
|
||||||
import { getZoeChipMovieID } from '@/providers/sources/zoechip/search';
|
import { getZoeChipMovieID } from '@/providers/sources/zoechip/search';
|
||||||
|
import { MovieScrapeContext } from '@/utils/context';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
export async function scrapeMovie(ctx: MovieContext) {
|
export async function scrapeMovie(ctx: MovieScrapeContext) {
|
||||||
const movieID = await getZoeChipMovieID(ctx, ctx.media);
|
const movieID = await getZoeChipMovieID(ctx, ctx.media);
|
||||||
if (!movieID) {
|
if (!movieID) {
|
||||||
throw new NotFoundError('no search results match');
|
throw new NotFoundError('no search results match');
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { ShowContext, createZoeChipStreamData } from '@/providers/sources/zoechip/common';
|
import { createZoeChipStreamData } from '@/providers/sources/zoechip/common';
|
||||||
import { getZoeChipEpisodeID, getZoeChipSeasonID } from '@/providers/sources/zoechip/scrape';
|
import { getZoeChipEpisodeID, getZoeChipSeasonID } from '@/providers/sources/zoechip/scrape';
|
||||||
import { getZoeChipShowID } from '@/providers/sources/zoechip/search';
|
import { getZoeChipShowID } from '@/providers/sources/zoechip/search';
|
||||||
|
import { ShowScrapeContext } from '@/utils/context';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
export async function scrapeShow(ctx: ShowContext) {
|
export async function scrapeShow(ctx: ShowScrapeContext) {
|
||||||
const showID = await getZoeChipShowID(ctx, ctx.media);
|
const showID = await getZoeChipShowID(ctx, ctx.media);
|
||||||
if (!showID) {
|
if (!showID) {
|
||||||
throw new NotFoundError('no search results match');
|
throw new NotFoundError('no search results match');
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
import { ShowMedia } from '@/main/media';
|
import { ShowMedia } from '@/main/media';
|
||||||
import { MovieContext, ShowContext, ZoeChipSourceDetails, zoeBase } from '@/providers/sources/zoechip/common';
|
import { ZoeChipSourceDetails, zoeBase } from '@/providers/sources/zoechip/common';
|
||||||
import { ScrapeContext } from '@/utils/context';
|
import { MovieScrapeContext, ScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
export async function getZoeChipSources(ctx: MovieContext | ShowContext, id: string) {
|
export async function getZoeChipSources(ctx: MovieScrapeContext | ShowScrapeContext, id: string) {
|
||||||
// Movies use /ajax/episode/list/ID
|
// Movies use /ajax/episode/list/ID
|
||||||
// Shows use /ajax/episode/servers/ID
|
// Shows use /ajax/episode/servers/ID
|
||||||
const endpoint = ctx.media.type === 'movie' ? 'list' : 'servers';
|
const endpoint = ctx.media.type === 'movie' ? 'list' : 'servers';
|
||||||
|
@@ -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';
|
||||||
@@ -12,12 +13,14 @@ export type FileBasedStream = {
|
|||||||
type: 'file';
|
type: 'file';
|
||||||
flags: Flags[];
|
flags: Flags[];
|
||||||
qualities: Partial<Record<Qualities, StreamFile>>;
|
qualities: Partial<Record<Qualities, StreamFile>>;
|
||||||
|
captions: Caption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HlsBasedStream = {
|
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;
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { UseableFetcher } from '@/fetchers/types';
|
import { UseableFetcher } from '@/fetchers/types';
|
||||||
|
import { MovieMedia, ShowMedia } from '@/main/media';
|
||||||
|
|
||||||
export type ScrapeContext = {
|
export type ScrapeContext = {
|
||||||
proxiedFetcher: <T>(...params: Parameters<UseableFetcher<T>>) => ReturnType<UseableFetcher<T>>;
|
proxiedFetcher: <T>(...params: Parameters<UseableFetcher<T>>) => ReturnType<UseableFetcher<T>>;
|
||||||
@@ -12,3 +13,11 @@ export type EmbedInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type EmbedScrapeContext = EmbedInput & ScrapeContext;
|
export type EmbedScrapeContext = EmbedInput & ScrapeContext;
|
||||||
|
|
||||||
|
export type MovieScrapeContext = ScrapeContext & {
|
||||||
|
media: MovieMedia;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShowScrapeContext = ScrapeContext & {
|
||||||
|
media: ShowMedia;
|
||||||
|
};
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
export class NotFoundError extends Error {
|
export class NotFoundError extends Error {
|
||||||
constructor(reason?: string) {
|
constructor(reason?: string) {
|
||||||
super(`Couldn't found a stream: ${reason ?? 'not found'}`);
|
super(`Couldn't find a stream: ${reason ?? 'not found'}`);
|
||||||
this.name = 'NotFoundError';
|
this.name = 'NotFoundError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
src/utils/valid.ts
Normal file
17
src/utils/valid.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Stream } from '@/providers/streams';
|
||||||
|
|
||||||
|
export function isValidStream(stream: Stream | undefined): boolean {
|
||||||
|
if (!stream) return false;
|
||||||
|
if (stream.type === 'hls') {
|
||||||
|
if (!stream.playlist) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stream.type === 'file') {
|
||||||
|
const validQualities = Object.values(stream.qualities).filter((v) => v.url.length > 0);
|
||||||
|
if (validQualities.length === 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown file type
|
||||||
|
return false;
|
||||||
|
}
|
Reference in New Issue
Block a user