simkl optimizations

This commit is contained in:
tapframe 2026-01-18 16:57:18 +05:30
parent f008654ac7
commit effab63a70
3 changed files with 137 additions and 52 deletions

View file

@ -16,6 +16,7 @@ export const usePlayerSetup = (
setBrightness: (bri: number) => void,
paused: boolean
) => {
const originalAppBrightnessRef = useRef<number | null>(null);
const originalSystemBrightnessRef = useRef<number | null>(null);
const originalSystemBrightnessModeRef = useRef<number | null>(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();
};
}, []);

View file

@ -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<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
// Basic lists
const [continueWatching, setContinueWatching] = useState<SimklPlaybackData[]>([]);
const [userSettings, setUserSettings] = useState<SimklUserSettings | null>(null);
const [userStats, setUserStats] = useState<SimklStats | null>(null);
const [userSettings, setUserSettings] = useState<SimklUserSettings | null>(() => cachedUserSettings);
const [userStats, setUserStats] = useState<SimklStats | null>(() => cachedUserStats);
const lastPlaybackFetchAt = useRef(0);
const lastActivitiesCheckAt = useRef(0);
const lastPlaybackActivityAt = useRef<number | null>(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<number | null> = [
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]);

View file

@ -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<SimklPlaybackData[]> {
// 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<SimklPlaybackData[]> => {
for (const endpoint of endpoints) {
try {
const res = await this.apiRequest<SimklPlaybackData[]>(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<SimklPlaybackData[]>('/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<SimklActivities | null> {
try {
const response = await this.apiRequest<SimklActivities>('/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<SimklStats | null> {
public async getUserStats(accountId?: number): Promise<SimklStats | null> {
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<SimklStats>(`/users/${settings.account.id}/stats`, 'POST');
const response = await this.apiRequest<SimklStats>(`/users/${resolvedAccountId}/stats`, 'POST');
logger.log('[SimklService] getUserStats:', JSON.stringify(response));
return response;
} catch (error) {