Adopt "Title (YYYY)" pool naming and clean Filmotéka link names
This commit is contained in:
+121
-4
@@ -7,6 +7,7 @@ from .tag_manager import TagManager
|
||||
from .pool_index import PoolIndex
|
||||
from .utils import list_files
|
||||
from .integrity import check_video_integrity, ffmpeg_available, IntegrityResult
|
||||
from .naming import canonical_pool_stem, is_valid_year, parse_pool_stem
|
||||
from typing import Callable, Iterable
|
||||
from src.core.config import (
|
||||
load_global_config, save_global_config, DEFAULT_TAG_SCHEMA
|
||||
@@ -326,8 +327,14 @@ class FileManager:
|
||||
def import_movie(
|
||||
self, source: Path, title: str, csfd_link: str | None = None,
|
||||
move: bool = False, on_conflict: str = "suffix",
|
||||
year: int | str | None = None,
|
||||
) -> File | None:
|
||||
"""Bring a video file into pool/Filmy as 'Title.ext' and index its metadata.
|
||||
"""Bring a video file into pool/Filmy and index its metadata.
|
||||
|
||||
The pooled filename follows the naming convention ``Title (YYYY).ext``
|
||||
when a ``year`` is given (validated to four digits); without a year it
|
||||
falls back to ``Title.ext``. The GUI import always supplies a year, so a
|
||||
library built through the app is uniformly ``Title (YYYY).ext``.
|
||||
|
||||
By default the original is **copied** (non-destructive). With ``move=True``
|
||||
the source file is moved into the pool instead, leaving nothing behind.
|
||||
@@ -339,6 +346,9 @@ class FileManager:
|
||||
- ``"replace"``: evict the existing same-named movie(s) (file + metadata)
|
||||
and import the new one under the plain name.
|
||||
- ``"skip"``: do not import; return ``None``.
|
||||
|
||||
Raises:
|
||||
ValueError: a ``year`` was given but is not a four-digit number.
|
||||
"""
|
||||
movies = self.movies_dir
|
||||
pool = self.pool_dir
|
||||
@@ -351,9 +361,11 @@ class FileManager:
|
||||
|
||||
source = Path(source)
|
||||
safe_title = title.strip() or source.stem
|
||||
target = movies / f"{safe_title}{source.suffix}"
|
||||
has_year = year is not None and str(year).strip() != ""
|
||||
stem = canonical_pool_stem(safe_title, year) if has_year else safe_title
|
||||
target = movies / f"{stem}{source.suffix}"
|
||||
|
||||
existing = self.pooled_with_stem(safe_title)
|
||||
existing = self.pooled_with_stem(stem)
|
||||
conflict = bool(existing) or target.exists()
|
||||
|
||||
if conflict and on_conflict == "skip":
|
||||
@@ -367,7 +379,7 @@ class FileManager:
|
||||
# "suffix": never clobber an existing exact filename
|
||||
counter = 1
|
||||
while target.exists():
|
||||
target = movies / f"{safe_title}_{counter}{source.suffix}"
|
||||
target = movies / f"{stem}_{counter}{source.suffix}"
|
||||
counter += 1
|
||||
|
||||
if move:
|
||||
@@ -420,6 +432,111 @@ class FileManager:
|
||||
self.on_files_changed(self.filelist)
|
||||
return file_obj
|
||||
|
||||
def _canonical_name_for(self, file_obj: File) -> str | None:
|
||||
"""The ``Title (YYYY)`` stem this movie should have, or ``None``.
|
||||
|
||||
Uses the clean title and the year from the movie's metadata
|
||||
(``name_context``: ČSFD cache → ``Rok`` tag → the current filename). A
|
||||
``(YYYY)`` that leaked into the title is stripped so it isn't doubled.
|
||||
Returns ``None`` when no four-digit year is known.
|
||||
"""
|
||||
ctx = file_obj.name_context()
|
||||
title = (file_obj.title or ctx.get("title") or file_obj.file_path.stem).strip()
|
||||
year = str(ctx.get("year") or "").strip()
|
||||
|
||||
parsed = parse_pool_stem(title)
|
||||
if parsed is not None:
|
||||
title = parsed[0]
|
||||
year = year or str(parsed[1])
|
||||
|
||||
title = title.replace("/", "-").replace("\\", "-").strip()
|
||||
if not (title and is_valid_year(year)):
|
||||
return None
|
||||
return canonical_pool_stem(title, year)
|
||||
|
||||
def rename_to_canonical(self, file_obj: File) -> File | None:
|
||||
"""Rename a pooled movie to ``Title (YYYY).ext`` from its metadata.
|
||||
|
||||
The year comes from the movie's ČSFD metadata (falling back to a ``Rok``
|
||||
tag or the existing filename). ``file_obj.title`` keeps the clean title
|
||||
(without the year). Does not fire ``on_files_changed`` — the caller
|
||||
refreshes once after a batch.
|
||||
|
||||
Returns:
|
||||
The file when renamed (or already canonical); ``None`` when no
|
||||
four-digit year is known, so no canonical name can be built.
|
||||
|
||||
Raises:
|
||||
FileExistsError: another pooled file already uses that name.
|
||||
"""
|
||||
new_stem = self._canonical_name_for(file_obj)
|
||||
if new_stem is None:
|
||||
return None
|
||||
|
||||
old_path = file_obj.file_path
|
||||
new_path = old_path.with_name(f"{new_stem}{old_path.suffix}")
|
||||
if new_path == old_path:
|
||||
return file_obj # already canonical
|
||||
if new_path.exists():
|
||||
raise FileExistsError(f"Soubor „{new_path.name}“ už v poolu existuje.")
|
||||
|
||||
parsed = parse_pool_stem(new_stem)
|
||||
old_path.rename(new_path)
|
||||
file_obj.relocate(new_path)
|
||||
if parsed is not None:
|
||||
file_obj.title = parsed[0]
|
||||
file_obj.save_metadata()
|
||||
return file_obj
|
||||
|
||||
def rename_all_to_canonical(
|
||||
self,
|
||||
files: list[File],
|
||||
on_progress: Callable[[int, int, File], None] | None = None,
|
||||
should_cancel: Callable[[], bool] | None = None,
|
||||
) -> dict:
|
||||
"""Rename each movie to ``Title (YYYY).ext`` from its metadata.
|
||||
|
||||
Skips files already canonical and those without a known year. Reports
|
||||
conflicts (target name taken) without aborting the rest.
|
||||
|
||||
Returns a dict with:
|
||||
renamed — (old_name, new_name) pairs actually renamed
|
||||
skipped_no_year — filenames with no four-digit year available
|
||||
conflicts — (old_name, message) where the target name was taken
|
||||
unchanged — count already in canonical form
|
||||
cancelled — True if stopped early
|
||||
"""
|
||||
renamed: list[tuple[str, str]] = []
|
||||
skipped_no_year: list[str] = []
|
||||
conflicts: list[tuple[str, str]] = []
|
||||
unchanged = 0
|
||||
cancelled = False
|
||||
total = len(files)
|
||||
for i, f in enumerate(files, 1):
|
||||
if should_cancel is not None and should_cancel():
|
||||
cancelled = True
|
||||
break
|
||||
old_name = f.filename
|
||||
try:
|
||||
result = self.rename_to_canonical(f)
|
||||
except FileExistsError as exc:
|
||||
conflicts.append((old_name, str(exc)))
|
||||
else:
|
||||
if result is None:
|
||||
skipped_no_year.append(old_name)
|
||||
elif f.filename != old_name:
|
||||
renamed.append((old_name, f.filename))
|
||||
else:
|
||||
unchanged += 1
|
||||
if on_progress is not None:
|
||||
on_progress(i, total, f)
|
||||
|
||||
if renamed and self.on_files_changed:
|
||||
self.on_files_changed(self.filelist)
|
||||
return {"renamed": renamed, "skipped_no_year": skipped_no_year,
|
||||
"conflicts": conflicts, "unchanged": unchanged,
|
||||
"cancelled": cancelled}
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user