feat: add Plex deep scan mode - #12
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional “deep scan” mode for Plex media checks that performs a real ffmpeg full-decode pass to derive more reliable stream metadata/durations, wires that toggle through settings → API → UI, and introduces backend tests plus container support for ffmpeg.
Changes:
- Implement
ffmpeg-based deep-scan batching (with concurrency + timeout/termination handling) for Plex movies and TV episodes in the backend Plex client. - Expose a persisted deep-scan setting and per-run deep-scan toggle + stats in the Movie Quality and Episode Duration views.
- Add backend test coverage for mixed movie/episode deep-scan behavior and timeout SIGTERM/SIGKILL escalation; install
ffmpegin the Docker image.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents the new deep-scan option and its performance tradeoffs in both check workflows. |
| frontend/src/views/SettingsView.vue | Adds a persisted “deep scan for Plex media checks” toggle in Settings and saves it to the backend. |
| frontend/src/views/MovieQualityView.vue | Adds a per-run deep-scan toggle, loads the default from settings, and shows deep-scan stats in the UI. |
| frontend/src/views/EpisodeDurationView.vue | Adds a per-run deep-scan toggle, loads the default from settings, and shows deep-scan stats in the UI. |
| frontend/src/stores/settings.js | Adds quality_deep_scan to the settings default shape. |
| frontend/src/stores/movieQuality.js | Extends runCheck to optionally pass deep=0/1 to the backend. |
| frontend/src/stores/episodeDuration.js | Extends runCheck to optionally pass deep=0/1 to the backend. |
| Dockerfile | Installs ffmpeg in the backend runtime image to support deep scan in Docker. |
| backend/test/api/plex.test.js | Adds node:test coverage for deep-scan batching + timeout escalation behavior via injected process/timer harnesses. |
| backend/src/routes/settings.js | Persists/validates the new deep-scan setting key in the settings API. |
| backend/src/routes/quality.js | Reads the deep-scan setting/query param and returns deep-scan stats in the response summary. |
| backend/src/routes/episodeDuration.js | Reads the deep-scan setting/query param and returns deep-scan stats in the response summary. |
| backend/src/db/index.js | Seeds the new deep-scan setting default in SQLite. |
| backend/src/api/plex.js | Implements ffmpeg deep scan helpers and integrates them into Plex movie/episode fetching. |
| backend/package.json | Adds a test script to run backend tests via node --test. |
| const detail = stderr.trim().split(/\r?\n/).slice(-3).join(' | ') || `exit=${code} signal=${signal || 'none'}`; | ||
| reject(new Error(`ffmpeg deep scan failed: ${detail}`)); |
There was a problem hiding this comment.
Addressed in 7f89920. ffmpeg stderr tails are now redacted before they are included in deep-scan error messages, so X-Plex-Token values do not leak into logs or API errors.
| if (timedOut) { | ||
| const killDetail = forcedKill ? ` after SIGTERM+SIGKILL (${termGraceMs}ms grace)` : ' after SIGTERM'; | ||
| reject(new Error(`ffmpeg deep scan timed out after ${Math.round(timeoutMs / 1000)}s${killDetail}`)); | ||
| return; |
There was a problem hiding this comment.
Addressed in 7f89920. Timeout text now reports sub-second values in milliseconds and rounds second-based values up, so short test timeouts no longer render as 0s.
| const { getAllSettings, setSetting } = require('../db'); | ||
|
|
||
| const ALLOWED_KEYS = ['plex_url', 'plex_token', 'radarr_url', 'radarr_api_key', 'sonarr_url', 'sonarr_api_key', 'leeway_percent', 'movie_min_diff_min', 'episode_min_diff_min', 'plex_library_ids', 'quality_thresholds']; | ||
| const ALLOWED_KEYS = ['plex_url', 'plex_token', 'radarr_url', 'radarr_api_key', 'sonarr_url', 'sonarr_api_key', 'leeway_percent', 'movie_min_diff_min', 'episode_min_diff_min', 'plex_library_ids', 'quality_thresholds', 'quality_deep_scan']; |
There was a problem hiding this comment.
Addressed in 7f89920. The persisted setting is now plex_deep_scan, and the backend migrates the legacy quality_deep_scan key forward so existing installs keep their saved preference.
| onMounted(async () => { | ||
| await settingsStore.fetchSettings() | ||
| deepScan.value = settingsStore.settings.quality_deep_scan === '1' | ||
| }) |
There was a problem hiding this comment.
Addressed in 7f89920. MovieQualityView now only hydrates the deep-scan toggle from settings if the user has not already changed it while the async fetch was in flight.
| onMounted(async () => { | ||
| await settingsStore.fetchSettings() | ||
| deepScan.value = settingsStore.settings.quality_deep_scan === '1' | ||
| }) |
There was a problem hiding this comment.
Addressed in 7f89920. EpisodeDurationView uses the same guarded hydration path now, so an in-flight settings fetch no longer reverts a user toggle.
|
@copilot fix all the commented issues |
Summary
ffmpegdecode pass for movie quality and episode duration checksffmpegsupportVerification
npm testinbackend/node --check src/api/plex.js src/routes/quality.js src/routes/settings.js src/routes/episodeDuration.js src/db/index.jsinbackend/npm run buildinfrontend/