mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
simkl optimizations
This commit is contained in:
parent
f008654ac7
commit
effab63a70
3 changed files with 137 additions and 52 deletions
|
|
@ -16,6 +16,7 @@ export const usePlayerSetup = (
|
||||||
setBrightness: (bri: number) => void,
|
setBrightness: (bri: number) => void,
|
||||||
paused: boolean
|
paused: boolean
|
||||||
) => {
|
) => {
|
||||||
|
const originalAppBrightnessRef = useRef<number | null>(null);
|
||||||
const originalSystemBrightnessRef = useRef<number | null>(null);
|
const originalSystemBrightnessRef = useRef<number | null>(null);
|
||||||
const originalSystemBrightnessModeRef = useRef<number | null>(null);
|
const originalSystemBrightnessModeRef = useRef<number | null>(null);
|
||||||
const isAppBackgrounded = useRef(false);
|
const isAppBackgrounded = useRef(false);
|
||||||
|
|
@ -100,6 +101,7 @@ export const usePlayerSetup = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const currentBrightness = await Brightness.getBrightnessAsync();
|
const currentBrightness = await Brightness.getBrightnessAsync();
|
||||||
|
originalAppBrightnessRef.current = currentBrightness;
|
||||||
setBrightness(currentBrightness);
|
setBrightness(currentBrightness);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('[usePlayerSetup] Error setting brightness', error);
|
logger.warn('[usePlayerSetup] Error setting brightness', error);
|
||||||
|
|
@ -111,12 +113,26 @@ export const usePlayerSetup = (
|
||||||
return () => {
|
return () => {
|
||||||
subscription?.remove();
|
subscription?.remove();
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
async function restoreBrightness() {
|
const restoreBrightness = async () => {
|
||||||
await Brightness.setBrightnessAsync(originalSystemBrightnessRef.current!);
|
try {
|
||||||
setBrightness(originalSystemBrightnessRef.current!);
|
if (Platform.OS === 'android') {
|
||||||
}
|
if (originalSystemBrightnessModeRef.current !== null) {
|
||||||
if (Platform.OS === 'android' && originalSystemBrightnessRef.current !== null)
|
await (Brightness as any).setSystemBrightnessModeAsync?.(originalSystemBrightnessModeRef.current);
|
||||||
restoreBrightness();
|
}
|
||||||
|
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();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,59 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { AppState, AppStateStatus } from 'react-native';
|
import { AppState, AppStateStatus } from 'react-native';
|
||||||
import {
|
import {
|
||||||
SimklService,
|
SimklService,
|
||||||
SimklContentData,
|
SimklContentData,
|
||||||
SimklPlaybackData,
|
SimklPlaybackData,
|
||||||
SimklUserSettings,
|
SimklUserSettings,
|
||||||
SimklStats
|
SimklStats,
|
||||||
|
SimklActivities
|
||||||
} from '../services/simklService';
|
} from '../services/simklService';
|
||||||
import { storageService } from '../services/storageService';
|
import { storageService } from '../services/storageService';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const simklService = SimklService.getInstance();
|
const simklService = SimklService.getInstance();
|
||||||
|
|
||||||
|
let hasLoadedProfileOnce = false;
|
||||||
|
let cachedUserSettings: SimklUserSettings | null = null;
|
||||||
|
let cachedUserStats: SimklStats | null = null;
|
||||||
|
|
||||||
export function useSimklIntegration() {
|
export function useSimklIntegration() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
// Basic lists
|
// Basic lists
|
||||||
const [continueWatching, setContinueWatching] = useState<SimklPlaybackData[]>([]);
|
const [continueWatching, setContinueWatching] = useState<SimklPlaybackData[]>([]);
|
||||||
const [userSettings, setUserSettings] = useState<SimklUserSettings | null>(null);
|
const [userSettings, setUserSettings] = useState<SimklUserSettings | null>(() => cachedUserSettings);
|
||||||
const [userStats, setUserStats] = useState<SimklStats | null>(null);
|
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
|
// Check authentication status
|
||||||
const checkAuthStatus = useCallback(async () => {
|
const checkAuthStatus = useCallback(async () => {
|
||||||
|
|
@ -56,9 +90,17 @@ export function useSimklIntegration() {
|
||||||
try {
|
try {
|
||||||
const settings = await simklService.getUserSettings();
|
const settings = await simklService.getUserSettings();
|
||||||
setUserSettings(settings);
|
setUserSettings(settings);
|
||||||
|
cachedUserSettings = settings;
|
||||||
|
|
||||||
const stats = await simklService.getUserStats();
|
const accountId = settings?.account?.id;
|
||||||
setUserStats(stats);
|
if (accountId) {
|
||||||
|
const stats = await simklService.getUserStats(accountId);
|
||||||
|
setUserStats(stats);
|
||||||
|
cachedUserStats = stats;
|
||||||
|
} else {
|
||||||
|
setUserStats(null);
|
||||||
|
cachedUserStats = null;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[useSimklIntegration] Error loading user profile:', error);
|
logger.error('[useSimklIntegration] Error loading user profile:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -170,9 +212,33 @@ export function useSimklIntegration() {
|
||||||
if (!isAuthenticated) return false;
|
if (!isAuthenticated) return false;
|
||||||
|
|
||||||
try {
|
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();
|
const playback = await simklService.getPlaybackStatus();
|
||||||
logger.log(`[useSimklIntegration] fetched Simkl playback: ${playback.length}`);
|
logger.log(`[useSimklIntegration] fetched Simkl playback: ${playback.length}`);
|
||||||
|
|
||||||
|
setContinueWatching(playback);
|
||||||
|
|
||||||
for (const item of playback) {
|
for (const item of playback) {
|
||||||
let id: string | undefined;
|
let id: string | undefined;
|
||||||
let type: string;
|
let type: string;
|
||||||
|
|
@ -215,9 +281,11 @@ export function useSimklIntegration() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
loadPlaybackStatus();
|
|
||||||
fetchAndMergeSimklProgress();
|
fetchAndMergeSimklProgress();
|
||||||
loadUserProfile();
|
if (!hasLoadedProfileOnce) {
|
||||||
|
hasLoadedProfileOnce = true;
|
||||||
|
loadUserProfile();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, loadPlaybackStatus, fetchAndMergeSimklProgress, loadUserProfile]);
|
}, [isAuthenticated, loadPlaybackStatus, fetchAndMergeSimklProgress, loadUserProfile]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { mmkvStorage } from './mmkvStorage';
|
import { mmkvStorage } from './mmkvStorage';
|
||||||
import { AppState, AppStateStatus } from 'react-native';
|
import { AppState, AppStateStatus } from 'react-native';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import Constants from 'expo-constants';
|
||||||
|
|
||||||
// Storage keys
|
// Storage keys
|
||||||
export const SIMKL_ACCESS_TOKEN_KEY = 'simkl_access_token';
|
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 {
|
export class SimklService {
|
||||||
private static instance: SimklService;
|
private static instance: SimklService;
|
||||||
private accessToken: string | null = null;
|
private accessToken: string | null = null;
|
||||||
|
|
@ -272,10 +286,12 @@ export class SimklService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appVersion = Constants.expoConfig?.version || (Constants as any).manifest?.version || 'unknown';
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${this.accessToken}`,
|
'Authorization': `Bearer ${this.accessToken}`,
|
||||||
'simkl-api-key': SIMKL_CLIENT_ID
|
'simkl-api-key': SIMKL_CLIENT_ID,
|
||||||
|
'User-Agent': `Nuvio/${appVersion}`
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: RequestInit = {
|
const options: RequestInit = {
|
||||||
|
|
@ -518,41 +534,27 @@ export class SimklService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPlaybackStatus(): Promise<SimklPlaybackData[]> {
|
public async getPlaybackStatus(): Promise<SimklPlaybackData[]> {
|
||||||
// Docs: GET /sync/playback/{type} with {type} values `movies` or `episodes`.
|
const playback = await this.apiRequest<SimklPlaybackData[]>('/sync/playback');
|
||||||
// Some docs also mention appending /movie or /episode; we try both variants for safety.
|
const items = Array.isArray(playback) ? playback : [];
|
||||||
const tryEndpoints = async (endpoints: string[]): Promise<SimklPlaybackData[]> => {
|
const sorted = items
|
||||||
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]
|
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime());
|
.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})`);
|
logger.log(`[SimklService] getPlaybackStatus: ${sorted.length} items`);
|
||||||
return combined;
|
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
|
* Get user stats
|
||||||
*/
|
*/
|
||||||
public async getUserStats(): Promise<SimklStats | null> {
|
public async getUserStats(accountId?: number): Promise<SimklStats | null> {
|
||||||
try {
|
try {
|
||||||
if (!await this.isAuthenticated()) {
|
if (!await this.isAuthenticated()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need account ID from settings first
|
const resolvedAccountId = accountId ?? (await this.getUserSettings())?.account?.id;
|
||||||
const settings = await this.getUserSettings();
|
if (!resolvedAccountId) {
|
||||||
if (!settings?.account?.id) {
|
|
||||||
logger.warn('[SimklService] Cannot get user stats: no account ID');
|
logger.warn('[SimklService] Cannot get user stats: no account ID');
|
||||||
return null;
|
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));
|
logger.log('[SimklService] getUserStats:', JSON.stringify(response));
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue