mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
Merge 64183894de into cbc9fc4fa6
This commit is contained in:
commit
bc68780c65
6 changed files with 280 additions and 7 deletions
138
src/hooks/useTraktRecommendations.ts
Normal file
138
src/hooks/useTraktRecommendations.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { traktService } from '../services/traktService';
|
||||||
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import type { CatalogContent, StreamingContent } from '../services/catalog/types';
|
||||||
|
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||||
|
|
||||||
|
const TRAKT_RECOMMENDED_MOVIES_KEY = 'trakt_recommended_movies_enabled';
|
||||||
|
const TRAKT_RECOMMENDED_SHOWS_KEY = 'trakt_recommended_shows_enabled';
|
||||||
|
|
||||||
|
const FALLBACK_POSTER = 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image';
|
||||||
|
|
||||||
|
/** Convert a Trakt extended item (movie or show) into a StreamingContent row. */
|
||||||
|
function traktItemToStreamingContent(
|
||||||
|
item: any,
|
||||||
|
mediaType: 'movie' | 'series'
|
||||||
|
): StreamingContent | null {
|
||||||
|
const media = mediaType === 'movie' ? item : item; // same shape, different fields
|
||||||
|
const title: string = media.title;
|
||||||
|
const imdbId: string | undefined = media.ids?.imdb;
|
||||||
|
const tmdbId: number | undefined = media.ids?.tmdb;
|
||||||
|
|
||||||
|
if (!title) return null;
|
||||||
|
|
||||||
|
// Use IMDb ID as content ID (same convention as Stremio addons)
|
||||||
|
const id = imdbId ?? (tmdbId ? `tmdb:${tmdbId}` : `trakt:${media.ids?.trakt}`);
|
||||||
|
|
||||||
|
// Metahub is already used by the app for Stremio content — use it for posters too
|
||||||
|
const poster = imdbId
|
||||||
|
? `https://images.metahub.space/poster/medium/${imdbId}/img`
|
||||||
|
: FALLBACK_POSTER;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: mediaType,
|
||||||
|
name: title,
|
||||||
|
poster,
|
||||||
|
posterShape: 'poster',
|
||||||
|
year: media.year ?? undefined,
|
||||||
|
description: media.overview ?? undefined,
|
||||||
|
genres: Array.isArray(media.genres)
|
||||||
|
? media.genres.map((g: string) => g.charAt(0).toUpperCase() + g.slice(1))
|
||||||
|
: undefined,
|
||||||
|
runtime: media.runtime ? `${media.runtime} min` : undefined,
|
||||||
|
certification: media.certification ?? undefined,
|
||||||
|
imdb_id: imdbId,
|
||||||
|
} as StreamingContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches personalized Trakt recommendations for the authenticated user
|
||||||
|
* and returns them as two CatalogContent rows (movies + shows).
|
||||||
|
* Returns an empty array when not authenticated.
|
||||||
|
*/
|
||||||
|
export function useTraktRecommendations(): {
|
||||||
|
catalogs: CatalogContent[];
|
||||||
|
loading: boolean;
|
||||||
|
} {
|
||||||
|
const [catalogs, setCatalogs] = useState<CatalogContent[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { lastUpdate } = useCatalogContext(); // re-run when refreshCatalogs() is called
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
const isAuth = await traktService.isAuthenticated();
|
||||||
|
if (!isAuth || cancelled) return;
|
||||||
|
|
||||||
|
// Read user toggles (default enabled)
|
||||||
|
const [moviesEnabledVal, showsEnabledVal] = await Promise.all([
|
||||||
|
mmkvStorage.getItem(TRAKT_RECOMMENDED_MOVIES_KEY),
|
||||||
|
mmkvStorage.getItem(TRAKT_RECOMMENDED_SHOWS_KEY),
|
||||||
|
]);
|
||||||
|
const moviesEnabled = moviesEnabledVal !== 'false';
|
||||||
|
const showsEnabled = showsEnabledVal !== 'false';
|
||||||
|
|
||||||
|
if (!moviesEnabled && !showsEnabled) {
|
||||||
|
setCatalogs([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [movies, shows] = await Promise.all([
|
||||||
|
moviesEnabled ? traktService.getRecommendations('movies', 20) : Promise.resolve([]),
|
||||||
|
showsEnabled ? traktService.getRecommendations('shows', 20) : Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
const result: CatalogContent[] = [];
|
||||||
|
|
||||||
|
if (moviesEnabled) {
|
||||||
|
const movieItems = movies
|
||||||
|
.map((m: any) => traktItemToStreamingContent(m, 'movie'))
|
||||||
|
.filter(Boolean) as StreamingContent[];
|
||||||
|
if (movieItems.length > 0) {
|
||||||
|
result.push({
|
||||||
|
addon: 'trakt',
|
||||||
|
type: 'movie',
|
||||||
|
id: 'trakt-recommended-movies',
|
||||||
|
name: 'Recommended Movies',
|
||||||
|
items: movieItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showsEnabled) {
|
||||||
|
const showItems = shows
|
||||||
|
.map((s: any) => traktItemToStreamingContent(s, 'series'))
|
||||||
|
.filter(Boolean) as StreamingContent[];
|
||||||
|
if (showItems.length > 0) {
|
||||||
|
result.push({
|
||||||
|
addon: 'trakt',
|
||||||
|
type: 'series',
|
||||||
|
id: 'trakt-recommended-shows',
|
||||||
|
name: 'Recommended Shows',
|
||||||
|
items: showItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCatalogs(result);
|
||||||
|
logger.log(`[useTraktRecommendations] movies=${moviesEnabled} shows=${showsEnabled}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[useTraktRecommendations] error:', err);
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [lastUpdate]); // re-runs whenever refreshCatalogs() is called from settings
|
||||||
|
|
||||||
|
return { catalogs, loading };
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import { RouteProp } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { Meta, stremioService, CatalogExtra } from '../services/stremioService';
|
import { Meta, stremioService, CatalogExtra } from '../services/stremioService';
|
||||||
|
import { traktService } from '../services/traktService';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
|
|
@ -500,10 +501,12 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
setItems(prev => [...prev, ...nextBatch]);
|
setItems(prev => [...prev, ...nextBatch]);
|
||||||
displayedCountRef.current += nextBatch.length;
|
displayedCountRef.current += nextBatch.length;
|
||||||
|
|
||||||
// Check if we still have more in buffer OR if we should try fetching more from network
|
// Check if we still have more in buffer OR if we should try fetching more from network.
|
||||||
// If buffer is exhausted, we might need to fetch next page from server
|
// Trakt catalogs are fully buffered on first load — no server-side next page exists,
|
||||||
|
// so once the buffer is drained we must not trigger another fetch.
|
||||||
const hasMoreInBuffer = displayedCountRef.current < allFetchedItemsRef.current.length;
|
const hasMoreInBuffer = displayedCountRef.current < allFetchedItemsRef.current.length;
|
||||||
setHasMore(hasMoreInBuffer || (addonId ? true : false)); // Simplified: if addon, assume potential server side more
|
const hasServerSideMore = addonId && addonId !== 'trakt';
|
||||||
|
setHasMore(hasMoreInBuffer || (hasServerSideMore ? true : false));
|
||||||
setIsFetchingMore(false);
|
setIsFetchingMore(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
@ -598,6 +601,44 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Trakt recommendation catalogs (not a Stremio addon).
|
||||||
|
// Trakt caps recommendations at 100 items. We fetch all at once and
|
||||||
|
// paginate client-side using the existing allFetchedItemsRef buffer.
|
||||||
|
if (addonId === 'trakt') {
|
||||||
|
const traktType = type === 'movie' ? 'movies' : 'shows';
|
||||||
|
// Fetch the maximum Trakt allows so the buffer is fully populated.
|
||||||
|
const raw = await traktService.getRecommendations(traktType, 100);
|
||||||
|
const metas: Meta[] = raw
|
||||||
|
.filter((item: any) => item?.title && item?.ids?.imdb)
|
||||||
|
.map((item: any): Meta => ({
|
||||||
|
id: item.ids.imdb,
|
||||||
|
type,
|
||||||
|
name: item.title,
|
||||||
|
poster: `https://images.metahub.space/poster/medium/${item.ids.imdb}/img`,
|
||||||
|
year: item.year,
|
||||||
|
description: item.overview,
|
||||||
|
genres: item.genres?.map((g: string) => g.charAt(0).toUpperCase() + g.slice(1)),
|
||||||
|
runtime: item.runtime ? `${item.runtime} min` : undefined,
|
||||||
|
certification: item.certification,
|
||||||
|
imdb_id: item.ids.imdb,
|
||||||
|
}));
|
||||||
|
|
||||||
|
InteractionManager.runAfterInteractions(() => {
|
||||||
|
allFetchedItemsRef.current = metas;
|
||||||
|
displayedCountRef.current = 0;
|
||||||
|
const firstBatch = metas.slice(0, CLIENT_PAGE_SIZE);
|
||||||
|
setItems(firstBatch);
|
||||||
|
displayedCountRef.current = firstBatch.length;
|
||||||
|
// Only enable load-more if the buffer has items beyond the first page.
|
||||||
|
// Never set true unconditionally — there is no server-side next page
|
||||||
|
// for Trakt recommendations, so once the buffer is drained we stop.
|
||||||
|
setHasMore(metas.length > CLIENT_PAGE_SIZE);
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// addon logic
|
// addon logic
|
||||||
let foundItems = false;
|
let foundItems = false;
|
||||||
let allItems: Meta[] = [];
|
let allItems: Meta[] = [];
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { traktService } from '../services/traktService';
|
||||||
import { stremioService } from '../services/stremioService';
|
import { stremioService } from '../services/stremioService';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||||
|
|
@ -68,6 +69,8 @@ interface GroupedCatalogs {
|
||||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||||
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
||||||
const CATALOG_MOBILE_COLUMNS_KEY = 'catalog_mobile_columns';
|
const CATALOG_MOBILE_COLUMNS_KEY = 'catalog_mobile_columns';
|
||||||
|
export const TRAKT_RECOMMENDED_MOVIES_KEY = 'trakt_recommended_movies_enabled';
|
||||||
|
export const TRAKT_RECOMMENDED_SHOWS_KEY = 'trakt_recommended_shows_enabled';
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
// Create a styles creator function that accepts the theme colors
|
// Create a styles creator function that accepts the theme colors
|
||||||
|
|
@ -270,6 +273,9 @@ const CatalogSettingsScreen = () => {
|
||||||
const [groupedSettings, setGroupedSettings] = useState<GroupedCatalogs>({});
|
const [groupedSettings, setGroupedSettings] = useState<GroupedCatalogs>({});
|
||||||
const [mobileColumns, setMobileColumns] = useState<'auto' | 2 | 3>('auto');
|
const [mobileColumns, setMobileColumns] = useState<'auto' | 2 | 3>('auto');
|
||||||
const [showTitles, setShowTitles] = useState(true); // Default to showing titles
|
const [showTitles, setShowTitles] = useState(true); // Default to showing titles
|
||||||
|
const [isTraktAuthenticated, setIsTraktAuthenticated] = useState(false);
|
||||||
|
const [traktMoviesEnabled, setTraktMoviesEnabled] = useState(true);
|
||||||
|
const [traktShowsEnabled, setTraktShowsEnabled] = useState(true);
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { refreshCatalogs } = useCatalogContext();
|
const { refreshCatalogs } = useCatalogContext();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
@ -385,6 +391,20 @@ const CatalogSettingsScreen = () => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load Trakt auth state and recommendation toggles
|
||||||
|
try {
|
||||||
|
const auth = await traktService.isAuthenticated();
|
||||||
|
setIsTraktAuthenticated(auth);
|
||||||
|
if (auth) {
|
||||||
|
const moviesVal = await mmkvStorage.getItem(TRAKT_RECOMMENDED_MOVIES_KEY);
|
||||||
|
const showsVal = await mmkvStorage.getItem(TRAKT_RECOMMENDED_SHOWS_KEY);
|
||||||
|
setTraktMoviesEnabled(moviesVal !== 'false');
|
||||||
|
setTraktShowsEnabled(showsVal !== 'false');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load catalog settings:', error);
|
logger.error('Failed to load catalog settings:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -618,6 +638,52 @@ const CatalogSettingsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Trakt Recommendations — only when logged in */}
|
||||||
|
{isTraktAuthenticated && (
|
||||||
|
<View style={styles.addonSection}>
|
||||||
|
<Text style={styles.addonTitle}>TRAKT</Text>
|
||||||
|
<View style={styles.card}>
|
||||||
|
<View style={styles.groupHeader}>
|
||||||
|
<Text style={styles.groupTitle}>Recommendations</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.catalogItem}>
|
||||||
|
<View style={styles.catalogInfo}>
|
||||||
|
<Text style={styles.catalogName}>Recommended Movies</Text>
|
||||||
|
<Text style={styles.catalogType}>Movie</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={traktMoviesEnabled}
|
||||||
|
onValueChange={async (value) => {
|
||||||
|
setTraktMoviesEnabled(value);
|
||||||
|
await mmkvStorage.setItem(TRAKT_RECOMMENDED_MOVIES_KEY, value ? 'true' : 'false');
|
||||||
|
refreshCatalogs();
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#505050', true: colors.primary }}
|
||||||
|
thumbColor={Platform.OS === 'android' ? colors.white : undefined}
|
||||||
|
ios_backgroundColor="#505050"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.catalogItem, { borderBottomWidth: 0 }]}>
|
||||||
|
<View style={styles.catalogInfo}>
|
||||||
|
<Text style={styles.catalogName}>Recommended Shows</Text>
|
||||||
|
<Text style={styles.catalogType}>Series</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={traktShowsEnabled}
|
||||||
|
onValueChange={async (value) => {
|
||||||
|
setTraktShowsEnabled(value);
|
||||||
|
await mmkvStorage.setItem(TRAKT_RECOMMENDED_SHOWS_KEY, value ? 'true' : 'false');
|
||||||
|
refreshCatalogs();
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#505050', true: colors.primary }}
|
||||||
|
thumbColor={Platform.OS === 'android' ? colors.white : undefined}
|
||||||
|
ios_backgroundColor="#505050"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.entries(groupedSettings).map(([addonId, group]) => (
|
{Object.entries(groupedSettings).map(([addonId, group]) => (
|
||||||
<View key={addonId} style={styles.addonSection}>
|
<View key={addonId} style={styles.addonSection}>
|
||||||
<Text style={styles.addonTitle}>
|
<Text style={styles.addonTitle}>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import {
|
||||||
clearCustomNameCache
|
clearCustomNameCache
|
||||||
} from '../utils/catalogNameUtils';
|
} from '../utils/catalogNameUtils';
|
||||||
import { useHomeCatalogs } from '../hooks/useHomeCatalogs';
|
import { useHomeCatalogs } from '../hooks/useHomeCatalogs';
|
||||||
|
import { useTraktRecommendations } from '../hooks/useTraktRecommendations';
|
||||||
import { useFeaturedContent } from '../hooks/useFeaturedContent';
|
import { useFeaturedContent } from '../hooks/useFeaturedContent';
|
||||||
import { useSettings, settingsEmitter } from '../hooks/useSettings';
|
import { useSettings, settingsEmitter } from '../hooks/useSettings';
|
||||||
import FeaturedContent from '../components/home/FeaturedContent';
|
import FeaturedContent from '../components/home/FeaturedContent';
|
||||||
|
|
@ -153,6 +154,7 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
const [catalogs, setCatalogs] = useState<(CatalogContent | null)[]>([]);
|
const [catalogs, setCatalogs] = useState<(CatalogContent | null)[]>([]);
|
||||||
const [catalogsLoading, setCatalogsLoading] = useState(true);
|
const [catalogsLoading, setCatalogsLoading] = useState(true);
|
||||||
|
const { catalogs: traktRecommendedCatalogs } = useTraktRecommendations();
|
||||||
const [loadedCatalogCount, setLoadedCatalogCount] = useState(0);
|
const [loadedCatalogCount, setLoadedCatalogCount] = useState(0);
|
||||||
const [pendingCatalogIndexes, setPendingCatalogIndexes] = useState<Record<number, boolean>>({});
|
const [pendingCatalogIndexes, setPendingCatalogIndexes] = useState<Record<number, boolean>>({});
|
||||||
const [hasAddons, setHasAddons] = useState<boolean | null>(null);
|
const [hasAddons, setHasAddons] = useState<boolean | null>(null);
|
||||||
|
|
@ -755,6 +757,11 @@ const HomeScreen = () => {
|
||||||
data.push({ type: 'thisWeek', key: 'thisWeek' });
|
data.push({ type: 'thisWeek', key: 'thisWeek' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject Trakt recommended catalogs at the top when authenticated
|
||||||
|
traktRecommendedCatalogs.forEach((catalog, index) => {
|
||||||
|
data.push({ type: 'catalog', catalog, key: `trakt-recommended-${index}` });
|
||||||
|
});
|
||||||
|
|
||||||
// Only show a limited number of catalogs initially for performance
|
// Only show a limited number of catalogs initially for performance
|
||||||
const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
|
const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
|
||||||
|
|
||||||
|
|
@ -773,7 +780,7 @@ const HomeScreen = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}, [hasAddons, catalogs, catalogsLoading, pendingCatalogIndexes, visibleCatalogCount, settings.showThisWeekSection]);
|
}, [hasAddons, catalogs, traktRecommendedCatalogs, catalogsLoading, pendingCatalogIndexes, visibleCatalogCount, settings.showThisWeekSection]);
|
||||||
|
|
||||||
const handleLoadMoreCatalogs = useCallback(() => {
|
const handleLoadMoreCatalogs = useCallback(() => {
|
||||||
setVisibleCatalogCount(prev => Math.min(prev + 3, catalogs.length));
|
setVisibleCatalogCount(prev => Math.min(prev + 3, catalogs.length));
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
if (response.type === 'success' && request?.codeVerifier) {
|
if (response.type === 'success' && request?.codeVerifier) {
|
||||||
const { code } = response.params;
|
const { code } = response.params;
|
||||||
logger.log('[TraktSettingsScreen] Auth code received:', code);
|
logger.log('[TraktSettingsScreen] Auth code received:', code);
|
||||||
traktService.exchangeCodeForToken(code, request.codeVerifier)
|
traktService.exchangeCodeForToken(code, request.codeVerifier, redirectUri)
|
||||||
.then(success => {
|
.then(success => {
|
||||||
if (success) {
|
if (success) {
|
||||||
logger.log('[TraktSettingsScreen] Token exchange successful');
|
logger.log('[TraktSettingsScreen] Token exchange successful');
|
||||||
|
|
|
||||||
|
|
@ -800,7 +800,7 @@ export class TraktService {
|
||||||
/**
|
/**
|
||||||
* Exchange the authorization code for an access token
|
* Exchange the authorization code for an access token
|
||||||
*/
|
*/
|
||||||
public async exchangeCodeForToken(code: string, codeVerifier: string): Promise<boolean> {
|
public async exchangeCodeForToken(code: string, codeVerifier: string, redirectUri?: string): Promise<boolean> {
|
||||||
// Block authentication during maintenance
|
// Block authentication during maintenance
|
||||||
if (this.isMaintenanceMode()) {
|
if (this.isMaintenanceMode()) {
|
||||||
logger.warn('[TraktService] Maintenance mode: blocking new authentication');
|
logger.warn('[TraktService] Maintenance mode: blocking new authentication');
|
||||||
|
|
@ -819,7 +819,7 @@ export class TraktService {
|
||||||
code,
|
code,
|
||||||
client_id: TRAKT_CLIENT_ID,
|
client_id: TRAKT_CLIENT_ID,
|
||||||
client_secret: TRAKT_CLIENT_SECRET,
|
client_secret: TRAKT_CLIENT_SECRET,
|
||||||
redirect_uri: TRAKT_REDIRECT_URI,
|
redirect_uri: redirectUri ?? TRAKT_REDIRECT_URI,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code_verifier: codeVerifier
|
code_verifier: codeVerifier
|
||||||
})
|
})
|
||||||
|
|
@ -1350,6 +1350,27 @@ export class TraktService {
|
||||||
return this.apiRequest<TraktRatingItem[]>(endpoint);
|
return this.apiRequest<TraktRatingItem[]>(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get personalized movie/show recommendations for the authenticated user.
|
||||||
|
* Returns extended metadata including IMDb/TMDB IDs, overview, genres, etc.
|
||||||
|
*/
|
||||||
|
public async getRecommendations(
|
||||||
|
type: 'movies' | 'shows',
|
||||||
|
limit: number = 20
|
||||||
|
): Promise<any[]> {
|
||||||
|
const isAuth = await this.isAuthenticated();
|
||||||
|
if (!isAuth) return [];
|
||||||
|
try {
|
||||||
|
const data = await this.apiRequest<any[]>(
|
||||||
|
`/recommendations/${type}?extended=full&limit=${limit}`
|
||||||
|
);
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`[TraktService] getRecommendations(${type}) error:`, err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user's watched movies with images
|
* Get the user's watched movies with images
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue