feat: profile support for independent marker sets

Each profile has its own set of timeline markers, so the same video
can be cut with different settings (e.g. landscape vs portrait) without
markers interfering. Profile selector in the top bar, persisted in
QSettings, stored per-row in the DB.

- Add `profile` column to ProcessedDB schema (migrates existing rows
  to 'default')
- Scope find_similar, get_markers, _get_markers_for by profile
- Add get_profiles() for populating the combo dropdown
- Thread profile through _DBWorker, _after_load, _refresh_markers,
  _on_clip_done, dropEvent, _on_open_files
- Editable profile QComboBox in top bar, refreshed after each export
- 5 new tests for profile isolation and backward compatibility (54 total)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:08:50 +02:00
parent 462af36bce
commit f8b148f77d
3 changed files with 138 additions and 21 deletions
+68
View File
@@ -344,3 +344,71 @@ def test_db_get_group_disabled():
def test_db_delete_group_disabled():
db = ProcessedDB("/no/such/directory/8cut.db")
assert db.delete_group("/out/clip_001.mp4") == []
# --- Profiles ---
def test_db_markers_isolated_by_profile():
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
path = f.name
try:
db = ProcessedDB(path)
db.add("video.mp4", 10.0, "/out/a_001.mp4", profile="landscape")
db.add("video.mp4", 20.0, "/out/b_001.mp4", profile="portrait")
land = db.get_markers("video.mp4", profile="landscape")
port = db.get_markers("video.mp4", profile="portrait")
assert len(land) == 1
assert land[0][0] == 10.0
assert len(port) == 1
assert port[0][0] == 20.0
finally:
os.unlink(path)
def test_db_find_similar_isolated_by_profile():
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
path = f.name
try:
db = ProcessedDB(path)
db.add("episode_2160p.mkv", 0.0, "/out/a.mp4", profile="hires")
# Same normalized name but different profile → no match
assert db.find_similar("episode_1080p.mkv", profile="lores") is None
# Same profile → match
assert db.find_similar("episode_1080p.mkv", profile="hires") == "episode_2160p.mkv"
finally:
os.unlink(path)
def test_db_get_profiles():
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
path = f.name
try:
db = ProcessedDB(path)
assert db.get_profiles() == []
db.add("a.mp4", 0.0, "/out/a.mp4", profile="beta")
db.add("b.mp4", 0.0, "/out/b.mp4", profile="alpha")
db.add("c.mp4", 0.0, "/out/c.mp4", profile="beta")
profiles = db.get_profiles()
assert profiles == ["alpha", "beta"]
finally:
os.unlink(path)
def test_db_get_profiles_disabled():
db = ProcessedDB("/no/such/directory/8cut.db")
assert db.get_profiles() == []
def test_db_default_profile_backward_compat():
"""Existing tests pass without explicit profile — defaults to 'default'."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
path = f.name
try:
db = ProcessedDB(path)
db.add("video.mp4", 5.0, "/out/clip.mp4")
markers = db.get_markers("video.mp4") # no profile arg
assert len(markers) == 1
assert markers[0][0] == 5.0
assert db.get_profiles() == ["default"]
finally:
os.unlink(path)