Some checks failed
CI / Lint & Type Check (push) Failing after 43s
CI / Tests (push) Successful in 1m0s
CI / Build Web (push) Has been skipped
CI / Security Scan (push) Successful in 46s
CI / Build Electron (Linux) (push) Has been skipped
CI / Build Tauri (ubuntu-latest) (push) Has been skipped
CI / Build Electron (Windows) (push) Has been cancelled
CI / Build Tauri (windows-latest) (push) Has been cancelled
- Local Profiles: Profile selector, manager, avatar system, profile-aware stores - Smart Features: Continue Watching, personalized recommendations, auto-quality, smart downloads - Google Cast: Cast service with web SDK and Capacitor Android plugin interface - Settings: New toggles for auto-quality and smart downloads Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
174 lines
5.0 KiB
TypeScript
174 lines
5.0 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import {
|
|
getPersonalizedRecommendations,
|
|
getBecauseYouWatched,
|
|
} from '../services/recommendations/recommendationService';
|
|
import { useHistoryStore } from '../stores/historyStore';
|
|
import type { Movie } from '../types';
|
|
|
|
interface UseRecommendationsResult {
|
|
movies: Movie[];
|
|
basedOnGenres: string[];
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
// Hook for personalized recommendations based on watch history
|
|
export function usePersonalizedRecommendations(limit = 20): UseRecommendationsResult {
|
|
const [movies, setMovies] = useState<Movie[]>([]);
|
|
const [basedOnGenres, setBasedOnGenres] = useState<string[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const { getProfileItems } = useHistoryStore();
|
|
const historyItems = getProfileItems();
|
|
|
|
// Memoize history data to prevent unnecessary re-fetches
|
|
const historyHash = useMemo(
|
|
() => historyItems.map((item) => item.movieId).join(','),
|
|
[historyItems]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (historyItems.length === 0) {
|
|
setMovies([]);
|
|
setBasedOnGenres([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const fetchRecommendations = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const historyMovies = historyItems.map((item) => item.movie);
|
|
const excludeIds = new Set(historyItems.map((item) => item.movieId));
|
|
|
|
const result = await getPersonalizedRecommendations(
|
|
historyMovies,
|
|
excludeIds,
|
|
limit
|
|
);
|
|
|
|
setMovies(result.movies);
|
|
setBasedOnGenres(result.basedOnGenres);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to get recommendations');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchRecommendations();
|
|
}, [historyHash, limit]);
|
|
|
|
return { movies, basedOnGenres, isLoading, error };
|
|
}
|
|
|
|
interface BecauseYouWatchedResult {
|
|
sourceMovie: Movie | null;
|
|
recommendations: Movie[];
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
// Hook for "Because You Watched [Movie]" recommendations
|
|
export function useBecauseYouWatched(movieId: number | undefined): BecauseYouWatchedResult {
|
|
const [sourceMovie, setSourceMovie] = useState<Movie | null>(null);
|
|
const [recommendations, setRecommendations] = useState<Movie[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const { getProfileItems } = useHistoryStore();
|
|
const historyItems = getProfileItems();
|
|
|
|
useEffect(() => {
|
|
if (!movieId) {
|
|
setSourceMovie(null);
|
|
setRecommendations([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const historyItem = historyItems.find((item) => item.movieId === movieId);
|
|
if (!historyItem) {
|
|
setSourceMovie(null);
|
|
setRecommendations([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const fetchRecommendations = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const excludeIds = new Set(historyItems.map((item) => item.movieId));
|
|
const recs = await getBecauseYouWatched(historyItem.movie, excludeIds, 10);
|
|
|
|
setSourceMovie(historyItem.movie);
|
|
setRecommendations(recs);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to get recommendations');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchRecommendations();
|
|
}, [movieId, historyItems]);
|
|
|
|
return { sourceMovie, recommendations, isLoading, error };
|
|
}
|
|
|
|
// Hook for multiple "Because You Watched" rows
|
|
export function useMultipleBecauseYouWatched(maxRows = 3): {
|
|
rows: Array<{ sourceMovie: Movie; recommendations: Movie[] }>;
|
|
isLoading: boolean;
|
|
} {
|
|
const [rows, setRows] = useState<Array<{ sourceMovie: Movie; recommendations: Movie[] }>>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
const { getProfileItems } = useHistoryStore();
|
|
const historyItems = getProfileItems();
|
|
|
|
useEffect(() => {
|
|
if (historyItems.length === 0) {
|
|
setRows([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const fetchAllRecommendations = async () => {
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const excludeIds = new Set(historyItems.map((item) => item.movieId));
|
|
const recentMovies = historyItems.slice(0, maxRows);
|
|
|
|
const rowPromises = recentMovies.map(async (item) => {
|
|
const recs = await getBecauseYouWatched(item.movie, excludeIds, 10);
|
|
return {
|
|
sourceMovie: item.movie,
|
|
recommendations: recs,
|
|
};
|
|
});
|
|
|
|
const results = await Promise.all(rowPromises);
|
|
// Filter out rows with no recommendations
|
|
setRows(results.filter((row) => row.recommendations.length > 0));
|
|
} catch (err) {
|
|
console.error('Error fetching recommendations:', err);
|
|
setRows([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchAllRecommendations();
|
|
}, [historyItems.length, maxRows]);
|
|
|
|
return { rows, isLoading };
|
|
}
|