mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
Merge branch 'p-stream:production' into production
This commit is contained in:
commit
7ca6d31fb5
27 changed files with 825 additions and 366 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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</0> or <1>TorBox</1> 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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
||||
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<T = TraktListResponse>(
|
||||
endpoint: string,
|
||||
): Promise<T> {
|
||||
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<T = TraktListResponse>(
|
|||
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<CuratedMovieList[]> => {
|
|||
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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -257,7 +257,17 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
|||
{deviceName && bufferSeed ? (
|
||||
<DropdownLink className="text-white" href="/settings">
|
||||
<UserAvatar />
|
||||
{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");
|
||||
}
|
||||
})()}
|
||||
</DropdownLink>
|
||||
) : (
|
||||
<DropdownLink href="/login" icon={Icons.RISING_STAR} highlight>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className={`absolute top-4 right-4 z-50 p-2 bg-mediaCard-shadow bg-opacity-70 backdrop-blur-sm rounded-md text-white text-xs
|
||||
|
|
@ -106,7 +116,7 @@ export function WatchPartyStatus() {
|
|||
className={`w-3 h-3 ${user.isHost ? "text-onboarding-best" : ""}`}
|
||||
/>
|
||||
<span className={user.isHost ? "text-onboarding-best" : ""}>
|
||||
{user.userId.substring(0, 8)}...
|
||||
{getDisplayName(user.userId)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-type-secondary">
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ export function WatchPartyView({ id }: { id: string }) {
|
|||
const [validationError, setValidationError] = useState<string | null>(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)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-type-secondary">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
|||
<AccountEditPart
|
||||
deviceName={props.deviceName}
|
||||
setDeviceName={props.setDeviceName}
|
||||
nickname={props.nickname}
|
||||
setNickname={props.setNickname}
|
||||
colorA={props.colorA}
|
||||
setColorA={props.setColorA}
|
||||
colorB={props.colorB}
|
||||
|
|
@ -365,8 +369,10 @@ export function SettingsPage() {
|
|||
const febboxKey = usePreferencesStore((s) => 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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<ExtensionStatus>("unknown");
|
||||
const [headersEnabled, setHeadersEnabled] = useState(false);
|
||||
const [headers, setHeaders] = useState<Array<{ key: string; value: string }>>(
|
||||
[{ 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<string, string> = {};
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{extensionState === "success" && (
|
||||
<div className="flex-1 mb-4">
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<div className="my-3">
|
||||
<p className="text-white font-bold">Headers (Beta)</p>
|
||||
</div>
|
||||
<div>
|
||||
<Toggle
|
||||
onClick={toggleHeaders}
|
||||
enabled={headersEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{headersEnabled && (
|
||||
<>
|
||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||
<div className="my-6 space-y-2">
|
||||
{headers.length === 0 ? (
|
||||
<p>No headers configured.</p>
|
||||
) : (
|
||||
headers.map((header, index) => (
|
||||
<div
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
className="grid grid-cols-[1fr,1fr,auto] items-center gap-2"
|
||||
>
|
||||
<AuthInputBox
|
||||
value={header.key}
|
||||
onChange={(value) =>
|
||||
updateHeader(index, "key", value)
|
||||
}
|
||||
placeholder="Key"
|
||||
/>
|
||||
<AuthInputBox
|
||||
value={header.value}
|
||||
onChange={(value) =>
|
||||
updateHeader(index, "value", value)
|
||||
}
|
||||
placeholder="Value"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeHeader(index)}
|
||||
className="h-full scale-90 hover:scale-100 rounded-full aspect-square bg-authentication-inputBg hover:bg-authentication-inputBgHover flex justify-center items-center transition-transform duration-200 hover:text-white cursor-pointer"
|
||||
>
|
||||
<Icon className="text-xl" icon={Icons.X} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button theme="purple" onClick={addHeader}>
|
||||
Add header
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => start(inputSource, selected as StreamType)}
|
||||
>
|
||||
Start stream
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Title>Preset tests</Title>
|
||||
<div className="grid grid-cols-[1fr,1fr] gap-2">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ export function useDiscoverMedia({
|
|||
const [sectionTitle, setSectionTitle] = useState<string>("");
|
||||
const [currentContentType, setCurrentContentType] =
|
||||
useState<string>(contentType);
|
||||
const [actualContentType, setActualContentType] =
|
||||
useState<DiscoverContentType>(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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export interface UseDiscoverMediaReturn {
|
|||
hasMore: boolean;
|
||||
refetch: () => Promise<void>;
|
||||
sectionTitle: string;
|
||||
actualContentType: DiscoverContentType;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
|
|
|
|||
|
|
@ -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<Status>("unset");
|
||||
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
||||
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 (
|
||||
<div className="mt-6">
|
||||
<SettingsCard>
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<div className="my-3">
|
||||
<p className="text-white font-bold mb-3">
|
||||
{t("settings.connections.realdebrid.title", "Real Debrid API")}
|
||||
</p>
|
||||
<p className="max-w-[30rem] font-medium">
|
||||
{t(
|
||||
"settings.connections.realdebrid.description",
|
||||
"Enter your Real Debrid API key to access premium sources.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Toggle onClick={toggleExpanded} enabled={isExpanded} />
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||
<p className="text-white font-bold mb-3">
|
||||
{t("settings.connections.realdebrid.tokenLabel", "API Key")}
|
||||
</p>
|
||||
<div className="flex items-center w-full">
|
||||
<StatusCircle type={statusMap[status]} className="mx-2 mr-4" />
|
||||
<AuthInputBox
|
||||
onChange={(newToken) => {
|
||||
setRealDebridKey(newToken);
|
||||
}}
|
||||
value={realDebridKey ?? ""}
|
||||
placeholder="API Key"
|
||||
passwordToggleable
|
||||
className="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
{status === "error" && (
|
||||
<p className="text-type-danger mt-4">
|
||||
{t(
|
||||
"settings.connections.realdebrid.status.failure",
|
||||
"Failed to connect to Real Debrid. Please check your API key.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{status === "api_down" && (
|
||||
<p className="text-type-danger mt-4">
|
||||
{t(
|
||||
"settings.connections.realdebrid.status.api_down",
|
||||
"Real Debrid API is currently unavailable. Please try again later.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{status === "invalid_token" && (
|
||||
<p className="text-type-danger mt-4">
|
||||
{t(
|
||||
"settings.connections.realdebrid.status.invalid_token",
|
||||
"Invalid API key or non-premium account. Real Debrid requires a premium account.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* <RealDebridSetup /> */}
|
||||
<div className="mt-6">
|
||||
<FebboxSetup
|
||||
febboxKey={usePreferencesStore((s) => s.febboxKey)}
|
||||
|
|
@ -427,6 +292,15 @@ export function OnboardingPage() {
|
|||
mode="onboarding"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<DebridEdit
|
||||
debridToken={usePreferencesStore((s) => s.debridToken)}
|
||||
setdebridToken={usePreferencesStore((s) => s.setdebridToken)}
|
||||
debridService={usePreferencesStore((s) => s.debridService)}
|
||||
setdebridService={usePreferencesStore((s) => s.setdebridService)}
|
||||
mode="onboarding"
|
||||
/>
|
||||
</div>
|
||||
</BiggerCenterContainer>
|
||||
</MinimalPageLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-8 max-w-xs">
|
||||
<AuthInputBox
|
||||
label={
|
||||
t("settings.account.accountDetails.deviceNameLabel") ??
|
||||
undefined
|
||||
}
|
||||
placeholder={
|
||||
t("settings.account.accountDetails.deviceNamePlaceholder") ??
|
||||
undefined
|
||||
}
|
||||
value={props.deviceName}
|
||||
onChange={(value) => props.setDeviceName(value)}
|
||||
/>
|
||||
<div className="flex space-x-3">
|
||||
<Button className="logout-button" theme="danger" onClick={logout}>
|
||||
{t("settings.account.accountDetails.logoutButton")}
|
||||
</Button>
|
||||
<div className="flex flex-col md:flex-row md:gap-4 gap-4">
|
||||
<div className="w-full">
|
||||
<AuthInputBox
|
||||
label={t("settings.account.accountDetails.nicknameLabel")}
|
||||
placeholder={t(
|
||||
"settings.account.accountDetails.nicknamePlaceholder",
|
||||
)}
|
||||
value={props.nickname}
|
||||
onChange={(value) => props.setNickname(value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<AuthInputBox
|
||||
label={
|
||||
t("settings.account.accountDetails.deviceNameLabel") ??
|
||||
undefined
|
||||
}
|
||||
placeholder={
|
||||
t("settings.account.accountDetails.deviceNamePlaceholder") ??
|
||||
undefined
|
||||
}
|
||||
value={props.deviceName}
|
||||
onChange={(value) => props.setDeviceName(value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-3 mt-4">
|
||||
<Button className="logout-button" theme="danger" onClick={logout}>
|
||||
{t("settings.account.accountDetails.logoutButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<SetStateAction<string | null>>;
|
||||
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({
|
|||
</div>
|
||||
<div>
|
||||
<Toggle
|
||||
onClick={toggleExpanded}
|
||||
onClick={toggleFebboxExpanded}
|
||||
enabled={
|
||||
mode === "onboarding" ? isExpanded : febboxKey !== null
|
||||
mode === "onboarding" ? isFebboxExpanded : febboxKey !== null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isVisible ? (
|
||||
{isFebboxVisible ? (
|
||||
<>
|
||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||
|
||||
|
|
@ -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<Status>("unset");
|
||||
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
||||
|
|
@ -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 (
|
||||
<SettingsCard>
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<div className="my-3">
|
||||
<p className="text-white font-bold mb-3">{t("realdebrid.title")}</p>
|
||||
<p className="max-w-[30rem] font-medium">
|
||||
{t("realdebrid.description")}
|
||||
<p className="text-white font-bold mb-3">{t("debrid.title")}</p>
|
||||
<Trans i18nKey="debrid.description">
|
||||
<MwLink to="https://real-debrid.com/" />
|
||||
{/* fifth's referral code */}
|
||||
<MwLink to="https://torbox.app/subscription?referral=3f665ece-0405-4012-9db7-c6f90e8567e1" />
|
||||
</Trans>
|
||||
<p className="text-type-danger mt-2 max-w-[30rem]">
|
||||
{t("debrid.notice")}
|
||||
</p>
|
||||
<MwLink>
|
||||
<a
|
||||
href="https://real-debrid.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
real-debrid.com
|
||||
</a>
|
||||
</MwLink>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Toggle
|
||||
onClick={() => setRealDebridKey((s) => (s === null ? "" : null))}
|
||||
enabled={realDebridKey !== null}
|
||||
/>
|
||||
<Toggle onClick={toggleDebridExpanded} enabled={isDebridVisible} />
|
||||
</div>
|
||||
</div>
|
||||
{realDebridKey !== null ? (
|
||||
{isDebridVisible ? (
|
||||
<>
|
||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||
<p className="text-white font-bold mb-3">
|
||||
{t("realdebrid.tokenLabel")}
|
||||
{t("debrid.tokenLabel")}
|
||||
</p>
|
||||
<div className="flex items-center w-full">
|
||||
<StatusCircle type={statusMap[status]} className="mx-2 mr-4" />
|
||||
<AuthInputBox
|
||||
onChange={(newToken) => {
|
||||
setRealDebridKey(newToken);
|
||||
}}
|
||||
value={realDebridKey ?? ""}
|
||||
placeholder="ABC123..."
|
||||
passwordToggleable
|
||||
className="flex-grow"
|
||||
/>
|
||||
<div className="flex md:flex-row flex-col items-center w-full gap-4">
|
||||
<div className="flex items-center w-full">
|
||||
<StatusCircle type={statusMap[status]} className="mx-2 mr-4" />
|
||||
<AuthInputBox
|
||||
onChange={(newToken) => {
|
||||
setdebridToken(newToken);
|
||||
}}
|
||||
value={debridToken ?? ""}
|
||||
placeholder="ABC123..."
|
||||
passwordToggleable
|
||||
className="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Dropdown
|
||||
options={[
|
||||
{
|
||||
id: "realdebrid",
|
||||
name: t("debrid.serviceOptions.realdebrid"),
|
||||
},
|
||||
{
|
||||
id: "torbox",
|
||||
name: t("debrid.serviceOptions.torbox"),
|
||||
},
|
||||
]}
|
||||
selectedItem={{
|
||||
id: debridService,
|
||||
name: t(`debrid.serviceOptions.${debridService}`),
|
||||
}}
|
||||
setSelectedItem={(item) => setdebridService(item.id)}
|
||||
direction="up"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{status === "error" && (
|
||||
<p className="text-type-danger mt-4">
|
||||
{t("realdebrid.status.failure")}
|
||||
{t("debrid.status.failure")}
|
||||
</p>
|
||||
)}
|
||||
{status === "api_down" && (
|
||||
<p className="text-type-danger mt-4">
|
||||
{t("realdebrid.status.api_down")}
|
||||
{t("debrid.status.api_down")}
|
||||
</p>
|
||||
)}
|
||||
{status === "invalid_token" && (
|
||||
<p className="text-type-danger mt-4">
|
||||
{t("realdebrid.status.invalid_token")}
|
||||
{t("debrid.status.invalid_token")}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
<RealDebridKeyEdit
|
||||
realDebridKey={props.realDebridKey}
|
||||
setRealDebridKey={props.setRealDebridKey}
|
||||
/>
|
||||
<FebboxSetup
|
||||
febboxKey={props.febboxKey}
|
||||
setFebboxKey={props.setFebboxKey}
|
||||
mode="settings"
|
||||
/>
|
||||
<DebridEdit
|
||||
debridToken={props.debridToken}
|
||||
setdebridToken={props.setdebridToken}
|
||||
debridService={props.debridService}
|
||||
setdebridService={props.setdebridService}
|
||||
mode="settings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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: {
|
|||
<Heading2 border className="mt-0 mb-9">
|
||||
{t("settings.account.devices.title")}
|
||||
</Heading2>
|
||||
{props.error ? (
|
||||
<p>{t("settings.account.devices.failed")}</p>
|
||||
) : props.loading ? (
|
||||
{props.loading ? (
|
||||
<Loading />
|
||||
) : props.error && deviceListSorted.length === 0 ? (
|
||||
<p>{t("settings.account.devices.failed")}</p>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{deviceListSorted.map((session) => (
|
||||
|
|
|
|||
|
|
@ -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<Status> {
|
||||
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<Status> {
|
|||
return "api_down";
|
||||
}
|
||||
|
||||
export async function testRealDebridKey(
|
||||
realDebridKey: string | null,
|
||||
export async function testdebridToken(
|
||||
debridToken: string | null,
|
||||
): Promise<Status> {
|
||||
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<Status> {
|
||||
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<SetupData> => {
|
||||
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")}
|
||||
</SetupCheckList>
|
||||
{conf().ALLOW_REAL_DEBRID_KEY && (
|
||||
<SetupCheckList status={setupStates.realDebridKeyTest || "unset"}>
|
||||
Real Debrid token
|
||||
{conf().ALLOW_DEBRID_KEY && (
|
||||
<SetupCheckList status={setupStates.debridTokenTest || "unset"}>
|
||||
Debrid Service
|
||||
</SetupCheckList>
|
||||
)}
|
||||
{conf().ALLOW_FEBBOX_KEY && (
|
||||
|
|
|
|||
|
|
@ -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 <script src="https://umami.com/script.js"></script>
|
||||
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<keyof Config, undefined | string> = {
|
||||
|
|
@ -85,12 +87,13 @@ const env: Record<keyof Config, undefined | string> = {
|
|||
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 <beforeA>:<afterA>,<beforeB>:<afterB>
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Account>): 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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue