diff --git a/internal/handlers/media_detail.go b/internal/handlers/media_detail.go index cc7e0ed..847280d 100644 --- a/internal/handlers/media_detail.go +++ b/internal/handlers/media_detail.go @@ -7,6 +7,10 @@ import "bookhoard/internal/database" type MediaDetail struct { database.MediaItems // Embedded - ALL book fields available + // Absolute on-disk location (library folder + relative path), resolved by + // the page handler so the UI can show where the file lives. + FileLocation string `json:"file_location"` + // User-specific data Rating *database.MediaRatings `json:"rating,omitempty"` Collections []database.Collections `json:"collections"` diff --git a/internal/router/frontend.go b/internal/router/frontend.go index cdae66c..265c1a8 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -7,6 +7,8 @@ import ( "fmt" "log" "net/http" + "os" + "path/filepath" "strconv" "time" @@ -984,12 +986,12 @@ func registerFrontendRoutes(cfg *Config) { } totalData := counts.ProgressCount + counts.HighlightsCount + counts.BookmarksCount + counts.NotesCount + counts.CollectionsCount conflict.Items = append(conflict.Items, templates.HashConflictItemData{ - ID: uuid.UUID(mi.ID.Bytes).String(), - Title: mi.Title, - Author: mi.Author.String, - FilePath: mi.FilePath, - FileSize: mi.FileSize.Int64, - UsageSummary: fmt.Sprintf("%d progress, %d highlights, %d bookmarks, %d notes, %d collections", + ID: uuid.UUID(mi.ID.Bytes).String(), + Title: mi.Title, + Author: mi.Author.String, + FilePath: mi.FilePath, + FileSize: mi.FileSize.Int64, + UsageSummary: fmt.Sprintf("%d progress, %d highlights, %d bookmarks, %d notes, %d collections", counts.ProgressCount, counts.HighlightsCount, counts.BookmarksCount, counts.NotesCount, counts.CollectionsCount), HasReadingData: totalData > 0, }) @@ -1090,9 +1092,9 @@ func registerFrontendRoutes(cfg *Config) { // already have their own dedicated UI cards (timezone dropdown, scan // settings) so they aren't listed twice. dedicatedUI := map[string]bool{ - "default_timezone": true, + "default_timezone": true, "scan_poll_interval_seconds": true, - "auto_scan_enabled": true, + "auto_scan_enabled": true, } var tunableSettings []templates.SettingEntry if cfg.Settings != nil { @@ -1267,6 +1269,23 @@ func registerFrontendRoutes(cfg *Config) { mediaItem.CoverImagePath = pgtype.Text{String: resolvedPath, Valid: true} } + // Resolve the on-disk location (library folder + relative path) so the + // detail page can show where the file lives, even for sparse metadata. + // Admin-only: everyday users never receive the absolute path. + fileLocation := "" + if user.Role == "admin" { + fileLocation = mediaItem.FilePath + if folders, ferr := cfg.Queries.GetLibraryFolders(c.Request().Context(), mediaItem.LibraryID); ferr == nil { + for _, folder := range folders { + candidate := filepath.Join(folder.FolderPath, mediaItem.FilePath) + if _, serr := os.Stat(candidate); serr == nil { + fileLocation = candidate + break + } + } + } + } + // Fetch rating var rating *database.MediaRatings userRating, err := cfg.Queries.GetMediaRating(c.Request().Context(), database.GetMediaRatingParams{ @@ -1330,6 +1349,7 @@ func registerFrontendRoutes(cfg *Config) { // Assemble response (no field duplication!) detail := handlers.MediaDetail{ MediaItems: mediaItem, // Embedded - ALL fields available + FileLocation: fileLocation, Rating: rating, Collections: collections, ReadingProgress: progress, diff --git a/templates/book_detail.templ b/templates/book_detail.templ index c394512..f1c762d 100644 --- a/templates/book_detail.templ +++ b/templates/book_detail.templ @@ -107,10 +107,12 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) { { book.NotesCount + book.HighlightsCount } } + if user.Role == "admin" { + }
Format
{ book.MimeType.String }
File Size
-{ formatFileSize(book.FileSize.Int64) }
-File Size
+{ formatFileSize(book.FileSize.Int64) }
+Location
+{ book.FileLocation }
+