Add edit dialogs, library organizer, and TMDb pool migration
This commit is contained in:
+137
-1
@@ -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())
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user