diff --git a/src/providers/all.ts b/src/providers/all.ts index 8314f30..40cf80a 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -35,6 +35,12 @@ import { autoembedTeluguScraper, } from './embeds/autoembed'; import { closeLoadScraper } from './embeds/closeload'; +import { + ConsumetStreamSBScraper, + ConsumetStreamTapeScraper, + ConsumetVidCloudScraper, + ConsumetVidStreamingScraper, +} from './embeds/consumet'; import { FedAPIPrivateScraper, FedDBScraper } from './embeds/fedapi'; import { mp4hydraServer1Scraper, mp4hydraServer2Scraper } from './embeds/mp4hydra'; import { ridooScraper } from './embeds/ridoo'; @@ -64,6 +70,7 @@ import { webtor1080Scraper, webtor480Scraper, webtor4kScraper, webtor720Scraper import { xprimeApolloEmbed, xprimeFoxEmbed, xprimeStreamboxEmbed } from './embeds/xprime'; import { EightStreamScraper } from './sources/8stream'; import { coitusScraper } from './sources/coitus'; +import { ConsumetScraper } from './sources/consumet'; import { embedsuScraper } from './sources/embedsu'; import { FedAPIScraper } from './sources/fedapi'; import { hdRezkaScraper } from './sources/hdrezka'; @@ -109,6 +116,7 @@ export function gatherAllSources(): Array { riveScraper, EightStreamScraper, xprimeScraper, + ConsumetScraper, ]; } @@ -169,5 +177,9 @@ export function gatherAllEmbeds(): Array { xprimeFoxEmbed, xprimeApolloEmbed, xprimeStreamboxEmbed, + ConsumetVidCloudScraper, + ConsumetStreamSBScraper, + ConsumetVidStreamingScraper, + ConsumetStreamTapeScraper, ]; } diff --git a/src/providers/embeds/consumet.ts b/src/providers/embeds/consumet.ts new file mode 100644 index 0000000..706eff5 --- /dev/null +++ b/src/providers/embeds/consumet.ts @@ -0,0 +1,152 @@ +/* eslint-disable no-console */ +import { flags } from '@/entrypoint/utils/targets'; +import { EmbedOutput, makeEmbed } from '@/providers/base'; +import { NotFoundError } from '@/utils/errors'; + +import { Caption } from '../captions'; + +interface StreamData { + headers: { + Referer: string; + Origin?: string; + }; + intro: { + start: number; + end: number; + }; + outro: { + start: number; + end: number; + }; + sources: Array<{ + url: string; + isM3U8: boolean; + type: string; + }>; + subtitles: Array<{ + url: string; + lang: string; + }>; +} + +const providers = [ + { + id: 'consumet-vidcloud', + rank: 405, + name: 'VidCloud', + server: 'vidcloud', + }, + { + id: 'consumet-streamsb', + rank: 404, + name: 'StreamSB', + server: 'streamsb', + disabled: true, + }, + { + id: 'consumet-vidstreaming', + rank: 403, + name: 'VidStreaming', + server: 'vidstreaming', + disabled: true, + }, + { + id: 'consumet-streamtape', + rank: 402, + name: 'StreamTape', + server: 'streamtape', + disabled: true, + }, +]; + +const languageMap: Record = { + English: 'en', + Spanish: 'es', + French: 'fr', + German: 'de', + Italian: 'it', + Portuguese: 'pt', + Arabic: 'ar', + Russian: 'ru', + Japanese: 'ja', + Korean: 'ko', + Chinese: 'zh', + Hindi: 'hi', + Turkish: 'tr', + Dutch: 'nl', + Polish: 'pl', + Swedish: 'sv', + Indonesian: 'id', + Thai: 'th', + Vietnamese: 'vi', +}; + +function embed(provider: { id: string; rank: number; name: string; server: string; disabled?: boolean }) { + return makeEmbed({ + id: provider.id, + name: provider.name, + rank: provider.rank, + disabled: provider.disabled, + async scrape(ctx): Promise { + const query = JSON.parse(ctx.url); + const apiUrl = `https://consumet.pstream.org/anime/zoro/watch?episodeId=${query.episodeId}&server=${provider.server}`; + + const data = await ctx.fetcher(apiUrl); + + if (!data?.sources?.length) { + throw new NotFoundError('No stream found'); + } + + ctx.progress(50); + + const captions: Caption[] = data.subtitles + .filter((sub) => sub.lang !== 'thumbnails') + .map((sub) => ({ + type: 'vtt', + id: sub.url, + url: sub.url, + language: languageMap[sub.lang] || 'unknown', + hasCorsRestrictions: false, + })); + + const streams = data.sources.reduce( + (acc, source) => { + if (source.isM3U8) { + acc.unknown = source.url; + } + return acc; + }, + {} as Record, + ); + + const thumbnailTrack = data.subtitles.find((sub) => sub.lang === 'thumbnails'); + + ctx.progress(90); + + return { + stream: [ + { + id: 'primary', + captions, + playlist: `https://proxy.fifthwit.net/m3u8-proxy?url=${encodeURIComponent(streams.unknown)}&headers=${encodeURIComponent(JSON.stringify({ ...(data.headers.Referer && { referer: data.headers.Referer }), ...(data.headers.Origin && { origin: data.headers.Origin }) }))}`, + type: 'hls', + flags: [flags.CORS_ALLOWED], + ...(thumbnailTrack && { + thumbnailTrack: { + type: 'vtt', + url: thumbnailTrack.url, + }, + }), + }, + ], + }; + }, + }); +} + +export const [ + ConsumetVidCloudScraper, + ConsumetStreamSBScraper, + ConsumetVidStreamingScraper, + ConsumetStreamTapeScraper, +] = providers.map(embed); diff --git a/src/providers/sources/consumet/index.ts b/src/providers/sources/consumet/index.ts new file mode 100644 index 0000000..295a00e --- /dev/null +++ b/src/providers/sources/consumet/index.ts @@ -0,0 +1,72 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { ShowScrapeContext } from '@/utils/context'; + +import { InfoResponse, SearchResponse } from './types'; + +async function consumetScraper(ctx: ShowScrapeContext): Promise { + // Search + const searchQuery = ctx.media.title; + const page = 1; + + const searchUrl = `https://consumet.pstream.org/anime/zoro/${encodeURIComponent(searchQuery)}?page=${page}`; + const searchResponse = await ctx.fetcher(searchUrl); + + if (!searchResponse?.results?.length) { + throw new Error('No results found'); + } + + const bestMatch = + searchResponse.results.find((result) => result.title.toLowerCase() === ctx.media.title.toLowerCase()) || + searchResponse.results[0]; + + // Get episode list + const infoUrl = `https://consumet.pstream.org/anime/zoro/info?id=${bestMatch.id}`; + const infoResponse = await ctx.fetcher(infoUrl); + + if (!infoResponse?.episodes?.length) { + throw new Error('No episodes found'); + } + + const targetEpisode = infoResponse.episodes.find((ep) => ep.number === ctx.media.episode.number); + + if (!targetEpisode) { + throw new Error('Episode not found'); + } + + // Parse embeds + const query = { + episodeId: `${bestMatch.id}$${ctx.media.season.number}$${targetEpisode.id}$both`, + }; + + const embeds = [ + { + embedId: 'consumet-vidcloud', + url: JSON.stringify({ ...query, server: 'vidcloud' }), + }, + { + embedId: 'consumet-streamsb', + url: JSON.stringify({ ...query, server: 'streamsb' }), + }, + { + embedId: 'consumet-vidstreaming', + url: JSON.stringify({ ...query, server: 'vidstreaming' }), + }, + { + embedId: 'consumet-streamtape', + url: JSON.stringify({ ...query, server: 'streamtape' }), + }, + ]; + + return { + embeds, + }; +} + +export const ConsumetScraper = makeSourcerer({ + id: 'consumet', + name: 'Zoro (Anime)', + rank: 4, + flags: [flags.CORS_ALLOWED], + scrapeShow: consumetScraper, +}); diff --git a/src/providers/sources/consumet/types.ts b/src/providers/sources/consumet/types.ts new file mode 100644 index 0000000..11bd741 --- /dev/null +++ b/src/providers/sources/consumet/types.ts @@ -0,0 +1,36 @@ +export interface SearchResult { + id: string; + title: string; + image: string; + releaseDate: string | null; + subOrDub: 'sub' | 'dub'; +} + +export interface SearchResponse { + totalPages: number; + currentPage: number; + hasNextPage: boolean; + results: SearchResult[]; +} + +export interface Episode { + id: string; + number: number; + url: string; +} + +export interface InfoResponse { + id: string; + title: string; + url: string; + image: string; + releaseDate: string | null; + description: string | null; + genres: string[]; + subOrDub: 'sub' | 'dub'; + type: string | null; + status: string; + otherName: string | null; + totalEpisodes: number; + episodes: Episode[]; +} diff --git a/src/utils/valid.ts b/src/utils/valid.ts index 66d906b..69c063b 100644 --- a/src/utils/valid.ts +++ b/src/utils/valid.ts @@ -1,5 +1,11 @@ // import { alphaScraper, deltaScraper } from '@/providers/embeds/nsbx'; // import { astraScraper, novaScraper, orionScraper } from '@/providers/embeds/whvx'; +import { + ConsumetStreamSBScraper, + ConsumetStreamTapeScraper, + ConsumetVidCloudScraper, + ConsumetVidStreamingScraper, +} from '@/providers/embeds/consumet'; import { FedAPIPrivateScraper, FedDBScraper } from '@/providers/embeds/fedapi'; import { warezcdnembedMp4Scraper } from '@/providers/embeds/warezcdn/mp4'; import { xprimeApolloEmbed, xprimeFoxEmbed, xprimeStreamboxEmbed } from '@/providers/embeds/xprime'; @@ -23,6 +29,10 @@ const SKIP_VALIDATION_CHECK_IDS = [ xprimeFoxEmbed.id, xprimeApolloEmbed.id, xprimeStreamboxEmbed.id, + ConsumetVidCloudScraper.id, + ConsumetStreamSBScraper.id, + ConsumetVidStreamingScraper.id, + ConsumetStreamTapeScraper.id, ]; export function isValidStream(stream: Stream | undefined): boolean {