diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10127a31..092e6db3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 1.8.0 "@p-stream/providers": specifier: github:p-stream/providers#production - version: https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957 + version: https://codeload.github.com/p-stream/providers/tar.gz/35199c5b8e88fe147b2e4d4d8a3289ffb9c6e06c "@plasmohq/messaging": specifier: ^0.6.2 version: 0.6.2(react@18.3.1) @@ -224,7 +224,7 @@ importers: version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) "@vitejs/plugin-react": specifier: ^4.7.0 - version: 4.7.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0)) + version: 4.7.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -302,22 +302,22 @@ importers: version: 5.9.3 vite: specifier: ^5.4.21 - version: 5.4.21(@types/node@20.19.23)(terser@5.44.0) + version: 5.4.21(@types/node@20.19.23)(terser@5.44.1) vite-plugin-checker: specifier: ^0.6.4 - version: 0.6.4(eslint@8.57.1)(optionator@0.9.4)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0)) + version: 0.6.4(eslint@8.57.1)(optionator@0.9.4)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1)) vite-plugin-package-version: specifier: ^1.1.0 - version: 1.1.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0)) + version: 1.1.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1)) vite-plugin-pwa: specifier: ^0.17.5 - version: 0.17.5(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) + version: 0.17.5(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-static-copy: specifier: ^3.1.4 - version: 3.1.4(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0)) + version: 3.1.4(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1)) vitest: specifier: ^1.6.1 - version: 1.6.1(@types/node@20.19.23)(jsdom@23.2.0)(terser@5.44.0) + version: 1.6.1(@types/node@20.19.23)(jsdom@23.2.0)(terser@5.44.1) workbox-window: specifier: ^7.3.0 version: 7.3.0 @@ -1709,10 +1709,10 @@ packages: } engines: { node: ">=12.4.0" } - "@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957": + "@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/35199c5b8e88fe147b2e4d4d8a3289ffb9c6e06c": resolution: { - tarball: https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957, + tarball: https://codeload.github.com/p-stream/providers/tar.gz/35199c5b8e88fe147b2e4d4d8a3289ffb9c6e06c, } version: 3.2.0 @@ -6495,10 +6495,10 @@ packages: } engines: { node: ">=10" } - terser@5.44.0: + terser@5.44.1: resolution: { - integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==, + integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==, } engines: { node: ">=10" } hasBin: true @@ -8380,7 +8380,7 @@ snapshots: "@nolyfill/is-core-module@1.0.39": {} - "@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957": + "@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/35199c5b8e88fe147b2e4d4d8a3289ffb9c6e06c": dependencies: abort-controller: 3.0.0 cheerio: 1.0.0-rc.12 @@ -8473,7 +8473,7 @@ snapshots: dependencies: serialize-javascript: 6.0.2 smob: 1.5.0 - terser: 5.44.0 + terser: 5.44.1 optionalDependencies: rollup: 4.43.0 @@ -8858,7 +8858,7 @@ snapshots: "@unrs/resolver-binding-win32-x64-msvc@1.11.1": optional: true - "@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0))": + "@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1))": dependencies: "@babel/core": 7.28.5 "@babel/plugin-transform-react-jsx-self": 7.27.1(@babel/core@7.28.5) @@ -8866,7 +8866,7 @@ snapshots: "@rolldown/pluginutils": 1.0.0-beta.27 "@types/babel__core": 7.20.5 react-refresh: 0.17.0 - vite: 5.4.21(@types/node@20.19.23)(terser@5.44.0) + vite: 5.4.21(@types/node@20.19.23)(terser@5.44.1) transitivePeerDependencies: - supports-color @@ -11354,7 +11354,7 @@ snapshots: type-fest: 0.16.0 unique-string: 2.0.0 - terser@5.44.0: + terser@5.44.1: dependencies: "@jridgewell/source-map": 0.3.11 acorn: 8.15.0 @@ -11562,13 +11562,13 @@ snapshots: util-deprecate@1.0.2: {} - vite-node@1.6.1(@types/node@20.19.23)(terser@5.44.0): + vite-node@1.6.1(@types/node@20.19.23)(terser@5.44.1): dependencies: cac: 6.7.14 debug: 4.4.3 pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.21(@types/node@20.19.23)(terser@5.44.0) + vite: 5.4.21(@types/node@20.19.23)(terser@5.44.1) transitivePeerDependencies: - "@types/node" - less @@ -11580,7 +11580,7 @@ snapshots: - supports-color - terser - vite-plugin-checker@0.6.4(eslint@8.57.1)(optionator@0.9.4)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0)): + vite-plugin-checker@0.6.4(eslint@8.57.1)(optionator@0.9.4)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1)): dependencies: "@babel/code-frame": 7.27.1 ansi-escapes: 4.3.2 @@ -11593,7 +11593,7 @@ snapshots: semver: 7.7.3 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - vite: 5.4.21(@types/node@20.19.23)(terser@5.44.0) + vite: 5.4.21(@types/node@20.19.23)(terser@5.44.1) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.12 @@ -11603,30 +11603,30 @@ snapshots: optionator: 0.9.4 typescript: 5.9.3 - vite-plugin-package-version@1.1.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0)): + vite-plugin-package-version@1.1.0(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1)): dependencies: - vite: 5.4.21(@types/node@20.19.23)(terser@5.44.0) + vite: 5.4.21(@types/node@20.19.23)(terser@5.44.1) - vite-plugin-pwa@0.17.5(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): + vite-plugin-pwa@0.17.5(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): dependencies: debug: 4.4.3 fast-glob: 3.3.3 pretty-bytes: 6.1.1 - vite: 5.4.21(@types/node@20.19.23)(terser@5.44.0) + vite: 5.4.21(@types/node@20.19.23)(terser@5.44.1) workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.3.0 transitivePeerDependencies: - supports-color - vite-plugin-static-copy@3.1.4(vite@5.4.21(@types/node@20.19.23)(terser@5.44.0)): + vite-plugin-static-copy@3.1.4(vite@5.4.21(@types/node@20.19.23)(terser@5.44.1)): dependencies: chokidar: 3.6.0 p-map: 7.0.3 picocolors: 1.1.1 tinyglobby: 0.2.15 - vite: 5.4.21(@types/node@20.19.23)(terser@5.44.0) + vite: 5.4.21(@types/node@20.19.23)(terser@5.44.1) - vite@5.4.21(@types/node@20.19.23)(terser@5.44.0): + vite@5.4.21(@types/node@20.19.23)(terser@5.44.1): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -11634,9 +11634,9 @@ snapshots: optionalDependencies: "@types/node": 20.19.23 fsevents: 2.3.3 - terser: 5.44.0 + terser: 5.44.1 - vitest@1.6.1(@types/node@20.19.23)(jsdom@23.2.0)(terser@5.44.0): + vitest@1.6.1(@types/node@20.19.23)(jsdom@23.2.0)(terser@5.44.1): dependencies: "@vitest/expect": 1.6.1 "@vitest/runner": 1.6.1 @@ -11655,8 +11655,8 @@ snapshots: strip-literal: 2.1.1 tinybench: 2.9.0 tinypool: 0.8.4 - vite: 5.4.21(@types/node@20.19.23)(terser@5.44.0) - vite-node: 1.6.1(@types/node@20.19.23)(terser@5.44.0) + vite: 5.4.21(@types/node@20.19.23)(terser@5.44.1) + vite-node: 1.6.1(@types/node@20.19.23)(terser@5.44.1) why-is-node-running: 2.3.0 optionalDependencies: "@types/node": 20.19.23 diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index f0a680a5..fdbdf4db 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -947,6 +947,8 @@ }, "account": { "accountDetails": { + "nicknameLabel": "Nickname", + "nicknamePlaceholder": "Enter your nickname", "deviceNameLabel": "Device name", "deviceNamePlaceholder": "Personal phone", "editProfile": "Edit", @@ -979,6 +981,7 @@ }, "devices": { "deviceNameLabel": "Device name", + "unknownDevice": "Unknown device, error decrypting name", "failed": "Failed to load sessions", "removeDevice": "Remove", "title": "Devices" @@ -1144,6 +1147,7 @@ "backendVersion": "Backend version", "hostname": "Hostname", "insecure": "Insecure", + "nickname": "Nickname", "notLoggedIn": "You are not logged in", "secure": "Secure", "title": "App stats", @@ -1263,14 +1267,19 @@ "invalid_token": "Failed to fetch a 'VIP' stream. Your token is invalid!" } }, - "realdebrid": { - "title": "Real Debrid (Beta)", - "description": "Enter your Real Debrid API key to access Real Debrid. Extension required.", + "debrid": { + "title": "Debrid (Beta)", + "description": "Enter your Debrid API key to access Debrid services. Requires a paid <0>Real-Debrid or <1>TorBox account!", + "notice": "Notice: This may not work on all browsers, use Chromium for the best compatibility. If you hear no audio, use another source. :(", "tokenLabel": "API Key", + "serviceOptions": { + "realdebrid": "Real Debrid", + "torbox": "TorBox" + }, "status": { - "failure": "Failed to connect to Real Debrid. Please check your API key.", - "api_down": "Real Debrid API is currently unavailable. Please try again later.", - "invalid_token": "Invalid API key or non-premium account. Real Debrid requires a premium account." + "failure": "Failed to connect to Debrid. Please check your API key.", + "api_down": "Hmm, something went wrong. Please try again later.", + "invalid_token": "Invalid API key or non-premium account. Debrid requires a premium account." } }, "watchParty": { diff --git a/src/backend/accounts/settings.ts b/src/backend/accounts/settings.ts index 14f9ca67..f0f69b33 100644 --- a/src/backend/accounts/settings.ts +++ b/src/backend/accounts/settings.ts @@ -9,7 +9,8 @@ export interface SettingsInput { defaultSubtitleLanguage?: string; proxyUrls?: string[] | null; febboxKey?: string | null; - realDebridKey?: string | null; + debridToken?: string | null; + debridService?: string; enableThumbnails?: boolean; enableAutoplay?: boolean; enableSkipCredits?: boolean; @@ -42,7 +43,8 @@ export interface SettingsResponse { defaultSubtitleLanguage?: string | null; proxyUrls?: string[] | null; febboxKey?: string | null; - realDebridKey?: string | null; + debridToken?: string | null; + debridService?: string; enableThumbnails?: boolean; enableAutoplay?: boolean; enableSkipCredits?: boolean; diff --git a/src/backend/accounts/user.ts b/src/backend/accounts/user.ts index 5ab9e76c..dd7de943 100644 --- a/src/backend/accounts/user.ts +++ b/src/backend/accounts/user.ts @@ -8,9 +8,8 @@ import { ProgressMediaItem } from "@/stores/progress"; export interface UserResponse { id: string; namespace: string; - name: string; - roles: string[]; - createdAt: string; + nickname: string; + permissions: string[]; profile: { colorA: string; colorB: string; @@ -24,6 +23,7 @@ export interface UserEdit { colorB: string; icon: string; }; + nickname?: string; } export interface BookmarkResponse { diff --git a/src/backend/metadata/traktApi.ts b/src/backend/metadata/traktApi.ts index 9c8b0142..67a78048 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,83 @@ 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 + * Returns an object indicating if the token was cached or freshly fetched + */ +const getFreshTurnstileToken = async (): Promise<{ + token: string; + isCached: boolean; +}> => { + 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, isCached: true }; + } + } 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, isCached: false }; + } 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 +132,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 +143,61 @@ 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 { token: turnstileToken, isCached } = + await getFreshTurnstileToken(); + + // 2. Only validate with server if token wasn't cached (newly fetched) + if (!isCached) { + 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 +211,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 +222,61 @@ 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 { token: turnstileToken, isCached } = + await getFreshTurnstileToken(); + + // 2. Only validate with server if token wasn't cached (newly fetched) + if (!isCached) { + 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 @@ -140,7 +315,6 @@ export const getNetworkContent = (tmdbId: string) => // Curated movie lists export const getNarrativeMovies = () => fetchFromTrakt("/narrative"); export const getTopMovies = () => fetchFromTrakt("/top"); -export const getLifetimeMovies = () => fetchFromTrakt("/lifetime"); export const getNeverHeardMovies = () => fetchFromTrakt("/never"); export const getLGBTQContent = () => fetchFromTrakt("/LGBTQ"); export const getMindfuckMovies = () => fetchFromTrakt("/mindfuck"); @@ -166,11 +340,6 @@ export const getCuratedMovieLists = async (): Promise => { slug: "top", endpoint: "/top", }, - { - name: "1001 Movies You Must See Before You Die", - slug: "lifetime", - endpoint: "/lifetime", - }, { name: "Great Movies You May Have Never Heard Of", slug: "never", diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index ec883def..7772abec 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto"; import { Icon, Icons } from "@/components/Icon"; @@ -55,11 +56,22 @@ export function UserAvatar(props: { : null, [auth], ); + const { t } = useTranslation(); if (!auth.account || auth.account === null) return null; const deviceName = bufferSeed - ? decryptData(auth.account.deviceName, bufferSeed) + ? (() => { + try { + return decryptData(auth.account.deviceName, bufferSeed); + } catch (error) { + console.warn( + "Failed to decrypt device name in Avatar, using fallback:", + error, + ); + return t("settings.account.devices.unknownDevice"); + } + })() : "..."; return ( diff --git a/src/components/LinksDropdown.tsx b/src/components/LinksDropdown.tsx index 4300d6fa..d4338b4b 100644 --- a/src/components/LinksDropdown.tsx +++ b/src/components/LinksDropdown.tsx @@ -257,7 +257,17 @@ export function LinksDropdown(props: { children: React.ReactNode }) { {deviceName && bufferSeed ? ( - {decryptData(deviceName, bufferSeed)} + {(() => { + try { + return decryptData(deviceName, bufferSeed); + } catch (error) { + console.warn( + "Failed to decrypt device name in LinksDropdown, using fallback:", + error, + ); + return t("settings.account.unknownDevice"); + } + })()} ) : ( diff --git a/src/components/player/atoms/WatchPartyStatus.tsx b/src/components/player/atoms/WatchPartyStatus.tsx index 5f04ed68..edcb82c1 100644 --- a/src/components/player/atoms/WatchPartyStatus.tsx +++ b/src/components/player/atoms/WatchPartyStatus.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; import { useWatchPartySync } from "@/hooks/useWatchPartySync"; +import { useAuthStore } from "@/stores/auth"; import { getProgressPercentage } from "@/stores/progress"; import { useWatchPartyStore } from "@/stores/watchParty"; @@ -13,6 +14,7 @@ export function WatchPartyStatus() { const [expanded, setExpanded] = useState(false); const [showNotification, setShowNotification] = useState(false); const [lastUserCount, setLastUserCount] = useState(1); + const account = useAuthStore((s) => s.account); const { roomUsers, @@ -43,6 +45,14 @@ export function WatchPartyStatus() { setExpanded(!expanded); }; + // Get display name for a user (nickname if it's the current user, otherwise truncated userId) + const getDisplayName = (userId: string) => { + if (account?.userId === userId && account?.nickname) { + return account.nickname; + } + return `${userId.substring(0, 12)}...`; + }; + return (
- {user.userId.substring(0, 8)}... + {getDisplayName(user.userId)} diff --git a/src/components/player/atoms/settings/WatchPartyView.tsx b/src/components/player/atoms/settings/WatchPartyView.tsx index d10b0eab..78297eb4 100644 --- a/src/components/player/atoms/settings/WatchPartyView.tsx +++ b/src/components/player/atoms/settings/WatchPartyView.tsx @@ -34,6 +34,14 @@ export function WatchPartyView({ id }: { id: string }) { const [validationError, setValidationError] = useState(null); const account = useAuthStore((s) => s.account); + // Get display name for a user (nickname if it's the current user, otherwise truncated userId) + const getDisplayName = (userId: string) => { + if (account?.userId === userId && account?.nickname) { + return account.nickname; + } + return `${userId.substring(0, 8)}...`; + }; + const backendMeta = useAsync(async () => { if (!backendUrl) return; return getBackendMeta(backendUrl); @@ -322,7 +330,7 @@ export function WatchPartyView({ id }: { id: string }) { : "text-type-secondary" } > - {user.userId.substring(0, 8)}... + {getDisplayName(user.userId)} diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts index 0bd48018..77c2fb86 100644 --- a/src/hooks/auth/useAuth.ts +++ b/src/hooks/auth/useAuth.ts @@ -188,6 +188,16 @@ export function useAuth() { getGroupOrder(backendUrl, account), ]); + // Update account store with fresh user data (including nickname) + const { setAccount } = useAuthStore.getState(); + if (account) { + setAccount({ + ...account, + nickname: user.user.nickname, + profile: user.user.profile, + }); + } + syncData( user.user, user.session, diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts index 68cc2ac8..e7e8b215 100644 --- a/src/hooks/auth/useAuthData.ts +++ b/src/hooks/auth/useAuthData.ts @@ -32,7 +32,8 @@ export function useAuthData() { (s) => s.importSubtitleLanguage, ); const setFebboxKey = usePreferencesStore((s) => s.setFebboxKey); - const setRealDebridKey = usePreferencesStore((s) => s.setRealDebridKey); + const setdebridToken = usePreferencesStore((s) => s.setdebridToken); + const setdebridService = usePreferencesStore((s) => s.setdebridService); const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks); const replaceItems = useProgressStore((s) => s.replaceItems); @@ -101,6 +102,7 @@ export function useAuthData() { sessionId: loginResponse.session.id, deviceName: session.device, profile: user.profile, + nickname: user.nickname, seed, }; setAccount(account); @@ -231,8 +233,12 @@ export function useAuthData() { setFebboxKey(settings.febboxKey); } - if (settings.realDebridKey !== undefined) { - setRealDebridKey(settings.realDebridKey); + if (settings.debridToken !== undefined) { + setdebridToken(settings.debridToken); + } + + if (settings.debridService !== undefined) { + setdebridService(settings.debridService); } if (settings.enableLowPerformanceMode !== undefined) { @@ -287,7 +293,8 @@ export function useAuthData() { setDisabledEmbeds, setProxyTmdb, setFebboxKey, - setRealDebridKey, + setdebridToken, + setdebridService, setEnableLowPerformanceMode, setEnableNativeSubtitles, setEnableHoldToBoost, diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index 48fc0c61..3ab198b0 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -42,10 +42,12 @@ export function useSettingsState( appLanguage: string, subtitleStyling: SubtitleStyling, deviceName: string, + nickname: string, proxyUrls: string[] | null, backendUrl: string | null, febboxKey: string | null, - realDebridKey: string | null, + debridToken: string | null, + debridService: string, profile: | { colorA: string; @@ -85,11 +87,17 @@ export function useSettingsState( const [febboxKeyState, setFebboxKey, resetFebboxKey, febboxKeyChanged] = useDerived(febboxKey); const [ - realDebridKeyState, - setRealDebridKey, - resetRealDebridKey, - realDebridKeyChanged, - ] = useDerived(realDebridKey); + debridTokenState, + setdebridToken, + resetdebridToken, + debridTokenChanged, + ] = useDerived(debridToken); + const [ + debridServiceState, + setdebridService, + _resetdebridService, + debridServiceChanged, + ] = useDerived(debridService); const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme); const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme); const resetPreviewTheme = useCallback( @@ -110,6 +118,8 @@ export function useSettingsState( resetDeviceName, deviceNameChanged, ] = useDerived(deviceName); + const [nicknameState, setNicknameState, resetNickname, nicknameChanged] = + useDerived(nickname); const [profileState, setProfileState, resetProfile, profileChanged] = useDerived(profile); const [ @@ -261,8 +271,9 @@ export function useSettingsState( resetProxyUrls(); resetBackendUrl(); resetFebboxKey(); - resetRealDebridKey(); + resetdebridToken(); resetDeviceName(); + resetNickname(); resetProfile(); resetEnableThumbnails(); resetEnableAutoplay(); @@ -295,10 +306,12 @@ export function useSettingsState( appLanguageChanged || subStylingChanged || deviceNameChanged || + nicknameChanged || backendUrlChanged || proxyUrlsChanged || febboxKeyChanged || - realDebridKeyChanged || + debridTokenChanged || + debridServiceChanged || profileChanged || enableThumbnailsChanged || enableAutoplayChanged || @@ -348,6 +361,11 @@ export function useSettingsState( set: setDeviceNameState, changed: deviceNameChanged, }, + nickname: { + state: nicknameState, + set: setNicknameState, + changed: nicknameChanged, + }, proxyUrls: { state: proxyUrlsState, set: setProxyUrls, @@ -363,10 +381,15 @@ export function useSettingsState( set: setFebboxKey, changed: febboxKeyChanged, }, - realDebridKey: { - state: realDebridKeyState, - set: setRealDebridKey, - changed: realDebridKeyChanged, + debridToken: { + state: debridTokenState, + set: setdebridToken, + changed: debridTokenChanged, + }, + debridService: { + state: debridServiceState, + set: setdebridService, + changed: debridServiceChanged, }, profile: { state: profileState, diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 2cda40cf..c4bb6f90 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -114,6 +114,8 @@ export function AccountSettings(props: { account: AccountWithToken; deviceName: string; setDeviceName: (s: string) => void; + nickname: string; + setNickname: (s: string) => void; colorA: string; setColorA: (s: string) => void; colorB: string; @@ -136,6 +138,8 @@ export function AccountSettings(props: { s.febboxKey); const setFebboxKey = usePreferencesStore((s) => s.setFebboxKey); - const realDebridKey = usePreferencesStore((s) => s.realDebridKey); - const setRealDebridKey = usePreferencesStore((s) => s.setRealDebridKey); + const debridToken = usePreferencesStore((s) => s.debridToken); + const setdebridToken = usePreferencesStore((s) => s.setdebridToken); + const debridService = usePreferencesStore((s) => s.debridService); + const setdebridService = usePreferencesStore((s) => s.setdebridService); const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails); @@ -483,10 +489,17 @@ export function SettingsPage() { const account = useAuthStore((s) => s.account); const updateProfile = useAuthStore((s) => s.setAccountProfile); const updateDeviceName = useAuthStore((s) => s.updateDeviceName); + const updateNickname = useAuthStore((s) => s.setAccountNickname); const decryptedName = useMemo(() => { if (!account) return ""; - return decryptData(account.deviceName, base64ToBuffer(account.seed)); - }, [account]); + try { + return decryptData(account.deviceName, base64ToBuffer(account.seed)); + } catch (error) { + console.warn("Failed to decrypt device name, using fallback:", error); + // Return a fallback device name if decryption fails + return t("settings.account.devices.unknownDevice"); + } + }, [account, t]); const backendUrl = useBackendUrl(); @@ -500,23 +513,25 @@ export function SettingsPage() { if (settings.febboxKey) { setFebboxKey(settings.febboxKey); } - if (settings.realDebridKey) { - setRealDebridKey(settings.realDebridKey); + if (settings.debridToken) { + setdebridToken(settings.debridToken); } } }; loadSettings(); - }, [account, backendUrl, setFebboxKey, setRealDebridKey]); + }, [account, backendUrl, setFebboxKey, setdebridToken, setdebridService]); const state = useSettingsState( activeTheme, appLanguage, subStyling, decryptedName, + account?.nickname || "", proxySet, backendUrlSetting, febboxKey, - realDebridKey, + debridToken, + debridService, account ? account.profile : undefined, enableThumbnails, enableAutoplay, @@ -586,7 +601,8 @@ export function SettingsPage() { state.theme.changed || state.proxyUrls.changed || state.febboxKey.changed || - state.realDebridKey.changed || + state.debridToken.changed || + state.debridService.changed || state.enableThumbnails.changed || state.enableAutoplay.changed || state.enableSkipCredits.changed || @@ -613,7 +629,8 @@ export function SettingsPage() { applicationTheme: state.theme.state, proxyUrls: state.proxyUrls.state?.filter((v) => v !== "") ?? null, febboxKey: state.febboxKey.state, - realDebridKey: state.realDebridKey.state, + debridToken: state.debridToken.state, + debridService: state.debridService.state, enableThumbnails: state.enableThumbnails.state, enableAutoplay: state.enableAutoplay.state, enableSkipCredits: state.enableSkipCredits.state, @@ -646,10 +663,17 @@ export function SettingsPage() { }); updateDeviceName(newDeviceName); } - if (state.profile.changed) { + if (state.nickname.changed) { + await editUser(backendUrl, account, { + nickname: state.nickname.state, + }); + updateNickname(state.nickname.state); + } + if (state.profile.changed && state.profile.state) { await editUser(backendUrl, account, { profile: state.profile.state, }); + updateProfile(state.profile.state); } } @@ -671,7 +695,8 @@ export function SettingsPage() { setProxySet(state.proxyUrls.state?.filter((v) => v !== "") ?? null); setEnableSourceOrder(state.enableSourceOrder.state); setFebboxKey(state.febboxKey.state); - setRealDebridKey(state.realDebridKey.state); + setdebridToken(state.debridToken.state); + setdebridService(state.debridService.state); setProxyTmdb(state.proxyTmdb.state); setEnableCarouselView(state.enableCarouselView.state); setForceCompactEpisodeView(state.forceCompactEpisodeView.state); @@ -701,7 +726,8 @@ export function SettingsPage() { backendUrl, setEnableThumbnails, setFebboxKey, - setRealDebridKey, + setdebridToken, + setdebridService, state, setEnableAutoplay, setEnableSkipCredits, @@ -720,6 +746,7 @@ export function SettingsPage() { setProxySet, updateDeviceName, updateProfile, + updateNickname, logout, setBackendUrl, setProxyTmdb, @@ -754,6 +781,8 @@ export function SettingsPage() { account={user.account} deviceName={state.deviceName.state} setDeviceName={state.deviceName.set} + nickname={state.nickname.state} + setNickname={state.nickname.set} colorA={state.profile.state.colorA} setColorA={(v) => { state.profile.set((s) => @@ -859,8 +888,10 @@ export function SettingsPage() { setProxyUrls={state.proxyUrls.set} febboxKey={state.febboxKey.state} setFebboxKey={state.febboxKey.set} - realDebridKey={state.realDebridKey.state} - setRealDebridKey={state.realDebridKey.set} + debridToken={state.debridToken.state} + setdebridToken={state.debridToken.set} + debridService={state.debridService.state} + setdebridService={state.debridService.set} proxyTmdb={state.proxyTmdb.state} setProxyTmdb={state.proxyTmdb.set} /> diff --git a/src/pages/developer/VideoTesterView.tsx b/src/pages/developer/VideoTesterView.tsx index a62968e4..ef218383 100644 --- a/src/pages/developer/VideoTesterView.tsx +++ b/src/pages/developer/VideoTesterView.tsx @@ -1,14 +1,20 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { prepareStream } from "@/backend/extension/streams"; import { Button } from "@/components/buttons/Button"; +import { Toggle } from "@/components/buttons/Toggle"; import { Dropdown } from "@/components/form/Dropdown"; +import { Icon, Icons } from "@/components/Icon"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { Title } from "@/components/text/Title"; +import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { TextInputControl } from "@/components/text-inputs/TextInputControl"; +import { Divider } from "@/components/utils/Divider"; import { PlaybackErrorPart } from "@/pages/parts/player/PlaybackErrorPart"; import { PlayerPart } from "@/pages/parts/player/PlayerPart"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { SourceSliceSource, StreamType } from "@/stores/player/utils/qualities"; +import { type ExtensionStatus, getExtensionState } from "@/utils/extension"; const testMeta: PlayerMeta = { releaseYear: 2010, @@ -32,14 +38,64 @@ export default function VideoTesterView() { const { status, playMedia, setMeta } = usePlayer(); const [selected, setSelected] = useState("mp4"); const [inputSource, setInputSource] = useState(""); + const [extensionState, setExtensionState] = + useState("unknown"); + const [headersEnabled, setHeadersEnabled] = useState(false); + const [headers, setHeaders] = useState>( + [{ key: "", value: "" }], + ); + + // Check extension state on mount + useEffect(() => { + getExtensionState().then(setExtensionState); + }, []); + + // Header management functions + const addHeader = useCallback(() => { + setHeaders((prev) => [...prev, { key: "", value: "" }]); + }, []); + + const updateHeader = useCallback( + (index: number, field: "key" | "value", value: string) => { + setHeaders((prev) => + prev.map((header, i) => + i === index ? { ...header, [field]: value } : header, + ), + ); + }, + [], + ); + + const removeHeader = useCallback((index: number) => { + setHeaders((prev) => prev.filter((_, i) => i !== index)); + }, []); + + const toggleHeaders = useCallback(() => { + const newEnabled = !headersEnabled; + setHeadersEnabled(newEnabled); + if (!newEnabled) { + setHeaders([{ key: "", value: "" }]); + } + }, [headersEnabled]); const start = useCallback( - (url: string, type: StreamType) => { + async (url: string, type: StreamType) => { + // Build headers object from enabled headers + const headersObj: Record = {}; + if (headersEnabled) { + headers.forEach(({ key, value }) => { + if (key.trim() && value.trim()) { + headersObj[key.trim()] = value.trim(); + } + }); + } + let source: SourceSliceSource; if (type === "hls") { source = { type: "hls", url, + ...(Object.keys(headersObj).length > 0 && { headers: headersObj }), }; } else if (type === "mp4") { source = { @@ -50,12 +106,38 @@ export default function VideoTesterView() { url, }, }, + ...(Object.keys(headersObj).length > 0 && { headers: headersObj }), }; } else throw new Error("Invalid type"); + + // Prepare stream headers if extension is active and headers are present + if (extensionState === "success" && Object.keys(headersObj).length > 0) { + // Create a mock Stream object for prepareStream + const mockStream: any = { + type: type === "hls" ? "hls" : "file", + ...(type === "hls" + ? { playlist: url } + : { + qualities: { + unknown: { + type: "mp4", + url, + }, + }, + }), + headers: headersObj, + }; + try { + await prepareStream(mockStream); + } catch (error) { + console.warn("Failed to prepare stream headers:", error); + } + } + setMeta(testMeta); playMedia(source, [], null); }, - [playMedia, setMeta], + [playMedia, setMeta, headersEnabled, headers, extensionState], ); return ( @@ -85,13 +167,73 @@ export default function VideoTesterView() { setSelectedItem={(item) => setSelected(item.id)} />
+ + {extensionState === "success" && ( +
+
+
+

Headers (Beta)

+
+
+ +
+
+ {headersEnabled && ( + <> + +
+ {headers.length === 0 ? ( +

No headers configured.

+ ) : ( + headers.map((header, index) => ( +
+ + updateHeader(index, "key", value) + } + placeholder="Key" + /> + + updateHeader(index, "value", value) + } + placeholder="Value" + /> + +
+ )) + )} +
+ + + + )} +
+ )} + -
Preset tests
diff --git a/src/pages/discover/MoreContent.tsx b/src/pages/discover/MoreContent.tsx index 29f86efb..af562f2b 100644 --- a/src/pages/discover/MoreContent.tsx +++ b/src/pages/discover/MoreContent.tsx @@ -97,6 +97,11 @@ export function MoreContent({ onShowDetails }: MoreContentProps) { setIsContentVisible(false); }, [isLoading, mediaItems, currentPage]); + // Scroll to top when entering the page + useEffect(() => { + window.scrollTo(0, 0); + }, [contentType, mediaType, id]); + const handleBack = () => { if (lastView) { navigate(lastView.url); diff --git a/src/pages/discover/components/MediaCarousel.tsx b/src/pages/discover/components/MediaCarousel.tsx index 49c148cc..177b1f42 100644 --- a/src/pages/discover/components/MediaCarousel.tsx +++ b/src/pages/discover/components/MediaCarousel.tsx @@ -198,7 +198,7 @@ export function MediaCarousel({ ]); // Fetch media using our hook - only when carousel has been visible - const { media, sectionTitle } = useDiscoverMedia({ + const { media, sectionTitle, actualContentType } = useDiscoverMedia({ contentType, mediaType, id: selectedProviderId || selectedGenreId || selectedRecommendationId, @@ -298,7 +298,7 @@ export function MediaCarousel({ if (showRecommendations && selectedRecommendationId) { return `${baseLink}/recommendations/${selectedRecommendationId}/${mediaType}`; } - return `${baseLink}/${content.type}/${mediaType}`; + return `${baseLink}/${actualContentType}/${mediaType}`; }, [ moreLink, showProviders, @@ -308,7 +308,7 @@ export function MediaCarousel({ showRecommendations, selectedRecommendationId, mediaType, - content.type, + actualContentType, ]); // Loading state diff --git a/src/pages/discover/hooks/useDiscoverMedia.ts b/src/pages/discover/hooks/useDiscoverMedia.ts index a20f1c07..ea4e719d 100644 --- a/src/pages/discover/hooks/useDiscoverMedia.ts +++ b/src/pages/discover/hooks/useDiscoverMedia.ts @@ -121,6 +121,8 @@ export function useDiscoverMedia({ const [sectionTitle, setSectionTitle] = useState(""); const [currentContentType, setCurrentContentType] = useState(contentType); + const [actualContentType, setActualContentType] = + useState(contentType); const { t } = useTranslation(); const userLanguage = useLanguageStore((s) => s.language); @@ -131,6 +133,7 @@ export function useDiscoverMedia({ if (contentType !== currentContentType) { setMedia([]); setCurrentContentType(contentType); + setActualContentType(contentType); // Reset actual content type to original } }, [contentType, currentContentType]); @@ -475,6 +478,7 @@ export function useDiscoverMedia({ console.info(`Falling back from ${contentType} to ${fallbackType}`); try { const fallbackData = await attemptFetch(fallbackType); + setActualContentType(fallbackType); // Set actual content type to fallback setMedia((prevMedia) => { // If page is 1, replace the media array, otherwise append return page === 1 @@ -526,5 +530,6 @@ export function useDiscoverMedia({ hasMore, refetch: fetchMedia, sectionTitle, + actualContentType, }; } diff --git a/src/pages/discover/types/discover.ts b/src/pages/discover/types/discover.ts index 6343b3e2..2754fa05 100644 --- a/src/pages/discover/types/discover.ts +++ b/src/pages/discover/types/discover.ts @@ -47,6 +47,7 @@ export interface UseDiscoverMediaReturn { hasMore: boolean; refetch: () => Promise; sectionTitle: string; + actualContentType: DiscoverContentType; } export interface Provider { diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index b45eab04..1d47aad9 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -1,10 +1,7 @@ -import { useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; -import { Toggle } from "@/components/buttons/Toggle"; import { Icon, Icons } from "@/components/Icon"; -import { SettingsCard } from "@/components/layout/SettingsCard"; import { Stepper } from "@/components/layout/Stepper"; import { BiggerCenterContainer } from "@/components/layout/ThinContainer"; import { VerticalLine } from "@/components/layout/VerticalLine"; @@ -14,11 +11,6 @@ import { ModalCard, useModal, } from "@/components/overlays/Modal"; -import { - StatusCircle, - StatusCircleProps, -} from "@/components/player/internals/StatusCircle"; -import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { Divider } from "@/components/utils/Divider"; import { Ol } from "@/components/utils/Ol"; import { @@ -43,133 +35,7 @@ import { conf } from "@/setup/config"; import { usePreferencesStore } from "@/stores/preferences"; import { getProxyUrls } from "@/utils/proxyUrls"; -import { FebboxSetup } from "../parts/settings/ConnectionsPart"; -import { Status, testRealDebridKey } from "../parts/settings/SetupPart"; - -async function getRealDebridKeyStatus(realDebridKey: string | null) { - if (realDebridKey) { - const status: Status = await testRealDebridKey(realDebridKey); - return status; - } - return "unset"; -} - -export function RealDebridSetup() { - const { t } = useTranslation(); - const realDebridKey = usePreferencesStore((s) => s.realDebridKey); - const setRealDebridKey = usePreferencesStore((s) => s.setRealDebridKey); - - // Initialize isExpanded based on whether realDebridKey has a value - const [isExpanded, setIsExpanded] = useState( - realDebridKey !== null && realDebridKey !== "", - ); - - // Add a separate effect to set the initial state - useEffect(() => { - // If we have a valid key, make sure the section is expanded - if (realDebridKey && realDebridKey.length > 0) { - setIsExpanded(true); - } - }, [realDebridKey]); - - const [status, setStatus] = useState("unset"); - const statusMap: Record = { - error: "error", - success: "success", - unset: "noresult", - api_down: "error", - invalid_token: "error", - }; - - useEffect(() => { - const checkTokenStatus = async () => { - const result = await getRealDebridKeyStatus(realDebridKey); - setStatus(result); - }; - checkTokenStatus(); - }, [realDebridKey]); - - // Toggle handler that preserves the key - const toggleExpanded = () => { - if (isExpanded) { - // Store the key temporarily instead of setting to null - setRealDebridKey(""); - setIsExpanded(false); - } else { - setIsExpanded(true); - } - }; - - if (conf().ALLOW_REAL_DEBRID_KEY) { - return ( -
- -
-
-

- {t("settings.connections.realdebrid.title", "Real Debrid API")} -

-

- {t( - "settings.connections.realdebrid.description", - "Enter your Real Debrid API key to access premium sources.", - )} -

-
-
- -
-
- {isExpanded ? ( - <> - -

- {t("settings.connections.realdebrid.tokenLabel", "API Key")} -

-
- - { - setRealDebridKey(newToken); - }} - value={realDebridKey ?? ""} - placeholder="API Key" - passwordToggleable - className="flex-grow" - /> -
- {status === "error" && ( -

- {t( - "settings.connections.realdebrid.status.failure", - "Failed to connect to Real Debrid. Please check your API key.", - )} -

- )} - {status === "api_down" && ( -

- {t( - "settings.connections.realdebrid.status.api_down", - "Real Debrid API is currently unavailable. Please try again later.", - )} -

- )} - {status === "invalid_token" && ( -

- {t( - "settings.connections.realdebrid.status.invalid_token", - "Invalid API key or non-premium account. Real Debrid requires a premium account.", - )} -

- )} - - ) : null} -
-
- ); - } - return null; -} +import { DebridEdit, FebboxSetup } from "../parts/settings/ConnectionsPart"; function Item(props: { title: string; children: React.ReactNode }) { return ( @@ -419,7 +285,6 @@ export function OnboardingPage() { )}
- {/* */}
s.febboxKey)} @@ -427,6 +292,15 @@ export function OnboardingPage() { mode="onboarding" />
+
+ s.debridToken)} + setdebridToken={usePreferencesStore((s) => s.setdebridToken)} + debridService={usePreferencesStore((s) => s.debridService)} + setdebridService={usePreferencesStore((s) => s.setdebridService)} + mode="onboarding" + /> +
); diff --git a/src/pages/parts/admin/M3U8TestPart.tsx b/src/pages/parts/admin/M3U8TestPart.tsx index 5cb4d1b4..f792542b 100644 --- a/src/pages/parts/admin/M3U8TestPart.tsx +++ b/src/pages/parts/admin/M3U8TestPart.tsx @@ -6,16 +6,10 @@ import { Button } from "@/components/buttons/Button"; import { Toggle } from "@/components/buttons/Toggle"; import { Icon, Icons } from "@/components/Icon"; import { Box } from "@/components/layout/Box"; -import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { Divider } from "@/components/utils/Divider"; import { Heading2 } from "@/components/utils/Text"; import { getM3U8ProxyUrls } from "@/utils/proxyUrls"; -interface M3U8Proxy { - id: string; - url: string; -} - export function M3U8ProxyItem(props: { name: string; errored?: boolean; @@ -63,26 +57,13 @@ export function M3U8ProxyItem(props: { } export function M3U8TestPart() { - const defaultProxyList = useMemo(() => { + const m3u8ProxyList = useMemo(() => { return getM3U8ProxyUrls().map((v, ind) => ({ id: ind.toString(), url: v, })); }, []); - // Load editable proxy list from localStorage - const [m3u8ProxyList, setM3u8ProxyList] = useState(() => { - const saved = localStorage.getItem("m3u8-proxy-list"); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return defaultProxyList; - } - } - return defaultProxyList; - }); - // Load enabled proxies from localStorage const [enabledProxies, setEnabledProxies] = useState>( () => { @@ -99,11 +80,6 @@ export function M3U8TestPart() { }, ); - // Save proxy list to localStorage - useEffect(() => { - localStorage.setItem("m3u8-proxy-list", JSON.stringify(m3u8ProxyList)); - }, [m3u8ProxyList]); - // Save enabled proxies to localStorage useEffect(() => { localStorage.setItem("m3u8-proxy-enabled", JSON.stringify(enabledProxies)); @@ -130,9 +106,9 @@ export function M3U8TestPart() { setProxyState([]); const activeProxies = m3u8ProxyList.filter( - (proxy: M3U8Proxy) => enabledProxies[proxy.id], + (proxy) => enabledProxies[proxy.id], ); - const proxyPromises = activeProxies.map(async (proxy: M3U8Proxy) => { + const proxyPromises = activeProxies.map(async (proxy) => { try { if (proxy.url.endsWith("/")) { updateProxy(proxy.id, { @@ -177,76 +153,29 @@ export function M3U8TestPart() { })); }; - const addProxy = () => { - const newId = Date.now().toString(); - setM3u8ProxyList((prev: M3U8Proxy[]) => [...prev, { id: newId, url: "" }]); - setEnabledProxies((prev) => ({ ...prev, [newId]: true })); - }; - - const changeProxy = (id: string, url: string) => { - setM3u8ProxyList((prev: M3U8Proxy[]) => - prev.map((proxy: M3U8Proxy) => - proxy.id === id ? { ...proxy, url } : proxy, - ), - ); - }; - - const removeProxy = (id: string) => { - setM3u8ProxyList((prev: M3U8Proxy[]) => - prev.filter((proxy: M3U8Proxy) => proxy.id !== id), - ); - setEnabledProxies((prev) => { - const newEnabled = { ...prev }; - delete newEnabled[id]; - return newEnabled; - }); - }; - - const resetProxies = () => { - setM3u8ProxyList(defaultProxyList); - setEnabledProxies( - Object.fromEntries( - defaultProxyList.map((proxy: M3U8Proxy) => [proxy.id, true]), - ), - ); - }; - - const allEnabled = m3u8ProxyList.every( - (proxy: M3U8Proxy) => enabledProxies[proxy.id], - ); - const noneEnabled = m3u8ProxyList.every( - (proxy: M3U8Proxy) => !enabledProxies[proxy.id], - ); + const allEnabled = m3u8ProxyList.every((proxy) => enabledProxies[proxy.id]); + const noneEnabled = m3u8ProxyList.every((proxy) => !enabledProxies[proxy.id]); const handleToggleAll = () => { if (allEnabled) { // Disable all setEnabledProxies( - Object.fromEntries( - m3u8ProxyList.map((proxy: M3U8Proxy) => [proxy.id, false]), - ), + Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, false])), ); } else { // Enable all setEnabledProxies( - Object.fromEntries( - m3u8ProxyList.map((proxy: M3U8Proxy) => [proxy.id, true]), - ), + Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, true])), ); } }; const enabledCount = m3u8ProxyList.filter( - (proxy: M3U8Proxy) => enabledProxies[proxy.id], + (proxy) => enabledProxies[proxy.id], ).length; return ( <> - M3U8 Proxy Configuration - -

M3U8 Proxy URLs

-
- M3U8 Proxy tests

@@ -262,7 +191,7 @@ export function M3U8TestPart() {

- {m3u8ProxyList.map((v: M3U8Proxy, i: number) => { + {m3u8ProxyList.map((v, i) => { const s = proxyState.find((segment) => segment.id === v.id); const name = `M3U8 Proxy ${i + 1}`; const enabled = enabledProxies[v.id]; @@ -380,40 +309,6 @@ export function M3U8TestPart() { )}
- -
- {m3u8ProxyList.length === 0 ? ( -

No M3U8 proxies configured.

- ) : ( - m3u8ProxyList.map((proxy: M3U8Proxy) => ( -
- changeProxy(proxy.id, url)} - placeholder="https://" - /> - -
- )) - )} -
-
- - -
); diff --git a/src/pages/parts/auth/VerifyPassphrasePart.tsx b/src/pages/parts/auth/VerifyPassphrasePart.tsx index 8490394b..fad3d7a0 100644 --- a/src/pages/parts/auth/VerifyPassphrasePart.tsx +++ b/src/pages/parts/auth/VerifyPassphrasePart.tsx @@ -59,7 +59,8 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) { disabledEmbeds: store.disabledEmbeds, proxyTmdb: store.proxyTmdb, febboxKey: store.febboxKey, - realDebridKey: store.realDebridKey, + debridToken: store.debridToken, + debridService: store.debridService, enableLowPerformanceMode: store.enableLowPerformanceMode, enableNativeSubtitles: store.enableNativeSubtitles, enableHoldToBoost: store.enableHoldToBoost, diff --git a/src/pages/parts/settings/AccountEditPart.tsx b/src/pages/parts/settings/AccountEditPart.tsx index 3b3bb16f..e989698e 100644 --- a/src/pages/parts/settings/AccountEditPart.tsx +++ b/src/pages/parts/settings/AccountEditPart.tsx @@ -13,6 +13,8 @@ import { ProfileEditModal } from "@/pages/parts/settings/ProfileEditModal"; export function AccountEditPart(props: { deviceName: string; setDeviceName: (s: string) => void; + nickname: string; + setNickname: (s: string) => void; colorA: string; setColorA: (s: string) => void; colorB: string; @@ -59,24 +61,38 @@ export function AccountEditPart(props: { />
-
- props.setDeviceName(value)} - /> -
- +
+
+ props.setNickname(value)} + className="w-full" + />
+
+ props.setDeviceName(value)} + className="w-full" + /> +
+
+
+
diff --git a/src/pages/parts/settings/ConnectionsPart.tsx b/src/pages/parts/settings/ConnectionsPart.tsx index 544706e0..c0888c3d 100644 --- a/src/pages/parts/settings/ConnectionsPart.tsx +++ b/src/pages/parts/settings/ConnectionsPart.tsx @@ -9,6 +9,7 @@ import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; import { Toggle } from "@/components/buttons/Toggle"; +import { Dropdown } from "@/components/form/Dropdown"; import { Icon, Icons } from "@/components/Icon"; import { SettingsCard } from "@/components/layout/SettingsCard"; import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; @@ -24,7 +25,8 @@ import { SetupPart, Status, testFebboxKey, - testRealDebridKey, + testTorboxToken, + testdebridToken, } from "@/pages/parts/settings/SetupPart"; import { conf } from "@/setup/config"; import { useAuthStore } from "@/stores/auth"; @@ -49,9 +51,13 @@ interface FebboxKeyProps { setFebboxKey: (value: string | null) => void; } -interface RealDebridKeyProps { - realDebridKey: string | null; - setRealDebridKey: Dispatch>; +interface DebridProps { + debridToken: string | null; + setdebridToken: (value: string | null) => void; + debridService: string; + setdebridService: (value: string) => void; + // eslint-disable-next-line react/no-unused-prop-types + mode?: "onboarding" | "settings"; } function ProxyEdit({ @@ -252,14 +258,14 @@ export function FebboxSetup({ const exampleModal = useModal("febbox-example"); // Initialize expansion state for onboarding mode - const [isExpanded, setIsExpanded] = useState( + const [isFebboxExpanded, setIsFebboxExpanded] = useState( mode === "onboarding" && febboxKey !== null && febboxKey !== "", ); // Expand when key is set in onboarding mode useEffect(() => { if (mode === "onboarding" && febboxKey && febboxKey.length > 0) { - setIsExpanded(true); + setIsFebboxExpanded(true); } }, [febboxKey, mode]); @@ -293,14 +299,14 @@ export function FebboxSetup({ }, [febboxKey]); // Toggle handler based on mode - const toggleExpanded = () => { + const toggleFebboxExpanded = () => { if (mode === "onboarding") { // Onboarding mode: expand/collapse, preserve key - if (isExpanded) { + if (isFebboxExpanded) { setFebboxKey(""); - setIsExpanded(false); + setIsFebboxExpanded(false); } else { - setIsExpanded(true); + setIsFebboxExpanded(true); } } else { // Settings mode: enable/disable @@ -309,7 +315,8 @@ export function FebboxSetup({ }; // Determine if content is visible - const isVisible = mode === "onboarding" ? isExpanded : febboxKey !== null; + const isFebboxVisible = + mode === "onboarding" ? isFebboxExpanded : febboxKey !== null; if (conf().ALLOW_FEBBOX_KEY) { return ( @@ -326,14 +333,14 @@ export function FebboxSetup({
- {isVisible ? ( + {isFebboxVisible ? ( <> @@ -460,33 +467,69 @@ export function FebboxSetup({ } } -async function getRealDebridKeyStatus(realDebridKey: string | null) { - if (realDebridKey) { - const status: Status = await testRealDebridKey(realDebridKey); +async function getdebridTokenStatus( + debridToken: string | null, + debridService: string, +) { + if (debridToken) { + const status: Status = + debridService === "torbox" + ? await testTorboxToken(debridToken) + : await testdebridToken(debridToken); return status; } return "unset"; } -function RealDebridKeyEdit({ - realDebridKey, - setRealDebridKey, -}: RealDebridKeyProps) { +export function DebridEdit({ + debridToken, + setdebridToken, + debridService, + setdebridService, + mode = "settings", +}: DebridProps) { const { t } = useTranslation(); const user = useAuthStore(); const preferences = usePreferencesStore(); + // Initialize expansion state for onboarding mode + const [isDebridExpanded, setIsDebridExpanded] = useState( + mode === "onboarding" && debridToken !== null && debridToken !== "", + ); + + // Expand when key is set in onboarding mode + useEffect(() => { + if (mode === "onboarding" && debridToken && debridToken.length > 0) { + setIsDebridExpanded(true); + } + }, [debridToken, mode]); + // Enable Real Debrid token when account is loaded and we have a token useEffect(() => { - if (user.account && realDebridKey === null && preferences.realDebridKey) { - setRealDebridKey(preferences.realDebridKey); + if (user.account && debridToken === null && preferences.debridToken) { + setdebridToken(preferences.debridToken); } - }, [ - user.account, - realDebridKey, - preferences.realDebridKey, - setRealDebridKey, - ]); + }, [user.account, debridToken, preferences.debridToken, setdebridToken]); + + // Determine if content is visible + const isDebridVisible = + mode === "onboarding" ? isDebridExpanded : debridToken !== null; + + // Toggle handler based on mode + const toggleDebridExpanded = () => { + if (mode === "onboarding") { + // Onboarding mode: expand/collapse, preserve key + if (isDebridExpanded) { + setdebridToken(""); + setIsDebridExpanded(false); + } else { + setIsDebridExpanded(true); + } + } else { + // Settings mode: enable/disable + setdebridToken(debridToken === null ? "" : null); + } + }; const [status, setStatus] = useState("unset"); const statusMap: Record = { @@ -499,69 +542,84 @@ function RealDebridKeyEdit({ useEffect(() => { const checkTokenStatus = async () => { - const result = await getRealDebridKeyStatus(realDebridKey); + const result = await getdebridTokenStatus(debridToken, debridService); setStatus(result); }; checkTokenStatus(); - }, [realDebridKey]); + }, [debridToken, debridService]); - if (conf().ALLOW_REAL_DEBRID_KEY) { + if (conf().ALLOW_DEBRID_KEY) { return (
-

{t("realdebrid.title")}

-

- {t("realdebrid.description")} +

{t("debrid.title")}

+ + + {/* fifth's referral code */} + + +

+ {t("debrid.notice")}

- - - real-debrid.com - -
- setRealDebridKey((s) => (s === null ? "" : null))} - enabled={realDebridKey !== null} - /> +
- {realDebridKey !== null ? ( + {isDebridVisible ? ( <>

- {t("realdebrid.tokenLabel")} + {t("debrid.tokenLabel")}

-
- - { - setRealDebridKey(newToken); - }} - value={realDebridKey ?? ""} - placeholder="ABC123..." - passwordToggleable - className="flex-grow" - /> +
+
+ + { + setdebridToken(newToken); + }} + value={debridToken ?? ""} + placeholder="ABC123..." + passwordToggleable + className="flex-grow" + /> +
+
+ setdebridService(item.id)} + direction="up" + /> +
{status === "error" && (

- {t("realdebrid.status.failure")} + {t("debrid.status.failure")}

)} {status === "api_down" && (

- {t("realdebrid.status.api_down")} + {t("debrid.status.api_down")}

)} {status === "invalid_token" && (

- {t("realdebrid.status.invalid_token")} + {t("debrid.status.invalid_token")}

)} @@ -573,10 +631,7 @@ function RealDebridKeyEdit({ } export function ConnectionsPart( - props: BackendEditProps & - ProxyEditProps & - FebboxKeyProps & - RealDebridKeyProps, + props: BackendEditProps & ProxyEditProps & FebboxKeyProps & DebridProps, ) { const { t } = useTranslation(); return ( @@ -594,15 +649,18 @@ export function ConnectionsPart( backendUrl={props.backendUrl} setBackendUrl={props.setBackendUrl} /> - +
); diff --git a/src/pages/parts/settings/DeviceListPart.tsx b/src/pages/parts/settings/DeviceListPart.tsx index 6f12d66e..1917ae69 100644 --- a/src/pages/parts/settings/DeviceListPart.tsx +++ b/src/pages/parts/settings/DeviceListPart.tsx @@ -75,7 +75,16 @@ export function DeviceListPart(props: { const deviceListSorted = useMemo(() => { if (!seed) return []; let list = sessions.map((session) => { - const decryptedName = decryptData(session.device, base64ToBuffer(seed)); + let decryptedName: string; + try { + decryptedName = decryptData(session.device, base64ToBuffer(seed)); + } catch (error) { + console.warn( + `Failed to decrypt device name for session ${session.id}:`, + error, + ); + decryptedName = t("settings.account.devices.unknownDevice"); + } return { current: session.id === currentSessionId, id: session.id, @@ -88,7 +97,7 @@ export function DeviceListPart(props: { return a.name.localeCompare(b.name); }); return list; - }, [seed, sessions, currentSessionId]); + }, [seed, sessions, currentSessionId, t]); if (!seed) return null; return ( @@ -96,10 +105,10 @@ export function DeviceListPart(props: { {t("settings.account.devices.title")} - {props.error ? ( -

{t("settings.account.devices.failed")}

- ) : props.loading ? ( + {props.loading ? ( + ) : props.error && deviceListSorted.length === 0 ? ( +

{t("settings.account.devices.failed")}

) : (
{deviceListSorted.map((session) => ( diff --git a/src/pages/parts/settings/SetupPart.tsx b/src/pages/parts/settings/SetupPart.tsx index 303af86c..0dd9ca3c 100644 --- a/src/pages/parts/settings/SetupPart.tsx +++ b/src/pages/parts/settings/SetupPart.tsx @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import classNames from "classnames"; +import { FetchError } from "ofetch"; import { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -42,7 +43,7 @@ type SetupData = { proxy: Status; defaultProxy: Status; febboxKeyTest?: Status; - realDebridKeyTest?: Status; + debridTokenTest?: Status; }; function testProxy(url: string) { @@ -58,7 +59,7 @@ function testProxy(url: string) { } export async function testFebboxKey(febboxKey: string | null): Promise { - const febboxApiTestUrl = `https://fed-api.pstream.mov/movie/412059`; + const febboxApiTestUrl = `https://fed-api.pstream.mov/movie/tt0325980`; if (!febboxKey) { return "unset"; @@ -142,10 +143,10 @@ export async function testFebboxKey(febboxKey: string | null): Promise { return "api_down"; } -export async function testRealDebridKey( - realDebridKey: string | null, +export async function testdebridToken( + debridToken: string | null, ): Promise { - if (!realDebridKey) { + if (!debridToken) { return "unset"; } @@ -160,7 +161,7 @@ export async function testRealDebridKey( { method: "GET", headers: { - Authorization: `Bearer ${realDebridKey}`, + Authorization: `Bearer ${debridToken}`, "Content-Type": "application/json", }, }, @@ -174,12 +175,32 @@ export async function testRealDebridKey( console.log("RD response did not indicate premium status"); attempts += 1; - if (attempts === maxAttempts) { + if (attempts === maxAttempts || data?.error_code === 8) { return "invalid_token"; } await sleep(3000); } catch (error) { console.error("RD API error:", error); + + // Check if it's a FetchError with error_code 8 (bad_token) + if (error instanceof FetchError) { + try { + const errorData = error.data; + if (errorData?.error_code === 8) { + console.log("RD token is invalid (error_code 8)"); + return "invalid_token"; + } + } catch (parseError) { + console.error("Failed to parse RD error response:", parseError); + } + + // For other HTTP errors (like 500, 502, etc.), treat as API down + if (error.statusCode && error.statusCode >= 500) { + console.log(`RD API down (status ${error.statusCode})`); + return "api_down"; + } + } + attempts += 1; if (attempts === maxAttempts) { return "api_down"; @@ -191,10 +212,22 @@ export async function testRealDebridKey( return "api_down"; } +export async function testTorboxToken( + torboxToken: string | null, +): Promise { + if (!torboxToken) { + return "unset"; + } + + // TODO: Implement Torbox token test + return "success"; +} + function useIsSetup() { const proxyUrls = useAuthStore((s) => s.proxySet); const febboxKey = usePreferencesStore((s) => s.febboxKey); - const realDebridKey = usePreferencesStore((s) => s.realDebridKey); + const debridToken = usePreferencesStore((s) => s.debridToken); + const debridService = usePreferencesStore((s) => s.debridService); const { loading, value } = useAsync(async (): Promise => { const extensionStatus: Status = (await isExtensionActive()) ? "success" @@ -210,7 +243,10 @@ function useIsSetup() { } const febboxKeyStatus: Status = await testFebboxKey(febboxKey); - const realDebridKeyStatus: Status = await testRealDebridKey(realDebridKey); + const debridTokenStatus: Status = + debridService === "torbox" + ? await testTorboxToken(debridToken) + : await testdebridToken(debridToken); return { extension: extensionStatus, @@ -219,23 +255,23 @@ function useIsSetup() { ...(conf().ALLOW_FEBBOX_KEY && { febboxKeyTest: febboxKeyStatus, }), - realDebridKeyTest: realDebridKeyStatus, + debridTokenTest: debridTokenStatus, }; - }, [proxyUrls, febboxKey, realDebridKey]); + }, [proxyUrls, febboxKey, debridToken, debridService]); let globalState: Status = "unset"; if ( value?.extension === "success" || value?.proxy === "success" || value?.febboxKeyTest === "success" || - value?.realDebridKeyTest === "success" + value?.debridTokenTest === "success" ) globalState = "success"; if ( value?.proxy === "error" || value?.extension === "error" || value?.febboxKeyTest === "error" || - value?.realDebridKeyTest === "error" + value?.debridTokenTest === "error" ) globalState = "error"; @@ -375,9 +411,9 @@ export function SetupPart() { > {t("settings.connections.setup.items.default")} - {conf().ALLOW_REAL_DEBRID_KEY && ( - - Real Debrid token + {conf().ALLOW_DEBRID_KEY && ( + + Debrid Service )} {conf().ALLOW_FEBBOX_KEY && ( diff --git a/src/setup/config.ts b/src/setup/config.ts index 42b00a58..d84bf55c 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -26,12 +26,13 @@ interface Config { ONBOARDING_PROXY_INSTALL_LINK: string; ALLOW_AUTOPLAY: boolean; ALLOW_FEBBOX_KEY: boolean; - ALLOW_REAL_DEBRID_KEY: boolean; + ALLOW_DEBRID_KEY: boolean; SHOW_AD: boolean; AD_CONTENT_URL: string; TRACK_SCRIPT: string; // like BANNER_MESSAGE: string; BANNER_ID: string; + USE_TRAKT: boolean; } export interface RuntimeConfig { @@ -41,7 +42,7 @@ export interface RuntimeConfig { DMCA_EMAIL: string | null; TWITTER_LINK: string; TMDB_READ_API_KEY: string | null; - ALLOW_REAL_DEBRID_KEY: boolean; + ALLOW_DEBRID_KEY: boolean; NORMAL_ROUTER: boolean; PROXY_URLS: string[]; M3U8_PROXY_URLS: string[]; @@ -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 = { @@ -85,12 +87,13 @@ const env: Record = { HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING, ALLOW_AUTOPLAY: import.meta.env.VITE_ALLOW_AUTOPLAY, ALLOW_FEBBOX_KEY: import.meta.env.VITE_ALLOW_FEBBOX_KEY, - ALLOW_REAL_DEBRID_KEY: import.meta.env.VITE_ALLOW_REAL_DEBRID_KEY, + ALLOW_DEBRID_KEY: import.meta.env.VITE_ALLOW_DEBRID_KEY, SHOW_AD: import.meta.env.VITE_SHOW_AD, AD_CONTENT_URL: import.meta.env.VITE_AD_CONTENT_URL, 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 { @@ -156,7 +159,7 @@ export function conf(): RuntimeConfig { ) .filter((v) => v.length === 2), // The format is :,: ALLOW_FEBBOX_KEY: getKey("ALLOW_FEBBOX_KEY", "false") === "true", - ALLOW_REAL_DEBRID_KEY: getKey("ALLOW_REAL_DEBRID_KEY", "false") === "true", + ALLOW_DEBRID_KEY: getKey("ALLOW_DEBRID_KEY", "false") === "true", SHOW_AD: getKey("SHOW_AD", "false") === "true", AD_CONTENT_URL: getKey("AD_CONTENT_URL", "") .split(",") @@ -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", }; } diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts index 0a3c007a..5555daaf 100644 --- a/src/stores/auth/index.ts +++ b/src/stores/auth/index.ts @@ -8,6 +8,7 @@ export interface Account { colorB: string; icon: string; }; + nickname: string; } export type AccountWithToken = Account & { @@ -25,8 +26,9 @@ interface AuthStore { removeAccount(): void; setAccount(acc: AccountWithToken): void; updateDeviceName(deviceName: string): void; - updateAccount(acc: Account): void; + updateAccount(acc: Partial): void; setAccountProfile(acc: Account["profile"]): void; + setAccountNickname(nickname: string): void; setBackendUrl(url: null | string): void; setProxySet(urls: null | string[]): void; } @@ -64,6 +66,13 @@ export const useAuthStore = create( } }); }, + setAccountNickname(nickname) { + set((s) => { + if (s.account) { + s.account.nickname = nickname; + } + }); + }, updateAccount(acc) { set((s) => { if (!s.account) return; diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx index b25b9867..b5ef6ff1 100644 --- a/src/stores/preferences/index.tsx +++ b/src/stores/preferences/index.tsx @@ -22,7 +22,8 @@ export interface PreferencesStore { disabledEmbeds: string[]; proxyTmdb: boolean; febboxKey: string | null; - realDebridKey: string | null; + debridToken: string | null; + debridService: string; enableLowPerformanceMode: boolean; enableNativeSubtitles: boolean; enableHoldToBoost: boolean; @@ -49,7 +50,8 @@ export interface PreferencesStore { setDisabledEmbeds(v: string[]): void; setProxyTmdb(v: boolean): void; setFebboxKey(v: string | null): void; - setRealDebridKey(v: string | null): void; + setdebridToken(v: string | null): void; + setdebridService(v: string): void; setEnableLowPerformanceMode(v: boolean): void; setEnableNativeSubtitles(v: boolean): void; setEnableHoldToBoost(v: boolean): void; @@ -80,7 +82,8 @@ export const usePreferencesStore = create( disabledEmbeds: [], proxyTmdb: false, febboxKey: null, - realDebridKey: null, + debridToken: null, + debridService: "realdebrid", enableLowPerformanceMode: false, enableNativeSubtitles: false, enableHoldToBoost: true, @@ -182,9 +185,14 @@ export const usePreferencesStore = create( s.febboxKey = v; }); }, - setRealDebridKey(v) { + setdebridToken(v) { set((s) => { - s.realDebridKey = v; + s.debridToken = v; + }); + }, + setdebridService(v) { + set((s) => { + s.debridService = v; }); }, setEnableLowPerformanceMode(v) {