From b7c0bc330437d8db9e24c26760f5f68f20af1b24 Mon Sep 17 00:00:00 2001 From: paregi12 Date: Mon, 9 Feb 2026 20:36:28 +0530 Subject: [PATCH] feat(mal): improve season-aware scrobbling and UI refinements --- src/components/metadata/SeriesContent.tsx | 22 +++------- src/components/player/KSPlayerCore.tsx | 5 ++- src/screens/MalSettingsScreen.tsx | 16 +++++-- src/screens/SettingsScreen.tsx | 45 ++++++++++--------- src/services/mal/MalSync.ts | 53 +++++++++++++++-------- src/services/pluginService.ts | 3 +- src/services/watchedService.ts | 11 ++--- 7 files changed, 81 insertions(+), 74 deletions(-) diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index b07b2b22..22233377 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -579,24 +579,12 @@ const SeriesContentComponent: React.FC = ({ showImdbId, metadata.id, episode.season_number, - episode.episode_number + episode.episode_number, + new Date(), + episode.air_date, + metadata?.name ); - // Sync to MAL - const malEnabled = mmkvStorage.getBoolean('mal_enabled') ?? true; - if (malEnabled && metadata?.name) { - const totalEpisodes = Object.values(groupedEpisodes).reduce((acc, curr) => acc + (curr?.length || 0), 0); - MalSync.scrobbleEpisode( - metadata.name, - episode.episode_number, - totalEpisodes, - 'series', - episode.season_number, - imdbId, - episode.air_date // Pass release date for ARM Sync converter - ); - } - // Reload to ensure consistency (e.g. if optimistic update was slightly off or for other effects) // But we don't strictly *need* to wait for this to update UI loadEpisodesProgress(); @@ -2383,4 +2371,4 @@ const styles = StyleSheet.create({ fontWeight: '800', }, -}); \ No newline at end of file +}); diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index df0f4c4c..342b2f56 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -78,6 +78,7 @@ interface PlayerRouteParams { backdrop?: string; availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; headers?: Record; + releaseDate?: string; initialPosition?: number; } @@ -232,7 +233,7 @@ const KSPlayerCore: React.FC = () => { imdbId, season, episode, - undefined // releaseDate not yet implemented for iOS + releaseDate ); // Gestures @@ -1159,4 +1160,4 @@ const KSPlayerCore: React.FC = () => { ); }; -export default KSPlayerCore; \ No newline at end of file +export default KSPlayerCore; diff --git a/src/screens/MalSettingsScreen.tsx b/src/screens/MalSettingsScreen.tsx index e4b1cbc2..f15b0e29 100644 --- a/src/screens/MalSettingsScreen.tsx +++ b/src/screens/MalSettingsScreen.tsx @@ -203,12 +203,20 @@ const MalSettingsScreen: React.FC = () => { { + onPress={async () => { setIsLoading(true); - MalSync.syncMalToLibrary().then(() => { + try { + const synced = await MalSync.syncMalToLibrary(); + if (synced) { + openAlert('Sync Complete', 'MAL data has been refreshed.'); + } else { + openAlert('Sync Failed', 'Could not refresh MAL data.'); + } + } catch { + openAlert('Sync Failed', 'Could not refresh MAL data.'); + } finally { setIsLoading(false); - openAlert('Sync Complete', 'MAL data has been refreshed.'); - }); + } }} > diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 4ddd039d..b2ec3f11 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -378,31 +378,29 @@ const SettingsScreen: React.FC = () => { return ( {isItemVisible('trakt') && ( - } + } renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} - isLast={!isItemVisible('simkl')} + isLast={!isItemVisible('simkl') && !isItemVisible('mal')} isTablet={isTablet} /> )} {isItemVisible('simkl') && ( - } + } renderControl={() => } onPress={() => navigation.navigate('SimklSettings')} - isLast={false} + isLast={!isItemVisible('mal')} isTablet={isTablet} /> )} - isTablet={isTablet} - /> - )} - } @@ -410,7 +408,8 @@ const SettingsScreen: React.FC = () => { onPress={() => navigation.navigate('MalSettings')} isLast={true} isTablet={isTablet} - /> + /> + )} ); @@ -695,7 +694,7 @@ const SettingsScreen: React.FC = () => { contentContainerStyle={styles.scrollContent} > {/* Account */} - {(settingsConfig?.categories?.['account']?.visible !== false) && (isItemVisible('trakt') || isItemVisible('simkl')) && ( + {(settingsConfig?.categories?.['account']?.visible !== false) && (isItemVisible('trakt') || isItemVisible('simkl') || isItemVisible('mal')) && ( {isItemVisible('trakt') && ( { customIcon={} renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} - isLast={!isItemVisible('simkl')} + isLast={!isItemVisible('simkl') && !isItemVisible('mal')} /> )} {isItemVisible('simkl') && ( @@ -714,19 +713,19 @@ const SettingsScreen: React.FC = () => { customIcon={} renderControl={() => } onPress={() => navigation.navigate('SimklSettings')} - isLast={false} + isLast={!isItemVisible('mal')} /> )} - /> - )} - } renderControl={() => } onPress={() => navigation.navigate('MalSettings')} isLast - /> + /> + )} )} @@ -1234,4 +1233,4 @@ const styles = StyleSheet.create({ }, }); -export default SettingsScreen; \ No newline at end of file +export default SettingsScreen; diff --git a/src/services/mal/MalSync.ts b/src/services/mal/MalSync.ts index 0ca04df7..91350af0 100644 --- a/src/services/mal/MalSync.ts +++ b/src/services/mal/MalSync.ts @@ -6,6 +6,10 @@ import { ArmSyncService } from './ArmSyncService'; import axios from 'axios'; const MAPPING_PREFIX = 'mal_map_'; +const getTitleCacheKey = (title: string, type: 'movie' | 'series', season = 1) => + `${MAPPING_PREFIX}${title.trim()}_${type}_${season}`; +const getLegacyTitleCacheKey = (title: string, type: 'movie' | 'series') => + `${MAPPING_PREFIX}${title.trim()}_${type}`; export const MalSync = { /** @@ -44,15 +48,24 @@ export const MalSync = { getMalId: async (title: string, type: 'movie' | 'series' = 'series', year?: number, season?: number, imdbId?: string, episode: number = 1, releaseDate?: string): Promise => { // Safety check: Never perform a MAL search for generic placeholders or empty strings. // This prevents "cache poisoning" where a generic term matches a random anime. - const normalizedTitle = title.trim().toLowerCase(); + const cleanTitle = title.trim(); + const normalizedTitle = cleanTitle.toLowerCase(); const isGenericTitle = !normalizedTitle || normalizedTitle === 'anime' || normalizedTitle === 'movie'; - - if (isGenericTitle) { - // If we have an offline mapping, we can still try it below, - // but we MUST skip the fuzzy search logic at the end. - if (!imdbId) return null; + + const seasonNumber = season || 1; + const cacheKey = getTitleCacheKey(cleanTitle, type, seasonNumber); + const legacyCacheKey = getLegacyTitleCacheKey(cleanTitle, type); + const cachedId = mmkvStorage.getNumber(cacheKey) || mmkvStorage.getNumber(legacyCacheKey); + if (cachedId) { + // Backfill to season-aware key for future lookups. + if (!mmkvStorage.getNumber(cacheKey)) { + mmkvStorage.setNumber(cacheKey, cachedId); + } + return cachedId; } + if (isGenericTitle && !imdbId) return null; + // 1. Try ARM + Jikan Sync (Most accurate for perfect season/episode matching) if (imdbId && type === 'series' && releaseDate) { try { @@ -72,13 +85,11 @@ export const MalSync = { } } - // 2. Try IMDb ID first (Via online MalSync API) - BUT only for Season 1 or Movies. - - // 2. Check Cache for Title - const cleanTitle = title.trim(); - const cacheKey = `${MAPPING_PREFIX}${cleanTitle}_${type}_${season || 1}`; - const cachedId = mmkvStorage.getNumber(cacheKey); - if (cachedId) return cachedId; + // 2. Try IMDb ID mapping when it is likely to be accurate, or when title is generic. + if (imdbId && (type === 'movie' || seasonNumber <= 1 || isGenericTitle)) { + const idFromImdb = await MalSync.getMalIdFromImdb(imdbId); + if (idFromImdb) return idFromImdb; + } // 3. Search MAL (Skip if generic title) if (isGenericTitle) return null; @@ -120,6 +131,7 @@ export const MalSync = { // Save to cache mmkvStorage.setNumber(cacheKey, bestMatch.id); + mmkvStorage.setNumber(legacyCacheKey, bestMatch.id); return bestMatch.id; } } catch (e) { @@ -290,8 +302,10 @@ export const MalSync = { for (const item of allItems) { const type = item.node.media_type === 'movie' ? 'movie' : 'series'; - const cacheKey = `${MAPPING_PREFIX}${item.node.title.trim()}_${type}`; - mmkvStorage.setNumber(cacheKey, item.node.id); + const title = item.node.title.trim(); + mmkvStorage.setNumber(getTitleCacheKey(title, type, 1), item.node.id); + // Keep legacy key for backwards compatibility with old cache readers. + mmkvStorage.setNumber(getLegacyTitleCacheKey(title, type), item.node.id); } console.log(`[MalSync] Synced ${allItems.length} items to mapping cache.`); return true; @@ -304,9 +318,11 @@ export const MalSync = { /** * Manually map an ID if auto-detection fails */ - setMapping: (title: string, malId: number, type: 'movie' | 'series' = 'series') => { - const cacheKey = `${MAPPING_PREFIX}${title.trim()}_${type}`; - mmkvStorage.setNumber(cacheKey, malId); + setMapping: (title: string, malId: number, type: 'movie' | 'series' = 'series', season: number = 1) => { + const cleanTitle = title.trim(); + mmkvStorage.setNumber(getTitleCacheKey(cleanTitle, type, season), malId); + // Keep legacy key for compatibility. + mmkvStorage.setNumber(getLegacyTitleCacheKey(cleanTitle, type), malId); }, /** @@ -449,4 +465,3 @@ export const MalSync = { } } }; - diff --git a/src/services/pluginService.ts b/src/services/pluginService.ts index 634964b5..1e0ed6c3 100644 --- a/src/services/pluginService.ts +++ b/src/services/pluginService.ts @@ -1476,7 +1476,6 @@ class LocalScraperService { } }; ->>>>>>> upstream/main // Execution timeout (1 minute) const PLUGIN_TIMEOUT_MS = 60000; const functionName = params.functionName || 'getStreams'; @@ -1829,4 +1828,4 @@ class LocalScraperService { export const localScraperService = LocalScraperService.getInstance(); export const pluginService = localScraperService; // Alias for UI consistency -export default localScraperService; \ No newline at end of file +export default localScraperService; diff --git a/src/services/watchedService.ts b/src/services/watchedService.ts index e441fd44..5fecc3e6 100644 --- a/src/services/watchedService.ts +++ b/src/services/watchedService.ts @@ -53,7 +53,7 @@ class WatchedService { const malToken = MalAuth.getToken(); if (malToken) { MalSync.scrobbleEpisode( - 'Movie', + imdbId, 1, 1, 'movie', @@ -93,7 +93,8 @@ class WatchedService { season: number, episode: number, watchedAt: Date = new Date(), - releaseDate?: string // Optional release date for precise matching + releaseDate?: string, // Optional release date for precise matching + showTitle?: string ): Promise<{ success: boolean; syncedToTrakt: boolean }> { try { logger.log(`[WatchedService] Marking episode as watched: ${showImdbId} S${season}E${episode}`); @@ -132,7 +133,7 @@ class WatchedService { // Strategy 2: Offline Mapping Fallback if (!synced) { MalSync.scrobbleEpisode( - 'Anime', // Title fallback + showTitle || showImdbId || 'Anime', episode, 0, 'series', @@ -411,10 +412,6 @@ class WatchedService { showImdbId, season ); - syncedToTrakt = await this.traktService.removeSeasonFromHistory( - showImdbId, - season - ); logger.log(`[WatchedService] Trakt season removal result: ${syncedToTrakt}`); }