From 8586bd26e1434ce67c07baba15a1286d77bf5dae Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 24 May 2026 19:01:13 +0000 Subject: [PATCH] Remove PWA/SW and fix blocking startup I/O - Remove vite-plugin-pwa and all service worker code. The app no longer registers a service worker; legacy SWs are unregistered on load. - Move snapshot parsing (thousands of sync Path.exists() calls) into a thread pool via asyncio.to_thread() so the health check responds immediately on startup. - Wrap aiopathlib AsyncPath.iterdir() (sync os.scandir under the hood) in asyncio.to_thread() in scanner and scanning modules so directory walks do not monopolize the event loop. --- frontend/package.json | 1 - frontend/src/main.ts | 11 ++- frontend/src/pwa.ts | 8 -- frontend/src/vite-env.d.ts | 1 - frontend/vite.config.ts | 47 ----------- mediahive/hivescan/scanner.py | 5 +- mediahive/hivescan/scanning.py | 3 +- mediahive/index_store.py | 139 +++++++++++++++++---------------- 8 files changed, 84 insertions(+), 131 deletions(-) delete mode 100644 frontend/src/pwa.ts diff --git a/frontend/package.json b/frontend/package.json index 71a1795..b8a137e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,6 @@ "@vitejs/plugin-vue": "^5.0.0", "typescript": "^5.3.0", "vite": "^5.0.0", - "vite-plugin-pwa": "^1.3.0", "vue-tsc": "^2.0.0" } } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index bc98070..3a350c1 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -11,10 +11,13 @@ installInputModalityTracking() installKeyboardNavigation() installGamepadNavigation() -if ('serviceWorker' in navigator && !navigator.serviceWorker.controller) { - navigator.serviceWorker.addEventListener('controllerchange', () => { - window.location.reload() - }, { once: true }) +// Unregister any legacy service workers — MediaHive no longer uses a PWA/SW. +if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then((registrations) => { + for (const registration of registrations) { + registration.unregister() + } + }) } createApp(App).use(router).mount('#app') diff --git a/frontend/src/pwa.ts b/frontend/src/pwa.ts deleted file mode 100644 index 650e249..0000000 --- a/frontend/src/pwa.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { registerSW } from 'virtual:pwa-register' - -export function registerMediaHivePwa(): void { - const updateServiceWorker = registerSW({ - immediate: true, - onNeedRefresh: () => void updateServiceWorker(true), - }) -} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 0dcaebc..323c78a 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1,5 +1,4 @@ /// -/// declare module '*.vue' { import type { DefineComponent } from 'vue' diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d6ecc5b..2fbc0ee 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,6 +1,5 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; -import { VitePWA } from "vite-plugin-pwa"; import fastapiVue from './vite-plugin-fastapi.js' // https://vitejs.dev/config/ @@ -8,52 +7,6 @@ export default defineConfig(async () => ({ plugins: [ fastapiVue(), vue(), - VitePWA({ - registerType: "autoUpdate", - injectRegister: "script", - manifest: { - id: "/", - name: "MediaHive", - short_name: "MediaHive", - description: "Movies and Series", - start_url: "/", - scope: "/", - display_override: ["window-controls-overlay", "fullscreen", "standalone"], - display: "fullscreen", - background_color: "#0a0a0a", - theme_color: "#0a0a0a", - icons: [ - { - src: "/mediahive-32.webp", - sizes: "32x32", - type: "image/webp" - }, - { - src: "/mediahive.webp", - sizes: "192x192", - type: "image/webp" - } - ] - }, - workbox: { - cleanupOutdatedCaches: true, - clientsClaim: true, - skipWaiting: true, - manifestTransforms: [ - async (entries) => { - const manifest = entries.map((entry) => - entry.url === "index.html" ? { ...entry, url: "/" } : entry - ); - return { manifest, warnings: [] }; - }, - ], - navigateFallback: null, - navigateFallbackDenylist: [/^\/api\//], - }, - devOptions: { - enabled: false, - }, - }), ], // Vite dev server options diff --git a/mediahive/hivescan/scanner.py b/mediahive/hivescan/scanner.py index bd087dc..486a125 100644 --- a/mediahive/hivescan/scanner.py +++ b/mediahive/hivescan/scanner.py @@ -182,7 +182,8 @@ class RootScanner: is_media_container = False try: - for item_async in ap.iterdir(): + entries = await asyncio.to_thread(lambda: list(ap.iterdir())) + for item_async in entries: item = Path(item_async) if item.name.startswith("."): continue @@ -249,7 +250,7 @@ class RootScanner: root_ap = AsyncPath(self.media_root) try: - root_children = list(root_ap.iterdir()) + root_children = await asyncio.to_thread(lambda: list(root_ap.iterdir())) except (OSError, PermissionError): logger.error("Cannot list media root: %s", self.media_root) return downloads diff --git a/mediahive/hivescan/scanning.py b/mediahive/hivescan/scanning.py index 54762c1..70d7183 100644 --- a/mediahive/hivescan/scanning.py +++ b/mediahive/hivescan/scanning.py @@ -174,7 +174,8 @@ async def find_playable_file(path: Path) -> Optional[str]: # Check nested Blu-ray structure (e.g., MovieName/DISC1/BDMV/) try: - for subdir in ap.iterdir(): + entries = await asyncio.to_thread(lambda: list(ap.iterdir())) + for subdir in entries: if await AsyncPath(subdir).is_dir(): nested_bdmv_dir = Path(subdir) / "BDMV" nested_movieobject = nested_bdmv_dir / "MovieObject.bdmv" diff --git a/mediahive/index_store.py b/mediahive/index_store.py index f17cbed..b84af98 100644 --- a/mediahive/index_store.py +++ b/mediahive/index_store.py @@ -86,83 +86,88 @@ class IndexStore: logger.info("No snapshot found at %s, starting fresh", self.snapshot_path) return try: - data = msgspec.json.decode(await ap.read_bytes(), type=IndexSnapshot) - for m in data.movies: - if m.showreel_source_sets: - filtered_source_sets = [] - for source_set in m.showreel_source_sets: - filtered_sources = [ - p - for p in source_set - if ( - Path(self.media_root, p).exists() - if self.media_root - else Path(p).exists() - ) - ] - if filtered_sources: - filtered_source_sets.append(filtered_sources) - m.showreel_source_sets = filtered_source_sets or None - m.showreel_images = ( - [source_set[0] for source_set in filtered_source_sets] - if filtered_source_sets - else None - ) - elif m.showreel_images: - filtered_images = [ + raw = await ap.read_bytes() + await asyncio.to_thread(self._load_snapshot_sync, raw) + except Exception: + logger.exception("Failed to load snapshot from %s", self.snapshot_path) + + def _load_snapshot_sync(self, raw: bytes) -> None: + """Synchronous snapshot parsing (runs in thread pool).""" + data = msgspec.json.decode(raw, type=IndexSnapshot) + for m in data.movies: + if m.showreel_source_sets: + filtered_source_sets = [] + for source_set in m.showreel_source_sets: + filtered_sources = [ p - for p in m.showreel_images + for p in source_set if ( Path(self.media_root, p).exists() if self.media_root else Path(p).exists() ) ] - m.showreel_images = filtered_images or None - m.showreel_source_sets = ( - [[p] for p in filtered_images] if filtered_images else None + if filtered_sources: + filtered_source_sets.append(filtered_sources) + m.showreel_source_sets = filtered_source_sets or None + m.showreel_images = ( + [source_set[0] for source_set in filtered_source_sets] + if filtered_source_sets + else None + ) + elif m.showreel_images: + filtered_images = [ + p + for p in m.showreel_images + if ( + Path(self.media_root, p).exists() + if self.media_root + else Path(p).exists() ) - m.id = self._maybe_migrate_id(m.id) - m.root_id = self.root_id - self.movies[m.id] = m - for s in data.series: - for season in s.seasons: - for ep in season.episodes: - if ep.reel_sources: - filtered_sources = [ - p - for p in ep.reel_sources - if ( - Path(self.media_root, p).exists() - if self.media_root - else Path(p).exists() - ) - ] - ep.reel_sources = filtered_sources or None - ep.reel_image = ( - filtered_sources[0] if filtered_sources else None - ) - elif ep.reel_image: - full = ( - Path(self.media_root, ep.reel_image) + ] + m.showreel_images = filtered_images or None + m.showreel_source_sets = ( + [[p] for p in filtered_images] if filtered_images else None + ) + m.id = self._maybe_migrate_id(m.id) + m.root_id = self.root_id + self.movies[m.id] = m + for s in data.series: + for season in s.seasons: + for ep in season.episodes: + if ep.reel_sources: + filtered_sources = [ + p + for p in ep.reel_sources + if ( + Path(self.media_root, p).exists() if self.media_root - else Path(ep.reel_image) + else Path(p).exists() ) - if full.exists(): - ep.reel_sources = [ep.reel_image] - else: - ep.reel_image = None - ep.reel_sources = None - s.id = self._maybe_migrate_id(s.id) - s.root_id = self.root_id - self.series[s.id] = s - logger.info( - "Loaded snapshot: %d movies, %d series", - len(self.movies), - len(self.series), - ) - except Exception: - logger.exception("Failed to load snapshot from %s", self.snapshot_path) + ] + ep.reel_sources = filtered_sources or None + ep.reel_image = ( + filtered_sources[0] if filtered_sources else None + ) + elif ep.reel_image: + full = ( + Path(self.media_root, ep.reel_image) + if self.media_root + else Path(ep.reel_image) + ) + if full.exists(): + ep.reel_sources = [ep.reel_image] + else: + ep.reel_image = None + ep.reel_sources = None + s.id = self._maybe_migrate_id(s.id) + s.root_id = self.root_id + self.series[s.id] = s + logger.info( + "Loaded snapshot: %d movies, %d series", + len(self.movies), + len(self.series), + ) async def _write_snapshot(self) -> None: """Write current index to disk (called from debounce task)."""