feat(audiobooks): A2 — narrator metadata, audio-aware UX, player polish, bookmarks
A2a (metadata round): narrators TEXT[] + narrators_search (tags pattern: accumulator + GIN pairs) + abridged bool on media_items; scanner fills from the Libation/ABS sidecar (narrators verbatim, abridged flag) with audio-payload promotion; editorial fields ride the real columns while audio_metadata stays scan-derived (duration/ codec/chapters). Detail/edit/save paths carry the fields; the web edit dialog grows an Audio section (Narrator(s) comma-input, Abridged toggle, scanned duration read-only); search weights + narrator filter (word_similarity over narrators_search) + narrator autocomplete added. A2c (per-media-type wording): copy helpers keyed on format_group — Listen Now, Mark Complete/Not Complete, Listening Progress, Your Listening Stats, Time Listening, Time listened (pages tile swapped for audio), Position H:MM:SS / total H:MM:SS on detail + progress pages (duration parsed from audio_metadata). Dashboard: system collections swap to Continue Listening / Recently Listened at DISPLAY time when the audiobooks library is selected (stored names unchanged; restores keep working); week card = Listening Time / Sessions / Books Touched; cover-corner badges identify audiobook (headphones) / comic / manga cards on mixed grids — books stay unbadged; hover action icon follows the media type. A2b (player): the scrubber binds value to the playhead with an explicit scrub guard (the handle previously froze at its initial max); header row = reader-style back arrow + Chapter n/total launcher; chapters open in a floating top-down drawer (current chapter highlighted, page never shifts); Skip/Speed rows share one label-left layout; honest wall-clock session accounting (playing-only deltas, paused flush). A2d (bookmarks): note-bearing time bookmarks ride the existing media_bookmarks substrate (notes column + sync-aware service already there) — audio locator convention audio:<seconds>, chapter_number filled, auto-title '<chapter> — H:MM:SS', capture popover with optional note, drawer with jump/edit-note/delete. Zero server changes needed. Verified live on the Libation sample: narrators backfilled by rescan; PUT edit persisted + overrides detected (narrators, abridged); force rescan preserved the user edit; reset-to-scanned-of-defaults restored truth; narrator filter/autocomplete scoped by library; dashboard listsening wording + badges render; bookmark save with note deduped to one row.
This commit is contained in:
1 parent
902cbe5915
commit
86f0af632e
38 files changed
+3069
-1552
No files matched your search
@@ -64,7 +64,17 @@ func (h *DashboardHandler) GetSections(c *echo.Context) error {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load dashboard sections"})
|
||||
}
|
||||
|
||||
sectionData := BuildSections(sections, libraryID)
|
||||
// Per-type wording: system-collection display swaps on audiobook scope.
|
||||
libraryType := ""
|
||||
if libraryID != "" {
|
||||
if pgLibID, perr := uuid.Parse(libraryID); perr == nil {
|
||||
if lib, lerr := h.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: pgLibID, Valid: true}); lerr == nil {
|
||||
libraryType = lib.TypeName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sectionData := BuildSections(sections, libraryID, libraryType)
|
||||
sectionData = MarkActiveConflictsSections(c.Request().Context(), h.db, user.ID, sectionData)
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData})
|
||||
@@ -168,10 +178,27 @@ func (h *DashboardHandler) RestoreSystemCollection(c *echo.Context) error {
|
||||
return c.JSON(http.StatusOK, map[string]string{"message": "System collection restored to defaults"})
|
||||
}
|
||||
|
||||
func BuildSections(sections []services.DashboardSection, currentLibraryID string) []SectionData {
|
||||
// BuildSections maps dashboard service sections to card data. When the
|
||||
// active library is audiobooks, system-collection titles/descriptions
|
||||
// swap to the listening wording at display time — the stored names stay
|
||||
// generic so restores and settings keep working across library types.
|
||||
func BuildSections(sections []services.DashboardSection, currentLibraryID, libraryType string) []SectionData {
|
||||
var result []SectionData
|
||||
|
||||
audioScope := libraryType == "audiobooks"
|
||||
|
||||
for _, ds := range sections {
|
||||
if audioScope && ds.IsSystem {
|
||||
switch ds.Title {
|
||||
case "Continue Reading":
|
||||
ds.Title = "Continue Listening"
|
||||
ds.Description = "Audiobooks you're currently listening to (0 < progress < 1)"
|
||||
case "Recently Read":
|
||||
ds.Title = "Recently Listened"
|
||||
ds.Description = "Audiobooks you've finished (progress >= 1)"
|
||||
}
|
||||
}
|
||||
|
||||
bookCards := make([]BookInfo, len(ds.Items))
|
||||
for i, item := range ds.Items {
|
||||
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
||||
@@ -180,6 +207,7 @@ func BuildSections(sections []services.DashboardSection, currentLibraryID string
|
||||
Title: item.Title,
|
||||
Author: textToString(item.Author),
|
||||
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||
FormatGroup: item.FormatGroup,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user