Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping
This commit is contained in:
+144
-23
@@ -1,48 +1,68 @@
|
||||
import json
|
||||
|
||||
from src.core.pool_index import PoolIndex, INDEX_FILENAME
|
||||
from src.core.pool_index import PoolIndex, INDEX_FILENAME, INDEX_VERSION
|
||||
from src.core.file import File
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
|
||||
class TestPoolIndex:
|
||||
"""Testy pro unifikovaný metadata index poolu"""
|
||||
"""Testy pro unifikovaný metadata index poolu (v2 — klíč = identita filmu)"""
|
||||
|
||||
def test_empty_index_when_no_file(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
assert index.records == {}
|
||||
assert index.get(tmp_path / "Filmy" / "x.mkv") is None
|
||||
assert index.get_movie("tmdb-603") is None
|
||||
|
||||
def test_set_and_get_by_relative_key(self, tmp_path):
|
||||
def test_set_and_get_movie_by_identity(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
path = tmp_path / "Filmy" / "Matrix.mkv"
|
||||
index.set(path, {"title": "Matrix"})
|
||||
index.set_movie("tmdb-603", {"title": "Matrix", "media": []})
|
||||
|
||||
assert index.get(path) == {"title": "Matrix"}
|
||||
# key is the pool-relative POSIX path
|
||||
assert "Filmy/Matrix.mkv" in index.records
|
||||
assert index.get_movie("tmdb-603") == {"title": "Matrix", "media": []}
|
||||
assert "tmdb-603" in index.records
|
||||
|
||||
def test_set_persists_to_disk(self, tmp_path):
|
||||
def test_set_persists_to_disk_with_version(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
index.set(tmp_path / "Filmy" / "A.mkv", {"title": "A"})
|
||||
index.set_movie("tmdb-603", {"title": "Matrix", "media": []})
|
||||
|
||||
on_disk = json.loads((tmp_path / INDEX_FILENAME).read_text(encoding="utf-8"))
|
||||
assert on_disk["movies"]["Filmy/A.mkv"]["title"] == "A"
|
||||
assert on_disk["version"] == INDEX_VERSION
|
||||
assert on_disk["movies"]["tmdb-603"]["title"] == "Matrix"
|
||||
|
||||
def test_reload_reads_existing_index(self, tmp_path):
|
||||
PoolIndex(tmp_path).set(tmp_path / "Filmy" / "A.mkv", {"title": "A"})
|
||||
PoolIndex(tmp_path).set_movie("tmdb-603", {"title": "Matrix", "media": []})
|
||||
|
||||
reloaded = PoolIndex(tmp_path)
|
||||
assert reloaded.get(tmp_path / "Filmy" / "A.mkv") == {"title": "A"}
|
||||
assert reloaded.get_movie("tmdb-603")["title"] == "Matrix"
|
||||
|
||||
def test_delete_removes_record(self, tmp_path):
|
||||
def test_delete_movie_removes_record(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
path = tmp_path / "Filmy" / "A.mkv"
|
||||
index.set(path, {"title": "A"})
|
||||
index.delete(path)
|
||||
index.set_movie("tmdb-603", {"title": "Matrix", "media": []})
|
||||
index.delete_movie("tmdb-603")
|
||||
|
||||
assert index.get(path) is None
|
||||
assert PoolIndex(tmp_path).get(path) is None
|
||||
assert index.get_movie("tmdb-603") is None
|
||||
assert PoolIndex(tmp_path).get_movie("tmdb-603") is None
|
||||
|
||||
def test_rekey_movie(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
index.set_movie("name-matrix-1999", {"title": "Matrix", "media": []})
|
||||
|
||||
index.rekey_movie("name-matrix-1999", "tmdb-603")
|
||||
|
||||
assert index.get_movie("name-matrix-1999") is None
|
||||
assert index.get_movie("tmdb-603")["title"] == "Matrix"
|
||||
|
||||
def test_find_movie_by_media(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
index.set_movie("tmdb-603", {
|
||||
"title": "Matrix",
|
||||
"media": [{"role": "video", "label": "", "file": "Filmy/Matrix.mkv"}]})
|
||||
|
||||
found = index.find_movie_by_media(tmp_path / "Filmy" / "Matrix.mkv")
|
||||
assert found is not None
|
||||
key, record = found
|
||||
assert key == "tmdb-603"
|
||||
assert record["title"] == "Matrix"
|
||||
assert index.find_movie_by_media(tmp_path / "Filmy" / "Jiny.mkv") is None
|
||||
|
||||
def test_corrupt_index_loads_empty(self, tmp_path):
|
||||
(tmp_path / INDEX_FILENAME).write_text("{ not json", encoding="utf-8")
|
||||
@@ -50,8 +70,89 @@ class TestPoolIndex:
|
||||
assert index.records == {}
|
||||
|
||||
|
||||
class TestV1Migration:
|
||||
"""Starý path-klíčovaný index se převede na identity-klíčovaný (bez hýbání soubory)"""
|
||||
|
||||
def _write_v1(self, tmp_path, movies: dict) -> None:
|
||||
(tmp_path / INDEX_FILENAME).write_text(
|
||||
json.dumps({"settings": {"tag_schema": []}, "movies": movies}),
|
||||
encoding="utf-8")
|
||||
|
||||
def test_flat_movie_becomes_identity_keyed_record(self, tmp_path):
|
||||
self._write_v1(tmp_path, {
|
||||
"Filmy/Matrix (1999) [tmdbid-603].mkv": {
|
||||
"title": "Matrix", "tags": ["Žánr/Akční"],
|
||||
"csfd_url": "https://www.csfd.cz/film/9499/",
|
||||
"date": "1999-03-31", "added": "2026-01-01T00:00:00",
|
||||
"attributes": {"collection_sort": "01"},
|
||||
}})
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
record = index.get_movie("tmdb-603")
|
||||
assert record is not None
|
||||
assert record["title"] == "Matrix"
|
||||
assert record["year"] == "1999"
|
||||
assert record["tmdb_id"] == 603
|
||||
assert record["library"] == "Filmy"
|
||||
assert record["media"] == [
|
||||
{"role": "video", "label": "",
|
||||
"file": "Filmy/Matrix (1999) [tmdbid-603].mkv"}]
|
||||
assert record["tags"] == ["Žánr/Akční"]
|
||||
assert record["csfd_url"] == "https://www.csfd.cz/film/9499/"
|
||||
assert record["attributes"] == {"collection_sort": "01"}
|
||||
|
||||
def test_grouped_versions_merge_into_one_record(self, tmp_path):
|
||||
base = "Filmy/Avatar (2009) [tmdbid-19995]"
|
||||
self._write_v1(tmp_path, {
|
||||
f"{base}/Avatar (2009) [tmdbid-19995] - Výchozí.mkv": {"title": "Avatar"},
|
||||
f"{base}/Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv": {"title": "Avatar"},
|
||||
})
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
assert len(index.all_movies()) == 1
|
||||
record = index.get_movie("tmdb-19995")
|
||||
assert {m["label"] for m in record["media"]} == {"Výchozí", "3D HSBS"}
|
||||
assert record["library"] == "Filmy"
|
||||
|
||||
def test_movie_without_tmdbid_gets_slug_key(self, tmp_path):
|
||||
self._write_v1(tmp_path, {
|
||||
"Filmy/Pelíšky (1999).mkv": {"title": "Pelíšky"}})
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
assert index.get_movie("name-pelisky-1999") is not None
|
||||
|
||||
def test_migration_backs_up_and_bumps_version(self, tmp_path):
|
||||
self._write_v1(tmp_path, {"Filmy/A (2000).mkv": {"title": "A"}})
|
||||
|
||||
PoolIndex(tmp_path)
|
||||
|
||||
backup = tmp_path / (INDEX_FILENAME + ".v1.bak")
|
||||
assert backup.exists()
|
||||
assert "Filmy/A (2000).mkv" in json.loads(backup.read_text(encoding="utf-8"))["movies"]
|
||||
on_disk = json.loads((tmp_path / INDEX_FILENAME).read_text(encoding="utf-8"))
|
||||
assert on_disk["version"] == INDEX_VERSION
|
||||
|
||||
def test_migration_keeps_settings(self, tmp_path):
|
||||
self._write_v1(tmp_path, {"Filmy/A (2000).mkv": {"title": "A"}})
|
||||
index = PoolIndex(tmp_path)
|
||||
assert index.get_setting("tag_schema") == []
|
||||
|
||||
def test_v2_index_is_not_migrated_again(self, tmp_path):
|
||||
(tmp_path / INDEX_FILENAME).write_text(json.dumps({
|
||||
"version": INDEX_VERSION, "settings": {},
|
||||
"movies": {"tmdb-1": {"title": "X", "media": []}}}), encoding="utf-8")
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
assert index.get_movie("tmdb-1")["title"] == "X"
|
||||
assert not (tmp_path / (INDEX_FILENAME + ".v1.bak")).exists()
|
||||
|
||||
|
||||
class TestFileWithIndex:
|
||||
"""File používá index místo sidecar souboru, je-li injektován"""
|
||||
"""File je pohled na jeden media záznam filmu v indexu"""
|
||||
|
||||
def test_index_backed_file_writes_no_sidecar(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
@@ -62,7 +163,7 @@ class TestFileWithIndex:
|
||||
f = File(movie, TagManager(), index=index)
|
||||
|
||||
assert not (movie.parent / ".Matrix.mkv.!tag").exists() # no sidecar
|
||||
assert index.get(movie) is not None # record created in index
|
||||
assert index.find_movie_by_media(movie) is not None # record created
|
||||
assert f.tags == [] # no automatic tags
|
||||
|
||||
def test_index_backed_metadata_persists_across_reload(self, tmp_path):
|
||||
@@ -94,4 +195,24 @@ class TestFileWithIndex:
|
||||
|
||||
f = File(movie, TagManager(), index=index)
|
||||
f.delete_metadata()
|
||||
assert index.get(movie) is None
|
||||
assert index.find_movie_by_media(movie) is None
|
||||
|
||||
def test_two_versions_share_one_movie_record(self, tmp_path):
|
||||
"""Obě verze čtou stejná metadata — to je smysl identity klíče."""
|
||||
index = PoolIndex(tmp_path)
|
||||
folder = tmp_path / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
||||
folder.mkdir(parents=True)
|
||||
a = folder / "Avatar (2009) [tmdbid-19995] - Výchozí.mkv"
|
||||
b = folder / "Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv"
|
||||
a.write_bytes(b"x")
|
||||
b.write_bytes(b"y")
|
||||
|
||||
tm = TagManager()
|
||||
fa = File(a, tm, index=index)
|
||||
fa.title = "Avatar"
|
||||
fa.add_tag("Žánr/Sci-Fi")
|
||||
fb = File(b, tm, index=index)
|
||||
|
||||
assert len(index.all_movies()) == 1
|
||||
assert fb.title == "Avatar"
|
||||
assert "Žánr/Sci-Fi" in {t.full_path for t in fb.tags}
|
||||
|
||||
Reference in New Issue
Block a user