* 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>
37 lines
1.8 KiB
SQL
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;
|