mirror of
https://github.com/movie-web/providers.git
synced 2025-09-13 15:43:26 +00:00
Merge branch 'dev' into pr/52
This commit is contained in:
90
src/__test__/providers/embedUtils.ts
Normal file
90
src/__test__/providers/embedUtils.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { buildProviders } from "@/entrypoint/builder";
|
||||
import { ScrapeMedia } from "@/entrypoint/utils/media";
|
||||
import { targets } from "@/entrypoint/utils/targets";
|
||||
import { makeStandardFetcher } from "@/fetchers/standardFetch";
|
||||
import { Embed, Sourcerer, SourcererEmbed } from "@/providers/base";
|
||||
import { TestTypes } from "./providerUtils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ProviderControls } from "@/entrypoint/controls";
|
||||
import { makeSimpleProxyFetcher } from "@/fetchers/simpleProxy";
|
||||
|
||||
export interface TestEmbedOptions {
|
||||
embed: Embed;
|
||||
source: Sourcerer;
|
||||
testSuite: ScrapeMedia[];
|
||||
types: TestTypes[];
|
||||
debug?: boolean;
|
||||
expect: {
|
||||
embeds: number;
|
||||
streams?: number;
|
||||
error?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
function makeBaseEmbedProviders() {
|
||||
const builder = buildProviders()
|
||||
.setTarget(targets.ANY)
|
||||
.setFetcher(makeStandardFetcher(fetch));
|
||||
return builder;
|
||||
}
|
||||
|
||||
export function testEmbed(ops: TestEmbedOptions) {
|
||||
if (ops.testSuite.length === 0) throw new Error("Test suite must have at least one test");
|
||||
describe(`embed:${ops.source.id}:${ops.embed.id}`, () => {
|
||||
ops.testSuite.forEach((test) => {
|
||||
describe(`test ${test.title}`, async () => {
|
||||
async function gatherEmbeds(providers: ProviderControls): Promise<SourcererEmbed[]> {
|
||||
const results = await providers.runSourceScraper({
|
||||
id: ops.source.id,
|
||||
media: test,
|
||||
})
|
||||
if (results.embeds.length !== ops.expect.embeds) throw new Error(`Embeds don't match expected amount of embeds (${ops.source.id}, ${ops.embed.id}, got ${results.embeds.length} but expected ${ops.expect.embeds})`);
|
||||
return results.embeds;
|
||||
}
|
||||
|
||||
async function runTest(providers: ProviderControls, embedUrl: string) {
|
||||
let hasError = false;
|
||||
let streamCount = 0;
|
||||
try {
|
||||
const result = await providers.runEmbedScraper({
|
||||
id: ops.embed.id,
|
||||
url: embedUrl,
|
||||
})
|
||||
if (ops.debug) console.log(result);
|
||||
streamCount = (result.stream ?? []).length;
|
||||
} catch (err) {
|
||||
if (ops.debug) console.log(err);
|
||||
hasError = true;
|
||||
}
|
||||
expect(ops.expect.error ?? false).toBe(hasError);
|
||||
expect(ops.expect.streams ?? 0).toBe(streamCount);
|
||||
}
|
||||
|
||||
for (const t of ops.types) {
|
||||
const builder = makeBaseEmbedProviders().addSource(ops.source).addEmbed(ops.embed);
|
||||
if (t === 'standard') {}
|
||||
else if (t === 'ip:standard')
|
||||
builder.enableConsistentIpForRequests();
|
||||
else if (t === 'proxied') {
|
||||
if (!process.env.MOVIE_WEB_PROXY_URL)
|
||||
throw new Error("Cant use proxied test without setting MOVIE_WEB_PROXY_URL env");
|
||||
builder.setProxiedFetcher(makeSimpleProxyFetcher(process.env.MOVIE_WEB_PROXY_URL, fetch));
|
||||
}
|
||||
const providers = builder.build();
|
||||
try {
|
||||
const embeds = await gatherEmbeds(providers);
|
||||
embeds.forEach((embed, i) => {
|
||||
it(`${t} - embed ${i}`, async () => {
|
||||
await runTest(providers, embed.url);
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
it(`${t} - embed ??`, () => {
|
||||
throw new Error("Failed to get streams: " + err);
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
118
src/__test__/providers/embeds.test.ts
Normal file
118
src/__test__/providers/embeds.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { febboxMp4Scraper } from "@/providers/embeds/febbox/mp4";
|
||||
import { testEmbed } from "./embedUtils";
|
||||
import { showboxScraper } from "@/providers/sources/showbox";
|
||||
import { testMedia } from "./testMedia";
|
||||
import { flixhqScraper } from "@/providers/sources/flixhq";
|
||||
import { upcloudScraper } from "@/providers/embeds/upcloud";
|
||||
import { goMoviesScraper } from "@/providers/sources/gomovies";
|
||||
import { smashyStreamScraper } from "@/providers/sources/smashystream";
|
||||
import { smashyStreamDScraper } from "@/providers/embeds/smashystream/dued";
|
||||
import { vidsrcembedScraper } from '@/providers/embeds/vidsrc';
|
||||
import { vidsrcScraper } from '@/providers/sources/vidsrc';
|
||||
import { vidSrcToScraper } from '@/providers/sources/vidsrcto';
|
||||
import { vidplayScraper } from '@/providers/embeds/vidplay';
|
||||
import { fileMoonScraper } from '@/providers/embeds/filemoon';
|
||||
import { zoechipScraper } from '@/providers/sources/zoechip';
|
||||
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
testEmbed({
|
||||
embed: febboxMp4Scraper,
|
||||
source: showboxScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: upcloudScraper,
|
||||
source: flixhqScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: upcloudScraper,
|
||||
source: goMoviesScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: smashyStreamDScraper,
|
||||
source: smashyStreamScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: vidsrcembedScraper,
|
||||
source: vidsrcScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: vidplayScraper,
|
||||
source: vidSrcToScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: fileMoonScraper,
|
||||
source: vidSrcToScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: upcloudScraper,
|
||||
source: zoechipScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 2,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testEmbed({
|
||||
embed: mixdropScraper,
|
||||
source: zoechipScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 2,
|
||||
streams: 1,
|
||||
}
|
||||
})
|
102
src/__test__/providers/providerUtils.ts
Normal file
102
src/__test__/providers/providerUtils.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ScrapeMedia } from "@/entrypoint/utils/media";
|
||||
import { Embed, Sourcerer, SourcererEmbed } from "@/providers/base";
|
||||
import { buildProviders } from "@/entrypoint/builder";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { makeStandardFetcher } from "@/fetchers/standardFetch";
|
||||
import { ProviderControls } from "@/entrypoint/controls";
|
||||
import { NotFoundError } from "@/utils/errors";
|
||||
import { targets } from "@/entrypoint/utils/targets";
|
||||
import { getBuiltinEmbeds } from "@/entrypoint/providers";
|
||||
import { makeSimpleProxyFetcher } from "@/fetchers/simpleProxy";
|
||||
|
||||
export type TestTypes = 'standard' | 'ip:standard' | 'proxied';
|
||||
|
||||
export interface TestSourceOptions {
|
||||
source: Sourcerer;
|
||||
testSuite: ScrapeMedia[];
|
||||
types: TestTypes[];
|
||||
debug?: boolean;
|
||||
expect: {
|
||||
embeds?: number;
|
||||
streams?: number;
|
||||
error?: boolean;
|
||||
notfound?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
function makeBaseProviders() {
|
||||
const builder = buildProviders()
|
||||
.setTarget(targets.ANY)
|
||||
.setFetcher(makeStandardFetcher(fetch));
|
||||
const embeds = getBuiltinEmbeds();
|
||||
embeds.forEach(embed => builder.addEmbed(embed));
|
||||
return builder;
|
||||
}
|
||||
|
||||
export function testSource(ops: TestSourceOptions) {
|
||||
if (ops.testSuite.length === 0) throw new Error("Test suite must have at least one test");
|
||||
describe(`source:${ops.source.id}`, () => {
|
||||
ops.testSuite.forEach((test) => {
|
||||
describe(`test ${test.title}`, () => {
|
||||
async function runTest(providers: ProviderControls) {
|
||||
let hasNotFound = false;
|
||||
let hasError = false;
|
||||
let streamCount = 0;
|
||||
let embedCount = 0;
|
||||
let embeds = [];
|
||||
try {
|
||||
const result = await providers.runSourceScraper({
|
||||
id: ops.source.id,
|
||||
media: test,
|
||||
})
|
||||
if (ops.debug) console.log(result);
|
||||
streamCount = (result.stream ?? []).length;
|
||||
embedCount = result.embeds.length;
|
||||
} catch (err) {
|
||||
if (ops.debug) console.log(err);
|
||||
if (err instanceof NotFoundError)
|
||||
hasNotFound = true;
|
||||
else
|
||||
hasError = true;
|
||||
}
|
||||
expect(ops.expect.error ?? false).toBe(hasError);
|
||||
expect(ops.expect.notfound ?? false).toBe(hasNotFound);
|
||||
expect(ops.expect.streams ?? 0).toBe(streamCount);
|
||||
expect(ops.expect.embeds ?? 0).toBe(embedCount);
|
||||
}
|
||||
|
||||
if (ops.types.includes('standard')) {
|
||||
it(`standard`, async () => {
|
||||
const providers = makeBaseProviders()
|
||||
.addSource(ops.source)
|
||||
.build();
|
||||
await runTest(providers);
|
||||
})
|
||||
}
|
||||
|
||||
if (ops.types.includes('ip:standard')) {
|
||||
it(`standard:ip`, async () => {
|
||||
const providers = makeBaseProviders()
|
||||
.addSource(ops.source)
|
||||
.enableConsistentIpForRequests()
|
||||
.build();
|
||||
await runTest(providers);
|
||||
})
|
||||
}
|
||||
|
||||
if (ops.types.includes('proxied')) {
|
||||
it(`proxied`, async () => {
|
||||
if (!process.env.MOVIE_WEB_PROXY_URL)
|
||||
throw new Error("Cant use proxied test without setting MOVIE_WEB_PROXY_URL env");
|
||||
const providers = makeBaseProviders()
|
||||
.addSource(ops.source)
|
||||
.setProxiedFetcher(makeSimpleProxyFetcher(process.env.MOVIE_WEB_PROXY_URL, fetch))
|
||||
.build();
|
||||
await runTest(providers);
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
95
src/__test__/providers/providers.test.ts
Normal file
95
src/__test__/providers/providers.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { testSource } from "./providerUtils";
|
||||
import { lookmovieScraper } from "@/providers/sources/lookmovie";
|
||||
import { testMedia } from "./testMedia";
|
||||
import { showboxScraper } from "@/providers/sources/showbox";
|
||||
import dotenv from 'dotenv';
|
||||
import { flixhqScraper } from "@/providers/sources/flixhq";
|
||||
import { goMoviesScraper } from "@/providers/sources/gomovies";
|
||||
import { smashyStreamScraper } from "@/providers/sources/smashystream";
|
||||
import { vidsrcScraper } from "@/providers/sources/vidsrc";
|
||||
import { vidSrcToScraper } from "@/providers/sources/vidsrcto";
|
||||
import { zoechipScraper } from "@/providers/sources/zoechip";
|
||||
import { remotestreamScraper } from "@/providers/sources/remotestream";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
testSource({
|
||||
source: lookmovieScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['ip:standard'],
|
||||
expect: {
|
||||
streams: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: showboxScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: flixhqScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: goMoviesScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: smashyStreamScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: vidsrcScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 1,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: vidSrcToScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 2,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: zoechipScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
embeds: 3,
|
||||
}
|
||||
})
|
||||
|
||||
testSource({
|
||||
source: remotestreamScraper,
|
||||
testSuite: [testMedia.arcane, testMedia.hamilton],
|
||||
types: ['standard', 'proxied'],
|
||||
expect: {
|
||||
streams: 1,
|
||||
}
|
||||
})
|
30
src/__test__/providers/testMedia.ts
Normal file
30
src/__test__/providers/testMedia.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ScrapeMedia } from "@/entrypoint/utils/media";
|
||||
|
||||
function makeMedia(media: ScrapeMedia): ScrapeMedia {
|
||||
return media;
|
||||
}
|
||||
|
||||
export const testMedia = {
|
||||
arcane: makeMedia({
|
||||
type: "show",
|
||||
title: "Arcane",
|
||||
tmdbId: "94605",
|
||||
releaseYear: 2021,
|
||||
episode: {
|
||||
number: 1,
|
||||
tmdbId: '1953812',
|
||||
},
|
||||
season: {
|
||||
number: 1,
|
||||
tmdbId: '134187',
|
||||
},
|
||||
imdbId: 'tt11126994'
|
||||
}),
|
||||
hamilton: makeMedia({
|
||||
type: 'movie',
|
||||
tmdbId: '556574',
|
||||
imdbId: 'tt8503618',
|
||||
releaseYear: 2020,
|
||||
title: 'Hamilton'
|
||||
})
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { gatherAllEmbeds, gatherAllSources } from '@/providers/all';
|
||||
import { Embed, Sourcerer } from '@/providers/base';
|
||||
import { makeEmbed, makeSourcerer } from '@/providers/base';
|
||||
|
||||
export function makeProviderMocks() {
|
||||
const embedsMock = vi.fn<Parameters<typeof gatherAllEmbeds>, ReturnType<typeof gatherAllEmbeds>>();
|
||||
@@ -13,104 +13,104 @@ export function makeProviderMocks() {
|
||||
};
|
||||
}
|
||||
|
||||
const sourceA = {
|
||||
const sourceA = makeSourcerer({
|
||||
id: 'a',
|
||||
name: 'A',
|
||||
rank: 1,
|
||||
disabled: false,
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
const sourceB = {
|
||||
});
|
||||
const sourceB = makeSourcerer({
|
||||
id: 'b',
|
||||
name: 'B',
|
||||
rank: 2,
|
||||
disabled: false,
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
const sourceCDisabled = {
|
||||
});
|
||||
const sourceCDisabled = makeSourcerer({
|
||||
id: 'c',
|
||||
name: 'C',
|
||||
rank: 3,
|
||||
disabled: true,
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
const sourceAHigherRank = {
|
||||
});
|
||||
const sourceAHigherRank = makeSourcerer({
|
||||
id: 'a',
|
||||
name: 'A',
|
||||
rank: 100,
|
||||
disabled: false,
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
const sourceGSameRankAsA = {
|
||||
});
|
||||
const sourceGSameRankAsA = makeSourcerer({
|
||||
id: 'g',
|
||||
name: 'G',
|
||||
rank: 1,
|
||||
disabled: false,
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
const fullSourceYMovie = {
|
||||
});
|
||||
const fullSourceYMovie = makeSourcerer({
|
||||
id: 'y',
|
||||
name: 'Y',
|
||||
rank: 105,
|
||||
scrapeMovie: vi.fn(),
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
const fullSourceYShow = {
|
||||
});
|
||||
const fullSourceYShow = makeSourcerer({
|
||||
id: 'y',
|
||||
name: 'Y',
|
||||
rank: 105,
|
||||
scrapeShow: vi.fn(),
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
const fullSourceZBoth = {
|
||||
});
|
||||
const fullSourceZBoth = makeSourcerer({
|
||||
id: 'z',
|
||||
name: 'Z',
|
||||
rank: 106,
|
||||
scrapeMovie: vi.fn(),
|
||||
scrapeShow: vi.fn(),
|
||||
flags: [],
|
||||
} as Sourcerer;
|
||||
});
|
||||
|
||||
const embedD = {
|
||||
const embedD = makeEmbed({
|
||||
id: 'd',
|
||||
rank: 4,
|
||||
disabled: false,
|
||||
} as Embed;
|
||||
const embedA = {
|
||||
} as any);
|
||||
const embedA = makeEmbed({
|
||||
id: 'a',
|
||||
rank: 5,
|
||||
disabled: false,
|
||||
} as Embed;
|
||||
const embedEDisabled = {
|
||||
} as any);
|
||||
const embedEDisabled = makeEmbed({
|
||||
id: 'e',
|
||||
rank: 6,
|
||||
disabled: true,
|
||||
} as Embed;
|
||||
const embedDHigherRank = {
|
||||
} as any);
|
||||
const embedDHigherRank = makeEmbed({
|
||||
id: 'd',
|
||||
rank: 4000,
|
||||
disabled: false,
|
||||
} as Embed;
|
||||
const embedFSameRankAsA = {
|
||||
} as any);
|
||||
const embedFSameRankAsA = makeEmbed({
|
||||
id: 'f',
|
||||
rank: 5,
|
||||
disabled: false,
|
||||
} as Embed;
|
||||
const embedHSameRankAsSourceA = {
|
||||
} as any);
|
||||
const embedHSameRankAsSourceA = makeEmbed({
|
||||
id: 'h',
|
||||
rank: 1,
|
||||
disabled: false,
|
||||
} as Embed;
|
||||
const fullEmbedX = {
|
||||
} as any);
|
||||
const fullEmbedX = makeEmbed({
|
||||
id: 'x',
|
||||
name: 'X',
|
||||
rank: 104,
|
||||
} as Embed;
|
||||
const fullEmbedZ = {
|
||||
} as any);
|
||||
const fullEmbedZ = makeEmbed({
|
||||
id: 'z',
|
||||
name: 'Z',
|
||||
rank: 109,
|
||||
} as Embed;
|
||||
} as any);
|
||||
|
||||
export const mockSources = {
|
||||
sourceA,
|
@@ -1,4 +1,4 @@
|
||||
import { mockEmbeds, mockSources } from '@/__test__/providerTests';
|
||||
import { mockEmbeds, mockSources } from '../providerTests';
|
||||
import { getBuiltinEmbeds, getBuiltinSources } from '@/entrypoint/providers';
|
||||
import { FeatureMap } from '@/entrypoint/utils/targets';
|
||||
import { getProviders } from '@/providers/get';
|
@@ -1,4 +1,4 @@
|
||||
import { mockEmbeds, mockSources } from '@/__test__/providerTests';
|
||||
import { mockEmbeds, mockSources } from '../providerTests.ts';
|
||||
import { makeProviders } from '@/entrypoint/declare';
|
||||
import { targets } from '@/entrypoint/utils/targets';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
@@ -1,4 +1,4 @@
|
||||
import { mockEmbeds, mockSources } from '@/__test__/providerTests';
|
||||
import { mockEmbeds, mockSources } from '../providerTests.ts';
|
||||
import { makeProviders } from '@/entrypoint/declare';
|
||||
import { targets } from '@/entrypoint/utils/targets';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
@@ -2,7 +2,7 @@ import { getConfig } from '@/dev-cli/config';
|
||||
|
||||
import { MovieMedia, ShowMedia } from '..';
|
||||
|
||||
export async function makeTMDBRequest(url: string): Promise<Response> {
|
||||
export async function makeTMDBRequest(url: string, appendToResponse?: string): Promise<Response> {
|
||||
const headers: {
|
||||
accept: 'application/json';
|
||||
authorization?: string;
|
||||
@@ -10,7 +10,7 @@ export async function makeTMDBRequest(url: string): Promise<Response> {
|
||||
accept: 'application/json',
|
||||
};
|
||||
|
||||
let requestURL = url;
|
||||
const requestURL = new URL(url);
|
||||
const key = getConfig().tmdbApiKey;
|
||||
|
||||
// * 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')) {
|
||||
headers.authorization = `Bearer ${key}`;
|
||||
} else {
|
||||
requestURL += `?api_key=${key}`;
|
||||
requestURL.searchParams.append('api_key', key);
|
||||
}
|
||||
|
||||
if (appendToResponse) {
|
||||
requestURL.searchParams.append('append_to_response', appendToResponse);
|
||||
}
|
||||
|
||||
return fetch(requestURL, {
|
||||
@@ -29,7 +33,7 @@ export async function makeTMDBRequest(url: string): Promise<Response> {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (movie.success === false) {
|
||||
@@ -45,13 +49,14 @@ export async function getMovieMediaDetails(id: string): Promise<MovieMedia> {
|
||||
title: movie.title,
|
||||
releaseYear: Number(movie.release_date.split('-')[0]),
|
||||
tmdbId: id,
|
||||
imdbId: movie.imdb_id,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getShowMediaDetails(id: string, seasonNumber: string, episodeNumber: string): Promise<ShowMedia> {
|
||||
// * TV shows require the TMDB ID for the series, season, and episode
|
||||
// * 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();
|
||||
|
||||
if (series.success === false) {
|
||||
@@ -91,5 +96,6 @@ export async function getShowMediaDetails(id: string, seasonNumber: string, epis
|
||||
number: season.season_number,
|
||||
tmdbId: season.id,
|
||||
},
|
||||
imdbId: series.external_ids.imdb_id,
|
||||
};
|
||||
}
|
||||
|
@@ -5,6 +5,10 @@ export const flags = {
|
||||
// the stream is locked on IP, so only works if
|
||||
// request maker is same as player (not compatible with proxies)
|
||||
IP_LOCKED: 'ip-locked',
|
||||
|
||||
// The source/embed is blocking cloudflare ip's
|
||||
// This flag is not compatible with a proxy hosted on cloudflare
|
||||
CF_BLOCKED: 'cf-blocked',
|
||||
} as const;
|
||||
|
||||
export type Flags = (typeof flags)[keyof typeof flags];
|
||||
|
@@ -7,6 +7,8 @@ const headerMap: Record<string, string> = {
|
||||
cookie: 'X-Cookie',
|
||||
referer: 'X-Referer',
|
||||
origin: 'X-Origin',
|
||||
'user-agent': 'X-User-Agent',
|
||||
'x-real-ip': 'X-X-Real-Ip',
|
||||
};
|
||||
|
||||
const responseHeaderMap: Record<string, string> = {
|
||||
|
@@ -18,11 +18,14 @@ import { vidsrcScraper } from '@/providers/sources/vidsrc/index';
|
||||
import { zoechipScraper } from '@/providers/sources/zoechip';
|
||||
|
||||
import { closeLoadScraper } from './embeds/closeload';
|
||||
import { fileMoonScraper } from './embeds/filemoon';
|
||||
import { ridooScraper } from './embeds/ridoo';
|
||||
import { smashyStreamDScraper } from './embeds/smashystream/dued';
|
||||
import { smashyStreamFScraper } from './embeds/smashystream/video1';
|
||||
import { vidplayScraper } from './embeds/vidplay';
|
||||
import { ridooMoviesScraper } from './sources/ridomovies';
|
||||
import { smashyStreamScraper } from './sources/smashystream';
|
||||
import { vidSrcToScraper } from './sources/vidsrcto';
|
||||
|
||||
export function gatherAllSources(): Array<Sourcerer> {
|
||||
// all sources are gathered here
|
||||
@@ -37,6 +40,7 @@ export function gatherAllSources(): Array<Sourcerer> {
|
||||
lookmovieScraper,
|
||||
smashyStreamScraper,
|
||||
ridooMoviesScraper,
|
||||
vidSrcToScraper,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -56,5 +60,7 @@ export function gatherAllEmbeds(): Array<Embed> {
|
||||
smashyStreamDScraper,
|
||||
ridooScraper,
|
||||
closeLoadScraper,
|
||||
fileMoonScraper,
|
||||
vidplayScraper,
|
||||
];
|
||||
}
|
||||
|
58
src/providers/embeds/filemoon/index.ts
Normal file
58
src/providers/embeds/filemoon/index.ts
Normal file
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
5
src/providers/embeds/filemoon/types.ts
Normal file
5
src/providers/embeds/filemoon/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type SubtitleResult = {
|
||||
file: string;
|
||||
label: string;
|
||||
kind: string;
|
||||
}[];
|
@@ -4,6 +4,9 @@ import { flags } from '@/entrypoint/utils/targets';
|
||||
import { makeEmbed } from '@/providers/base';
|
||||
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
|
||||
|
||||
const origin = 'https://rabbitstream.net';
|
||||
const referer = 'https://rabbitstream.net/';
|
||||
|
||||
const { AES, enc } = crypto;
|
||||
|
||||
interface StreamRes {
|
||||
@@ -126,6 +129,10 @@ export const upcloudScraper = makeEmbed({
|
||||
playlist: sources.file,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions,
|
||||
preferredHeaders: {
|
||||
Referer: referer,
|
||||
Origin: origin,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
53
src/providers/embeds/vidplay/common.ts
Normal file
53
src/providers/embeds/vidplay/common.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { makeFullUrl } from '@/fetchers/common';
|
||||
import { decodeData } from '@/providers/sources/vidsrcto/common';
|
||||
import { EmbedScrapeContext } from '@/utils/context';
|
||||
|
||||
export const vidplayBase = 'https://vidplay.online';
|
||||
export const referer = `${vidplayBase}/`;
|
||||
|
||||
// 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/Ciarands/vidsrc-keys/main/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',
|
||||
},
|
||||
});
|
||||
};
|
57
src/providers/embeds/vidplay/index.ts
Normal file
57
src/providers/embeds/vidplay/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { makeEmbed } from '@/providers/base';
|
||||
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
|
||||
|
||||
import { getFileUrl, referer } 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,
|
||||
preferredHeaders: {
|
||||
Referer: referer,
|
||||
Origin: referer,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
19
src/providers/embeds/vidplay/types.ts
Normal file
19
src/providers/embeds/vidplay/types.ts
Normal file
@@ -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;
|
||||
}[];
|
@@ -4,6 +4,14 @@ import { makeEmbed } from '@/providers/base';
|
||||
const hlsURLRegex = /file:"(.*?)"/;
|
||||
const setPassRegex = /var pass_path = "(.*set_pass\.php.*)";/;
|
||||
|
||||
function formatHlsB64(data: string): string {
|
||||
const encodedB64 = data.replace(/\/@#@\/[^=/]+==/g, '');
|
||||
if (encodedB64.match(/\/@#@\/[^=/]+==/)) {
|
||||
return formatHlsB64(encodedB64);
|
||||
}
|
||||
return encodedB64;
|
||||
}
|
||||
|
||||
export const vidsrcembedScraper = makeEmbed({
|
||||
id: 'vidsrcembed', // VidSrc is both a source and an embed host
|
||||
name: 'VidSrc',
|
||||
@@ -15,13 +23,12 @@ export const vidsrcembedScraper = makeEmbed({
|
||||
},
|
||||
});
|
||||
|
||||
const match = html
|
||||
.match(hlsURLRegex)?.[1]
|
||||
?.replace(/(\/\/\S+?=)/g, '')
|
||||
.replace('#2', '');
|
||||
if (!match) throw new Error('Unable to find HLS playlist');
|
||||
const finalUrl = atob(match);
|
||||
|
||||
// When this eventually breaks see the player js @ pjs_main.js
|
||||
// If you know what youre doing and are slightly confused about how to reverse this feel free to reach out to ciaran_ds on discord with any queries
|
||||
let hlsMatch = html.match(hlsURLRegex)?.[1]?.slice(2);
|
||||
if (!hlsMatch) throw new Error('Unable to find HLS playlist');
|
||||
hlsMatch = formatHlsB64(hlsMatch);
|
||||
const finalUrl = atob(hlsMatch);
|
||||
if (!finalUrl.includes('.m3u8')) throw new Error('Unable to find HLS playlist');
|
||||
|
||||
let setPassLink = html.match(setPassRegex)?.[1];
|
||||
|
@@ -32,7 +32,7 @@ async function universalScraper(ctx: MovieScrapeContext | ShowScrapeContext): Pr
|
||||
export const lookmovieScraper = makeSourcerer({
|
||||
id: 'lookmovie',
|
||||
name: 'LookMovie',
|
||||
rank: 1,
|
||||
rank: 700,
|
||||
flags: [flags.IP_LOCKED],
|
||||
scrapeShow: universalScraper,
|
||||
scrapeMovie: universalScraper,
|
||||
|
@@ -4,6 +4,9 @@ import { NotFoundError } from '@/utils/errors';
|
||||
|
||||
const remotestreamBase = atob('aHR0cHM6Ly9mc2IuOG1ldDNkdGpmcmNxY2hjb25xcGtsd3hzeGIyb2N1bWMuc3RyZWFt');
|
||||
|
||||
const origin = 'https://remotestre.am';
|
||||
const referer = 'https://remotestre.am/';
|
||||
|
||||
export const remotestreamScraper = makeSourcerer({
|
||||
id: 'remotestream',
|
||||
name: 'Remote Stream',
|
||||
@@ -16,9 +19,12 @@ export const remotestreamScraper = makeSourcerer({
|
||||
const playlistLink = `${remotestreamBase}/Shows/${ctx.media.tmdbId}/${seasonNumber}/${episodeNumber}/${episodeNumber}.m3u8`;
|
||||
|
||||
ctx.progress(30);
|
||||
const streamRes = await ctx.fetcher.full(playlistLink, {
|
||||
method: 'HEAD',
|
||||
const streamRes = await ctx.proxiedFetcher.full(playlistLink, {
|
||||
method: 'GET',
|
||||
readHeaders: ['content-type'],
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
});
|
||||
if (!streamRes.headers.get('content-type')?.toLowerCase().includes('application/x-mpegurl'))
|
||||
throw new NotFoundError('No watchable item found');
|
||||
@@ -33,6 +39,10 @@ export const remotestreamScraper = makeSourcerer({
|
||||
playlist: playlistLink,
|
||||
type: 'hls',
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
preferredHeaders: {
|
||||
Referer: referer,
|
||||
Origin: origin,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -41,9 +51,12 @@ export const remotestreamScraper = makeSourcerer({
|
||||
const playlistLink = `${remotestreamBase}/Movies/${ctx.media.tmdbId}/${ctx.media.tmdbId}.m3u8`;
|
||||
|
||||
ctx.progress(30);
|
||||
const streamRes = await ctx.fetcher.full(playlistLink, {
|
||||
method: 'HEAD',
|
||||
const streamRes = await ctx.proxiedFetcher.full(playlistLink, {
|
||||
method: 'GET',
|
||||
readHeaders: ['content-type'],
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
});
|
||||
if (!streamRes.headers.get('content-type')?.toLowerCase().includes('application/x-mpegurl'))
|
||||
throw new NotFoundError('No watchable item found');
|
||||
@@ -58,6 +71,10 @@ export const remotestreamScraper = makeSourcerer({
|
||||
playlist: playlistLink,
|
||||
type: 'hls',
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
preferredHeaders: {
|
||||
Referer: referer,
|
||||
Origin: origin,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@@ -42,7 +42,7 @@ export const showboxScraper = makeSourcerer({
|
||||
id: 'showbox',
|
||||
name: 'Showbox',
|
||||
rank: 300,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
flags: [flags.CORS_ALLOWED, flags.CF_BLOCKED],
|
||||
scrapeShow: comboScraper,
|
||||
scrapeMovie: comboScraper,
|
||||
});
|
||||
|
@@ -49,9 +49,9 @@ export const sendRequest = async (ctx: ScrapeContext, data: object, altApi = fal
|
||||
headers: {
|
||||
Platform: 'android',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'okhttp/3.2.0',
|
||||
},
|
||||
body: formatted,
|
||||
});
|
||||
|
||||
return JSON.parse(response);
|
||||
};
|
||||
|
49
src/providers/sources/vidsrcto/common.ts
Normal file
49
src/providers/sources/vidsrcto/common.ts
Normal file
@@ -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));
|
||||
};
|
84
src/providers/sources/vidsrcto/index.ts
Normal file
84
src/providers/sources/vidsrcto/index.ts
Normal file
@@ -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,
|
||||
});
|
15
src/providers/sources/vidsrcto/types.ts
Normal file
15
src/providers/sources/vidsrcto/types.ts
Normal file
@@ -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;
|
||||
}>;
|
@@ -58,6 +58,14 @@ export async function scrapeInvidualSource(
|
||||
}
|
||||
|
||||
if (!output) throw new Error('output is null');
|
||||
|
||||
// filter output with only valid embeds that are not disabled
|
||||
output.embeds = output.embeds.filter((embed) => {
|
||||
const e = list.embeds.find((v) => v.id === embed.embedId);
|
||||
if (!e || e.disabled) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if ((!output.stream || output.stream.length === 0) && output.embeds.length === 0)
|
||||
throw new NotFoundError('No streams found');
|
||||
return output;
|
||||
|
@@ -116,8 +116,14 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt
|
||||
};
|
||||
}
|
||||
|
||||
// run embed scrapers on listed embeds
|
||||
const sortedEmbeds = output.embeds.sort((a, b) => embedIds.indexOf(a.embedId) - embedIds.indexOf(b.embedId));
|
||||
// filter disabled and run embed scrapers on listed embeds
|
||||
const sortedEmbeds = output.embeds
|
||||
.filter((embed) => {
|
||||
const e = list.embeds.find((v) => v.id === embed.embedId);
|
||||
if (!e || e.disabled) return false;
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => embedIds.indexOf(a.embedId) - embedIds.indexOf(b.embedId));
|
||||
|
||||
if (sortedEmbeds.length > 0) {
|
||||
ops.events?.discoverEmbeds?.({
|
||||
|
Reference in New Issue
Block a user