Merge branch 'dev' into pr-14-v2

This commit is contained in:
Jorrin
2023-12-25 23:23:45 +01:00
17 changed files with 273 additions and 254 deletions

View File

@@ -10,7 +10,7 @@ import { SourcererOutput } from "@movie-web/providers";
// scrape a stream from upcloud
let output: EmbedOutput;
try {
output = await providers.runSourceScraper({
output = await providers.runEmbedScraper({
id: 'upcloud',
url: 'https://example.com/123',
})

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 movie-web
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,5 +1,6 @@
import { Embed, Sourcerer } from '@/providers/base';
import { febBoxScraper } from '@/providers/embeds/febBox';
import { febboxHlsScraper } from '@/providers/embeds/febbox/hls';
import { febboxMp4Scraper } from '@/providers/embeds/febbox/mp4';
import { mixdropScraper } from '@/providers/embeds/mixdrop';
import { mp4uploadScraper } from '@/providers/embeds/mp4upload';
import { streambucketScraper } from '@/providers/embeds/streambucket';
@@ -12,13 +13,12 @@ import { goMoviesScraper } from '@/providers/sources/gomovies/index';
import { kissAsianScraper } from '@/providers/sources/kissasian/index';
import { lookmovieScraper } from '@/providers/sources/lookmovie';
import { remotestreamScraper } from '@/providers/sources/remotestream';
import { superStreamScraper } from '@/providers/sources/superstream/index';
import { vidsrcScraper } from '@/providers/sources/vidsrc';
import { showboxScraper } from '@/providers/sources/showbox/index';
import { vidsrcScraper } from '@/providers/sources/vidsrc/index';
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> {
@@ -27,12 +27,11 @@ export function gatherAllSources(): Array<Sourcerer> {
flixhqScraper,
remotestreamScraper,
kissAsianScraper,
superStreamScraper,
showboxScraper,
goMoviesScraper,
zoechipScraper,
vidsrcScraper,
lookmovieScraper,
showBoxScraper,
smashyStreamScraper,
];
}
@@ -44,10 +43,11 @@ export function gatherAllEmbeds(): Array<Embed> {
mp4uploadScraper,
streamsbScraper,
upstreamScraper,
febboxMp4Scraper,
febboxHlsScraper,
mixdropScraper,
vidsrcembedScraper,
streambucketScraper,
febBoxScraper,
smashyStreamFScraper,
smashyStreamDScraper,
];

View File

@@ -1,74 +0,0 @@
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,
},
};
},
});

View File

@@ -0,0 +1,24 @@
import { MediaTypes } from '@/main/media';
export const febBoxBase = `https://www.febbox.com`;
export interface FebboxFileList {
file_name: string;
ext: string;
fid: number;
oss_fid: number;
is_dir: 0 | 1;
}
export function parseInputUrl(url: string) {
const [type, id, seasonId, episodeId] = url.slice(1).split('/');
const season = seasonId ? parseInt(seasonId, 10) : undefined;
const episode = episodeId ? parseInt(episodeId, 10) : undefined;
return {
type: type as MediaTypes,
id,
season,
episode,
};
}

View File

@@ -0,0 +1,69 @@
import { MediaTypes } from '@/main/media';
import { FebboxFileList, febBoxBase } from '@/providers/embeds/febbox/common';
import { EmbedScrapeContext } from '@/utils/context';
export async function getFileList(
ctx: EmbedScrapeContext,
shareKey: string,
parentId?: number,
): Promise<FebboxFileList[]> {
const query: Record<string, string> = {
share_key: shareKey,
pwd: '',
};
if (parentId) {
query.parent_id = parentId.toString();
query.page = '1';
}
const streams = await ctx.proxiedFetcher<{
data?: {
file_list?: FebboxFileList[];
};
}>('/file/file_share_list', {
headers: {
'accept-language': 'en', // without this header, the request is marked as a webscraper
},
baseUrl: febBoxBase,
query,
});
return streams.data?.file_list ?? [];
}
function isValidStream(file: FebboxFileList): boolean {
return file.ext === 'mp4' || file.ext === 'mkv';
}
export async function getStreams(
ctx: EmbedScrapeContext,
shareKey: string,
type: MediaTypes,
season?: number,
episode?: number,
): Promise<FebboxFileList[]> {
const streams = await getFileList(ctx, shareKey);
if (type === 'show') {
const seasonFolder = streams.find((v) => {
if (!v.is_dir) return false;
return v.file_name.toLowerCase() === `season ${season}`;
});
if (!seasonFolder) return [];
const episodes = await getFileList(ctx, shareKey, seasonFolder.fid);
const s = season?.toString() ?? '0';
const e = episode?.toString() ?? '0';
const episodeRegex = new RegExp(`[Ss]0*${s}[Ee]0*${e}`);
return episodes
.filter((file) => {
if (file.is_dir) return false;
const match = file.file_name.match(episodeRegex);
if (!match) return false;
return true;
})
.filter(isValidStream);
}
return streams.filter((v) => !v.is_dir).filter(isValidStream);
}

View File

@@ -0,0 +1,47 @@
import { MediaTypes } from '@/main/media';
import { flags } from '@/main/targets';
import { makeEmbed } from '@/providers/base';
import { parseInputUrl } from '@/providers/embeds/febbox/common';
import { getStreams } from '@/providers/embeds/febbox/fileList';
import { getSubtitles } from '@/providers/embeds/febbox/subtitles';
import { showboxBase } from '@/providers/sources/showbox/common';
// structure: https://www.febbox.com/share/<random_key>
export function extractShareKey(url: string): string {
const parsedUrl = new URL(url);
const shareKey = parsedUrl.pathname.split('/')[2];
return shareKey;
}
export const febboxHlsScraper = makeEmbed({
id: 'febbox-hls',
name: 'Febbox (HLS)',
rank: 160,
async scrape(ctx) {
const { type, id, season, episode } = parseInputUrl(ctx.url);
const sharelinkResult = await ctx.proxiedFetcher<{
data?: { link?: string };
}>('/index/share_link', {
baseUrl: showboxBase,
query: {
id,
type: type === 'movie' ? '1' : '2',
},
});
if (!sharelinkResult?.data?.link) throw new Error('No embed url found');
ctx.progress(30);
const shareKey = extractShareKey(sharelinkResult.data.link);
const fileList = await getStreams(ctx, shareKey, type, season, episode);
const firstStream = fileList[0];
if (!firstStream) throw new Error('No playable mp4 stream found');
ctx.progress(70);
return {
stream: {
type: 'hls',
flags: [flags.NO_CORS],
captions: await getSubtitles(ctx, id, firstStream.fid, type as MediaTypes, season, episode),
playlist: `https://www.febbox.com/hls/main/${firstStream.oss_fid}.m3u8`,
},
};
},
});

View File

@@ -0,0 +1,50 @@
import { flags } from '@/main/targets';
import { makeEmbed } from '@/providers/base';
import { parseInputUrl } from '@/providers/embeds/febbox/common';
import { getStreamQualities } from '@/providers/embeds/febbox/qualities';
import { getSubtitles } from '@/providers/embeds/febbox/subtitles';
export const febboxMp4Scraper = makeEmbed({
id: 'febbox-mp4',
name: 'Febbox (MP4)',
rank: 190,
async scrape(ctx) {
const { type, id, season, episode } = parseInputUrl(ctx.url);
let apiQuery: object | null = null;
if (type === 'movie') {
apiQuery = {
uid: '',
module: 'Movie_downloadurl_v3',
mid: id,
oss: '1',
group: '',
};
} else if (type === 'show') {
apiQuery = {
uid: '',
module: 'TV_downloadurl_v3',
tid: id,
season,
episode,
oss: '1',
group: '',
};
}
if (!apiQuery) throw Error('Incorrect type');
const { qualities, fid } = await getStreamQualities(ctx, apiQuery);
if (fid === undefined) throw new Error('No streamable file found');
ctx.progress(70);
return {
stream: {
captions: await getSubtitles(ctx, id, fid, type, episode, season),
qualities,
type: 'file',
flags: [flags.NO_CORS],
},
};
},
});

View File

@@ -1,13 +1,11 @@
import { sendRequest } from '@/providers/sources/showbox/sendRequest';
import { StreamFile } from '@/providers/streams';
import { ScrapeContext } from '@/utils/context';
import { sendRequest } from './sendRequest';
const allowedQualities = ['360', '480', '720', '1080'];
const allowedQualities = ['360', '480', '720', '1080', '4k'];
export async function getStreamQualities(ctx: ScrapeContext, apiQuery: object) {
const mediaRes: { list: { path: string; quality: string; fid?: number }[] } = (await sendRequest(ctx, apiQuery)).data;
ctx.progress(66);
const qualityMap = mediaRes.list
.filter((file) => allowedQualities.includes(file.quality.replace('p', '')))

View File

@@ -1,9 +1,8 @@
import { Caption, getCaptionTypeFromUrl, isValidLanguageCode } from '@/providers/captions';
import { sendRequest } from '@/providers/sources/superstream/sendRequest';
import { captionsDomains } from '@/providers/sources/showbox/common';
import { sendRequest } from '@/providers/sources/showbox/sendRequest';
import { ScrapeContext } from '@/utils/context';
import { captionsDomains } from './common';
interface CaptionApiResponse {
data: {
list: {

View File

@@ -12,3 +12,5 @@ export const apiUrls = [
export const appKey = atob('bW92aWVib3g=');
export const appId = atob('Y29tLnRkby5zaG93Ym94');
export const captionsDomains = [atob('bWJwaW1hZ2VzLmNodWF4aW4uY29t'), atob('aW1hZ2VzLnNoZWd1Lm5ldA==')];
export const showboxBase = 'https://www.showbox.media';

View File

@@ -1,64 +1,53 @@
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 { SourcererOutput, makeSourcerer } from '@/providers/base';
import { febboxHlsScraper } from '@/providers/embeds/febbox/hls';
import { febboxMp4Scraper } from '@/providers/embeds/febbox/mp4';
import { compareTitle } from '@/utils/compare';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const showboxBase = `https://www.showbox.media`;
import { sendRequest } from './sendRequest';
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: {
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const searchQuery = {
module: 'Search4',
page: '1',
type: 'all',
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,
pagelimit: '20',
};
})
.find((v) => v && compareMedia(ctx.media, v.title, v.year ? v.year : undefined));
if (!result?.path) throw new NotFoundError('no result found');
const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list;
ctx.progress(50);
const febboxResult = await ctx.proxiedFetcher<{
data?: { link?: string };
}>('/index/share_link', {
baseUrl: showboxBase,
query: {
id: result.path.split('/')[3],
type: '1',
},
});
const showboxEntry = searchRes.find(
(res: any) => compareTitle(res.title, ctx.media.title) && res.year === Number(ctx.media.releaseYear),
);
if (!showboxEntry) throw new NotFoundError('No entry found');
if (!febboxResult?.data?.link) throw new NotFoundError('no result found');
const id = showboxEntry.id;
const season = ctx.media.type === 'show' ? ctx.media.season.number : '';
const episode = ctx.media.type === 'show' ? ctx.media.episode.number : '';
return {
embeds: [
{
embedId: febBoxScraper.id,
url: febboxResult.data.link,
embedId: febboxHlsScraper.id,
url: `/${ctx.media.type}/${id}/${season}/${episode}`,
},
{
embedId: febboxMp4Scraper.id,
url: `/${ctx.media.type}/${id}/${season}/${episode}`,
},
],
};
},
}
export const showboxScraper = makeSourcerer({
id: 'showbox',
name: 'Showbox',
rank: 300,
flags: [flags.NO_CORS],
scrapeShow: comboScraper,
scrapeMovie: comboScraper,
});

View File

@@ -1,106 +0,0 @@
import { flags } from '@/main/targets';
import { makeSourcerer } from '@/providers/base';
import { getSubtitles } from '@/providers/sources/superstream/subtitles';
import { compareTitle } from '@/utils/compare';
import { NotFoundError } from '@/utils/errors';
import { getStreamQualities } from './getStreamQualities';
import { sendRequest } from './sendRequest';
export const superStreamScraper = makeSourcerer({
id: 'superstream',
name: 'Superstream',
rank: 300,
flags: [flags.NO_CORS],
async scrapeShow(ctx) {
const searchQuery = {
module: 'Search4',
page: '1',
type: 'all',
keyword: ctx.media.title,
pagelimit: '20',
};
const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list;
ctx.progress(33);
const superstreamEntry = searchRes.find(
(res: any) => compareTitle(res.title, ctx.media.title) && res.year === Number(ctx.media.releaseYear),
);
if (!superstreamEntry) throw new NotFoundError('No entry found');
const superstreamId = superstreamEntry.id;
// Fetch requested episode
const apiQuery = {
uid: '',
module: 'TV_downloadurl_v3',
tid: superstreamId,
season: ctx.media.season.number,
episode: ctx.media.episode.number,
oss: '1',
group: '',
};
const { qualities, fid } = await getStreamQualities(ctx, apiQuery);
if (fid === undefined) throw new NotFoundError('No streamable file found');
return {
embeds: [],
stream: {
captions: await getSubtitles(
ctx,
superstreamId,
fid,
'show',
ctx.media.episode.number,
ctx.media.season.number,
),
qualities,
type: 'file',
flags: [flags.NO_CORS],
},
};
},
async scrapeMovie(ctx) {
const searchQuery = {
module: 'Search4',
page: '1',
type: 'all',
keyword: ctx.media.title,
pagelimit: '20',
};
const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list;
ctx.progress(33);
const superstreamEntry = searchRes.find(
(res: any) => compareTitle(res.title, ctx.media.title) && res.year === Number(ctx.media.releaseYear),
);
if (!superstreamEntry) throw new NotFoundError('No entry found');
const superstreamId = superstreamEntry.id;
// Fetch requested episode
const apiQuery = {
uid: '',
module: 'Movie_downloadurl_v3',
mid: superstreamId,
oss: '1',
group: '',
};
const { qualities, fid } = await getStreamQualities(ctx, apiQuery);
if (fid === undefined) throw new NotFoundError('No streamable file found');
return {
embeds: [],
stream: {
captions: await getSubtitles(ctx, superstreamId, fid, 'movie'),
qualities,
type: 'file',
flags: [flags.NO_CORS],
},
};
},
});

View File

@@ -7,7 +7,7 @@ export type StreamFile = {
headers?: Record<string, string>;
};
export type Qualities = 'unknown' | '360' | '480' | '720' | '1080';
export type Qualities = 'unknown' | '360' | '480' | '720' | '1080' | '4k';
export type FileBasedStream = {
type: 'file';