Skip to content

fix(importer): move single-file audiobooks into their folder on reorganize (#2894) - #2901

Merged
vavallee merged 2 commits into
vavallee:mainfrom
zikeji:fix/2894-reorganize-single-file-audiobook
Oct 1, 2026
Merged

vavallee merged 2 commits into
vavallee:mainfrom
zikeji:fix/2894-reorganize-single-file-audiobook

Conversation

@zikeji

@zikeji zikeji commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Rename files gave a single-file audiobook its templated folder path as its own destination, so Author/Title.m4b was moved to a file named Author/Title (Year). proposedPathFor used AudiobookDestDir for every audiobook row, and moveTrackedFile then moved the file onto that path through the ebook branch.

A file row now resolves to <templated folder>/<its own name>, mirroring the single-file import branch:

  • Joins the book's own ebook folder when that is the templated folder (added "(2)" to dir on import when ebook import dir is the same as the audiobook import dir (regression?) #2686), same as import.
  • Otherwise uniquifies with the file's current folder treated as available. Before this, a file already sitting in Title (Year)/ was proposed as a move to Title (Year) (2), which is where the (2) folders in my library came from.
  • The orphaned-sidecar cleanup and the sidecar write use file vs folder instead of format, so metadata.opf lands inside the folder.

Folder rows are unchanged.

PreviewImportDestination shares proposedPathFor, so the Fix Match preview for a single audiobook file now names <folder>/<file> instead of the folder. That's where the single-file import branch writes it, so the preview now matches the import. TestPreviewImportDestination_FormatHintWins is updated to expect the file inside the folder; it still asserts the format hint routes to the audiobook root.

Closes #2894

How it was verified

New tests in internal/importer/reorganize_test.go, failing on main (31ccc40):

--- FAIL: TestReorganize_SingleFileAudiobookMovesIntoFolder
    preview = [... Current:.../Jane Doe/My Book.m4b Proposed:.../Jane Doe/My Book (2020) Status:move ...], want move to ".../Jane Doe/My Book (2020)/My Book.m4b"
--- FAIL: TestReorganize_SingleFileAudiobookInPlaceIsNoop
    preview = [... Current:.../Jane Doe/My Book (2020)/My Book.m4b Proposed:.../Jane Doe/My Book (2020) (2) Status:move ...], want noop at ".../Jane Doe/My Book (2020)/My Book.m4b"
--- FAIL: TestReorganize_SingleFileAudiobookSidecarGoesInsideTheFolder
--- FAIL: TestReorganize_SingleFileAudiobookJoinsItsEbookFolder
    audiobook: {... Proposed:.../Jane Doe/My Book (2020) (2) Status:move ...}, want move to ".../Jane Doe/My Book (2020)/My Book.m4b"

All 18 TestReorganize_* pass on this branch.

End to end against an image built from this branch, using the repro from #2894 (flat Stephen King/The Shining.m4b, library scan, Rename files):

v1.39.0 this branch
Proposed Stephen King/The Shining (1977) Stephen King/The Shining (1977)/The Shining.m4b
On disk after apply file The Shining (1977), no extension The Shining (1977)/The Shining.m4b
metadata.opf not a directory warning written inside the folder
Second preview noop on the broken path noop on the correct path

Follow-ups (not in this PR)

A file already flattened by the old behaviour has lost its extension, and this doesn't restore it. Recovery stays manual: rename it back into a folder, deregister the old path, rescan.

Checklist

  • Every commit carries a Signed-off-by that matches its author (git commit -s), see Sign your work. Commits authored by a coding agent fail this check; author them as yourself.
  • Changelog fragment added as changelog.d/<issue-number>-<slug>.md, not an edit to CHANGELOG.md, see changelog.d/README.md
  • Tests added or updated
  • docs/DEPLOYMENT.md updated if env vars, config, or upgrade path changed
  • Wiki pages under docs/ updated if user-facing behaviour changed
  • No new dependency, or its licence is permissive (Bindery is MIT; GPL, AGPL, SSPL and BUSL are not accepted) and THIRD_PARTY_LICENSES.md is regenerated

No docs change: the User Guide already says a reorganized library and a freshly imported one come out identical, and this makes that hold for single files.

Test plan

  • gofmt, go vet ./..., golangci-lint run (v2.11.4), govulncheck ./... on Go 1.26.6
  • go test ./cmd/... ./internal/...
  • cd web && npm run build (no web/ changes)

…anize (vavallee#2894)

A single-file audiobook tracked as the .m4b itself was given the
templated folder path as its own destination, so Rename files turned
Author/Title.m4b into a file named "Title (Year)" with no extension.

Resolve a file row to <templated folder>/<its name>, mirroring the
single-file import branch: join the book's own ebook folder (vavallee#2686),
otherwise uniquify with the file's current folder treated as available
so a file already in place stays a noop instead of moving to
"Title (Year) (2)". The sidecar cleanup and write follow file vs folder
rather than format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Zikeji <me@zikeji.com>
@zikeji
zikeji marked this pull request as ready for review October 1, 2026 06:36
@github-actions github-actions Bot added the bindery-notified Discord notification already sent for this PR label Oct 1, 2026
@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…nstead of moving them again

A file the old Rename files left on its folder path (named "Title (Year)"
with no extension) used to read as a noop. With the file row fix it would
instead be proposed as a move to "Title (Year) (2)/Title (Year)", burying
the extensionless file in a new folder. Report it as an error naming the
manual recovery and leave it in place.

Also cover the stale sidecar cleanup for a lone audiobook file, apply the
shared folder case end to end, and spell out the recovery steps and credit
in the changelog fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
@vavallee

vavallee commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Nice fix, the preview now matches what import does. One gap I hit: a file that's already been flattened (named Title (Year) with no extension) got proposed as a move into Title (Year) (2)/Title (Year), which just buries it. Pushed a small commit that reports that shape as an error with the recovery steps instead, plus a couple of tests and a credit line in the changelog. Holler if anything looks off.

}

func isSingleFile(path string) bool {
info, err := os.Stat(path)
@vavallee
vavallee merged commit 6d487af into vavallee:main Oct 1, 2026
40 of 41 checks passed
vavallee added a commit that referenced this pull request Oct 1, 2026
…emplate (#2900) (#2905)

With naming.audiobook_file_template set, a folder of tracks was renamed
per track but a lone .m4b kept its source filename: the template was only
read on the directory branch, and the single-file branch placed the file
as filepath.Base(src).

The single-file branch now renders the same template through the same
renderer, via a new Renamer.AudiobookSingleFileName. {Part} renders empty
when every {Part} sits in a #1127 conditional group, so
"{Title}{ - Pt. Part:3}.{ext}" gives "Title.m4b". A bare {Part} keeps its
glue outside the braces and an empty value would leave "Title - Part .m4b"
(the default template), so such a template numbers the file as part 1.
No template keeps the source name exactly as before.

The same naming applies to:
- a folder holding a single track, so the name does not depend on how the
  release was packed (the flatten namer now receives the track count);
- Rename files (singleFileAudiobookDest from #2901) and the Fix Match
  destination preview, which share one Scanner helper with the import.

The settings validator checked for the substring "{Part", which rejected
every conditional form; it now asks the renderer whether a group carries
a Part token. The drop folder (External mode) has its own placement that
never used the file template and is unchanged.


Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9

Signed-off-by: vavallee <vavallee@protonmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bindery-notified Discord notification already sent for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename files moves a single-file audiobook onto its folder path, leaving a file with no extension

3 participants