Files
silo-server/migrations/sql/20260706144033_media_identity_overrides.sql
0fb5afe479 feat(matching): split wrongly merged versions with watch-state reattribution; anchor group keys on provider tags (#319)
* feat(matching): split wrongly merged versions, reattribute watch state, anchor group keys on provider tags

Wrong merges (two titles normalizing to the same title+year key) stacked
different films as fake "versions" of one item with no in-app repair, and
explicit {tmdb-…}/[imdb-…] folder tags could not prevent it because the
content-group key ignored provider IDs entirely. Merges also silently
orphaned all per-user watch state.

- Anchor group keys on structured provider tags: same tag always groups,
  different tags can never merge; untagged files keep title+year keys.
- media_identity_overrides: path-scoped (root/file) forced identities applied
  during group inference, so admin splits survive rescans.
- internal/catalog/reattribute: shared user-state mover — exact moves for
  file-linked rows, evidence-based user_watch_history classification via the
  playback session log, newest-wins progress conflicts; wired into
  rebindItemToExistingItem to stop merge orphaning (with S/E episode mapping).
- POST /admin/items/{id}/split (dry-run = full transaction + rollback, so
  previews are exact), POST /admin/items/{id}/merge, GET /admin/items/{id}/files.
- Web admin: Split Versions dialog (files by folder → candidate search →
  preview → split), Resolve link from ambiguous-roots diagnostics.

Part of #318

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reattribute): classify history before moving session log; cover managed downloads and series-scoped preferences

Review findings on #319, all reproduced against a migrated scratch database:

- moveFileSubset re-pointed playback_history_admin before the history
  evidence query ran, erasing exactly the evidence proving a profile's plays
  were all on moved files — their history stayed behind as ambiguous.
  History classification now runs first; the pre-fix code demonstrably fails
  TestRun_HistoryEvidenceClassification.
- Managed offline downloads (downloads.content_id/episode_id) were not
  remapped on split or merge, stranding rows on the old id. Now moved per
  file on splits and swept per id pair on merges/episode re-anchoring.
- Series merges left user_audio_preferences, user_subtitle_preferences,
  user_series_playback_preferences (series_id-keyed) and the denormalized
  user_home_item_dismissals.series_id behind. All four now move, mirroring
  the provider-merge remap.

All five reattribute DB tests now verified green against PostgreSQL, with
new coverage for managed downloads, subtitle preferences, and dismissal
series ids.

Part of #318

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:23:32 -04:00

37 lines
1.8 KiB
SQL

-- Path-scoped identity overrides for the split-versions flow (see
-- docs/superpowers/specs/2026-07-06-split-versions-reassign-design.md).
--
-- media_group_overrides forces an identity for an entire inferred group, which
-- cannot fix a wrong merge: the misgrouped files share one group key. These
-- overrides bind to *paths* instead — a root folder or a single file — and are
-- applied per file during group inference, before bucketing, so overridden
-- files form their own group and rescans converge on the corrected assignment.
-- +goose Up
CREATE TABLE media_identity_overrides (
id bigserial PRIMARY KEY,
media_folder_id integer NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE,
scope text NOT NULL CHECK (scope IN ('root', 'file')),
root_path text NOT NULL DEFAULT '',
file_path text NOT NULL DEFAULT '',
forced_type text NOT NULL DEFAULT '',
forced_title text NOT NULL DEFAULT '',
forced_year integer NOT NULL DEFAULT 0,
forced_tmdb_id text NOT NULL DEFAULT '',
forced_imdb_id text NOT NULL DEFAULT '',
forced_tvdb_id text NOT NULL DEFAULT '',
note text NOT NULL DEFAULT '',
created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT media_identity_overrides_scope_path CHECK (
(scope = 'root' AND root_path <> '' AND file_path = '') OR
(scope = 'file' AND file_path <> '' AND root_path = '')
),
CONSTRAINT media_identity_overrides_unique UNIQUE (media_folder_id, scope, root_path, file_path)
);
-- +goose Down
DROP TABLE media_identity_overrides;