diff --git a/README.md b/README.md
index 8df0bd5..1a66a3a 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ Why? Because if the cast list and IMDb/RT rating show up in the first minute, my
- Insert plot summaries into existing .srt files without shifting timings
- Fetch metadata (plot, runtime, director, cast, IMDb/RT ratings) using OMDb, TMDb, and TVMaze — add these integrations under Settings before scanning
- Preserve original dialogue and timing with safe insertion logic
+- 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
- Three themes included: OLED, Ocean, and Dracula White
diff --git a/frontend/src/components/AppSidebar.svelte b/frontend/src/components/AppSidebar.svelte
index 2c35bfc..eae194b 100644
--- a/frontend/src/components/AppSidebar.svelte
+++ b/frontend/src/components/AppSidebar.svelte
@@ -157,7 +157,7 @@
>
{#if !collapsed}
v1.0.5 Release Candiatev1.0.6 Release Candiate
{:else}
v
diff --git a/frontend/src/components/ResultsList.svelte b/frontend/src/components/ResultsList.svelte
index 711bdcb..c902900 100644
--- a/frontend/src/components/ResultsList.svelte
+++ b/frontend/src/components/ResultsList.svelte
@@ -23,6 +23,7 @@
export let onSelectionChange = () => {};
export let disabled = false;
export let metadataProvider = "omdb";
+ export let metadataLanguage = "";
export let activeIntegrations = { omdb: true, tmdb: true, tvmaze: true };
export let loading = false;
@@ -198,7 +199,10 @@
searchingInline = { ...searchingInline };
try {
- const response = await searchTitle(query);
+ const response = await searchTitle(query, "quick", {
+ preferredSource: metadataProvider,
+ language: metadataLanguage,
+ });
inlineSearchResults[file.path] = response.results || [];
inlineSearchResults = { ...inlineSearchResults };
} catch (err) {
@@ -261,7 +265,10 @@
try {
// Use "full" mode for manual search to get multiple results to choose from
- const response = await searchTitle(cleanTitle, "full");
+ const response = await searchTitle(cleanTitle, "full", {
+ preferredSource: metadataProvider,
+ language: metadataLanguage,
+ });
searchResults = response.results || [];
} catch (err) {
searchError = "Failed to search for titles. Please try again.";
@@ -320,7 +327,10 @@
const cleanTitle = extractSearchableTitle(file.name, file.title);
try {
- const response = await searchTitle(cleanTitle);
+ const response = await searchTitle(cleanTitle, "quick", {
+ preferredSource: metadataProvider,
+ language: metadataLanguage,
+ });
const results = response.results || [];
if (results.length > 0) {
@@ -520,7 +530,10 @@
);
try {
- const response = await searchTitle(cleanTitle);
+ const response = await searchTitle(cleanTitle, "quick", {
+ preferredSource: metadataProvider,
+ language: metadataLanguage,
+ });
const results = response.results || [];
if (results.length > 0) {
@@ -620,7 +633,10 @@
);
try {
- const response = await searchTitle(cleanTitle);
+ const response = await searchTitle(cleanTitle, "quick", {
+ preferredSource: metadataProvider,
+ language: metadataLanguage,
+ });
const results = response.results || [];
if (results.length > 0) {
diff --git a/frontend/src/components/ScanPanel.svelte b/frontend/src/components/ScanPanel.svelte
index ff527f2..1b1db4f 100644
--- a/frontend/src/components/ScanPanel.svelte
+++ b/frontend/src/components/ScanPanel.svelte
@@ -6,6 +6,7 @@
updateSettings,
processFiles,
clearAllSuggestedMatches,
+ getFolderRules,
} from "../lib/api.js";
import ResultsList from "./ResultsList.svelte";
import TypewriterQuote from "./TypewriterQuote.svelte";
@@ -63,6 +64,7 @@
let lastScan = null;
let selectedFilePaths = [];
let metadataProvider = "omdb";
+ let metadataLanguage = "";
let omdbEnabled = false;
let tmdbEnabled = false;
let tvmazeEnabled = false;
@@ -126,6 +128,9 @@
export let onOpenHistory = null;
export { selectedFilePaths, metadataProvider };
let onboardingComplete = false;
+ let hasScannedBefore = false;
+ let folderRules = [];
+ let activeFolderRule = null;
onMount(async () => {
const initialTimer = setTimeout(() => {
@@ -152,6 +157,14 @@
if (typeof localStorage !== "undefined") {
onboardingComplete =
localStorage.getItem("sublogue_onboarding_complete") === "true";
+ hasScannedBefore =
+ localStorage.getItem("sublogue_has_scanned") === "true";
+ }
+ try {
+ const rulesResponse = await getFolderRules();
+ folderRules = rulesResponse.rules || [];
+ } catch (err) {
+ console.error("Failed to load folder rules:", err);
}
} catch (err) {
console.error("Failed to load initial data:", err);
@@ -234,6 +247,10 @@
// Save to store
scanResults.setScanResults(files, directory);
+ if (typeof localStorage !== "undefined") {
+ localStorage.setItem("sublogue_has_scanned", "true");
+ hasScannedBefore = true;
+ }
// Show save prompt if scanning a new directory
if (isDifferentDirectory) {
@@ -484,6 +501,31 @@
metadataProvider = event.detail.source;
}
+ function normalizePath(path) {
+ return (path || "")
+ .replace(/\//g, "\\")
+ .replace(/\\+$/, "")
+ .toLowerCase();
+ }
+
+ function findFolderRuleForDirectory(path, rules) {
+ const target = normalizePath(path);
+ if (!target) return null;
+ let bestRule = null;
+ let bestLength = -1;
+ for (const rule of rules) {
+ const dir = normalizePath(rule.directory);
+ if (!dir) continue;
+ if (target === dir || target.startsWith(dir + "\\")) {
+ if (dir.length > bestLength) {
+ bestLength = dir.length;
+ bestRule = rule;
+ }
+ }
+ }
+ return bestRule;
+ }
+
function formatMetadataLabel(source) {
if (source === "both") return "OMDb + TMDb";
if (source === "tvmaze") return "TVmaze";
@@ -521,6 +563,19 @@
$: failureCount = processingResults?.filter((r) => !r.success).length || 0;
$: metadataSelected = !!metadataProvider;
$: resolveMetadataProvider(activeMetadataOptions);
+ $: if (folderRules.length && directory) {
+ const matchedRule = findFolderRuleForDirectory(directory, folderRules);
+ activeFolderRule = matchedRule;
+ if (matchedRule) {
+ if (matchedRule.preferred_source) {
+ metadataProvider = matchedRule.preferred_source;
+ }
+ metadataLanguage = matchedRule.language || "";
+ } else {
+ activeFolderRule = null;
+ metadataLanguage = "";
+ }
+ }
$: allStepsComplete = apiConfigured && metadataSelected && hasScanned;
$: if (allStepsComplete && !onboardingComplete) {
onboardingComplete = true;
@@ -528,7 +583,7 @@
localStorage.setItem("sublogue_onboarding_complete", "true");
}
}
- $: showTutorial = !onboardingComplete && !hasScanned;
+ $: showTutorial = !onboardingComplete && !hasScanned && !hasScannedBefore;
@@ -746,6 +801,7 @@
onSelectionChange={handleSelectionChange}
disabled={!apiConfigured || processing}
{metadataProvider}
+ {metadataLanguage}
activeIntegrations={{
omdb: omdbEnabled,
tmdb: tmdbEnabled,
diff --git a/frontend/src/components/SettingsPanel.svelte b/frontend/src/components/SettingsPanel.svelte
index 572a658..ef320e3 100644
--- a/frontend/src/components/SettingsPanel.svelte
+++ b/frontend/src/components/SettingsPanel.svelte
@@ -6,9 +6,10 @@
import IntegrationsSettings from './settings/IntegrationsSettings.svelte'
import FilenameCleaningSettings from './settings/FilenameCleaningSettings.svelte'
import ScheduledScansSettings from './settings/ScheduledScansSettings.svelte'
+ import FolderRulesSettings from './settings/FolderRulesSettings.svelte'
import TasksSettings from './settings/TasksSettings.svelte'
import { addToast } from '../lib/toastStore.js'
- import { Bolt, Calendar, Plug, Settings, Wand2 } from 'lucide-svelte'
+ import { Bolt, Calendar, Folder, Plug, Settings, Wand2 } from 'lucide-svelte'
let currentSection = 'general'
let settings = {}
@@ -19,6 +20,7 @@
const sections = [
{ id: 'general', label: 'General', icon: 'settings' },
+ { id: 'folder-rules', label: 'Folder Rules', icon: 'folder' },
{ id: 'scheduled', label: 'Scheduled Scans', icon: 'calendar' },
{ id: 'cleanup', label: 'Cleanup', icon: 'wand' },
{ id: 'integrations', label: 'Integrations', icon: 'plug' },
@@ -135,6 +137,8 @@
{:else if section.icon === 'plug'}
+ {:else if section.icon === 'folder'}
+
{/if}
{section.label}
@@ -159,6 +163,8 @@
{#if currentSection === 'general'}
+ {:else if currentSection === 'folder-rules'}
+
{:else if currentSection === 'scheduled'}
{:else if currentSection === 'cleanup'}
diff --git a/frontend/src/components/ThemeSelector.svelte b/frontend/src/components/ThemeSelector.svelte
index 2bb4dd5..477ba6e 100644
--- a/frontend/src/components/ThemeSelector.svelte
+++ b/frontend/src/components/ThemeSelector.svelte
@@ -22,6 +22,7 @@
className={className}
dropup={true}
searchable={false}
+ displayPrefix="Theme: "
on:change={handleThemeChange}
>
diff --git a/frontend/src/components/settings/FolderRulesSettings.svelte b/frontend/src/components/settings/FolderRulesSettings.svelte
new file mode 100644
index 0000000..e2ea734
--- /dev/null
+++ b/frontend/src/components/settings/FolderRulesSettings.svelte
@@ -0,0 +1,283 @@
+
+
+
+
+
Per-folder rules
+
+ Override metadata source and subtitle formatting per directory.
+
+
+
+ {#if error}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Used for TMDb localized plots (e.g. it-IT, fr-FR). OMDb is always English.
+
+
+
+
+
+
+
+
+ {#if editingDirectory}
+
+ {/if}
+
+
+
+
+
+
Active rules
+
+
+
+ {#if loading}
+
Loading rules...
+ {:else if rules.length === 0}
+
+ No folder rules yet.
+
+ {:else}
+
+ {#each rules as rule}
+
+
+
+
{rule.directory}
+
+ {rule.preferred_source || settings.preferred_source || 'omdb'} ·
+ {rule.insertion_position || settings.insertion_position || 'start'} ·
+ {rule.language || 'default language'}
+
+
+
+
+
+
+
+
+ {/each}
+
+ {/if}
+
+
diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js
index e9e62d9..3e28369 100644
--- a/frontend/src/lib/api.js
+++ b/frontend/src/lib/api.js
@@ -74,6 +74,38 @@ export async function updateSettings(settings) {
})
}
+// ============ FOLDER RULES API ============
+
+/**
+ * GET /api/folder-rules - Fetch folder rules
+ * Returns: { success, rules: [...] }
+ */
+export async function getFolderRules() {
+ return apiFetch('/folder-rules')
+}
+
+/**
+ * POST /api/folder-rules - Create or update a folder rule
+ * Body: { directory, preferred_source?, insertion_position?, language?, subtitle_*? }
+ * Returns: { success }
+ */
+export async function saveFolderRule(rule) {
+ return apiFetch('/folder-rules', {
+ method: 'POST',
+ body: JSON.stringify(rule)
+ })
+}
+
+/**
+ * DELETE /api/folder-rules/ - Delete a folder rule
+ * Returns: { success }
+ */
+export async function deleteFolderRule(directory) {
+ return apiFetch(`/folder-rules/${encodeURIComponent(directory)}`, {
+ method: 'DELETE'
+ })
+}
+
// ============ SCAN API ============
/**
@@ -218,10 +250,17 @@ export async function getScanStatus() {
* - "full": Returns multiple results to choose from (2 API calls) - good for manual search
* Returns: { success, results: [{title, year, plot, runtime, imdb_rating, media_type, poster, imdb_id}] }
*/
-export async function searchTitle(query, mode = "quick") {
+export async function searchTitle(query, mode = "quick", options = {}) {
+ const body = { query, mode }
+ if (options.preferredSource) {
+ body.preferred_source = options.preferredSource
+ }
+ if (options.language) {
+ body.language = options.language
+ }
return apiFetch('/search', {
method: 'POST',
- body: JSON.stringify({ query, mode })
+ body: JSON.stringify(body)
})
}
diff --git a/frontend/src/lib/components/ui/combobox/Combobox.svelte b/frontend/src/lib/components/ui/combobox/Combobox.svelte
index 688fc8b..986ff30 100644
--- a/frontend/src/lib/components/ui/combobox/Combobox.svelte
+++ b/frontend/src/lib/components/ui/combobox/Combobox.svelte
@@ -13,6 +13,7 @@
export let className = ''
export let searchable = true
export let dropup = false
+ export let displayPrefix = ''
let open = false
let search = ''
@@ -87,7 +88,7 @@
- {selectedLabel || placeholder}
+ {selectedLabel ? `${displayPrefix}${selectedLabel}` : placeholder}