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)."""