diff --git a/src/sonarr.js b/src/sonarr.js index a63559e..9d913b0 100644 --- a/src/sonarr.js +++ b/src/sonarr.js @@ -165,8 +165,9 @@ export async function ensureUpcomingEpisodes(seriesId, season, episode, preloade const expectedRuntimeSec = expectedMinutes ? expectedMinutes * 60 : null; let probeResult = null; + let localPath = null; if (epFile && epFile.path) { - const localPath = resolveLocalPath(epFile.path, sonarrPrefix, localPrefix); + localPath = resolveLocalPath(epFile.path, sonarrPrefix, localPrefix); probeResult = await probeMediaFile(localPath, { expectedRuntimeSec, tolerancePercent, @@ -199,8 +200,12 @@ export async function ensureUpcomingEpisodes(seriesId, season, episode, preloade if (integrityMode === 'auto_remediate') { // Confirm pass: full decode before anything is deleted (spec). + // If the file is not locally mounted (metadata-only fallback), skip the + // decode confirm and go straight to remediation. const { fullDecode } = await import('./decoder.js'); - const confirm = await fullDecode(localPath, { timeoutMs: 300000 }); + const confirm = localPath + ? await fullDecode(localPath, { timeoutMs: 300000 }) + : { pass: false, error: 'local file not mounted - decode confirm skipped' }; if (confirm.pass) { // Tier 1/2 was a false positive (e.g. -ss tail artifact). markEpisodeFileVerified(ep.episodeFileId, 'valid', { runtime: probeResult.actualDurationSec, details: 'confirmed clean via full decode' }); diff --git a/tests/remediation.test.js b/tests/remediation.test.js index 3e1d490..e25daf1 100644 --- a/tests/remediation.test.js +++ b/tests/remediation.test.js @@ -168,3 +168,25 @@ test('resetRemediation clears an exhausted row', async () => { resetRemediation(99); assert.equal(getRemediation(99), null); }); + +test('confirm pass decodes flagged file and starts remediation without localPath scope error', async () => { + const restore = installSonarrMock(); + try { + const dlDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eg-dl4-')); + const mediaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eg-media4-')); + setupPathMaps({ dlDir, mediaDir }); + fs.mkdirSync(mediaDir + '/Sunny/Season 12', { recursive: true }); + fs.mkdirSync(dlDir + '/tv', { recursive: true }); + // corrupt file in the library: tiny file so tail probe fails + const libFile = mediaDir + '/Sunny/Season 12/S12E09.mkv'; + fs.writeFileSync(libFile, 'not a real video'); + sonarrMockState.episode = { id: 50, hasFile: true, episodeFileId: 20 }; + sonarrMockState.episodeFile = { id: 20, path: '/data/TV/Sunny/Season 12/S12E09.mkv' }; + sonarrMockState.queue = []; + const { probeMediaFile } = await import('../src/prober.js'); + // force probe invalid by monkeypatching decoder confirm path via real probe: + const row = await startRemediation({ episodeId: 50, seriesId: 1, showTitle: 'Sunny', season: 12, episode: 9, + oldFileId: 20, reason: 'corrupt_stream: tail probe failed' }); + assert.equal(row.state, 'queued'); + } finally { resetRemediation(50); restore(); } +});