feat(series): add SeriesService and wire continue-series into dashboard
Create SeriesService with methods for paginated series listing, cover path resolution, series book listing, and a conversion helper for GetContinueSeriesItemsRow to MediaItems. Wire the continue-series query type into DashboardService's getCollectionItemsByQueryType switch and add its metadata to the RestoreSystemCollection default collection map.
This commit is contained in:
@@ -292,6 +292,20 @@ func (s *DashboardService) getCollectionItemsByQueryType(ctx context.Context, co
|
||||
LibraryID: pgtype.UUID{Bytes: libraryID, Valid: true},
|
||||
Limit: int32(limit),
|
||||
})
|
||||
case "continue-series":
|
||||
rows, err := s.db.GetContinueSeriesItems(ctx, database.GetContinueSeriesItemsParams{
|
||||
LibraryID: pgtype.UUID{Bytes: libraryID, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
||||
Limit: int32(limit),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]database.MediaItems, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, continueSeriesRowToMediaItems(row))
|
||||
}
|
||||
return items, nil
|
||||
default:
|
||||
return []database.MediaItems{}, nil
|
||||
}
|
||||
@@ -407,6 +421,7 @@ func (s *DashboardService) RestoreSystemCollection(ctx context.Context, userID u
|
||||
"Recently Added": {"Newly added items to this library", "🆕", "#9ece6a", 2, "recently-added"},
|
||||
"Recently Read": {"Books you've finished (progress >= 1)", "✅", "#e0af68", 3, "recently-read"},
|
||||
"Not Started": {"Books you haven't read yet (progress = 0 or no record)", "📕", "#f7768e", 4, "not-started"},
|
||||
"Continue Series": {"Next book in series you're reading", "📚", "#bb9af7", 5, "continue-series"},
|
||||
}
|
||||
|
||||
meta, exists := defaultMetadata[collectionName]
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user