diff --git a/README.md b/README.md index 70d2bc7..46eed9d 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,10 @@ Why? Because if the cast list and IMDb/RT rating show up in the first minute, my ## Features - Insert plot summaries into existing .srt files without shifting timings - Fetch metadata (plot, runtime, director, cast, IMDb/RT ratings) using OMDb, TMDb, TVMaze and Wikipedia - add these integrations under Settings before scanning -- Preserve original dialogue and timing with safe insertion logic +- Preserve original dialogue and timing with safe insertion logic while cleaning watermarks (YTS, OpenSubtitles, etc.) +- Automation rules can run cleanup-only mode on schedules, or combined cleanup + metadata enrichment - Folder Rules to have seperate logic for different folders (for example TV shows could have runtime but not actors, etc) -- Clean, fast web UI for scanning and batch processing built with Svelte + Python/Flask +- Clean, fast web UI for scanning and batch processing built with Svelte (frontend) + Python/Flask (server) - Three themes included: OLED, Ocean, and Dracula White ## Screenshots @@ -27,7 +28,8 @@ Why? Because if the cast list and IMDb/RT rating show up in the first minute, my ## Getting started -To get started installing Sublogue, expand on your posion below. Personally, I recommend Komodo. It's great. +To get started installing Sublogue, expand on your posion below. Personally, I recommend **Komodo**. It's great. +
⚓ Docker Compose Create `data/` and `media/` folders next to the compose file, then run: diff --git a/docs/documentation.html b/docs/documentation.html new file mode 100644 index 0000000..a0ca36b --- /dev/null +++ b/docs/documentation.html @@ -0,0 +1,380 @@ + + + + + + + Sublogue Documentation + + + + + + + + + + + +
+
+ +
+ +
+
+
+

Documentation

+

+ Sublogue usage and setup +

+

+ Sublogue is a lightweight, open source tool that enriches subtitle files with metadata and plot + summaries. It keeps timing intact, respects safe insertion windows, and lets you choose the metadata + provider per scan or per folder. +

+

+ This page mirrors the README but adds extra context, limits, and setup notes so you can move from + install to first scan without guesswork. +

+
+
+
+ +

Quick checklist

+
+
    +
  • + + Install the container and map /config + /media volumes. +
  • +
  • + + Enable integrations and add API keys where required. +
  • +
  • + + Scan a folder and review matches before applying. +
  • +
  • + + Use per-folder rules for language and formatting differences. +
  • +
+
+
+ +
+
+ +

Features

+
+

+ Sublogue focuses on safe subtitle enrichment. It never shifts existing timecodes, and only inserts + content when there is a safe gap at the start or end of the file. +

+
+
+

Metadata injection

+

+ Pull plot, runtime, ratings, and credits from OMDb, TMDb, TVmaze, or Wikipedia and insert them in a + clean, readable format. +

+
+
+

Safe timing

+

+ Sublogue refuses to insert if there is not enough gap, so subtitle timing stays intact. +

+
+
+

Folder rules

+

+ Override metadata source, language, and formatting per directory for mixed media libraries. +

+
+
+

Web UI

+

+ Scan folders, batch process results, and track history from a lightweight Svelte UI. +

+
+
+
+ +
+
+ +

Integrations

+
+

+ Each provider has a different signup and usage policy. Wikipedia is strict by design: exact title + matches with year validation are required to reduce false matches from an open encyclopedia. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProviderSignup or API keyRate limitsNotes
OMDb + Get an API key + Free tier has a daily capBest for movie matching
TMDb + Request an API key + Per-second rate limitGreat for localization and posters
TVmaze + View API docs + Polite usage limitsNo API key required
Wikipedia + MediaWiki API + No hard limits, be politeStrict title matching
+
+
+ +
+
+ +

Screenshots

+
+

+ The interface is designed for fast scans and quick metadata validation. These screenshots show the scan + results and settings panels. +

+
+ Scan results + Settings panel +
+
+ +
+
+ +

Getting started

+
+

+ Sublogue runs as a single container. Map /config for settings and /media for subtitles, then open the web + UI at http://localhost:5000. +

+
+
+

Docker Compose

+

+ Create data/ and media/ folders next to your compose file. +

+
version: "3.9"
+services:
+  sublogue:
+    image: ghcr.io/ponzischeme89/sublogue:latest
+    container_name: sublogue
+    restart: unless-stopped
+    environment:
+      - TZ=Pacific/Auckland
+    volumes:
+      - ./data:/config
+      - ./media:/media
+    ports:
+      - "5000:5000"
+
+
+

Unraid

+

+ Use the bundled template and map host paths into /config and /media. +

+
+

/mnt/user/appdata/sublogue -> /config

+

/mnt/user/appdata/sublogue/media -> /media

+
+
+
+

Komodo

+

+ Create a new stack and attach your external network. +

+
version: "3.9"
+services:
+  sublogue:
+    image: ghcr.io/ponzischeme89/sublogue:latest
+    container_name: sublogue
+    ports:
+      - "5000:5000"
+    environment:
+      - TZ=Etc/UTC
+      - PUID=1000
+      - PGID=1000
+    volumes:
+      - /volume1/Docker/sublogue/data:/config
+      - /volume1/Media:/media
+    restart: unless-stopped
+    networks:
+      - npm_network
+networks:
+  npm_network:
+    external: true
+
+
+
+ +
+
+ +

Limitations

+
+
    +
  • API limits vary per provider and can throttle large scans.
  • +
  • Metadata gaps appear when providers do not have a title or plot.
  • +
  • Only TMDb supports localization. OMDb and TVmaze are English-first.
  • +
  • Long plots are inserted as-is. TVs may split them across screens.
  • +
  • Only .srt files are supported.
  • +
  • Reprocessing a file can insert multiple plot blocks.
  • +
+
+ +
+
+ +

Roadmap

+
+
    +
  • More UI themes and high contrast variants
  • +
  • Poster and backdrop previews in results
  • +
  • Duplicate plot detection and auto-avoidance
  • +
  • Automatic backoff and retry logic
  • +
  • Short plot mode for long summaries
  • +
  • Expanded localization for plots and cast
  • +
  • Subtitle format support beyond SRT
  • +
  • Optional local cache for metadata lookups
  • +
+
+ +
+
+
+ +

Support

+
+

+ If Sublogue helps your library, consider starring the repo or sharing it with someone who cares about + clean subtitles. Bug reports and feature requests are welcome. +

+
+
+
+ +
+
+

© 2026 ponzischeme89. Released under AGLPL-3.0 licence.

+
+ + github.com/ponzischeme89/Sublogue +
+
+
+
+ + + + + diff --git a/docs/index.html b/docs/index.html index 0e5f127..f7e50ce 100644 --- a/docs/index.html +++ b/docs/index.html @@ -70,6 +70,7 @@ @@ -77,6 +78,7 @@ class="flex flex-col gap-3 rounded-2xl border border-white/10 bg-night-800/90 p-4 text-sm shadow-2xl md:hidden"> Install Integrations + Documentation Quickstart @@ -449,4 +451,4 @@ You are early. - \ No newline at end of file + diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 3f36295..354de6d 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -8,6 +8,7 @@ import ScanPanel from "./components/scan/ScanPanel.svelte"; import HistoryPanel from "./components/HistoryPanel.svelte"; import LibraryPanel from "./components/library/LibraryPanel.svelte"; + import AutomationList from "./routes/automation/AutomationList.svelte"; import { Menu } from "lucide-svelte"; import ToastHost from "./components/ToastHost.svelte"; import { healthCheck } from "./lib/api.js"; @@ -15,6 +16,8 @@ let currentView = "scanner"; let apiConfigured = false; + let apiReachable = true; + let apiErrorMessage = ""; let selectedFiles = []; let metadataProvider = "omdb"; let scanPanelKey = 0; @@ -59,8 +62,12 @@ try { const health = await healthCheck(); apiConfigured = health.api_key_configured; + apiReachable = true; + apiErrorMessage = ""; } catch (err) { console.error("Health check failed:", err); + apiReachable = false; + apiErrorMessage = "Backend unreachable. Start the server to use Sublogue."; } return () => { window.removeEventListener("resize", onResize); @@ -76,8 +83,12 @@ try { const health = await healthCheck(); apiConfigured = health.api_key_configured; + apiReachable = true; + apiErrorMessage = ""; } catch (err) { console.error("Health check failed:", err); + apiReachable = false; + apiErrorMessage = "Backend unreachable. Start the server to use Sublogue."; } } @@ -143,11 +154,18 @@ {/if} - {#if !apiConfigured && currentView === "scanner"} + {#if !apiReachable} +
+
+

{apiErrorMessage}

+
+
+ {:else if !apiConfigured && currentView === "scanner"}
-

- Configure a metadata source in Settings to get started +

+ Configure a metadata source in Settings > Integrations to get + started

@@ -170,6 +188,8 @@ {/key} {:else if currentView === "library"} + {:else if currentView === "automation"} + {/if} diff --git a/frontend/src/components/AppSidebar.svelte b/frontend/src/components/AppSidebar.svelte index 0073dc9..39d0fee 100644 --- a/frontend/src/components/AppSidebar.svelte +++ b/frontend/src/components/AppSidebar.svelte @@ -10,6 +10,7 @@ Settings, History, Library, + Zap, } from "lucide-svelte"; import ThemeSelector from "./ThemeSelector.svelte"; import sublogueLogo from "../assets/sublogue_v2.png"; @@ -89,6 +90,24 @@ {/if} + + + {/each} + + + + +
+ {#if error} +
- {#if section.icon === 'settings'} - - {:else if section.icon === 'calendar'} - - {:else if section.icon === 'bolt'} - - {:else if section.icon === 'wand'} - - {:else if section.icon === 'plug'} - - {:else if section.icon === 'folder'} - - {/if} - {section.label} - - {/each} - - - - -
- {#if error} -
-

{error}

-
- {/if} - - {#if successMessage} -
-

{successMessage}

-
- {/if} - -
- {#if currentSection === 'general'} - - {:else if currentSection === 'folder-rules'} - - {:else if currentSection === 'scheduled'} - - {:else if currentSection === 'cleanup'} - - {:else if currentSection === 'integrations'} - - {:else if currentSection === 'tasks'} - +

{error}

+
{/if} + + {#if successMessage} +
+

{successMessage}

+
+ {/if} + +
+ {#if currentSection === "general"} + + {:else if currentSection === "folder-rules"} + + {:else if currentSection === "scheduled"} + + {:else if currentSection === "cleanup"} + + {:else if currentSection === "integrations"} + + {:else if currentSection === "tasks"} + + {/if} +
- {/if} diff --git a/frontend/src/components/ToastHost.svelte b/frontend/src/components/ToastHost.svelte index ea31879..6bc9284 100644 --- a/frontend/src/components/ToastHost.svelte +++ b/frontend/src/components/ToastHost.svelte @@ -5,7 +5,7 @@ const toneStyles = { info: 'border-white/10 bg-bg-card text-text-primary', - success: 'border-green-500/30 bg-green-500/10 text-green-100', + success: 'border-emerald-500/50 bg-emerald-900/60 text-emerald-50', error: 'border-red-500/30 bg-red-500/10 text-red-100' } diff --git a/frontend/src/components/scan/ResultsList.svelte b/frontend/src/components/scan/ResultsList.svelte index 85a0485..7d8201e 100644 --- a/frontend/src/components/scan/ResultsList.svelte +++ b/frontend/src/components/scan/ResultsList.svelte @@ -24,6 +24,7 @@ export let disabled = false; export let metadataProvider = "omdb"; export let metadataLanguage = ""; + export let cleanMode = false; export let activeIntegrations = { omdb: true, tmdb: true, @@ -43,6 +44,12 @@ ); $: pageStart = (currentPage - 1) * itemsPerPage + 1; $: pageEnd = Math.min(currentPage * itemsPerPage, files.length); + $: cleanModeWithKeywords = paginatedFiles.filter( + (file) => file.clean_keywords && file.clean_keywords.length > 0, + ); + $: cleanModeNoKeywords = paginatedFiles.filter( + (file) => !file.clean_keywords || file.clean_keywords.length === 0, + ); function goToPage(page) { currentPage = Math.max(1, Math.min(page, totalPages)); @@ -814,6 +821,12 @@ $: if (!hasActiveIntegrations && showMetadataDropup) { showMetadataDropup = false; } + $: if (cleanMode && showMetadataDropup) { + showMetadataDropup = false; + } + $: if (cleanMode && openSearchDropdown) { + openSearchDropdown = null; + } $: metadataDropdownOptions = [ { value: "omdb", @@ -972,6 +985,7 @@
+ {#if !cleanMode}
@@ -1151,7 +1166,9 @@ Status Filename - Matched Result + + {cleanMode ? "Clean Scan" : "Matched Result"} + @@ -1182,6 +1199,206 @@ {/each} + {:else if cleanMode} + {#if cleanModeWithKeywords.length > 0} + + +
+ Results With Keywords +
+
+
+ {#each cleanModeWithKeywords as file (file.path)} + + + toggleSelection(file)} + class="h-4 w-4 rounded border-input bg-background text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + /> + + + + + + + + + + {file.name} + + + +
+ Detected: {file.clean_keywords.join(", ")} +
+
+ +
+ + Clean only + +
+
+
+ {#if expandedRows[file.path]} + + +
+
+
+ Detected Keywords +
+
+ {file.clean_keywords.join(", ")} +
+
+
+
+ Cleanup Summary +
+
+ {file.summary || "No changes needed"} +
+
+
+
+
+ {/if} + {/each} + {/if} + + {#if cleanModeNoKeywords.length > 0} + + +
+ Clean Files (No Keywords Found) +
+
+
+ {#each cleanModeNoKeywords as file (file.path)} + + + toggleSelection(file)} + class="h-4 w-4 rounded border-input bg-background text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + /> + + + + + + + + + + {file.name} + + + +
+ Detected: None +
+
+ +
+ + Clean only + +
+
+
+ {#if expandedRows[file.path]} + + +
+
+
+ Detected Keywords +
+
+ None +
+
+
+
+ Cleanup Summary +
+
+ {file.summary || "No changes needed"} +
+
+
+
+
+ {/if} + {/each} + {/if} {:else} {#each paginatedFiles as file (file.path)} @@ -1230,6 +1447,15 @@ + {#if cleanMode} +
+ {#if file.clean_keywords && file.clean_keywords.length > 0} + Detected: {file.clean_keywords.join(", ")} + {:else} + Detected: None + {/if} +
+ {:else}
@@ -1359,9 +1585,15 @@ {/if} {/if}
+ {/if}
+ {#if cleanMode} + + Clean only + + {:else} {#if suggestedMatches[file.path]}
@@ -1422,6 +1655,36 @@ {#if expandedRows[file.path]} + {#if cleanMode} +
+
+
+ Detected Keywords +
+
+ {#if file.clean_keywords && file.clean_keywords.length > 0} + {file.clean_keywords.join(", ")} + {:else} + None + {/if} +
+
+
+
+ Cleanup Summary +
+
+ {file.summary || "No changes needed"} +
+
+
+ {:else}
@@ -1483,6 +1746,7 @@
+ {/if} {/if} @@ -1569,7 +1833,7 @@
- {#if hasMatches || bulkApplying} + {#if !cleanMode && (hasMatches || bulkApplying)}
@@ -2029,17 +2293,19 @@ > Close - + {#if !cleanMode} + + {/if}
diff --git a/frontend/src/components/scan/ScanPanel.svelte b/frontend/src/components/scan/ScanPanel.svelte index a918457..20ac21c 100644 --- a/frontend/src/components/scan/ScanPanel.svelte +++ b/frontend/src/components/scan/ScanPanel.svelte @@ -82,7 +82,8 @@ let pendingTitleOverride = null; let resultsListRef = null; let pendingForceReprocess = false; - let quoteStyle = 'sarcastic'; + let cleanOnly = false; + let quoteStyle = "sarcastic"; let initialLoading = true; let scanToastedError = false; let metadataOptions = []; @@ -273,7 +274,12 @@ files = [...files, ...data.batch]; // Trigger auto-matching for the new batch if ResultsList is ready - if (resultsListRef && data.batch && data.batch.length > 0) { + if ( + !cleanOnly && + resultsListRef && + data.batch && + data.batch.length > 0 + ) { console.log( "Auto-matching new batch of", data.batch.length, @@ -327,7 +333,7 @@ Object.keys(existingMatches).length, ); - if (resultsListRef) { + if (!cleanOnly && resultsListRef) { // ALWAYS load existing matches from database first (even if empty, to reset state) console.log( "Loading existing matches into component:", @@ -372,7 +378,8 @@ } scanToastedError = true; addToast({ - message: err.name === "AbortError" ? "Scan cancelled." : "Scan failed.", + message: + err.name === "AbortError" ? "Scan cancelled." : "Scan failed.", tone: err.name === "AbortError" ? "info" : "error", }); }, @@ -387,7 +394,8 @@ } if (!scanToastedError) { addToast({ - message: err.name === "AbortError" ? "Scan cancelled." : "Scan failed.", + message: + err.name === "AbortError" ? "Scan cancelled." : "Scan failed.", tone: err.name === "AbortError" ? "info" : "error", }); } @@ -502,6 +510,7 @@ duration, pendingTitleOverride, forceReprocess, + cleanOnly, ); processingResults = response.results; @@ -510,6 +519,15 @@ files = files.map((file) => { const result = processingResults.find((r) => r.file === file.path); if (result) { + if (result.clean_only) { + return { + ...file, + status: result.status || (result.success ? "Cleaned" : "Error"), + summary: result.summary || file.summary || "", + clean_only: true, + clean_keywords: result.clean_keywords || [], + }; + } return { ...file, status: result.status || (result.success ? "Processed" : "Error"), @@ -554,10 +572,7 @@ } function normalizePath(path) { - return (path || "") - .replace(/\//g, "\\") - .replace(/\\+$/, "") - .toLowerCase(); + return (path || "").replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase(); } function findFolderRuleForDirectory(path, rules) { @@ -668,370 +683,427 @@ {:else} - - - - Scan your SRT files - - Scan folders for subtitles missing plot summaries - - + + + + Scan Files + - -
-
- - -
- -
- -
- - - Metadata Source - - - selectMetadataSource(event.detail.value)} - > - - - - - {activeMetadataOptions.length === 0 - ? "Enable an integration in Settings to select a source." - : "Choose which API to use for fetching plot summaries"} - -
- - {#if error} -
-

{error}

-
- {/if} - - {#if scanning} -
-
-
-
- {scanProgress.message} -
- + +
+
+ +
+ +
- {#if scanProgress.filesFound > 0} -
- Files found: {scanProgress.filesFound} - Scanning in progress... +
+ + + Metadata Source + + + selectMetadataSource(event.detail.value)} + > + + + + + {cleanOnly + ? "Clean-only mode skips metadata lookups." + : activeMetadataOptions.length === 0 + ? "Enable an integration in Settings to select a source." + : "Choose which API to use for fetching plot summaries"} + +
+ +