diff --git a/src/dev-cli/config.ts b/src/dev-cli/config.ts index 38f8039..c2a6601 100644 --- a/src/dev-cli/config.ts +++ b/src/dev-cli/config.ts @@ -1,5 +1,7 @@ export function getConfig() { - let tmdbApiKey = process.env.MOVIE_WEB_TMDB_API_KEY ?? ''; + let tmdbApiKey = + process.env.MOVIE_WEB_TMDB_API_KEY ?? + 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMjUwMjc3YmZjNjNkNzNiYjY5NmI3MWU2NThjMjUyMSIsIm5iZiI6MTcyNTcyNTQ1OS4wOTksInN1YiI6IjY2ZGM3YjEzNDQyYjExNDQzMmRkMTU5MSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.W7jVmiYfA3XQg0eXTdnjer5EkNlJ1F4k4bJyw5zdgVY'; tmdbApiKey = tmdbApiKey.trim(); if (!tmdbApiKey) { diff --git a/src/providers/all.ts b/src/providers/all.ts index b08aabc..aaed49b 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -10,6 +10,7 @@ import { fsharetvScraper } from '@/providers/sources/fsharetv'; import { insertunitScraper } from '@/providers/sources/insertunit'; import { mp4hydraScraper } from '@/providers/sources/mp4hydra'; import { nepuScraper } from '@/providers/sources/nepu'; +import { pirxcyScraper } from '@/providers/sources/pirxcy'; import { tugaflixScraper } from '@/providers/sources/tugaflix'; import { vidsrcScraper } from '@/providers/sources/vidsrc'; import { vidsrcsuScraper } from '@/providers/sources/vidsrcsu'; @@ -103,6 +104,7 @@ export function gatherAllSources(): Array { animeflvScraper, cinemaosScraper, nepuScraper, + pirxcyScraper, ]; } diff --git a/src/providers/sources/pirxcy.ts b/src/providers/sources/pirxcy.ts new file mode 100644 index 0000000..53beca1 --- /dev/null +++ b/src/providers/sources/pirxcy.ts @@ -0,0 +1,162 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const API_SERVER = 'https://mbp.pirxcy.dev'; + +// Helper function to convert quality string to standard format +function normalizeQuality(quality: string): string { + const qualityMap: { [key: string]: string } = { + '4K': '2160', + '1080p': '1080', + '720p': '720', + HDTV: '720', + '480p': '480', + '360p': '360', + }; + return qualityMap[quality] || '720'; +} + +// Helper function to build qualities object from all available streams +function buildQualitiesFromStreams(streams: Array<{ path: string; real_quality: string }>) { + const mp4Streams = streams.filter((s) => s.path && new URL(s.path).pathname.endsWith('.mp4')); + if (mp4Streams.length === 0) { + throw new NotFoundError('No playable MP4 streams found'); + } + + const qualities: { [key: string]: { url: string } } = {}; + + // Add all available qualities + for (const stream of mp4Streams) { + const normalizedQuality = normalizeQuality(stream.real_quality); + qualities[normalizedQuality] = { url: stream.path }; + } + + return qualities; +} + +// Helper function to find media by TMDB ID +async function findMediaByTMDBId( + ctx: MovieScrapeContext | ShowScrapeContext, + tmdbId: string, + title: string, + type: 'movie' | 'tv', + year?: string, +): Promise { + const searchUrl = `${API_SERVER}/search?q=${encodeURIComponent(title)}&type=${type}${year ? `&year=${year}` : ''}`; + const searchRes = await ctx.proxiedFetcher(searchUrl); + + if (!searchRes.data || searchRes.data.length === 0) { + throw new NotFoundError('No results found in search'); + } + + // Find the correct internal ID by matching TMDB ID + for (const result of searchRes.data) { + const detailUrl = `${API_SERVER}/details/${type}/${result.id}`; + const detailRes = await ctx.proxiedFetcher(detailUrl); + + if (detailRes.data && detailRes.data.tmdb_id.toString() === tmdbId) { + return result.id; + } + } + + throw new NotFoundError('Could not find matching media item for TMDB ID'); +} + +async function scrapeMovie(ctx: MovieScrapeContext): Promise { + const tmdbId = ctx.media.tmdbId; + const title = ctx.media.title; + const year = ctx.media.releaseYear?.toString(); + + if (!tmdbId || !title) { + throw new NotFoundError('Missing required media information'); + } + + try { + // Find internal media ID + const mediaId = await findMediaByTMDBId(ctx, tmdbId, title, 'movie', year); + + // Get stream links + const streamUrl = `${API_SERVER}/movie/${mediaId}`; + const streamData = await ctx.proxiedFetcher(streamUrl); + + if (!streamData.data || !streamData.data.list) { + throw new NotFoundError('No streams found for this movie'); + } + + const qualities = buildQualitiesFromStreams(streamData.data.list); + + return { + stream: [ + { + id: 'pirxcy', + type: 'file', + qualities, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + embeds: [], + }; + } catch (error) { + if (error instanceof NotFoundError) { + throw error; + } + throw new NotFoundError(`Failed to scrape movie: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} + +async function scrapeShow(ctx: ShowScrapeContext): Promise { + const tmdbId = ctx.media.tmdbId; + const title = ctx.media.title; + const year = ctx.media.releaseYear?.toString(); + const season = ctx.media.season.number; + const episode = ctx.media.episode.number; + + if (!tmdbId || !title || !season || !episode) { + throw new NotFoundError('Missing required media information'); + } + + try { + // Find internal media ID + const mediaId = await findMediaByTMDBId(ctx, tmdbId, title, 'tv', year); + + // Get stream links + const streamUrl = `${API_SERVER}/tv/${mediaId}/${season}/${episode}`; + const streamData = await ctx.proxiedFetcher(streamUrl); + + if (!streamData.data || !streamData.data.list) { + throw new NotFoundError('No streams found for this episode'); + } + + const qualities = buildQualitiesFromStreams(streamData.data.list); + + return { + stream: [ + { + id: 'pirxcy', + type: 'file', + qualities, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + embeds: [], + }; + } catch (error) { + if (error instanceof NotFoundError) { + throw error; + } + throw new NotFoundError(`Failed to scrape show: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} + +export const pirxcyScraper = makeSourcerer({ + id: 'pirxcy', + name: 'Pirxcy', + rank: 150, + flags: [flags.CORS_ALLOWED], + scrapeMovie, + scrapeShow, +});