import { Media, Staff, UserSettings, MediaCategory } from './types'; const BASE_URL = 'http://192.168.1.102:6400'; function normalizeUrl(url: string | null): string { if (!url) return ''; if (url.startsWith('http://') || url.startsWith('https://')) { return url; } // Remove leading slash if present and add base URL const cleanPath = url.startsWith('/') ? url.slice(1) : url; return `${BASE_URL}/${cleanPath}`; } // API Response Types export interface ApiResponse { success: boolean; data: T; } export interface PaginatedResponse { items: T[]; total: number; page: number; limit: number; totalPages?: number; } // Media Types 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; createdAt: string; updatedAt: string; genres?: string[]; tags?: string[]; studios?: string[]; staff?: ApiStaff[]; categories?: string[]; platforms?: string[]; developers?: string[]; completionStatus?: string; source?: string; playCount?: number; lastActivity?: string | null; playtime?: number; } 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; genres?: string[]; tags?: string[]; studios?: string[]; staff?: CreateStaffInput[]; } export interface UpdateMediaInput extends Partial {} 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 {} 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 { // Convert staff from API to Media staff format 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`, })); // Determine aspect ratio from API format 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'; } } // Map API type to Media type allowed values 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'; } // Map API category to MediaCategory 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 { // If category doesn't match any known category, use the original value capitalized // This handles cases where the API returns unexpected category values console.warn('Unknown category:', apiItem.category, 'defaulting to Movies'); mediaCategory = 'Movies'; } // Map API status to Media status allowed values 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, status: mediaStatus, staff: staff.length > 0 ? staff : undefined, aspectRatio: aspectRatio, categories: apiItem.categories, platforms: apiItem.platforms, developers: apiItem.developers, completionStatus: apiItem.completionStatus, source: apiItem.source, playCount: apiItem.playCount, lastActivity: apiItem.lastActivity, playtime: apiItem.playtime }; } // Media API Functions export async function fetchAllMedia(page: number = 1, limit: number = 10000): Promise { 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> = 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 { try { const response = await fetch(`${BASE_URL}/api/media/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: ApiResponse = 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 { 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 = 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 { 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 = 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 { 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; } } // Cast API Functions export async function fetchAllCast(page: number = 1, limit: number = 100000): Promise { 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> = 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 { try { const response = await fetch(`${BASE_URL}/api/cast/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: ApiResponse = 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 { 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> = 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 { 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 = 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 { 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 = 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 { 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 function for compatibility - fetches all unique staff members from media export async function fetchAllActors(): Promise> { try { const media = await fetchAllMedia(1, 1000); const actorMap = new Map(); 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 []; } } // Legacy function for compatibility - fetches all unique tags from media export async function fetchAllTags(): Promise { try { const media = await fetchAllMedia(1, 1000); const tagSet = new Set(); media.forEach(item => { item.tags?.forEach(tag => tagSet.add(tag)); item.genres?.forEach(genre => tagSet.add(genre)); }); return Array.from(tagSet).sort(); } catch (error) { console.error('Error fetching all tags:', error); return []; } } // Legacy function for compatibility - fetches media by actor name export async function fetchMediaByActor(actorName: string): Promise { try { const media = await 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 []; } } // Legacy function for compatibility - fetches media by tag export async function fetchMediaByTag(tag: string): Promise { try { const media = await fetchAllMedia(1, 1000); return media.filter(item => item.tags?.some(t => t.toLowerCase().includes(tag.toLowerCase())) || item.genres?.some(g => g.toLowerCase().includes(tag.toLowerCase())) ); } catch (error) { console.error('Error fetching media by tag:', error); return []; } } // Convenience function - fetch media from API (legacy compatibility) export async function fetchMediaFromApi(apiUrl?: string): Promise { return fetchAllMedia(); } // Convenience function - fetch media from local JSON (legacy compatibility) export async function fetchMediaFromLocalJson(): Promise { return fetchAllMedia(); } // Settings API Types export interface ApiSettingsItem { id?: number; enabled_categories: string[]; items_per_page: number; default_view: string; show_adult_content: boolean; auto_play_trailers: boolean; language: string; theme: string; created_at?: string; updated_at?: string; } export interface CreateSettingsInput { enabled_categories: string[]; items_per_page?: number; default_view?: string; show_adult_content?: boolean; auto_play_trailers?: boolean; language?: string; theme?: string; } export interface UpdateSettingsInput extends Partial {} export function convertApiToSettings(apiItem: ApiSettingsItem): UserSettings { return { id: apiItem.id, enabledCategories: apiItem.enabled_categories as MediaCategory[], itemsPerPage: apiItem.items_per_page || 20, 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', createdAt: apiItem.created_at, updatedAt: apiItem.updated_at, }; } export function convertSettingsToApi(settings: UserSettings): CreateSettingsInput { return { enabled_categories: settings.enabledCategories, items_per_page: settings.itemsPerPage, default_view: settings.defaultView, show_adult_content: settings.showAdultContent, auto_play_trailers: settings.autoPlayTrailers, language: settings.language, theme: settings.theme, }; } // Settings API Functions export async function fetchSettings(): Promise { try { const response = await fetch(`${BASE_URL}/api/settings`); if (!response.ok) { // If settings don't exist (404), return null to use defaults if (response.status === 404) { return null; } throw new Error(`HTTP error! status: ${response.status}`); } const data: ApiResponse = 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 { try { const apiSettings = convertSettingsToApi(settings); console.log('Creating settings:', apiSettings); const response = await fetch(`${BASE_URL}/api/settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(apiSettings), }); console.log('Create settings response status:', response.status); 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 = await response.json(); console.log('Create settings response:', data); 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 { try { const apiSettings = convertSettingsToApi(settings); console.log('Updating settings:', apiSettings); const response = await fetch(`${BASE_URL}/api/settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(apiSettings), }); console.log('Update settings response status:', response.status); if (!response.ok) { // If settings don't exist (404), try creating them instead if (response.status === 404) { console.log('Settings not found, attempting to create...'); 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 = await response.json(); console.log('Update settings response:', data); if (data.success && data.data) { return convertApiToSettings(data.data); } return null; } catch (error) { console.error('Error updating settings:', error); return null; } }