frontend: add OXC lint/format setup and apply semicolon fixes

This commit is contained in:
2026-05-25 02:54:30 +00:00
parent 836a563897
commit f6a40babc9
26 changed files with 3452 additions and 3130 deletions
+100 -95
View File
@@ -1,99 +1,97 @@
export interface PlayerStatus {
remote: boolean;
remote: boolean
}
export interface RootStatus {
root_id: string;
name: string;
path: string;
status: string;
error: string | null;
movies: number;
series: number;
root_id: string
name: string
path: string
status: string
error: string | null
movies: number
series: number
}
export interface RootsResponse {
roots: RootStatus[];
roots: RootStatus[]
}
export function normalizeMediaPath(input: string): string {
return input
.replace(/\\/g, '/')
.replace(/^[A-Za-z]:\//, '')
.replace(/^\/+/, '');
.replace(/\\/g, "/")
.replace(/^[A-Za-z]:\//, "")
.replace(/^\/+/, "")
}
export function isVideoPath(path: string | null | undefined): boolean {
return Boolean(path && /\.(webm|mp4|mkv|avi|mov)$/i.test(path));
return Boolean(path && /\.(webm|mp4|mkv|avi|mov)$/i.test(path))
}
export interface VideoSourceAttributes {
type: string;
codecs: string;
type: string
codecs: string
}
export function isSafariBrowser(): boolean {
if (typeof navigator === 'undefined') {
return false;
if (typeof navigator === "undefined") {
return false
}
const ua = navigator.userAgent;
return /Safari/i.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|FxiOS/i.test(ua);
const ua = navigator.userAgent
return /Safari/i.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|FxiOS/i.test(ua)
}
export function getVideoPreviewUrl(url: string): string {
if (!url || !isSafariBrowser()) {
return url;
return url
}
if (url.includes('#')) {
return url;
if (url.includes("#")) {
return url
}
// Safari often needs a tiny time offset to paint the first frame before playback.
return `${url}#t=0.001`;
return `${url}#t=0.001`
}
export function getVideoSourceAttributes(path: string | null | undefined): VideoSourceAttributes {
if (!path) {
return { type: 'video/mp4', codecs: 'hvc1' };
return { type: "video/mp4", codecs: "hvc1" }
}
if (/\.webm$/i.test(path)) {
return { type: 'video/webm', codecs: 'av1' };
return { type: "video/webm", codecs: "av1" }
}
if (/\.mp4$/i.test(path) || /\.m4v$/i.test(path)) {
return { type: 'video/mp4', codecs: 'hvc1' };
return { type: "video/mp4", codecs: "hvc1" }
}
if (/\.mov$/i.test(path)) {
return { type: 'video/quicktime', codecs: 'hvc1' };
return { type: "video/quicktime", codecs: "hvc1" }
}
if (/\.avi$/i.test(path)) {
return { type: 'video/x-msvideo', codecs: '' };
return { type: "video/x-msvideo", codecs: "" }
}
if (/\.mkv$/i.test(path)) {
return { type: 'video/x-matroska', codecs: '' };
return { type: "video/x-matroska", codecs: "" }
}
return { type: 'video/mp4', codecs: 'hvc1' };
return { type: "video/mp4", codecs: "hvc1" }
}
/**
* Fetch active roots and their statuses
*/
export async function fetchRoots(): Promise<RootStatus[]> {
const response = await fetch('/api/roots');
const response = await fetch("/api/roots")
if (!response.ok) {
throw new Error(`Failed to load roots: ${response.statusText}`);
throw new Error(`Failed to load roots: ${response.statusText}`)
}
const data = await response.json();
return data.roots || [];
const data = await response.json()
return data.roots || []
}
/**
@@ -101,59 +99,63 @@ export async function fetchRoots(): Promise<RootStatus[]> {
*/
export async function fetchResumePositions(): Promise<Record<string, number>> {
try {
const roots = await fetchRoots();
const merged: Record<string, number> = {};
const roots = await fetchRoots()
const merged: Record<string, number> = {}
await Promise.all(
roots.map(async (root) => {
const response = await fetch(`/api/roots/${encodeURIComponent(root.root_id)}/playback/resume-positions`);
if (!response.ok) return;
const data = await response.json().catch(() => ({}));
const positions = data?.resume_positions;
if (positions && typeof positions === 'object') {
Object.assign(merged, positions);
const response = await fetch(
`/api/roots/${encodeURIComponent(root.root_id)}/playback/resume-positions`,
)
if (!response.ok) return
const data = await response.json().catch(() => ({}))
const positions = data?.resume_positions
if (positions && typeof positions === "object") {
Object.assign(merged, positions)
}
})
);
return merged;
}),
)
return merged
} catch {
return {};
return {}
}
}
/**
* Replace the full root set atomically
*/
export async function replaceRoots(roots: Record<string, string>): Promise<{ accepted: RootStatus[]; failed: unknown[] }> {
const response = await fetch('/api/roots', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
export async function replaceRoots(
roots: Record<string, string>,
): Promise<{ accepted: RootStatus[]; failed: unknown[] }> {
const response = await fetch("/api/roots", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roots }),
});
})
if (!response.ok) {
const err = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(err.detail || response.statusText);
const err = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(err.detail || response.statusText)
}
return response.json();
return response.json()
}
/**
* Play a media file with the system's default player
*/
export async function playMedia(rootId: string, filePath: string): Promise<void> {
const normalizedPath = normalizeMediaPath(filePath);
const normalizedPath = normalizeMediaPath(filePath)
try {
const response = await fetch(`/api/roots/${encodeURIComponent(rootId)}/play`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file_path: normalizedPath }),
});
})
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || response.statusText);
const error = await response.json()
throw new Error(error.detail || response.statusText)
}
} catch (e) {
console.error('Play media error:', e);
alert(`Failed to play media.\n\n${e}`);
console.error("Play media error:", e)
alert(`Failed to play media.\n\n${e}`)
}
}
@@ -161,20 +163,20 @@ export async function playMedia(rootId: string, filePath: string): Promise<void>
* Open a folder in the system file manager
*/
export async function openFolder(rootId: string, folderPath: string): Promise<void> {
const normalizedPath = normalizeMediaPath(folderPath);
const normalizedPath = normalizeMediaPath(folderPath)
try {
const response = await fetch(`/api/roots/${encodeURIComponent(rootId)}/open-folder`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folder_path: normalizedPath }),
});
})
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || response.statusText);
const error = await response.json()
throw new Error(error.detail || response.statusText)
}
} catch (e) {
console.error('Open folder error:', e);
alert(`Failed to open folder.\n\n${e}`);
console.error("Open folder error:", e)
alert(`Failed to open folder.\n\n${e}`)
}
}
@@ -182,11 +184,11 @@ export async function openFolder(rootId: string, folderPath: string): Promise<vo
* Return player integration capabilities for the current OS.
*/
export async function getPlayerStatus(): Promise<PlayerStatus> {
const response = await fetch('/api/player/status');
const response = await fetch("/api/player/status")
if (!response.ok) {
throw new Error(`Failed to load player status: ${response.statusText}`);
throw new Error(`Failed to load player status: ${response.statusText}`)
}
return response.json();
return response.json()
}
/**
@@ -194,12 +196,12 @@ export async function getPlayerStatus(): Promise<PlayerStatus> {
*/
export async function isMpcBeReachable(): Promise<boolean> {
try {
const response = await fetch('/api/mpcbe/status');
if (!response.ok) return false;
const data = await response.json().catch(() => ({}));
return Boolean(data.reachable);
const response = await fetch("/api/mpcbe/status")
if (!response.ok) return false
const data = await response.json().catch(() => ({}))
return Boolean(data.reachable)
} catch {
return false;
return false
}
}
@@ -212,33 +214,36 @@ export async function isMpcBeReachable(): Promise<boolean> {
*/
export function getCoverUrl(coverPath: string | null, rootId?: string | null): string {
if (!coverPath) {
return '';
return ""
}
// Ignore TMDB relative paths (start with /) - these are bugs in the index
if (coverPath.startsWith('/')) {
return '';
if (coverPath.startsWith("/")) {
return ""
}
// Convert relative path to URL path for FastAPI server
// .mediahive/covers/Movies/... -> /api/media/{root_id}/.mediahive/covers/Movies/...
let urlPath = coverPath;
let urlPath = coverPath
// Remove drive letter (Z:) and convert backslashes to forward slashes
if (urlPath.match(/^[A-Za-z]:/)) {
urlPath = urlPath.substring(2);
urlPath = urlPath.substring(2)
}
urlPath = urlPath.replace(/\\/g, '/');
urlPath = urlPath.replace(/\\/g, "/")
// Ensure path starts with /
if (!urlPath.startsWith('/')) {
urlPath = '/' + urlPath;
if (!urlPath.startsWith("/")) {
urlPath = "/" + urlPath
}
// Encode URI components but preserve slashes
const encodedPath = urlPath.split('/').map(segment => encodeURIComponent(segment)).join('/');
const encodedPath = urlPath
.split("/")
.map((segment) => encodeURIComponent(segment))
.join("/")
const rid = rootId || 'unknown';
return `/api/media/${encodeURIComponent(rid)}${encodedPath}`;
const rid = rootId || "unknown"
return `/api/media/${encodeURIComponent(rid)}${encodedPath}`
}
/**
@@ -247,8 +252,8 @@ export function getCoverUrl(coverPath: string | null, rootId?: string | null): s
*/
export async function pickFolderAndAddRoot(): Promise<string | null> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const api = (window as any).pywebview?.api;
if (!api) return null;
const folder: string | null = await api.pick_folder();
return folder;
const api = (window as any).pywebview?.api
if (!api) return null
const folder: string | null = await api.pick_folder()
return folder
}