mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-06 17:59:39 +00:00
fixed mark as watched items not syncing
This commit is contained in:
parent
4135b4725a
commit
c18f1130fc
4 changed files with 137 additions and 66 deletions
|
|
@ -193,38 +193,36 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
case 'watched': {
|
||||
const targetWatched = !isWatched;
|
||||
setIsWatched(targetWatched);
|
||||
try {
|
||||
await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
|
||||
} catch { }
|
||||
showInfo(targetWatched ? t('library.marked_watched') : t('library.marked_unwatched'), targetWatched ? t('library.item_marked_watched') : t('library.item_marked_unwatched'));
|
||||
setTimeout(() => {
|
||||
DeviceEventEmitter.emit('watchedStatusChanged');
|
||||
}, 100);
|
||||
|
||||
// Best-effort sync: record local progress and push to Trakt if available
|
||||
if (targetWatched) {
|
||||
try {
|
||||
await storageService.setWatchProgress(
|
||||
item.id,
|
||||
item.type,
|
||||
{ currentTime: 1, duration: 1, lastUpdated: Date.now() },
|
||||
undefined,
|
||||
{ forceNotify: true, forceWrite: true }
|
||||
);
|
||||
} catch { }
|
||||
|
||||
if (item.type === 'movie') {
|
||||
try {
|
||||
const trakt = TraktService.getInstance();
|
||||
if (await trakt.isAuthenticated()) {
|
||||
await trakt.addToWatchedMovies(item.id);
|
||||
try {
|
||||
await storageService.updateTraktSyncStatus(item.id, item.type, true, 100);
|
||||
} catch { }
|
||||
}
|
||||
} catch { }
|
||||
|
||||
// Use the centralized watchedService to handle all the sync logic (Supabase, Trakt, Simkl, MAL)
|
||||
import('../../services/watchedService').then(({ watchedService }) => {
|
||||
if (targetWatched) {
|
||||
if (item.type === 'movie') {
|
||||
// Pass the title so it correctly populates the database instead of defaulting to IMDb ID
|
||||
watchedService.markMovieAsWatched(item.id, new Date(), undefined, undefined, item.name);
|
||||
} else {
|
||||
// For series from the homescreen drop-down, we mark S1E1 as watched as a baseline
|
||||
watchedService.markEpisodeAsWatched(item.id, item.id, 1, 1, new Date(), undefined, item.name);
|
||||
}
|
||||
} else {
|
||||
if (item.type === 'movie') {
|
||||
watchedService.unmarkMovieAsWatched(item.id);
|
||||
} else {
|
||||
// Unmarking a series from the top level is tricky as we don't know the exact episodes.
|
||||
// For safety and consistency with old behavior, we just clear the legacy flag.
|
||||
mmkvStorage.removeItem(`watched:${item.type}:${item.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showInfo(
|
||||
targetWatched ? t('library.marked_watched') : t('library.marked_unwatched'),
|
||||
targetWatched ? t('library.item_marked_watched') : t('library.item_marked_unwatched')
|
||||
);
|
||||
setTimeout(() => {
|
||||
DeviceEventEmitter.emit('watchedStatusChanged');
|
||||
}, 100);
|
||||
});
|
||||
|
||||
setMenuVisible(false);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,7 +335,6 @@ class StremioService {
|
|||
if (lowerPrefix.endsWith(':') || lowerPrefix.endsWith('_')) return true;
|
||||
return lowerId.length > lowerPrefix.length;
|
||||
});
|
||||
if (__DEV__) console.log(`🔍 [isValidContentId] Prefix match result: ${result} for ID '${id}'`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1135,8 +1135,16 @@ class SupabaseSyncService {
|
|||
}
|
||||
|
||||
private async isExternalProgressSyncConnected(): Promise<boolean> {
|
||||
if (await this.isTraktConnected()) return true;
|
||||
return await this.isSimklConnected();
|
||||
const trakt = await this.isTraktConnected();
|
||||
if (trakt) {
|
||||
logger.log('[SupabaseSyncService] isExternalProgressSyncConnected: Trakt is connected, returning true');
|
||||
return true;
|
||||
}
|
||||
const simkl = await this.isSimklConnected();
|
||||
if (simkl) {
|
||||
logger.log('[SupabaseSyncService] isExternalProgressSyncConnected: Simkl is connected, returning true');
|
||||
}
|
||||
return simkl;
|
||||
}
|
||||
|
||||
private async pullPluginsToLocal(): Promise<void> {
|
||||
|
|
@ -1409,62 +1417,90 @@ class SupabaseSyncService {
|
|||
|
||||
private async pushWatchProgressFromLocal(): Promise<void> {
|
||||
const all = await storageService.getAllWatchProgress();
|
||||
const allKeys = Object.keys(all);
|
||||
|
||||
const nextSeenKeys = new Set<string>();
|
||||
const changedEntries: Array<{ key: string; row: WatchProgressRow; signature: string }> = [];
|
||||
let skippedSameSignature = 0;
|
||||
let skippedParseFailure = 0;
|
||||
|
||||
for (const [key, value] of Object.entries(all)) {
|
||||
nextSeenKeys.add(key);
|
||||
const signature = this.getWatchProgressEntrySignature(value);
|
||||
if (this.watchProgressPushedSignatures.get(key) === signature) {
|
||||
skippedSameSignature++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = this.parseWatchProgressKey(key);
|
||||
if (!parsed) {
|
||||
skippedParseFailure++;
|
||||
continue;
|
||||
}
|
||||
|
||||
changedEntries.push({
|
||||
key,
|
||||
signature,
|
||||
row: {
|
||||
content_id: parsed.contentId,
|
||||
content_type: parsed.contentType,
|
||||
video_id: parsed.videoId,
|
||||
season: parsed.season,
|
||||
episode: parsed.episode,
|
||||
position: this.secondsToMsLong(value.currentTime),
|
||||
duration: this.secondsToMsLong(value.duration),
|
||||
last_watched: this.normalizeEpochMs(value.lastUpdated || Date.now()),
|
||||
progress_key: parsed.progressKey,
|
||||
},
|
||||
});
|
||||
const row: WatchProgressRow = {
|
||||
content_id: parsed.contentId,
|
||||
content_type: parsed.contentType,
|
||||
video_id: parsed.videoId,
|
||||
season: parsed.season,
|
||||
episode: parsed.episode,
|
||||
position: this.secondsToMsLong(value.currentTime),
|
||||
duration: this.secondsToMsLong(value.duration),
|
||||
last_watched: this.normalizeEpochMs(value.lastUpdated || Date.now()),
|
||||
progress_key: parsed.progressKey,
|
||||
};
|
||||
|
||||
changedEntries.push({ key, signature, row });
|
||||
}
|
||||
|
||||
// Prune signatures for entries no longer present locally (deletes are handled separately).
|
||||
let prunedSignatures = 0;
|
||||
for (const existingKey of Array.from(this.watchProgressPushedSignatures.keys())) {
|
||||
if (!nextSeenKeys.has(existingKey)) {
|
||||
this.watchProgressPushedSignatures.delete(existingKey);
|
||||
prunedSignatures++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(`[SupabaseSyncService] pushWatchProgressFromLocal: skippedSameSignature=${skippedSameSignature} skippedParseFailure=${skippedParseFailure} prunedStaleSignatures=${prunedSignatures}`);
|
||||
|
||||
if (changedEntries.length === 0) {
|
||||
logger.log('[SupabaseSyncService] pushWatchProgressFromLocal: no changed entries; skipping push');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.callRpc<void>('sync_push_watch_progress', {
|
||||
p_entries: changedEntries.map((entry) => entry.row),
|
||||
});
|
||||
const rpcPayload = changedEntries.map((entry) => entry.row);
|
||||
logger.log(`[SupabaseSyncService] pushWatchProgressFromLocal: calling sync_push_watch_progress with ${rpcPayload.length} entries`);
|
||||
try {
|
||||
await this.callRpc<void>('sync_push_watch_progress', {
|
||||
p_entries: rpcPayload,
|
||||
});
|
||||
logger.log(`[SupabaseSyncService] pushWatchProgressFromLocal: RPC success`);
|
||||
} catch (rpcError: any) {
|
||||
logger.error(`[SupabaseSyncService] pushWatchProgressFromLocal: RPC FAILED`, rpcError?.message || rpcError);
|
||||
throw rpcError;
|
||||
}
|
||||
|
||||
for (const entry of changedEntries) {
|
||||
this.watchProgressPushedSignatures.set(entry.key, entry.signature);
|
||||
}
|
||||
logger.log(`[SupabaseSyncService] pushWatchProgressFromLocal: pushedChanged=${changedEntries.length} totalLocal=${Object.keys(all).length}`);
|
||||
logger.log(`[SupabaseSyncService] pushWatchProgressFromLocal: pushedChanged=${changedEntries.length} totalLocal=${allKeys.length}`);
|
||||
}
|
||||
|
||||
private async pullLibraryToLocal(): Promise<void> {
|
||||
const rows = await this.callRpc<LibraryRow[]>('sync_pull_library', {});
|
||||
const PAGE_SIZE = 500;
|
||||
const rows: LibraryRow[] = [];
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const page = await this.callRpc<LibraryRow[]>('sync_pull_library', {
|
||||
p_limit: PAGE_SIZE,
|
||||
p_offset: offset,
|
||||
});
|
||||
if (!page || page.length === 0) break;
|
||||
rows.push(...page);
|
||||
if (page.length < PAGE_SIZE) break;
|
||||
offset += PAGE_SIZE;
|
||||
}
|
||||
const localItems = await catalogService.getLibraryItems();
|
||||
const existing = new Set(localItems.map((item) => `${item.type}:${item.id}`));
|
||||
const remoteSet = new Set<string>();
|
||||
|
|
@ -1532,6 +1568,8 @@ class SupabaseSyncService {
|
|||
|
||||
private async pushWatchedItemsFromLocal(): Promise<void> {
|
||||
const items = await watchedService.getAllWatchedItems();
|
||||
if (items.length === 0) return;
|
||||
|
||||
const payload: WatchedRow[] = items.map((item) => ({
|
||||
content_id: item.content_id,
|
||||
content_type: item.content_type,
|
||||
|
|
@ -1540,7 +1578,13 @@ class SupabaseSyncService {
|
|||
episode: item.episode,
|
||||
watched_at: item.watched_at,
|
||||
}));
|
||||
await this.callRpc<void>('sync_push_watched_items', { p_items: payload });
|
||||
|
||||
try {
|
||||
await this.callRpc<void>('sync_push_watched_items', { p_items: payload });
|
||||
} catch (rpcError: any) {
|
||||
logger.error(`[SupabaseSyncService] pushWatchedItemsFromLocal: RPC FAILED`, rpcError?.message || rpcError);
|
||||
throw rpcError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -175,13 +175,22 @@ class WatchedService {
|
|||
.filter((item) => Boolean(item.content_id));
|
||||
|
||||
// Guard: do not wipe local watched data if backend temporarily returns empty.
|
||||
|
||||
if (normalizedRemote.length === 0) {
|
||||
logger.log('[WatchedService] reconcileRemoteWatchedItems: remote is empty, doing nothing');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLocal = await this.loadWatchedItems();
|
||||
const remoteKeys = new Set(normalizedRemote.map(r => this.watchedKey(r)));
|
||||
|
||||
// Find local items that need to be removed because they don't exist remotely
|
||||
const toRemove = currentLocal.filter(l => !remoteKeys.has(this.watchedKey(l)));
|
||||
|
||||
await this.saveWatchedItems(normalizedRemote);
|
||||
this.notifyWatchedSubscribers();
|
||||
|
||||
// 1. Set watched status for all remote items
|
||||
for (const item of normalizedRemote) {
|
||||
if (item.content_type === 'movie') {
|
||||
await this.setLocalWatchedStatus(item.content_id, 'movie', true, undefined, new Date(item.watched_at));
|
||||
|
|
@ -192,8 +201,27 @@ class WatchedService {
|
|||
const episodeId = `${item.content_id}:${item.season}:${item.episode}`;
|
||||
await this.setLocalWatchedStatus(item.content_id, 'series', true, episodeId, new Date(item.watched_at));
|
||||
}
|
||||
|
||||
// 2. Unset watched status for local items that were deleted remotely
|
||||
for (const item of toRemove) {
|
||||
if (item.content_type === 'movie') {
|
||||
await this.setLocalWatchedStatus(item.content_id, 'movie', false);
|
||||
} else if (item.season != null && item.episode != null) {
|
||||
const episodeId = `${item.content_id}:${item.season}:${item.episode}`;
|
||||
await this.setLocalWatchedStatus(item.content_id, 'series', false, episodeId);
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.length > 0) {
|
||||
logger.log(`[WatchedService] reconcileRemoteWatchedItems: Removed ${toRemove.length} local items that were deleted remotely`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a movie as watched
|
||||
* @param imdbId - The IMDb ID of the movie
|
||||
* @param watchedAt - Optional date when watched
|
||||
*/
|
||||
/**
|
||||
* Mark a movie as watched
|
||||
* @param imdbId - The IMDb ID of the movie
|
||||
|
|
@ -207,7 +235,7 @@ class WatchedService {
|
|||
title?: string
|
||||
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
||||
try {
|
||||
logger.log(`[WatchedService] Marking movie as watched: ${imdbId}`);
|
||||
logger.log(`[WatchedService] Marking movie as watched: ${imdbId} (${title || 'No title'})`);
|
||||
|
||||
const isTraktAuth = await this.traktService.isAuthenticated();
|
||||
let syncedToTrakt = false;
|
||||
|
|
@ -222,10 +250,10 @@ class WatchedService {
|
|||
if (MalAuth.isAuthenticated()) {
|
||||
MalSync.scrobbleEpisode(
|
||||
title || 'Movie', // Use real title or generic fallback
|
||||
1,
|
||||
1,
|
||||
'movie',
|
||||
undefined,
|
||||
1,
|
||||
1,
|
||||
'movie',
|
||||
undefined,
|
||||
imdbId,
|
||||
undefined,
|
||||
malId,
|
||||
|
|
@ -247,7 +275,7 @@ class WatchedService {
|
|||
{
|
||||
content_id: imdbId,
|
||||
content_type: 'movie',
|
||||
title: imdbId,
|
||||
title: title || imdbId,
|
||||
season: null,
|
||||
episode: null,
|
||||
watched_at: watchedAt.getTime(),
|
||||
|
|
@ -342,7 +370,7 @@ class WatchedService {
|
|||
'series',
|
||||
season,
|
||||
showImdbId,
|
||||
releaseDate,
|
||||
releaseDate,
|
||||
malId,
|
||||
dayIndex,
|
||||
tmdbId
|
||||
|
|
@ -373,7 +401,7 @@ class WatchedService {
|
|||
{
|
||||
content_id: showImdbId,
|
||||
content_type: 'series',
|
||||
title: showImdbId,
|
||||
title: showTitle || showImdbId,
|
||||
season,
|
||||
episode,
|
||||
watched_at: watchedAt.getTime(),
|
||||
|
|
@ -398,7 +426,8 @@ class WatchedService {
|
|||
showImdbId: string,
|
||||
showId: string,
|
||||
episodes: Array<{ season: number; episode: number }>,
|
||||
watchedAt: Date = new Date()
|
||||
watchedAt: Date = new Date(),
|
||||
showTitle?: string
|
||||
): Promise<{ success: boolean; syncedToTrakt: boolean; count: number }> {
|
||||
try {
|
||||
if (episodes.length === 0) {
|
||||
|
|
@ -479,7 +508,8 @@ class WatchedService {
|
|||
showId: string,
|
||||
season: number,
|
||||
episodeNumbers: number[],
|
||||
watchedAt: Date = new Date()
|
||||
watchedAt: Date = new Date(),
|
||||
showTitle?: string
|
||||
): Promise<{ success: boolean; syncedToTrakt: boolean; count: number }> {
|
||||
try {
|
||||
logger.log(`[WatchedService] Marking season ${season} as watched for ${showImdbId}`);
|
||||
|
|
@ -525,7 +555,7 @@ class WatchedService {
|
|||
episodeNumbers.map((episode) => ({
|
||||
content_id: showImdbId,
|
||||
content_type: 'series' as const,
|
||||
title: showImdbId,
|
||||
title: showTitle || showImdbId,
|
||||
season,
|
||||
episode,
|
||||
watched_at: watchedAt.getTime(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue