From 900fcea638c74a83411d9a29bb815e7eb4c21c41 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 30 May 2026 01:05:27 +0000 Subject: [PATCH] refactor index schema and metadata format and directory tree --- docs/development.md | 8 + docs/multi-index-plan.md | 8 +- frontend/src/App.vue | 16 +- frontend/src/components/CollageHero.vue | 2 +- frontend/src/components/MediaDetail.vue | 21 +- .../src/components/ReleaseVersionCard.vue | 6 +- frontend/src/components/SeriesFullView.vue | 10 +- frontend/src/composables/useMediaWebSocket.ts | 167 +++++++-- frontend/src/composables/useSettings.ts | 6 +- frontend/src/search-worker.ts | 28 +- frontend/src/types.ts | 50 ++- mediahive/hivescan/images.py | 21 +- mediahive/hivescan/indexer.py | 343 ++++++++++++------ mediahive/hivescan/scanner.py | 37 +- mediahive/hivescan/showreel.py | 22 +- mediahive/hivescan/tmdb_client.py | 196 +++++----- mediahive/hivescan/utils.py | 28 +- mediahive/index_store.py | 284 ++++++++++----- mediahive/models/data.py | 42 +-- mediahive/models/events.py | 3 + mediahive/models/protocol.py | 6 +- mediahive/models/tmdb.py | 19 +- mediahive/root_registry.py | 14 +- 23 files changed, 879 insertions(+), 458 deletions(-) diff --git a/docs/development.md b/docs/development.md index 553a852..5dffabf 100644 --- a/docs/development.md +++ b/docs/development.md @@ -40,6 +40,14 @@ uv run --extra gui python -m mediahive.winmain /path/to/media/folder This launches the same pywebview-based desktop flow used by the Windows build. +## Migrate Existing Index Snapshots + +```bash +uv run python scripts/indexmigr.py /path/to/media/root --write +``` + +This applies versioned snapshot migrations to `.mediahive/index.json` outside the main application. Use it before starting a newer build against an older index. + ## Notes - The selected media folder is scanned continuously by the backend. diff --git a/docs/multi-index-plan.md b/docs/multi-index-plan.md index 30a1b19..06a2c17 100644 --- a/docs/multi-index-plan.md +++ b/docs/multi-index-plan.md @@ -35,9 +35,11 @@ Each active root gets an isolated `RootContext` managed by the `Supervisor`: ### Item IDs -Every `Movie.id` and `Series.id` is namespaced with its `root_id`: -- Format: `{root_id}:{content_hash}` -- Old snapshots are auto-migrated on load: IDs lacking the prefix get it prepended. +`root_id` is stored separately on each item. + +- `Movie.id` uses a slug built from the movie title and year, for example `spider-man-no-way-home-2021`. +- `Series.id` uses a slug built from the series title, for example `lost`. +- Legacy snapshot migrations are handled by `scripts/indexmigr.py`, not during app startup. ## API diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a518b92..2042068 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -176,7 +176,9 @@ import { ref, computed, onMounted, onUnmounted, watch } from "vue" import { useRouter, useRoute } from "vue-router" import type { Movie, + MovieUi, Series, + SeriesUi, MediaItem, EpisodeWithSeries, TaskInfo, @@ -732,7 +734,7 @@ function showDetail(item: MediaItem) { if (item.type === "episode") { // For episodes, play directly if possible, otherwise show the series const epData = item.data as EpisodeWithSeries - const playableFile = Object.values(epData.episode.torrents || {})[0]?.playable_file + const playableFile = Object.values(epData.episode.files || {})[0]?.playable_file if (playableFile) { handlePlay(playableFile) } else { @@ -800,10 +802,10 @@ function focusDetailEntryTarget(item: MediaItem): boolean { } // Convert raw data to MediaItem format -function movieToMediaItem(movie: Movie): MediaItem { +function movieToMediaItem(movie: MovieUi): MediaItem { // Get resolution from first torrent if available - const torrents = Object.values(movie.torrents || {}) - const resolution = torrents.length > 0 ? torrents[0].resolution : null + const files = Object.values(movie.files || {}) + const resolution = files.length > 0 ? files[0].resolution : null return { id: movie.id, @@ -819,7 +821,7 @@ function movieToMediaItem(movie: Movie): MediaItem { } } -function seriesToMediaItem(series: Series): MediaItem { +function seriesToMediaItem(series: SeriesUi): MediaItem { // For series, collect reel images from all episodes const reelImages: string[] = [] const reelSourceSets: string[][] = [] @@ -1249,7 +1251,7 @@ function reloadPage() { function findRootIdForPath(filePath: string): string | null { if (!mediaIndex.value) return null for (const movie of mediaIndex.value.movies) { - for (const torrent of Object.values(movie.torrents || {})) { + for (const torrent of Object.values(movie.files || {})) { if (torrent.playable_file === filePath) { return torrent.root_id || movie.root_id } @@ -1258,7 +1260,7 @@ function findRootIdForPath(filePath: string): string | null { for (const series of mediaIndex.value.series) { for (const season of series.seasons || []) { for (const episode of season.episodes || []) { - for (const torrent of Object.values(episode.torrents || {})) { + for (const torrent of Object.values(episode.files || {})) { if (torrent.playable_file === filePath) { return torrent.root_id || series.root_id } diff --git a/frontend/src/components/CollageHero.vue b/frontend/src/components/CollageHero.vue index 23f3397..f88f1e6 100644 --- a/frontend/src/components/CollageHero.vue +++ b/frontend/src/components/CollageHero.vue @@ -570,7 +570,7 @@ function getRatingClass(item: MediaItem): string { function getResolution(item: MediaItem): string | null { if (item.type === "movies") { const movie = item.data as Movie - return Object.values(movie.torrents || {})[0]?.resolution ?? null + return Object.values(movie.files || {})[0]?.resolution ?? null } return null } diff --git a/frontend/src/components/MediaDetail.vue b/frontend/src/components/MediaDetail.vue index 4ae9e05..ab18f2b 100644 --- a/frontend/src/components/MediaDetail.vue +++ b/frontend/src/components/MediaDetail.vue @@ -124,7 +124,7 @@ > @@ -202,7 +202,7 @@