Use Zustand store; modularize API & routes
Introduce a centralized Zustand store and refactor app state out of App.tsx into src/store/appStore.ts. Modularize API surface by moving media/cast/settings/converters/types into src/lib/api/* and re-exporting from src/api.ts for backward compatibility. Replace inline route helpers with dedicated route components (MediaDetailRoute, CastDetailRoute, CategoryBrowseRoute) and wire CATEGORY_PATHS/PATH_TO_CATEGORY constants. Update AddMediaView UI (icons, layout) and adjust settings/category handling to use DEFAULT_SETTINGS and the store. Add zustand to package.json/package-lock.json and include a new React SKILL.md. Overall changes improve state management, API organization, and route/component separation for better maintainability and code-splitting.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import { Staff, Media } from '../../types';
|
||||
import { ApiResponse, PaginatedResponse, ApiCastItem, CreateCastInput, UpdateCastInput } from './types';
|
||||
import { convertApiCastToStaff, convertApiToMedia } from './converters';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
export async function fetchAllCast(page: number = 1, limit: number = 100000): Promise<Staff[]> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/cast?page=${page}&limit=${limit}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<PaginatedResponse<ApiCastItem>> = await response.json();
|
||||
|
||||
if (data.success && data.data.items) {
|
||||
return data.data.items.map(convertApiCastToStaff);
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching cast from API:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCastById(id: number | string): Promise<ApiCastItem | null> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/cast/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiCastItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching cast by ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCastMedia(castId: number | string): Promise<Media[]> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/cast/${castId}/media`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<PaginatedResponse<any>> = await response.json();
|
||||
|
||||
if (data.success && data.data.items) {
|
||||
return data.data.items.map(convertApiToMedia);
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching cast media:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCast(cast: CreateCastInput): Promise<ApiCastItem | null> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/cast`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(cast),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiCastItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error creating cast:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCast(id: number | string, cast: UpdateCastInput): Promise<ApiCastItem | null> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/cast/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(cast),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiCastItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error updating cast:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCast(id: number | string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/cast/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<{ message: string }> = await response.json();
|
||||
return data.success;
|
||||
} catch (error) {
|
||||
console.error('Error deleting cast:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy functions for compatibility
|
||||
export async function fetchAllActors(): Promise<Array<{id: number, name: string, photo: string | null}>> {
|
||||
try {
|
||||
const media = await (await import('./mediaApi')).fetchAllMedia(1, 1000);
|
||||
const actorMap = new Map<number, {id: number, name: string, photo: string | null}>();
|
||||
|
||||
media.forEach(item => {
|
||||
item.staff?.forEach(staffMember => {
|
||||
const id = parseInt(staffMember.id);
|
||||
if (!actorMap.has(id)) {
|
||||
actorMap.set(id, {
|
||||
id: id,
|
||||
name: staffMember.name,
|
||||
photo: staffMember.photo
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(actorMap.values());
|
||||
} catch (error) {
|
||||
console.error('Error fetching all actors:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMediaByActor(actorName: string): Promise<Media[]> {
|
||||
try {
|
||||
const media = await (await import('./mediaApi')).fetchAllMedia(1, 1000);
|
||||
return media.filter(item =>
|
||||
item.staff?.some(staffMember =>
|
||||
staffMember.name.toLowerCase().includes(actorName.toLowerCase())
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching media by actor:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { Media, Staff, UserSettings, MediaCategory } from '../../types';
|
||||
import { ApiMediaItem, ApiStaff, ApiCastItem, ApiSettingsItem, CreateSettingsInput } from './types';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
function normalizeUrl(url: string | null): string {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
const cleanPath = url.startsWith('/') ? url.slice(1) : url;
|
||||
return `${BASE_URL}/${cleanPath}`;
|
||||
}
|
||||
|
||||
export function convertApiCastToStaff(apiItem: ApiCastItem): Staff {
|
||||
return {
|
||||
id: apiItem.id.toString(),
|
||||
name: apiItem.name,
|
||||
cleanname: apiItem.cleanname,
|
||||
role: apiItem.occupations?.[0] || 'Actor',
|
||||
photo: normalizeUrl(apiItem.photo) || `https://picsum.photos/seed/cast-${apiItem.id}/200/200`,
|
||||
bio: apiItem.bio || undefined,
|
||||
birthDate: apiItem.birthDate || undefined,
|
||||
birthPlace: apiItem.birthPlace || undefined,
|
||||
occupations: apiItem.occupations || ['Actor'],
|
||||
createdAt: apiItem.createdAt,
|
||||
updatedAt: apiItem.updatedAt,
|
||||
bust_size: apiItem.bust_size,
|
||||
cup_size: apiItem.cup_size,
|
||||
waist_size: apiItem.waist_size,
|
||||
hip_size: apiItem.hip_size,
|
||||
height: apiItem.height,
|
||||
weight: apiItem.weight,
|
||||
hair_color: apiItem.hair_color,
|
||||
eye_color: apiItem.eye_color,
|
||||
ethnicity: apiItem.ethnicity,
|
||||
filmography: apiItem.filmography?.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
year: item.year,
|
||||
poster: normalizeUrl(item.poster) || `https://picsum.photos/seed/${item.id}/400/600`,
|
||||
category: item.category,
|
||||
type: item.type,
|
||||
role: item.role,
|
||||
characterName: item.characterName
|
||||
})),
|
||||
media_types: apiItem.media_types,
|
||||
adult_specifics: apiItem.adult_specifics
|
||||
};
|
||||
}
|
||||
|
||||
export function convertApiToMedia(apiItem: ApiMediaItem): Media {
|
||||
const staff: Staff[] = (apiItem.staff || []).map((staffMember) => ({
|
||||
id: staffMember.id.toString(),
|
||||
name: staffMember.name,
|
||||
role: staffMember.role,
|
||||
photo: normalizeUrl(staffMember.photo) || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
||||
characterName: staffMember.characterName || staffMember.name,
|
||||
characterImage: normalizeUrl(staffMember.characterImage) || normalizeUrl(staffMember.photo) || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
||||
}));
|
||||
|
||||
let aspectRatio: '2/3' | '16/9' | '1/1' = '2/3';
|
||||
if (apiItem.aspectRatio) {
|
||||
const ratio = apiItem.aspectRatio.toLowerCase();
|
||||
if (ratio.includes('16:9') || ratio.includes('16/9') || ratio.includes('1.78') || ratio.includes('2.39')) {
|
||||
aspectRatio = '16/9';
|
||||
} else if (ratio.includes('1:1') || ratio.includes('1/1') || ratio.includes('1.00')) {
|
||||
aspectRatio = '1/1';
|
||||
} else if (ratio.includes('2/3')) {
|
||||
aspectRatio = '2/3';
|
||||
}
|
||||
}
|
||||
|
||||
let mediaType: 'TV' | 'Movie' | 'OVA' | 'ONA' | 'Album' | 'Single' | 'Hardcover' | 'E-book' | 'Console' | 'Game' = 'Movie';
|
||||
const apiType = apiItem.type?.toLowerCase();
|
||||
if (apiType === 'tv' || apiType === 'episode') {
|
||||
mediaType = 'TV';
|
||||
} else if (apiType === 'album' || apiType === 'single') {
|
||||
mediaType = apiType === 'album' ? 'Album' : 'Single';
|
||||
} else if (apiType === 'game' || apiType === 'console') {
|
||||
mediaType = apiType === 'game' ? 'Game' : 'Console';
|
||||
} else if (apiType === 'ova') {
|
||||
mediaType = 'OVA';
|
||||
} else if (apiType === 'ona') {
|
||||
mediaType = 'ONA';
|
||||
} else if (apiType === 'hardcover' || apiType === 'e-book') {
|
||||
mediaType = apiType === 'hardcover' ? 'Hardcover' : 'E-book';
|
||||
}
|
||||
|
||||
let mediaCategory: 'Anime' | 'Movies' | 'TV Series' | 'Music' | 'Books' | 'Adult' | 'Consoles' | 'Games' = 'Movies';
|
||||
const apiCategory = apiItem.category?.toLowerCase();
|
||||
|
||||
if (apiCategory === 'anime') {
|
||||
mediaCategory = 'Anime';
|
||||
} else if (apiCategory === 'movie' || apiCategory === 'movies') {
|
||||
mediaCategory = 'Movies';
|
||||
} else if (apiCategory === 'tv' || apiCategory === 'series' || apiCategory === 'tv series' || apiType === 'tv' || apiType === 'episode') {
|
||||
mediaCategory = 'TV Series';
|
||||
} else if (apiCategory === 'music' || apiType === 'album' || apiType === 'single') {
|
||||
mediaCategory = 'Music';
|
||||
} else if (apiCategory === 'book' || apiCategory === 'books' || apiType === 'hardcover' || apiType === 'e-book') {
|
||||
mediaCategory = 'Books';
|
||||
} else if (apiCategory === 'adult') {
|
||||
mediaCategory = 'Adult';
|
||||
} else if (apiCategory === 'console' || apiCategory === 'consoles' || apiType === 'console') {
|
||||
mediaCategory = 'Consoles';
|
||||
} else if (apiCategory === 'game' || apiCategory === 'games' || apiType === 'game') {
|
||||
mediaCategory = 'Games';
|
||||
} else {
|
||||
console.warn('Unknown category:', apiItem.category, 'defaulting to Movies');
|
||||
mediaCategory = 'Movies';
|
||||
}
|
||||
|
||||
let mediaStatus: 'watching' | 'completed' | 'planned' | 'dropped' | 'reading' | 'listening' | 'playing' | 'on-hold' = 'completed';
|
||||
const apiStatus = apiItem.status?.toLowerCase();
|
||||
if (apiStatus === 'ongoing' || apiStatus === 'watching') {
|
||||
mediaStatus = 'watching';
|
||||
} else if (apiStatus === 'upcoming' || apiStatus === 'planned') {
|
||||
mediaStatus = 'planned';
|
||||
} else if (apiStatus === 'dropped') {
|
||||
mediaStatus = 'dropped';
|
||||
} else if (apiStatus === 'reading') {
|
||||
mediaStatus = 'reading';
|
||||
} else if (apiStatus === 'listening') {
|
||||
mediaStatus = 'listening';
|
||||
} else if (apiStatus === 'playing') {
|
||||
mediaStatus = 'playing';
|
||||
} else if (apiStatus === 'on-hold') {
|
||||
mediaStatus = 'on-hold';
|
||||
}
|
||||
|
||||
return {
|
||||
id: apiItem.id.toString(),
|
||||
title: apiItem.title,
|
||||
year: apiItem.year?.toString() || 'Unknown',
|
||||
poster: normalizeUrl(apiItem.poster) || `https://picsum.photos/seed/${apiItem.id}/400/600`,
|
||||
category: mediaCategory,
|
||||
banner: normalizeUrl(apiItem.banner) || undefined,
|
||||
description: apiItem.description || undefined,
|
||||
rating: apiItem.rating || undefined,
|
||||
genres: apiItem.genres || [],
|
||||
tags: apiItem.tags || [],
|
||||
studios: apiItem.studios,
|
||||
type: mediaType,
|
||||
source: apiItem.source || undefined,
|
||||
status: mediaStatus,
|
||||
staff: staff.length > 0 ? staff : undefined,
|
||||
aspectRatio: aspectRatio,
|
||||
categories: apiItem.categories,
|
||||
platforms: apiItem.platforms,
|
||||
developers: apiItem.developers,
|
||||
completionStatus: apiItem.completionStatus,
|
||||
playCount: apiItem.playCount,
|
||||
lastActivity: apiItem.lastActivity,
|
||||
playtime: apiItem.playtime,
|
||||
episodes: apiItem.episodes,
|
||||
tracks: apiItem.tracks
|
||||
};
|
||||
}
|
||||
|
||||
export function convertApiToSettings(apiItem: ApiSettingsItem): UserSettings {
|
||||
return {
|
||||
id: apiItem.id,
|
||||
enabledCategories: apiItem.enabled_categories as MediaCategory[],
|
||||
itemsPerPage: apiItem.items_per_page || 20,
|
||||
gridItemSize: apiItem.grid_item_size || 5,
|
||||
defaultView: (apiItem.default_view as 'grid' | 'list') || 'grid',
|
||||
showAdultContent: apiItem.show_adult_content || false,
|
||||
autoPlayTrailers: apiItem.auto_play_trailers || false,
|
||||
language: apiItem.language || 'en',
|
||||
theme: (apiItem.theme as 'light' | 'dark' | 'system') || 'system',
|
||||
jellyfinLibraryMappings: apiItem.jellyfin_library_mappings,
|
||||
createdAt: apiItem.created_at,
|
||||
updatedAt: apiItem.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export function convertSettingsToApi(settings: UserSettings): CreateSettingsInput {
|
||||
return {
|
||||
enabled_categories: settings.enabledCategories,
|
||||
items_per_page: settings.itemsPerPage,
|
||||
grid_item_size: settings.gridItemSize,
|
||||
default_view: settings.defaultView,
|
||||
show_adult_content: settings.showAdultContent,
|
||||
auto_play_trailers: settings.autoPlayTrailers,
|
||||
language: settings.language,
|
||||
theme: settings.theme,
|
||||
jellyfin_library_mappings: settings.jellyfinLibraryMappings,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Media } from '../../types';
|
||||
import { ApiResponse, PaginatedResponse, ApiMediaItem, CreateMediaInput, UpdateMediaInput } from './types';
|
||||
import { convertApiToMedia } from './converters';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
export async function fetchAllMedia(page: number = 1, limit: number = 10000): Promise<Media[]> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/media?page=${page}&limit=${limit}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<PaginatedResponse<ApiMediaItem>> = await response.json();
|
||||
|
||||
if (data.success && data.data.items) {
|
||||
return data.data.items.map(convertApiToMedia);
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching media from API:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMediaById(id: number | string): Promise<Media | null> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/media/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiMediaItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return convertApiToMedia(data.data);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching media by ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMedia(media: CreateMediaInput): Promise<Media | null> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/media`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(media),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiMediaItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return convertApiToMedia(data.data);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error creating media:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMedia(id: number | string, media: UpdateMediaInput): Promise<Media | null> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/media/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(media),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiMediaItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return convertApiToMedia(data.data);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error updating media:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMedia(id: number | string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/media/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<{ message: string }> = await response.json();
|
||||
return data.success;
|
||||
} catch (error) {
|
||||
console.error('Error deleting media:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { UserSettings } from '../../types';
|
||||
import { ApiResponse, ApiSettingsItem, CreateSettingsInput, UpdateSettingsInput } from './types';
|
||||
import { convertApiToSettings, convertSettingsToApi } from './converters';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
export async function fetchSettings(): Promise<UserSettings | null> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api/settings`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiSettingsItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return convertApiToSettings(data.data);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSettings(settings: UserSettings): Promise<UserSettings | null> {
|
||||
try {
|
||||
const apiSettings = convertSettingsToApi(settings);
|
||||
const response = await fetch(`${BASE_URL}/api/settings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(apiSettings),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Create settings error response:', errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiSettingsItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return convertApiToSettings(data.data);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error creating settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSettings(settings: UserSettings): Promise<UserSettings | null> {
|
||||
try {
|
||||
const apiSettings = convertSettingsToApi(settings);
|
||||
const response = await fetch(`${BASE_URL}/api/settings`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(apiSettings),
|
||||
});
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return createSettings(settings);
|
||||
}
|
||||
const errorText = await response.text();
|
||||
console.error('Update settings error response:', errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data: ApiResponse<ApiSettingsItem> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return convertApiToSettings(data.data);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error updating settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// API Response Types
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages?: number;
|
||||
}
|
||||
|
||||
// Media Types
|
||||
export interface ApiEpisode {
|
||||
id: number;
|
||||
media_id: number;
|
||||
season: number;
|
||||
episode_number: number;
|
||||
title: string;
|
||||
description: string;
|
||||
air_date: string;
|
||||
duration: number;
|
||||
thumbnail: string;
|
||||
}
|
||||
|
||||
export interface ApiTrack {
|
||||
id: number;
|
||||
media_id: number;
|
||||
track_number: number;
|
||||
title: string;
|
||||
duration: number | null;
|
||||
artist: string;
|
||||
}
|
||||
|
||||
export interface ApiMediaItem {
|
||||
id: number;
|
||||
title: string;
|
||||
year: number;
|
||||
poster: string | null;
|
||||
banner: string | null;
|
||||
description: string | null;
|
||||
rating: number | null;
|
||||
category: string | null;
|
||||
type: string;
|
||||
status: string;
|
||||
aspectRatio: string | null;
|
||||
runtime: number | null;
|
||||
director: string | null;
|
||||
writer: string | null;
|
||||
releaseDate: string | null;
|
||||
source?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
genres?: string[];
|
||||
tags?: string[];
|
||||
studios?: string[];
|
||||
staff?: ApiStaff[];
|
||||
categories?: string[];
|
||||
platforms?: string[];
|
||||
developers?: string[];
|
||||
completionStatus?: string;
|
||||
playCount?: number;
|
||||
lastActivity?: string | null;
|
||||
playtime?: number;
|
||||
episodes?: ApiEpisode[];
|
||||
tracks?: ApiTrack[];
|
||||
}
|
||||
|
||||
export interface ApiStaff {
|
||||
id: number;
|
||||
name: string;
|
||||
photo: string | null;
|
||||
bio: string | null;
|
||||
birthDate: string | null;
|
||||
birthPlace: string | null;
|
||||
role: string;
|
||||
characterName: string | null;
|
||||
characterImage: string | null;
|
||||
occupations?: string[];
|
||||
}
|
||||
|
||||
export interface CreateMediaInput {
|
||||
title: string;
|
||||
year: number;
|
||||
poster?: string | null;
|
||||
banner?: string | null;
|
||||
description?: string | null;
|
||||
rating?: number | null;
|
||||
category?: string | null;
|
||||
type?: string;
|
||||
status?: string;
|
||||
aspectRatio?: string | null;
|
||||
runtime?: number | null;
|
||||
director?: string | null;
|
||||
writer?: string | null;
|
||||
releaseDate?: string | null;
|
||||
source?: string | null;
|
||||
genres?: string[];
|
||||
tags?: string[];
|
||||
studios?: string[];
|
||||
staff?: CreateStaffInput[];
|
||||
}
|
||||
|
||||
export interface UpdateMediaInput extends Partial<CreateMediaInput> {}
|
||||
|
||||
export interface CreateStaffInput {
|
||||
name: string;
|
||||
photo?: string | null;
|
||||
bio?: string | null;
|
||||
birthDate?: string | null;
|
||||
birthPlace?: string | null;
|
||||
role: string;
|
||||
characterName?: string | null;
|
||||
characterImage?: string | null;
|
||||
occupations?: string[];
|
||||
}
|
||||
|
||||
// Cast Types
|
||||
export interface ApiCastItem {
|
||||
id: number;
|
||||
name: string;
|
||||
cleanname?: string;
|
||||
photo: string | null;
|
||||
bio: string | null;
|
||||
birthDate: string | null;
|
||||
birthPlace: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
occupations?: string[];
|
||||
filmography?: ApiCastMediaItem[];
|
||||
media_types?: string[];
|
||||
bust_size?: number | null;
|
||||
cup_size?: string | null;
|
||||
waist_size?: number | null;
|
||||
hip_size?: number | null;
|
||||
height?: number | null;
|
||||
weight?: number | null;
|
||||
hair_color?: string | null;
|
||||
eye_color?: string | null;
|
||||
ethnicity?: string | null;
|
||||
adult_specifics?: {
|
||||
id: number;
|
||||
cast_id: number;
|
||||
bust_size?: number | null;
|
||||
cup_size?: string | null;
|
||||
waist_size?: number | null;
|
||||
hip_size?: number | null;
|
||||
height?: number | null;
|
||||
weight?: number | null;
|
||||
hair_color?: string | null;
|
||||
eye_color?: string | null;
|
||||
ethnicity?: string | null;
|
||||
tattoos?: string | null;
|
||||
piercings?: string | null;
|
||||
measurements?: string | null;
|
||||
shoe_size?: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApiCastMediaItem {
|
||||
id: number;
|
||||
title: string;
|
||||
year: number;
|
||||
poster: string | null;
|
||||
category: string | null;
|
||||
type: string;
|
||||
role: string;
|
||||
characterName?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateCastInput {
|
||||
name: string;
|
||||
photo?: string | null;
|
||||
bio?: string | null;
|
||||
birthDate?: string | null;
|
||||
birthPlace?: string | null;
|
||||
occupations?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateCastInput extends Partial<CreateCastInput> {}
|
||||
|
||||
// Settings Types
|
||||
export interface ApiSettingsItem {
|
||||
id?: number;
|
||||
enabled_categories: string[];
|
||||
items_per_page: number;
|
||||
grid_item_size?: number;
|
||||
default_view: string;
|
||||
show_adult_content: boolean;
|
||||
auto_play_trailers: boolean;
|
||||
language: string;
|
||||
theme: string;
|
||||
jellyfin_library_mappings?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface CreateSettingsInput {
|
||||
enabled_categories: string[];
|
||||
items_per_page?: number;
|
||||
grid_item_size?: number;
|
||||
default_view?: string;
|
||||
show_adult_content?: boolean;
|
||||
auto_play_trailers?: boolean;
|
||||
language?: string;
|
||||
theme?: string;
|
||||
jellyfin_library_mappings?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSettingsInput extends Partial<CreateSettingsInput> {}
|
||||
+42
-14
@@ -1,7 +1,7 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
// Import the source mapping
|
||||
import { SOURCE_CATEGORY_MAPPING } from '@/types';
|
||||
// Import the source mapping and types
|
||||
import { SOURCE_CATEGORY_MAPPING, Media, Staff, Episode, Track } from '@/types';
|
||||
|
||||
export interface JellyfinConfig {
|
||||
url: string;
|
||||
@@ -56,7 +56,7 @@ export interface JellyfinItem {
|
||||
Type: string;
|
||||
Role?: string;
|
||||
PrimaryImageTag?: string;
|
||||
ImageBlurHashes?: any;
|
||||
ImageBlurHashes?: Record<string, Record<string, string>>;
|
||||
}>;
|
||||
ImageTags?: {
|
||||
Primary?: string;
|
||||
@@ -96,7 +96,7 @@ export interface JellyfinPerson {
|
||||
Name: string;
|
||||
Type: string;
|
||||
PrimaryImageTag?: string;
|
||||
ImageBlurHashes?: any;
|
||||
ImageBlurHashes?: Record<string, Record<string, string>>;
|
||||
PremiereDate?: string;
|
||||
ProductionYear?: number;
|
||||
Overview?: string;
|
||||
@@ -105,6 +105,28 @@ export interface JellyfinPerson {
|
||||
PlaceOfBirth?: string;
|
||||
}
|
||||
|
||||
export interface JellyfinEpisode {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
PremiereDate?: string;
|
||||
RunTimeTicks?: number;
|
||||
ParentIndexNumber?: number;
|
||||
IndexNumber?: number;
|
||||
ImageTags?: {
|
||||
Primary?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface JellyfinTrack {
|
||||
Id: string;
|
||||
Name: string;
|
||||
IndexNumber?: number;
|
||||
RunTimeTicks?: number;
|
||||
AlbumArtist?: string;
|
||||
Artists?: string[];
|
||||
}
|
||||
|
||||
export type LogCallback = (message: string) => void;
|
||||
export type ProgressCallback = (progress: Partial<ImportProgress>) => void;
|
||||
|
||||
@@ -575,10 +597,12 @@ async function convertJellyfinSeriesToMedia(
|
||||
const writers = item.People?.filter(p => p.Type === 'Writer').map(p => p.Name) || [];
|
||||
|
||||
// Fetch episodes for this series
|
||||
let episodes: any[] = [];
|
||||
let episodes: Episode[] = [];
|
||||
try {
|
||||
const jellyfinEpisodes = await fetchJellyfinSeriesEpisodes(config, item.Id);
|
||||
episodes = jellyfinEpisodes.map(ep => ({
|
||||
id: parseInt(ep.Id),
|
||||
media_id: parseInt(item.Id),
|
||||
season: ep.ParentIndexNumber || 1,
|
||||
episode_number: ep.IndexNumber || 1,
|
||||
title: ep.Name,
|
||||
@@ -682,14 +706,16 @@ async function convertJellyfinAlbumToMedia(
|
||||
}));
|
||||
|
||||
// Fetch tracks for this album
|
||||
let tracks: any[] = [];
|
||||
let tracks: Track[] = [];
|
||||
try {
|
||||
const jellyfinTracks = await fetchJellyfinAlbumTracks(config, item.Id);
|
||||
tracks = jellyfinTracks.map((track, index) => ({
|
||||
id: parseInt(track.Id),
|
||||
media_id: parseInt(item.Id),
|
||||
track_number: track.IndexNumber || (index + 1),
|
||||
title: track.Name,
|
||||
duration: track.RunTimeTicks ? `${Math.floor(track.RunTimeTicks / 600000000 / 60)}:${String(Math.floor((track.RunTimeTicks / 600000000) % 60)).padStart(2, '0')}` : null,
|
||||
artist: track.AlbumArtist || track.Artists?.[0] || albumArtists[0] || 'Unknown'
|
||||
duration: track.RunTimeTicks ? Math.floor(track.RunTimeTicks / 600000000) : null,
|
||||
artist: (track.AlbumArtist || track.Artists?.[0] || albumArtists[0] || 'Unknown') as string
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch tracks for album ${item.Name}:`, error);
|
||||
@@ -721,18 +747,20 @@ async function convertJellyfinAlbumToMedia(
|
||||
}
|
||||
|
||||
// Convert Jellyfin person to API cast format
|
||||
function convertJellyfinPersonToCast(person: JellyfinPerson, config: JellyfinConfig): any {
|
||||
function convertJellyfinPersonToCast(person: JellyfinPerson, config: JellyfinConfig): Staff {
|
||||
const photo = person.PrimaryImageTag
|
||||
? getJellyfinImageUrl(config, person.Id, person.PrimaryImageTag, 'Primary')
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: person.Id,
|
||||
name: person.Name,
|
||||
role: person.Type || 'Actor',
|
||||
photo: photo,
|
||||
bio: person.Overview || null,
|
||||
birthDate: person.BirthDate ? formatDate(person.BirthDate) : null,
|
||||
birthPlace: person.PlaceOfBirth || null,
|
||||
occupations: [person.Type === 'Actor' ? 'Actor' : person.Type || 'Person']
|
||||
occupations: ['Actor']
|
||||
};
|
||||
}
|
||||
|
||||
@@ -771,7 +799,7 @@ export async function importFromJellyfin(
|
||||
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=10000`);
|
||||
const existingMediaData = await existingMediaResponse.json();
|
||||
const existingMedia = new Map(
|
||||
(existingMediaData.data?.items || []).map((m: any) => [m.title, m])
|
||||
(existingMediaData.data?.items || []).map((m: Media) => [m.title, m])
|
||||
);
|
||||
logCallback(`Found ${existingMedia.size} existing media items in database`);
|
||||
|
||||
@@ -779,7 +807,7 @@ export async function importFromJellyfin(
|
||||
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
|
||||
const existingCastData = await existingCastResponse.json();
|
||||
const existingCast = new Map(
|
||||
(existingCastData.data?.items || []).map((c: any) => [c.name, c])
|
||||
(existingCastData.data?.items || []).map((c: Staff) => [c.name, c])
|
||||
);
|
||||
logCallback(`Found ${existingCast.size} existing cast members in database`);
|
||||
|
||||
@@ -1173,14 +1201,14 @@ export async function cleanupJellyfinMedia(
|
||||
logCallback('Fetching existing media from Kyoo API...');
|
||||
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=10000`);
|
||||
const existingMediaData = await existingMediaResponse.json();
|
||||
const jellyfinMedia = (existingMediaData.data?.items || []).filter((m: any) => m.source === 'jellyfin');
|
||||
const jellyfinMedia = (existingMediaData.data?.items || []).filter((m: Media) => m.source === 'jellyfin');
|
||||
logCallback(`Found ${jellyfinMedia.length} Jellyfin media items in database`);
|
||||
|
||||
// Fetch all existing cast from Kyoo API
|
||||
logCallback('Fetching existing cast from Kyoo API...');
|
||||
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
|
||||
const existingCastData = await existingCastResponse.json();
|
||||
const jellyfinCast = (existingCastData.data?.items || []).filter((c: any) => c.photo && c.photo.includes(normalizeUrl(config.url)));
|
||||
const jellyfinCast = (existingCastData.data?.items || []).filter((c: Staff) => c.photo && c.photo.includes(normalizeUrl(config.url)));
|
||||
logCallback(`Found ${jellyfinCast.length} Jellyfin cast members in database`);
|
||||
|
||||
// Fetch current items from Jellyfin
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
// Import the source mapping
|
||||
import { SOURCE_CATEGORY_MAPPING } from '@/types';
|
||||
// Import the source mapping and types
|
||||
import { SOURCE_CATEGORY_MAPPING, Media, Staff } from '@/types';
|
||||
|
||||
export interface PlayniteConfig {
|
||||
ip: string;
|
||||
@@ -54,6 +54,9 @@ export interface PlayniteGame {
|
||||
lastPlayed?: string;
|
||||
source?: string;
|
||||
isInstalled?: boolean;
|
||||
coverBase64?: string;
|
||||
backgroundBase64?: string;
|
||||
iconBase64?: string;
|
||||
}
|
||||
|
||||
export interface PlayniteGamesResponse {
|
||||
@@ -65,7 +68,7 @@ export interface PlayniteGamesResponse {
|
||||
|
||||
export type LogCallback = (message: string) => void;
|
||||
export type ProgressCallback = (progress: Partial<ImportProgress>) => void;
|
||||
|
||||
/*
|
||||
async function fetchGameCover(baseUrl: string, headers: Record<string, string>, gameId: string): Promise<string | null> {
|
||||
try {
|
||||
const coverResponse = await fetch(`${baseUrl}/api/games/${gameId}/cover`, {
|
||||
@@ -89,6 +92,50 @@ async function fetchGameCover(baseUrl: string, headers: Record<string, string>,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGameBackground(baseUrl: string, headers: Record<string, string>, gameId: string): Promise<string | null> {
|
||||
try {
|
||||
const backgroundResponse = await fetch(`${baseUrl}/api/games/${gameId}/background`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!backgroundResponse.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blob = await backgroundResponse.blob();
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||
|
||||
const mimeType = blob.type || 'image/jpeg';
|
||||
return `data:${mimeType};base64,${base64}`;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGameIcon(baseUrl: string, headers: Record<string, string>, gameId: string): Promise<string | null> {
|
||||
try {
|
||||
const iconResponse = await fetch(`${baseUrl}/api/games/${gameId}/icon`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!iconResponse.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blob = await iconResponse.blob();
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||
|
||||
const mimeType = blob.type || 'image/png';
|
||||
return `data:${mimeType};base64,${base64}`;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
export async function importFromPlaynite(
|
||||
config: PlayniteConfig,
|
||||
logCallback: LogCallback,
|
||||
@@ -117,7 +164,7 @@ export async function importFromPlaynite(
|
||||
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=1000`);
|
||||
const existingMediaData = await existingMediaResponse.json();
|
||||
const existingMedia = new Map(
|
||||
(existingMediaData.data?.items || []).map((m: any) => [m.title, m])
|
||||
(existingMediaData.data?.items || []).map((m: Media) => [m.title, m])
|
||||
);
|
||||
logCallback(`Found ${existingMedia.size} existing games in database`);
|
||||
|
||||
@@ -159,6 +206,18 @@ export async function importFromPlaynite(
|
||||
|
||||
if (detailResponse.ok) {
|
||||
const detailData: PlayniteGame = await detailResponse.json();
|
||||
/*
|
||||
// Fetch images
|
||||
const [cover, background, icon] = await Promise.all([
|
||||
fetchGameCover(baseUrl, headers, game.id),
|
||||
fetchGameBackground(baseUrl, headers, game.id),
|
||||
fetchGameIcon(baseUrl, headers, game.id)
|
||||
]);
|
||||
|
||||
detailData.coverBase64 = cover;
|
||||
detailData.backgroundBase64 = background;
|
||||
detailData.iconBase64 = icon;
|
||||
*/
|
||||
detailedGames.push(detailData);
|
||||
logCallback(`✓ Fetched details for: ${game.name}`);
|
||||
} else {
|
||||
@@ -231,7 +290,7 @@ export async function importFromPlaynite(
|
||||
}
|
||||
|
||||
// Staff is for actors/performers only - leave empty for games
|
||||
const staff: any[] = [];
|
||||
const staff: Staff[] = [];
|
||||
// Determine type based on genres/features
|
||||
let type = 'Game';
|
||||
//if (game.genres?.includes('Visual Novel') || game.genres?.includes('Adventure')) {
|
||||
|
||||
+34
-11
@@ -1,7 +1,7 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
// Import the source mapping
|
||||
import { SOURCE_CATEGORY_MAPPING } from '@/types';
|
||||
// Import the source mapping and types
|
||||
import { SOURCE_CATEGORY_MAPPING, Media, Staff } from '@/types';
|
||||
|
||||
export interface StashAPPConfig {
|
||||
url: string;
|
||||
@@ -81,7 +81,30 @@ export interface StashAPPScene {
|
||||
export interface StashAPPScenePerformer {
|
||||
id: string;
|
||||
name: string;
|
||||
disambiguation: string;
|
||||
url: string;
|
||||
gender: string;
|
||||
birthdate: string;
|
||||
ethnicity: string;
|
||||
country: string;
|
||||
eye_color: string;
|
||||
height_cm: number;
|
||||
measurements: string;
|
||||
fake_tits: boolean;
|
||||
career_length: string;
|
||||
tattoos: string;
|
||||
piercings: string;
|
||||
alias_list: string[];
|
||||
favorite: boolean;
|
||||
ignore_auto_tag: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
details: string;
|
||||
death_date: string;
|
||||
hair_color: string;
|
||||
weight: number;
|
||||
image_path: string;
|
||||
scene_count: number;
|
||||
}
|
||||
|
||||
export interface StashAPPPerformer {
|
||||
@@ -163,8 +186,8 @@ export async function updateActorsFromStashAPP(
|
||||
logCallback('Fetching existing cast from Kyoo API...');
|
||||
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
|
||||
const existingCastData = await existingCastResponse.json();
|
||||
const existingActors = new Map(
|
||||
(existingCastData.data?.items || []).map((c: any) => [c.name, c])
|
||||
const existingActors = new Map<string, Staff>(
|
||||
(existingCastData.data?.items || []).map((c: Staff) => [c.name, c])
|
||||
);
|
||||
logCallback(`Found ${existingActors.size} existing actors in database`);
|
||||
|
||||
@@ -249,12 +272,12 @@ export async function updateActorsFromStashAPP(
|
||||
|
||||
for (let i = 0; i < performers.length; i++) {
|
||||
const performer = performers[i];
|
||||
const existingActor: any = existingActors.get(performer.name);
|
||||
const existingActor: Staff | undefined = existingActors.get(performer.name);
|
||||
|
||||
try {
|
||||
if (existingActor) {
|
||||
// Update existing actor
|
||||
const updateData: any = {
|
||||
const updateData: Partial<Staff> = {
|
||||
name: performer.name,
|
||||
};
|
||||
|
||||
@@ -386,15 +409,15 @@ export async function importFromStashAPP(
|
||||
const existingMediaResponse = await fetch(`${BASE_URL}/api/media`);
|
||||
const existingMediaData = await existingMediaResponse.json();
|
||||
const existingTitles = new Set(
|
||||
existingMediaData.data?.items?.map((m: any) => m.title) || []
|
||||
existingMediaData.data?.items?.map((m: Media) => m.title) || []
|
||||
);
|
||||
logCallback(`Found ${existingTitles.size} existing videos in database`);
|
||||
|
||||
logCallback('Fetching existing cast from Kyoo API...');
|
||||
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`, {});
|
||||
const existingCastData = await existingCastResponse.json();
|
||||
const existingActors = new Map(
|
||||
(existingCastData.data?.items || []).map((c: any) => [c.name, c])
|
||||
const existingActors = new Map<string, Staff>(
|
||||
(existingCastData.data?.items || []).map((c: Staff) => [c.name, c])
|
||||
);
|
||||
logCallback(`Found ${existingActors.size} existing actors in database`);
|
||||
|
||||
@@ -525,12 +548,12 @@ export async function importFromStashAPP(
|
||||
|
||||
for (let i = 0; i < uniquePerformers.length; i++) {
|
||||
const performer = uniquePerformers[i];
|
||||
const existingActor: any = existingActors.get(performer.name);
|
||||
const existingActor: Staff | undefined = existingActors.get(performer.name);
|
||||
|
||||
try {
|
||||
if (existingActor) {
|
||||
// Update existing actor
|
||||
const updateData: any = {
|
||||
const updateData: Partial<Staff> = {
|
||||
name: performer.name,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
// Import the source mapping
|
||||
import { SOURCE_CATEGORY_MAPPING } from '@/types';
|
||||
// Import the source mapping and types
|
||||
import { SOURCE_CATEGORY_MAPPING, Media, Staff } from '@/types';
|
||||
|
||||
export interface XBVRConfig {
|
||||
url: string;
|
||||
@@ -83,7 +83,7 @@ export async function importFromXBVR(
|
||||
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=1000`);
|
||||
const existingMediaData = await existingMediaResponse.json();
|
||||
const existingTitles = new Set(
|
||||
existingMediaData.data?.items?.map((m: any) => m.title) || []
|
||||
existingMediaData.data?.items?.map((m: Media) => m.title) || []
|
||||
);
|
||||
logCallback(`Found ${existingTitles.size} existing videos in database`);
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function importFromXBVR(
|
||||
const existingCastResponse = await fetch(`${BASE_URL}/api/cast?limit=1000`);
|
||||
const existingCastData = await existingCastResponse.json();
|
||||
const existingActors = new Map(
|
||||
(existingCastData.data?.items || []).map((c: any) => [c.name, c])
|
||||
(existingCastData.data?.items || []).map((c: Staff) => [c.name, c])
|
||||
);
|
||||
logCallback(`Found ${existingActors.size} existing actors in database`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user