From ff7a5f4947f94fc2bfcaba5d59ec4aa833006c65 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:10:16 -0700 Subject: [PATCH] update trakt api --- src/backend/metadata/traktApi.ts | 201 ++++++++++++++++++++++++++++--- src/setup/config.ts | 4 + 2 files changed, 187 insertions(+), 18 deletions(-) diff --git a/src/backend/metadata/traktApi.ts b/src/backend/metadata/traktApi.ts index 9c8b0142..a598f297 100644 --- a/src/backend/metadata/traktApi.ts +++ b/src/backend/metadata/traktApi.ts @@ -1,4 +1,6 @@ +import { conf } from "@/setup/config"; import { SimpleCache } from "@/utils/cache"; +import { getTurnstileToken } from "@/utils/turnstile"; import { getMediaDetails } from "./tmdb"; import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb"; @@ -11,6 +13,79 @@ import type { export const TRAKT_BASE_URL = "https://fed-airdate.pstream.mov"; +// Token cookie configuration +const TOKEN_COOKIE_NAME = "turnstile_token"; +const TOKEN_CACHE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds + +/** + * Get turnstile token from cookie or fetch new one + */ +const getFreshTurnstileToken = async (): Promise => { + const now = Date.now(); + + // Check if we have a valid cached token in cookie + if (typeof window !== "undefined") { + const cookies = document.cookie.split(";"); + const tokenCookie = cookies.find((cookie) => + cookie.trim().startsWith(`${TOKEN_COOKIE_NAME}=`), + ); + + if (tokenCookie) { + try { + const cookieValue = tokenCookie.split("=")[1]; + const cookieData = JSON.parse(decodeURIComponent(cookieValue)); + const { token, timestamp } = cookieData; + + // Check if token is still valid (within 10 minutes) + if (token && timestamp && now - timestamp < TOKEN_CACHE_DURATION) { + return token; + } + } catch (error) { + // Invalid cookie format, continue to get new token + console.warn("Invalid turnstile token cookie:", error); + } + } + } + + // Get new token from Cloudflare + try { + const token = await getTurnstileToken("0x4AAAAAAB6ocCCpurfWRZyC"); + + // Store token in cookie with expiration + if (typeof window !== "undefined") { + const expiresAt = new Date(now + TOKEN_CACHE_DURATION); + const cookieData = { + token, + timestamp: now, + }; + const cookieValue = encodeURIComponent(JSON.stringify(cookieData)); + + document.cookie = `${TOKEN_COOKIE_NAME}=${cookieValue}; expires=${expiresAt.toUTCString()}; path=/; SameSite=Strict`; + } + + return token; + } catch (error) { + throw new Error(`Failed to get turnstile token: ${error}`); + } +}; + +/** + * Validate turnstile token with server and store for 10 minutes within api. + */ +const validateAndStoreToken = async (token: string): Promise => { + const response = await fetch(`${TRAKT_BASE_URL}/auth`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + + if (!response.ok) { + throw new Error(`Token validation failed: ${response.statusText}`); + } +}; + // Map provider names to their Trakt endpoints export const PROVIDER_TO_TRAKT_MAP = { "8": "netflixmovies", // Netflix Movies @@ -53,6 +128,10 @@ traktCache.initialize(); async function fetchFromTrakt( endpoint: string, ): Promise { + if (!conf().USE_TRAKT) { + return null as T; + } + // Check cache first const cacheKey: TraktCacheKey = { endpoint }; const cachedResult = traktCache.get(cacheKey); @@ -60,17 +139,58 @@ async function fetchFromTrakt( return cachedResult as T; } - // Make the API request - const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`); - if (!response.ok) { - throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`); + // Try up to 2 times: first with cached/fresh token, retry with forced fresh token if auth fails + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + // 1. Get turnstile token (cached or fresh) + const turnstileToken = await getFreshTurnstileToken(); + + // 2. Validate token with server and store for 10 minutes + await validateAndStoreToken(turnstileToken); + + // 3. Make the API request with validated token + const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`, { + headers: { + "x-turnstile-token": turnstileToken, + }, + }); + + if (!response.ok) { + // If auth error on first attempt, clear cookie and retry with fresh token + if ( + (response.status === 401 || response.status === 403) && + attempt === 0 + ) { + // Clear the cookie to force fresh token on retry + if (typeof window !== "undefined") { + document.cookie = `${TOKEN_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + } + continue; // Try again + } + throw new Error( + `Failed to fetch from ${endpoint}: ${response.statusText}`, + ); + } + + const result = await response.json(); + + // Cache the result for 1 hour (3600 seconds) + traktCache.set(cacheKey, result, 3600); + + return result as T; + } catch (error) { + // If this was the second attempt or not an auth error, throw + if ( + attempt === 1 || + !(error instanceof Error && error.message.includes("401")) + ) { + throw error; + } + // Otherwise, continue to retry + } } - const result = await response.json(); - // Cache the result for 1 hour (3600 seconds) - traktCache.set(cacheKey, result, 3600); - - return result as T; + throw new Error(`Failed to fetch from ${endpoint} after retries`); } // Release details @@ -84,6 +204,10 @@ export async function getReleaseDetails( url += `/${season}/${episode}`; } + if (!conf().USE_TRAKT) { + return null as unknown as TraktReleaseResponse; + } + // Check cache first const cacheKey: TraktCacheKey = { endpoint: url }; const cachedResult = traktCache.get(cacheKey); @@ -91,17 +215,58 @@ export async function getReleaseDetails( return cachedResult as TraktReleaseResponse; } - // Make the API request - const response = await fetch(`${TRAKT_BASE_URL}${url}`); - if (!response.ok) { - throw new Error(`Failed to fetch release details: ${response.statusText}`); + // Try up to 2 times: first with cached/fresh token, retry with forced fresh token if auth fails + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + // 1. Get turnstile token (cached or fresh) + const turnstileToken = await getFreshTurnstileToken(); + + // 2. Validate token with server and store for 10 minutes + await validateAndStoreToken(turnstileToken); + + // 3. Make the API request with validated token + const response = await fetch(`${TRAKT_BASE_URL}${url}`, { + headers: { + "x-turnstile-token": turnstileToken, + }, + }); + + if (!response.ok) { + // If auth error on first attempt, clear cookie and retry with fresh token + if ( + (response.status === 401 || response.status === 403) && + attempt === 0 + ) { + // Clear the cookie to force fresh token on retry + if (typeof window !== "undefined") { + document.cookie = `${TOKEN_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + } + continue; // Try again + } + throw new Error( + `Failed to fetch release details: ${response.statusText}`, + ); + } + + const result = await response.json(); + + // Cache the result for 1 hour (3600 seconds) + traktCache.set(cacheKey, result, 3600); + + return result as TraktReleaseResponse; + } catch (error) { + // If this was the second attempt or not an auth error, throw + if ( + attempt === 1 || + !(error instanceof Error && error.message.includes("401")) + ) { + throw error; + } + // Otherwise, continue to retry + } } - const result = await response.json(); - // Cache the result for 1 hour (3600 seconds) - traktCache.set(cacheKey, result, 3600); - - return result as TraktReleaseResponse; + throw new Error(`Failed to fetch release details after retries`); } // Latest releases diff --git a/src/setup/config.ts b/src/setup/config.ts index 42b00a58..884d92b1 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -32,6 +32,7 @@ interface Config { TRACK_SCRIPT: string; // like BANNER_MESSAGE: string; BANNER_ID: string; + USE_TRAKT: boolean; } export interface RuntimeConfig { @@ -60,6 +61,7 @@ export interface RuntimeConfig { TRACK_SCRIPT: string | null; BANNER_MESSAGE: string | null; BANNER_ID: string | null; + USE_TRAKT: boolean; } const env: Record = { @@ -91,6 +93,7 @@ const env: Record = { TRACK_SCRIPT: import.meta.env.VITE_TRACK_SCRIPT, BANNER_MESSAGE: import.meta.env.VITE_BANNER_MESSAGE, BANNER_ID: import.meta.env.VITE_BANNER_ID, + USE_TRAKT: import.meta.env.VITE_USE_TRAKT, }; function coerceUndefined(value: string | null | undefined): string | undefined { @@ -165,5 +168,6 @@ export function conf(): RuntimeConfig { TRACK_SCRIPT: getKey("TRACK_SCRIPT"), BANNER_MESSAGE: getKey("BANNER_MESSAGE"), BANNER_ID: getKey("BANNER_ID"), + USE_TRAKT: getKey("USE_TRAKT", "false") === "true", }; }