diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 2049b701..0b7db54c 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -1576,7 +1576,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat let tmdbId; let stremioId = id; // Use TMDB-resolved type if available — handles "other", "Movie", etc. - let effectiveStreamType: string = resolvedTypeRef.current || normalizedType; + // Use metadata.type first (from addon meta response), then TMDB-resolved, then normalized + let effectiveStreamType: string = metadata?.type || resolvedTypeRef.current || normalizedType; if (id.startsWith('tmdb:')) { tmdbId = id.split(':')[1]; @@ -1631,7 +1632,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const allStremioAddons = await stremioService.getInstalledAddons(); const localScrapers = await localScraperService.getInstalledScrapers(); - const requestedStreamType = type; + // Use the best available type — not raw type which may be "other" + const requestedStreamType = metadata?.type || resolvedTypeRef.current || normalizedType; const pickEligibleStreamAddons = (requestType: string) => allStremioAddons.filter(addon => { diff --git a/src/hooks/useTraktAutosync.ts b/src/hooks/useTraktAutosync.ts index 4c2f681a..f4cbe5a7 100644 --- a/src/hooks/useTraktAutosync.ts +++ b/src/hooks/useTraktAutosync.ts @@ -309,14 +309,6 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { return; } - // Skip if session was already stopped (e.g. after background/pause). - // Without this, the fallback "force start" block inside handleProgressUpdate - // would fire a new /scrobble/start on the first periodic save after a remount, - // bypassing the hasStopped guard in handlePlaybackStart entirely. - if (hasStopped.current) { - return; - } - try { const rawProgress = (currentTime / duration) * 100; const progressPercent = Math.min(100, Math.max(0, rawProgress)); @@ -340,6 +332,19 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { lastSyncTime.current = now; lastSyncProgress.current = progressPercent; + // If this update crossed the completion threshold, Trakt will have silently + // scrobbled it. Mark complete now so unmount/background don't fire a second + // /scrobble/stop above threshold and create a duplicate history entry. + if (progressPercent >= autosyncSettings.completionThreshold) { + isSessionComplete.current = true; + const ck = getContentKey(options); + const existing = recentlyStoppedSessions.get(ck); + if (existing) { + recentlyStoppedSessions.set(ck, { ...existing, isComplete: true, progress: progressPercent }); + } + logger.log(`[TraktAutosync] Threshold reached via immediate progress update (${progressPercent.toFixed(1)}%), marking session complete`); + } + // Update local storage sync status await storageService.updateTraktSyncStatus( options.id, @@ -375,6 +380,19 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { lastSyncTime.current = now; lastSyncProgress.current = progressPercent; + // If this periodic update crossed the completion threshold, Trakt will have + // silently scrobbled it. Mark complete now so unmount/background don't fire + // a second /scrobble/stop above threshold and create a duplicate history entry. + if (progressPercent >= autosyncSettings.completionThreshold) { + isSessionComplete.current = true; + const ck = getContentKey(options); + const existing = recentlyStoppedSessions.get(ck); + if (existing) { + recentlyStoppedSessions.set(ck, { ...existing, isComplete: true, progress: progressPercent }); + } + logger.log(`[TraktAutosync] Threshold reached via progress update (${progressPercent.toFixed(1)}%), marking session complete to prevent duplicate scrobble`); + } + // Update local storage sync status await storageService.updateTraktSyncStatus( options.id, @@ -385,7 +403,6 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { currentTime ); - // Progress sync logging removed logger.log(`[TraktAutosync] Trakt: Progress updated to ${progressPercent.toFixed(1)}%`); } } diff --git a/src/services/catalog/search.ts b/src/services/catalog/search.ts index 95407c8e..b9142a48 100644 --- a/src/services/catalog/search.ts +++ b/src/services/catalog/search.ts @@ -325,12 +325,21 @@ async function searchAddonCatalog( // meta addon by ID prefix matching. Setting it here causes 404s when two addons // are installed and one returns IDs the other can't serve metadata for. - const normalizedCatalogType = type ? type.toLowerCase() : type; - if (normalizedCatalogType && content.type !== normalizedCatalogType) { - content.type = normalizedCatalogType; - } else if (content.type) { + // Always lowercase the item's own type first + if (content.type) { content.type = content.type.toLowerCase(); } + + // Only stamp the catalog type if the item doesn't already have a standard type. + // Prevents catalog type "other" from overwriting correct types like "movie"/"series" + // that the addon already set on individual items. + const normalizedCatalogType = type ? type.toLowerCase() : type; + const STANDARD_TYPES = new Set(['movie', 'series', 'anime.movie', 'anime.series', 'anime', 'tv', 'channel']); + if (normalizedCatalogType && !STANDARD_TYPES.has(content.type) && STANDARD_TYPES.has(normalizedCatalogType)) { + content.type = normalizedCatalogType; + } else if (normalizedCatalogType && !content.type) { + content.type = normalizedCatalogType; + } return content; }); @@ -384,9 +393,17 @@ function dedupeAndStampResults(results: StreamingContent[], catalogType: string) } } - return Array.from(bestById.values()).map(item => - catalogType && item.type !== catalogType ? { ...item, type: catalogType } : item - ); + const normalizedCatalogType = catalogType ? catalogType.toLowerCase() : catalogType; + const STANDARD_TYPES = new Set(['movie', 'series', 'anime.movie', 'anime.series', 'anime', 'tv', 'channel']); + + return Array.from(bestById.values()).map(item => { + // Only stamp catalog type if item doesn't already have a standard type. + // Prevents "other" from overwriting correct types like "movie"/"series". + if (normalizedCatalogType && !STANDARD_TYPES.has(item.type) && STANDARD_TYPES.has(normalizedCatalogType)) { + return { ...item, type: normalizedCatalogType }; + } + return item; + }); } function buildSectionName(