Fixes #605.
### The problem
`mergeMetadata.merge()` copies tags, performers, g…alleries, urls, studio, title, director, date, details, rating100 and code — but nothing about how the scene was watched. Play count, O count, total play duration and the `organized` flag are all left behind on the scene that is about to be deleted.
So merge-then-delete silently destroys watch history. For a deduplication tool that is the one thing that must not happen: the file is replaceable, the history is not.
### Why the obvious fix does not work
Setting `play_count` or `o_counter` through `update_scene` has no effect — both are `@deprecated(reason: "Unsupported")` on `SceneUpdateInput`, and the values are dropped server-side.
`sceneAddPlay` and `sceneAddO` take a list of timestamps. That is a better fix than the issue asks for: the merged scene keeps *when* each play happened, not merely how many there were.
### What this does
- **Play and O history** — replayed onto the destination with the source's original timestamps, via `sceneAddPlay` / `sceneAddO`.
- **Play duration** — summed. Total time played is cumulative across two copies of the same content.
- **`organized`** — follows the existing fill-the-blank rule used by every other scalar field: copied only when the destination does not already have it set.
Counts and history are cumulative and therefore summed, not overwritten. Existing fill-the-blank behaviour for the other fields is untouched.
### Two fixes that only appeared under end-to-end testing
Calling `mergeMetadata` directly passes full scene dictionaries, and against that the change worked first time. Running the plugin's real **Delete Duplicates** task told a different story, twice:
1. **`KeyError: 'organized'` — every merge failed.** `find_duplicate_scenes_diff` returns a reduced scene fragment, and `mergeItem` indexes fields directly. Merging `organized` raised on every pair, burned all five retries, and the merge never happened — while deletion carried on regardless. `mergeItem` now skips fields the query did not return.
2. **The history merge silently did nothing.** With the `KeyError` guarded, merges appeared to succeed, but that reduced fragment carries none of the playback fields, so every history field was skipped. `merge()` now re-reads both scenes when the playback fields are absent.
Worth flagging for maintainers: when the merge fails, deletion still proceeds. That is how a reported merge failure becomes lost data rather than a skipped merge.
### Testing
Against a clean Stash **v0.31.1** in Docker with two genuine phash duplicates (one source encoded at two qualities, distance 1), driven through the plugin's own **Delete Duplicates** task with **Merge Duplicate Tags** enabled.
**Empty destination** — the doomed copy carries `play_count 4`, `o_counter 2`, `play_duration 250`, rating, title and `organized`:
```
surviving scene: master_hq.mp4
play_count=4 o_counter=2 play_duration=250 organized=True rating=95
play_history=['2026-08-01T10:00:00Z','2026-08-02T11:00:00Z','2026-08-03T12:00:00Z','2026-08-04T13:00:00Z']
o_history=['2026-08-05T14:00:00Z','2026-08-06T15:00:00Z']
```
**Destination with its own history** — the case where both copies were watched:
```
before: low(delete) plays 4, Os 2, duration 250 | hq(keep) plays 1, O 1, duration 50
after : play_count 5 (expect 5) o_counter 3 (expect 3) play_duration 300 (expect 300)
history lists are the union of both; title "HQ Keeps Its Title" preserved
```
Also covered: the integer-id entry point used by the report path (`mergeMetadata(int, int)`) — counts and duration sum correctly there too.
Before this change, the same fixture produced `play_count 4 -> 0`, `o_counter 2 -> 0`, `organized true -> false` on the surviving scene.
### Scope
Applied to all three copies of `StashPluginHelper.py`, which are byte-identical in this region. FileMonitor and RenameFile are covered by a compile check only — neither appears to exercise the merge class.
Deliberately left for separate changes, to keep this reviewable:
- `stash_ids` (StashDB links) and scene markers are still not merged.
- The `# ToDo: Figure out how to merge groups` at the top of `merge()` is untouched.
- `resume_time` is intentionally *not* merged — unlike the others it is a playback position rather than an accumulated total, and there is no obviously correct answer when both scenes have one.
Independent of #757 — different region of the same file, so the two apply in either order.
### LLM-assisted contribution disclosure
Per the repository's contribution policy: this change was prepared with LLM assistance (Claude). The diff has been reviewed by me, the testing described above was carried out and its results are reproduced verbatim, and I take full responsibility for the change and its license compliance.