diff --git a/src/providers/embeds/hianime.ts b/src/providers/embeds/hianime.ts index 4d49f48..71efd2c 100644 --- a/src/providers/embeds/hianime.ts +++ b/src/providers/embeds/hianime.ts @@ -4,7 +4,7 @@ import { NotFoundError } from '@/utils/errors'; export const hianimeHd1DubEmbed = makeEmbed({ id: 'hianime-hd1-dub', - name: 'Hianime HD-1 (Dub)', + name: 'HD-1 (Dub)', disabled: true, rank: 250, async scrape(ctx): Promise { @@ -42,7 +42,7 @@ export const hianimeHd1DubEmbed = makeEmbed({ export const hianimeHd2DubEmbed = makeEmbed({ id: 'hianime-hd2-dub', - name: 'Hianime HD-2 (Dub)', + name: 'HD-2 (Dub)', rank: 251, async scrape(ctx): Promise { const query = JSON.parse(ctx.url); @@ -55,11 +55,34 @@ export const hianimeHd2DubEmbed = makeEmbed({ const thumbnailTrack = data.data.tracks?.find((track: { kind: string }) => track.kind === 'thumbnails')?.file; return { + stream: [ + { + type: 'hls', + id: 'primary', + playlist: `https://proxy-m3u8.uira.live/m3u8-proxy?url=${data.data.sources[0].url}&headers=${encodeURIComponent(JSON.stringify(data.data.headers))}`, + flags: [flags.CORS_ALLOWED], + captions: [], + ...(thumbnailTrack + ? { + thumbnailTrack: { + type: 'vtt', + url: thumbnailTrack, + }, + } + : {}), + // headers: data.data.headers, + }, + ], // stream: [ // { - // type: 'hls', + // type: 'file', // id: 'primary', - // playlist: data.data.sources[0].url, + // qualities: { + // unknown: { + // type: 'mp4', + // url: data.data.sources[0].url, + // }, + // }, // flags: [flags.CORS_ALLOWED], // captions: [], // ...(thumbnailTrack @@ -73,36 +96,13 @@ export const hianimeHd2DubEmbed = makeEmbed({ // headers: data.data.headers, // }, // ], - stream: [ - { - type: 'file', - id: 'primary', - qualities: { - unknown: { - type: 'mp4', - url: data.data.sources[0].url, - }, - }, - flags: [flags.CORS_ALLOWED], - captions: [], - ...(thumbnailTrack - ? { - thumbnailTrack: { - type: 'vtt', - url: thumbnailTrack, - }, - } - : {}), - headers: data.data.headers, - }, - ], }; }, }); export const hianimeHd1SubEmbed = makeEmbed({ id: 'hianime-hd1-sub', - name: 'Hianime HD-1 (Sub)', + name: 'HD-1 (Sub)', disabled: true, rank: 252, async scrape(ctx): Promise { @@ -140,7 +140,7 @@ export const hianimeHd1SubEmbed = makeEmbed({ export const hianimeHd2SubEmbed = makeEmbed({ id: 'hianime-hd2-sub', - name: 'Hianime HD-2 (Sub)', + name: 'HD-2 (Sub)', rank: 253, async scrape(ctx): Promise { const query = JSON.parse(ctx.url); @@ -153,11 +153,34 @@ export const hianimeHd2SubEmbed = makeEmbed({ const thumbnailTrack = data.data.tracks?.find((track: { kind: string }) => track.kind === 'thumbnails')?.file; return { + stream: [ + { + type: 'hls', + id: 'primary', + playlist: `https://proxy-m3u8.uira.live/m3u8-proxy?url=${data.data.sources[0].url}&headers=${encodeURIComponent(JSON.stringify(data.data.headers))}`, + flags: [flags.CORS_ALLOWED], + captions: [], + ...(thumbnailTrack + ? { + thumbnailTrack: { + type: 'vtt', + url: thumbnailTrack, + }, + } + : {}), + // headers: data.data.headers, + }, + ], // stream: [ // { - // type: 'hls', + // type: 'file', // id: 'primary', - // playlist: data.data.sources[0].url, + // qualities: { + // unknown: { + // type: 'mp4', + // url: data.data.sources[0].url, + // }, + // }, // flags: [flags.CORS_ALLOWED], // captions: [], // ...(thumbnailTrack @@ -171,29 +194,6 @@ export const hianimeHd2SubEmbed = makeEmbed({ // headers: data.data.headers, // }, // ], - stream: [ - { - type: 'file', - id: 'primary', - qualities: { - unknown: { - type: 'mp4', - url: data.data.sources[0].url, - }, - }, - flags: [flags.CORS_ALLOWED], - captions: [], - ...(thumbnailTrack - ? { - thumbnailTrack: { - type: 'vtt', - url: thumbnailTrack, - }, - } - : {}), - headers: data.data.headers, - }, - ], }; }, }); diff --git a/src/providers/sources/consumet/index.ts b/src/providers/sources/consumet/index.ts index 295a00e..18cdb4d 100644 --- a/src/providers/sources/consumet/index.ts +++ b/src/providers/sources/consumet/index.ts @@ -65,7 +65,7 @@ async function consumetScraper(ctx: ShowScrapeContext): Promise export const ConsumetScraper = makeSourcerer({ id: 'consumet', - name: 'Zoro (Anime)', + name: 'Consumet (Anime)', rank: 4, flags: [flags.CORS_ALLOWED], scrapeShow: consumetScraper, diff --git a/src/providers/sources/hianime.ts b/src/providers/sources/hianime.ts index 9e00686..b32c7ff 100644 --- a/src/providers/sources/hianime.ts +++ b/src/providers/sources/hianime.ts @@ -1,6 +1,8 @@ +/* eslint-disable no-console */ import { flags } from '@/entrypoint/utils/targets'; import { SourcererOutput, makeSourcerer } from '@/providers/base'; import { ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; interface HianimeSearchResult { success: boolean; @@ -32,25 +34,59 @@ async function searchAnime(title: string): Promise { throw new Error('Anime not found'); } - // Return the ID of the first matching anime - return data.data.animes[0].id; + // Try to find exact match (case-insensitive) + const match = data.data.animes.find((anime) => anime.name.toLowerCase() === title.toLowerCase()); + + // Return the matched ID or fallback to the first result + return match?.id ?? data.data.animes[0].id; +} + +async function fetchTmdbSeasonEpisodes(tmdbShowId: string, seasonNumber: number): Promise { + const apiKey = '5b9790d9305dca8713b9a0afad42ea8d'; // plz dont abuse + const response = await fetch( + `https://api.themoviedb.org/3/tv/${tmdbShowId}/season/${seasonNumber}?api_key=${apiKey}`, + ); + if (!response.ok) throw new NotFoundError('Failed to fetch season data from TMDB'); + const data = await response.json(); + return data.episodes; // each item has 'episode_number' and 'season_number' +} + +async function calculateAbsoluteEpisodeNumber( + tmdbShowId: string, + seasonNumber: number, + episodeNumber: number, +): Promise { + const previousSeasons = await Promise.all( + Array.from({ length: seasonNumber - 1 }, (_, i) => fetchTmdbSeasonEpisodes(tmdbShowId, i + 1)), + ); + + const episodesBefore = previousSeasons.reduce((sum, seasonEpisodes) => sum + seasonEpisodes.length, 0); + + return episodesBefore + episodeNumber; } async function fetchEpisodeData(animeId: string): Promise { const response = await fetch(`https://hianime.pstream.org/api/v2/hianime/anime/${animeId}/episodes`); - if (!response.ok) throw new Error('Failed to fetch episode data'); + if (!response.ok) throw new NotFoundError('Failed to fetch episode data'); return response.json(); } async function comboScraper(ctx: ShowScrapeContext): Promise { - // First, search for the anime to get its Hianime ID const animeId = await searchAnime(ctx.media.title); + // console.log(animeId); + + const absoluteEp = await calculateAbsoluteEpisodeNumber( + ctx.media.tmdbId, + ctx.media.season.number, + ctx.media.episode.number, + ); + + // console.log(absoluteEp); - // Then, get the episode data const episodeData = await fetchEpisodeData(animeId); - const episode = episodeData.data.episodes.find((ep) => ep.number === ctx.media.episode.number); - - if (!episode) throw new Error('Episode not found'); + // console.log(episodeData); + const episode = episodeData.data.episodes.find((ep) => ep.number === absoluteEp); + if (!episode) throw new NotFoundError('Episode not found'); const embeds = [ { @@ -84,7 +120,7 @@ async function comboScraper(ctx: ShowScrapeContext): Promise { export const hianimeScraper = makeSourcerer({ id: 'hianime', - name: 'Hianime', + name: 'Zoro (Anime)', rank: 7, disabled: false, flags: [flags.CORS_ALLOWED],