From effab63a70a267aacaee98ece2cc7b919635c2a8 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:57:18 +0530 Subject: [PATCH] simkl optimizations --- .../player/android/hooks/usePlayerSetup.ts | 28 +++++-- src/hooks/useSimklIntegration.ts | 84 +++++++++++++++++-- src/services/simklService.ts | 77 ++++++++--------- 3 files changed, 137 insertions(+), 52 deletions(-) diff --git a/src/components/player/android/hooks/usePlayerSetup.ts b/src/components/player/android/hooks/usePlayerSetup.ts index 05630a8d..f5f1d211 100644 --- a/src/components/player/android/hooks/usePlayerSetup.ts +++ b/src/components/player/android/hooks/usePlayerSetup.ts @@ -16,6 +16,7 @@ export const usePlayerSetup = ( setBrightness: (bri: number) => void, paused: boolean ) => { + const originalAppBrightnessRef = useRef(null); const originalSystemBrightnessRef = useRef(null); const originalSystemBrightnessModeRef = useRef(null); const isAppBackgrounded = useRef(false); @@ -100,6 +101,7 @@ export const usePlayerSetup = ( } } const currentBrightness = await Brightness.getBrightnessAsync(); + originalAppBrightnessRef.current = currentBrightness; setBrightness(currentBrightness); } catch (error) { logger.warn('[usePlayerSetup] Error setting brightness', error); @@ -111,12 +113,26 @@ export const usePlayerSetup = ( return () => { subscription?.remove(); disableImmersiveMode(); - async function restoreBrightness() { - await Brightness.setBrightnessAsync(originalSystemBrightnessRef.current!); - setBrightness(originalSystemBrightnessRef.current!); - } - if (Platform.OS === 'android' && originalSystemBrightnessRef.current !== null) - restoreBrightness(); + const restoreBrightness = async () => { + try { + if (Platform.OS === 'android') { + if (originalSystemBrightnessModeRef.current !== null) { + await (Brightness as any).setSystemBrightnessModeAsync?.(originalSystemBrightnessModeRef.current); + } + if (originalSystemBrightnessRef.current !== null) { + await (Brightness as any).setSystemBrightnessAsync?.(originalSystemBrightnessRef.current); + } + } + if (originalAppBrightnessRef.current !== null) { + await Brightness.setBrightnessAsync(originalAppBrightnessRef.current); + setBrightness(originalAppBrightnessRef.current); + } + } catch (e) { + logger.warn('[usePlayerSetup] Error restoring brightness', e); + } + }; + + restoreBrightness(); }; }, []); diff --git a/src/hooks/useSimklIntegration.ts b/src/hooks/useSimklIntegration.ts index 2e2f19bd..3fe37d53 100644 --- a/src/hooks/useSimklIntegration.ts +++ b/src/hooks/useSimklIntegration.ts @@ -1,25 +1,59 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { AppState, AppStateStatus } from 'react-native'; import { SimklService, SimklContentData, SimklPlaybackData, SimklUserSettings, - SimklStats + SimklStats, + SimklActivities } from '../services/simklService'; import { storageService } from '../services/storageService'; import { logger } from '../utils/logger'; const simklService = SimklService.getInstance(); +let hasLoadedProfileOnce = false; +let cachedUserSettings: SimklUserSettings | null = null; +let cachedUserStats: SimklStats | null = null; + export function useSimklIntegration() { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); // Basic lists const [continueWatching, setContinueWatching] = useState([]); - const [userSettings, setUserSettings] = useState(null); - const [userStats, setUserStats] = useState(null); + const [userSettings, setUserSettings] = useState(() => cachedUserSettings); + const [userStats, setUserStats] = useState(() => cachedUserStats); + + const lastPlaybackFetchAt = useRef(0); + const lastActivitiesCheckAt = useRef(0); + const lastPlaybackActivityAt = useRef(null); + + const parseActivityDate = (value?: string): number | null => { + if (!value) return null; + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? null : parsed; + }; + + const getLatestPlaybackActivity = (activities: SimklActivities | null): number | null => { + if (!activities) return null; + + const candidates: Array = [ + parseActivityDate(activities.playback?.all), + parseActivityDate(activities.playback?.movies), + parseActivityDate(activities.playback?.episodes), + parseActivityDate(activities.playback?.tv), + parseActivityDate(activities.playback?.anime), + parseActivityDate(activities.all), + parseActivityDate((activities as any).last_update), + parseActivityDate((activities as any).updated_at) + ]; + + const timestamps = candidates.filter((value): value is number => typeof value === 'number'); + if (timestamps.length === 0) return null; + return Math.max(...timestamps); + }; // Check authentication status const checkAuthStatus = useCallback(async () => { @@ -56,9 +90,17 @@ export function useSimklIntegration() { try { const settings = await simklService.getUserSettings(); setUserSettings(settings); + cachedUserSettings = settings; - const stats = await simklService.getUserStats(); - setUserStats(stats); + const accountId = settings?.account?.id; + if (accountId) { + const stats = await simklService.getUserStats(accountId); + setUserStats(stats); + cachedUserStats = stats; + } else { + setUserStats(null); + cachedUserStats = null; + } } catch (error) { logger.error('[useSimklIntegration] Error loading user profile:', error); } @@ -170,9 +212,33 @@ export function useSimklIntegration() { if (!isAuthenticated) return false; try { + const now = Date.now(); + if (now - lastActivitiesCheckAt.current < 30000) { + return true; + } + lastActivitiesCheckAt.current = now; + + const activities = await simklService.getActivities(); + const latestPlaybackActivity = getLatestPlaybackActivity(activities); + + if (latestPlaybackActivity && lastPlaybackActivityAt.current === latestPlaybackActivity) { + return true; + } + + if (latestPlaybackActivity) { + lastPlaybackActivityAt.current = latestPlaybackActivity; + } + + if (now - lastPlaybackFetchAt.current < 60000) { + return true; + } + lastPlaybackFetchAt.current = now; + const playback = await simklService.getPlaybackStatus(); logger.log(`[useSimklIntegration] fetched Simkl playback: ${playback.length}`); + setContinueWatching(playback); + for (const item of playback) { let id: string | undefined; let type: string; @@ -215,9 +281,11 @@ export function useSimklIntegration() { useEffect(() => { if (isAuthenticated) { - loadPlaybackStatus(); fetchAndMergeSimklProgress(); - loadUserProfile(); + if (!hasLoadedProfileOnce) { + hasLoadedProfileOnce = true; + loadUserProfile(); + } } }, [isAuthenticated, loadPlaybackStatus, fetchAndMergeSimklProgress, loadUserProfile]); diff --git a/src/services/simklService.ts b/src/services/simklService.ts index 7f6691dd..677664a8 100644 --- a/src/services/simklService.ts +++ b/src/services/simklService.ts @@ -1,6 +1,7 @@ import { mmkvStorage } from './mmkvStorage'; import { AppState, AppStateStatus } from 'react-native'; import { logger } from '../utils/logger'; +import Constants from 'expo-constants'; // Storage keys export const SIMKL_ACCESS_TOKEN_KEY = 'simkl_access_token'; @@ -119,6 +120,19 @@ export interface SimklStats { }; } +export interface SimklActivities { + all?: string; + playback?: { + all?: string; + movies?: string; + episodes?: string; + tv?: string; + anime?: string; + [key: string]: string | undefined; + }; + [key: string]: any; +} + export class SimklService { private static instance: SimklService; private accessToken: string | null = null; @@ -272,10 +286,12 @@ export class SimklService { return null; } + const appVersion = Constants.expoConfig?.version || (Constants as any).manifest?.version || 'unknown'; const headers: HeadersInit = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.accessToken}`, - 'simkl-api-key': SIMKL_CLIENT_ID + 'simkl-api-key': SIMKL_CLIENT_ID, + 'User-Agent': `Nuvio/${appVersion}` }; const options: RequestInit = { @@ -518,41 +534,27 @@ export class SimklService { } public async getPlaybackStatus(): Promise { - // Docs: GET /sync/playback/{type} with {type} values `movies` or `episodes`. - // Some docs also mention appending /movie or /episode; we try both variants for safety. - const tryEndpoints = async (endpoints: string[]): Promise => { - for (const endpoint of endpoints) { - try { - const res = await this.apiRequest(endpoint); - if (Array.isArray(res)) { - logger.log(`[SimklService] getPlaybackStatus: ${endpoint} -> ${res.length} items`); - return res; - } - } catch (e) { - logger.warn(`[SimklService] getPlaybackStatus: ${endpoint} failed`, e); - } - } - return []; - }; - - const movies = await tryEndpoints([ - '/sync/playback/movies', - '/sync/playback/movie', - '/sync/playback?type=movies' - ]); - - const episodes = await tryEndpoints([ - '/sync/playback/episodes', - '/sync/playback/episode', - '/sync/playback?type=episodes' - ]); - - const combined = [...episodes, ...movies] + const playback = await this.apiRequest('/sync/playback'); + const items = Array.isArray(playback) ? playback : []; + const sorted = items .filter(Boolean) .sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()); - logger.log(`[SimklService] getPlaybackStatus: combined ${combined.length} items (episodes=${episodes.length}, movies=${movies.length})`); - return combined; + logger.log(`[SimklService] getPlaybackStatus: ${sorted.length} items`); + return sorted; + } + + /** + * SYNC: Get account activity timestamps + */ + public async getActivities(): Promise { + try { + const response = await this.apiRequest('/sync/activities'); + return response || null; + } catch (error) { + logger.error('[SimklService] Failed to get activities:', error); + return null; + } } /** @@ -585,20 +587,19 @@ export class SimklService { /** * Get user stats */ - public async getUserStats(): Promise { + public async getUserStats(accountId?: number): Promise { try { if (!await this.isAuthenticated()) { return null; } - // Need account ID from settings first - const settings = await this.getUserSettings(); - if (!settings?.account?.id) { + const resolvedAccountId = accountId ?? (await this.getUserSettings())?.account?.id; + if (!resolvedAccountId) { logger.warn('[SimklService] Cannot get user stats: no account ID'); return null; } - const response = await this.apiRequest(`/users/${settings.account.id}/stats`, 'POST'); + const response = await this.apiRequest(`/users/${resolvedAccountId}/stats`, 'POST'); logger.log('[SimklService] getUserStats:', JSON.stringify(response)); return response; } catch (error) {