package services import ( "bookhoard/internal/database" "bookhoard/internal/utils" "context" "github.com/google/uuid" "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 uuid.UUID, limit, offset int) ([]SeriesInfo, int, error) { libUUID := pgtype.UUID{Bytes: libraryID, Valid: true} totalCount, err := s.db.GetDistinctSeriesCount(ctx, libUUID) if err != nil { return nil, 0, err } rows, err := s.db.GetDistinctSeries(ctx, database.GetDistinctSeriesParams{ LibraryID: libUUID, Limit: int32(limit), Offset: int32(offset), }) 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 uuid.UUID, seriesName string, limit int) ([]string, error) { covers, err := s.db.GetSeriesCovers(ctx, database.GetSeriesCoversParams{ LibraryID: pgtype.UUID{Bytes: libraryID, Valid: true}, Series: pgtype.Text{String: seriesName, Valid: true}, Limit: int32(limit), }) 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, libraryID uuid.UUID, seriesName string) ([]database.MediaItems, error) { return s.db.GetSeriesBooks(ctx, database.GetSeriesBooksParams{ LibraryID: pgtype.UUID{Bytes: libraryID, Valid: true}, Series: 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, } }