You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a manual import is refused, the file has already been written to its destination inside the library folder, and the refusal path leaves it there. The catalog holds no row for it, the source file is untouched, and the API reply says only that the file could not be published and registered safely. So the library folder ends up holding a file the application cannot see, and nothing tells the user it is there.
What was measured, and the control
Measured on stock ghcr.io/listenarrs/listenarr:canary, digest sha256:216f95f37d0179fc8c2638f24c599ed09d18c808a3d727d8a22b4bf8a2bff9c4. Fixtures are one second of silence built with ffmpeg at run time, imported with POST /api/v1/library/manual-import against an audiobook added from a generated test corpus. Stream contents were read back with ffprobe rather than assumed from the filenames.
Each case was judged on three observables, since a file sitting at the destination with no database row behind it has not been imported:
the API reply
the filesystem at the source and at the planned destination, read from outside the container
the catalog file rows, from GET /api/v1/library/{id}/files-debug
fixture
verdict
destination
source
catalog file rows
audio-only.mp4
refused
file left behind
preserved
0
with-cover.mp4 (audio, plus a stream with attached_pic=1)
refused
file left behind
preserved
0
with-video.mp4 (audio, plus a playable video stream)
refused
file left behind
preserved
0
video-only.mp4
refused
file left behind
preserved
0
empty.mp4 (no streams at all)
refused
file left behind
preserved
0
audio-only.mkv
refused
file left behind
preserved
0
same-bytes.m4b (sha256 identical to audio-only.mp4)
imported
consumed into the library
consumed
1
That last row is the control. The one fixture that imports leaves nothing behind, and its bytes are identical to the first fixture, which does leave something behind. All six refusals left a file in the library folder and produced no catalog row, and all six came back with the same text: "The file could not be published and registered safely."
Not specific to one file type
The refused fixtures cover three different stream shapes in an .mp4, an .mp4 with no streams at all, and an .mkv. Those refusals do not share a cause, and the file left behind is the same in every one of them. The fixtures look like this because they came out of measuring #995; the leftover file is the part that has nothing to do with .mp4.
Why it matters
No row references the file, so nothing in the application will surface it or tidy it up. For these fixtures a later library scan does not find it either, since the scan walk filters on extension before it looks at anything else (ScanFileDiscovery.Enumeration.cs:76, read). I did not measure a refusal of a file whose extension the scanner does accept, so I cannot say what happens in that case.
The reply gives the user nothing to act on. It says the import did not happen, which is accurate, but it never mentions the file that got written on the way to working that out. Retry a failing import a few times and the folder holds several copies nobody put there.
What I read
Graded read, at canary a630572e9. Not executed.
ManualImportController.ProcessItem.cs:299-311 calls PrepareOwnedManualImportActionForRegistrationAsync, which reaches IFileMover.PrepareActionForRegistrationAsync. In the markerless path the destination is published during that preparation step (FileMover.MarkerlessRegistration.cs:178-189), and for a Move the source is deleted only later, in the completion step (FileMover.MarkerlessRegistrationMove.cs:194). That ordering matches what the matrix shows, with the destination written and the source still in place.
Registration runs after preparation (ProcessItem.cs:327-343). When it returns false, ProcessItem.cs:344-354 returns the failure reply, and nothing on that path removes the prepared destination. The neighbouring failure branch at :356-375 does call RollbackPublishedGenerationIfStaleAsync, but that rolls back a database generation rather than the file.
Leaving the using block does not remove it either. PinnedAudiobookFileRegistrationLease.Dispose() (PinnedAudiobookFileRegistrationLease.cs:423-432) disposes the stable handle and the file handle and does nothing else.
I am not claiming which refusal branch each fixture above took. Four sites return that one message, and the reply does not distinguish them.
Nothing about a restart. All of the above was measured inside a single run, so whether startup reconciliation would remove the file is untested.
Nothing about automatic import. Only the manual import endpoint was exercised.
This is stock canary behaviour at the digest named above, not something I introduced, and it held for every refused fixture rather than intermittently.
Two of the three files cited above are in #993's diff, FileMover.MarkerlessRegistration.cs and PinnedAudiobookFileRegistrationLease.cs (checked with gh pr view 993 --json files). ManualImportController.ProcessItem.cs is not. Since the cleanup may well belong in the part you are rewriting, this is evidence rather than a patch. I am happy to contribute a regression test that fails on the current behaviour and passes once a refused import leaves the folder empty, in whatever shape fits #993.
Reproduction
Stock canary at the digest above, one root folder, one audiobook in the library.
Build a file the import will refuse. A container with no audio stream is the simplest: ffmpeg -f lavfi -i color=c=black:s=64x64:d=1 -t 1 -c:v libx264 video-only.mp4.
Put it in a directory outside every root folder, list it with GET /api/v1/library/manual-import/preview?path=<that directory>, then POST /api/v1/library/manual-import with the item matched to the audiobook.
The reply is a failure carrying "The file could not be published and registered safely."
Look in the audiobook's folder under the root folder. The file is there, named by the naming pattern. GET /api/v1/library/{id}/files-debug reports no row for it.
Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.
Summary
When a manual import is refused, the file has already been written to its destination inside the library folder, and the refusal path leaves it there. The catalog holds no row for it, the source file is untouched, and the API reply says only that the file could not be published and registered safely. So the library folder ends up holding a file the application cannot see, and nothing tells the user it is there.
What was measured, and the control
Measured on stock
ghcr.io/listenarrs/listenarr:canary, digestsha256:216f95f37d0179fc8c2638f24c599ed09d18c808a3d727d8a22b4bf8a2bff9c4. Fixtures are one second of silence built with ffmpeg at run time, imported withPOST /api/v1/library/manual-importagainst an audiobook added from a generated test corpus. Stream contents were read back with ffprobe rather than assumed from the filenames.Each case was judged on three observables, since a file sitting at the destination with no database row behind it has not been imported:
GET /api/v1/library/{id}/files-debugaudio-only.mp4with-cover.mp4(audio, plus a stream withattached_pic=1)with-video.mp4(audio, plus a playable video stream)video-only.mp4empty.mp4(no streams at all)audio-only.mkvsame-bytes.m4b(sha256 identical toaudio-only.mp4)That last row is the control. The one fixture that imports leaves nothing behind, and its bytes are identical to the first fixture, which does leave something behind. All six refusals left a file in the library folder and produced no catalog row, and all six came back with the same text: "The file could not be published and registered safely."
Not specific to one file type
The refused fixtures cover three different stream shapes in an
.mp4, an.mp4with no streams at all, and an.mkv. Those refusals do not share a cause, and the file left behind is the same in every one of them. The fixtures look like this because they came out of measuring #995; the leftover file is the part that has nothing to do with.mp4.Why it matters
No row references the file, so nothing in the application will surface it or tidy it up. For these fixtures a later library scan does not find it either, since the scan walk filters on extension before it looks at anything else (
ScanFileDiscovery.Enumeration.cs:76, read). I did not measure a refusal of a file whose extension the scanner does accept, so I cannot say what happens in that case.The reply gives the user nothing to act on. It says the import did not happen, which is accurate, but it never mentions the file that got written on the way to working that out. Retry a failing import a few times and the folder holds several copies nobody put there.
What I read
Graded read, at canary
a630572e9. Not executed.ManualImportController.ProcessItem.cs:299-311callsPrepareOwnedManualImportActionForRegistrationAsync, which reachesIFileMover.PrepareActionForRegistrationAsync. In the markerless path the destination is published during that preparation step (FileMover.MarkerlessRegistration.cs:178-189), and for a Move the source is deleted only later, in the completion step (FileMover.MarkerlessRegistrationMove.cs:194). That ordering matches what the matrix shows, with the destination written and the source still in place.ProcessItem.cs:327-343). When it returns false,ProcessItem.cs:344-354returns the failure reply, and nothing on that path removes the prepared destination. The neighbouring failure branch at:356-375does callRollbackPublishedGenerationIfStaleAsync, but that rolls back a database generation rather than the file.usingblock does not remove it either.PinnedAudiobookFileRegistrationLease.Dispose()(PinnedAudiobookFileRegistrationLease.cs:423-432) disposes the stable handle and the file handle and does nothing else.I am not claiming which refusal branch each fixture above took. Four sites return that one message, and the reply does not distinguish them.
What this does not claim
canarybehaviour at the digest named above, not something I introduced, and it held for every refused fixture rather than intermittently.Overlap with #993
Two of the three files cited above are in #993's diff,
FileMover.MarkerlessRegistration.csandPinnedAudiobookFileRegistrationLease.cs(checked withgh pr view 993 --json files).ManualImportController.ProcessItem.csis not. Since the cleanup may well belong in the part you are rewriting, this is evidence rather than a patch. I am happy to contribute a regression test that fails on the current behaviour and passes once a refused import leaves the folder empty, in whatever shape fits #993.Reproduction
canaryat the digest above, one root folder, one audiobook in the library.ffmpeg -f lavfi -i color=c=black:s=64x64:d=1 -t 1 -c:v libx264 video-only.mp4.GET /api/v1/library/manual-import/preview?path=<that directory>, thenPOST /api/v1/library/manual-importwith the item matched to the audiobook.GET /api/v1/library/{id}/files-debugreports no row for it.Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.