diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx index fb110c7..2198268 100644 --- a/frontend/src/components/ConfigPanel.tsx +++ b/frontend/src/components/ConfigPanel.tsx @@ -1,8 +1,8 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { RotateCcw, Send } from "lucide-react"; -import { api, type ConfigItem } from "../lib/api"; +import { Plug, RotateCcw, Send } from "lucide-react"; +import { api, type ConfigItem, type PlexTestResult } from "../lib/api"; import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; import Tabs, { usePersistedTab } from "./Tabs"; @@ -14,7 +14,7 @@ import { DraftSaveBar } from "./ui/DraftSaveBar"; // applied together via one Save/Discard bar (consistent with the Settings page); nothing hits // the server until Save. Group order is fixed where known so logically related settings (e.g. // Email/SMTP) stay together. -const GROUP_ORDER = ["access", "email", "youtube", "google", "quota", "backfill", "shorts", "batch"]; +const GROUP_ORDER = ["access", "email", "youtube", "google", "quota", "backfill", "shorts", "batch", "downloads", "plex"]; type SaveState = "idle" | "saving" | "saved" | "error"; export default function ConfigPanel() { @@ -93,6 +93,29 @@ export default function ConfigPanel() { onError: () => notify({ level: "error", message: t("config.testFailed") }), }); + // Plex "Test connection": verifies the server URL + token and returns the library sections, + // which drive the library-picker below (checked sections are written into the plex_libraries + // draft as a comma-separated list of section keys, applied on Save). + const [plexResult, setPlexResult] = useState(null); + const testPlex = useMutation({ + mutationFn: () => api.testPlex(), + onSuccess: (r) => { + setPlexResult(r); + notify({ level: "success", message: t("config.plexTestOk", { name: r.server_name }) }); + }, + onError: () => { + setPlexResult(null); + notify({ level: "error", message: t("config.plexTestFailed") }); + }, + }); + const plexLibs = (draft["plex_libraries"] ?? "").split(",").map((s) => s.trim()).filter(Boolean); + const togglePlexLib = (key: string) => { + const next = plexLibs.includes(key) + ? plexLibs.filter((k) => k !== key) + : [...plexLibs, key]; + setDraft((d) => ({ ...d, plex_libraries: next.join(",") })); + }; + if (q.isLoading || !data) { return
{t("config.loading")}
; } @@ -149,6 +172,54 @@ export default function ConfigPanel() { )} + + {activeGroup === "plex" && ( +
+ + + + {dirty &&

{t("config.plexTestDirty")}

} + {plexResult && ( +
+

+ {t("config.plexConnected", { + name: plexResult.server_name, + version: plexResult.version ?? "", + })} +

+

{t("config.plexPickLibraries")}

+
+ {plexResult.sections.map((s) => ( + + ))} +
+

{t("config.plexPickHint")}

+
+ )} +
+ )} )} diff --git a/frontend/src/i18n/locales/de/config.json b/frontend/src/i18n/locales/de/config.json index e830ef2..03f2b38 100644 --- a/frontend/src/i18n/locales/de/config.json +++ b/frontend/src/i18n/locales/de/config.json @@ -10,7 +10,9 @@ "backfill": "Nachladen (Backfill)", "shorts": "Shorts-Prüfung", "batch": "Stapelgrößen", - "explore": "Kanal erkunden" + "explore": "Kanal erkunden", + "downloads": "Downloads", + "plex": "Plex" }, "fields": { "smtp_host": { @@ -100,6 +102,34 @@ "search_grace_days": { "label": "Suchergebnisse — Karenzzeit (Tage)", "hint": "Wie lange ein nicht behaltenes Live-Suchergebnis-Video (niemand hat es angesehen/gespeichert/abonniert) behalten wird, bevor die Entdeckungs-Aufräumaufgabe es entfernt." + }, + "plex_enabled": { + "label": "Plex aktivieren", + "hint": "Zeigt die Plex-Bibliothek als Feed-Quelle mit einem umfangreichen Player. Erfordert, dass der Plex-Server von diesem Host erreichbar ist und die Medien auf einem lokalen Mount liegen (siehe Pfad-Zuordnung)." + }, + "plex_server_url": { + "label": "Plex-Server-URL", + "hint": "Von diesem Host erreichbare Basis-URL, z. B. http://192.168.1.112:32400." + }, + "plex_server_token": { + "label": "Plex-Server-Token", + "hint": "Der Admin-X-Plex-Token des Servers. Nur serverseitig genutzt (Metadaten + Bilder); wird nie an den Browser gesendet. Verschlüsselt gespeichert; nur schreibbar." + }, + "plex_path_map": { + "label": "Medien-Pfad-Zuordnung", + "hint": "Ordnet die Plex-Dateipfade dem lokalen Mount dieses Hosts zu, damit Siftlode die physische Datei abspielen kann. Durch Zeilenumbruch/Komma getrennte plexPrefix=localPrefix-Paare, z. B. /data=/plex-media. Leer = auf beiden Seiten identisch." + }, + "plex_libraries": { + "label": "Angezeigte Bibliotheken", + "hint": "Durch Kommas getrennte Plex-Sektionsschlüssel. Leer = alle Sektionen. Nutze nach dem Verbindungstest die Bibliotheksauswahl unten." + }, + "plex_sync_interval_min": { + "label": "Sync-Intervall (Minuten)", + "hint": "Wie oft die Katalog-Sync-Aufgabe die Plex-Metadaten spiegelt." + }, + "plex_max_transcodes": { + "label": "Max. gleichzeitige Transcodes", + "hint": "Obergrenze für gleichzeitige CPU-Transcodes inkompatibler Dateien (späterer Fallback). Direkt abspielbare Dateien sind nicht begrenzt." } }, "save": "Speichern", @@ -121,5 +151,15 @@ "testEmail": "Test-E-Mail senden", "testEmailHint": "Sende eine Test-E-Mail an deine eigene Adresse mit den obigen Einstellungen, um zu prüfen, ob sie funktionieren.", "testSent": "Test-E-Mail gesendet an {{to}}", - "testFailed": "Test-E-Mail fehlgeschlagen — prüfe die Einstellungen" + "testFailed": "Test-E-Mail fehlgeschlagen — prüfe die Einstellungen", + "plexTest": "Verbindung testen", + "plexTestHint": "Prüft die Plex-Server-URL + den Token und lädt die Bibliotheksliste. Speichere zuerst deine Änderungen.", + "plexTestDirty": "Speichere deine Änderungen vor dem Verbindungstest.", + "plexTestOk": "Verbunden mit {{name}}", + "plexTestFailed": "Plex-Verbindung fehlgeschlagen — prüfe URL und Token", + "plexConnected": "Verbunden mit {{name}} {{version}}", + "plexPickLibraries": "Anzuzeigende Bibliotheken:", + "plexPickHint": "Keine auszuwählen entspricht dem Anzeigen aller Bibliotheken. Nicht vergessen zu speichern.", + "plexMovies": "Filme", + "plexShows": "Serien" } diff --git a/frontend/src/i18n/locales/en/config.json b/frontend/src/i18n/locales/en/config.json index df49ea3..587594e 100644 --- a/frontend/src/i18n/locales/en/config.json +++ b/frontend/src/i18n/locales/en/config.json @@ -10,7 +10,9 @@ "backfill": "Backfill", "shorts": "Shorts probe", "batch": "Batch sizes", - "explore": "Channel explore" + "explore": "Channel explore", + "downloads": "Downloads", + "plex": "Plex" }, "fields": { "smtp_host": { @@ -100,6 +102,34 @@ "search_grace_days": { "label": "Search results — grace period (days)", "hint": "How long an un-kept live-search result video (nobody watched / saved / subscribed to) is kept before the discovery-cleanup job removes it." + }, + "plex_enabled": { + "label": "Enable Plex", + "hint": "Expose the Plex library as a feed source with a rich player. Requires the Plex server to be reachable from this host and its media on a local mount (see the path map)." + }, + "plex_server_url": { + "label": "Plex server URL", + "hint": "Base URL reachable from this host, e.g. http://192.168.1.112:32400." + }, + "plex_server_token": { + "label": "Plex server token", + "hint": "The server admin X-Plex-Token. Used server-side only (metadata + images); never sent to the browser. Stored encrypted; write-only." + }, + "plex_path_map": { + "label": "Media path map", + "hint": "Maps Plex's on-disk paths to this host's local mount so Siftlode can play the physical file. Newline/comma-separated plexPrefix=localPrefix pairs, e.g. /data=/plex-media. Empty = identical on both sides." + }, + "plex_libraries": { + "label": "Exposed libraries", + "hint": "Comma-separated Plex section keys to expose. Empty = all sections. Use the library picker below after testing the connection." + }, + "plex_sync_interval_min": { + "label": "Sync interval (minutes)", + "hint": "How often the catalog sync job mirrors Plex metadata." + }, + "plex_max_transcodes": { + "label": "Max concurrent transcodes", + "hint": "Cap on simultaneous CPU transcodes for incompatible files (a later fallback). Direct-playable files aren't limited." } }, "save": "Save", @@ -121,5 +151,15 @@ "testEmail": "Send test email", "testEmailHint": "Send a test email to your own address using the settings above, to confirm they work.", "testSent": "Test email sent to {{to}}", - "testFailed": "Test email failed — check the settings" + "testFailed": "Test email failed — check the settings", + "plexTest": "Test connection", + "plexTestHint": "Verify the Plex server URL + token and load the library list. Save any edits first.", + "plexTestDirty": "Save your changes before testing the connection.", + "plexTestOk": "Connected to {{name}}", + "plexTestFailed": "Plex connection failed — check the URL and token", + "plexConnected": "Connected to {{name}} {{version}}", + "plexPickLibraries": "Libraries to expose:", + "plexPickHint": "Unchecking all is the same as exposing every library. Remember to Save.", + "plexMovies": "Movies", + "plexShows": "TV Shows" } diff --git a/frontend/src/i18n/locales/hu/config.json b/frontend/src/i18n/locales/hu/config.json index b56fdcf..bfd765f 100644 --- a/frontend/src/i18n/locales/hu/config.json +++ b/frontend/src/i18n/locales/hu/config.json @@ -10,7 +10,9 @@ "backfill": "Letöltés (backfill)", "shorts": "Shorts-vizsgálat", "batch": "Kötegméretek", - "explore": "Csatorna-felfedezés" + "explore": "Csatorna-felfedezés", + "downloads": "Letöltések", + "plex": "Plex" }, "fields": { "smtp_host": { @@ -100,6 +102,34 @@ "search_grace_days": { "label": "Keresési találatok — türelmi idő (nap)", "hint": "Meddig marad meg egy meg nem tartott YouTube-keresési találat (amit senki nem nézett meg / mentett el / iratkozott fel rá), mielőtt a felfedezés-takarító feladat törli." + }, + "plex_enabled": { + "label": "Plex engedélyezése", + "hint": "A Plex-könyvtár megjelenítése forrásként, gazdag lejátszóval. Ehhez a Plex-szervernek elérhetőnek kell lennie erről a gépről, a média pedig helyi mounton (lásd az útvonal-leképezést)." + }, + "plex_server_url": { + "label": "Plex-szerver URL", + "hint": "Erről a gépről elérhető alap-URL, pl. http://192.168.1.112:32400." + }, + "plex_server_token": { + "label": "Plex-szerver token", + "hint": "A szerver admin X-Plex-Tokenje. Csak szerveroldalon használjuk (metaadat + képek); soha nem kerül a böngészőbe. Titkosítva tárolva; csak írható." + }, + "plex_path_map": { + "label": "Média-útvonal leképezés", + "hint": "A Plex lemezes útvonalait képezi le erre a gépre, hogy a Siftlode lejátszhassa a fizikai fájlt. Új sorral/vesszővel elválasztott plexPrefix=localPrefix párok, pl. /data=/plex-media. Üres = mindkét oldalon azonos." + }, + "plex_libraries": { + "label": "Megjelenített könyvtárak", + "hint": "Vesszővel elválasztott Plex-szekciókulcsok. Üres = minden szekció. Kapcsolat-teszt után használd az alábbi könyvtár-választót." + }, + "plex_sync_interval_min": { + "label": "Szinkron gyakorisága (perc)", + "hint": "Milyen gyakran tükrözi a katalógus-szinkron a Plex metaadatait." + }, + "plex_max_transcodes": { + "label": "Max párhuzamos transcode", + "hint": "A nem kompatibilis fájlok egyidejű CPU-transcode-jainak felső korlátja (későbbi tartalék). A direktben játszható fájlokra nincs korlát." } }, "save": "Mentés", @@ -121,5 +151,15 @@ "testEmail": "Teszt-email küldése", "testEmailHint": "Teszt-email küldése a saját címedre a fenti beállításokkal, hogy ellenőrizd, működnek-e.", "testSent": "Teszt-email elküldve ide: {{to}}", - "testFailed": "A teszt-email sikertelen — ellenőrizd a beállításokat" + "testFailed": "A teszt-email sikertelen — ellenőrizd a beállításokat", + "plexTest": "Kapcsolat tesztelése", + "plexTestHint": "Ellenőrzi a Plex-szerver URL-t és tokent, és betölti a könyvtárlistát. Előbb mentsd a módosításokat.", + "plexTestDirty": "Mentsd a változásokat a kapcsolat tesztelése előtt.", + "plexTestOk": "Kapcsolódva: {{name}}", + "plexTestFailed": "A Plex-kapcsolat sikertelen — ellenőrizd az URL-t és a tokent", + "plexConnected": "Kapcsolódva: {{name}} {{version}}", + "plexPickLibraries": "Megjelenítendő könyvtárak:", + "plexPickHint": "Ha egyiket sem jelölöd be, az az összes könyvtár megjelenítésével egyenértékű. Ne feledj menteni.", + "plexMovies": "Filmek", + "plexShows": "Sorozatok" } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 24f2246..5f92a79 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -609,6 +609,20 @@ export interface SystemConfigData { secrets_manageable: boolean; } +// Admin: result of testing the Plex server connection (also drives the library-picker). +export interface PlexSection { + key: string; + title: string; + type: string; // "movie" | "show" + count?: number; +} +export interface PlexTestResult { + ok: boolean; + server_name: string; + version?: string; + sections: PlexSection[]; +} + // Admin Users & roles page. export interface AdminUserRow { id: number; @@ -921,6 +935,7 @@ export const api = { req(`/api/admin/config/${key}`, { method: "DELETE" }), testEmail: (): Promise<{ sent: boolean; to: string }> => req("/api/admin/config/test-email", { method: "POST" }), + testPlex: (): Promise => req("/api/plex/test", { method: "POST" }), // --- admin: users & roles --- adminUsers: (): Promise => req("/api/admin/users"), setUserRole: (id: number, role: "user" | "admin"): Promise =>