Compare commits
2 Commits
63c5d0a7c0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34bb4a27be | ||
|
|
e5cdd6b383 |
@@ -1,6 +1,10 @@
|
||||
# Kyoo - Media Discovery Platform
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
|
||||
BIN
img/banner.png
Normal file
BIN
img/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
BIN
img/logo.png
Normal file
BIN
img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My Google AI Studio App</title>
|
||||
<title>Omnyx - Media Discovery</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
|
||||
53
src/App.tsx
53
src/App.tsx
@@ -77,6 +77,22 @@ function AppContent() {
|
||||
setEnabledCategories(loadedSettings.enabledCategories);
|
||||
// Sync theme with theme context
|
||||
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) {
|
||||
console.error('Failed to load settings from API:', error);
|
||||
@@ -86,6 +102,22 @@ function AppContent() {
|
||||
loadSettingsFromApi();
|
||||
}, [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 () => {
|
||||
try {
|
||||
const loadedSettings = await fetchSettings();
|
||||
@@ -94,6 +126,22 @@ function AppContent() {
|
||||
setEnabledCategories(loadedSettings.enabledCategories);
|
||||
// Sync theme with theme context
|
||||
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) {
|
||||
console.error('Failed to reload settings from API:', error);
|
||||
@@ -315,6 +363,7 @@ function AppContent() {
|
||||
<Sidebar
|
||||
enabledCategories={enabledCategories}
|
||||
onToggleCategory={toggleCategory}
|
||||
pageTitle={settings?.pageTitle}
|
||||
/>
|
||||
|
||||
<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="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" />
|
||||
<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 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>
|
||||
@@ -393,7 +442,7 @@ function AppContent() {
|
||||
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Contact</a>
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
© 2026 Kyoo Media Discovery. All rights reserved.
|
||||
© 2026 Omnyx Media Discovery. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function Header({
|
||||
)} />
|
||||
</div>
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-white to-white/80">
|
||||
kyoo
|
||||
omnyx
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MediaCategory, UserSettings } from '@/types';
|
||||
import { MediaCategory, UserSettings, CustomColors } from '@/types';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
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 { fetchSettings, updateSettings } from '@/api';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
@@ -46,6 +46,12 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
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(() => {
|
||||
loadSettings();
|
||||
@@ -56,6 +62,10 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
|
||||
const loadedSettings = await fetchSettings();
|
||||
if (loadedSettings) {
|
||||
setSettings(loadedSettings);
|
||||
setPageTitle(loadedSettings.pageTitle || '');
|
||||
setFavicon(loadedSettings.favicon || '');
|
||||
setCustomColors(loadedSettings.customColors || {});
|
||||
setFaviconPreview(loadedSettings.favicon || '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
@@ -68,7 +78,13 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
|
||||
setIsSaving(true);
|
||||
setSaveStatus('idle');
|
||||
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) {
|
||||
setSettings(savedSettings);
|
||||
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) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
@@ -342,6 +383,115 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -31,9 +31,10 @@ import { CATEGORY_PATHS } from '@/constants';
|
||||
interface SidebarProps {
|
||||
enabledCategories: MediaCategory[];
|
||||
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 [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
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-5 h-5 rounded-full bg-white" />
|
||||
</div>
|
||||
<span className="text-xl font-black text-foreground">kyoo</span>
|
||||
<span className="text-xl font-black text-foreground">{pageTitle || 'omnyx'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -170,6 +170,12 @@ export function convertApiToSettings(apiItem: ApiSettingsItem): UserSettings {
|
||||
language: apiItem.language || 'en',
|
||||
theme: (apiItem.theme as 'light' | 'dark' | 'system') || 'system',
|
||||
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,
|
||||
updatedAt: apiItem.updated_at,
|
||||
};
|
||||
@@ -186,5 +192,10 @@ export function convertSettingsToApi(settings: UserSettings): CreateSettingsInpu
|
||||
language: settings.language,
|
||||
theme: settings.theme,
|
||||
jellyfin_library_mappings: settings.jellyfinLibraryMappings,
|
||||
|
||||
// Page Settings
|
||||
page_title: settings.pageTitle,
|
||||
favicon: settings.favicon,
|
||||
custom_colors: settings.customColors ? JSON.stringify(settings.customColors) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,6 +193,12 @@ export interface ApiSettingsItem {
|
||||
language: string;
|
||||
theme: string;
|
||||
jellyfin_library_mappings?: string;
|
||||
|
||||
// Page Settings
|
||||
page_title?: string;
|
||||
favicon?: string;
|
||||
custom_colors?: string; // JSON string of CustomColors
|
||||
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -207,6 +213,11 @@ export interface CreateSettingsInput {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
jellyfin_library_mappings?: string;
|
||||
|
||||
// Page Settings
|
||||
page_title?: string;
|
||||
favicon?: string;
|
||||
custom_colors?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSettingsInput extends Partial<CreateSettingsInput> {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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.
|
||||
* The module handles library mapping to categorize content appropriately and supports both new imports
|
||||
* 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 {
|
||||
/** 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:
|
||||
* 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)
|
||||
* 3. Imports movies (if enabled)
|
||||
* 4. Imports TV series with episodes (if enabled)
|
||||
@@ -895,7 +895,7 @@ export async function importFromJellyfin(
|
||||
logCallback('Starting Jellyfin import...');
|
||||
|
||||
// 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 existingMediaData = await existingMediaResponse.json();
|
||||
const existingMedia = new Map(
|
||||
@@ -903,7 +903,7 @@ export async function importFromJellyfin(
|
||||
);
|
||||
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 existingCastData = await existingCastResponse.json();
|
||||
const existingCast = new Map(
|
||||
@@ -1297,15 +1297,15 @@ export async function cleanupJellyfinMedia(
|
||||
try {
|
||||
logCallback('Starting Jellyfin cleanup...');
|
||||
|
||||
// Fetch all existing media from Kyoo API
|
||||
logCallback('Fetching existing media from Kyoo API...');
|
||||
// Fetch all existing media from Omnyx API
|
||||
logCallback('Fetching existing media from Omnyx API...');
|
||||
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=10000`);
|
||||
const existingMediaData = await existingMediaResponse.json();
|
||||
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...');
|
||||
// Fetch all existing cast from Omnyx API
|
||||
logCallback('Fetching existing cast from Omnyx API...');
|
||||
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
|
||||
const existingCastData = await existingCastResponse.json();
|
||||
const jellyfinCast = (existingCastData.data?.items || []).filter((c: Staff) => c.photo && c.photo.includes(normalizeUrl(config.url)));
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Playnite Importer Module
|
||||
*
|
||||
* This module provides functionality to import games from a Playnite library into the Kyoo media database.
|
||||
* It fetches game data from the Playnite API, converts it to the Kyoo media format, and handles both
|
||||
* 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 Omnyx media format, and handles both
|
||||
* new imports and updates to existing entries.
|
||||
*
|
||||
* @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:
|
||||
* 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
|
||||
* 3. Fetches detailed information for each game
|
||||
* 4. Converts Playnite game data to Kyoo media format
|
||||
* 5. Imports or updates each game in the Kyoo database
|
||||
* 4. Converts Playnite game data to Omnyx media format
|
||||
* 5. Imports or updates each game in the Omnyx database
|
||||
*
|
||||
* @param config - Configuration for connecting to Playnite
|
||||
* @param logCallback - Callback function for logging progress messages
|
||||
@@ -264,7 +264,7 @@ export async function importFromPlaynite(
|
||||
logCallback('Starting Playnite import...');
|
||||
|
||||
// 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 existingMediaData = await existingMediaResponse.json();
|
||||
const existingMedia = new Map(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* StashAPP Importer Module
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @module stashappImporter
|
||||
@@ -226,7 +226,7 @@ function isPathBlacklisted(filePath: string, blacklist: string[]): boolean {
|
||||
* Updates or creates actor entries from StashAPP performers
|
||||
*
|
||||
* 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 logCallback - Callback function for logging progress messages
|
||||
@@ -251,8 +251,8 @@ export async function updateActorsFromStashAPP(
|
||||
try {
|
||||
logCallback('Starting StashAPP actor update...');
|
||||
|
||||
// Fetch existing cast from Kyoo API
|
||||
logCallback('Fetching existing cast from Kyoo API...');
|
||||
// Fetch existing cast from Omnyx API
|
||||
logCallback('Fetching existing cast from Omnyx API...');
|
||||
const existingCastResponse = await fetch(`${BASE_URL}/api/cast`);
|
||||
const existingCastData = await existingCastResponse.json();
|
||||
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:
|
||||
* 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
|
||||
* 3. Extracts unique performers from all scenes
|
||||
* 4. Imports or updates performers first
|
||||
@@ -499,7 +499,7 @@ export async function importFromStashAPP(
|
||||
logCallback('Starting StashAPP import...');
|
||||
|
||||
// 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 existingMediaData = await existingMediaResponse.json();
|
||||
const existingTitles = new Set(
|
||||
@@ -507,7 +507,7 @@ export async function importFromStashAPP(
|
||||
);
|
||||
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 existingCastData = await existingCastResponse.json();
|
||||
const existingActors = new Map<string, Staff>(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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:
|
||||
* 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
|
||||
* 3. Extracts videos from the 'Recent' scene group
|
||||
* 4. Fetches detailed information for each video
|
||||
@@ -170,7 +170,7 @@ export async function importFromXBVR(
|
||||
logCallback('Starting DeoVR import...');
|
||||
|
||||
// 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 existingMediaData = await existingMediaResponse.json();
|
||||
const existingTitles = new Set(
|
||||
@@ -178,7 +178,7 @@ export async function importFromXBVR(
|
||||
);
|
||||
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 existingCastData = await existingCastResponse.json();
|
||||
const existingActors = new Map(
|
||||
|
||||
16
src/types.ts
16
src/types.ts
@@ -119,10 +119,26 @@ export interface UserSettings {
|
||||
language: string;
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
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;
|
||||
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
|
||||
export const SOURCE_CATEGORY_MAPPING: Record<string, MediaCategory[]> = {
|
||||
'xbvr': ['Adult'],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./src/lib/xbvrImporter.ts"
|
||||
],
|
||||
"out": "docs",
|
||||
"name": "Kyoo Importer Documentation",
|
||||
"name": "Omnyx Importer Documentation",
|
||||
"theme": "default",
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": false,
|
||||
|
||||
Reference in New Issue
Block a user