Skip to content

fix: detect media_metadata changes for save-changes prompt (#377)#520

Open
FelipeDefensor wants to merge 2 commits into
TimeLineAnnotator:devfrom
FelipeDefensor:fix/377-dirty-on-notes-edit
Open

fix: detect media_metadata changes for save-changes prompt (#377)#520
FelipeDefensor wants to merge 2 commits into
TimeLineAnnotator:devfrom
FelipeDefensor:fix/377-dirty-on-notes-edit

Conversation

@FelipeDefensor

Copy link
Copy Markdown
Collaborator

Closes #377.

Summary

`FileManager.is_file_modified` compared the current app state against `self.file.dict`, but for `media_metadata` both sides referenced the same live `MediaMetadata` object: `on_set_media_metadata_field` mutates `self.file.media_metadata` in place, and `get_app_state` takes `dict(self.file.media_metadata)` of that same mutated object. The comparison was always equal, so editing notes never set the file to "modified" and the save-changes dialog never appeared on close.

The reporter flagged "notes" specifically, but it's not unique to that field — the title, custom fields, etc. were broken in the same way. Verified manually before the fix:

```
Initial is_file_modified: False
After title change is_file_modified: False
After notes change is_file_modified: False
```

After the fix:

```
Initial is_file_modified: False
After title change is_file_modified: True
After notes change is_file_modified: True
```

Approach

Track a snapshot of `media_metadata` taken at the last save / file-load.

  • `self._saved_metadata` is set by a property setter on `file`, so every place that reassigns the file (`init`, `_setup_file`, `save`, `update_file`, `app.on_open`'s `self.file_manager.file = file`) refreshes the snapshot for free — no need to thread an extra method-call through every call site.
  • `is_file_modified` compares `dict(current_data["media_metadata"])` against this snapshot. The existing `are_tilia_data_equal` call is kept for the other fields (`timelines_hash`, `media_path`); only the metadata-comparison branch is broken.
  • `on_player_duration_changed` and `set_media_duration` also update the snapshot, since those mutations are system-driven (the player reports the loaded media's length) and shouldn't show up as user edits — without this, the existing `test_is_file_modified_empty_file` assertion would flip to True after the test fixture sets the duration to 100.

Test plan

  • New regression tests `test_is_file_modified_after_notes_edit` and `test_is_file_modified_after_title_edit` cover the case the reporter hit. Both fail without the fix and pass with it.
  • All existing modification tests keep passing (`test_is_file_modified_modified_tile`, `..._removed_field_from_media_metadata`, `..._modified_media_path`, `..._not_modified_after_update`, `..._when_modified_timelines`, `..._empty_file`). Their flow already used different objects on each side of the comparison, so the snapshot path is harmlessly redundant for them.
  • Targeted suites pass: `tests/file/` 20 passed, 1 skipped; `tests/test_app.py` 63 passed (1 unrelated sentry-logging test deselected).

Note for the reviewer

The pre-existing `media_path` comparison has the same structural shape — `on_player_url_changed` writes the live path to `self.file.media_path`, and `get(Get.MEDIA_PATH)` reads it from the player, so the two sides drift only because the player doesn't immediately propagate. It happens to work in current usage because of that timing, but it's fragile. Out of scope here — happy to follow up if you'd like the same snapshot treatment for it.

FelipeDefensor and others added 2 commits May 14, 2026 18:02
…nnotator#377)

is_file_modified compared current state against self.file.__dict__,
but for media_metadata both sides referenced the SAME live
MediaMetadata object — on_set_media_metadata_field mutates
self.file.media_metadata in place, and get_app_state takes
dict(self.file.media_metadata) of that same mutated object. The
comparison was always equal, so editing notes (or any other field)
never set the file to "modified" and the save-changes dialog never
appeared on close.

The reporter only flagged "notes" because the notes UI is the only
field whose edits go straight to MEDIA_METADATA_FIELD_SET without
sitting next to a side-channel that happens to also dirty the file.
Title, custom-field edits, etc. were broken too in the same way —
verified locally before the fix.

Track a snapshot of media_metadata at the last save/load:

- self._saved_metadata is set by a property setter on `file`, so
  every place that reassigns the file (init, _setup_file, save,
  update_file, app.on_open's `file_manager.file = file`) refreshes
  it for free.
- is_file_modified compares dict(current_data["media_metadata"])
  against this snapshot. The existing are_tilia_data_equal call is
  kept for the other fields (timelines_hash, media_path).
- on_player_duration_changed and set_media_duration also update the
  snapshot, since those mutations are system-driven (the player
  reports the loaded media's length) and shouldn't show up as user
  edits — without this, the existing test_is_file_modified_empty_file
  assertion would flip to True after the fixture sets the duration.

Tests: test_is_file_modified_after_notes_edit and
test_is_file_modified_after_title_edit cover the regression. The
existing modification tests (modified title via custom params dict,
removed metadata field, modified media_path, etc.) keep working —
their flow already used different objects on each side, so the
snapshot path is harmlessly redundant for them.
@azfoo azfoo force-pushed the fix/377-dirty-on-notes-edit branch from ce2294e to 9abb24c Compare May 14, 2026 17:34
@azfoo azfoo self-requested a review May 14, 2026 17:34

@azfoo azfoo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some tests, don't know if their behaviour is expected. Invert their logic if they are!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants