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
|
## 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>
|
<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>
|
||||||
|
|||||||
@@ -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": []
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/App.tsx
53
src/App.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -46,6 +46,12 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {}
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
16
src/types.ts
16
src/types.ts
@@ -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'],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user