refactor(digest): Remove grouper abstraction and add date primary group #55
No files matched your search
@@ -0,0 +1,34 @@
|
||||
package digest
|
||||
|
||||
import (
|
||||
"miniflux-digest/internal/models"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GroupByDate(entries []*models.Entry) []*primaryGroup {
|
||||
groups := make(map[string]*primaryGroup)
|
||||
|
||||
for _, entry := range entries {
|
||||
dateKey := entry.Date.Format("2006-01-02")
|
||||
if _, ok := groups[dateKey]; !ok {
|
||||
groups[dateKey] = &primaryGroup{
|
||||
Title: entry.Date.Format("Jan 2, 2006"),
|
||||
}
|
||||
}
|
||||
groups[dateKey].Entries = append(groups[dateKey].Entries, entry)
|
||||
}
|
||||
|
||||
var result []*primaryGroup
|
||||
for _, pg := range groups {
|
||||
result = append(result, pg)
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
iDate, _ := time.Parse("Jan 2, 2006", result[i].Title)
|
||||
jDate, _ := time.Parse("Jan 2, 2006", result[j].Title)
|
||||
return iDate.Before(jDate) // Oldest dates first
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package digest
|
||||
|
||||
import (
|
||||
"miniflux-digest/internal/testutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGroupByDate(t *testing.T) {
|
||||
entries := testutil.CreateMockEntries(0)
|
||||
groups := GroupByDate(entries)
|
||||
|
||||
if len(groups) != 3 {
|
||||
t.Fatalf("Expected 3 date groups, got %d", len(groups))
|
||||
}
|
||||
|
||||
if groups[0].Title != "Jan 1, 2024" {
|
||||
t.Errorf("Expected first group to be Jan 1, 2024, got %s", groups[0].Title)
|
||||
}
|
||||
|
||||
if len(groups[0].Entries) != 3 {
|
||||
t.Errorf("Expected 3 entries in first group, got %d", len(groups[0].Entries))
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
package digest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"miniflux-digest/internal/models"
|
||||
"miniflux-digest/internal/utils"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DayGrouper struct{}
|
||||
const (
|
||||
DayGroupLayout = "2006-01-02"
|
||||
DayGroupTitleLayout = "Jan 2, 2006"
|
||||
)
|
||||
|
||||
func (g *DayGrouper) GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupDigestData, *string) {
|
||||
func SubGroupByDay(pgs []*primaryGroup) ([]*models.PrimaryGroupDigestData, *string) {
|
||||
var allPrimaryGroups []*models.PrimaryGroupDigestData
|
||||
|
||||
for _, pg := range pgs {
|
||||
@@ -50,9 +53,7 @@ func (g *DayGrouper) GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupDi
|
||||
})
|
||||
}
|
||||
|
||||
for _, seg := range sortedEntryGroups {
|
||||
seg.Title = fmt.Sprintf("%s - %s", pg.Title, seg.Title)
|
||||
}
|
||||
|
||||
|
||||
allPrimaryGroups = append(allPrimaryGroups, &models.PrimaryGroupDigestData{
|
||||
ID: pg.ID,
|
||||
|
||||
+32
-22
@@ -7,11 +7,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type primaryGroup struct {
|
||||
ID int64
|
||||
Title string
|
||||
Entries []*models.Entry
|
||||
}
|
||||
|
||||
|
||||
func GroupEntries(entries []*models.Entry, groupBy string) []*primaryGroup {
|
||||
groups := make(map[int64]*primaryGroup)
|
||||
@@ -62,29 +58,43 @@ func (s *digestServiceImpl) BuildDigestData(entries []*models.Entry, icons map[i
|
||||
iconsSlice = append(iconsSlice, icon)
|
||||
}
|
||||
|
||||
var groupBy, subGroupBy string
|
||||
var allPrimaryGroups []*models.PrimaryGroupDigestData
|
||||
var overallDigestSummary *string
|
||||
|
||||
switch view {
|
||||
case "date":
|
||||
groupBy = "category"
|
||||
subGroupBy = "date"
|
||||
primaryGroups := GroupByDate(entries)
|
||||
allPrimaryGroups, overallDigestSummary = SubGroupByFeed(primaryGroups)
|
||||
case "category":
|
||||
groupBy = "category"
|
||||
subGroupBy = "feed"
|
||||
primaryGroups := GroupEntries(entries, "category")
|
||||
allPrimaryGroups, overallDigestSummary = SubGroupByFeed(primaryGroups)
|
||||
sort.Slice(allPrimaryGroups, func(i, j int) bool {
|
||||
return allPrimaryGroups[i].Title < allPrimaryGroups[j].Title
|
||||
})
|
||||
case "ai":
|
||||
groupBy = "category"
|
||||
subGroupBy = "ai"
|
||||
default: // also "category"
|
||||
groupBy = "category"
|
||||
subGroupBy = "feed"
|
||||
primaryGroups := GroupEntries(entries, "category")
|
||||
allPrimaryGroups, overallDigestSummary = SubGroupByAI(primaryGroups, s.llmService)
|
||||
sort.Slice(allPrimaryGroups, func(i, j int) bool {
|
||||
return allPrimaryGroups[i].Title < allPrimaryGroups[j].Title
|
||||
})
|
||||
default: // category
|
||||
primaryGroups := GroupEntries(entries, "category")
|
||||
allPrimaryGroups, overallDigestSummary = SubGroupByFeed(primaryGroups)
|
||||
sort.Slice(allPrimaryGroups, func(i, j int) bool {
|
||||
return allPrimaryGroups[i].Title < allPrimaryGroups[j].Title
|
||||
})
|
||||
}
|
||||
|
||||
primaryGroups := GroupEntries(entries, groupBy)
|
||||
grouper := NewSubGrouper(subGroupBy, s.llmService)
|
||||
allPrimaryGroups, overallDigestSummary := grouper.GroupEntries(primaryGroups)
|
||||
|
||||
sort.Slice(allPrimaryGroups, func(i, j int) bool {
|
||||
return allPrimaryGroups[i].Title < allPrimaryGroups[j].Title
|
||||
})
|
||||
// Sort entries by date for date and category views
|
||||
if view == "date" || view == "category" {
|
||||
for _, pg := range allPrimaryGroups {
|
||||
for _, sg := range pg.SubGroups {
|
||||
sort.Slice(sg.Entries, func(i, j int) bool {
|
||||
return sg.Entries[i].Date.Before(sg.Entries[j].Date)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, primaryGroup := range allPrimaryGroups {
|
||||
primaryGroupTotalEntries := 0
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"miniflux-digest/internal/models"
|
||||
"miniflux-digest/internal/testutil"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/genai"
|
||||
@@ -51,8 +52,7 @@ func TestDayGrouper_GroupEntries(t *testing.T) {
|
||||
entries := testutil.CreateMockEntries(0)
|
||||
primaryGroupsMap := GroupEntries(entries, "category")
|
||||
|
||||
grouper := &DayGrouper{}
|
||||
groups, summary := grouper.GroupEntries(primaryGroupsMap)
|
||||
groups, summary := SubGroupByDay(primaryGroupsMap)
|
||||
|
||||
if summary != nil {
|
||||
t.Errorf("Expected an empty summary for DayGrouper, got %q", *summary)
|
||||
@@ -67,7 +67,7 @@ func TestDayGrouper_GroupEntries(t *testing.T) {
|
||||
t.Fatalf("Incorrect sub-groups for Category A: %+v", catAGroup)
|
||||
}
|
||||
|
||||
subGroup := findSubGroup(catAGroup, "Category A - Jan 2, 2024")
|
||||
subGroup := findSubGroup(catAGroup, "Jan 2, 2024")
|
||||
if subGroup == nil || len(subGroup.Entries) != 4 {
|
||||
t.Fatalf("Incorrect entries for sub-group: %+v", subGroup)
|
||||
}
|
||||
@@ -77,8 +77,7 @@ func TestFeedGrouper_GroupEntries(t *testing.T) {
|
||||
entries := testutil.CreateMockEntries(0)
|
||||
primaryGroupsMap := GroupEntries(entries, "category")
|
||||
|
||||
grouper := &FeedGrouper{}
|
||||
groups, summary := grouper.GroupEntries(primaryGroupsMap)
|
||||
groups, summary := SubGroupByFeed(primaryGroupsMap)
|
||||
|
||||
if summary != nil {
|
||||
t.Errorf("Expected an empty summary for FeedGrouper, got %q", *summary)
|
||||
@@ -119,8 +118,7 @@ func TestLLMGrouper_GroupEntries(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
grouper := &LLMGrouper{LLMService: mockLLM}
|
||||
groups, summary := grouper.GroupEntries(primaryGroupsMap)
|
||||
groups, summary := SubGroupByAI(primaryGroupsMap, mockLLM)
|
||||
|
||||
if summary == nil || *summary != "This is a summary of all entries." {
|
||||
t.Errorf("Expected summary 'This is a summary of all entries.', got %q", *summary)
|
||||
@@ -181,14 +179,21 @@ func TestDigestService_BuildDigestData(t *testing.T) {
|
||||
if len(overviewData.PrimaryGroups) != 3 {
|
||||
t.Errorf("Expected 3 primary groups, got %d", len(overviewData.PrimaryGroups))
|
||||
}
|
||||
// Check for date-based sub-grouping
|
||||
catAGroup := findPrimaryGroup(overviewData.PrimaryGroups, "Category A")
|
||||
if catAGroup == nil || len(catAGroup.SubGroups) != 1 {
|
||||
t.Fatalf("Incorrect sub-groups for Category A: %+v", catAGroup)
|
||||
// Check for date-based primary grouping
|
||||
dateGroup := findPrimaryGroup(overviewData.PrimaryGroups, "Jan 1, 2024")
|
||||
if dateGroup == nil {
|
||||
t.Fatalf("Did not find expected primary group 'Jan 1, 2024'")
|
||||
}
|
||||
subGroup := findSubGroup(catAGroup, "Category A - Jan 2, 2024")
|
||||
if subGroup == nil {
|
||||
t.Fatalf("Did not find expected sub-group 'Category A - Jan 2, 2024'")
|
||||
// Check for feed-based sub-grouping
|
||||
if len(dateGroup.SubGroups) != 2 {
|
||||
t.Fatalf("Incorrect sub-groups for Jan 1, 2024: %+v", dateGroup)
|
||||
}
|
||||
// Check that entries are sorted by date
|
||||
firstSubGroup := dateGroup.SubGroups[0]
|
||||
if !sort.SliceIsSorted(firstSubGroup.Entries, func(i, j int) bool {
|
||||
return firstSubGroup.Entries[i].Date.Before(firstSubGroup.Entries[j].Date)
|
||||
}) {
|
||||
t.Errorf("Entries in sub-group are not sorted by date")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -205,6 +210,13 @@ func TestDigestService_BuildDigestData(t *testing.T) {
|
||||
if catAGroup == nil || len(catAGroup.SubGroups) != 3 {
|
||||
t.Fatalf("Incorrect sub-groups for Category A: %+v", catAGroup)
|
||||
}
|
||||
// Check that entries are sorted by date
|
||||
firstSubGroup := catAGroup.SubGroups[0]
|
||||
if !sort.SliceIsSorted(firstSubGroup.Entries, func(i, j int) bool {
|
||||
return firstSubGroup.Entries[i].Date.Before(firstSubGroup.Entries[j].Date)
|
||||
}) {
|
||||
t.Errorf("Entries in sub-group are not sorted by date")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("view=ai", func(t *testing.T) {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package digest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"miniflux-digest/internal/models"
|
||||
"miniflux-digest/internal/utils"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type FeedGrouper struct{}
|
||||
|
||||
func (g *FeedGrouper) GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupDigestData, *string) {
|
||||
func SubGroupByFeed(pgs []*primaryGroup) ([]*models.PrimaryGroupDigestData, *string) {
|
||||
var allPrimaryGroups []*models.PrimaryGroupDigestData
|
||||
|
||||
for _, pg := range pgs {
|
||||
@@ -46,9 +44,7 @@ func (g *FeedGrouper) GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupD
|
||||
})
|
||||
}
|
||||
|
||||
for _, seg := range entryGroups {
|
||||
seg.Title = fmt.Sprintf("%s - %s", pg.Title, seg.Title)
|
||||
}
|
||||
|
||||
|
||||
allPrimaryGroups = append(allPrimaryGroups, &models.PrimaryGroupDigestData{
|
||||
ID: pg.ID,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package digest
|
||||
|
||||
import (
|
||||
"miniflux-digest/internal/app/services"
|
||||
"miniflux-digest/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
DayGroupLayout = "2006-01-02"
|
||||
DayGroupTitleLayout = "Jan 2, 2006"
|
||||
)
|
||||
|
||||
type SubGrouper interface {
|
||||
GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupDigestData, *string)
|
||||
}
|
||||
|
||||
func NewSubGrouper(subGroupBy string, llmService services.LLMService) SubGrouper {
|
||||
switch subGroupBy {
|
||||
case "ai":
|
||||
return &LLMGrouper{LLMService: llmService}
|
||||
case "feed":
|
||||
return &FeedGrouper{}
|
||||
default: // day
|
||||
return &DayGrouper{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package digest
|
||||
|
||||
import "miniflux-digest/internal/models"
|
||||
|
||||
type primaryGroup struct {
|
||||
ID int64
|
||||
Title string
|
||||
Entries []*models.Entry
|
||||
}
|
||||
@@ -28,9 +28,7 @@ type LLMResponse struct {
|
||||
} `json:"sub_groups"`
|
||||
}
|
||||
|
||||
type LLMGrouper struct {
|
||||
LLMService services.LLMService
|
||||
}
|
||||
|
||||
|
||||
type llmEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
@@ -120,7 +118,7 @@ var llmResponseSchema = &genai.Schema{
|
||||
},
|
||||
}
|
||||
|
||||
func (g *LLMGrouper) GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupDigestData, *string) {
|
||||
func SubGroupByAI(pgs []*primaryGroup, llmService services.LLMService) ([]*models.PrimaryGroupDigestData, *string) {
|
||||
var llmEntries []llmEntry
|
||||
entryMap := make(map[int64]*models.Entry)
|
||||
primaryGroupMap := make(map[int64]*primaryGroup)
|
||||
@@ -153,7 +151,7 @@ func (g *LLMGrouper) GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupDi
|
||||
ctx, cancel := context.WithTimeout(context.Background(), LLMTimeout)
|
||||
defer cancel()
|
||||
|
||||
llmResponse, err := g.LLMService.GenerateContent(ctx, prompt, llmResponseSchema)
|
||||
llmResponse, err := llmService.GenerateContent(ctx, prompt, llmResponseSchema)
|
||||
if err != nil {
|
||||
log.Printf("LLM service failed, falling back to feed grouping: %v\n", err)
|
||||
return fallbackToFeedGrouper(pgs)
|
||||
@@ -236,5 +234,5 @@ func (g *LLMGrouper) GroupEntries(pgs []*primaryGroup) ([]*models.PrimaryGroupDi
|
||||
}
|
||||
|
||||
func fallbackToFeedGrouper(pgs []*primaryGroup) ([]*models.PrimaryGroupDigestData, *string) {
|
||||
return (&FeedGrouper{}).GroupEntries(pgs)
|
||||
return SubGroupByFeed(pgs)
|
||||
}
|
||||
Reference in new issue
Block a user