Merge: devops repo split — public repo keeps only example configs
This commit is contained in:
commit
cb72106329
12 changed files with 285 additions and 8 deletions
12
.env.example
12
.env.example
|
|
@ -62,3 +62,15 @@ SCHEDULER_ENABLED=true
|
||||||
# directory instead — e.g. one your Plex server can read — to keep the Plex-style tree there.
|
# directory instead — e.g. one your Plex server can read — to keep the Plex-style tree there.
|
||||||
# The path must be writable by the container user (uid of `appuser`, 1000): chown 1000:1000 <dir>.
|
# The path must be writable by the container user (uid of `appuser`, 1000): chown 1000:1000 <dir>.
|
||||||
# DOWNLOAD_HOST_PATH=/mnt/media/youtube
|
# DOWNLOAD_HOST_PATH=/mnt/media/youtube
|
||||||
|
|
||||||
|
# --- Plex integration (optional) ---
|
||||||
|
# The optional Plex module browses/plays your Plex library, playing the LOCAL physical file
|
||||||
|
# directly (Plex is used for metadata only). Enable it in the admin Config page (server URL,
|
||||||
|
# token, path map, libraries). For playback the container must be able to READ the media files,
|
||||||
|
# so bind-mount your Plex media into the container read-only via PLEX_MEDIA_HOST_PATH — it maps
|
||||||
|
# to /plex-media inside the container (the local side of the admin "path map", whose default is
|
||||||
|
# /data=/plex-media). Leave unset if you don't use Plex.
|
||||||
|
# PLEX_MEDIA_HOST_PATH=/mnt/media
|
||||||
|
# Optional: scratch dir for on-the-fly HLS remux segments. Defaults to DOWNLOAD_ROOT; point it at
|
||||||
|
# fast local storage if your download volume is slow.
|
||||||
|
# PLEX_HLS_DIR=/var/tmp/plex-hls
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,14 @@ _TS_CONFIG = "public.unaccent_simple"
|
||||||
@router.post("/test")
|
@router.post("/test")
|
||||||
def test_connection(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> dict:
|
def test_connection(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> dict:
|
||||||
"""Admin: verify the configured Plex server URL + token, returning the server name and the
|
"""Admin: verify the configured Plex server URL + token, returning the server name and the
|
||||||
movie/show sections (for the config library-picker)."""
|
movie/show sections (for the config library-picker). Also probes ONE real media file through
|
||||||
|
the local mount, so a missing bind-mount / wrong path-map is caught here rather than only
|
||||||
|
surfacing as a dead player later."""
|
||||||
try:
|
try:
|
||||||
with PlexClient(db) as plex:
|
with PlexClient(db) as plex:
|
||||||
info = plex.server_info()
|
info = plex.server_info()
|
||||||
sections = plex.sections()
|
sections = plex.sections()
|
||||||
|
media_check = _media_check(db, plex, sections)
|
||||||
except PlexNotConfigured as e:
|
except PlexNotConfigured as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except PlexError as e:
|
except PlexError as e:
|
||||||
|
|
@ -55,9 +58,44 @@ def test_connection(_: User = Depends(admin_user), db: Session = Depends(get_db)
|
||||||
for s in sections
|
for s in sections
|
||||||
if s.get("type") in ("movie", "show")
|
if s.get("type") in ("movie", "show")
|
||||||
],
|
],
|
||||||
|
"media_check": media_check,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sample_plex_path(db: Session, plex: PlexClient, sections: list[dict]) -> str | None:
|
||||||
|
"""A real Plex-side media file path to validate the mount + path-map against. Prefers the
|
||||||
|
already-mirrored catalog (cheap); before the first sync, pulls one movie leaf from an enabled
|
||||||
|
section straight from Plex."""
|
||||||
|
row = db.query(PlexItem.file_path).filter(PlexItem.file_path.isnot(None)).first()
|
||||||
|
if row and row[0]:
|
||||||
|
return row[0]
|
||||||
|
wanted = plex_sync._enabled_section_keys(db)
|
||||||
|
for s in sections:
|
||||||
|
if s.get("type") != "movie":
|
||||||
|
continue
|
||||||
|
key = str(s.get("key"))
|
||||||
|
if wanted is not None and key not in wanted:
|
||||||
|
continue
|
||||||
|
items, _ = plex.section_items(key, 1, 0, 1)
|
||||||
|
if items:
|
||||||
|
fp = plex_sync._media_facts(items[0]).get("file_path")
|
||||||
|
if fp:
|
||||||
|
return fp
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _media_check(db: Session, plex: PlexClient, sections: list[dict]) -> dict:
|
||||||
|
"""Resolve a sample Plex file path to its local mount path and confirm it's readable, so the
|
||||||
|
admin sees at Test time whether the physical media is actually reachable (bind-mount +
|
||||||
|
path-map correct). ``checked=False`` when no sample is available (empty library)."""
|
||||||
|
plex_path = _sample_plex_path(db, plex, sections)
|
||||||
|
if not plex_path:
|
||||||
|
return {"checked": False}
|
||||||
|
local_path = plex_paths.map_path(db, plex_path)
|
||||||
|
ok = plex_paths.local_media_path(db, plex_path) is not None
|
||||||
|
return {"checked": True, "ok": ok, "plex_path": plex_path, "local_path": local_path}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sync")
|
@router.post("/sync")
|
||||||
def trigger_sync(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> dict:
|
def trigger_sync(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> dict:
|
||||||
"""Admin: mirror the enabled Plex sections into the local catalog. Synchronous (can take a
|
"""Admin: mirror the enabled Plex sections into the local catalog. Synchronous (can take a
|
||||||
|
|
|
||||||
175
docker-compose.home.yml.example
Normal file
175
docker-compose.home.yml.example
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
# Home deployment — the app + database run on the home server (a Proxmox LXC), and a small
|
||||||
|
# public VPS reverse-proxies the public hostname to it over a private WireGuard tunnel. This
|
||||||
|
# keeps the heavy work (full-catalog feed/count/facet scans) on strong, always-on home
|
||||||
|
# hardware while the VPS stays a thin, cheap public endpoint.
|
||||||
|
#
|
||||||
|
# Differences from docker-compose.prod.yml (the bare-VPS stack): runs as Docker-in-LXC
|
||||||
|
# (security_opt apparmor:unconfined), Postgres gets real RAM + tuning so the catalog stays
|
||||||
|
# hot in cache, and the api is published on the LXC (reachable only via the WireGuard peer /
|
||||||
|
# trusted LAN — never port-forwarded to the public internet; the VPS Caddy is the public TLS
|
||||||
|
# front door, so the OAuth redirect URL is unchanged).
|
||||||
|
#
|
||||||
|
# Deploy (from the build box, over the LAN):
|
||||||
|
# cd /docker && docker compose -f docker-compose.home.yml --env-file .env pull && \
|
||||||
|
# docker compose -f docker-compose.home.yml --env-file .env up -d
|
||||||
|
#
|
||||||
|
# Required keys in /docker/.env: POSTGRES_PASSWORD, SECRET_KEY (>=32 chars),
|
||||||
|
# TOKEN_ENCRYPTION_KEY (Fernet), GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
|
||||||
|
# OAUTH_REDIRECT_URL=https://siftlode.example.com/auth/callback, ALLOWED_EMAILS, ADMIN_EMAILS,
|
||||||
|
# YOUTUBE_API_KEY. APP_VERSION is exported by the deploy step (selects the image tag).
|
||||||
|
# Optional: DOWNLOAD_HOST_PATH (host dir for the download tree) and PLEX_MEDIA_HOST_PATH (host dir
|
||||||
|
# holding the Plex library, mounted read-only for the optional Plex module).
|
||||||
|
|
||||||
|
name: siftlode
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-siftlode}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in the env file}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-siftlode}
|
||||||
|
# Tuned for the home box (plenty of RAM): keep the ~350 MB catalog hot in shared_buffers,
|
||||||
|
# tell the planner about the large OS cache, give sorts headroom, and price random access
|
||||||
|
# like an SSD so index plans aren't penalised. (See the feed EXPLAIN analysis.)
|
||||||
|
command:
|
||||||
|
- postgres
|
||||||
|
- -c
|
||||||
|
- shared_buffers=1GB
|
||||||
|
- -c
|
||||||
|
- effective_cache_size=4GB
|
||||||
|
- -c
|
||||||
|
- work_mem=32MB
|
||||||
|
- -c
|
||||||
|
- maintenance_work_mem=256MB
|
||||||
|
- -c
|
||||||
|
- random_page_cost=1.1
|
||||||
|
- -c
|
||||||
|
- max_parallel_workers_per_gather=2
|
||||||
|
volumes:
|
||||||
|
- siftlode_pgdata:/var/lib/postgresql/data
|
||||||
|
networks: [internal]
|
||||||
|
mem_limit: 4g
|
||||||
|
cpus: 3.0
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
- apparmor:unconfined
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-siftlode} -d ${POSTGRES_DB:-siftlode}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
# Pulled prebuilt image published by `siftlode publish`; the tag follows the released
|
||||||
|
# VERSION (the deploy step exports APP_VERSION). No build on the host.
|
||||||
|
image: registry.example.com/you/siftlode:${APP_VERSION:-latest}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-siftlode}
|
||||||
|
# The public instance owns the background scheduler (single writer).
|
||||||
|
SCHEDULER_ENABLED: "true"
|
||||||
|
# Download center: the API serves finished files + builds filmstrips (read-only root is fine —
|
||||||
|
# it only writes to the /downloads mount + tmpfs). The yt-dlp job loop runs in `worker` below.
|
||||||
|
DOWNLOAD_ROOT: /downloads
|
||||||
|
WORKER_ENABLED: "false"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [internal]
|
||||||
|
ports:
|
||||||
|
# Published on the LXC. Reached by the VPS Caddy over the WireGuard tunnel
|
||||||
|
# (10.x.x.x:8080) and on the trusted home LAN. Never forwarded to the public internet.
|
||||||
|
- "8080:8000"
|
||||||
|
volumes:
|
||||||
|
# Downloaded media. Defaults to a Docker-managed named volume; set DOWNLOAD_HOST_PATH in .env
|
||||||
|
# to a host directory (e.g. one your Plex server reads) to keep the Plex-style tree there.
|
||||||
|
- ${DOWNLOAD_HOST_PATH:-siftlode_downloads}:/downloads
|
||||||
|
# Plex media, read-only. Only used when the optional Plex module is enabled: set
|
||||||
|
# PLEX_MEDIA_HOST_PATH in .env to the host directory holding the Plex library (the local side
|
||||||
|
# of plex_path_map, default /plex-media). Left unset → an empty named volume (harmless; the
|
||||||
|
# Plex module is off by default). On this home stack (LXC on the Plex host) it's a plain host
|
||||||
|
# bind-mount — no SMB (that's the localdev-only path).
|
||||||
|
- ${PLEX_MEDIA_HOST_PATH:-siftlode_plex_media}:/plex-media:ro
|
||||||
|
mem_limit: 1536m
|
||||||
|
cpus: 2.0
|
||||||
|
pids_limit: 512
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
- apparmor:unconfined
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/healthz').status==200 else 1)"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Download worker: same image, runs the yt-dlp / ffmpeg job loop instead of the API. Shares the
|
||||||
|
# DB (job queue) and the downloads mount with the API. Not read-only (yt-dlp/deno/ffmpeg write to
|
||||||
|
# $HOME caches + the staging dir); heavier limits since it does the video encoding.
|
||||||
|
worker:
|
||||||
|
image: registry.example.com/you/siftlode:${APP_VERSION:-latest}
|
||||||
|
command: ["python", "-m", "app.worker"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-siftlode}
|
||||||
|
# The API owns the scheduler; the worker must not also run it.
|
||||||
|
SCHEDULER_ENABLED: "false"
|
||||||
|
DOWNLOAD_ROOT: /downloads
|
||||||
|
WORKER_ENABLED: "true"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
# Start only after the API is healthy — it applies the DB migrations on its own startup,
|
||||||
|
# so this guarantees the download tables exist before the worker touches them.
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
bgutil-pot:
|
||||||
|
condition: service_started
|
||||||
|
networks: [internal]
|
||||||
|
volumes:
|
||||||
|
- ${DOWNLOAD_HOST_PATH:-siftlode_downloads}:/downloads
|
||||||
|
# Plex media (read-only) — same mount as the api; used by Plex clip jobs (a later phase).
|
||||||
|
- ${PLEX_MEDIA_HOST_PATH:-siftlode_plex_media}:/plex-media:ro
|
||||||
|
mem_limit: 2g
|
||||||
|
cpus: 2.0
|
||||||
|
pids_limit: 512
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
- apparmor:unconfined
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# PO-token provider sidecar: mints YouTube Proof-of-Origin tokens on demand so the worker beats
|
||||||
|
# bot-detection + unlocks high-quality formats. The worker reaches it at http://bgutil-pot:4416
|
||||||
|
# (config default). Pin the tag to the plugin version in requirements.txt.
|
||||||
|
bgutil-pot:
|
||||||
|
image: brainicism/bgutil-ytdlp-pot-provider:1.3.1
|
||||||
|
init: true
|
||||||
|
networks: [internal]
|
||||||
|
mem_limit: 512m
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
- apparmor:unconfined
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
siftlode_pgdata:
|
||||||
|
siftlode_downloads:
|
||||||
|
# Empty fallback for the Plex media mount when PLEX_MEDIA_HOST_PATH is unset (Plex module off).
|
||||||
|
siftlode_plex_media:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
|
@ -194,6 +194,14 @@ export default function ConfigPanel() {
|
||||||
version: plexResult.version ?? "",
|
version: plexResult.version ?? "",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
{plexResult.media_check?.checked &&
|
||||||
|
(plexResult.media_check.ok ? (
|
||||||
|
<p className="text-[11px] text-emerald-500 mb-2">{t("config.plexMediaOk")}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-[11px] text-red-400 mb-2">
|
||||||
|
{t("config.plexMediaMissing", { path: plexResult.media_check.local_path ?? "" })}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
<p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
|
<p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{plexResult.sections.map((s) => (
|
{plexResult.sections.map((s) => (
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
||||||
const [muted, setMuted] = useState(false);
|
const [muted, setMuted] = useState(false);
|
||||||
const [fs, setFs] = useState(false);
|
const [fs, setFs] = useState(false);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
const [uiVisible, setUiVisible] = useState(true);
|
const [uiVisible, setUiVisible] = useState(true);
|
||||||
const hideTimer = useRef<number | null>(null);
|
const hideTimer = useRef<number | null>(null);
|
||||||
// Selected audio / subtitle stream ordinals (null audio = default first; null subtitle = off).
|
// Selected audio / subtitle stream ordinals (null audio = default first; null subtitle = off).
|
||||||
|
|
@ -83,10 +84,21 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
setReady(false);
|
setReady(false);
|
||||||
|
setLoadError(null);
|
||||||
let sess;
|
let sess;
|
||||||
try {
|
try {
|
||||||
sess = await api.plexSession(id, startAt, audioRef.current, subRef.current);
|
sess = await api.plexSession(id, startAt, audioRef.current, subRef.current);
|
||||||
} catch {
|
} catch (e: any) {
|
||||||
|
// Surface WHY playback can't start instead of an eternal spinner. 404 = the physical file
|
||||||
|
// isn't reachable (missing media bind-mount or a wrong plex_path_map); 501 = the codec
|
||||||
|
// needs full transcoding (a later phase).
|
||||||
|
setLoadError(
|
||||||
|
e?.status === 501
|
||||||
|
? t("plex.player.errTranscode")
|
||||||
|
: e?.status === 404
|
||||||
|
? t("plex.player.errNotFound")
|
||||||
|
: t("plex.player.errGeneric"),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionStartRef.current = sess.start_s;
|
sessionStartRef.current = sess.start_s;
|
||||||
|
|
@ -112,6 +124,11 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
||||||
setReady(true);
|
setReady(true);
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
});
|
});
|
||||||
|
// A fatal HLS error (e.g. the remux session died / the file became unreadable) would
|
||||||
|
// otherwise leave the spinner up forever — surface it instead.
|
||||||
|
hls.on(Hls.Events.ERROR, (_e, data) => {
|
||||||
|
if (data.fatal) setLoadError(t("plex.player.errGeneric"));
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// direct file (or Safari native HLS)
|
// direct file (or Safari native HLS)
|
||||||
video.src = sess.url;
|
video.src = sess.url;
|
||||||
|
|
@ -125,7 +142,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
||||||
video.addEventListener("loadedmetadata", onMeta);
|
video.addEventListener("loadedmetadata", onMeta);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[id],
|
[id, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start playback when the detail arrives (resume from the saved position unless already watched).
|
// Start playback when the detail arrives (resume from the saved position unless already watched).
|
||||||
|
|
@ -407,8 +424,12 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!ready && (
|
{!ready && (
|
||||||
<div className="absolute inset-0 grid place-items-center text-white/70 text-sm pointer-events-none">
|
<div className="absolute inset-0 grid place-items-center px-6 text-center text-sm pointer-events-none">
|
||||||
{t("plex.player.loading")}
|
{loadError ? (
|
||||||
|
<span className="max-w-md text-red-300">{loadError}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-white/70">{t("plex.player.loading")}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,8 @@
|
||||||
"plexTestOk": "Verbunden mit {{name}}",
|
"plexTestOk": "Verbunden mit {{name}}",
|
||||||
"plexTestFailed": "Plex-Verbindung fehlgeschlagen — prüfe URL und Token",
|
"plexTestFailed": "Plex-Verbindung fehlgeschlagen — prüfe URL und Token",
|
||||||
"plexConnected": "Verbunden mit {{name}} {{version}}",
|
"plexConnected": "Verbunden mit {{name}} {{version}}",
|
||||||
|
"plexMediaOk": "Medieneinbindung OK — eine Beispieldatei wurde auf der Festplatte gefunden.",
|
||||||
|
"plexMediaMissing": "Medien unter {{path}} nicht lesbar — prüfe, ob die Plex-Medien in den Container eingebunden sind und die Pfadzuordnung stimmt. Die Wiedergabe schlägt fehl, bis dies behoben ist.",
|
||||||
"plexPickLibraries": "Anzuzeigende Bibliotheken:",
|
"plexPickLibraries": "Anzuzeigende Bibliotheken:",
|
||||||
"plexPickHint": "Keine auszuwählen entspricht dem Anzeigen aller Bibliotheken. Nicht vergessen zu speichern.",
|
"plexPickHint": "Keine auszuwählen entspricht dem Anzeigen aller Bibliotheken. Nicht vergessen zu speichern.",
|
||||||
"plexMovies": "Filme",
|
"plexMovies": "Filme",
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@
|
||||||
"tracks": "Audio & Untertitel",
|
"tracks": "Audio & Untertitel",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Untertitel",
|
"subtitles": "Untertitel",
|
||||||
"subOff": "Aus"
|
"subOff": "Aus",
|
||||||
|
"errNotFound": "Diese Datei kann nicht abgespielt werden — die Medien sind auf dem Server nicht erreichbar (die Plex-Medieneinbindung fehlt oder die Pfadzuordnung ist falsch). Bitte den Administrator, die Plex-Konfiguration zu prüfen.",
|
||||||
|
"errTranscode": "Das Format dieser Datei erfordert eine Transkodierung, die noch nicht unterstützt wird.",
|
||||||
|
"errGeneric": "Die Wiedergabe konnte nicht gestartet werden. Bitte erneut versuchen."
|
||||||
},
|
},
|
||||||
"playable": {
|
"playable": {
|
||||||
"direct": "Spielt direkt im Browser",
|
"direct": "Spielt direkt im Browser",
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,8 @@
|
||||||
"plexTestOk": "Connected to {{name}}",
|
"plexTestOk": "Connected to {{name}}",
|
||||||
"plexTestFailed": "Plex connection failed — check the URL and token",
|
"plexTestFailed": "Plex connection failed — check the URL and token",
|
||||||
"plexConnected": "Connected to {{name}} {{version}}",
|
"plexConnected": "Connected to {{name}} {{version}}",
|
||||||
|
"plexMediaOk": "Media mount OK — a sample file was found on disk.",
|
||||||
|
"plexMediaMissing": "Media not readable at {{path}} — check that the Plex media is bind-mounted into the container and that the path map is correct. Playback will fail until this is fixed.",
|
||||||
"plexPickLibraries": "Libraries to expose:",
|
"plexPickLibraries": "Libraries to expose:",
|
||||||
"plexPickHint": "Unchecking all is the same as exposing every library. Remember to Save.",
|
"plexPickHint": "Unchecking all is the same as exposing every library. Remember to Save.",
|
||||||
"plexMovies": "Movies",
|
"plexMovies": "Movies",
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@
|
||||||
"tracks": "Audio & subtitles",
|
"tracks": "Audio & subtitles",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Subtitles",
|
"subtitles": "Subtitles",
|
||||||
"subOff": "Off"
|
"subOff": "Off",
|
||||||
|
"errNotFound": "This file can't be played — the media isn't reachable on the server (the Plex media mount is missing or the path map is wrong). Ask the admin to check the Plex configuration.",
|
||||||
|
"errTranscode": "This file's format needs transcoding, which isn't supported yet.",
|
||||||
|
"errGeneric": "Playback couldn't start. Please try again."
|
||||||
},
|
},
|
||||||
"playable": {
|
"playable": {
|
||||||
"direct": "Plays directly in the browser",
|
"direct": "Plays directly in the browser",
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,8 @@
|
||||||
"plexTestOk": "Kapcsolódva: {{name}}",
|
"plexTestOk": "Kapcsolódva: {{name}}",
|
||||||
"plexTestFailed": "A Plex-kapcsolat sikertelen — ellenőrizd az URL-t és a tokent",
|
"plexTestFailed": "A Plex-kapcsolat sikertelen — ellenőrizd az URL-t és a tokent",
|
||||||
"plexConnected": "Kapcsolódva: {{name}} {{version}}",
|
"plexConnected": "Kapcsolódva: {{name}} {{version}}",
|
||||||
|
"plexMediaOk": "A media-mount rendben — egy mintafájl megtalálható a lemezen.",
|
||||||
|
"plexMediaMissing": "A média nem olvasható itt: {{path}} — ellenőrizd, hogy a Plex média be van-e mountolva a konténerbe, és hogy helyes-e az útvonal-leképezés. A lejátszás addig nem fog működni, amíg ez nincs javítva.",
|
||||||
"plexPickLibraries": "Megjelenítendő könyvtárak:",
|
"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.",
|
"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",
|
"plexMovies": "Filmek",
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@
|
||||||
"tracks": "Audió és felirat",
|
"tracks": "Audió és felirat",
|
||||||
"audio": "Audió",
|
"audio": "Audió",
|
||||||
"subtitles": "Felirat",
|
"subtitles": "Felirat",
|
||||||
"subOff": "Ki"
|
"subOff": "Ki",
|
||||||
|
"errNotFound": "Ez a fájl nem játszható le — a média nem érhető el a szerveren (hiányzik a Plex media-mount, vagy hibás az útvonal-leképezés). Kérd meg az adminisztrátort, hogy ellenőrizze a Plex beállításait.",
|
||||||
|
"errTranscode": "Ennek a fájlnak a formátuma transzkódolást igényel, ami még nem támogatott.",
|
||||||
|
"errGeneric": "A lejátszást nem sikerült elindítani. Próbáld újra."
|
||||||
},
|
},
|
||||||
"playable": {
|
"playable": {
|
||||||
"direct": "Közvetlenül játszható a böngészőben",
|
"direct": "Közvetlenül játszható a böngészőben",
|
||||||
|
|
|
||||||
|
|
@ -622,6 +622,14 @@ export interface PlexTestResult {
|
||||||
server_name: string;
|
server_name: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
sections: PlexSection[];
|
sections: PlexSection[];
|
||||||
|
// Whether a sample media file resolved + was readable through the local mount (bind-mount +
|
||||||
|
// path-map check). checked=false when the library is empty (nothing to probe yet).
|
||||||
|
media_check?: {
|
||||||
|
checked: boolean;
|
||||||
|
ok?: boolean;
|
||||||
|
plex_path?: string;
|
||||||
|
local_path?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plex browse/search/drill-down (the mirrored catalog rendered as feed-style cards).
|
// Plex browse/search/drill-down (the mirrored catalog rendered as feed-style cards).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue