diff --git a/src/core/anilistExternalSync.ts b/src/core/anilistExternalSync.ts index 96a042b..a711a00 100644 --- a/src/core/anilistExternalSync.ts +++ b/src/core/anilistExternalSync.ts @@ -64,13 +64,12 @@ export async function syncAniListNow(payload: Record): 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): Promise< const watchedBefore = (lib.watched as Record | undefined) ?? {}; const progressBefore = (lib.progress as Record | 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): Promise< await persistStatusListMerge(droppedBefore, lib.dropped as LibraryItemRecord[], 'dropped', profileKey); await persistWatchedMerge(watchedBefore, lib.watched as Record, 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); await persistProgressMerge(progressBefore, lib.progress as Record, profileKey); @@ -99,20 +98,20 @@ export async function syncAniListNow(payload: Record): 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, }; } diff --git a/src/core/coreMethods.ts b/src/core/coreMethods.ts index c864202..d72ad77 100644 --- a/src/core/coreMethods.ts +++ b/src/core/coreMethods.ts @@ -110,6 +110,7 @@ export const CORE_METHODS = [ 'homeOverlapRatio', 'homePersonalizationScore', 'identity', + 'importApplyPlan', 'importCollections', 'integrationSettingsPlan', 'isEpisodeReleased', diff --git a/src/core/engine.ts b/src/core/engine.ts index d6c32bc..3a1dec2 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -369,17 +369,24 @@ export async function coreDetectAnimePlayback( export async function coreAnilistEntriesToSync( entries: unknown[], nowMs: number, + categories?: string[], + dryRun?: boolean, ): Promise< { - watchlist: Record[]; - completed: Record[]; - dropped: Record[]; - watching: Record[]; - watched: Record; - progress: Record; + watchlist: Record[] | null; + watchlistCount: number; + completed: Record[] | null; + completedCount: number; + dropped: Record[] | null; + droppedCount: number; + watching: Record[] | null; + watchingCount: number; + watched: Record | null; + watchedUpdatedAtMs: Record | null; + progress: Record | 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; watchProgress: unknown[] | null; watchHistory: unknown[] | null; + categories?: string[]; + dryRun?: boolean; }): Promise< - { progress: Record; watched: Record } | null + { + progress: Record | null; + progressCount: number; + watched: Record | null; + watchedCount: number; + } | null > { return coreInvoke("nuvioImportMergePlan", JSON.stringify(args)); } diff --git a/src/core/engineExternalSync.ts b/src/core/engineExternalSync.ts index e2997f6..67a83b8 100644 --- a/src/core/engineExternalSync.ts +++ b/src/core/engineExternalSync.ts @@ -1,5 +1,26 @@ import { coreInvoke } from './engine'; +export interface CoreImportApplyPlan { + watchlist: Record[] | null; + watchlistCount: number; + watched: Record | null; + watchedCount: number; + continueWatchingApply: boolean; +} + +export async function coreImportApplyPlan(request: { + localWatchlist: unknown[]; + externalWatchlist: unknown[]; + localWatched: Record; + externalWatched: Record; + categories?: string[]; + dryRun?: boolean; +}): Promise { + return (await coreInvoke("importApplyPlan", JSON.stringify(request))) ?? { + watchlist: null, watchlistCount: 0, watched: null, watchedCount: 0, continueWatchingApply: false, + }; +} + export async function coreTraktScrobblePlan( videoId: string, isEpisode: boolean, diff --git a/src/core/nuvioSync.ts b/src/core/nuvioSync.ts index fb31a97..a710c25 100644 --- a/src/core/nuvioSync.ts +++ b/src/core/nuvioSync.ts @@ -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[]) ?? [], watching: libDoc.continueWatching as Record[], @@ -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, }; diff --git a/src/core/simklExternalSync.ts b/src/core/simklExternalSync.ts index 62a2b77..82637f3 100644 --- a/src/core/simklExternalSync.ts +++ b/src/core/simklExternalSync.ts @@ -1,6 +1,5 @@ import { - coreMergeExternalWatched, - coreMergeExternalWatchlist, + coreImportApplyPlan, coreInvoke, coreSimklMergeDelta, coreSimklResourceSyncPlan, @@ -21,24 +20,18 @@ type SimklDeltaCache = { resources: Record; }; -async function mergeExternalWatchlist(externalItems: Record[], profileKey?: string): Promise { +async function applyWatchlistMerge(merged: Record[], before: Record[], profileKey?: string): Promise { + if (merged.length <= before.length) return; const lib = await loadLibrary(profileKey); - const local = (lib.watchlist as Record[] | undefined) ?? []; - const mergedJson = await coreMergeExternalWatchlist(JSON.stringify(local), JSON.stringify(externalItems)); - const mergedList = mergedJson as Record[]; - 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, profileKey?: string): Promise { +async function applyWatchedMerge(merged: Record, before: Record, profileKey?: string): Promise { const lib = await loadLibrary(profileKey); - const local = (lib.watched as Record | 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): Promise[]; - 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; - 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[] | undefined) ?? []; + const localWatched = (localLib.watched as Record | 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, profileKey); + + return { synced: true, provider: 'simkl', continueWatchingCount: items.length, watchlistCount: applyPlan.watchlistCount, watchedCount: applyPlan.watchedCount }; } export async function fetchSimklCalendarItems( diff --git a/src/core/stremioExternalSync.ts b/src/core/stremioExternalSync.ts index 1ee515a..8c283bd 100644 --- a/src/core/stremioExternalSync.ts +++ b/src/core/stremioExternalSync.ts @@ -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[], profileKey?: string): Promise { +async function applyWatchlistMerge(merged: Record[], before: Record[], profileKey?: string): Promise { + if (merged.length <= before.length) return; const lib = await loadLibrary(profileKey); - const local = (lib.watchlist as Record[] | 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, profileKey?: string): Promise { +async function applyWatchedMerge(merged: Record, before: Record, profileKey?: string): Promise { const lib = await loadLibrary(profileKey); - const local = (lib.watched as Record | 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): 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[]; - 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; - watchedCount = Object.keys(watchedIds).length; - if (!dryRun) await mergeExternalWatched(watchedIds, profileKey); - } catch {} - } + try { + const watchlistItems = ((await coreStremioWatchlistToItems(libraryItems)) ?? []) as Record[]; + const watchedIds = ((await coreStremioWatchedToIds(libraryItems)) ?? {}) as Record; + + const localLib = await loadLibrary(profileKey); + const localWatchlist = (localLib.watchlist as Record[] | undefined) ?? []; + const localWatched = (localLib.watched as Record | 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, profileKey); + } catch {} let addonCount = 0; if (wants('addons')) { diff --git a/src/core/traktExternalSync.ts b/src/core/traktExternalSync.ts index 655d999..fe6f894 100644 --- a/src/core/traktExternalSync.ts +++ b/src/core/traktExternalSync.ts @@ -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[], profileKey?: string): Promise { +async function applyWatchlistMerge(merged: Record[], before: Record[], profileKey?: string): Promise { + if (merged.length <= before.length) return; const lib = await loadLibrary(profileKey); - const local = (lib.watchlist as Record[] | undefined) ?? []; - const mergedJson = await coreMergeExternalWatchlist(JSON.stringify(local), JSON.stringify(externalItems)); - const mergedList = mergedJson as Record[]; - 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, profileKey?: string): Promise { +async function applyWatchedMerge(merged: Record, before: Record, profileKey?: string): Promise { const lib = await loadLibrary(profileKey); - const local = (lib.watched as Record | 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): Promise[]; - watchlistCount = watchlistItems.length; - if (wants('watchlist') && !dryRun) await mergeExternalWatchlist(watchlistItems, profileKey); + const watchedIds = ((await coreTraktWatchedToIds(JSON.stringify(watchedMovies), JSON.stringify(watchedShows))) ?? {}) as Record; if (!dryRun) await saveProviderLibrary('trakt', { watchlist: watchlistItems, watching: items, completed: [], dropped: [] }, profileKey); - const watchedIds = ((await coreTraktWatchedToIds(JSON.stringify(watchedMovies), JSON.stringify(watchedShows))) ?? {}) as Record; - watchedCount = Object.keys(watchedIds).length; - if (wants('watched') && !dryRun) await mergeExternalWatched(watchedIds, profileKey); + const localLib = await loadLibrary(profileKey); + const localWatchlist = (localLib.watchlist as Record[] | undefined) ?? []; + const localWatched = (localLib.watched as Record | 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, profileKey); if (activities) { await storageWrite(cacheKey, { activities, playbackItems, watchlistMovies, watchlistShows, watchedMovies, watchedShows } satisfies TraktDeltaCache);