Compare commits

..

2 Commits

Author SHA1 Message Date
Lars Behrends
34bb4a27be Add logo and banner to README
Embed project logo and banner in the README and add the corresponding image assets. Adds img/logo.png (displayed above the title) and img/banner.png (displayed below the title) to improve repository branding and visual presentation.
2026-04-20 22:55:48 +02:00
Lars Behrends
e5cdd6b383 Rename Kyoo to Omnyx & add page settings
Rename project branding from "Kyoo" to "Omnyx" across README, index.html, metadata.json, typedoc and various UI components. Add support for page-level settings: pageTitle, favicon (Base64 upload/preview), and customColors (color scheme) — introduced CustomColors type, persisted via API types and converters, and wired into updateSettings/fetchSettings flows. UI: SettingsView adds page settings UI (upload, preview, color pickers) and handlers; App applies pageTitle, favicon and sets CSS variables for customColors; Sidebar and Header now display the configured page title. Also update importer modules and docs to use the new project name in logs/comments.
2026-04-20 22:51:33 +02:00
17 changed files with 285 additions and 43 deletions

View File

@@ -1,6 +1,10 @@
# Kyoo - Media Discovery Platform ![Omnyx Logo](img/logo.png)
A modern web application for browsing, managing, and discovering media across multiple categories. Kyoo provides a unified interface for your media library with support for importing from external sources like Playnite, StashAPP, and XBVR. # Omnyx - Media Discovery Platform
![Omnyx Banner](img/banner.png)
A modern web application for browsing, managing, and discovering media across multiple categories. Omnyx provides a unified interface for your media library with support for importing from external sources like Playnite, StashAPP, and XBVR.
## Features ## Features

BIN
img/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title> <title>Omnyx - Media Discovery</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,5 +1,5 @@
{ {
"name": "Kyoo - Media Discovery", "name": "Omnyx - Media Discovery",
"description": "A polished media discovery and tracking application inspired by modern anime platforms.", "description": "A polished media discovery and tracking application inspired by modern anime platforms.",
"requestFramePermissions": [] "requestFramePermissions": []
} }

View File

@@ -77,6 +77,22 @@ function AppContent() {
setEnabledCategories(loadedSettings.enabledCategories); setEnabledCategories(loadedSettings.enabledCategories);
// Sync theme with theme context // Sync theme with theme context
setTheme(loadedSettings.theme); setTheme(loadedSettings.theme);
// Set custom page title
if (loadedSettings.pageTitle) {
document.title = loadedSettings.pageTitle;
}
// Set custom favicon
if (loadedSettings.favicon) {
let faviconLink = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
if (!faviconLink) {
faviconLink = document.createElement('link');
faviconLink.rel = 'icon';
document.head.appendChild(faviconLink);
}
faviconLink.href = loadedSettings.favicon;
}
} }
} catch (error) { } catch (error) {
console.error('Failed to load settings from API:', error); console.error('Failed to load settings from API:', error);
@@ -86,6 +102,22 @@ function AppContent() {
loadSettingsFromApi(); loadSettingsFromApi();
}, [setTheme]); }, [setTheme]);
// Apply custom colors when settings change
useEffect(() => {
if (settings?.customColors) {
const root = document.documentElement;
const colors = settings.customColors;
if (colors.primary) root.style.setProperty('--color-primary', colors.primary);
if (colors.secondary) root.style.setProperty('--color-secondary', colors.secondary);
if (colors.background) root.style.setProperty('--color-background', colors.background);
if (colors.surface) root.style.setProperty('--color-surface', colors.surface);
if (colors.text) root.style.setProperty('--color-text', colors.text);
if (colors.muted) root.style.setProperty('--color-muted', colors.muted);
if (colors.border) root.style.setProperty('--color-border', colors.border);
}
}, [settings?.customColors]);
const reloadSettings = async () => { const reloadSettings = async () => {
try { try {
const loadedSettings = await fetchSettings(); const loadedSettings = await fetchSettings();
@@ -94,6 +126,22 @@ function AppContent() {
setEnabledCategories(loadedSettings.enabledCategories); setEnabledCategories(loadedSettings.enabledCategories);
// Sync theme with theme context // Sync theme with theme context
setTheme(loadedSettings.theme); setTheme(loadedSettings.theme);
// Set custom page title
if (loadedSettings.pageTitle) {
document.title = loadedSettings.pageTitle;
}
// Set custom favicon
if (loadedSettings.favicon) {
let faviconLink = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
if (!faviconLink) {
faviconLink = document.createElement('link');
faviconLink.rel = 'icon';
document.head.appendChild(faviconLink);
}
faviconLink.href = loadedSettings.favicon;
}
} }
} catch (error) { } catch (error) {
console.error('Failed to reload settings from API:', error); console.error('Failed to reload settings from API:', error);
@@ -315,6 +363,7 @@ function AppContent() {
<Sidebar <Sidebar
enabledCategories={enabledCategories} enabledCategories={enabledCategories}
onToggleCategory={toggleCategory} onToggleCategory={toggleCategory}
pageTitle={settings?.pageTitle}
/> />
<main className="flex-1 lg:ml-72 flex flex-col"> <main className="flex-1 lg:ml-72 flex flex-col">
@@ -385,7 +434,7 @@ function AppContent() {
<div className="max-w-[1920px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4"> <div className="max-w-[1920px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-2 text-lg font-black text-muted-foreground"> <div className="flex items-center gap-2 text-lg font-black text-muted-foreground">
<div className="w-5 h-5 bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] rounded-full" /> <div className="w-5 h-5 bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] rounded-full" />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/70">kyoo</span> <span className="bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/70">{settings?.pageTitle || 'omnyx'}</span>
</div> </div>
<div className="flex items-center gap-6 text-sm font-bold text-muted-foreground"> <div className="flex items-center gap-6 text-sm font-bold text-muted-foreground">
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Terms</a> <a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Terms</a>
@@ -393,7 +442,7 @@ function AppContent() {
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Contact</a> <a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Contact</a>
</div> </div>
<p className="text-xs font-medium text-muted-foreground"> <p className="text-xs font-medium text-muted-foreground">
© 2026 Kyoo Media Discovery. All rights reserved. © 2026 Omnyx Media Discovery. All rights reserved.
</p> </p>
</div> </div>
</footer> </footer>

View File

@@ -95,7 +95,7 @@ export default function Header({
)} /> )} />
</div> </div>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-white to-white/80"> <span className="bg-clip-text text-transparent bg-gradient-to-r from-white to-white/80">
kyoo omnyx
</span> </span>
</Link> </Link>
<button <button

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { MediaCategory, UserSettings } from '@/types'; import { MediaCategory, UserSettings, CustomColors } from '@/types';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Film, Music, Book, Tv, Gamepad2, ShieldAlert, LayoutGrid, List, Globe, Monitor, Sun, Moon, Save, ArrowLeft } from 'lucide-react'; import { Film, Music, Book, Tv, Gamepad2, ShieldAlert, LayoutGrid, List, Globe, Monitor, Sun, Moon, Save, ArrowLeft, Type, Image, Palette } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { fetchSettings, updateSettings } from '@/api'; import { fetchSettings, updateSettings } from '@/api';
import { useTheme } from '@/contexts/ThemeContext'; import { useTheme } from '@/contexts/ThemeContext';
@@ -47,6 +47,12 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
// Page Settings State
const [pageTitle, setPageTitle] = useState<string>('');
const [favicon, setFavicon] = useState<string>('');
const [customColors, setCustomColors] = useState<CustomColors>({});
const [faviconPreview, setFaviconPreview] = useState<string>('');
useEffect(() => { useEffect(() => {
loadSettings(); loadSettings();
}, []); }, []);
@@ -56,6 +62,10 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
const loadedSettings = await fetchSettings(); const loadedSettings = await fetchSettings();
if (loadedSettings) { if (loadedSettings) {
setSettings(loadedSettings); setSettings(loadedSettings);
setPageTitle(loadedSettings.pageTitle || '');
setFavicon(loadedSettings.favicon || '');
setCustomColors(loadedSettings.customColors || {});
setFaviconPreview(loadedSettings.favicon || '');
} }
} catch (error) { } catch (error) {
console.error('Failed to load settings:', error); console.error('Failed to load settings:', error);
@@ -68,7 +78,13 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
setIsSaving(true); setIsSaving(true);
setSaveStatus('idle'); setSaveStatus('idle');
try { try {
const savedSettings = await updateSettings(settings); const updatedSettings: UserSettings = {
...settings,
pageTitle: pageTitle || undefined,
favicon: favicon || undefined,
customColors: Object.keys(customColors).length > 0 ? customColors : undefined,
};
const savedSettings = await updateSettings(updatedSettings);
if (savedSettings) { if (savedSettings) {
setSettings(savedSettings); setSettings(savedSettings);
setSaveStatus('success'); setSaveStatus('success');
@@ -96,6 +112,31 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
})); }));
}; };
const handleFaviconUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result as string;
setFavicon(base64);
setFaviconPreview(base64);
};
reader.readAsDataURL(file);
}
};
const handleRemoveFavicon = () => {
setFavicon('');
setFaviconPreview('');
};
const handleColorChange = (colorKey: keyof CustomColors, value: string) => {
setCustomColors(prev => ({
...prev,
[colorKey]: value || undefined,
}));
};
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center"> <div className="min-h-screen bg-background flex items-center justify-center">
@@ -342,6 +383,115 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
</div> </div>
</div> </div>
</section> </section>
{/* Page Settings */}
<section>
<h2 className="text-2xl font-black text-foreground mb-6">Page Settings</h2>
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 space-y-6">
{/* Page Title */}
<div>
<div className="flex items-center gap-2 mb-2">
<Type size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-foreground">Custom Page Title</Label>
</div>
<input
type="text"
value={pageTitle}
onChange={(e) => setPageTitle(e.target.value)}
placeholder="Leave empty for default title"
className="w-full px-4 py-3 rounded-xl bg-background border border-border/50 text-foreground placeholder:text-muted-foreground/50 focus:border-[#6d28d9] focus:outline-none transition-all"
/>
<p className="text-xs font-medium text-muted-foreground mt-2">
Custom title for your page. Leave empty to use the default title.
</p>
</div>
{/* Favicon Upload */}
<div>
<div className="flex items-center gap-2 mb-2">
<Image size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-foreground">Favicon / Icon</Label>
</div>
<div className="flex items-center gap-4">
{faviconPreview && (
<div className="relative">
<img
src={faviconPreview}
alt="Favicon preview"
className="w-16 h-16 rounded-xl object-cover border border-border/50"
/>
<button
onClick={handleRemoveFavicon}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-red-600 transition-colors"
>
×
</button>
</div>
)}
<div className="flex-1">
<input
type="file"
accept="image/*"
onChange={handleFaviconUpload}
className="hidden"
id="favicon-upload"
/>
<label
htmlFor="favicon-upload"
className="inline-flex items-center gap-2 px-4 py-3 rounded-xl bg-background border border-border/50 text-foreground hover:bg-muted hover:border-[#6d28d9]/30 cursor-pointer transition-all"
>
<Image size={16} />
{favicon ? 'Change favicon' : 'Upload favicon'}
</label>
</div>
</div>
<p className="text-xs font-medium text-muted-foreground mt-2">
Upload a custom favicon or icon. The image will be converted to Base64 format.
</p>
</div>
{/* Custom Colors */}
<div>
<div className="flex items-center gap-2 mb-4">
<Palette size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-foreground">Custom Colors</Label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ key: 'primary', label: 'Primary Color' },
{ key: 'secondary', label: 'Secondary Color' },
{ key: 'background', label: 'Background Color' },
{ key: 'surface', label: 'Surface Color' },
{ key: 'text', label: 'Text Color' },
{ key: 'muted', label: 'Muted Text Color' },
{ key: 'border', label: 'Border Color' },
].map(({ key, label }) => (
<div key={key} className="flex items-center gap-3 p-3 rounded-xl bg-background border border-border/50">
<input
type="color"
value={customColors[key as keyof CustomColors] || '#6d28d9'}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
className="w-10 h-10 rounded-lg cursor-pointer border-0"
/>
<div className="flex-1">
<Label className="text-xs font-black text-foreground">{label}</Label>
<input
type="text"
value={customColors[key as keyof CustomColors] || ''}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
placeholder="#6d28d9"
className="w-full mt-1 px-2 py-1 rounded-lg bg-muted border border-border/30 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-[#6d28d9] focus:outline-none transition-all"
/>
</div>
</div>
))}
</div>
<p className="text-xs font-medium text-muted-foreground mt-2">
Leave color fields empty to use the default theme colors.
</p>
</div>
</div>
</section>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -31,9 +31,10 @@ import { CATEGORY_PATHS } from '@/constants';
interface SidebarProps { interface SidebarProps {
enabledCategories: MediaCategory[]; enabledCategories: MediaCategory[];
onToggleCategory: (category: MediaCategory) => void; onToggleCategory: (category: MediaCategory) => void;
pageTitle?: string;
} }
export default function Sidebar({ enabledCategories, onToggleCategory }: SidebarProps) { export default function Sidebar({ enabledCategories, onToggleCategory, pageTitle }: SidebarProps) {
const [isMediaExpanded, setIsMediaExpanded] = useState(true); const [isMediaExpanded, setIsMediaExpanded] = useState(true);
const [isMobileOpen, setIsMobileOpen] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false);
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
@@ -120,7 +121,7 @@ export default function Sidebar({ enabledCategories, onToggleCategory }: Sidebar
<div className="w-10 h-10 bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] rounded-xl flex items-center justify-center shadow-lg shadow-[#6d28d9]/30"> <div className="w-10 h-10 bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] rounded-xl flex items-center justify-center shadow-lg shadow-[#6d28d9]/30">
<div className="w-5 h-5 rounded-full bg-white" /> <div className="w-5 h-5 rounded-full bg-white" />
</div> </div>
<span className="text-xl font-black text-foreground">kyoo</span> <span className="text-xl font-black text-foreground">{pageTitle || 'omnyx'}</span>
</div> </div>
</div> </div>

View File

@@ -170,6 +170,12 @@ export function convertApiToSettings(apiItem: ApiSettingsItem): UserSettings {
language: apiItem.language || 'en', language: apiItem.language || 'en',
theme: (apiItem.theme as 'light' | 'dark' | 'system') || 'system', theme: (apiItem.theme as 'light' | 'dark' | 'system') || 'system',
jellyfinLibraryMappings: apiItem.jellyfin_library_mappings, jellyfinLibraryMappings: apiItem.jellyfin_library_mappings,
// Page Settings
pageTitle: apiItem.page_title,
favicon: apiItem.favicon,
customColors: apiItem.custom_colors ? JSON.parse(apiItem.custom_colors) : undefined,
createdAt: apiItem.created_at, createdAt: apiItem.created_at,
updatedAt: apiItem.updated_at, updatedAt: apiItem.updated_at,
}; };
@@ -186,5 +192,10 @@ export function convertSettingsToApi(settings: UserSettings): CreateSettingsInpu
language: settings.language, language: settings.language,
theme: settings.theme, theme: settings.theme,
jellyfin_library_mappings: settings.jellyfinLibraryMappings, jellyfin_library_mappings: settings.jellyfinLibraryMappings,
// Page Settings
page_title: settings.pageTitle,
favicon: settings.favicon,
custom_colors: settings.customColors ? JSON.stringify(settings.customColors) : undefined,
}; };
} }

View File

@@ -193,6 +193,12 @@ export interface ApiSettingsItem {
language: string; language: string;
theme: string; theme: string;
jellyfin_library_mappings?: string; jellyfin_library_mappings?: string;
// Page Settings
page_title?: string;
favicon?: string;
custom_colors?: string; // JSON string of CustomColors
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
@@ -207,6 +213,11 @@ export interface CreateSettingsInput {
language?: string; language?: string;
theme?: string; theme?: string;
jellyfin_library_mappings?: string; jellyfin_library_mappings?: string;
// Page Settings
page_title?: string;
favicon?: string;
custom_colors?: string;
} }
export interface UpdateSettingsInput extends Partial<CreateSettingsInput> {} export interface UpdateSettingsInput extends Partial<CreateSettingsInput> {}

View File

@@ -1,7 +1,7 @@
/** /**
* Jellyfin Importer Module * Jellyfin Importer Module
* *
* This module provides functionality to import media from a Jellyfin media server into the Kyoo media database. * This module provides functionality to import media from a Jellyfin media server into the Omnyx media database.
* It supports importing movies, TV series (including episodes), music albums, and cast members. * It supports importing movies, TV series (including episodes), music albums, and cast members.
* The module handles library mapping to categorize content appropriately and supports both new imports * The module handles library mapping to categorize content appropriately and supports both new imports
* and updates to existing entries. * and updates to existing entries.
@@ -25,7 +25,7 @@ export interface JellyfinConfig {
} }
/** /**
* Mapping configuration for Jellyfin libraries to Kyoo categories * Mapping configuration for Jellyfin libraries to Omnyx categories
*/ */
export interface LibraryMapping { export interface LibraryMapping {
/** Name of the Jellyfin library */ /** Name of the Jellyfin library */
@@ -838,10 +838,10 @@ function convertJellyfinPersonToCast(person: JellyfinPerson, config: JellyfinCon
} }
/** /**
* Imports media from a Jellyfin instance into the Kyoo media database * Imports media from a Jellyfin instance into the Omnyx media database
* *
* This function performs the following steps: * This function performs the following steps:
* 1. Fetches existing media and cast from Kyoo to check for duplicates * 1. Fetches existing media and cast from Omnyx to check for duplicates
* 2. Fetches Jellyfin libraries for category mapping (if library mappings are provided) * 2. Fetches Jellyfin libraries for category mapping (if library mappings are provided)
* 3. Imports movies (if enabled) * 3. Imports movies (if enabled)
* 4. Imports TV series with episodes (if enabled) * 4. Imports TV series with episodes (if enabled)
@@ -895,7 +895,7 @@ export async function importFromJellyfin(
logCallback('Starting Jellyfin import...'); logCallback('Starting Jellyfin import...');
// Step 0: Fetch existing media and cast to check for duplicates // Step 0: Fetch existing media and cast to check for duplicates
logCallback('Fetching existing media from Kyoo API...'); logCallback('Fetching existing media from Omnyx API...');
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=10000`); const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=10000`);
const existingMediaData = await existingMediaResponse.json(); const existingMediaData = await existingMediaResponse.json();
const existingMedia = new Map( const existingMedia = new Map(
@@ -903,7 +903,7 @@ export async function importFromJellyfin(
); );
logCallback(`Found ${existingMedia.size} existing media items in database`); logCallback(`Found ${existingMedia.size} existing media items in database`);
logCallback('Fetching existing cast from Kyoo API...'); logCallback('Fetching existing cast from Omnyx API...');
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`); const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
const existingCastData = await existingCastResponse.json(); const existingCastData = await existingCastResponse.json();
const existingCast = new Map( const existingCast = new Map(
@@ -1297,15 +1297,15 @@ export async function cleanupJellyfinMedia(
try { try {
logCallback('Starting Jellyfin cleanup...'); logCallback('Starting Jellyfin cleanup...');
// Fetch all existing media from Kyoo API // Fetch all existing media from Omnyx API
logCallback('Fetching existing media from Kyoo API...'); logCallback('Fetching existing media from Omnyx API...');
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=10000`); const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=10000`);
const existingMediaData = await existingMediaResponse.json(); const existingMediaData = await existingMediaResponse.json();
const jellyfinMedia = (existingMediaData.data?.items || []).filter((m: Media) => m.source === 'jellyfin'); const jellyfinMedia = (existingMediaData.data?.items || []).filter((m: Media) => m.source === 'jellyfin');
logCallback(`Found ${jellyfinMedia.length} Jellyfin media items in database`); logCallback(`Found ${jellyfinMedia.length} Jellyfin media items in database`);
// Fetch all existing cast from Kyoo API // Fetch all existing cast from Omnyx API
logCallback('Fetching existing cast from Kyoo API...'); logCallback('Fetching existing cast from Omnyx API...');
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`); const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
const existingCastData = await existingCastResponse.json(); const existingCastData = await existingCastResponse.json();
const jellyfinCast = (existingCastData.data?.items || []).filter((c: Staff) => c.photo && c.photo.includes(normalizeUrl(config.url))); const jellyfinCast = (existingCastData.data?.items || []).filter((c: Staff) => c.photo && c.photo.includes(normalizeUrl(config.url)));

View File

@@ -1,8 +1,8 @@
/** /**
* Playnite Importer Module * Playnite Importer Module
* *
* This module provides functionality to import games from a Playnite library into the Kyoo media database. * This module provides functionality to import games from a Playnite library into the Omnyx media database.
* It fetches game data from the Playnite API, converts it to the Kyoo media format, and handles both * It fetches game data from the Playnite API, converts it to the Omnyx media format, and handles both
* new imports and updates to existing entries. * new imports and updates to existing entries.
* *
* @module playniteImporter * @module playniteImporter
@@ -216,14 +216,14 @@ async function fetchGameIcon(baseUrl: string, headers: Record<string, string>, g
} }
*/ */
/** /**
* Imports games from a Playnite library into the Kyoo media database * Imports games from a Playnite library into the Omnyx media database
* *
* This function performs the following steps: * This function performs the following steps:
* 1. Fetches existing media from Kyoo to check for duplicates * 1. Fetches existing media from Omnyx to check for duplicates
* 2. Fetches all games from the Playnite API * 2. Fetches all games from the Playnite API
* 3. Fetches detailed information for each game * 3. Fetches detailed information for each game
* 4. Converts Playnite game data to Kyoo media format * 4. Converts Playnite game data to Omnyx media format
* 5. Imports or updates each game in the Kyoo database * 5. Imports or updates each game in the Omnyx database
* *
* @param config - Configuration for connecting to Playnite * @param config - Configuration for connecting to Playnite
* @param logCallback - Callback function for logging progress messages * @param logCallback - Callback function for logging progress messages
@@ -264,7 +264,7 @@ export async function importFromPlaynite(
logCallback('Starting Playnite import...'); logCallback('Starting Playnite import...');
// Step 0: Fetch existing media to check for duplicates and enable updates // Step 0: Fetch existing media to check for duplicates and enable updates
logCallback('Fetching existing media from Kyoo API...'); logCallback('Fetching existing media from Omnyx API...');
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=1000`); const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=1000`);
const existingMediaData = await existingMediaResponse.json(); const existingMediaData = await existingMediaResponse.json();
const existingMedia = new Map( const existingMedia = new Map(

View File

@@ -2,7 +2,7 @@
* StashAPP Importer Module * StashAPP Importer Module
* *
* This module provides functionality to import adult video content and performers from a StashAPP instance * This module provides functionality to import adult video content and performers from a StashAPP instance
* into the Kyoo media database. It fetches scene and performer data via GraphQL, converts it to the Kyoo * into the Omnyx media database. It fetches scene and performer data via GraphQL, converts it to the Omnyx
* media format, and handles both new imports and updates to existing entries. * media format, and handles both new imports and updates to existing entries.
* *
* @module stashappImporter * @module stashappImporter
@@ -226,7 +226,7 @@ function isPathBlacklisted(filePath: string, blacklist: string[]): boolean {
* Updates or creates actor entries from StashAPP performers * Updates or creates actor entries from StashAPP performers
* *
* This function fetches all performers from StashAPP and updates or creates * This function fetches all performers from StashAPP and updates or creates
* corresponding actor entries in the Kyoo database. * corresponding actor entries in the Omnyx database.
* *
* @param config - Configuration for connecting to StashAPP * @param config - Configuration for connecting to StashAPP
* @param logCallback - Callback function for logging progress messages * @param logCallback - Callback function for logging progress messages
@@ -251,8 +251,8 @@ export async function updateActorsFromStashAPP(
try { try {
logCallback('Starting StashAPP actor update...'); logCallback('Starting StashAPP actor update...');
// Fetch existing cast from Kyoo API // Fetch existing cast from Omnyx API
logCallback('Fetching existing cast from Kyoo API...'); logCallback('Fetching existing cast from Omnyx API...');
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`); const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
const existingCastData = await existingCastResponse.json(); const existingCastData = await existingCastResponse.json();
const existingActors = new Map<string, Staff>( const existingActors = new Map<string, Staff>(
@@ -456,10 +456,10 @@ export async function updateActorsFromStashAPP(
} }
/** /**
* Imports scenes and performers from a StashAPP instance into the Kyoo media database * Imports scenes and performers from a StashAPP instance into the Omnyx media database
* *
* This function performs the following steps: * This function performs the following steps:
* 1. Fetches existing media and cast from Kyoo to check for duplicates * 1. Fetches existing media and cast from Omnyx to check for duplicates
* 2. Fetches all scenes from StashAPP via GraphQL * 2. Fetches all scenes from StashAPP via GraphQL
* 3. Extracts unique performers from all scenes * 3. Extracts unique performers from all scenes
* 4. Imports or updates performers first * 4. Imports or updates performers first
@@ -499,7 +499,7 @@ export async function importFromStashAPP(
logCallback('Starting StashAPP import...'); logCallback('Starting StashAPP import...');
// Step 0: Fetch existing media and cast to check for duplicates // Step 0: Fetch existing media and cast to check for duplicates
logCallback('Fetching existing media from Kyoo API...'); logCallback('Fetching existing media from Omnyx API...');
const existingMediaResponse = await fetch(`${BASE_URL}/api/media`); const existingMediaResponse = await fetch(`${BASE_URL}/api/media`);
const existingMediaData = await existingMediaResponse.json(); const existingMediaData = await existingMediaResponse.json();
const existingTitles = new Set( const existingTitles = new Set(
@@ -507,7 +507,7 @@ export async function importFromStashAPP(
); );
logCallback(`Found ${existingTitles.size} existing videos in database`); logCallback(`Found ${existingTitles.size} existing videos in database`);
logCallback('Fetching existing cast from Kyoo API...'); logCallback('Fetching existing cast from Omnyx API...');
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`, {}); const existingCastResponse = await fetch(`${BASE_URL}/api/cast`, {});
const existingCastData = await existingCastResponse.json(); const existingCastData = await existingCastResponse.json();
const existingActors = new Map<string, Staff>( const existingActors = new Map<string, Staff>(

View File

@@ -1,7 +1,7 @@
/** /**
* XBVR Importer Module * XBVR Importer Module
* *
* This module provides functionality to import VR adult video content from an XBVR instance into the Kyoo media database. * This module provides functionality to import VR adult video content from an XBVR instance into the Omnyx media database.
* It fetches scene data from the DeoVR API endpoint, extracts actors and video details, and handles both new imports * It fetches scene data from the DeoVR API endpoint, extracts actors and video details, and handles both new imports
* and updates to existing entries. The module specifically filters for content in the 'Recent' scene group. * and updates to existing entries. The module specifically filters for content in the 'Recent' scene group.
* *
@@ -124,10 +124,10 @@ export type LogCallback = (message: string) => void;
export type ProgressCallback = (progress: Partial<ImportProgress>) => void; export type ProgressCallback = (progress: Partial<ImportProgress>) => void;
/** /**
* Imports VR adult videos and actors from an XBVR instance into the Kyoo media database * Imports VR adult videos and actors from an XBVR instance into the Omnyx media database
* *
* This function performs the following steps: * This function performs the following steps:
* 1. Fetches existing media and cast from Kyoo to check for duplicates * 1. Fetches existing media and cast from Omnyx to check for duplicates
* 2. Fetches the scene list from the DeoVR API endpoint * 2. Fetches the scene list from the DeoVR API endpoint
* 3. Extracts videos from the 'Recent' scene group * 3. Extracts videos from the 'Recent' scene group
* 4. Fetches detailed information for each video * 4. Fetches detailed information for each video
@@ -170,7 +170,7 @@ export async function importFromXBVR(
logCallback('Starting DeoVR import...'); logCallback('Starting DeoVR import...');
// Step 0: Fetch existing media and cast to check for duplicates // Step 0: Fetch existing media and cast to check for duplicates
logCallback('Fetching existing media from Kyoo API...'); logCallback('Fetching existing media from Omnyx API...');
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=1000`); const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=1000`);
const existingMediaData = await existingMediaResponse.json(); const existingMediaData = await existingMediaResponse.json();
const existingTitles = new Set( const existingTitles = new Set(
@@ -178,7 +178,7 @@ export async function importFromXBVR(
); );
logCallback(`Found ${existingTitles.size} existing videos in database`); logCallback(`Found ${existingTitles.size} existing videos in database`);
logCallback('Fetching existing cast from Kyoo API...'); logCallback('Fetching existing cast from Omnyx API...');
const existingCastResponse = await fetch(`${BASE_URL}/api/cast?limit=1000`); const existingCastResponse = await fetch(`${BASE_URL}/api/cast?limit=1000`);
const existingCastData = await existingCastResponse.json(); const existingCastData = await existingCastResponse.json();
const existingActors = new Map( const existingActors = new Map(

View File

@@ -119,10 +119,26 @@ export interface UserSettings {
language: string; language: string;
theme: 'light' | 'dark' | 'system'; theme: 'light' | 'dark' | 'system';
jellyfinLibraryMappings?: string; // JSON string of LibraryMapping[] jellyfinLibraryMappings?: string; // JSON string of LibraryMapping[]
// Page Settings
pageTitle?: string; // Custom page title
favicon?: string; // Base64 encoded favicon/image
customColors?: CustomColors; // Custom color scheme
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
} }
export interface CustomColors {
primary?: string; // Primary accent color (hex)
secondary?: string; // Secondary accent color (hex)
background?: string; // Background color (hex)
surface?: string; // Surface/card color (hex)
text?: string; // Text color (hex)
muted?: string; // Muted text color (hex)
border?: string; // Border color (hex)
}
// Source to Category mapping - ensures sources are only used with appropriate categories // Source to Category mapping - ensures sources are only used with appropriate categories
export const SOURCE_CATEGORY_MAPPING: Record<string, MediaCategory[]> = { export const SOURCE_CATEGORY_MAPPING: Record<string, MediaCategory[]> = {
'xbvr': ['Adult'], 'xbvr': ['Adult'],

View File

@@ -7,7 +7,7 @@
"./src/lib/xbvrImporter.ts" "./src/lib/xbvrImporter.ts"
], ],
"out": "docs", "out": "docs",
"name": "Kyoo Importer Documentation", "name": "Omnyx Importer Documentation",
"theme": "default", "theme": "default",
"excludePrivate": true, "excludePrivate": true,
"excludeProtected": false, "excludeProtected": false,