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 @@