refactor: derive import category/dry-run gating from fluxa-core

Trakt/Simkl/Stremio now call the new importApplyPlan core method
instead of each hand-writing its own wants()/dryRun guards around
mergeExternalWatchlist/mergeExternalWatched; AniList and Nuvio pass
categories/dryRun straight into their existing combined-plan core
calls instead of gating the result afterward by hand. No behavior
change — same merges, same counts, same writes fire as before, now
sourced from one shared decision function per provider shape instead
of duplicated per-file TS conditionals.
This commit is contained in:
KhooLy 2026-07-29 19:58:46 +03:00
parent d141f3a378
commit 5e64b20ea9
8 changed files with 154 additions and 110 deletions

View file

@ -64,13 +64,12 @@ export async function syncAniListNow(payload: Record<string, unknown>): Promise<
.flatMap((list) => list.entries ?? [])
.filter((entry): entry is AniListEntry => Boolean(entry?.media?.id));
const plan = await coreAnilistEntriesToSync(entries, Date.now());
if (!plan) return { synced: false, error: 'AniList entries could not be processed' };
const categories = payload.categories as ImportCategory[] | undefined;
const wants = (category: ImportCategory) => !categories || categories.includes(category);
const dryRun = payload.dryRun === true;
const plan = await coreAnilistEntriesToSync(entries, Date.now(), categories, dryRun);
if (!plan) return { synced: false, error: 'AniList entries could not be processed' };
const lib = await loadLibrary(profileKey);
const watchlistBefore = (lib.watchlist as LibraryItemRecord[] | undefined) ?? [];
const completedBefore = (lib.completed as LibraryItemRecord[] | undefined) ?? [];
@ -78,11 +77,11 @@ export async function syncAniListNow(payload: Record<string, unknown>): Promise<
const watchedBefore = (lib.watched as Record<string, boolean> | undefined) ?? {};
const progressBefore = (lib.progress as Record<string, unknown> | undefined) ?? {};
if (wants('watchlist') && !dryRun) {
if (plan.watchlist != null) {
lib.watchlist = await coreMergeLibraryItemsById(watchlistBefore, plan.watchlist);
await persistStatusListMerge(watchlistBefore, lib.watchlist as LibraryItemRecord[], 'watchlist', profileKey);
}
if (wants('watched') && !dryRun) {
if (plan.completed != null && plan.dropped != null && plan.watched != null) {
lib.completed = await coreMergeLibraryItemsById(completedBefore, plan.completed);
lib.dropped = await coreMergeLibraryItemsById(droppedBefore, plan.dropped);
lib.watched = { ...watchedBefore, ...plan.watched };
@ -90,7 +89,7 @@ export async function syncAniListNow(payload: Record<string, unknown>): Promise<
await persistStatusListMerge(droppedBefore, lib.dropped as LibraryItemRecord[], 'dropped', profileKey);
await persistWatchedMerge(watchedBefore, lib.watched as Record<string, boolean>, profileKey);
}
if (wants('continueWatching') && !dryRun) {
if (plan.progress != null && plan.watching != null) {
lib.progress = { ...progressBefore, ...plan.progress };
lib.continueWatching = await buildContinueWatching(lib.progress as Record<string, unknown>);
await persistProgressMerge(progressBefore, lib.progress as Record<string, unknown>, profileKey);
@ -99,20 +98,20 @@ export async function syncAniListNow(payload: Record<string, unknown>): Promise<
if (!dryRun) {
await saveLibrary(lib, profileKey);
await saveProviderLibrary('anilist', {
watchlist: plan.watchlist,
watching: plan.watching,
completed: plan.completed,
dropped: plan.dropped,
watchlist: plan.watchlist ?? [],
watching: plan.watching ?? [],
completed: plan.completed ?? [],
dropped: plan.dropped ?? [],
}, profileKey);
}
return {
synced: true,
provider: 'anilist',
continueWatchingCount: plan.watching.length,
watchlistCount: plan.watchlist.length,
completedCount: plan.completed.length,
droppedCount: plan.dropped.length,
continueWatchingCount: plan.watchingCount,
watchlistCount: plan.watchlistCount,
completedCount: plan.completedCount,
droppedCount: plan.droppedCount,
};
}

View file

@ -110,6 +110,7 @@ export const CORE_METHODS = [
'homeOverlapRatio',
'homePersonalizationScore',
'identity',
'importApplyPlan',
'importCollections',
'integrationSettingsPlan',
'isEpisodeReleased',

View file

@ -369,17 +369,24 @@ export async function coreDetectAnimePlayback(
export async function coreAnilistEntriesToSync(
entries: unknown[],
nowMs: number,
categories?: string[],
dryRun?: boolean,
): Promise<
{
watchlist: Record<string, unknown>[];
completed: Record<string, unknown>[];
dropped: Record<string, unknown>[];
watching: Record<string, unknown>[];
watched: Record<string, boolean>;
progress: Record<string, unknown>;
watchlist: Record<string, unknown>[] | null;
watchlistCount: number;
completed: Record<string, unknown>[] | null;
completedCount: number;
dropped: Record<string, unknown>[] | null;
droppedCount: number;
watching: Record<string, unknown>[] | null;
watchingCount: number;
watched: Record<string, boolean> | null;
watchedUpdatedAtMs: Record<string, unknown> | null;
progress: Record<string, unknown> | null;
} | null
> {
return coreInvoke("anilistEntriesToSync", JSON.stringify({ entries, nowMs }));
return coreInvoke("anilistEntriesToSync", JSON.stringify({ entries, nowMs, categories, dryRun }));
}
export async function coreMergeLibraryItemsById(
@ -883,8 +890,15 @@ export async function coreNuvioImportMergePlan(args: {
addonMetas: Record<string, unknown>;
watchProgress: unknown[] | null;
watchHistory: unknown[] | null;
categories?: string[];
dryRun?: boolean;
}): Promise<
{ progress: Record<string, unknown>; watched: Record<string, boolean> } | null
{
progress: Record<string, unknown> | null;
progressCount: number;
watched: Record<string, boolean> | null;
watchedCount: number;
} | null
> {
return coreInvoke("nuvioImportMergePlan", JSON.stringify(args));
}

View file

@ -1,5 +1,26 @@
import { coreInvoke } from './engine';
export interface CoreImportApplyPlan {
watchlist: Record<string, unknown>[] | null;
watchlistCount: number;
watched: Record<string, boolean> | null;
watchedCount: number;
continueWatchingApply: boolean;
}
export async function coreImportApplyPlan(request: {
localWatchlist: unknown[];
externalWatchlist: unknown[];
localWatched: Record<string, unknown>;
externalWatched: Record<string, unknown>;
categories?: string[];
dryRun?: boolean;
}): Promise<CoreImportApplyPlan> {
return (await coreInvoke<CoreImportApplyPlan>("importApplyPlan", JSON.stringify(request))) ?? {
watchlist: null, watchlistCount: 0, watched: null, watchedCount: 0, continueWatchingApply: false,
};
}
export async function coreTraktScrobblePlan(
videoId: string,
isEpisode: boolean,

View file

@ -375,22 +375,24 @@ export async function importNuvioProfileData(
}
}
const plan = await coreNuvioImportMergePlan({
progress: progressBefore,
watched: watchedBefore,
library,
addonMetas,
watchProgress,
watchHistory,
categories,
dryRun,
});
if (!dryRun) {
const plan = await coreNuvioImportMergePlan({
progress: progressBefore,
watched: watchedBefore,
library,
addonMetas,
watchProgress,
watchHistory,
});
let appliedRemoteWatchState = false;
if (plan) {
if (wants('continueWatching')) {
libDoc.progress = plan.progress;
libDoc.continueWatching = await buildContinueWatching(plan.progress);
}
if (wants('watched')) libDoc.watched = plan.watched;
if (plan?.progress != null) {
libDoc.progress = plan.progress;
libDoc.continueWatching = await buildContinueWatching(plan.progress);
}
if (plan?.watched != null) libDoc.watched = plan.watched;
if (plan?.progress != null || plan?.watched != null) {
await saveProviderLibrary('nuvio', {
watchlist: (libDoc.watchlist as Record<string, unknown>[]) ?? [],
watching: libDoc.continueWatching as Record<string, unknown>[],
@ -438,8 +440,8 @@ export async function importNuvioProfileData(
const counts = {
watchlist: watchlistCount,
continueWatching: watchProgress?.length ?? 0,
watched: watchHistory?.length ?? 0,
continueWatching: plan?.progressCount ?? 0,
watched: plan?.watchedCount ?? 0,
collections: collectionsCount,
addons: addonCount,
};

View file

@ -1,6 +1,5 @@
import {
coreMergeExternalWatched,
coreMergeExternalWatchlist,
coreImportApplyPlan,
coreInvoke,
coreSimklMergeDelta,
coreSimklResourceSyncPlan,
@ -21,24 +20,18 @@ type SimklDeltaCache = {
resources: Record<string, unknown>;
};
async function mergeExternalWatchlist(externalItems: Record<string, unknown>[], profileKey?: string): Promise<void> {
async function applyWatchlistMerge(merged: Record<string, unknown>[], before: Record<string, unknown>[], profileKey?: string): Promise<void> {
if (merged.length <= before.length) return;
const lib = await loadLibrary(profileKey);
const local = (lib.watchlist as Record<string, unknown>[] | undefined) ?? [];
const mergedJson = await coreMergeExternalWatchlist(JSON.stringify(local), JSON.stringify(externalItems));
const mergedList = mergedJson as Record<string, unknown>[];
if (mergedList.length > local.length) {
lib.watchlist = mergedList;
await persistStatusListMerge(local, mergedList, 'watchlist', profileKey);
await saveLibrary(lib, profileKey);
}
lib.watchlist = merged;
await persistStatusListMerge(before, merged, 'watchlist', profileKey);
await saveLibrary(lib, profileKey);
}
async function mergeExternalWatched(externalWatched: Record<string, boolean>, profileKey?: string): Promise<void> {
async function applyWatchedMerge(merged: Record<string, boolean>, before: Record<string, boolean>, profileKey?: string): Promise<void> {
const lib = await loadLibrary(profileKey);
const local = (lib.watched as Record<string, boolean> | undefined) ?? {};
const merged = await coreMergeExternalWatched(JSON.stringify(local), JSON.stringify(externalWatched));
lib.watched = merged;
await persistWatchedMerge(local, merged, profileKey);
await persistWatchedMerge(before, merged, profileKey);
await saveLibrary(lib, profileKey);
}
@ -116,15 +109,27 @@ export async function syncSimklNow(payload: Record<string, unknown>): Promise<un
const wlShowsData = JSON.stringify(wlShows);
const wlMoviesData = JSON.stringify(wlMovies);
const watchlistItems = ((await coreSimklWatchlistToItems(wlShowsData, wlMoviesData)) ?? []) as Record<string, unknown>[];
if (wants('watchlist') && !dryRun) await mergeExternalWatchlist(watchlistItems, profileKey);
if (!dryRun) await saveProviderLibrary('simkl', { watchlist: watchlistItems, watching: items, completed: [], dropped: [] }, profileKey);
const doneShowsData = JSON.stringify(doneShows);
const doneMoviesData = JSON.stringify(doneMovies);
const watchedMap = ((await coreSimklWatchedToIds(doneShowsData, doneMoviesData)) ?? {}) as Record<string, boolean>;
if (wants('watched') && !dryRun) await mergeExternalWatched(watchedMap, profileKey);
return { synced: true, provider: 'simkl', continueWatchingCount: items.length, watchlistCount: watchlistItems.length, watchedCount: Object.keys(watchedMap).length };
const localLib = await loadLibrary(profileKey);
const localWatchlist = (localLib.watchlist as Record<string, unknown>[] | undefined) ?? [];
const localWatched = (localLib.watched as Record<string, unknown> | undefined) ?? {};
const applyPlan = await coreImportApplyPlan({
localWatchlist,
externalWatchlist: watchlistItems,
localWatched,
externalWatched: watchedMap,
categories,
dryRun,
});
if (applyPlan.watchlist != null) await applyWatchlistMerge(applyPlan.watchlist, localWatchlist, profileKey);
if (applyPlan.watched != null) await applyWatchedMerge(applyPlan.watched, localWatched as Record<string, boolean>, profileKey);
return { synced: true, provider: 'simkl', continueWatchingCount: items.length, watchlistCount: applyPlan.watchlistCount, watchedCount: applyPlan.watchedCount };
}
export async function fetchSimklCalendarItems(

View file

@ -1,8 +1,7 @@
import {
coreImportApplyPlan,
coreLibraryContinueWatchingItems,
coreInvoke,
coreMergeExternalWatched,
coreMergeExternalWatchlist,
coreStremioWatchedToIds,
coreStremioWatchlistToItems,
} from './engine';
@ -73,24 +72,18 @@ export async function syncStremioAddons(profile: UserProfile, addons: AddonDescr
await stremioReplaceAddons(profile.stremioAuthKey, addons);
}
async function mergeExternalWatchlist(externalItems: Record<string, unknown>[], profileKey?: string): Promise<number> {
async function applyWatchlistMerge(merged: Record<string, unknown>[], before: Record<string, unknown>[], profileKey?: string): Promise<void> {
if (merged.length <= before.length) return;
const lib = await loadLibrary(profileKey);
const local = (lib.watchlist as Record<string, unknown>[] | undefined) ?? [];
const merged = await coreMergeExternalWatchlist(JSON.stringify(local), JSON.stringify(externalItems));
if (merged.length > local.length) {
lib.watchlist = merged;
await persistStatusListMerge(local, merged, 'watchlist', profileKey);
await saveLibrary(lib, profileKey);
}
return externalItems.length;
lib.watchlist = merged;
await persistStatusListMerge(before, merged, 'watchlist', profileKey);
await saveLibrary(lib, profileKey);
}
async function mergeExternalWatched(externalWatched: Record<string, boolean>, profileKey?: string): Promise<void> {
async function applyWatchedMerge(merged: Record<string, boolean>, before: Record<string, boolean>, profileKey?: string): Promise<void> {
const lib = await loadLibrary(profileKey);
const local = (lib.watched as Record<string, boolean> | undefined) ?? {};
const merged = await coreMergeExternalWatched(JSON.stringify(local), JSON.stringify(externalWatched));
lib.watched = merged;
await persistWatchedMerge(local, merged, profileKey);
await persistWatchedMerge(before, merged, profileKey);
await saveLibrary(lib, profileKey);
}
@ -115,25 +108,30 @@ export async function syncStremioNow(payload: Record<string, unknown>): Promise<
if (wants('continueWatching') && !dryRun) await replaceExternalContinueWatching({ items, provider: 'stremio', profileKey });
let watchlistCount = 0;
if (wants('watchlist')) {
try {
const watchlistItems = ((await coreStremioWatchlistToItems(libraryItems)) ?? []) as Record<string, unknown>[];
watchlistCount = watchlistItems.length;
if (!dryRun) {
await mergeExternalWatchlist(watchlistItems, profileKey);
await saveProviderLibrary('stremio', { watchlist: watchlistItems, watching: items, completed: [], dropped: [] }, profileKey);
}
} catch {}
}
let watchedCount = 0;
if (wants('watched')) {
try {
const watchedIds = ((await coreStremioWatchedToIds(libraryItems)) ?? {}) as Record<string, boolean>;
watchedCount = Object.keys(watchedIds).length;
if (!dryRun) await mergeExternalWatched(watchedIds, profileKey);
} catch {}
}
try {
const watchlistItems = ((await coreStremioWatchlistToItems(libraryItems)) ?? []) as Record<string, unknown>[];
const watchedIds = ((await coreStremioWatchedToIds(libraryItems)) ?? {}) as Record<string, boolean>;
const localLib = await loadLibrary(profileKey);
const localWatchlist = (localLib.watchlist as Record<string, unknown>[] | undefined) ?? [];
const localWatched = (localLib.watched as Record<string, unknown> | undefined) ?? {};
const applyPlan = await coreImportApplyPlan({
localWatchlist,
externalWatchlist: watchlistItems,
localWatched,
externalWatched: watchedIds,
categories,
dryRun,
});
watchlistCount = applyPlan.watchlistCount;
watchedCount = applyPlan.watchedCount;
if (applyPlan.watchlist != null) {
await applyWatchlistMerge(applyPlan.watchlist, localWatchlist, profileKey);
await saveProviderLibrary('stremio', { watchlist: watchlistItems, watching: items, completed: [], dropped: [] }, profileKey);
}
if (applyPlan.watched != null) await applyWatchedMerge(applyPlan.watched, localWatched as Record<string, boolean>, profileKey);
} catch {}
let addonCount = 0;
if (wants('addons')) {

View file

@ -1,8 +1,7 @@
import {
coreBuildTraktIds,
coreImportApplyPlan,
coreInvoke,
coreMergeExternalWatched,
coreMergeExternalWatchlist,
coreTraktActivityDiff,
coreTraktMarkWatchedBody,
coreTraktPlaybackItemsDedup,
@ -51,24 +50,18 @@ async function fetchAllPages(url: string, headers: HeadersInit, limit: number):
return plan?.items ?? [];
}
async function mergeExternalWatchlist(externalItems: Record<string, unknown>[], profileKey?: string): Promise<void> {
async function applyWatchlistMerge(merged: Record<string, unknown>[], before: Record<string, unknown>[], profileKey?: string): Promise<void> {
if (merged.length <= before.length) return;
const lib = await loadLibrary(profileKey);
const local = (lib.watchlist as Record<string, unknown>[] | undefined) ?? [];
const mergedJson = await coreMergeExternalWatchlist(JSON.stringify(local), JSON.stringify(externalItems));
const mergedList = mergedJson as Record<string, unknown>[];
if (mergedList.length > local.length) {
lib.watchlist = mergedList;
await persistStatusListMerge(local, mergedList, 'watchlist', profileKey);
await saveLibrary(lib, profileKey);
}
lib.watchlist = merged;
await persistStatusListMerge(before, merged, 'watchlist', profileKey);
await saveLibrary(lib, profileKey);
}
async function mergeExternalWatched(externalWatched: Record<string, boolean>, profileKey?: string): Promise<void> {
async function applyWatchedMerge(merged: Record<string, boolean>, before: Record<string, boolean>, profileKey?: string): Promise<void> {
const lib = await loadLibrary(profileKey);
const local = (lib.watched as Record<string, boolean> | undefined) ?? {};
const merged = await coreMergeExternalWatched(JSON.stringify(local), JSON.stringify(externalWatched));
lib.watched = merged;
await persistWatchedMerge(local, merged, profileKey);
await persistWatchedMerge(before, merged, profileKey);
await saveLibrary(lib, profileKey);
}
@ -154,14 +147,25 @@ export async function syncTraktNow(payload: Record<string, unknown>): Promise<un
items = await enrichWithAddonMeta(rawItems);
const watchlistItems = ((await coreTraktWatchlistToItems(JSON.stringify(watchlistMovies), JSON.stringify(watchlistShows))) ?? []) as Record<string, unknown>[];
watchlistCount = watchlistItems.length;
if (wants('watchlist') && !dryRun) await mergeExternalWatchlist(watchlistItems, profileKey);
const watchedIds = ((await coreTraktWatchedToIds(JSON.stringify(watchedMovies), JSON.stringify(watchedShows))) ?? {}) as Record<string, boolean>;
if (!dryRun) await saveProviderLibrary('trakt', { watchlist: watchlistItems, watching: items, completed: [], dropped: [] }, profileKey);
const watchedIds = ((await coreTraktWatchedToIds(JSON.stringify(watchedMovies), JSON.stringify(watchedShows))) ?? {}) as Record<string, boolean>;
watchedCount = Object.keys(watchedIds).length;
if (wants('watched') && !dryRun) await mergeExternalWatched(watchedIds, profileKey);
const localLib = await loadLibrary(profileKey);
const localWatchlist = (localLib.watchlist as Record<string, unknown>[] | undefined) ?? [];
const localWatched = (localLib.watched as Record<string, unknown> | undefined) ?? {};
const applyPlan = await coreImportApplyPlan({
localWatchlist,
externalWatchlist: watchlistItems,
localWatched,
externalWatched: watchedIds,
categories,
dryRun,
});
watchlistCount = applyPlan.watchlistCount;
watchedCount = applyPlan.watchedCount;
if (applyPlan.watchlist != null) await applyWatchlistMerge(applyPlan.watchlist, localWatchlist, profileKey);
if (applyPlan.watched != null) await applyWatchedMerge(applyPlan.watched, localWatched as Record<string, boolean>, profileKey);
if (activities) {
await storageWrite(cacheKey, { activities, playbackItems, watchlistMovies, watchlistShows, watchedMovies, watchedShows } satisfies TraktDeltaCache);