···11VITE_TMDB_READ_API_KEY=...
22VITE_OPENSEARCH_ENABLED=false
33+VITE_ENABLE_TRAKT=false
3445# make sure the cors proxy url does NOT have a slash at the end
56VITE_CORS_PROXY_URL=...
+213-11
src/backend/metadata/traktApi.ts
···11+import { conf } from "@/setup/config";
12import { SimpleCache } from "@/utils/cache";
33+import { getTurnstileToken } from "@/utils/turnstile";
2435import { getMediaDetails } from "./tmdb";
46import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb";
···4951traktCache.setCompare((a, b) => a.endpoint === b.endpoint);
5052traktCache.initialize();
51535454+// Authentication state - only track concurrent requests
5555+let isAuthenticating = false;
5656+let authToken: string | null = null;
5757+let tokenExpiry: Date | null = null;
5858+5959+/**
6060+ * Clears the authentication token
6161+ */
6262+function clearAuthToken(): void {
6363+ authToken = null;
6464+ tokenExpiry = null;
6565+ localStorage.removeItem("trakt_auth_token");
6666+ localStorage.removeItem("trakt_token_expiry");
6767+}
6868+6969+/**
7070+ * Stores the authentication token in memory and localStorage
7171+ */
7272+function storeAuthToken(token: string, expiresAt: string): void {
7373+ const expiryDate = new Date(expiresAt);
7474+ if (Number.isNaN(expiryDate.getTime())) {
7575+ console.error("Invalid expiry date format:", expiresAt);
7676+ return;
7777+ }
7878+7979+ authToken = token;
8080+ tokenExpiry = expiryDate;
8181+8282+ // Store in localStorage for persistence
8383+ localStorage.setItem("trakt_auth_token", token);
8484+ localStorage.setItem("trakt_token_expiry", expiresAt);
8585+}
8686+8787+/**
8888+ * Checks if user is authenticated by checking token validity
8989+ */
9090+function isAuthenticated(): boolean {
9191+ // Check memory first
9292+ if (authToken && tokenExpiry && tokenExpiry > new Date()) {
9393+ return true;
9494+ }
9595+9696+ // Check localStorage
9797+ const storedToken = localStorage.getItem("trakt_auth_token");
9898+ const storedExpiry = localStorage.getItem("trakt_token_expiry");
9999+100100+ if (storedToken && storedExpiry) {
101101+ const expiryDate = new Date(storedExpiry);
102102+ if (expiryDate > new Date()) {
103103+ authToken = storedToken;
104104+ tokenExpiry = expiryDate;
105105+ return true;
106106+ }
107107+ // Token expired, clear it
108108+ clearAuthToken();
109109+ }
110110+111111+ return false;
112112+}
113113+114114+/**
115115+ * Authenticates with the Trakt API using Cloudflare Turnstile
116116+ * Stores the auth token for use in API requests
117117+ */
118118+async function authenticateWithTurnstile(): Promise<void> {
119119+ // Prevent concurrent authentication attempts
120120+ if (isAuthenticating) {
121121+ // Wait for existing authentication to complete
122122+ await new Promise<void>((resolve) => {
123123+ const checkAuth = () => {
124124+ if (!isAuthenticating) {
125125+ resolve();
126126+ } else {
127127+ setTimeout(checkAuth, 100);
128128+ }
129129+ };
130130+ checkAuth();
131131+ });
132132+ return;
133133+ }
134134+135135+ isAuthenticating = true;
136136+137137+ try {
138138+ const turnstileToken = await getTurnstileToken("0x4AAAAAAB6ocCCpurfWRZyC");
139139+140140+ // Authenticate with the API
141141+ const response = await fetch(`${TRAKT_BASE_URL}/auth`, {
142142+ method: "POST",
143143+ headers: {
144144+ "Content-Type": "application/json",
145145+ },
146146+ body: JSON.stringify({
147147+ token: turnstileToken,
148148+ }),
149149+ });
150150+151151+ if (!response.ok) {
152152+ throw new Error(`Authentication failed: ${response.statusText}`);
153153+ }
154154+155155+ const result = await response.json();
156156+157157+ if (!result.success) {
158158+ throw new Error(result.message || "Authentication failed");
159159+ }
160160+161161+ // Store the auth token
162162+ storeAuthToken(result.auth_token, result.expires_at);
163163+ } finally {
164164+ isAuthenticating = false;
165165+ }
166166+}
167167+52168// Base function to fetch from Trakt API
53169async function fetchFromTrakt<T = TraktListResponse>(
54170 endpoint: string,
55171): Promise<T> {
172172+ // Check if Trakt is enabled
173173+ if (!conf().ENABLE_TRAKT) {
174174+ throw new Error("Trakt API is not enabled, using tmdb lists instead.");
175175+ }
176176+56177 // Check cache first
57178 const cacheKey: TraktCacheKey = { endpoint };
58179 const cachedResult = traktCache.get(cacheKey);
···60181 return cachedResult as T;
61182 }
621836363- // Make the API request
6464- const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`);
184184+ // Ensure we're authenticated
185185+ if (!isAuthenticated()) {
186186+ await authenticateWithTurnstile();
187187+ }
188188+189189+ // Make the API request with authorization header
190190+ const headers: Record<string, string> = {};
191191+ if (authToken) {
192192+ headers.Authorization = `Bearer ${authToken}`;
193193+ }
194194+195195+ let response = await fetch(`${TRAKT_BASE_URL}${endpoint}`, {
196196+ headers,
197197+ });
198198+199199+ // If request fails, try re-authenticating and retry once
65200 if (!response.ok) {
6666- throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`);
201201+ // If 401, clear token and re-authenticate
202202+ if (response.status === 401) {
203203+ clearAuthToken();
204204+ }
205205+206206+ // Re-authenticate and retry
207207+ await authenticateWithTurnstile();
208208+209209+ // Rebuild headers after re-authentication
210210+ const retryHeaders: Record<string, string> = {};
211211+ if (authToken) {
212212+ retryHeaders.Authorization = `Bearer ${authToken}`;
213213+ }
214214+215215+ response = await fetch(`${TRAKT_BASE_URL}${endpoint}`, {
216216+ headers: retryHeaders,
217217+ });
218218+219219+ // If retry also fails, throw error
220220+ if (!response.ok) {
221221+ throw new Error(
222222+ `Failed to fetch from ${endpoint}: ${response.statusText}`,
223223+ );
224224+ }
67225 }
226226+68227 const result = await response.json();
6922870229 // Cache the result for 1 hour (3600 seconds)
···84243 url += `/${season}/${episode}`;
85244 }
86245246246+ // Check if Trakt is enabled
247247+ if (!conf().ENABLE_TRAKT) {
248248+ throw new Error("Trakt API is not enabled");
249249+ }
250250+87251 // Check cache first
88252 const cacheKey: TraktCacheKey = { endpoint: url };
89253 const cachedResult = traktCache.get(cacheKey);
···91255 return cachedResult as TraktReleaseResponse;
92256 }
932579494- // Make the API request
9595- const response = await fetch(`${TRAKT_BASE_URL}${url}`);
258258+ // Ensure we're authenticated
259259+ if (!isAuthenticated()) {
260260+ await authenticateWithTurnstile();
261261+ }
262262+263263+ // Make the API request with authorization header
264264+ const headers: Record<string, string> = {};
265265+ if (authToken) {
266266+ headers.Authorization = `Bearer ${authToken}`;
267267+ }
268268+269269+ let response = await fetch(`${TRAKT_BASE_URL}${url}`, {
270270+ headers,
271271+ });
272272+273273+ // If request fails, try re-authenticating and retry once
96274 if (!response.ok) {
9797- throw new Error(`Failed to fetch release details: ${response.statusText}`);
275275+ // If 401, clear token and re-authenticate
276276+ if (response.status === 401) {
277277+ clearAuthToken();
278278+ }
279279+280280+ // Re-authenticate and retry
281281+ await authenticateWithTurnstile();
282282+283283+ // Rebuild headers after re-authentication
284284+ const retryHeaders: Record<string, string> = {};
285285+ if (authToken) {
286286+ retryHeaders.Authorization = `Bearer ${authToken}`;
287287+ }
288288+289289+ response = await fetch(`${TRAKT_BASE_URL}${url}`, {
290290+ headers: retryHeaders,
291291+ });
292292+293293+ // If retry also fails, throw error
294294+ if (!response.ok) {
295295+ throw new Error(
296296+ `Failed to fetch release details: ${response.statusText}`,
297297+ );
298298+ }
98299 }
300300+99301 const result = await response.json();
100302101303 // Cache the result for 1 hour (3600 seconds)
···200402201403 const lists: CuratedMovieList[] = [];
202404203203- for (const config of listConfigs) {
405405+ for (const listConfig of listConfigs) {
204406 try {
205205- const response = await fetchFromTrakt(config.endpoint);
407407+ const response = await fetchFromTrakt(listConfig.endpoint);
206408 lists.push({
207207- listName: config.name,
208208- listSlug: config.slug,
409409+ listName: listConfig.name,
410410+ listSlug: listConfig.slug,
209411 tmdbIds: response.movie_tmdb_ids.slice(0, 30), // Limit to first 30 items
210412 count: Math.min(response.movie_tmdb_ids.length, 30), // Update count to reflect the limit
211413 });
212414 } catch (error) {
213213- console.error(`Failed to fetch ${config.name}:`, error);
415415+ console.error(`Failed to fetch ${listConfig.name}:`, error);
214416 }
215417 }
216418