first commit

This commit is contained in:
Lars Behrends
2026-04-09 10:29:11 +02:00
commit dda118a2f7
36 changed files with 14470 additions and 0 deletions

290
src/api.ts Normal file
View File

@@ -0,0 +1,290 @@
import { Media, Staff } from './types';
const BASE_URL = 'http://192.168.1.102:57000';
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}`;
}
export interface ApiResponse {
success: boolean;
data: {
items: ApiMediaItem[];
};
}
export interface ApiMediaItem {
id: number;
title: string;
overview: string;
poster_url: string;
poster_aspect_ratio: string | null;
backdrop_url: string | null;
backdrop_aspect_ratio: string | null;
rating: string;
runtime_minutes: number;
release_date: string;
director: string | null;
writer: string | null;
cast: string | null;
genre: string | null;
metadata: string;
actors?: Array<{
id: number;
name: string;
thumbnail_path: string | null;
metadata?: string;
created_at?: string;
updated_at?: string;
}>;
}
export interface ApiMetadata {
xbvr_id: number;
xbvr_url: string | null;
cast: string[];
actors: Array<{
id: number;
name: string;
thumbnail_path: string | null;
}>;
tags: string[];
is_available: boolean;
is_watched: boolean;
watch_count: number;
video_length: number;
video_width: number | null;
video_height: number | null;
video_codec: string | null;
file_path: string | null;
cover_url: string;
[key: string]: any;
}
export function convertApiToMedia(apiItem: ApiMediaItem): Media {
let metadata: ApiMetadata;
try {
metadata = JSON.parse(apiItem.metadata);
} catch (e) {
metadata = {
xbvr_id: 0,
xbvr_url: null,
cast: [],
actors: [],
tags: [],
is_available: false,
is_watched: false,
watch_count: 0,
video_length: 0,
video_width: null,
video_height: null,
video_codec: null,
file_path: null,
cover_url: apiItem.poster_url,
};
}
// Use actors from the main item if available, otherwise from metadata
const actors = apiItem.actors || metadata.actors || [];
const staff: Staff[] = actors.map((actor, index) => ({
id: `actor-${actor.id}`,
name: actor.name,
role: 'Actor',
photo: normalizeUrl(actor.thumbnail_path) || `https://picsum.photos/seed/actor-${actor.id}/200/200`,
characterName: actor.name,
characterImage: normalizeUrl(actor.thumbnail_path) || `https://picsum.photos/seed/actor-${actor.id}/200/200`,
}));
// Determine aspect ratio from poster_aspect_ratio or default to 2/3
let aspectRatio: '2/3' | '16/9' | '1/1' = '2/3';
if (apiItem.poster_aspect_ratio) {
const ratio = apiItem.poster_aspect_ratio.toLowerCase();
if (ratio.includes('16:9') || ratio.includes('1.78')) {
aspectRatio = '16/9';
} else if (ratio.includes('1:1') || ratio.includes('1.00')) {
aspectRatio = '1/1';
}
}
return {
id: apiItem.id.toString() || undefined,
title: apiItem.title || undefined,
year: apiItem.release_date ? new Date(apiItem.release_date).getFullYear().toString() : 'Unknown',
poster: normalizeUrl(apiItem.poster_url) || `https://picsum.photos/seed/${apiItem.id}/400/600`,
banner: normalizeUrl(apiItem.backdrop_url) || undefined,
description: apiItem.overview || undefined,
rating: apiItem.rating ? parseFloat(apiItem.rating) : undefined,
genres: metadata.tags || [],
tags: metadata.tags || [],
studios: apiItem.director ? [apiItem.director] : undefined,
type: 'Movie',
status: 'completed',
staff: staff.length > 0 ? staff : undefined,
runtime: apiItem.runtime_minutes,
director: apiItem.director || undefined,
writer: apiItem.writer || undefined,
releaseDate: apiItem.release_date || undefined,
aspectRatio: aspectRatio
};
}
export async function fetchMediaFromApi(apiUrl: string = `${BASE_URL}/api/adult`): Promise<Media[]> {
console.error('Error fetching');
try {
const response = await fetch(apiUrl);
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 fetchMediaFromLocalJson(): Promise<Media[]> {
try {
const response = await fetch('/adult.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse = await response.json();
if (data.data.items) {
return data.data.items.map(convertApiToMedia);
}
return [];
} catch (error) {
console.error('Error fetching media from local JSON:', error);
return [];
}
}
export async function fetchMediaById(id: number): Promise<Media | null> {
try {
const response = await fetch('/adult.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse = await response.json();
if (data.data.items) {
const item = data.data.items.find(item => item.id === id);
return item ? convertApiToMedia(item) : null;
}
return null;
} catch (error) {
console.error('Error fetching media by ID:', error);
return null;
}
}
export async function fetchMediaByActor(actorName: string): Promise<Media[]> {
try {
const response = await fetch('/adult.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse = await response.json();
if (data.data.items) {
return data.data.items
.filter(item => item.actors?.some(actor => actor.name.toLowerCase().includes(actorName.toLowerCase())))
.map(convertApiToMedia);
}
return [];
} catch (error) {
console.error('Error fetching media by actor:', error);
return [];
}
}
export async function fetchMediaByTag(tag: string): Promise<Media[]> {
try {
const response = await fetch('/adult.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse = await response.json();
if (data.data.items) {
return data.data.items
.filter(item => {
try {
const metadata = JSON.parse(item.metadata);
return metadata.tags?.some(t => t.toLowerCase().includes(tag.toLowerCase()));
} catch {
return false;
}
})
.map(convertApiToMedia);
}
return [];
} catch (error) {
console.error('Error fetching media by tag:', error);
return [];
}
}
export async function fetchAllActors(): Promise<Array<{id: number, name: string, thumbnail_path: string | null}>> {
try {
const response = await fetch('/adult.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse = await response.json();
if (data.data.items) {
const actorMap = new Map();
data.data.items.forEach(item => {
item.actors?.forEach(actor => {
if (!actorMap.has(actor.id)) {
actorMap.set(actor.id, {
id: actor.id,
name: actor.name,
thumbnail_path: actor.thumbnail_path
});
}
});
});
return Array.from(actorMap.values());
}
return [];
} catch (error) {
console.error('Error fetching all actors:', error);
return [];
}
}
export async function fetchAllTags(): Promise<string[]> {
try {
const response = await fetch('/adult.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse = await response.json();
if (data.data.items) {
const tagSet = new Set<string>();
data.data.items.forEach(item => {
try {
const metadata = JSON.parse(item.metadata);
metadata.tags?.forEach((tag: string) => tagSet.add(tag));
} catch {
// Ignore metadata parsing errors
}
});
return Array.from(tagSet).sort();
}
return [];
} catch (error) {
console.error('Error fetching all tags:', error);
return [];
}
}