feat: enhance Stripstream configuration handling

- Introduced a new resolver function to streamline fetching Stripstream configuration from the database or environment variables.
- Updated various components and API routes to utilize the new configuration resolver, improving code maintainability and reducing direct database calls.
- Added optional environment variables for Stripstream URL and token in the .env.example file.
- Refactored image loading logic in the reader components to improve performance and error handling.
This commit is contained in:
2026-03-11 21:25:58 +01:00
parent e74b02e3a2
commit 7e4c48469a
12 changed files with 183 additions and 84 deletions

View File

@@ -5,4 +5,8 @@ MONGODB_URI=mongodb://admin:password@host.docker.internal:27017/stripstream?auth
NEXTAUTH_SECRET=SECRET NEXTAUTH_SECRET=SECRET
#openssl rand -base64 32 #openssl rand -base64 32
NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_URL=http://localhost:3000
# Stripstream Librarian (optionnel : fallback si l'utilisateur n'a pas sauvegardé d'URL/token en base)
# STRIPSTREAM_URL=https://librarian.example.com
# STRIPSTREAM_TOKEN=stl_xxxx_xxxxxxxx

View File

@@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { getCurrentUser } from "@/lib/auth-utils"; import { getCurrentUser } from "@/lib/auth-utils";
import { StripstreamProvider } from "@/lib/providers/stripstream/stripstream.provider"; import { StripstreamProvider } from "@/lib/providers/stripstream/stripstream.provider";
import { getResolvedStripstreamConfig } from "@/lib/providers/stripstream/stripstream-config-resolver";
import { AppError } from "@/utils/errors"; import { AppError } from "@/utils/errors";
import { ERROR_CODES } from "@/constants/errorCodes"; import { ERROR_CODES } from "@/constants/errorCodes";
import type { ProviderType } from "@/lib/providers/types"; import type { ProviderType } from "@/lib/providers/types";
@@ -81,8 +82,8 @@ export async function setActiveProvider(
if (!config) { if (!config) {
return { success: false, message: "Komga n'est pas encore configuré" }; return { success: false, message: "Komga n'est pas encore configuré" };
} }
} else if (provider === "stripstream") { } else if (provider === "stripstream") {
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } }); const config = await getResolvedStripstreamConfig(userId);
if (!config) { if (!config) {
return { success: false, message: "Stripstream n'est pas encore configuré" }; return { success: false, message: "Stripstream n'est pas encore configuré" };
} }
@@ -108,7 +109,8 @@ export async function setActiveProvider(
} }
/** /**
* Récupère la configuration Stripstream de l'utilisateur * Récupère la configuration Stripstream de l'utilisateur (affichage settings).
* Priorité : config en base, sinon env STRIPSTREAM_URL / STRIPSTREAM_TOKEN.
*/ */
export async function getStripstreamConfig(): Promise<{ export async function getStripstreamConfig(): Promise<{
url?: string; url?: string;
@@ -119,13 +121,9 @@ export async function getStripstreamConfig(): Promise<{
if (!user) return null; if (!user) return null;
const userId = parseInt(user.id, 10); const userId = parseInt(user.id, 10);
const config = await prisma.stripstreamConfig.findUnique({ const resolved = await getResolvedStripstreamConfig(userId);
where: { userId }, if (!resolved) return null;
select: { url: true }, return { url: resolved.url, hasToken: true };
});
if (!config) return null;
return { url: config.url, hasToken: true };
} catch { } catch {
return null; return null;
} }
@@ -166,15 +164,15 @@ export async function getProvidersStatus(): Promise<{
} }
const userId = parseInt(user.id, 10); const userId = parseInt(user.id, 10);
const [dbUser, komgaConfig, stripstreamConfig] = await Promise.all([ const [dbUser, komgaConfig, stripstreamResolved] = await Promise.all([
prisma.user.findUnique({ where: { id: userId }, select: { activeProvider: true } }), prisma.user.findUnique({ where: { id: userId }, select: { activeProvider: true } }),
prisma.komgaConfig.findUnique({ where: { userId }, select: { id: true } }), prisma.komgaConfig.findUnique({ where: { userId }, select: { id: true } }),
prisma.stripstreamConfig.findUnique({ where: { userId }, select: { id: true } }), getResolvedStripstreamConfig(userId),
]); ]);
return { return {
komgaConfigured: !!komgaConfig, komgaConfigured: !!komgaConfig,
stripstreamConfigured: !!stripstreamConfig, stripstreamConfigured: !!stripstreamResolved,
activeProvider: (dbUser?.activeProvider as ProviderType) ?? "komga", activeProvider: (dbUser?.activeProvider as ProviderType) ?? "komga",
}; };
} catch { } catch {

View File

@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth-utils"; import { getCurrentUser } from "@/lib/auth-utils";
import prisma from "@/lib/prisma"; import { getResolvedStripstreamConfig } from "@/lib/providers/stripstream/stripstream-config-resolver";
import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client"; import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client";
import { ERROR_CODES } from "@/constants/errorCodes"; import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors"; import { AppError } from "@/utils/errors";
@@ -23,7 +23,7 @@ export async function GET(
} }
const userId = parseInt(user.id, 10); const userId = parseInt(user.id, 10);
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } }); const config = await getResolvedStripstreamConfig(userId);
if (!config) { if (!config) {
throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG); throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG);
} }

View File

@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth-utils"; import { getCurrentUser } from "@/lib/auth-utils";
import prisma from "@/lib/prisma"; import { getResolvedStripstreamConfig } from "@/lib/providers/stripstream/stripstream-config-resolver";
import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client"; import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client";
import { AppError } from "@/utils/errors"; import { AppError } from "@/utils/errors";
import { ERROR_CODES } from "@/constants/errorCodes"; import { ERROR_CODES } from "@/constants/errorCodes";
@@ -20,7 +20,7 @@ export async function GET(
} }
const userId = parseInt(user.id, 10); const userId = parseInt(user.id, 10);
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } }); const config = await getResolvedStripstreamConfig(userId);
if (!config) { if (!config) {
throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG); throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG);
} }

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { ErrorMessage } from "@/components/ui/ErrorMessage"; import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { useTranslate } from "@/hooks/useTranslate"; import { useTranslate } from "@/hooks/useTranslate";
@@ -16,7 +15,6 @@ interface LoginFormProps {
} }
export function LoginForm({ from }: LoginFormProps) { export function LoginForm({ from }: LoginFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AppErrorType | null>(null); const [error, setError] = useState<AppErrorType | null>(null);
const { t } = useTranslate(); const { t } = useTranslate();
@@ -57,8 +55,7 @@ export function LoginForm({ from }: LoginFormProps) {
} }
const redirectPath = getSafeRedirectPath(from); const redirectPath = getSafeRedirectPath(from);
window.location.assign(redirectPath); window.location.href = redirectPath;
router.refresh();
} catch { } catch {
setError({ setError({
code: "AUTH_FETCH_ERROR", code: "AUTH_FETCH_ERROR",

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { ErrorMessage } from "@/components/ui/ErrorMessage"; import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { useTranslate } from "@/hooks/useTranslate"; import { useTranslate } from "@/hooks/useTranslate";
@@ -16,7 +15,6 @@ interface RegisterFormProps {
} }
export function RegisterForm({ from }: RegisterFormProps) { export function RegisterForm({ from }: RegisterFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AppErrorType | null>(null); const [error, setError] = useState<AppErrorType | null>(null);
const { t } = useTranslate(); const { t } = useTranslate();
@@ -77,8 +75,7 @@ export function RegisterForm({ from }: RegisterFormProps) {
}); });
} else { } else {
const redirectPath = getSafeRedirectPath(from); const redirectPath = getSafeRedirectPath(from);
window.location.assign(redirectPath); window.location.href = redirectPath;
router.refresh();
} }
} catch { } catch {
setError({ setError({

View File

@@ -42,12 +42,14 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
const { const {
loadedImages, loadedImages,
imageBlobUrls, imageBlobUrls,
prefetchImage,
prefetchPages, prefetchPages,
prefetchNextBook, prefetchNextBook,
cancelAllPrefetches, cancelAllPrefetches,
handleForceReload, handleForceReload,
getPageUrl, getPageUrl,
prefetchCount, prefetchCount,
isPageLoading,
} = useImageLoader({ } = useImageLoader({
pageUrlBuilder: bookPageUrlBuilder, pageUrlBuilder: bookPageUrlBuilder,
pages, pages,
@@ -87,8 +89,26 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
// Prefetch current and next pages // Prefetch current and next pages
useEffect(() => { useEffect(() => {
// Prefetch pages starting from current page // Determine visible pages that need to be loaded immediately
prefetchPages(currentPage, prefetchCount); const visiblePages: number[] = [];
if (isDoublePage && shouldShowDoublePage(currentPage, pages.length)) {
visiblePages.push(currentPage, currentPage + 1);
} else {
visiblePages.push(currentPage);
}
// Load visible pages first (priority) to avoid duplicate requests from <img> tags
// These will populate imageBlobUrls so <img> tags use blob URLs instead of making HTTP requests
const loadVisiblePages = async () => {
await Promise.all(visiblePages.map((page) => prefetchImage(page)));
};
loadVisiblePages().catch(() => {
// Silently fail - will fallback to direct HTTP requests
});
// Then prefetch other pages, excluding visible ones to avoid duplicates
const concurrency = isDoublePage && shouldShowDoublePage(currentPage, pages.length) ? 2 : 4;
prefetchPages(currentPage, prefetchCount, visiblePages, concurrency);
// If double page mode, also prefetch additional pages for smooth double page navigation // If double page mode, also prefetch additional pages for smooth double page navigation
if ( if (
@@ -96,7 +116,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
shouldShowDoublePage(currentPage, pages.length) && shouldShowDoublePage(currentPage, pages.length) &&
currentPage + prefetchCount < pages.length currentPage + prefetchCount < pages.length
) { ) {
prefetchPages(currentPage + prefetchCount, 1); prefetchPages(currentPage + prefetchCount, 1, visiblePages, concurrency);
} }
// If we're near the end of the book, prefetch the next book // If we're near the end of the book, prefetch the next book
@@ -108,6 +128,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
currentPage, currentPage,
isDoublePage, isDoublePage,
shouldShowDoublePage, shouldShowDoublePage,
prefetchImage,
prefetchPages, prefetchPages,
prefetchNextBook, prefetchNextBook,
prefetchCount, prefetchCount,
@@ -229,6 +250,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
imageBlobUrls={imageBlobUrls} imageBlobUrls={imageBlobUrls}
getPageUrl={getPageUrl} getPageUrl={getPageUrl}
isRTL={isRTL} isRTL={isRTL}
isPageLoading={isPageLoading}
/> />
<NavigationBar <NavigationBar

View File

@@ -9,6 +9,7 @@ interface PageDisplayProps {
imageBlobUrls: Record<number, string>; imageBlobUrls: Record<number, string>;
getPageUrl: (pageNum: number) => string; getPageUrl: (pageNum: number) => string;
isRTL: boolean; isRTL: boolean;
isPageLoading?: (pageNum: number) => boolean;
} }
export function PageDisplay({ export function PageDisplay({
@@ -19,6 +20,7 @@ export function PageDisplay({
imageBlobUrls, imageBlobUrls,
getPageUrl, getPageUrl,
isRTL, isRTL,
isPageLoading,
}: PageDisplayProps) { }: PageDisplayProps) {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
@@ -102,7 +104,10 @@ export function PageDisplay({
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
key={`page-${currentPage}-${imageBlobUrls[currentPage] || ""}`} key={`page-${currentPage}-${imageBlobUrls[currentPage] || ""}`}
src={imageBlobUrls[currentPage] || getPageUrl(currentPage)} src={
imageBlobUrls[currentPage] ||
(isPageLoading && isPageLoading(currentPage) ? undefined : getPageUrl(currentPage))
}
alt={`Page ${currentPage}`} alt={`Page ${currentPage}`}
className={cn( className={cn(
"max-h-full max-w-full cursor-pointer object-contain transition-opacity", "max-h-full max-w-full cursor-pointer object-contain transition-opacity",
@@ -166,7 +171,12 @@ export function PageDisplay({
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1] || ""}`} key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1] || ""}`}
src={imageBlobUrls[currentPage + 1] || getPageUrl(currentPage + 1)} src={
imageBlobUrls[currentPage + 1] ||
(isPageLoading && isPageLoading(currentPage + 1)
? undefined
: getPageUrl(currentPage + 1))
}
alt={`Page ${currentPage + 1}`} alt={`Page ${currentPage + 1}`}
className={cn( className={cn(
"max-h-full max-w-full cursor-pointer object-contain transition-opacity", "max-h-full max-w-full cursor-pointer object-contain transition-opacity",

View File

@@ -30,6 +30,8 @@ export function useImageLoader({
// Track ongoing fetch requests to prevent duplicates // Track ongoing fetch requests to prevent duplicates
const pendingFetchesRef = useRef<Set<ImageKey>>(new Set()); const pendingFetchesRef = useRef<Set<ImageKey>>(new Set());
const abortControllersRef = useRef<Map<ImageKey, AbortController>>(new Map()); const abortControllersRef = useRef<Map<ImageKey, AbortController>>(new Map());
// Track promises for pages being loaded so we can await them
const loadingPromisesRef = useRef<Map<ImageKey, Promise<void>>>(new Map());
// Keep refs in sync with state // Keep refs in sync with state
useEffect(() => { useEffect(() => {
@@ -44,12 +46,14 @@ export function useImageLoader({
isMountedRef.current = true; isMountedRef.current = true;
const abortControllers = abortControllersRef.current; const abortControllers = abortControllersRef.current;
const pendingFetches = pendingFetchesRef.current; const pendingFetches = pendingFetchesRef.current;
const loadingPromises = loadingPromisesRef.current;
return () => { return () => {
isMountedRef.current = false; isMountedRef.current = false;
abortControllers.forEach((controller) => controller.abort()); abortControllers.forEach((controller) => controller.abort());
abortControllers.clear(); abortControllers.clear();
pendingFetches.clear(); pendingFetches.clear();
loadingPromises.clear();
}; };
}, []); }, []);
@@ -57,6 +61,7 @@ export function useImageLoader({
abortControllersRef.current.forEach((controller) => controller.abort()); abortControllersRef.current.forEach((controller) => controller.abort());
abortControllersRef.current.clear(); abortControllersRef.current.clear();
pendingFetchesRef.current.clear(); pendingFetchesRef.current.clear();
loadingPromisesRef.current.clear();
}, []); }, []);
const runWithConcurrency = useCallback( const runWithConcurrency = useCallback(
@@ -92,73 +97,96 @@ export function useImageLoader({
return; return;
} }
// Check if this page is already being fetched // Check if this page is already being fetched - if so, wait for it
if (pendingFetchesRef.current.has(pageNum)) { const existingPromise = loadingPromisesRef.current.get(pageNum);
return; if (existingPromise) {
return existingPromise;
} }
// Mark as pending // Mark as pending and create promise
pendingFetchesRef.current.add(pageNum); pendingFetchesRef.current.add(pageNum);
const controller = new AbortController(); const controller = new AbortController();
abortControllersRef.current.set(pageNum, controller); abortControllersRef.current.set(pageNum, controller);
try { const promise = (async () => {
// Use browser cache if available - the server sets Cache-Control headers try {
const response = await fetch(getPageUrl(pageNum), { // Use browser cache if available - the server sets Cache-Control headers
cache: "default", // Respect Cache-Control headers from server const response = await fetch(getPageUrl(pageNum), {
signal: controller.signal, cache: "default", // Respect Cache-Control headers from server
}); signal: controller.signal,
if (!response.ok) { });
return; if (!response.ok) {
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
// Create image to get dimensions
const img = new Image();
img.onload = () => {
if (!isMountedRef.current || controller.signal.aborted) {
URL.revokeObjectURL(blobUrl);
return; return;
} }
setLoadedImages((prev) => ({ const blob = await response.blob();
...prev, const blobUrl = URL.createObjectURL(blob);
[pageNum]: { width: img.naturalWidth, height: img.naturalHeight },
}));
// Store the blob URL for immediate use // Create image to get dimensions
setImageBlobUrls((prev) => ({ const img = new Image();
...prev,
[pageNum]: blobUrl, // Wait for image to load before resolving promise
})); await new Promise<void>((resolve, reject) => {
}; img.onload = () => {
if (!isMountedRef.current || controller.signal.aborted) {
URL.revokeObjectURL(blobUrl);
reject(new Error("Aborted"));
return;
}
img.onerror = () => { setLoadedImages((prev) => ({
URL.revokeObjectURL(blobUrl); ...prev,
}; [pageNum]: { width: img.naturalWidth, height: img.naturalHeight },
}));
img.src = blobUrl; // Store the blob URL for immediate use
} catch { setImageBlobUrls((prev) => ({
// Silently fail prefetch ...prev,
} finally { [pageNum]: blobUrl,
// Remove from pending set }));
pendingFetchesRef.current.delete(pageNum);
abortControllersRef.current.delete(pageNum); resolve();
} };
img.onerror = () => {
URL.revokeObjectURL(blobUrl);
reject(new Error("Image load error"));
};
img.src = blobUrl;
});
} catch {
// Silently fail prefetch
} finally {
// Remove from pending set and promise map
pendingFetchesRef.current.delete(pageNum);
abortControllersRef.current.delete(pageNum);
loadingPromisesRef.current.delete(pageNum);
}
})();
// Store promise so other calls can await it
loadingPromisesRef.current.set(pageNum, promise);
return promise;
}, },
[getPageUrl] [getPageUrl]
); );
// Prefetch multiple pages starting from a given page // Prefetch multiple pages starting from a given page
const prefetchPages = useCallback( const prefetchPages = useCallback(
async (startPage: number, count: number = prefetchCount) => { async (
startPage: number,
count: number = prefetchCount,
excludePages: number[] = [],
concurrency?: number
) => {
const pagesToPrefetch = []; const pagesToPrefetch = [];
const excludeSet = new Set(excludePages);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const pageNum = startPage + i; const pageNum = startPage + i;
if (pageNum <= _pages.length) { if (pageNum <= _pages.length && !excludeSet.has(pageNum)) {
const hasDimensions = loadedImagesRef.current[pageNum]; const hasDimensions = loadedImagesRef.current[pageNum];
const hasBlobUrl = imageBlobUrlsRef.current[pageNum]; const hasBlobUrl = imageBlobUrlsRef.current[pageNum];
const isPending = pendingFetchesRef.current.has(pageNum); const isPending = pendingFetchesRef.current.has(pageNum);
@@ -170,10 +198,13 @@ export function useImageLoader({
} }
} }
// Use provided concurrency or default
const effectiveConcurrency = concurrency ?? PREFETCH_CONCURRENCY;
// Let all prefetch requests run - the server queue will manage concurrency // Let all prefetch requests run - the server queue will manage concurrency
// The browser cache and our deduplication prevent redundant requests // The browser cache and our deduplication prevent redundant requests
if (pagesToPrefetch.length > 0) { if (pagesToPrefetch.length > 0) {
runWithConcurrency(pagesToPrefetch, prefetchImage).catch(() => { runWithConcurrency(pagesToPrefetch, prefetchImage, effectiveConcurrency).catch(() => {
// Silently fail - prefetch is non-critical // Silently fail - prefetch is non-critical
}); });
} }
@@ -340,6 +371,14 @@ export function useImageLoader({
}; };
}, []); // Empty dependency array - only cleanup on unmount }, []); // Empty dependency array - only cleanup on unmount
// Check if a page is currently being loaded
const isPageLoading = useCallback(
(pageNum: number) => {
return pendingFetchesRef.current.has(pageNum);
},
[]
);
return { return {
loadedImages, loadedImages,
imageBlobUrls, imageBlobUrls,
@@ -350,5 +389,6 @@ export function useImageLoader({
handleForceReload, handleForceReload,
getPageUrl, getPageUrl,
prefetchCount, prefetchCount,
isPageLoading,
}; };
} }

View File

@@ -1,5 +1,6 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { getCurrentUser } from "@/lib/auth-utils"; import { getCurrentUser } from "@/lib/auth-utils";
import { getResolvedStripstreamConfig } from "./stripstream/stripstream-config-resolver";
import type { IMediaProvider } from "./provider.interface"; import type { IMediaProvider } from "./provider.interface";
export async function getProvider(): Promise<IMediaProvider | null> { export async function getProvider(): Promise<IMediaProvider | null> {
@@ -13,7 +14,7 @@ export async function getProvider(): Promise<IMediaProvider | null> {
select: { select: {
activeProvider: true, activeProvider: true,
config: { select: { url: true, authHeader: true } }, config: { select: { url: true, authHeader: true } },
stripstreamConfig: { select: { url: true, token: true } }, stripstreamConfig: { select: { id: true } },
}, },
}); });
@@ -21,12 +22,12 @@ export async function getProvider(): Promise<IMediaProvider | null> {
const activeProvider = dbUser.activeProvider ?? "komga"; const activeProvider = dbUser.activeProvider ?? "komga";
if (activeProvider === "stripstream" && dbUser.stripstreamConfig) { if (activeProvider === "stripstream") {
const { StripstreamProvider } = await import("./stripstream/stripstream.provider"); const resolved = await getResolvedStripstreamConfig(userId);
return new StripstreamProvider( if (resolved) {
dbUser.stripstreamConfig.url, const { StripstreamProvider } = await import("./stripstream/stripstream.provider");
dbUser.stripstreamConfig.token return new StripstreamProvider(resolved.url, resolved.token);
); }
} }
if (activeProvider === "komga" || !dbUser.activeProvider) { if (activeProvider === "komga" || !dbUser.activeProvider) {

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
export interface ResolvedStripstreamConfig {
url: string;
token: string;
source: "db" | "env";
}
/**
* Résout la config Stripstream : d'abord en base (par utilisateur), sinon depuis les env STRIPSTREAM_URL et STRIPSTREAM_TOKEN.
*/
export async function getResolvedStripstreamConfig(
userId: number
): Promise<ResolvedStripstreamConfig | null> {
const fromDb = await prisma.stripstreamConfig.findUnique({
where: { userId },
select: { url: true, token: true },
});
if (fromDb) return { ...fromDb, source: "db" };
const url = process.env.STRIPSTREAM_URL?.trim();
const token = process.env.STRIPSTREAM_TOKEN?.trim();
if (url && token) return { url, token, source: "env" };
return null;
}

4
src/types/env.d.ts vendored
View File

@@ -3,5 +3,9 @@ declare namespace NodeJS {
NEXT_PUBLIC_APP_URL: string; NEXT_PUBLIC_APP_URL: string;
NEXT_PUBLIC_DEFAULT_KOMGA_URL?: string; NEXT_PUBLIC_DEFAULT_KOMGA_URL?: string;
NEXT_PUBLIC_APP_VERSION: string; NEXT_PUBLIC_APP_VERSION: string;
/** URL Stripstream Librarian (fallback si pas de config en base) */
STRIPSTREAM_URL?: string;
/** Token API Stripstream (fallback si pas de config en base) */
STRIPSTREAM_TOKEN?: string;
} }
} }