Merge branch 'pr/9' into production

This commit is contained in:
Pas 2025-07-11 16:39:30 -06:00
commit 390d83e2df
3 changed files with 167 additions and 1 deletions

View file

@ -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) {

View file

@ -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<Sourcerer> {
animeflvScraper,
cinemaosScraper,
nepuScraper,
pirxcyScraper,
];
}

View file

@ -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<string> {
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<SourcererOutput> {
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<SourcererOutput> {
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,
});