From 24794a67e9952c070bb30aad2a29493ac7809047 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:30:24 +0530 Subject: [PATCH] minor db deletion logic fix --- src/services/supabaseSyncService.ts | 116 ++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 24 deletions(-) diff --git a/src/services/supabaseSyncService.ts b/src/services/supabaseSyncService.ts index 6c4265bb..635baeb8 100644 --- a/src/services/supabaseSyncService.ts +++ b/src/services/supabaseSyncService.ts @@ -121,6 +121,8 @@ class SupabaseSyncService { private appStateSub: { remove: () => void } | null = null; private lastForegroundPullAt = 0; private readonly foregroundPullCooldownMs = 30000; + private pendingWatchProgressDeleteKeys = new Set(); + private watchProgressDeleteTimer: ReturnType | null = null; private pendingPushTimers: Record | null> = { plugins: null, @@ -544,7 +546,10 @@ class SupabaseSyncService { catalogService.onLibraryRemove(() => this.schedulePush('library')); storageService.subscribeToWatchProgressUpdates(() => this.schedulePush('watch_progress')); - storageService.onWatchProgressRemoved(() => this.schedulePush('watch_progress')); + storageService.onWatchProgressRemoved((id, type, episodeId) => { + this.schedulePush('watch_progress'); + this.scheduleWatchProgressDelete(id, type, episodeId); + }); watchedService.subscribeToWatchedUpdates(() => this.schedulePush('watched_items')); @@ -593,6 +598,91 @@ class SupabaseSyncService { }, DEFAULT_SYNC_DEBOUNCE_MS); } + private scheduleWatchProgressDelete(id: string, type: string, episodeId?: string): void { + if (!this.isConfigured() || this.suppressPushes) return; + + const keys = this.resolveWatchProgressDeleteKeys(id, type, episodeId); + if (keys.length === 0) return; + keys.forEach((key) => this.pendingWatchProgressDeleteKeys.add(key)); + + if (this.watchProgressDeleteTimer) { + clearTimeout(this.watchProgressDeleteTimer); + } + this.watchProgressDeleteTimer = setTimeout(() => { + this.watchProgressDeleteTimer = null; + this.flushWatchProgressDeletes().catch((error) => { + logger.error('[SupabaseSyncService] watch progress delete flush failed:', error); + }); + }, DEFAULT_SYNC_DEBOUNCE_MS); + } + + private async flushWatchProgressDeletes(): Promise { + if (!this.isConfigured() || this.suppressPushes) return; + + const keys = Array.from(this.pendingWatchProgressDeleteKeys); + if (keys.length === 0) return; + this.pendingWatchProgressDeleteKeys.clear(); + + await this.initialize(); + if (!this.session) { + keys.forEach((key) => this.pendingWatchProgressDeleteKeys.add(key)); + return; + } + + const traktConnected = await this.isTraktConnected(); + if (traktConnected) return; + + try { + logger.log(`[SupabaseSyncService] flushWatchProgressDeletes: deleting ${keys.length} keys`); + await this.callRpc('sync_delete_watch_progress', { p_keys: keys }); + } catch (error) { + keys.forEach((key) => this.pendingWatchProgressDeleteKeys.add(key)); + throw error; + } + } + + private resolveWatchProgressDeleteKeys(id: string, type: string, episodeId?: string): string[] { + const contentId = (id || '').trim(); + const contentType = (type || '').trim().toLowerCase(); + if (!contentId || !contentType) return []; + + const keys = new Set(); + + if (contentType === 'movie') { + keys.add(contentId); + return Array.from(keys); + } + + // Always delete the series mirror key when removing series progress. + keys.add(contentId); + + const normalizedEpisodeId = (episodeId || '').trim(); + if (normalizedEpisodeId) { + const parsed = this.parseSeasonEpisodeFromEpisodeId(normalizedEpisodeId); + if (parsed) { + keys.add(`${contentId}_s${parsed.season}e${parsed.episode}`); + } else { + // Fallback for any non-standard legacy progress_key format. + keys.add(`${contentId}_${normalizedEpisodeId}`); + } + } + + return Array.from(keys); + } + + private parseSeasonEpisodeFromEpisodeId( + episodeId: string + ): { season: number; episode: number } | null { + const match = episodeId.match(/(?:^|:)(\d+):(\d+)$/); + if (!match) return null; + + const season = Number(match[1]); + const episode = Number(match[2]); + if (!Number.isFinite(season) || !Number.isFinite(episode)) return null; + + return { season, episode }; + } + private async executeScheduledPush(target: PushTarget): Promise { await this.initialize(); if (!this.session) return; @@ -1194,29 +1284,7 @@ class SupabaseSyncService { ); } - // Reconcile removals only when remote has at least one entry to avoid wiping local - // data if backend temporarily returns an empty set. - if (remoteSet.size > 0) { - const allLocal = await storageService.getAllWatchProgress(); - let removedCount = 0; - - for (const [key] of Object.entries(allLocal)) { - const parsed = this.parseWatchProgressKey(key); - if (!parsed) continue; - const localSig = `${parsed.contentType}:${parsed.contentId}:${parsed.season ?? ''}:${parsed.episode ?? ''}`; - if (remoteSet.has(localSig)) continue; - - const episodeId = parsed.contentType === 'series' && parsed.season != null && parsed.episode != null - ? `${parsed.contentId}:${parsed.season}:${parsed.episode}` - : undefined; - - await storageService.removeWatchProgress(parsed.contentId, parsed.contentType, episodeId); - removedCount += 1; - } - logger.log(`[SupabaseSyncService] pullWatchProgressToLocal: removedLocalExtras=${removedCount}`); - } else { - logger.log('[SupabaseSyncService] pullWatchProgressToLocal: remote set empty, skipped local prune'); - } + logger.log(`[SupabaseSyncService] pullWatchProgressToLocal: merged ${(rows || []).length} remote entries (no local prune)`); } private async pushWatchProgressFromLocal(): Promise {