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>
This commit is contained in:
committed by
GitHub
co-authored by
Claude Fable 5
parent
d08a4f232f
commit
0fb5afe479
@@ -2945,6 +2945,8 @@ export interface LibraryRoot {
|
||||
first_seen_at: string;
|
||||
last_seen_at: string;
|
||||
active_override?: LibraryRootOverride;
|
||||
/** Catalog item this group matched to, when known. */
|
||||
content_id?: string;
|
||||
}
|
||||
|
||||
export interface LibraryRootsResponse {
|
||||
@@ -4135,6 +4137,68 @@ export interface ItemMatchApplyRequest {
|
||||
library_id?: number;
|
||||
}
|
||||
|
||||
// Split/merge (wrong version-grouping repair) types
|
||||
|
||||
export interface ItemFile {
|
||||
id: number;
|
||||
library_id: number;
|
||||
file_path: string;
|
||||
observed_root_path: string;
|
||||
season_number?: number;
|
||||
episode_number?: number;
|
||||
}
|
||||
|
||||
export interface ItemFilesResponse {
|
||||
files: ItemFile[];
|
||||
}
|
||||
|
||||
export type SplitHistoryMode = "evidence" | "keep" | "move_all";
|
||||
|
||||
export interface ItemSplitTarget {
|
||||
provider_ids?: Record<string, string>;
|
||||
content_id?: string;
|
||||
unmatched?: boolean;
|
||||
title?: string;
|
||||
year?: number;
|
||||
}
|
||||
|
||||
export interface ItemSplitRequest {
|
||||
file_ids: number[];
|
||||
target: ItemSplitTarget;
|
||||
history_mode?: SplitHistoryMode;
|
||||
persist_override?: boolean;
|
||||
dry_run?: boolean;
|
||||
}
|
||||
|
||||
export interface ReattributionReport {
|
||||
playback_session_log: number;
|
||||
downloads: number;
|
||||
progress_moved: number;
|
||||
progress_conflicts: number;
|
||||
history_moved: number;
|
||||
history_stayed: number;
|
||||
history_ambiguous: number;
|
||||
intent_moved: number;
|
||||
episode_pairs_moved: number;
|
||||
ambiguous_history?: {
|
||||
user_id: number;
|
||||
profile_id: string;
|
||||
watched_at: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ItemSplitResponse {
|
||||
dry_run: boolean;
|
||||
source_content_id: string;
|
||||
target_content_id: string;
|
||||
target_created: boolean;
|
||||
files_moved: number;
|
||||
root_overrides: string[];
|
||||
file_overrides: string[];
|
||||
episode_pairs: number;
|
||||
reattribution: ReattributionReport;
|
||||
}
|
||||
|
||||
// Image selector types
|
||||
export interface RemoteImage {
|
||||
provider_id: string;
|
||||
|
||||
Reference in New Issue
Block a user