Store library settings in the pool index and drop legacy sidecars

This commit is contained in:
2026-07-03 16:06:45 +02:00
parent f0f38d4257
commit 7ae0709f38
13 changed files with 379 additions and 2292 deletions
+68 -97
View File
@@ -7,30 +7,33 @@ from .tag_manager import TagManager
from .pool_index import PoolIndex
from .utils import list_files
from typing import Iterable
import fnmatch
from src.core.config import (
load_global_config, save_global_config,
load_folder_config, save_folder_config
load_global_config, save_global_config, DEFAULT_TAG_SCHEMA
)
# Top-level folders inside the managed pool
POOL_MOVIES = "Filmy"
POOL_SERIES = "Seriály"
# Curator metadata files that must never be treated as content
# Library-level settings that live in the pool index (not the global config).
_LIBRARY_SETTINGS_DEFAULTS = {
"tag_schema": DEFAULT_TAG_SCHEMA,
"copyasis_folders": [POOL_SERIES],
}
# Curator metadata files that must never be treated as content. The legacy
# per-file (.!tag) and per-folder (.!ftag) sidecars are no longer written, but
# stray ones are still skipped so they never leak into the Filmotéka.
METADATA_SUFFIXES = (".!tag", ".!ftag", ".!gtag", ".!index")
class FileManager:
def __init__(self, tagmanager: TagManager):
self.filelist: list[File] = []
self.folders: list[Path] = []
self.tagmanager = tagmanager
self.on_files_changed = None # callback do GUI
self.global_config = load_global_config()
self.folder_configs: dict[Path, dict] = {} # folder -> config
self.current_folder: Path | None = None
self.index: PoolIndex | None = None # unified pool metadata index
self.index: PoolIndex | None = None # unified pool metadata + settings
# ------------------------------------------------------------------
# Pool (single source of truth) and Filmotéka (generated output)
@@ -51,27 +54,75 @@ class FileManager:
pool = self.pool_dir
return pool / POOL_SERIES if pool else None
# ------------------------------------------------------------------
# Pool index + library-level settings (stored in the index, per pool)
# ------------------------------------------------------------------
def _open_index(self, pool: Path) -> PoolIndex:
"""(Re)load the pool index and migrate any legacy global settings."""
self.index = PoolIndex(pool)
self._migrate_global_settings()
return self.index
def _ensure_index(self) -> PoolIndex | None:
"""Return the pool index, opening it lazily when a pool is configured."""
if self.index is None and self.pool_dir:
self._open_index(self.pool_dir)
return self.index
def _migrate_global_settings(self) -> None:
"""Move library settings from the old global config into the index once.
Earlier versions stored ``tag_schema`` / ``copyasis_folders`` in the
global ``.!gtag``. They now belong to the pool; seed the index from the
old values (if present and not already migrated) and drop them from the
global config so it carries app data only.
"""
if self.index is None:
return
moved = False
for key in _LIBRARY_SETTINGS_DEFAULTS:
if key in self.global_config:
if key not in self.index.settings:
self.index.set_setting(key, self.global_config[key])
del self.global_config[key]
moved = True
if moved:
save_global_config(self.global_config)
def _get_setting(self, key: str):
"""Read a library setting from the index (falls back to the default)."""
default = _LIBRARY_SETTINGS_DEFAULTS[key]
idx = self._ensure_index()
if idx is None:
return default
return idx.get_setting(key, default)
def _set_setting(self, key: str, value) -> None:
"""Persist a library setting into the index (needs a configured pool)."""
idx = self._ensure_index()
if idx is None:
raise RuntimeError("Pool není nastaven — není kam uložit nastavení.")
idx.set_setting(key, value)
@property
def copyasis_folders(self) -> list[str]:
"""Names of pool subfolders mirrored 1:1 (copy-as-is) into the output."""
return self.global_config.get("copyasis_folders", [POOL_SERIES])
return self._get_setting("copyasis_folders")
def set_copyasis_folders(self, names: list[str]) -> None:
"""Set the copy-as-is subfolder list and persist it."""
cleaned = [n.strip() for n in names if n.strip()]
self.global_config["copyasis_folders"] = cleaned
save_global_config(self.global_config)
self._set_setting("copyasis_folders", cleaned)
@property
def tag_schema(self) -> list[dict]:
"""Tag categories + ČSFD/Filmotéka rules (see config.DEFAULT_TAG_SCHEMA)."""
from src.core.config import DEFAULT_TAG_SCHEMA
return self.global_config.get("tag_schema", DEFAULT_TAG_SCHEMA)
return self._get_setting("tag_schema")
def set_tag_schema(self, schema: list[dict]) -> None:
"""Set the tag schema and persist it."""
self.global_config["tag_schema"] = schema
save_global_config(self.global_config)
self._set_setting("tag_schema", schema)
def filmoteka_category_roots(self) -> dict[str, str]:
"""Category → output root-folder map derived from the tag schema.
@@ -131,7 +182,7 @@ class FileManager:
if not (movies and movies.is_dir() and pool):
return
self.index = PoolIndex(pool)
self._open_index(pool)
for each in list_files(movies):
if each.name.endswith(METADATA_SUFFIXES):
continue
@@ -252,7 +303,7 @@ class FileManager:
movies.mkdir(parents=True, exist_ok=True)
if self.index is None:
self.index = PoolIndex(pool)
self._open_index(pool)
source = Path(source)
safe_title = title.strip() or source.stem
@@ -325,80 +376,6 @@ class FileManager:
self.on_files_changed(self.filelist)
return file_obj
def append(self, folder: Path) -> None:
"""Add a folder to scan for files"""
self.folders.append(folder)
self.current_folder = folder
# Update global config with last folder
self.global_config["last_folder"] = str(folder)
# Update recent folders list
recent = self.global_config.get("recent_folders", [])
folder_str = str(folder)
if folder_str in recent:
recent.remove(folder_str)
recent.insert(0, folder_str)
self.global_config["recent_folders"] = recent[:10] # Keep max 10
save_global_config(self.global_config)
# Load folder-specific config
folder_config = load_folder_config(folder)
self.folder_configs[folder] = folder_config
# Get ignore patterns from folder config
ignore_patterns = folder_config.get("ignore_patterns", [])
for each in list_files(folder):
# Skip all Curator metadata files (.!tag / .!ftag / .!gtag / .!index)
if each.name.endswith(METADATA_SUFFIXES):
continue
full_path = each.as_posix()
# Check against ignore patterns
if any(
fnmatch.fnmatch(each.name, pat) or fnmatch.fnmatch(full_path, pat)
for pat in ignore_patterns
):
continue
file_obj = File(each, self.tagmanager)
self.filelist.append(file_obj)
def get_folder_config(self, folder: Path = None) -> dict:
"""Get config for a folder (or current folder if not specified)"""
if folder is None:
folder = self.current_folder
if folder is None:
return {}
if folder not in self.folder_configs:
self.folder_configs[folder] = load_folder_config(folder)
return self.folder_configs[folder]
def save_folder_config(self, folder: Path = None, config: dict = None):
"""Save config for a folder"""
if folder is None:
folder = self.current_folder
if folder is None:
return
if config is None:
config = self.folder_configs.get(folder, {})
self.folder_configs[folder] = config
save_folder_config(folder, config)
def set_ignore_patterns(self, patterns: list[str], folder: Path = None):
"""Set ignore patterns for a folder"""
config = self.get_folder_config(folder)
config["ignore_patterns"] = patterns
self.save_folder_config(folder, config)
def get_ignore_patterns(self, folder: Path = None) -> list[str]:
"""Get ignore patterns for a folder"""
config = self.get_folder_config(folder)
return config.get("ignore_patterns", [])
def assign_tag_to_file_objects(self, files_objs: list[File], tag):
"""Přiřadí tag (Tag nebo 'category/name' string) ke každému souboru v seznamu."""
for f in files_objs:
@@ -460,9 +437,3 @@ class FileManager:
if all(tag in file_tags for tag in target_full_paths):
filtered.append(f)
return filtered
# Legacy property for backwards compatibility
@property
def config(self):
"""Legacy: returns global config"""
return self.global_config