Video analyzer and library for HDR content with deep metadata extraction, flexible filtering, and data cleanup tooling.
See CHANGELOG.md for release notes and notable changes.
- HDR Detection: Dolby Vision profiles, EL types (FEL/MEL), HDR10+, HDR10, HLG detection.
- ARR Integration: Right-click to queue Sonarr/Radarr search commands; separate status dots for each service with individual tooltips.
- Metadata Enrichment: Filename parsing, Kodi
.nfoingestion, and backfill tools. - Smart Filtering: Multi‑select filters with counts,
All+Blanksoptions, and advanced search tokens. - Media-type–aware ribbons: When filtering on Movies or TV, ribbons and charts show totals for that type only; ribbon and badge clicks preserve the media type.
- Charts: Real‑time visualizations with Totals / Filtered toggle.
- Manual Edits: Edit titles, year, source, HDR info, and media type directly in the file modal.
- Bulk Edit/Rescan: Multi‑select rows and apply edits or rescan selected files.
- Column Control: Show/hide, resize, and drag‑reorder columns; one wide table in
#table-h-scroll. Freeze-pane headers: thead stays pinned at the top of the table scrollport while rows scroll (Excel-like); checkbox/delete columns stay frozen left/right. The scrollport uses both axes (overflow: auto) and is sized to the remaining viewport height. The grid usesborder-collapse: separateso sticky headers stay aligned with column widths. Column widths are global:<colgroup>and layout use only persistedcolumn_widths(and per‑column minimum defaults when unset). Filtering and scrolling do not readth.offsetWidthor otherwise remeasure — widths change only when you drag a resize handle (or use reset‑to‑minimum). Movies / TV usedisplay: noneon irrelevant title columns. Savedcolumn_widthsare unchanged;<col>widths are0for collapsed columns (media or menu‑hidden) so the table grid matches what you see. After every colgroup sync, inlinewidth/min-width/max-widthare stripped from allth/tdso only<colgroup>defines column widths. Resize uses delegated capture on#video-table(12px hit zone); mouseup snapshots all visible column widths from<colgroup>intocolumn_widths. Drag reorder is blocked only while a resize gesture is active. col-chk / col-del stay 40px. - Scheduling: Manual, daily, and interval scanning.
- Exports: CSV/JSON of All, Movies, TV, Filtered, or Current page.
- Notifications: Toasts for scan actions, backfill, settings saves, and more.
- Scan Controls: Split scan button with All/TV/Movie and per-folder targeting.
- Missing File Tracking:
missingcolumn and filter for files no longer on disk; optional "Remove Missing from DB" setting (delete vs mark). - Scan Report: Shows scanned, new, removed, failures, and whether missing files were deleted or marked.
- Duplicate Detection Suite: Persistent duplicate keys/counts, duplicate groups modal, exact fingerprint rebuild option, and
Dupbadges in the main table.
- Duplicate check (persistent):
- Added DB fields:
dup_group_key,dup_exact_key,dup_count - Added duplicate indexes for faster grouping
- Added duplicate APIs:
POST /api/duplicates/rebuildPOST /api/duplicates/groupsPOST /api/duplicates/members
- Added DB fields:
- Duplicate check modal + actions:
- New Duplicates button next to Bulk Edit/Rescan
- Group view + inline member dropdown (opens directly under selected group row) with keep recommendation
- QoL actions: copy path/folder, copy selected paths, rescan selected, delete selected
- Controls:
- Refresh Groups
- Rebuild Keys
- Rebuild + Exact Fingerprints
- Recommended first-time workflow:
- Run Rebuild Keys (fast; builds logical grouping and
dup_count) - Open groups and review with View Files
- Run Rebuild + Exact Fingerprints only when you need byte-level exact duplicate verification
- Run Rebuild Keys (fast; builds logical grouping and
- Scan automation setting:
- New checkbox in settings: Duplicate Check While Scanning
- Default is off (normal scans do not do duplicate-key work unless enabled)
- Main table duplicate badges:
- New Dup column with sortable
xNbadges (dup_count) - Clicking a duplicate badge opens duplicate details for that row group
- New Dup column with sortable
- Metadata precedence hardening:
- NFO media type now takes priority over filename parsing
- Movie NFO results clear TV-only fields to avoid stale cross-type metadata
- Table/column UX polish:
- Restored horizontal scrollbar panel for table navigation
- Sticky edge dividers for
col-chkandcol-delremain visible while side scrolling - Column menu layout fix for reset-widths overlap/clipping
col-delsizing/alignment adjusted to matchcol-chk
- Frontend: HTML5, CSS3, classic JavaScript (
app/static/js/*.js+app/static/css/app.css; Chart.js) - Backend: Python / Flask — package
app/video_analyzer/with thin WSGI entryapp/analyzer.py(analyzer:app) - Database: SQLite
Layout (3.0+): domain modules under video_analyzer/ (config, state, db, queries, analysis, scan, routes, …). Prefer from video_analyzer… for internals; Gunicorn still loads analyzer:app.
- Copy env file and edit values:
cp .env.example .env
- Map media volumes in
docker-compose.yml(and setSCAN_PATHSin.envto those container paths if you want fixed roots). - Start container:
Confirm the UI version badge matches the latest entry in
docker compose build --no-cache docker compose up -d
CHANGELOG.md/app/CHANGELOG.md(e.g. v3.0.5). Sync the full repo to the NAS before building (not justdocker-compose.yml). Prefer leavingAPP_VERSIONunset in.envso the changelog drives the badge. - Open:
http://localhost:6002(or host IP)
The compose service uses restart: unless-stopped and a health check against /api/health. Python deps are pinned in app/requirements.txt. Images build for amd64 and arm64 (dovi_tool musl binaries).
cd app
pip install -r requirements.txt -r requirements-dev.txt
set VIDEO_ANALYZER_TESTING=1 # PowerShell: $env:VIDEO_ANALYZER_TESTING=1
pytest -qCI runs the same suite via .github/workflows/ci.yml on push/PR.
This app is designed for trusted LAN use only. It exposes scan, database restore, delete, cleanup, and maintenance actions without built-in authentication, so do not publish it directly to the internet. If remote access is needed, place it behind a secured reverse proxy or VPN with authentication.
To enable right-click ARR search/replace actions, set these environment variables in your compose service:
RADARR_URL(example:http://192.168.5.10:7878)RADARR_API_KEYSONARR_URL(example:http://192.168.5.10:8989)SONARR_API_KEY
Notes:
- API keys are read from environment variables only (not stored in app settings DB).
- For TV rows, Sonarr lookup uses
tvdb_series_idonly (fromtvshow.nfo) andseason; triggers SeasonSearch for the season. - For movie rows, Radarr lookup uses
tmdb_idorimdb_id; triggers MoviesSearch. - Two status dots (Sonarr and Radarr) next to the ARR menu option show connectivity per service; each dot has its own tooltip (checked when you open the context menu).
The top‑left menu contains:
- Export format toggle (CSV/JSON)
- Database tools: Backup, Restore, Optimize, Backfill Metadata, Clean DB
- Health: System status, database, scan status, uptime, app/tool versions, latency, and Sonarr/Radarr connection status
- Scan folders: Pick specific folders per volume, assign type, and mute
- Filter presets (save/load/delete)
- Remove Missing from DB: When enabled (default), files no longer on disk are deleted from the DB during scan. When disabled, they are marked
missing=1so you can filter and delete them manually. - Duplicate Check While Scanning: When enabled, scan batches also compute duplicate grouping keys/counts during scan saves. Keep this off for faster scans if you only run duplicate checks on demand.
The Health button opens a modal showing:
- Status (healthy/degraded), Database, Scan Status, Uptime, Version, Latency
- Tool versions for MediaInfo, FFmpeg, FFprobe, dovi_tool, and Python
- Sonarr and Radarr connection status: green = connected (with version), yellow = not configured, red = connection failed (error message shown)
| Badge | Meaning |
|---|---|
| DV P7 FEL | Dolby Vision P7 + Full EL |
| DV P7 MEL | Dolby Vision P7 + Minimal EL |
| DV P5 / P8.x / P10.x | Dolby Vision profiles |
| HDR10+ / HDR10 / HLG / SDR | Base HDR format |
Clicking badges applies a filter immediately. When you have Movies or TV selected, badge clicks keep that media type and add the chosen filter on top.
- Multi‑select filters include All + Blanks and show counts.
- Media Type filter (Movie/TV).
- Missing filter (Yes/No) for files no longer on disk.
- Resolution, volumes, codecs, formats, source, container, edition, etc.
Supports tokens and quoted values:
source:"UHD Bluray"res:2160pyear:2020type:tvcategory:dovistatus:failed
Tokens supported: year, source, format, codec, res, category, volume, container, edition, type, media_type, status, hybrid, 3d, missing.
Charts can toggle between:
- Totals (entire library)
- Filtered (current filters)
The toggle is in the lower‑left of the chart panel.
- Show/hide in column menu
- Resize by dragging column edge
- Reorder by dragging column headers
- Order and widths are persisted
- Freeze-pane header: sort/filter headers stay visible while scrolling rows inside the table viewport
- Sticky left/right columns keep checkbox and delete visible (including in the frozen header)
Dupcolumn shows duplicate count badge (xN) and can open duplicate-group details directly
Editable fields saved on modal close:
- Type, Show Title, Episode Title, Movie Title
- Season, Episode, Year
- Source, Source Format
- Main HDR (category), Secondary HDR
- Use Bulk Edit/Rescan button next to Search (shown when rows are selected).
- Ctrl+Click (Windows) or Cmd+Click (macOS) to multi‑select rows.
- Apply edits across selected rows or rescan selected files.
- Per‑field Clear toggles allow blanking specific fields.
- Bulk rescan shows a busy overlay while rescanning.
- Right-click one or more selected rows and choose Search/Replace: Sonarr | Radarr.
- Two status dots (Sonarr | Radarr) show connectivity per service; each has its own tooltip. Green = reachable, red = connection failed.
- The app queues search commands in Radarr (movies) or Sonarr (TV) for matching library items.
- Mixed selection is supported; each row routes to Sonarr or Radarr by
media_type(with ID fallback). - Fallbacks: If direct ID lookup returns empty (e.g. NFO has wrong tvdbId or Sonarr lookup is down), the app fetches all series/movies and matches by
tvdb_series_id/tmdb_id/imdb_id, or by show/movie title when IDs don't match. - Series vs Episode IDs: TV rows store
tvdb_series_id(fromtvshow.nfo, e.g. 73940 for 'Allo 'Allo) andtvdb_episode_id(from episode NFO, e.g. 133064). Same for imdb, tmdb, trakt, rotten, metacritic. Movies continue to usetvdb_id,imdb_id, etc. After adding these columns, a full rescan is required to populate them from NFOs.
- Folders button in the main menu opens a folder picker.
- Choose a volume, browse directories, and add folders to the scan list.
- Type selector per folder (Auto/TV/Movie) for targeted scans.
- Mute keeps a folder in the list but skips it during scans.
- Any folder containing an empty
.scanignorefile is skipped (including all subfolders). - If no scan folders are configured, scans default to all mounted volumes.
Example:
/media/Movies/.scanignore
Any folder with .scanignore is skipped.
The IGNORE setting is a comma-separated, case-insensitive list. Rules apply while crawling a scan:
| Rule | Matches | Example |
|---|---|---|
name |
Files whose filename contains name |
sample,trailer,.extras |
*.pattern |
Files matching a filename glob | *.trickplay,*.nfo |
/name |
Folders whose name is exactly name |
/extras,/.extras |
/*.pattern |
Folders matching a folder-name glob | /*.trickplay,/.cache* |
%name |
Files and folders containing name |
%sample,%extras |
%*.pattern |
Files and folders matching a glob | %*.trickplay |
Examples:
sample,trailer,.extras,*.trickplay,/.extras,/*.trickplay,%cache
This skips sample/trailer files, filenames containing .extras, files ending in
.trickplay, the exact .extras folder, folders ending in .trickplay, and
anything containing cache. A bare token such as .extras applies to files
only; prefix it with / to target a folder. Use % only when both files and
folders should match.
- Split scan button: All, TV, Movie.
- Hover TV/Movie to pick a specific typed folder from a submenu.
- The main button shows the selected mode and folder target.
- During scan: "Starting" shows for up to 3 seconds per volume, then "Found X (Y new / Z removed)".
- Click the progress bar during a scan to pause/resume scanning and analyzing.
- History button appears when idle and shows recent scan history with a per-entry report view (scanned, new, removed, failures).
- Split export button with scopes: All, All Movies, All TV, All filtered, Current page.
- Output format is set in the main menu (CSV/JSON).
- Episode
.nfo(episodedetails) → episode title, season, episode, year tvshow.nfo→ show titlemovie.nfo→ movie title + year
- NFO indicates whether a matching
.nfofile was found for that row. - Movies: Counted only if an
.nfoexists in the same folder as the video (same stem,movie.nfo, or folder-named.nfo). Amovie.nfoin a parent folder does not count. - TV episodes: Counted only if an episode-specific
.nfoexists (same stem as the video, or same-folder.nfowith matching season/episode).tvshow.nfois series-level and does not count as “NFO found” for individual episodes.
- Missing indicates whether the file no longer exists on disk (Yes/No).
- When "Remove Missing from DB" is disabled, scans mark missing files instead of deleting them; use the Missing filter to find and delete them manually.
- Movie title fallback (only if
.nfomissing) - Episode title fallback after
SxxEyyor1x02 - Remux source inference:
1080p+ remux → Bluray2160p+ remux → UHD Bluray
- Backfill Metadata button in settings panel
- Fills missing fields using
.nfo+ filename - Progress shown in scan info panel + logs
DOVI P7 FEL→category=dovi,profile=7,el=FELDOVI P7 MEL→category=dovi,profile=7,el=MELDOVI P10.1/DOVI P10.4→ AV1 Dolby Vision with HDR10/HLG backward compatibilityHDR10+→category=hdr10plusHDR10→category=hdr10HLG→category=hlgSDR→category=sdr_only
Secondary HDR is detected from HDR side‑data and stored in secondary_hdr.
Returns system health: status, database, scan_status, uptime_seconds, version, tools, sonarr, radarr. Each tool object includes installed, version, and message; each ARR object has ok, configured, message.
Paginated table rows with filters and sorting. Fast path: returns rows, page, total_items, total_pages, and library_total only.
Key params:
search,category,profile,el,resolution,volume,containervideo_source,source_format,video_codec,media_typesecondary_hdr,status,nfo_missing,missing(1=yes, 0=no)size_op,size_val,bit_op,bit_valsort,order,page,per_page
Includes duplicate fields in each row payload:
dup_group_keydup_exact_keydup_count
Heavy dashboard metadata for the same filter params: stats, stats_filtered, stats_media_scoped, and filtered total_items.
Optional include_options=0|1 (default 1): when 1, also returns filter_options facet counts for dropdowns. The UI loads stats with include_options=0 first, then refreshes facet counts on a short delay.
Rebuild persistent duplicate keys/counts for current filters.
Payload:
{"filters": {}, "include_exact": false}include_exact=false: rebuild logical group keys/counts onlyinclude_exact=true: also recompute exact fingerprints
Return duplicate groups (logical + exact) for current filters.
Payload:
{"filters": {}}Return all files in a duplicate group.
Payload:
{"group_id": "logical|movie:tmdb:12345"}Backfill missing metadata using .nfo + filename.
Payload:
{"fill_blanks_only": true}Update manual fields:
{
"full_path": "/path/file.mkv",
"show_title": "Show Name",
"episode_title": "Episode Title",
"movie_title": "Movie Title",
"season": 1,
"episode": 4,
"year": 2020,
"video_source": "Bluray",
"source_format": "Remux",
"category": "dovi",
"secondary_hdr": "HDR10+"
}{"full_path": "/path/file.mkv", "media_type": "tv"}Rescan a single file and update DB entry:
{"full_path": "/path/file.mkv"}Batch rescan (max 50 paths per request; UI chunks larger selections):
{"paths": ["/path/a.mkv", "/path/b.mkv"], "threads": 2}Returns ok / failed counts and optional per-path errors. Paths must lie under allowed media mounts. Blocked while a scan/backfill is running.
Remove DB rows (and optionally disk files/folders). Disk/folder delete requires an explicit paths list (not delete_all_filter).
{
"paths": ["/media/Movie/Movie.mkv"],
"delete_files_on_disk": true,
"delete_folders": true
}Folder delete is skipped for media mount roots and folders that still contain other video files.
Dry-run folder impact for an explicit path list (no mutations).
Queue Sonarr/Radarr search commands for selected files:
{"paths": ["/path/file1.mkv", "/path/file2.mkv"]}- Check
/progressand/api/logs - Ensure no scan is running when backfill starts
- Clear filters and re‑apply
- Verify search token syntax and quoting