diff --git a/README.md b/README.md index c6e1b64..4609ecf 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,10 @@ The frontend also installs the shared Emberline design system directly from GitH 2. Optionally select which Plex libraries to scan under **Plex Libraries** in Settings. 3. Set your preferred leeway percentage (default: **5%**) and movie minimum difference (default: **5 min**). 4. Navigate to **Movie Duration Check** and click **Run Check**. -5. Movies are flagged only when the runtime difference is greater than both the percentage threshold and the minimum difference, i.e. `|actual - expected| > max(expected * leeway%, min_diff_minutes)`. -6. Radarr runtimes are cached in SQLite. Use **Force Refresh Cache** to re-fetch. -7. Select flagged movies and click **Redownload** to delete the existing file in Radarr and trigger a new automatic search. +5. Optional: enable **Deep scan** to run a full `ffmpeg` decode pass per movie before comparing durations. Plex metadata is used only as a fallback to locate a media part when needed. +6. Movies are flagged only when the runtime difference is greater than both the percentage threshold and the minimum difference, i.e. `|actual - expected| > max(expected * leeway%, min_diff_minutes)`. +7. Radarr runtimes are cached in SQLite. Use **Force Refresh Cache** to re-fetch. +8. Select flagged movies and click **Redownload** to delete the existing file in Radarr and trigger a new automatic search. ## Video Quality Check diff --git a/backend/src/routes/movies.js b/backend/src/routes/movies.js index 89cc615..99cef91 100644 --- a/backend/src/routes/movies.js +++ b/backend/src/routes/movies.js @@ -3,6 +3,7 @@ const router = express.Router(); const { fetchPlexMovies } = require('../api/plex'); const { fetchRadarrMovies } = require('../api/radarr'); const { + DEEP_SCAN_SETTING_KEY, getSetting, getCachedRuntime, upsertCachedRuntime, @@ -32,6 +33,14 @@ function validateConfig(config) { return missing; } +function parseBoolean(value) { + if (value == null) return null; + const normalized = String(value).trim().toLowerCase(); + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; + if (['0', 'false', 'no', 'off'].includes(normalized)) return false; + return null; +} + // Normalize a title for fuzzy matching (lowercase, strip punctuation/articles) function normalizeTitle(title) { return (title || '') @@ -51,11 +60,20 @@ router.get('/check', async (req, res) => { } const forceRefresh = req.query.force === 'true'; + const deepScanSetting = parseBoolean(getSetting(DEEP_SCAN_SETTING_KEY)) || false; + const deepScan = parseBoolean(req.query.deep) ?? deepScanSetting; console.log(`\n[Check] Starting duration check (forceRefresh=${forceRefresh})`); + console.log('[Check] Deep scan:', deepScan); try { // --- Step 1: Fetch Plex movies --- - const { movies: plexMovies, machineIdentifier } = await fetchPlexMovies(config.plexUrl, config.plexToken, config.selectedLibraryIds); + const { + movies: plexMovies, + machineIdentifier, + deepScanFallbacks, + deepScanMetadataLookups, + deepScanScanned, + } = await fetchPlexMovies(config.plexUrl, config.plexToken, config.selectedLibraryIds, { deepScan }); const plexWithTmdb = plexMovies.filter((m) => m.tmdbId).length; console.log(`[Check] Plex: ${plexMovies.length} total, ${plexWithTmdb} have TMDB IDs, ${plexMovies.length - plexWithTmdb} missing TMDB IDs`); @@ -204,6 +222,11 @@ router.get('/check', async (req, res) => { plexUrl: config.plexUrl, radarrUrl: config.radarrUrl, plexMachineId: machineIdentifier, + deepScan, + deepScanFallbacks, + deepScanMetadataLookups, + deepScanScanned, + deepScanMode: deepScan ? 'ffmpeg_full_decode' : 'library_metadata', }, flagged, noMatch, @@ -283,4 +306,3 @@ router.get('/cache-stats', (req, res) => { }); module.exports = router; - diff --git a/frontend/src/stores/movieDuration.js b/frontend/src/stores/movieDuration.js index c133a21..769fc2f 100644 --- a/frontend/src/stores/movieDuration.js +++ b/frontend/src/stores/movieDuration.js @@ -45,11 +45,22 @@ export const useMovieDurationStore = defineStore('movieDuration', () => { const loading = ref(false) const error = ref(null) - async function runCheck(force = false) { + async function runCheck(options = {}) { loading.value = true error.value = null try { - const data = await apiGet(`/api/movies/check${force ? '?force=true' : ''}`) + const normalized = typeof options === 'boolean' ? { force: options } : options + const params = new URLSearchParams() + + if (normalized.force) { + params.set('force', 'true') + } + if (normalized.deepScan !== undefined) { + params.set('deep', normalized.deepScan ? '1' : '0') + } + + const path = params.size > 0 ? `/api/movies/check?${params.toString()}` : '/api/movies/check' + const data = await apiGet(path) // markRaw prevents Vue from making the large result arrays deeply reactive, // which keeps the table views from doing unnecessary work on large result sets. result.value = markRaw(data) diff --git a/frontend/src/views/MovieDurationView.vue b/frontend/src/views/MovieDurationView.vue index 4cfd604..df35ae3 100644 --- a/frontend/src/views/MovieDurationView.vue +++ b/frontend/src/views/MovieDurationView.vue @@ -8,7 +8,16 @@

- + + {{ loading ? 'Running…' : 'Run Check' }} @@ -19,7 +28,7 @@ variant="ghost" :disabled="loading" title="Clears the Radarr runtime cache and re-fetches all runtimes" - @click="store.runCheck(true)" + @click="store.runCheck({ force: true, deepScan })" > @@ -33,6 +42,13 @@ {{ error }} + + Deep scan ran a full `ffmpeg` decode pass for each movie in this run. + + {{ result.summary.deepScanScanned || 0 }} decode(s), {{ result.summary.deepScanMetadataLookups || 0 }} metadata lookup(s), {{ result.summary.deepScanFallbacks || 0 }} fallback(s). + + +
@@ -158,22 +174,24 @@
-

Fetching movie data from Plex and Radarr...

+

{{ deepScan ? 'Running ffmpeg deep scan on Plex movies...' : 'Fetching movie data from Plex and Radarr...' }}