mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
added simkl watchlists
This commit is contained in:
parent
f4bd44d3e0
commit
c36210e9c2
5 changed files with 1453 additions and 77 deletions
17
App.tsx
17
App.tsx
|
|
@ -30,6 +30,7 @@ import 'react-native-reanimated';
|
|||
import { CatalogProvider } from './src/contexts/CatalogContext';
|
||||
import { GenreProvider } from './src/contexts/GenreContext';
|
||||
import { TraktProvider } from './src/contexts/TraktContext';
|
||||
import { SimklProvider } from './src/contexts/SimklContext';
|
||||
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
|
||||
import { TrailerProvider } from './src/contexts/TrailerContext';
|
||||
import { DownloadsProvider } from './src/contexts/DownloadsContext';
|
||||
|
|
@ -263,13 +264,15 @@ function App(): React.JSX.Element {
|
|||
<GenreProvider>
|
||||
<CatalogProvider>
|
||||
<TraktProvider>
|
||||
<ThemeProvider>
|
||||
<TrailerProvider>
|
||||
<ToastProvider>
|
||||
<ThemedApp />
|
||||
</ToastProvider>
|
||||
</TrailerProvider>
|
||||
</ThemeProvider>
|
||||
<SimklProvider>
|
||||
<ThemeProvider>
|
||||
<TrailerProvider>
|
||||
<ToastProvider>
|
||||
<ThemedApp />
|
||||
</ToastProvider>
|
||||
</TrailerProvider>
|
||||
</ThemeProvider>
|
||||
</SimklProvider>
|
||||
</TraktProvider>
|
||||
</CatalogProvider>
|
||||
</GenreProvider>
|
||||
|
|
|
|||
85
src/contexts/SimklContext.tsx
Normal file
85
src/contexts/SimklContext.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { useSimklIntegration } from '../hooks/useSimklIntegration';
|
||||
import {
|
||||
SimklWatchlistItem,
|
||||
SimklPlaybackData,
|
||||
SimklRatingItem,
|
||||
SimklUserSettings,
|
||||
SimklStats,
|
||||
SimklStatus
|
||||
} from '../services/simklService';
|
||||
|
||||
export interface SimklContextProps {
|
||||
// Authentication
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
userSettings: SimklUserSettings | null;
|
||||
userStats: SimklStats | null;
|
||||
|
||||
// Collections - Shows
|
||||
watchingShows: SimklWatchlistItem[];
|
||||
planToWatchShows: SimklWatchlistItem[];
|
||||
completedShows: SimklWatchlistItem[];
|
||||
onHoldShows: SimklWatchlistItem[];
|
||||
droppedShows: SimklWatchlistItem[];
|
||||
|
||||
// Collections - Movies
|
||||
watchingMovies: SimklWatchlistItem[];
|
||||
planToWatchMovies: SimklWatchlistItem[];
|
||||
completedMovies: SimklWatchlistItem[];
|
||||
onHoldMovies: SimklWatchlistItem[];
|
||||
droppedMovies: SimklWatchlistItem[];
|
||||
|
||||
// Collections - Anime
|
||||
watchingAnime: SimklWatchlistItem[];
|
||||
planToWatchAnime: SimklWatchlistItem[];
|
||||
completedAnime: SimklWatchlistItem[];
|
||||
onHoldAnime: SimklWatchlistItem[];
|
||||
droppedAnime: SimklWatchlistItem[];
|
||||
|
||||
// Special collections
|
||||
continueWatching: SimklPlaybackData[];
|
||||
ratedContent: SimklRatingItem[];
|
||||
|
||||
// Lookup Sets (for O(1) status checks)
|
||||
watchingSet: Set<string>;
|
||||
planToWatchSet: Set<string>;
|
||||
completedSet: Set<string>;
|
||||
onHoldSet: Set<string>;
|
||||
droppedSet: Set<string>;
|
||||
|
||||
// Methods
|
||||
checkAuthStatus: () => Promise<void>;
|
||||
refreshAuthStatus: () => Promise<void>;
|
||||
loadAllCollections: () => Promise<void>;
|
||||
addToStatus: (imdbId: string, type: 'movie' | 'show' | 'anime', status: SimklStatus) => Promise<boolean>;
|
||||
removeFromStatus: (imdbId: string, type: 'movie' | 'show' | 'anime', status: SimklStatus) => Promise<boolean>;
|
||||
isInStatus: (imdbId: string, type: 'movie' | 'show' | 'anime', status: SimklStatus) => boolean;
|
||||
|
||||
// Scrobbling methods (from existing hook)
|
||||
startWatching?: (content: any, progress: number) => Promise<boolean>;
|
||||
updateProgress?: (content: any, progress: number) => Promise<boolean>;
|
||||
stopWatching?: (content: any, progress: number) => Promise<boolean>;
|
||||
syncAllProgress?: () => Promise<boolean>;
|
||||
fetchAndMergeSimklProgress?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const SimklContext = createContext<SimklContextProps | undefined>(undefined);
|
||||
|
||||
export function SimklProvider({ children }: { children: ReactNode }) {
|
||||
const simklIntegration = useSimklIntegration();
|
||||
|
||||
return (
|
||||
<SimklContext.Provider value={simklIntegration}>
|
||||
{children}
|
||||
</SimklContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSimklContext() {
|
||||
const context = useContext(SimklContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSimklContext must be used within a SimklProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -6,36 +6,106 @@ import {
|
|||
SimklPlaybackData,
|
||||
SimklUserSettings,
|
||||
SimklStats,
|
||||
SimklActivities
|
||||
SimklActivities,
|
||||
SimklWatchlistItem,
|
||||
SimklRatingItem,
|
||||
SimklStatus
|
||||
} from '../services/simklService';
|
||||
import { storageService } from '../services/storageService';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const simklService = SimklService.getInstance();
|
||||
|
||||
// Cache keys
|
||||
const SIMKL_ACTIVITIES_CACHE = '@simkl:activities';
|
||||
const SIMKL_COLLECTIONS_CACHE = '@simkl:collections';
|
||||
const SIMKL_CACHE_TIMESTAMP = '@simkl:cache_timestamp';
|
||||
|
||||
let hasLoadedProfileOnce = false;
|
||||
let cachedUserSettings: SimklUserSettings | null = null;
|
||||
let cachedUserStats: SimklStats | null = null;
|
||||
|
||||
interface CollectionsCache {
|
||||
timestamp: number;
|
||||
watchingShows: SimklWatchlistItem[];
|
||||
watchingMovies: SimklWatchlistItem[];
|
||||
watchingAnime: SimklWatchlistItem[];
|
||||
planToWatchShows: SimklWatchlistItem[];
|
||||
planToWatchMovies: SimklWatchlistItem[];
|
||||
planToWatchAnime: SimklWatchlistItem[];
|
||||
completedShows: SimklWatchlistItem[];
|
||||
completedMovies: SimklWatchlistItem[];
|
||||
completedAnime: SimklWatchlistItem[];
|
||||
onHoldShows: SimklWatchlistItem[];
|
||||
onHoldMovies: SimklWatchlistItem[];
|
||||
onHoldAnime: SimklWatchlistItem[];
|
||||
droppedShows: SimklWatchlistItem[];
|
||||
droppedMovies: SimklWatchlistItem[];
|
||||
droppedAnime: SimklWatchlistItem[];
|
||||
continueWatching: SimklPlaybackData[];
|
||||
ratedContent: SimklRatingItem[];
|
||||
}
|
||||
|
||||
export function useSimklIntegration() {
|
||||
// Authentication state
|
||||
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>(() => cachedUserSettings);
|
||||
const [userStats, setUserStats] = useState<SimklStats | null>(() => cachedUserStats);
|
||||
|
||||
// Collection state - Shows
|
||||
const [watchingShows, setWatchingShows] = useState<SimklWatchlistItem[]>([]);
|
||||
const [planToWatchShows, setPlanToWatchShows] = useState<SimklWatchlistItem[]>([]);
|
||||
const [completedShows, setCompletedShows] = useState<SimklWatchlistItem[]>([]);
|
||||
const [onHoldShows, setOnHoldShows] = useState<SimklWatchlistItem[]>([]);
|
||||
const [droppedShows, setDroppedShows] = useState<SimklWatchlistItem[]>([]);
|
||||
|
||||
// Collection state - Movies
|
||||
const [watchingMovies, setWatchingMovies] = useState<SimklWatchlistItem[]>([]);
|
||||
const [planToWatchMovies, setPlanToWatchMovies] = useState<SimklWatchlistItem[]>([]);
|
||||
const [completedMovies, setCompletedMovies] = useState<SimklWatchlistItem[]>([]);
|
||||
const [onHoldMovies, setOnHoldMovies] = useState<SimklWatchlistItem[]>([]);
|
||||
const [droppedMovies, setDroppedMovies] = useState<SimklWatchlistItem[]>([]);
|
||||
|
||||
// Collection state - Anime
|
||||
const [watchingAnime, setWatchingAnime] = useState<SimklWatchlistItem[]>([]);
|
||||
const [planToWatchAnime, setPlanToWatchAnime] = useState<SimklWatchlistItem[]>([]);
|
||||
const [completedAnime, setCompletedAnime] = useState<SimklWatchlistItem[]>([]);
|
||||
const [onHoldAnime, setOnHoldAnime] = useState<SimklWatchlistItem[]>([]);
|
||||
const [droppedAnime, setDroppedAnime] = useState<SimklWatchlistItem[]>([]);
|
||||
|
||||
// Special collections
|
||||
const [continueWatching, setContinueWatching] = useState<SimklPlaybackData[]>([]);
|
||||
const [ratedContent, setRatedContent] = useState<SimklRatingItem[]>([]);
|
||||
|
||||
// Lookup Sets for O(1) status checks (combined across types)
|
||||
const [watchingSet, setWatchingSet] = useState<Set<string>>(new Set());
|
||||
const [planToWatchSet, setPlanToWatchSet] = useState<Set<string>>(new Set());
|
||||
const [completedSet, setCompletedSet] = useState<Set<string>>(new Set());
|
||||
const [onHoldSet, setOnHoldSet] = useState<Set<string>>(new Set());
|
||||
const [droppedSet, setDroppedSet] = useState<Set<string>>(new Set());
|
||||
|
||||
// Activity tracking for caching
|
||||
const [lastActivityCheck, setLastActivityCheck] = useState<SimklActivities | null>(null);
|
||||
|
||||
const lastPlaybackFetchAt = useRef(0);
|
||||
const lastActivitiesCheckAt = useRef(0);
|
||||
const lastPlaybackActivityAt = useRef<number | null>(null);
|
||||
|
||||
// Helper: Normalize IMDB ID
|
||||
const normalizeImdbId = (imdbId: string): string => {
|
||||
return imdbId.replace('tt', '');
|
||||
};
|
||||
|
||||
// Helper: Parse activity date
|
||||
const parseActivityDate = (value?: string): number | null => {
|
||||
if (!value) return null;
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
};
|
||||
|
||||
// Helper: Get latest playback activity timestamp
|
||||
const getLatestPlaybackActivity = (activities: SimklActivities | null): number | null => {
|
||||
if (!activities) return null;
|
||||
|
||||
|
|
@ -55,6 +125,127 @@ export function useSimklIntegration() {
|
|||
return Math.max(...timestamps);
|
||||
};
|
||||
|
||||
// Helper: Build lookup Sets
|
||||
const buildLookupSets = useCallback((
|
||||
watchingItems: SimklWatchlistItem[],
|
||||
planItems: SimklWatchlistItem[],
|
||||
completedItems: SimklWatchlistItem[],
|
||||
holdItems: SimklWatchlistItem[],
|
||||
droppedItems: SimklWatchlistItem[]
|
||||
) => {
|
||||
const buildSet = (items: SimklWatchlistItem[]): Set<string> => {
|
||||
const set = new Set<string>();
|
||||
items.forEach(item => {
|
||||
const content = item.show || item.movie || item.anime;
|
||||
if (content?.ids?.imdb) {
|
||||
const type = item.show ? 'show' : item.movie ? 'movie' : 'anime';
|
||||
const key = `${type}:${normalizeImdbId(content.ids.imdb)}`;
|
||||
set.add(key);
|
||||
}
|
||||
});
|
||||
return set;
|
||||
};
|
||||
|
||||
setWatchingSet(buildSet(watchingItems));
|
||||
setPlanToWatchSet(buildSet(planItems));
|
||||
setCompletedSet(buildSet(completedItems));
|
||||
setOnHoldSet(buildSet(holdItems));
|
||||
setDroppedSet(buildSet(droppedItems));
|
||||
}, []);
|
||||
|
||||
// Load collections from cache
|
||||
const loadFromCache = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
const cachedData = await mmkvStorage.getItem(SIMKL_COLLECTIONS_CACHE);
|
||||
if (!cachedData) return false;
|
||||
|
||||
const cache: CollectionsCache = JSON.parse(cachedData);
|
||||
|
||||
// Check cache age (5 minutes)
|
||||
const age = Date.now() - cache.timestamp;
|
||||
if (age > 5 * 60 * 1000) {
|
||||
logger.log('[useSimklIntegration] Cache expired');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug: Log cache sample to check poster data
|
||||
if (cache.watchingShows && cache.watchingShows.length > 0) {
|
||||
logger.log('[useSimklIntegration] Cache sample - first watching show:', JSON.stringify(cache.watchingShows[0], null, 2));
|
||||
}
|
||||
if (cache.watchingMovies && cache.watchingMovies.length > 0) {
|
||||
logger.log('[useSimklIntegration] Cache sample - first watching movie:', JSON.stringify(cache.watchingMovies[0], null, 2));
|
||||
}
|
||||
|
||||
// Load into state
|
||||
setWatchingShows(cache.watchingShows || []);
|
||||
setWatchingMovies(cache.watchingMovies || []);
|
||||
setWatchingAnime(cache.watchingAnime || []);
|
||||
setPlanToWatchShows(cache.planToWatchShows || []);
|
||||
setPlanToWatchMovies(cache.planToWatchMovies || []);
|
||||
setPlanToWatchAnime(cache.planToWatchAnime || []);
|
||||
setCompletedShows(cache.completedShows || []);
|
||||
setCompletedMovies(cache.completedMovies || []);
|
||||
setCompletedAnime(cache.completedAnime || []);
|
||||
setOnHoldShows(cache.onHoldShows || []);
|
||||
setOnHoldMovies(cache.onHoldMovies || []);
|
||||
setOnHoldAnime(cache.onHoldAnime || []);
|
||||
setDroppedShows(cache.droppedShows || []);
|
||||
setDroppedMovies(cache.droppedMovies || []);
|
||||
setDroppedAnime(cache.droppedAnime || []);
|
||||
setContinueWatching(cache.continueWatching || []);
|
||||
setRatedContent(cache.ratedContent || []);
|
||||
|
||||
// Build lookup Sets
|
||||
buildLookupSets(
|
||||
[...cache.watchingShows, ...cache.watchingMovies, ...cache.watchingAnime],
|
||||
[...cache.planToWatchShows, ...cache.planToWatchMovies, ...cache.planToWatchAnime],
|
||||
[...cache.completedShows, ...cache.completedMovies, ...cache.completedAnime],
|
||||
[...cache.onHoldShows, ...cache.onHoldMovies, ...cache.onHoldAnime],
|
||||
[...cache.droppedShows, ...cache.droppedMovies, ...cache.droppedAnime]
|
||||
);
|
||||
|
||||
logger.log('[useSimklIntegration] Loaded from cache');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('[useSimklIntegration] Failed to load from cache:', error);
|
||||
return false;
|
||||
}
|
||||
}, [buildLookupSets]);
|
||||
|
||||
// Save collections to cache
|
||||
const saveToCache = useCallback(async (collections: Omit<CollectionsCache, 'timestamp'>) => {
|
||||
try {
|
||||
const cache: CollectionsCache = {
|
||||
...collections,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
await mmkvStorage.setItem(SIMKL_COLLECTIONS_CACHE, JSON.stringify(cache));
|
||||
logger.log('[useSimklIntegration] Saved to cache');
|
||||
} catch (error) {
|
||||
logger.error('[useSimklIntegration] Failed to save to cache:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Compare activities to check if refresh needed
|
||||
const compareActivities = useCallback((
|
||||
newActivities: SimklActivities | null,
|
||||
cachedActivities: SimklActivities | null
|
||||
): boolean => {
|
||||
if (!cachedActivities) return true;
|
||||
if (!newActivities) return false;
|
||||
|
||||
// Compare timestamps
|
||||
const newAll = parseActivityDate(newActivities.all);
|
||||
const cachedAll = parseActivityDate(cachedActivities.all);
|
||||
|
||||
if (newAll && cachedAll && newAll > cachedAll) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
// Check authentication status
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
|
|
@ -73,7 +264,262 @@ export function useSimklIntegration() {
|
|||
await checkAuthStatus();
|
||||
}, [checkAuthStatus]);
|
||||
|
||||
// Load playback/continue watching
|
||||
// Load all collections (main data loading method)
|
||||
const loadAllCollections = useCallback(async () => {
|
||||
if (!isAuthenticated) {
|
||||
logger.log('[useSimklIntegration] Cannot load collections: not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// 1. Check activities first (efficient timestamp check)
|
||||
const activities = await simklService.getActivities();
|
||||
|
||||
// 2. Try to load from cache if activities haven't changed
|
||||
const cachedActivitiesStr = await mmkvStorage.getItem(SIMKL_ACTIVITIES_CACHE);
|
||||
const cachedActivities: SimklActivities | null = cachedActivitiesStr ? JSON.parse(cachedActivitiesStr) : null;
|
||||
|
||||
const needsRefresh = compareActivities(activities, cachedActivities);
|
||||
|
||||
if (!needsRefresh && cachedActivities) {
|
||||
const cacheLoaded = await loadFromCache();
|
||||
if (cacheLoaded) {
|
||||
setLastActivityCheck(activities);
|
||||
logger.log('[useSimklIntegration] Using cached collections');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('[useSimklIntegration] Fetching fresh collections from API');
|
||||
|
||||
// 3. Fetch all collections in parallel
|
||||
const [
|
||||
watchingShowsData,
|
||||
watchingMoviesData,
|
||||
watchingAnimeData,
|
||||
planToWatchShowsData,
|
||||
planToWatchMoviesData,
|
||||
planToWatchAnimeData,
|
||||
completedShowsData,
|
||||
completedMoviesData,
|
||||
completedAnimeData,
|
||||
onHoldShowsData,
|
||||
onHoldMoviesData,
|
||||
onHoldAnimeData,
|
||||
droppedShowsData,
|
||||
droppedMoviesData,
|
||||
droppedAnimeData,
|
||||
continueWatchingData,
|
||||
ratingsData
|
||||
] = await Promise.all([
|
||||
simklService.getAllItems('shows', 'watching'),
|
||||
simklService.getAllItems('movies', 'watching'),
|
||||
simklService.getAllItems('anime', 'watching'),
|
||||
simklService.getAllItems('shows', 'plantowatch'),
|
||||
simklService.getAllItems('movies', 'plantowatch'),
|
||||
simklService.getAllItems('anime', 'plantowatch'),
|
||||
simklService.getAllItems('shows', 'completed'),
|
||||
simklService.getAllItems('movies', 'completed'),
|
||||
simklService.getAllItems('anime', 'completed'),
|
||||
simklService.getAllItems('shows', 'hold'),
|
||||
simklService.getAllItems('movies', 'hold'),
|
||||
simklService.getAllItems('anime', 'hold'),
|
||||
simklService.getAllItems('shows', 'dropped'),
|
||||
simklService.getAllItems('movies', 'dropped'),
|
||||
simklService.getAllItems('anime', 'dropped'),
|
||||
simklService.getPlaybackStatus(),
|
||||
simklService.getRatings()
|
||||
]);
|
||||
|
||||
// 4. Update state
|
||||
setWatchingShows(watchingShowsData);
|
||||
setWatchingMovies(watchingMoviesData);
|
||||
setWatchingAnime(watchingAnimeData);
|
||||
setPlanToWatchShows(planToWatchShowsData);
|
||||
setPlanToWatchMovies(planToWatchMoviesData);
|
||||
setPlanToWatchAnime(planToWatchAnimeData);
|
||||
setCompletedShows(completedShowsData);
|
||||
setCompletedMovies(completedMoviesData);
|
||||
setCompletedAnime(completedAnimeData);
|
||||
setOnHoldShows(onHoldShowsData);
|
||||
setOnHoldMovies(onHoldMoviesData);
|
||||
setOnHoldAnime(onHoldAnimeData);
|
||||
setDroppedShows(droppedShowsData);
|
||||
setDroppedMovies(droppedMoviesData);
|
||||
setDroppedAnime(droppedAnimeData);
|
||||
setContinueWatching(continueWatchingData);
|
||||
setRatedContent(ratingsData);
|
||||
|
||||
// 5. Build lookup Sets
|
||||
buildLookupSets(
|
||||
[...watchingShowsData, ...watchingMoviesData, ...watchingAnimeData],
|
||||
[...planToWatchShowsData, ...planToWatchMoviesData, ...planToWatchAnimeData],
|
||||
[...completedShowsData, ...completedMoviesData, ...completedAnimeData],
|
||||
[...onHoldShowsData, ...onHoldMoviesData, ...onHoldAnimeData],
|
||||
[...droppedShowsData, ...droppedMoviesData, ...droppedAnimeData]
|
||||
);
|
||||
|
||||
// 6. Cache everything
|
||||
await saveToCache({
|
||||
watchingShows: watchingShowsData,
|
||||
watchingMovies: watchingMoviesData,
|
||||
watchingAnime: watchingAnimeData,
|
||||
planToWatchShows: planToWatchShowsData,
|
||||
planToWatchMovies: planToWatchMoviesData,
|
||||
planToWatchAnime: planToWatchAnimeData,
|
||||
completedShows: completedShowsData,
|
||||
completedMovies: completedMoviesData,
|
||||
completedAnime: completedAnimeData,
|
||||
onHoldShows: onHoldShowsData,
|
||||
onHoldMovies: onHoldMoviesData,
|
||||
onHoldAnime: onHoldAnimeData,
|
||||
droppedShows: droppedShowsData,
|
||||
droppedMovies: droppedMoviesData,
|
||||
droppedAnime: droppedAnimeData,
|
||||
continueWatching: continueWatchingData,
|
||||
ratedContent: ratingsData
|
||||
});
|
||||
|
||||
// Save activities
|
||||
if (activities) {
|
||||
await mmkvStorage.setItem(SIMKL_ACTIVITIES_CACHE, JSON.stringify(activities));
|
||||
setLastActivityCheck(activities);
|
||||
}
|
||||
|
||||
logger.log('[useSimklIntegration] Collections loaded successfully');
|
||||
} catch (error) {
|
||||
logger.error('[useSimklIntegration] Error loading collections:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isAuthenticated, buildLookupSets, compareActivities, loadFromCache, saveToCache]);
|
||||
|
||||
// Status management methods
|
||||
const addToStatus = useCallback(async (
|
||||
imdbId: string,
|
||||
type: 'movie' | 'show' | 'anime',
|
||||
status: SimklStatus
|
||||
): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const success = await simklService.addToList(imdbId, type, status);
|
||||
|
||||
if (success) {
|
||||
// Optimistic Set update
|
||||
const normalizedId = normalizeImdbId(imdbId);
|
||||
const key = `${type}:${normalizedId}`;
|
||||
|
||||
// Update appropriate Set
|
||||
switch (status) {
|
||||
case 'watching':
|
||||
setWatchingSet(prev => new Set(prev).add(key));
|
||||
break;
|
||||
case 'plantowatch':
|
||||
setPlanToWatchSet(prev => new Set(prev).add(key));
|
||||
break;
|
||||
case 'completed':
|
||||
setCompletedSet(prev => new Set(prev).add(key));
|
||||
break;
|
||||
case 'hold':
|
||||
setOnHoldSet(prev => new Set(prev).add(key));
|
||||
break;
|
||||
case 'dropped':
|
||||
setDroppedSet(prev => new Set(prev).add(key));
|
||||
break;
|
||||
}
|
||||
|
||||
// Reload collections to get fresh data
|
||||
setTimeout(() => loadAllCollections(), 1000);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('[useSimklIntegration] Error adding to status:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated, loadAllCollections]);
|
||||
|
||||
const removeFromStatus = useCallback(async (
|
||||
imdbId: string,
|
||||
type: 'movie' | 'show' | 'anime',
|
||||
status: SimklStatus
|
||||
): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const success = await simklService.removeFromList(imdbId, type);
|
||||
|
||||
if (success) {
|
||||
// Optimistic Set update
|
||||
const normalizedId = normalizeImdbId(imdbId);
|
||||
const key = `${type}:${normalizedId}`;
|
||||
|
||||
// Remove from all Sets
|
||||
setWatchingSet(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(key);
|
||||
return newSet;
|
||||
});
|
||||
setPlanToWatchSet(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(key);
|
||||
return newSet;
|
||||
});
|
||||
setCompletedSet(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(key);
|
||||
return newSet;
|
||||
});
|
||||
setOnHoldSet(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(key);
|
||||
return newSet;
|
||||
});
|
||||
setDroppedSet(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(key);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Reload collections
|
||||
setTimeout(() => loadAllCollections(), 1000);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('[useSimklIntegration] Error removing from status:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated, loadAllCollections]);
|
||||
|
||||
const isInStatus = useCallback((
|
||||
imdbId: string,
|
||||
type: 'movie' | 'show' | 'anime',
|
||||
status: SimklStatus
|
||||
): boolean => {
|
||||
const normalizedId = normalizeImdbId(imdbId);
|
||||
const key = `${type}:${normalizedId}`;
|
||||
|
||||
switch (status) {
|
||||
case 'watching':
|
||||
return watchingSet.has(key);
|
||||
case 'plantowatch':
|
||||
return planToWatchSet.has(key);
|
||||
case 'completed':
|
||||
return completedSet.has(key);
|
||||
case 'hold':
|
||||
return onHoldSet.has(key);
|
||||
case 'dropped':
|
||||
return droppedSet.has(key);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [watchingSet, planToWatchSet, completedSet, onHoldSet, droppedSet]);
|
||||
|
||||
// Load playback/continue watching (kept from original)
|
||||
const loadPlaybackStatus = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
try {
|
||||
|
|
@ -84,7 +530,7 @@ export function useSimklIntegration() {
|
|||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Load user settings and stats
|
||||
// Load user settings and stats (kept from original)
|
||||
const loadUserProfile = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
try {
|
||||
|
|
@ -106,7 +552,7 @@ export function useSimklIntegration() {
|
|||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Start watching (scrobble start)
|
||||
// Scrobbling methods (kept from original)
|
||||
const startWatching = useCallback(async (content: SimklContentData, progress: number): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
try {
|
||||
|
|
@ -118,7 +564,6 @@ export function useSimklIntegration() {
|
|||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Update progress (scrobble pause)
|
||||
const updateProgress = useCallback(async (content: SimklContentData, progress: number): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
try {
|
||||
|
|
@ -130,7 +575,6 @@ export function useSimklIntegration() {
|
|||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Stop watching (scrobble stop)
|
||||
const stopWatching = useCallback(async (content: SimklContentData, progress: number): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
try {
|
||||
|
|
@ -142,16 +586,12 @@ export function useSimklIntegration() {
|
|||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Sync All Local Progress -> Simkl
|
||||
// Sync methods (kept from original)
|
||||
const syncAllProgress = useCallback(async (): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const unsynced = await storageService.getUnsyncedProgress();
|
||||
// Filter for items that specifically need SIMKL sync (unsynced.filter(i => !i.progress.simklSynced...))
|
||||
// storageService.getUnsyncedProgress currently returns items that need Trakt OR Simkl sync.
|
||||
// We should check simklSynced specifically here.
|
||||
|
||||
const itemsToSync = unsynced.filter(i => !i.progress.simklSynced || (i.progress.simklLastSynced && i.progress.lastUpdated > i.progress.simklLastSynced));
|
||||
|
||||
if (itemsToSync.length === 0) return true;
|
||||
|
|
@ -163,10 +603,9 @@ export function useSimklIntegration() {
|
|||
const season = item.episodeId ? parseInt(item.episodeId.split('S')[1]?.split('E')[0] || '0') : undefined;
|
||||
const episode = item.episodeId ? parseInt(item.episodeId.split('E')[1] || '0') : undefined;
|
||||
|
||||
// Construct content data
|
||||
const content: SimklContentData = {
|
||||
type: item.type === 'series' ? 'episode' : 'movie',
|
||||
title: 'Unknown', // Ideally storage has title, but it might not. Simkl needs IDs mainly.
|
||||
title: 'Unknown',
|
||||
ids: { imdb: item.id },
|
||||
season,
|
||||
episode
|
||||
|
|
@ -174,21 +613,15 @@ export function useSimklIntegration() {
|
|||
|
||||
const progressPercent = (item.progress.currentTime / item.progress.duration) * 100;
|
||||
|
||||
// If completed (>=80% or 95% depending on logic, let's say 85% safe), add to history
|
||||
// Simkl: Stop with >= 80% marks as watched.
|
||||
// Or explicitly add to history.
|
||||
|
||||
let success = false;
|
||||
if (progressPercent >= 85) {
|
||||
// Add to history
|
||||
if (content.type === 'movie') {
|
||||
await simklService.addToHistory({ movies: [{ ids: { imdb: item.id } }] });
|
||||
} else {
|
||||
await simklService.addToHistory({ shows: [{ ids: { imdb: item.id }, seasons: [{ number: season, episodes: [{ number: episode }] }] }] });
|
||||
}
|
||||
success = true; // Assume success if no throw
|
||||
success = true;
|
||||
} else {
|
||||
// Pause (scrobble)
|
||||
const res = await simklService.scrobblePause(content, progressPercent);
|
||||
success = !!res;
|
||||
}
|
||||
|
|
@ -207,7 +640,6 @@ export function useSimklIntegration() {
|
|||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Fetch Simkl -> Merge Local
|
||||
const fetchAndMergeSimklProgress = useCallback(async (): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
|
|
@ -263,7 +695,6 @@ export function useSimklIntegration() {
|
|||
episodeId
|
||||
);
|
||||
|
||||
// Mark as synced locally so we don't push it back
|
||||
await storageService.updateSimklSyncStatus(id, type!, true, item.progress, episodeId);
|
||||
}
|
||||
}
|
||||
|
|
@ -272,7 +703,7 @@ export function useSimklIntegration() {
|
|||
logger.error('[useSimklIntegration] Error fetching/merging Simkl progress', e);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
}, [isAuthenticated, getLatestPlaybackActivity]);
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
|
|
@ -287,7 +718,7 @@ export function useSimklIntegration() {
|
|||
loadUserProfile();
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, loadPlaybackStatus, fetchAndMergeSimklProgress, loadUserProfile]);
|
||||
}, [isAuthenticated, fetchAndMergeSimklProgress, loadUserProfile]);
|
||||
|
||||
// App state listener for sync
|
||||
useEffect(() => {
|
||||
|
|
@ -295,24 +726,64 @@ export function useSimklIntegration() {
|
|||
const sub = AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') {
|
||||
fetchAndMergeSimklProgress();
|
||||
loadAllCollections();
|
||||
}
|
||||
});
|
||||
return () => sub.remove();
|
||||
}, [isAuthenticated, fetchAndMergeSimklProgress]);
|
||||
|
||||
}, [isAuthenticated, fetchAndMergeSimklProgress, loadAllCollections]);
|
||||
|
||||
return {
|
||||
// Authentication
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
userSettings,
|
||||
userStats,
|
||||
checkAuthStatus,
|
||||
refreshAuthStatus,
|
||||
|
||||
// Collections - Shows
|
||||
watchingShows,
|
||||
planToWatchShows,
|
||||
completedShows,
|
||||
onHoldShows,
|
||||
droppedShows,
|
||||
|
||||
// Collections - Movies
|
||||
watchingMovies,
|
||||
planToWatchMovies,
|
||||
completedMovies,
|
||||
onHoldMovies,
|
||||
droppedMovies,
|
||||
|
||||
// Collections - Anime
|
||||
watchingAnime,
|
||||
planToWatchAnime,
|
||||
completedAnime,
|
||||
onHoldAnime,
|
||||
droppedAnime,
|
||||
|
||||
// Special collections
|
||||
continueWatching,
|
||||
ratedContent,
|
||||
|
||||
// Lookup Sets
|
||||
watchingSet,
|
||||
planToWatchSet,
|
||||
completedSet,
|
||||
onHoldSet,
|
||||
droppedSet,
|
||||
|
||||
// Methods
|
||||
loadAllCollections,
|
||||
addToStatus,
|
||||
removeFromStatus,
|
||||
isInStatus,
|
||||
|
||||
// Scrobbling (kept from original)
|
||||
startWatching,
|
||||
updateProgress,
|
||||
stopWatching,
|
||||
syncAllProgress,
|
||||
fetchAndMergeSimklProgress,
|
||||
continueWatching,
|
||||
userSettings,
|
||||
userStats,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
Platform,
|
||||
ScrollView,
|
||||
BackHandler,
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
|
@ -34,6 +35,7 @@ import { logger } from '../utils/logger';
|
|||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTraktContext } from '../contexts/TraktContext';
|
||||
import { useSimklContext } from '../contexts/SimklContext';
|
||||
import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
||||
import { traktService, TraktService, TraktImages } from '../services/traktService';
|
||||
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
|
||||
|
|
@ -87,6 +89,8 @@ function getGridLayout(screenWidth: number): { numColumns: number; itemWidth: nu
|
|||
return { numColumns, itemWidth };
|
||||
}
|
||||
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
|
||||
const TraktItem = React.memo(({
|
||||
item,
|
||||
width,
|
||||
|
|
@ -109,12 +113,47 @@ const TraktItem = React.memo(({
|
|||
const url = TraktService.getTraktPosterUrl(item.images);
|
||||
if (isMounted && url) {
|
||||
setPosterUrl(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.imdbId || item.traktId) {
|
||||
try {
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
let tmdbId: number | null = null;
|
||||
|
||||
if (item.imdbId) {
|
||||
tmdbId = await tmdbService.findTMDBIdByIMDB(item.imdbId);
|
||||
}
|
||||
|
||||
if (!tmdbId && item.traktId) {
|
||||
|
||||
}
|
||||
|
||||
if (tmdbId) {
|
||||
let posterPath: string | null = null;
|
||||
|
||||
if (item.type === 'movie') {
|
||||
const details = await tmdbService.getMovieDetails(String(tmdbId));
|
||||
posterPath = details?.poster_path ?? null;
|
||||
} else {
|
||||
const details = await tmdbService.getTVShowDetails(tmdbId);
|
||||
posterPath = details?.poster_path ?? null;
|
||||
}
|
||||
|
||||
if (isMounted && posterPath) {
|
||||
const url = tmdbService.getImageUrl(posterPath, 'w500');
|
||||
setPosterUrl(url);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Failed to fetch poster from TMDB', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchPoster();
|
||||
return () => { isMounted = false; };
|
||||
}, [item.images]);
|
||||
}, [item.images, item.imdbId, item.traktId, item.type]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (item.imdbId) {
|
||||
|
|
@ -219,9 +258,11 @@ const LibraryScreen = () => {
|
|||
const { numColumns, itemWidth } = useMemo(() => getGridLayout(width), [width]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
||||
const [filter, setFilter] = useState<'trakt' | 'movies' | 'series'>('movies');
|
||||
const [filter, setFilter] = useState<'trakt' | 'simkl' | 'movies' | 'series'>('movies');
|
||||
const [showTraktContent, setShowTraktContent] = useState(false);
|
||||
const [selectedTraktFolder, setSelectedTraktFolder] = useState<string | null>(null);
|
||||
const [showSimklContent, setShowSimklContent] = useState(false);
|
||||
const [selectedSimklFolder, setSelectedSimklFolder] = useState<string | null>(null);
|
||||
const { showInfo, showError } = useToast();
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<LibraryItem | null>(null);
|
||||
|
|
@ -230,7 +271,6 @@ const LibraryScreen = () => {
|
|||
const { settings } = useSettings();
|
||||
const flashListRef = useRef<any>(null);
|
||||
|
||||
// Scroll to top handler
|
||||
const scrollToTop = useCallback(() => {
|
||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
}, []);
|
||||
|
|
@ -252,6 +292,29 @@ const LibraryScreen = () => {
|
|||
loadAllCollections
|
||||
} = useTraktContext();
|
||||
|
||||
const {
|
||||
isAuthenticated: simklAuthenticated,
|
||||
isLoading: simklLoading,
|
||||
watchingShows,
|
||||
watchingMovies,
|
||||
watchingAnime,
|
||||
planToWatchShows,
|
||||
planToWatchMovies,
|
||||
planToWatchAnime,
|
||||
completedShows,
|
||||
completedMovies,
|
||||
completedAnime,
|
||||
onHoldShows,
|
||||
onHoldMovies,
|
||||
onHoldAnime,
|
||||
droppedShows,
|
||||
droppedMovies,
|
||||
droppedAnime,
|
||||
continueWatching: simklContinueWatching,
|
||||
ratedContent: simklRatedContent,
|
||||
loadAllCollections: loadSimklCollections
|
||||
} = useSimklContext();
|
||||
|
||||
useEffect(() => {
|
||||
const applyStatusBarConfig = () => {
|
||||
StatusBar.setBarStyle('light-content');
|
||||
|
|
@ -276,12 +339,20 @@ const LibraryScreen = () => {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
if (showSimklContent) {
|
||||
if (selectedSimklFolder) {
|
||||
setSelectedSimklFolder(null);
|
||||
} else {
|
||||
setShowSimklContent(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
|
||||
return () => backHandler.remove();
|
||||
}, [showTraktContent, selectedTraktFolder]);
|
||||
}, [showTraktContent, showSimklContent, selectedTraktFolder, selectedSimklFolder]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLibrary = async () => {
|
||||
|
|
@ -396,6 +467,117 @@ const LibraryScreen = () => {
|
|||
return folders.filter(folder => folder.itemCount > 0);
|
||||
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
||||
|
||||
const simklFolders = useMemo((): TraktFolder[] => {
|
||||
if (!simklAuthenticated) return [];
|
||||
|
||||
const folders: TraktFolder[] = [
|
||||
{
|
||||
id: 'continue-watching',
|
||||
name: t('library.continue'),
|
||||
icon: 'play-circle-outline',
|
||||
itemCount: simklContinueWatching?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'watching-shows',
|
||||
name: 'Watching Shows',
|
||||
icon: 'visibility',
|
||||
itemCount: watchingShows?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'watching-movies',
|
||||
name: 'Watching Movies',
|
||||
icon: 'visibility',
|
||||
itemCount: watchingMovies?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'watching-anime',
|
||||
name: 'Watching Anime',
|
||||
icon: 'visibility',
|
||||
itemCount: watchingAnime?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'plantowatch-shows',
|
||||
name: 'Plan to Watch Shows',
|
||||
icon: 'bookmark-border',
|
||||
itemCount: planToWatchShows?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'plantowatch-movies',
|
||||
name: 'Plan to Watch Movies',
|
||||
icon: 'bookmark-border',
|
||||
itemCount: planToWatchMovies?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'plantowatch-anime',
|
||||
name: 'Plan to Watch Anime',
|
||||
icon: 'bookmark-border',
|
||||
itemCount: planToWatchAnime?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'completed-shows',
|
||||
name: 'Completed Shows',
|
||||
icon: 'check-circle',
|
||||
itemCount: completedShows?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'completed-movies',
|
||||
name: 'Completed Movies',
|
||||
icon: 'check-circle',
|
||||
itemCount: completedMovies?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'completed-anime',
|
||||
name: 'Completed Anime',
|
||||
icon: 'check-circle',
|
||||
itemCount: completedAnime?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'onhold-shows',
|
||||
name: 'On Hold Shows',
|
||||
icon: 'pause-circle-outline',
|
||||
itemCount: onHoldShows?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'onhold-movies',
|
||||
name: 'On Hold Movies',
|
||||
icon: 'pause-circle-outline',
|
||||
itemCount: onHoldMovies?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'onhold-anime',
|
||||
name: 'On Hold Anime',
|
||||
icon: 'pause-circle-outline',
|
||||
itemCount: onHoldAnime?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'dropped-shows',
|
||||
name: 'Dropped Shows',
|
||||
icon: 'cancel',
|
||||
itemCount: droppedShows?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'dropped-movies',
|
||||
name: 'Dropped Movies',
|
||||
icon: 'cancel',
|
||||
itemCount: droppedMovies?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'dropped-anime',
|
||||
name: 'Dropped Anime',
|
||||
icon: 'cancel',
|
||||
itemCount: droppedAnime?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'ratings',
|
||||
name: t('library.rated'),
|
||||
icon: 'star',
|
||||
itemCount: simklRatedContent?.length || 0,
|
||||
}
|
||||
];
|
||||
|
||||
return folders.filter(folder => folder.itemCount > 0);
|
||||
}, [simklAuthenticated, watchingShows, watchingMovies, watchingAnime, planToWatchShows, planToWatchMovies, planToWatchAnime, completedShows, completedMovies, completedAnime, onHoldShows, onHoldMovies, onHoldAnime, droppedShows, droppedMovies, droppedAnime, simklContinueWatching, simklRatedContent, t]);
|
||||
|
||||
const renderItem = ({ item }: { item: LibraryItem }) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.itemContainer, { width: itemWidth }]}
|
||||
|
|
@ -712,6 +894,233 @@ const LibraryScreen = () => {
|
|||
});
|
||||
}, [watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
||||
|
||||
const getSimklFolderItems = useCallback((folderId: string): TraktDisplayItem[] => {
|
||||
const items: TraktDisplayItem[] = [];
|
||||
|
||||
switch (folderId) {
|
||||
case 'continue-watching':
|
||||
return (simklContinueWatching || []).map(item => {
|
||||
const content = item.show || item.movie;
|
||||
return {
|
||||
id: String(content?.ids?.simkl || Math.random()),
|
||||
name: content?.title || 'Unknown',
|
||||
type: item.show ? 'series' : 'movie',
|
||||
poster: '',
|
||||
year: content?.year,
|
||||
lastWatched: item.paused_at,
|
||||
imdbId: content?.ids?.imdb,
|
||||
traktId: content?.ids?.simkl || 0,
|
||||
};
|
||||
});
|
||||
|
||||
case 'watching-shows':
|
||||
return (watchingShows || []).map(item => ({
|
||||
id: String(item.show?.ids?.simkl || Math.random()),
|
||||
name: item.show?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.show?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
rating: item.user_rating,
|
||||
imdbId: item.show?.ids?.imdb,
|
||||
traktId: item.show?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'watching-movies':
|
||||
return (watchingMovies || []).map(item => ({
|
||||
id: String(item.movie?.ids?.simkl || Math.random()),
|
||||
name: item.movie?.title || 'Unknown',
|
||||
type: 'movie' as const,
|
||||
poster: '',
|
||||
year: item.movie?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
rating: item.user_rating,
|
||||
imdbId: item.movie?.ids?.imdb,
|
||||
traktId: item.movie?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'watching-anime':
|
||||
return (watchingAnime || []).map(item => ({
|
||||
id: String(item.anime?.ids?.simkl || Math.random()),
|
||||
name: item.anime?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.anime?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
rating: item.user_rating,
|
||||
imdbId: item.anime?.ids?.imdb,
|
||||
traktId: item.anime?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'plantowatch-shows':
|
||||
return (planToWatchShows || []).map(item => ({
|
||||
id: String(item.show?.ids?.simkl || Math.random()),
|
||||
name: item.show?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.show?.year,
|
||||
lastWatched: item.added_to_watchlist_at,
|
||||
imdbId: item.show?.ids?.imdb,
|
||||
traktId: item.show?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'plantowatch-movies':
|
||||
return (planToWatchMovies || []).map(item => ({
|
||||
id: String(item.movie?.ids?.simkl || Math.random()),
|
||||
name: item.movie?.title || 'Unknown',
|
||||
type: 'movie' as const,
|
||||
poster: '',
|
||||
year: item.movie?.year,
|
||||
lastWatched: item.added_to_watchlist_at,
|
||||
imdbId: item.movie?.ids?.imdb,
|
||||
traktId: item.movie?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'plantowatch-anime':
|
||||
return (planToWatchAnime || []).map(item => ({
|
||||
id: String(item.anime?.ids?.simkl || Math.random()),
|
||||
name: item.anime?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.anime?.year,
|
||||
lastWatched: item.added_to_watchlist_at,
|
||||
imdbId: item.anime?.ids?.imdb,
|
||||
traktId: item.anime?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'completed-shows':
|
||||
return (completedShows || []).map(item => ({
|
||||
id: String(item.show?.ids?.simkl || Math.random()),
|
||||
name: item.show?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.show?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.show?.ids?.imdb,
|
||||
traktId: item.show?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'completed-movies':
|
||||
return (completedMovies || []).map(item => ({
|
||||
id: String(item.movie?.ids?.simkl || Math.random()),
|
||||
name: item.movie?.title || 'Unknown',
|
||||
type: 'movie' as const,
|
||||
poster: '',
|
||||
year: item.movie?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.movie?.ids?.imdb,
|
||||
traktId: item.movie?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'completed-anime':
|
||||
return (completedAnime || []).map(item => ({
|
||||
id: String(item.anime?.ids?.simkl || Math.random()),
|
||||
name: item.anime?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.anime?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.anime?.ids?.imdb,
|
||||
traktId: item.anime?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'onhold-shows':
|
||||
return (onHoldShows || []).map(item => ({
|
||||
id: String(item.show?.ids?.simkl || Math.random()),
|
||||
name: item.show?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.show?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.show?.ids?.imdb,
|
||||
traktId: item.show?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'onhold-movies':
|
||||
return (onHoldMovies || []).map(item => ({
|
||||
id: String(item.movie?.ids?.simkl || Math.random()),
|
||||
name: item.movie?.title || 'Unknown',
|
||||
type: 'movie' as const,
|
||||
poster: '',
|
||||
year: item.movie?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.movie?.ids?.imdb,
|
||||
traktId: item.movie?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'onhold-anime':
|
||||
return (onHoldAnime || []).map(item => ({
|
||||
id: String(item.anime?.ids?.simkl || Math.random()),
|
||||
name: item.anime?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.anime?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.anime?.ids?.imdb,
|
||||
traktId: item.anime?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'dropped-shows':
|
||||
return (droppedShows || []).map(item => ({
|
||||
id: String(item.show?.ids?.simkl || Math.random()),
|
||||
name: item.show?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.show?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.show?.ids?.imdb,
|
||||
traktId: item.show?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'dropped-movies':
|
||||
return (droppedMovies || []).map(item => ({
|
||||
id: String(item.movie?.ids?.simkl || Math.random()),
|
||||
name: item.movie?.title || 'Unknown',
|
||||
type: 'movie' as const,
|
||||
poster: '',
|
||||
year: item.movie?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.movie?.ids?.imdb,
|
||||
traktId: item.movie?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'dropped-anime':
|
||||
return (droppedAnime || []).map(item => ({
|
||||
id: String(item.anime?.ids?.simkl || Math.random()),
|
||||
name: item.anime?.title || 'Unknown',
|
||||
type: 'series' as const,
|
||||
poster: '',
|
||||
year: item.anime?.year,
|
||||
lastWatched: item.last_watched_at,
|
||||
imdbId: item.anime?.ids?.imdb,
|
||||
traktId: item.anime?.ids?.simkl || 0,
|
||||
}));
|
||||
|
||||
case 'ratings':
|
||||
return (simklRatedContent || []).map(item => {
|
||||
const content = item.show || item.movie || item.anime;
|
||||
const type = item.show ? 'series' : item.movie ? 'movie' : 'series';
|
||||
return {
|
||||
id: String(content?.ids?.simkl || Math.random()),
|
||||
name: content?.title || 'Unknown',
|
||||
type,
|
||||
poster: '',
|
||||
year: content?.year,
|
||||
lastWatched: item.rated_at,
|
||||
rating: item.rating,
|
||||
imdbId: content?.ids?.imdb,
|
||||
traktId: content?.ids?.simkl || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return items.sort((a, b) => {
|
||||
const dateA = a.lastWatched ? new Date(a.lastWatched).getTime() : 0;
|
||||
const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}, [simklContinueWatching, watchingShows, watchingMovies, watchingAnime, planToWatchShows, planToWatchMovies, planToWatchAnime, completedShows, completedMovies, completedAnime, onHoldShows, onHoldMovies, onHoldAnime, droppedShows, droppedMovies, droppedAnime, simklRatedContent]);
|
||||
|
||||
const renderTraktContent = () => {
|
||||
if (traktLoading) {
|
||||
return <TraktLoadingSpinner />;
|
||||
|
|
@ -800,7 +1209,133 @@ const LibraryScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderFilter = (filterType: 'trakt' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => {
|
||||
const renderSimklCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.itemContainer, { width: itemWidth }]}
|
||||
onPress={() => {
|
||||
setSelectedSimklFolder(folder.id);
|
||||
loadSimklCollections();
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<View style={styles.folderGradient}>
|
||||
<MaterialIcons
|
||||
name={folder.icon}
|
||||
size={48}
|
||||
color={currentTheme.colors.white}
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
|
||||
{folder.name}
|
||||
</Text>
|
||||
<Text style={styles.folderCount}>
|
||||
{folder.itemCount} {t('library.items')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const renderSimklContent = () => {
|
||||
if (simklLoading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', paddingBottom: 80 }}>
|
||||
<Image
|
||||
source={require('../../assets/simkl-logo.png')}
|
||||
style={{ width: 120, height: 40, tintColor: currentTheme.colors.text }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedSimklFolder) {
|
||||
if (simklFolders.length === 0) {
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="video-library" size={80} color={currentTheme.colors.lightGray} />
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
{t('library.no_trakt')}
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
{t('library.no_trakt_desc')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
}]}
|
||||
onPress={() => {
|
||||
loadSimklCollections();
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>{t('library.load_collections')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={simklFolders}
|
||||
renderItem={({ item }) => renderSimklCollectionFolder({ folder: item })}
|
||||
keyExtractor={item => item.id}
|
||||
numColumns={numColumns}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onEndReachedThreshold={0.7}
|
||||
onEndReached={() => { }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const folderItems = getSimklFolderItems(selectedSimklFolder);
|
||||
|
||||
if (folderItems.length === 0) {
|
||||
const folderName = simklFolders.find(f => f.id === selectedSimklFolder)?.name || t('library.collection');
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="video-library" size={80} color={currentTheme.colors.lightGray} />
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>{t('library.empty_folder', { folder: folderName })}</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
{t('library.empty_folder_desc')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
}]}
|
||||
onPress={() => {
|
||||
loadSimklCollections();
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Refresh</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={folderItems}
|
||||
renderItem={({ item }) => renderTraktItem({ item })}
|
||||
keyExtractor={(item) => `${item.type}-${item.id}`}
|
||||
numColumns={numColumns}
|
||||
style={styles.traktContainer}
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom + 80 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onEndReachedThreshold={0.7}
|
||||
onEndReached={() => { }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFilter = (filterType: 'trakt' | 'simkl' | 'movies' | 'series', label: string) => {
|
||||
const isActive = filter === filterType;
|
||||
|
||||
return (
|
||||
|
|
@ -821,22 +1356,20 @@ const LibraryScreen = () => {
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (filterType === 'simkl') {
|
||||
if (!simklAuthenticated) {
|
||||
navigation.navigate('Settings');
|
||||
} else {
|
||||
setShowSimklContent(true);
|
||||
setSelectedSimklFolder(null);
|
||||
loadSimklCollections();
|
||||
}
|
||||
return;
|
||||
}
|
||||
setFilter(filterType);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{filterType === 'trakt' ? (
|
||||
<View style={[styles.filterIcon, { justifyContent: 'center', alignItems: 'center' }]}>
|
||||
<TraktIcon width={18} height={18} style={{ opacity: isActive ? 1 : 0.6 }} />
|
||||
</View>
|
||||
) : (
|
||||
<MaterialIcons
|
||||
name={iconName}
|
||||
size={22}
|
||||
color={isActive ? currentTheme.colors.white : currentTheme.colors.mediumGray}
|
||||
style={styles.filterIcon}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
styles.filterText,
|
||||
|
|
@ -912,14 +1445,26 @@ const LibraryScreen = () => {
|
|||
? (selectedTraktFolder
|
||||
? traktFolders.find(f => f.id === selectedTraktFolder)?.name || t('library.collection')
|
||||
: t('library.trakt_collection'))
|
||||
: t('library.title')
|
||||
: showSimklContent
|
||||
? (selectedSimklFolder
|
||||
? simklFolders.find(f => f.id === selectedSimklFolder)?.name || t('library.collection')
|
||||
: 'SIMKL Collections')
|
||||
: t('library.title')
|
||||
}
|
||||
showBackButton={showTraktContent}
|
||||
onBackPress={showTraktContent ? () => {
|
||||
if (selectedTraktFolder) {
|
||||
setSelectedTraktFolder(null);
|
||||
} else {
|
||||
setShowTraktContent(false);
|
||||
showBackButton={showTraktContent || showSimklContent}
|
||||
onBackPress={(showTraktContent || showSimklContent) ? () => {
|
||||
if (showTraktContent) {
|
||||
if (selectedTraktFolder) {
|
||||
setSelectedTraktFolder(null);
|
||||
} else {
|
||||
setShowTraktContent(false);
|
||||
}
|
||||
} else if (showSimklContent) {
|
||||
if (selectedSimklFolder) {
|
||||
setSelectedSimklFolder(null);
|
||||
} else {
|
||||
setShowSimklContent(false);
|
||||
}
|
||||
}
|
||||
} : undefined}
|
||||
useMaterialIcons={showTraktContent}
|
||||
|
|
@ -929,15 +1474,16 @@ const LibraryScreen = () => {
|
|||
/>
|
||||
|
||||
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
{!showTraktContent && (
|
||||
{!showTraktContent && !showSimklContent && (
|
||||
<View style={styles.filtersContainer}>
|
||||
{renderFilter('trakt', 'Trakt', 'pan-tool')}
|
||||
{renderFilter('movies', t('search.movies'), 'movie')}
|
||||
{renderFilter('series', t('search.tv_shows'), 'live-tv')}
|
||||
{renderFilter('trakt', 'Trakt')}
|
||||
{renderFilter('simkl', 'SIMKL')}
|
||||
{renderFilter('movies', t('search.movies'))}
|
||||
{renderFilter('series', t('search.tv_shows'))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{showTraktContent ? renderTraktContent() : renderContent()}
|
||||
{showTraktContent ? renderTraktContent() : showSimklContent ? renderSimklContent() : renderContent()}
|
||||
</View>
|
||||
|
||||
{selectedItem && (
|
||||
|
|
@ -1065,13 +1611,11 @@ const styles = StyleSheet.create({
|
|||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||
aspectRatio: 2 / 3,
|
||||
// Consistent shadow/elevation matching ContentItem
|
||||
elevation: Platform.OS === 'android' ? 1 : 0,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 1,
|
||||
// Consistent border styling
|
||||
borderWidth: 1.5,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -66,11 +66,13 @@ export interface SimklPlaybackData {
|
|||
movie?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
show?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
episode?: {
|
||||
|
|
@ -130,9 +132,95 @@ export interface SimklActivities {
|
|||
anime?: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
movies?: {
|
||||
all?: string;
|
||||
rated_at?: string;
|
||||
plantowatch?: string;
|
||||
completed?: string;
|
||||
dropped?: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
shows?: {
|
||||
all?: string;
|
||||
rated_at?: string;
|
||||
playback?: string;
|
||||
plantowatch?: string;
|
||||
watching?: string;
|
||||
completed?: string;
|
||||
hold?: string;
|
||||
dropped?: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
anime?: {
|
||||
all?: string;
|
||||
rated_at?: string;
|
||||
playback?: string;
|
||||
plantowatch?: string;
|
||||
watching?: string;
|
||||
completed?: string;
|
||||
hold?: string;
|
||||
dropped?: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SimklWatchlistItem {
|
||||
movie?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
show?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
anime?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
status?: 'watching' | 'plantowatch' | 'completed' | 'hold' | 'dropped';
|
||||
last_watched_at?: string;
|
||||
user_rating?: number;
|
||||
watched_episodes_count?: number;
|
||||
total_episodes_count?: number;
|
||||
last_watched?: string;
|
||||
next_to_watch?: string;
|
||||
added_to_watchlist_at?: string;
|
||||
}
|
||||
|
||||
export interface SimklRatingItem {
|
||||
movie?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
show?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
anime?: {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string;
|
||||
ids: SimklIds;
|
||||
};
|
||||
rating: number;
|
||||
rated_at: string;
|
||||
last_watched_at?: string;
|
||||
user_rated_at?: string;
|
||||
}
|
||||
|
||||
export type SimklStatus = 'watching' | 'plantowatch' | 'completed' | 'hold' | 'dropped';
|
||||
|
||||
export class SimklService {
|
||||
private static instance: SimklService;
|
||||
private accessToken: string | null = null;
|
||||
|
|
@ -603,16 +691,201 @@ export class SimklService {
|
|||
}
|
||||
|
||||
/**
|
||||
* SYNC: Get Full Watch History (summary)
|
||||
* Optimization: Check /sync/activities first in real usage.
|
||||
* For now, we implement simple fetch.
|
||||
* SYNC: Get all items from user's lists
|
||||
* Enhanced version with type and status filtering
|
||||
*/
|
||||
public async getAllItems(dateFrom?: string): Promise<any> {
|
||||
public async getAllItems(
|
||||
type?: 'movies' | 'shows' | 'anime',
|
||||
status?: SimklStatus,
|
||||
dateFrom?: string
|
||||
): Promise<SimklWatchlistItem[]> {
|
||||
let url = '/sync/all-items/';
|
||||
if (dateFrom) {
|
||||
url += `?date_from=${dateFrom}`;
|
||||
|
||||
if (type) {
|
||||
url += `${type}/`;
|
||||
}
|
||||
return await this.apiRequest(url);
|
||||
|
||||
if (status) {
|
||||
url += status;
|
||||
}
|
||||
|
||||
const params: string[] = [];
|
||||
if (dateFrom) {
|
||||
params.push(`date_from=${dateFrom}`);
|
||||
}
|
||||
|
||||
if (params.length > 0) {
|
||||
url += `?${params.join('&')}`;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.log(`[SimklService] getAllItems: Fetching ${url}`);
|
||||
const response = await this.apiRequest<any>(url);
|
||||
if (!response) {
|
||||
logger.log('[SimklService] getAllItems: No response from API');
|
||||
return [];
|
||||
}
|
||||
|
||||
logger.log('[SimklService] getAllItems: Response keys:', Object.keys(response));
|
||||
|
||||
// Parse response based on type
|
||||
const items: SimklWatchlistItem[] = [];
|
||||
|
||||
if (response.movies && type === 'movies') {
|
||||
logger.log(`[SimklService] getAllItems: Returning ${response.movies.length} movies`);
|
||||
if (response.movies.length > 0) {
|
||||
logger.log('[SimklService] getAllItems: First movie sample:', JSON.stringify(response.movies[0], null, 2));
|
||||
}
|
||||
return response.movies || [];
|
||||
}
|
||||
if (response.shows && type === 'shows') {
|
||||
logger.log(`[SimklService] getAllItems: Returning ${response.shows.length} shows`);
|
||||
if (response.shows.length > 0) {
|
||||
logger.log('[SimklService] getAllItems: First show sample:', JSON.stringify(response.shows[0], null, 2));
|
||||
}
|
||||
return response.shows || [];
|
||||
}
|
||||
if (response.anime && type === 'anime') {
|
||||
logger.log(`[SimklService] getAllItems: Returning ${response.anime.length} anime`);
|
||||
if (response.anime.length > 0) {
|
||||
logger.log('[SimklService] getAllItems: First anime sample:', JSON.stringify(response.anime[0], null, 2));
|
||||
}
|
||||
return response.anime || [];
|
||||
}
|
||||
|
||||
// If no type specified, return all
|
||||
if (!type) {
|
||||
if (response.movies) items.push(...response.movies);
|
||||
if (response.shows) items.push(...response.shows);
|
||||
if (response.anime) items.push(...response.anime);
|
||||
logger.log(`[SimklService] getAllItems: Returning ${items.length} total items`);
|
||||
return items;
|
||||
}
|
||||
|
||||
logger.log('[SimklService] getAllItems: No matching type found, returning empty array');
|
||||
return [];
|
||||
} catch (error) {
|
||||
logger.error('[SimklService] Failed to get all items:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SYNC: Get user ratings
|
||||
*/
|
||||
public async getRatings(
|
||||
type?: 'movies' | 'shows' | 'anime',
|
||||
ratingFilter?: string
|
||||
): Promise<SimklRatingItem[]> {
|
||||
let url = '/sync/ratings/';
|
||||
|
||||
if (type) {
|
||||
url += type;
|
||||
if (ratingFilter) {
|
||||
url += `/${ratingFilter}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.apiRequest<any>(url);
|
||||
if (!response) return [];
|
||||
|
||||
const items: SimklRatingItem[] = [];
|
||||
|
||||
// Aggregate ratings from all types
|
||||
if (response.movies) items.push(...response.movies);
|
||||
if (response.shows) items.push(...response.shows);
|
||||
if (response.anime) items.push(...response.anime);
|
||||
|
||||
return items;
|
||||
} catch (error) {
|
||||
logger.error('[SimklService] Failed to get ratings:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to a specific status list
|
||||
*/
|
||||
public async addToList(
|
||||
imdbId: string,
|
||||
type: 'movie' | 'show' | 'anime',
|
||||
status: SimklStatus
|
||||
): Promise<boolean> {
|
||||
const contentType = type === 'movie' ? 'movies' : type === 'show' ? 'shows' : 'anime';
|
||||
|
||||
const payload = {
|
||||
[contentType]: [{
|
||||
to: status,
|
||||
ids: { imdb: imdbId }
|
||||
}]
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.apiRequest('/sync/add-to-list', 'POST', payload);
|
||||
return !!response;
|
||||
} catch (error) {
|
||||
logger.error('[SimklService] Failed to add to list:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from list (marks as not interested)
|
||||
*/
|
||||
public async removeFromList(
|
||||
imdbId: string,
|
||||
type: 'movie' | 'show' | 'anime'
|
||||
): Promise<boolean> {
|
||||
const contentType = type === 'movie' ? 'movies' : type === 'show' ? 'shows' : 'anime';
|
||||
|
||||
const payload = {
|
||||
[contentType]: [{
|
||||
ids: { imdb: imdbId }
|
||||
}]
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.apiRequest('/sync/remove-from-list', 'POST', payload);
|
||||
return !!response;
|
||||
} catch (error) {
|
||||
logger.error('[SimklService] Failed to remove from list:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rating for item
|
||||
*/
|
||||
public async addRating(
|
||||
imdbId: string,
|
||||
type: 'movie' | 'show' | 'anime',
|
||||
rating: number
|
||||
): Promise<boolean> {
|
||||
const contentType = type === 'movie' ? 'movies' : type === 'show' ? 'shows' : 'anime';
|
||||
|
||||
const payload = {
|
||||
[contentType]: [{
|
||||
ids: { imdb: imdbId },
|
||||
rating: Math.max(1, Math.min(10, rating))
|
||||
}]
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.apiRequest('/sync/ratings', 'POST', payload);
|
||||
return !!response;
|
||||
} catch (error) {
|
||||
logger.error('[SimklService] Failed to add rating:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get poster URL - returns empty string to let app's existing poster infrastructure handle it
|
||||
* The app will use IMDB ID or TMDB ID to fetch posters through existing metadata services
|
||||
*/
|
||||
public static getPosterUrl(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue