When no filters are selected, doom scroll now recursively fetches only
items under the current directory instead of the entire library root.
Navigating to a new directory invalidates the cached listing. Filter-
based doom scroll (search or tags) continues to search library-wide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add library_ai_settings table with migration for per-library overrides
- Extend AiConfig with editable prompt parts for description, tagging,
extraction, and translation steps; defaults match previous hardcoded values
- Add getEffectiveAiConfig(libraryId) that merges global settings with
library-level overrides (empty override falls through to global)
- Update all ai-tagger functions to use getEffectiveAiConfig and build
prompts from configurable parts
- Add GET/PUT /api/ai-settings/library/[id] for per-library overrides
- Update /api/ai-settings GET/PUT to include prompt fields
- Add Prompts section and Library Overrides section to admin UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds automatic image tagging that runs as a post-scan phase, sending
thumbnails to an OpenAI-compatible vision API and applying matching
tags from the user-defined tag vocabulary.
- New ai-tagger module with batch processing, failure tolerance, and
tag validation against existing vocabulary
- Admin settings page (Manage > AI Tagging) for endpoint, model, and
enable toggle with connection testing
- DB migration for ai_tagged_at tracking column and AI config seeds
- Re-tag All support to queue items for reprocessing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New /api/game-screenshots route handles GET (list), POST (upload), and DELETE (remove single file)
- Screenshots stored in a screenshots/ subfolder inside each game directory
- Images converted to JPEG via Sharp on upload, named shot-{timestamp}.jpg
- Modal shows a horizontal scrollable strip of 16:9 thumbnail tiles
- Hover a tile to reveal a delete button; uploading placeholders appear per in-flight upload
- Dashed + tile triggers multi-file picker for batch uploads
- Click any thumbnail to open a full-screen lightbox with prev/next arrows, counter, and keyboard nav (←/→/Esc)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Detect Windows (.zip), Linux (.tar.gz), and macOS (.dmg / .app bundle) game archives during scan
- Store GameFile[] with platform metadata in DB instead of plain zipFiles[]
- Stream .app bundles as on-the-fly zip archives via archiver
- Show WIN/LIN/MAC platform badge pills on GameCard and SeriesCard
- Auto-select the download matching the user's OS in GameDetailModal
- Persist cover URL to DB immediately on upload (no re-scan needed)
- Backward-compatible: legacy zipFiles entries map to platform 'windows'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Formats like .mkv, .avi, .wmv, .ts, .m2ts and .tiff are not natively
supported by browsers and would stall silently in Doom Scroll mode.
Add src/lib/browser-media.ts with BROWSER_VIDEO_EXTENSIONS (.mp4,
.webm, .mov, .m4v), BROWSER_IMAGE_EXTENSIONS (.jpg, .jpeg, .png, .gif,
.webp, .bmp), and an isBrowserPlayable() helper that extracts the
extension without importing Node's path module.
Filter doomScrollItems in MixedView, MoviesView, and TvView using this
helper so only natively renderable files are passed to DoomScrollView.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When history hit the 100-entry cap, setHistory sliced the array back to
indices 0–99 but setHistoryIndex still returned idx + 1 = 100, making
current = history[100] = undefined. Nothing rendered and no API calls
were made until the user went back (decrementing to index 99, which
held the newly-picked item).
Fix: cap the returned historyIndex at HISTORY_CAP - 1 so it always
points to a valid entry in the sliced array. Extract HISTORY_CAP = 100
as a named constant so the slice and the index cap stay in sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
media_key was a lossy shortening of item_key (libraryId:lastSegment) that
introduced a real collision bug: two TV episodes from different series with
the same filename would share the same media_key and each other's tags.
- DB migration converts existing media_tags rows from short format to full
item_key by joining against media_items; ambiguous/orphaned rows are dropped
- media_tags column renamed media_key → item_key
- Removed itemKeyToMediaKey() from scanner; reconcileAndPrune now passes
item_key directly to reKeyMediaItem
- DB reader functions (tv, movies, games) now expose item_key on returned
entities; frontend components use entity.item_key instead of constructing
the short libraryId:id form
- MixedView now constructs the full mixed_file: item_key format
- Tag API renamed mediaKey param → itemKey throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TV library fix: the unfiltered Doom Scroll path was calling
/api/browse which explicitly rejects non-mixed libraries with a 400
error, leaving the item list empty. Replace it with the same TV API
hierarchy fetch already used by the filtered path (series → seasons →
episodes), matching how the rest of the TV library is loaded.
Autoplay fix (all library types): two interacting bugs caused videos
to silently stall on navigation. First, the play/pause effect had
current?.url in its deps, so navigating while paused would call
pause() on the freshly-mounted video element before the isPaused reset
could take effect. Second, browser autoplay policy blocks unmuted
play() calls and the rejection was silently swallowed with no recovery.
Fix by merging the isPaused reset and the play() call into one
navigation effect, and adding a muted fallback on rejection so playback
always starts — the user can unmute manually afterward.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove fire-and-forget thumbnail pre-warming from scanMixed(): firing
48k+ simultaneous unresolved getThumbnailPath() promises was saturating
sharp and ffmpeg after scan completion, keeping CPU pegged. Mixed-library
thumbnails are now generated on-demand by /api/thumbnail as before.
- Add incremental fingerprinting: load existing (item_key → fingerprint)
map from DB before each walk; reuse stored fingerprint for unchanged paths
instead of re-reading 64 KB per file. Stable re-scans now do ~0 bytes of
fingerprint I/O.
- Wrap all bulk DB upsert and delete loops in db.transaction() in
scanMovies(), scanTv(), scanMixed(), and reconcileAndPrune(). Reduces
N auto-committed WAL writes to a single batch commit per scan phase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reKeyMediaItem was called with item_key values (e.g. "lib1:movie:Inception")
but media_tags stores keys in the shorter UI format (e.g. "lib1:Inception"),
so the UPDATE never matched any rows.
Add itemKeyToMediaKey() to extract the terminal segment of an item_key and
reconstruct the media_key format the UI uses:
lib1:movie:Inception%20(2010) → lib1:Inception%20(2010)
lib1:tv_episode:Show:Season1:ep.mkv → lib1:ep.mkv
lib1:mixed_file:dir%2Ffile.mp4 → lib1:dir%2Ffile.mp4
Also skip the UPDATE when old and new media_keys are identical (e.g. a TV
episode moved between seasons keeps the same filename-based media_key).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Computes a SHA-256 partial-content fingerprint (file size + first 64 KB) for
movies, TV episodes, and mixed files during scans. When a file is moved or
renamed within a library, the scan detects the fingerprint match, renames the
media_items row in-place, and updates media_tags.media_key to match — so tags
and NFO metadata survive the move transparently.
- src/lib/fingerprint.ts: new computeFingerprint() using sync FS reads
- src/lib/db.ts: fingerprint TEXT column + index migration
- src/lib/tags.ts: reKeyMediaItem() to update media_tags on rename
- src/lib/scanner.ts: replace clear+upsert with detectMoves/reconcileAndPrune
for movies, TV episodes, and mixed files; games retain clear+upsert (v1)
- TV scan restructured to a single filesystem pass (no double-scanning)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- API reads now serve from media_items cache instead of scanning the filesystem
on every request; scans (manual or scheduled) remain the write path
- NFO metadata is no longer parsed automatically during scans; title falls back
to folder/filename — metadata can be refreshed per-item via the kabob menu
- Mixed libraries are now indexed in media_items (new mixed_file item type)
with file_path stored; scanMixed walks recursively and upserts all files
- Added file_path column to media_items and migrated item_type CHECK constraint
to include mixed_file via safe table-recreation migration
- New POST /api/nfo-refresh endpoint reads the .nfo for a single item and
patches its DB row (supports movie, tv_series, tv_episode)
- Added "Refresh metadata" button to movie and TV series kabob menus
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Game series: filter now checks child games for both search and tag matches instead of always passing series through
- TV episodes: tag selector no longer closes after picking a tag
- TV episodes: filter panel now filters episodes within a season view
- TV series list: series now appear when any of their episodes match the active tag filter (via new /api/tv/series-episode-tags endpoint backed by media_items)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract shared utilities (HIDDEN_FILES, VIDEO_EXTENSIONS, fileApiUrl,
thumbnailApiUrl, findFile) into new src/lib/media-utils.ts, removing
identical copies from games.ts, movies.ts, tv.ts, files.ts, and scanner.ts
- Add comment in files.ts clarifying why its VIDEO_EXTENSIONS set intentionally
differs from the media library set (web-playable formats for the mixed browser)
- Rewrite README to reflect the current feature set: Movies/TV libraries, auth
system, tag system, background scanner, updated project structure, folder
conventions for all four library types, and a complete API reference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add prev/next arrow buttons and ArrowLeft/ArrowRight keyboard shortcuts to ImageLightbox and VideoPlayerModal
- Wire prev/next navigation in MixedView (through filtered media entries), TvView (through season episodes), and MoviesView/MovieDetailModal (through filtered movie list)
- Add new DoomScrollView component: fullscreen random-media mode with scroll/swipe/keyboard navigation, 100-item back-history, and per-library mute settings
- Add Doom Scroll button to mixed, movies, and TV library views
- Doom scroll respects active filters: mixed uses filtered entries, movies uses filtered movie list, TV fetches episodes from matching series only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>