Files
john-okeefe 6a352a6afb refactor(services): accept optional libraryID for All Libraries support
- dashboard_service.go: Change libraryID parameter from uuid.UUID to
  pgtype.UUID across GetDashboardSections, GetDashboardPreferences,
  and all helper methods. pgtype.UUID{Valid: false} now signals
  "no library filter" (All Libraries), which gets passed through
  to sqlc.narg() in the SQL layer.

- series_service.go: Drop libraryID parameter from GetSeriesBooks
  entirely. Series are not library-specific — all books in a series
  are shown regardless of which library they belong to.
2026-05-18 17:52:24 -04:00

141 lines
4.5 KiB
Go

package services
import (
"bookhoard/internal/database"
"bookhoard/internal/utils"
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type SeriesInfo struct {
Name string
BookCount int64
TotalInSeries int
CoverPaths []string
LastEntryAt string
}
type SeriesService struct {
db *database.Queries
}
func NewSeriesService(db *database.Queries) *SeriesService {
return &SeriesService{db: db}
}
func (s *SeriesService) GetSeriesPage(ctx context.Context, libraryID pgtype.UUID, limit, offset int) ([]SeriesInfo, int, error) {
totalCount, err := s.db.GetDistinctSeriesCount(ctx, libraryID)
if err != nil {
return nil, 0, err
}
rows, err := s.db.GetDistinctSeries(ctx, database.GetDistinctSeriesParams{
LibraryID: libraryID,
Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
})
if err != nil {
return nil, 0, err
}
var series []SeriesInfo
for _, row := range rows {
seriesName := row.Series.String
coverPaths, _ := s.GetSeriesCovers(ctx, libraryID, seriesName, 7)
totalInSeries := 0
if row.TotalInSeries != nil {
if v, ok := row.TotalInSeries.(int32); ok {
totalInSeries = int(v)
} else if v, ok := row.TotalInSeries.(int64); ok {
totalInSeries = int(v)
}
}
lastEntry := ""
if row.LastEntryAt != nil {
switch v := row.LastEntryAt.(type) {
case pgtype.Timestamptz:
if v.Valid {
lastEntry = v.Time.String()
}
case string:
lastEntry = v
}
}
if totalInSeries == 0 || totalInSeries < int(row.BookCount) {
totalInSeries = int(row.BookCount)
}
series = append(series, SeriesInfo{
Name: seriesName,
BookCount: row.BookCount,
TotalInSeries: totalInSeries,
CoverPaths: coverPaths,
LastEntryAt: lastEntry,
})
}
if series == nil {
series = []SeriesInfo{}
}
return series, int(totalCount), nil
}
func (s *SeriesService) GetSeriesCovers(ctx context.Context, libraryID pgtype.UUID, seriesName string, limit int) ([]string, error) {
covers, err := s.db.GetSeriesCovers(ctx, database.GetSeriesCoversParams{
LibraryID: libraryID,
Series: pgtype.Text{String: seriesName, Valid: true},
Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
})
if err != nil {
return nil, err
}
paths := make([]string, 0, len(covers))
for _, c := range covers {
resolved := utils.ResolveMediaURL(c.LibraryID, c.CoverImagePath)
if resolved != "" {
paths = append(paths, resolved)
}
}
return paths, nil
}
func (s *SeriesService) GetSeriesBooks(ctx context.Context, seriesName string) ([]database.MediaItems, error) {
return s.db.GetSeriesBooks(ctx, pgtype.Text{String: seriesName, Valid: true})
}
func continueSeriesRowToMediaItems(row database.GetContinueSeriesItemsRow) database.MediaItems {
return database.MediaItems{
ID: row.ID, LibraryID: row.LibraryID, Title: row.Title, Author: row.Author,
Isbn: row.Isbn, Description: row.Description, FilePath: row.FilePath,
FileSize: row.FileSize, MimeType: row.MimeType, CoverImagePath: row.CoverImagePath,
Series: row.Series, SeriesNumber: row.SeriesNumber, Tags: row.Tags, Asin: row.Asin,
DatePublished: row.DatePublished, Publisher: row.Publisher, Contributors: row.Contributors,
Language: row.Language, Edition: row.Edition, PageCount: row.PageCount,
Genre: row.Genre, CopyrightYear: row.CopyrightYear, GoodreadsID: row.GoodreadsID,
OpenlibraryID: row.OpenlibraryID, GoogleBooksID: row.GoogleBooksID,
AddedByAdminID: row.AddedByAdminID, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
FormatGroup: row.FormatGroup, FormatMimetype: row.FormatMimetype,
IsReflowable: row.IsReflowable, HasFixedLayout: row.HasFixedLayout,
TotalCharacters: row.TotalCharacters, ChapterCount: row.ChapterCount,
EntitlementID: row.EntitlementID, RevisionNumber: row.RevisionNumber,
KoboContentID: row.KoboContentID, KoboMetadata: row.KoboMetadata,
MangaType: row.MangaType, ReadingDirection: row.ReadingDirection,
SeriesCount: row.SeriesCount, Volume: row.Volume, Imprint: row.Imprint,
AgeRating: row.AgeRating, WebUrl: row.WebUrl, StoryArc: row.StoryArc,
IsBlackAndWhite: row.IsBlackAndWhite, MetadataNotes: row.MetadataNotes,
CommunityRating: row.CommunityRating, AlternateInfo: row.AlternateInfo,
ScanInformation: row.ScanInformation, Summary: row.Summary,
ChapterMetadata: row.ChapterMetadata, LibraryTypeName: row.LibraryTypeName,
TagsSearch: row.TagsSearch, ContributorsSearch: row.ContributorsSearch,
FileSha256: row.FileSha256, OpfIdentifier: row.OpfIdentifier,
OpfUuid: row.OpfUuid, HashConfidence: row.HashConfidence,
}
}