Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping
This commit is contained in:
+121
-15
@@ -33,15 +33,64 @@ class File:
|
||||
# free-form per-movie attributes (e.g. "collection_sort"); usable in
|
||||
# miniDLNA filename templates via name_context.
|
||||
self.attributes: dict[str, str] = {}
|
||||
# Identity key of the movie record this file belongs to (set on load /
|
||||
# first save); ``None`` only for an index-less, in-memory File.
|
||||
self._movie_key: str | None = None
|
||||
self.get_metadata()
|
||||
|
||||
def _identity_from_path(self) -> tuple[str, str, str | None, int | None, str]:
|
||||
"""``(key, title, year, tmdb_id, label)`` implied by this file's location.
|
||||
|
||||
A file inside its own ``Title (rok) [tmdbid]`` folder is one version of
|
||||
that movie — identity comes from the **folder**, and the rest of its own
|
||||
stem is the version label. A flat file carries both in its own name.
|
||||
"""
|
||||
from .movie import identity_key
|
||||
from .naming import (is_canonical_pool_stem, parse_3d_format,
|
||||
parse_pool_stem, parse_tmdb_id)
|
||||
|
||||
parent = self.file_path.parent.name
|
||||
grouped = is_canonical_pool_stem(parent)
|
||||
identity_stem = parent if grouped else self.file_path.stem
|
||||
parsed = parse_pool_stem(identity_stem)
|
||||
year = str(parsed[1]) if parsed else None
|
||||
tmdb_id = parse_tmdb_id(identity_stem)
|
||||
title = self.title or (parsed[0] if parsed else self.file_path.stem)
|
||||
|
||||
label = ""
|
||||
if grouped and self.file_path.stem.startswith(f"{parent} - "):
|
||||
label = self.file_path.stem[len(parent) + 3:]
|
||||
elif parse_3d_format(self.file_path.stem):
|
||||
label = parse_3d_format(self.file_path.stem) or ""
|
||||
return identity_key(tmdb_id, title, year), title, year, tmdb_id, label
|
||||
|
||||
def get_metadata(self) -> None:
|
||||
record = self.index.get(self.file_path) if self.index is not None else None
|
||||
if record is None:
|
||||
"""Load this file's metadata from the movie record that owns it.
|
||||
|
||||
Since the pool index became identity-keyed (see :mod:`src.core.movie`),
|
||||
a File is a *view* on one media entry of a movie record — every version
|
||||
of the same film therefore reads the same title/tags/ČSFD/… A file the
|
||||
index doesn't know yet joins the movie of the same identity when there
|
||||
is one (that's how a newly added version inherits the shared metadata
|
||||
instead of blanking it), otherwise it starts a new record.
|
||||
"""
|
||||
if self.index is None:
|
||||
self._init_new_metadata()
|
||||
self.save_metadata()
|
||||
else:
|
||||
return
|
||||
|
||||
found = self.index.find_movie_by_media(self.file_path)
|
||||
if found is not None:
|
||||
self._movie_key, record = found
|
||||
self._apply_record(record)
|
||||
return
|
||||
|
||||
self._init_new_metadata()
|
||||
key, *_ = self._identity_from_path()
|
||||
existing = self.index.get_movie(key)
|
||||
if existing is not None:
|
||||
self._movie_key = key
|
||||
self._apply_record(existing)
|
||||
self.save_metadata()
|
||||
|
||||
def _init_new_metadata(self) -> None:
|
||||
self.new = True
|
||||
@@ -101,9 +150,42 @@ class File:
|
||||
self.tags.append(tag)
|
||||
|
||||
def save_metadata(self):
|
||||
"""Persist metadata to the pool index; a no-op for an index-less File."""
|
||||
if self.index is not None:
|
||||
self.index.set(self.file_path, self._build_record())
|
||||
"""Persist metadata into this file's movie record (no-op without index).
|
||||
|
||||
Writes the shared fields onto the movie record and makes sure the
|
||||
record lists this file among its media. A file the index has never seen
|
||||
starts a new record under its own identity key.
|
||||
"""
|
||||
if self.index is None:
|
||||
return
|
||||
from .movie import ROLE_VIDEO
|
||||
|
||||
derived_key, title, year, tmdb_id, label = self._identity_from_path()
|
||||
key = self._movie_key or derived_key
|
||||
record = self.index.get_movie(key) or {}
|
||||
record.update(self._build_record())
|
||||
# The file's actual location is the truth — a move must update this.
|
||||
record["library"] = self._library_name()
|
||||
record["title"] = title
|
||||
record["year"] = year
|
||||
record["tmdb_id"] = tmdb_id
|
||||
|
||||
# Keep this file listed exactly once, with the label its name implies.
|
||||
rel = self.index._key(self.file_path)
|
||||
media = [m for m in record.get("media", []) if m.get("file") != rel]
|
||||
media.append({"role": ROLE_VIDEO, "label": label, "file": rel})
|
||||
record["media"] = media
|
||||
|
||||
self.index.set_movie(key, record)
|
||||
self._movie_key = key
|
||||
|
||||
def _library_name(self) -> str:
|
||||
"""Top-level pool folder this file sits in (its library), or ``""``."""
|
||||
if self.index is None:
|
||||
return ""
|
||||
rel = self.index._key(self.file_path)
|
||||
parts = Path(rel).parts
|
||||
return parts[0] if len(parts) > 1 else ""
|
||||
|
||||
def added_timestamp(self) -> float:
|
||||
"""Epoch seconds for 'date added' — the stored ``added`` or the file mtime.
|
||||
@@ -169,19 +251,43 @@ class File:
|
||||
self.save_metadata()
|
||||
|
||||
def delete_metadata(self) -> None:
|
||||
"""Remove this file's metadata from the pool index (no-op without one)."""
|
||||
if self.index is not None:
|
||||
self.index.delete(self.file_path)
|
||||
"""Drop this file from its movie record; the last one takes the record.
|
||||
|
||||
Removing one version of a multi-version movie leaves the movie (and its
|
||||
metadata) in place — only when its last media entry goes does the whole
|
||||
record go with it.
|
||||
"""
|
||||
if self.index is None:
|
||||
return
|
||||
found = self.index.find_movie_by_media(self.file_path)
|
||||
if found is None:
|
||||
return
|
||||
key, record = found
|
||||
rel = self.index._key(self.file_path)
|
||||
record["media"] = [m for m in record.get("media", []) if m.get("file") != rel]
|
||||
if record["media"]:
|
||||
self.index.set_movie(key, record)
|
||||
else:
|
||||
self.index.delete_movie(key)
|
||||
|
||||
def relocate(self, new_path: Path) -> None:
|
||||
"""Point this File at a new path, moving its metadata along.
|
||||
"""Point this File at a new path, moving its media entry along.
|
||||
|
||||
The physical file must already have been moved/renamed by the caller.
|
||||
Drops the metadata under the old index key and rebinds to the new path;
|
||||
call ``save_metadata()`` afterwards to write it back.
|
||||
The physical file must already have been moved/renamed by the caller;
|
||||
this only repoints the movie record's media entry (and the File), so a
|
||||
rename never detaches a file from its movie.
|
||||
"""
|
||||
if self.index is not None:
|
||||
self.index.delete(self.file_path)
|
||||
found = self.index.find_movie_by_media(self.file_path)
|
||||
if found is not None:
|
||||
key, record = found
|
||||
old_rel = self.index._key(self.file_path)
|
||||
new_rel = self.index._key(Path(new_path))
|
||||
for media in record.get("media", []):
|
||||
if media.get("file") == old_rel:
|
||||
media["file"] = new_rel
|
||||
self.index.set_movie(key, record)
|
||||
self._movie_key = key
|
||||
self.file_path = Path(new_path)
|
||||
self.filename = self.file_path.name
|
||||
|
||||
|
||||
Reference in New Issue
Block a user