Vellum/internal/natsort/natsort.go
mithral 1b11867efe Phase 6a: directory grouping, audiobooks and music playback
A book is a folder of chapters, not a file. The scanner gained a grouping
mode so a directory of tracks becomes one item with ordered files.

- registry: MediaType.Group (file | directory). Audiobooks and music group by
  directory; everything else stays one item per file.
- scanner: builds groups, upserts one item per group, marks items missing when
  their directory disappears, and keeps the file-level added/updated/missing
  accounting. Tracks are ordered naturally, so chapter 2 precedes chapter 10.
- natsort: one shared natural-order comparator, now used by both the scanner
  and the comics module (which had its own copy).
- media: GET /items/{id}/files/{fileID} streams one specific track. It
  verifies the file belongs to the item, so an item id cannot be paired with
  another item's file id.

No new module was needed for playback: the polymorphic progress table already
keys on file_id, so per-chapter resume fell out of the existing design. The
web player resumes the exact track and offset.

Verified: 3 chapters grouped into one book titled "Mistborn", natural order,
range request returned 206, mismatched file id 404s, per-track progress
round-trips.
2026-10-09 21:20:47 -04:00

42 lines
998 B
Go

// Package natsort compares strings the way humans number things: "page2" sorts
// before "page10". Lexical ordering gets this wrong, which silently shuffles
// comic pages and audiobook tracks.
package natsort
import "strings"
// Less reports whether a sorts before b.
func Less(a, b string) bool {
ai, bi := 0, 0
for ai < len(a) && bi < len(b) {
ad, bd := isDigit(a[ai]), isDigit(b[bi])
if ad && bd {
// Compare the whole digit runs as numbers, ignoring leading zeros
// so "01" and "1" are equal.
as, bs := ai, bi
for ai < len(a) && isDigit(a[ai]) {
ai++
}
for bi < len(b) && isDigit(b[bi]) {
bi++
}
an := strings.TrimLeft(a[as:ai], "0")
bn := strings.TrimLeft(b[bs:bi], "0")
if len(an) != len(bn) {
return len(an) < len(bn)
}
if an != bn {
return an < bn
}
continue
}
if a[ai] != b[bi] {
return a[ai] < b[bi]
}
ai++
bi++
}
return len(a)-ai < len(b)-bi
}
func isDigit(c byte) bool { return c >= '0' && c <= '9' }