added zoechip source provider
This commit is contained in:
parent
adf9c2b09a
commit
610aee16df
|
@ -9,10 +9,11 @@ import { goMoviesScraper } from '@/providers/sources/gomovies/index';
|
||||||
import { kissAsianScraper } from '@/providers/sources/kissasian/index';
|
import { kissAsianScraper } from '@/providers/sources/kissasian/index';
|
||||||
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 { zoechipScraper } from '@/providers/sources/zoechip';
|
||||||
|
|
||||||
export function gatherAllSources(): Array<Sourcerer> {
|
export function gatherAllSources(): Array<Sourcerer> {
|
||||||
// all sources are gathered here
|
// all sources are gathered here
|
||||||
return [flixhqScraper, remotestreamScraper, kissAsianScraper, superStreamScraper, goMoviesScraper];
|
return [flixhqScraper, remotestreamScraper, kissAsianScraper, superStreamScraper, goMoviesScraper, zoechipScraper];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gatherAllEmbeds(): Array<Embed> {
|
export function gatherAllEmbeds(): Array<Embed> {
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { MovieMedia, ShowMedia } from '@/main/media';
|
||||||
|
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
||||||
|
import { upcloudScraper } from '@/providers/embeds/upcloud';
|
||||||
|
import { upstreamScraper } from '@/providers/embeds/upstream';
|
||||||
|
import { getZoeChipSourceURL, getZoeChipSources } from '@/providers/sources/zoechip/scrape';
|
||||||
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
export const zoeBase = 'https://zoechip.cc';
|
||||||
|
|
||||||
|
export type MovieContext = ScrapeContext & {
|
||||||
|
media: MovieMedia;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShowContext = ScrapeContext & {
|
||||||
|
media: ShowMedia;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ZoeChipSourceDetails = {
|
||||||
|
type: string; // Only seen "iframe" so far
|
||||||
|
link: string;
|
||||||
|
sources: string[]; // Never seen this populated, assuming it's a string array
|
||||||
|
tracks: string[]; // Never seen this populated, assuming it's a string array
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function formatSource(ctx: MovieContext | ShowContext, source: { embed: string; episodeId: string }) {
|
||||||
|
const link = await getZoeChipSourceURL(ctx, source.episodeId);
|
||||||
|
if (link) {
|
||||||
|
const embed = {
|
||||||
|
embedId: '',
|
||||||
|
url: link,
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedUrl = new URL(link);
|
||||||
|
|
||||||
|
switch (parsedUrl.host) {
|
||||||
|
case 'rabbitstream.net':
|
||||||
|
embed.embedId = upcloudScraper.id;
|
||||||
|
break;
|
||||||
|
case 'upstream.to':
|
||||||
|
embed.embedId = upstreamScraper.id;
|
||||||
|
break;
|
||||||
|
case 'mixdrop.co':
|
||||||
|
embed.embedId = mixdropScraper.id;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Failed to find ZoeChip embed source for ${link}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createZoeChipStreamData(ctx: MovieContext | ShowContext, id: string) {
|
||||||
|
const sources = await getZoeChipSources(ctx, id);
|
||||||
|
const embeds: {
|
||||||
|
embedId: string;
|
||||||
|
url: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
const formatted = await formatSource(ctx, source);
|
||||||
|
if (formatted) {
|
||||||
|
embeds.push(formatted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { flags } from '@/main/targets';
|
||||||
|
import { makeSourcerer } from '@/providers/base';
|
||||||
|
import { scrapeMovie } from '@/providers/sources/zoechip/scrape-movie';
|
||||||
|
import { scrapeShow } from '@/providers/sources/zoechip/scrape-show';
|
||||||
|
|
||||||
|
export const zoechipScraper = makeSourcerer({
|
||||||
|
id: 'zoechip',
|
||||||
|
name: 'ZoeChip',
|
||||||
|
rank: 110,
|
||||||
|
flags: [flags.NO_CORS],
|
||||||
|
scrapeMovie,
|
||||||
|
scrapeShow,
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { MovieContext, createZoeChipStreamData } from '@/providers/sources/zoechip/common';
|
||||||
|
import { getZoeChipMovieID } from '@/providers/sources/zoechip/search';
|
||||||
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
export async function scrapeMovie(ctx: MovieContext) {
|
||||||
|
const movieID = await getZoeChipMovieID(ctx, ctx.media);
|
||||||
|
if (!movieID) {
|
||||||
|
throw new NotFoundError('no search results match');
|
||||||
|
}
|
||||||
|
|
||||||
|
return createZoeChipStreamData(ctx, movieID);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ShowContext, createZoeChipStreamData } from '@/providers/sources/zoechip/common';
|
||||||
|
import { getZoeChipEpisodeID, getZoeChipSeasonID } from '@/providers/sources/zoechip/scrape';
|
||||||
|
import { getZoeChipShowID } from '@/providers/sources/zoechip/search';
|
||||||
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
export async function scrapeShow(ctx: ShowContext) {
|
||||||
|
const showID = await getZoeChipShowID(ctx, ctx.media);
|
||||||
|
if (!showID) {
|
||||||
|
throw new NotFoundError('no search results match');
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonID = await getZoeChipSeasonID(ctx, ctx.media, showID);
|
||||||
|
if (!seasonID) {
|
||||||
|
throw new NotFoundError('no season found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodeID = await getZoeChipEpisodeID(ctx, ctx.media, seasonID);
|
||||||
|
if (!episodeID) {
|
||||||
|
throw new NotFoundError('no episode found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return createZoeChipStreamData(ctx, episodeID);
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
|
import { ShowMedia } from '@/main/media';
|
||||||
|
import { MovieContext, ShowContext, ZoeChipSourceDetails, zoeBase } from '@/providers/sources/zoechip/common';
|
||||||
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
export async function getZoeChipSources(ctx: MovieContext | ShowContext, id: string) {
|
||||||
|
// Movies use /ajax/episode/list/ID
|
||||||
|
// Shows use /ajax/episode/servers/ID
|
||||||
|
const endpoint = ctx.media.type === 'movie' ? 'list' : 'servers';
|
||||||
|
const html = await ctx.proxiedFetcher<string>(`/ajax/episode/${endpoint}/${id}`, {
|
||||||
|
baseUrl: zoeBase,
|
||||||
|
});
|
||||||
|
const $ = load(html);
|
||||||
|
|
||||||
|
return $('.nav-item a')
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
// Movies use data-linkid
|
||||||
|
// Shows use data-id
|
||||||
|
const idAttribute = ctx.media.type === 'movie' ? 'data-linkid' : 'data-id';
|
||||||
|
const element = $(el);
|
||||||
|
const embedTitle = element.attr('title');
|
||||||
|
const linkId = element.attr(idAttribute);
|
||||||
|
|
||||||
|
if (!embedTitle || !linkId) {
|
||||||
|
throw new Error('invalid sources');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
embed: embedTitle,
|
||||||
|
episodeId: linkId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getZoeChipSourceURL(ctx: ScrapeContext, sourceID: string): Promise<string | null> {
|
||||||
|
const details = await ctx.proxiedFetcher<ZoeChipSourceDetails>(`/ajax/sources/${sourceID}`, {
|
||||||
|
baseUrl: zoeBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO - Support non-iframe sources
|
||||||
|
if (details.type !== 'iframe') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - Extract the other data from the source
|
||||||
|
|
||||||
|
return details.link;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getZoeChipSeasonID(ctx: ScrapeContext, media: ShowMedia, showID: string): Promise<string | null> {
|
||||||
|
const html = await ctx.proxiedFetcher<string>(`/ajax/season/list/${showID}`, {
|
||||||
|
baseUrl: zoeBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
const $ = load(html);
|
||||||
|
|
||||||
|
const seasons = $('.dropdown-menu a')
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
const element = $(el);
|
||||||
|
const seasonID = element.attr('data-id');
|
||||||
|
const seasonNumber = element.html()?.split(' ')[1];
|
||||||
|
|
||||||
|
if (!seasonID || !seasonNumber || Number.isNaN(Number(seasonNumber))) {
|
||||||
|
throw new Error('invalid season');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: seasonID,
|
||||||
|
season: Number(seasonNumber),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const season of seasons) {
|
||||||
|
if (season.season === media.season.number) {
|
||||||
|
return season.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getZoeChipEpisodeID(
|
||||||
|
ctx: ScrapeContext,
|
||||||
|
media: ShowMedia,
|
||||||
|
seasonID: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const episodeNumberRegex = /Eps (\d*):/;
|
||||||
|
const html = await ctx.proxiedFetcher<string>(`/ajax/season/episodes/${seasonID}`, {
|
||||||
|
baseUrl: zoeBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
const $ = load(html);
|
||||||
|
|
||||||
|
const episodes = $('.eps-item')
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
const element = $(el);
|
||||||
|
const episodeID = element.attr('data-id');
|
||||||
|
const title = element.attr('title');
|
||||||
|
|
||||||
|
if (!episodeID || !title) {
|
||||||
|
throw new Error('invalid episode');
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexResult = title.match(episodeNumberRegex);
|
||||||
|
if (!regexResult || Number.isNaN(Number(regexResult[1]))) {
|
||||||
|
throw new Error('invalid episode');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: episodeID,
|
||||||
|
episode: Number(regexResult[1]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const episode of episodes) {
|
||||||
|
if (episode.episode === media.episode.number) {
|
||||||
|
return episode.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
|
import { MovieMedia, ShowMedia } from '@/main/media';
|
||||||
|
import { zoeBase } from '@/providers/sources/zoechip/common';
|
||||||
|
import { compareMedia } from '@/utils/compare';
|
||||||
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
export async function getZoeChipSearchResults(ctx: ScrapeContext, media: MovieMedia | ShowMedia) {
|
||||||
|
const titleCleaned = media.title.toLocaleLowerCase().replace(/ /g, '-');
|
||||||
|
|
||||||
|
const html = await ctx.proxiedFetcher<string>(`/search/${titleCleaned}`, {
|
||||||
|
baseUrl: zoeBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
const $ = load(html);
|
||||||
|
return $('.film_list-wrap .flw-item .film-detail')
|
||||||
|
.toArray()
|
||||||
|
.map((element) => {
|
||||||
|
const movie = $(element);
|
||||||
|
const anchor = movie.find('.film-name a');
|
||||||
|
const info = movie.find('.fd-infor');
|
||||||
|
|
||||||
|
const title = anchor.attr('title');
|
||||||
|
const href = anchor.attr('href');
|
||||||
|
const type = info.find('.fdi-type').html();
|
||||||
|
let year = info.find('.fdi-item').html();
|
||||||
|
const id = href?.split('-').pop();
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!href) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TV shows on ZoeChip do not have a year in their search results
|
||||||
|
// Allow TV shows to pass this failure
|
||||||
|
if (!year || Number.isNaN(Number(year))) {
|
||||||
|
if (type === 'TV') {
|
||||||
|
year = '0';
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
year: Number(year),
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
href,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getZoeChipMovieID(ctx: ScrapeContext, media: MovieMedia): Promise<string | null> {
|
||||||
|
const searchResults = await getZoeChipSearchResults(ctx, media);
|
||||||
|
|
||||||
|
const matchingItem = searchResults.find((v) => v && v.type === 'Movie' && compareMedia(media, v.title, v.year));
|
||||||
|
|
||||||
|
if (!matchingItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingItem.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getZoeChipShowID(ctx: ScrapeContext, media: ShowMedia): Promise<string | null> {
|
||||||
|
// ZoeChip TV shows don't have a year on their search results
|
||||||
|
// This makes it hard to filter between shows with the same name
|
||||||
|
// To find the year, we must query each shows details page
|
||||||
|
// This is slower, but more reliable
|
||||||
|
|
||||||
|
const releasedRegex = /<\/strong><\/span> (\d.*)-\d.*-\d.*/;
|
||||||
|
const searchResults = await getZoeChipSearchResults(ctx, media);
|
||||||
|
|
||||||
|
// Since we don't have a year here, force them to be the same. Only compare titles
|
||||||
|
const filtered = searchResults.filter((v) => v && v.type === 'TV' && compareMedia(media, v.title, media.releaseYear));
|
||||||
|
|
||||||
|
for (const result of filtered) {
|
||||||
|
// This gets filtered above but the linter Gods don't think so
|
||||||
|
if (!result) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await ctx.proxiedFetcher<string>(result.href, {
|
||||||
|
baseUrl: zoeBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The HTML is not structured in a way that makes using Cheerio clean
|
||||||
|
// There are no unique IDs or classes to query, resulting in long ugly queries
|
||||||
|
// Regex is faster and cleaner in this case
|
||||||
|
const regexResult = html.match(releasedRegex);
|
||||||
|
if (regexResult) {
|
||||||
|
const year = Number(regexResult[1]);
|
||||||
|
if (!Number.isNaN(year) && year === media.releaseYear) {
|
||||||
|
return result.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
Loading…
Reference in New Issue