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
@@ -59,6 +59,9 @@ type CreateMediaItemRequest struct {
|
||||
AlternateInfo string `json:"alternate_info"` // JSON string
|
||||
ScanInformation string `json:"scan_information"`
|
||||
Summary string `json:"summary"`
|
||||
// Audio editorial facts (A2a)
|
||||
Narrators []string `json:"narrators"`
|
||||
Abridged bool `json:"abridged"`
|
||||
}
|
||||
|
||||
// UpdateMediaItemRequest represents the request for updating a media item
|
||||
@@ -98,6 +101,10 @@ type UpdateMediaItemRequest struct {
|
||||
AlternateInfo string `form:"alternate_info" json:"alternate_info"`
|
||||
ScanInformation string `form:"scan_information" json:"scan_information"`
|
||||
Summary string `form:"summary" json:"summary"`
|
||||
// Audiobook editorial facts (A2a): narrators verbatim display order;
|
||||
// abridged as the Libation/Audible flag.
|
||||
Narrators []string `form:"narrators" json:"narrators"`
|
||||
Abridged bool `form:"abridged" json:"abridged"`
|
||||
}
|
||||
|
||||
// CreateMediaNoteRequest represents the request for creating a media note
|
||||
@@ -897,6 +904,8 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
|
||||
"updated_at": item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
|
||||
"format_group": item.FormatGroup,
|
||||
"format_mimetype": textToString(item.FormatMimetype),
|
||||
"narrators": item.Narrators,
|
||||
"abridged": item.Abridged,
|
||||
// NEW: Reading direction fields
|
||||
"manga_type": textToString(item.MangaType),
|
||||
"reading_direction": textToString(item.ReadingDirection),
|
||||
@@ -1405,6 +1414,7 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error {
|
||||
|
||||
tagsSearch := utils.NormalizeTagsSearch(req.Tags)
|
||||
contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)
|
||||
narratorsSearch := utils.NormalizeNarratorsSearch(req.Narrators)
|
||||
|
||||
// Validate and normalize ISBN
|
||||
normalizedISBN, err := utils.NormalizeISBN(req.ISBN)
|
||||
@@ -1454,6 +1464,9 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error {
|
||||
SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
|
||||
Tags: req.Tags,
|
||||
TagsSearch: tagsSearch,
|
||||
Narrators: req.Narrators,
|
||||
NarratorsSearch: narratorsSearch,
|
||||
Abridged: req.Abridged,
|
||||
Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
|
||||
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
||||
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
||||
@@ -1519,6 +1532,7 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
|
||||
|
||||
tagsSearch := utils.NormalizeTagsSearch(req.Tags)
|
||||
contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)
|
||||
narratorsSearch := utils.NormalizeNarratorsSearch(req.Narrators)
|
||||
|
||||
normalizedISBN, err := utils.NormalizeISBN(req.ISBN)
|
||||
if err != nil && req.ISBN != "" {
|
||||
@@ -1572,6 +1586,9 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
|
||||
SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
|
||||
Tags: req.Tags,
|
||||
TagsSearch: tagsSearch,
|
||||
Narrators: req.Narrators,
|
||||
NarratorsSearch: narratorsSearch,
|
||||
Abridged: req.Abridged,
|
||||
Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
|
||||
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
||||
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
||||
@@ -2427,6 +2444,9 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
|
||||
if tags := c.QueryParam("tags"); tags != "" {
|
||||
return mh.handleFieldValuesSearch(c, userID.ID, "tags", tags)
|
||||
}
|
||||
if narrator := c.QueryParam("narrator"); narrator != "" {
|
||||
return mh.handleFieldValuesSearch(c, userID.ID, "narrator", narrator)
|
||||
}
|
||||
if series := c.QueryParam("series"); series != "" {
|
||||
return mh.handleFieldValuesSearch(c, userID.ID, "series", series)
|
||||
}
|
||||
@@ -2468,6 +2488,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
|
||||
genreFilter := c.QueryParam("genre_filter")
|
||||
publisherFilter := c.QueryParam("publisher_filter")
|
||||
tagsFilter := c.QueryParam("tags_filter")
|
||||
narratorFilter := c.QueryParam("narrator_filter")
|
||||
languageFilter := c.QueryParam("language_filter")
|
||||
yearMin, _ := strconv.Atoi(c.QueryParam("year_min"))
|
||||
yearMax, _ := strconv.Atoi(c.QueryParam("year_max"))
|
||||
@@ -2494,6 +2515,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
|
||||
GenreFilter: genreFilter,
|
||||
PublisherFilter: publisherFilter,
|
||||
TagsFilter: tagsFilter,
|
||||
NarratorFilter: narratorFilter,
|
||||
LanguageFilter: languageFilter,
|
||||
YearMin: yearMin,
|
||||
YearMax: yearMax,
|
||||
|
||||
Reference in new issue
Block a user