diff --git a/.env.example b/.env.example index 617bbf885..af96ba868 100644 --- a/.env.example +++ b/.env.example @@ -148,6 +148,27 @@ BETTER_AUTH_SECRET= # Maximum log entries per worker in memory (default: 1000) # WORKER_MAX_LOGS=1000 +# ============================================================================= +# CAPTCHA / CLOUDFLARE BROWSER SOLVING (Advanced) +# ============================================================================= +# Cinephage solves Cloudflare/DDoS-Guard challenges with an embedded headless +# Firefox (Camoufox). Each browser is a full Firefox process (300MB-1GB+ RSS), +# so concurrency is hard-capped to avoid OOM under heavy fan-out. + +# Maximum concurrent Camoufox browsers (default: 2, minimum: 1). +# Lower to 1 on memory-constrained hosts (e.g. small LXCs); raise only if you +# also raise the container's shm_size. +# CAPTCHA_MAX_BROWSERS=2 + +# Force-close a browser if it lives longer than this many ms (default: 120000). +# Safety net for wedged/orphaned browsers; must exceed the solve timeout. +# CAPTCHA_BROWSER_MAX_AGE_MS=120000 + +# NOTE on /dev/shm: headless Firefox uses shared memory. Containers default to +# 64MB which causes crashes. docker-compose.yaml sets shm_size: 256m. For +# `docker run`, add `--shm-size=256m`. On Proxmox LXC, the host's /dev/shm may +# also need raising (mount a larger tmpfs at /dev/shm inside the container). + # ============================================================================= # STREAMING CONFIGURATION (Advanced) # ============================================================================= diff --git a/.github/workflows/ghcr-retag-latest.yml b/.github/workflows/ghcr-retag-latest.yml index eb811d794..77df49ad6 100644 --- a/.github/workflows/ghcr-retag-latest.yml +++ b/.github/workflows/ghcr-retag-latest.yml @@ -205,5 +205,5 @@ jobs: echo "- Source tag: \`${SOURCE_TAG}\`" echo "- Source digest: \`${SOURCE_DIGEST}\`" echo "- New latest digest: \`${NEW_LATEST_DIGEST}\`" - echo "- Result: ✅ \`latest\` now points to \`${SOURCE_TAG}\`" + echo "- Result: \`latest\` now points to \`${SOURCE_TAG}\`" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 0a3eb84bc..7ac003e93 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ opencode opencode.md OPENCODE.md AGENTS.md +HANDOVER.md # Qwen local settings .qwen/ diff --git a/Dockerfile b/Dockerfile index b3aec9e18..c4f139014 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,19 @@ # ========================================== -# Build Stage +# Build-deps base (shared by builder + prod-deps to avoid duplicate apt layers) # ========================================== -FROM node:24-trixie-slim AS builder +FROM node:24-trixie-slim AS build-deps-base WORKDIR /app - RUN apt-get update && apt-get install -y --no-install-recommends \ python3 \ make \ g++ \ && rm -rf /var/lib/apt/lists/* +# ========================================== +# Build Stage +# ========================================== +FROM build-deps-base AS builder + COPY package*.json ./ COPY .npmrc ./ @@ -32,14 +36,7 @@ RUN npm run build # ========================================== # Production Dependencies Stage # ========================================== -FROM node:24-trixie-slim AS prod-deps -WORKDIR /app - -RUN apt-get update && apt-get install -y --no-install-recommends \ - python3 \ - make \ - g++ \ - && rm -rf /var/lib/apt/lists/* +FROM build-deps-base AS prod-deps COPY package*.json ./ COPY .npmrc ./ @@ -50,6 +47,7 @@ COPY .npmrc ./ RUN npm ci --omit=dev --no-audit --no-fund \ && find node_modules -type f -name '*.map' -delete \ && find node_modules -type d \( -name test -o -name tests -o -name __tests__ -o -name docs -o -name doc -o -name examples -o -name example \) -prune -exec rm -rf '{}' + \ + && find node_modules -name 'lightningcss-linux-x64-musl' -prune -exec rm -rf '{}' + \ && find node_modules -type d -empty -delete # ========================================== @@ -63,6 +61,7 @@ ARG APP_SOURCE=https://github.com/MoldyTaint/Cinephage ARG VCS_REF=unknown ARG BUILD_CREATED=unknown ENV APP_VERSION=${APP_VERSION} +ENV APP_COMMIT=${VCS_REF} RUN printf '%s\n' "$APP_VERSION" > /app/version.txt LABEL org.opencontainers.image.title='Cinephage' \ @@ -101,11 +100,14 @@ COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/server.js ./server.js COPY --from=builder /app/scripts ./scripts COPY --from=builder /app/data ./bundled-data +# Camoufox shadow-unlock addon (loaded at runtime for Cloudflare Turnstile solving) +COPY --from=builder /app/src/lib/server/captcha/browser/addon ./camoufox-addon COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/cinephage-entrypoint COPY --chmod=755 docker-script-runner.sh /usr/local/bin/cinephage-script RUN ln -sf /usr/local/bin/cinephage-script /usr/local/bin/cine-run ENV NODE_ENV=production \ + NODE_OPTIONS="--max-old-space-size=2048" \ HOST=0.0.0.0 \ PORT=3000 \ FFPROBE_PATH=/usr/bin/ffprobe \ diff --git a/LICENSE b/LICENSE index 82f08ba71..152931d79 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + + GNU GENERAL PUBLIC LICENSE + Cinephage - Self-hosted Media Management Copyright (C) 2025 Cinephage Contributors @@ -16,7 +20,7 @@ along with this program. If not, see . -------------------------------------------------------------------------------- - GNU GENERAL PUBLIC LICENSE + GNU GENERAL PUBLIC LICENSE (full text) Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. diff --git a/README.md b/README.md index a6938ff2c..22fb03b1c 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,9 @@ Dynamic content discovery with auto-add to library. Import from IMDb, Trakt, TMD ## Requirements - **TMDB API Key** — Free at [themoviedb.org/settings/api](https://www.themoviedb.org/settings/api) -- **Download Client** (optional) — qBittorrent, SABnzbd, or NZBGet +- **Download Client** (optional) — qBittorrent, SABnzbd, NZBGet, Real-Debrid, or TorBox - Or use streaming mode — no download client needed + - Real-Debrid and TorBox accept torrent releases, materialize ready files directly into the resolved library destination, and can optionally remove provider items after a durable import. ## Quick Start diff --git a/data/indexers/definitions/1337x.yaml b/data/indexers/definitions/1337x.yaml new file mode 100644 index 000000000..23ae98c3c --- /dev/null +++ b/data/indexers/definitions/1337x.yaml @@ -0,0 +1,108 @@ +--- +# 1337x Public Torrent Indexer +# Adapted for Cinephage from the Prowlarr/Jackett community definition. +# Cloudflare-protected: relies on Cinephage's built-in Camoufox captcha solver. +id: 1337x +name: 1337x +description: 1337x is a Public torrent site that offers verified torrent downloads +language: en-US +type: public +protocol: torrent +encoding: UTF-8 +requestdelay: 2 + +links: + - https://1337x.to/ + - https://1337x.st/ + - https://x1337x.ws/ + +settings: + - name: info_flaresolverr + type: info_flaresolverr + - name: downloadlink + type: select + label: Download link + default: 'magnet:' + options: + 'magnet:': Magnet + 'http://itorrents.org/': iTorrents.org + - name: downloadlink2 + type: select + label: Download link (fallback) + default: 'http://itorrents.org/' + options: + 'http://itorrents.org/': iTorrents.org + 'magnet:': Magnet + +caps: + categorymappings: + - { id: 28, cat: TV/Anime, desc: 'Anime/Anime' } + - { id: 1, cat: Movies/DVD, desc: 'Movies/DVD' } + - { id: 2, cat: Movies/SD, desc: 'Movies/Divx/Xvid' } + - { id: 42, cat: Movies/HD, desc: 'Movies/HD' } + - { id: 54, cat: Movies/HD, desc: 'Movies/h.264/x264' } + - { id: 70, cat: Movies/HD, desc: 'Movies/HEVC/x265' } + - { id: 76, cat: Movies/UHD, desc: 'Movies/UHD' } + - { id: 41, cat: TV/HD, desc: 'TV/HD' } + - { id: 71, cat: TV, desc: 'TV/HEVC/x265' } + - { id: 75, cat: TV/SD, desc: 'TV/SD' } + - { id: 5, cat: TV, desc: 'TV/DVD' } + + modes: + search: [q] + tv-search: [q, season, ep] + movie-search: [q] + +download: + # The .torrent URL and magnet URI live on the details page. + selectors: + - selector: ul li a[href^="{{ .Config.downloadlink }}"] + attribute: href + - selector: ul li a[href^="{{ .Config.downloadlink2 }}"] + attribute: href + +search: + # 1337x serves a stricter Cloudflare challenge on deep links; the captcha + # solver warms up the site root first, so a simple search path is enough. + paths: + - path: 'search/{{ .Keywords }}/1/' + + keywordsfilters: + - name: re_replace # daily standard S2023 > 2023 + args: ['\bS(20\d{2})\b', '$1'] + + rows: + selector: 'tr:has(a[href^="/torrent/"])' + + fields: + title: + selector: td[class^="coll-1"] a[href^="/torrent/"] + details: + selector: td[class^="coll-1"] a[href^="/torrent/"] + attribute: href + download: + selector: td[class^="coll-1"] a[href^="/torrent/"] + attribute: href + category: + selector: td[class^="coll-1"] a[href^="/sub/"] + attribute: href + optional: true + default: '42' + filters: + - name: split + args: ['/', 2] + seeders: + selector: td[class^="coll-2"] + leechers: + selector: td[class^="coll-3"] + size: + selector: td[class^="coll-4"] + date: + selector: td[class^="coll-date"] + optional: true + filters: + - name: fuzzytime + downloadvolumefactor: + text: '0' + uploadvolumefactor: + text: '1' diff --git a/data/indexers/definitions/bitsearch.yaml b/data/indexers/definitions/bitsearch.yaml index 9a09c445c..de79f8596 100644 --- a/data/indexers/definitions/bitsearch.yaml +++ b/data/indexers/definitions/bitsearch.yaml @@ -9,9 +9,10 @@ type: public encoding: UTF-8 requestdelay: 2 links: - - https://bitsearch.to/ + - https://bitsearch.eu/ - https://solidtorrents.eu/ legacylinks: + - https://bitsearch.to/ - https://bitsearch.nocensor.cloud/ - https://bitsearch.mrunblock.bond/ - https://solidtorrents.net/ diff --git a/data/indexers/definitions/bt4g.yaml b/data/indexers/definitions/bt4g.yaml new file mode 100644 index 000000000..1d09cfdf0 --- /dev/null +++ b/data/indexers/definitions/bt4g.yaml @@ -0,0 +1,141 @@ +--- +# BT4G Public DHT Search Engine +# Uses the XML/RSS endpoint instead of HTML parsing. The RSS element +# carries the magnet URI directly, and packs +# "Title
Size
Category
Infohash" - size and infohash are extracted +# from it via re_replace (guarded by definitions/bt4g.test.ts). +# RSS provides no seed/leech data, so seeders/leechers are static placeholders. +id: bt4g +name: BT4G +description: BT4G is a public DHT search engine (XML/RSS endpoint) +language: en-US +type: public +encoding: UTF-8 +protocol: torrent +requestdelay: 3 + +links: + - https://bt4gprx.com/ + - https://bt4g.com/ + +caps: + categories: + '2000': Movies + '5000': TV + '3000': Audio + '8000': Other + + categorymappings: + - id: movie + cat: '2000' + desc: Movies + default: true + - id: movie + cat: '5000' + desc: TV + - id: audio + cat: '3000' + desc: Audio + - id: doc + cat: '8000' + desc: Documents + - id: app + cat: '8000' + desc: Applications + - id: other + cat: '8000' + desc: Other + + modes: + search: [q] + movie-search: [q] + tv-search: [q, season, ep] + +settings: + - name: category + type: select + label: Search category + default: movie + options: + all: All + movie: Movies and TV + audio: Audio + doc: Documents + app: Applications + other: Other + - name: orderby + type: select + label: Sort + default: seeders + options: + relevance: Relevance + size: Size + seeders: Seeders + time: Time + +search: + paths: + - path: search?q={{ .Keywords }}&orderby={{ .Config.orderby }}&category={{ .Config.category }}&p=1&page=rss + method: get + + response: + type: xml + + headers: + Accept: + - application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8 + User-Agent: + - Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/126 Safari/537.36 + + rows: + selector: channel > item + filters: + - name: notcontains + args: mnsn.click + + fields: + title: + selector: title + + details: + selector: guid + + # carries the magnet URI directly + download: + selector: link + + date: + selector: pubDate + optional: true + + size: + selector: description + optional: true + filters: + - name: re_replace + args: + - (?is)^.*?\s*([^<]+).*$ + - $1 + - name: trim + + infohash: + selector: description + optional: true + filters: + - name: re_replace + args: + - (?is)^.*\s*([a-fA-F0-9]{40})\s*$ + - $1 + + # RSS feed exposes no swarm data + seeders: + text: '1' + + leechers: + text: '0' + + downloadvolumefactor: + text: '0' + + uploadvolumefactor: + text: '1' diff --git a/data/indexers/definitions/cinephage-stream.yaml b/data/indexers/definitions/cinephage-stream.yaml index 807d9e353..4367acf65 100644 --- a/data/indexers/definitions/cinephage-stream.yaml +++ b/data/indexers/definitions/cinephage-stream.yaml @@ -3,6 +3,10 @@ # This internal indexer queries the local library database to provide # streaming URLs for content in your library. # +# Auto-managed by the CinephageAPI subsystem's library-streaming module. +# Config (useHttps, externalHost, version/commit) lives in the subsystem +# settings UI, NOT here — this YAML is only the search-execution template. +# # Quality is determined at playback time by the streaming proxy, # which extracts streams from providers and selects the best available quality. @@ -14,36 +18,15 @@ protocol: streaming language: en-US encoding: UTF-8 +# Hides this definition from the indexers-page picker and marks the seeded +# indexer row as isBuiltIn (non-deletable, non-editable from the UI). +internal: true + links: - https://api.cinephage.net -settings: - - name: useHttps - type: checkbox - label: Use HTTPS - default: false - helpText: 'Enable only if your own Cinephage server address is exposed over HTTPS. If you use a non-default port for HTTP or HTTPS, include it in Cinephage Server Address.' - - - name: externalHost - type: text - label: Cinephage Server Address - default: '' - placeholder: '192.168.1.100:3000' - helpText: 'Manual address for your own Cinephage server, used by Jellyfin and .strm playback. You can enter an IP address or a web domain. Include the port when Cinephage is not using the default HTTP or HTTPS port, for example 192.168.1.100:3000 or media.example.com:8443. Do not enter api.cinephage.net here.' - - - name: cinephageCommit - type: text - label: Cinephage API Commit - default: '' - placeholder: 'abc123def' - helpText: 'Manual dev value used for X-Cinephage-Commit. Must match the current published build accepted by api.cinephage.net.' - - - name: cinephageVersion - type: text - label: Cinephage API Version - default: '' - placeholder: '2.3.3' - helpText: 'Manual dev value used for X-Cinephage-Version. Must match the current published build accepted by api.cinephage.net.' +# User-configurable settings have moved to the CinephageAPI subsystem +# (Settings -> Integrations -> Cinephage). This YAML no longer declares any. # Protocol-specific configuration for streaming protocolConfig: diff --git a/data/indexers/definitions/digitalcore.yaml b/data/indexers/definitions/digitalcore.yaml new file mode 100644 index 000000000..bc820b7e2 --- /dev/null +++ b/data/indexers/definitions/digitalcore.yaml @@ -0,0 +1,256 @@ +--- +# DigitalCore Private Torrent Tracker (JSON API) +# Auth via X-API-KEY header; API endpoint verified live (returns a proper +# 401 "Authentication Required" without a key). Text-only searching - the +# site's IMDb search parameter is unreliable, so queries go through +# searchText exclusively. +id: digitalcore +name: DigitalCore +description: DigitalCore is a private torrent tracker for 0-day / general content (JSON API) +language: en-US +type: private +encoding: UTF-8 +protocol: torrent +requestdelay: 2 + +links: + - https://digitalcore.club/ + - https://prxy.digitalcore.club/ + +caps: + categorymappings: + - id: 2 + cat: Movies/SD + desc: Movies/SD + - id: 1 + cat: Movies/DVD + desc: Movies/DVDR + - id: 5 + cat: Movies/HD + desc: Movies/720p + - id: 6 + cat: Movies/HD + desc: Movies/1080p + - id: 4 + cat: Movies/UHD + desc: Movies/2160p + - id: 3 + cat: Movies/BluRay + desc: Movies/BluRay + - id: 38 + cat: Movies/UHD + desc: Movies/BluRay/UHD + - id: 7 + cat: Movies/HD + desc: Movies/PACKS + - id: 10 + cat: TV/SD + desc: TV/SD + - id: 11 + cat: TV/SD + desc: TV/DVDR + - id: 8 + cat: TV/HD + desc: TV/720p + - id: 9 + cat: TV/HD + desc: TV/1080p + - id: 13 + cat: TV/UHD + desc: TV/2160p + - id: 14 + cat: TV/HD + desc: TV/BluRay + - id: 12 + cat: TV/HD + desc: TV/PACKS + - id: 15 + cat: TV/Sport + desc: TV/SPORTS + - id: 17 + cat: Other + desc: Unknown + - id: 18 + cat: PC/0day + desc: Apps/0DAY + - id: 20 + cat: PC/ISO + desc: Apps/PC + - id: 21 + cat: PC/Mac + desc: Apps/Mac + - id: 33 + cat: PC + desc: Apps/Tutorials + - id: 22 + cat: Audio/MP3 + desc: Music/MP3 + - id: 23 + cat: Audio/Lossless + desc: Music/FLAC + - id: 24 + cat: Audio/Video + desc: Music/MTV + - id: 29 + cat: Audio + desc: Music/PACKS + - id: 39 + cat: Audio/Video + desc: Music/DVD + - id: 40 + cat: Audio/Video + desc: Music/BluRay + - id: 25 + cat: PC/Games + desc: Games/PC + - id: 27 + cat: PC/Mac + desc: Games/Mac + - id: 26 + cat: Console + desc: Games/Consoles + - id: 43 + cat: Console + desc: Games/ROMS + - id: 44 + cat: Audio/Audiobook + desc: Audiobooks + - id: 28 + cat: Books/EBook + desc: Ebooks + + modes: + search: [q] + tv-search: [q, season, ep] + movie-search: [q] + music-search: [q] + book-search: [q] + +settings: + - name: apikey + type: password + label: API Key + - name: freeleech + type: checkbox + label: Search freeleech only + default: false + - name: p2p_releases_only + type: checkbox + label: Search P2P releases only + default: false + - name: unrar_releases_only + type: checkbox + label: Search UNRAR releases only + default: false + - name: sort + type: select + label: Sort + default: d + options: + d: created + up: seeders + s: size + n: title + - name: type + type: select + label: Order + default: desc + options: + desc: desc + asc: asc + +search: + paths: + - path: api/v1/torrents?dead=true&extendedDead=true&extendedSearch=false&freeleech={{ if .Config.freeleech }}true{{ else }}false{{ end }}&index=0&limit=100&order={{ .Config.type }}&p2p={{ if .Config.p2p_releases_only }}true{{ else }}false{{ end }}&page=search&searchText={{ .Keywords }}§ion=all&sort={{ .Config.sort }}&stereoscopic=false&unrar={{ if .Config.unrar_releases_only }}true{{ else }}false{{ end }}&watchview=false{{ range .Categories }}&categories[]={{ . }}{{ end }} + method: get + + response: + type: json + + headers: + X-API-KEY: + - '{{ .Config.apikey }}' + Accept: + - application/json + User-Agent: + - Cinephage/1.0 + X-Requested-With: + - XMLHttpRequest + + rows: + selector: $ + + fields: + category: + selector: category + + title: + selector: name + + details: + selector: id + filters: + - name: prepend + args: /torrent/ + - name: append + args: / + + download: + selector: id + filters: + - name: prepend + args: /api/v1/torrents/download/ + + imdbid: + selector: imdbid2 + optional: true + + date: + selector: added + filters: + - name: append + args: ' +02:00' + - name: dateparse + args: YYYY-MM-DD HH:mm:ss Z + + size: + selector: size + + files: + selector: numfiles + optional: true + + grabs: + selector: times_completed + optional: true + + seeders: + selector: seeders + optional: true + + leechers: + selector: leechers + optional: true + + downloadvolumefactor: + selector: frileech + optional: true + case: + '0': '1' + '1': '0' + '*': '1' + + uploadvolumefactor: + text: '1' + + minimumratio: + text: '1.0' + + minimumseedtime: + text: '432000' + + guid: + selector: id + filters: + - name: prepend + args: digitalcore- diff --git a/data/indexers/definitions/jackett.yaml b/data/indexers/definitions/jackett.yaml new file mode 100644 index 000000000..59357710f --- /dev/null +++ b/data/indexers/definitions/jackett.yaml @@ -0,0 +1,149 @@ +--- +id: jackett +name: Jackett +description: Direct integration with Jackett's native JSON search API +type: semi-private +language: en-US +encoding: UTF-8 +requestdelay: 0 +# Per-indexer request; 15 s is generous enough for a single tracker +requesttimeout: 15000 + +links: + - http://localhost:9117/ + +settings: + - name: apikey + type: password + label: API Key + required: true + - name: trackerId + type: text + label: Tracker ID + required: true + +caps: + modes: + search: [q] + movie-search: [q] + tv-search: [q] + categories: + '2000': Movies + '2010': Movies/Foreign + '2020': Movies/Other + '2030': Movies/SD + '2040': Movies/HD + '2045': Movies/UHD + '2050': Movies/BluRay + '2060': Movies/3D + '2070': Movies/DVD + '2080': Movies/WEB-DL + '2090': Movies/x265 + '5000': TV + '5010': TV/WEB-DL + '5020': TV/Foreign + '5030': TV/SD + '5040': TV/HD + '5045': TV/UHD + '5050': TV/Other + '5060': TV/Sport + '5070': TV/Anime + '5080': TV/Documentary + '5090': TV/x265 + '6000': XXX + '8000': Other + '8010': Other/Misc + +search: + paths: + # Movie search — category-filtered + - path: api/v2.0/indexers/all/results + method: get + categories: [Movies] + inputs: + Query: '{{ .Keywords }}' + apikey: '{{ .Config.apikey }}' + $raw: 'Tracker[]={{ .Config.trackerId }}{{ if .Categories }}&Category[]={{ join .Categories "&Category[]=" }}{{ end }}' + + # TV search — category-filtered + - path: api/v2.0/indexers/all/results + method: get + categories: [TV] + inputs: + Query: '{{ .Keywords }}' + apikey: '{{ .Config.apikey }}' + $raw: 'Tracker[]={{ .Config.trackerId }}{{ if .Categories }}&Category[]={{ join .Categories "&Category[]=" }}{{ end }}' + + # General keyword search fallback + - path: api/v2.0/indexers/all/results + method: get + inputs: + Query: '{{ .Keywords }}' + apikey: '{{ .Config.apikey }}' + $raw: 'Tracker[]={{ .Config.trackerId }}' + + response: + type: json + + rows: + selector: Results + + fields: + title: + selector: Title + details: + selector: Details + optional: true + download: + selector: Link + optional: true + magnet: + selector: MagnetUri + optional: true + guid: + selector: Guid + optional: true + infohash: + selector: InfoHash + optional: true + date: + selector: PublishDate + size: + selector: Size + optional: true + seeders: + selector: Seeders + optional: true + leechers: + selector: Peers + optional: true + category: + selector: Category[0] + optional: true + grabs: + selector: Grabs + optional: true + downloadvolumefactor: + selector: DownloadVolumeFactor + optional: true + uploadvolumefactor: + selector: UploadVolumeFactor + optional: true + imdbid: + selector: Imdb + optional: true + filters: + - name: re_replace + args: ['^0$', ''] + tmdbid: + selector: Tmdb + optional: true + filters: + - name: re_replace + args: ['^0$', ''] + tvdbid: + selector: Tvdb + optional: true + filters: + - name: re_replace + args: ['^0$', ''] diff --git a/data/indexers/definitions/knaben.yaml b/data/indexers/definitions/knaben.yaml index 83d3b02c4..07797882d 100644 --- a/data/indexers/definitions/knaben.yaml +++ b/data/indexers/definitions/knaben.yaml @@ -73,7 +73,7 @@ search: order_direction: desc size: '150' hide_unsafe: 'true' - hide_xxx: '{{ .Config.hide_xxx }}' + hide_xxx: '{{ if .Config.hide_xxx }}true{{ else }}false{{ end }}' # TV search - path: /v1 @@ -87,7 +87,7 @@ search: order_direction: desc size: '150' hide_unsafe: 'true' - hide_xxx: '{{ .Config.hide_xxx }}' + hide_xxx: '{{ if .Config.hide_xxx }}true{{ else }}false{{ end }}' # XXX search — override hide_xxx so adult searches always return adult results # regardless of the user's "Hide XXX Content" preference for general browsing @@ -113,7 +113,7 @@ search: order_direction: desc size: '150' hide_unsafe: 'true' - hide_xxx: '{{ .Config.hide_xxx }}' + hide_xxx: '{{ if .Config.hide_xxx }}true{{ else }}false{{ end }}' headers: Content-Type: application/json diff --git a/data/indexers/definitions/magnetz.yaml b/data/indexers/definitions/magnetz.yaml new file mode 100644 index 000000000..4b31d05b2 --- /dev/null +++ b/data/indexers/definitions/magnetz.yaml @@ -0,0 +1,98 @@ +--- +# Magnetz.eu - Native Cinephage Definition +# Uses the official public JSON API (no auth required) +# https://magnetz.eu/apis + +id: magnetz +name: Magnetz +description: 'Magnetz is a public magnet/torrent metadata search engine with a JSON API' +language: en-US +type: public +protocol: torrent +encoding: UTF-8 +requestdelay: 1 + +links: + - https://magnetz.eu/ + +caps: + categories: + '2000': Movies + '5000': TV + + categorymappings: + # API doesn't return a type/genre field per result, so this indexer is + # mapped as supporting both Movies and TV by default so Cinephage's + # search-type eligibility filter includes it for both search kinds. + - id: all + cat: '2000' + desc: Movies + default: true + - id: all + cat: '5000' + desc: TV + default: true + + modes: + search: [q] + tv-search: [q] + movie-search: [q] + +search: + paths: + # https://magnetz.eu/api/magnets/search?query=ubuntu+22.04&page=1 + - path: 'api/magnets/search' + method: get + inputs: + query: '{{ .Keywords }}' + page: '1' + + response: + type: json + + rows: + selector: data + + fields: + guid: + selector: sqid + + title: + selector: name + + details: + selector: web_url + + magnet: + selector: magnet_link + + download: + selector: magnet_link + + infohash: + selector: info_hash + + size: + selector: size + + seeders: + selector: seeders + + leechers: + selector: leechers + + date: + selector: created_at + filters: + - name: dateparse + args: 'YYYY-MM-DDTHH:mm:ssZ' + + verified: + selector: is_verified + optional: true + + downloadvolumefactor: + text: '0' + + uploadvolumefactor: + text: '1' diff --git a/data/indexers/definitions/prowlarr.yaml b/data/indexers/definitions/prowlarr.yaml new file mode 100644 index 000000000..0d59eee42 --- /dev/null +++ b/data/indexers/definitions/prowlarr.yaml @@ -0,0 +1,142 @@ +--- +id: prowlarr +name: Prowlarr +description: Direct integration with Prowlarr's native search API +type: semi-private +language: en-US +encoding: UTF-8 +requestdelay: 0.1 +# Prowlarr aggregate search queries all indexers; allow up to 90 seconds for a response +requesttimeout: 90000 + +links: + - http://localhost:9696/ + +settings: + - name: apikey + type: password + label: API Key + required: true + - name: indexerId + type: text + label: Indexer ID + required: false + +caps: + modes: + search: [q] + movie-search: [q, imdbid, tmdbid] + tv-search: [q, imdbid, tvdbid, season, ep] + categories: + '2000': Movies + '2010': Movies/Foreign + '2020': Movies/Other + '2030': Movies/SD + '2040': Movies/HD + '2045': Movies/UHD + '2050': Movies/BluRay + '2060': Movies/3D + '2070': Movies/DVD + '2080': Movies/WEB-DL + '2090': Movies/x265 + '5000': TV + '5010': TV/WEB-DL + '5020': TV/Foreign + '5030': TV/SD + '5040': TV/HD + '5045': TV/UHD + '5050': TV/Other + '5060': TV/Sport + '5070': TV/Anime + '5080': TV/Documentary + '5090': TV/x265 + '6000': XXX + '6010': XXX/DVD + '6020': XXX/WMV + '6030': XXX/XviD + '6040': XXX/x264 + '6045': XXX/UHD + '6050': XXX/Pack + '6060': XXX/ImgSet + '6070': XXX/Other + '6080': XXX/SD + '6090': XXX/x265 + '8000': Other + '8010': Other/Misc + '8020': Other/Hashed + +search: + paths: + # Movie search — passes TMDB/IMDB IDs when available + - path: api/v1/search + method: get + categories: [Movies] + inputs: + query: '{{ .Keywords }}' + type: movie + imdbId: '{{ .Query.IMDBID }}' + tmdbId: '{{ .Query.TMDBID }}' + apikey: '{{ .Config.apikey }}' + $raw: '{{ if .Config.indexerId }}indexerIds={{ .Config.indexerId }}&{{ end }}{{ if .Categories }}categories={{ join .Categories "&categories=" }}{{ end }}' + + # TV search — passes TVDB/IMDB IDs, season, and episode when available + - path: api/v1/search + method: get + categories: [TV] + inputs: + query: '{{ .Keywords }}' + type: tv + imdbId: '{{ .Query.IMDBID }}' + tvdbId: '{{ .Query.TVDBID }}' + seasonNumber: '{{ .Query.Season }}' + episodeNumber: '{{ .Query.Ep }}' + apikey: '{{ .Config.apikey }}' + $raw: '{{ if .Config.indexerId }}indexerIds={{ .Config.indexerId }}&{{ end }}{{ if .Categories }}categories={{ join .Categories "&categories=" }}{{ end }}' + + # General keyword search fallback + - path: api/v1/search + method: get + inputs: + query: '{{ .Keywords }}' + type: search + apikey: '{{ .Config.apikey }}' + $raw: '{{ if .Config.indexerId }}indexerIds={{ .Config.indexerId }}&{{ end }}{{ if .Categories }}categories={{ join .Categories "&categories=" }}{{ end }}' + + response: + type: json + + fields: + title: + selector: title + details: + selector: infoUrl + optional: true + download: + selector: downloadUrl + optional: true + guid: + selector: guid + optional: true + date: + selector: publishDate + size: + selector: size + optional: true + seeders: + selector: seeders + optional: true + leechers: + selector: leechers + optional: true + infohash: + selector: infoHash + optional: true + magnet: + selector: magnetUrl + optional: true + category: + selector: category + optional: true + protocol: + selector: protocol + optional: true diff --git a/data/indexers/definitions/seedpool.yaml b/data/indexers/definitions/seedpool.yaml new file mode 100644 index 000000000..0f94a5500 --- /dev/null +++ b/data/indexers/definitions/seedpool.yaml @@ -0,0 +1,203 @@ +--- +# seedpool Private Torrent Tracker (UNIT3D JSON API) +# Auth via api/torrents/filter apiToken query param + Bearer header. +# Endpoint verified live (serves the UNIT3D login wall without a token). +# Same UNIT3D API shape as oldtoonsworld.yaml. +id: seedpool +name: seedpool +description: seedpool is a private torrent tracker for 0-day movies, TV and general content +language: en-US +type: private +encoding: UTF-8 +protocol: torrent +requestdelay: 2 + +links: + - https://seedpool.org/ + +caps: + categorymappings: + - id: 1 + cat: Movies + desc: Movie + - id: 2 + cat: TV + desc: TV Show + - id: 3 + cat: PC/Games + desc: Game + - id: 4 + cat: Console/Other + desc: Deprecated - NSW Game + - id: 5 + cat: Audio + desc: Music + - id: 6 + cat: TV/Anime + desc: Anime + - id: 7 + cat: Books/EBook + desc: E-Book + - id: 8 + cat: TV/Sport + desc: Sports + - id: 9 + cat: Audio/Audiobook + desc: Audiobook + - id: 10 + cat: Movies/UHD + desc: Deprecated - 4K Movie + - id: 11 + cat: Other + desc: Other + - id: 12 + cat: Other + desc: Hobby + - id: 13 + cat: TV + desc: Deprecated - BoxSet + - id: 14 + cat: PC/Games + desc: Deprecated - Game + - id: 15 + cat: Other + desc: Deprecated - Education + - id: 16 + cat: PC + desc: Software + - id: 17 + cat: PC/0day + desc: Deprecated - Linux + - id: 18 + cat: PC/Mac + desc: Deprecated - macOS + - id: 19 + cat: Console + desc: Deprecated - Consoles + - id: 20 + cat: TV/Foreign + desc: Deprecated - JPTV + - id: 21 + cat: PC + desc: Music Production + + modes: + search: [q] + tv-search: [q, season, ep, imdbid, tvdbid, tmdbid] + movie-search: [q, imdbid, tmdbid] + music-search: [q] + book-search: [q] + +settings: + - name: apikey + type: password + label: API Key + - name: freeleech + type: checkbox + label: Search freeleech only + default: false + - name: sort + type: select + label: Sort + default: created_at + options: + created_at: created + seeders: seeders + size: size + name: title + - name: type + type: select + label: Order + default: desc + options: + desc: desc + asc: asc + +search: + paths: + - path: api/torrents/filter?apiToken={{ .Config.apikey }}{{ range .Categories }}&categories[]={{.}}{{end}}{{ if .Config.freeleech }}&free[]=100{{ end }}{{ if .Keywords }}&name={{ .Keywords }}{{ end }}{{ if .Query.IMDBIDShort }}&imdbId={{ .Query.IMDBIDShort }}{{ end }}{{ if .Query.TMDBID }}&tmdbId={{ .Query.TMDBID }}{{ end }}{{ if .Query.TVDBID }}&tvdbId={{ .Query.TVDBID }}{{ end }}{{ if .Query.Season }}&seasonNumber={{ .Query.Season }}{{ end }}{{ if .Query.Ep }}&episodeNumber={{ .Query.Ep }}{{ end }}&sortField={{ .Config.sort }}&sortDirection={{ .Config.type }}&perPage=100 + method: get + + response: + type: json + + headers: + Authorization: + - Bearer {{ .Config.apikey }} + + rows: + selector: data + + fields: + category: + selector: attributes.category_id + optional: true + + title: + selector: attributes.name + + details: + selector: attributes.details_link + + download: + selector: attributes.download_link + + infohash: + selector: attributes.info_hash + optional: true + + imdbid: + selector: attributes.imdb_id + optional: true + + tmdbid: + selector: attributes.tmdb_id + optional: true + + tvdbid: + selector: attributes.tvdb_id + optional: true + + seeders: + selector: attributes.seeders + optional: true + + leechers: + selector: attributes.leechers + optional: true + + grabs: + selector: attributes.times_completed + optional: true + + date: + selector: attributes.created_at + filters: + - name: re_replace + args: + - \.\d+Z$ + - Z + - name: dateparse + args: YYYY-MM-DDTHH:mm:ssZ + + size: + selector: attributes.size + + downloadvolumefactor: + selector: attributes.freeleech + optional: true + case: + '100': '0' + 100%: '0' + '*': '1' + + uploadvolumefactor: + selector: attributes.double_upload + optional: true + case: + 'True': '2' + 'true': '2' + '*': '1' + + minimumseedtime: + text: '864000' diff --git a/data/indexers/definitions/therarbg.yaml b/data/indexers/definitions/therarbg.yaml new file mode 100644 index 000000000..c7506bf4b --- /dev/null +++ b/data/indexers/definitions/therarbg.yaml @@ -0,0 +1,146 @@ +--- +# TheRARBG - Native Cinephage Definition +# HTML scraper for therarbg.to (get-posts listing pages) +# Magnet links are not on the listing page, only on the /post-detail/ page, +# so this uses a two-stage download (details page -> extract magnet), same +# pattern as limetorrents.yaml. + +id: therarbg +name: TheRARBG +description: 'TheRARBG is a public torrent index covering Movies, TV, Anime, Music, Books, Games, Apps and XXX' +language: en-US +type: public +encoding: UTF-8 +protocol: torrent +requestdelay: 2 + +links: + - https://therarbg.to/ + +legacylinks: + - https://therarbg.com/ + - https://t-rb.org/ + +caps: + categories: + '2000': Movies + '5000': TV + '5070': TV/Anime + '3000': Audio + '7000': Books + '1000': PC + '4000': PC/Games + '6000': XXX + '8000': Other + + categorymappings: + - id: Movies + cat: '2000' + desc: Movies + default: true + - id: TV + cat: '5000' + desc: TV + - id: Anime + cat: '5070' + desc: Anime + - id: Music + cat: '3000' + desc: Music + - id: Books + cat: '7000' + desc: Books + - id: Apps + cat: '1000' + desc: Apps + - id: Games + cat: '4000' + desc: Games + - id: XXX + cat: '6000' + desc: XXX + - id: Documentaries + cat: '8000' + desc: Documentaries + - id: Other + cat: '8000' + desc: Other + + modes: + search: [q] + tv-search: [q, season, ep] + movie-search: [q] + music-search: [q] + book-search: [q] + +settings: + - name: info_flaresolverr + type: info + label: FlareSolverr + default: 'This site may require FlareSolverr to be configured if it is behind Cloudflare protection.' + +# Magnet is not present on the listing/search page - only on the post-detail +# page. "download" points to the post-detail URL (from fields.download below); +# Jackett/Cardigann fetches that page and applies these selectors to pull the +# actual magnet link out of it. +download: + selectors: + - selector: a[href^="magnet:?xt"] + attribute: href + +search: + paths: + # https://therarbg.to/get-posts/keywords:ubuntu:order:-se:paginate_by:100/ + - path: 'get-posts/keywords:{{ .Keywords }}:order:-se:paginate_by:100/' + + keywordsfilters: + - name: re_replace + args: ['\.', ' '] + - name: re_replace + args: ['\s+', ' '] + - name: trim + + rows: + # Every result row has a link to a /post-detail/ page - anchor on that + # rather than a specific table/class, since row markup can vary by section. + selector: 'table tr:has(a[href^="/post-detail/"])' + + fields: + title: + selector: 'td:nth-child(2) a[href^="/post-detail/"]' + + details: + selector: 'td:nth-child(2) a[href^="/post-detail/"]' + attribute: href + + # Same URL as details - the download block above fetches this page and + # extracts the real magnet link from it. + download: + selector: 'td:nth-child(2) a[href^="/post-detail/"]' + attribute: href + + category: + selector: 'td:nth-child(3) a' + optional: true + default: Movies + + date: + selector: 'td:nth-child(4)' + filters: + - name: dateparse + args: 'YYYY-MM-DD' + + size: + selector: 'td:nth-child(6)' + + seeders: + selector: 'td:nth-child(7)' + + leechers: + selector: 'td:nth-child(8)' + + downloadvolumefactor: + text: '0' + + uploadvolumefactor: + text: '1' diff --git a/data/indexers/definitions/torrentgalaxyclone.yaml b/data/indexers/definitions/torrentgalaxyclone.yaml new file mode 100644 index 000000000..7a2c6eff1 --- /dev/null +++ b/data/indexers/definitions/torrentgalaxyclone.yaml @@ -0,0 +1,165 @@ +--- +# TorrentGalaxy Clone (torrentgalaxy.one / torrentgalaxy.info) +# Public torrent index running the RARBG-style engine (get-posts paths). +# Magnet links live on the /post-detail/ page only, so this uses the same +# two-stage download pattern as therarbg.yaml and limetorrents.yaml. +# Selector notes (verified against live HTML, 2026-07-18): +# - category comes from the first cell's /get-posts/category: link +# - the seed/leech cell holds ONE span: [seeds/leech] +# - the date cell shows compound relative ages ("1 week, 5 days") and embeds a +# div.bighide mobile summary that must be removed before parsing +# Filter chains are guarded by definitions/torrentgalaxyclone.test.ts. +id: torrentgalaxyclone +name: TorrentGalaxy Clone +description: TorrentGalaxy Clone is a public torrent index covering Movies, TV, Anime, Music, Books, Games, Apps, Documentaries and Other content. +language: en-US +type: public +encoding: UTF-8 +protocol: torrent +requestdelay: 2 + +links: + - https://torrentgalaxy.one/ + - https://torrentgalaxy.info/ + +legacylinks: + - https://torrentgalaxy.space/ + +caps: + categories: + '2000': Movies + '5000': TV + '5070': TV/Anime + '5080': TV/Documentary + '3000': Audio + '7000': Books + '1000': PC + '4000': PC/Games + '8000': Other + + categorymappings: + - id: Anime + cat: '5070' + desc: Anime + - id: Apps + cat: '1000' + desc: Apps + - id: Books + cat: '7000' + desc: Books + - id: Docus + cat: '5080' + desc: Documentaries + - id: Documentaries + cat: '5080' + desc: Documentaries + - id: E-books + cat: '7000' + desc: E-books + - id: Games + cat: '4000' + desc: Games + - id: Movies + cat: '2000' + desc: Movies + default: true + - id: Music + cat: '3000' + desc: Music + - id: Other + cat: '8000' + desc: Other + - id: TV + cat: '5000' + desc: TV + + modes: + search: [q] + tv-search: [q, season, ep] + movie-search: [q] + music-search: [q] + book-search: [q] + +settings: + - name: uploader + type: text + label: Filter by Uploader + - name: info_uploader + type: info + label: About filtering by Uploader + default: Enter the case-sensitive uploader username, or leave empty to return results from all uploaders. + - name: info_flaresolverr + type: info + label: FlareSolverr + default: This site may require FlareSolverr if a configured domain is protected by Cloudflare. + +download: + selectors: + - selector: a[href^="magnet:?xt="] + attribute: href + +search: + paths: + - path: get-posts/{{ if .Keywords }}keywords:{{ .Keywords }}{{ end }}{{ range .Categories }}:category:{{ . }}{{ end }} + + keywordsfilters: + - name: trim + + rows: + selector: div.tgxtablerow{{ if .Config.uploader }}:has(a.username:contains({{ .Config.uploader }})){{ end }} + + fields: + category: + selector: a[href^="/get-posts/category:"] small + optional: true + default: Movies + + title: + selector: a[href^="/post-detail/"] + attribute: title + + details: + selector: a[href^="/post-detail/"] + attribute: href + + download: + selector: a[href^="/post-detail/"] + attribute: href + + imdbid: + selector: a[href^="/get-posts/keywords:tt"] + attribute: href + optional: true + filters: + - name: re_replace + args: + - .*keywords:(tt\d+).* + - $1 + + size: + selector: div.tgxtablecell:nth-last-child(5) + + seeders: + selector: div.tgxtablecell:nth-last-child(2) font:nth-of-type(1) b + optional: true + + leechers: + selector: div.tgxtablecell:nth-last-child(2) font:nth-of-type(2) b + optional: true + + date: + selector: div.tgxtablecell:nth-last-child(1) + remove: div.bighide + optional: true + filters: + - name: split + args: [',', '0'] + - name: append + args: ' ago' + - name: timeago + + downloadvolumefactor: + text: '0' + + uploadvolumefactor: + text: '1' diff --git a/deploy/unraid/ca_profile.xml b/deploy/unraid/ca_profile.xml new file mode 100644 index 000000000..afe9e7ea1 --- /dev/null +++ b/deploy/unraid/ca_profile.xml @@ -0,0 +1,20 @@ + + Cinephage + Self-hosted media management for automated movie/TV acquisition, quality upgrades, subtitle sync, and library management. + +Cinephage is a self-hosted media management application that helps you build and maintain your media library. + +**Features:** +* Automated movie and TV show acquisition from Usenet and torrent indexers +* Quality profile management with upgrade tracking +* Smart subtitle downloading and synchronization +* Library health monitoring and storage insights +* Import existing media libraries with naming conventions +* Real-Debrid and TorBox acquisition support +* Multi-arch support (amd64 + arm64) + + MediaApp:Video + https://raw.githubusercontent.com/MoldyTaint/Cinephage/main/deploy/unraid/icon.png + https://github.com/MoldyTaint/Cinephage + https://github.com/MoldyTaint/Cinephage/issues + diff --git a/deploy/unraid/cinephage.xml b/deploy/unraid/cinephage.xml new file mode 100644 index 000000000..968838ec5 --- /dev/null +++ b/deploy/unraid/cinephage.xml @@ -0,0 +1,29 @@ + + + cinephage + ghcr.io/moldytaint/cinephage:latest + https://ghcr.io + bridge + false + bash + https://github.com/MoldyTaint/Cinephage/issues + https://github.com/MoldyTaint/Cinephage + +Cinephage is a self-hosted media management application — automated movie/TV library management, quality upgrades, subtitle sync, and streaming. + +**Setup:** On first launch, visit the WebUI to create your admin account. + + MediaApp:Video + http://[IP]:[PORT:3000] + https://raw.githubusercontent.com/MoldyTaint/Cinephage/main/deploy/unraid/icon.png + https://raw.githubusercontent.com/MoldyTaint/Cinephage/main/deploy/unraid/cinephage.xml + --shm-size=256m + + + + + + + + + diff --git a/deploy/unraid/icon.png b/deploy/unraid/icon.png new file mode 100644 index 000000000..7b5ee4f05 Binary files /dev/null and b/deploy/unraid/icon.png differ diff --git a/docker-compose.yaml b/docker-compose.yaml index 803c2cde4..12e85a1f4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,6 +17,10 @@ services: container_name: cinephage restart: unless-stopped init: true + # Headless Firefox (captcha solving) uses /dev/shm; the container default of + # 64MB causes crashes/instability. Proxmox LXC users may also need to raise the + # host's shm — see .env.example. Increase if you raise CAPTCHA_MAX_BROWSERS. + shm_size: 256m security_opt: - no-new-privileges:true ports: diff --git a/eslint-rules/no-emoji.js b/eslint-rules/no-emoji.js new file mode 100644 index 000000000..287afa69e --- /dev/null +++ b/eslint-rules/no-emoji.js @@ -0,0 +1,81 @@ +/** + * Local ESLint rule: no-emoji + * + * Disallows pictographic emoji anywhere in a linted source file — string + * literals, template literals, comments, AND Svelte template markup. Emoji in + * code/PRs is the most widely recognized low-quality / AI-generated tell, and + * they render as tofu boxes on systems without an emoji font. + * + * Scope is deliberately narrow: only what people colloquially call "emoji" + * (the high pictographic Unicode planes plus a few BMP emoji-presentation + * characters). Unicode *symbols* that Google's JS style guide explicitly + * endorses as readable (arrows like `→`, `★`, `✓`, `✗`, `⚠`, `♪`) are NOT + * banned — they are not the AI tell and render widely. Escapes such as + * `\u266A` are allowed because the raw source text contains the six characters + * `\`,`u`,`2`,`6`,`6`,`A` and no actual emoji codepoint. + * + * The rule scans the full raw source text (not AST nodes) so that Svelte + * template markup (e.g. a pictographic glyph placed directly inside an element) + * is caught as well as script content. + */ + +const PICTOGRAPHIC_RANGES = [ + [0x1f000, 0x1f0ff], // mahjong, dominoes, playing cards + [0x1f1e6, 0x1f1ff], // regional indicator letters (flag pairs) + [0x1f300, 0x1faff] // emoticons, misc symbols & pictographs, transport, supplemental symbols +]; + +// BMP characters outside the high planes that nonetheless render as emoji. +const EXPLICIT_EMOJI_CODEPOINTS = new Set([ + 0x2705, // U+2705 white heavy check mark + 0x274c, // U+274C cross mark + 0x2728, // U+2728 sparkles + 0x2764, // U+2764 heavy black heart + 0x2b50 // U+2B50 white medium star +]); + +function isBannedEmoji(cp) { + if (EXPLICIT_EMOJI_CODEPOINTS.has(cp)) return true; + for (const [lo, hi] of PICTOGRAPHIC_RANGES) { + if (cp >= lo && cp <= hi) return true; + } + return false; +} + +export default { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow pictographic emoji anywhere in source (code, comments, Svelte markup)' + }, + schema: [], + messages: { + emoji: + 'Pictographic emoji are not allowed in source (found U+{{cp}}). Emoji in code/PRs is a common low-quality signal and renders inconsistently; use text, an icon component, or a \\u escape. Unicode symbols (arrows, stars, check marks) are fine — only pictographic emoji are banned.' + } + }, + create(context) { + const text = context.sourceCode.text; + const lines = text.split(/\r?\n/); + + return { + Program() { + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + // ESLint columns are 0-based and counted in UTF-16 code units. + for (let column = 0; column < line.length; ) { + const cp = line.codePointAt(column); + if (isBannedEmoji(cp)) { + context.report({ + loc: { line: lineIndex + 1, column }, + messageId: 'emoji', + data: { cp: cp.toString(16).toUpperCase().padStart(4, '0') } + }); + } + column += cp > 0xffff ? 2 : 1; + } + } + } + }; + } +}; diff --git a/eslint.config.js b/eslint.config.js index 0139428c5..6564f8e47 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,9 +7,16 @@ import { defineConfig } from 'eslint/config'; import globals from 'globals'; import ts from 'typescript-eslint'; import svelteConfig from './svelte.config.js'; +import noEmoji from './eslint-rules/no-emoji.js'; const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); +// Local plugin housing project-specific rules (e.g. no-emoji). +const local = { + meta: { name: 'cinephage-local' }, + rules: { 'no-emoji': noEmoji } +}; + export default defineConfig( includeIgnoreFile(gitignorePath), { @@ -17,6 +24,8 @@ export default defineConfig( // External/separate projects 'Flyx-main/**', 'Cinephage-Streamer/**', + // Browser extension asset shipped as-is (loaded by Camoufox, not app code) + 'src/lib/server/captcha/browser/addon/**', // Build artifacts '.svelte-kit/**' ] @@ -33,6 +42,15 @@ export default defineConfig( ...svelte.configs.recommended, prettier, ...svelte.configs.prettier, + { + // Project-specific rules + plugins: { local }, + rules: { + // Disallow pictographic emoji in code/comments (see eslint-rules/no-emoji.js). + // Svelte template markup is naturally exempt — only JS/TS nodes are checked. + 'local/no-emoji': 'error' + } + }, { languageOptions: { globals: { ...globals.browser, ...globals.node } diff --git a/flake.lock b/flake.lock index ce85b2ab8..19b5a5b10 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1778716662, - "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1779560665, - "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", + "lastModified": 1784497964, + "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", + "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", "type": "github" }, "original": { @@ -36,11 +36,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1777168982, - "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", + "lastModified": 1782614948, + "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", "type": "github" }, "original": { @@ -77,11 +77,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1775636079, - "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", + "lastModified": 1784369104, + "narHash": "sha256-47cxbcZODibHv3rELFQ9vZly0vUNkND/atn/U7HLeb0=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", + "rev": "df3c0640565d04a0261253cdd89fce78ec50168a", "type": "github" }, "original": { diff --git a/messages/de.json b/messages/de.json index a14136f81..a70bf4b14 100644 --- a/messages/de.json +++ b/messages/de.json @@ -76,7 +76,6 @@ "action_upload": "Hochladen", "action_view": "Anzeigen", "activity_activeCount": "{total} aktive Downloads", - "activity_statusCount": "{total} {status}", "activity_activeFilters_clearAll": "Alle löschen", "activity_activeFilters_client": "Client", "activity_activeFilters_from": "Von", @@ -88,6 +87,8 @@ "activity_activeFilters_toSeparator": "bis", "activity_activeFilters_until": "Bis", "activity_activeFilters_upgradesOnly": "Nur Upgrades", + "activity_detail_confirmRemoveFromClient": "Vom Client entfernen", + "activity_detail_deleteRecordWarning": "Dieser Aktivitätseintrag wird dauerhaft gelöscht.", "activity_detail_downloadPaused": "Download pausiert", "activity_detail_downloadRemoved": "Download entfernt", "activity_detail_downloadResumed": "Download fortgesetzt", @@ -96,12 +97,13 @@ "activity_detail_failedToRemove": "Entfernen fehlgeschlagen", "activity_detail_failedToResume": "Fortsetzen fehlgeschlagen", "activity_detail_failedToRetry": "Wiederholen fehlgeschlagen", - "activity_detail_importRetryInitiated": "Import-Wiederholung initiiert", "activity_detail_importedTo": "Importiert nach", + "activity_detail_importRetryInitiated": "Import-Wiederholung initiiert", "activity_detail_indexer": "Indexer", "activity_detail_mediaType": "Medientyp", "activity_detail_protocol": "Protokoll", "activity_detail_releaseGroup": "Release-Gruppe", + "activity_detail_removeFromClientWarning": "Dadurch wird der Download dauerhaft vom Download-Client entfernt.", "activity_detail_retryImport": "Import wiederholen", "activity_detail_statusReason": "Status-Grund", "activity_detail_taskType": "Aufgabentyp", @@ -131,7 +133,8 @@ "activity_filters_torrent": "Torrent", "activity_filters_upgradesOnly": "Nur Upgrades", "activity_filters_usenet": "Usenet", - "activity_historyCount": "{total} Verlaufsaktivitäten", + "activity_history_alsoRemoveFromClient": "Auch vom Download-Client entfernen", + "activity_history_alsoRemoveFromClientHint": "{count} fehlgeschlagene(r) Download(s) noch im Client", "activity_history_confirmDeleteSelectedMessage": "{count} ausgewählte Verlaufseinträge löschen? Dies kann nicht rückgängig gemacht werden.", "activity_history_confirmDeleteSelectedTitle": "Ausgewählten Verlauf löschen", "activity_history_confirmPurgeAllMessage": "Gesamten Aktivitätsverlauf dauerhaft löschen? Dies kann nicht rückgängig gemacht werden.", @@ -154,11 +157,12 @@ "activity_history_selectRows": "Zeilen auswählen", "activity_history_skippedActiveDownloads": "{count} aktive Downloads übersprungen", "activity_history_skippedRetryableFailed": "{count} wiederholbare/fehlgeschlagene Elemente übersprungen", + "activity_historyCount": "{total} Verlaufsaktivitäten", "activity_importFailed": "Import fehlgeschlagen", "activity_pageTitle": "Aktivität - Cinephage", - "activity_queueStats_total": "Gesamt", "activity_queue_bulkActionsHeading": "Massenaktionen", "activity_queue_bulkHint": "Wähle Warteschlangenzeilen aus für Massenpause, -fortsetzung, -wiederholung fehlgeschlagener oder -entfernen fehlgeschlagener Downloads.", + "activity_queue_clearAllFailed": "Alle fehlgeschlagenen löschen", "activity_queue_clearSelection": "Auswahl löschen", "activity_queue_confirmPauseMessage": "{count} Warteschlangenelemente pausieren?", "activity_queue_confirmPauseTitle": "Ausgewählte Downloads pausieren", @@ -181,6 +185,7 @@ "activity_queue_selectAllCount": "Alle auswählen ({count})", "activity_queue_selectRows": "Zeilen auswählen", "activity_queue_skippedItems": "({count} nicht berechtigt übersprungen)", + "activity_queueStats_total": "Gesamt", "activity_relativeTime_daysAgo": "{count}d her", "activity_relativeTime_hoursAgo": "{count}h her", "activity_relativeTime_justNow": "Gerade eben", @@ -188,6 +193,7 @@ "activity_resolutionAuto": "Auto", "activity_retryMode_importInProgress": "Import-Wiederholung läuft", "activity_retryMode_waitingForPath": "Warte auf Download-Pfad vor Import-Wiederholung", + "activity_statusCount": "{total} {status}", "activity_subtitle": "Download- und Suchverlauf", "activity_tabActive": "Aktive Downloads", "activity_tabHistory": "Verlauf", @@ -204,26 +210,6 @@ "activity_table_showTimeline": "Timeline anzeigen", "activity_taskError": "{task} Suchfehler", "activity_title": "Aktivität", - "calendar_addToLibrary": "Zur Bibliothek hinzufügen", - "calendar_comingUp": "Demnächst", - "calendar_inLibrary": "In Bibliothek", - "calendar_more": "+{count} weitere", - "calendar_noUpcoming": "Nichts demnächst", - "calendar_today": "Heute", - "calendar_viewAll": "Kalender anzeigen", - "calendar_prefs_show": "Anzeigen", - "calendar_prefs_all": "Alle", - "calendar_prefs_options": "Optionen", - "calendar_prefs_libraryOnly": "Nur Bibliotheksfilme", - "calendar_prefs_upcomingNonLibrary": "Neue Releases in Demnächst anzeigen", - "calendar_prefs_minRating": "Mindestbewertung", - "calendar_prefs_contentRating": "Altersfreigabe", - "calendar_prefs_allRatings": "Alle", - "calendar_prefs_excludeAdult": "Erwachseneninhalt ausblenden", - "calendar_prefs_genres": "Genres", - "calendar_prefs_anyRating": "Alle", - "calendar_prefs_allGenres": "Alle Genres", - "calendar_airingNow": "Läuft jetzt", "allFormatScoresZero": "Alle Format-Punktzahlen beginnen bei 0", "allowUpgrades": "Upgrades erlauben", "aria_deleteClient": "Client löschen", @@ -234,12 +220,58 @@ "auth_blankToKeep": "leer zum Beibehalten", "auth_password_label": "Passwort", "auth_username_label": "Benutzername", + "blockedMedia_alreadyBlocked": "Dieses Medium ist bereits blockiert", + "blockedMedia_blocked": "\"{title}\" wurde blockiert", + "blockedMedia_confirmBlockLabel": "Blockieren", + "blockedMedia_confirmBlockMessage": "\"{title}\" blockieren? Es wird in Entdeckung, Suche und Bibliothek ausgeblendet.", + "blockedMedia_confirmBlockTitle": "Medium blockieren", + "blockedMedia_confirmUnblockLabel": "Entblockieren", + "blockedMedia_confirmUnblockMessage": "\"{title}\" entblockieren? Es wird wieder in Entdeckung und Suche angezeigt, aber nicht erneut zur Bibliothek hinzugefügt.", + "blockedMedia_confirmUnblockTitle": "Medium entblockieren", + "blockedMedia_dateBlocked": "Blockiert am {date}", + "blockedMedia_emptyDescription": "Blockierte Filme und Serien werden hier angezeigt.", + "blockedMedia_emptyTitle": "Keine blockierten Medien", + "blockedMedia_entryCount": "{total} Einträge", + "blockedMedia_filterAll": "Alle", + "blockedMedia_filterMovies": "Filme", + "blockedMedia_filterTV": "Serien", + "blockedMedia_mediaTypeMovie": "Film", + "blockedMedia_mediaTypeTV": "Serie", + "blockedMedia_noReason": "Kein Grund", + "blockedMedia_pageSubtitle": "Blockierte Filme und Serien verwalten, die in Entdeckung, Suche und Bibliothek ausgeblendet sind", + "blockedMedia_pageTitle": "Blockierte Medien", + "blockedMedia_searchPlaceholder": "Blockierte Medien suchen...", + "blockedMedia_unblock": "Entblockieren", + "blockedMedia_unblocked": "\"{title}\" wurde entblockiert", + "blocklist_tab_blockedKeywords": "Blockierte Schlüsselwörter", + "blocklist_tab_blockedMedia": "Blockierte Medien", + "blocklist_tab_releases": "Releases", "builtInProfiles": "Integrierte Profile", "bulkActions_delete": "Löschen", "bulkActions_disable": "Deaktivieren", "bulkActions_enable": "Aktivieren", "bulkActions_selected": "{count} ausgewählt", "bulkActions_test": "Testen", + "calendar_addToLibrary": "Zur Bibliothek hinzufügen", + "calendar_airingNow": "Läuft jetzt", + "calendar_comingUp": "Demnächst", + "calendar_inLibrary": "In Bibliothek", + "calendar_more": "+{count} weitere", + "calendar_noUpcoming": "Nichts demnächst", + "calendar_prefs_all": "Alle", + "calendar_prefs_allGenres": "Alle Genres", + "calendar_prefs_allRatings": "Alle", + "calendar_prefs_anyRating": "Alle", + "calendar_prefs_contentRating": "Altersfreigabe", + "calendar_prefs_excludeAdult": "Erwachseneninhalt ausblenden", + "calendar_prefs_genres": "Genres", + "calendar_prefs_libraryOnly": "Nur Bibliotheksfilme", + "calendar_prefs_minRating": "Mindestbewertung", + "calendar_prefs_options": "Optionen", + "calendar_prefs_show": "Anzeigen", + "calendar_prefs_upcomingNonLibrary": "Neue Releases in Demnächst anzeigen", + "calendar_today": "Heute", + "calendar_viewAll": "Kalender anzeigen", "channels": "Kanäle", "collapseAll": "Alle einklappen", "common_actions": "Aktionen", @@ -249,12 +281,18 @@ "common_all": "Alle", "common_alpha": "Alpha", "common_alphaShort": "A", + "common_announced": "Angekündigt", "common_appName": "Cinephage", "common_available": "Verfügbar", + "common_availableDigital": "Verfügbar - Digital", + "common_availablePhysical": "Verfügbar - Physisch", + "common_availableStreaming": "Verfügbar - Streaming", "common_browseFolders": "Ordner durchsuchen", "common_builtIn": "Integriert", "common_categories": "Kategorien", "common_clientBehavior": "Client-Verhalten", + "common_comingSoon": "Demnächst", + "common_comingToTheaters": "In {days} Tagen im Kino", "common_connecting": "Verbinde...", "common_connection": "Verbindung", "common_copiedToClipboard": "In Zwischenablage kopiert", @@ -264,6 +302,7 @@ "common_deleting": "Löschen...", "common_description": "Beschreibung", "common_details": "Details", + "common_digitalInDays": "Digital in {days} Tagen", "common_disabled": "Deaktiviert", "common_disconnected": "Getrennt", "common_downloaded": "Heruntergeladen", @@ -272,8 +311,8 @@ "common_enabled": "Aktiviert", "common_endOfResults": "Ende der Ergebnisse", "common_episode": "Episode", - "common_episodeSize": "Episodengröße", "common_episodes": "Episoden", + "common_episodeSize": "Episodengröße", "common_failedToCopy": "Kopieren fehlgeschlagen", "common_failedToSave": "Speichern fehlgeschlagen", "common_file": "Datei", @@ -282,6 +321,7 @@ "common_hide": "Ausblenden", "common_host": "Host", "common_inactive": "Inaktiv", + "common_inTheaters": "Im Kino", "common_language": "Sprache", "common_languages": "Sprachen", "common_live": "Live", @@ -290,21 +330,22 @@ "common_monitored": "Überwacht", "common_mountMode": "Altmount / NZBDav (Mount-Modus)", "common_movie": "Film", - "common_movieSize": "Filmgröße", "common_movies": "Filme", + "common_movieSize": "Filmgröße", "common_na": "N/A", "common_name": "Name", "common_network": "Netzwerk", "common_never": "Nie", "common_no": "Nein", - "common_noResults": "Keine Ergebnisse gefunden", "common_none": "Keine", + "common_noResults": "Keine Ergebnisse gefunden", "common_notSet": "Nicht festgelegt", "common_off": "Aus", "common_on": "An", "common_overview": "Übersicht", "common_path": "Pfad", "common_pending": "Ausstehend", + "common_physicalInDays": "Physisch in {days} Tagen", "common_port": "Port", "common_priority": "Priorität", "common_protocol": "Protokoll", @@ -312,6 +353,7 @@ "common_qualityProfile": "Qualitätsprofil", "common_rating": "Bewertung", "common_reconnecting": "Verbinde neu...", + "common_released": "Veröffentlicht", "common_retry": "Wiederholen", "common_rootFolder": "Stammordner", "common_runtime": "Laufzeit", @@ -329,6 +371,7 @@ "common_size": "Größe", "common_standard": "Standard SABnzbd", "common_status": "Status", + "common_streamingInDays": "Streaming in {days} Tagen", "common_syncing": "Synchronisiere", "common_testConnection": "Verbindung testen", "common_testing": "Teste...", @@ -341,17 +384,6 @@ "common_unknown": "Unbekannt", "common_unmonitored": "Nicht überwacht", "common_unreleased": "Noch nicht veröffentlicht", - "common_inTheaters": "Im Kino", - "common_released": "Veröffentlicht", - "common_availableDigital": "Verfügbar - Digital", - "common_availablePhysical": "Verfügbar - Physisch", - "common_availableStreaming": "Verfügbar - Streaming", - "common_comingToTheaters": "In {days} Tagen im Kino", - "common_digitalInDays": "Digital in {days} Tagen", - "common_physicalInDays": "Physisch in {days} Tagen", - "common_streamingInDays": "Streaming in {days} Tagen", - "common_announced": "Angekündigt", - "common_comingSoon": "Demnächst", "common_usenet": "Usenet", "common_viewAll": "Alle anzeigen", "common_year": "Jahr", @@ -407,6 +439,7 @@ "dashboard_stats_filesNeedAttention": "Dateien benötigen Aufmerksamkeit", "dashboard_stats_freeSpace": "{size} frei", "dashboard_stats_ignoredCount": "{count} ignoriert", + "dashboard_stats_inTheatersCount": "{count} im Kino", "dashboard_stats_missingCount": "{count} fehlend", "dashboard_stats_missingEpisodes": "Fehlende Episoden", "dashboard_stats_missingEpisodesShort": "Fehlende Eps", @@ -422,26 +455,25 @@ "dashboard_stats_unairedCount": "{count} nicht ausgestrahlt", "dashboard_stats_unmatched": "Nicht zugeordnet", "dashboard_stats_unreleasedCount": "{count} unveröffentlicht", - "dashboard_stats_inTheatersCount": "{count} im Kino", "dashboard_subtitle": "Willkommen bei Cinephage", "dashboard_title": "Dashboard", "discover_applyFilters": "Filter anwenden", + "discover_certificationTvNote": "Die Altersfreigabe-Filterung gilt nur für Filme.", "discover_clearFilters": "Filter löschen", "discover_clearSearch": "Suche löschen", "discover_closeFilters": "Filter schließen", "discover_failedToLoadMore": "Weitere Elemente konnten nicht geladen werden", - "discover_filterBadgeGenres": "{count} Genres", - "discover_filterBadgeLanguage": "Sprache", - "discover_filterBadgeMovies": "Filme", - "discover_filterBadgeTvShows": "Serien", + "discover_filter_contentRating": "Altersfreigabe", "discover_filter_mediaType": "Medientyp", "discover_filter_minRating": "Min. Bewertung", - "discover_filter_contentRating": "Altersfreigabe", - "discover_certificationTvNote": "Die Altersfreigabe-Filterung gilt nur für Filme.", "discover_filter_originalLanguage": "Originalsprache", "discover_filter_releaseYear": "Erscheinungsjahr", "discover_filter_sortBy": "Sortieren nach", "discover_filter_watchProviders": "Anbieter", + "discover_filterBadgeGenres": "{count} Genres", + "discover_filterBadgeLanguage": "Sprache", + "discover_filterBadgeMovies": "Filme", + "discover_filterBadgeTvShows": "Serien", "discover_filters": "Filter", "discover_filtersButton": "Filter", "discover_heading": "Entdecken", @@ -485,6 +517,22 @@ "discover_tv_seasons": "Staffeln", "discover_tv_similarTitles": "Ähnliche Titel", "discover_tv_topCast": "Top-Besetzung", + "downloadClient_addDownloadClient": "Download-Client hinzufügen", + "downloadClient_apiKeyHelp": "Zu finden unter SABnzbd Config > General > API Key", + "downloadClient_aria2": "aria2", + "downloadClient_deluge": "Deluge", + "downloadClient_desc_nntp": "Direct Usenet server connection for NZB streaming", + "downloadClient_desc_qbittorrent": "Beliebter Open-Source-Torrent-Client", + "downloadClient_editDownloadClient": "Download-Client bearbeiten", + "downloadClient_nzbget": "NZBGet", + "downloadClient_protocol_nntp": "NNTP", + "downloadClient_protocol_torrent": "Torrent", + "downloadClient_protocol_usenet": "Usenet", + "downloadClient_qbittorrent": "qBittorrent", + "downloadClient_rtorrent": "rTorrent", + "downloadClient_sabnzbd": "SABnzbd", + "downloadClient_selectType": "Wähle den Typ des Download-Clients, den du hinzufügen möchtest:", + "downloadClient_transmission": "Transmission", "downloadClientModal_addTitle": "Download-Client hinzufügen", "downloadClientModal_categoriesSection": "Kategorien", "downloadClientModal_clientPath": "Client-Pfad", @@ -502,18 +550,6 @@ "downloadClientModal_portLabel": "Port", "downloadClientModal_recentPriority": "Kürzlich", "downloadClientModal_selectType": "Wähle den Typ des Download-Clients, den du hinzufügen möchtest:", - "downloadClientModal_startAs": "Starten als", - "downloadClientModal_tempFolder": "Temporärer Download-Ordner", - "downloadClientModal_tvCategory": "Serien", - "downloadClientModal_urlBaseDescription": "Pfad-Präfix, der nach Host und Port hinzugefügt wird.", - "downloadClientModal_urlBaseLabel": "URL-Base", - "downloadClientModal_useUrlBase": "URL-Base verwenden", - "downloadClient_addDownloadClient": "Download-Client hinzufügen", - "downloadClient_apiKeyHelp": "Zu finden unter SABnzbd Config > General > API Key", - "downloadClient_aria2": "aria2", - "downloadClient_deluge": "Deluge", - "downloadClient_desc_nntp": "Direct Usenet server connection for NZB streaming", - "downloadClient_desc_qbittorrent": "Beliebter Open-Source-Torrent-Client", "downloadClientModal_stalledDescription": "Blockierte Torrents nach {timeout} Min. automatisch entfernen, wenn Fortschritt unter {threshold}% liegt.", "downloadClientModal_stalledDisabled": "Behandlung blockierter Downloads ist deaktiviert.", "downloadClientModal_stalledDownloads": "Blockierte Downloads", @@ -521,16 +557,12 @@ "downloadClientModal_stalledProgress": "Fortschritt unter (%)", "downloadClientModal_stalledSaved": "Gespeichert", "downloadClientModal_stalledTimeout": "Zeitlimit (Min.)", - "downloadClient_editDownloadClient": "Download-Client bearbeiten", - "downloadClient_nzbget": "NZBGet", - "downloadClient_protocol_nntp": "NNTP", - "downloadClient_protocol_torrent": "Torrent", - "downloadClient_protocol_usenet": "Usenet", - "downloadClient_qbittorrent": "qBittorrent", - "downloadClient_rtorrent": "rTorrent", - "downloadClient_sabnzbd": "SABnzbd", - "downloadClient_selectType": "Wähle den Typ des Download-Clients, den du hinzufügen möchtest:", - "downloadClient_transmission": "Transmission", + "downloadClientModal_startAs": "Starten als", + "downloadClientModal_tempFolder": "Temporärer Download-Ordner", + "downloadClientModal_tvCategory": "Serien", + "downloadClientModal_urlBaseDescription": "Pfad-Präfix, der nach Host und Port hinzugefügt wird.", + "downloadClientModal_urlBaseLabel": "URL-Base", + "downloadClientModal_useUrlBase": "URL-Base verwenden", "episodeMax": "Episode Max (MB)", "episodeMin": "Episode Min (MB)", "error_description": "Cinephage ist auf ein unerwartetes Problem beim Laden dieser Ansicht gestoßen. Du kannst es erneut versuchen, zur Startseite zurückkehren oder die Support-ID teilen, wenn du Hilfe bei der Fehlersuche benötigst.", @@ -543,7 +575,6 @@ "error_supportIdLabel": "Fehler-ID", "error_supportIdUnavailable": "Nicht verfügbar", "expandAll": "Alle erweitern", - "formatScoresCopied": "Format-Punktzahlen werden vom ausgewählten Profil kopiert", "formats": "Format-Punktzahlen", "formats_addCondition": "Bedingung hinzufügen", "formats_allCategories": "Alle Kategorien", @@ -561,10 +592,10 @@ "formats_conditionLogicTitle": "Bedingungslogik:", "formats_conditionNamePlaceholder": "Bedingungsname", "formats_conditionRemoveAria": "Bedingung entfernen", - "formats_conditionTypeLabel": "Typ", - "formats_conditionValueLabel": "Wert", "formats_conditionsDivider": "Bedingungen", "formats_conditionsHeader": "Bedingungen", + "formats_conditionTypeLabel": "Typ", + "formats_conditionValueLabel": "Wert", "formats_createCustomFormat": "Benutzerdefiniertes Format erstellen", "formats_createFormat": "Format erstellen", "formats_custom": "Benutzerdefiniert", @@ -597,7 +628,10 @@ "formats_testNotMatched": "Keine Übereinstimmung", "formats_testPlaceholder": "Film.2024.2160p.BluRay.REMUX.HEVC.TrueHD.Atmos-GROUP", "formats_viewFormat": "Format anzeigen", + "formatScoresCopied": "Format-Punktzahlen werden vom ausgewählten Profil kopiert", "general": "Allgemein", + "hero_blockMedia": "Medium blockieren", + "hero_blockMediaTooltip": "In Entdeckung und Bibliothek blockieren", "indexer_desc_automaticSearch": "Suchen wenn Elemente hinzugefügt werden", "indexer_desc_interactiveSearch": "Manuelle Suchen aus der Benutzeroberfläche", "indexer_desc_rejectDeadTorrents": "Releases mit 0 Seedern überspringen", @@ -612,14 +646,14 @@ "library_add_autoDownloadSubtitlesYes": "Untertitel automatisch herunterladen", "library_add_episodeCount": "{count} Episoden", "library_add_monitor": "Überwachen", - "library_add_monitorNewItems": "Neue Elemente überwachen", - "library_add_monitorSpecials": "Specials überwachen", - "library_add_monitorSpecialsNo": "Specials nicht überwachen", - "library_add_monitorSpecialsYes": "Specials überwachen", "library_add_monitoredDescNo": "Nicht überwacht - neue Episoden werden nicht heruntergeladen", "library_add_monitoredDescYes": "Überwacht - neue Episoden werden heruntergeladen", "library_add_monitoredEps": "Überwachte Episoden", "library_add_monitoringPreview": "Überwachungsvorschau", + "library_add_monitorNewItems": "Neue Elemente überwachen", + "library_add_monitorSpecials": "Specials überwachen", + "library_add_monitorSpecialsNo": "Specials nicht überwachen", + "library_add_monitorSpecialsYes": "Specials überwachen", "library_add_noRootFoldersConfigured": "Keine Stammordner konfiguriert", "library_add_notMonitored": "Nicht überwacht", "library_add_rootFolderDesc": "Dateien werden in diesem Ordner organisiert", @@ -664,11 +698,11 @@ "library_batchMatch_failedToGeneratePreview": "Vorschau konnte nicht generiert werden", "library_batchMatch_failedToMatchFiles": "Dateien konnten nicht abgeglichen werden", "library_batchMatch_filesFailed": "{count} Dateien fehlgeschlagen", + "library_batchMatch_matchedFiles": "{count} Dateien abgeglichen", "library_batchMatch_matchFiles": "Dateien abgleichen", + "library_batchMatch_matchingFiles": "Dateien werden abgeglichen...", "library_batchMatch_matchPreview": "Abgleich-Vorschau", "library_batchMatch_matchPreviewLoading": "Vorschau wird generiert...", - "library_batchMatch_matchedFiles": "{count} Dateien abgeglichen", - "library_batchMatch_matchingFiles": "Dateien werden abgeglichen...", "library_batchMatch_searchFailed": "Suche fehlgeschlagen", "library_batchMatch_searchForMovie": "Nach Film suchen...", "library_batchMatch_searchForTvShow": "Nach TV-Sendung suchen...", @@ -677,6 +711,8 @@ "library_batchMatch_sizeMB": "{size} MB", "library_batchMatch_title": "Dateien massenhaft abgleichen", "library_batchMatch_unknownSize": "Unbekannte Größe", + "library_blockMedia": "Entfernen und blockieren", + "library_blockMediaTooltip": "Aus Bibliothek entfernen und blockieren", "library_bulkActionBar_autoDownloadSubs": "Untertitel automatisch herunterladen", "library_bulkActionBar_autoDownloadTitle": "Automatischen Untertitel-Download aktivieren", "library_bulkActionBar_episodesSelected": "{count} Episoden ausgewählt", @@ -797,10 +833,10 @@ "library_import_createNewHint": "Erstellt Bibliothekseintrag falls erforderlich und importiert in ausgewählten Stammordner.", "library_import_destinationNotNeededForExistingMedia": "Alle Elemente in dieser Medienauswahl sind bestehenden Bibliothekseinträgen zugeordnet; eine Zielauswahl ist nicht erforderlich.", "library_import_destinationRootFolder": "Ziel-Stammordner", - "library_import_detectMedia": "Medien erkennen", "library_import_detectedItems": "Erkannte Elemente", "library_import_detectedItemsHint": "Überprüfen und wählen Sie Elemente aus dieser Warteschlange für den Import.", "library_import_detecting": "Erkenne", + "library_import_detectMedia": "Medien erkennen", "library_import_directImportFor": "Dies ist ein direkter Import für", "library_import_episodeCount": "{count} Episoden", "library_import_episodeCountSingular": "{count} Episode", @@ -822,17 +858,17 @@ "library_import_heading": "Medien importieren", "library_import_importAnother": "Weiteren importieren", "library_import_importComplete": "Import abgeschlossen", + "library_import_importedCount": "{count} importiert", + "library_import_importedPathLabel": "Importierter Pfad:", "library_import_importFailed": "Import fehlgeschlagen", + "library_import_importing": "Importiere...", + "library_import_importingIntoExisting": "Importiere in bestehenden Bibliothekseintrag für:", "library_import_importNextDetected": "Nächstes erkanntes Element importieren ({count})", "library_import_importOpenedFor": "Dieser Import wurde geöffnet für", "library_import_importSelectionHeading": "Importauswahl", "library_import_importSelectionHint": "Importaktionen werden ab diesem Schritt ausgeführt. Finalisieren Sie Ihre ausgewählten Elemente unten und starten Sie den Import.", "library_import_importTargetHeading": "Importziel", "library_import_importTargetHint": "Wählen Sie, ob Sie in einen bestehenden Bibliothekseintrag importieren oder einen neuen erstellen möchten.", - "library_import_importedCount": "{count} importiert", - "library_import_importedPathLabel": "Importierter Pfad:", - "library_import_importing": "Importiere...", - "library_import_importingIntoExisting": "Importiere in bestehenden Bibliothekseintrag für:", "library_import_inLibrary": "In Bibliothek", "library_import_itemSkipped": "Dieses Element wird übersprungen und vom Bulk-Import ausgeschlossen.", "library_import_loadingFolders": "Ordner laden...", @@ -870,19 +906,20 @@ "library_import_seasonOverrideHint": "Wird nur auf Dateien angewendet, denen eine erkannte Staffel fehlt.", "library_import_seasonOverrideLabel": "Staffel-Überschreibung (optional)", "library_import_seasonOverridePlaceholder": "Verwenden, wenn Dateinamen keine Staffelnummern enthalten", - "library_import_seasonUnmapped": "Staffel nicht zugeordnet", "library_import_seasonsDetectedInline": "Staffeln: {seasons}", "library_import_seasonsLabel": "Staffeln: {seasons}", - "library_import_selectItem": "Element auswählen", - "library_import_selectRootFolder": "Stammordner auswählen", - "library_import_selectSeason": "Staffel auswählen", + "library_import_seasonUnmapped": "Staffel nicht zugeordnet", "library_import_selectedCount": "{count} ausgewählt", "library_import_selectedItem": "Ausgewähltes Element", "library_import_selectedItemsHeading": "Ausgewählte Elemente", + "library_import_selectItem": "Element auswählen", + "library_import_selectRootFolder": "Stammordner auswählen", + "library_import_selectSeason": "Staffel auswählen", + "library_import_showRootFolders": "Stammordner anzeigen", "library_import_skipItem": "Element überspringen", - "library_import_skipSeason": "Staffel überspringen", "library_import_skippedCount": "{count} übersprungen", "library_import_skippedOfTotal": "{skipped} von {total} übersprungen", + "library_import_skipSeason": "Staffel überspringen", "library_import_source": "Quelle", "library_import_sourcePathHintFile": "Wählen Sie die Mediendatei für diesen Film zum Importieren aus.", "library_import_sourcePathHintGeneral": "Wählen Sie den Pfad zu Ihrer Mediendatei oder Ihrem Ordner aus.", @@ -908,7 +945,6 @@ "library_import_tvShowLabel": "TV-Sendung", "library_import_unmatchedTvSeries": "Nicht zugeordnete TV-Serie", "library_import_useFolder": "Ordner verwenden", - "library_import_showRootFolders": "Stammordner anzeigen", "library_import_viewInLibrary": "In Bibliothek anzeigen", "library_import_viewLastImported": "Zuletzt importiert anzeigen", "library_libraryMediaTable_addedColumn": "Hinzugefügt", @@ -939,9 +975,9 @@ "library_matchFile_clickToMatch": "Zum Abgleichen klicken", "library_matchFile_errorMatching": "Fehler beim Abgleichen der Datei", "library_matchFile_failedToMatch": "Datei konnte nicht abgeglichen werden", - "library_matchFile_matchTo": "Abgleichen mit {title}", "library_matchFile_matchedTo": "Abgeglichen mit {title}", "library_matchFile_matchedToEpisode": "Abgeglichen mit {title} - {episode}", + "library_matchFile_matchTo": "Abgleichen mit {title}", "library_matchFile_searchFailed": "Suche fehlgeschlagen", "library_matchFile_searchHint": "TMDB durchsuchen, um eine Übereinstimmung zu finden", "library_matchFile_searchPlaceholder": "TMDB durchsuchen...", @@ -954,8 +990,8 @@ "library_matchFolder_fileCount_other": "{count} Dateien", "library_matchFolder_filesFailed": "{count} Dateien fehlgeschlagen", "library_matchFolder_filesWillBeMatched": "{count} Dateien werden abgeglichen", - "library_matchFolder_matchFiles": "Dateien abgleichen", "library_matchFolder_matchedFiles": "{count} Dateien abgeglichen", + "library_matchFolder_matchFiles": "Dateien abgleichen", "library_matchFolder_searchFailed": "Suche fehlgeschlagen", "library_matchFolder_searchHint": "TMDB durchsuchen, um eine Übereinstimmung zu finden", "library_matchFolder_searchPlaceholder": "TMDB durchsuchen...", @@ -1018,8 +1054,8 @@ "library_movieDetail_monitoringEnabled": "Überwachung aktiviert.", "library_movieDetail_moveQueued": "Verschiebung in Warteschlange. Dateiübertragung hat begonnen und wird unter Aktivität angezeigt bis zum Abschluss.", "library_movieDetail_originalTitle": "Originaltitel", - "library_movieDetail_overviewHeading": "Übersicht", "library_movieDetail_otherMoviesInCollection": "Weitere Filme in dieser Collection", + "library_movieDetail_overviewHeading": "Übersicht", "library_movieDetail_pageTitle": "{title} - Bibliothek - Cinephage", "library_movieDetail_path": "Pfad", "library_movieDetail_rename": "Umbenennen", @@ -1084,6 +1120,7 @@ "library_movies_noFilterMatchHint": "Versuche deine Filter anzupassen, um mehr Ergebnisse zu sehen.", "library_movies_noSearchMatch": "Keine Filme passen zu \"{query}\"", "library_movies_ofTotal": "von {total}", + "library_movies_other": "Sonstige", "library_movies_pageTitle": "Filme - Bibliothek - Cinephage", "library_movies_profileDefault": "{name} (Standard)", "library_movies_searchPlaceholder": "Filme suchen...", @@ -1092,10 +1129,10 @@ "library_movies_selectAllShort": "Alle", "library_movies_sortAddedAsc": "Hinzugefügt (Älteste)", "library_movies_sortAddedDesc": "Hinzugefügt (Neueste)", - "library_movies_sortSizeAsc": "Größe (Kleinste)", - "library_movies_sortSizeDesc": "Größe (Größte)", "library_movies_sortCollectionAsc": "Sammlung (A-Z)", "library_movies_sortCollectionDesc": "Sammlung (Z-A)", + "library_movies_sortSizeAsc": "Größe (Kleinste)", + "library_movies_sortSizeDesc": "Größe (Größte)", "library_movies_sortTitleAsc": "Titel (A-Z)", "library_movies_sortTitleDesc": "Titel (Z-A)", "library_movies_sortYearAsc": "Jahr (Älteste)", @@ -1103,9 +1140,8 @@ "library_movies_switchToGrid": "Zur Rasteransicht wechseln", "library_movies_switchToList": "Zur Listenansicht wechseln", "library_movies_tryDifferentSearch": "Versuche einen anderen Suchbegriff.", - "library_movies_viewGrouped": "Gruppiert", - "library_movies_other": "Sonstige", "library_movies_unmonitorAll": "Alle nicht überwachen", + "library_movies_viewGrouped": "Gruppiert", "library_renamePreview_allCorrect": "Alle Dateinamen sind korrekt", "library_renamePreview_change": "Ändern", "library_renamePreview_collisionBadge": "Kollision", @@ -1173,9 +1209,9 @@ "library_scoreDetail_streamingServiceLabel": "Streaming-Dienst", "library_scoreDetail_title": "Bewertungsdetails", "library_scoreDetail_total": "Gesamt", - "library_scoreDetail_upgradeStatus": "Upgrade-Status", "library_scoreDetail_upgradesDisabled": "Upgrades deaktiviert", "library_scoreDetail_upgradesEnabled": "Upgrades aktiviert", + "library_scoreDetail_upgradeStatus": "Upgrade-Status", "library_seasonAccordion_actionsColumn": "Aktionen", "library_seasonAccordion_airDateColumn": "Ausstrahlungsdatum", "library_seasonAccordion_autoDownloadSubs": "Untertitel automatisch herunterladen", @@ -1218,9 +1254,9 @@ "library_seriesEdit_standardDesc": "Standard-TV-Serien mit Staffel-/Episodennummerierung", "library_seriesEdit_title": "Serie bearbeiten", "library_seriesHeader_allMissing": "Alles fehlt", - "library_seriesHeader_autoGrab": "Auto-Grab", "library_seriesHeader_autoDownloadSubs": "Untertitel automatisch herunterladen", "library_seriesHeader_autoDownloadSubsTooltip": "Untertitel für alle fehlenden Episoden herunterladen", + "library_seriesHeader_autoGrab": "Auto-Grab", "library_seriesHeader_autoGrabTooltip": "Nach fehlenden Episoden suchen und laden ({count})", "library_seriesHeader_complete": "Vollständig", "library_seriesHeader_grabbedCount": "{count} geladen", @@ -1243,32 +1279,6 @@ "library_seriesType_dailyLabel": "Täglich", "library_seriesType_standardDesc": "Standard-TV-Serien mit Staffel-/Episodennummerierung", "library_seriesType_standardLabel": "Standard", - "library_tvDetail_deleteEpisodeTitle": "Episode löschen", - "library_tvDetail_deleteSeasonTitle": "Staffel löschen", - "library_tvDetail_deleteSeriesTitle": "Serie löschen", - "library_tvDetail_done": "Fertig", - "library_tvDetail_episodeFallback": "Episode {number}", - "library_tvDetail_exitSelectionMode": "Auswahlmodus verlassen", - "library_tvDetail_metaDescription": "{title} in Ihrer Bibliothek verwalten", - "library_tvDetail_monitoringDisabled": "Überwachung ist deaktiviert.", - "library_tvDetail_monitoringDisabledHint": "Staffel- und Episoden-Schalter sind gesperrt. Aktivieren Sie die Serienüberwachung, um sie zu entsperren.", - "library_tvDetail_monitoringEnabled": "Serienüberwachung ist aktiviert.", - "library_tvDetail_noSeasonsFound": "Keine Staffeln gefunden", - "library_tvDetail_overviewHeading": "Übersicht", - "library_tvDetail_pageTitle": "{title} - Bibliothek - Cinephage", - "library_tvDetail_rename": "Umbenennen", - "library_tvDetail_renameFilesTitle": "Dateien umbenennen", - "library_tvDetail_seasonFallback": "Staffel {number}", - "library_tvDetail_seasonFolders": "Staffelordner", - "library_tvDetail_seasonsHeading": "Staffeln", - "library_tvDetail_select": "Auswählen", - "library_tvDetail_selectEpisodes": "Episoden auswählen", - "library_tvDetail_sseConnecting": "Verbinde", - "library_tvDetail_sseLive": "Live", - "library_tvDetail_sseOffline": "Offline", - "library_tvDetail_sseReconnecting": "Verbinde neu", - "library_tvDetail_syncSubtitles": "Untertitel synchronisieren", - "library_tvDetail_tvdbId": "TVDB-ID", "library_tv_clearFilters": "Filter löschen", "library_tv_clearSearch": "Suche löschen", "library_tv_clearSearchBtn": "Suche löschen", @@ -1326,6 +1336,32 @@ "library_tv_switchToList": "Zur Listenansicht wechseln", "library_tv_tryDifferentSearch": "Versuche einen anderen Suchbegriff.", "library_tv_unmonitorAll": "Alle nicht überwachen", + "library_tvDetail_deleteEpisodeTitle": "Episode löschen", + "library_tvDetail_deleteSeasonTitle": "Staffel löschen", + "library_tvDetail_deleteSeriesTitle": "Serie löschen", + "library_tvDetail_done": "Fertig", + "library_tvDetail_episodeFallback": "Episode {number}", + "library_tvDetail_exitSelectionMode": "Auswahlmodus verlassen", + "library_tvDetail_metaDescription": "{title} in Ihrer Bibliothek verwalten", + "library_tvDetail_monitoringDisabled": "Überwachung ist deaktiviert.", + "library_tvDetail_monitoringDisabledHint": "Staffel- und Episoden-Schalter sind gesperrt. Aktivieren Sie die Serienüberwachung, um sie zu entsperren.", + "library_tvDetail_monitoringEnabled": "Serienüberwachung ist aktiviert.", + "library_tvDetail_noSeasonsFound": "Keine Staffeln gefunden", + "library_tvDetail_overviewHeading": "Übersicht", + "library_tvDetail_pageTitle": "{title} - Bibliothek - Cinephage", + "library_tvDetail_rename": "Umbenennen", + "library_tvDetail_renameFilesTitle": "Dateien umbenennen", + "library_tvDetail_seasonFallback": "Staffel {number}", + "library_tvDetail_seasonFolders": "Staffelordner", + "library_tvDetail_seasonsHeading": "Staffeln", + "library_tvDetail_select": "Auswählen", + "library_tvDetail_selectEpisodes": "Episoden auswählen", + "library_tvDetail_sseConnecting": "Verbinde", + "library_tvDetail_sseLive": "Live", + "library_tvDetail_sseOffline": "Offline", + "library_tvDetail_sseReconnecting": "Verbinde neu", + "library_tvDetail_syncSubtitles": "Untertitel synchronisieren", + "library_tvDetail_tvdbId": "TVDB-ID", "library_unmatched_collapseAll": "Alle einklappen", "library_unmatched_deleteFileTitle": "Datei löschen", "library_unmatched_expandAll": "Alle erweitern", @@ -1351,6 +1387,32 @@ "livetv_accountModal_streamVerified": "Stream verifiziert", "livetv_accountModal_testSuccess": "Verbindung erfolgreich", "livetv_accountModal_testSuccessWithStatus": "Verbindung erfolgreich • {status}", + "livetv_accounts_addAccount": "Konto hinzufügen", + "livetv_accounts_connectionTestFailed": "Verbindungstest fehlgeschlagen: {name}", + "livetv_accounts_connectionTestPassed": "Verbindungstest bestanden: {name}", + "livetv_accounts_deleteAccountMessage": "\"{name}\" löschen? Dies wird es aus Live TV entfernen und die Kanal-Sync stoppen.", + "livetv_accounts_deleteAccountMessagePrefix": "Are you sure you want to delete ", + "livetv_accounts_deleteAccountMessageSuffix": "? This will remove it from Live TV and stop channel sync.", + "livetv_accounts_deleteAccountTitle": "Konto löschen", + "livetv_accounts_epgNotConfigured": "EPG nicht konfiguriert", + "livetv_accounts_epgReachable": "EPG erreichbar", + "livetv_accounts_epgUnreachable": "EPG nicht erreichbar", + "livetv_accounts_epgUnreachableWithError": "EPG nicht erreichbar ({error})", + "livetv_accounts_failedToDeleteAccount": "Konto konnte nicht gelöscht werden", + "livetv_accounts_failedToLoadAccounts": "Konten konnten nicht geladen werden", + "livetv_accounts_failedToSaveAccount": "Konto konnte nicht gespeichert werden", + "livetv_accounts_failedToSyncAccount": "Konto konnte nicht synchronisiert werden", + "livetv_accounts_failedToTestAccount": "Konto konnte nicht getestet werden", + "livetv_accounts_failedToTestConfig": "Kontokonfiguration konnte nicht getestet werden", + "livetv_accounts_failedToUpdateAccount": "Konto konnte nicht aktualisiert werden", + "livetv_accounts_heading": "Live TV Konten", + "livetv_accounts_invalidTestEndpointResponse": "Ungültige Antwort vom Test-Endpunkt", + "livetv_accounts_invalidTestResponse": "Ungültige Testantwort", + "livetv_accounts_pageTitle": "Live TV Konten - Cinephage", + "livetv_accounts_subtitle": "Verwalte deine IPTV-Konten (Stalker, XStream, M3U)", + "livetv_accounts_testSummaryCategories": "{count} Kategorien", + "livetv_accounts_testSummaryChannels": "{count} Kanäle", + "livetv_accounts_unknownTestFailure": "Unbekannter Testfehler", "livetv_accountTable_accountColumn": "Konto", "livetv_accountTable_actionsColumn": "Aktionen", "livetv_accountTable_addHint": "Fügen Sie ein IPTV-Konto hinzu, um zu beginnen", @@ -1379,36 +1441,10 @@ "livetv_accountTable_syncingStatus": "Synchronisiere...", "livetv_accountTable_typeColumn": "Typ", "livetv_accountTable_urlSource": "URL-Quelle", - "livetv_accounts_addAccount": "Konto hinzufügen", - "livetv_accounts_connectionTestFailed": "Verbindungstest fehlgeschlagen: {name}", - "livetv_accounts_connectionTestPassed": "Verbindungstest bestanden: {name}", - "livetv_accounts_deleteAccountMessage": "\"{name}\" löschen? Dies wird es aus Live TV entfernen und die Kanal-Sync stoppen.", - "livetv_accounts_deleteAccountMessagePrefix": "Are you sure you want to delete ", - "livetv_accounts_deleteAccountMessageSuffix": "? This will remove it from Live TV and stop channel sync.", - "livetv_accounts_deleteAccountTitle": "Konto löschen", - "livetv_accounts_epgNotConfigured": "EPG nicht konfiguriert", - "livetv_accounts_epgReachable": "EPG erreichbar", - "livetv_accounts_epgUnreachable": "EPG nicht erreichbar", - "livetv_accounts_epgUnreachableWithError": "EPG nicht erreichbar ({error})", - "livetv_accounts_failedToDeleteAccount": "Konto konnte nicht gelöscht werden", - "livetv_accounts_failedToLoadAccounts": "Konten konnten nicht geladen werden", - "livetv_accounts_failedToSaveAccount": "Konto konnte nicht gespeichert werden", - "livetv_accounts_failedToSyncAccount": "Konto konnte nicht synchronisiert werden", - "livetv_accounts_failedToTestAccount": "Konto konnte nicht getestet werden", - "livetv_accounts_failedToTestConfig": "Kontokonfiguration konnte nicht getestet werden", - "livetv_accounts_failedToUpdateAccount": "Konto konnte nicht aktualisiert werden", - "livetv_accounts_heading": "Live TV Konten", - "livetv_accounts_invalidTestEndpointResponse": "Ungültige Antwort vom Test-Endpunkt", - "livetv_accounts_invalidTestResponse": "Ungültige Testantwort", - "livetv_accounts_pageTitle": "Live TV Konten - Cinephage", - "livetv_accounts_subtitle": "Verwalte deine IPTV-Konten (Stalker, XStream, M3U)", - "livetv_accounts_testSummaryCategories": "{count} Kategorien", - "livetv_accounts_testSummaryChannels": "{count} Kanäle", - "livetv_accounts_unknownTestFailure": "Unbekannter Testfehler", "livetv_channelBrowserModal_add": "Hinzufügen", + "livetv_channelBrowserModal_added": "Hinzugefügt", "livetv_channelBrowserModal_addEntireCategory": "Kategorie hinzufügen", "livetv_channelBrowserModal_addSelected": "Ausgewählte hinzufügen", - "livetv_channelBrowserModal_added": "Hinzugefügt", "livetv_channelBrowserModal_allAccounts": "Alle Konten", "livetv_channelBrowserModal_allCategories": "Alle Kategorien", "livetv_channelBrowserModal_allChannelsAdded": "Alle Kanäle hinzugefügt", @@ -1550,19 +1586,14 @@ "livetv_channelRemoveModal_removeMultiple": "{count} Kanäle aus Ihrer Aufstellung entfernen?", "livetv_channelRemoveModal_removeSingle": "\"{name}\" aus Ihrer Aufstellung entfernen?", "livetv_channelRemoveModal_title": "Kanal entfernen", - "livetv_channelScheduleModal_epgFrom": "EPG von {source}", - "livetv_channelScheduleModal_failedToLoad": "Zeitplan konnte nicht geladen werden", - "livetv_channelScheduleModal_liveBadge": "LIVE", - "livetv_channelScheduleModal_noPrograms": "Keine Programme geplant", - "livetv_channelScheduleModal_percentRemaining": "Noch {percent}%", "livetv_channels_appliedCleanNames": "Bereinigte Namen auf {count} Kanäle angewendet", "livetv_channels_browseChannels": "Kanäle durchsuchen", "livetv_channels_categoriesButton": "Kategorien", "livetv_channels_copyEpgUrl": "EPG-URL kopieren", "livetv_channels_copyM3uUrl": "M3U-URL kopieren", + "livetv_channels_downloadedLogos": "{count} Logos heruntergeladen", "livetv_channels_downloadFailed": "Download fehlgeschlagen", "livetv_channels_downloadLogosTitle": "Kanal-Logos herunterladen", - "livetv_channels_downloadedLogos": "{count} Logos heruntergeladen", "livetv_channels_emptyDescription": "Füge Kanäle von deinen Konten hinzu, um deine Aufstellung zu erstellen", "livetv_channels_emptyHeading": "Keine Kanäle in deiner Aufstellung", "livetv_channels_exportButton": "Exportieren", @@ -1597,6 +1628,19 @@ "livetv_channels_subtitle": "Organisiere deine Kanalaufstellung", "livetv_channels_updatedCategory": "Kategorie für {count} Kanäle aktualisiert", "livetv_channels_xmltvEpgGuide": "XMLTV EPG-Guide", + "livetv_channelScheduleModal_epgFrom": "EPG von {source}", + "livetv_channelScheduleModal_failedToLoad": "Zeitplan konnte nicht geladen werden", + "livetv_channelScheduleModal_liveBadge": "LIVE", + "livetv_channelScheduleModal_noPrograms": "Keine Programme geplant", + "livetv_channelScheduleModal_percentRemaining": "Noch {percent}%", + "livetv_epg_failedToCancelSync": "EPG-Sync konnte nicht abgebrochen werden", + "livetv_epg_failedToTriggerSync": "EPG-Sync konnte nicht ausgelöst werden", + "livetv_epg_heading": "EPG", + "livetv_epg_pageTitle": "EPG - Live TV - Cinephage", + "livetv_epg_subtitle": "Elektronischer Programmführer", + "livetv_epg_tabCoverage": "Abdeckung", + "livetv_epg_tabGuide": "Guide", + "livetv_epg_tabStatus": "Status", "livetv_epgCoverage_currentProgram": "Aktuelles Programm", "livetv_epgCoverage_default": "Standard", "livetv_epgCoverage_doubleClickToChange": "Doppelklicken, um EPG-Quelle zu ändern", @@ -1641,29 +1685,6 @@ "livetv_epgSourcePicker_programsCount": "{count} Programme", "livetv_epgSourcePicker_searchPlaceholder": "Kanäle durchsuchen...", "livetv_epgSourcePicker_title": "EPG-Quelle auswählen", - "livetv_epgStatusPanel_accountsHeading": "Konten", - "livetv_epgStatusPanel_cancelAccountTooltip": "Synchronisation für dieses Konto abbrechen", - "livetv_epgStatusPanel_cancelRequestedTooltip": "Abbruch angefordert", - "livetv_epgStatusPanel_cancelSync": "Synchronisation abbrechen", - "livetv_epgStatusPanel_cancelling": "Breche ab...", - "livetv_epgStatusPanel_errorTitle": "Fehler", - "livetv_epgStatusPanel_errorTitlePlural": "Fehler", - "livetv_epgStatusPanel_lastSync": "Letzte Synchronisation", - "livetv_epgStatusPanel_moreAccountIssues": "+{count} weitere Probleme", - "livetv_epgStatusPanel_moreAccountWarnings": "+{count} weitere Warnungen", - "livetv_epgStatusPanel_nextSync": "Nächste Synchronisation", - "livetv_epgStatusPanel_noAccounts": "Keine Konten konfiguriert", - "livetv_epgStatusPanel_noEpgDataMessage": "Keine EPG-Daten verfügbar. Konfigurieren Sie Konten mit EPG-Quellen.", - "livetv_epgStatusPanel_syncAccountTooltip": "Dieses Konto synchronisieren", - "livetv_epgStatusPanel_syncAll": "Alle synchronisieren", - "livetv_epgStatusPanel_syncInProgress": "Synchronisation läuft...", - "livetv_epgStatusPanel_syncRunningTooltip": "Synchronisation läuft", - "livetv_epgStatusPanel_syncedRelative": "Vor {time} synchronisiert", - "livetv_epgStatusPanel_syncingAll": "Alle Konten werden synchronisiert...", - "livetv_epgStatusPanel_totalPrograms": "{count} Programme", - "livetv_epgStatusPanel_unknownError": "Unbekannter Fehler", - "livetv_epgStatusPanel_warningTitle": "Warnung", - "livetv_epgStatusPanel_warningTitlePlural": "Warnungen", "livetv_epgStatus_accountDetails": "Kontodetails", "livetv_epgStatus_daysAgo": "Vor {count} Tg", "livetv_epgStatus_errorBadge": "Fehler", @@ -1682,18 +1703,33 @@ "livetv_epgStatus_noEpgTooltip": "EPG für dieses Konto nicht konfiguriert", "livetv_epgStatus_programsLabel": "Programme", "livetv_epgStatus_syncButton": "Jetzt synchronisieren", - "livetv_epgStatus_syncTooltip": "EPG-Daten von allen Quellen synchronisieren", "livetv_epgStatus_syncing": "Synchronisiere...", + "livetv_epgStatus_syncTooltip": "EPG-Daten von allen Quellen synchronisieren", "livetv_epgStatus_title": "EPG-Status", "livetv_epgStatus_unableToLoad": "EPG-Status konnte nicht geladen werden", - "livetv_epg_failedToCancelSync": "EPG-Sync konnte nicht abgebrochen werden", - "livetv_epg_failedToTriggerSync": "EPG-Sync konnte nicht ausgelöst werden", - "livetv_epg_heading": "EPG", - "livetv_epg_pageTitle": "EPG - Live TV - Cinephage", - "livetv_epg_subtitle": "Elektronischer Programmführer", - "livetv_epg_tabCoverage": "Abdeckung", - "livetv_epg_tabGuide": "Guide", - "livetv_epg_tabStatus": "Status", + "livetv_epgStatusPanel_accountsHeading": "Konten", + "livetv_epgStatusPanel_cancelAccountTooltip": "Synchronisation für dieses Konto abbrechen", + "livetv_epgStatusPanel_cancelling": "Breche ab...", + "livetv_epgStatusPanel_cancelRequestedTooltip": "Abbruch angefordert", + "livetv_epgStatusPanel_cancelSync": "Synchronisation abbrechen", + "livetv_epgStatusPanel_errorTitle": "Fehler", + "livetv_epgStatusPanel_errorTitlePlural": "Fehler", + "livetv_epgStatusPanel_lastSync": "Letzte Synchronisation", + "livetv_epgStatusPanel_moreAccountIssues": "+{count} weitere Probleme", + "livetv_epgStatusPanel_moreAccountWarnings": "+{count} weitere Warnungen", + "livetv_epgStatusPanel_nextSync": "Nächste Synchronisation", + "livetv_epgStatusPanel_noAccounts": "Keine Konten konfiguriert", + "livetv_epgStatusPanel_noEpgDataMessage": "Keine EPG-Daten verfügbar. Konfigurieren Sie Konten mit EPG-Quellen.", + "livetv_epgStatusPanel_syncAccountTooltip": "Dieses Konto synchronisieren", + "livetv_epgStatusPanel_syncAll": "Alle synchronisieren", + "livetv_epgStatusPanel_syncedRelative": "Vor {time} synchronisiert", + "livetv_epgStatusPanel_syncingAll": "Alle Konten werden synchronisiert...", + "livetv_epgStatusPanel_syncInProgress": "Synchronisation läuft...", + "livetv_epgStatusPanel_syncRunningTooltip": "Synchronisation läuft", + "livetv_epgStatusPanel_totalPrograms": "{count} Programme", + "livetv_epgStatusPanel_unknownError": "Unbekannter Fehler", + "livetv_epgStatusPanel_warningTitle": "Warnung", + "livetv_epgStatusPanel_warningTitlePlural": "Warnungen", "livetv_form_m3u_autoRefresh": "Automatisch aktualisieren", "livetv_form_m3u_enabled": "Aktiviert", "livetv_form_m3u_epgUrlHint": "URL für Electronic Program Guide-Daten (optional)", @@ -1752,9 +1788,9 @@ "livetv_iptvOrg_loadingCountries": "Länder werden geladen...", "livetv_iptvOrg_removeCountry": "{country} entfernen", "livetv_iptvOrg_searchPlaceholder": "Länder durchsuchen...", - "livetv_iptvOrg_selectHint": "Wählen Sie Länder aus, um Kanäle hinzuzufügen", "livetv_iptvOrg_selectedCount": "{count} Länder ausgewählt", "livetv_iptvOrg_selectedCountSingular": "1 Land ausgewählt", + "livetv_iptvOrg_selectHint": "Wählen Sie Länder aus, um Kanäle hinzuzufügen", "livetv_portalScanModal_addNewPortal": "Neues Portal hinzufügen", "livetv_portalScanModal_backButton": "Zurück", "livetv_portalScanModal_cancelButton": "Abbrechen", @@ -1788,8 +1824,8 @@ "livetv_portalScanModal_portalUrlPlaceholder": "https://portal.example.com", "livetv_portalScanModal_randomScan": "Zufälliger Scan", "livetv_portalScanModal_rangeSize": "Bereichsgröße", - "livetv_portalScanModal_scanTypeLabel": "Scan-Typ", "livetv_portalScanModal_scanningPortalLabel": "Scanne Portal", + "livetv_portalScanModal_scanTypeLabel": "Scan-Typ", "livetv_portalScanModal_selectExistingPortal": "Vorhandenes Portal auswählen", "livetv_portalScanModal_sequentialScan": "Sequentieller Scan", "livetv_portalScanModal_startMacLabel": "Start-MAC", @@ -1812,8 +1848,8 @@ "livetv_portalScanProgress_scanCancelled": "Scan abgebrochen", "livetv_portalScanProgress_scanComplete": "Scan abgeschlossen", "livetv_portalScanProgress_scanFailed": "Scan fehlgeschlagen", - "livetv_portalScanProgress_scanType": "Scan-Typ", "livetv_portalScanProgress_scanningPortal": "Scanne {name}", + "livetv_portalScanProgress_scanType": "Scan-Typ", "livetv_portalScanProgress_tested": "Getestet", "livetv_portalScanProgress_timeRemaining": "Verbleibende Zeit", "livetv_portalScanProgress_total": "Gesamt", @@ -1827,9 +1863,9 @@ "livetv_scanResults_accountCol": "Konto", "livetv_scanResults_actionsCol": "Aktionen", "livetv_scanResults_allTab": "Alle", + "livetv_scanResults_approvedTab": "Genehmigt", "livetv_scanResults_approveSelected": "Ausgewählte genehmigen", "livetv_scanResults_approveTitle": "Ergebnisse genehmigen", - "livetv_scanResults_approvedTab": "Genehmigt", "livetv_scanResults_categoriesCount": "{count} Kategorien", "livetv_scanResults_channelsCol": "Kanäle", "livetv_scanResults_channelsCount": "{count} Kanäle", @@ -1844,9 +1880,9 @@ "livetv_scanResults_failedToClear": "Ignorierte Ergebnisse konnten nicht gelöscht werden", "livetv_scanResults_failedToIgnore": "Ignorieren fehlgeschlagen", "livetv_scanResults_failedToLoadResults": "Scan-Ergebnisse konnten nicht geladen werden", + "livetv_scanResults_ignoredTab": "Ignoriert", "livetv_scanResults_ignoreSelected": "Ausgewählte ignorieren", "livetv_scanResults_ignoreTitle": "Ergebnisse ignorieren", - "livetv_scanResults_ignoredTab": "Ignoriert", "livetv_scanResults_macAddressCol": "MAC-Adresse", "livetv_scanResults_noApproved": "Keine genehmigten Ergebnisse", "livetv_scanResults_noIgnored": "Keine ignorierten Ergebnisse", @@ -1874,31 +1910,6 @@ "login_usernameLabel": "Benutzername", "login_usernamePlaceholder": "Gib deinen Benutzernamen ein", "login_welcomeBack": "Willkommen zurück", - "mediaBrowserModal_addTitle": "Medienserver hinzufügen", - "mediaBrowserModal_apiKeyHint": "Zu finden unter Dashboard > API-Keys", - "mediaBrowserModal_editTitle": "Medienserver bearbeiten", - "mediaBrowserModal_embyDescription": "Persönlicher Medienserver", - "mediaBrowserModal_failedToSave": "Speichern fehlgeschlagen", - "mediaBrowserModal_hostHint": "http:// oder https:// einschließen", - "mediaBrowserModal_jellyfinDescription": "Kostenloser und Open-Source-Medienserver", - "mediaBrowserModal_localPathPlaceholder": "Lokaler Pfad (z.B. /media)", - "mediaBrowserModal_mediaServerBadge": "Medienserver", - "mediaBrowserModal_notificationEvents": "Benachrichtigungsereignisse", - "mediaBrowserModal_onDelete": "Bei Löschung", - "mediaBrowserModal_onDeleteDesc": "Benachrichtigen, wenn Medien gelöscht werden", - "mediaBrowserModal_onImport": "Bei Import", - "mediaBrowserModal_onImportDesc": "Benachrichtigen, wenn neue Medien importiert werden", - "mediaBrowserModal_onRename": "Bei Umbenennung", - "mediaBrowserModal_onRenameDesc": "Benachrichtigen, wenn Mediendateien umbenannt werden", - "mediaBrowserModal_onUpgrade": "Bei Upgrade", - "mediaBrowserModal_onUpgradeDesc": "Benachrichtigen, wenn Medien auf bessere Qualität aktualisiert werden", - "mediaBrowserModal_pathMappings": "Pfad-Zuordnungen", - "mediaBrowserModal_pathMappingsHint": "Ordne lokale Pfade zu Remote-Pfaden zu, wenn dein Medienserver in Docker oder auf einem anderen Computer läuft.", - "mediaBrowserModal_plexApiKeyHint": "Zu finden in Plex-Konto oder Server-Token-Einstellungen", - "mediaBrowserModal_plexDescription": "Persönlicher Medienserver", - "mediaBrowserModal_remotePathPlaceholder": "Remote-Pfad (z.B. /data/media)", - "mediaBrowserModal_selectType": "Wähle den Typ des Medienservers, den du hinzufügen möchtest:", - "mediaBrowserModal_to": "zu", "mediaBrowser_actions": "Aktionen", "mediaBrowser_addMapping": "Zuordnung hinzufügen", "mediaBrowser_addServer": "Medienserver hinzufügen", @@ -1946,6 +1957,31 @@ "mediaBrowser_serverType_plex": "Plex", "mediaBrowser_serverType_plexDesc": "Persönlicher Medienserver", "mediaBrowser_to": "zu", + "mediaBrowserModal_addTitle": "Medienserver hinzufügen", + "mediaBrowserModal_apiKeyHint": "Zu finden unter Dashboard > API-Keys", + "mediaBrowserModal_editTitle": "Medienserver bearbeiten", + "mediaBrowserModal_embyDescription": "Persönlicher Medienserver", + "mediaBrowserModal_failedToSave": "Speichern fehlgeschlagen", + "mediaBrowserModal_hostHint": "http:// oder https:// einschließen", + "mediaBrowserModal_jellyfinDescription": "Kostenloser und Open-Source-Medienserver", + "mediaBrowserModal_localPathPlaceholder": "Lokaler Pfad (z.B. /media)", + "mediaBrowserModal_mediaServerBadge": "Medienserver", + "mediaBrowserModal_notificationEvents": "Benachrichtigungsereignisse", + "mediaBrowserModal_onDelete": "Bei Löschung", + "mediaBrowserModal_onDeleteDesc": "Benachrichtigen, wenn Medien gelöscht werden", + "mediaBrowserModal_onImport": "Bei Import", + "mediaBrowserModal_onImportDesc": "Benachrichtigen, wenn neue Medien importiert werden", + "mediaBrowserModal_onRename": "Bei Umbenennung", + "mediaBrowserModal_onRenameDesc": "Benachrichtigen, wenn Mediendateien umbenannt werden", + "mediaBrowserModal_onUpgrade": "Bei Upgrade", + "mediaBrowserModal_onUpgradeDesc": "Benachrichtigen, wenn Medien auf bessere Qualität aktualisiert werden", + "mediaBrowserModal_pathMappings": "Pfad-Zuordnungen", + "mediaBrowserModal_pathMappingsHint": "Ordne lokale Pfade zu Remote-Pfaden zu, wenn dein Medienserver in Docker oder auf einem anderen Computer läuft.", + "mediaBrowserModal_plexApiKeyHint": "Zu finden in Plex-Konto oder Server-Token-Einstellungen", + "mediaBrowserModal_plexDescription": "Persönlicher Medienserver", + "mediaBrowserModal_remotePathPlaceholder": "Remote-Pfad (z.B. /data/media)", + "mediaBrowserModal_selectType": "Wähle den Typ des Medienservers, den du hinzufügen möchtest:", + "mediaBrowserModal_to": "zu", "movieMax": "Film Max (GB)", "movieMin": "Film Min (GB)", "naming_preview": "Vorschau", @@ -1989,7 +2025,7 @@ "nav_downloadClients": "Download-Clients", "nav_epg": "EPG", "nav_general": "Allgemein", - "nav_globalFilters": "Globale Filter", + "nav_globalFilters": "Entdeckungsfilter", "nav_home": "Startseite", "nav_import": "Importieren", "nav_indexers": "Indexer", @@ -2002,6 +2038,7 @@ "nav_mediaServers": "Medienserver", "nav_mediaStorage": "Medienspeicher", "nav_metadataProviders": "Metadatenanbieter", + "nav_monitoring": "Überwachung", "nav_movies": "Filme", "nav_naming": "Benennung", "nav_nntpServers": "NNTP-Server", @@ -2017,23 +2054,6 @@ "nav_tasks": "Aufgaben", "nav_tvShows": "Serien", "nav_unmatchedFiles": "Nicht zugeordnete Dateien", - "nntpServerModal_addTitle": "Usenet-Server hinzufügen", - "nntpServerModal_editTitle": "Usenet-Server bearbeiten", - "nntpServerModal_failedToSave": "Speichern fehlgeschlagen", - "nntpServerModal_maxConnections": "Max. Verbindungen", - "nntpServerModal_maxConnectionsHint": "Überprüfe deinen Usenet-Anbieter für Verbindungslimits (normalerweise 10-50)", - "nntpServerModal_priority": "Priorität", - "nntpServerModal_priorityHint": "Niedrigere Werte = höhere Priorität. Verwende für Server-Failover.", - "nntpServer_addUsenetServer": "Usenet-Server hinzufügen", - "nntpServer_deleteServer": "Server löschen", - "nntpServer_disableServer": "Server deaktivieren", - "nntpServer_editServer": "Server bearbeiten", - "nntpServer_editUsenetServer": "Usenet-Server bearbeiten", - "nntpServer_enableServer": "Server aktivieren", - "nntpServer_maxConnections_help": "Überprüfe deinen Usenet-Anbieter für Verbindungslimits (normalerweise 10-50)", - "nntpServer_maxConnections_label": "Max. Verbindungen", - "nntpServer_priority_help": "Niedrigere Werte = höhere Priorität. Verwende für Server-Failover.", - "nntpServer_settings_title": "Einstellungen", "nntp_clearFiltersToReorder": "Lösche Filter, um Prioritäten neu anzuordnen.", "nntp_deleteServer": "Server löschen", "nntp_disableServer": "Server deaktivieren", @@ -2051,6 +2071,23 @@ "nntp_reorderModeActive": "Neuanordnungsmodus ist aktiv", "nntp_reorderPriorities": "Prioritäten neu anordnen", "nntp_testConnection": "Verbindung testen", + "nntpServer_addUsenetServer": "Usenet-Server hinzufügen", + "nntpServer_deleteServer": "Server löschen", + "nntpServer_disableServer": "Server deaktivieren", + "nntpServer_editServer": "Server bearbeiten", + "nntpServer_editUsenetServer": "Usenet-Server bearbeiten", + "nntpServer_enableServer": "Server aktivieren", + "nntpServer_maxConnections_help": "Überprüfe deinen Usenet-Anbieter für Verbindungslimits (normalerweise 10-50)", + "nntpServer_maxConnections_label": "Max. Verbindungen", + "nntpServer_priority_help": "Niedrigere Werte = höhere Priorität. Verwende für Server-Failover.", + "nntpServer_settings_title": "Einstellungen", + "nntpServerModal_addTitle": "Usenet-Server hinzufügen", + "nntpServerModal_editTitle": "Usenet-Server bearbeiten", + "nntpServerModal_failedToSave": "Speichern fehlgeschlagen", + "nntpServerModal_maxConnections": "Max. Verbindungen", + "nntpServerModal_maxConnectionsHint": "Überprüfe deinen Usenet-Anbieter für Verbindungslimits (normalerweise 10-50)", + "nntpServerModal_priority": "Priorität", + "nntpServerModal_priorityHint": "Niedrigere Werte = höhere Priorität. Verwende für Server-Failover.", "noFormatsMatch": "Keine Formate entsprechen deiner Suche", "noMax": "Kein Maximum", "noMin": "Kein Minimum", @@ -2065,18 +2102,6 @@ "person_gender_male": "Männlich", "person_gender_nonBinary": "Nicht-binär", "person_noBiography": "Keine Biografie verfügbar.", - "profileModal_allowUpgrades": "Upgrades erlauben", - "profileModal_copyFrom": "Kopieren von", - "profileModal_createTitle": "Profil erstellen", - "profileModal_descPlaceholder": "Beschreibe, wofür dieses Profil gedacht ist...", - "profileModal_description": "Beschreibung", - "profileModal_editSizeLimits": "Größenlimits bearbeiten", - "profileModal_editTitle": "Profil bearbeiten", - "profileModal_failedToSave": "Speichern fehlgeschlagen", - "profileModal_name": "Name", - "profileModal_profile": "Profil", - "profileModal_setAsDefault": "Als Standard setzen", - "profileModal_streamingProfileInfo": "Streaming-Profile verwenden externe Quellen, daher sind Größenlimits nicht konfigurierbar.", "profile_accountSecurity": "Kontosicherheit", "profile_accountSecurityDescription": "Verwalte Passwörter, Zwei-Faktor-Authentifizierung und Anmeldesitzungen.", "profile_comingSoon": "Demnächst verfügbar", @@ -2095,6 +2120,18 @@ "profile_singleAdminNote": "Derzeit läuft Cinephage im Einzeladmin-Modus. Alle Einstellungen werden über das Einstellungsmenü verwaltet.", "profile_subtitle": "Verwalte deine Kontoeinstellungen und Präferenzen", "profile_title": "Benutzerprofil", + "profileModal_allowUpgrades": "Upgrades erlauben", + "profileModal_copyFrom": "Kopieren von", + "profileModal_createTitle": "Profil erstellen", + "profileModal_descPlaceholder": "Beschreibe, wofür dieses Profil gedacht ist...", + "profileModal_description": "Beschreibung", + "profileModal_editSizeLimits": "Größenlimits bearbeiten", + "profileModal_editTitle": "Profil bearbeiten", + "profileModal_failedToSave": "Speichern fehlgeschlagen", + "profileModal_name": "Name", + "profileModal_profile": "Profil", + "profileModal_setAsDefault": "Als Standard setzen", + "profileModal_streamingProfileInfo": "Streaming-Profile verwenden externe Quellen, daher sind Größenlimits nicht konfigurierbar.", "profiles_addProfile": "Profil hinzufügen", "profiles_allowUpgrades_label": "Upgrades erlauben", "profiles_preventDowngrades_label": "Auflösungs-Downgrades verhindern", @@ -2114,6 +2151,7 @@ "profiles_movieMaxSize_label": "Film Max (GB)", "profiles_movieMinSize_label": "Film Min (GB)", "profiles_noFormatsMatchSearch": "Keine Formate entsprechen deiner Suche", + "profiles_preventDowngrades_label": "Auflösungs-Downgrades verhindern", "profiles_scoredCount": "{count} bewertet", "profiles_searchFormats": "Formate suchen...", "profiles_seasonPackSizeInfo": "Für Staffelpakete wird die durchschnittliche Größe pro Episode berechnet.", @@ -2161,7 +2199,6 @@ "rootFolders_unknown": "Unbekannt", "rootFolders_validate": "Validieren", "rootFolders_validationFailed": "Pfadvalidierung fehlgeschlagen", - "searchFormats": "Formate suchen...", "search_actionsCol": "Aktionen", "search_afterDeduplication": "2. Nach Deduplizierung:", "search_afterQualityScoring": "4. Nach Qualitätsbewertung & Smart-Deduplizierung:", @@ -2183,8 +2220,8 @@ "search_failed": "Fehlgeschlagen", "search_freeleech": "Freeleech", "search_grab": "Greifen", - "search_grabButtonTooltip": "Herunterladen", "search_grabbed": "Gegriffen", + "search_grabButtonTooltip": "Herunterladen", "search_hideDebugPanel": "Debug-Panel ausblenden", "search_indexerCol": "Indexer", "search_indexerCount": "{count} Indexer", @@ -2211,23 +2248,23 @@ "search_resultsCount": "{filtered} von {total} Ergebnissen", "search_scoreCol": "Punktzahl", "search_scoreLabel": "Punktzahl: {score}", - "search_searchResultsPlaceholder": "Suchergebnisse...", - "search_searchTime": "Suche: {time}ms", "search_searched": "Gesucht:", "search_searchedMultiPack": "Gesucht (Multi-Pack-Übereinstimmungen / Roh):", "search_searching": "Indexer werden durchsucht...", + "search_searchResultsPlaceholder": "Suchergebnisse...", + "search_searchTime": "Suche: {time}ms", "search_showDebugPanel": "Debug-Panel anzeigen", "search_showRejected": "Abgelehnte anzeigen", "search_sizeCol": "Größe", "search_skipped": "Übersprungen:", "search_slCol": "S/L", + "search_sort_score": "Bewertung", + "search_sort_seeders": "Seeder", + "search_sort_size": "Größe", "search_sortAge": "Alter", "search_sortScore": "Punktzahl", "search_sortSeeders": "Seeder", "search_sortSize": "Größe", - "search_sort_score": "Bewertung", - "search_sort_seeders": "Seeder", - "search_sort_size": "Größe", "search_stream": "Streamen", "search_streamButtonTooltip": "Streamen (NNTP)", "search_streamProtocol": "Stream", @@ -2237,27 +2274,42 @@ "search_uploadMultiplier": "{multiplier}x Upload", "search_usenetProtocol": "Usenet", "search_viewDetails": "Details anzeigen", + "searchFormats": "Formate suchen...", "setAsDefault": "Als Standard setzen", + "settings_blocklist_tabsAriaLabel": "Sperrlisten-Navigations-Tabs", + "settings_duplicates_all": "All libraries", + "settings_duplicates_both": "Both", + "settings_duplicates_description": "Find and manage duplicate media files across your library using filename or content hash comparison.", + "settings_duplicates_filehash": "Content hash (sha256)", + "settings_duplicates_filename": "Filename (normalized)", + "settings_duplicates_files": "files", + "settings_duplicates_mode": "Detection mode", + "settings_duplicates_no_dupes": "No duplicates found", + "settings_duplicates_off": "Off", + "settings_duplicates_select_library": "Select library", + "settings_duplicates_suppress": "Dismiss", + "settings_duplicates_title": "Duplicate Detection", + "settings_duplicates_unsuppress": "Restore", "settings_filters_configureTmdbForGenres": "Konfiguriere deinen TMDB-API-Schlüssel, um verfügbare Genres zu laden.", "settings_filters_contentPreferences": "Inhaltspräferenzen", "settings_filters_excludedGenres": "Ausgeschlossene Genres", "settings_filters_excludedGenresHint": "Wähle Genres aus, die aus allen Ergebnissen ausgeschlossen werden sollen.", - "settings_filters_failedToSave": "Globale Filter konnten nicht gespeichert werden", - "settings_filters_heading": "Globale Filter", + "settings_filters_failedToSave": "Speichern der Entdeckungsfilter fehlgeschlagen", + "settings_filters_heading": "Entdeckungsfilter", "settings_filters_includeAdult": "Erwachseneninhalt einschließen", "settings_filters_includeAdultHint": "Aktivieren, um Erwachseneninhalt in Suchergebnissen und Entdeckungen zuzulassen.", "settings_filters_localization": "Lokalisierung", "settings_filters_minScore": "Mindestpunktzahl (0-10)", "settings_filters_minVoteCount": "Mindeststimmenanzahl", "settings_filters_noGenresAvailable": "Keine Genres verfügbar.", - "settings_filters_pageTitle": "Globale Filter - Einstellungen - Cinephage", + "settings_filters_pageTitle": "Entdeckungsfilter - Einstellungen - Cinephage", "settings_filters_preferredLanguage": "Bevorzugte Sprache", "settings_filters_preferredRegion": "Bevorzugte Region", "settings_filters_qualityStandards": "Qualitätsstandards", "settings_filters_saveButton": "Globale Filter speichern", - "settings_filters_subtitle": "Konfiguriere globale Inhaltsfilter. Diese Einstellungen gelten für alle Suchergebnisse, Entdeckungen und automatisierten Aufgaben.", + "settings_filters_subtitle": "Globale Inhaltsfilter konfigurieren. Diese Filter gelten für alle Metadatenquellen (TMDB, ergänzende TVDB-Daten und die zukünftige vereinheitlichte Cinephage-API) und steuern, was in Ihrer Bibliothek entdeckt und hinzugefügt wird.", "settings_filters_tmdbRequired": "Konfiguriere deinen TMDB-API-Schlüssel, um Genre-Filterung und andere TMDB-gestützte Funktionen zu aktivieren.", - "settings_filters_updated": "Globale Filter aktualisiert", + "settings_filters_updated": "Entdeckungsfilter aktualisiert", "settings_filters_updatedSuccess": "Globale Filter erfolgreich aktualisiert.", "settings_general_addFolder": "Ordner hinzufügen", "settings_general_addFolderFirst": "Füge zuerst einen Stammordner oben hinzu, bevor du deine Bibliothek scannst.", @@ -2274,8 +2326,8 @@ "settings_general_badgeSystem": "System", "settings_general_capacityUnknown": "Kapazität unbekannt", "settings_general_cinephageTracked": "Cinephage: {used}", - "settings_general_classShort": "Klasse", "settings_general_classification": "Klassifizierung", + "settings_general_classShort": "Klasse", "settings_general_close": "schließen", "settings_general_columnClassification": "Klassifizierung", "settings_general_columnItems": "Elemente", @@ -2346,11 +2398,11 @@ "settings_general_noMovieLibrariesConfigured": "Keine Film-Bibliotheken konfiguriert.", "settings_general_noRootFolder": "Kein Stammordner", "settings_general_noRootFolderAssigned": "Kein Stammordner zugewiesen", - "settings_general_noRootFolderUsageDataYet": "Noch keine Stammordner-Nutzungsdaten.", "settings_general_noRootFoldersAssigned": "Keine Stammordner zugewiesen", + "settings_general_noRootFolderUsageDataYet": "Noch keine Stammordner-Nutzungsdaten.", "settings_general_noScanHistory": "Kein Scan-Verlauf", - "settings_general_noTvLibrariesConfigured": "Keine TV-Bibliotheken konfiguriert.", "settings_general_notAssigned": "Nicht zugewiesen", + "settings_general_noTvLibrariesConfigured": "Keine TV-Bibliotheken konfiguriert.", "settings_general_optionalChooseCompatibleLibrary": "Optional: Wählen Sie eine andere kompatible benutzerdefinierte Bibliothek.", "settings_general_otherDiskUsage": "Andere: {used}", "settings_general_pageTitle": "Medienspeicher - Cinephage", @@ -2360,7 +2412,6 @@ "settings_general_processing": "Verarbeite...", "settings_general_reviewLibrary": "Bibliothek prüfen", "settings_general_rootFolderLabel": "Stammordner", - "settings_general_rootFolderUsage": "Stammordnernutzung", "settings_general_rootFolders": "Stammordner", "settings_general_rootFoldersCount": "{count} Ordner", "settings_general_rootFoldersDescription": "Konfiguriere Medienbibliotheksordner, in denen Inhalte organisiert werden.", @@ -2370,6 +2421,7 @@ "settings_general_rootFoldersSummaryMore": "{names} +{count} weitere", "settings_general_rootFoldersTabDescription": "Konfiguriere Medienbibliotheksordner, in denen Inhalte organisiert werden.", "settings_general_rootFoldersTitle": "Stammordner", + "settings_general_rootFolderUsage": "Stammordnernutzung", "settings_general_runLibraryScan": "Bibliotheksscan ausführen", "settings_general_running": "Läuft", "settings_general_saveLibrary": "Bibliothek speichern", @@ -2381,14 +2433,14 @@ "settings_general_scanHealth": "Scan-Zustand", "settings_general_scanLibraries": "Scan libraries", "settings_general_scanLibrary": "Bibliothek scannen", + "settings_general_scanned": "Gescannt", + "settings_general_scanning": "Scanne...", "settings_general_scanNow": "Jetzt scannen", "settings_general_scanRemoved": "Entfernt", "settings_general_scanRootFolders": "Scan root folders", "settings_general_scanStatus": "Scan-Status", "settings_general_scanUnmatched": "Nicht zugeordnet", "settings_general_scanUpdated": "Aktualisiert", - "settings_general_scanned": "Gescannt", - "settings_general_scanning": "Scanne...", "settings_general_searchOnAddLabel": "Beim Hinzufügen suchen", "settings_general_selectedCount": "{count} ausgewählt", "settings_general_seriesCount": "{count} Serien", @@ -2398,9 +2450,6 @@ "settings_general_subtitle": "Verwalte Stammordner, Bibliotheksregeln und Scan-Verhalten.", "settings_general_subtitleFilesCount": "{count} Untertiteldateien", "settings_general_subtitlesLabel": "Untertitel", - "settings_general_tabLibraries": "Bibliotheken", - "settings_general_tabMaintenance": "Speicherwartung", - "settings_general_tabRootFolders": "Stammordner", "settings_general_topLibrariesByUsage": "Größte Bibliotheken nach Nutzung", "settings_general_topRootFoldersByUsage": "Größte Stammordner nach Nutzung", "settings_general_totalStorage": "Gesamtspeicher", @@ -2418,6 +2467,20 @@ "settings_general_viewUnmatchedFiles": "{count} nicht zugeordnete Dateien anzeigen", "settings_general_wantSubtitles": "Untertitel gewünscht", "settings_general_yes": "Ja", + "settings_history_description": "Control how long historical data is kept before automatic cleanup. 0 days = unlimited.", + "settings_history_failed": "Failed to save retention settings", + "settings_history_file_days": "File history (days)", + "settings_history_file_days_help": "Per-file analysis snapshots. Default: 30 days.", + "settings_history_forecast": "Storage Forecast", + "settings_history_forecast_30d": "Projected (30 days)", + "settings_history_forecast_90d": "Projected (90 days)", + "settings_history_forecast_current": "Current estimated storage", + "settings_history_library_days": "Library history (days)", + "settings_history_library_days_help": "Daily per-library snapshots. Default: 365 days.", + "settings_history_saved": "Retention settings saved", + "settings_history_scan_days": "Scan history (days)", + "settings_history_scan_days_help": "Completed scan job records. Default: 30 days.", + "settings_history_title": "History Retention", "settings_indexers_addIndexer": "Indexer hinzufügen", "settings_indexers_addIndexerHint": "Füge einen Indexer hinzu, um nach Inhalten zu suchen", "settings_indexers_authentication": "Authentifizierung", @@ -2449,8 +2512,8 @@ "settings_indexers_edit": "Indexer bearbeiten", "settings_indexers_editIndexer": "Indexer bearbeiten", "settings_indexers_enable": "Aktivieren", - "settings_indexers_enableReorderHint": "Aktiviere den Neuanordnungsmodus, um die Indexer-Priorität zu ändern.", "settings_indexers_enabled": "Aktiviert", + "settings_indexers_enableReorderHint": "Aktiviere den Neuanordnungsmodus, um die Indexer-Priorität zu ändern.", "settings_indexers_externalUrl": "Externe URL", "settings_indexers_externalUrlHint": "Die externe URL, unter der Jellyfin/Kodi diesen Server erreichen kann", "settings_indexers_externalUrlRequired": "Erforderlich für Streaming", @@ -2485,11 +2548,11 @@ "settings_indexers_reorderModeActive": "Neuanordnungsmodus ist aktiv", "settings_indexers_reorderPriorities": "Prioritäten neu anordnen", "settings_indexers_searchPlaceholder": "Indexer suchen...", + "settings_indexers_seedersHint": "0+", "settings_indexers_seedRatio": "Seed-Ratio", "settings_indexers_seedRatioHint": "Leer = Client-Standard", "settings_indexers_seedTime": "Seed-Zeit", "settings_indexers_seedTimeUnit": "Minuten", - "settings_indexers_seedersHint": "0+", "settings_indexers_selectAll": "Alle auswählen", "settings_indexers_skipReleasesBelow": "Releases darunter überspringen", "settings_indexers_status": "Status", @@ -2526,8 +2589,8 @@ "settings_integrations_captcha_failedToClearCache": "Captcha-Cache konnte nicht geleert werden", "settings_integrations_captcha_failedToLoad": "Captcha-Löser-Daten konnten nicht geladen werden", "settings_integrations_captcha_failedToSaveSettings": "Einstellungen konnten nicht gespeichert werden", - "settings_integrations_captcha_fetchSuccessRate": "Abruf-Erfolgsquote", "settings_integrations_captcha_fetchesAttempted": "{count} Abrufversuche", + "settings_integrations_captcha_fetchSuccessRate": "Abruf-Erfolgsquote", "settings_integrations_captcha_headlessDesc": "Browser ohne sichtbares Fenster ausführen. Für Debugging-Zwecke deaktivieren.", "settings_integrations_captcha_headlessLabel": "Headless-Modus", "settings_integrations_captcha_hour1Default": "1 Stunde (Standard)", @@ -2547,8 +2610,8 @@ "settings_integrations_captcha_optional": "Optional", "settings_integrations_captcha_pageTitle": "Captcha-Löser - Cinephage", "settings_integrations_captcha_proxyOptional": "Proxy (Optional)", - "settings_integrations_captcha_proxyPlaceholder": "http://proxy.example.com:8080", "settings_integrations_captcha_proxyPassword": "Passwort", + "settings_integrations_captcha_proxyPlaceholder": "http://proxy.example.com:8080", "settings_integrations_captcha_proxyUrl": "Proxy-URL", "settings_integrations_captcha_proxyUrlHelp": "Browser über einen Proxy leiten. Format: http://host:port oder socks5://host:port", "settings_integrations_captcha_proxyUsername": "Benutzername", @@ -2557,11 +2620,11 @@ "settings_integrations_captcha_seconds60Default": "60 Sekunden (Standard)", "settings_integrations_captcha_seconds90": "90 Sekunden", "settings_integrations_captcha_settingsSaved": "Einstellungen erfolgreich gespeichert", + "settings_integrations_captcha_solvedChallengeIn": "{challengeType}-Herausforderung in {solveTimeMs}ms gelöst", + "settings_integrations_captcha_solvesAttempted": "{count} Lösungsversuche", "settings_integrations_captcha_solveSuccessRate": "Lösungs-Erfolgsquote", "settings_integrations_captcha_solveTimeout": "Lösungs-Timeout", "settings_integrations_captcha_solveTimeoutHelp": "Maximale Zeit, die auf die Lösung eines CAPTCHA gewartet wird, bevor abgebrochen wird.", - "settings_integrations_captcha_solvedChallengeIn": "{challengeType}-Herausforderung in {solveTimeMs}ms gelöst", - "settings_integrations_captcha_solvesAttempted": "{count} Lösungsversuche", "settings_integrations_captcha_statistics": "Statistiken", "settings_integrations_captcha_statusBusy": "Beschäftigt", "settings_integrations_captcha_statusDisabled": "Captcha-Löser ist deaktiviert", @@ -2589,7 +2652,6 @@ "settings_integrations_connectionSuccessful": "Verbindung erfolgreich", "settings_integrations_deleteConfirmPrefix": "Bist du sicher, dass du ", "settings_integrations_deleteConfirmSuffix": "löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", - "settings_integrations_downloadClientsDesc": "Anwendungen, die das Herunterladen von Releases von Indexern verwalten.", "settings_integrations_downloadClients_addButton": "Download-Client hinzufügen", "settings_integrations_downloadClients_altmountMode": "Altmount / NZBDav (Mount-Modus)", "settings_integrations_downloadClients_bulkDeleteCount": "{count} ausgewählte Clients", @@ -2598,8 +2660,8 @@ "settings_integrations_downloadClients_clientPath": "Client-Pfad", "settings_integrations_downloadClients_completedDownloadFolder": "Ordner für abgeschlossene Downloads", "settings_integrations_downloadClients_downloadBehavior": "Download-Verhalten", - "settings_integrations_downloadClients_downloadFolder": "Download-Ordner", "settings_integrations_downloadClients_downloaderColumn": "Downloader", + "settings_integrations_downloadClients_downloadFolder": "Download-Ordner", "settings_integrations_downloadClients_emptyDescription": "Füge einen Download-Client hinzu, um Downloads zu verwalten", "settings_integrations_downloadClients_emptyTitle": "Keine Download-Clients konfiguriert", "settings_integrations_downloadClients_forcePriority": "Force", @@ -2625,25 +2687,25 @@ "settings_integrations_downloadClients_torrent": "Torrent", "settings_integrations_downloadClients_urlBaseDescription": "Pfad-Präfix, der nach Host und Port hinzugefügt wird.", "settings_integrations_downloadClients_urlBaseLabel": "URL-Base", - "settings_integrations_downloadClients_useUrlBase": "URL-Base verwenden", "settings_integrations_downloadClients_usenet": "Usenet", + "settings_integrations_downloadClients_useUrlBase": "URL-Base verwenden", + "settings_integrations_downloadClientsDesc": "Anwendungen, die das Herunterladen von Releases von Indexern verwalten.", "settings_integrations_gettingStarted": "Erste Schritte", "settings_integrations_gettingStartedBoth": "Füge mindestens einen Indexer und einen Download-Client hinzu, um Inhalte herunterzuladen.", "settings_integrations_gettingStartedDownloadClient": "Füge mindestens einen Download-Client hinzu, um Releases herunterzuladen.", "settings_integrations_gettingStartedIndexer": "Füge mindestens einen Indexer hinzu, um nach Releases zu suchen.", - "settings_integrations_indexersDesc": "Suchanbieter für das Finden von Releases in Torrent- und Usenet-Netzwerken.", "settings_integrations_indexers_addButton": "Indexer hinzufügen", "settings_integrations_indexers_bulkDeleteCount": "{count} ausgewählte Indexer", "settings_integrations_indexers_created": "Indexer erfolgreich erstellt", "settings_integrations_indexers_definitionErrors": "{count} Indexer-Definitionsfehler gefunden", "settings_integrations_indexers_subtitle": "Verwalte deine Indexer für die Suche nach Torrent- und Usenet-Releases.", "settings_integrations_indexers_updated": "Indexer erfolgreich aktualisiert", - "settings_integrations_languageProfilesDesc": "Konfiguriere bevorzugte Sprachen und Untertitel-Anforderungen.", + "settings_integrations_indexersDesc": "Suchanbieter für das Finden von Releases in Torrent- und Usenet-Netzwerken.", "settings_integrations_languageProfiles_addLanguage": "Sprache hinzufügen", "settings_integrations_languageProfiles_addProfile": "Profil hinzufügen", "settings_integrations_languageProfiles_addTitle": "Sprachprofil hinzufügen", - "settings_integrations_languageProfiles_allowUpgrades": "Upgrades erlauben", "settings_integrations_languageProfiles_allowed": "Erlaubt", + "settings_integrations_languageProfiles_allowUpgrades": "Upgrades erlauben", "settings_integrations_languageProfiles_cutoff": "Cutoff", "settings_integrations_languageProfiles_cutoffIndex": "Cutoff-Index", "settings_integrations_languageProfiles_cutoffIndexHelp": "Stoppe das Upgraden von Untertiteln, sobald eine Übereinstimmung bei oder über diesem Sprachindex gefunden wurde.", @@ -2660,9 +2722,9 @@ "settings_integrations_languageProfiles_forced": "Erzwungen", "settings_integrations_languageProfiles_hi": "HI", "settings_integrations_languageProfiles_maxChars": "Max. {max} Zeichen", - "settings_integrations_languageProfiles_minScore": "Min. Punktzahl", "settings_integrations_languageProfiles_minimumScore": "Mindestpunktzahl", "settings_integrations_languageProfiles_minimumScoreHelp": "Minimale Untertitel-Übereinstimmungspunktzahl (0-100), die erforderlich ist, bevor ein Untertitel akzeptiert wird.", + "settings_integrations_languageProfiles_minScore": "Min. Punktzahl", "settings_integrations_languageProfiles_nameAndLanguageRequired": "Profilname und mindestens eine Sprache sind erforderlich", "settings_integrations_languageProfiles_nameTooLong": "Profilname darf nicht länger als {max} Zeichen sein", "settings_integrations_languageProfiles_noProfiles": "Keine Sprachprofile konfiguriert", @@ -2675,6 +2737,7 @@ "settings_integrations_languageProfiles_setAsDefault": "Als Standard setzen", "settings_integrations_languageProfiles_subtitle": "Konfiguriere Sprachpräferenzen und Untertitel-Abgleichsprofile.", "settings_integrations_languageProfiles_upgrades": "Upgrades", + "settings_integrations_languageProfilesDesc": "Konfiguriere bevorzugte Sprachen und Untertitel-Anforderungen.", "settings_integrations_mediaBrowsers_addServer": "Server hinzufügen", "settings_integrations_mediaBrowsers_bulkDeleteCount": "{count} ausgewählte Server", "settings_integrations_mediaBrowsers_emptyDescription": "Füge einen Jellyfin, Emby oder Plex-Server hinzu, um Bibliotheksbenachrichtigungen zu ermöglichen", @@ -2683,7 +2746,6 @@ "settings_integrations_mediaBrowsers_subtitle": "Verbinde dich mit Plex, Jellyfin oder Emby, um Bibliotheken und Metadaten zu synchronisieren.", "settings_integrations_mediaBrowsers_thisServer": "diesen Server", "settings_integrations_mediaServersDesc": "Medienserver zum Durchsuchen und Streamen deiner Bibliothek.", - "settings_integrations_nntpServersDesc": "Usenet-News-Server zum Herunterladen von Inhalten über das NNTP-Protokoll.", "settings_integrations_nntpServers_addServer": "Server hinzufügen", "settings_integrations_nntpServers_bulkDeleteCount": "{count} ausgewählte Server", "settings_integrations_nntpServers_emptyDescription": "Füge einen NNTP-Server hinzu, um direktes NZB-Streaming zu ermöglichen", @@ -2696,6 +2758,7 @@ "settings_integrations_nntpServers_serverSettings": "Server-Einstellungen", "settings_integrations_nntpServers_subtitle": "Verwalten Sie Ihre NNTP-Server für Usenet-Downloads.", "settings_integrations_nntpServers_thisServer": "diesen Server", + "settings_integrations_nntpServersDesc": "Usenet-News-Server zum Herunterladen von Inhalten über das NNTP-Protokoll.", "settings_integrations_notConfigured": "Nicht konfiguriert", "settings_integrations_pageTitle": "Integrationen - Cinephage", "settings_integrations_profiles_emptyDescription": "Füge ein Profil hinzu, um Qualitätspräferenzen zu verwalten", @@ -2705,13 +2768,13 @@ "settings_integrations_statSet": "Gesetzt", "settings_integrations_statTotal": "Gesamt", "settings_integrations_subtitle": "Verwalte deine externen Dienste und Verbindungen", - "settings_integrations_subtitleProvidersDesc": "Dienste, die Untertitel für deine Medienbibliothek bereitstellen.", "settings_integrations_subtitleProviders_addProvider": "Anbieter hinzufügen", "settings_integrations_subtitleProviders_bulkDeleteCount": "{count} ausgewählte Anbieter", "settings_integrations_subtitleProviders_connectionSuccessful": "Verbindung zu {name} erfolgreich (Antwortzeit: {responseTime}ms)", "settings_integrations_subtitleProviders_connectionTestFailed": "Unbekannter Fehler", "settings_integrations_subtitleProviders_searchPlaceholder": "Untertitel-Anbieter suchen...", "settings_integrations_subtitleProviders_subtitle": "Konfiguriere Untertitel-Download-Anbieter für automatisches Untertitel-Herunterladen.", + "settings_integrations_subtitleProvidersDesc": "Dienste, die Untertitel für deine Medienbibliothek bereitstellen.", "settings_integrations_tabsAriaLabel": "Integrations-Navigations-Tabs", "settings_integrations_tmdbApiKeyDescription": "Gib deinen TMDB-API-Schlüssel ein (v3 Auth). Du kannst einen erhalten von", "settings_integrations_tmdbApiKeyTitle": "TMDB-API-Schlüssel", @@ -2719,6 +2782,23 @@ "settings_integrations_tmdbFailedToSave": "TMDB-API-Schlüssel konnte nicht gespeichert werden", "settings_integrations_tmdbKeySaved": "TMDB-API-Schlüssel erfolgreich gespeichert", "settings_integrations_tmdbTitle": "TMDB", + "settings_libraries_scan_description": "Globales Bibliothek-Scan-Verhalten. Diese Einstellungen werden in einem zukünftigen Update pro Bibliothek verfügbar.", + "settings_libraries_scan_failed": "Speichern der Scan-Einstellungen fehlgeschlagen", + "settings_libraries_scan_interval": "Scan-Intervall (Stunden)", + "settings_libraries_scan_interval_hint": "Wie oft periodische Scans ausgeführt werden (1-168 Stunden)", + "settings_libraries_scan_loading": "Scan-Einstellungen werden geladen...", + "settings_libraries_scan_on_startup": "Beim Start scannen", + "settings_libraries_scan_on_startup_hint": "Einen Scan beim Start der Anwendung ausführen", + "settings_libraries_scan_saved": "Scan-Einstellungen gespeichert", + "settings_libraries_scan_threshold": "Auto-Match-Konfidenzschwellwert", + "settings_libraries_scan_threshold_hint": "Mindestkonfidenz (0-1) für das automatische Zuordnen gefundener Dateien zu Metadaten", + "settings_libraries_scan_title": "Scan-Einstellungen", + "settings_libraries_scan_watch": "Dateisystem auf Änderungen überwachen", + "settings_libraries_scan_watch_hint": "Automatisch neue, geänderte oder entfernte Dateien in Echtzeit erkennen", + "settings_library_tabFileManagement": "Dateiverwaltung", + "settings_library_tabLibraries": "Bibliotheken", + "settings_library_tabRootFolders": "Stammordner", + "settings_library_tabsAriaLabel": "Bibliotheks-Navigations-Tabs", "settings_logs_adjustFilters": "Filter anpassen oder auf neue Ereignisse warten", "settings_logs_allDomains": "Alle Domains", "settings_logs_bufferedEntries": "{count} neue Einträge gepuffert", @@ -2735,6 +2815,8 @@ "settings_logs_scrollToBottom": "Nach unten scrollen", "settings_logs_subtitle": "Live-Anwendungsprotokoll-Stream. Klicke auf einen Eintrag, um Details zu prüfen.", "settings_logs_toggleLevel": "{level}-Protokolle umschalten", + "settings_monitoring_tabMaintenance": "Speicherwartung", + "settings_monitoring_tabsAriaLabel": "Überwachungs-Navigations-Tabs", "settings_naming_advancedOptions": "Erweiterte Optionen", "settings_naming_allFieldsClean": "Alle Formatfelder sind gültig.", "settings_naming_allFormatsValid": "Alle Formatfelder sind gültig.", @@ -2788,11 +2870,11 @@ "settings_naming_movieNamingDesc": "Ordner- und Dateinamensmuster für Filme", "settings_naming_multiEpDuplicate": "Duplizieren (S01E01.S01E02)", "settings_naming_multiEpExtend": "Erweitern (S01E01E02E03)", + "settings_naming_multiEpisodeStyle": "Multi-Episoden-Stil", + "settings_naming_multiEpisodeStyleHelp": "Wie Multi-Episoden-Dateien im Dateinamen dargestellt werden.", "settings_naming_multiEpRange": "Bereich (S01E01-E03)", "settings_naming_multiEpRepeat": "Wiederholen (S01E01.E02.E03)", "settings_naming_multiEpScene": "Scene (S01E01-S01E03)", - "settings_naming_multiEpisodeStyle": "Multi-Episoden-Stil", - "settings_naming_multiEpisodeStyleHelp": "Wie Multi-Episoden-Dateien im Dateinamen dargestellt werden.", "settings_naming_namingStyle": "Namensstil", "settings_naming_nextStep": "Nächster Schritt", "settings_naming_nextStepDesc": "Speichere diesen Entwurf, wenn er richtig aussieht, dann überprüfe den Umbenennungsplan, bevor du Dateiänderungen in der Bibliothek anwendest.", @@ -2853,9 +2935,9 @@ "settings_naming_reviewRenamePlan": "Umbenennungsplan überprüfen", "settings_naming_saveChanges": "Änderungen speichern", "settings_naming_saveCurrentAsPreset": "Aktuelle als Voreinstellung speichern", - "settings_naming_savePreset": "Voreinstellung speichern", "settings_naming_saved": "Gespeichert", "settings_naming_savedCustomSource": "Gespeicherte benutzerdefinierte Quelle", + "settings_naming_savePreset": "Voreinstellung speichern", "settings_naming_seasonFolder": "Staffel-Ordner", "settings_naming_seasonFolderFormat": "Staffel-Ordner-Format", "settings_naming_selectCustomPreset": "Benutzerdefinierte Voreinstellung auswählen...", @@ -2875,10 +2957,68 @@ "settings_naming_unsavedChangesDesc": "Du hast ungespeicherte Benennungsänderungen. Speichere, um sie anzuwenden, oder überprüfe den Umbenennungsplan, um zu sehen, wie sie bestehende Dateien beeinflussen würden.", "settings_naming_updatingPreviews": "Vorschauen aktualisieren", "settings_naming_validating": "Validiere", + "settings_patterns_all_libraries": "Alle Bibliotheken (globale Standardwerte)", + "settings_patterns_bonus": "Bonus-Muster", + "settings_patterns_bonus_add": "Muster hinzufügen", + "settings_patterns_bonus_desc": "Glob-Muster (Groß-/Kleinschreibung ignorieren). Übereinstimmende Dateien werden als Bonus/Extras markiert.", + "settings_patterns_bonus_placeholder": "*trailer*", + "settings_patterns_description": "Steuert, wie Dateien während Bibliotheksscans gefiltert und klassifiziert werden. Ignorieren-Muster verhindern die Indizierung; Bonus-Muster markieren Dateien als Extras; Strukturerkennung extrahiert Serien/Staffel/Episode aus Ordnerlayouts.", + "settings_patterns_failed": "Speichern der Musterkonfiguration fehlgeschlagen", + "settings_patterns_ignore": "Ignorieren-Muster", + "settings_patterns_ignore_desc": "Glob-Muster (Groß-/Kleinschreibung beachten). Übereinstimmende Dateien und Ordner werden nie indiziert.", + "settings_patterns_ignore_enabled": "Integrierte Standardeinstellungen verwenden", + "settings_patterns_ignore_user": "Benutzermuster", + "settings_patterns_ignore_user_add": "Muster hinzufügen", + "settings_patterns_ignore_user_placeholder": "*Privat*", + "settings_patterns_saved": "Musterkonfiguration gespeichert", + "settings_patterns_scope_global": "Globale Standardwerte (für alle Bibliotheken, sofern nicht überschrieben)", + "settings_patterns_scope_library": "Bibliotheksüberschreibung", + "settings_patterns_select_library": "Bibliothek für pro-Bibliothek-Einstellungen auswählen", + "settings_patterns_structure": "Strukturerkennung", + "settings_patterns_structure_desc": "Serien/Staffel/Episode aus Ordnerlayouts erkennen. Nur für TV-Bibliotheken.", + "settings_patterns_structure_folder_depth": "Ordnertiefe", + "settings_patterns_structure_mode": "Erkennungsmodus", + "settings_patterns_structure_none": "Keine (flache Bibliothek)", + "settings_patterns_structure_regex": "Benutzerdefinierte Regex", + "settings_patterns_structure_season_depth": "Staffelordner-Tiefe (relativ zur Serie)", + "settings_patterns_structure_series_depth": "Serienordner-Tiefe (0 = Wurzel)", + "settings_patterns_title": "Mustererkennung", "settings_profiles_redirecting": "Weiterleiten...", "settings_quality_confirmDeleteFormatMessage": "Bist du sicher, dass du {name} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "settings_quality_confirmDeleteProfileMessage": "Bist du sicher, dass du {name} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "settings_quality_confirmDeleteTitle": "Löschen bestätigen", + "settings_quality_delay_addProfile": "Profil hinzufügen", + "settings_quality_delay_badgeDisabled": "Deaktiviert", + "settings_quality_delay_bypass4k": "Bei 4K umgehen", + "settings_quality_delay_bypassScorePrefix": "Umgehen wenn Wert >", + "settings_quality_delay_createButton": "Erstellen", + "settings_quality_delay_created": "Verzögerungsprofil erstellt", + "settings_quality_delay_deleted": "\"{name}\" gelöscht", + "settings_quality_delay_deleteMessage": "\"{name}\" löschen? Medien, die diesem Profil zugewiesen sind, fallen auf keine Verzögerung zurück.", + "settings_quality_delay_deleteTitle": "Verzögerungsprofil löschen", + "settings_quality_delay_empty": "Keine Verzögerungsprofile", + "settings_quality_delay_emptyHint": "Erstellen Sie ein Profil, um Releases vor dem Abruf zurückzuhalten.", + "settings_quality_delay_field_bypass4k": "Verzögerung für 4K (2160p) Releases umgehen", + "settings_quality_delay_field_bypassScore": "Verzögerung umgehen, wenn Wert überschritten wird", + "settings_quality_delay_field_bypassScorePlaceholder": "Leer lassen zum Deaktivieren", + "settings_quality_delay_field_enabled": "Aktiviert", + "settings_quality_delay_field_name": "Name", + "settings_quality_delay_field_namePlaceholder": "Dub Wartezeit", + "settings_quality_delay_field_preferredProtocol": "Bevorzugtes Protokoll (sofort abrufen)", + "settings_quality_delay_field_torrentDelay": "Torrent-Verzögerung (Minuten)", + "settings_quality_delay_field_usenetDelay": "Usenet-Verzögerung (Minuten)", + "settings_quality_delay_formatImmediate": "Sofort", + "settings_quality_delay_modalAddTitle": "Verzögerungsprofil hinzufügen", + "settings_quality_delay_modalEditTitle": "Verzögerungsprofil bearbeiten", + "settings_quality_delay_option_none": "Keines", + "settings_quality_delay_option_torrent": "Torrent", + "settings_quality_delay_option_usenet": "Usenet", + "settings_quality_delay_preferredPrefix": "Bevorzugt:", + "settings_quality_delay_sectionDescription": "Releases in einer Warteschlange halten, bevor sie an einen Download-Client gesendet werden, um Zeit für bessere Versionen zu haben.", + "settings_quality_delay_tab": "Verzögerungsprofile", + "settings_quality_delay_torrentPrefix": "Torrent:", + "settings_quality_delay_updated": "Verzögerungsprofil aktualisiert", + "settings_quality_delay_usenetPrefix": "Usenet:", "settings_quality_failedToDeleteFormat": "Format konnte nicht gelöscht werden", "settings_quality_failedToDeleteProfile": "Profil konnte nicht gelöscht werden", "settings_quality_failedToSetDefault": "Standardprofil konnte nicht gesetzt werden", @@ -2888,12 +3028,25 @@ "settings_quality_subtitle": "Verwalte Qualitätsprofile und benutzerdefinierte Formate, um zu steuern, wie Releases bewertet und ausgewählt werden.", "settings_quality_tabFormats": "Benutzerdefinierte Formate", "settings_quality_tabProfiles": "Profile", + "settings_resolutions_add": "Kategorie hinzufügen", + "settings_resolutions_delete": "Löschen", + "settings_resolutions_description": "Bearbeitbare Auflösungsklassen für Qualitätsbewertung, Anzeigeabzeichen und Statistiken.", + "settings_resolutions_failed": "Speichern fehlgeschlagen", + "settings_resolutions_fallback": "Rückfall", + "settings_resolutions_fallback_help": "Standard-Bucket für nicht zugeordnete Auflösungen.", + "settings_resolutions_label": "Bezeichnung", + "settings_resolutions_min_height": "Min. Höhe", + "settings_resolutions_min_width": "Min. Breite", + "settings_resolutions_saved": "Auflösungskategorien gespeichert", + "settings_resolutions_search_terms": "Suchbegriffe", + "settings_resolutions_search_terms_help": "Kommagetrennte Aliase für die Suche", + "settings_resolutions_title": "Auflösungskategorien", "settings_streaming_cacheDefaultsNotice": "Cache-Einstellungen verwenden derzeit Standardwerte. Benutzerdefinierte Konfiguration kommt bald.", + "settings_streaming_cachedFiles": "Zwischengespeicherte Dateien", "settings_streaming_cacheSettings": "Cache-Einstellungen", "settings_streaming_cacheSize": "Cache-Größe", - "settings_streaming_cachedFiles": "Zwischengespeicherte Dateien", - "settings_streaming_cleanExpiredFiles": "Abgelaufene Dateien bereinigen", "settings_streaming_cleanedUpResult": "{count} Datei(en) bereinigt, {size} freigegeben", + "settings_streaming_cleanExpiredFiles": "Abgelaufene Dateien bereinigen", "settings_streaming_cleaning": "Bereinige...", "settings_streaming_expired": "Abgelaufen", "settings_streaming_expiredCleaned": "Abgelaufene Cache-Dateien bereinigt", @@ -2953,25 +3106,25 @@ "settings_subtitles_reSyncSubtitle": "Untertitel re-synchronisieren", "settings_subtitles_retry": "Wiederholen", "settings_subtitles_sdh": "SDH", + "settings_subtitles_searchingProviders": "Untertitel-Anbieter werden durchsucht...", "settings_subtitles_searchStats": "{total} Ergebnisse", "settings_subtitles_searchSubs": "Untertitel suchen", "settings_subtitles_searchTime": "Suche: {time}ms", - "settings_subtitles_searchingProviders": "Untertitel-Anbieter werden durchsucht...", "settings_subtitles_splitPenalty": "Split-Strafe", "settings_subtitles_splitRecommended": "5-20 empfohlen", "settings_subtitles_subtitleSearch": "Untertitelsuche", "settings_subtitles_subtitleSync": "Untertitel-Synchronisation", "settings_subtitles_sync": "Synchronisieren", "settings_subtitles_syncAll": "Alle synchronisieren ({count})", + "settings_subtitles_synced": "Synchronisiert", + "settings_subtitles_syncedBadge": "SYNC", + "settings_subtitles_syncedCount": "{count} synchronisiert", + "settings_subtitles_syncing": "Synchronisiere {completed}/{total}...", "settings_subtitles_syncProgress": "{synced}/{total}", "settings_subtitles_syncSettings": "Sync-Einstellungen", "settings_subtitles_syncTooltip": "Untertitel synchronisiert ({offset})", "settings_subtitles_syncTooltipNoOffset": "Untertitel synchronisiert", "settings_subtitles_syncUnsynced": "Unsynchronisierte synchronisieren ({count})", - "settings_subtitles_synced": "Synchronisiert", - "settings_subtitles_syncedBadge": "SYNC", - "settings_subtitles_syncedCount": "{count} synchronisiert", - "settings_subtitles_syncing": "Synchronisiere {completed}/{total}...", "settings_subtitles_tryAdjustingFilters": "Versuche dein Sprachprofil oder Suchfilter anzupassen", "settings_subtitles_unknownOffset": "Unbekannt", "settings_system_apiAuth": "API-Authentifizierung", @@ -2993,11 +3146,11 @@ "settings_system_backup_errorSelectSectionFirst": "Bitte mindestens einen Backup-Bereich zur Wiederherstellung auswählen.", "settings_system_backup_exportButton": "Backup exportieren", "settings_system_backup_exportDescription": "Exportiert Konfiguration für Einstellungen, Integrationen, Indexer, Download-Clients, Profile und weitere nutzerverwaltete Einrichtung, um die Instanz wieder aufzubauen.", + "settings_system_backup_exporting": "Exportiere...", "settings_system_backup_exportPassphraseLabel": "Verschlüsselungs-Passphrase", "settings_system_backup_exportPassphrasePlaceholder": "Mindestens 16 Zeichen zur Verschlüsselung exportierter Geheimnisse", "settings_system_backup_exportSuccess": "Konfigurations-Backup erfolgreich exportiert.", "settings_system_backup_exportTitle": "Konfiguration exportieren", - "settings_system_backup_exporting": "Exportiere...", "settings_system_backup_fileLabel": "Backup-Datei", "settings_system_backup_includeCookiesHelp": "Aktive 2FA-/Sitzungs-Cookies für Indexer sichern, die darauf angewiesen sind. Diese sind verschlüsselt, kurzlebig und sensibler als gespeicherte Zugangsdaten.", "settings_system_backup_includeCookiesLabel": "Indexer-Sitzungs-Cookies einschließen", @@ -3036,8 +3189,8 @@ "settings_system_created": "Erstellt", "settings_system_exampleEmpty": "Lass leer für direkten HTTP-Zugriff", "settings_system_exampleReverseProxy": "https://cinephage.beispiel.de (hinter Reverse-Proxy)", - "settings_system_exampleSubpath": "https://media.deinedomain.de/cinephage (Unterpfad)", "settings_system_examples": "Beispiele", + "settings_system_exampleSubpath": "https://media.deinedomain.de/cinephage (Unterpfad)", "settings_system_externalUrl": "Externe URL", "settings_system_externalUrlDescription": "Setze die öffentlich zugängliche URL für deine Cinephage-Instanz. Dies wird für Authentifizierungs-Callbacks und die Generierung korrekter Links hinter einem Reverse-Proxy verwendet.", "settings_system_externalUrlPlaceholder": "https://cinephage.deinedomain.de", @@ -3050,10 +3203,10 @@ "settings_system_failedToSave": "Speichern fehlgeschlagen", "settings_system_failedToSaveExternalUrl": "Externe URL konnte nicht gespeichert werden", "settings_system_fullAccess": "Voller Zugriff", - "settings_system_generateApiKeys": "API-Schlüssel generieren", - "settings_system_generating": "Generiere...", "settings_system_general_pageTitle": "Allgemein - Cinephage", "settings_system_general_subtitle": "Verwalte API-Schlüssel und externe URL-Einstellungen.", + "settings_system_generateApiKeys": "API-Schlüssel generieren", + "settings_system_generating": "Generiere...", "settings_system_heading": "Systemeinstellungen", "settings_system_hideKey": "Schlüssel ausblenden", "settings_system_invalidUrlFormat": "Bitte gib eine gültige URL ein (z.B. https://cinephage.beispiel.de)", @@ -3069,13 +3222,13 @@ "settings_system_noApiKeys": "Keine API-Schlüssel gefunden. Generiere Schlüssel, um externen Zugriff zu ermöglichen.", "settings_system_noKeyToRegenerate": "Kein {label}-API-Schlüssel zum Neu-Generieren gefunden", "settings_system_pageTitle": "Systemeinstellungen - Cinephage", - "settings_system_permEpg": "✓ EPG-Daten (/api/livetv/epg.xml)", - "settings_system_permLiveTvStreams": "✓ Live-TV-Streams (/api/livetv/stream/*)", - "settings_system_permM3u": "✓ M3U-Playlist (/api/livetv/playlist.m3u)", - "settings_system_permNoLibrary": "✗ Bibliothekszugriff", - "settings_system_permNoSettings": "✗ Einstellungszugriff", - "settings_system_permStreamingContent": "✓ Streaming-Inhalte (/api/streaming/*)", + "settings_system_permEpg": "EPG-Daten (/api/livetv/epg.xml)", "settings_system_permissions": "Berechtigungen", + "settings_system_permLiveTvStreams": "Live-TV-Streams (/api/livetv/stream/*)", + "settings_system_permM3u": "M3U-Playlist (/api/livetv/playlist.m3u)", + "settings_system_permNoLibrary": "Bibliothekszugriff", + "settings_system_permNoSettings": "Einstellungszugriff", + "settings_system_permStreamingContent": "Streaming-Inhalte (/api/streaming/*)", "settings_system_publicUrlConfig": "Öffentliche URL-Konfiguration", "settings_system_publicUrlHint": "Wenn du Cinephage über einen Reverse-Proxy mit HTTPS erreichst, gib hier deine öffentliche URL ein (z.B. https://cinephage.deinedomain.de). Lass leer für lokalen HTTP-Zugriff.", "settings_system_regenerate": "Neu generieren", @@ -3085,6 +3238,7 @@ "settings_system_saveExternalUrl": "Externe URL speichern", "settings_system_showKey": "Schlüssel anzeigen", "settings_system_subtitle": "Konfiguriere Systemeinstellungen einschließlich API-Authentifizierung.", + "settings_system_tabsAriaLabel": "System-Navigations-Tabs", "settings_tasks_activityAdded": "{count} hinzugefügt", "settings_tasks_activityErrors": "{count} Fehler", "settings_tasks_activityFailed": "{count} fehlgeschlagen", @@ -3141,8 +3295,8 @@ "settings_tasks_ok": "OK", "settings_tasks_overdue": "Überfällig", "settings_tasks_pageTitle": "Aufgaben - Einstellungen - Cinephage", - "settings_tasks_runNow": "Jetzt ausführen", "settings_tasks_running": "Läuft...", + "settings_tasks_runNow": "Jetzt ausführen", "settings_tasks_scheduledTasks": "Geplante Aufgaben", "settings_tasks_statusError": "Fehler", "settings_tasks_statusFound": "{count} gefunden", @@ -3222,9 +3376,9 @@ "smartlists_desc_years": "{min}–{max}", "smartlists_detail_addAllButton": "Alle hinzufügen", "smartlists_detail_addAllConfirm": "Alle sichtbaren Elemente zur Bibliothek hinzufügen?", - "smartlists_detail_addToLibrary": "Zur Bibliothek hinzufügen", "smartlists_detail_addedBulkPartial": "{added} hinzugefügt, {failed} fehlgeschlagen", "smartlists_detail_addedToLibrary": "\"{title}\" zur Bibliothek hinzugefügt", + "smartlists_detail_addToLibrary": "Zur Bibliothek hinzufügen", "smartlists_detail_autoAddEnabled": "Auto-hinzufügen aktiviert", "smartlists_detail_backToSmartLists": "Zurück zu Smart Lists", "smartlists_detail_badgeExcluded": "Ausgeschlossen", @@ -3323,9 +3477,9 @@ "smartlists_filter_exclude": "Ausschließen", "smartlists_filter_excludeGenres": "Genres ausschließen", "smartlists_filter_genreMatchMode": "Genre-Abgleichsmodus", - "smartlists_filter_inProduction": "In Produktion", "smartlists_filter_include": "Einschließen", "smartlists_filter_includeGenres": "Genres einschließen", + "smartlists_filter_inProduction": "In Produktion", "smartlists_filter_keywords": "Keywords", "smartlists_filter_keywordsPlaceholder": "Keywords suchen...", "smartlists_filter_loading": "Laden...", @@ -3361,9 +3515,9 @@ "smartlists_formatDate_minutesAgo": "Vor {count} Min", "smartlists_formatDate_never": "Nie", "smartlists_heading": "Smart Lists", - "smartlists_inLibraryBadge": "{count} in Bibliothek", "smartlists_info_mixedContentDescription": "Filme und Serien aus der Quelle werden zusammen angezeigt. Die Medientyp-Auswahl oben wirkt sich nur auf TMDB Entdecken-Listen aus.", "smartlists_info_mixedContentTitle": "Importlisten zeigen alle Inhaltstypen", + "smartlists_inLibraryBadge": "{count} in Bibliothek", "smartlists_itemsBadge": "{count} Elemente", "smartlists_label_customHeaders": "Benutzerdefinierte Header (Optional)", "smartlists_label_jsonUrl": "JSON-URL", @@ -3440,17 +3594,32 @@ "smartlists_subtitle": "Erstelle dynamische Listen, die sich automatisch aus TMDB basierend auf deinen Filtern füllen.", "smartlists_test_success": "Erfolgreich verbunden! {count} Elemente gefunden.", "startFromScratch": "Von Grund auf neu beginnen", + "status_back_to_dashboard": "Dashboard", "status_completed": "Abgeschlossen", "status_connected": "Verbunden", + "status_dashboard_subtitle": "Storage health, library maintenance, and media server analytics", + "status_dashboard_title": "Storage Maintenance", "status_disabled": "Deaktiviert", "status_disconnected": "Getrennt", "status_downloading": "Wird heruntergeladen", "status_error": "Fehler", "status_failed": "Fehlgeschlagen", + "status_folder_not_found": "Root folder not found", + "status_folder_save_failed": "Failed to save root folder", + "status_folder_updated": "Root folder updated", + "status_folders_subtitle": "Manage root folders, free space, and scan status", + "status_folders_title": "Root Folders", "status_healthy": "Gesund", "status_idle": "Leerlauf", "status_imported": "Importiert", + "status_materializing": "Materialisierung", + "status_insights_subtitle": "Issues detected across your library and media servers", + "status_insights_title": "Storage Insights", + "status_libraries_subtitle": "Manage your media libraries and their root folders", + "status_libraries_title": "Libraries", "status_matched": "Zugeordnet", + "status_media_subtitle": "Browse all media with stats, quality info, and playback data", + "status_media_title": "Media Explorer", "status_noResults": "Keine Ergebnisse", "status_paused": "Pausiert", "status_queued": "In Warteschlange", @@ -3458,17 +3627,20 @@ "status_rejected": "Abgelehnt", "status_removed": "Entfernt", "status_running": "Läuft", + "status_scan_started": "Scan started", "status_searchError": "Suchfehler", "status_searching": "Suche", "status_seeding": "Seeding", "status_stopped": "Gestoppt", "status_streaming": "Streamen", "status_success": "Erfolg", + "status_sync_failed": "Sync failed", "status_tooltipDegraded": "{count} kürzliche Fehler. Letzter: {time}", "status_tooltipDisabled": "Download-Client ist vom Benutzer deaktiviert", "status_tooltipHealthy": "Download-Client ist gesund und erreichbar", "status_tooltipUnhealthy": "{count} aufeinanderfolgende Fehler. Letzter: {time}", "status_unhealthy": "Ungesund", + "status_unknown_error": "Unknown error", "status_unmatched": "Nicht zugeordnet", "status_warning": "Warnung", "streamingProfileInfo": "Streaming-Profile verwenden externe Quellen, daher sind Größenlimits nicht konfigurierbar.", @@ -3551,8 +3723,8 @@ "subtitleProviders_table_featureHashMatching": "Hash-Abgleich", "subtitleProviders_table_featureLanguageMatching": "Sprachabgleich", "subtitleProviders_table_featureProviderApi": "Anbieter-API", - "subtitleProviders_table_featureSubtitleSearch": "Untertitelsuche", "subtitleProviders_table_features": "Funktionen", + "subtitleProviders_table_featureSubtitleSearch": "Untertitelsuche", "subtitleProviders_table_moveDown": "Nach unten", "subtitleProviders_table_moveUp": "Nach oben", "subtitleProviders_table_name": "Name", @@ -3609,6 +3781,9 @@ "task_desc_subtitleUpgrade": "Suche nach besser bewerteten Untertiteln, um bestehende zu ersetzen", "task_desc_updateStrmUrls": "Aktualisiere alle .strm-Dateien mit der aktuellen Streaming-Basis-URL. Führen Sie dies nach Änderung Ihrer Serveradresse, des Ports oder der Domain aus.", "task_desc_upgrade": "Suche nach besseren Qualitäts-Releases, um bestehende Dateien zu ersetzen", + "task_history_empty": "Noch keine Ausführungshistorie", + "task_history_loading": "Verlauf wird geladen...", + "task_history_loadingActivity": "Aktivität wird geladen...", "task_historyModal_added": "Hinzugefügt: {count}", "task_historyModal_close": "Schließen", "task_historyModal_details": "Details", @@ -3627,9 +3802,6 @@ "task_historyModal_updated": "Aktualisiert: {count}", "task_historyModal_updatedFiles": "Aktualisierte Dateien: {count}", "task_historyModal_viewFailedItems": "Fehlgeschlagene Elemente anzeigen", - "task_history_empty": "Noch keine Ausführungshistorie", - "task_history_loading": "Verlauf wird geladen...", - "task_history_loadingActivity": "Aktivität wird geladen...", "task_name_cutoffUnmet": "Cutoff-nicht-erfüllt-Suche", "task_name_historyCleanup": "Verlaufsbereinigung", "task_name_libraryScan": "Bibliotheksscan", @@ -3687,8 +3859,10 @@ "tmdb_seasonList_noOverview": "Keine Übersicht verfügbar", "tmdb_watchProviders_notAvailable": "Nicht zum Streamen verfügbar", "toast_activity_adminRequired": "Admin-Zugriff erforderlich", + "toast_activity_clearedFailedQueue": "{count} fehlgeschlagene(n) Download(s) aus Warteschlange gelöscht", "toast_activity_deleteCompleted": "Löschen abgeschlossen. Aktivitätsliste wird in Kürze aktualisiert.", "toast_activity_deletedEntries": "{count} Aktivitätseinträge gelöscht", + "toast_activity_failedToClearFailed": "Fehlgeschlagene Downloads konnten nicht gelöscht werden", "toast_activity_failedToDeleteSelected": "Ausgewählte Aktivitätseinträge konnten nicht gelöscht werden", "toast_activity_failedToLoadActivity": "Aktivität konnte nicht geladen werden", "toast_activity_failedToLoadMore": "Mehr Aktivität konnte nicht geladen werden", @@ -3739,9 +3913,9 @@ "toast_library_movieDetail_movieFilesDeleted": "Filmdateien gelöscht", "toast_library_movieDetail_movieRemoved": "Film aus Bibliothek entfernt", "toast_library_movieDetail_noSuitableReleases": "Keine geeigneten Releases gefunden", + "toast_library_movieDetail_subtitleSynced": "Untertitel synchronisiert", "toast_library_movieDetail_subtitleSyncFailed": "Untertitelsynchronisation fehlgeschlagen", "toast_library_movieDetail_subtitleSyncOffset": "Versatz von {offset}ms angewendet", - "toast_library_movieDetail_subtitleSynced": "Untertitel synchronisiert", "toast_library_movies_autoGrabbed": "Automatisch \"{release}\" für \"{title}\" gegriffen", "toast_library_movies_deletedFilesCount": "Dateien für {count} Filme gelöscht", "toast_library_movies_failedAutoGrab": "Automatisches Greifen für \"{title}\" fehlgeschlagen", @@ -3753,13 +3927,32 @@ "toast_library_movies_filesDeleted": "\"{title}\" Dateien gelöscht", "toast_library_movies_foundNoMatch": "Releases gefunden, aber keine erfüllten die Kriterien für \"{title}\"", "toast_library_movies_grabbed": "\"{title}\" gegriffen", - "toast_library_movies_monitorToggle": "\"{title}\" {status}", "toast_library_movies_monitoringCount": "{count} Filme werden überwacht", + "toast_library_movies_monitorToggle": "\"{title}\" {status}", "toast_library_movies_noReleases": "Keine Releases für \"{title}\" gefunden", "toast_library_movies_qualityUpdatedCount": "Qualitätsprofil für {count} Filme aktualisiert", "toast_library_movies_removedCount": "{count} Filme aus Bibliothek entfernt", "toast_library_movies_removedFromLibrary": "\"{title}\" aus Bibliothek entfernt", "toast_library_movies_unmonitoredCount": "{count} Filme nicht überwacht", + "toast_library_tv_autoGrabbedCount": "Automatisch {count} Release(s) für \"{title}\" gegriffen", + "toast_library_tv_deletedFilesCount": "Dateien für {count} Serien gelöscht", + "toast_library_tv_failedAutoGrab": "Automatisches Greifen für \"{title}\" fehlgeschlagen", + "toast_library_tv_failedToDelete": "Löschen fehlgeschlagen", + "toast_library_tv_failedToDeleteSeries": "Serie konnte nicht gelöscht werden", + "toast_library_tv_failedToGrab": "Release konnte nicht gegriffen werden", + "toast_library_tv_failedToUpdate": "Serie konnte nicht aktualisiert werden", + "toast_library_tv_failedToUpdateSeries": "Serie konnte nicht aktualisiert werden", + "toast_library_tv_failedToUpdateSingle": "Aktualisierung fehlgeschlagen", + "toast_library_tv_filesDeleted": "\"{title}\" Dateien gelöscht", + "toast_library_tv_foundNoMatch": "{count} Releases gefunden, aber keine erfüllten die Kriterien für \"{title}\"", + "toast_library_tv_grabbed": "\"{title}\" gegriffen", + "toast_library_tv_monitoringCount": "{count} Serien werden überwacht", + "toast_library_tv_monitorToggle": "\"{title}\" {status}", + "toast_library_tv_noReleases": "Keine Releases für \"{title}\" gefunden", + "toast_library_tv_qualityUpdatedCount": "Qualitätsprofil für {count} Serien aktualisiert", + "toast_library_tv_removedCount": "{count} Serien aus Bibliothek entfernt", + "toast_library_tv_removedFromLibrary": "\"{title}\" aus Bibliothek entfernt", + "toast_library_tv_unmonitoredCount": "{count} Serien nicht überwacht", "toast_library_tvDetail_bulkSearchFailed": "Massensuche fehlgeschlagen", "toast_library_tvDetail_bulkSubAutoSearchFailed": "Massen-Untertitel-Autosuche fehlgeschlagen", "toast_library_tvDetail_bulkSyncError": "Massensynchronisation fehlgeschlagen", @@ -3796,29 +3989,10 @@ "toast_library_tvDetail_seriesRefreshFailed": "Serienaktualisierung fehlgeschlagen", "toast_library_tvDetail_seriesRemoved": "Serie aus Bibliothek entfernt", "toast_library_tvDetail_subtitleDeleted": "Untertitel gelöscht", + "toast_library_tvDetail_subtitleSynced": "Untertitel synchronisiert", "toast_library_tvDetail_subtitleSyncFailed": "Untertitelsynchronisation fehlgeschlagen", "toast_library_tvDetail_subtitleSyncOffset": "Versatz von {offset}ms angewendet", - "toast_library_tvDetail_subtitleSynced": "Untertitel synchronisiert", "toast_library_tvDetail_syncedSubtitles": "{count} Untertitel synchronisiert", - "toast_library_tv_autoGrabbedCount": "Automatisch {count} Release(s) für \"{title}\" gegriffen", - "toast_library_tv_deletedFilesCount": "Dateien für {count} Serien gelöscht", - "toast_library_tv_failedAutoGrab": "Automatisches Greifen für \"{title}\" fehlgeschlagen", - "toast_library_tv_failedToDelete": "Löschen fehlgeschlagen", - "toast_library_tv_failedToDeleteSeries": "Serie konnte nicht gelöscht werden", - "toast_library_tv_failedToGrab": "Release konnte nicht gegriffen werden", - "toast_library_tv_failedToUpdate": "Serie konnte nicht aktualisiert werden", - "toast_library_tv_failedToUpdateSeries": "Serie konnte nicht aktualisiert werden", - "toast_library_tv_failedToUpdateSingle": "Aktualisierung fehlgeschlagen", - "toast_library_tv_filesDeleted": "\"{title}\" Dateien gelöscht", - "toast_library_tv_foundNoMatch": "{count} Releases gefunden, aber keine erfüllten die Kriterien für \"{title}\"", - "toast_library_tv_grabbed": "\"{title}\" gegriffen", - "toast_library_tv_monitorToggle": "\"{title}\" {status}", - "toast_library_tv_monitoringCount": "{count} Serien werden überwacht", - "toast_library_tv_noReleases": "Keine Releases für \"{title}\" gefunden", - "toast_library_tv_qualityUpdatedCount": "Qualitätsprofil für {count} Serien aktualisiert", - "toast_library_tv_removedCount": "{count} Serien aus Bibliothek entfernt", - "toast_library_tv_removedFromLibrary": "\"{title}\" aus Bibliothek entfernt", - "toast_library_tv_unmonitoredCount": "{count} Serien nicht überwacht", "toast_library_unmatched_bulkDeleteNotImpl": "Massenlöschung noch nicht implementiert", "toast_library_unmatched_failedToDelete": "Datei konnte nicht gelöscht werden", "toast_library_unmatched_fileDeletedFromDisk": "Datei von Festplatte gelöscht", @@ -3852,6 +4026,7 @@ "ui_deleteModal_warningRemoveOnly": "Der Eintrag wird aus der Bibliothek entfernt, aber die Dateien bleiben erhalten.", "ui_deleteModal_warningUnmatch": "Die Zuordnung zwischen Dateien und Bibliothekseintrag wird aufgehoben.", "ui_failedToChangeLanguage": "Sprache konnte nicht geändert werden", + "ui_languageLabel": "Sprache", "ui_mediaType_movie": "Film", "ui_mediaType_stream": "Stream", "ui_mediaType_tv": "TV", @@ -3869,7 +4044,6 @@ "ui_operationProgress_pending": "Ausstehend", "ui_operationProgress_processing": "Verarbeitung...", "ui_operationProgress_running": "Läuft", - "ui_languageLabel": "Sprache", "ui_selectLanguage": "Sprache auswählen", "ui_selectTheme": "Theme auswählen", "ui_themeDark": "Dunkel", @@ -3931,14 +4105,14 @@ "unmatched_libraryIssues_noMovieFolders": "Keine Film-Stammordner konfiguriert.", "unmatched_libraryIssues_noTvFolders": "Keine TV-Stammordner konfiguriert.", "unmatched_libraryIssues_rootFolderNotSet": "Stammordner nicht festgelegt", - "unmatched_libraryIssues_rootFolderUpdateFailed": "Stammordner konnte nicht aktualisiert werden", "unmatched_libraryIssues_rootFolderUpdated": "Stammordner aktualisiert", + "unmatched_libraryIssues_rootFolderUpdateFailed": "Stammordner konnte nicht aktualisiert werden", "unmatched_libraryIssues_selectAllMovies": "Alle Filme auswählen", "unmatched_libraryIssues_selectAllTv": "Alle Serien auswählen", + "unmatched_libraryIssues_selectedCount": "{count} ausgewählt", "unmatched_libraryIssues_selectItemsFirst": "Wähle Elemente aus, um die Massenaktion anzuwenden", "unmatched_libraryIssues_selectRootFolder": "Stammordner auswählen", "unmatched_libraryIssues_selectRootFolderFirst": "Wähle zuerst einen Stammordner", - "unmatched_libraryIssues_selectedCount": "{count} ausgewählt", "unmatched_libraryIssues_title": "Bibliotheksprobleme", "unmatched_libraryIssues_tv": "TV", "unmatched_libraryIssues_tvShows": "Serien", @@ -3947,34 +4121,16 @@ "unmatched_pagination_perPage50": "50 pro Seite", "unmatched_pagination_showing": "Zeige {start} - {end} von {total} Dateien", "validation_maxChars": "Max. {max} Zeichen.", - "blocklist_tab_releases": "Releases", - "blocklist_tab_blockedMedia": "Blockierte Medien", - "blocklist_tab_blockedKeywords": "Blockierte Schlüsselwörter", - "blockedMedia_pageTitle": "Blockierte Medien", - "blockedMedia_pageSubtitle": "Blockierte Filme und Serien verwalten, die in Entdeckung, Suche und Bibliothek ausgeblendet sind", - "blockedMedia_emptyTitle": "Keine blockierten Medien", - "blockedMedia_emptyDescription": "Blockierte Filme und Serien werden hier angezeigt.", - "blockedMedia_searchPlaceholder": "Blockierte Medien suchen...", - "blockedMedia_filterAll": "Alle", - "blockedMedia_filterMovies": "Filme", - "blockedMedia_filterTV": "Serien", - "blockedMedia_entryCount": "{total} Einträge", - "blockedMedia_unblock": "Entblockieren", - "blockedMedia_confirmUnblockTitle": "Medium entblockieren", - "blockedMedia_confirmUnblockMessage": "\"{title}\" entblockieren? Es wird wieder in Entdeckung und Suche angezeigt, aber nicht erneut zur Bibliothek hinzugefügt.", - "blockedMedia_confirmUnblockLabel": "Entblockieren", - "blockedMedia_confirmBlockTitle": "Medium blockieren", - "blockedMedia_confirmBlockMessage": "\"{title}\" blockieren? Es wird in Entdeckung, Suche und Bibliothek ausgeblendet.", - "blockedMedia_confirmBlockLabel": "Blockieren", - "blockedMedia_blocked": "\"{title}\" wurde blockiert", - "blockedMedia_unblocked": "\"{title}\" wurde entblockiert", - "blockedMedia_alreadyBlocked": "Dieses Medium ist bereits blockiert", - "blockedMedia_mediaTypeMovie": "Film", - "blockedMedia_mediaTypeTV": "Serie", - "blockedMedia_dateBlocked": "Blockiert am {date}", - "blockedMedia_noReason": "Kein Grund", - "hero_blockMedia": "Medium blockieren", - "hero_blockMediaTooltip": "In Entdeckung und Bibliothek blockieren", - "library_blockMedia": "Entfernen und blockieren", - "library_blockMediaTooltip": "Aus Bibliothek entfernen und blockieren" + "downloadClient_realdebrid": "Real-Debrid", + "downloadClient_torbox": "TorBox", + "downloadClient_protocol_debrid": "Debrid", + "downloadClient_apiToken": "API-Token", + "downloadClient_removeAfterImport": "Nach dem Import beim Anbieter entfernen", + "settings_integrations_downloadClients_debrid": "Debrid", + "acquisition_acquireVia": "Beziehen über", + "acquisition_default": "Standard", + "acquisition_torrent": "Torrent", + "acquisition_debrid": "Debrid", + "acquisition_debridUnavailableReason": "Fügen Sie einen Debrid-Client mit API-Token hinzu und aktivieren Sie ihn, um Debrid zu verwenden.", + "acquisition_defaultPreference": "Standardmäßiger Torrent-Bezug" } diff --git a/messages/en.json b/messages/en.json index ee2a4f70f..f52a9dbba 100644 --- a/messages/en.json +++ b/messages/en.json @@ -76,7 +76,6 @@ "action_upload": "Upload", "action_view": "View", "activity_activeCount": "{total} active downloads", - "activity_statusCount": "{total} {status}", "activity_activeFilters_clearAll": "Clear All", "activity_activeFilters_client": "Client", "activity_activeFilters_from": "From", @@ -88,6 +87,8 @@ "activity_activeFilters_toSeparator": "to", "activity_activeFilters_until": "Until", "activity_activeFilters_upgradesOnly": "Upgrades Only", + "activity_detail_confirmRemoveFromClient": "Remove from Client", + "activity_detail_deleteRecordWarning": "This will permanently delete this activity record.", "activity_detail_downloadPaused": "Download paused", "activity_detail_downloadRemoved": "Download removed", "activity_detail_downloadResumed": "Download resumed", @@ -96,12 +97,13 @@ "activity_detail_failedToRemove": "Failed to remove", "activity_detail_failedToResume": "Failed to resume", "activity_detail_failedToRetry": "Failed to retry", - "activity_detail_importRetryInitiated": "Import retry initiated", "activity_detail_importedTo": "Imported to", + "activity_detail_importRetryInitiated": "Import retry initiated", "activity_detail_indexer": "Indexer", "activity_detail_mediaType": "Media Type", "activity_detail_protocol": "Protocol", "activity_detail_releaseGroup": "Release Group", + "activity_detail_removeFromClientWarning": "This will permanently remove the download from your download client.", "activity_detail_retryImport": "Retry Import", "activity_detail_statusReason": "Status Reason", "activity_detail_taskType": "Task Type", @@ -131,7 +133,8 @@ "activity_filters_torrent": "Torrent", "activity_filters_upgradesOnly": "Upgrades only", "activity_filters_usenet": "Usenet", - "activity_historyCount": "{total} history activities", + "activity_history_alsoRemoveFromClient": "Also remove from download client", + "activity_history_alsoRemoveFromClientHint": "{count} failed download(s) still in client", "activity_history_confirmDeleteSelectedMessage": "Delete {count} selected history entries? This cannot be undone.", "activity_history_confirmDeleteSelectedTitle": "Delete Selected History", "activity_history_confirmPurgeAllMessage": "Permanently delete all activity history? This cannot be undone.", @@ -154,11 +157,17 @@ "activity_history_selectRows": "Select Rows", "activity_history_skippedActiveDownloads": "Skipped {count} active downloads", "activity_history_skippedRetryableFailed": "Skipped {count} retryable/failed items", + "activity_historyCount": "{total} history activities", "activity_importFailed": "Import Failed", "activity_pageTitle": "Activity - Cinephage", - "activity_queueStats_total": "Total", + "activity_popover_media": "Media:", + "activity_popover_reason": "Reason:", + "activity_popover_release": "Release:", + "activity_popover_viewInActivity": "View in Activity", + "activity_popover_when": "When:", "activity_queue_bulkActionsHeading": "Bulk Actions", - "activity_queue_bulkHint": "Select queue rows to bulk pause, resume, retry failed, or remove failed downloads.", + "activity_queue_bulkHint": "Select queue items to bulk pause or resume downloads.", + "activity_queue_clearAllFailed": "Clear All Failed", "activity_queue_clearSelection": "Clear Selection", "activity_queue_confirmPauseMessage": "Pause {count} queue items?", "activity_queue_confirmPauseTitle": "Pause Selected Downloads", @@ -172,6 +181,8 @@ "activity_queue_exitSelection": "Exit Selection", "activity_queue_pauseCount": "Pause ({count})", "activity_queue_pauseSelected": "Pause Selected", + "activity_queue_refresh": "Refresh", + "activity_queue_relinkOrphans": "Re-link Orphans", "activity_queue_removeFailed": "Remove Failed", "activity_queue_removeFailedCount": "Remove Failed ({count})", "activity_queue_resumeCount": "Resume ({count})", @@ -181,6 +192,7 @@ "activity_queue_selectAllCount": "Select All ({count})", "activity_queue_selectRows": "Select Rows", "activity_queue_skippedItems": "(skipped {count} ineligible)", + "activity_queueStats_total": "Total", "activity_relativeTime_daysAgo": "{count}d ago", "activity_relativeTime_hoursAgo": "{count}h ago", "activity_relativeTime_justNow": "Just now", @@ -188,42 +200,34 @@ "activity_resolutionAuto": "Auto", "activity_retryMode_importInProgress": "Import retry in progress", "activity_retryMode_waitingForPath": "Waiting for download path before import retry", + "activity_statusCount": "{total} {status}", "activity_subtitle": "Download and search history", "activity_tabActive": "Active Downloads", "activity_tabHistory": "History", "activity_table_failedToAction": "Failed to {action}", + "activity_table_group": "Group", "activity_table_hideReason": "Hide reason", "activity_table_hideTimeline": "Hide timeline", + "activity_table_importedTo": "Imported to:", "activity_table_media": "Media", "activity_table_noActivity": "No activity found", "activity_table_noActivityHint": "Try adjusting your filters or check back later", + "activity_table_quality": "Quality", "activity_table_reason": "Reason", "activity_table_release": "Release", "activity_table_selectAllVisible": "Select all visible", "activity_table_showReason": "Show reason", "activity_table_showTimeline": "Show timeline", + "activity_table_source": "Source", + "activity_table_timeline": "Timeline", + "activity_table_upgrade": "Upgrade:", + "activity_tag_download": "Download", + "activity_tag_list": "List", + "activity_tag_media": "Media", + "activity_tag_move": "Move", + "activity_tag_sub": "Sub", "activity_taskError": "{task} Search Error", "activity_title": "Activity", - "calendar_addToLibrary": "Add to Library", - "calendar_comingUp": "Coming Up", - "calendar_inLibrary": "In Library", - "calendar_more": "+{count} more", - "calendar_noUpcoming": "Nothing coming up", - "calendar_today": "Today", - "calendar_viewAll": "View Calendar", - "calendar_prefs_show": "Show", - "calendar_prefs_all": "All", - "calendar_prefs_options": "Options", - "calendar_prefs_libraryOnly": "Library movies only", - "calendar_prefs_upcomingNonLibrary": "Show new releases in Coming Up", - "calendar_prefs_minRating": "Min. rating", - "calendar_prefs_contentRating": "Content rating", - "calendar_prefs_allRatings": "All", - "calendar_prefs_excludeAdult": "Exclude adult", - "calendar_prefs_genres": "Genres", - "calendar_prefs_anyRating": "Any", - "calendar_prefs_allGenres": "All genres", - "calendar_airingNow": "Airing now", "allFormatScoresZero": "All format scores will start at 0", "allowUpgrades": "Allow Upgrades", "aria_deleteClient": "Delete client", @@ -234,12 +238,112 @@ "auth_blankToKeep": "blank to keep", "auth_password_label": "Password", "auth_username_label": "Username", + "blockedMedia_alreadyBlocked": "This media is already blocked", + "blockedMedia_blocked": "\"{title}\" has been blocked", + "blockedMedia_confirmBlockLabel": "Block", + "blockedMedia_confirmBlockMessage": "Block \"{title}\"? It will be hidden from discover, search, and your library.", + "blockedMedia_confirmBlockTitle": "Block Media", + "blockedMedia_confirmUnblockLabel": "Unblock", + "blockedMedia_confirmUnblockMessage": "Unblock \"{title}\"? It will appear in discover and search again, but won't be re-added to your library.", + "blockedMedia_confirmUnblockTitle": "Unblock Media", + "blockedMedia_dateBlocked": "Blocked {date}", + "blockedMedia_emptyDescription": "Blocked movies and TV shows will appear here.", + "blockedMedia_emptyTitle": "No blocked media", + "blockedMedia_entryCount": "{total} items", + "blockedMedia_filterAll": "All Types", + "blockedMedia_filterMovies": "Movies", + "blockedMedia_filterTV": "TV Shows", + "blockedMedia_mediaTypeMovie": "Movie", + "blockedMedia_mediaTypeTV": "TV", + "blockedMedia_noReason": "No reason", + "blockedMedia_pageSubtitle": "Manage blocked movies and TV shows that are hidden from discover, search, and library", + "blockedMedia_pageTitle": "Blocked Media", + "blockedMedia_searchPlaceholder": "Search blocked media...", + "blockedMedia_unblock": "Unblock", + "blockedMedia_unblocked": "\"{title}\" has been unblocked", + "blocklist_addedToBlocklist": "Release added to blocklist", + "blocklist_bulkDeleteMessage": "Remove {count} entries from the blocklist?", + "blocklist_confirmBlockLabel": "Block", + "blocklist_confirmBlockMessage": "Block this release from being downloaded again?", + "blocklist_confirmBlockTitle": "Block Release", + "blocklist_confirmBulkDeleteTitle": "Remove Selected", + "blocklist_confirmPurgeLabel": "Purge", + "blocklist_confirmPurgeMessage": "Remove all expired blocklist entries? This cannot be undone.", + "blocklist_confirmPurgeTitle": "Purge Expired Entries", + "blocklist_confirmRemoveLabel": "Remove", + "blocklist_confirmRemoveMessage": "Remove this release from the blocklist? It may be grabbed again by monitoring.", + "blocklist_confirmRemoveTitle": "Remove from Blocklist", + "blocklist_daysLeft": "{days}d left", + "blocklist_deleteSelected": "Delete Selected", + "blocklist_duration_1hour": "1 hour", + "blocklist_duration_1week": "1 week", + "blocklist_duration_24hours": "24 hours", + "blocklist_duration_6hours": "6 hours", + "blocklist_duration_72hours": "3 days", + "blocklist_duration_expired": "Expired", + "blocklist_duration_permanent": "Permanent", + "blocklist_emptyDescription": "Blocked releases will appear here when downloads fail repeatedly.", + "blocklist_emptyTitle": "No blocklisted releases", + "blocklist_entriesRemoved": "{count} entries removed", + "blocklist_entryCount": "{total} entries", + "blocklist_entryRemoved": "Entry removed from blocklist", + "blocklist_expiredPurged": "Expired entries purged", + "blocklist_expiryUpdated": "Block expiry updated", + "blocklist_filterActiveOnly": "Active only", + "blocklist_filterAllProtocols": "All Protocols", + "blocklist_filterAllReasons": "All Reasons", + "blocklist_hoursLeft": "{hours}h left", + "blocklist_pageSubtitle": "Manage blocked releases that won't be re-downloaded", + "blocklist_pageTitle": "Blocked Releases", + "blocklist_purgeExpired": "Purge Expired", + "blocklist_reason_badRelease": "Bad Release", + "blocklist_reason_downloadFailed": "Download Failed", + "blocklist_reason_duplicate": "Duplicate", + "blocklist_reason_importFailed": "Import Failed", + "blocklist_reason_manual": "Manual", + "blocklist_reason_qualityMismatch": "Quality Mismatch", + "blocklist_searchPlaceholder": "Search releases...", + "blocklist_selected": "{selectedCount} selected", + "blocklist_tab_blockedKeywords": "Blocked Keywords", + "blocklist_tab_blockedMedia": "Blocked Media", + "blocklist_tab_releases": "Blocked Releases", + "blocklist_tableActions": "Actions", + "blocklist_tableAdded": "Added", + "blocklist_tableEditExpiry": "Edit Expiry", + "blocklist_tableExpires": "Expires", + "blocklist_tableMessage": "Message", + "blocklist_tableProtocol": "Protocol", + "blocklist_tableReason": "Reason", + "blocklist_tableRemoveEntry": "Remove {title}", + "blocklist_tableSelectAll": "Select all", + "blocklist_tableSelectEntry": "Select {title}", + "blocklist_tableTitle": "Title", "builtInProfiles": "Built-in Profiles", "bulkActions_delete": "Delete", "bulkActions_disable": "Disable", "bulkActions_enable": "Enable", "bulkActions_selected": "{count} selected", "bulkActions_test": "Test", + "calendar_addToLibrary": "Add to Library", + "calendar_airingNow": "Airing now", + "calendar_comingUp": "Coming Up", + "calendar_inLibrary": "In Library", + "calendar_more": "+{count} more", + "calendar_noUpcoming": "Nothing coming up", + "calendar_prefs_all": "All", + "calendar_prefs_allGenres": "All genres", + "calendar_prefs_allRatings": "All", + "calendar_prefs_anyRating": "Any", + "calendar_prefs_contentRating": "Content rating", + "calendar_prefs_excludeAdult": "Exclude adult", + "calendar_prefs_genres": "Genres", + "calendar_prefs_libraryOnly": "Library movies only", + "calendar_prefs_minRating": "Min. rating", + "calendar_prefs_options": "Options", + "calendar_prefs_show": "Show", + "calendar_prefs_upcomingNonLibrary": "Show new releases in Coming Up", + "calendar_today": "Today", + "calendar_viewAll": "View Calendar", "channels": "channels", "collapseAll": "Collapse All", "common_actions": "Actions", @@ -249,12 +353,18 @@ "common_all": "All", "common_alpha": "Alpha", "common_alphaShort": "A", + "common_announced": "Announced", "common_appName": "Cinephage", "common_available": "Available", + "common_availableDigital": "Available - Digital", + "common_availablePhysical": "Available - Physical", + "common_availableStreaming": "Available - Streaming", "common_browseFolders": "Browse folders", "common_builtIn": "Built-in", "common_categories": "Categories", "common_clientBehavior": "Client Behavior", + "common_comingSoon": "Coming Soon", + "common_comingToTheaters": "Coming to Theaters in {days} days", "common_connecting": "Connecting...", "common_connection": "Connection", "common_copiedToClipboard": "Copied to clipboard", @@ -264,6 +374,7 @@ "common_deleting": "Deleting...", "common_description": "Description", "common_details": "Details", + "common_digitalInDays": "Digital in {days} days", "common_disabled": "Disabled", "common_disconnected": "Disconnected", "common_downloaded": "Downloaded", @@ -272,8 +383,8 @@ "common_enabled": "Enabled", "common_endOfResults": "End of results", "common_episode": "Episode", - "common_episodeSize": "Episode Size", "common_episodes": "Episodes", + "common_episodeSize": "Episode Size", "common_failedToCopy": "Failed to copy", "common_failedToSave": "Failed to save", "common_file": "File", @@ -282,6 +393,7 @@ "common_hide": "Hide", "common_host": "Host", "common_inactive": "Inactive", + "common_inTheaters": "In Theaters", "common_language": "Language", "common_languages": "Languages", "common_live": "Live", @@ -290,21 +402,22 @@ "common_monitored": "Monitored", "common_mountMode": "Altmount / NZBDav (Mount Mode)", "common_movie": "Movie", - "common_movieSize": "Movie Size", "common_movies": "Movies", + "common_movieSize": "Movie Size", "common_na": "N/A", "common_name": "Name", "common_network": "Network", "common_never": "Never", "common_no": "No", - "common_noResults": "No results found", "common_none": "None", + "common_noResults": "No results found", "common_notSet": "Not set", "common_off": "Off", "common_on": "On", "common_overview": "Overview", "common_path": "Path", "common_pending": "Pending", + "common_physicalInDays": "Physical in {days} days", "common_port": "Port", "common_priority": "Priority", "common_protocol": "Protocol", @@ -312,6 +425,7 @@ "common_qualityProfile": "Quality profile", "common_rating": "Rating", "common_reconnecting": "Reconnecting...", + "common_released": "Released", "common_retry": "Retry", "common_rootFolder": "Root folder", "common_runtime": "Runtime", @@ -329,6 +443,7 @@ "common_size": "Size", "common_standard": "Standard SABnzbd", "common_status": "Status", + "common_streamingInDays": "Streaming in {days} days", "common_syncing": "Syncing", "common_testConnection": "Test connection", "common_testing": "Testing...", @@ -341,17 +456,6 @@ "common_unknown": "Unknown", "common_unmonitored": "Unmonitored", "common_unreleased": "Unreleased", - "common_inTheaters": "In Theaters", - "common_released": "Released", - "common_availableDigital": "Available - Digital", - "common_availablePhysical": "Available - Physical", - "common_availableStreaming": "Available - Streaming", - "common_comingToTheaters": "Coming to Theaters in {days} days", - "common_digitalInDays": "Digital in {days} days", - "common_physicalInDays": "Physical in {days} days", - "common_streamingInDays": "Streaming in {days} days", - "common_announced": "Announced", - "common_comingSoon": "Coming Soon", "common_usenet": "Usenet", "common_viewAll": "View All", "common_year": "Year", @@ -407,6 +511,7 @@ "dashboard_stats_filesNeedAttention": "Files need attention", "dashboard_stats_freeSpace": "{size} free", "dashboard_stats_ignoredCount": "{count} ignored", + "dashboard_stats_inTheatersCount": "{count} in theaters", "dashboard_stats_missingCount": "{count} missing", "dashboard_stats_missingEpisodes": "Missing Episodes", "dashboard_stats_missingEpisodesShort": "Missing Eps", @@ -422,26 +527,25 @@ "dashboard_stats_unairedCount": "{count} unaired", "dashboard_stats_unmatched": "Unmatched", "dashboard_stats_unreleasedCount": "{count} unreleased", - "dashboard_stats_inTheatersCount": "{count} in theaters", "dashboard_subtitle": "Welcome to Cinephage", "dashboard_title": "Dashboard", "discover_applyFilters": "Apply Filters", + "discover_certificationTvNote": "Content rating filtering only applies to movies.", "discover_clearFilters": "Clear Filters", "discover_clearSearch": "Clear search", "discover_closeFilters": "Close Filters", "discover_failedToLoadMore": "Failed to load more items", - "discover_filterBadgeGenres": "{count} Genres", - "discover_filterBadgeLanguage": "Language", - "discover_filterBadgeMovies": "Movies", - "discover_filterBadgeTvShows": "TV Shows", + "discover_filter_contentRating": "Content Rating", "discover_filter_mediaType": "Media Type", "discover_filter_minRating": "Min Rating", - "discover_filter_contentRating": "Content Rating", - "discover_certificationTvNote": "Content rating filtering only applies to movies.", "discover_filter_originalLanguage": "Original Language", "discover_filter_releaseYear": "Release Year", "discover_filter_sortBy": "Sort By", "discover_filter_watchProviders": "Watch Providers", + "discover_filterBadgeGenres": "{count} Genres", + "discover_filterBadgeLanguage": "Language", + "discover_filterBadgeMovies": "Movies", + "discover_filterBadgeTvShows": "TV Shows", "discover_filters": "Filters", "discover_filtersButton": "Filters", "discover_heading": "Discover", @@ -485,6 +589,22 @@ "discover_tv_seasons": "Seasons", "discover_tv_similarTitles": "Similar Titles", "discover_tv_topCast": "Top Cast", + "downloadClient_addDownloadClient": "Add Download Client", + "downloadClient_apiKeyHelp": "Found in SABnzbd Config > General > API Key", + "downloadClient_aria2": "aria2", + "downloadClient_deluge": "Deluge", + "downloadClient_desc_nntp": "Direct Usenet server connection for NZB streaming", + "downloadClient_desc_qbittorrent": "Desc qbittorrent", + "downloadClient_editDownloadClient": "Edit Download Client", + "downloadClient_nzbget": "NZBGet", + "downloadClient_protocol_nntp": "NNTP", + "downloadClient_protocol_torrent": "Torrent", + "downloadClient_protocol_usenet": "Usenet", + "downloadClient_qbittorrent": "qBittorrent", + "downloadClient_rtorrent": "rTorrent", + "downloadClient_sabnzbd": "SABnzbd", + "downloadClient_selectType": "Select the type of download client you want to add:", + "downloadClient_transmission": "Transmission", "downloadClientModal_addTitle": "Add Download Client", "downloadClientModal_categoriesSection": "Categories", "downloadClientModal_clientPath": "Client Path", @@ -502,18 +622,9 @@ "downloadClientModal_portLabel": "Port", "downloadClientModal_recentPriority": "Recent", "downloadClientModal_selectType": "Select the type of download client you want to add:", - "downloadClientModal_startAs": "Start As", - "downloadClientModal_tempFolder": "Temporary Download Folder", - "downloadClientModal_tvCategory": "TV Shows", - "downloadClientModal_urlBaseDescription": "Path prefix added after host and port.", - "downloadClientModal_urlBaseLabel": "URL Base", - "downloadClientModal_useUrlBase": "Use URL Base", - "downloadClient_addDownloadClient": "Add Download Client", - "downloadClient_apiKeyHelp": "Found in SABnzbd Config > General > API Key", - "downloadClient_aria2": "aria2", - "downloadClient_deluge": "Deluge", - "downloadClient_desc_nntp": "Direct Usenet server connection for NZB streaming", - "downloadClient_desc_qbittorrent": "Desc qbittorrent", + "downloadClientModal_stalledBlocklist": "Blocklist duration (hours)", + "downloadClientModal_stalledBlocklistDescription": "Removed releases are blocklisted for {hours}h.", + "downloadClientModal_stalledBlocklistPermanent": "Removed releases are blocklisted permanently.", "downloadClientModal_stalledDescription": "Auto-remove stalled downloads after {timeout}m if progress is at or below {threshold}%.", "downloadClientModal_stalledDisabled": "Stalled download handling is disabled.", "downloadClientModal_stalledDownloads": "Stalled downloads", @@ -521,16 +632,12 @@ "downloadClientModal_stalledProgress": "Progress threshold (%)", "downloadClientModal_stalledSaved": "Saved", "downloadClientModal_stalledTimeout": "Timeout (min)", - "downloadClient_editDownloadClient": "Edit Download Client", - "downloadClient_nzbget": "NZBGet", - "downloadClient_protocol_nntp": "NNTP", - "downloadClient_protocol_torrent": "Torrent", - "downloadClient_protocol_usenet": "Usenet", - "downloadClient_qbittorrent": "qBittorrent", - "downloadClient_rtorrent": "rTorrent", - "downloadClient_sabnzbd": "SABnzbd", - "downloadClient_selectType": "Select the type of download client you want to add:", - "downloadClient_transmission": "Transmission", + "downloadClientModal_startAs": "Start As", + "downloadClientModal_tempFolder": "Temporary Download Folder", + "downloadClientModal_tvCategory": "TV Shows", + "downloadClientModal_urlBaseDescription": "Path prefix added after host and port.", + "downloadClientModal_urlBaseLabel": "URL Base", + "downloadClientModal_useUrlBase": "Use URL Base", "episodeMax": "Episode Max (MB)", "episodeMin": "Episode Min (MB)", "error_description": "Cinephage hit an unexpected problem while loading this view. You can retry, head back home, or include the error ID if you file a GitHub issue for debugging.", @@ -543,7 +650,6 @@ "error_supportIdLabel": "Error ID", "error_supportIdUnavailable": "Unavailable", "expandAll": "Expand All", - "formatScoresCopied": "Format scores will be copied from the selected profile", "formats": "Format Scores", "formats_addCondition": "Add Condition", "formats_allCategories": "All Categories", @@ -561,10 +667,10 @@ "formats_conditionLogicTitle": "Condition Logic:", "formats_conditionNamePlaceholder": "Condition name", "formats_conditionRemoveAria": "Remove condition", - "formats_conditionTypeLabel": "Type", - "formats_conditionValueLabel": "Value", "formats_conditionsDivider": "Conditions", "formats_conditionsHeader": "Conditions", + "formats_conditionTypeLabel": "Type", + "formats_conditionValueLabel": "Value", "formats_createCustomFormat": "Create Custom Format", "formats_createFormat": "Create Format", "formats_custom": "Custom", @@ -597,8 +703,37 @@ "formats_testNotMatched": "Not matched", "formats_testPlaceholder": "Movie.2024.2160p.BluRay.REMUX.HEVC.TrueHD.Atmos-GROUP", "formats_viewFormat": "View Format", + "formatScoresCopied": "Format scores will be copied from the selected profile", "general": "General", - "indexer_desc_automaticSearch": "Search when items are added", + "hero_addToLibrary": "Add to Library", + "hero_available": "Available", + "hero_blockMedia": "Block Media", + "hero_blockMediaTooltip": "Block from discover and library", + "hero_failedToCheckLibraryStatus": "Failed to check library status", + "hero_metadata_budget": "Budget", + "hero_metadata_episodes": "Episodes", + "hero_metadata_firstAired": "First Aired", + "hero_metadata_language": "Language", + "hero_metadata_network": "Network", + "hero_metadata_nextEpisode": "Next Episode", + "hero_metadata_rated": "Rated", + "hero_metadata_released": "Released", + "hero_metadata_revenue": "Revenue", + "hero_metadata_seasons": "Seasons", + "hero_metadata_status": "Status", + "hero_metadata_studio": "Studio", + "hero_metadata_whereToWatch": "Where to Watch", + "hero_monitored": "Monitored", + "hero_releaseType_digital": "Digital", + "hero_releaseType_limitedTheatrical": "Limited Theatrical", + "hero_releaseType_physical": "Physical", + "hero_releaseType_premiere": "Premiere", + "hero_releaseType_theatrical": "Theatrical", + "hero_releaseType_tv": "TV", + "hero_trailer": "Trailer", + "hero_viewInLibrary": "View in Library", + "hero_website": "Website", + "indexer_desc_automaticSearch": "Automatic searches and scheduled tasks", "indexer_desc_interactiveSearch": "Manual searches from UI", "indexer_desc_rejectDeadTorrents": "Skip releases with 0 seeders", "indexer_label_automaticSearch": "Automatic Search", @@ -612,14 +747,18 @@ "library_add_autoDownloadSubtitlesYes": "Add auto download subtitles yes", "library_add_episodeCount": "{count} episodes", "library_add_monitor": "Monitor", - "library_add_monitorNewItems": "Monitor new items", - "library_add_monitorSpecials": "Monitor specials", - "library_add_monitorSpecialsNo": "Don't monitor specials", - "library_add_monitorSpecialsYes": "Monitor specials", "library_add_monitoredDescNo": "Not monitored - new episodes won't be downloaded", "library_add_monitoredDescYes": "Monitored - new episodes will be downloaded", "library_add_monitoredEps": "Monitored Episodes", "library_add_monitoringPreview": "Monitoring Preview", + "library_add_monitorNewItems": "Monitor new items", + "library_add_monitorSpecials": "Monitor specials", + "library_add_monitorSpecialsNo": "Don't monitor specials", + "library_add_monitorSpecialsYes": "Monitor specials", + "library_add_movie_monitoredDescNo": "Not monitored - won't be downloaded", + "library_add_movie_monitoredDescYes": "Monitored - will be downloaded when available", + "library_add_movie_searchImmediatelyDescNo": "Don't search for the movie after adding", + "library_add_movie_searchImmediatelyDescYes": "Search for the movie after adding", "library_add_noRootFoldersConfigured": "No root folders configured", "library_add_notMonitored": "Not Monitored", "library_add_rootFolderDesc": "Files will be organized in this folder", @@ -664,11 +803,11 @@ "library_batchMatch_failedToGeneratePreview": "Failed to generate match preview", "library_batchMatch_failedToMatchFiles": "Failed to match files", "library_batchMatch_filesFailed": "{count} files failed", + "library_batchMatch_matchedFiles": "Matched {count} files", "library_batchMatch_matchFiles": "Match Files", + "library_batchMatch_matchingFiles": "Matching files...", "library_batchMatch_matchPreview": "Match Preview", "library_batchMatch_matchPreviewLoading": "Generating preview...", - "library_batchMatch_matchedFiles": "Matched {count} files", - "library_batchMatch_matchingFiles": "Matching files...", "library_batchMatch_searchFailed": "Search failed", "library_batchMatch_searchForMovie": "Search for movie...", "library_batchMatch_searchForTvShow": "Search for TV show...", @@ -677,6 +816,8 @@ "library_batchMatch_sizeMB": "{size} MB", "library_batchMatch_title": "Batch Match Files", "library_batchMatch_unknownSize": "Unknown size", + "library_blockMedia": "Remove and Block", + "library_blockMediaTooltip": "Remove from library and block", "library_bulkActionBar_autoDownloadSubs": "Auto-download subtitles", "library_bulkActionBar_autoDownloadTitle": "Download Subtitles", "library_bulkActionBar_episodesSelected": "{count} episodes selected", @@ -727,6 +868,7 @@ "library_editMovie_monitoredDesc": "Monitor this movie for new releases", "library_editMovie_qualityProfileDesc": "Quality profile to use for this movie", "library_editMovie_title": "Edit Movie", + "library_episodeProgress": "{episodeFileCount} of {episodeCount} episodes", "library_episodeRow_audioLabel": "Audio: {codec} ({channels}ch)", "library_episodeRow_autoGrabBest": "Auto-grab best", "library_episodeRow_deleteEpisode": "Delete episode", @@ -770,6 +912,7 @@ "library_folderBrowser_noSubdirectories": "No subdirectories", "library_folderBrowser_selectFolder": "Select Folder", "library_import_alreadyImported": "This group is already imported in this session.", + "library_import_andNMore": "and {count} more", "library_import_applyDestinationToMedia": "Apply Destination to Media", "library_import_applyMatchToSeason": "Apply Match to Season", "library_import_autoDetect": "Auto Detect", @@ -789,6 +932,7 @@ "library_import_cancelImportStay": "Stay", "library_import_cancelImportTitle": "Cancel Import?", "library_import_clearDetectedSearch": "Clear detected item search", + "library_import_clearSelection": "Clear", "library_import_clearTmdbSearch": "Clear TMDB search", "library_import_confidenceMatch": "{percent}% match", "library_import_configureSelectedItem": "Configure Selected Item", @@ -797,10 +941,11 @@ "library_import_createNewHint": "Creates library entry if needed and imports into selected root folder.", "library_import_destinationNotNeededForExistingMedia": "All items in this media selection are matched to existing library entries; destination selection is not required.", "library_import_destinationRootFolder": "Destination Root Folder", - "library_import_detectMedia": "Detect Media", "library_import_detectedItems": "Detected Items", "library_import_detectedItemsHint": "Review and select items from this queue for import.", "library_import_detecting": "Detecting", + "library_import_detectMedia": "Detect Media", + "library_import_detectSelected": "Detect Selected ({count})", "library_import_directImportFor": "This is a direct import for", "library_import_episodeCount": "{count} episodes", "library_import_episodeCountSingular": "{count} episode", @@ -822,17 +967,25 @@ "library_import_heading": "Import Media", "library_import_importAnother": "Import Another", "library_import_importComplete": "Import Complete", + "library_import_importedCount": "{count} imported", + "library_import_importedItems": "Imported Items", + "library_import_importedPathLabel": "Imported Path:", "library_import_importFailed": "Import Failed", + "library_import_importing": "Importing...", + "library_import_importingIntoExisting": "Importing into existing library item for:", + "library_import_importMethodCopy": "Copy", + "library_import_importMethodCopyDesc": "Keep original files in place", + "library_import_importMethodHeading": "Import Method", + "library_import_importMethodMove": "Move", + "library_import_importMethodMoveDesc": "Delete originals after import", + "library_import_importMethodSymlink": "Symlink", + "library_import_importMethodSymlinkDesc": "Create symbolic links to originals", "library_import_importNextDetected": "Import Next Detected Item ({count})", "library_import_importOpenedFor": "This import was opened for", "library_import_importSelectionHeading": "Import Selection", "library_import_importSelectionHint": "Import actions run from this step. Finalize your selected items below and start import.", "library_import_importTargetHeading": "Import Target", "library_import_importTargetHint": "Select whether to import into an existing library item or create a new one.", - "library_import_importedCount": "{count} imported", - "library_import_importedPathLabel": "Imported Path:", - "library_import_importing": "Importing...", - "library_import_importingIntoExisting": "Importing into existing library item for:", "library_import_inLibrary": "In library", "library_import_itemSkipped": "This item is skipped and excluded from bulk import.", "library_import_loadingFolders": "Loading folders...", @@ -870,19 +1023,20 @@ "library_import_seasonOverrideHint": "Applied only to files missing a detected season.", "library_import_seasonOverrideLabel": "Season Override (optional)", "library_import_seasonOverridePlaceholder": "Use when filenames are missing season numbers", - "library_import_seasonUnmapped": "Season Unmapped", "library_import_seasonsDetectedInline": "Seasons: {seasons}", "library_import_seasonsLabel": "seasons: {seasons}", - "library_import_selectItem": "Select Item", - "library_import_selectRootFolder": "Select root folder", - "library_import_selectSeason": "Select Season", + "library_import_seasonUnmapped": "Season Unmapped", "library_import_selectedCount": "{count} selected", "library_import_selectedItem": "Selected Item", "library_import_selectedItemsHeading": "Selected Items", + "library_import_selectItem": "Select Item", + "library_import_selectRootFolder": "Select root folder", + "library_import_selectSeason": "Select Season", + "library_import_showRootFolders": "Show root folders", "library_import_skipItem": "Skip Item", - "library_import_skipSeason": "Skip Season", "library_import_skippedCount": "{count} skipped", "library_import_skippedOfTotal": "{skipped} of {total} skipped", + "library_import_skipSeason": "Skip Season", "library_import_source": "Source", "library_import_sourcePathHintFile": "Select the media file to import for this movie.", "library_import_sourcePathHintGeneral": "Select the path to your media file or folder.", @@ -908,7 +1062,6 @@ "library_import_tvShowLabel": "TV Show", "library_import_unmatchedTvSeries": "Unmatched TV Series", "library_import_useFolder": "Use Folder", - "library_import_showRootFolders": "Show root folders", "library_import_viewInLibrary": "View in Library", "library_import_viewLastImported": "View Last Imported", "library_libraryMediaTable_addedColumn": "Added", @@ -939,9 +1092,9 @@ "library_matchFile_clickToMatch": "Click to match file", "library_matchFile_errorMatching": "Error matching file", "library_matchFile_failedToMatch": "Failed to match file", - "library_matchFile_matchTo": "Match to {title}", "library_matchFile_matchedTo": "Matched to {title}", "library_matchFile_matchedToEpisode": "Matched to {title} - {episode}", + "library_matchFile_matchTo": "Match to {title}", "library_matchFile_searchFailed": "Search failed", "library_matchFile_searchHint": "Search TMDB to find a match", "library_matchFile_searchPlaceholder": "Search TMDB...", @@ -954,8 +1107,8 @@ "library_matchFolder_fileCount_other": "{count} files", "library_matchFolder_filesFailed": "{count} files failed", "library_matchFolder_filesWillBeMatched": "{count} files will be matched", - "library_matchFolder_matchFiles": "Match Files", "library_matchFolder_matchedFiles": "Matched {count} files", + "library_matchFolder_matchFiles": "Match Files", "library_matchFolder_searchFailed": "Search failed", "library_matchFolder_searchHint": "Search TMDB to find a match", "library_matchFolder_searchPlaceholder": "Search TMDB...", @@ -1018,8 +1171,8 @@ "library_movieDetail_monitoringEnabled": "Monitoring enabled.", "library_movieDetail_moveQueued": "Move queued. File transfer has started and will appear in Activity until completion.", "library_movieDetail_originalTitle": "Original Title", - "library_movieDetail_overviewHeading": "Overview", "library_movieDetail_otherMoviesInCollection": "Other movies in this collection", + "library_movieDetail_overviewHeading": "Overview", "library_movieDetail_pageTitle": "{title} - Library - Cinephage", "library_movieDetail_path": "Path", "library_movieDetail_rename": "Rename", @@ -1039,8 +1192,8 @@ "library_movieFilesTab_searchDownloads": "Search for Downloads", "library_movieFilesTab_unknownTotalSize": "Unknown total size", "library_movieHeader_autoGrab": "Auto-grab", - "library_movieHeader_backToLibrary": "Back to ", "library_movieHeader_autoGrabTooltip": "Automatically search and grab movie", + "library_movieHeader_backToLibrary": "Back to ", "library_movieHeader_failed": "Failed", "library_movieHeader_grabbed": "Grabbed", "library_movieHeader_imdbLink": "IMDb", @@ -1085,6 +1238,7 @@ "library_movies_noFilterMatchHint": "Try adjusting your filters to see more results.", "library_movies_noSearchMatch": "No movies match \"{query}\"", "library_movies_ofTotal": "of {total}", + "library_movies_other": "Other", "library_movies_pageTitle": "Movies - Library - Cinephage", "library_movies_profileDefault": "{name} (Default)", "library_movies_searchPlaceholder": "Search Movies…", @@ -1093,10 +1247,10 @@ "library_movies_selectAllShort": "All", "library_movies_sortAddedAsc": "Date Added (Oldest)", "library_movies_sortAddedDesc": "Date Added (Newest)", - "library_movies_sortSizeAsc": "Size (Smallest)", - "library_movies_sortSizeDesc": "Size (Largest)", "library_movies_sortCollectionAsc": "Collection (A-Z)", "library_movies_sortCollectionDesc": "Collection (Z-A)", + "library_movies_sortSizeAsc": "Size (Smallest)", + "library_movies_sortSizeDesc": "Size (Largest)", "library_movies_sortTitleAsc": "Title (A-Z)", "library_movies_sortTitleDesc": "Title (Z-A)", "library_movies_sortYearAsc": "Year (Oldest)", @@ -1104,9 +1258,8 @@ "library_movies_switchToGrid": "Switch to grid view", "library_movies_switchToList": "Switch to list view", "library_movies_tryDifferentSearch": "Try a different search term.", - "library_movies_viewGrouped": "Grouped", - "library_movies_other": "Other", "library_movies_unmonitorAll": "Unmonitor All", + "library_movies_viewGrouped": "Grouped", "library_renamePreview_allCorrect": "All file names are correct", "library_renamePreview_change": "Change", "library_renamePreview_collisionBadge": "Collision", @@ -1174,9 +1327,9 @@ "library_scoreDetail_streamingServiceLabel": "Streaming Service", "library_scoreDetail_title": "Score Details", "library_scoreDetail_total": "Total", - "library_scoreDetail_upgradeStatus": "Upgrade Status", "library_scoreDetail_upgradesDisabled": "Upgrades disabled", "library_scoreDetail_upgradesEnabled": "Upgrades enabled", + "library_scoreDetail_upgradeStatus": "Upgrade Status", "library_seasonAccordion_actionsColumn": "Actions", "library_seasonAccordion_airDateColumn": "Air Date", "library_seasonAccordion_autoDownloadSubs": "Auto-download subtitles", @@ -1244,32 +1397,6 @@ "library_seriesType_dailyLabel": "Daily", "library_seriesType_standardDesc": "Standard TV series with season/episode numbering", "library_seriesType_standardLabel": "Standard", - "library_tvDetail_deleteEpisodeTitle": "Delete Episode", - "library_tvDetail_deleteSeasonTitle": "Delete Season", - "library_tvDetail_deleteSeriesTitle": "Delete Series", - "library_tvDetail_done": "Done", - "library_tvDetail_episodeFallback": "Episode {number}", - "library_tvDetail_exitSelectionMode": "Exit selection mode", - "library_tvDetail_metaDescription": "Manage {title} in your library", - "library_tvDetail_monitoringDisabled": "Monitoring is disabled.", - "library_tvDetail_monitoringDisabledHint": "Season and episode toggles are locked. Enable series monitoring to unlock them.", - "library_tvDetail_monitoringEnabled": "Series monitoring is enabled.", - "library_tvDetail_noSeasonsFound": "No seasons found", - "library_tvDetail_overviewHeading": "Overview", - "library_tvDetail_pageTitle": "{title} - Library - Cinephage", - "library_tvDetail_rename": "Rename", - "library_tvDetail_renameFilesTitle": "Rename files", - "library_tvDetail_seasonFallback": "Season {number}", - "library_tvDetail_seasonFolders": "Season Folders", - "library_tvDetail_seasonsHeading": "Seasons", - "library_tvDetail_select": "Select", - "library_tvDetail_selectEpisodes": "Select episodes", - "library_tvDetail_sseConnecting": "Connecting", - "library_tvDetail_sseLive": "Live", - "library_tvDetail_sseOffline": "Offline", - "library_tvDetail_sseReconnecting": "Reconnecting", - "library_tvDetail_syncSubtitles": "Sync Subtitles", - "library_tvDetail_tvdbId": "TVDB ID", "library_tv_clearFilters": "Clear Filters", "library_tv_clearSearch": "Clear search", "library_tv_clearSearchBtn": "Clear Search", @@ -1327,6 +1454,32 @@ "library_tv_switchToList": "Switch to list view", "library_tv_tryDifferentSearch": "Try a different search term.", "library_tv_unmonitorAll": "Unmonitor All", + "library_tvDetail_deleteEpisodeTitle": "Delete Episode", + "library_tvDetail_deleteSeasonTitle": "Delete Season", + "library_tvDetail_deleteSeriesTitle": "Delete Series", + "library_tvDetail_done": "Done", + "library_tvDetail_episodeFallback": "Episode {number}", + "library_tvDetail_exitSelectionMode": "Exit selection mode", + "library_tvDetail_metaDescription": "Manage {title} in your library", + "library_tvDetail_monitoringDisabled": "Monitoring is disabled.", + "library_tvDetail_monitoringDisabledHint": "Season and episode toggles are locked. Enable series monitoring to unlock them.", + "library_tvDetail_monitoringEnabled": "Series monitoring is enabled.", + "library_tvDetail_noSeasonsFound": "No seasons found", + "library_tvDetail_overviewHeading": "Overview", + "library_tvDetail_pageTitle": "{title} - Library - Cinephage", + "library_tvDetail_rename": "Rename", + "library_tvDetail_renameFilesTitle": "Rename files", + "library_tvDetail_seasonFallback": "Season {number}", + "library_tvDetail_seasonFolders": "Season Folders", + "library_tvDetail_seasonsHeading": "Seasons", + "library_tvDetail_select": "Select", + "library_tvDetail_selectEpisodes": "Select episodes", + "library_tvDetail_sseConnecting": "Connecting", + "library_tvDetail_sseLive": "Live", + "library_tvDetail_sseOffline": "Offline", + "library_tvDetail_sseReconnecting": "Reconnecting", + "library_tvDetail_syncSubtitles": "Sync Subtitles", + "library_tvDetail_tvdbId": "TVDB ID", "library_unmatched_collapseAll": "Collapse All", "library_unmatched_deleteFileTitle": "Delete File", "library_unmatched_expandAll": "Expand All", @@ -1352,6 +1505,32 @@ "livetv_accountModal_streamVerified": "Stream verified", "livetv_accountModal_testSuccess": "Connection successful", "livetv_accountModal_testSuccessWithStatus": "Connection successful • {status}", + "livetv_accounts_addAccount": "Add Account", + "livetv_accounts_connectionTestFailed": "Connection test failed: {name}", + "livetv_accounts_connectionTestPassed": "Connection test passed: {name}", + "livetv_accounts_deleteAccountMessage": "Delete \"{name}\"? This will remove it from Live TV and stop channel sync.", + "livetv_accounts_deleteAccountMessagePrefix": "Are you sure you want to delete ", + "livetv_accounts_deleteAccountMessageSuffix": "? This will remove it from Live TV and stop channel sync.", + "livetv_accounts_deleteAccountTitle": "Delete Account", + "livetv_accounts_epgNotConfigured": "EPG not configured", + "livetv_accounts_epgReachable": "EPG reachable", + "livetv_accounts_epgUnreachable": "EPG unreachable", + "livetv_accounts_epgUnreachableWithError": "EPG unreachable ({error})", + "livetv_accounts_failedToDeleteAccount": "Failed to delete account", + "livetv_accounts_failedToLoadAccounts": "Failed to load accounts", + "livetv_accounts_failedToSaveAccount": "Failed to save account", + "livetv_accounts_failedToSyncAccount": "Failed to sync account", + "livetv_accounts_failedToTestAccount": "Failed to test account", + "livetv_accounts_failedToTestConfig": "Failed to test account configuration", + "livetv_accounts_failedToUpdateAccount": "Failed to update account", + "livetv_accounts_heading": "Live TV Accounts", + "livetv_accounts_invalidTestEndpointResponse": "Invalid response from test endpoint", + "livetv_accounts_invalidTestResponse": "Invalid test response", + "livetv_accounts_pageTitle": "Live TV Accounts - Cinephage", + "livetv_accounts_subtitle": "Manage your IPTV accounts (Stalker, XStream, M3U)", + "livetv_accounts_testSummaryCategories": "{count} categories", + "livetv_accounts_testSummaryChannels": "{count} channels", + "livetv_accounts_unknownTestFailure": "Unknown test failure", "livetv_accountTable_accountColumn": "Account", "livetv_accountTable_actionsColumn": "Actions", "livetv_accountTable_addHint": "Add an IPTV account to get started", @@ -1380,36 +1559,10 @@ "livetv_accountTable_syncingStatus": "Syncing...", "livetv_accountTable_typeColumn": "Type", "livetv_accountTable_urlSource": "URL Source", - "livetv_accounts_addAccount": "Add Account", - "livetv_accounts_connectionTestFailed": "Connection test failed: {name}", - "livetv_accounts_connectionTestPassed": "Connection test passed: {name}", - "livetv_accounts_deleteAccountMessage": "Delete \"{name}\"? This will remove it from Live TV and stop channel sync.", - "livetv_accounts_deleteAccountMessagePrefix": "Are you sure you want to delete ", - "livetv_accounts_deleteAccountMessageSuffix": "? This will remove it from Live TV and stop channel sync.", - "livetv_accounts_deleteAccountTitle": "Delete Account", - "livetv_accounts_epgNotConfigured": "EPG not configured", - "livetv_accounts_epgReachable": "EPG reachable", - "livetv_accounts_epgUnreachable": "EPG unreachable", - "livetv_accounts_epgUnreachableWithError": "EPG unreachable ({error})", - "livetv_accounts_failedToDeleteAccount": "Failed to delete account", - "livetv_accounts_failedToLoadAccounts": "Failed to load accounts", - "livetv_accounts_failedToSaveAccount": "Failed to save account", - "livetv_accounts_failedToSyncAccount": "Failed to sync account", - "livetv_accounts_failedToTestAccount": "Failed to test account", - "livetv_accounts_failedToTestConfig": "Failed to test account configuration", - "livetv_accounts_failedToUpdateAccount": "Failed to update account", - "livetv_accounts_heading": "Live TV Accounts", - "livetv_accounts_invalidTestEndpointResponse": "Invalid response from test endpoint", - "livetv_accounts_invalidTestResponse": "Invalid test response", - "livetv_accounts_pageTitle": "Live TV Accounts - Cinephage", - "livetv_accounts_subtitle": "Manage your IPTV accounts (Stalker, XStream, M3U)", - "livetv_accounts_testSummaryCategories": "{count} categories", - "livetv_accounts_testSummaryChannels": "{count} channels", - "livetv_accounts_unknownTestFailure": "Unknown test failure", "livetv_channelBrowserModal_add": "Add", + "livetv_channelBrowserModal_added": "Added", "livetv_channelBrowserModal_addEntireCategory": "Add Category", "livetv_channelBrowserModal_addSelected": "Add Selected", - "livetv_channelBrowserModal_added": "Added", "livetv_channelBrowserModal_allAccounts": "All Accounts", "livetv_channelBrowserModal_allCategories": "All Categories", "livetv_channelBrowserModal_allChannelsAdded": "All channels added", @@ -1551,19 +1704,14 @@ "livetv_channelRemoveModal_removeMultiple": "Remove {count} channels from your lineup?", "livetv_channelRemoveModal_removeSingle": "Remove \"{name}\" from your lineup?", "livetv_channelRemoveModal_title": "Remove Channel", - "livetv_channelScheduleModal_epgFrom": "EPG from {source}", - "livetv_channelScheduleModal_failedToLoad": "Failed to load schedule", - "livetv_channelScheduleModal_liveBadge": "LIVE", - "livetv_channelScheduleModal_noPrograms": "No programs scheduled", - "livetv_channelScheduleModal_percentRemaining": "{percent}% remaining", "livetv_channels_appliedCleanNames": "Applied cleaned names to {count} channels", "livetv_channels_browseChannels": "Browse Channels", "livetv_channels_categoriesButton": "Categories", "livetv_channels_copyEpgUrl": "Copy EPG URL", "livetv_channels_copyM3uUrl": "Copy M3U URL", + "livetv_channels_downloadedLogos": "{count} logos downloaded", "livetv_channels_downloadFailed": "Download failed", "livetv_channels_downloadLogosTitle": "Download channel logos", - "livetv_channels_downloadedLogos": "{count} logos downloaded", "livetv_channels_emptyDescription": "Add channels from your accounts to build your lineup", "livetv_channels_emptyHeading": "No channels in your lineup", "livetv_channels_exportButton": "Export", @@ -1598,6 +1746,19 @@ "livetv_channels_subtitle": "Organize your channel lineup", "livetv_channels_updatedCategory": "Updated category for {count} channels", "livetv_channels_xmltvEpgGuide": "XMLTV EPG Guide", + "livetv_channelScheduleModal_epgFrom": "EPG from {source}", + "livetv_channelScheduleModal_failedToLoad": "Failed to load schedule", + "livetv_channelScheduleModal_liveBadge": "LIVE", + "livetv_channelScheduleModal_noPrograms": "No programs scheduled", + "livetv_channelScheduleModal_percentRemaining": "{percent}% remaining", + "livetv_epg_failedToCancelSync": "Failed to cancel EPG sync", + "livetv_epg_failedToTriggerSync": "Failed to trigger EPG sync", + "livetv_epg_heading": "EPG", + "livetv_epg_pageTitle": "EPG - Live TV - Cinephage", + "livetv_epg_subtitle": "Electronic Program Guide", + "livetv_epg_tabCoverage": "Coverage", + "livetv_epg_tabGuide": "Guide", + "livetv_epg_tabStatus": "Status", "livetv_epgCoverage_currentProgram": "Current Program", "livetv_epgCoverage_default": "Default", "livetv_epgCoverage_doubleClickToChange": "Double-click to change EPG source", @@ -1642,29 +1803,6 @@ "livetv_epgSourcePicker_programsCount": "{count} programs", "livetv_epgSourcePicker_searchPlaceholder": "Search channels...", "livetv_epgSourcePicker_title": "Select EPG Source", - "livetv_epgStatusPanel_accountsHeading": "Accounts", - "livetv_epgStatusPanel_cancelAccountTooltip": "Cancel sync for this account", - "livetv_epgStatusPanel_cancelRequestedTooltip": "Cancel requested", - "livetv_epgStatusPanel_cancelSync": "Cancel Sync", - "livetv_epgStatusPanel_cancelling": "Cancelling...", - "livetv_epgStatusPanel_errorTitle": "Error", - "livetv_epgStatusPanel_errorTitlePlural": "Errors", - "livetv_epgStatusPanel_lastSync": "Last sync", - "livetv_epgStatusPanel_moreAccountIssues": "+{count} more issues", - "livetv_epgStatusPanel_moreAccountWarnings": "+{count} more warnings", - "livetv_epgStatusPanel_nextSync": "Next sync", - "livetv_epgStatusPanel_noAccounts": "No accounts configured", - "livetv_epgStatusPanel_noEpgDataMessage": "No EPG data available. Configure accounts with EPG sources.", - "livetv_epgStatusPanel_syncAccountTooltip": "Sync this account", - "livetv_epgStatusPanel_syncAll": "Sync All", - "livetv_epgStatusPanel_syncInProgress": "Sync in progress", - "livetv_epgStatusPanel_syncRunningTooltip": "Sync is running", - "livetv_epgStatusPanel_syncedRelative": "Synced {time} ago", - "livetv_epgStatusPanel_syncingAll": "Syncing all accounts...", - "livetv_epgStatusPanel_totalPrograms": "{count} programs", - "livetv_epgStatusPanel_unknownError": "Unknown error", - "livetv_epgStatusPanel_warningTitle": "Warning", - "livetv_epgStatusPanel_warningTitlePlural": "Warnings", "livetv_epgStatus_accountDetails": "Account Details", "livetv_epgStatus_daysAgo": "{count}d ago", "livetv_epgStatus_errorBadge": "Error", @@ -1683,18 +1821,33 @@ "livetv_epgStatus_noEpgTooltip": "EPG not configured for this account", "livetv_epgStatus_programsLabel": "Programs", "livetv_epgStatus_syncButton": "Sync Now", - "livetv_epgStatus_syncTooltip": "Sync EPG data from all sources", "livetv_epgStatus_syncing": "Syncing...", + "livetv_epgStatus_syncTooltip": "Sync EPG data from all sources", "livetv_epgStatus_title": "EPG Status", "livetv_epgStatus_unableToLoad": "Unable to load EPG status", - "livetv_epg_failedToCancelSync": "Failed to cancel EPG sync", - "livetv_epg_failedToTriggerSync": "Failed to trigger EPG sync", - "livetv_epg_heading": "EPG", - "livetv_epg_pageTitle": "EPG - Live TV - Cinephage", - "livetv_epg_subtitle": "Electronic Program Guide", - "livetv_epg_tabCoverage": "Coverage", - "livetv_epg_tabGuide": "Guide", - "livetv_epg_tabStatus": "Status", + "livetv_epgStatusPanel_accountsHeading": "Accounts", + "livetv_epgStatusPanel_cancelAccountTooltip": "Cancel sync for this account", + "livetv_epgStatusPanel_cancelling": "Cancelling...", + "livetv_epgStatusPanel_cancelRequestedTooltip": "Cancel requested", + "livetv_epgStatusPanel_cancelSync": "Cancel Sync", + "livetv_epgStatusPanel_errorTitle": "Error", + "livetv_epgStatusPanel_errorTitlePlural": "Errors", + "livetv_epgStatusPanel_lastSync": "Last sync", + "livetv_epgStatusPanel_moreAccountIssues": "+{count} more issues", + "livetv_epgStatusPanel_moreAccountWarnings": "+{count} more warnings", + "livetv_epgStatusPanel_nextSync": "Next sync", + "livetv_epgStatusPanel_noAccounts": "No accounts configured", + "livetv_epgStatusPanel_noEpgDataMessage": "No EPG data available. Configure accounts with EPG sources.", + "livetv_epgStatusPanel_syncAccountTooltip": "Sync this account", + "livetv_epgStatusPanel_syncAll": "Sync All", + "livetv_epgStatusPanel_syncedRelative": "Synced {time} ago", + "livetv_epgStatusPanel_syncingAll": "Syncing all accounts...", + "livetv_epgStatusPanel_syncInProgress": "Sync in progress", + "livetv_epgStatusPanel_syncRunningTooltip": "Sync is running", + "livetv_epgStatusPanel_totalPrograms": "{count} programs", + "livetv_epgStatusPanel_unknownError": "Unknown error", + "livetv_epgStatusPanel_warningTitle": "Warning", + "livetv_epgStatusPanel_warningTitlePlural": "Warnings", "livetv_form_m3u_autoRefresh": "Auto-refresh", "livetv_form_m3u_enabled": "Enabled", "livetv_form_m3u_epgUrlHint": "URL for Electronic Program Guide data (optional)", @@ -1753,9 +1906,9 @@ "livetv_iptvOrg_loadingCountries": "Loading countries...", "livetv_iptvOrg_removeCountry": "Remove {country}", "livetv_iptvOrg_searchPlaceholder": "Search countries...", - "livetv_iptvOrg_selectHint": "Select countries to add channels", "livetv_iptvOrg_selectedCount": "{count} countries selected", "livetv_iptvOrg_selectedCountSingular": "1 country selected", + "livetv_iptvOrg_selectHint": "Select countries to add channels", "livetv_portalScanModal_addNewPortal": "Add New Portal", "livetv_portalScanModal_backButton": "Back", "livetv_portalScanModal_cancelButton": "Cancel", @@ -1789,8 +1942,8 @@ "livetv_portalScanModal_portalUrlPlaceholder": "https://portal.example.com", "livetv_portalScanModal_randomScan": "Random Scan", "livetv_portalScanModal_rangeSize": "Range Size", - "livetv_portalScanModal_scanTypeLabel": "Scan Type", "livetv_portalScanModal_scanningPortalLabel": "Scanning Portal", + "livetv_portalScanModal_scanTypeLabel": "Scan Type", "livetv_portalScanModal_selectExistingPortal": "Select Existing Portal", "livetv_portalScanModal_sequentialScan": "Sequential Scan", "livetv_portalScanModal_startMacLabel": "Start MAC", @@ -1813,8 +1966,8 @@ "livetv_portalScanProgress_scanCancelled": "Scan Cancelled", "livetv_portalScanProgress_scanComplete": "Scan Complete", "livetv_portalScanProgress_scanFailed": "Scan Failed", - "livetv_portalScanProgress_scanType": "Scan Type", "livetv_portalScanProgress_scanningPortal": "Scanning {name}", + "livetv_portalScanProgress_scanType": "Scan Type", "livetv_portalScanProgress_tested": "Tested", "livetv_portalScanProgress_timeRemaining": "Time Remaining", "livetv_portalScanProgress_total": "Total", @@ -1828,9 +1981,9 @@ "livetv_scanResults_accountCol": "Account", "livetv_scanResults_actionsCol": "Actions", "livetv_scanResults_allTab": "All", + "livetv_scanResults_approvedTab": "Approved", "livetv_scanResults_approveSelected": "Approve Selected", "livetv_scanResults_approveTitle": "Approve Results", - "livetv_scanResults_approvedTab": "Approved", "livetv_scanResults_categoriesCount": "{count} categories", "livetv_scanResults_channelsCol": "Channels", "livetv_scanResults_channelsCount": "{count} channels", @@ -1845,9 +1998,9 @@ "livetv_scanResults_failedToClear": "Failed to clear ignored results", "livetv_scanResults_failedToIgnore": "Failed to ignore", "livetv_scanResults_failedToLoadResults": "Failed to load scan results", + "livetv_scanResults_ignoredTab": "Ignored", "livetv_scanResults_ignoreSelected": "Ignore Selected", "livetv_scanResults_ignoreTitle": "Ignore Results", - "livetv_scanResults_ignoredTab": "Ignored", "livetv_scanResults_macAddressCol": "MAC Address", "livetv_scanResults_noApproved": "No approved results", "livetv_scanResults_noIgnored": "No ignored results", @@ -1875,31 +2028,6 @@ "login_usernameLabel": "Username", "login_usernamePlaceholder": "Enter your username", "login_welcomeBack": "Welcome Back", - "mediaBrowserModal_addTitle": "Add Media Server", - "mediaBrowserModal_apiKeyHint": "Found in Dashboard > API Keys", - "mediaBrowserModal_editTitle": "Edit Media Server", - "mediaBrowserModal_embyDescription": "Personal media server", - "mediaBrowserModal_failedToSave": "Failed to save", - "mediaBrowserModal_hostHint": "Include http:// or https://", - "mediaBrowserModal_jellyfinDescription": "Free and open source media server", - "mediaBrowserModal_localPathPlaceholder": "Local path (e.g., /media)", - "mediaBrowserModal_mediaServerBadge": "Media Server", - "mediaBrowserModal_notificationEvents": "Notification Events", - "mediaBrowserModal_onDelete": "On Delete", - "mediaBrowserModal_onDeleteDesc": "Notify when media is deleted", - "mediaBrowserModal_onImport": "On Import", - "mediaBrowserModal_onImportDesc": "Notify when new media is imported", - "mediaBrowserModal_onRename": "On Rename", - "mediaBrowserModal_onRenameDesc": "Notify when media files are renamed", - "mediaBrowserModal_onUpgrade": "On Upgrade", - "mediaBrowserModal_onUpgradeDesc": "Notify when media is upgraded to better quality", - "mediaBrowserModal_pathMappings": "Path Mappings", - "mediaBrowserModal_pathMappingsHint": "Map local paths to remote paths if your media server runs in Docker or on a different machine.", - "mediaBrowserModal_plexApiKeyHint": "Found in Plex account or server token settings", - "mediaBrowserModal_plexDescription": "Personal media server", - "mediaBrowserModal_remotePathPlaceholder": "Remote path (e.g., /data/media)", - "mediaBrowserModal_selectType": "Select the type of media server you want to add:", - "mediaBrowserModal_to": "to", "mediaBrowser_actions": "Actions", "mediaBrowser_addMapping": "Add Mapping", "mediaBrowser_addServer": "Add Media Server", @@ -1947,6 +2075,50 @@ "mediaBrowser_serverType_plex": "Plex", "mediaBrowser_serverType_plexDesc": "Personal media server", "mediaBrowser_to": "to", + "mediaBrowserModal_addTitle": "Add Media Server", + "mediaBrowserModal_apiKeyHint": "Found in Dashboard > API Keys", + "mediaBrowserModal_editTitle": "Edit Media Server", + "mediaBrowserModal_embyDescription": "Personal media server", + "mediaBrowserModal_failedToSave": "Failed to save", + "mediaBrowserModal_hostHint": "Include http:// or https://", + "mediaBrowserModal_jellyfinDescription": "Free and open source media server", + "mediaBrowserModal_localPathPlaceholder": "Local path (e.g., /media)", + "mediaBrowserModal_mediaServerBadge": "Media Server", + "mediaBrowserModal_notificationEvents": "Notification Events", + "mediaBrowserModal_onDelete": "On Delete", + "mediaBrowserModal_onDeleteDesc": "Notify when media is deleted", + "mediaBrowserModal_onImport": "On Import", + "mediaBrowserModal_onImportDesc": "Notify when new media is imported", + "mediaBrowserModal_onRename": "On Rename", + "mediaBrowserModal_onRenameDesc": "Notify when media files are renamed", + "mediaBrowserModal_onUpgrade": "On Upgrade", + "mediaBrowserModal_onUpgradeDesc": "Notify when media is upgraded to better quality", + "mediaBrowserModal_pathMappings": "Path Mappings", + "mediaBrowserModal_pathMappingsHint": "Map local paths to remote paths if your media server runs in Docker or on a different machine.", + "mediaBrowserModal_plexApiKeyHint": "Found in Plex account or server token settings", + "mediaBrowserModal_plexDescription": "Personal media server", + "mediaBrowserModal_remotePathPlaceholder": "Remote path (e.g., /data/media)", + "mediaBrowserModal_selectType": "Select the type of media server you want to add:", + "mediaBrowserModal_to": "to", + "mediaExplorer_complete": "Complete", + "mediaExplorer_episodes": "episodes", + "mediaExplorer_headerAdded": "Added", + "mediaExplorer_headerLibrary": "Library", + "mediaExplorer_headerPlayback": "Playback", + "mediaExplorer_headerQuality": "Quality", + "mediaExplorer_headerSize": "Size", + "mediaExplorer_headerStatus": "Status", + "mediaExplorer_headerTitle": "Title", + "mediaExplorer_headerType": "Type", + "mediaExplorer_noMediaFound": "No media found", + "mediaExplorer_statusDone": "Done", + "mediaExplorer_statusFileAvailable": "File available", + "mediaExplorer_statusMonitored": "Monitored", + "mediaExplorer_statusNoFile": "No file", + "mediaExplorer_statusUnmonitored": "Unmonitored", + "mediaExplorer_typeAnime": "Anime", + "mediaExplorer_typeMovie": "Movie", + "mediaExplorer_typeTv": "TV", "movieMax": "Movie Max (GB)", "movieMin": "Movie Min (GB)", "naming_preview": "Preview", @@ -1981,29 +2153,37 @@ "nav_animeMovies": "Anime Movies", "nav_animeSeries": "Anime Series", "nav_backupRestore": "Backup & Restore", + "nav_blockedExtensions": "Blocked Extensions", + "nav_blockedKeywords": "Blocked Keywords", + "nav_blockedMedia": "Blocked Media", + "nav_blockedReleases": "Blocked Releases", + "nav_blocklist": "Blocklist", "nav_calendar": "Calendar", "nav_captchaSolver": "Captcha Solver", "nav_channels": "Channels", + "nav_cinephage": "Cinephage", "nav_closeSidebar": "close sidebar", "nav_dashboard": "Dashboard", "nav_discover": "Discover", "nav_downloadClients": "Download Clients", "nav_epg": "EPG", "nav_general": "General", - "nav_globalFilters": "Global Filters", - "nav_blockedExtensions": "Blocked Extensions", + "nav_globalFilters": "Discovery Filters", "nav_home": "Home", "nav_import": "Import", "nav_indexers": "Indexers", "nav_integrations": "Integrations", "nav_languageProfiles": "Language Profiles", + "nav_libraries": "Libraries", "nav_library": "Library", "nav_libraryStorage": "Library & Storage", "nav_liveTv": "Live TV", "nav_logs": "Logs", + "nav_mediaExplorer": "Media Explorer", "nav_mediaServers": "Media Servers", "nav_mediaStorage": "Media Storage", "nav_metadataProviders": "Metadata Providers", + "nav_monitoring": "Monitoring", "nav_movies": "Movies", "nav_naming": "Naming", "nav_nntpServers": "NNTP Servers", @@ -2011,31 +2191,16 @@ "nav_profile": "Profile", "nav_profiles": "Profiles", "nav_qualitySettings": "Quality Settings", + "nav_rootFolders": "Root Folders", "nav_settings": "Settings", "nav_smartLists": "Smart Lists", + "nav_storageMaintenance": "Storage Maintenance", "nav_streaming": "Streaming", "nav_subtitleProviders": "Subtitle Providers", "nav_system": "System", "nav_tasks": "Tasks", "nav_tvShows": "TV Shows", "nav_unmatchedFiles": "Unmatched Files", - "nntpServerModal_addTitle": "Add Usenet Server", - "nntpServerModal_editTitle": "Edit Usenet Server", - "nntpServerModal_failedToSave": "Failed to save", - "nntpServerModal_maxConnections": "Max Connections", - "nntpServerModal_maxConnectionsHint": "Check your usenet provider for connection limits (usually 10-50)", - "nntpServerModal_priority": "Priority", - "nntpServerModal_priorityHint": "Lower values = higher priority. Use for server failover.", - "nntpServer_addUsenetServer": "Add Usenet Server", - "nntpServer_deleteServer": "Delete server", - "nntpServer_disableServer": "Disable server", - "nntpServer_editServer": "Edit server", - "nntpServer_editUsenetServer": "Edit Usenet Server", - "nntpServer_enableServer": "Enable server", - "nntpServer_maxConnections_help": "Check your usenet provider for connection limits (usually 10-50)", - "nntpServer_maxConnections_label": "Max Connections", - "nntpServer_priority_help": "Lower values = higher priority. Use for server failover.", - "nntpServer_settings_title": "Settings", "nntp_clearFiltersToReorder": "Clear filters to reorder priorities.", "nntp_deleteServer": "Delete server", "nntp_disableServer": "Disable server", @@ -2053,6 +2218,23 @@ "nntp_reorderModeActive": "Reorder mode is active", "nntp_reorderPriorities": "Reorder Priorities", "nntp_testConnection": "Test connection", + "nntpServer_addUsenetServer": "Add Usenet Server", + "nntpServer_deleteServer": "Delete server", + "nntpServer_disableServer": "Disable server", + "nntpServer_editServer": "Edit server", + "nntpServer_editUsenetServer": "Edit Usenet Server", + "nntpServer_enableServer": "Enable server", + "nntpServer_maxConnections_help": "Check your usenet provider for connection limits (usually 10-50)", + "nntpServer_maxConnections_label": "Max Connections", + "nntpServer_priority_help": "Lower values = higher priority. Use for server failover.", + "nntpServer_settings_title": "Settings", + "nntpServerModal_addTitle": "Add Usenet Server", + "nntpServerModal_editTitle": "Edit Usenet Server", + "nntpServerModal_failedToSave": "Failed to save", + "nntpServerModal_maxConnections": "Max Connections", + "nntpServerModal_maxConnectionsHint": "Check your usenet provider for connection limits (usually 10-50)", + "nntpServerModal_priority": "Priority", + "nntpServerModal_priorityHint": "Lower values = higher priority. Use for server failover.", "noFormatsMatch": "No formats match your search", "noMax": "No max", "noMin": "No min", @@ -2067,18 +2249,6 @@ "person_gender_male": "Male", "person_gender_nonBinary": "Non-binary", "person_noBiography": "No biography available.", - "profileModal_allowUpgrades": "Allow Upgrades", - "profileModal_copyFrom": "Copy From", - "profileModal_createTitle": "Create Profile", - "profileModal_descPlaceholder": "Describe what this profile is for...", - "profileModal_description": "Description", - "profileModal_editSizeLimits": "Edit Size Limits", - "profileModal_editTitle": "Edit Profile", - "profileModal_failedToSave": "Failed to save", - "profileModal_name": "Name", - "profileModal_profile": "Profile", - "profileModal_setAsDefault": "Set as Default", - "profileModal_streamingProfileInfo": "Streaming profiles use external sources, so size limits are not configurable.", "profile_accountSecurity": "Account Security", "profile_accountSecurityDescription": "Manage passwords, two-factor authentication, and login sessions.", "profile_comingSoon": "Coming Soon", @@ -2097,6 +2267,18 @@ "profile_singleAdminNote": "For now, Cinephage runs in single-admin mode. All settings are managed through the Settings menu.", "profile_subtitle": "Manage your account settings and preferences", "profile_title": "User Profile", + "profileModal_allowUpgrades": "Allow Upgrades", + "profileModal_copyFrom": "Copy From", + "profileModal_createTitle": "Create Profile", + "profileModal_descPlaceholder": "Describe what this profile is for...", + "profileModal_description": "Description", + "profileModal_editSizeLimits": "Edit Size Limits", + "profileModal_editTitle": "Edit Profile", + "profileModal_failedToSave": "Failed to save", + "profileModal_name": "Name", + "profileModal_profile": "Profile", + "profileModal_setAsDefault": "Set as Default", + "profileModal_streamingProfileInfo": "Streaming profiles use external sources, so size limits are not configurable.", "profiles_addProfile": "Add Profile", "profiles_allowUpgrades_label": "Allow Upgrades", "profiles_preventDowngrades_label": "Prevent resolution downgrades", @@ -2116,6 +2298,7 @@ "profiles_movieMaxSize_label": "Movie Max (GB)", "profiles_movieMinSize_label": "Movie Min (GB)", "profiles_noFormatsMatchSearch": "No formats match your search", + "profiles_preventDowngrades_label": "Prevent resolution downgrades", "profiles_scoredCount": "{count} scored", "profiles_searchFormats": "Search formats...", "profiles_seasonPackSizeInfo": "For season packs, the average size per episode is calculated.", @@ -2150,6 +2333,8 @@ "rootFolders_pathReadable": "Path is readable", "rootFolders_pathValid": "Path is valid", "rootFolders_preserveSymlinksLabel": "Preserve symlinks (for Rclone mounts)", + "rootFolders_preserveSymlinksLockedBadge": "Required by symlink mode", + "rootFolders_preserveSymlinksLockedHint": "Preserve symlinks is required when symlink import mode is active. Disable symlink mode in File Management settings to change this.", "rootFolders_readOnlyAlertDesc": "This folder will be used for cataloging existing content only. Imports and new media will not be written to this folder. Useful for virtual mounts like NZBDav.", "rootFolders_readOnlyAlertTitle": "Read-only mode enabled", "rootFolders_readOnlyLabel": "Read-only folder (catalog only, no imports)", @@ -2163,7 +2348,6 @@ "rootFolders_unknown": "Unknown", "rootFolders_validate": "Validate", "rootFolders_validationFailed": "Path validation failed", - "searchFormats": "Search formats...", "search_actionsCol": "Actions", "search_afterDeduplication": "2. After deduplication:", "search_afterQualityScoring": "4. After quality scoring & smart dedup:", @@ -2174,31 +2358,60 @@ "search_ageMonths": "{count} months", "search_ageWeeks": "{count} weeks", "search_ageYears": "{count} years", + "search_ago": "ago", "search_autoQuality": "Auto", + "search_blockRelease": "Block", + "search_blockUnavailable": "Unable to block this release", + "search_collapseDetails": "Collapse details", + "search_comments": "Comments", "search_debugAllReleases": "All Releases ({count})", "search_debugClickHint": "Click on any release row below to view its detailed JSON here", "search_debugSelected": "Selected: {title}", + "search_directDownload": "Direct Download", "search_displayed": "5. Displayed (after limit):", "search_displayedAfterLimit": "3. Displayed (after limit):", "search_downloadDebugJson": "Download Debug JSON", + "search_enhancement": "Enhancement", "search_enrichTime": "Enrich: {time}ms", + "search_expandDetails": "Expand details", "search_failed": "Failed", + "search_failedBadge": "Failed", "search_freeleech": "Freeleech", + "search_freeleechBadge": "Freeleech", "search_grab": "Grab", - "search_grabButtonTooltip": "Download", "search_grabbed": "Grabbed", + "search_grabbedBadge": "Grabbed", + "search_grabButtonTooltip": "Download", + "search_group": "Group", + "search_hardcodedSubs": "Hardcoded Subs", "search_hideDebugPanel": "Hide Debug Panel", + "search_indexerAndTorrent": "Indexer & Torrent", "search_indexerCol": "Indexer", "search_indexerCount": "{count} indexers", "search_interactiveSearch": "Interactive Search", "search_label_showRejected": "Show rejected", "search_label_sort": "Sort:", + "search_labelCodec": "Codec", + "search_labelHash": "Hash", + "search_labelHdr": "HDR", + "search_labelIndexer": "Indexer", + "search_labelLeechers": "Leechers", + "search_labelProtocol": "Protocol", + "search_labelPublished": "Published", + "search_labelRatio": "Ratio", + "search_labelResolution": "Resolution", + "search_labelSeeders": "Seeders", + "search_labelSize": "Size", + "search_labelSource": "Source", + "search_labelUpload": "Upload", + "search_magnetLink": "Magnet Link", "search_multiPackCandidates": "1. Multi-pack candidates:", "search_multiPackMatches": "{filtered} of {total} multi-pack matches", "search_multiSeasonPackBadge": "Complete Series / Multi-Season Only", "search_multiSeasonPackSearch": "Multi-Season Pack Search", "search_noMultiSeasonPacks": "No multi-season packs found", "search_noResults": "No results found", + "search_pack": "Pack", "search_partialFreeleech": "{percent}% Freeleech", "search_pipeline": "Pipeline", "search_pipelineFiltering": "Filtering Pipeline:", @@ -2206,60 +2419,155 @@ "search_placeholder_filterResults": "Filter results…", "search_qualityRejected": "└ Quality rejected (hidden by default):", "search_qualityRejectedHidden": "2. Quality rejected (hidden by default):", + "search_qualityScore": "Quality score", "search_rawFromIndexers": "1. Raw from indexers:", "search_refresh": "Refresh", "search_rejectedCount": "{count} rejected", + "search_releaseBlocked": "Blocked", "search_releaseCol": "Release", "search_resultsCount": "{filtered} of {total} results", + "search_scoreBreakdown": "Score Breakdown", "search_scoreCol": "Score", "search_scoreLabel": "Score: {score}", - "search_searchResultsPlaceholder": "Search results…", - "search_searchTime": "Search: {time}ms", "search_searched": "Searched:", "search_searchedMultiPack": "Searched (multi-pack matches / raw):", "search_searching": "Searching indexers...", + "search_searchResultsPlaceholder": "Search results…", + "search_searchTime": "Search: {time}ms", "search_showDebugPanel": "Show Debug Panel", "search_showRejected": "Show rejected", "search_sizeCol": "Size", "search_skipped": "Skipped:", "search_slCol": "S/L", + "search_sort_score": "Score", + "search_sort_seeders": "Seeders", + "search_sort_size": "Size", "search_sortAge": "Age", "search_sortScore": "Score", "search_sortSeeders": "Seeders", "search_sortSize": "Size", - "search_sort_score": "Score", - "search_sort_seeders": "Seeders", - "search_sort_size": "Size", "search_stream": "Stream", "search_streamButtonTooltip": "Stream (NNTP)", "search_streamProtocol": "Stream", + "search_technicalDetails": "Technical Details", "search_today": "Today", "search_torrentProtocol": "Torrent", "search_tryIndividualSeason": "Try searching by individual season instead", + "search_unavailable": "Unavailable", "search_uploadMultiplier": "{multiplier}x Upload", "search_usenetProtocol": "Usenet", + "search_viewComments": "View comments", "search_viewDetails": "View details", + "searchFormats": "Search formats...", + "sectionRow_scrollLeft": "Scroll left", + "sectionRow_scrollRight": "Scroll right", "setAsDefault": "Set as Default", + "settings_blocklist_tabsAriaLabel": "Blocklist navigation tabs", + "settings_cinephage_advanced": "Advanced", + "settings_cinephage_commitOverride": "Commit override", + "settings_cinephage_description": "Connect to the Cinephage network for streaming, playback, and future platform features.", + "settings_cinephage_detectedIdentity": "Detected server identity", + "settings_cinephage_externalHost": "Cinephage server address", + "settings_cinephage_externalHostHint": "Your own Cinephage server address (host:port) used by Jellyfin and .strm playback. Do not enter api.cinephage.net here.", + "settings_cinephage_heading": "Cinephage Network", + "settings_cinephage_masterToggle": "Enable Cinephage Network", + "settings_cinephage_moduleEnabled": "Enabled", + "settings_cinephage_modules": "Modules", + "settings_cinephage_moduleSettings": "Settings", + "settings_cinephage_overrideHint": "Leave blank to auto-detect from APP_VERSION / APP_COMMIT env vars.", + "settings_cinephage_saveSettings": "Save", + "settings_cinephage_statusChecking": "Checking...", + "settings_cinephage_statusConnected": "Connected", + "settings_cinephage_statusDisconnected": "Disconnected", + "settings_cinephage_subsystemDisabled": "Disable the Cinephage Network first to manage modules", + "settings_cinephage_testConnection": "Test connection", + "settings_cinephage_useHttps": "Use HTTPS", + "settings_cinephage_versionOverride": "Version override", + "settings_duplicates_all": "All libraries", + "settings_duplicates_both": "Both", + "settings_duplicates_description": "Find and manage duplicate media files across your library using filename or content hash comparison.", + "settings_duplicates_filehash": "Content hash (sha256)", + "settings_duplicates_filename": "Filename (normalized)", + "settings_duplicates_files": "files", + "settings_duplicates_mode": "Detection mode", + "settings_duplicates_no_dupes": "No duplicates found", + "settings_duplicates_off": "Off", + "settings_duplicates_select_library": "Select library", + "settings_duplicates_suppress": "Dismiss", + "settings_duplicates_title": "Duplicate Detection", + "settings_duplicates_unsuppress": "Restore", + "settings_fileManagement_copyDesc": "Files are copied or hardlinked to your library. Original files are kept in their source location.", + "settings_fileManagement_copyLabel": "Copy", + "settings_fileManagement_defaultImportFolderBrowse": "Browse", + "settings_fileManagement_defaultImportFolderClear": "Clear", + "settings_fileManagement_defaultImportFolderDesc": "The manual importer will open to this folder by default. You can still navigate anywhere from there.", + "settings_fileManagement_defaultImportFolderLabel": "Default Import Folder", + "settings_fileManagement_defaultImportFolderPlaceholder": "e.g. /downloads", + "settings_fileManagement_defaultImportFolderSectionDescription": "Skip the folder navigation on every import by starting the file browser in your usual download location.", + "settings_fileManagement_defaultImportFolderSectionTitle": "Default Import Folder", + "settings_fileManagement_deleteEmptyFoldersDesc": "After moving a file, remove any folders that are left empty up to the download root. Has no effect when using Copy/Symlink mode.", + "settings_fileManagement_deleteEmptyFoldersLabel": "Delete Empty Folders", + "settings_fileManagement_diskSpaceSectionDescription": "Protect against imports that would fill your drive.", + "settings_fileManagement_diskSpaceSectionTitle": "Disk Space", + "settings_fileManagement_extraFilesHint": "Extensions to import alongside the video (e.g. .srt, .nfo, .jpg). Leave empty to disable. Each extension must start with a dot.", + "settings_fileManagement_extraFilesLabel": "Extra File Extensions", + "settings_fileManagement_extraFilesSectionDescription": "Import sidecar files alongside the main video when importing from a download client.", + "settings_fileManagement_extraFilesSectionTitle": "Import Extra Files", + "settings_fileManagement_failedToSave": "Failed to save file management settings", + "settings_fileManagement_folderCleanupSectionDescription": "Automatically tidy up the source location after a successful move import.", + "settings_fileManagement_folderCleanupSectionTitle": "Folder Cleanup", + "settings_fileManagement_importMethod": "Import Method", + "settings_fileManagement_minimumFreeSpaceDesc": "Imports are rejected when the destination drive has less than this amount of free space. Set to 0 to disable.", + "settings_fileManagement_minimumFreeSpaceLabel": "Minimum Free Space", + "settings_fileManagement_minimumFreeSpaceUnit": "GB", + "settings_fileManagement_moveDesc": "Files are moved to your library. On the same filesystem, a hardlink is created instantly with no extra disk usage. Across filesystems, the file is copied then deleted from the source.", + "settings_fileManagement_moveLabel": "Move", + "settings_fileManagement_pageDescription": "Control how files are handled when importing media into your library.", + "settings_fileManagement_pageTitle": "File Management", + "settings_fileManagement_permissionsChmodError": "Must be 3 or 4 octal digits (0–7), e.g. 664 or 0664.", + "settings_fileManagement_permissionsChmodPlaceholder": "e.g. 664", + "settings_fileManagement_permissionsCustomDesc": "Apply a specific octal permission to every imported file (e.g. 664 for owner/group read-write, others read-only).", + "settings_fileManagement_permissionsCustomLabel": "Custom chmod", + "settings_fileManagement_permissionsDefaultDesc": "Use the process umask. Files are created with standard permissions determined by your OS and user account.", + "settings_fileManagement_permissionsDefaultLabel": "Default", + "settings_fileManagement_permissionsPreserveDesc": "Inherit permissions from the source files. Useful on multi-user setups where the download client runs as a different user.", + "settings_fileManagement_permissionsPreserveLabel": "Preserve source permissions", + "settings_fileManagement_permissionsSectionDescription": "Control the permissions applied to imported files. Custom chmod takes precedence over Preserve source.", + "settings_fileManagement_permissionsSectionTitle": "File Permissions", + "settings_fileManagement_preferHardlinkDesc": "On the same filesystem, create a hardlink instead of copying. The file appears in both locations instantly with no extra disk usage. Disable if your media server treats hardlinked files as duplicates.", + "settings_fileManagement_preferHardlinkLabel": "Use Hardlinks When Possible", + "settings_fileManagement_recycleBinSectionDescription": "Keep replaced files instead of permanently deleting them when a better version is imported.", + "settings_fileManagement_recycleBinSectionTitle": "Recycle Bin", + "settings_fileManagement_recycleEnabledDesc": "When a file is replaced by an upgrade, move it to a hidden .trash folder inside your library root folder instead of permanently deleting it.", + "settings_fileManagement_recycleEnabledLabel": "Enable Recycle Bin", + "settings_fileManagement_saved": "File management settings saved", + "settings_fileManagement_sectionDescription": "Choose how files are transferred when importing media. This applies to both automatic imports from download clients and manual imports.", + "settings_fileManagement_sectionTitle": "File Transfer", + "settings_fileManagement_symlinkDesc": "Create a symbolic link in your library pointing to the source file. Nothing is moved or copied.", + "settings_fileManagement_symlinkDescNote": "Note: the library entry will break if the source file is moved, deleted, or your download folder is cleaned up.", + "settings_fileManagement_symlinkLabel": "Symlink", + "settings_fileManagement_symlinkPreserveAutoEnabled": "Preserve symlinks enabled on {count} root folder(s), this is required for symlink import mode to work correctly.", + "settings_fileManagement_symlinkPreserveAutoReverted": "Preserve symlinks reverted on {count} root folder(s) that were auto-enabled by symlink mode.", "settings_filters_configureTmdbForGenres": "Configure your TMDB API key to load available genres.", "settings_filters_contentPreferences": "Content Preferences", "settings_filters_excludedGenres": "Excluded Genres", "settings_filters_excludedGenresHint": "Select genres to exclude from all results.", - "settings_filters_failedToSave": "Failed to save global filters", - "settings_filters_heading": "Global Filters", + "settings_filters_failedToSave": "Failed to save discovery filters", + "settings_filters_heading": "Discovery Filters", "settings_filters_includeAdult": "Include Adult Content", "settings_filters_includeAdultHint": "Enable to allow adult content in search results and discovery.", "settings_filters_localization": "Localization", "settings_filters_minScore": "Minimum Score (0-10)", "settings_filters_minVoteCount": "Minimum Vote Count", "settings_filters_noGenresAvailable": "No genres available.", - "settings_filters_pageTitle": "Global Filters - Settings - Cinephage", + "settings_filters_pageTitle": "Discovery Filters - Settings - Cinephage", "settings_filters_preferredLanguage": "Preferred Language", "settings_filters_preferredRegion": "Preferred Region", "settings_filters_qualityStandards": "Quality Standards", "settings_filters_saveButton": "Save Global Filters", - "settings_filters_subtitle": "Configure global content filters. These settings apply to all search results, discoveries, and automated tasks.", + "settings_filters_subtitle": "Configure global content filters. These filters apply to all metadata sources (TMDB, TVDB supplementary data, and the future Cinephage unified API) and control what gets discovered and added to your library.", "settings_filters_tmdbRequired": "Configure your TMDB API key to enable genre filtering and other TMDB-powered features.", - "settings_filters_updated": "Global filters updated", + "settings_filters_updated": "Discovery filters updated", "settings_filters_updatedSuccess": "Global filters updated successfully.", "settings_general_addFolder": "Add Folder", "settings_general_addFolderFirst": "Add a root folder above before scanning your library.", @@ -2276,8 +2584,8 @@ "settings_general_badgeSystem": "System", "settings_general_capacityUnknown": "Capacity unknown", "settings_general_cinephageTracked": "Cinephage: {used}", - "settings_general_classShort": "Class", "settings_general_classification": "Classification", + "settings_general_classShort": "Class", "settings_general_close": "close", "settings_general_columnClassification": "Classification", "settings_general_columnItems": "Items", @@ -2348,11 +2656,11 @@ "settings_general_noMovieLibrariesConfigured": "No movie libraries configured.", "settings_general_noRootFolder": "No root folder", "settings_general_noRootFolderAssigned": "No root folder assigned", - "settings_general_noRootFolderUsageDataYet": "No root folder usage data yet.", "settings_general_noRootFoldersAssigned": "No root folders assigned", + "settings_general_noRootFolderUsageDataYet": "No root folder usage data yet.", "settings_general_noScanHistory": "No scan history", - "settings_general_noTvLibrariesConfigured": "No TV libraries configured.", "settings_general_notAssigned": "Not assigned", + "settings_general_noTvLibrariesConfigured": "No TV libraries configured.", "settings_general_optionalChooseCompatibleLibrary": "Optional: choose a different compatible custom library.", "settings_general_otherDiskUsage": "Other: {used}", "settings_general_pageTitle": "Library & Storage - Cinephage", @@ -2362,7 +2670,6 @@ "settings_general_processing": "Processing...", "settings_general_reviewLibrary": "Review library", "settings_general_rootFolderLabel": "Root folder", - "settings_general_rootFolderUsage": "Root folder usage", "settings_general_rootFolders": "Root Folders", "settings_general_rootFoldersCount": "{count} folders", "settings_general_rootFoldersDescription": "Configure media library folders where content will be organized.", @@ -2372,6 +2679,7 @@ "settings_general_rootFoldersSummaryMore": "{names} +{count} more", "settings_general_rootFoldersTabDescription": "Configure media library folders where content will be organized.", "settings_general_rootFoldersTitle": "Root Folders", + "settings_general_rootFolderUsage": "Root folder usage", "settings_general_runLibraryScan": "Run library scan", "settings_general_running": "Running", "settings_general_saveLibrary": "Save library", @@ -2383,14 +2691,14 @@ "settings_general_scanHealth": "Scan health", "settings_general_scanLibraries": "Scan libraries", "settings_general_scanLibrary": "Scan Library", + "settings_general_scanned": "Scanned", + "settings_general_scanning": "Scanning...", "settings_general_scanNow": "Scan now", "settings_general_scanRemoved": "Removed", "settings_general_scanRootFolders": "Scan root folders", "settings_general_scanStatus": "Scan status", "settings_general_scanUnmatched": "Unmatched", "settings_general_scanUpdated": "Updated", - "settings_general_scanned": "Scanned", - "settings_general_scanning": "Scanning...", "settings_general_searchOnAddLabel": "Search on add", "settings_general_selectedCount": "{count} selected", "settings_general_seriesCount": "{count} series", @@ -2400,9 +2708,6 @@ "settings_general_subtitle": "Manage libraries, root folders, and storage.", "settings_general_subtitleFilesCount": "{count} subtitle files", "settings_general_subtitlesLabel": "Subtitles", - "settings_general_tabLibraries": "Libraries", - "settings_general_tabMaintenance": "Storage Maintenance", - "settings_general_tabRootFolders": "Root Folders", "settings_general_topLibrariesByUsage": "Top libraries by usage", "settings_general_topRootFoldersByUsage": "Top root folders by usage", "settings_general_totalStorage": "Total storage", @@ -2420,11 +2725,25 @@ "settings_general_viewUnmatchedFiles": "View {count} unmatched files", "settings_general_wantSubtitles": "Want subtitles", "settings_general_yes": "Yes", + "settings_history_description": "Control how long historical data is kept before automatic cleanup. 0 days = unlimited.", + "settings_history_failed": "Failed to save retention settings", + "settings_history_file_days": "File history (days)", + "settings_history_file_days_help": "Per-file analysis snapshots. Default: 30 days.", + "settings_history_forecast": "Storage Forecast", + "settings_history_forecast_30d": "Projected (30 days)", + "settings_history_forecast_90d": "Projected (90 days)", + "settings_history_forecast_current": "Current estimated storage", + "settings_history_library_days": "Library history (days)", + "settings_history_library_days_help": "Daily per-library snapshots. Default: 365 days.", + "settings_history_saved": "Retention settings saved", + "settings_history_scan_days": "Scan history (days)", + "settings_history_scan_days_help": "Completed scan job records. Default: 30 days.", + "settings_history_title": "History Retention", "settings_indexers_addIndexer": "Add Indexer", "settings_indexers_addIndexerHint": "Add an indexer to start searching for content", "settings_indexers_authentication": "Authentication", "settings_indexers_automaticSearch": "Automatic Search", - "settings_indexers_automaticSearchDesc": "Search when items are added", + "settings_indexers_automaticSearchDesc": "On-add searches and scheduled tasks", "settings_indexers_badgeAuthRequired": "Auth Required", "settings_indexers_badgePrivate": "Private", "settings_indexers_badgePublic": "Public", @@ -2451,8 +2770,8 @@ "settings_indexers_edit": "Edit indexer", "settings_indexers_editIndexer": "Edit Indexer", "settings_indexers_enable": "Enable", - "settings_indexers_enableReorderHint": "Enable reorder mode to change Indexer priority.", "settings_indexers_enabled": "Enabled", + "settings_indexers_enableReorderHint": "Enable reorder mode to change Indexer priority.", "settings_indexers_externalUrl": "External URL", "settings_indexers_externalUrlHint": "The external URL where Jellyfin/Kodi can reach this server", "settings_indexers_externalUrlRequired": "Required for streaming", @@ -2487,11 +2806,11 @@ "settings_indexers_reorderModeActive": "Reorder mode is active", "settings_indexers_reorderPriorities": "Reorder Priorities", "settings_indexers_searchPlaceholder": "Search indexers...", + "settings_indexers_seedersHint": "0+", "settings_indexers_seedRatio": "Seed Ratio", "settings_indexers_seedRatioHint": "Empty = client default", "settings_indexers_seedTime": "Seed Time", "settings_indexers_seedTimeUnit": "minutes", - "settings_indexers_seedersHint": "0+", "settings_indexers_selectAll": "Select all", "settings_indexers_skipReleasesBelow": "Skip releases below this", "settings_indexers_status": "Status", @@ -2528,8 +2847,8 @@ "settings_integrations_captcha_failedToClearCache": "Failed to clear captcha cache", "settings_integrations_captcha_failedToLoad": "Failed to load captcha solver data", "settings_integrations_captcha_failedToSaveSettings": "Failed to save settings", - "settings_integrations_captcha_fetchSuccessRate": "Fetch Success Rate", "settings_integrations_captcha_fetchesAttempted": "{count} fetches attempted", + "settings_integrations_captcha_fetchSuccessRate": "Fetch Success Rate", "settings_integrations_captcha_headlessDesc": "Run the browser without a visible window. Disable for debugging purposes.", "settings_integrations_captcha_headlessLabel": "Headless Mode", "settings_integrations_captcha_hour1Default": "1 hour (default)", @@ -2549,8 +2868,8 @@ "settings_integrations_captcha_optional": "Optional", "settings_integrations_captcha_pageTitle": "Captcha Solver - Cinephage", "settings_integrations_captcha_proxyOptional": "Proxy (Optional)", - "settings_integrations_captcha_proxyPlaceholder": "http://proxy.example.com:8080", "settings_integrations_captcha_proxyPassword": "Password", + "settings_integrations_captcha_proxyPlaceholder": "http://proxy.example.com:8080", "settings_integrations_captcha_proxyUrl": "Proxy URL", "settings_integrations_captcha_proxyUrlHelp": "Route the browser through a proxy. Format: http://host:port or socks5://host:port", "settings_integrations_captcha_proxyUsername": "Username", @@ -2559,11 +2878,11 @@ "settings_integrations_captcha_seconds60Default": "60 seconds (default)", "settings_integrations_captcha_seconds90": "90 seconds", "settings_integrations_captcha_settingsSaved": "Settings saved successfully", + "settings_integrations_captcha_solvedChallengeIn": "Solved {challengeType} challenge in {solveTimeMs}ms", + "settings_integrations_captcha_solvesAttempted": "{count} solves attempted", "settings_integrations_captcha_solveSuccessRate": "Solve Success Rate", "settings_integrations_captcha_solveTimeout": "Solve Timeout", "settings_integrations_captcha_solveTimeoutHelp": "Maximum time to wait for a CAPTCHA to be solved before giving up.", - "settings_integrations_captcha_solvedChallengeIn": "Solved {challengeType} challenge in {solveTimeMs}ms", - "settings_integrations_captcha_solvesAttempted": "{count} solves attempted", "settings_integrations_captcha_statistics": "Statistics", "settings_integrations_captcha_statusBusy": "Busy", "settings_integrations_captcha_statusDisabled": "Captcha solver is disabled", @@ -2591,7 +2910,6 @@ "settings_integrations_connectionSuccessful": "Connection successful", "settings_integrations_deleteConfirmPrefix": "Are you sure you want to delete ", "settings_integrations_deleteConfirmSuffix": "? This action cannot be undone.", - "settings_integrations_downloadClientsDesc": "Applications that handle downloading releases from indexers.", "settings_integrations_downloadClients_addButton": "Add Download Client", "settings_integrations_downloadClients_altmountMode": "Altmount / NZBDav (Mount Mode)", "settings_integrations_downloadClients_bulkDeleteCount": "{count} selected clients", @@ -2600,8 +2918,8 @@ "settings_integrations_downloadClients_clientPath": "Client Path", "settings_integrations_downloadClients_completedDownloadFolder": "Completed Download Folder", "settings_integrations_downloadClients_downloadBehavior": "Download Behavior", - "settings_integrations_downloadClients_downloadFolder": "Download Folder", "settings_integrations_downloadClients_downloaderColumn": "Downloader", + "settings_integrations_downloadClients_downloadFolder": "Download Folder", "settings_integrations_downloadClients_emptyDescription": "Add a download client to start managing downloads", "settings_integrations_downloadClients_emptyTitle": "No download clients configured", "settings_integrations_downloadClients_forcePriority": "Force", @@ -2627,25 +2945,25 @@ "settings_integrations_downloadClients_torrent": "Torrent", "settings_integrations_downloadClients_urlBaseDescription": "Path prefix added after host and port.", "settings_integrations_downloadClients_urlBaseLabel": "URL Base", - "settings_integrations_downloadClients_useUrlBase": "Use URL Base", "settings_integrations_downloadClients_usenet": "Usenet", + "settings_integrations_downloadClients_useUrlBase": "Use URL Base", + "settings_integrations_downloadClientsDesc": "Applications that handle downloading releases from indexers.", "settings_integrations_gettingStarted": "Getting Started", "settings_integrations_gettingStartedBoth": "Add at least one indexer and one download client to start downloading content.", "settings_integrations_gettingStartedDownloadClient": "Add at least one download client to download releases.", "settings_integrations_gettingStartedIndexer": "Add at least one indexer to search for releases.", - "settings_integrations_indexersDesc": "Search providers for finding releases on torrent and Usenet networks.", "settings_integrations_indexers_addButton": "Add Indexer", "settings_integrations_indexers_bulkDeleteCount": "{count} selected indexers", "settings_integrations_indexers_created": "Indexer created successfully", "settings_integrations_indexers_definitionErrors": "{count} indexer definition errors found", "settings_integrations_indexers_subtitle": "Manage your indexers for searching torrent and Usenet releases.", "settings_integrations_indexers_updated": "Indexer updated successfully", - "settings_integrations_languageProfilesDesc": "Configure preferred languages and subtitle requirements.", + "settings_integrations_indexersDesc": "Search providers for finding releases on torrent and Usenet networks.", "settings_integrations_languageProfiles_addLanguage": "Add Language", "settings_integrations_languageProfiles_addProfile": "Add Profile", "settings_integrations_languageProfiles_addTitle": "Add Language Profile", - "settings_integrations_languageProfiles_allowUpgrades": "Allow Upgrades", "settings_integrations_languageProfiles_allowed": "Allowed", + "settings_integrations_languageProfiles_allowUpgrades": "Allow Upgrades", "settings_integrations_languageProfiles_cutoff": "Cutoff", "settings_integrations_languageProfiles_cutoffIndex": "Cutoff Index", "settings_integrations_languageProfiles_cutoffIndexHelp": "Stop upgrading subtitles once a match at or above this language index is found.", @@ -2662,9 +2980,9 @@ "settings_integrations_languageProfiles_forced": "Forced", "settings_integrations_languageProfiles_hi": "HI", "settings_integrations_languageProfiles_maxChars": "Max {max} characters", - "settings_integrations_languageProfiles_minScore": "Min Score", "settings_integrations_languageProfiles_minimumScore": "Minimum Score", "settings_integrations_languageProfiles_minimumScoreHelp": "Minimum subtitle match score (0-100) required before a subtitle is accepted.", + "settings_integrations_languageProfiles_minScore": "Min Score", "settings_integrations_languageProfiles_nameAndLanguageRequired": "Profile name and at least one language are required", "settings_integrations_languageProfiles_nameTooLong": "Profile name must be {max} characters or less", "settings_integrations_languageProfiles_noProfiles": "No language profiles configured", @@ -2677,6 +2995,7 @@ "settings_integrations_languageProfiles_setAsDefault": "Set as Default", "settings_integrations_languageProfiles_subtitle": "Configure language preferences and subtitle matching profiles.", "settings_integrations_languageProfiles_upgrades": "Upgrades", + "settings_integrations_languageProfilesDesc": "Configure preferred languages and subtitle requirements.", "settings_integrations_mediaBrowsers_addServer": "Add Server", "settings_integrations_mediaBrowsers_bulkDeleteCount": "{count} selected servers", "settings_integrations_mediaBrowsers_emptyDescription": "Add a Jellyfin, Emby, or Plex server to enable library notifications", @@ -2685,7 +3004,6 @@ "settings_integrations_mediaBrowsers_subtitle": "Connect to Plex, Jellyfin, or Emby to sync libraries and metadata.", "settings_integrations_mediaBrowsers_thisServer": "this server", "settings_integrations_mediaServersDesc": "Media servers for browsing and streaming your library.", - "settings_integrations_nntpServersDesc": "Usenet news servers for downloading content via NNTP protocol.", "settings_integrations_nntpServers_addServer": "Add Server", "settings_integrations_nntpServers_bulkDeleteCount": "{count} selected servers", "settings_integrations_nntpServers_emptyDescription": "Add an NNTP server to enable direct NZB streaming", @@ -2698,6 +3016,7 @@ "settings_integrations_nntpServers_serverSettings": "Server Settings", "settings_integrations_nntpServers_subtitle": "Configure Usenet (NNTP) servers for downloading content.", "settings_integrations_nntpServers_thisServer": "this server", + "settings_integrations_nntpServersDesc": "Usenet news servers for downloading content via NNTP protocol.", "settings_integrations_notConfigured": "Not Configured", "settings_integrations_pageTitle": "Integrations - Cinephage", "settings_integrations_profiles_emptyDescription": "Add a profile to manage quality preferences", @@ -2707,13 +3026,13 @@ "settings_integrations_statSet": "Set", "settings_integrations_statTotal": "Total", "settings_integrations_subtitle": "Manage your external services and connections", - "settings_integrations_subtitleProvidersDesc": "Services that provide subtitles for your media library.", "settings_integrations_subtitleProviders_addProvider": "Add Provider", "settings_integrations_subtitleProviders_bulkDeleteCount": "{count} selected providers", "settings_integrations_subtitleProviders_connectionSuccessful": "Connection to {name} successful (response time: {responseTime}ms)", "settings_integrations_subtitleProviders_connectionTestFailed": "Unknown error", "settings_integrations_subtitleProviders_searchPlaceholder": "Search subtitle providers...", "settings_integrations_subtitleProviders_subtitle": "Configure subtitle download providers for automatic subtitle fetching.", + "settings_integrations_subtitleProvidersDesc": "Services that provide subtitles for your media library.", "settings_integrations_tabsAriaLabel": "Integration navigation tabs", "settings_integrations_tmdbApiKeyDescription": "Enter your TMDB API key (v3 auth). You can get one from", "settings_integrations_tmdbApiKeyTitle": "TMDB API Key", @@ -2721,6 +3040,23 @@ "settings_integrations_tmdbFailedToSave": "Failed to save TMDB API key", "settings_integrations_tmdbKeySaved": "TMDB API key saved successfully", "settings_integrations_tmdbTitle": "TMDB", + "settings_libraries_scan_description": "Global library scan behavior. These settings will move per-library in a future update.", + "settings_libraries_scan_failed": "Failed to save scan settings", + "settings_libraries_scan_interval": "Scan interval (hours)", + "settings_libraries_scan_interval_hint": "How often to run periodic scans (1-168 hours)", + "settings_libraries_scan_loading": "Loading scan settings...", + "settings_libraries_scan_on_startup": "Scan on startup", + "settings_libraries_scan_on_startup_hint": "Run a scan when the application starts", + "settings_libraries_scan_saved": "Scan settings saved", + "settings_libraries_scan_threshold": "Auto-match confidence threshold", + "settings_libraries_scan_threshold_hint": "Minimum confidence (0-1) for auto-matching discovered files to metadata", + "settings_libraries_scan_title": "Scan Settings", + "settings_libraries_scan_watch": "Watch filesystem for changes", + "settings_libraries_scan_watch_hint": "Automatically detect new, changed, or removed files in real time", + "settings_library_tabFileManagement": "File Management", + "settings_library_tabLibraries": "Libraries", + "settings_library_tabRootFolders": "Root Folders", + "settings_library_tabsAriaLabel": "Library navigation tabs", "settings_logs_adjustFilters": "Adjust filters or wait for new events", "settings_logs_allDomains": "All domains", "settings_logs_bufferedEntries": "{count} new entries buffered", @@ -2737,6 +3073,8 @@ "settings_logs_scrollToBottom": "Scroll to bottom", "settings_logs_subtitle": "Live application log stream. Click any entry to inspect its details.", "settings_logs_toggleLevel": "Toggle {level} logs", + "settings_monitoring_tabMaintenance": "Storage Maintenance", + "settings_monitoring_tabsAriaLabel": "Monitoring navigation tabs", "settings_naming_advancedOptions": "Advanced Options", "settings_naming_allFieldsClean": "All format fields validate cleanly.", "settings_naming_allFormatsValid": "All format fields validate cleanly.", @@ -2790,11 +3128,11 @@ "settings_naming_movieNamingDesc": "Folder and file naming patterns for movies", "settings_naming_multiEpDuplicate": "Duplicate (S01E01.S01E02)", "settings_naming_multiEpExtend": "Extend (S01E01E02E03)", + "settings_naming_multiEpisodeStyle": "Multi-Episode Style", + "settings_naming_multiEpisodeStyleHelp": "How multi-episode files are represented in the filename.", "settings_naming_multiEpRange": "Range (S01E01-E03)", "settings_naming_multiEpRepeat": "Repeat (S01E01.E02.E03)", "settings_naming_multiEpScene": "Scene (S01E01-S01E03)", - "settings_naming_multiEpisodeStyle": "Multi-Episode Style", - "settings_naming_multiEpisodeStyleHelp": "How multi-episode files are represented in the filename.", "settings_naming_namingStyle": "Naming Style", "settings_naming_nextStep": "Next step", "settings_naming_nextStepDesc": "Save this draft when it looks right, then review the rename plan before applying file changes across the library.", @@ -2855,9 +3193,9 @@ "settings_naming_reviewRenamePlan": "Review Rename Plan", "settings_naming_saveChanges": "Save Changes", "settings_naming_saveCurrentAsPreset": "Save Current as Preset", - "settings_naming_savePreset": "Save Preset", "settings_naming_saved": "Saved", "settings_naming_savedCustomSource": "Saved custom source", + "settings_naming_savePreset": "Save Preset", "settings_naming_seasonFolder": "Season Folder", "settings_naming_seasonFolderFormat": "Season Folder Format", "settings_naming_selectCustomPreset": "Select a custom preset…", @@ -2877,10 +3215,68 @@ "settings_naming_unsavedChangesDesc": "You have unsaved naming changes. Save to apply them, or review the rename plan to see how they would affect existing files.", "settings_naming_updatingPreviews": "Updating previews", "settings_naming_validating": "Validating", + "settings_patterns_all_libraries": "All libraries (global defaults)", + "settings_patterns_bonus": "Bonus Patterns", + "settings_patterns_bonus_add": "Add pattern", + "settings_patterns_bonus_desc": "Glob patterns (case-insensitive). Matching files are indexed but tagged as bonus/extras content.", + "settings_patterns_bonus_placeholder": "*trailer*", + "settings_patterns_description": "Control how files are filtered and classified during library scans. Ignore patterns drop files before indexing; bonus patterns tag files as extras without removing them; structure recognition parses series/season/episode from folder layouts.", + "settings_patterns_failed": "Failed to save pattern config", + "settings_patterns_ignore": "Ignore Patterns", + "settings_patterns_ignore_desc": "Glob patterns (case-sensitive). Matching files and folders are never indexed. Seeded with defaults for common NAS/system files.", + "settings_patterns_ignore_enabled": "Use built-in defaults", + "settings_patterns_ignore_user": "User patterns", + "settings_patterns_ignore_user_add": "Add pattern", + "settings_patterns_ignore_user_placeholder": "*Private*", + "settings_patterns_saved": "Pattern config saved", + "settings_patterns_scope_global": "Global defaults (apply to all libraries unless overridden)", + "settings_patterns_scope_library": "Library override", + "settings_patterns_select_library": "Select a library for per-library settings", + "settings_patterns_structure": "Structure Recognition", + "settings_patterns_structure_desc": "Parse series/season/episode structure from folder layouts. TV libraries only.", + "settings_patterns_structure_folder_depth": "Folder depth", + "settings_patterns_structure_mode": "Recognition mode", + "settings_patterns_structure_none": "None (flat library)", + "settings_patterns_structure_regex": "Custom regexes", + "settings_patterns_structure_season_depth": "Season folder depth (relative to series)", + "settings_patterns_structure_series_depth": "Series folder depth (0 = root)", + "settings_patterns_title": "Pattern Recognition", "settings_profiles_redirecting": "Redirecting...", "settings_quality_confirmDeleteFormatMessage": "Are you sure you want to delete {name}? This action cannot be undone.", "settings_quality_confirmDeleteProfileMessage": "Are you sure you want to delete {name}? This action cannot be undone.", "settings_quality_confirmDeleteTitle": "Confirm Delete", + "settings_quality_delay_addProfile": "Add Profile", + "settings_quality_delay_badgeDisabled": "Disabled", + "settings_quality_delay_bypass4k": "Bypass at 4K", + "settings_quality_delay_bypassScorePrefix": "Bypass if score >", + "settings_quality_delay_createButton": "Create", + "settings_quality_delay_created": "Delay profile created", + "settings_quality_delay_deleted": "\"{name}\" deleted", + "settings_quality_delay_deleteMessage": "Delete \"{name}\"? Media assigned to this profile will fall back to no delay.", + "settings_quality_delay_deleteTitle": "Delete Delay Profile", + "settings_quality_delay_empty": "No delay profiles", + "settings_quality_delay_emptyHint": "Create a profile to hold releases before grabbing them.", + "settings_quality_delay_field_bypass4k": "Bypass delay for 4K (2160p) releases", + "settings_quality_delay_field_bypassScore": "Bypass delay if score exceeds", + "settings_quality_delay_field_bypassScorePlaceholder": "Leave empty to disable", + "settings_quality_delay_field_enabled": "Enabled", + "settings_quality_delay_field_name": "Name", + "settings_quality_delay_field_namePlaceholder": "Dub Wait", + "settings_quality_delay_field_preferredProtocol": "Preferred protocol (grabs immediately)", + "settings_quality_delay_field_torrentDelay": "Torrent delay (minutes)", + "settings_quality_delay_field_usenetDelay": "Usenet delay (minutes)", + "settings_quality_delay_formatImmediate": "Immediate", + "settings_quality_delay_modalAddTitle": "Add Delay Profile", + "settings_quality_delay_modalEditTitle": "Edit Delay Profile", + "settings_quality_delay_option_none": "None", + "settings_quality_delay_option_torrent": "Torrent", + "settings_quality_delay_option_usenet": "Usenet", + "settings_quality_delay_preferredPrefix": "Preferred:", + "settings_quality_delay_sectionDescription": "Hold releases in a queue before sending to a download client, giving time for better versions to appear.", + "settings_quality_delay_tab": "Delay Profiles", + "settings_quality_delay_torrentPrefix": "Torrent:", + "settings_quality_delay_updated": "Delay profile updated", + "settings_quality_delay_usenetPrefix": "Usenet:", "settings_quality_failedToDeleteFormat": "Failed to delete format", "settings_quality_failedToDeleteProfile": "Failed to delete profile", "settings_quality_failedToSetDefault": "Failed to set default profile", @@ -2890,12 +3286,25 @@ "settings_quality_subtitle": "Manage quality profiles and custom formats to control how releases are scored and selected.", "settings_quality_tabFormats": "Custom Formats", "settings_quality_tabProfiles": "Profiles", + "settings_resolutions_add": "Add Category", + "settings_resolutions_delete": "Delete", + "settings_resolutions_description": "Editable resolution buckets used for quality scoring, display badges, and statistics. Categories use orientation-normalized dimensions (cinema-scope content lands correctly). One fallback category is required.", + "settings_resolutions_failed": "Failed to save resolution categories", + "settings_resolutions_fallback": "Fallback", + "settings_resolutions_fallback_help": "The default bucket for unmatched resolutions. Cannot be deleted.", + "settings_resolutions_label": "Label", + "settings_resolutions_min_height": "Min height", + "settings_resolutions_min_width": "Min width", + "settings_resolutions_saved": "Resolution categories saved", + "settings_resolutions_search_terms": "Search terms", + "settings_resolutions_search_terms_help": "Comma-separated aliases for search (e.g. '2160p, 4k, uhd')", + "settings_resolutions_title": "Resolution Categories", "settings_streaming_cacheDefaultsNotice": "Cache settings are currently using defaults. Custom configuration coming soon.", + "settings_streaming_cachedFiles": "Cached Files", "settings_streaming_cacheSettings": "Cache Settings", "settings_streaming_cacheSize": "Cache Size", - "settings_streaming_cachedFiles": "Cached Files", - "settings_streaming_cleanExpiredFiles": "Clean Expired Files", "settings_streaming_cleanedUpResult": "Cleaned up {count} file(s), freed {size}", + "settings_streaming_cleanExpiredFiles": "Clean Expired Files", "settings_streaming_cleaning": "Cleaning...", "settings_streaming_expired": "Expired", "settings_streaming_expiredCleaned": "Expired cache files cleaned", @@ -2955,25 +3364,25 @@ "settings_subtitles_reSyncSubtitle": "Re-sync subtitle", "settings_subtitles_retry": "Retry", "settings_subtitles_sdh": "SDH", + "settings_subtitles_searchingProviders": "Searching subtitle providers...", "settings_subtitles_searchStats": "{total} results", "settings_subtitles_searchSubs": "Search subs", "settings_subtitles_searchTime": "Search: {time}ms", - "settings_subtitles_searchingProviders": "Searching subtitle providers...", "settings_subtitles_splitPenalty": "Split Penalty", "settings_subtitles_splitRecommended": "5-20 recommended", "settings_subtitles_subtitleSearch": "Subtitle Search", "settings_subtitles_subtitleSync": "Subtitle Sync", "settings_subtitles_sync": "Sync", "settings_subtitles_syncAll": "Sync All ({count})", + "settings_subtitles_synced": "Synced", + "settings_subtitles_syncedBadge": "SYNC", + "settings_subtitles_syncedCount": "{count} synced", + "settings_subtitles_syncing": "Syncing {completed}/{total}...", "settings_subtitles_syncProgress": "{synced}/{total}", "settings_subtitles_syncSettings": "Sync Settings", "settings_subtitles_syncTooltip": "Subtitle synced ({offset})", "settings_subtitles_syncTooltipNoOffset": "Subtitle synced", "settings_subtitles_syncUnsynced": "Sync Unsynced ({count})", - "settings_subtitles_synced": "Synced", - "settings_subtitles_syncedBadge": "SYNC", - "settings_subtitles_syncedCount": "{count} synced", - "settings_subtitles_syncing": "Syncing {completed}/{total}...", "settings_subtitles_tryAdjustingFilters": "Try adjusting your language profile or search filters", "settings_subtitles_unknownOffset": "Unknown", "settings_system_apiAuth": "API Authentication", @@ -2995,11 +3404,11 @@ "settings_system_backup_errorSelectSectionFirst": "Select at least one backup section to restore.", "settings_system_backup_exportButton": "Export backup", "settings_system_backup_exportDescription": "Exports configuration for settings, integrations, indexers, download clients, profiles, and other user-managed setup required to rebuild the instance.", + "settings_system_backup_exporting": "Exporting...", "settings_system_backup_exportPassphraseLabel": "Encryption passphrase", "settings_system_backup_exportPassphrasePlaceholder": "At least 16 characters to encrypt exported secrets", "settings_system_backup_exportSuccess": "Configuration backup exported successfully.", "settings_system_backup_exportTitle": "Export configuration", - "settings_system_backup_exporting": "Exporting...", "settings_system_backup_fileLabel": "Backup file", "settings_system_backup_includeCookiesHelp": "Back up active 2FA/session cookies for indexers that rely on them. These are encrypted, short-lived, and more sensitive than saved credentials.", "settings_system_backup_includeCookiesLabel": "Include indexer session cookies", @@ -3038,8 +3447,8 @@ "settings_system_created": "Created", "settings_system_exampleEmpty": "Leave empty for direct HTTP access", "settings_system_exampleReverseProxy": "https://cinephage.example.com (behind reverse proxy)", - "settings_system_exampleSubpath": "https://media.yourdomain.com/cinephage (subpath)", "settings_system_examples": "Examples", + "settings_system_exampleSubpath": "https://media.yourdomain.com/cinephage (subpath)", "settings_system_externalUrl": "External URL", "settings_system_externalUrlDescription": "Set the public-facing URL for your Cinephage instance. This is used for authentication callbacks and generating correct links when behind a reverse proxy.", "settings_system_externalUrlPlaceholder": "https://cinephage.yourdomain.com", @@ -3052,10 +3461,10 @@ "settings_system_failedToSave": "Failed to save", "settings_system_failedToSaveExternalUrl": "Failed to save external URL", "settings_system_fullAccess": "Full Access", - "settings_system_generateApiKeys": "Generate API Keys", - "settings_system_generating": "Generating...", "settings_system_general_pageTitle": "General - Cinephage", "settings_system_general_subtitle": "Manage API keys and external URL settings.", + "settings_system_generateApiKeys": "Generate API Keys", + "settings_system_generating": "Generating...", "settings_system_heading": "System Settings", "settings_system_hideKey": "Hide key", "settings_system_invalidUrlFormat": "Please enter a valid URL (e.g., https://cinephage.example.com)", @@ -3071,13 +3480,13 @@ "settings_system_noApiKeys": "No API keys found. Generate keys to enable external access.", "settings_system_noKeyToRegenerate": "No {label} API key found to regenerate", "settings_system_pageTitle": "System Settings - Cinephage", - "settings_system_permEpg": "✓ EPG Data (/api/livetv/epg.xml)", - "settings_system_permLiveTvStreams": "✓ Live TV Streams (/api/livetv/stream/*)", - "settings_system_permM3u": "✓ M3U Playlist (/api/livetv/playlist.m3u)", - "settings_system_permNoLibrary": "✗ Library Access", - "settings_system_permNoSettings": "✗ Settings Access", - "settings_system_permStreamingContent": "✓ Streaming Content (/api/streaming/*)", + "settings_system_permEpg": "EPG Data (/api/livetv/epg.xml)", "settings_system_permissions": "Permissions", + "settings_system_permLiveTvStreams": "Live TV Streams (/api/livetv/stream/*)", + "settings_system_permM3u": "M3U Playlist (/api/livetv/playlist.m3u)", + "settings_system_permNoLibrary": "Library Access", + "settings_system_permNoSettings": "Settings Access", + "settings_system_permStreamingContent": "Streaming Content (/api/streaming/*)", "settings_system_publicUrlConfig": "Public URL Configuration", "settings_system_publicUrlHint": "If you access Cinephage through a reverse proxy with HTTPS, enter your public URL here (e.g., https://cinephage.yourdomain.com). Leave empty for local HTTP access.", "settings_system_regenerate": "Regenerate", @@ -3087,6 +3496,7 @@ "settings_system_saveExternalUrl": "Save External URL", "settings_system_showKey": "Show key", "settings_system_subtitle": "Configure system-level settings including API authentication.", + "settings_system_tabsAriaLabel": "System navigation tabs", "settings_tasks_activityAdded": "{count} added", "settings_tasks_activityErrors": "{count} errors", "settings_tasks_activityFailed": "{count} failed", @@ -3143,8 +3553,8 @@ "settings_tasks_ok": "OK", "settings_tasks_overdue": "Overdue", "settings_tasks_pageTitle": "Tasks - Settings - Cinephage", - "settings_tasks_runNow": "Run Now", "settings_tasks_running": "Running...", + "settings_tasks_runNow": "Run Now", "settings_tasks_scheduledTasks": "Scheduled Tasks", "settings_tasks_statusError": "error", "settings_tasks_statusFound": "{count} found", @@ -3224,9 +3634,9 @@ "smartlists_desc_years": "{min}–{max}", "smartlists_detail_addAllButton": "Add All", "smartlists_detail_addAllConfirm": "Add all visible items to your library?", - "smartlists_detail_addToLibrary": "Add to Library", "smartlists_detail_addedBulkPartial": "Added {added}, failed {failed}", "smartlists_detail_addedToLibrary": "{title} added to library", + "smartlists_detail_addToLibrary": "Add to Library", "smartlists_detail_autoAddEnabled": "Auto-add enabled", "smartlists_detail_backToSmartLists": "Back to Smart Lists", "smartlists_detail_badgeExcluded": "Excluded", @@ -3325,9 +3735,9 @@ "smartlists_filter_exclude": "Exclude", "smartlists_filter_excludeGenres": "Exclude Genres", "smartlists_filter_genreMatchMode": "Genre Match Mode", - "smartlists_filter_inProduction": "In Production", "smartlists_filter_include": "Include", "smartlists_filter_includeGenres": "Include Genres", + "smartlists_filter_inProduction": "In Production", "smartlists_filter_keywords": "Keywords", "smartlists_filter_keywordsPlaceholder": "Search keywords...", "smartlists_filter_loading": "Loading...", @@ -3363,9 +3773,9 @@ "smartlists_formatDate_minutesAgo": "{count}m ago", "smartlists_formatDate_never": "Never", "smartlists_heading": "Smart Lists", - "smartlists_inLibraryBadge": "{count} in library", "smartlists_info_mixedContentDescription": "Movies and TV shows from the source will be displayed together. The media type selector at the top only affects TMDB Discover lists.", "smartlists_info_mixedContentTitle": "Import lists show all content types", + "smartlists_inLibraryBadge": "{count} in library", "smartlists_itemsBadge": "{count} items", "smartlists_label_customHeaders": "Custom Headers (Optional)", "smartlists_label_jsonUrl": "JSON URL", @@ -3442,17 +3852,32 @@ "smartlists_subtitle": "Create dynamic lists that automatically populate from TMDB based on your filters.", "smartlists_test_success": "Successfully connected! Found {count} items.", "startFromScratch": "Start from scratch", + "status_back_to_dashboard": "Dashboard", "status_completed": "Completed", "status_connected": "Connected", + "status_dashboard_subtitle": "Storage health, library maintenance, and media server analytics", + "status_dashboard_title": "Storage Maintenance", "status_disabled": "Disabled", "status_disconnected": "Disconnected", "status_downloading": "Downloading", "status_error": "Error", "status_failed": "Failed", + "status_folder_not_found": "Root folder not found", + "status_folder_save_failed": "Failed to save root folder", + "status_folder_updated": "Root folder updated", + "status_folders_subtitle": "Manage root folders, free space, and scan status", + "status_folders_title": "Root Folders", "status_healthy": "Healthy", "status_idle": "Idle", "status_imported": "Imported", + "status_materializing": "Materializing", + "status_insights_subtitle": "Issues detected across your library and media servers", + "status_insights_title": "Storage Insights", + "status_libraries_subtitle": "Manage your media libraries and their root folders", + "status_libraries_title": "Libraries", "status_matched": "Matched", + "status_media_subtitle": "Browse all media with stats, quality info, and playback data", + "status_media_title": "Media Explorer", "status_noResults": "No Results", "status_paused": "Paused", "status_queued": "Queued", @@ -3460,17 +3885,20 @@ "status_rejected": "Rejected", "status_removed": "Removed", "status_running": "Running", + "status_scan_started": "Scan started", "status_searchError": "Search Error", "status_searching": "Searching", "status_seeding": "Seeding", "status_stopped": "Stopped", "status_streaming": "Streaming", "status_success": "Success", + "status_sync_failed": "Sync failed", "status_tooltipDegraded": "{count} recent failures. Last: {time}", "status_tooltipDisabled": "Download client is disabled by user", "status_tooltipHealthy": "Download client is healthy and reachable", "status_tooltipUnhealthy": "{count} consecutive failures. Last: {time}", "status_unhealthy": "Unhealthy", + "status_unknown_error": "Unknown error", "status_unmatched": "Unmatched", "status_warning": "Warning", "streamingProfileInfo": "Streaming profiles use external sources, so size limits are not configurable.", @@ -3553,8 +3981,8 @@ "subtitleProviders_table_featureHashMatching": "Hash matching", "subtitleProviders_table_featureLanguageMatching": "Language matching", "subtitleProviders_table_featureProviderApi": "Provider API", - "subtitleProviders_table_featureSubtitleSearch": "Subtitle search", "subtitleProviders_table_features": "Features", + "subtitleProviders_table_featureSubtitleSearch": "Subtitle search", "subtitleProviders_table_moveDown": "Move down", "subtitleProviders_table_moveUp": "Move up", "subtitleProviders_table_name": "Name", @@ -3611,6 +4039,9 @@ "task_desc_subtitleUpgrade": "Search for better-scoring subtitles to replace existing ones", "task_desc_updateStrmUrls": "Update all .strm files with the current streaming base URL. Run this after changing your server address, port, or domain.", "task_desc_upgrade": "Search for better quality releases to replace existing files", + "task_history_empty": "No execution history yet", + "task_history_loading": "Loading history...", + "task_history_loadingActivity": "Loading activity...", "task_historyModal_added": "Added: {count}", "task_historyModal_close": "Close", "task_historyModal_details": "Details", @@ -3629,9 +4060,6 @@ "task_historyModal_updated": "Updated: {count}", "task_historyModal_updatedFiles": "Updated files: {count}", "task_historyModal_viewFailedItems": "View failed items", - "task_history_empty": "No execution history yet", - "task_history_loading": "Loading history...", - "task_history_loadingActivity": "Loading activity...", "task_name_cutoffUnmet": "Cutoff Unmet Search", "task_name_historyCleanup": "History Cleanup", "task_name_libraryScan": "Library Scan", @@ -3689,14 +4117,18 @@ "tmdb_seasonList_noOverview": "No overview available", "tmdb_watchProviders_notAvailable": "Not available for streaming", "toast_activity_adminRequired": "Admin access is required", + "toast_activity_clearedFailedQueue": "Cleared {count} failed download(s) from queue", "toast_activity_deleteCompleted": "Delete completed. Activity list will refresh shortly.", "toast_activity_deletedEntries": "Deleted {count} activity entries", + "toast_activity_failedToClearFailed": "Failed to clear failed downloads", "toast_activity_failedToDeleteSelected": "Failed to delete selected activity entries", "toast_activity_failedToLoadActivity": "Failed to load activity", "toast_activity_failedToLoadMore": "Failed to load more activity", "toast_activity_failedToLoadSettings": "Failed to load history settings", "toast_activity_failedToPause": "Failed to pause downloads", "toast_activity_failedToPurge": "Failed to purge activity entries", + "toast_activity_failedToRefresh": "Failed to refresh download queue", + "toast_activity_failedToRelink": "Failed to re-link orphaned downloads", "toast_activity_failedToRemove": "Failed to remove downloads", "toast_activity_failedToResume": "Failed to resume downloads", "toast_activity_failedToRetry": "Failed to retry downloads", @@ -3705,6 +4137,9 @@ "toast_activity_pausedDownloads": "Paused {count} downloads", "toast_activity_purgeCompleted": "Purge completed. Activity list will refresh shortly.", "toast_activity_queueActionCompleted": "Queue action completed. Activity list will refresh shortly.", + "toast_activity_refreshed": "Download client sync complete", + "toast_activity_relinked": "Re-linked {count} orphaned download(s)", + "toast_activity_relinkNone": "No orphaned downloads found to re-link", "toast_activity_removedDownloads": "Removed {count} downloads", "toast_activity_resumedDownloads": "Resumed {count} downloads", "toast_activity_retentionUpdated": "Retention updated to {count} days", @@ -3741,9 +4176,9 @@ "toast_library_movieDetail_movieFilesDeleted": "Movie files deleted", "toast_library_movieDetail_movieRemoved": "Movie removed from library", "toast_library_movieDetail_noSuitableReleases": "No suitable releases found", + "toast_library_movieDetail_subtitleSynced": "Subtitle synced", "toast_library_movieDetail_subtitleSyncFailed": "Subtitle sync failed", "toast_library_movieDetail_subtitleSyncOffset": "Applied offset of {offset}ms", - "toast_library_movieDetail_subtitleSynced": "Subtitle synced", "toast_library_movies_autoGrabbed": "Auto-grabbed \"{release}\" for \"{title}\"", "toast_library_movies_deletedFilesCount": "Deleted files for {count} movies", "toast_library_movies_failedAutoGrab": "Failed to auto-grab for \"{title}\"", @@ -3755,13 +4190,32 @@ "toast_library_movies_filesDeleted": "\"{title}\" files deleted", "toast_library_movies_foundNoMatch": "Found releases but none met criteria for \"{title}\"", "toast_library_movies_grabbed": "Grabbed \"{title}\"", - "toast_library_movies_monitorToggle": "\"{title}\" {status}", "toast_library_movies_monitoringCount": "Monitoring {count} movies", + "toast_library_movies_monitorToggle": "\"{title}\" {status}", "toast_library_movies_noReleases": "No releases found for \"{title}\"", "toast_library_movies_qualityUpdatedCount": "Updated quality profile for {count} movies", "toast_library_movies_removedCount": "Removed {count} movies from library", "toast_library_movies_removedFromLibrary": "Removed \"{title}\" from library", "toast_library_movies_unmonitoredCount": "Unmonitored {count} movies", + "toast_library_tv_autoGrabbedCount": "Auto-grabbed {count} release(s) for \"{title}\"", + "toast_library_tv_deletedFilesCount": "Deleted files for {count} series", + "toast_library_tv_failedAutoGrab": "Failed to auto-grab for \"{title}\"", + "toast_library_tv_failedToDelete": "Failed to delete", + "toast_library_tv_failedToDeleteSeries": "Failed to delete series", + "toast_library_tv_failedToGrab": "Failed to grab release", + "toast_library_tv_failedToUpdate": "Failed to update series", + "toast_library_tv_failedToUpdateSeries": "Failed to update series", + "toast_library_tv_failedToUpdateSingle": "Failed to update", + "toast_library_tv_filesDeleted": "\"{title}\" files deleted", + "toast_library_tv_foundNoMatch": "Found {count} releases but none met criteria for \"{title}\"", + "toast_library_tv_grabbed": "Grabbed \"{title}\"", + "toast_library_tv_monitoringCount": "Monitoring {count} series", + "toast_library_tv_monitorToggle": "\"{title}\" {status}", + "toast_library_tv_noReleases": "No releases found for \"{title}\"", + "toast_library_tv_qualityUpdatedCount": "Updated quality profile for {count} series", + "toast_library_tv_removedCount": "Removed {count} series from library", + "toast_library_tv_removedFromLibrary": "Removed \"{title}\" from library", + "toast_library_tv_unmonitoredCount": "Unmonitored {count} series", "toast_library_tvDetail_bulkSearchFailed": "Bulk search failed", "toast_library_tvDetail_bulkSubAutoSearchFailed": "Bulk subtitle auto-search failed", "toast_library_tvDetail_bulkSyncError": "Bulk sync failed", @@ -3798,29 +4252,10 @@ "toast_library_tvDetail_seriesRefreshFailed": "Series refresh failed", "toast_library_tvDetail_seriesRemoved": "Series removed from library", "toast_library_tvDetail_subtitleDeleted": "Subtitle deleted", + "toast_library_tvDetail_subtitleSynced": "Subtitle synced", "toast_library_tvDetail_subtitleSyncFailed": "Subtitle sync failed", "toast_library_tvDetail_subtitleSyncOffset": "Applied offset of {offset}ms", - "toast_library_tvDetail_subtitleSynced": "Subtitle synced", "toast_library_tvDetail_syncedSubtitles": "Synced {count} subtitles", - "toast_library_tv_autoGrabbedCount": "Auto-grabbed {count} release(s) for \"{title}\"", - "toast_library_tv_deletedFilesCount": "Deleted files for {count} series", - "toast_library_tv_failedAutoGrab": "Failed to auto-grab for \"{title}\"", - "toast_library_tv_failedToDelete": "Failed to delete", - "toast_library_tv_failedToDeleteSeries": "Failed to delete series", - "toast_library_tv_failedToGrab": "Failed to grab release", - "toast_library_tv_failedToUpdate": "Failed to update series", - "toast_library_tv_failedToUpdateSeries": "Failed to update series", - "toast_library_tv_failedToUpdateSingle": "Failed to update", - "toast_library_tv_filesDeleted": "\"{title}\" files deleted", - "toast_library_tv_foundNoMatch": "Found {count} releases but none met criteria for \"{title}\"", - "toast_library_tv_grabbed": "Grabbed \"{title}\"", - "toast_library_tv_monitorToggle": "\"{title}\" {status}", - "toast_library_tv_monitoringCount": "Monitoring {count} series", - "toast_library_tv_noReleases": "No releases found for \"{title}\"", - "toast_library_tv_qualityUpdatedCount": "Updated quality profile for {count} series", - "toast_library_tv_removedCount": "Removed {count} series from library", - "toast_library_tv_removedFromLibrary": "Removed \"{title}\" from library", - "toast_library_tv_unmonitoredCount": "Unmonitored {count} series", "toast_library_unmatched_bulkDeleteNotImpl": "Bulk delete not yet implemented", "toast_library_unmatched_failedToDelete": "Failed to delete file", "toast_library_unmatched_fileDeletedFromDisk": "File deleted from disk", @@ -3854,6 +4289,7 @@ "ui_deleteModal_warningRemoveOnly": "Warning: This will only remove the item from your library.", "ui_deleteModal_warningUnmatch": "Warning: This will unmatch the item.", "ui_failedToChangeLanguage": "Failed to change language", + "ui_languageLabel": "Language", "ui_mediaType_movie": "Movie", "ui_mediaType_stream": "Stream", "ui_mediaType_tv": "TV", @@ -3871,7 +4307,6 @@ "ui_operationProgress_pending": "Pending", "ui_operationProgress_processing": "Processing...", "ui_operationProgress_running": "Running", - "ui_languageLabel": "Language", "ui_selectLanguage": "Select Language", "ui_selectTheme": "Select Theme", "ui_themeDark": "Dark", @@ -3933,14 +4368,14 @@ "unmatched_libraryIssues_noMovieFolders": "No Movie root folders configured.", "unmatched_libraryIssues_noTvFolders": "No TV root folders configured.", "unmatched_libraryIssues_rootFolderNotSet": "Root folder not set", - "unmatched_libraryIssues_rootFolderUpdateFailed": "Failed to update root folder", "unmatched_libraryIssues_rootFolderUpdated": "Root folder updated", + "unmatched_libraryIssues_rootFolderUpdateFailed": "Failed to update root folder", "unmatched_libraryIssues_selectAllMovies": "Select all Movies", "unmatched_libraryIssues_selectAllTv": "Select all TV shows", + "unmatched_libraryIssues_selectedCount": "{count} selected", "unmatched_libraryIssues_selectItemsFirst": "Select items to apply the bulk action", "unmatched_libraryIssues_selectRootFolder": "Select root folder", "unmatched_libraryIssues_selectRootFolderFirst": "Select a root folder first", - "unmatched_libraryIssues_selectedCount": "{count} selected", "unmatched_libraryIssues_title": "Library Issues", "unmatched_libraryIssues_tv": "TV", "unmatched_libraryIssues_tvShows": "TV Shows", @@ -3949,190 +4384,16 @@ "unmatched_pagination_perPage50": "50 per page", "unmatched_pagination_showing": "Showing {start} - {end} of {total} files", "validation_maxChars": "Max {max} characters.", - "activity_popover_media": "Media:", - "activity_popover_reason": "Reason:", - "activity_popover_release": "Release:", - "activity_popover_when": "When:", - "activity_popover_viewInActivity": "View in Activity", - "activity_tag_sub": "Sub", - "activity_tag_media": "Media", - "activity_tag_list": "List", - "activity_tag_move": "Move", - "activity_tag_download": "Download", - "blocklist_reason_downloadFailed": "Download Failed", - "blocklist_reason_importFailed": "Import Failed", - "blocklist_reason_qualityMismatch": "Quality Mismatch", - "blocklist_reason_manual": "Manual", - "blocklist_reason_duplicate": "Duplicate", - "blocklist_reason_badRelease": "Bad Release", - "blocklist_duration_permanent": "Permanent", - "blocklist_duration_expired": "Expired", - "blocklist_selected": "{selectedCount} selected", - "blocklist_deleteSelected": "Delete Selected", - "blocklist_purgeExpired": "Purge Expired", - "blocklist_pageTitle": "Blocked Releases", - "blocklist_pageSubtitle": "Manage blocked releases that won't be re-downloaded", - "blocklist_searchPlaceholder": "Search releases...", - "blocklist_filterAllReasons": "All Reasons", - "blocklist_filterAllProtocols": "All Protocols", - "blocklist_filterActiveOnly": "Active only", - "blocklist_entryCount": "{total} entries", - "blocklist_emptyTitle": "No blocklisted releases", - "blocklist_emptyDescription": "Blocked releases will appear here when downloads fail repeatedly.", - "blocklist_confirmRemoveTitle": "Remove from Blocklist", - "blocklist_confirmRemoveMessage": "Remove this release from the blocklist? It may be grabbed again by monitoring.", - "blocklist_confirmRemoveLabel": "Remove", - "blocklist_confirmBulkDeleteTitle": "Remove Selected", - "blocklist_confirmPurgeTitle": "Purge Expired Entries", - "blocklist_confirmPurgeMessage": "Remove all expired blocklist entries? This cannot be undone.", - "blocklist_confirmPurgeLabel": "Purge", - "blocklist_entryRemoved": "Entry removed from blocklist", - "blocklist_entriesRemoved": "{count} entries removed", - "blocklist_expiredPurged": "Expired entries purged", - "blocklist_bulkDeleteMessage": "Remove {count} entries from the blocklist?", - "blocklist_tableTitle": "Title", - "blocklist_tableReason": "Reason", - "blocklist_tableMessage": "Message", - "blocklist_tableProtocol": "Protocol", - "blocklist_tableAdded": "Added", - "blocklist_tableExpires": "Expires", - "blocklist_tableActions": "Actions", - "blocklist_hoursLeft": "{hours}h left", - "blocklist_daysLeft": "{days}d left", - "blocklist_tableEditExpiry": "Edit Expiry", - "blocklist_duration_1hour": "1 hour", - "blocklist_duration_6hours": "6 hours", - "blocklist_duration_24hours": "24 hours", - "blocklist_duration_72hours": "3 days", - "blocklist_duration_1week": "1 week", - "blocklist_confirmBlockTitle": "Block Release", - "blocklist_confirmBlockMessage": "Block this release from being downloaded again?", - "blocklist_confirmBlockLabel": "Block", - "blocklist_addedToBlocklist": "Release added to blocklist", - "blocklist_expiryUpdated": "Block expiry updated", - "mediaExplorer_typeMovie": "Movie", - "mediaExplorer_typeTv": "TV", - "mediaExplorer_typeAnime": "Anime", - "mediaExplorer_headerTitle": "Title", - "mediaExplorer_headerType": "Type", - "mediaExplorer_headerLibrary": "Library", - "mediaExplorer_headerQuality": "Quality", - "mediaExplorer_headerSize": "Size", - "mediaExplorer_headerPlayback": "Playback", - "mediaExplorer_headerStatus": "Status", - "mediaExplorer_headerAdded": "Added", - "mediaExplorer_statusMonitored": "Monitored", - "mediaExplorer_statusUnmonitored": "Unmonitored", - "mediaExplorer_statusFileAvailable": "File available", - "mediaExplorer_statusNoFile": "No file", - "mediaExplorer_statusDone": "Done", - "mediaExplorer_episodes": "episodes", - "mediaExplorer_complete": "Complete", - "mediaExplorer_noMediaFound": "No media found", - "search_today": "Today", - "search_freeleechBadge": "Freeleech", - "search_grabbedBadge": "Grabbed", - "search_failedBadge": "Failed", - "search_stream": "Stream", - "search_unavailable": "Unavailable", - "search_viewComments": "View comments", - "search_collapseDetails": "Collapse details", - "search_expandDetails": "Expand details", - "search_scoreBreakdown": "Score Breakdown", - "search_technicalDetails": "Technical Details", - "search_indexerAndTorrent": "Indexer & Torrent", - "search_labelResolution": "Resolution", - "search_labelSource": "Source", - "search_labelCodec": "Codec", - "search_labelHdr": "HDR", - "search_labelProtocol": "Protocol", - "search_labelSize": "Size", - "search_labelIndexer": "Indexer", - "search_labelHash": "Hash", - "search_labelRatio": "Ratio", - "search_labelUpload": "Upload", - "search_labelSeeders": "Seeders", - "search_labelLeechers": "Leechers", - "search_labelPublished": "Published", - "search_ago": "ago", - "search_directDownload": "Direct Download", - "search_magnetLink": "Magnet Link", - "search_comments": "Comments", - "search_qualityScore": "Quality score", - "search_group": "Group", - "search_enhancement": "Enhancement", - "search_pack": "Pack", - "search_hardcodedSubs": "Hardcoded Subs", - "search_blockRelease": "Block", - "search_releaseBlocked": "Blocked", - "search_blockUnavailable": "Unable to block this release", - "hero_releaseType_premiere": "Premiere", - "hero_releaseType_limitedTheatrical": "Limited Theatrical", - "hero_releaseType_theatrical": "Theatrical", - "hero_releaseType_digital": "Digital", - "hero_releaseType_physical": "Physical", - "hero_releaseType_tv": "TV", - "hero_failedToCheckLibraryStatus": "Failed to check library status", - "hero_available": "Available", - "hero_viewInLibrary": "View in Library", - "hero_monitored": "Monitored", - "hero_addToLibrary": "Add to Library", - "hero_trailer": "Trailer", - "hero_website": "Website", - "hero_metadata_status": "Status", - "hero_metadata_language": "Language", - "hero_metadata_rated": "Rated", - "hero_metadata_released": "Released", - "hero_metadata_budget": "Budget", - "hero_metadata_revenue": "Revenue", - "hero_metadata_network": "Network", - "hero_metadata_seasons": "Seasons", - "hero_metadata_episodes": "Episodes", - "hero_metadata_firstAired": "First Aired", - "hero_metadata_nextEpisode": "Next Episode", - "hero_metadata_studio": "Studio", - "hero_metadata_whereToWatch": "Where to Watch", - "activity_table_quality": "Quality", - "activity_table_group": "Group", - "activity_table_source": "Source", - "activity_table_timeline": "Timeline", - "activity_table_importedTo": "Imported to:", - "activity_table_upgrade": "Upgrade:", - "library_episodeProgress": "{episodeFileCount} of {episodeCount} episodes", - "sectionRow_scrollLeft": "Scroll left", - "sectionRow_scrollRight": "Scroll right", - "nav_blocklist": "Blocklist", - "blocklist_tableSelectAll": "Select all", - "blocklist_tableSelectEntry": "Select {title}", - "blocklist_tableRemoveEntry": "Remove {title}", - "blocklist_tab_releases": "Blocked Releases", - "blocklist_tab_blockedMedia": "Blocked Media", - "blocklist_tab_blockedKeywords": "Blocked Keywords", - "blockedMedia_pageTitle": "Blocked Media", - "blockedMedia_pageSubtitle": "Manage blocked movies and TV shows that are hidden from discover, search, and library", - "blockedMedia_emptyTitle": "No blocked media", - "blockedMedia_emptyDescription": "Blocked movies and TV shows will appear here.", - "blockedMedia_searchPlaceholder": "Search blocked media...", - "blockedMedia_filterAll": "All Types", - "blockedMedia_filterMovies": "Movies", - "blockedMedia_filterTV": "TV Shows", - "blockedMedia_entryCount": "{total} items", - "blockedMedia_unblock": "Unblock", - "blockedMedia_confirmUnblockTitle": "Unblock Media", - "blockedMedia_confirmUnblockMessage": "Unblock \"{title}\"? It will appear in discover and search again, but won't be re-added to your library.", - "blockedMedia_confirmUnblockLabel": "Unblock", - "blockedMedia_confirmBlockTitle": "Block Media", - "blockedMedia_confirmBlockMessage": "Block \"{title}\"? It will be hidden from discover, search, and your library.", - "blockedMedia_confirmBlockLabel": "Block", - "blockedMedia_blocked": "\"{title}\" has been blocked", - "blockedMedia_unblocked": "\"{title}\" has been unblocked", - "blockedMedia_alreadyBlocked": "This media is already blocked", - "blockedMedia_mediaTypeMovie": "Movie", - "blockedMedia_mediaTypeTV": "TV", - "blockedMedia_dateBlocked": "Blocked {date}", - "blockedMedia_noReason": "No reason", - "hero_blockMedia": "Block Media", - "hero_blockMediaTooltip": "Block from discover and library", - "library_blockMedia": "Remove and Block", - "library_blockMediaTooltip": "Remove from library and block" + "downloadClient_realdebrid": "Real-Debrid", + "downloadClient_torbox": "TorBox", + "downloadClient_protocol_debrid": "Debrid", + "downloadClient_apiToken": "API token", + "downloadClient_removeAfterImport": "Remove from provider after import", + "settings_integrations_downloadClients_debrid": "Debrid", + "acquisition_acquireVia": "Acquire via", + "acquisition_default": "Default", + "acquisition_torrent": "Torrent", + "acquisition_debrid": "Debrid", + "acquisition_debridUnavailableReason": "Add and enable a debrid client with an API token to use debrid acquisition.", + "acquisition_defaultPreference": "Default torrent acquisition" } diff --git a/messages/es.json b/messages/es.json index 20215148b..a07e206a7 100644 --- a/messages/es.json +++ b/messages/es.json @@ -76,7 +76,6 @@ "action_upload": "Subir", "action_view": "Ver", "activity_activeCount": "{total} descargas activas", - "activity_statusCount": "{total} {status}", "activity_activeFilters_clearAll": "Limpiar todo", "activity_activeFilters_client": "Cliente", "activity_activeFilters_from": "Desde", @@ -88,6 +87,8 @@ "activity_activeFilters_toSeparator": "a", "activity_activeFilters_until": "Hasta", "activity_activeFilters_upgradesOnly": "Solo actualizaciones", + "activity_detail_confirmRemoveFromClient": "Eliminar del cliente", + "activity_detail_deleteRecordWarning": "Esto eliminará permanentemente este registro de actividad.", "activity_detail_downloadPaused": "Descarga pausada", "activity_detail_downloadRemoved": "Descarga eliminada", "activity_detail_downloadResumed": "Descarga reanudada", @@ -96,12 +97,13 @@ "activity_detail_failedToRemove": "Error al eliminar", "activity_detail_failedToResume": "Error al reanudar", "activity_detail_failedToRetry": "Error al reintentar", - "activity_detail_importRetryInitiated": "Reintento de importación iniciado", "activity_detail_importedTo": "Importado a", + "activity_detail_importRetryInitiated": "Reintento de importación iniciado", "activity_detail_indexer": "Indexador", "activity_detail_mediaType": "Tipo de medio", "activity_detail_protocol": "Protocolo", "activity_detail_releaseGroup": "Grupo de lanzamiento", + "activity_detail_removeFromClientWarning": "Esto eliminará permanentemente la descarga de tu cliente de descarga.", "activity_detail_retryImport": "Reintentar importación", "activity_detail_statusReason": "Razón del estado", "activity_detail_taskType": "Tipo de tarea", @@ -131,7 +133,8 @@ "activity_filters_torrent": "Torrent", "activity_filters_upgradesOnly": "Solo actualizaciones", "activity_filters_usenet": "Usenet", - "activity_historyCount": "{total} actividades en historial", + "activity_history_alsoRemoveFromClient": "También eliminar del cliente de descarga", + "activity_history_alsoRemoveFromClientHint": "{count} descarga(s) fallida(s) aún en el cliente", "activity_history_confirmDeleteSelectedMessage": "¿Eliminar {count} entradas de historial seleccionadas? Esto no se puede deshacer.", "activity_history_confirmDeleteSelectedTitle": "Eliminar historial seleccionado", "activity_history_confirmPurgeAllMessage": "¿Eliminar permanentemente todo el historial de actividad? Esto no se puede deshacer.", @@ -154,11 +157,12 @@ "activity_history_selectRows": "Seleccionar filas", "activity_history_skippedActiveDownloads": "{count} descargas activas omitidas", "activity_history_skippedRetryableFailed": "{count} elementos fallidos/reintentables omitidos", + "activity_historyCount": "{total} actividades en historial", "activity_importFailed": "Importación fallida", "activity_pageTitle": "Actividad - Cinephage", - "activity_queueStats_total": "Total", "activity_queue_bulkActionsHeading": "Acciones masivas", "activity_queue_bulkHint": "Selecciona filas de la cola para pausar en masa, reanudar, reintentar fallidos o quitar descargas fallidas.", + "activity_queue_clearAllFailed": "Borrar todos los fallidos", "activity_queue_clearSelection": "Limpiar selección", "activity_queue_confirmPauseMessage": "¿Pausar {count} elementos de la cola?", "activity_queue_confirmPauseTitle": "Pausar descargas seleccionadas", @@ -181,6 +185,7 @@ "activity_queue_selectAllCount": "Seleccionar todo ({count})", "activity_queue_selectRows": "Seleccionar filas", "activity_queue_skippedItems": "({count} no elegibles omitidos)", + "activity_queueStats_total": "Total", "activity_relativeTime_daysAgo": "hace {count}d", "activity_relativeTime_hoursAgo": "hace {count}h", "activity_relativeTime_justNow": "Ahora mismo", @@ -188,6 +193,7 @@ "activity_resolutionAuto": "Auto", "activity_retryMode_importInProgress": "Reintento de importación en progreso", "activity_retryMode_waitingForPath": "Esperando ruta de descarga antes de reintentar importación", + "activity_statusCount": "{total} {status}", "activity_subtitle": "Historial de descargas y búsquedas", "activity_tabActive": "Descargas activas", "activity_tabHistory": "Historial", @@ -204,26 +210,6 @@ "activity_table_showTimeline": "Mostrar línea de tiempo", "activity_taskError": "Error de búsqueda de {task}", "activity_title": "Actividad", - "calendar_addToLibrary": "Añadir a la biblioteca", - "calendar_comingUp": "Próximamente", - "calendar_inLibrary": "En biblioteca", - "calendar_more": "+{count} más", - "calendar_noUpcoming": "Nada próximamente", - "calendar_today": "Hoy", - "calendar_viewAll": "Ver Calendario", - "calendar_prefs_show": "Mostrar", - "calendar_prefs_all": "Todo", - "calendar_prefs_options": "Opciones", - "calendar_prefs_libraryOnly": "Solo películas de la biblioteca", - "calendar_prefs_upcomingNonLibrary": "Mostrar nuevos lanzamientos en Próximamente", - "calendar_prefs_minRating": "Valoración mínima", - "calendar_prefs_contentRating": "Clasificación", - "calendar_prefs_allRatings": "Todas", - "calendar_prefs_excludeAdult": "Excluir adultos", - "calendar_prefs_genres": "Géneros", - "calendar_prefs_anyRating": "Cualquiera", - "calendar_prefs_allGenres": "Todos los géneros", - "calendar_airingNow": "Emitiendo ahora", "allFormatScoresZero": "Todas las puntuaciones de formato empezarán en 0", "allowUpgrades": "Permitir actualizaciones", "aria_deleteClient": "Eliminar cliente", @@ -234,12 +220,58 @@ "auth_blankToKeep": "en blanco para mantener", "auth_password_label": "Contraseña", "auth_username_label": "Nombre de usuario", + "blockedMedia_alreadyBlocked": "Este contenido ya está bloqueado", + "blockedMedia_blocked": "\"{title}\" ha sido bloqueado", + "blockedMedia_confirmBlockLabel": "Bloquear", + "blockedMedia_confirmBlockMessage": "¿Bloquear \"{title}\"? Se ocultará del descubrimiento, búsqueda y biblioteca.", + "blockedMedia_confirmBlockTitle": "Bloquear Multimedia", + "blockedMedia_confirmUnblockLabel": "Desbloquear", + "blockedMedia_confirmUnblockMessage": "¿Desbloquear \"{title}\"? Aparecerá en descubrimiento y búsqueda, pero no se volverá a agregar a tu biblioteca.", + "blockedMedia_confirmUnblockTitle": "Desbloquear Multimedia", + "blockedMedia_dateBlocked": "Bloqueado {date}", + "blockedMedia_emptyDescription": "Las películas y programas bloqueados aparecerán aquí.", + "blockedMedia_emptyTitle": "No hay multimedia bloqueado", + "blockedMedia_entryCount": "{total} elementos", + "blockedMedia_filterAll": "Todos", + "blockedMedia_filterMovies": "Películas", + "blockedMedia_filterTV": "Programas de TV", + "blockedMedia_mediaTypeMovie": "Película", + "blockedMedia_mediaTypeTV": "TV", + "blockedMedia_noReason": "Sin razón", + "blockedMedia_pageSubtitle": "Administrar películas y programas bloqueados que están ocultos del descubrimiento, búsqueda y biblioteca", + "blockedMedia_pageTitle": "Multimedia Bloqueado", + "blockedMedia_searchPlaceholder": "Buscar multimedia bloqueado...", + "blockedMedia_unblock": "Desbloquear", + "blockedMedia_unblocked": "\"{title}\" ha sido desbloqueado", + "blocklist_tab_blockedKeywords": "Palabras Clave Bloqueadas", + "blocklist_tab_blockedMedia": "Multimedia Bloqueado", + "blocklist_tab_releases": "Lanzamientos", "builtInProfiles": "Perfiles integrados", "bulkActions_delete": "Eliminar", "bulkActions_disable": "Deshabilitar", "bulkActions_enable": "Habilitar", "bulkActions_selected": "{count} seleccionado(s)", "bulkActions_test": "Probar", + "calendar_addToLibrary": "Añadir a la biblioteca", + "calendar_airingNow": "Emitiendo ahora", + "calendar_comingUp": "Próximamente", + "calendar_inLibrary": "En biblioteca", + "calendar_more": "+{count} más", + "calendar_noUpcoming": "Nada próximamente", + "calendar_prefs_all": "Todo", + "calendar_prefs_allGenres": "Todos los géneros", + "calendar_prefs_allRatings": "Todas", + "calendar_prefs_anyRating": "Cualquiera", + "calendar_prefs_contentRating": "Clasificación", + "calendar_prefs_excludeAdult": "Excluir adultos", + "calendar_prefs_genres": "Géneros", + "calendar_prefs_libraryOnly": "Solo películas de la biblioteca", + "calendar_prefs_minRating": "Valoración mínima", + "calendar_prefs_options": "Opciones", + "calendar_prefs_show": "Mostrar", + "calendar_prefs_upcomingNonLibrary": "Mostrar nuevos lanzamientos en Próximamente", + "calendar_today": "Hoy", + "calendar_viewAll": "Ver Calendario", "channels": "canales", "collapseAll": "Contraer todo", "common_actions": "Acciones", @@ -249,12 +281,18 @@ "common_all": "Todos", "common_alpha": "Alfa", "common_alphaShort": "A", + "common_announced": "Anunciado", "common_appName": "Cinephage", "common_available": "Disponible", + "common_availableDigital": "Disponible - Digital", + "common_availablePhysical": "Disponible - Físico", + "common_availableStreaming": "Disponible - Streaming", "common_browseFolders": "Explorar carpetas", "common_builtIn": "Integrado", "common_categories": "Categorías", "common_clientBehavior": "Comportamiento del cliente", + "common_comingSoon": "Próximamente", + "common_comingToTheaters": "En cines en {days} días", "common_connecting": "Conectando...", "common_connection": "Conexión", "common_copiedToClipboard": "Copiado al portapapeles", @@ -264,6 +302,7 @@ "common_deleting": "Eliminando...", "common_description": "Descripción", "common_details": "Detalles", + "common_digitalInDays": "Digital en {days} días", "common_disabled": "Deshabilitado", "common_disconnected": "Desconectado", "common_downloaded": "Descargado", @@ -272,8 +311,8 @@ "common_enabled": "Habilitado", "common_endOfResults": "Fin de resultados", "common_episode": "Episodio", - "common_episodeSize": "Tamaño de episodio", "common_episodes": "Episodios", + "common_episodeSize": "Tamaño de episodio", "common_failedToCopy": "Error al copiar", "common_failedToSave": "Error al guardar", "common_file": "Archivo", @@ -282,6 +321,7 @@ "common_hide": "Ocultar", "common_host": "Servidor", "common_inactive": "Inactivo", + "common_inTheaters": "En cines", "common_language": "Idioma", "common_languages": "Idiomas", "common_live": "En vivo", @@ -290,21 +330,22 @@ "common_monitored": "Monitoreado", "common_mountMode": "Altmount / NZBDav (Modo montaje)", "common_movie": "Película", - "common_movieSize": "Tamaño de película", "common_movies": "Películas", + "common_movieSize": "Tamaño de película", "common_na": "N/D", "common_name": "Nombre", "common_network": "Red", "common_never": "Nunca", "common_no": "No", - "common_noResults": "No se encontraron resultados", "common_none": "Ninguno", + "common_noResults": "No se encontraron resultados", "common_notSet": "No establecido", "common_off": "Desactivado", "common_on": "Activado", "common_overview": "Sinopsis", "common_path": "Ruta", "common_pending": "Pendiente", + "common_physicalInDays": "Físico en {days} días", "common_port": "Puerto", "common_priority": "Prioridad", "common_protocol": "Protocolo", @@ -312,6 +353,7 @@ "common_qualityProfile": "Perfil de calidad", "common_rating": "Puntuación", "common_reconnecting": "Reconectando...", + "common_released": "Estrenada", "common_retry": "Reintentar", "common_rootFolder": "Carpeta raíz", "common_runtime": "Duración", @@ -329,6 +371,7 @@ "common_size": "Tamaño", "common_standard": "SABnzbd estándar", "common_status": "Estado", + "common_streamingInDays": "Streaming en {days} días", "common_syncing": "Sincronizando", "common_testConnection": "Probar conexión", "common_testing": "Probando...", @@ -341,17 +384,6 @@ "common_unknown": "Desconocido", "common_unmonitored": "No monitoreado", "common_unreleased": "No lanzado", - "common_inTheaters": "En cines", - "common_released": "Estrenada", - "common_availableDigital": "Disponible - Digital", - "common_availablePhysical": "Disponible - Físico", - "common_availableStreaming": "Disponible - Streaming", - "common_comingToTheaters": "En cines en {days} días", - "common_digitalInDays": "Digital en {days} días", - "common_physicalInDays": "Físico en {days} días", - "common_streamingInDays": "Streaming en {days} días", - "common_announced": "Anunciado", - "common_comingSoon": "Próximamente", "common_usenet": "Usenet", "common_viewAll": "Ver todo", "common_year": "Año", @@ -407,6 +439,7 @@ "dashboard_stats_filesNeedAttention": "Archivos que necesitan atención", "dashboard_stats_freeSpace": "{size} libre", "dashboard_stats_ignoredCount": "{count} ignorados", + "dashboard_stats_inTheatersCount": "{count} en cines", "dashboard_stats_missingCount": "{count} faltantes", "dashboard_stats_missingEpisodes": "Episodios faltantes", "dashboard_stats_missingEpisodesShort": "Eps faltantes", @@ -422,26 +455,25 @@ "dashboard_stats_unairedCount": "{count} no transmitidos", "dashboard_stats_unmatched": "Sin coincidencia", "dashboard_stats_unreleasedCount": "{count} sin lanzar", - "dashboard_stats_inTheatersCount": "{count} en cines", "dashboard_subtitle": "Bienvenido a Cinephage", "dashboard_title": "Panel", "discover_applyFilters": "Aplicar filtros", + "discover_certificationTvNote": "El filtrado por clasificación solo aplica a películas.", "discover_clearFilters": "Limpiar filtros", "discover_clearSearch": "Limpiar búsqueda", "discover_closeFilters": "Cerrar filtros", "discover_failedToLoadMore": "Error al cargar más elementos", - "discover_filterBadgeGenres": "{count} géneros", - "discover_filterBadgeLanguage": "Idioma", - "discover_filterBadgeMovies": "Películas", - "discover_filterBadgeTvShows": "Series de TV", + "discover_filter_contentRating": "Clasificación", "discover_filter_mediaType": "Tipo de medio", "discover_filter_minRating": "Puntuación mín.", - "discover_filter_contentRating": "Clasificación", - "discover_certificationTvNote": "El filtrado por clasificación solo aplica a películas.", "discover_filter_originalLanguage": "Idioma original", "discover_filter_releaseYear": "Año de lanzamiento", "discover_filter_sortBy": "Ordenar por", "discover_filter_watchProviders": "Proveedores de streaming", + "discover_filterBadgeGenres": "{count} géneros", + "discover_filterBadgeLanguage": "Idioma", + "discover_filterBadgeMovies": "Películas", + "discover_filterBadgeTvShows": "Series de TV", "discover_filters": "Filtros", "discover_filtersButton": "Filtros", "discover_heading": "Descubrir", @@ -485,6 +517,22 @@ "discover_tv_seasons": "Temporadas", "discover_tv_similarTitles": "Títulos similares", "discover_tv_topCast": "Reparto principal", + "downloadClient_addDownloadClient": "Añadir cliente de descarga", + "downloadClient_apiKeyHelp": "Encontrado en SABnzbd Config > General > Clave API", + "downloadClient_aria2": "aria2", + "downloadClient_deluge": "Deluge", + "downloadClient_desc_nntp": "Conexión directa a servidor Usenet para streaming NZB", + "downloadClient_desc_qbittorrent": "Desc qBittorrent", + "downloadClient_editDownloadClient": "Editar cliente de descarga", + "downloadClient_nzbget": "NZBGet", + "downloadClient_protocol_nntp": "NNTP", + "downloadClient_protocol_torrent": "Torrent", + "downloadClient_protocol_usenet": "Usenet", + "downloadClient_qbittorrent": "qBittorrent", + "downloadClient_rtorrent": "rTorrent", + "downloadClient_sabnzbd": "SABnzbd", + "downloadClient_selectType": "Selecciona el tipo de cliente de descarga que quieres añadir:", + "downloadClient_transmission": "Transmission", "downloadClientModal_addTitle": "Añadir cliente de descarga", "downloadClientModal_categoriesSection": "Categorías", "downloadClientModal_clientPath": "Ruta del cliente", @@ -502,18 +550,6 @@ "downloadClientModal_portLabel": "Puerto", "downloadClientModal_recentPriority": "Reciente", "downloadClientModal_selectType": "Selecciona el tipo de cliente de descarga que quieres añadir:", - "downloadClientModal_startAs": "Iniciar como", - "downloadClientModal_tempFolder": "Carpeta temporal de descarga", - "downloadClientModal_tvCategory": "Series de TV", - "downloadClientModal_urlBaseDescription": "Prefijo de ruta añadido después del host y puerto.", - "downloadClientModal_urlBaseLabel": "Base de URL", - "downloadClientModal_useUrlBase": "Usar base de URL", - "downloadClient_addDownloadClient": "Añadir cliente de descarga", - "downloadClient_apiKeyHelp": "Encontrado en SABnzbd Config > General > Clave API", - "downloadClient_aria2": "aria2", - "downloadClient_deluge": "Deluge", - "downloadClient_desc_nntp": "Conexión directa a servidor Usenet para streaming NZB", - "downloadClient_desc_qbittorrent": "Desc qBittorrent", "downloadClientModal_stalledDescription": "Eliminar automáticamente torrents estancados después de {timeout}m si el progreso es inferior al {threshold}%.", "downloadClientModal_stalledDisabled": "El manejo de descargas estancadas está desactivado.", "downloadClientModal_stalledDownloads": "Descargas estancadas", @@ -521,16 +557,12 @@ "downloadClientModal_stalledProgress": "Progreso inferior a (%)", "downloadClientModal_stalledSaved": "Guardado", "downloadClientModal_stalledTimeout": "Tiempo de espera (min)", - "downloadClient_editDownloadClient": "Editar cliente de descarga", - "downloadClient_nzbget": "NZBGet", - "downloadClient_protocol_nntp": "NNTP", - "downloadClient_protocol_torrent": "Torrent", - "downloadClient_protocol_usenet": "Usenet", - "downloadClient_qbittorrent": "qBittorrent", - "downloadClient_rtorrent": "rTorrent", - "downloadClient_sabnzbd": "SABnzbd", - "downloadClient_selectType": "Selecciona el tipo de cliente de descarga que quieres añadir:", - "downloadClient_transmission": "Transmission", + "downloadClientModal_startAs": "Iniciar como", + "downloadClientModal_tempFolder": "Carpeta temporal de descarga", + "downloadClientModal_tvCategory": "Series de TV", + "downloadClientModal_urlBaseDescription": "Prefijo de ruta añadido después del host y puerto.", + "downloadClientModal_urlBaseLabel": "Base de URL", + "downloadClientModal_useUrlBase": "Usar base de URL", "episodeMax": "Episodio máx. (MB)", "episodeMin": "Episodio mín. (MB)", "error_description": "Cinephage encontró un problema inesperado al cargar esta vista. Puedes reintentar, volver al inicio o compartir el ID de soporte si necesitas ayuda para depurarlo.", @@ -543,7 +575,6 @@ "error_supportIdLabel": "ID de error", "error_supportIdUnavailable": "No disponible", "expandAll": "Expandir todo", - "formatScoresCopied": "Las puntuaciones de formato se copiarán del perfil seleccionado", "formats": "Puntuaciones de formato", "formats_addCondition": "Añadir condición", "formats_allCategories": "Todas las categorías", @@ -561,10 +592,10 @@ "formats_conditionLogicTitle": "Lógica de condición:", "formats_conditionNamePlaceholder": "Nombre de condición", "formats_conditionRemoveAria": "Quitar condición", - "formats_conditionTypeLabel": "Tipo", - "formats_conditionValueLabel": "Valor", "formats_conditionsDivider": "Condiciones", "formats_conditionsHeader": "Condiciones", + "formats_conditionTypeLabel": "Tipo", + "formats_conditionValueLabel": "Valor", "formats_createCustomFormat": "Crear formato personalizado", "formats_createFormat": "Crear formato", "formats_custom": "Personalizado", @@ -597,7 +628,10 @@ "formats_testNotMatched": "No coincide", "formats_testPlaceholder": "Pelicula.2024.2160p.BluRay.REMUX.HEVC.TrueHD.Atmos-GRUPO", "formats_viewFormat": "Ver formato", + "formatScoresCopied": "Las puntuaciones de formato se copiarán del perfil seleccionado", "general": "General", + "hero_blockMedia": "Bloquear medio", + "hero_blockMediaTooltip": "Bloquear del descubrimiento y biblioteca", "indexer_desc_automaticSearch": "Buscar cuando se añaden elementos", "indexer_desc_interactiveSearch": "Búsquedas manuales desde la interfaz", "indexer_desc_rejectDeadTorrents": "Omitir lanzamientos con 0 seeders", @@ -612,14 +646,14 @@ "library_add_autoDownloadSubtitlesYes": "Auto descargar subtítulos sí", "library_add_episodeCount": "{count} episodios", "library_add_monitor": "Monitorear", - "library_add_monitorNewItems": "Monitorear elementos nuevos", - "library_add_monitorSpecials": "Monitorear especiales", - "library_add_monitorSpecialsNo": "No monitorear especiales", - "library_add_monitorSpecialsYes": "Monitorear especiales", "library_add_monitoredDescNo": "No monitoreado - nuevos episodios no serán descargados", "library_add_monitoredDescYes": "Monitoreado - nuevos episodios serán descargados", "library_add_monitoredEps": "Episodios monitoreados", "library_add_monitoringPreview": "Vista previa de monitoreo", + "library_add_monitorNewItems": "Monitorear elementos nuevos", + "library_add_monitorSpecials": "Monitorear especiales", + "library_add_monitorSpecialsNo": "No monitorear especiales", + "library_add_monitorSpecialsYes": "Monitorear especiales", "library_add_noRootFoldersConfigured": "Sin carpetas raíz configuradas", "library_add_notMonitored": "No monitoreado", "library_add_rootFolderDesc": "Los archivos se organizarán en esta carpeta", @@ -664,11 +698,11 @@ "library_batchMatch_failedToGeneratePreview": "Error al generar vista previa de coincidencia", "library_batchMatch_failedToMatchFiles": "Error al coincidir archivos", "library_batchMatch_filesFailed": "{count} archivos fallidos", + "library_batchMatch_matchedFiles": "{count} archivos coincididos", "library_batchMatch_matchFiles": "Coincidir archivos", + "library_batchMatch_matchingFiles": "Coincidiendo archivos...", "library_batchMatch_matchPreview": "Vista previa de coincidencia", "library_batchMatch_matchPreviewLoading": "Generando vista previa...", - "library_batchMatch_matchedFiles": "{count} archivos coincididos", - "library_batchMatch_matchingFiles": "Coincidiendo archivos...", "library_batchMatch_searchFailed": "Búsqueda fallida", "library_batchMatch_searchForMovie": "Buscar película...", "library_batchMatch_searchForTvShow": "Buscar serie de TV...", @@ -677,6 +711,8 @@ "library_batchMatch_sizeMB": "{size} MB", "library_batchMatch_title": "Coincidir archivos en lote", "library_batchMatch_unknownSize": "Tamaño desconocido", + "library_blockMedia": "Eliminar y bloquear", + "library_blockMediaTooltip": "Eliminar de la biblioteca y bloquear", "library_bulkActionBar_autoDownloadSubs": "Auto-descargar subtítulos", "library_bulkActionBar_autoDownloadTitle": "Descargar subtítulos", "library_bulkActionBar_episodesSelected": "{count} episodios seleccionados", @@ -797,10 +833,10 @@ "library_import_createNewHint": "Crea entrada de biblioteca si es necesario e importa a la carpeta raíz seleccionada.", "library_import_destinationNotNeededForExistingMedia": "Todos los elementos de esta selección de medios coinciden con entradas de biblioteca existentes; no se requiere selección de destino.", "library_import_destinationRootFolder": "Carpeta raíz de destino", - "library_import_detectMedia": "Detectar medios", "library_import_detectedItems": "Elementos detectados", "library_import_detectedItemsHint": "Revisa y selecciona elementos de esta cola para importar.", "library_import_detecting": "Detectando", + "library_import_detectMedia": "Detectar medios", "library_import_directImportFor": "Esta es una importación directa para", "library_import_episodeCount": "{count} episodios", "library_import_episodeCountSingular": "{count} episodio", @@ -822,17 +858,17 @@ "library_import_heading": "Importar medios", "library_import_importAnother": "Importar otro", "library_import_importComplete": "Importación completa", + "library_import_importedCount": "{count} importado(s)", + "library_import_importedPathLabel": "Ruta importada:", "library_import_importFailed": "Importación fallida", + "library_import_importing": "Importando...", + "library_import_importingIntoExisting": "Importando a elemento de biblioteca existente para:", "library_import_importNextDetected": "Importar siguiente elemento detectado ({count})", "library_import_importOpenedFor": "Esta importación se abrió para", "library_import_importSelectionHeading": "Selección de importación", "library_import_importSelectionHint": "Las acciones de importación se ejecutan desde este paso. Finaliza tus elementos seleccionados abajo e inicia la importación.", "library_import_importTargetHeading": "Objetivo de importación", "library_import_importTargetHint": "Selecciona si importar a un elemento de biblioteca existente o crear uno nuevo.", - "library_import_importedCount": "{count} importado(s)", - "library_import_importedPathLabel": "Ruta importada:", - "library_import_importing": "Importando...", - "library_import_importingIntoExisting": "Importando a elemento de biblioteca existente para:", "library_import_inLibrary": "En biblioteca", "library_import_itemSkipped": "Este elemento está omitido y excluido de la importación masiva.", "library_import_loadingFolders": "Cargando carpetas...", @@ -870,19 +906,20 @@ "library_import_seasonOverrideHint": "Solo se aplica a archivos a los que les falte una temporada detectada.", "library_import_seasonOverrideLabel": "Anulación de temporada (opcional)", "library_import_seasonOverridePlaceholder": "Usar cuando los nombres de archivo no tengan números de temporada", - "library_import_seasonUnmapped": "Temporada sin mapear", "library_import_seasonsDetectedInline": "Temporadas: {seasons}", "library_import_seasonsLabel": "temporadas: {seasons}", - "library_import_selectItem": "Seleccionar elemento", - "library_import_selectRootFolder": "Seleccionar carpeta raíz", - "library_import_selectSeason": "Seleccionar temporada", + "library_import_seasonUnmapped": "Temporada sin mapear", "library_import_selectedCount": "{count} seleccionado(s)", "library_import_selectedItem": "Elemento seleccionado", "library_import_selectedItemsHeading": "Elementos seleccionados", + "library_import_selectItem": "Seleccionar elemento", + "library_import_selectRootFolder": "Seleccionar carpeta raíz", + "library_import_selectSeason": "Seleccionar temporada", + "library_import_showRootFolders": "Mostrar carpetas raíz", "library_import_skipItem": "Omitir elemento", - "library_import_skipSeason": "Omitir temporada", "library_import_skippedCount": "{count} omitido(s)", "library_import_skippedOfTotal": "{skipped} de {total} omitidos", + "library_import_skipSeason": "Omitir temporada", "library_import_source": "Origen", "library_import_sourcePathHintFile": "Selecciona el archivo de medios a importar para esta película.", "library_import_sourcePathHintGeneral": "Selecciona la ruta a tu archivo o carpeta de medios.", @@ -908,7 +945,6 @@ "library_import_tvShowLabel": "Serie de TV", "library_import_unmatchedTvSeries": "Series de TV sin coincidencia", "library_import_useFolder": "Usar carpeta", - "library_import_showRootFolders": "Mostrar carpetas raíz", "library_import_viewInLibrary": "Ver en biblioteca", "library_import_viewLastImported": "Ver último importado", "library_libraryMediaTable_addedColumn": "Añadido", @@ -939,9 +975,9 @@ "library_matchFile_clickToMatch": "Haz clic para coincidir archivo", "library_matchFile_errorMatching": "Error al coincidir archivo", "library_matchFile_failedToMatch": "Error al coincidir archivo", - "library_matchFile_matchTo": "Coincidir con {title}", "library_matchFile_matchedTo": "Coincidido con {title}", "library_matchFile_matchedToEpisode": "Coincidido con {title} - {episode}", + "library_matchFile_matchTo": "Coincidir con {title}", "library_matchFile_searchFailed": "Búsqueda fallida", "library_matchFile_searchHint": "Buscar en TMDB para encontrar coincidencia", "library_matchFile_searchPlaceholder": "Buscar en TMDB...", @@ -954,8 +990,8 @@ "library_matchFolder_fileCount_other": "{count} archivos", "library_matchFolder_filesFailed": "{count} archivos fallidos", "library_matchFolder_filesWillBeMatched": "{count} archivos serán coincidentes", - "library_matchFolder_matchFiles": "Coincidir archivos", "library_matchFolder_matchedFiles": "{count} archivos coincidentes", + "library_matchFolder_matchFiles": "Coincidir archivos", "library_matchFolder_searchFailed": "Búsqueda fallida", "library_matchFolder_searchHint": "Buscar en TMDB para encontrar coincidencia", "library_matchFolder_searchPlaceholder": "Buscar en TMDB...", @@ -1018,8 +1054,8 @@ "library_movieDetail_monitoringEnabled": "Monitoreo activado.", "library_movieDetail_moveQueued": "Movimiento en cola. La transferencia de archivos ha comenzado y aparecerá en Actividad hasta su finalización.", "library_movieDetail_originalTitle": "Título original", - "library_movieDetail_overviewHeading": "Sinopsis", "library_movieDetail_otherMoviesInCollection": "Otras películas en esta colección", + "library_movieDetail_overviewHeading": "Sinopsis", "library_movieDetail_pageTitle": "{title} - Biblioteca - Cinephage", "library_movieDetail_path": "Ruta", "library_movieDetail_rename": "Renombrar", @@ -1084,6 +1120,7 @@ "library_movies_noFilterMatchHint": "Prueba ajustando tus filtros para ver más resultados.", "library_movies_noSearchMatch": "Ninguna película coincide con \"{query}\"", "library_movies_ofTotal": "de {total}", + "library_movies_other": "Otros", "library_movies_pageTitle": "Películas - Biblioteca - Cinephage", "library_movies_profileDefault": "{name} (Predeterminado)", "library_movies_searchPlaceholder": "Buscar películas...", @@ -1092,10 +1129,10 @@ "library_movies_selectAllShort": "Todos", "library_movies_sortAddedAsc": "Fecha añadida (Más antiguas)", "library_movies_sortAddedDesc": "Fecha añadida (Más recientes)", - "library_movies_sortSizeAsc": "Tamaño (Menor)", - "library_movies_sortSizeDesc": "Tamaño (Mayor)", "library_movies_sortCollectionAsc": "Colección (A-Z)", "library_movies_sortCollectionDesc": "Colección (Z-A)", + "library_movies_sortSizeAsc": "Tamaño (Menor)", + "library_movies_sortSizeDesc": "Tamaño (Mayor)", "library_movies_sortTitleAsc": "Título (A-Z)", "library_movies_sortTitleDesc": "Título (Z-A)", "library_movies_sortYearAsc": "Año (Más antiguos)", @@ -1103,9 +1140,8 @@ "library_movies_switchToGrid": "Cambiar a vista de cuadrícula", "library_movies_switchToList": "Cambiar a vista de lista", "library_movies_tryDifferentSearch": "Prueba con un término de búsqueda diferente.", - "library_movies_viewGrouped": "Agrupado", - "library_movies_other": "Otros", "library_movies_unmonitorAll": "No monitorear todo", + "library_movies_viewGrouped": "Agrupado", "library_renamePreview_allCorrect": "Todos los nombres de archivo son correctos", "library_renamePreview_change": "Cambiar", "library_renamePreview_collisionBadge": "Colisión", @@ -1173,9 +1209,9 @@ "library_scoreDetail_streamingServiceLabel": "Servicio de streaming", "library_scoreDetail_title": "Detalles de puntuación", "library_scoreDetail_total": "Total", - "library_scoreDetail_upgradeStatus": "Estado de actualización", "library_scoreDetail_upgradesDisabled": "Mejoras deshabilitadas", "library_scoreDetail_upgradesEnabled": "Mejoras habilitadas", + "library_scoreDetail_upgradeStatus": "Estado de actualización", "library_seasonAccordion_actionsColumn": "Acciones", "library_seasonAccordion_airDateColumn": "Fecha de emisión", "library_seasonAccordion_autoDownloadSubs": "Auto-descargar subtitulos", @@ -1218,9 +1254,9 @@ "library_seriesEdit_standardDesc": "Series de TV estándar con numeración de temporada/episodio", "library_seriesEdit_title": "Editar serie", "library_seriesHeader_allMissing": "Todos los episodios faltantes", - "library_seriesHeader_autoGrab": "Auto-captura", "library_seriesHeader_autoDownloadSubs": "Auto-descargar subtitulos", "library_seriesHeader_autoDownloadSubsTooltip": "Descargar subtitulos para todos los episodios faltantes", + "library_seriesHeader_autoGrab": "Auto-captura", "library_seriesHeader_autoGrabTooltip": "Buscar y capturar automáticamente episodios faltantes", "library_seriesHeader_complete": "Completo", "library_seriesHeader_grabbedCount": "{count} capturados", @@ -1243,32 +1279,6 @@ "library_seriesType_dailyLabel": "Diario", "library_seriesType_standardDesc": "Series de TV estándar con numeración de temporada/episodio", "library_seriesType_standardLabel": "Estándar", - "library_tvDetail_deleteEpisodeTitle": "Eliminar episodio", - "library_tvDetail_deleteSeasonTitle": "Eliminar temporada", - "library_tvDetail_deleteSeriesTitle": "Eliminar serie", - "library_tvDetail_done": "Hecho", - "library_tvDetail_episodeFallback": "Episodio {number}", - "library_tvDetail_exitSelectionMode": "Salir del modo de selección", - "library_tvDetail_metaDescription": "Gestionar {title} en tu biblioteca", - "library_tvDetail_monitoringDisabled": "El monitoreo está desactivado.", - "library_tvDetail_monitoringDisabledHint": "Los interruptores de temporada y episodio están bloqueados. Activa el monitoreo de la serie para desbloquearlos.", - "library_tvDetail_monitoringEnabled": "El monitoreo de la serie está activado.", - "library_tvDetail_noSeasonsFound": "No se encontraron temporadas", - "library_tvDetail_overviewHeading": "Sinopsis", - "library_tvDetail_pageTitle": "{title} - Biblioteca - Cinephage", - "library_tvDetail_rename": "Renombrar", - "library_tvDetail_renameFilesTitle": "Renombrar archivos", - "library_tvDetail_seasonFallback": "Temporada {number}", - "library_tvDetail_seasonFolders": "Carpetas de temporada", - "library_tvDetail_seasonsHeading": "Temporadas", - "library_tvDetail_select": "Seleccionar", - "library_tvDetail_selectEpisodes": "Seleccionar episodios", - "library_tvDetail_sseConnecting": "Conectando", - "library_tvDetail_sseLive": "En vivo", - "library_tvDetail_sseOffline": "Desconectado", - "library_tvDetail_sseReconnecting": "Reconectando", - "library_tvDetail_syncSubtitles": "Sincronizar subtítulos", - "library_tvDetail_tvdbId": "ID de TVDB", "library_tv_clearFilters": "Limpiar filtros", "library_tv_clearSearch": "Limpiar búsqueda", "library_tv_clearSearchBtn": "Limpiar búsqueda", @@ -1326,6 +1336,32 @@ "library_tv_switchToList": "Cambiar a vista de lista", "library_tv_tryDifferentSearch": "Prueba con un término de búsqueda diferente.", "library_tv_unmonitorAll": "No monitorear todo", + "library_tvDetail_deleteEpisodeTitle": "Eliminar episodio", + "library_tvDetail_deleteSeasonTitle": "Eliminar temporada", + "library_tvDetail_deleteSeriesTitle": "Eliminar serie", + "library_tvDetail_done": "Hecho", + "library_tvDetail_episodeFallback": "Episodio {number}", + "library_tvDetail_exitSelectionMode": "Salir del modo de selección", + "library_tvDetail_metaDescription": "Gestionar {title} en tu biblioteca", + "library_tvDetail_monitoringDisabled": "El monitoreo está desactivado.", + "library_tvDetail_monitoringDisabledHint": "Los interruptores de temporada y episodio están bloqueados. Activa el monitoreo de la serie para desbloquearlos.", + "library_tvDetail_monitoringEnabled": "El monitoreo de la serie está activado.", + "library_tvDetail_noSeasonsFound": "No se encontraron temporadas", + "library_tvDetail_overviewHeading": "Sinopsis", + "library_tvDetail_pageTitle": "{title} - Biblioteca - Cinephage", + "library_tvDetail_rename": "Renombrar", + "library_tvDetail_renameFilesTitle": "Renombrar archivos", + "library_tvDetail_seasonFallback": "Temporada {number}", + "library_tvDetail_seasonFolders": "Carpetas de temporada", + "library_tvDetail_seasonsHeading": "Temporadas", + "library_tvDetail_select": "Seleccionar", + "library_tvDetail_selectEpisodes": "Seleccionar episodios", + "library_tvDetail_sseConnecting": "Conectando", + "library_tvDetail_sseLive": "En vivo", + "library_tvDetail_sseOffline": "Desconectado", + "library_tvDetail_sseReconnecting": "Reconectando", + "library_tvDetail_syncSubtitles": "Sincronizar subtítulos", + "library_tvDetail_tvdbId": "ID de TVDB", "library_unmatched_collapseAll": "Contraer todo", "library_unmatched_deleteFileTitle": "Eliminar archivo", "library_unmatched_expandAll": "Expandir todo", @@ -1351,6 +1387,32 @@ "livetv_accountModal_streamVerified": "Stream verificado", "livetv_accountModal_testSuccess": "Conexión exitosa", "livetv_accountModal_testSuccessWithStatus": "Conexión exitosa ({status})", + "livetv_accounts_addAccount": "Añadir cuenta", + "livetv_accounts_connectionTestFailed": "Prueba de conexión fallida: {name}", + "livetv_accounts_connectionTestPassed": "Prueba de conexión exitosa: {name}", + "livetv_accounts_deleteAccountMessage": "¿Eliminar \"{name}\"? Esto lo eliminará de TV en vivo y detendrá la sincronización de canales.", + "livetv_accounts_deleteAccountMessagePrefix": "¿Seguro que quieres eliminar ", + "livetv_accounts_deleteAccountMessageSuffix": "? Esto lo eliminará de Live TV y detendrá la sincronización de canales.", + "livetv_accounts_deleteAccountTitle": "Eliminar cuenta", + "livetv_accounts_epgNotConfigured": "EPG no configurado", + "livetv_accounts_epgReachable": "EPG accesible", + "livetv_accounts_epgUnreachable": "EPG no accesible", + "livetv_accounts_epgUnreachableWithError": "EPG no accesible ({error})", + "livetv_accounts_failedToDeleteAccount": "Error al eliminar cuenta", + "livetv_accounts_failedToLoadAccounts": "Error al cargar cuentas", + "livetv_accounts_failedToSaveAccount": "Error al guardar cuenta", + "livetv_accounts_failedToSyncAccount": "Error al sincronizar cuenta", + "livetv_accounts_failedToTestAccount": "Error al probar cuenta", + "livetv_accounts_failedToTestConfig": "Error al probar configuración de cuenta", + "livetv_accounts_failedToUpdateAccount": "Error al actualizar cuenta", + "livetv_accounts_heading": "Cuentas de TV en vivo", + "livetv_accounts_invalidTestEndpointResponse": "Respuesta inválida del endpoint de prueba", + "livetv_accounts_invalidTestResponse": "Respuesta de prueba inválida", + "livetv_accounts_pageTitle": "Cuentas de TV en vivo - Cinephage", + "livetv_accounts_subtitle": "Gestiona tus cuentas IPTV (Stalker, XStream, M3U)", + "livetv_accounts_testSummaryCategories": "{count} categorías", + "livetv_accounts_testSummaryChannels": "{count} canales", + "livetv_accounts_unknownTestFailure": "Fallo de prueba desconocido", "livetv_accountTable_accountColumn": "Cuenta", "livetv_accountTable_actionsColumn": "Acciones", "livetv_accountTable_addHint": "Añade una cuenta IPTV para comenzar", @@ -1379,36 +1441,10 @@ "livetv_accountTable_syncingStatus": "Sincronizando...", "livetv_accountTable_typeColumn": "Tipo", "livetv_accountTable_urlSource": "Fuente URL", - "livetv_accounts_addAccount": "Añadir cuenta", - "livetv_accounts_connectionTestFailed": "Prueba de conexión fallida: {name}", - "livetv_accounts_connectionTestPassed": "Prueba de conexión exitosa: {name}", - "livetv_accounts_deleteAccountMessage": "¿Eliminar \"{name}\"? Esto lo eliminará de TV en vivo y detendrá la sincronización de canales.", - "livetv_accounts_deleteAccountMessagePrefix": "¿Seguro que quieres eliminar ", - "livetv_accounts_deleteAccountMessageSuffix": "? Esto lo eliminará de Live TV y detendrá la sincronización de canales.", - "livetv_accounts_deleteAccountTitle": "Eliminar cuenta", - "livetv_accounts_epgNotConfigured": "EPG no configurado", - "livetv_accounts_epgReachable": "EPG accesible", - "livetv_accounts_epgUnreachable": "EPG no accesible", - "livetv_accounts_epgUnreachableWithError": "EPG no accesible ({error})", - "livetv_accounts_failedToDeleteAccount": "Error al eliminar cuenta", - "livetv_accounts_failedToLoadAccounts": "Error al cargar cuentas", - "livetv_accounts_failedToSaveAccount": "Error al guardar cuenta", - "livetv_accounts_failedToSyncAccount": "Error al sincronizar cuenta", - "livetv_accounts_failedToTestAccount": "Error al probar cuenta", - "livetv_accounts_failedToTestConfig": "Error al probar configuración de cuenta", - "livetv_accounts_failedToUpdateAccount": "Error al actualizar cuenta", - "livetv_accounts_heading": "Cuentas de TV en vivo", - "livetv_accounts_invalidTestEndpointResponse": "Respuesta inválida del endpoint de prueba", - "livetv_accounts_invalidTestResponse": "Respuesta de prueba inválida", - "livetv_accounts_pageTitle": "Cuentas de TV en vivo - Cinephage", - "livetv_accounts_subtitle": "Gestiona tus cuentas IPTV (Stalker, XStream, M3U)", - "livetv_accounts_testSummaryCategories": "{count} categorías", - "livetv_accounts_testSummaryChannels": "{count} canales", - "livetv_accounts_unknownTestFailure": "Fallo de prueba desconocido", "livetv_channelBrowserModal_add": "Añadir", + "livetv_channelBrowserModal_added": "Añadido", "livetv_channelBrowserModal_addEntireCategory": "Añadir categoría", "livetv_channelBrowserModal_addSelected": "Añadir seleccionados", - "livetv_channelBrowserModal_added": "Añadido", "livetv_channelBrowserModal_allAccounts": "Todas las cuentas", "livetv_channelBrowserModal_allCategories": "Todas las categorías", "livetv_channelBrowserModal_allChannelsAdded": "Todos los canales añadidos", @@ -1550,19 +1586,14 @@ "livetv_channelRemoveModal_removeMultiple": "¿Eliminar {count} canales de tu programación?", "livetv_channelRemoveModal_removeSingle": "¿Eliminar \"{name}\" de tu programación?", "livetv_channelRemoveModal_title": "Eliminar canal", - "livetv_channelScheduleModal_epgFrom": "EPG desde {source}", - "livetv_channelScheduleModal_failedToLoad": "Error al cargar horario", - "livetv_channelScheduleModal_liveBadge": "EN VIVO", - "livetv_channelScheduleModal_noPrograms": "Sin programas programados", - "livetv_channelScheduleModal_percentRemaining": "{percent}% restante", "livetv_channels_appliedCleanNames": "Nombres limpiados aplicados a {count} canales", "livetv_channels_browseChannels": "Explorar canales", "livetv_channels_categoriesButton": "Categorías", "livetv_channels_copyEpgUrl": "Copiar URL EPG", "livetv_channels_copyM3uUrl": "Copiar URL M3U", + "livetv_channels_downloadedLogos": "{count} logos descargados", "livetv_channels_downloadFailed": "Descarga fallida", "livetv_channels_downloadLogosTitle": "Descargar logos de canales", - "livetv_channels_downloadedLogos": "{count} logos descargados", "livetv_channels_emptyDescription": "Añade canales desde tus cuentas para construir tu línea", "livetv_channels_emptyHeading": "Sin canales en tu línea", "livetv_channels_exportButton": "Exportar", @@ -1597,6 +1628,19 @@ "livetv_channels_subtitle": "Organiza tu línea de canales", "livetv_channels_updatedCategory": "Categoría actualizada para {count} canales", "livetv_channels_xmltvEpgGuide": "Guía EPG XMLTV", + "livetv_channelScheduleModal_epgFrom": "EPG desde {source}", + "livetv_channelScheduleModal_failedToLoad": "Error al cargar horario", + "livetv_channelScheduleModal_liveBadge": "EN VIVO", + "livetv_channelScheduleModal_noPrograms": "Sin programas programados", + "livetv_channelScheduleModal_percentRemaining": "{percent}% restante", + "livetv_epg_failedToCancelSync": "Error al cancelar sincronización de EPG", + "livetv_epg_failedToTriggerSync": "Error al activar sincronización de EPG", + "livetv_epg_heading": "EPG", + "livetv_epg_pageTitle": "EPG - TV en vivo - Cinephage", + "livetv_epg_subtitle": "Guía electrónica de programas", + "livetv_epg_tabCoverage": "Cobertura", + "livetv_epg_tabGuide": "Guía", + "livetv_epg_tabStatus": "Estado", "livetv_epgCoverage_currentProgram": "Programa actual", "livetv_epgCoverage_default": "Predeterminado", "livetv_epgCoverage_doubleClickToChange": "Doble clic para cambiar fuente de EPG", @@ -1641,29 +1685,6 @@ "livetv_epgSourcePicker_programsCount": "{count} programas", "livetv_epgSourcePicker_searchPlaceholder": "Buscar canales...", "livetv_epgSourcePicker_title": "Seleccionar fuente de EPG", - "livetv_epgStatusPanel_accountsHeading": "Cuentas", - "livetv_epgStatusPanel_cancelAccountTooltip": "Cancelar sincronización para esta cuenta", - "livetv_epgStatusPanel_cancelRequestedTooltip": "Cancelación solicitada", - "livetv_epgStatusPanel_cancelSync": "Cancelar sincronización", - "livetv_epgStatusPanel_cancelling": "Cancelando...", - "livetv_epgStatusPanel_errorTitle": "Error", - "livetv_epgStatusPanel_errorTitlePlural": "Errores", - "livetv_epgStatusPanel_lastSync": "Última sincronización", - "livetv_epgStatusPanel_moreAccountIssues": "+{count} problemas más", - "livetv_epgStatusPanel_moreAccountWarnings": "+{count} advertencias más", - "livetv_epgStatusPanel_nextSync": "Próxima sincronización", - "livetv_epgStatusPanel_noAccounts": "No hay cuentas configuradas", - "livetv_epgStatusPanel_noEpgDataMessage": "No hay datos de EPG disponibles. Configura cuentas con fuentes de EPG.", - "livetv_epgStatusPanel_syncAccountTooltip": "Sincronizar esta cuenta", - "livetv_epgStatusPanel_syncAll": "Sincronizar todo", - "livetv_epgStatusPanel_syncInProgress": "Sincronización en progreso", - "livetv_epgStatusPanel_syncRunningTooltip": "Sincronización en ejecución", - "livetv_epgStatusPanel_syncedRelative": "Sincronizado hace {time}", - "livetv_epgStatusPanel_syncingAll": "Sincronizando todas las cuentas...", - "livetv_epgStatusPanel_totalPrograms": "{count} programas", - "livetv_epgStatusPanel_unknownError": "Error desconocido", - "livetv_epgStatusPanel_warningTitle": "Advertencia", - "livetv_epgStatusPanel_warningTitlePlural": "Advertencias", "livetv_epgStatus_accountDetails": "Detalles de cuenta", "livetv_epgStatus_daysAgo": "hace {count}d", "livetv_epgStatus_errorBadge": "Error", @@ -1682,18 +1703,33 @@ "livetv_epgStatus_noEpgTooltip": "EPG no configurado para esta cuenta", "livetv_epgStatus_programsLabel": "Programas", "livetv_epgStatus_syncButton": "Sincronizar ahora", - "livetv_epgStatus_syncTooltip": "Sincronizar datos de EPG desde todas las fuentes", "livetv_epgStatus_syncing": "Sincronizando...", + "livetv_epgStatus_syncTooltip": "Sincronizar datos de EPG desde todas las fuentes", "livetv_epgStatus_title": "Estado de EPG", "livetv_epgStatus_unableToLoad": "No se pudo cargar el estado de EPG", - "livetv_epg_failedToCancelSync": "Error al cancelar sincronización de EPG", - "livetv_epg_failedToTriggerSync": "Error al activar sincronización de EPG", - "livetv_epg_heading": "EPG", - "livetv_epg_pageTitle": "EPG - TV en vivo - Cinephage", - "livetv_epg_subtitle": "Guía electrónica de programas", - "livetv_epg_tabCoverage": "Cobertura", - "livetv_epg_tabGuide": "Guía", - "livetv_epg_tabStatus": "Estado", + "livetv_epgStatusPanel_accountsHeading": "Cuentas", + "livetv_epgStatusPanel_cancelAccountTooltip": "Cancelar sincronización para esta cuenta", + "livetv_epgStatusPanel_cancelling": "Cancelando...", + "livetv_epgStatusPanel_cancelRequestedTooltip": "Cancelación solicitada", + "livetv_epgStatusPanel_cancelSync": "Cancelar sincronización", + "livetv_epgStatusPanel_errorTitle": "Error", + "livetv_epgStatusPanel_errorTitlePlural": "Errores", + "livetv_epgStatusPanel_lastSync": "Última sincronización", + "livetv_epgStatusPanel_moreAccountIssues": "+{count} problemas más", + "livetv_epgStatusPanel_moreAccountWarnings": "+{count} advertencias más", + "livetv_epgStatusPanel_nextSync": "Próxima sincronización", + "livetv_epgStatusPanel_noAccounts": "No hay cuentas configuradas", + "livetv_epgStatusPanel_noEpgDataMessage": "No hay datos de EPG disponibles. Configura cuentas con fuentes de EPG.", + "livetv_epgStatusPanel_syncAccountTooltip": "Sincronizar esta cuenta", + "livetv_epgStatusPanel_syncAll": "Sincronizar todo", + "livetv_epgStatusPanel_syncedRelative": "Sincronizado hace {time}", + "livetv_epgStatusPanel_syncingAll": "Sincronizando todas las cuentas...", + "livetv_epgStatusPanel_syncInProgress": "Sincronización en progreso", + "livetv_epgStatusPanel_syncRunningTooltip": "Sincronización en ejecución", + "livetv_epgStatusPanel_totalPrograms": "{count} programas", + "livetv_epgStatusPanel_unknownError": "Error desconocido", + "livetv_epgStatusPanel_warningTitle": "Advertencia", + "livetv_epgStatusPanel_warningTitlePlural": "Advertencias", "livetv_form_m3u_autoRefresh": "Auto-actualizar", "livetv_form_m3u_enabled": "Habilitado", "livetv_form_m3u_epgUrlHint": "URL para datos de Guía Electrónica de Programación (opcional)", @@ -1752,9 +1788,9 @@ "livetv_iptvOrg_loadingCountries": "Cargando países...", "livetv_iptvOrg_removeCountry": "Eliminar {country}", "livetv_iptvOrg_searchPlaceholder": "Buscar países...", - "livetv_iptvOrg_selectHint": "Selecciona países para añadir canales", "livetv_iptvOrg_selectedCount": "{count} países seleccionados", "livetv_iptvOrg_selectedCountSingular": "1 país seleccionado", + "livetv_iptvOrg_selectHint": "Selecciona países para añadir canales", "livetv_portalScanModal_addNewPortal": "Añadir nuevo portal", "livetv_portalScanModal_backButton": "Atrás", "livetv_portalScanModal_cancelButton": "Cancelar", @@ -1788,8 +1824,8 @@ "livetv_portalScanModal_portalUrlPlaceholder": "https://portal.example.com", "livetv_portalScanModal_randomScan": "Escaneo aleatorio", "livetv_portalScanModal_rangeSize": "Tamaño del rango", - "livetv_portalScanModal_scanTypeLabel": "Tipo de escaneo", "livetv_portalScanModal_scanningPortalLabel": "Escaneando portal", + "livetv_portalScanModal_scanTypeLabel": "Tipo de escaneo", "livetv_portalScanModal_selectExistingPortal": "Seleccionar portal existente", "livetv_portalScanModal_sequentialScan": "Escaneo secuencial", "livetv_portalScanModal_startMacLabel": "MAC inicial", @@ -1812,8 +1848,8 @@ "livetv_portalScanProgress_scanCancelled": "Escaneo cancelado", "livetv_portalScanProgress_scanComplete": "Escaneo completo", "livetv_portalScanProgress_scanFailed": "Escaneo fallido", - "livetv_portalScanProgress_scanType": "Tipo de escaneo", "livetv_portalScanProgress_scanningPortal": "Escaneando {name}", + "livetv_portalScanProgress_scanType": "Tipo de escaneo", "livetv_portalScanProgress_tested": "Probado", "livetv_portalScanProgress_timeRemaining": "Tiempo restante", "livetv_portalScanProgress_total": "Total", @@ -1827,9 +1863,9 @@ "livetv_scanResults_accountCol": "Cuenta", "livetv_scanResults_actionsCol": "Acciones", "livetv_scanResults_allTab": "Todos", + "livetv_scanResults_approvedTab": "Aprobados", "livetv_scanResults_approveSelected": "Aprobar seleccionados", "livetv_scanResults_approveTitle": "Aprobar resultados", - "livetv_scanResults_approvedTab": "Aprobados", "livetv_scanResults_categoriesCount": "{count} categorías", "livetv_scanResults_channelsCol": "Canales", "livetv_scanResults_channelsCount": "{count} canales", @@ -1844,9 +1880,9 @@ "livetv_scanResults_failedToClear": "Error al limpiar resultados ignorados", "livetv_scanResults_failedToIgnore": "Error al ignorar", "livetv_scanResults_failedToLoadResults": "Error al cargar resultados de escaneo", + "livetv_scanResults_ignoredTab": "Ignorados", "livetv_scanResults_ignoreSelected": "Ignorar seleccionados", "livetv_scanResults_ignoreTitle": "Ignorar resultados", - "livetv_scanResults_ignoredTab": "Ignorados", "livetv_scanResults_macAddressCol": "Dirección MAC", "livetv_scanResults_noApproved": "Sin resultados aprobados", "livetv_scanResults_noIgnored": "Sin resultados ignorados", @@ -1874,31 +1910,6 @@ "login_usernameLabel": "Nombre de usuario", "login_usernamePlaceholder": "Ingresa tu nombre de usuario", "login_welcomeBack": "Bienvenido de nuevo", - "mediaBrowserModal_addTitle": "Añadir servidor multimedia", - "mediaBrowserModal_apiKeyHint": "Encontrado en Panel > Claves API", - "mediaBrowserModal_editTitle": "Editar servidor multimedia", - "mediaBrowserModal_embyDescription": "Servidor multimedia personal", - "mediaBrowserModal_failedToSave": "Error al guardar", - "mediaBrowserModal_hostHint": "Incluye http:// o https://", - "mediaBrowserModal_jellyfinDescription": "Servidor multimedia gratuito y de código abierto", - "mediaBrowserModal_localPathPlaceholder": "Ruta local (ej., /media)", - "mediaBrowserModal_mediaServerBadge": "Servidor multimedia", - "mediaBrowserModal_notificationEvents": "Eventos de notificación", - "mediaBrowserModal_onDelete": "Al eliminar", - "mediaBrowserModal_onDeleteDesc": "Notificar cuando se eliminan medios", - "mediaBrowserModal_onImport": "Al importar", - "mediaBrowserModal_onImportDesc": "Notificar cuando se importan nuevos medios", - "mediaBrowserModal_onRename": "Al renombrar", - "mediaBrowserModal_onRenameDesc": "Notificar cuando se renombran archivos de medios", - "mediaBrowserModal_onUpgrade": "Al actualizar", - "mediaBrowserModal_onUpgradeDesc": "Notificar cuando los medios se actualizan a mejor calidad", - "mediaBrowserModal_pathMappings": "Mapeos de rutas", - "mediaBrowserModal_pathMappingsHint": "Mapea rutas locales a rutas remotas si tu servidor multimedia corre en Docker o en una máquina diferente.", - "mediaBrowserModal_plexApiKeyHint": "Encontrado en cuenta Plex o configuración de token del servidor", - "mediaBrowserModal_plexDescription": "Servidor multimedia personal", - "mediaBrowserModal_remotePathPlaceholder": "Ruta remota (ej., /data/media)", - "mediaBrowserModal_selectType": "Selecciona el tipo de servidor multimedia que quieres añadir:", - "mediaBrowserModal_to": "a", "mediaBrowser_actions": "Acciones", "mediaBrowser_addMapping": "Añadir mapeo", "mediaBrowser_addServer": "Añadir servidor multimedia", @@ -1946,6 +1957,31 @@ "mediaBrowser_serverType_plex": "Plex", "mediaBrowser_serverType_plexDesc": "Servidor multimedia personal", "mediaBrowser_to": "a", + "mediaBrowserModal_addTitle": "Añadir servidor multimedia", + "mediaBrowserModal_apiKeyHint": "Encontrado en Panel > Claves API", + "mediaBrowserModal_editTitle": "Editar servidor multimedia", + "mediaBrowserModal_embyDescription": "Servidor multimedia personal", + "mediaBrowserModal_failedToSave": "Error al guardar", + "mediaBrowserModal_hostHint": "Incluye http:// o https://", + "mediaBrowserModal_jellyfinDescription": "Servidor multimedia gratuito y de código abierto", + "mediaBrowserModal_localPathPlaceholder": "Ruta local (ej., /media)", + "mediaBrowserModal_mediaServerBadge": "Servidor multimedia", + "mediaBrowserModal_notificationEvents": "Eventos de notificación", + "mediaBrowserModal_onDelete": "Al eliminar", + "mediaBrowserModal_onDeleteDesc": "Notificar cuando se eliminan medios", + "mediaBrowserModal_onImport": "Al importar", + "mediaBrowserModal_onImportDesc": "Notificar cuando se importan nuevos medios", + "mediaBrowserModal_onRename": "Al renombrar", + "mediaBrowserModal_onRenameDesc": "Notificar cuando se renombran archivos de medios", + "mediaBrowserModal_onUpgrade": "Al actualizar", + "mediaBrowserModal_onUpgradeDesc": "Notificar cuando los medios se actualizan a mejor calidad", + "mediaBrowserModal_pathMappings": "Mapeos de rutas", + "mediaBrowserModal_pathMappingsHint": "Mapea rutas locales a rutas remotas si tu servidor multimedia corre en Docker o en una máquina diferente.", + "mediaBrowserModal_plexApiKeyHint": "Encontrado en cuenta Plex o configuración de token del servidor", + "mediaBrowserModal_plexDescription": "Servidor multimedia personal", + "mediaBrowserModal_remotePathPlaceholder": "Ruta remota (ej., /data/media)", + "mediaBrowserModal_selectType": "Selecciona el tipo de servidor multimedia que quieres añadir:", + "mediaBrowserModal_to": "a", "movieMax": "Película máx. (GB)", "movieMin": "Película mín. (GB)", "naming_preview": "Vista previa", @@ -1989,7 +2025,7 @@ "nav_downloadClients": "Clientes de descarga", "nav_epg": "Guía", "nav_general": "General", - "nav_globalFilters": "Filtros globales", + "nav_globalFilters": "Filtros de descubrimiento", "nav_home": "Inicio", "nav_import": "Importar", "nav_indexers": "Indexadores", @@ -2002,6 +2038,7 @@ "nav_mediaServers": "Servidores multimedia", "nav_mediaStorage": "Almacenamiento", "nav_metadataProviders": "Proveedores de metadatos", + "nav_monitoring": "Monitorización", "nav_movies": "Películas", "nav_naming": "Nomenclatura", "nav_nntpServers": "Servidores NNTP", @@ -2011,29 +2048,12 @@ "nav_qualitySettings": "Calidad", "nav_settings": "Configuración", "nav_smartLists": "Listas inteligentes", - "nav_streaming": "Transmitiendo", + "nav_streaming": "Transmisión", "nav_subtitleProviders": "Proveedores de subtítulos", "nav_system": "Sistema", "nav_tasks": "Tareas", "nav_tvShows": "Series de TV", "nav_unmatchedFiles": "Archivos sin coincidencia", - "nntpServerModal_addTitle": "Añadir servidor Usenet", - "nntpServerModal_editTitle": "Editar servidor Usenet", - "nntpServerModal_failedToSave": "Error al guardar", - "nntpServerModal_maxConnections": "Máx. conexiones", - "nntpServerModal_maxConnectionsHint": "Consulta tu proveedor usenet para límites de conexión (generalmente 10-50)", - "nntpServerModal_priority": "Prioridad", - "nntpServerModal_priorityHint": "Valores más bajos = mayor prioridad. Úsalo para failover de servidor.", - "nntpServer_addUsenetServer": "Añadir servidor Usenet", - "nntpServer_deleteServer": "Eliminar servidor", - "nntpServer_disableServer": "Deshabilitar servidor", - "nntpServer_editServer": "Editar servidor", - "nntpServer_editUsenetServer": "Editar servidor Usenet", - "nntpServer_enableServer": "Habilitar servidor", - "nntpServer_maxConnections_help": "Consulta tu proveedor usenet para límites de conexión (generalmente 10-50)", - "nntpServer_maxConnections_label": "Máx. conexiones", - "nntpServer_priority_help": "Valores más bajos = mayor prioridad. Úsalo para failover de servidor.", - "nntpServer_settings_title": "Configuración", "nntp_clearFiltersToReorder": "Limpia los filtros para reordenar prioridades.", "nntp_deleteServer": "Eliminar servidor", "nntp_disableServer": "Deshabilitar servidor", @@ -2051,6 +2071,23 @@ "nntp_reorderModeActive": "Modo de reordenamiento activo", "nntp_reorderPriorities": "Reordenar prioridades", "nntp_testConnection": "Probar conexión", + "nntpServer_addUsenetServer": "Añadir servidor Usenet", + "nntpServer_deleteServer": "Eliminar servidor", + "nntpServer_disableServer": "Deshabilitar servidor", + "nntpServer_editServer": "Editar servidor", + "nntpServer_editUsenetServer": "Editar servidor Usenet", + "nntpServer_enableServer": "Habilitar servidor", + "nntpServer_maxConnections_help": "Consulta tu proveedor usenet para límites de conexión (generalmente 10-50)", + "nntpServer_maxConnections_label": "Máx. conexiones", + "nntpServer_priority_help": "Valores más bajos = mayor prioridad. Úsalo para failover de servidor.", + "nntpServer_settings_title": "Configuración", + "nntpServerModal_addTitle": "Añadir servidor Usenet", + "nntpServerModal_editTitle": "Editar servidor Usenet", + "nntpServerModal_failedToSave": "Error al guardar", + "nntpServerModal_maxConnections": "Máx. conexiones", + "nntpServerModal_maxConnectionsHint": "Consulta tu proveedor usenet para límites de conexión (generalmente 10-50)", + "nntpServerModal_priority": "Prioridad", + "nntpServerModal_priorityHint": "Valores más bajos = mayor prioridad. Úsalo para failover de servidor.", "noFormatsMatch": "Ningún formato coincide con tu búsqueda", "noMax": "Sin máx.", "noMin": "Sin mín.", @@ -2065,18 +2102,6 @@ "person_gender_male": "Masculino", "person_gender_nonBinary": "No binario", "person_noBiography": "No hay biografía disponible.", - "profileModal_allowUpgrades": "Permitir actualizaciones", - "profileModal_copyFrom": "Copiar de", - "profileModal_createTitle": "Crear perfil", - "profileModal_descPlaceholder": "Describe para qué es este perfil...", - "profileModal_description": "Descripción", - "profileModal_editSizeLimits": "Editar límites de tamaño", - "profileModal_editTitle": "Editar perfil", - "profileModal_failedToSave": "Error al guardar", - "profileModal_name": "Nombre", - "profileModal_profile": "Perfil", - "profileModal_setAsDefault": "Establecer como predeterminado", - "profileModal_streamingProfileInfo": "Los perfiles de streaming usan fuentes externas, así que los límites de tamaño no son configurables.", "profile_accountSecurity": "Seguridad de la cuenta", "profile_accountSecurityDescription": "Gestiona contraseñas, autenticación de dos factores y sesiones de inicio de sesión.", "profile_comingSoon": "Próximamente", @@ -2095,6 +2120,18 @@ "profile_singleAdminNote": "Por ahora, Cinephage funciona en modo de único administrador. Toda la configuración se gestiona a través del menú de Configuración.", "profile_subtitle": "Gestiona la configuración y preferencias de tu cuenta", "profile_title": "Perfil de usuario", + "profileModal_allowUpgrades": "Permitir actualizaciones", + "profileModal_copyFrom": "Copiar de", + "profileModal_createTitle": "Crear perfil", + "profileModal_descPlaceholder": "Describe para qué es este perfil...", + "profileModal_description": "Descripción", + "profileModal_editSizeLimits": "Editar límites de tamaño", + "profileModal_editTitle": "Editar perfil", + "profileModal_failedToSave": "Error al guardar", + "profileModal_name": "Nombre", + "profileModal_profile": "Perfil", + "profileModal_setAsDefault": "Establecer como predeterminado", + "profileModal_streamingProfileInfo": "Los perfiles de streaming usan fuentes externas, así que los límites de tamaño no son configurables.", "profiles_addProfile": "Añadir perfil", "profiles_allowUpgrades_label": "Permitir actualizaciones", "profiles_preventDowngrades_label": "Evitar degradación de resolución", @@ -2114,6 +2151,7 @@ "profiles_movieMaxSize_label": "Película máx. (GB)", "profiles_movieMinSize_label": "Película mín. (GB)", "profiles_noFormatsMatchSearch": "Ningún formato coincide con tu búsqueda", + "profiles_preventDowngrades_label": "Evitar degradación de resolución", "profiles_scoredCount": "{count} puntuados", "profiles_searchFormats": "Buscar formatos...", "profiles_seasonPackSizeInfo": "Para paquetes de temporada, se calcula el tamaño promedio por episodio.", @@ -2161,7 +2199,6 @@ "rootFolders_unknown": "Desconocido", "rootFolders_validate": "Validar", "rootFolders_validationFailed": "Validación de ruta fallida", - "searchFormats": "Buscar formatos...", "search_actionsCol": "Acciones", "search_afterDeduplication": "2. Después de deduplicación:", "search_afterQualityScoring": "4. Después de puntuación de calidad y dedup inteligente:", @@ -2183,8 +2220,8 @@ "search_failed": "Fallido", "search_freeleech": "Freeleech", "search_grab": "Capturar", - "search_grabButtonTooltip": "Descargar", "search_grabbed": "Capturado", + "search_grabButtonTooltip": "Descargar", "search_hideDebugPanel": "Ocultar panel de depuración", "search_indexerCol": "Indexador", "search_indexerCount": "{count} indexadores", @@ -2211,23 +2248,23 @@ "search_resultsCount": "{filtered} de {total} resultados", "search_scoreCol": "Puntuación", "search_scoreLabel": "Puntuación: {score}", - "search_searchResultsPlaceholder": "Resultados de búsqueda…", - "search_searchTime": "Búsqueda: {time}ms", "search_searched": "Buscado:", "search_searchedMultiPack": "Buscado (coincidencias multi-paquete / crudo):", "search_searching": "Buscando en indexadores...", + "search_searchResultsPlaceholder": "Resultados de búsqueda…", + "search_searchTime": "Búsqueda: {time}ms", "search_showDebugPanel": "Mostrar panel de depuración", "search_showRejected": "Mostrar rechazados", "search_sizeCol": "Tamaño", "search_skipped": "Omitido:", "search_slCol": "S/L", + "search_sort_score": "Puntuación", + "search_sort_seeders": "Sembradores", + "search_sort_size": "Tamaño", "search_sortAge": "Antigüedad", "search_sortScore": "Puntuación", "search_sortSeeders": "Sembradores", "search_sortSize": "Tamaño", - "search_sort_score": "Puntuación", - "search_sort_seeders": "Sembradores", - "search_sort_size": "Tamaño", "search_stream": "Transmitir", "search_streamButtonTooltip": "Transmitir (NNTP)", "search_streamProtocol": "Streaming", @@ -2237,27 +2274,42 @@ "search_uploadMultiplier": "{multiplier}x Subida", "search_usenetProtocol": "Usenet", "search_viewDetails": "Ver detalles", + "searchFormats": "Buscar formatos...", "setAsDefault": "Establecer como predeterminado", + "settings_blocklist_tabsAriaLabel": "Pestañas de navegación de la lista de bloqueo", + "settings_duplicates_all": "All libraries", + "settings_duplicates_both": "Both", + "settings_duplicates_description": "Find and manage duplicate media files across your library using filename or content hash comparison.", + "settings_duplicates_filehash": "Content hash (sha256)", + "settings_duplicates_filename": "Filename (normalized)", + "settings_duplicates_files": "files", + "settings_duplicates_mode": "Detection mode", + "settings_duplicates_no_dupes": "No duplicates found", + "settings_duplicates_off": "Off", + "settings_duplicates_select_library": "Select library", + "settings_duplicates_suppress": "Dismiss", + "settings_duplicates_title": "Duplicate Detection", + "settings_duplicates_unsuppress": "Restore", "settings_filters_configureTmdbForGenres": "Configura tu clave API de TMDB para cargar géneros disponibles.", "settings_filters_contentPreferences": "Preferencias de contenido", "settings_filters_excludedGenres": "Géneros excluidos", "settings_filters_excludedGenresHint": "Selecciona géneros a excluir de todos los resultados.", - "settings_filters_failedToSave": "Error al guardar filtros globales", - "settings_filters_heading": "Filtros globales", + "settings_filters_failedToSave": "Error al guardar los filtros de descubrimiento", + "settings_filters_heading": "Filtros de descubrimiento", "settings_filters_includeAdult": "Incluir contenido para adultos", "settings_filters_includeAdultHint": "Activa para permitir contenido para adultos en resultados de búsqueda y descubrimiento.", "settings_filters_localization": "Localización", "settings_filters_minScore": "Puntuación mínima (0-10)", "settings_filters_minVoteCount": "Conteo mínimo de votos", "settings_filters_noGenresAvailable": "No hay géneros disponibles.", - "settings_filters_pageTitle": "Filtros globales - Configuración - Cinephage", + "settings_filters_pageTitle": "Filtros de descubrimiento - Ajustes - Cinephage", "settings_filters_preferredLanguage": "Idioma preferido", "settings_filters_preferredRegion": "Región preferida", "settings_filters_qualityStandards": "Estándares de calidad", "settings_filters_saveButton": "Guardar filtros globales", - "settings_filters_subtitle": "Configura filtros de contenido globales. Estos ajustes se aplican a todos los resultados de búsqueda, descubrimientos y tareas automatizadas.", + "settings_filters_subtitle": "Configura filtros de contenido globales. Estos filtros se aplican a todas las fuentes de metadatos (TMDB, datos complementarios de TVDB y la futura API unificada de Cinephage) y controlan lo que se descubre y añade a tu biblioteca.", "settings_filters_tmdbRequired": "Configura tu clave API de TMDB para habilitar el filtrado por género y otras funciones basadas en TMDB.", - "settings_filters_updated": "Filtros globales actualizados", + "settings_filters_updated": "Filtros de descubrimiento actualizados", "settings_filters_updatedSuccess": "Filtros globales actualizados exitosamente.", "settings_general_addFolder": "Añadir carpeta", "settings_general_addFolderFirst": "Añade una carpeta raíz arriba antes de escanear tu biblioteca.", @@ -2274,8 +2326,8 @@ "settings_general_badgeSystem": "Sistema", "settings_general_capacityUnknown": "Capacidad desconocida", "settings_general_cinephageTracked": "Cinephage: {used}", - "settings_general_classShort": "Clase", "settings_general_classification": "Clasificación", + "settings_general_classShort": "Clase", "settings_general_close": "cerrar", "settings_general_columnClassification": "Clasificación", "settings_general_columnItems": "Elementos", @@ -2346,11 +2398,11 @@ "settings_general_noMovieLibrariesConfigured": "No hay bibliotecas de películas configuradas.", "settings_general_noRootFolder": "Sin carpeta raíz", "settings_general_noRootFolderAssigned": "Sin carpeta raíz asignada", - "settings_general_noRootFolderUsageDataYet": "Aún no hay datos de uso de carpetas raíz.", "settings_general_noRootFoldersAssigned": "No hay carpetas raíz asignadas", + "settings_general_noRootFolderUsageDataYet": "Aún no hay datos de uso de carpetas raíz.", "settings_general_noScanHistory": "Sin historial de escaneo", - "settings_general_noTvLibrariesConfigured": "No hay bibliotecas de TV configuradas.", "settings_general_notAssigned": "No asignado", + "settings_general_noTvLibrariesConfigured": "No hay bibliotecas de TV configuradas.", "settings_general_optionalChooseCompatibleLibrary": "Opcional: elige otra biblioteca personalizada compatible.", "settings_general_otherDiskUsage": "Otros: {used}", "settings_general_pageTitle": "Almacenamiento - Cinephage", @@ -2360,7 +2412,6 @@ "settings_general_processing": "Procesando...", "settings_general_reviewLibrary": "Revisar biblioteca", "settings_general_rootFolderLabel": "Carpeta raíz", - "settings_general_rootFolderUsage": "Uso de carpetas raíz", "settings_general_rootFolders": "Carpetas raíz", "settings_general_rootFoldersCount": "{count} carpetas", "settings_general_rootFoldersDescription": "Configura carpetas de biblioteca multimedia donde se organizará el contenido.", @@ -2370,6 +2421,7 @@ "settings_general_rootFoldersSummaryMore": "{names} +{count} más", "settings_general_rootFoldersTabDescription": "Configura las carpetas de la biblioteca multimedia donde se organizará el contenido.", "settings_general_rootFoldersTitle": "Carpetas raíz", + "settings_general_rootFolderUsage": "Uso de carpetas raíz", "settings_general_runLibraryScan": "Ejecutar escaneo de biblioteca", "settings_general_running": "En ejecución", "settings_general_saveLibrary": "Guardar biblioteca", @@ -2381,14 +2433,14 @@ "settings_general_scanHealth": "Salud del escaneo", "settings_general_scanLibraries": "Escanear bibliotecas", "settings_general_scanLibrary": "Escanear biblioteca", + "settings_general_scanned": "Escaneado", + "settings_general_scanning": "Escaneando...", "settings_general_scanNow": "Escanear ahora", "settings_general_scanRemoved": "Eliminados", "settings_general_scanRootFolders": "Escanear carpetas raíz", "settings_general_scanStatus": "Estado del escaneo", "settings_general_scanUnmatched": "Sin coincidencia", "settings_general_scanUpdated": "Actualizados", - "settings_general_scanned": "Escaneado", - "settings_general_scanning": "Escaneando...", "settings_general_searchOnAddLabel": "Buscar al añadir", "settings_general_selectedCount": "{count} seleccionados", "settings_general_seriesCount": "{count} series", @@ -2398,9 +2450,6 @@ "settings_general_subtitle": "Gestiona carpetas raíz, reglas de organización de biblioteca y comportamiento de escaneo.", "settings_general_subtitleFilesCount": "{count} archivos de subtítulos", "settings_general_subtitlesLabel": "Subtítulos", - "settings_general_tabLibraries": "Bibliotecas", - "settings_general_tabMaintenance": "Mantenimiento de almacenamiento", - "settings_general_tabRootFolders": "Carpetas raíz", "settings_general_topLibrariesByUsage": "Bibliotecas principales por uso", "settings_general_topRootFoldersByUsage": "Carpetas raíz principales por uso", "settings_general_totalStorage": "Almacenamiento total", @@ -2418,6 +2467,20 @@ "settings_general_viewUnmatchedFiles": "Ver {count} archivos sin coincidencia", "settings_general_wantSubtitles": "Querer subtítulos", "settings_general_yes": "Sí", + "settings_history_description": "Control how long historical data is kept before automatic cleanup. 0 days = unlimited.", + "settings_history_failed": "Failed to save retention settings", + "settings_history_file_days": "File history (days)", + "settings_history_file_days_help": "Per-file analysis snapshots. Default: 30 days.", + "settings_history_forecast": "Storage Forecast", + "settings_history_forecast_30d": "Projected (30 days)", + "settings_history_forecast_90d": "Projected (90 days)", + "settings_history_forecast_current": "Current estimated storage", + "settings_history_library_days": "Library history (days)", + "settings_history_library_days_help": "Daily per-library snapshots. Default: 365 days.", + "settings_history_saved": "Retention settings saved", + "settings_history_scan_days": "Scan history (days)", + "settings_history_scan_days_help": "Completed scan job records. Default: 30 days.", + "settings_history_title": "History Retention", "settings_indexers_addIndexer": "Añadir indexador", "settings_indexers_addIndexerHint": "Añade un indexador para comenzar a buscar contenido", "settings_indexers_authentication": "Autenticación", @@ -2449,8 +2512,8 @@ "settings_indexers_edit": "Editar indexador", "settings_indexers_editIndexer": "Editar indexador", "settings_indexers_enable": "Habilitar", - "settings_indexers_enableReorderHint": "Activa el modo de reordenamiento para cambiar la prioridad del indexador.", "settings_indexers_enabled": "Habilitado", + "settings_indexers_enableReorderHint": "Activa el modo de reordenamiento para cambiar la prioridad del indexador.", "settings_indexers_externalUrl": "URL externa", "settings_indexers_externalUrlHint": "La URL externa donde Jellyfin/Kodi puede alcanzar este servidor", "settings_indexers_externalUrlRequired": "Requerido para streaming", @@ -2485,11 +2548,11 @@ "settings_indexers_reorderModeActive": "Modo de reordenamiento activo", "settings_indexers_reorderPriorities": "Reordenar prioridades", "settings_indexers_searchPlaceholder": "Buscar indexadores...", + "settings_indexers_seedersHint": "0+", "settings_indexers_seedRatio": "Ratio de seeding", "settings_indexers_seedRatioHint": "Vacío = predeterminado del cliente", "settings_indexers_seedTime": "Tiempo de seeding", "settings_indexers_seedTimeUnit": "minutos", - "settings_indexers_seedersHint": "0+", "settings_indexers_selectAll": "Seleccionar todo", "settings_indexers_skipReleasesBelow": "Omitir lanzamientos debajo de esto", "settings_indexers_status": "Estado", @@ -2526,8 +2589,8 @@ "settings_integrations_captcha_failedToClearCache": "Error al limpiar caché de captcha", "settings_integrations_captcha_failedToLoad": "Error al cargar datos del solucionador de captcha", "settings_integrations_captcha_failedToSaveSettings": "Error al guardar la configuración", - "settings_integrations_captcha_fetchSuccessRate": "Tasa de éxito de obtención", "settings_integrations_captcha_fetchesAttempted": "{count} obtenciones intentadas", + "settings_integrations_captcha_fetchSuccessRate": "Tasa de éxito de obtención", "settings_integrations_captcha_headlessDesc": "Ejecuta el navegador sin una ventana visible. Desactiva para propósitos de depuración.", "settings_integrations_captcha_headlessLabel": "Modo headless", "settings_integrations_captcha_hour1Default": "1 hora (predeterminado)", @@ -2547,8 +2610,8 @@ "settings_integrations_captcha_optional": "Opcional", "settings_integrations_captcha_pageTitle": "Solucionador de captcha - Cinephage", "settings_integrations_captcha_proxyOptional": "Proxy (Opcional)", - "settings_integrations_captcha_proxyPlaceholder": "http://proxy.example.com:8080", "settings_integrations_captcha_proxyPassword": "Contraseña", + "settings_integrations_captcha_proxyPlaceholder": "http://proxy.example.com:8080", "settings_integrations_captcha_proxyUrl": "URL del proxy", "settings_integrations_captcha_proxyUrlHelp": "Ruta el navegador a través de un proxy. Formato: http://host:puerto o socks5://host:puerto", "settings_integrations_captcha_proxyUsername": "Nombre de usuario", @@ -2557,11 +2620,11 @@ "settings_integrations_captcha_seconds60Default": "60 segundos (predeterminado)", "settings_integrations_captcha_seconds90": "90 segundos", "settings_integrations_captcha_settingsSaved": "Configuración guardada exitosamente", + "settings_integrations_captcha_solvedChallengeIn": "Desafío {challengeType} resuelto en {solveTimeMs}ms", + "settings_integrations_captcha_solvesAttempted": "{count} resoluciones intentadas", "settings_integrations_captcha_solveSuccessRate": "Tasa de éxito de resolución", "settings_integrations_captcha_solveTimeout": "Tiempo de resolución", "settings_integrations_captcha_solveTimeoutHelp": "Tiempo máximo de espera para resolver un CAPTCHA antes de rendirse.", - "settings_integrations_captcha_solvedChallengeIn": "Desafío {challengeType} resuelto en {solveTimeMs}ms", - "settings_integrations_captcha_solvesAttempted": "{count} resoluciones intentadas", "settings_integrations_captcha_statistics": "Estadísticas", "settings_integrations_captcha_statusBusy": "Ocupado", "settings_integrations_captcha_statusDisabled": "El solucionador de captcha está desactivado", @@ -2589,7 +2652,6 @@ "settings_integrations_connectionSuccessful": "Conexión exitosa", "settings_integrations_deleteConfirmPrefix": "¿Estás seguro de que quieres eliminar ", "settings_integrations_deleteConfirmSuffix": "? Esta acción no se puede deshacer.", - "settings_integrations_downloadClientsDesc": "Aplicaciones que manejan la descarga de lanzamientos desde indexadores.", "settings_integrations_downloadClients_addButton": "Añadir cliente de descarga", "settings_integrations_downloadClients_altmountMode": "Altmount / NZBDav (Modo montaje)", "settings_integrations_downloadClients_bulkDeleteCount": "{count} clientes seleccionados", @@ -2598,8 +2660,8 @@ "settings_integrations_downloadClients_clientPath": "Ruta del cliente", "settings_integrations_downloadClients_completedDownloadFolder": "Carpeta de descarga completada", "settings_integrations_downloadClients_downloadBehavior": "Comportamiento de descarga", - "settings_integrations_downloadClients_downloadFolder": "Carpeta de descarga", "settings_integrations_downloadClients_downloaderColumn": "Descargador", + "settings_integrations_downloadClients_downloadFolder": "Carpeta de descarga", "settings_integrations_downloadClients_emptyDescription": "Añade un cliente de descarga para comenzar a gestionar descargas", "settings_integrations_downloadClients_emptyTitle": "Sin clientes de descarga configurados", "settings_integrations_downloadClients_forcePriority": "Forzar", @@ -2625,25 +2687,25 @@ "settings_integrations_downloadClients_torrent": "Torrent", "settings_integrations_downloadClients_urlBaseDescription": "Prefijo de ruta añadido después del host y puerto.", "settings_integrations_downloadClients_urlBaseLabel": "Base de URL", - "settings_integrations_downloadClients_useUrlBase": "Usar base de URL", "settings_integrations_downloadClients_usenet": "Usenet", + "settings_integrations_downloadClients_useUrlBase": "Usar base de URL", + "settings_integrations_downloadClientsDesc": "Aplicaciones que manejan la descarga de lanzamientos desde indexadores.", "settings_integrations_gettingStarted": "Primeros pasos", "settings_integrations_gettingStartedBoth": "Añade al menos un indexador y un cliente de descarga para comenzar a descargar contenido.", "settings_integrations_gettingStartedDownloadClient": "Añade al menos un cliente de descarga para descargar lanzamientos.", "settings_integrations_gettingStartedIndexer": "Añade al menos un indexador para buscar lanzamientos.", - "settings_integrations_indexersDesc": "Proveedores de búsqueda para encontrar lanzamientos en redes torrent y Usenet.", "settings_integrations_indexers_addButton": "Añadir indexador", "settings_integrations_indexers_bulkDeleteCount": "{count} indexadores seleccionados", "settings_integrations_indexers_created": "Indexador creado exitosamente", "settings_integrations_indexers_definitionErrors": "{count} errores de definición de indexador encontrados", "settings_integrations_indexers_subtitle": "Gestiona tus indexadores para buscar lanzamientos torrent y Usenet.", "settings_integrations_indexers_updated": "Indexador actualizado exitosamente", - "settings_integrations_languageProfilesDesc": "Configura idiomas preferidos y requisitos de subtítulos.", + "settings_integrations_indexersDesc": "Proveedores de búsqueda para encontrar lanzamientos en redes torrent y Usenet.", "settings_integrations_languageProfiles_addLanguage": "Añadir idioma", "settings_integrations_languageProfiles_addProfile": "Añadir perfil", "settings_integrations_languageProfiles_addTitle": "Añadir perfil de idioma", - "settings_integrations_languageProfiles_allowUpgrades": "Permitir actualizaciones", "settings_integrations_languageProfiles_allowed": "Permitidos", + "settings_integrations_languageProfiles_allowUpgrades": "Permitir actualizaciones", "settings_integrations_languageProfiles_cutoff": "Corte", "settings_integrations_languageProfiles_cutoffIndex": "Índice de corte", "settings_integrations_languageProfiles_cutoffIndexHelp": "Deja de actualizar subtítulos una vez se encuentra una coincidencia en o por encima de este índice de idioma.", @@ -2660,9 +2722,9 @@ "settings_integrations_languageProfiles_forced": "Forzado", "settings_integrations_languageProfiles_hi": "HI", "settings_integrations_languageProfiles_maxChars": "Máx. {max} caracteres", - "settings_integrations_languageProfiles_minScore": "Punt. mín.", "settings_integrations_languageProfiles_minimumScore": "Puntuación mínima", "settings_integrations_languageProfiles_minimumScoreHelp": "Puntuación mínima de coincidencia de subtítulo (0-100) requerida antes de aceptar un subtítulo.", + "settings_integrations_languageProfiles_minScore": "Punt. mín.", "settings_integrations_languageProfiles_nameAndLanguageRequired": "Se requiere nombre del perfil y al menos un idioma", "settings_integrations_languageProfiles_nameTooLong": "El nombre del perfil debe tener {max} caracteres o menos", "settings_integrations_languageProfiles_noProfiles": "Sin perfiles de idioma configurados", @@ -2675,6 +2737,7 @@ "settings_integrations_languageProfiles_setAsDefault": "Establecer como predeterminado", "settings_integrations_languageProfiles_subtitle": "Configura preferencias de idioma y perfiles de coincidencia de subtítulos.", "settings_integrations_languageProfiles_upgrades": "Actualizaciones", + "settings_integrations_languageProfilesDesc": "Configura idiomas preferidos y requisitos de subtítulos.", "settings_integrations_mediaBrowsers_addServer": "Añadir servidor", "settings_integrations_mediaBrowsers_bulkDeleteCount": "{count} servidores seleccionados", "settings_integrations_mediaBrowsers_emptyDescription": "Añade un servidor Jellyfin, Emby o Plex para habilitar notificaciones de biblioteca", @@ -2683,7 +2746,6 @@ "settings_integrations_mediaBrowsers_subtitle": "Conecta a Plex, Jellyfin o Emby para sincronizar bibliotecas y metadatos.", "settings_integrations_mediaBrowsers_thisServer": "este servidor", "settings_integrations_mediaServersDesc": "Servidores multimedia para explorar y transmitir tu biblioteca.", - "settings_integrations_nntpServersDesc": "Servidores de noticias Usenet para descargar contenido vía protocolo NNTP.", "settings_integrations_nntpServers_addServer": "Añadir servidor", "settings_integrations_nntpServers_bulkDeleteCount": "{count} servidores seleccionados", "settings_integrations_nntpServers_emptyDescription": "Añade un servidor NNTP para habilitar streaming NZB directo", @@ -2696,6 +2758,7 @@ "settings_integrations_nntpServers_serverSettings": "Configuración del servidor", "settings_integrations_nntpServers_subtitle": "Configura servidores Usenet (NNTP) para descargar contenido.", "settings_integrations_nntpServers_thisServer": "este servidor", + "settings_integrations_nntpServersDesc": "Servidores de noticias Usenet para descargar contenido vía protocolo NNTP.", "settings_integrations_notConfigured": "No configurado", "settings_integrations_pageTitle": "Integraciones - Cinephage", "settings_integrations_profiles_emptyDescription": "Añade un perfil para gestionar preferencias de calidad", @@ -2705,13 +2768,13 @@ "settings_integrations_statSet": "Establecido", "settings_integrations_statTotal": "Total", "settings_integrations_subtitle": "Gestiona tus servicios externos y conexiones", - "settings_integrations_subtitleProvidersDesc": "Servicios que proporcionan subtítulos para tu biblioteca multimedia.", "settings_integrations_subtitleProviders_addProvider": "Añadir proveedor", "settings_integrations_subtitleProviders_bulkDeleteCount": "{count} proveedores seleccionados", "settings_integrations_subtitleProviders_connectionSuccessful": "Conexión a {name} exitosa (tiempo de respuesta: {responseTime}ms)", "settings_integrations_subtitleProviders_connectionTestFailed": "Error desconocido", "settings_integrations_subtitleProviders_searchPlaceholder": "Buscar proveedores de subtítulos...", "settings_integrations_subtitleProviders_subtitle": "Configura proveedores de descarga de subtítulos para obtención automática de subtítulos.", + "settings_integrations_subtitleProvidersDesc": "Servicios que proporcionan subtítulos para tu biblioteca multimedia.", "settings_integrations_tabsAriaLabel": "Pestañas de navegación de integraciones", "settings_integrations_tmdbApiKeyDescription": "Ingresa tu clave API de TMDB (v3 auth). Puedes obtener una desde", "settings_integrations_tmdbApiKeyTitle": "Clave API de TMDB", @@ -2719,6 +2782,23 @@ "settings_integrations_tmdbFailedToSave": "Error al guardar la clave API de TMDB", "settings_integrations_tmdbKeySaved": "Clave API de TMDB guardada exitosamente", "settings_integrations_tmdbTitle": "TMDB", + "settings_libraries_scan_description": "Comportamiento global de escaneo de biblioteca. Estos ajustes se moverán por biblioteca en una futura actualización.", + "settings_libraries_scan_failed": "Error al guardar los ajustes de escaneo", + "settings_libraries_scan_interval": "Intervalo de escaneo (horas)", + "settings_libraries_scan_interval_hint": "Cada cuánto ejecutar escaneos periódicos (1-168 horas)", + "settings_libraries_scan_loading": "Cargando ajustes de escaneo...", + "settings_libraries_scan_on_startup": "Escanear al inicio", + "settings_libraries_scan_on_startup_hint": "Ejecutar un escaneo cuando se inicie la aplicación", + "settings_libraries_scan_saved": "Ajustes de escaneo guardados", + "settings_libraries_scan_threshold": "Umbral de confianza de auto-coincidencia", + "settings_libraries_scan_threshold_hint": "Confianza mínima (0-1) para auto-coincidencia de archivos descubiertos con metadatos", + "settings_libraries_scan_title": "Ajustes de escaneo", + "settings_libraries_scan_watch": "Vigilar el sistema de archivos en busca de cambios", + "settings_libraries_scan_watch_hint": "Detectar automáticamente archivos nuevos, modificados o eliminados en tiempo real", + "settings_library_tabFileManagement": "Gestión de archivos", + "settings_library_tabLibraries": "Bibliotecas", + "settings_library_tabRootFolders": "Carpetas raíz", + "settings_library_tabsAriaLabel": "Pestañas de navegación de la biblioteca", "settings_logs_adjustFilters": "Ajusta los filtros o espera nuevos eventos", "settings_logs_allDomains": "Todos los dominios", "settings_logs_bufferedEntries": "{count} nuevas entradas en búfer", @@ -2735,6 +2815,8 @@ "settings_logs_scrollToBottom": "Desplazarse al final", "settings_logs_subtitle": "Flujo en vivo del registro de la aplicación. Haz clic en cualquier entrada para inspeccionar sus detalles.", "settings_logs_toggleLevel": "Alternar registros {level}", + "settings_monitoring_tabMaintenance": "Mantenimiento de almacenamiento", + "settings_monitoring_tabsAriaLabel": "Pestañas de navegación de monitoreo", "settings_naming_advancedOptions": "Opciones avanzadas", "settings_naming_allFieldsClean": "Todos los campos de formato se validan correctamente.", "settings_naming_allFormatsValid": "Todos los campos de formato se validan correctamente.", @@ -2788,11 +2870,11 @@ "settings_naming_movieNamingDesc": "Patrones de nomenclatura de carpetas y archivos para películas", "settings_naming_multiEpDuplicate": "Duplicar (S01E01.S01E02)", "settings_naming_multiEpExtend": "Extender (S01E01E02E03)", + "settings_naming_multiEpisodeStyle": "Estilo de multi-episodio", + "settings_naming_multiEpisodeStyleHelp": "Cómo los archivos de multi-episodio se representan en el nombre de archivo.", "settings_naming_multiEpRange": "Rango (S01E01-E03)", "settings_naming_multiEpRepeat": "Repetir (S01E01.E02.E03)", "settings_naming_multiEpScene": "Escena (S01E01-S01E03)", - "settings_naming_multiEpisodeStyle": "Estilo de multi-episodio", - "settings_naming_multiEpisodeStyleHelp": "Cómo los archivos de multi-episodio se representan en el nombre de archivo.", "settings_naming_namingStyle": "Estilo de nomenclatura", "settings_naming_nextStep": "Siguiente paso", "settings_naming_nextStepDesc": "Guarda este borrador cuando se vea bien, luego revisa el plan de renombrado antes de aplicar cambios de archivos en toda la biblioteca.", @@ -2853,9 +2935,9 @@ "settings_naming_reviewRenamePlan": "Revisar plan de renombrado", "settings_naming_saveChanges": "Guardar cambios", "settings_naming_saveCurrentAsPreset": "Guardar actual como preajuste", - "settings_naming_savePreset": "Guardar preajuste", "settings_naming_saved": "Guardado", "settings_naming_savedCustomSource": "Fuente personalizada guardada", + "settings_naming_savePreset": "Guardar preajuste", "settings_naming_seasonFolder": "Carpeta de temporada", "settings_naming_seasonFolderFormat": "Formato de carpeta de temporada", "settings_naming_selectCustomPreset": "Selecciona un preajuste personalizado…", @@ -2875,10 +2957,68 @@ "settings_naming_unsavedChangesDesc": "Tienes cambios de nomenclatura sin guardar. Guarda para aplicarlos, o revisa el plan de renombrado para ver cómo afectarían a los archivos existentes.", "settings_naming_updatingPreviews": "Actualizando vistas previas", "settings_naming_validating": "Validando", + "settings_patterns_all_libraries": "Todas las bibliotecas (valores predeterminados globales)", + "settings_patterns_bonus": "Patrones de bonus", + "settings_patterns_bonus_add": "Añadir patrón", + "settings_patterns_bonus_desc": "Patrones glob (sin distinción de mayúsculas). Los archivos coincidentes se marcan como bonus/extras.", + "settings_patterns_bonus_placeholder": "*trailer*", + "settings_patterns_description": "Controla cómo se filtran y clasifican los archivos durante los escaneos de biblioteca. Los patrones de ignorar impiden la indexación; los patrones de bonus marcan archivos como extras; el reconocimiento de estructura extrae series/temporada/episodio.", + "settings_patterns_failed": "Error al guardar la configuración de patrones", + "settings_patterns_ignore": "Patrones de ignorar", + "settings_patterns_ignore_desc": "Patrones glob (sensible a mayúsculas). Los archivos y carpetas coincidentes nunca se indexan.", + "settings_patterns_ignore_enabled": "Usar valores predeterminados integrados", + "settings_patterns_ignore_user": "Patrones de usuario", + "settings_patterns_ignore_user_add": "Añadir patrón", + "settings_patterns_ignore_user_placeholder": "*Privado*", + "settings_patterns_saved": "Configuración de patrones guardada", + "settings_patterns_scope_global": "Valores predeterminados globales (se aplican a todas las bibliotecas)", + "settings_patterns_scope_library": "Anulación de biblioteca", + "settings_patterns_select_library": "Seleccionar una biblioteca para configuración por biblioteca", + "settings_patterns_structure": "Reconocimiento de estructura", + "settings_patterns_structure_desc": "Reconocer series/temporada/episodio desde diseños de carpetas. Solo bibliotecas de TV.", + "settings_patterns_structure_folder_depth": "Profundidad de carpeta", + "settings_patterns_structure_mode": "Modo de reconocimiento", + "settings_patterns_structure_none": "Ninguno (biblioteca plana)", + "settings_patterns_structure_regex": "Regex personalizado", + "settings_patterns_structure_season_depth": "Profundidad de carpeta de temporada (relativa a la serie)", + "settings_patterns_structure_series_depth": "Profundidad de carpeta de serie (0 = raíz)", + "settings_patterns_title": "Reconocimiento de patrones", "settings_profiles_redirecting": "Redirigiendo...", "settings_quality_confirmDeleteFormatMessage": "¿Estás seguro de que quieres eliminar {name}? Esta acción no se puede deshacer.", "settings_quality_confirmDeleteProfileMessage": "¿Estás seguro de que quieres eliminar {name}? Esta acción no se puede deshacer.", "settings_quality_confirmDeleteTitle": "Confirmar eliminación", + "settings_quality_delay_addProfile": "Añadir perfil", + "settings_quality_delay_badgeDisabled": "Desactivado", + "settings_quality_delay_bypass4k": "Omitir en 4K", + "settings_quality_delay_bypassScorePrefix": "Omitir si puntaje >", + "settings_quality_delay_createButton": "Crear", + "settings_quality_delay_created": "Perfil de retraso creado", + "settings_quality_delay_deleted": "\"{name}\" eliminado", + "settings_quality_delay_deleteMessage": "¿Eliminar \"{name}\"? Los medios asignados a este perfil volverán a sin retraso.", + "settings_quality_delay_deleteTitle": "Eliminar perfil de retraso", + "settings_quality_delay_empty": "Sin perfiles de retraso", + "settings_quality_delay_emptyHint": "Crea un perfil para retener releases antes de obtenerlos.", + "settings_quality_delay_field_bypass4k": "Omitir retraso para releases 4K (2160p)", + "settings_quality_delay_field_bypassScore": "Omitir retraso si el puntaje excede", + "settings_quality_delay_field_bypassScorePlaceholder": "Dejar vacío para desactivar", + "settings_quality_delay_field_enabled": "Activado", + "settings_quality_delay_field_name": "Nombre", + "settings_quality_delay_field_namePlaceholder": "Espera de doblaje", + "settings_quality_delay_field_preferredProtocol": "Protocolo preferido (obtiene inmediatamente)", + "settings_quality_delay_field_torrentDelay": "Retraso de torrent (minutos)", + "settings_quality_delay_field_usenetDelay": "Retraso de usenet (minutos)", + "settings_quality_delay_formatImmediate": "Inmediato", + "settings_quality_delay_modalAddTitle": "Añadir perfil de retraso", + "settings_quality_delay_modalEditTitle": "Editar perfil de retraso", + "settings_quality_delay_option_none": "Ninguno", + "settings_quality_delay_option_torrent": "Torrent", + "settings_quality_delay_option_usenet": "Usenet", + "settings_quality_delay_preferredPrefix": "Preferido:", + "settings_quality_delay_sectionDescription": "Mantén releases en una cola antes de enviarlos a un cliente de descarga, dando tiempo a que aparezcan mejores versiones.", + "settings_quality_delay_tab": "Perfiles de retraso", + "settings_quality_delay_torrentPrefix": "Torrent:", + "settings_quality_delay_updated": "Perfil de retraso actualizado", + "settings_quality_delay_usenetPrefix": "Usenet:", "settings_quality_failedToDeleteFormat": "Error al eliminar formato", "settings_quality_failedToDeleteProfile": "Error al eliminar perfil", "settings_quality_failedToSetDefault": "Error al establecer perfil predeterminado", @@ -2888,12 +3028,25 @@ "settings_quality_subtitle": "Gestiona perfiles de calidad y formatos personalizados para controlar cómo se puntúan y seleccionan los lanzamientos.", "settings_quality_tabFormats": "Formatos personalizados", "settings_quality_tabProfiles": "Perfiles", + "settings_resolutions_add": "Añadir categoría", + "settings_resolutions_delete": "Eliminar", + "settings_resolutions_description": "Categorías de resolución editables para puntuación de calidad, insignias y estadísticas.", + "settings_resolutions_failed": "Error al guardar", + "settings_resolutions_fallback": "Respaldo", + "settings_resolutions_fallback_help": "Categoría predeterminada. No se puede eliminar.", + "settings_resolutions_label": "Etiqueta", + "settings_resolutions_min_height": "Alto mín.", + "settings_resolutions_min_width": "Ancho mín.", + "settings_resolutions_saved": "Categorías guardadas", + "settings_resolutions_search_terms": "Términos de búsqueda", + "settings_resolutions_search_terms_help": "Alias separados por comas para búsqueda", + "settings_resolutions_title": "Categorías de resolución", "settings_streaming_cacheDefaultsNotice": "La configuración de caché está usando valores predeterminados. Configuración personalizada próximamente.", + "settings_streaming_cachedFiles": "Archivos en caché", "settings_streaming_cacheSettings": "Configuración de caché", "settings_streaming_cacheSize": "Tamaño de caché", - "settings_streaming_cachedFiles": "Archivos en caché", - "settings_streaming_cleanExpiredFiles": "Limpiar archivos expirados", "settings_streaming_cleanedUpResult": "Limpiados {count} archivo(s), liberados {size}", + "settings_streaming_cleanExpiredFiles": "Limpiar archivos expirados", "settings_streaming_cleaning": "Limpiando...", "settings_streaming_expired": "Expirado", "settings_streaming_expiredCleaned": "Archivos de caché expirados limpiados", @@ -2953,25 +3106,25 @@ "settings_subtitles_reSyncSubtitle": "Resincronizar subtítulo", "settings_subtitles_retry": "Reintentar", "settings_subtitles_sdh": "SDH", + "settings_subtitles_searchingProviders": "Buscando proveedores de subtítulos...", "settings_subtitles_searchStats": "{total} resultados", "settings_subtitles_searchSubs": "Buscar subs", "settings_subtitles_searchTime": "Búsqueda: {time}ms", - "settings_subtitles_searchingProviders": "Buscando proveedores de subtítulos...", "settings_subtitles_splitPenalty": "Penalización de división", "settings_subtitles_splitRecommended": "5-20 recomendado", "settings_subtitles_subtitleSearch": "Búsqueda de subtítulos", "settings_subtitles_subtitleSync": "Sincronización de subtítulos", "settings_subtitles_sync": "Sincronizar", "settings_subtitles_syncAll": "Sincronizar todos ({count})", + "settings_subtitles_synced": "Sincronizado", + "settings_subtitles_syncedBadge": "SYNC", + "settings_subtitles_syncedCount": "{count} sincronizados", + "settings_subtitles_syncing": "Sincronizando {completed}/{total}...", "settings_subtitles_syncProgress": "{synced}/{total}", "settings_subtitles_syncSettings": "Configuración de sincronización", "settings_subtitles_syncTooltip": "Subtítulo sincronizado ({offset})", "settings_subtitles_syncTooltipNoOffset": "Subtítulo sincronizado", "settings_subtitles_syncUnsynced": "Sincronizar no sincronizados ({count})", - "settings_subtitles_synced": "Sincronizado", - "settings_subtitles_syncedBadge": "SYNC", - "settings_subtitles_syncedCount": "{count} sincronizados", - "settings_subtitles_syncing": "Sincronizando {completed}/{total}...", "settings_subtitles_tryAdjustingFilters": "Intenta ajustar tu perfil de idioma o filtros de búsqueda", "settings_subtitles_unknownOffset": "Desconocido", "settings_system_apiAuth": "Autenticación API", @@ -2993,11 +3146,11 @@ "settings_system_backup_errorSelectSectionFirst": "Selecciona al menos una sección de respaldo para restaurar.", "settings_system_backup_exportButton": "Exportar respaldo", "settings_system_backup_exportDescription": "Exporta la configuración de ajustes, integraciones, indexadores, clientes de descarga, perfiles y demás configuración administrada por el usuario para reconstruir la instancia.", + "settings_system_backup_exporting": "Exportando...", "settings_system_backup_exportPassphraseLabel": "Frase de cifrado", "settings_system_backup_exportPassphrasePlaceholder": "Al menos 16 caracteres para cifrar secretos exportados", "settings_system_backup_exportSuccess": "Respaldo de configuración exportado correctamente.", "settings_system_backup_exportTitle": "Exportar configuración", - "settings_system_backup_exporting": "Exportando...", "settings_system_backup_fileLabel": "Archivo de respaldo", "settings_system_backup_includeCookiesHelp": "Respaldar cookies activas de 2FA/sesión para indexadores que dependen de ellas. Están cifradas, son de corta duración y más sensibles que las credenciales guardadas.", "settings_system_backup_includeCookiesLabel": "Incluir cookies de sesión de indexadores", @@ -3036,8 +3189,8 @@ "settings_system_created": "Creada", "settings_system_exampleEmpty": "Deja vacío para acceso HTTP directo", "settings_system_exampleReverseProxy": "https://cinephage.ejemplo.com (detrás de proxy inverso)", - "settings_system_exampleSubpath": "https://media.tudominio.com/cinephage (subruta)", "settings_system_examples": "Ejemplos", + "settings_system_exampleSubpath": "https://media.tudominio.com/cinephage (subruta)", "settings_system_externalUrl": "URL externa", "settings_system_externalUrlDescription": "Establece la URL pública para tu instancia de Cinephage. Esto se usa para retrollamadas de autenticación y generar enlaces correctos cuando está detrás de un proxy inverso.", "settings_system_externalUrlPlaceholder": "https://cinephage.tudominio.com", @@ -3050,10 +3203,10 @@ "settings_system_failedToSave": "Error al guardar", "settings_system_failedToSaveExternalUrl": "Error al guardar URL externa", "settings_system_fullAccess": "Acceso completo", - "settings_system_generateApiKeys": "Generar claves API", - "settings_system_generating": "Generando...", "settings_system_general_pageTitle": "General - Cinephage", "settings_system_general_subtitle": "Administra claves API y ajustes de URL externa.", + "settings_system_generateApiKeys": "Generar claves API", + "settings_system_generating": "Generando...", "settings_system_heading": "Configuración del sistema", "settings_system_hideKey": "Ocultar clave", "settings_system_invalidUrlFormat": "Por favor ingresa una URL válida (ej., https://cinephage.ejemplo.com)", @@ -3069,13 +3222,13 @@ "settings_system_noApiKeys": "No se encontraron claves API. Genera claves para habilitar acceso externo.", "settings_system_noKeyToRegenerate": "No se encontró clave API {label} para regenerar", "settings_system_pageTitle": "Configuración del sistema - Cinephage", - "settings_system_permEpg": "✓ Datos EPG (/api/livetv/epg.xml)", - "settings_system_permLiveTvStreams": "✓ Streams de TV en vivo (/api/livetv/stream/*)", - "settings_system_permM3u": "✓ Lista de reproducción M3U (/api/livetv/playlist.m3u)", - "settings_system_permNoLibrary": "✗ Acceso a biblioteca", - "settings_system_permNoSettings": "✗ Acceso a configuración", - "settings_system_permStreamingContent": "✓ Contenido de streaming (/api/streaming/*)", + "settings_system_permEpg": "Datos EPG (/api/livetv/epg.xml)", "settings_system_permissions": "Permisos", + "settings_system_permLiveTvStreams": "Streams de TV en vivo (/api/livetv/stream/*)", + "settings_system_permM3u": "Lista de reproducción M3U (/api/livetv/playlist.m3u)", + "settings_system_permNoLibrary": "Acceso a biblioteca", + "settings_system_permNoSettings": "Acceso a configuración", + "settings_system_permStreamingContent": "Contenido de streaming (/api/streaming/*)", "settings_system_publicUrlConfig": "Configuración de URL pública", "settings_system_publicUrlHint": "Si accedes a Cinephage a través de un proxy inverso con HTTPS, ingresa tu URL pública aquí (ej., https://cinephage.tudominio.com). Deja vacío para acceso HTTP local.", "settings_system_regenerate": "Regenerar", @@ -3085,6 +3238,7 @@ "settings_system_saveExternalUrl": "Guardar URL externa", "settings_system_showKey": "Mostrar clave", "settings_system_subtitle": "Configura ajustes a nivel de sistema incluyendo autenticación API.", + "settings_system_tabsAriaLabel": "Pestañas de navegación del sistema", "settings_tasks_activityAdded": "{count} añadidos", "settings_tasks_activityErrors": "{count} errores", "settings_tasks_activityFailed": "{count} fallidos", @@ -3141,8 +3295,8 @@ "settings_tasks_ok": "OK", "settings_tasks_overdue": "Atrasado", "settings_tasks_pageTitle": "Tareas - Configuración - Cinephage", - "settings_tasks_runNow": "Ejecutar ahora", "settings_tasks_running": "Ejecutando...", + "settings_tasks_runNow": "Ejecutar ahora", "settings_tasks_scheduledTasks": "Tareas programadas", "settings_tasks_statusError": "error", "settings_tasks_statusFound": "{count} encontrados", @@ -3222,9 +3376,9 @@ "smartlists_desc_years": "{min}–{max}", "smartlists_detail_addAllButton": "Añadir todo", "smartlists_detail_addAllConfirm": "¿Añadir todos los elementos visibles a tu biblioteca?", - "smartlists_detail_addToLibrary": "Añadir a biblioteca", "smartlists_detail_addedBulkPartial": "Añadidos {added}, fallidos {failed}", "smartlists_detail_addedToLibrary": "{title} añadida a biblioteca", + "smartlists_detail_addToLibrary": "Añadir a biblioteca", "smartlists_detail_autoAddEnabled": "Auto-añadir activado", "smartlists_detail_backToSmartLists": "Volver a listas inteligentes", "smartlists_detail_badgeExcluded": "Excluido", @@ -3323,9 +3477,9 @@ "smartlists_filter_exclude": "Excluir", "smartlists_filter_excludeGenres": "Excluir géneros", "smartlists_filter_genreMatchMode": "Modo de coincidencia de género", - "smartlists_filter_inProduction": "En producción", "smartlists_filter_include": "Incluir", "smartlists_filter_includeGenres": "Incluir géneros", + "smartlists_filter_inProduction": "En producción", "smartlists_filter_keywords": "Palabras clave", "smartlists_filter_keywordsPlaceholder": "Buscar palabras clave...", "smartlists_filter_loading": "Cargando...", @@ -3361,9 +3515,9 @@ "smartlists_formatDate_minutesAgo": "hace {count}m", "smartlists_formatDate_never": "Nunca", "smartlists_heading": "Listas inteligentes", - "smartlists_inLibraryBadge": "{count} en biblioteca", "smartlists_info_mixedContentDescription": "Las películas y series de la fuente se mostrarán juntas. El selector de tipo de medio arriba solo afecta a listas de Descubrir de TMDB.", "smartlists_info_mixedContentTitle": "Las listas de importación muestran todos los tipos de contenido", + "smartlists_inLibraryBadge": "{count} en biblioteca", "smartlists_itemsBadge": "{count} elementos", "smartlists_label_customHeaders": "Encabezados personalizados (Opcional)", "smartlists_label_jsonUrl": "URL JSON", @@ -3440,17 +3594,32 @@ "smartlists_subtitle": "Crea listas dinámicas que se llenan automáticamente desde TMDB según tus filtros.", "smartlists_test_success": "¡Conectado exitosamente! Se encontraron {count} elementos.", "startFromScratch": "Empezar desde cero", + "status_back_to_dashboard": "Dashboard", "status_completed": "Completado", "status_connected": "Conectado", + "status_dashboard_subtitle": "Storage health, library maintenance, and media server analytics", + "status_dashboard_title": "Storage Maintenance", "status_disabled": "Deshabilitado", "status_disconnected": "Desconectado", "status_downloading": "Descargando", "status_error": "Error", "status_failed": "Fallido", + "status_folder_not_found": "Root folder not found", + "status_folder_save_failed": "Failed to save root folder", + "status_folder_updated": "Root folder updated", + "status_folders_subtitle": "Manage root folders, free space, and scan status", + "status_folders_title": "Root Folders", "status_healthy": "Saludable", "status_idle": "Inactivo", "status_imported": "Importado", + "status_materializing": "Materializando", + "status_insights_subtitle": "Issues detected across your library and media servers", + "status_insights_title": "Storage Insights", + "status_libraries_subtitle": "Manage your media libraries and their root folders", + "status_libraries_title": "Libraries", "status_matched": "Coincidido", + "status_media_subtitle": "Browse all media with stats, quality info, and playback data", + "status_media_title": "Media Explorer", "status_noResults": "Sin resultados", "status_paused": "Pausado", "status_queued": "En cola", @@ -3458,17 +3627,20 @@ "status_rejected": "Rechazado", "status_removed": "Eliminado", "status_running": "Ejecutando", + "status_scan_started": "Scan started", "status_searchError": "Error de búsqueda", "status_searching": "Buscando", "status_seeding": "Compartiendo", "status_stopped": "Detenido", "status_streaming": "Transmitiendo", "status_success": "Éxito", + "status_sync_failed": "Sync failed", "status_tooltipDegraded": "{count} fallos recientes. Último: {time}", "status_tooltipDisabled": "El cliente de descarga está deshabilitado por el usuario", "status_tooltipHealthy": "El cliente de descarga está saludable y es accesible", "status_tooltipUnhealthy": "{count} fallos consecutivos. Último: {time}", "status_unhealthy": "No saludable", + "status_unknown_error": "Unknown error", "status_unmatched": "No coincidente", "status_warning": "Advertencia", "streamingProfileInfo": "Los perfiles de streaming usan fuentes externas, así que los límites de tamaño no son configurables.", @@ -3551,8 +3723,8 @@ "subtitleProviders_table_featureHashMatching": "Coincidencia de hash", "subtitleProviders_table_featureLanguageMatching": "Coincidencia de idioma", "subtitleProviders_table_featureProviderApi": "API del proveedor", - "subtitleProviders_table_featureSubtitleSearch": "Búsqueda de subtítulos", "subtitleProviders_table_features": "Características", + "subtitleProviders_table_featureSubtitleSearch": "Búsqueda de subtítulos", "subtitleProviders_table_moveDown": "Mover abajo", "subtitleProviders_table_moveUp": "Mover arriba", "subtitleProviders_table_name": "Nombre", @@ -3609,6 +3781,9 @@ "task_desc_subtitleUpgrade": "Buscar subtítulos con mejor puntuación para reemplazar los existentes", "task_desc_updateStrmUrls": "Actualizar todos los archivos .strm con la URL base de streaming actual. Ejecuta esto después de cambiar la dirección de tu servidor, puerto o dominio.", "task_desc_upgrade": "Buscar lanzamientos de mejor calidad para reemplazar archivos existentes", + "task_history_empty": "Sin historial de ejecución aún", + "task_history_loading": "Cargando historial...", + "task_history_loadingActivity": "Cargando actividad...", "task_historyModal_added": "Añadidos: {count}", "task_historyModal_close": "Cerrar", "task_historyModal_details": "Detalles", @@ -3627,9 +3802,6 @@ "task_historyModal_updated": "Actualizados: {count}", "task_historyModal_updatedFiles": "Archivos actualizados: {count}", "task_historyModal_viewFailedItems": "Ver elementos fallidos", - "task_history_empty": "Sin historial de ejecución aún", - "task_history_loading": "Cargando historial...", - "task_history_loadingActivity": "Cargando actividad...", "task_name_cutoffUnmet": "Búsqueda de corte no cumplido", "task_name_historyCleanup": "Limpieza de historial", "task_name_libraryScan": "Escaneo de biblioteca", @@ -3687,8 +3859,10 @@ "tmdb_seasonList_noOverview": "Sin sinopsis disponible", "tmdb_watchProviders_notAvailable": "No disponible para streaming", "toast_activity_adminRequired": "Se requiere acceso de administrador", + "toast_activity_clearedFailedQueue": "{count} descarga(s) fallida(s) eliminadas de la cola", "toast_activity_deleteCompleted": "Eliminación completada. La lista de actividad se actualizará pronto.", "toast_activity_deletedEntries": "Eliminadas {count} entradas de actividad", + "toast_activity_failedToClearFailed": "Error al borrar descargas fallidas", "toast_activity_failedToDeleteSelected": "Error al eliminar entradas de actividad seleccionadas", "toast_activity_failedToLoadActivity": "Error al cargar actividad", "toast_activity_failedToLoadMore": "Error al cargar más actividad", @@ -3739,9 +3913,9 @@ "toast_library_movieDetail_movieFilesDeleted": "Archivos de la película eliminados", "toast_library_movieDetail_movieRemoved": "Película eliminada de la biblioteca", "toast_library_movieDetail_noSuitableReleases": "No se encontraron lanzamientos adecuados", + "toast_library_movieDetail_subtitleSynced": "Subtítulo sincronizado", "toast_library_movieDetail_subtitleSyncFailed": "Sincronización de subtítulo fallida", "toast_library_movieDetail_subtitleSyncOffset": "Offset aplicado de {offset}ms", - "toast_library_movieDetail_subtitleSynced": "Subtítulo sincronizado", "toast_library_movies_autoGrabbed": "Captura automática de \"{release}\" para \"{title}\"", "toast_library_movies_deletedFilesCount": "Archivos eliminados de {count} películas", "toast_library_movies_failedAutoGrab": "Error en captura automática para \"{title}\"", @@ -3753,13 +3927,32 @@ "toast_library_movies_filesDeleted": "Archivos de \"{title}\" eliminados", "toast_library_movies_foundNoMatch": "Se encontraron lanzamientos pero ninguno cumplió los criterios para \"{title}\"", "toast_library_movies_grabbed": "\"\"{title}\" capturada", - "toast_library_movies_monitorToggle": "\"{title}\" {status}", "toast_library_movies_monitoringCount": "Monitoreando {count} películas", + "toast_library_movies_monitorToggle": "\"{title}\" {status}", "toast_library_movies_noReleases": "No se encontraron lanzamientos para \"{title}\"", "toast_library_movies_qualityUpdatedCount": "Perfil de calidad actualizado para {count} películas", "toast_library_movies_removedCount": "{count} películas eliminadas de la biblioteca", "toast_library_movies_removedFromLibrary": "\"\"{title}\" eliminada de la biblioteca", "toast_library_movies_unmonitoredCount": "{count} películas no monitoreadas", + "toast_library_tv_autoGrabbedCount": "{count} lanzamiento(s) capturado(s) automáticamente para \"{title}\"", + "toast_library_tv_deletedFilesCount": "Archivos eliminados de {count} series", + "toast_library_tv_failedAutoGrab": "Error en captura automática para \"{title}\"", + "toast_library_tv_failedToDelete": "Error al eliminar", + "toast_library_tv_failedToDeleteSeries": "Error al eliminar serie", + "toast_library_tv_failedToGrab": "Error al capturar lanzamiento", + "toast_library_tv_failedToUpdate": "Error al actualizar series", + "toast_library_tv_failedToUpdateSeries": "Error al actualizar serie", + "toast_library_tv_failedToUpdateSingle": "Error al actualizar", + "toast_library_tv_filesDeleted": "Archivos de \"{title}\" eliminados", + "toast_library_tv_foundNoMatch": "Se encontraron {count} lanzamientos pero ninguno cumplió los criterios para \"{title}\"", + "toast_library_tv_grabbed": "\"\"{title}\" capturada", + "toast_library_tv_monitoringCount": "Monitoreando {count} series", + "toast_library_tv_monitorToggle": "\"{title}\" {status}", + "toast_library_tv_noReleases": "No se encontraron lanzamientos para \"{title}\"", + "toast_library_tv_qualityUpdatedCount": "Perfil de calidad actualizado para {count} series", + "toast_library_tv_removedCount": "{count} series eliminadas de la biblioteca", + "toast_library_tv_removedFromLibrary": "\"\"{title}\" eliminada de la biblioteca", + "toast_library_tv_unmonitoredCount": "{count} series no monitoreadas", "toast_library_tvDetail_bulkSearchFailed": "Búsqueda masiva fallida", "toast_library_tvDetail_bulkSubAutoSearchFailed": "Búsqueda automática de subtítulos masiva fallida", "toast_library_tvDetail_bulkSyncError": "Sincronización masiva fallida", @@ -3796,29 +3989,10 @@ "toast_library_tvDetail_seriesRefreshFailed": "La actualización de la serie falló", "toast_library_tvDetail_seriesRemoved": "Serie eliminada de la biblioteca", "toast_library_tvDetail_subtitleDeleted": "Subtítulo eliminado", + "toast_library_tvDetail_subtitleSynced": "Subtítulo sincronizado", "toast_library_tvDetail_subtitleSyncFailed": "Sincronización de subtítulo fallida", "toast_library_tvDetail_subtitleSyncOffset": "Offset aplicado de {offset}ms", - "toast_library_tvDetail_subtitleSynced": "Subtítulo sincronizado", "toast_library_tvDetail_syncedSubtitles": "{count} subtítulos sincronizados", - "toast_library_tv_autoGrabbedCount": "{count} lanzamiento(s) capturado(s) automáticamente para \"{title}\"", - "toast_library_tv_deletedFilesCount": "Archivos eliminados de {count} series", - "toast_library_tv_failedAutoGrab": "Error en captura automática para \"{title}\"", - "toast_library_tv_failedToDelete": "Error al eliminar", - "toast_library_tv_failedToDeleteSeries": "Error al eliminar serie", - "toast_library_tv_failedToGrab": "Error al capturar lanzamiento", - "toast_library_tv_failedToUpdate": "Error al actualizar series", - "toast_library_tv_failedToUpdateSeries": "Error al actualizar serie", - "toast_library_tv_failedToUpdateSingle": "Error al actualizar", - "toast_library_tv_filesDeleted": "Archivos de \"{title}\" eliminados", - "toast_library_tv_foundNoMatch": "Se encontraron {count} lanzamientos pero ninguno cumplió los criterios para \"{title}\"", - "toast_library_tv_grabbed": "\"\"{title}\" capturada", - "toast_library_tv_monitorToggle": "\"{title}\" {status}", - "toast_library_tv_monitoringCount": "Monitoreando {count} series", - "toast_library_tv_noReleases": "No se encontraron lanzamientos para \"{title}\"", - "toast_library_tv_qualityUpdatedCount": "Perfil de calidad actualizado para {count} series", - "toast_library_tv_removedCount": "{count} series eliminadas de la biblioteca", - "toast_library_tv_removedFromLibrary": "\"\"{title}\" eliminada de la biblioteca", - "toast_library_tv_unmonitoredCount": "{count} series no monitoreadas", "toast_library_unmatched_bulkDeleteNotImpl": "Eliminación masiva aún no implementada", "toast_library_unmatched_failedToDelete": "Error al eliminar archivo", "toast_library_unmatched_fileDeletedFromDisk": "Archivo eliminado del disco", @@ -3852,6 +4026,7 @@ "ui_deleteModal_warningRemoveOnly": "Advertencia: Esto solo eliminará el elemento de tu biblioteca.", "ui_deleteModal_warningUnmatch": "Advertencia: Esto desemparejará el elemento.", "ui_failedToChangeLanguage": "Error al cambiar idioma", + "ui_languageLabel": "Idioma", "ui_mediaType_movie": "Película", "ui_mediaType_stream": "Transmitir", "ui_mediaType_tv": "TV", @@ -3869,7 +4044,6 @@ "ui_operationProgress_pending": "Pendiente", "ui_operationProgress_processing": "Procesando...", "ui_operationProgress_running": "Ejecutando", - "ui_languageLabel": "Idioma", "ui_selectLanguage": "Seleccionar idioma", "ui_selectTheme": "Seleccionar tema", "ui_themeDark": "Oscuro", @@ -3931,14 +4105,14 @@ "unmatched_libraryIssues_noMovieFolders": "No hay carpetas raíz de películas configuradas.", "unmatched_libraryIssues_noTvFolders": "No hay carpetas raíz de TV configuradas.", "unmatched_libraryIssues_rootFolderNotSet": "Carpeta raíz no establecida", - "unmatched_libraryIssues_rootFolderUpdateFailed": "Error al actualizar carpeta raíz", "unmatched_libraryIssues_rootFolderUpdated": "Carpeta raíz actualizada", + "unmatched_libraryIssues_rootFolderUpdateFailed": "Error al actualizar carpeta raíz", "unmatched_libraryIssues_selectAllMovies": "Seleccionar todas las películas", "unmatched_libraryIssues_selectAllTv": "Seleccionar todos los programas de TV", + "unmatched_libraryIssues_selectedCount": "{count} seleccionados", "unmatched_libraryIssues_selectItemsFirst": "Selecciona elementos para aplicar la acción masiva", "unmatched_libraryIssues_selectRootFolder": "Seleccionar carpeta raíz", "unmatched_libraryIssues_selectRootFolderFirst": "Selecciona una carpeta raíz primero", - "unmatched_libraryIssues_selectedCount": "{count} seleccionados", "unmatched_libraryIssues_title": "Problemas de biblioteca", "unmatched_libraryIssues_tv": "TV", "unmatched_libraryIssues_tvShows": "Programas de TV", @@ -3947,34 +4121,16 @@ "unmatched_pagination_perPage50": "50 por página", "unmatched_pagination_showing": "Mostrando {start} - {end} de {total} archivos", "validation_maxChars": "Máx. {max} caracteres.", - "blocklist_tab_releases": "Lanzamientos", - "blocklist_tab_blockedMedia": "Multimedia Bloqueado", - "blocklist_tab_blockedKeywords": "Palabras Clave Bloqueadas", - "blockedMedia_pageTitle": "Multimedia Bloqueado", - "blockedMedia_pageSubtitle": "Administrar películas y programas bloqueados que están ocultos del descubrimiento, búsqueda y biblioteca", - "blockedMedia_emptyTitle": "No hay multimedia bloqueado", - "blockedMedia_emptyDescription": "Las películas y programas bloqueados aparecerán aquí.", - "blockedMedia_searchPlaceholder": "Buscar multimedia bloqueado...", - "blockedMedia_filterAll": "Todos", - "blockedMedia_filterMovies": "Películas", - "blockedMedia_filterTV": "Programas de TV", - "blockedMedia_entryCount": "{total} elementos", - "blockedMedia_unblock": "Desbloquear", - "blockedMedia_confirmUnblockTitle": "Desbloquear Multimedia", - "blockedMedia_confirmUnblockMessage": "¿Desbloquear \"{title}\"? Aparecerá en descubrimiento y búsqueda, pero no se volverá a agregar a tu biblioteca.", - "blockedMedia_confirmUnblockLabel": "Desbloquear", - "blockedMedia_confirmBlockTitle": "Bloquear Multimedia", - "blockedMedia_confirmBlockMessage": "¿Bloquear \"{title}\"? Se ocultará del descubrimiento, búsqueda y biblioteca.", - "blockedMedia_confirmBlockLabel": "Bloquear", - "blockedMedia_blocked": "\"{title}\" ha sido bloqueado", - "blockedMedia_unblocked": "\"{title}\" ha sido desbloqueado", - "blockedMedia_alreadyBlocked": "Este contenido ya está bloqueado", - "blockedMedia_mediaTypeMovie": "Película", - "blockedMedia_mediaTypeTV": "TV", - "blockedMedia_dateBlocked": "Bloqueado {date}", - "blockedMedia_noReason": "Sin razón", - "hero_blockMedia": "Bloquear medio", - "hero_blockMediaTooltip": "Bloquear del descubrimiento y biblioteca", - "library_blockMedia": "Eliminar y bloquear", - "library_blockMediaTooltip": "Eliminar de la biblioteca y bloquear" + "downloadClient_realdebrid": "Real-Debrid", + "downloadClient_torbox": "TorBox", + "downloadClient_protocol_debrid": "Debrid", + "downloadClient_apiToken": "Token de API", + "downloadClient_removeAfterImport": "Eliminar del proveedor después de importar", + "settings_integrations_downloadClients_debrid": "Debrid", + "acquisition_acquireVia": "Adquirir mediante", + "acquisition_default": "Predeterminado", + "acquisition_torrent": "Torrent", + "acquisition_debrid": "Debrid", + "acquisition_debridUnavailableReason": "Añade y habilita un cliente debrid con un token de API para usar la adquisición debrid.", + "acquisition_defaultPreference": "Adquisición torrent predeterminada" } diff --git a/package-lock.json b/package-lock.json index 93edb09ca..26c1b986e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,10 +34,10 @@ "@inlang/paraglide-js": "^2.18.2", "@semantic-release/exec": "^7.1.0", "@semantic-release/github": "^12.0.8", - "@sveltejs/adapter-node": "^5.5.4", - "@sveltejs/kit": "^2.64.0", + "@sveltejs/adapter-node": "^5.5.7", + "@sveltejs/kit": "^2.68.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.3.0", + "@tailwindcss/vite": "^4.3.1", "@types/adm-zip": "^0.5.8", "@types/better-sqlite3": "^7.6.13", "@types/js-yaml": "^4.0.9", @@ -46,7 +46,7 @@ "@vitest/browser": "^4.1.8", "@vitest/browser-playwright": "^4.1.5", "@vitest/coverage-v8": "^4.1.5", - "daisyui": "^5.5.23", + "daisyui": "^5.6.3", "drizzle-orm": "^0.45.2", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", @@ -58,8 +58,8 @@ "prettier-plugin-svelte": "^4.1.0", "prettier-plugin-tailwindcss": "^0.8.0", "semantic-release": "^25.0.5", - "svelte": "^5.56.3", - "svelte-check": "^4.6.0", + "svelte": "^5.56.4", + "svelte-check": "^4.7.1", "tailwindcss": "^4.3.0", "typescript": "^6.0.3", "typescript-eslint": "^8.61.0", @@ -136,7 +136,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -146,7 +146,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -156,7 +156,7 @@ "version": "7.29.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -172,7 +172,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -186,7 +186,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -343,7 +343,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@colors/colors": { @@ -361,7 +361,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -373,7 +372,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -384,7 +382,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -732,7 +729,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1025,7 +1021,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1042,7 +1037,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1059,7 +1053,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1076,7 +1069,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1093,7 +1085,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1110,7 +1101,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1127,7 +1117,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1144,7 +1133,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1161,7 +1149,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1178,7 +1165,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1195,7 +1181,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1212,7 +1197,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1229,7 +1213,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1248,7 +1231,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1265,7 +1247,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1355,6 +1336,28 @@ } } }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", @@ -2148,15 +2151,16 @@ } }, "node_modules/@sveltejs/adapter-node": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", - "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.7.tgz", + "integrity": "sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g==", "dev": true, "license": "MIT", "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.3", "rollup": "^4.59.0" }, "peerDependencies": { @@ -2164,9 +2168,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.64.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.64.0.tgz", - "integrity": "sha512-24MXA/xyugUqJd09DD4v4bLd4k7FaYhdBck/pTXiu3XFtDBcwXYpJc1SO1+lqdjkNBUo1r4eek5FS7cfzBQUmA==", + "version": "2.68.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.68.0.tgz", + "integrity": "sha512-PdKiWsqinAoubVsSiRgVFkg3MHzGhQPnwQ8VxnGQKpZYijpapZ3UHHBje0GeByt2TvfjHPw+kxV+dNK2RIZg9g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2206,9 +2210,9 @@ } }, "node_modules/@sveltejs/load-config": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.1.1.tgz", - "integrity": "sha512-BXXm+VOH/9X4N7Dd1iZ2MqA1h7M+9i2noI8QYuLDY8QcN2WHYn7D/VK/+IJNfcAmRw7ACNJ538UT9GXIhnBTiA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", "dev": true, "license": "MIT", "engines": { @@ -2236,49 +2240,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" + "tailwindcss": "4.3.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", "cpu": [ "arm64" ], @@ -2293,9 +2297,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", "cpu": [ "arm64" ], @@ -2310,9 +2314,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", "cpu": [ "x64" ], @@ -2327,9 +2331,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", "cpu": [ "x64" ], @@ -2344,9 +2348,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", "cpu": [ "arm" ], @@ -2361,13 +2365,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2378,13 +2385,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2395,13 +2405,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2412,13 +2425,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2429,9 +2445,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2451,17 +2467,83 @@ "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.1", + "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", "cpu": [ "arm64" ], @@ -2476,9 +2558,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", "cpu": [ "x64" ], @@ -2493,15 +2575,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", - "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "tailwindcss": "4.3.0" + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -2523,7 +2605,6 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2544,7 +2625,7 @@ "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -2617,7 +2698,7 @@ "version": "25.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" @@ -2816,7 +2897,7 @@ "version": "8.61.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2900,7 +2981,7 @@ "version": "4.1.8", "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.8.tgz", "integrity": "sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@blazediff/core": "1.9.1", @@ -2923,7 +3004,7 @@ "version": "4.1.8", "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.8.tgz", "integrity": "sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/browser": "4.1.8", @@ -2947,7 +3028,7 @@ "version": "4.1.8", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", @@ -3282,7 +3363,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", @@ -3294,7 +3375,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -4426,9 +4507,9 @@ } }, "node_modules/daisyui": { - "version": "5.5.23", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.23.tgz", - "integrity": "sha512-xuheNUSL4T6ZVtWXoioqcNkjoyGX85QTDz4HTw2aBPfqk4fuMjax5HDo8qCmpV6M1YN8bGvfx5BpYCoDeRlt+A==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.6.3.tgz", + "integrity": "sha512-QvdtXnQ/tD5a18Y/+NJkJ1+ggwRtMF84GHTHCCAto5SNqYwZj8SUQQviKMpe1cKR7vMo/evTBDExkYd19KloBQ==", "dev": true, "license": "MIT", "funding": { @@ -4861,9 +4942,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.21.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.4.tgz", - "integrity": "sha512-wE4fDO8OjJhrPFH69HUQStq5oKvGRTNXEyW+k5C/pUQLASSsTu7obd2V3GvCDgPcY9AWjhJ4jz9Kh7iRvrxhJg==", + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5278,9 +5359,9 @@ } }, "node_modules/esrap": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", - "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", + "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5694,7 +5775,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5906,7 +5986,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -5987,7 +6067,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/htmlparser2": { @@ -6545,7 +6625,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -6555,7 +6635,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -6570,7 +6650,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -6594,7 +6674,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -6630,7 +6710,7 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -6828,7 +6908,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6849,7 +6928,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6870,7 +6948,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6891,7 +6968,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6912,7 +6988,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6933,7 +7008,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6954,7 +7028,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6975,7 +7048,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6996,7 +7068,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7017,7 +7088,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7038,7 +7108,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7205,7 +7274,7 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.3", @@ -7263,7 +7332,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -10202,7 +10271,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "dependencies": { @@ -10234,7 +10303,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14.19.0" @@ -11616,7 +11685,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -11656,9 +11725,9 @@ } }, "node_modules/svelte": { - "version": "5.56.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", - "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", + "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -11673,7 +11742,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.11", + "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -11684,14 +11753,14 @@ } }, "node_modules/svelte-check": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.6.0.tgz", - "integrity": "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.1.tgz", + "integrity": "sha512-FGUOmAqxXdN/H9Zm8slrqO7SLtFisXRB7rfOsHNJ3MLTD2po/+Stg8XyErkpumPHbuUiYTcqrEIzxpVWKTLqtg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", - "@sveltejs/load-config": "0.1.1", + "@sveltejs/load-config": "^0.2.0", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", @@ -11841,9 +11910,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", "dev": true, "license": "MIT" }, @@ -12186,7 +12255,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12307,7 +12376,7 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicode-emoji-modifier-base": { @@ -12561,7 +12630,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12858,7 +12926,7 @@ "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 963b6cbf5..22c9395a0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "migrate:remove-enabled": "tsx scripts/remove-monitoring-enabled.ts", "auth:reset-admin-password": "node scripts/reset-admin-password.js", "fix:tv-subtitles": "node scripts/fix-tv-subtitle-paths.js", + "fix:episode-file-links": "node scripts/repair-episode-file-links.js", "library:backfill-release-metadata": "npx tsx scripts/backfill-release-metadata.ts", "library:export-release-metadata-audit": "npx tsx scripts/export-release-metadata-audit.ts", "db:reset": "rm -f data/cinephage.db && echo 'Database deleted. Will be recreated on next startup.'", @@ -35,10 +36,10 @@ "@inlang/paraglide-js": "^2.18.2", "@semantic-release/exec": "^7.1.0", "@semantic-release/github": "^12.0.8", - "@sveltejs/adapter-node": "^5.5.4", - "@sveltejs/kit": "^2.64.0", + "@sveltejs/adapter-node": "^5.5.7", + "@sveltejs/kit": "^2.68.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.3.0", + "@tailwindcss/vite": "^4.3.1", "@types/adm-zip": "^0.5.8", "@types/better-sqlite3": "^7.6.13", "@types/js-yaml": "^4.0.9", @@ -47,7 +48,7 @@ "@vitest/browser": "^4.1.8", "@vitest/browser-playwright": "^4.1.5", "@vitest/coverage-v8": "^4.1.5", - "daisyui": "^5.5.23", + "daisyui": "^5.6.3", "drizzle-orm": "^0.45.2", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", @@ -59,8 +60,8 @@ "prettier-plugin-svelte": "^4.1.0", "prettier-plugin-tailwindcss": "^0.8.0", "semantic-release": "^25.0.5", - "svelte": "^5.56.3", - "svelte-check": "^4.6.0", + "svelte": "^5.56.4", + "svelte-check": "^4.7.1", "tailwindcss": "^4.3.0", "typescript": "^6.0.3", "typescript-eslint": "^8.61.0", diff --git a/renovate.json b/renovate.json index 20f797447..76f1abea7 100644 --- a/renovate.json +++ b/renovate.json @@ -10,7 +10,8 @@ "timezone": "UTC", "labels": ["Dependencies"], "assignees": ["MoldyTaint"], - "baseBranchPatterns": ["dev"], + "baseBranches": ["dev"], + "automergeType": "pr", "packageRules": [ { "description": "Automerge patch and minor npm updates after 3-day stability window", diff --git a/scripts/remove-monitoring-enabled.ts b/scripts/remove-monitoring-enabled.ts index 8156c105f..6675e0351 100644 --- a/scripts/remove-monitoring-enabled.ts +++ b/scripts/remove-monitoring-enabled.ts @@ -12,15 +12,15 @@ import { monitoringSettings } from '../src/lib/server/db/schema.js'; import { eq } from 'drizzle-orm'; async function main() { - console.log('🗑️ Removing "enabled" key from monitoring_settings...'); + console.log('Removing "enabled" key from monitoring_settings...'); try { await db.delete(monitoringSettings).where(eq(monitoringSettings.key, 'enabled')); - console.log('✅ Successfully removed "enabled" key'); + console.log('Successfully removed "enabled" key'); console.log(' Monitoring will now always run on server startup'); } catch (error) { - console.error('❌ Failed to remove "enabled" key:', error); + console.error('Failed to remove "enabled" key:', error); process.exit(1); } } diff --git a/scripts/repair-episode-file-links.js b/scripts/repair-episode-file-links.js new file mode 100644 index 000000000..81c913948 --- /dev/null +++ b/scripts/repair-episode-file-links.js @@ -0,0 +1,204 @@ +/** + * Repair broken episode_files.episode_ids references. + * + * Symptom: ReconciliationService falls back to file-granularity rows (null + * episode number) for episode files whose episode_ids point at UUIDs that no + * longer exist in the episodes table. This happens when the episodes table is + * regenerated (re-import / re-scan) and episode_files keeps stale UUIDs. The + * visible effect is a storage maintenance insight reporting episodes as + * "missing from media server" / "not tracked by Cinephage" that actually match. + * + * This script re-links episode_files.episode_ids by parsing the season/episode + * numbers from each file's relative_path and looking up the current episode row + * by (series_id, season_number, episode_number). It only touches files whose + * current episode_ids are orphaned, and only updates when every parsed episode + * resolves to exactly one episode row. + * + * Usage: + * node scripts/repair-episode-file-links.js # dry-run (default) + * DRY_RUN=0 node scripts/repair-episode-file-links.js # apply + * CINEPHAGE_DB_PATH=/path/cinephage.db node scripts/repair-episode-file-links.js + */ +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import Database from 'better-sqlite3'; + +const DRY_RUN = process.env.DRY_RUN !== '0' && process.env.DRY_RUN !== 'false'; +const DB_PATH = resolveDbPath(); + +function resolveDbPath() { + if (process.env.CINEPHAGE_DB_PATH) { + return process.env.CINEPHAGE_DB_PATH; + } + if (process.env.DATA_DIR) { + return join(process.env.DATA_DIR, 'cinephage.db'); + } + if (existsSync('/config/data')) { + return '/config/data/cinephage.db'; + } + return 'data/cinephage.db'; +} + +function assertDbPath(dbPath) { + const dir = dirname(dbPath); + if (!existsSync(dir)) { + throw new Error( + `Database directory does not exist: ${dir}. Set CINEPHAGE_DB_PATH or DATA_DIR.` + ); + } + if (!existsSync(dbPath)) { + const hint = + process.env.CINEPHAGE_DB_PATH || process.env.DATA_DIR + ? 'Check CINEPHAGE_DB_PATH or DATA_DIR.' + : 'If running in Docker, set CINEPHAGE_DB_PATH=/config/data/cinephage.db.'; + throw new Error(`Database not found at ${dbPath}. ${hint}`); + } +} + +function parseEpisodeIds(value) { + if (!value) return []; + if (Array.isArray(value)) return value; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +/** + * Extract the season and one-or-more episode numbers from a file path. + * Recognises S01E01, S01E01-E02, S01E01-02, and S01E01E02 forms. Returns null + * when no season/episode marker is present (the file can't be repaired by name). + */ +function parseSeasonEpisode(relativePath) { + const filename = relativePath.split('/').pop() ?? relativePath; + const marker = filename.match(/s(\d+)\s*e(\d+)/i); + if (!marker) return null; + const season = parseInt(marker[1], 10); + const firstEp = parseInt(marker[2], 10); + const episodes = [firstEp]; + + const rest = filename.slice(marker.index + marker[0].length); + + // Range form: "-E02" or "-02" immediately after the marker. + const range = rest.match(/^\s*-\s*(?:e)?(\d+)/i); + if (range) { + const last = parseInt(range[1], 10); + if (last > firstEp) { + for (let e = firstEp + 1; e <= last; e++) episodes.push(e); + return { season, episodes }; + } + } + + // Multi-episode form: "E02E03" with no separator immediately after the marker. + const multi = rest.match(/^(?:e(\d+))+/i); + if (multi) { + const re = /e(\d+)/gi; + let m; + while ((m = re.exec(rest)) !== null) { + const n = parseInt(m[1], 10); + if (!episodes.includes(n)) episodes.push(n); + } + } + + return { season, episodes }; +} + +function main() { + assertDbPath(DB_PATH); + const db = new Database(DB_PATH); + + const episodeExistsStmt = db.prepare('SELECT 1 FROM episodes WHERE id = ? LIMIT 1'); + const episodesByNumberStmt = db.prepare( + 'SELECT id, episode_number FROM episodes WHERE series_id = ? AND season_number = ? AND episode_number = ? LIMIT 2' + ); + + const allFiles = db + .prepare('SELECT id, series_id, season_number, episode_ids, relative_path FROM episode_files') + .all(); + + let checked = 0; + let orphaned = 0; + let repaired = 0; + let skippedNoMarker = 0; + let skippedUnresolved = 0; + const samples = []; + const SAMPLE_LIMIT = 20; + + for (const file of allFiles) { + checked += 1; + const ids = parseEpisodeIds(file.episode_ids); + if (ids.length === 0) continue; + + // Is the linkage already healthy? (all referenced ids exist) + const orphans = ids.filter((id) => !episodeExistsStmt.get(id)); + if (orphans.length === 0) continue; + + orphaned += 1; + + const parsed = parseSeasonEpisode(file.relative_path); + if (!parsed) { + skippedNoMarker += 1; + if (samples.length < SAMPLE_LIMIT) { + samples.push({ + episodeFileId: file.id, + reason: 'no SxxExx marker in relative_path', + relativePath: file.relative_path + }); + } + continue; + } + + // Resolve every parsed episode number to exactly one episode row. + const newIds = []; + let resolvedAll = true; + for (const epNum of parsed.episodes) { + const rows = episodesByNumberStmt.all(file.series_id, parsed.season, epNum); + if (rows.length !== 1) { + resolvedAll = false; + break; + } + newIds.push(rows[0].id); + } + + if (!resolvedAll || newIds.length === 0) { + skippedUnresolved += 1; + if (samples.length < SAMPLE_LIMIT) { + samples.push({ + episodeFileId: file.id, + reason: `parsed S${parsed.season}E${parsed.episodes.join(',')} did not resolve 1:1 to episodes`, + relativePath: file.relative_path + }); + } + continue; + } + + if (!DRY_RUN) { + db.prepare('UPDATE episode_files SET episode_ids = ? WHERE id = ?').run( + JSON.stringify(newIds), + file.id + ); + } + repaired += 1; + } + + const summary = { + checked, + orphaned, + repaired, + skippedNoMarker, + skippedUnresolved, + dryRun: DRY_RUN + }; + console.log('[repair-episode-file-links] Completed', summary); + if (samples.length > 0) { + console.log('[repair-episode-file-links] Unrepaired samples:'); + for (const s of samples) { + console.log(`- ${s.episodeFileId}: ${s.reason}`); + console.log(` relative_path: ${s.relativePath}`); + } + } +} + +main(); diff --git a/scripts/repair-library.ts b/scripts/repair-library.ts index 486c2b718..428a1191c 100644 --- a/scripts/repair-library.ts +++ b/scripts/repair-library.ts @@ -51,14 +51,14 @@ function parseSeasonEpisode(filename: string): { season: number; episode: number } async function main() { - console.log('🔧 Starting Library Repair...\n'); + console.log(' Starting Library Repair...\n'); let totalFixed = 0; // ===================================================== // Step 1: Remove duplicate episode_files // ===================================================== - console.log('📁 Step 1: Removing duplicate episode_files...'); + console.log(' Step 1: Removing duplicate episode_files...'); // Find all duplicates grouped by series_id + relative_path const allFiles = await db.select().from(episodeFiles); @@ -89,13 +89,13 @@ async function main() { } } } - console.log(` ✅ Removed ${duplicatesRemoved} duplicate episode_files\n`); + console.log(` Removed ${duplicatesRemoved} duplicate episode_files\n`); totalFixed += duplicatesRemoved; // ===================================================== // Step 1B: Remove duplicate movie_files // ===================================================== - console.log('🎬 Step 1B: Removing duplicate movie_files...'); + console.log(' Step 1B: Removing duplicate movie_files...'); const allMovieFiles = await db.select().from(movieFiles); const movieFileGroups = new Map(); @@ -125,17 +125,17 @@ async function main() { } if (files.length > 2) { - console.log(` ⚠️ Movie had ${files.length} duplicates: ${files[0].relativePath}`); + console.log(` Movie had ${files.length} duplicates: ${files[0].relativePath}`); } } } - console.log(` ✅ Removed ${movieDuplicatesRemoved} duplicate movie_files\n`); + console.log(` Removed ${movieDuplicatesRemoved} duplicate movie_files\n`); totalFixed += movieDuplicatesRemoved; // ===================================================== // Step 2: Process unmatched_files in known series folders // ===================================================== - console.log('📂 Step 2: Linking unmatched files in series folders...'); + console.log(' Step 2: Linking unmatched files in series folders...'); // Get all series with their root folders const allSeries = await db @@ -181,7 +181,7 @@ async function main() { // Parse season/episode const parsed = parseSeasonEpisode(filename); if (!parsed) { - console.log(` ⚠️ Could not parse S/E from: ${filename}`); + console.log(` Could not parse S/E from: ${filename}`); continue; } @@ -198,7 +198,7 @@ async function main() { .limit(1); if (existing.length > 0) { - console.log(` ⏭️ Already exists: ${relativePath}`); + console.log(` Already exists: ${relativePath}`); // Remove from unmatched since it's already linked await db.delete(unmatchedFiles).where(eq(unmatchedFiles.id, unmatched.id)); continue; @@ -233,20 +233,20 @@ async function main() { await db.delete(unmatchedFiles).where(eq(unmatchedFiles.id, unmatched.id)); console.log( - ` ✅ Linked: ${relativePath} → S${String(parsed.season).padStart(2, '0')}E${String(parsed.episode).padStart(2, '0')}` + ` Linked: ${relativePath} → S${String(parsed.season).padStart(2, '0')}E${String(parsed.episode).padStart(2, '0')}` ); filesLinked++; break; // Found the series, no need to check others } } } - console.log(` ✅ Linked ${filesLinked} unmatched files to series\n`); + console.log(` Linked ${filesLinked} unmatched files to series\n`); totalFixed += filesLinked; // ===================================================== // Step 3: Reset hasFile flags based on actual episode_files // ===================================================== - console.log('🔄 Step 3: Resetting hasFile flags...'); + console.log(' Step 3: Resetting hasFile flags...'); // First, set all hasFile to false await db.update(episodes).set({ hasFile: false }); @@ -280,12 +280,12 @@ async function main() { } hasFileUpdated = idsArray.length; } - console.log(` ✅ Set hasFile=true for ${hasFileUpdated} episodes\n`); + console.log(` Set hasFile=true for ${hasFileUpdated} episodes\n`); // ===================================================== // Step 4: Recalculate series.episode_file_count // ===================================================== - console.log('📊 Step 4: Recalculating series episode counts...'); + console.log(' Step 4: Recalculating series episode counts...'); const allSeriesForUpdate = await db.select({ id: series.id }).from(series); @@ -312,12 +312,12 @@ async function main() { }) .where(eq(series.id, s.id)); } - console.log(` ✅ Updated ${allSeriesForUpdate.length} series counts\n`); + console.log(` Updated ${allSeriesForUpdate.length} series counts\n`); // ===================================================== // Step 5: Recalculate seasons.episode_file_count // ===================================================== - console.log('📅 Step 5: Recalculating season episode counts...'); + console.log(' Step 5: Recalculating season episode counts...'); const allSeasons = await db.select().from(seasons); @@ -346,12 +346,12 @@ async function main() { }) .where(eq(seasons.id, season.id)); } - console.log(` ✅ Updated ${allSeasons.length} season counts\n`); + console.log(` Updated ${allSeasons.length} season counts\n`); // ===================================================== // Step 6: Recalculate movies.hasFile // ===================================================== - console.log('🎬 Step 6: Recalculating movie hasFile flags...'); + console.log(' Step 6: Recalculating movie hasFile flags...'); // First, set all hasFile to false await db.update(movies).set({ hasFile: false }); @@ -373,12 +373,12 @@ async function main() { } movieHasFileUpdated = idsArray.length; } - console.log(` ✅ Set hasFile=true for ${movieHasFileUpdated} movies\n`); + console.log(` Set hasFile=true for ${movieHasFileUpdated} movies\n`); // ===================================================== // Step 7: Remove duplicate queue entries (same download) // ===================================================== - console.log('📥 Step 7: Removing duplicate queue entries...'); + console.log(' Step 7: Removing duplicate queue entries...'); const allQueueItems = await db.select().from(downloadQueue); const queueGroups = new Map(); @@ -408,30 +408,30 @@ async function main() { } if (items.length > 2) { - console.log(` ⚠️ Queue had ${items.length} duplicates: ${items[0].title}`); + console.log(` Queue had ${items.length} duplicates: ${items[0].title}`); } } } - console.log(` ✅ Removed ${queueDuplicatesRemoved} duplicate queue entries\n`); + console.log(` Removed ${queueDuplicatesRemoved} duplicate queue entries\n`); totalFixed += queueDuplicatesRemoved; // ===================================================== // Step 8: Clean up completed/removed download_queue // ===================================================== - console.log('🗑️ Step 8: Cleaning up completed queue entries...'); + console.log(' Step 8: Cleaning up completed queue entries...'); const deletedQueue = await db .delete(downloadQueue) .where(inArray(downloadQueue.status, ['imported', 'removed'])) .returning({ id: downloadQueue.id }); - console.log(` ✅ Removed ${deletedQueue.length} completed queue entries\n`); + console.log(` Removed ${deletedQueue.length} completed queue entries\n`); totalFixed += deletedQueue.length; // ===================================================== // Step 9: Fetch missing external IDs from TMDB // ===================================================== - console.log('🔗 Step 9: Fetching missing external IDs from TMDB...'); + console.log(' Step 9: Fetching missing external IDs from TMDB...'); // Import TMDB client const { tmdb } = await import('../src/lib/server/tmdb.js'); @@ -463,11 +463,11 @@ async function main() { await db.update(movies).set({ imdbId: externalIds.imdb_id }).where(eq(movies.id, movie.id)); moviesUpdated++; - console.log(` ✅ Updated movie: ${movie.title} → ${externalIds.imdb_id}`); + console.log(` Updated movie: ${movie.title} → ${externalIds.imdb_id}`); } } catch { movieErrors++; - console.log(` ⚠️ Failed to fetch for movie: ${movie.title}`); + console.log(` Failed to fetch for movie: ${movie.title}`); } // Rate limit: 250ms between calls (TMDB allows 40 req/10s) @@ -509,21 +509,21 @@ async function main() { seriesUpdated++; console.log( - ` ✅ Updated series: ${show.title} → IMDB: ${updateData.imdbId || 'N/A'}, TVDB: ${updateData.tvdbId || 'N/A'}` + ` Updated series: ${show.title} → IMDB: ${updateData.imdbId || 'N/A'}, TVDB: ${updateData.tvdbId || 'N/A'}` ); } } catch { seriesErrors++; - console.log(` ⚠️ Failed to fetch for series: ${show.title}`); + console.log(` Failed to fetch for series: ${show.title}`); } // Rate limit: 250ms between calls await delay(250); } - console.log(` ✅ Updated ${moviesUpdated} movies, ${seriesUpdated} series`); + console.log(` Updated ${moviesUpdated} movies, ${seriesUpdated} series`); if (movieErrors > 0 || seriesErrors > 0) { - console.log(` ⚠️ Errors: ${movieErrors} movies, ${seriesErrors} series\n`); + console.log(` Errors: ${movieErrors} movies, ${seriesErrors} series\n`); } else { console.log(''); } @@ -533,7 +533,7 @@ async function main() { // Summary // ===================================================== console.log('═══════════════════════════════════════════════════'); - console.log('✅ Library repair complete!'); + console.log(' Library repair complete!'); console.log(` Total items fixed: ${totalFixed}`); console.log(' - Duplicate episode_files removed: ' + duplicatesRemoved); console.log(' - Duplicate movie_files removed: ' + movieDuplicatesRemoved); @@ -544,12 +544,12 @@ async function main() { console.log(' - Series with external IDs updated: ' + seriesUpdated); console.log('═══════════════════════════════════════════════════\n'); - console.log('📌 Next steps:'); + console.log(' Next steps:'); console.log(' 1. Run a library scan to pick up any remaining unmatched files'); console.log(' 2. Check the UI to verify episode and movie counts are correct'); } main().catch((error) => { - console.error('❌ Repair failed:', error); + console.error(' Repair failed:', error); process.exit(1); }); diff --git a/scripts/test-full-integration.ts b/scripts/test-full-integration.ts index 5cfc1b2cf..f038666b9 100644 --- a/scripts/test-full-integration.ts +++ b/scripts/test-full-integration.ts @@ -54,7 +54,7 @@ async function testTmdbMetadata() { const { tmdb } = await import('../src/lib/server/tmdb'); for (const test of TEST_CASES) { - console.log(`\n📺 ${test.name} (TMDB: ${test.tmdbId})`); + console.log(`\n ${test.name} (TMDB: ${test.tmdbId})`); try { if (test.type === 'tv') { @@ -102,7 +102,7 @@ async function testAniListResolution() { const animeTests = TEST_CASES.filter((t) => t.isAnime); for (const test of animeTests) { - console.log(`\n🎌 ${test.name}`); + console.log(`\n ${test.name}`); try { // Get title from TMDB first (like the resolve endpoint does) @@ -154,7 +154,7 @@ async function testProviderExtraction() { ]; for (const test of testCases) { - console.log(`\n🎬 ${test.name}`); + console.log(`\n ${test.name}`); try { // Fetch TMDB metadata (like resolve endpoint) @@ -237,7 +237,7 @@ async function testAnimeKaiSpecific() { // Test with Attack on Titan const test = TEST_CASES.find((t) => t.name === 'Attack on Titan S1E1')!; - console.log(`\n🎌 Testing: ${test.name}`); + console.log(`\n Testing: ${test.name}`); try { // Get TMDB metadata @@ -305,7 +305,7 @@ async function main() { console.log('Integration test complete!'); console.log('='.repeat(70)); } catch (error) { - console.error('\n❌ Test failed with error:', error); + console.error('\n Test failed with error:', error); process.exit(1); } } diff --git a/scripts/test-list-providers.ts b/scripts/test-list-providers.ts index 79812a325..58b8d35a4 100644 --- a/scripts/test-list-providers.ts +++ b/scripts/test-list-providers.ts @@ -67,9 +67,9 @@ async function main() { await testTmdbPopularProvider(); await testTmdbListProvider(); await testProviderRegistry(); - console.log('\n✅ All tests passed!'); + console.log('\nAll tests passed!'); } catch (error) { - console.error('\n❌ Test failed:', error); + console.error('\nTest failed:', error); process.exit(1); } } diff --git a/scripts/test-yaml-indexers.ts b/scripts/test-yaml-indexers.ts index 7b62aab66..517bccf72 100644 --- a/scripts/test-yaml-indexers.ts +++ b/scripts/test-yaml-indexers.ts @@ -379,7 +379,7 @@ async function runLiveTests( // Dynamically import modules const { YamlDefinitionLoader } = await import('../src/lib/server/indexers/loader/YamlDefinitionLoader.js'); - const { YamlIndexer } = await import('../src/lib/server/indexers/runtime/YamlIndexer.js'); + const { UnifiedIndexer } = await import('../src/lib/server/indexers/runtime/UnifiedIndexer.js'); // Load all definitions const loader = new YamlDefinitionLoader(); @@ -450,7 +450,7 @@ async function runLiveTests( ` ${c.cyan}Testing ${c.bold}${definition.name}${c.reset}${c.cyan} (${defResult.type}/${defResult.protocol})${c.reset}` ); - const result = await testIndexerLive(definition, YamlIndexer, options); + const result = await testIndexerLive(definition, UnifiedIndexer, options); results.push(result); // Show results @@ -482,7 +482,7 @@ async function runLiveTests( async function testIndexerLive( definition: Record, - YamlIndexer: new (config: unknown) => { + IndexerClass: new (config: unknown) => { search(criteria: unknown): Promise; test?(): Promise; capabilities: { @@ -503,24 +503,33 @@ async function testIndexerLive( }; try { - // Create synthetic config - const config = { + // Create synthetic indexer record (mirrors the indexers table shape) + const now = new Date().toISOString(); + const record = { id: `test-${definition.id}`, name: definition.name as string, definitionId: definition.id as string, - baseUrl: ((definition.links as string[]) ?? [])[0] || '', - alternateUrls: [], - protocol: (definition.protocol as string) ?? 'torrent', enabled: true, + upstreamEnabled: null, + orphaned: false, + isBuiltIn: false, + baseUrl: ((definition.links as string[]) ?? [])[0] || '', + alternateUrls: [] as string[], priority: 50, enableAutomaticSearch: true, enableInteractiveSearch: true, - settings: {} + settings: {}, + protocolSettings: null, + cachedCategories: null, + additionalCategories: null, + createdAt: now, + updatedAt: now }; // Create indexer instance - const indexer = new YamlIndexer({ - config, + const indexer = new IndexerClass({ + record, + settings: {}, definition }); diff --git a/src/lib/api/activity.ts b/src/lib/api/activity.ts index 705175b4a..623541db5 100644 --- a/src/lib/api/activity.ts +++ b/src/lib/api/activity.ts @@ -16,6 +16,12 @@ export async function setRetentionDays(retentionDays: number) { return apiPut('/api/activity/settings', { retentionDays }); } -export async function purgeHistory(action: 'older_than_retention' | 'all') { - return apiPost('/api/activity/settings', { action }); +export async function purgeHistory( + action: 'older_than_retention' | 'all', + options?: { removeFromClient?: boolean } +) { + return apiPost('/api/activity/settings', { + action, + removeFromClient: options?.removeFromClient ?? false + }); } diff --git a/src/lib/api/cinephage.ts b/src/lib/api/cinephage.ts new file mode 100644 index 000000000..fdd11b4fd --- /dev/null +++ b/src/lib/api/cinephage.ts @@ -0,0 +1,52 @@ +import type { CinephageSubsystemUpdate, CinephageModuleUpdate } from '$lib/validation/schemas.js'; + +import { apiGet, apiPost, apiPut } from './client.js'; + +/** + * CinephageAPI subsystem state (returned from GET /api/cinephage). + * Mirrors the shape constructed by the +page.server.ts load function. + */ +export interface CinephageSubsystemState { + config: { + enabled: boolean; + baseUrl: string; + versionOverride: string | null; + commitOverride: string | null; + }; + identity: { + version: string; + commit: string | null; + isConfigured: boolean; + }; + modules: Array<{ + moduleId: string; + name: string; + description: string; + maturity: 'stable' | 'beta'; + enabled: boolean; + settings: Record; + lastError: string | null; + capabilities: { providesIndexer?: { definitionId: string } }; + }>; +} + +export interface CinephageTestResult { + success: boolean; + error?: string; +} + +export async function getCinephageState() { + return apiGet('/api/cinephage'); +} + +export async function testCinephageConnection() { + return apiPost('/api/cinephage/test'); +} + +export async function updateCinephageConfig(payload: CinephageSubsystemUpdate) { + return apiPut('/api/cinephage/config', payload); +} + +export async function updateCinephageModule(payload: CinephageModuleUpdate) { + return apiPut(`/api/cinephage/modules/${payload.moduleId}`, payload); +} diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index cfe331c1d..749d5d1d5 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -22,13 +22,13 @@ async function request(url: string, options: RequestInit = {}): Promise>; diff --git a/src/lib/api/delay-profiles.ts b/src/lib/api/delay-profiles.ts new file mode 100644 index 000000000..e245072c6 --- /dev/null +++ b/src/lib/api/delay-profiles.ts @@ -0,0 +1,81 @@ +/** + * Delay Profiles API client + * + * Typed CRUD for release delay profiles (download-timing policy). + * Replaces raw fetch() calls in the Quality settings page that violated + * the project's no-fetch-in-components rule. + * + * Endpoints: + * GET /api/settings/delay-profiles - list all profiles + * POST /api/settings/delay-profiles - create a profile + * PUT /api/settings/delay-profiles/[id] - update a profile + * DELETE /api/settings/delay-profiles/[id] - delete a profile + */ + +import { apiGet, apiPost, apiPut, apiDelete } from './client.js'; + +export interface DelayProfile { + id: string; + name: string; + sortOrder: number; + enabled: boolean | null; + usenetDelay: number; + torrentDelay: number; + qualityDelays: Record | null; + preferredProtocol: string | null; + tags: string[] | null; + bypassIfHighestQuality: boolean | null; + bypassIfAboveScore: number | null; + createdAt: string | null; + updatedAt: string | null; +} + +export interface DelayProfileInput { + name: string; + enabled?: boolean; + torrentDelay?: number; + usenetDelay?: number; + preferredProtocol?: string | null; + bypassIfHighestQuality?: boolean; + bypassIfAboveScore?: number | null; + tags?: string[]; + qualityDelays?: Record; + sortOrder?: number; +} + +/** + * List all delay profiles. + * The endpoint returns the profiles array directly. + */ +export async function listDelayProfiles(): Promise { + return apiGet('/api/settings/delay-profiles'); +} + +/** + * Create a new delay profile. + * Returns { success, id } from the endpoint. + */ +export async function createDelayProfile( + input: DelayProfileInput +): Promise<{ success: boolean; id: string }> { + return apiPost<{ success: boolean; id: string }>('/api/settings/delay-profiles', input); +} + +/** + * Update an existing delay profile. + * Returns { success } from the endpoint. + */ +export async function updateDelayProfile( + id: string, + input: DelayProfileInput +): Promise<{ success: boolean }> { + return apiPut<{ success: boolean }>(`/api/settings/delay-profiles/${id}`, input); +} + +/** + * Delete a delay profile. + * Media assigned to the profile falls back to no delay. + */ +export async function deleteDelayProfile(id: string): Promise<{ success: boolean }> { + return apiDelete<{ success: boolean }>(`/api/settings/delay-profiles/${id}`); +} diff --git a/src/lib/api/downloads.ts b/src/lib/api/downloads.ts index d28f22ba7..f38d1f608 100644 --- a/src/lib/api/downloads.ts +++ b/src/lib/api/downloads.ts @@ -37,6 +37,18 @@ export async function retryQueueItem(id: string): Promise { return apiPost(`/api/queue/${id}/retry`); } +export async function refreshQueue(): Promise { + return apiPost('/api/queue/refresh'); +} + +export async function relinkOrphans(): Promise { + return apiPost('/api/queue/relink-orphans'); +} + +export async function clearFailedQueue(): Promise { + return apiPost('/api/queue/clear-failed'); +} + function buildQuery(params: Record): string { const entries = Object.entries(params); return entries.length ? '?' + new URLSearchParams(params).toString() : ''; diff --git a/src/lib/api/duplicates.ts b/src/lib/api/duplicates.ts new file mode 100644 index 000000000..7a12f78b9 --- /dev/null +++ b/src/lib/api/duplicates.ts @@ -0,0 +1,43 @@ +import { apiGet, apiPost } from './client.js'; + +export interface DuplicateGroup { + signature: string; + signatureType: 'filename' | 'filehash'; + fileIds: string[]; + paths: string[]; + count: number; + suppressed: boolean; +} + +export async function getDuplicates( + libraryId: string, + mode: 'filename' | 'filehash' = 'filename' +): Promise { + return apiGet('/api/library/duplicates', { libraryId, mode }); +} + +export async function suppressDuplicate( + libraryId: string, + signature: string, + signatureType: 'filename' | 'filehash' +): Promise { + return apiPost('/api/library/duplicates', { + libraryId, + signature, + signatureType, + action: 'suppress' + }); +} + +export async function unsuppressDuplicate( + libraryId: string, + signature: string, + signatureType: 'filename' | 'filehash' +): Promise { + return apiPost('/api/library/duplicates', { + libraryId, + signature, + signatureType, + action: 'unsuppress' + }); +} diff --git a/src/lib/api/history-retention.ts b/src/lib/api/history-retention.ts new file mode 100644 index 000000000..13d791418 --- /dev/null +++ b/src/lib/api/history-retention.ts @@ -0,0 +1,28 @@ +import { apiGet, apiPut } from './client.js'; + +export interface HistoryRetentionSettings { + fileHistoryDays: number; + libraryHistoryDays: number; + scanHistoryDays: number; +} + +export interface StorageForecast { + currentEstimatedBytes: number; + averageDailyBytes: number; + projectedBytes30d: number; + projectedBytes90d: number; +} + +export async function getHistoryRetention(): Promise { + return apiGet('/api/settings/library/history-retention'); +} + +export async function saveHistoryRetention( + input: HistoryRetentionSettings +): Promise<{ success: boolean }> { + return apiPut<{ success: boolean }>('/api/settings/library/history-retention', input); +} + +export async function getStorageForecast(): Promise { + return apiGet('/api/settings/library/history-retention/forecast'); +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 58f5d8774..8d9cb940b 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -17,6 +17,7 @@ export * from './tasks.js'; export * from './subtitles.js'; export * from './smartlists.js'; export * from './indexers.js'; +export * from './cinephage.js'; export * from './monitoring.js'; export * from './livetv.js'; export * from './calendar.js'; diff --git a/src/lib/api/library-settings.ts b/src/lib/api/library-settings.ts new file mode 100644 index 000000000..057f4aed8 --- /dev/null +++ b/src/lib/api/library-settings.ts @@ -0,0 +1,24 @@ +/** + * Library Settings API client + * + * Typed client for library-level settings that are not per-library-entity + * (scan scheduler config, watcher, auto-match). Per-library-entity settings + * go through $lib/api/settings.ts (libraries CRUD). + */ + +import { apiGet, apiPut } from './client.js'; + +export interface ScanSettings { + scanIntervalHours: number; + watchEnabled: boolean; + autoMatchThreshold: number; + scanOnStartup: boolean; +} + +export async function getScanSettings(): Promise { + return apiGet('/api/settings/library/scan-settings'); +} + +export async function saveScanSettings(input: ScanSettings): Promise<{ success: boolean }> { + return apiPut<{ success: boolean }>('/api/settings/library/scan-settings', input); +} diff --git a/src/lib/api/pattern-config.ts b/src/lib/api/pattern-config.ts new file mode 100644 index 000000000..92fac69a4 --- /dev/null +++ b/src/lib/api/pattern-config.ts @@ -0,0 +1,36 @@ +/** + * Pattern Config API client + */ + +import { apiGet, apiPut } from './client.js'; + +export interface PatternConfigRow { + id: string; + libraryId: string | null; + scope: string; + ignoreDefaultsEnabled: boolean | null; + ignoreUserPatterns: string[] | null; + bonusPatterns: string[] | null; + structureMode: string | null; + structureConfig: Record | null; + createdAt: string | null; + updatedAt: string | null; +} + +export interface PatternConfigUpdate { + libraryId?: string; + ignoreDefaultsEnabled?: boolean; + ignoreUserPatterns?: string[]; + bonusPatterns?: string[]; + structureMode?: 'none' | 'folder_depth' | 'regex' | null; + structureConfig?: Record | null; +} + +export async function getPatternConfig(libraryId?: string): Promise { + const params = libraryId ? { libraryId } : undefined; + return apiGet('/api/settings/library/pattern-config', params); +} + +export async function savePatternConfig(input: PatternConfigUpdate): Promise { + return apiPut('/api/settings/library/pattern-config', input); +} diff --git a/src/lib/api/resolution-categories.ts b/src/lib/api/resolution-categories.ts new file mode 100644 index 000000000..050ea347c --- /dev/null +++ b/src/lib/api/resolution-categories.ts @@ -0,0 +1,35 @@ +import { apiGet, apiPost, apiPut, apiDelete } from './client.js'; + +export interface ResolutionCategory { + id: string; + label: string; + minWidth: number; + minHeight: number; + searchTerms: string[] | null; + isFallback: boolean | null; + createdAt: string | null; +} + +export async function getResolutionCategories(): Promise { + return apiGet('/api/settings/library/resolution-categories'); +} + +export async function createResolutionCategory(input: { + label: string; + minWidth?: number; + minHeight?: number; + searchTerms?: string[]; +}): Promise { + return apiPost('/api/settings/library/resolution-categories', input); +} + +export async function updateResolutionCategory( + id: string, + input: { label?: string; minWidth?: number; minHeight?: number; searchTerms?: string[] } +): Promise { + return apiPut(`/api/settings/library/resolution-categories/${id}`, input); +} + +export async function deleteResolutionCategory(id: string): Promise { + return apiDelete(`/api/settings/library/resolution-categories/${id}`); +} diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 5018979b9..1d736565a 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -19,7 +19,8 @@ import type { NamingPresetUpdate, NamingPreview, LibraryClassificationUpdate, - BackupImport + BackupImport, + FileManagementSettings } from '$lib/validation/schemas.js'; import { apiGet, apiPost, apiPut, apiDelete } from './client.js'; @@ -132,6 +133,26 @@ export async function executeRename(fileIds: string[], mediaType?: string) { return apiPost('/api/rename/execute', { fileIds, mediaType }); } +export async function reorganizeFolder(mediaId: string, mediaType: 'movie' | 'series') { + return apiPost('/api/rename/reorganize', { mediaId, mediaType }); +} + +export async function reorganizeFolderBatch( + items: Array<{ mediaId: string; mediaType: 'movie' | 'series' }> +) { + return apiPost<{ + success: boolean; + organized: number; + failed: number; + results: Array<{ + mediaId: string; + mediaType: 'movie' | 'series'; + success: boolean; + error?: string; + }>; + }>('/api/rename/reorganize-batch', { items }); +} + export async function getScoringProfiles() { return apiGet('/api/scoring-profiles'); } @@ -458,3 +479,11 @@ export async function seedBlockedKeywords(): Promise<{ added: number }> { added: number; }>; } + +export async function getFileManagementSettings() { + return apiGet('/api/settings/file-management'); +} + +export async function updateFileManagementSettings(payload: FileManagementSettings) { + return apiPut('/api/settings/file-management', payload); +} diff --git a/src/lib/api/storage.ts b/src/lib/api/storage.ts new file mode 100644 index 000000000..729ca1af2 --- /dev/null +++ b/src/lib/api/storage.ts @@ -0,0 +1,31 @@ +import { apiGet, type ApiResponse } from './client.js'; + +export interface InsightItem { + id: string; + kind: 'movie' | 'series' | 'episode' | 'file' | 'folder'; + title: string; + subtitle?: string; + sizeBytes?: number; + badges?: Array<{ label: string; tone: 'info' | 'warn' | 'critical' }>; + href?: string; +} + +export interface InsightItemsResponse { + items: InsightItem[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +export async function getInsightItems( + insightId: string, + params?: { page?: number; limit?: number } +): Promise> { + return apiGet(`/api/storage/insights/${insightId}/items`, { + page: String(params?.page ?? 1), + limit: String(params?.limit ?? 50) + }); +} diff --git a/src/lib/components/activity/ActivityDetailModal.svelte b/src/lib/components/activity/ActivityDetailModal.svelte index 09f9a2db6..e6f05d5b5 100644 --- a/src/lib/components/activity/ActivityDetailModal.svelte +++ b/src/lib/components/activity/ActivityDetailModal.svelte @@ -12,6 +12,7 @@ import { statusConfig, getStatusLabel, + supportsQueuePauseResume, formatRelativeTime, getResolutionBadge } from './activity-display-utils.js'; @@ -270,12 +271,12 @@ {#if activity.queueItemId}
- {#if activity.status === 'downloading' || activity.status === 'seeding'} + {#if supportsQueuePauseResume(activity) && (activity.status === 'downloading' || activity.status === 'seeding')} - {:else if activity.status === 'paused'} + {:else if supportsQueuePauseResume(activity) && activity.status === 'paused'} - {:else if activity.status === 'paused'} + {:else if supportsQueuePauseResume(activity) && activity.status === 'paused'} +
+ +{#if delayProfiles.length === 0} +
+ +

{m.settings_quality_delay_empty()}

+

+ {m.settings_quality_delay_emptyHint()} +

+ +
+{:else} +
+ {#each delayProfiles as profile (profile.id)} +
+
+
+ +
+
+
+ {profile.name} + {#if !profile.enabled} + + {m.settings_quality_delay_badgeDisabled()} + + {/if} +
+
+ {m.settings_quality_delay_torrentPrefix()} + {formatDelay(profile.torrentDelay)} + {m.settings_quality_delay_usenetPrefix()} {formatDelay(profile.usenetDelay)} + {#if profile.preferredProtocol} + + {m.settings_quality_delay_preferredPrefix()} + {profile.preferredProtocol} + + {/if} + {#if profile.bypassIfHighestQuality} + {m.settings_quality_delay_bypass4k()} + {/if} + {#if profile.bypassIfAboveScore != null} + + {m.settings_quality_delay_bypassScorePrefix()} + {profile.bypassIfAboveScore} + + {/if} +
+
+
+
+ + +
+
+ {/each} +
+{/if} + + + (dpModalOpen = false)}> + (dpModalOpen = false)} + /> +
+
+ + +
+
+ +
+
+
+ + + {formatDelay(dpTorrentDelay)} +
+
+ + + {formatDelay(dpUsenetDelay)} +
+
+
+ + +
+
+ +
+
+ + { + const v = (e.currentTarget as HTMLInputElement).value; + dpBypassIfAboveScore = v === '' ? null : parseInt(v, 10); + }} + placeholder={m.settings_quality_delay_field_bypassScorePlaceholder()} + /> +
+
+ (dpModalOpen = false)} + onSave={saveDelay} + saving={dpSaving} + saveDisabled={!dpName.trim()} + saveLabel={dpModalMode === 'add' + ? m.settings_quality_delay_createButton() + : m.settings_general_saveLibrary()} + /> +
+ + + (dpDeleteOpen = false)} +/> diff --git a/src/lib/components/delayProfiles/index.ts b/src/lib/components/delayProfiles/index.ts new file mode 100644 index 000000000..d5cd86137 --- /dev/null +++ b/src/lib/components/delayProfiles/index.ts @@ -0,0 +1,2 @@ +// DelayProfile components barrel export +export { default as DelayProfileList } from './DelayProfileList.svelte'; diff --git a/src/lib/components/downloadClients/ClientSpecificOptions.svelte b/src/lib/components/downloadClients/ClientSpecificOptions.svelte index f3565cd72..7b10c44fb 100644 --- a/src/lib/components/downloadClients/ClientSpecificOptions.svelte +++ b/src/lib/components/downloadClients/ClientSpecificOptions.svelte @@ -19,6 +19,7 @@ tempPathRemote: string; stalledTimeout: number; stalledThreshold: number; + stalledBlocklist: number; saveStalledBehaviorSuccess: boolean; isNntpServer: boolean; selectedDefinition: DownloadClientDefinition | null; @@ -41,6 +42,7 @@ tempPathRemote = $bindable(), stalledTimeout = $bindable(), stalledThreshold = $bindable(), + stalledBlocklist = $bindable(), saveStalledBehaviorSuccess = $bindable(), isNntpServer, selectedDefinition = null, @@ -82,9 +84,11 @@ >
-
-
diff --git a/src/lib/components/downloadClients/DownloadClientModal.svelte b/src/lib/components/downloadClients/DownloadClientModal.svelte index 986897639..cad155ea6 100644 --- a/src/lib/components/downloadClients/DownloadClientModal.svelte +++ b/src/lib/components/downloadClients/DownloadClientModal.svelte @@ -54,7 +54,12 @@ ) => Promise; stalledTimeoutMinutes: number; stalledProgressThreshold: number; - onSaveStalledBehavior: (timeout: number, threshold: number) => Promise; + stalledBlocklistHours: number; + onSaveStalledBehavior: ( + timeout: number, + threshold: number, + blocklistHours: number + ) => Promise; } let { @@ -71,6 +76,7 @@ onTest, stalledTimeoutMinutes, stalledProgressThreshold, + stalledBlocklistHours, onSaveStalledBehavior }: Props = $props(); @@ -86,6 +92,8 @@ let mountMode = $state<'nzbdav' | 'altmount' | ''>(''); let username = $state(''); let password = $state(''); + let apiToken = $state(''); + let removeAfterImport = $state(false); let movieCategory = $state('movies'); let tvCategory = $state('tv'); @@ -105,6 +113,7 @@ let stalledTimeout = $state(0); let stalledThreshold = $state(0); + let stalledBlocklist = $state(72); let saveStalledBehaviorSuccess = $state(false); let testing = $state(false); @@ -127,6 +136,8 @@ selectedDefinition?.protocol === 'usenet' && selectedDefinition?.id === 'sabnzbd' ); const isNntpServer = $derived(implementation === 'nntp'); + const isDebrid = $derived(selectedDefinition?.isDebrid === true); + const hasApiToken = $derived((client as DownloadClient | undefined)?.hasApiToken ?? false); const isSabnzbd = $derived(implementation === 'sabnzbd'); const isMountModeClient = $derived( isSabnzbd && (mountMode === 'nzbdav' || mountMode === 'altmount') @@ -160,6 +171,7 @@ if (open) { stalledTimeout = stalledTimeoutMinutes; stalledThreshold = stalledProgressThreshold; + stalledBlocklist = stalledBlocklistHours; saveStalledBehaviorSuccess = false; } }); @@ -168,12 +180,18 @@ if (!open) return; const timeout = stalledTimeout; const threshold = stalledThreshold; + const blocklistHours = stalledBlocklist; - if (timeout === stalledTimeoutMinutes && threshold === stalledProgressThreshold) return; + if ( + timeout === stalledTimeoutMinutes && + threshold === stalledProgressThreshold && + blocklistHours === stalledBlocklistHours + ) + return; const timer = setTimeout(async () => { try { - await onSaveStalledBehavior(timeout, threshold); + await onSaveStalledBehavior(timeout, threshold, blocklistHours); saveStalledBehaviorSuccess = true; setTimeout(() => (saveStalledBehaviorSuccess = false), 2000); } catch { @@ -204,6 +222,7 @@ mountMode = storedMountMode === 'altmount' || storedMountMode === 'nzbdav' ? 'nzbdav' : ''; username = client?.username ?? ''; password = ''; + apiToken = ''; const dcClient = client as DownloadClient | undefined; movieCategory = dcClient?.movieCategory ?? 'movies'; @@ -211,6 +230,7 @@ recentPriority = dcClient?.recentPriority ?? 'normal'; olderPriority = dcClient?.olderPriority ?? 'normal'; initialState = dcClient?.initialState ?? 'start'; + removeAfterImport = dcClient?.removeAfterImport ?? false; downloadPathLocal = dcClient?.downloadPathLocal ?? ''; downloadPathRemote = dcClient?.downloadPathRemote ?? ''; tempPathLocal = dcClient?.tempPathLocal ?? ''; @@ -280,7 +300,9 @@ tempPathRemote, maxConnections, priority, - implementation: implementation as DownloadClientImplementation + implementation: implementation as DownloadClientImplementation, + apiToken, + removeAfterImport }; return serializeDownloadClientForm(formState, isNntpServer, mode); } @@ -310,8 +332,11 @@ implementation && name.trim() && name.trim().length <= MAX_NAME_LENGTH && - host.trim() && - isValidPort(port) + (isDebrid + ? mode === 'edit' + ? hasApiToken || !!apiToken.trim() + : !!apiToken.trim() + : host.trim() && isValidPort(port)) ); } @@ -354,7 +379,7 @@ showFolderBrowser = true; } - function getProtocolLabel(protocol: 'torrent' | 'usenet' | 'nntp'): string { + function getProtocolLabel(protocol: 'torrent' | 'usenet' | 'nntp' | 'debrid'): string { switch (protocol) { case 'torrent': return m.downloadClient_protocol_torrent(); @@ -362,6 +387,8 @@ return m.downloadClient_protocol_usenet(); case 'nntp': return m.downloadClient_protocol_nntp(); + case 'debrid': + return m.downloadClient_protocol_debrid(); } } @@ -429,10 +456,10 @@
{selectedDefinition.name}
-
- {m.common_port()} - {selectedDefinition.defaultPort} -
+ {#if !selectedDefinition.isDebrid}
+ {m.common_port()} + {selectedDefinition.defaultPort} +
{/if}
{:else} -
+ {#if isDebrid}
- +
+ + +
+
+ + +
+
+ + +
+ +
+ {:else}
+
+ +
-
- -
-
+
+ +
+
{/if} - {#if !isNntpServer} + {#if !isNntpServer && !isDebrid}
0 && clients.every((c) => selectedIds.has(c.id))); const someSelected = $derived(clients.some((c) => selectedIds.has(c.id)) && !allSelected); @@ -162,20 +173,20 @@ {getProtocolLabel(client.implementation)} - {m.common_movies()}: {client.movieCategory ?? '-'} - {m.common_tvShows()}: {client.tvCategory ?? '-'} + {#if !isDebrid(client)}{m.common_movies()}: {client.movieCategory ?? '-'} + {m.common_tvShows()}: {client.tvCategory ?? '-'}{/if}
-
- {getClientUrl(client)} -
+ {#if !isDebrid(client)}
+ {getClientUrl(client)} +
{/if}
{#if onTest} @@ -321,17 +332,19 @@ > -
{getClientUrl(client)}
+ {#if !isDebrid(client)}
+ {getClientUrl(client)} +
{/if} -
- {m.common_movies()}: {client.movieCategory ?? '-'} - {m.common_tvShows()}: {client.tvCategory ?? '-'} -
+ {#if !isDebrid(client)}
+ {m.common_movies()}: {client.movieCategory ?? '-'} + {m.common_tvShows()}: {client.tvCategory ?? '-'} +
{/if}
diff --git a/src/lib/components/downloadClients/debrid-settings.test.ts b/src/lib/components/downloadClients/debrid-settings.test.ts new file mode 100644 index 000000000..80c7a3c8c --- /dev/null +++ b/src/lib/components/downloadClients/debrid-settings.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from 'vitest'; +import { clientDefinitions } from './forms/clientDefinitions'; +import { serializeDownloadClientForm, type DownloadClientFormState } from './formSerializer'; + +function formState(overrides: Record = {}): DownloadClientFormState { + return { + name: ' qBit ', + enabled: true, + host: ' localhost ', + port: 8080, + useSsl: false, + urlBase: '/qb/', + urlBaseEnabled: true, + mountMode: '', + username: ' admin ', + password: 'password', + movieCategory: 'movies', + tvCategory: 'tv', + recentPriority: 'high', + olderPriority: 'normal', + initialState: 'start', + downloadPathLocal: '/downloads', + downloadPathRemote: '/remote', + tempPathLocal: '/incomplete', + tempPathRemote: '/remote-incomplete', + maxConnections: 10, + priority: 2, + implementation: 'qbittorrent', + ...overrides + } as DownloadClientFormState; +} + +describe('debrid download-client definitions and serialization', () => { + it.each([ + ['realdebrid', 'Real-Debrid'], + ['torbox', 'TorBox'] + ] as const)('%s is a first-class debrid picker definition', (id, name) => { + const definition = clientDefinitions.find((candidate) => candidate.id === id); + + expect(definition).toEqual( + expect.objectContaining({ + id, + name, + protocol: 'debrid', + isDebrid: true, + supportsCategories: false, + supportsSeedingLimits: false + }) + ); + }); + + it('serializes a debrid form using only the approved fields', () => { + const payload = serializeDownloadClientForm( + formState({ + name: ' Real-Debrid ', + implementation: 'realdebrid', + apiToken: ' rd-secret ', + removeAfterImport: true + } as Record), + false, + 'add' + ); + + expect(payload).toEqual({ + name: 'Real-Debrid', + implementation: 'realdebrid', + enabled: true, + priority: 2, + apiToken: 'rd-secret', + removeAfterImport: true + }); + expect(JSON.stringify(payload)).not.toContain('localhost'); + }); + + it.each(['', ' ', '********'])( + 'omits a blank/redacted token on debrid edit: %j', + (apiToken) => { + const payload = serializeDownloadClientForm( + formState({ implementation: 'torbox', apiToken, removeAfterImport: false }), + false, + 'edit' + ) as unknown as Record; + + expect(payload).toEqual({ + name: 'qBit', + implementation: 'torbox', + enabled: true, + priority: 2, + removeAfterImport: false + }); + expect(payload).not.toHaveProperty('apiToken'); + } + ); + + it('keeps legacy torrent serialization unchanged', () => { + expect(serializeDownloadClientForm(formState(), false, 'add')).toEqual({ + name: 'qBit', + implementation: 'qbittorrent', + enabled: true, + host: 'localhost', + port: 8080, + useSsl: false, + urlBase: 'qb', + mountMode: null, + username: 'admin', + password: 'password', + movieCategory: 'movies', + tvCategory: 'tv', + recentPriority: 'high', + olderPriority: 'normal', + initialState: 'start', + seedRatioLimit: null, + seedTimeLimit: null, + downloadPathLocal: '/downloads', + downloadPathRemote: '/remote', + tempPathLocal: '/incomplete', + tempPathRemote: '/remote-incomplete', + priority: 2 + }); + }); +}); diff --git a/src/lib/components/downloadClients/formSerializer.ts b/src/lib/components/downloadClients/formSerializer.ts index 0d53dd056..85e5c221b 100644 --- a/src/lib/components/downloadClients/formSerializer.ts +++ b/src/lib/components/downloadClients/formSerializer.ts @@ -42,6 +42,8 @@ export interface DownloadClientFormState { maxConnections: number; priority: number; implementation: DownloadClientImplementation; + apiToken?: string; + removeAfterImport?: boolean; } export function serializeDownloadClientForm( @@ -72,6 +74,20 @@ export function serializeDownloadClientForm( return data; } + if (formState.implementation === 'realdebrid' || formState.implementation === 'torbox') { + const data: DownloadClientFormData = { + name: normalizedName, + implementation: formState.implementation, + enabled: formState.enabled, + priority: formState.priority, + removeAfterImport: formState.removeAfterImport ?? false + }; + const apiToken = formState.apiToken?.trim(); + if (!(mode === 'edit' && (isBlankOrRedacted(apiToken) || apiToken === '********')) && apiToken) + data.apiToken = apiToken; + return data; + } + const data: DownloadClientFormData = { name: normalizedName, implementation: formState.implementation, diff --git a/src/lib/components/downloadClients/forms/clientDefinitions.ts b/src/lib/components/downloadClients/forms/clientDefinitions.ts index d502d2166..c149834b2 100644 --- a/src/lib/components/downloadClients/forms/clientDefinitions.ts +++ b/src/lib/components/downloadClients/forms/clientDefinitions.ts @@ -86,5 +86,28 @@ export const clientDefinitions: DownloadClientDefinition[] = [ supportsCategories: false, supportsPriority: false, supportsSeedingLimits: false + }, + // Debrid providers + { + id: 'realdebrid', + name: 'Real-Debrid', + description: 'Real-Debrid torrent acquisition provider', + defaultPort: 443, + protocol: 'debrid', + supportsCategories: false, + supportsPriority: false, + supportsSeedingLimits: false, + isDebrid: true + }, + { + id: 'torbox', + name: 'TorBox', + description: 'TorBox torrent acquisition provider', + defaultPort: 443, + protocol: 'debrid', + supportsCategories: false, + supportsPriority: false, + supportsSeedingLimits: false, + isDebrid: true } ]; diff --git a/src/lib/components/indexers/IndexerDefinitionPicker.svelte b/src/lib/components/indexers/IndexerDefinitionPicker.svelte index 106d98b1f..f69ffe3de 100644 --- a/src/lib/components/indexers/IndexerDefinitionPicker.svelte +++ b/src/lib/components/indexers/IndexerDefinitionPicker.svelte @@ -14,9 +14,10 @@ let searchQuery = $state(''); const filteredDefinitions = $derived(() => { - if (!searchQuery.trim()) return definitions; + const pickable = definitions.filter((d) => d.id !== 'prowlarr' && d.id !== 'jackett'); + if (!searchQuery.trim()) return pickable; const query = searchQuery.toLowerCase(); - return definitions.filter( + return pickable.filter( (d) => d.name.toLowerCase().includes(query) || d.description?.toLowerCase().includes(query) || @@ -170,7 +171,9 @@

- {m.settings_indexers_indexersAvailable({ count: definitions.length })} + {m.settings_indexers_indexersAvailable({ + count: definitions.filter((d) => d.id !== 'prowlarr' && d.id !== 'jackett').length + })}

diff --git a/src/lib/components/indexers/IndexerFormRegular.svelte b/src/lib/components/indexers/IndexerFormRegular.svelte index 4833bc423..cc26e2595 100644 --- a/src/lib/components/indexers/IndexerFormRegular.svelte +++ b/src/lib/components/indexers/IndexerFormRegular.svelte @@ -33,6 +33,8 @@ alternateUrls: string[]; prowlarrManaged?: boolean; jackettManaged?: boolean; + /** Override URL shown in the read-only managed display (e.g. virtual path for Jackett). */ + managedDisplayUrl?: string; onNameChange: (value: string) => void; onUrlChange: (value: string) => void; onUrlBlur: () => void; @@ -78,6 +80,7 @@ alternateUrls, prowlarrManaged = false, jackettManaged = false, + managedDisplayUrl = undefined, onNameChange, onUrlChange, onUrlBlur, @@ -106,8 +109,13 @@ let usenetSettingsOpen = $state(false); let categoriesOpen = $state(false); - // Only show the category restriction panel for newznab/torznab definitions - const isNewznabLike = $derived(definition?.id === 'newznab' || definition?.id === 'torznab'); + // Show the category restriction panel for newznab/torznab/prowlarr/jackett definitions + const isNewznabLike = $derived( + definition?.id === 'newznab' || + definition?.id === 'torznab' || + definition?.id === 'prowlarr' || + definition?.id === 'jackett' + ); // Build a parent→children tree from the YAML-defined category map. // Parents have IDs where id % 1000 === 0 (2000, 5000, 6000, 8000). @@ -308,7 +316,9 @@ {/if} {#if prowlarrManaged || jackettManaged} -

{url}

+

+ {managedDisplayUrl ?? url} +

{:else if definitionUrls.length > 1} { + syncAddNew = (e.currentTarget as HTMLInputElement).checked; + scheduleSettingsSave(); + }} + /> +
+ {/if} + +
Search mode
+ +
-

Import new indexers during sync

+

Use aggregate endpoint

- Automatically add Prowlarr indexers you haven't imported yet + Search all Prowlarr indexers through a single endpoint instead of managing them + individually

{ - syncAddNew = (e.currentTarget as HTMLInputElement).checked; - scheduleSettingsSave(); + const checked = (e.currentTarget as HTMLInputElement).checked; + if (checked) { + confirmingAggregateEnable = true; + } else { + handleAggregateDisable(); + } }} />
+ {#if useAggregateEndpoint && prowlarrIndexerCount !== null} +
+ Searching across {prowlarrIndexerCount} indexer{prowlarrIndexerCount !== 1 ? 's' : ''} in Prowlarr +
+ {/if} + + {#if confirmingAggregateEnable} +
+

Switch to aggregate mode?

+

+ All individually imported Prowlarr indexers will be removed from Cinephage and replaced + with a single aggregate indexer. They can be re-imported individually if you switch + back. +

+
+ + +
+
+ {/if} + + {#if aggregateError} +
{aggregateError}
+ {/if} + {#if connectError}
{connectError}
{/if} @@ -635,20 +785,27 @@ {/if}
- + +
- - {/if} - - - {#if libRootFolders.length > 0 && !collapsed[libItem.id]} -
- {#each libRootFolders as rf (rf!.id)} - - {/each} -
- {/if} - - {/each} - - {#if unassignedRootFolders.length > 0} -
-
- Unassigned Root Folders -
-
- {#each unassignedRootFolders as rf (rf.id)} - - {/each} -
-
- {/if} - diff --git a/src/lib/components/libraries/LibraryBreakdownTable.svelte b/src/lib/components/libraries/LibraryBreakdownTable.svelte deleted file mode 100644 index 04ba154fe..000000000 --- a/src/lib/components/libraries/LibraryBreakdownTable.svelte +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/src/lib/components/libraries/LibraryEditModal.svelte b/src/lib/components/libraries/LibraryEditModal.svelte new file mode 100644 index 000000000..c7165b4b5 --- /dev/null +++ b/src/lib/components/libraries/LibraryEditModal.svelte @@ -0,0 +1,280 @@ + + + + +
+ {#if librarySaveError} +
+ + {librarySaveError} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ + {m.settings_general_rootFoldersLabel()} + + + {m.settings_general_selectedCount({ count: selectedLibraryRootFolderCount })} + +
+ +
+ {#if filteredLibraryRootFolders.length === 0} +
+ +
+
+ {m.settings_general_noMatchingRootFolders()} +
+
+
+ {:else} + {#each filteredLibraryRootFolders as folder (folder.id)} + + {/each} + {/if} +
+
+
+
+ +
+ + +
+
+ +
diff --git a/src/lib/components/libraries/LibraryList.svelte b/src/lib/components/libraries/LibraryList.svelte index 268c45d15..5ef40dc14 100644 --- a/src/lib/components/libraries/LibraryList.svelte +++ b/src/lib/components/libraries/LibraryList.svelte @@ -5,14 +5,12 @@ Tv, Settings, Trash2, - Eye, - EyeOff, Search, SearchSlash, Captions, CaptionsOff } from 'lucide-svelte'; - import type { LayoutData } from '../../../routes/settings/general/$types'; + import type { PageData } from '../../../routes/settings/library/libraries/$types'; type LibraryRootFolder = { id: string; @@ -20,7 +18,7 @@ path?: string; }; - type LibraryEntity = NonNullable[number] & { + type LibraryEntity = NonNullable[number] & { rootFolders?: LibraryRootFolder[]; }; @@ -175,26 +173,7 @@ -
-
- - {#if library.defaultMonitored} - - {:else} - - {/if} - -
+
- import * as m from '$lib/paraglide/messages.js'; - import { FolderOpen } from 'lucide-svelte'; - import type { RootFolderBreakdownItem } from './storage-utils.js'; - import { - formatTimestamp, - getRootFolderTotalBytes, - getUsedRatio, - getRootFolderScanLabel, - getRootFolderScanBadgeClass - } from './storage-utils.js'; - import DiskUsageBar from './DiskUsageBar.svelte'; - - interface Props { - item: RootFolderBreakdownItem; - scanning: boolean; - formatBytes: (value: number) => string; - hasRootFolder: boolean; - onEditRootFolder: (rootFolderId: string) => void; - onScanRootFolder: (rootFolderId: string) => void; - } - - let { item, scanning, formatBytes, hasRootFolder, onEditRootFolder, onScanRootFolder }: Props = - $props(); - - - - -
-
- - {item.name} - {#if item.accessible === false} - - {m.settings_general_inaccessible()} - - {/if} - {#if item.readOnly} - - {m.rootFolders_badgeReadOnly()} - - {/if} - {#if (item.unmatchedCount ?? 0) > 0} - - {m.settings_general_unmatchedCount({ count: item.unmatchedCount ?? 0 })} - - {/if} -
-
{item.path}
-
- {m.settings_general_lastScanLabel({ - value: formatTimestamp(item.lastScannedAt ?? null) - })} -
- {#if (item.accessible === false || item.needsScan) && hasRootFolder} -
- - {#if item.needsScan} - - {/if} -
- {/if} -
- - {item.mediaType} / {item.mediaSubType} - {item.itemCount} - -
{m.settings_general_trackedUsed({ used: formatBytes(item.usedBytes) })}
-
- {#if getRootFolderTotalBytes(item)} - {m.settings_general_diskFreeOfTotal({ - free: item.freeSpaceFormatted ?? m.common_na(), - total: formatBytes(getRootFolderTotalBytes(item) ?? 0) - })} - {:else} - {m.settings_general_capacityUnknown()} - {/if} -
- - - {#if getUsedRatio(item) !== null} -
- -
- {:else} - - {/if} - - - - {getRootFolderScanLabel(item)} - - - diff --git a/src/lib/components/libraries/RootFolderMobileCard.svelte b/src/lib/components/libraries/RootFolderMobileCard.svelte deleted file mode 100644 index f5cc0c1d8..000000000 --- a/src/lib/components/libraries/RootFolderMobileCard.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - -
-
-
- - {item.name} -
-
- {#if item.accessible === false} - - {m.settings_general_inaccessible()} - - {/if} - {#if item.readOnly} - - {m.rootFolders_badgeReadOnly()} - - {/if} -
-
-
{item.path}
-
-
-
{m.settings_general_columnItems()}
-
{item.itemCount}
-
-
-
{m.settings_general_columnUsed()}
-
{formatBytes(item.usedBytes)}
-
-
-
{m.settings_general_diskFree()}
-
{item.freeSpaceFormatted ?? 'N/A'}
-
-
-
- -
-
- - {getRootFolderScanLabel(item)} - - {#if (item.accessible === false || item.needsScan) && hasRootFolder} -
- - {#if item.needsScan} - - {/if} -
- {/if} -
-
diff --git a/src/lib/components/libraries/StorageMaintenanceSection.svelte b/src/lib/components/libraries/StorageMaintenanceSection.svelte deleted file mode 100644 index 4f9f24330..000000000 --- a/src/lib/components/libraries/StorageMaintenanceSection.svelte +++ /dev/null @@ -1,350 +0,0 @@ - - -{#if attentionItems.length > 0} -
-
- -

{m.settings_general_needsAttention()}

-
-
- {#each attentionItems as item (item.label)} - {#if item.href} - - {item.label} - - {:else} - - {item.label} - - {/if} - {/each} -
-
-{/if} - -{#if rootFolderCount === 0} -
- - {m.settings_general_addFolderFirst()} -
-{:else} -
-
-
-
{formatBytes(storage.totalUsedBytes)}
-
- {storage.movieCount} movies - · - {storage.seriesCount} series - · - {storage.subtitleCount} subtitles -
-
-
- {#if storage.totalUsedBytes > 0} -
-
-
-
-
- {/if} -
- - - Movies ({formatBytes(storage.moviesUsedBytes)}) - - - - TV ({formatBytes(storage.tvUsedBytes)}) - - - - Subtitles ({formatBytes(storage.subtitlesUsedBytes)}) - -
-
-
-
- -{/if} - -{#if scanError} -
- - {scanError} -
-{/if} - -{#if scanSuccess} -
- -
- {scanSuccess.message} - {#if scanSuccess.unmatchedCount > 0} - - {m.settings_general_viewUnmatchedFiles({ count: scanSuccess.unmatchedCount })} - - - {/if} -
-
-{/if} - -{#if scanning && scanProgress} -
-
- - {scanProgress.phase === 'scanning' ? m.settings_general_discoveringFiles() : ''} - {scanProgress.phase === 'processing' ? m.settings_general_processing() : ''} - {scanProgress.phase === 'matching' ? m.settings_general_matchingFiles() : ''} - {scanProgress.rootFolderPath ?? ''} - - - {scanProgress.filesProcessed} / {scanProgress.filesFound} - {m.common_files()} - -
- -
- {m.settings_general_scanAdded()}: {scanProgress.filesAdded} - {m.settings_general_scanUpdated()}: {scanProgress.filesUpdated} - {m.settings_general_scanRemoved()}: {scanProgress.filesRemoved} - {m.settings_general_scanUnmatched()}: {scanProgress.unmatchedCount} -
- {#if scanProgress.currentFile} -
- {scanProgress.currentFile} -
- {/if} -
-{/if} - -
-
- - {#if storage.health.lastScan} - - {m.settings_general_lastScan()}: - {formatTimestamp( - storage.health.lastScan.completedAt ?? storage.health.lastScan.startedAt - )} - {#if storage.health.lastScan.durationMs} - ({formatDuration(storage.health.lastScan.durationMs)}) - {/if} - - - {:else} - {m.settings_general_noScanHistory()} - {/if} -
- {#if serverStatuses.length > 0} -
- {#each serverStatuses as server (server.serverId)} - - {getServerTypeIcon(server.serverType)} - {server.serverName} - - {server.lastSyncStatus ?? 'pending'} - - - {/each} -
- {:else} -
- - No media servers -
- {/if} -
- -
-
- -

Libraries & Storage

-
- - - - -
diff --git a/src/lib/components/libraries/index.ts b/src/lib/components/libraries/index.ts index ef30ea8e5..c113dbbdd 100644 --- a/src/lib/components/libraries/index.ts +++ b/src/lib/components/libraries/index.ts @@ -1,7 +1,2 @@ export { default as LibraryList } from './LibraryList.svelte'; -export { default as StorageMaintenanceSection } from './StorageMaintenanceSection.svelte'; -export { default as DiskUsageBar } from './DiskUsageBar.svelte'; -export { default as RootFolderDesktopRow } from './RootFolderDesktopRow.svelte'; -export { default as RootFolderMobileCard } from './RootFolderMobileCard.svelte'; -export { default as LibraryBreakdownTable } from './LibraryBreakdownTable.svelte'; -export { default as LibraryBreakdownMobileList } from './LibraryBreakdownMobileList.svelte'; +export { default as LibraryEditModal } from './LibraryEditModal.svelte'; diff --git a/src/lib/components/library/AddMovieForm.svelte b/src/lib/components/library/AddMovieForm.svelte index 3a4b45f5d..fe0bf8882 100644 --- a/src/lib/components/library/AddMovieForm.svelte +++ b/src/lib/components/library/AddMovieForm.svelte @@ -2,6 +2,7 @@ import CommonOptions from './add/CommonOptions.svelte'; import MovieAddOptions, { type MinimumAvailability } from './add/MovieAddOptions.svelte'; import type { RootFolderWithSpaceAndDefault as RootFolder } from '$lib/types/downloadClient.js'; + import type { DesiredQuality } from '$lib/types/library.js'; interface ScoringProfile { id: string; @@ -9,6 +10,8 @@ description?: string; isBuiltIn: boolean; isDefault?: boolean; + minResolution?: string | null; + maxResolution?: string | null; } interface CollectionPart { @@ -39,6 +42,7 @@ minimumAvailability: MinimumAvailability; availabilityDelay: number; monitored: boolean; + desiredQualities: DesiredQuality[]; collection: CollectionInfo | null; addEntireCollection: boolean; error: string | null; @@ -63,6 +67,7 @@ minimumAvailability = $bindable(), availabilityDelay = $bindable(), monitored = $bindable(), + desiredQualities = $bindable(), collection, addEntireCollection = $bindable(), error, @@ -72,6 +77,8 @@ onWantsSubtitlesInput, onMonitoredInput }: Props = $props(); + + const selectedProfileObj = $derived(scoringProfiles.find((p) => p.id === selectedScoringProfile));
@@ -131,6 +138,9 @@ bind:minimumAvailability bind:availabilityDelay bind:monitored + bind:desiredQualities + minResolution={selectedProfileObj?.minResolution} + maxResolution={selectedProfileObj?.maxResolution} {collection} bind:addEntireCollection /> diff --git a/src/lib/components/library/AddToLibraryModal.svelte b/src/lib/components/library/AddToLibraryModal.svelte index 7416995b1..e0a1ee6b7 100644 --- a/src/lib/components/library/AddToLibraryModal.svelte +++ b/src/lib/components/library/AddToLibraryModal.svelte @@ -6,6 +6,7 @@ import { sortRootFoldersForMediaType } from '$lib/utils/root-folders.js'; import { isLikelyAnimeMedia } from '$lib/shared/anime-classification.js'; import type { RootFolderWithSpaceAndDefault as RootFolder } from '$lib/types/downloadClient.js'; + import type { DesiredQuality } from '$lib/types/library.js'; import type { MinimumAvailability } from './add/MovieAddOptions.svelte'; import type { MonitorType, MonitorNewItems, SeriesType } from './add/SeriesAddOptions.svelte'; import AddMovieForm from './AddMovieForm.svelte'; @@ -35,7 +36,6 @@ interface LibraryEntity { id: string; mediaType: 'movie' | 'tv'; - defaultMonitored: boolean; defaultSearchOnAdd: boolean; defaultWantsSubtitles: boolean; rootFolders: Array<{ id: string }>; @@ -47,6 +47,8 @@ description?: string; isBuiltIn: boolean; isDefault?: boolean; + minResolution?: string | null; + maxResolution?: string | null; } interface Season { @@ -116,6 +118,7 @@ let minimumAvailability = $state('released'); let availabilityDelay = $state(0); let monitored = $state(true); + let desiredQualities = $state([]); let monitorType = $state('all'); let monitorNewItems = $state('all'); @@ -142,6 +145,9 @@ const selectedRootFolderLibrary = $derived( selectedRootFolder ? rootFolderLibraryMap.get(selectedRootFolder) : undefined ); + const selectedRootFolderObj = $derived( + selectedRootFolder ? rootFolders.find((folder) => folder.id === selectedRootFolder) : undefined + ); function getRecommendedRootFolderId(folders: RootFolder[]): string | undefined { if (folders.length === 0) return undefined; @@ -183,7 +189,7 @@ const willBeMonitored = $derived(mediaType === 'tv' ? monitorType !== 'none' : monitored); - const willSearchOnAdd = $derived(searchOnAdd && willBeMonitored); + const willSearchOnAdd = $derived(searchOnAdd); $effect(() => { if (open) { @@ -192,6 +198,7 @@ wantsSubtitles = true; minimumAvailability = 'released'; availabilityDelay = 0; + desiredQualities = []; monitorType = 'all'; monitorNewItems = 'all'; monitorSpecials = false; @@ -243,10 +250,11 @@ wantsSubtitles = selectedRootFolderLibrary.defaultWantsSubtitles; } if (!monitoredTouched) { + const defaultMonitored = selectedRootFolderObj?.defaultMonitored ?? true; if (mediaType === 'movie') { - monitored = selectedRootFolderLibrary.defaultMonitored; + monitored = defaultMonitored; } else { - monitorType = selectedRootFolderLibrary.defaultMonitored ? 'all' : 'none'; + monitorType = defaultMonitored ? 'all' : 'none'; } } }); @@ -443,7 +451,12 @@ }; const result = (mediaType === 'movie' - ? await createMovie({ ...basePayload, minimumAvailability, availabilityDelay }) + ? await createMovie({ + ...basePayload, + minimumAvailability, + availabilityDelay, + desiredQualities: desiredQualities.length > 0 ? desiredQualities : null + }) : await createSeries({ ...basePayload, monitorType, @@ -452,18 +465,33 @@ seriesType, seasonFolder, monitoredSeasons: Array.from(monitoredSeasons) - })) as unknown as { success: boolean; id?: string }; - - toasts.success(`${title} added to library`, { - description: willSearchOnAdd ? 'Searching for releases...' : undefined, - action: result.id - ? { - label: 'View', - href: - mediaType === 'movie' ? `/library/movie/${result.id}` : `/library/tv/${result.id}` - } - : undefined - }); + })) as unknown as { + success: boolean; + id?: string; + movie?: { id?: string; searchWarning?: string }; + series?: { id?: string; searchWarning?: string }; + }; + + const mediaId = result.movie?.id ?? result.series?.id ?? result.id; + const searchWarning = result.movie?.searchWarning ?? result.series?.searchWarning; + const viewAction = mediaId + ? { + label: 'View', + href: mediaType === 'movie' ? `/library/movie/${mediaId}` : `/library/tv/${mediaId}` + } + : undefined; + + if (searchWarning) { + toasts.warning(`${title} added to library`, { + description: searchWarning, + action: viewAction + }); + } else { + toasts.success(`${title} added to library`, { + description: willSearchOnAdd ? 'Searching for releases...' : undefined, + action: viewAction + }); + } onClose(); onSuccess?.(); @@ -570,6 +598,7 @@ bind:minimumAvailability bind:availabilityDelay bind:monitored + bind:desiredQualities bind:addEntireCollection /> {:else} diff --git a/src/lib/components/library/CollectionPickerModal.svelte b/src/lib/components/library/CollectionPickerModal.svelte new file mode 100644 index 000000000..974442d92 --- /dev/null +++ b/src/lib/components/library/CollectionPickerModal.svelte @@ -0,0 +1,275 @@ + + +{#if open} + + +
+
e.stopPropagation()} + onkeydown={handleKeydown} + role="dialog" + aria-modal="true" + aria-labelledby="collection-picker-title" + tabindex="0" + > + +
+
+ +

Set Collection

+
+ +
+ + +
+ + {#if currentCollectionName} +
+
+ + {currentCollectionName} + current +
+ +
+ {/if} + + +
+ + {#if searching} + + {/if} + +
+ + {#if error} +
{error}
+ {/if} + + +
+ {#if results.length === 0 && query.trim() && !searching} +

No collections found

+ {:else if results.length === 0 && !query.trim()} +

+ Type to search for a collection +

+ {:else} +
    + {#each results as col (col.id)} + {@const isCurrent = col.id === currentCollectionId} +
  • + +
  • + {/each} +
+ {/if} +
+
+ + +
+ +
+
+
+{/if} diff --git a/src/lib/components/library/DesiredQualitiesPicker.svelte b/src/lib/components/library/DesiredQualitiesPicker.svelte new file mode 100644 index 000000000..ca51ecfe0 --- /dev/null +++ b/src/lib/components/library/DesiredQualitiesPicker.svelte @@ -0,0 +1,85 @@ + + +
+
+ Desired Qualities +
+
+ {#each RESOLUTION_OPTIONS as option (option.value)} + {@const allowed = allowedResolutions.includes(option.value)} + {@const selected = desiredQualities.includes(option.value)} + + {/each} +
+
+ + Select two or more resolutions to keep multiple quality versions of this movie. Fewer than two + keeps the default single-best behavior. + +
+ {#if hasOutOfRangeSelection} +

+ Some selected resolutions are outside the current quality profile's range and will be ignored + until the profile allows them. +

+ {/if} + {#if multiQualityActive} +

+ Multi-quality mode active: each resolution is downloaded and upgraded independently. +

+ {/if} +
diff --git a/src/lib/components/library/LibraryMediaCard.svelte b/src/lib/components/library/LibraryMediaCard.svelte index 333e76bb7..b9eb762f3 100644 --- a/src/lib/components/library/LibraryMediaCard.svelte +++ b/src/lib/components/library/LibraryMediaCard.svelte @@ -1,7 +1,7 @@ @@ -243,200 +357,408 @@
-
- - - - - - - -
- - -
- - {#if currentProfile} - {currentProfile.description} - {:else} - {m.library_editMovie_qualityProfileDesc()} - {/if} - -
-
- - -
- - + + +
+
+
+ + +
+
+
+ Desired Qualities +
+
+ {#each resolutionOptions as option (option.value)} + + {/each} +
+
+
+ {#if showRemoveUnwantedFiles} + {/if} - {#each eligibleRootFolders as folder (folder.id)} - - {/each} - -
- - {m.library_add_rootFolderDesc()} -
- {#if enforceAnimeSubtype} -
- Anime root folder enforcement is enabled. New folder selections are limited to {requiredMediaSubType === 'anime' ? 'Anime' : 'Standard'} root folders for this movie. -
- {/if} -
- - {#if canMoveExistingFiles} - { - moveOptionTouched = true; - }} - label="Move existing files to new root folder" - description="Moves the existing movie folder after saving. Same-disk moves are instant; cross-disk moves copy then delete." - variant="toggle" - color="warning" - /> - {/if} + - - {#if movie.path} -
- - {#if showFolderPicker} - { - const root = selectedRootFolderObj?.path ?? ''; - folderPath = - root && selected.startsWith(root + '/') - ? selected.slice(root.length + 1) - : selected; - showFolderPicker = false; - }} - onCancel={() => (showFolderPicker = false)} - /> - {:else} -
+ +
+

+ Scheduling +

+
+
+ + +
+
+ + +
+
+ +
- + {m.library_availabilityDelay_unit()}
- {#if resolvedFolderPath} -

{resolvedFolderPath}

+
+
+
+ + +
+

+ Files +

+
+
+ + + {#if enforceAnimeSubtype} +
+ Limited to {requiredMediaSubType === 'anime' ? 'Anime' : 'Standard'} root + folders. +
{/if} -

- Folder name relative to the root folder. Edit only if the name on disk no longer - matches; saving will update the database and trigger a rescan to re-link existing files. -

- {#if folderPathChanged} -

- Folder name changed. A rescan will run automatically after saving. -

+
+
+
+ Collection +
+ {#if collectionSearchOpen} +
+
+ + + {#if collectionQuery} + + {/if} +
+ {#if collectionResults.length > 0} +
    + {#each collectionResults as col (col.id)} +
  • + +
  • + {/each} +
+ {/if} + +
+ {:else} +
+ + + {collectionName ?? 'No collection'} + + {#if collectionName} + + {/if} + +
{/if} +
+ + {#if canMoveExistingFiles} + + {/if} + + {#if movie.path} +
+ + {#if showFolderPicker} + { + const root = selectedRootFolderObj?.path ?? ''; + folderPath = + root && selected.startsWith(root + '/') + ? selected.slice(root.length + 1) + : selected; + showFolderPicker = false; + }} + onCancel={() => (showFolderPicker = false)} + /> + {:else} +
+ + +
+ {/if} + {#if resolvedFolderPath} +

{resolvedFolderPath}

+ {/if} + {#if folderPathChanged} +

+ Folder name changed. A rescan will run after saving. +

+ {/if} +
{/if}
- {/if} +
- -
- - -
- - {availabilityOptions.find((o) => o.value === minimumAvailability)?.description} - -
-
- - -
- -
- - {m.library_availabilityDelay_unit()} -
-
- - {m.library_availabilityDelay_desc()} - + Metadata + +
+
+ + +
+
-
+
diff --git a/src/lib/components/library/RenamePreviewModal.svelte b/src/lib/components/library/RenamePreviewModal.svelte index ba3ffd562..e83e0f395 100644 --- a/src/lib/components/library/RenamePreviewModal.svelte +++ b/src/lib/components/library/RenamePreviewModal.svelte @@ -25,6 +25,7 @@ // State let loading = $state(false); let executing = $state(false); + let refreshing = $state(false); let error = $state(null); let success = $state(null); let preview = $state(null); @@ -70,6 +71,20 @@ } } + async function refreshMetadata() { + refreshing = true; + error = null; + try { + const res = await fetch(`/api/library/movies/${mediaId}/refresh`, { method: 'POST' }); + if (!res.ok) throw new Error(await res.text()); + await loadPreview(); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to refresh metadata'; + } finally { + refreshing = false; + } + } + async function executeRenames() { if (selectedIds.size === 0) return; @@ -285,6 +300,33 @@ {/if}
+ {#if preview.missingCollectionData} +
+ +
+

No collection data

+

+ Your folder template uses {'{Collection}'} but this movie + has no collection data - paths may be incorrect. Refresh metadata to fetch it from TMDB. +

+
+ +
+ {/if} + {#if preview.totalFiles === 0}
{m.library_renamePreview_noFilesFound({ type: mediaTypeLabel(mediaType) })}. diff --git a/src/lib/components/library/SeriesEditModal.svelte b/src/lib/components/library/SeriesEditModal.svelte index 74d3a6bea..e2d905682 100644 --- a/src/lib/components/library/SeriesEditModal.svelte +++ b/src/lib/components/library/SeriesEditModal.svelte @@ -1,9 +1,8 @@ @@ -325,216 +350,305 @@
-
- - - - - - - - - - -
- - -
- - {seriesTypeOptions.find((option) => option.value === seriesType)?.description} - -
-
- - -
- - {#if episodeGroupsLoading} - - {:else} - - {/if} -
- - Alternative episode ordering from TMDB episode groups. Switching will rebuild all seasons - and episodes. - -
-
- - -
- - -
- - {#if currentProfile} - {currentProfile.description} - {:else} - {m.library_seriesEdit_qualityProfileDesc()} - {/if} - -
-
- - -
- - + + +
+
+ + +
+
+
+ + +
+
+ + {#if episodeGroupsLoading} + + {:else} + {/if} - - {/each} - -
- - {m.library_seriesEdit_rootFolderDesc()} - +
+
- {#if enforceAnimeSubtype} -
- Anime root folder enforcement is enabled. New folder selections are limited to {requiredMediaSubType === 'anime' ? 'Anime' : 'Standard'} - root folders for this series. + + + + {#if delayProfiles.length > 0} +
+

+ Scheduling +

+
+ +
- {/if} -
- - {#if canMoveExistingFiles} - { - moveOptionTouched = true; - }} - label="Move existing files to new root folder" - description="Moves the existing series folder after saving. Same-disk moves are instant; cross-disk moves copy then delete." - variant="toggle" - color="warning" - /> + {/if} - - {#if series.path} -
- - {#if showFolderPicker} - { - const root = selectedRootFolderObj?.path ?? ''; - folderPath = - root && selected.startsWith(root + '/') - ? selected.slice(root.length + 1) - : selected; - showFolderPicker = false; - }} - onCancel={() => (showFolderPicker = false)} + +
+

+ Files +

+
+
+ + +
+

+ Metadata +

+
+
+ + +
+ +
+
diff --git a/src/lib/components/library/add/CommonOptions.svelte b/src/lib/components/library/add/CommonOptions.svelte index 223c71995..d4a702d0b 100644 --- a/src/lib/components/library/add/CommonOptions.svelte +++ b/src/lib/components/library/add/CommonOptions.svelte @@ -12,6 +12,8 @@ description?: string; isBuiltIn: boolean; isDefault?: boolean; + minResolution?: string | null; + maxResolution?: string | null; } interface Props { @@ -72,7 +74,7 @@ : m.common_tvShows().toLowerCase() })} {/if} - {m.library_add_addOneInSettings()} @@ -140,9 +142,15 @@ {m.library_add_searchImmediately()}

- {searchOnAdd - ? m.library_add_searchImmediatelyDescYes() - : m.library_add_searchImmediatelyDescNo()} + {#if mediaType === 'movie'} + {searchOnAdd + ? m.library_add_movie_searchImmediatelyDescYes() + : m.library_add_movie_searchImmediatelyDescNo()} + {:else} + {searchOnAdd + ? m.library_add_searchImmediatelyDescYes() + : m.library_add_searchImmediatelyDescNo()} + {/if}

diff --git a/src/lib/components/library/add/MovieAddOptions.svelte b/src/lib/components/library/add/MovieAddOptions.svelte index a038d6bef..dadc9dd7d 100644 --- a/src/lib/components/library/add/MovieAddOptions.svelte +++ b/src/lib/components/library/add/MovieAddOptions.svelte @@ -1,6 +1,8 @@ {#if activeGroup} @@ -193,16 +200,63 @@ onchange={onSeasonNumberChange} /> - + {#if activeGroup?.parsedEpisodes && activeGroup.parsedEpisodes.length > 1 && !episodeOverrideActive} +
+ {m.library_import_episodeLabel()} +
+
+ E{activeGroup.parsedEpisodes[0]}-E{activeGroup.parsedEpisodes[ + activeGroup.parsedEpisodes.length - 1 + ]} + (auto) +
+ +
+
+ {:else if activeGroup?.parsedEpisodes && activeGroup.parsedEpisodes.length > 1 && episodeOverrideActive} +
+ {m.library_import_episodeLabel()} +
+ + +
+
+ {:else} + + {/if} {#if canApplyActiveSeasonOverride()}
{#if selectedMatch} + {#if !showMatchList} + + {selectedMatch.title}{selectedMatch.year ? ` (${selectedMatch.year})` : ''} + + {/if}
{browserPath}
- {#if !isFileOnlyContext} {/if} + {#if selectedPaths.size > 0} + + {m.library_import_selectedCount({ count: selectedPaths.size })} + + + {/if}
+ {#if !browserLoading && !browserError && browserEntries.length > 0} +
+
+
+ +
+ + {#if browserFilter} + + {/if} +
+
+ {/if} +
{#if browserLoading}
@@ -162,32 +241,64 @@
{m.library_import_noFoldersOrFiles()}
+ {:else if filteredEntries.length === 0} +
+ No entries match "{browserFilter}" +
{:else}
- {#each browserEntries as entry (entry.path)} -
- {#if sourcePath === entry.path} - - {/if} - {#if entry.isDirectory} - - {/if} - +
+
{entry.name}
+ {#if !entry.isDirectory} +
{formatSize(entry.size)}
+ {/if} +
+ {#if sourcePath === entry.path && !selectedPaths.has(entry.path)} + + {/if} + {#if entry.isDirectory} + + {/if} + +
{/each}
{/if} diff --git a/src/lib/components/library/import/Step3MultiImport.svelte b/src/lib/components/library/import/Step3MultiImport.svelte index 4c85af55c..1d0f045b4 100644 --- a/src/lib/components/library/import/Step3MultiImport.svelte +++ b/src/lib/components/library/import/Step3MultiImport.svelte @@ -10,6 +10,7 @@ mediaType: MediaType; mediaSubType?: string | null; isDefault?: boolean; + isDefaultFolder?: boolean; defaultRootFolderId?: string | null; defaultRootFolderPath?: string | null; } @@ -23,6 +24,7 @@ activeImportSeasonSection = null, hasMultipleImportTvSeries = false, importMediaFilter = $bindable('all' as QueueMediaFilter), + importMode = $bindable('move' as 'move' | 'copy' | 'symlink'), bulkDestinationBySectionId = {}, selectedImportGroupCount = 0, selectedNeedsInputCount = 0, @@ -51,6 +53,7 @@ activeImportSeasonSection: TvSeasonSection | null; hasMultipleImportTvSeries: boolean; importMediaFilter: QueueMediaFilter; + importMode: 'move' | 'copy' | 'symlink'; bulkDestinationBySectionId: Record; selectedImportGroupCount: number; selectedNeedsInputCount: number; @@ -384,6 +387,69 @@ {/if}
+ +
+

{m.library_import_importMethodHeading()}

+
+ + + +
+
+ {#if selectedNeedsInputCount > 0}
diff --git a/src/lib/components/library/import/Step3SingleImport.svelte b/src/lib/components/library/import/Step3SingleImport.svelte index 6be8e344d..7ff9b5021 100644 --- a/src/lib/components/library/import/Step3SingleImport.svelte +++ b/src/lib/components/library/import/Step3SingleImport.svelte @@ -10,6 +10,7 @@ mediaType: MediaType; mediaSubType?: string | null; isDefault?: boolean; + isDefaultFolder?: boolean; defaultRootFolderId?: string | null; defaultRootFolderPath?: string | null; } @@ -29,6 +30,7 @@ importTarget = $bindable('new' as 'new' | 'existing'), destinationLibrariesForType = [], selectedRootFolder = $bindable(''), + importMode = $bindable('move' as 'move' | 'copy' | 'symlink'), loadingRootFolders = false, seasonNumber = 1, episodeNumber = 1, @@ -49,6 +51,7 @@ importTarget: 'new' | 'existing'; destinationLibrariesForType: DestinationLibrary[]; selectedRootFolder: string; + importMode: 'move' | 'copy' | 'symlink'; loadingRootFolders: boolean; seasonNumber: number; episodeNumber: number; @@ -157,6 +160,69 @@
{/if} + +
+

{m.library_import_importMethodHeading()}

+
+ + + +
+
+

{m.library_import_summaryHeading()}

{#if selectedMatchContextMismatch && routeImportContext} @@ -211,7 +277,13 @@ {/if} {:else} {m.library_import_summaryEpisode()} - S{seasonNumber}E{episodeNumber} + {#if activeGroup.parsedEpisodes && activeGroup.parsedEpisodes.length > 1} + S{seasonNumber}E{activeGroup.parsedEpisodes[0]}-E{activeGroup.parsedEpisodes[ + activeGroup.parsedEpisodes.length - 1 + ]} + {:else} + S{seasonNumber}E{episodeNumber} + {/if} {/if}
{/if} diff --git a/src/lib/components/library/import/Step4Completion.svelte b/src/lib/components/library/import/Step4Completion.svelte index 5f05d5abd..43e2c7979 100644 --- a/src/lib/components/library/import/Step4Completion.svelte +++ b/src/lib/components/library/import/Step4Completion.svelte @@ -1,6 +1,9 @@ {#if executeError} @@ -61,7 +98,7 @@
-{:else if executeResult} +{:else if executeResult || bulkImportSummary}
@@ -87,21 +124,66 @@ {m.library_import_bulkSkippedItems({ count: skippedGroupCount })}

{/if} - {:else} + {:else if executeResult}

{executeResult.importedCount && executeResult.importedCount > 1 ? m.library_import_filesImportedPlural({ count: executeResult.importedCount }) : m.library_import_fileImportedSingular()}

{/if} -
-
- {m.library_import_importedPathLabel()} - {executeResult.importedPath} + + {#if executeResult} +
+
+ {m.library_import_importedPathLabel()} + {executeResult.importedPath} +
-
+ {/if} + + + {#if bulkImportSummary && uniqueLibraryLinks.length > 0} + {#if singleLibraryLink} + + {:else} + +
+

+ {m.library_import_importedItems()} +

+
+ {#each displayedLinks as item (item.href ?? item.title)} + {#if item.href} + + + {item.title}{item.year ? ` (${item.year})` : ''} + + {:else} + + {item.title}{item.year ? ` (${item.year})` : ''} + + {/if} + {/each} + {#if hiddenLinkCount > 0} +

+ {m.library_import_andNMore({ count: hiddenLinkCount })} +

+ {/if} +
+
+ {/if} + {/if} +
- {#if completionLink} + + {#if singleLibraryLink && bulkImportSummary} + + {m.library_import_viewInLibrary()} + + {:else if completionLink} {bulkImportSummary ? m.library_import_viewLastImported() diff --git a/src/lib/components/library/import/types.ts b/src/lib/components/library/import/types.ts index 998235899..0e53f866f 100644 --- a/src/lib/components/library/import/types.ts +++ b/src/lib/components/library/import/types.ts @@ -27,6 +27,7 @@ export interface DetectionGroup { parsedYear?: number; parsedSeason?: number; parsedEpisode?: number; + parsedEpisodes?: number[]; inferredMediaType: MediaType; matches?: MatchResult[]; } diff --git a/src/lib/components/library/index.ts b/src/lib/components/library/index.ts index e7e01f90a..133780b12 100644 --- a/src/lib/components/library/index.ts +++ b/src/lib/components/library/index.ts @@ -4,6 +4,7 @@ export { default as QualityBadge } from './QualityBadge.svelte'; export { default as ScoreBadge } from './ScoreBadge.svelte'; export { default as ScoreDetailModal } from './ScoreDetailModal.svelte'; export { default as MonitorToggle } from './MonitorToggle.svelte'; +export { default as DesiredQualitiesPicker } from './DesiredQualitiesPicker.svelte'; export { default as StatusIndicator } from './StatusIndicator.svelte'; export { default as AutoSearchStatus } from './AutoSearchStatus.svelte'; export { default as FileCard } from './FileCard.svelte'; @@ -12,6 +13,7 @@ export { default as MovieFilesTab } from './MovieFilesTab.svelte'; export { default as LibraryMovieHeader } from './LibraryMovieHeader.svelte'; export { default as MovieEditModal } from './MovieEditModal.svelte'; export { default as RenamePreviewModal } from './RenamePreviewModal.svelte'; +export { default as CollectionPickerModal } from './CollectionPickerModal.svelte'; export { default as AddToLibraryModal } from './AddToLibraryModal.svelte'; export { default as AddMovieForm } from './AddMovieForm.svelte'; export { default as AddSeriesForm } from './AddSeriesForm.svelte'; diff --git a/src/lib/components/library/mediaTableUtils.ts b/src/lib/components/library/mediaTableUtils.ts index 69e9e9834..d8d526c44 100644 --- a/src/lib/components/library/mediaTableUtils.ts +++ b/src/lib/components/library/mediaTableUtils.ts @@ -1,4 +1,5 @@ import type { LibraryMovie, LibrarySeries } from '$lib/types/library'; +import { pickBestMovieFile } from '$lib/shared/best-file.js'; export function isMovie(item: LibraryMovie | LibrarySeries): item is LibraryMovie { return 'hasFile' in item; @@ -25,7 +26,8 @@ export function getQualityBadges( const badges: Array<{ label: string; type: string }> = []; if (isMovie(item) && item.files.length > 0) { - const file = item.files[0]; + const file = pickBestMovieFile(item.files); + if (!file) return badges; const useAutoResolution = hasStreamerProfileFn(item); if (file.quality?.resolution) { diff --git a/src/lib/components/library/tv/TVSeriesSidebar.svelte b/src/lib/components/library/tv/TVSeriesSidebar.svelte index 2ecadd097..77f73953d 100644 --- a/src/lib/components/library/tv/TVSeriesSidebar.svelte +++ b/src/lib/components/library/tv/TVSeriesSidebar.svelte @@ -132,7 +132,7 @@
{m.library_tvDetail_tvdbId()}
+ import { ChevronDown, ChevronUp } from 'lucide-svelte'; + import * as m from '$lib/paraglide/messages.js'; + import { FormInput, FormSelect } from '$lib/components/ui/form'; + import type { NamingConfigShape } from '$lib/naming/setup-presets'; + + interface Props { + config: NamingConfigShape; + open?: boolean; + } + + let { config, open = $bindable(true) }: Props = $props(); + + +
+ + {#if open} +
+
+
+

+ {m.settings_naming_presetAwareOptions()} +

+

+ {m.settings_naming_presetAwareOptionsDesc()} +

+
+ + + + + + + + + +
+ + + + + +
+
+
+ {/if} +
diff --git a/src/lib/components/naming/NamingFormatField.svelte b/src/lib/components/naming/NamingFormatField.svelte index efad71503..622c64d6f 100644 --- a/src/lib/components/naming/NamingFormatField.svelte +++ b/src/lib/components/naming/NamingFormatField.svelte @@ -94,7 +94,7 @@