547 lines
16 KiB
TypeScript
547 lines
16 KiB
TypeScript
import { Media, Staff } 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<T> {
|
|
success: boolean;
|
|
data: T;
|
|
}
|
|
|
|
export interface PaginatedResponse<T> {
|
|
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[];
|
|
}
|
|
|
|
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<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;
|
|
photo: string | null;
|
|
bio: string | null;
|
|
birthDate: string | null;
|
|
birthPlace: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
occupations?: string[];
|
|
filmography?: ApiCastMediaItem[];
|
|
}
|
|
|
|
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> {}
|
|
|
|
|
|
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('1.78') || ratio.includes('2.39')) {
|
|
aspectRatio = '16/9';
|
|
} else if (ratio.includes('1:1') || ratio.includes('1.00')) {
|
|
aspectRatio = '1/1';
|
|
}
|
|
}
|
|
|
|
// 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' | 'Music' | 'Books' | 'Adult' | 'Consoles' | 'Games' = 'Movies';
|
|
const apiCategory = apiItem.category?.toLowerCase();
|
|
|
|
console.log('API Category:', apiItem.category, 'Lowercased:', apiCategory, 'Type:', apiType);
|
|
|
|
if (apiCategory === 'anime') {
|
|
mediaCategory = 'Anime';
|
|
} else if (apiCategory === 'movie' || apiCategory === 'movies') {
|
|
mediaCategory = 'Movies';
|
|
} 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';
|
|
}
|
|
|
|
console.log('Mapped to:', mediaCategory);
|
|
|
|
// 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
|
|
};
|
|
}
|
|
|
|
// Media API Functions
|
|
export async function fetchAllMedia(page: number = 1, limit: number = 50): 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;
|
|
}
|
|
}
|
|
|
|
// Cast API Functions
|
|
export async function fetchAllCast(page: number = 1, limit: number = 50): Promise<ApiCastItem[]> {
|
|
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;
|
|
}
|
|
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<ApiMediaItem>> = 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 function for compatibility - fetches all unique staff members from media
|
|
export async function fetchAllActors(): Promise<Array<{id: number, name: string, photo: string | null}>> {
|
|
try {
|
|
const media = await 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 [];
|
|
}
|
|
}
|
|
|
|
// Legacy function for compatibility - fetches all unique tags from media
|
|
export async function fetchAllTags(): Promise<string[]> {
|
|
try {
|
|
const media = await fetchAllMedia(1, 1000);
|
|
const tagSet = new Set<string>();
|
|
|
|
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<Media[]> {
|
|
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<Media[]> {
|
|
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<Media[]> {
|
|
return fetchAllMedia();
|
|
}
|
|
|
|
// Convenience function - fetch media from local JSON (legacy compatibility)
|
|
export async function fetchMediaFromLocalJson(): Promise<Media[]> {
|
|
return fetchAllMedia();
|
|
}
|