diff --git a/src/providers/.DS_Store b/src/providers/.DS_Store new file mode 100644 index 0000000..5a0f80a Binary files /dev/null and b/src/providers/.DS_Store differ diff --git a/src/providers/all.ts b/src/providers/all.ts index 9f87249..fe9ff4e 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -12,6 +12,7 @@ import { mp4hydraScraper } from '@/providers/sources/mp4hydra'; import { tugaflixScraper } from '@/providers/sources/tugaflix'; import { vidsrcScraper } from '@/providers/sources/vidsrc'; import { vidsrcsuScraper } from '@/providers/sources/vidsrcsu'; +import { zoechipScraper } from '@/providers/sources/zoechip'; import { autoembedBengaliScraper, @@ -86,6 +87,7 @@ export function gatherAllSources(): Array { fsharetvScraper, vidsrcsuScraper, vidsrcScraper, + zoechipScraper, mp4hydraScraper, embedsuScraper, slidemoviesScraper, diff --git a/src/providers/archive/sources/.DS_Store b/src/providers/archive/sources/.DS_Store index c7652e5..ec9676c 100644 Binary files a/src/providers/archive/sources/.DS_Store and b/src/providers/archive/sources/.DS_Store differ diff --git a/src/providers/archive/sources/zoechip/common.ts b/src/providers/archive/sources/zoechip/common.ts deleted file mode 100644 index 7c18e75..0000000 --- a/src/providers/archive/sources/zoechip/common.ts +++ /dev/null @@ -1,71 +0,0 @@ -// import { mixdropScraper } from '@/providers/embeds/mixdrop'; -// import { upcloudScraper } from '@/providers/embeds/upcloud'; -// import { upstreamScraper } from '@/providers/embeds/upstream'; -// import { vidCloudScraper } from '@/providers/embeds/vidcloud'; -// import { getZoeChipSourceURL, getZoeChipSources } from '@/providers/sources/zoechip/scrape'; -// import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; - -// export const zoeBase = 'https://zoechip.cc'; - -// 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: MovieScrapeContext | ShowScrapeContext, -// 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: -// return null; -// } - -// return embed; -// } -// } - -// export async function createZoeChipStreamData(ctx: MovieScrapeContext | ShowScrapeContext, 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) { -// // Zoechip does not return titles for their sources, so we can not check if a source is upcloud or vidcloud because the domain is the same. -// const upCloudAlreadyExists = embeds.find((e) => e.embedId === upcloudScraper.id); -// if (formatted.embedId === upcloudScraper.id && upCloudAlreadyExists) { -// formatted.embedId = vidCloudScraper.id; -// } -// embeds.push(formatted); -// } -// } - -// return { -// embeds, -// }; -// } diff --git a/src/providers/archive/sources/zoechip/index.ts b/src/providers/archive/sources/zoechip/index.ts deleted file mode 100644 index 7e0d250..0000000 --- a/src/providers/archive/sources/zoechip/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -// import { flags } from '@/entrypoint/utils/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: 240, -// flags: [flags.CORS_ALLOWED], -// disabled: true, -// scrapeMovie, -// scrapeShow, -// }); diff --git a/src/providers/archive/sources/zoechip/scrape-movie.ts b/src/providers/archive/sources/zoechip/scrape-movie.ts deleted file mode 100644 index c157d7b..0000000 --- a/src/providers/archive/sources/zoechip/scrape-movie.ts +++ /dev/null @@ -1,13 +0,0 @@ -// import { createZoeChipStreamData } from '@/providers/sources/zoechip/common'; -// import { getZoeChipMovieID } from '@/providers/sources/zoechip/search'; -// import { MovieScrapeContext } from '@/utils/context'; -// import { NotFoundError } from '@/utils/errors'; - -// export async function scrapeMovie(ctx: MovieScrapeContext) { -// const movieID = await getZoeChipMovieID(ctx, ctx.media); -// if (!movieID) { -// throw new NotFoundError('no search results match'); -// } - -// return createZoeChipStreamData(ctx, movieID); -// } diff --git a/src/providers/archive/sources/zoechip/scrape-show.ts b/src/providers/archive/sources/zoechip/scrape-show.ts deleted file mode 100644 index f264c26..0000000 --- a/src/providers/archive/sources/zoechip/scrape-show.ts +++ /dev/null @@ -1,24 +0,0 @@ -// import { createZoeChipStreamData } from '@/providers/sources/zoechip/common'; -// import { getZoeChipEpisodeID, getZoeChipSeasonID } from '@/providers/sources/zoechip/scrape'; -// import { getZoeChipShowID } from '@/providers/sources/zoechip/search'; -// import { ShowScrapeContext } from '@/utils/context'; -// import { NotFoundError } from '@/utils/errors'; - -// export async function scrapeShow(ctx: ShowScrapeContext) { -// 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); -// } diff --git a/src/providers/archive/sources/zoechip/scrape.ts b/src/providers/archive/sources/zoechip/scrape.ts deleted file mode 100644 index 8ba3e58..0000000 --- a/src/providers/archive/sources/zoechip/scrape.ts +++ /dev/null @@ -1,126 +0,0 @@ -// import { load } from 'cheerio'; - -// import { ShowMedia } from '@/entrypoint/utils/media'; -// import { ZoeChipSourceDetails, zoeBase } from '@/providers/sources/zoechip/common'; -// import { MovieScrapeContext, ScrapeContext, ShowScrapeContext } from '@/utils/context'; - -// export async function getZoeChipSources(ctx: MovieScrapeContext | ShowScrapeContext, 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(`/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 { -// const details = await ctx.proxiedFetcher(`/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 { -// const html = await ctx.proxiedFetcher(`/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), -// }; -// }); - -// const foundSeason = seasons.find((season) => season.season === media.season.number); - -// if (!foundSeason) { -// return null; -// } - -// return foundSeason.id; -// } - -// export async function getZoeChipEpisodeID( -// ctx: ScrapeContext, -// media: ShowMedia, -// seasonID: string, -// ): Promise { -// const episodeNumberRegex = /Eps (\d*):/; -// const html = await ctx.proxiedFetcher(`/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]), -// }; -// }); - -// const foundEpisode = episodes.find((episode) => episode.episode === media.episode.number); - -// if (!foundEpisode) { -// return null; -// } - -// return foundEpisode.id; -// } diff --git a/src/providers/archive/sources/zoechip/search.ts b/src/providers/archive/sources/zoechip/search.ts deleted file mode 100644 index 4455cc8..0000000 --- a/src/providers/archive/sources/zoechip/search.ts +++ /dev/null @@ -1,111 +0,0 @@ -// import { load } from 'cheerio'; - -// import { MovieMedia, ShowMedia } from '@/entrypoint/utils/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(`/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 { -// 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 { -// // 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); - -// const filtered = searchResults.filter((v) => v && v.type === 'TV' && compareMedia(media, v.title)); - -// 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(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) && compareMedia(media, result.title, year)) { -// return result.id; -// } -// } -// } - -// return null; -// } diff --git a/src/providers/sources/.DS_Store b/src/providers/sources/.DS_Store index 08f4a9f..d752853 100644 Binary files a/src/providers/sources/.DS_Store and b/src/providers/sources/.DS_Store differ diff --git a/src/providers/sources/zoechip.ts b/src/providers/sources/zoechip.ts new file mode 100644 index 0000000..311e2b0 --- /dev/null +++ b/src/providers/sources/zoechip.ts @@ -0,0 +1,241 @@ +import { load } from 'cheerio'; +import { unpack } from 'unpacker'; + +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const zoeBase = 'https://zoechip.org'; + +function createSlug(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim(); +} + +async function extractFileFromFilemoon( + ctx: MovieScrapeContext | ShowScrapeContext, + filemoonUrl: string, +): Promise { + const headers = { + Referer: zoeBase, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + }; + + try { + // console.log(`Extracting from Filemoon URL: ${filemoonUrl}`); + + // Follow redirects to get the actual iframe URL + const redirectResponse = await ctx.proxiedFetcher.full(filemoonUrl, { + method: 'HEAD', + headers, + }); + + const redirectUrl = redirectResponse.finalUrl; + // console.log(`Redirect URL: ${redirectUrl}`); + + if (!redirectUrl) { + // console.log('No redirect URL found'); + return null; + } + + // Get the redirect page content + const redirectHtml = await ctx.proxiedFetcher(redirectUrl, { + headers, + }); + + const redirectCheerio = load(redirectHtml); + const iframeUrl = redirectCheerio('iframe').attr('src'); + // console.log(`Iframe URL: ${iframeUrl}`); + + if (!iframeUrl) { + // console.log('No iframe URL found'); + throw new NotFoundError('No iframe URL found'); + } + + // Fetch the iframe content + const iframeHtml = await ctx.proxiedFetcher(iframeUrl, { + headers, + }); + + // Extract the packed JavaScript code + const evalMatch = iframeHtml.match(/eval\(function\(p,a,c,k,e,.*\)\)/i); + if (!evalMatch) { + // console.log('No packed JavaScript found'); + throw new NotFoundError('No packed JavaScript found'); + } + + // console.log('Found packed JavaScript, unpacking...'); + + // Unpack the JavaScript + const unpacked = unpack(evalMatch[0]); + // console.log(`Unpacked JavaScript (first 200 chars): ${unpacked.substring(0, 200)}`); + + // Extract the file URL from the unpacked code + const fileMatch = unpacked.match(/file\s*:\s*"([^"]+)"/i); + if (!fileMatch) { + // console.log('No file URL found in unpacked JavaScript'); + throw new NotFoundError('No file URL found in unpacked JavaScript'); + } + + const fileUrl = fileMatch[1]; + // console.log(`Extracted file URL: ${fileUrl}`); + return fileUrl; + } catch (error) { + // console.error('Error in extractFileFromFilemoon:', error); + throw new NotFoundError('Failed to extract file URL from streaming server'); + } +} + +async function comboScraper(ctx: MovieScrapeContext | ShowScrapeContext): Promise { + const headers = { + Referer: zoeBase, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + }; + + // console.log(`Starting scrape for ${ctx.media.type}: ${ctx.media.title}`); + + let url: string; + let movieId: string | undefined; + + try { + // Construct URLs based on media type + if (ctx.media.type === 'movie') { + const slug = createSlug(ctx.media.title); + url = `${zoeBase}/film/${slug}-${ctx.media.releaseYear}`; + // console.log(`Movie URL: ${url}`); + } else { + const slug = createSlug(ctx.media.title); + url = `${zoeBase}/episode/${slug}-season-${ctx.media.season.number}-episode-${ctx.media.episode.number}`; + // console.log(`Show URL: ${url}`); + } + + ctx.progress(20); + + // Get the page and extract movie ID + const html = await ctx.proxiedFetcher(url, { headers }); + const $ = load(html); + + movieId = $('div#show_player_ajax').attr('movie-id'); + // console.log(`Movie ID: ${movieId}`); + + if (!movieId) { + // Try alternative methods to find content + // console.log('No movie ID found, trying alternative search...'); + + // Look for other possible IDs + const altId = + $('[data-movie-id]').attr('data-movie-id') || + $('[movie-id]').attr('movie-id') || + $('.player-wrapper').attr('data-id'); + + if (altId) { + movieId = altId; + // console.log(`Found alternative ID: ${movieId}`); + } else { + throw new NotFoundError(`No content found for ${ctx.media.type === 'movie' ? 'movie' : 'episode'}`); + } + } + + ctx.progress(40); + + // Make AJAX request to get sources + const ajaxUrl = `${zoeBase}/wp-admin/admin-ajax.php`; + const ajaxHeaders = { + ...headers, + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: url, + }; + + const body = new URLSearchParams({ + action: 'lazy_player', + movieID: movieId, + }); + + // console.log('Making AJAX request for sources...'); + const ajaxHtml = await ctx.proxiedFetcher(ajaxUrl, { + method: 'POST', + headers: ajaxHeaders, + body: body.toString(), + }); + + const $ajax = load(ajaxHtml); + const filemoonUrl = $ajax('ul.nav a:contains(Filemoon)').attr('data-server'); + // console.log(`Filemoon URL: ${filemoonUrl}`); + + if (!filemoonUrl) { + // Try to find other available servers + const allServers = $ajax('ul.nav a') + .map((_, el) => ({ + name: $ajax(el).text().trim(), + url: $ajax(el).attr('data-server'), + })) + .get(); + + // console.log('Available servers:', allServers); + + if (allServers.length === 0) { + throw new NotFoundError('No streaming servers found'); + } + + throw new NotFoundError('Filemoon server not available'); + } + + ctx.progress(60); + + // Extract file URL from Filemoon + const fileUrl = await extractFileFromFilemoon(ctx, filemoonUrl); + if (!fileUrl) { + throw new NotFoundError('Failed to extract file URL from streaming server'); + } + + ctx.progress(90); + + return { + stream: [ + { + id: 'primary', + type: 'hls' as const, + playlist: fileUrl, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + embeds: [], + }; + } catch (error) { + // console.error('Error during scraping:', error); + + if (error instanceof NotFoundError) { + throw error; + } + + // Provide more specific error messages + if (error instanceof Error) { + if (error.message.includes('fetch')) { + throw new NotFoundError('Failed to connect to ZoeChip'); + } + if (error.message.includes('timeout')) { + throw new NotFoundError('Request timed out'); + } + } + + throw new NotFoundError(`ZoeChip scraping failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} + +export const zoechipScraper = makeSourcerer({ + id: 'zoechip', + name: 'ZoeChip', + rank: 170, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +});