From 0f0adc6a9d2e5eea9e26a41184ef7ee2ee166741 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:07:27 -0700 Subject: [PATCH] add skip validation check to stream type --- src/providers/embeds/server-mirrors.ts | 2 + src/providers/sources/uira.ts | 214 +++++++++++++++++++++++++ src/providers/streams.ts | 1 + src/utils/valid.ts | 1 + 4 files changed, 218 insertions(+) create mode 100644 src/providers/sources/uira.ts diff --git a/src/providers/embeds/server-mirrors.ts b/src/providers/embeds/server-mirrors.ts index dca8f21..652981a 100644 --- a/src/providers/embeds/server-mirrors.ts +++ b/src/providers/embeds/server-mirrors.ts @@ -16,6 +16,7 @@ export const serverMirrorEmbed = makeEmbed({ headers: context.headers, flags: context.flags, captions: context.captions, + skipValidation: context.skipvalid, }, ], }; @@ -29,6 +30,7 @@ export const serverMirrorEmbed = makeEmbed({ flags: context.flags, captions: context.captions, headers: context.headers, + skipValidation: context.skipvalid, }, ], }; diff --git a/src/providers/sources/uira.ts b/src/providers/sources/uira.ts new file mode 100644 index 0000000..3049ae9 --- /dev/null +++ b/src/providers/sources/uira.ts @@ -0,0 +1,214 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; +import { createM3U8ProxyUrl } from '@/utils/proxy'; +import { getTurnstileToken } from '@/utils/turnstile'; + +const TURNSTILE_SITEKEY = '0x4AAAAAACCe9bUxQlRKwDT5'; +const TOKEN_COOKIE_NAME = 'uiralive-turnstile-token'; +const TOKEN_EXPIRY_MINUTES = 9; +const baseUrl = 'https://pasmells.uira.live'; + +interface UiraScraperConfig { + id: string; + name: string; + rank: number; + scraperName: string; // e.g., 'watch32', 'spencerdevs', 'vidzee' +} + +/** + * Get stored turnstile token from cookies + */ +const getStoredToken = async (): Promise => { + if (typeof window === 'undefined') return null; + + const cookies = document.cookie.split(';'); + const tokenCookie = cookies.find((cookie) => cookie.trim().startsWith(`${TOKEN_COOKIE_NAME}=`)); + + if (!tokenCookie) return null; + + const cookieValue = tokenCookie.split('=')[1]; + if (!cookieValue) return null; + + // Parse the cookie value which contains both token and creation time + const cookieData = JSON.parse(decodeURIComponent(cookieValue)); + const { token, createdAt } = cookieData; + + if (!token || !createdAt) return null; + + return token; +}; + +/** + * Store turnstile token in cookies with expiration + */ +const storeToken = (token: string): void => { + try { + if (typeof window === 'undefined') return; + + const expiresAt = new Date(); + expiresAt.setMinutes(expiresAt.getMinutes() + TOKEN_EXPIRY_MINUTES); + + // Store token with creation timestamp + const cookieData = { + token, + createdAt: Math.floor(Date.now() / 1000), // Unix timestamp in seconds + }; + + const cookieValue = encodeURIComponent(JSON.stringify(cookieData)); + document.cookie = `${TOKEN_COOKIE_NAME}=${cookieValue}; expires=${expiresAt.toUTCString()}; path=/`; + } catch (e) { + console.warn('Failed to store turnstile token:', e); + } +}; + +/** + * Clear the turnstile token cookie when verification fails + */ +const clearTurnstileToken = (): void => { + try { + if (typeof window === 'undefined') return; + document.cookie = `${TOKEN_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + console.warn('Turnstile token cleared due to verification failure'); + } catch (e) { + console.warn('Failed to clear turnstile token:', e); + } +}; + +/** + * Get turnstile token - either from cache or fetch new one + */ +const getTurnstileTokenWithCache = async (): Promise => { + // 1. Check if token exists in cache and validate against server uptime + const cachedToken = await getStoredToken(); + if (cachedToken) { + return cachedToken; + } + + // 2. Fetch new turnstile token + try { + const token = await getTurnstileToken(TURNSTILE_SITEKEY); + + // 3. Store token + storeToken(token); + + return token; + } catch (error) { + // 4. If it fails, show error + throw new Error(`Turnstile verification failed: ${error}`); + } +}; + +/** + * Create a unified scraper function for Uira providers + */ +function createUiraScraper(config: UiraScraperConfig) { + async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const turnstileToken = await getTurnstileTokenWithCache(); + + ctx.progress(20); + + const fetchUrl = + ctx.media.type === 'movie' + ? `/api/scrapers/${config.scraperName}/stream/${ctx.media.tmdbId}` + : `/api/scrapers/${config.scraperName}/stream/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + + const hasExtension = ctx.features && !ctx.features.requires.includes(flags.CORS_ALLOWED); + + let result; + try { + result = await ctx.fetcher(`${baseUrl}${fetchUrl}${hasExtension ? '' : '?proxy=true'}`, { + headers: { 'X-Turnstile-Token': turnstileToken }, + }); + } catch (e: any) { + if (e instanceof NotFoundError) throw new NotFoundError(`${e.message}`); + throw e; + } + + // Try again cause uira is smelly + if (!result) { + try { + result = await ctx.fetcher(`${baseUrl}${fetchUrl}${hasExtension ? '' : '?proxy=true'}`, { + headers: { 'X-Turnstile-Token': turnstileToken }, + }); + } catch (e: any) { + if (e instanceof NotFoundError) throw new NotFoundError(`${e.message}`); + throw e; + } + } + + // If the turnstile token is invalid, clear it and throw an error + if (result.error === 'Invalid turnstile token') { + clearTurnstileToken(); + // eslint-disable-next-line no-alert + alert('Uira.live Turnstile verification failed. Please refresh the page and try again.'); + throw new NotFoundError('Turnstile verification failed. Token has been cleared.'); + } + + if (!result || !result.sources || result.sources.length === 0) { + throw new NotFoundError('No sources found'); + } + + ctx.progress(90); + + const embeds = result.sources.map((source: any) => ({ + embedId: 'mirror', + url: JSON.stringify({ + type: source.type === 'hls' ? 'hls' : 'file', + stream: + source.type === 'hls' && + (hasExtension ? source.file : createM3U8ProxyUrl(source.file, ctx.features, source.headers)), + headers: source.headers, + flags: [flags.CORS_ALLOWED], + captions: result.subtitles || [], + skipvalid: source.type !== 'hls', + qualities: + source.type !== 'hls' + ? { + [source.quality]: { + type: 'mp4', + url: source.file, + }, + } + : undefined, + }), + })); + + return { + embeds, + }; + } + + return makeSourcerer({ + id: config.id, + name: config.name, + rank: config.rank, + disabled: false, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, + }); +} + +// Export three individual scraper instances +export const uira32Scraper = createUiraScraper({ + id: 'uira32', + name: 'Uira 32 🤝', + rank: 245, + scraperName: 'watch32', +}); + +export const uiraspencerScraper = createUiraScraper({ + id: 'uiraspencer', + name: 'Uira Spencer 🤝', + rank: 243, + scraperName: 'spencerdevs', +}); + +export const uiravidzeeScraper = createUiraScraper({ + id: 'uiravidzee', + name: 'Uira Vidzee 🤝', + rank: 244, + scraperName: 'vidzee', +}); diff --git a/src/providers/streams.ts b/src/providers/streams.ts index 113b9de..dd3bf0a 100644 --- a/src/providers/streams.ts +++ b/src/providers/streams.ts @@ -20,6 +20,7 @@ type StreamCommon = { thumbnailTrack?: ThumbnailTrack; headers?: Record; // these headers HAVE to be set to watch the stream preferredHeaders?: Record; // these headers are optional, would improve the stream + skipValidation?: boolean; // skip stream validation if true }; export type FileBasedStream = StreamCommon & { diff --git a/src/utils/valid.ts b/src/utils/valid.ts index 043ddc1..c3c8293 100644 --- a/src/utils/valid.ts +++ b/src/utils/valid.ts @@ -50,6 +50,7 @@ export async function validatePlayableStream( sourcererId: string, ): Promise { if (SKIP_VALIDATION_CHECK_IDS.includes(sourcererId)) return stream; + if (stream.skipValidation) return stream; const alwaysUseNormalFetch = UNPROXIED_VALIDATION_CHECK_IDS.includes(sourcererId);