Rework settings dialog: floating window, integrated activity log and scan progress, auto-update controls
This commit is contained in:
@@ -11,6 +11,9 @@ All media paths are scoped to a **root**, identified by a friendly `root_id`
|
||||
| `GET` | `/api/health` | Lightweight health check. |
|
||||
| `GET` | `/api/config` | Returns the current root configuration. |
|
||||
| `PUT` | `/api/config/roots` | Atomically replace the full root set. Returns `{ "status": "ok", "accepted": [{path, root_id}], "failed": [...] }`. |
|
||||
| `GET` | `/api/update` | Returns `{ "version", "auto_update", "pending_version" }` — installed version, auto-update preference, and any downloaded update staged for the next launch (Velopack GUI builds only; `null` elsewhere). |
|
||||
| `PUT` | `/api/config/auto-update` | Enable/disable automatic update downloads. Body `{ "enabled": bool }`, persisted in config. |
|
||||
| `POST` | `/api/update/restart` | Applies the staged update and restarts into it. `404` when no update is pending. |
|
||||
| `POST` | `/api/play/{root_id}` | Opens a media file with a media player. Also starts an assumed-playback session (see notes). |
|
||||
| `GET` | `/api/players` | Lists detected media players. Returns `{ "players": [{id, name, family, path}] }`, including synthetic `default` and `custom` entries. |
|
||||
| `POST` | `/api/activity` | Reports user input activity; finalizes any assumed-playback session. Returns `{ "status": "ok", "finalized": bool }`. |
|
||||
|
||||
+16
-145
@@ -45,6 +45,9 @@
|
||||
:current-view="headerCurrentView"
|
||||
:search-query="searchQuery"
|
||||
:roots="headerRoots"
|
||||
:scan-tasks="tasks"
|
||||
:scan-connected="wsConnected"
|
||||
:initial-scan-mode="isInitialScanMode"
|
||||
:mpc-be-connected="mpcBeConnected"
|
||||
:nav-row="1"
|
||||
:position="headerPosition"
|
||||
@@ -209,7 +212,6 @@ import type {
|
||||
SeriesUi,
|
||||
MediaItem,
|
||||
EpisodeWithSeries,
|
||||
TaskInfo,
|
||||
SeriesResumePoint,
|
||||
} from "./types"
|
||||
import {
|
||||
@@ -228,6 +230,8 @@ import {
|
||||
} from "./composables/useKeyboardNavigation"
|
||||
import type { SyncedRowScrollSnapshot } from "./composables/useKeyboardNavigation"
|
||||
import { useMediaWebSocket } from "./composables/useMediaWebSocket"
|
||||
import { computeProgressRoots, type RootTaskInfo } from "./composables/useScanProgress"
|
||||
import { useSettingsOpen } from "./composables/useSettingsOpen"
|
||||
import Header from "./components/Header.vue"
|
||||
import CollageHero from "./components/CollageHero.vue"
|
||||
import MediaRow from "./components/MediaRow.vue"
|
||||
@@ -292,20 +296,6 @@ const {
|
||||
roots: rootStatuses,
|
||||
} = useMediaWebSocket()
|
||||
|
||||
type RootTaskInfo = TaskInfo & { root_id: string }
|
||||
|
||||
interface ProgressRootState {
|
||||
rootId: string
|
||||
rootLabel: string
|
||||
scanTarget: string | null
|
||||
phaseLabel: string
|
||||
phaseDetail: string | null
|
||||
progressPercent: number
|
||||
progressLabel: string | null
|
||||
isDeterminate: boolean
|
||||
toneClass: string
|
||||
}
|
||||
|
||||
const activeTasks = computed<RootTaskInfo[]>(() => Array.from(tasks.value.values()))
|
||||
|
||||
function getRootName(rootId: string | null | undefined): string | null {
|
||||
@@ -313,132 +303,15 @@ function getRootName(rootId: string | null | undefined): string | null {
|
||||
return rootStatuses.value.get(rootId)?.root_id || null
|
||||
}
|
||||
|
||||
function normalizePosixPath(value: string): string {
|
||||
return value.replace(/\\/g, "/")
|
||||
}
|
||||
const progressRoots = computed(() =>
|
||||
computeProgressRoots(
|
||||
activeTasks.value,
|
||||
(rootId) => rootStatuses.value.get(rootId)?.path || null,
|
||||
isInitialScanMode.value,
|
||||
),
|
||||
)
|
||||
|
||||
function extractScanPath(detail: string): string | null {
|
||||
if (!detail.startsWith("Scanning:")) return null
|
||||
let value = detail.replace(/^Scanning:\s*/i, "").trim()
|
||||
value = value.replace(/\s*\(\d+\s+found\)\s*$/i, "").trim()
|
||||
return value || null
|
||||
}
|
||||
|
||||
function buildScanTarget(rootId: string, rootPath: string | null, detail: string): string | null {
|
||||
const rawPath = extractScanPath(detail)
|
||||
if (!rawPath) return null
|
||||
|
||||
const posixRaw = normalizePosixPath(rawPath)
|
||||
const posixRoot = rootPath ? normalizePosixPath(rootPath) : null
|
||||
|
||||
let relative = posixRaw
|
||||
if (posixRoot) {
|
||||
const lowRaw = posixRaw.toLowerCase()
|
||||
const lowRoot = posixRoot.toLowerCase()
|
||||
if (lowRaw === lowRoot) {
|
||||
relative = ""
|
||||
} else if (lowRaw.startsWith(`${lowRoot}/`)) {
|
||||
relative = posixRaw.slice(posixRoot.length).replace(/^\/+/, "")
|
||||
}
|
||||
}
|
||||
|
||||
if (!relative) return rootId
|
||||
if (relative.toLowerCase().startsWith(`${rootId.toLowerCase()}/`)) return relative
|
||||
return `${rootId}/${relative}`
|
||||
}
|
||||
|
||||
function describeRootProgress(
|
||||
rootId: string,
|
||||
rootPath: string | null,
|
||||
tasksForRoot: RootTaskInfo[],
|
||||
isInitialScanMode: boolean,
|
||||
): ProgressRootState | null {
|
||||
const running = tasksForRoot.filter((task) => task.status === "running")
|
||||
const latestError = [...tasksForRoot].reverse().find((task) => task.status === "error") || null
|
||||
|
||||
if (running.length === 0 && !latestError) return null
|
||||
|
||||
const scanTask = running.find((task) => task.id.startsWith("scan-")) || null
|
||||
const showreelCount = running.filter((task) => task.id.startsWith("showreel-")).length
|
||||
const otherRunningCount = running.length - (scanTask ? 1 : 0) - showreelCount
|
||||
|
||||
let phaseLabel = "Processing media"
|
||||
let phaseDetail: string | null = null
|
||||
let scanTarget: string | null = null
|
||||
let isDeterminate = false
|
||||
let progressPercent = 0
|
||||
let progressLabel: string | null = null
|
||||
let toneClass = ""
|
||||
|
||||
if (scanTask) {
|
||||
const detail = (scanTask.detail || "").trim()
|
||||
scanTarget = buildScanTarget(rootId, rootPath, detail)
|
||||
if (detail.startsWith("Scanning:")) {
|
||||
phaseLabel = isInitialScanMode ? "Scanning folders" : "Checking for updates"
|
||||
phaseDetail = isInitialScanMode ? "Looking for new files" : "Running background scan"
|
||||
} else if (/^Processing\s+\d+\s+(items|movies|series)/i.test(detail)) {
|
||||
phaseLabel = "Preparing titles"
|
||||
phaseDetail = "Matching files and grouping releases"
|
||||
} else {
|
||||
phaseLabel = "Fetching metadata"
|
||||
phaseDetail = detail ? `Current title: ${detail}` : "Updating titles and artwork"
|
||||
}
|
||||
if (scanTask.progress > 0 && scanTask.progress <= 1) {
|
||||
isDeterminate = true
|
||||
progressPercent = Math.max(1, Math.round(scanTask.progress * 100))
|
||||
progressLabel = `${progressPercent}%`
|
||||
}
|
||||
} else if (showreelCount > 0) {
|
||||
phaseLabel = "Generating previews"
|
||||
phaseDetail =
|
||||
showreelCount === 1 ? "Building 1 preview reel" : `Building ${showreelCount} preview reels`
|
||||
} else if (otherRunningCount > 0) {
|
||||
phaseLabel = "Finalizing updates"
|
||||
phaseDetail = "Applying library changes"
|
||||
} else if (latestError) {
|
||||
phaseLabel = "Needs attention"
|
||||
phaseDetail = latestError.detail || "A background task failed"
|
||||
toneClass = "activity-root-error"
|
||||
}
|
||||
|
||||
if (showreelCount > 0 && scanTask) {
|
||||
phaseDetail = phaseDetail
|
||||
? `${phaseDetail}. Preview generation is running in parallel.`
|
||||
: "Preview generation is running in parallel"
|
||||
}
|
||||
|
||||
return {
|
||||
rootId,
|
||||
rootLabel: rootId,
|
||||
scanTarget,
|
||||
phaseLabel,
|
||||
phaseDetail,
|
||||
progressPercent,
|
||||
progressLabel,
|
||||
isDeterminate,
|
||||
toneClass,
|
||||
}
|
||||
}
|
||||
|
||||
const progressRoots = computed<ProgressRootState[]>(() => {
|
||||
const byRoot = new Map<string, RootTaskInfo[]>()
|
||||
for (const task of activeTasks.value) {
|
||||
const list = byRoot.get(task.root_id) || []
|
||||
list.push(task)
|
||||
byRoot.set(task.root_id, list)
|
||||
}
|
||||
|
||||
const rows: ProgressRootState[] = []
|
||||
const initial = isInitialScanMode.value
|
||||
for (const [rootId, rootTasks] of byRoot) {
|
||||
const rootPath = rootStatuses.value.get(rootId)?.path || null
|
||||
const row = describeRootProgress(rootId, rootPath, rootTasks, initial)
|
||||
if (row) rows.push(row)
|
||||
}
|
||||
return rows.sort((a, b) => a.rootLabel.localeCompare(b.rootLabel))
|
||||
})
|
||||
|
||||
const isSettingsView = computed(() => route.path === "/settings")
|
||||
const isSettingsView = useSettingsOpen()
|
||||
const hasLibraryItems = computed(() => {
|
||||
if (!mediaIndex.value) return false
|
||||
return mediaIndex.value.movies.length > 0 || mediaIndex.value.series.length > 0
|
||||
@@ -453,10 +326,8 @@ const headerRoots = computed(() =>
|
||||
)
|
||||
|
||||
const showProgressPanel = computed(() => {
|
||||
if (isInitialScanMode.value) {
|
||||
return !wsConnected.value || progressRoots.value.length > 0
|
||||
}
|
||||
if (!isSettingsView.value) return false
|
||||
if (isSettingsView.value) return false
|
||||
if (!isInitialScanMode.value) return false
|
||||
return !wsConnected.value || progressRoots.value.length > 0
|
||||
})
|
||||
|
||||
@@ -1075,7 +946,7 @@ const activePanelScrollTop = computed(() => {
|
||||
})
|
||||
|
||||
const headerStyle = computed(() => {
|
||||
if (route.path === "/settings") {
|
||||
if (isSettingsView.value) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
|
||||
+32
-3
@@ -426,13 +426,42 @@ export function getCoverUrl(coverPath: string | null, rootId?: string | null): s
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the native OS folder picker via pywebview, then add the selected
|
||||
* folder to the server's root list. Only works inside the packaged desktop app.
|
||||
* Invoke the native OS folder picker via pywebview.
|
||||
* Only works inside the packaged desktop app; returns null elsewhere.
|
||||
*/
|
||||
export async function pickFolderAndAddRoot(): Promise<string | null> {
|
||||
export async function pickFolder(): 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
|
||||
}
|
||||
|
||||
export interface UpdateStatus {
|
||||
version: string
|
||||
auto_update: boolean
|
||||
pending_version: string | null
|
||||
}
|
||||
|
||||
/** Fetch version, auto-update preference, and any downloaded pending update. */
|
||||
export async function fetchUpdateStatus(): Promise<UpdateStatus> {
|
||||
const response = await fetch("/api/update")
|
||||
if (!response.ok) throw new Error(`Failed to fetch update status: ${response.status}`)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/** Enable or disable automatic update downloads (persisted server-side). */
|
||||
export async function setAutoUpdate(enabled: boolean): Promise<void> {
|
||||
const response = await fetch("/api/config/auto-update", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled }),
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to save auto-update setting: ${response.status}`)
|
||||
}
|
||||
|
||||
/** Apply a downloaded update and restart the app into it. */
|
||||
export async function restartForUpdate(): Promise<void> {
|
||||
const response = await fetch("/api/update/restart", { method: "POST" })
|
||||
if (!response.ok) throw new Error(`Failed to restart for update: ${response.status}`)
|
||||
}
|
||||
|
||||
+637
-258
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
import type { TaskInfo } from "../types"
|
||||
|
||||
export type RootTaskInfo = TaskInfo & { root_id: string }
|
||||
|
||||
export interface ProgressRootState {
|
||||
rootId: string
|
||||
rootLabel: string
|
||||
scanTarget: string | null
|
||||
phaseLabel: string
|
||||
phaseDetail: string | null
|
||||
progressPercent: number
|
||||
progressLabel: string | null
|
||||
isDeterminate: boolean
|
||||
toneClass: string
|
||||
}
|
||||
|
||||
function normalizePosixPath(value: string): string {
|
||||
return value.replace(/\\/g, "/")
|
||||
}
|
||||
|
||||
function extractScanPath(detail: string): string | null {
|
||||
if (!detail.startsWith("Scanning:")) return null
|
||||
let value = detail.replace(/^Scanning:\s*/i, "").trim()
|
||||
value = value.replace(/\s*\(\d+\s+found\)\s*$/i, "").trim()
|
||||
return value || null
|
||||
}
|
||||
|
||||
function buildScanTarget(rootPath: string | null, detail: string): string | null {
|
||||
const rawPath = extractScanPath(detail)
|
||||
if (!rawPath) return null
|
||||
|
||||
const posixRaw = normalizePosixPath(rawPath)
|
||||
const posixRoot = rootPath ? normalizePosixPath(rootPath) : null
|
||||
|
||||
if (posixRoot) {
|
||||
const lowRaw = posixRaw.toLowerCase()
|
||||
const lowRoot = posixRoot.toLowerCase()
|
||||
if (lowRaw === lowRoot) return posixRoot
|
||||
if (lowRaw.startsWith(`${lowRoot}/`)) {
|
||||
return posixRaw.slice(posixRoot.length).replace(/^\/+/, "")
|
||||
}
|
||||
}
|
||||
return posixRaw
|
||||
}
|
||||
|
||||
function describeRootProgress(
|
||||
rootId: string,
|
||||
rootPath: string | null,
|
||||
tasksForRoot: RootTaskInfo[],
|
||||
isInitialScanMode: boolean,
|
||||
): ProgressRootState | null {
|
||||
const running = tasksForRoot.filter((task) => task.status === "running")
|
||||
const latestError = [...tasksForRoot].reverse().find((task) => task.status === "error") || null
|
||||
|
||||
if (running.length === 0 && !latestError) return null
|
||||
|
||||
const scanTask = running.find((task) => task.id.startsWith("scan-")) || null
|
||||
const showreelCount = running.filter((task) => task.id.startsWith("showreel-")).length
|
||||
const otherRunningCount = running.length - (scanTask ? 1 : 0) - showreelCount
|
||||
|
||||
let phaseLabel = "Processing media"
|
||||
let phaseDetail: string | null = null
|
||||
let scanTarget: string | null = null
|
||||
let isDeterminate = false
|
||||
let progressPercent = 0
|
||||
let progressLabel: string | null = null
|
||||
let toneClass = ""
|
||||
|
||||
if (scanTask) {
|
||||
const detail = (scanTask.detail || "").trim()
|
||||
scanTarget = buildScanTarget(rootPath, detail)
|
||||
if (detail.startsWith("Scanning:")) {
|
||||
phaseLabel = isInitialScanMode ? "Scanning folders" : "Checking for updates"
|
||||
} else if (/^Processing\s+\d+\s+(items|movies|series)/i.test(detail)) {
|
||||
phaseLabel = "Preparing titles"
|
||||
} else if (/^(Starting scan|No new items|Done|Scan cancelled)/i.test(detail)) {
|
||||
phaseLabel = isInitialScanMode ? "Scanning folders" : "Checking for updates"
|
||||
} else {
|
||||
phaseLabel = "Fetching metadata"
|
||||
phaseDetail = detail || null
|
||||
}
|
||||
if (scanTask.progress > 0 && scanTask.progress <= 1) {
|
||||
isDeterminate = true
|
||||
progressPercent = Math.max(1, Math.round(scanTask.progress * 100))
|
||||
progressLabel = `${progressPercent}%`
|
||||
}
|
||||
} else if (showreelCount > 0) {
|
||||
phaseLabel = "Generating previews"
|
||||
} else if (otherRunningCount > 0) {
|
||||
phaseLabel = "Finalizing updates"
|
||||
} else if (latestError) {
|
||||
phaseLabel = "Needs attention"
|
||||
phaseDetail = latestError.detail || "A background task failed"
|
||||
toneClass = "activity-root-error"
|
||||
}
|
||||
|
||||
return {
|
||||
rootId,
|
||||
rootLabel: rootId,
|
||||
scanTarget,
|
||||
phaseLabel,
|
||||
phaseDetail,
|
||||
progressPercent,
|
||||
progressLabel,
|
||||
isDeterminate,
|
||||
toneClass,
|
||||
}
|
||||
}
|
||||
|
||||
export function computeProgressRoots(
|
||||
tasks: Iterable<RootTaskInfo>,
|
||||
getRootPath: (rootId: string) => string | null,
|
||||
isInitialScanMode: boolean,
|
||||
): ProgressRootState[] {
|
||||
const byRoot = new Map<string, RootTaskInfo[]>()
|
||||
for (const task of tasks) {
|
||||
const list = byRoot.get(task.root_id) || []
|
||||
list.push(task)
|
||||
byRoot.set(task.root_id, list)
|
||||
}
|
||||
|
||||
const rows: ProgressRootState[] = []
|
||||
for (const [rootId, rootTasks] of byRoot) {
|
||||
const row = describeRootProgress(rootId, getRootPath(rootId), rootTasks, isInitialScanMode)
|
||||
if (row) rows.push(row)
|
||||
}
|
||||
return rows.sort((a, b) => a.rootLabel.localeCompare(b.rootLabel))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
// Settings is an overlay, not a route: opening it must not change the URL or
|
||||
// the view behind it, so the open state is plain shared local state.
|
||||
const settingsOpen = ref(false)
|
||||
|
||||
export function useSettingsOpen() {
|
||||
return settingsOpen
|
||||
}
|
||||
@@ -37,9 +37,9 @@ const router = createRouter({
|
||||
component: EmptyRouteComponent,
|
||||
},
|
||||
{
|
||||
// Settings is now an overlay with no URL of its own; keep old links working.
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
component: EmptyRouteComponent,
|
||||
redirect: "/movies",
|
||||
},
|
||||
{
|
||||
path: "/series",
|
||||
|
||||
@@ -19,6 +19,7 @@ from platformdirs import user_config_path, user_log_path
|
||||
|
||||
class Config(msgspec.Struct, omit_defaults=True):
|
||||
roots: dict[str, str] | None = None
|
||||
auto_update: bool = True
|
||||
|
||||
|
||||
# Runtime config shared between the CLI entrypoint and the server process via
|
||||
|
||||
+43
-1
@@ -29,6 +29,7 @@ from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
import msgspec
|
||||
import msgspec.structs
|
||||
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import (
|
||||
@@ -39,7 +40,8 @@ from fastapi.responses import (
|
||||
)
|
||||
from fastapi_vue import Frontend, env
|
||||
|
||||
from mediahive.config import config, load_config, log_dir
|
||||
from mediahive import updater
|
||||
from mediahive.config import config, load_config, log_dir, save_config
|
||||
from mediahive.hivescan.images import close_image_client
|
||||
from mediahive.hivescan.scanner import RootScanner
|
||||
from mediahive.hivescan.tmdb_client import close_http_client
|
||||
@@ -1079,6 +1081,46 @@ async def get_version():
|
||||
return {"version": version}
|
||||
|
||||
|
||||
# --- Auto-update (Velopack, GUI builds only) ---
|
||||
|
||||
|
||||
@app.get("/api/update")
|
||||
async def get_update_status():
|
||||
"""Version, auto-update preference, and any staged (downloaded) update."""
|
||||
version = (await get_version())["version"]
|
||||
cfg = load_config()
|
||||
pending = await asyncio.to_thread(updater.pending_update)
|
||||
return {
|
||||
"version": version,
|
||||
"auto_update": cfg.auto_update,
|
||||
"pending_version": pending,
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/config/auto-update")
|
||||
async def put_auto_update(request: Request):
|
||||
"""Enable/disable automatic update downloads (persisted in config)."""
|
||||
body = msgspec.json.decode(await request.body())
|
||||
enabled = bool(body.get("enabled", True))
|
||||
cfg = load_config()
|
||||
save_config(msgspec.structs.replace(cfg, auto_update=enabled))
|
||||
with suppress(AttributeError, TypeError):
|
||||
config.auto_update = enabled
|
||||
if enabled:
|
||||
# Catch up on anything missed while updates were disabled.
|
||||
asyncio.create_task(asyncio.to_thread(updater.check_and_download))
|
||||
return {"auto_update": enabled}
|
||||
|
||||
|
||||
@app.post("/api/update/restart")
|
||||
async def restart_for_update():
|
||||
"""Apply the staged update and restart into it (never returns on success)."""
|
||||
applied = await asyncio.to_thread(updater.apply_pending_and_restart)
|
||||
if not applied:
|
||||
raise HTTPException(status_code=404, detail="No downloaded update to apply")
|
||||
return {"status": "restarting"}
|
||||
|
||||
|
||||
def _read_log() -> str:
|
||||
"""Return the full application log file."""
|
||||
path = log_dir() / "mediahive.log"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Optional Velopack auto-update integration (GUI builds only).
|
||||
|
||||
In development and portable-ZIP runs Velopack is either not installed or the
|
||||
app is not a Velopack installation; every helper degrades to a no-op then, so
|
||||
callers never need to special-case those environments.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from mediahive.config import load_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VELOPACK_REPO_URL = "https://git.zi.fi/LeoVasanko/mediahive"
|
||||
|
||||
|
||||
def _manager():
|
||||
"""Return a Velopack UpdateManager, or None when updates are unavailable."""
|
||||
try:
|
||||
import velopack
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
return velopack.UpdateManager(velopack.GiteaSource(VELOPACK_REPO_URL))
|
||||
except RuntimeError, OSError:
|
||||
# Not a Velopack installation (dev / portable run).
|
||||
return None
|
||||
|
||||
|
||||
def pending_update() -> str | None:
|
||||
"""Version of a downloaded update staged for the next launch, if any."""
|
||||
mgr = _manager()
|
||||
if mgr is None:
|
||||
return None
|
||||
try:
|
||||
asset = mgr.get_update_pending_restart()
|
||||
except RuntimeError, OSError:
|
||||
return None
|
||||
return str(asset.Version) if asset is not None else None
|
||||
|
||||
|
||||
def apply_pending_and_restart() -> bool:
|
||||
"""Apply the staged update and restart into it. False when nothing pending."""
|
||||
mgr = _manager()
|
||||
if mgr is None:
|
||||
return False
|
||||
try:
|
||||
asset = mgr.get_update_pending_restart()
|
||||
if asset is None:
|
||||
return False
|
||||
logger.info("Velopack: applying staged update %s and restarting", asset.Version)
|
||||
mgr.apply_updates_and_restart(asset)
|
||||
except (RuntimeError, OSError) as exc:
|
||||
logger.warning("Velopack: failed to apply staged update: %s", exc)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_and_download() -> None:
|
||||
"""Download available updates in the background, unless disabled in config.
|
||||
|
||||
Downloaded updates are applied automatically by Velopack on the next app
|
||||
start, so the running session is never interrupted. Network failures and
|
||||
non-Velopack runs are expected and skipped quietly.
|
||||
"""
|
||||
if not load_config().auto_update:
|
||||
logger.info("Velopack: automatic updates disabled, skipping check")
|
||||
return
|
||||
mgr = _manager()
|
||||
if mgr is None:
|
||||
return
|
||||
try:
|
||||
info = mgr.check_for_updates()
|
||||
if info is None:
|
||||
logger.info("Velopack: no update available")
|
||||
return
|
||||
version = info.TargetFullRelease.Version
|
||||
logger.info("Velopack: downloading update %s", version)
|
||||
mgr.download_updates(info)
|
||||
logger.info("Velopack: update %s staged, applies on next launch", version)
|
||||
except (RuntimeError, OSError) as exc:
|
||||
logger.info("Velopack update check skipped: %s", exc)
|
||||
+3
-14
@@ -37,6 +37,7 @@ from fastapi_vue.logging import patch_log_config
|
||||
from fastapi_vue.startupbox import print_box
|
||||
from tracerite.html import html_traceback
|
||||
|
||||
from mediahive import updater
|
||||
from mediahive.config import config, load_config, log_dir, save_config
|
||||
from mediahive.volume_control import get_volume, set_volume, volume_max
|
||||
|
||||
@@ -48,7 +49,6 @@ HEALTH_TIMEOUT = 2 # seconds
|
||||
BACKEND_HEALTH_REQUEST_TIMEOUT = 2 # seconds
|
||||
BACKEND_HEALTH_POLL_SECONDS = 0.25
|
||||
MPC_BE_URL = "http://127.0.0.1:13579"
|
||||
VELOPACK_REPO_URL = "https://git.zi.fi/LeoVasanko/mediahive"
|
||||
GAMEPAD_REPEAT_SECONDS = 0.008
|
||||
GAMEPAD_POLL_SECONDS = 0.008
|
||||
MPC_BE_FRAME_REPEAT_SECONDS = 0.016
|
||||
@@ -989,25 +989,14 @@ def _velopack_startup() -> None:
|
||||
|
||||
|
||||
def _check_for_updates() -> None:
|
||||
"""Download available updates in the background.
|
||||
"""Download available updates in the background (unless disabled in config).
|
||||
|
||||
Downloaded updates are applied automatically by Velopack on the next app
|
||||
start (via _velopack_startup), so the running session is never
|
||||
interrupted. Not a Velopack install (dev/portable) and network failures
|
||||
are expected and skipped quietly.
|
||||
"""
|
||||
try:
|
||||
mgr = velopack.UpdateManager(velopack.GiteaSource(VELOPACK_REPO_URL))
|
||||
info = mgr.check_for_updates()
|
||||
if info is None:
|
||||
logger.info("Velopack: no update available")
|
||||
return
|
||||
version = info.TargetFullRelease.Version
|
||||
logger.info("Velopack: downloading update %s", version)
|
||||
mgr.download_updates(info)
|
||||
logger.info("Velopack: update %s staged, applies on next launch", version)
|
||||
except (RuntimeError, OSError) as exc:
|
||||
logger.info("Velopack update check skipped: %s", exc)
|
||||
updater.check_and_download()
|
||||
|
||||
|
||||
def gui_main() -> None:
|
||||
|
||||
Reference in New Issue
Block a user