frontend: add OXC lint/format setup and apply semicolon fixes
This commit is contained in:
+100
-95
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user