Add edit dialogs, library organizer, and TMDb pool migration

This commit is contained in:
2026-09-08 07:37:40 +02:00
parent 39712d7283
commit 79c9e34a5d
10 changed files with 1371 additions and 110 deletions
+137 -1
View File
@@ -427,6 +427,92 @@ class TestPoolManagement:
assert movie.file_path.name == "Matrix (1999) [tmdbid-603].mkv"
assert movie.title == "Matrix" # clean title, no year/id
def test_libraries_default_and_derivation(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
assert file_manager.libraries == [
{"name": "Filmy", "kind": "movie"},
{"name": "Seriály", "kind": "series"},
]
assert file_manager.movie_libraries == ["Filmy"]
assert file_manager.copyasis_folders == ["Seriály"]
def test_set_libraries_with_all_kinds(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
file_manager.set_libraries([
{"name": "Hlavní", "kind": "mixed"},
{"name": "Dokumenty", "kind": "mixed"},
{"name": "Anime", "kind": "series"},
{"name": "Chajda", "kind": "homevideo"},
])
assert file_manager.copyasis_folders == ["Anime"]
assert file_manager.movie_libraries == ["Filmy"] # none of kind movie → fallback
assert file_manager.libraries_of_kind("mixed") == ["Hlavní", "Dokumenty"]
assert file_manager.libraries_of_kind("homevideo") == ["Chajda"]
def test_set_pool_dir_creates_all_library_folders(self, file_manager, tmp_path):
pool = tmp_path / "pool"
file_manager.set_pool_dir(pool)
file_manager.set_libraries([
{"name": "Hlavní", "kind": "mixed"},
{"name": "Chajda", "kind": "homevideo"},
])
file_manager.set_pool_dir(pool) # re-run creates the newly defined folders
assert (pool / "Hlavní").is_dir()
assert (pool / "Chajda").is_dir()
def test_mixed_library_loads_movies_not_episodes(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
hlavni = tmp_path / "pool" / "Hlavní"
hlavni.mkdir(parents=True, exist_ok=True)
(hlavni / "Matrix (1999) [tmdbid-603].mkv").write_bytes(b"x") # movie (top level)
ep = hlavni / "Breaking Bad (2008) [tmdbid-1396]" / "Season 01"
ep.mkdir(parents=True)
(ep / "S01E01 - pilot.mkv").write_bytes(b"y") # series episode (subfolder)
file_manager.load_pool_movies()
names = [f.filename for f in file_manager.filelist]
assert names == ["Matrix (1999) [tmdbid-603].mkv"] # episode not loaded as movie
assert file_manager.library_of(file_manager.filelist[0]) == "Hlavní"
def test_libraries_accepting_movies(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
file_manager.set_libraries([
{"name": "Filmy", "kind": "movie"},
{"name": "Hlavní", "kind": "mixed"},
{"name": "Anime", "kind": "series"},
])
assert file_manager.libraries_accepting_movies == ["Filmy", "Hlavní"]
def test_orphan_folder_cleanup(self, file_manager, tmp_path):
pool = tmp_path / "pool"
file_manager.set_pool_dir(pool)
file_manager.set_libraries([{"name": "Filmy", "kind": "movie"}])
(pool / "OldKids").mkdir() # empty orphan
(pool / "OldStuff").mkdir()
(pool / "OldStuff" / "film.mkv").write_bytes(b"x") # non-empty orphan
orphans = dict(file_manager.orphan_pool_folders())
assert orphans.get("OldKids") is True
assert orphans.get("OldStuff") is False
assert "Filmy" not in orphans # a configured library is not an orphan
assert file_manager.remove_empty_pool_folder("OldKids") is True
assert not (pool / "OldKids").exists()
assert file_manager.remove_empty_pool_folder("OldStuff") is False # non-empty
assert file_manager.remove_empty_pool_folder("Filmy") is False # a library
def test_move_movie_into_mixed_library(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
file_manager.set_libraries([
{"name": "Filmy", "kind": "movie"},
{"name": "Hlavní", "kind": "mixed"},
])
(tmp_path / "a.mkv").write_bytes(b"x")
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
file_manager.move_to_library(movie, "Hlavní")
assert file_manager.library_of(movie) == "Hlavní"
def test_import_into_named_library(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
file_manager.set_movie_libraries(["Filmy", "Kids"])
@@ -490,6 +576,52 @@ class TestPoolManagement:
assert len(reloaded.filelist) == 1
assert reloaded.filelist[0].file_path.suffix == ".mkv"
def test_apply_movie_edits_all_at_once(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
file_manager.set_movie_libraries(["Filmy", "Kids"])
(tmp_path / "a.mkv").write_bytes(b"x")
movie = file_manager.import_movie(tmp_path / "a.mkv", "Puvodni", year=2000)
file_manager.apply_movie_edits(
movie, title="Auta", year=2006, tmdb_id=920, library="Kids",
date="2026-01-02", attributes={"collection_sort": "01"},
tags=["Žánr/Animovaný", "Sbírka/Oblíbené"])
assert movie.file_path == tmp_path / "pool" / "Kids" / "Auta (2006) [tmdbid-920].mkv"
assert movie.title == "Auta"
assert file_manager.library_of(movie) == "Kids"
assert movie.date == "2026-01-02"
assert movie.attributes == {"collection_sort": "01"}
assert {t.full_path for t in movie.tags} == {"Žánr/Animovaný", "Sbírka/Oblíbené"}
def test_import_and_edit_store_csfd_url(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
(tmp_path / "a.mkv").write_bytes(b"x")
movie = file_manager.import_movie(
tmp_path / "a.mkv", "Matrix", year=1999, tmdb_id=603,
csfd_url="https://www.csfd.cz/film/9499/")
assert movie.csfd_url == "https://www.csfd.cz/film/9499/"
# persists across reload
reloaded = FileManager(TagManager())
reloaded.set_pool_dir(tmp_path / "pool")
reloaded.load_pool_movies()
assert reloaded.filelist[0].csfd_url == "https://www.csfd.cz/film/9499/"
# editable via apply_movie_edits
file_manager.apply_movie_edits(movie, title="Matrix", year=1999,
csfd_url="https://www.csfd.cz/film/9499-matrix/")
assert movie.csfd_url == "https://www.csfd.cz/film/9499-matrix/"
def test_apply_movie_edits_rejects_name_clash(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
(tmp_path / "a.mkv").write_bytes(b"x")
(tmp_path / "b.mkv").write_bytes(b"y")
file_manager.import_movie(tmp_path / "a.mkv", "Solaris", year=1972)
other = file_manager.import_movie(tmp_path / "b.mkv", "Jiny", year=2002)
with pytest.raises(FileExistsError):
file_manager.apply_movie_edits(other, title="Solaris", year=1972)
def test_rename_to_canonical_preserves_tmdb_id(self, file_manager, tmp_path):
file_manager.set_pool_dir(tmp_path / "pool")
(tmp_path / "raw.mkv").write_bytes(b"x")
@@ -581,7 +713,11 @@ class TestPoolManagement:
file_manager.set_copyasis_folders(["Seriály", "Dokumenty"])
assert "copyasis_folders" not in file_manager.global_config
assert file_manager.index.get_setting("copyasis_folders") == ["Seriály", "Dokumenty"]
# series libraries now persist in the unified "libraries" setting (in the index)
stored = file_manager.index.get_setting("libraries")
assert stored is not None
assert [lib["name"] for lib in stored if lib["kind"] == "series"] == \
["Seriály", "Dokumenty"]
# a fresh manager over the same pool reads the stored settings back
reloaded = FileManager(TagManager())
+95
View File
@@ -0,0 +1,95 @@
"""Tests for pool → TMDb migration (src.core.migrate + FileManager)."""
import pytest
from src.core.migrate import classify, normalize
from src.core.tmdb import TmdbResult
def test_normalize_folds_diacritics_and_punct():
assert normalize("Šíleně smutná princezna!") == "silene smutna princezna"
assert normalize("Amélie z Montmartru") == "amelie z montmartru"
def test_classify_auto_on_exact_unique():
cands = [TmdbResult(603, "Matrix", "1999", "movie")]
status, chosen = classify("Matrix", "1999", cands)
assert status == "auto" and chosen.tmdb_id == 603
def test_classify_auto_disambiguates_by_year():
cands = [
TmdbResult(1, "Solaris", "1972", "movie"),
TmdbResult(2, "Solaris", "2002", "movie"),
]
# local year 1972 → unique exact-year match → auto picks the 1972 one
assert classify("Solaris", "1972", cands) == ("auto", cands[0])
def test_classify_ambiguous_when_year_matches_none():
cands = [
TmdbResult(1, "Solaris", "1972", "movie"),
TmdbResult(2, "Solaris", "2002", "movie"),
]
status, chosen = classify("Solaris", "1990", cands)
# year close to neither → no unique exact-year → ambiguous (top hit preselected)
assert status == "ambiguous" and chosen.tmdb_id == 1
def test_classify_ambiguous_without_year():
cands = [TmdbResult(603, "Matrix", "1999", "movie")]
status, _ = classify("Matrix", None, cands)
assert status == "ambiguous" # no local year → user confirms
def test_classify_no_match():
assert classify("Nic", "2000", []) == ("no_match", None)
class TestFileManagerMigrate:
@pytest.fixture
def file_manager(self, tmp_path, monkeypatch):
import src.core.config as config_module
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", tmp_path / ".gtag")
from src.core.tag_manager import TagManager
from src.core.file_manager import FileManager
fm = FileManager(TagManager())
fm.set_pool_dir(tmp_path / "pool")
return fm
def test_match_and_assign(self, file_manager, tmp_path, monkeypatch):
(tmp_path / "a.mkv").write_bytes(b"x")
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
assert "[tmdbid" not in movie.file_path.name # not migrated yet
import src.core.tmdb as tmdb
monkeypatch.setattr(
tmdb, "search_movie",
lambda q, year=None, limit=5: [TmdbResult(603, "Matrix", "1999", "movie")])
proposals = file_manager.match_pool_to_tmdb()
assert len(proposals) == 1
p = proposals[0]
assert p.status == "auto" and p.chosen.tmdb_id == 603
file_manager.assign_tmdb_id(p.file, p.chosen.tmdb_id, p.chosen.year)
assert movie.file_path.name == "Matrix (1999) [tmdbid-603].mkv"
def test_match_skips_already_migrated(self, file_manager, tmp_path, monkeypatch):
(tmp_path / "a.mkv").write_bytes(b"x")
file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999, tmdb_id=603)
import src.core.tmdb as tmdb
called = []
monkeypatch.setattr(
tmdb, "search_movie",
lambda q, year=None, limit=5: called.append(q) or [])
proposals = file_manager.match_pool_to_tmdb()
assert proposals == [] and called == [] # already has id → not searched
def test_assign_fills_missing_year_from_tmdb(self, file_manager, tmp_path):
(tmp_path / "a.mkv").write_bytes(b"x")
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix") # no year
assert movie.file_path.name == "Matrix.mkv"
file_manager.assign_tmdb_id(movie, 603, "1999") # TMDb supplies the year
assert movie.file_path.name == "Matrix (1999) [tmdbid-603].mkv"
+43 -9
View File
@@ -29,8 +29,13 @@ def test_season_folder_name():
assert season_folder_name(12) == "Season 12"
def test_episode_filename():
assert episode_filename("Breaking Bad", 1, 2, ".mkv") == "Breaking Bad S01E02.mkv"
def test_episode_filename_keeps_original_name():
assert episode_filename(1, 2, "bb.pilot.hdtv", ".mkv") == "S01E02 - bb.pilot.hdtv.mkv"
# already-canonical stem → no doubled prefix
assert episode_filename(1, 2, "S01E02 - bb.pilot.hdtv", ".mkv") == \
"S01E02 - bb.pilot.hdtv.mkv"
# stem that was only the code → collapses to just the code
assert episode_filename(3, 4, "S03E04", ".mkv") == "S03E04.mkv"
def test_series_folder_stem():
@@ -42,12 +47,12 @@ def test_series_folder_stem():
def test_is_canonical_episode(tmp_path):
folder = tmp_path / "Breaking Bad (2008) [tmdbid-1396]" / "Season 01"
folder.mkdir(parents=True)
good = folder / "Breaking Bad S01E02.mkv"
good = folder / "S01E02 - bb.pilot.mkv"
good.write_bytes(b"x")
bad = folder / "bb.s01e02.mkv"
bad.write_bytes(b"x")
assert is_canonical_episode(good, "Breaking Bad")
assert not is_canonical_episode(bad, "Breaking Bad")
assert is_canonical_episode(good)
assert not is_canonical_episode(bad)
def test_scan_series(tmp_path):
@@ -89,8 +94,22 @@ class TestFileManagerSeries:
assert result["imported"] == 2
base = (tmp_path / "pool" / "Seriály"
/ "Breaking Bad (2008) [tmdbid-1396]" / "Season 01")
assert (base / "Breaking Bad S01E01.mkv").exists()
assert (base / "Breaking Bad S01E02.mkv").exists()
assert (base / "S01E01 - bb.mkv").exists()
assert (base / "S01E02 - bb.mkv").exists()
def test_import_episodes_into_mixed_library(self, file_manager, tmp_path):
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
result = file_manager.import_episodes(
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008, tmdb_id=1396,
library="Hlavní")
assert result["imported"] == 1
assert (tmp_path / "pool" / "Hlavní" / "Breaking Bad (2008) [tmdbid-1396]"
/ "Season 01" / "S01E01 - bb.mkv").exists()
# a mixed library is scanned for series too
found = file_manager.load_series()
assert [s.title for s in found] == ["Breaking Bad"]
def test_import_episodes_skips_uncoded(self, file_manager, tmp_path):
(tmp_path / "random.mkv").write_bytes(b"x")
@@ -110,5 +129,20 @@ class TestFileManagerSeries:
assert len(series_list) == 1
result = file_manager.rename_episodes_to_canonical(series_list[0])
assert result["renamed"] == [("bb.1x03.HDTV.mkv", "Breaking Bad S01E03.mkv")]
assert (folder / "Season 01" / "Breaking Bad S01E03.mkv").exists()
assert result["renamed"] == [("bb.1x03.HDTV.mkv", "S01E03 - bb.HDTV.mkv")]
assert (folder / "Season 01" / "S01E03 - bb.HDTV.mkv").exists()
def test_apply_series_edits_renames_folder_and_episodes(self, file_manager, tmp_path):
folder = tmp_path / "pool" / "Seriály" / "Wrong Name"
(folder / "Season 01").mkdir(parents=True)
(folder / "Season 01" / "old S01E01.mkv").write_bytes(b"x")
ser = file_manager.load_series()[0]
result = file_manager.apply_series_edits(
ser, title="Breaking Bad", year=2008, tmdb_id=1396)
new_folder = (tmp_path / "pool" / "Seriály"
/ "Breaking Bad (2008) [tmdbid-1396]")
assert result["folder"] == new_folder
assert (new_folder / "Season 01" / "S01E01 - old.mkv").exists()
assert not folder.exists()