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 { remotestreamScraper } from '@/providers/sources/remotestream';
|
||||
import { superStreamScraper } from '@/providers/sources/superstream/index';
|
||||
import { zoechipScraper } from '@/providers/sources/zoechip';
|
||||
|
||||
export function gatherAllSources(): Array<Sourcerer> {
|
||||
// all sources are gathered here
|
||||
return [flixhqScraper, remotestreamScraper, kissAsianScraper, superStreamScraper, goMoviesScraper];
|
||||
return [flixhqScraper, remotestreamScraper, kissAsianScraper, superStreamScraper, goMoviesScraper, zoechipScraper];
|
||||
}
|
||||
|
||||
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