pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

update trakt api

Pas ff7a5f49 054612b9

+187 -18
+183 -18
src/backend/metadata/traktApi.ts
··· 1 + import { conf } from "@/setup/config"; 1 2 import { SimpleCache } from "@/utils/cache"; 3 + import { getTurnstileToken } from "@/utils/turnstile"; 2 4 3 5 import { getMediaDetails } from "./tmdb"; 4 6 import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb"; ··· 11 13 12 14 export const TRAKT_BASE_URL = "https://fed-airdate.pstream.mov"; 13 15 16 + // Token cookie configuration 17 + const TOKEN_COOKIE_NAME = "turnstile_token"; 18 + const TOKEN_CACHE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds 19 + 20 + /** 21 + * Get turnstile token from cookie or fetch new one 22 + */ 23 + const getFreshTurnstileToken = async (): Promise<string> => { 24 + const now = Date.now(); 25 + 26 + // Check if we have a valid cached token in cookie 27 + if (typeof window !== "undefined") { 28 + const cookies = document.cookie.split(";"); 29 + const tokenCookie = cookies.find((cookie) => 30 + cookie.trim().startsWith(`${TOKEN_COOKIE_NAME}=`), 31 + ); 32 + 33 + if (tokenCookie) { 34 + try { 35 + const cookieValue = tokenCookie.split("=")[1]; 36 + const cookieData = JSON.parse(decodeURIComponent(cookieValue)); 37 + const { token, timestamp } = cookieData; 38 + 39 + // Check if token is still valid (within 10 minutes) 40 + if (token && timestamp && now - timestamp < TOKEN_CACHE_DURATION) { 41 + return token; 42 + } 43 + } catch (error) { 44 + // Invalid cookie format, continue to get new token 45 + console.warn("Invalid turnstile token cookie:", error); 46 + } 47 + } 48 + } 49 + 50 + // Get new token from Cloudflare 51 + try { 52 + const token = await getTurnstileToken("0x4AAAAAAB6ocCCpurfWRZyC"); 53 + 54 + // Store token in cookie with expiration 55 + if (typeof window !== "undefined") { 56 + const expiresAt = new Date(now + TOKEN_CACHE_DURATION); 57 + const cookieData = { 58 + token, 59 + timestamp: now, 60 + }; 61 + const cookieValue = encodeURIComponent(JSON.stringify(cookieData)); 62 + 63 + document.cookie = `${TOKEN_COOKIE_NAME}=${cookieValue}; expires=${expiresAt.toUTCString()}; path=/; SameSite=Strict`; 64 + } 65 + 66 + return token; 67 + } catch (error) { 68 + throw new Error(`Failed to get turnstile token: ${error}`); 69 + } 70 + }; 71 + 72 + /** 73 + * Validate turnstile token with server and store for 10 minutes within api. 74 + */ 75 + const validateAndStoreToken = async (token: string): Promise<void> => { 76 + const response = await fetch(`${TRAKT_BASE_URL}/auth`, { 77 + method: "POST", 78 + headers: { 79 + "Content-Type": "application/json", 80 + }, 81 + body: JSON.stringify({ token }), 82 + }); 83 + 84 + if (!response.ok) { 85 + throw new Error(`Token validation failed: ${response.statusText}`); 86 + } 87 + }; 88 + 14 89 // Map provider names to their Trakt endpoints 15 90 export const PROVIDER_TO_TRAKT_MAP = { 16 91 "8": "netflixmovies", // Netflix Movies ··· 53 128 async function fetchFromTrakt<T = TraktListResponse>( 54 129 endpoint: string, 55 130 ): Promise<T> { 131 + if (!conf().USE_TRAKT) { 132 + return null as T; 133 + } 134 + 56 135 // Check cache first 57 136 const cacheKey: TraktCacheKey = { endpoint }; 58 137 const cachedResult = traktCache.get(cacheKey); ··· 60 139 return cachedResult as T; 61 140 } 62 141 63 - // Make the API request 64 - const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`); 65 - if (!response.ok) { 66 - throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`); 67 - } 68 - const result = await response.json(); 142 + // Try up to 2 times: first with cached/fresh token, retry with forced fresh token if auth fails 143 + for (let attempt = 0; attempt < 2; attempt += 1) { 144 + try { 145 + // 1. Get turnstile token (cached or fresh) 146 + const turnstileToken = await getFreshTurnstileToken(); 147 + 148 + // 2. Validate token with server and store for 10 minutes 149 + await validateAndStoreToken(turnstileToken); 150 + 151 + // 3. Make the API request with validated token 152 + const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`, { 153 + headers: { 154 + "x-turnstile-token": turnstileToken, 155 + }, 156 + }); 157 + 158 + if (!response.ok) { 159 + // If auth error on first attempt, clear cookie and retry with fresh token 160 + if ( 161 + (response.status === 401 || response.status === 403) && 162 + attempt === 0 163 + ) { 164 + // Clear the cookie to force fresh token on retry 165 + if (typeof window !== "undefined") { 166 + document.cookie = `${TOKEN_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; 167 + } 168 + continue; // Try again 169 + } 170 + throw new Error( 171 + `Failed to fetch from ${endpoint}: ${response.statusText}`, 172 + ); 173 + } 174 + 175 + const result = await response.json(); 69 176 70 - // Cache the result for 1 hour (3600 seconds) 71 - traktCache.set(cacheKey, result, 3600); 177 + // Cache the result for 1 hour (3600 seconds) 178 + traktCache.set(cacheKey, result, 3600); 72 179 73 - return result as T; 180 + return result as T; 181 + } catch (error) { 182 + // If this was the second attempt or not an auth error, throw 183 + if ( 184 + attempt === 1 || 185 + !(error instanceof Error && error.message.includes("401")) 186 + ) { 187 + throw error; 188 + } 189 + // Otherwise, continue to retry 190 + } 191 + } 192 + 193 + throw new Error(`Failed to fetch from ${endpoint} after retries`); 74 194 } 75 195 76 196 // Release details ··· 84 204 url += `/${season}/${episode}`; 85 205 } 86 206 207 + if (!conf().USE_TRAKT) { 208 + return null as unknown as TraktReleaseResponse; 209 + } 210 + 87 211 // Check cache first 88 212 const cacheKey: TraktCacheKey = { endpoint: url }; 89 213 const cachedResult = traktCache.get(cacheKey); ··· 91 215 return cachedResult as TraktReleaseResponse; 92 216 } 93 217 94 - // Make the API request 95 - const response = await fetch(`${TRAKT_BASE_URL}${url}`); 96 - if (!response.ok) { 97 - throw new Error(`Failed to fetch release details: ${response.statusText}`); 98 - } 99 - const result = await response.json(); 218 + // Try up to 2 times: first with cached/fresh token, retry with forced fresh token if auth fails 219 + for (let attempt = 0; attempt < 2; attempt += 1) { 220 + try { 221 + // 1. Get turnstile token (cached or fresh) 222 + const turnstileToken = await getFreshTurnstileToken(); 100 223 101 - // Cache the result for 1 hour (3600 seconds) 102 - traktCache.set(cacheKey, result, 3600); 224 + // 2. Validate token with server and store for 10 minutes 225 + await validateAndStoreToken(turnstileToken); 226 + 227 + // 3. Make the API request with validated token 228 + const response = await fetch(`${TRAKT_BASE_URL}${url}`, { 229 + headers: { 230 + "x-turnstile-token": turnstileToken, 231 + }, 232 + }); 103 233 104 - return result as TraktReleaseResponse; 234 + if (!response.ok) { 235 + // If auth error on first attempt, clear cookie and retry with fresh token 236 + if ( 237 + (response.status === 401 || response.status === 403) && 238 + attempt === 0 239 + ) { 240 + // Clear the cookie to force fresh token on retry 241 + if (typeof window !== "undefined") { 242 + document.cookie = `${TOKEN_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; 243 + } 244 + continue; // Try again 245 + } 246 + throw new Error( 247 + `Failed to fetch release details: ${response.statusText}`, 248 + ); 249 + } 250 + 251 + const result = await response.json(); 252 + 253 + // Cache the result for 1 hour (3600 seconds) 254 + traktCache.set(cacheKey, result, 3600); 255 + 256 + return result as TraktReleaseResponse; 257 + } catch (error) { 258 + // If this was the second attempt or not an auth error, throw 259 + if ( 260 + attempt === 1 || 261 + !(error instanceof Error && error.message.includes("401")) 262 + ) { 263 + throw error; 264 + } 265 + // Otherwise, continue to retry 266 + } 267 + } 268 + 269 + throw new Error(`Failed to fetch release details after retries`); 105 270 } 106 271 107 272 // Latest releases
+4
src/setup/config.ts
··· 32 32 TRACK_SCRIPT: string; // like <script src="https://umami.com/script.js"></script> 33 33 BANNER_MESSAGE: string; 34 34 BANNER_ID: string; 35 + USE_TRAKT: boolean; 35 36 } 36 37 37 38 export interface RuntimeConfig { ··· 60 61 TRACK_SCRIPT: string | null; 61 62 BANNER_MESSAGE: string | null; 62 63 BANNER_ID: string | null; 64 + USE_TRAKT: boolean; 63 65 } 64 66 65 67 const env: Record<keyof Config, undefined | string> = { ··· 91 93 TRACK_SCRIPT: import.meta.env.VITE_TRACK_SCRIPT, 92 94 BANNER_MESSAGE: import.meta.env.VITE_BANNER_MESSAGE, 93 95 BANNER_ID: import.meta.env.VITE_BANNER_ID, 96 + USE_TRAKT: import.meta.env.VITE_USE_TRAKT, 94 97 }; 95 98 96 99 function coerceUndefined(value: string | null | undefined): string | undefined { ··· 165 168 TRACK_SCRIPT: getKey("TRACK_SCRIPT"), 166 169 BANNER_MESSAGE: getKey("BANNER_MESSAGE"), 167 170 BANNER_ID: getKey("BANNER_ID"), 171 + USE_TRAKT: getKey("USE_TRAKT", "false") === "true", 168 172 }; 169 173 }