Skip to content

Commit 9a515f0

Browse files
committed
feat: Filter on published at
1 parent 4d4042a commit 9a515f0

17 files changed

+2094
-56
lines changed

graphql/schema/torrent_content.graphqls

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ input TorrentContentFacetsInput {
7474
releaseYear: ReleaseYearFacetInput
7575
videoResolution: VideoResolutionFacetInput
7676
videoSource: VideoSourceFacetInput
77+
publishedAt: String
7778
}
7879

7980
type ContentTypeAgg {
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package search
2+
3+
import (
4+
"errors"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
"time"
9+
10+
"github.com/bitmagnet-io/bitmagnet/internal/database/query"
11+
"gorm.io/gen/field"
12+
)
13+
14+
// timeNow is a replaceable function for time.Now, making testing easier
15+
var timeNow = time.Now
16+
17+
// TorrentContentPublishedAtCriteria returns a criteria that filters torrents by published_at timestamp
18+
func TorrentContentPublishedAtCriteria(timeFrame string) query.Criteria {
19+
return query.DaoCriteria{
20+
Conditions: func(ctx query.DbContext) ([]field.Expr, error) {
21+
if timeFrame == "" {
22+
return nil, nil
23+
}
24+
25+
startTime, endTime, err := parseTimeFrame(timeFrame)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
return []field.Expr{
31+
ctx.Query().TorrentContent.PublishedAt.Gte(startTime),
32+
ctx.Query().TorrentContent.PublishedAt.Lte(endTime),
33+
}, nil
34+
},
35+
}
36+
}
37+
38+
// ParseTimeFrame parses a time frame string into start and end times
39+
func parseTimeFrame(timeFrame string) (time.Time, time.Time, error) {
40+
timeFrame = strings.TrimSpace(timeFrame)
41+
42+
// Default end time is now
43+
endTime := timeNow().UTC()
44+
var startTime time.Time
45+
46+
// Empty string means no time filter
47+
if timeFrame == "" {
48+
return time.Time{}, time.Time{}, nil
49+
}
50+
51+
// Handle relative time expressions (e.g., "3h", "7d")
52+
if relativeMatch, _ := regexp.MatchString(`^\d+[smhdwMy]$`, timeFrame); relativeMatch {
53+
duration, err := parseRelativeTime(timeFrame)
54+
if err != nil {
55+
return time.Time{}, time.Time{}, err
56+
}
57+
startTime = endTime.Add(-duration)
58+
return startTime, endTime, nil
59+
}
60+
61+
// Handle special expressions
62+
switch timeFrame {
63+
case "today":
64+
startTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, time.UTC)
65+
return startTime, endTime, nil
66+
67+
case "yesterday":
68+
yesterday := endTime.AddDate(0, 0, -1)
69+
startTime = time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, time.UTC)
70+
endTime = time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 23, 59, 59, 999999999, time.UTC)
71+
return startTime, endTime, nil
72+
73+
case "this week":
74+
// Calculate days since start of week (Monday)
75+
daysSinceMonday := int(endTime.Weekday())
76+
if daysSinceMonday == 0 { // Sunday
77+
daysSinceMonday = 6
78+
} else {
79+
daysSinceMonday--
80+
}
81+
startTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day()-daysSinceMonday, 0, 0, 0, 0, time.UTC)
82+
return startTime, endTime, nil
83+
84+
case "last week":
85+
// Calculate days since start of week (Monday)
86+
daysSinceMonday := int(endTime.Weekday())
87+
if daysSinceMonday == 0 { // Sunday
88+
daysSinceMonday = 6
89+
} else {
90+
daysSinceMonday--
91+
}
92+
// Start of this week
93+
thisWeekStart := time.Date(endTime.Year(), endTime.Month(), endTime.Day()-daysSinceMonday, 0, 0, 0, 0, time.UTC)
94+
// Start of last week is 7 days before start of this week
95+
startTime = thisWeekStart.AddDate(0, 0, -7)
96+
// End of last week is 1 second before start of this week
97+
endTime = thisWeekStart.Add(-time.Second)
98+
return startTime, endTime, nil
99+
100+
case "this month":
101+
startTime = time.Date(endTime.Year(), endTime.Month(), 1, 0, 0, 0, 0, time.UTC)
102+
return startTime, endTime, nil
103+
104+
case "last month":
105+
// Start of this month
106+
thisMonthStart := time.Date(endTime.Year(), endTime.Month(), 1, 0, 0, 0, 0, time.UTC)
107+
// Start of last month
108+
startTime = thisMonthStart.AddDate(0, -1, 0)
109+
// End of last month is 1 second before start of this month
110+
endTime = thisMonthStart.Add(-time.Second)
111+
return startTime, endTime, nil
112+
113+
case "this year":
114+
startTime = time.Date(endTime.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
115+
return startTime, endTime, nil
116+
117+
case "last year":
118+
// Start of this year
119+
thisYearStart := time.Date(endTime.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
120+
// Start of last year
121+
startTime = thisYearStart.AddDate(-1, 0, 0)
122+
// End of last year is 1 second before start of this year
123+
endTime = thisYearStart.Add(-time.Second)
124+
return startTime, endTime, nil
125+
}
126+
127+
// Try to parse as absolute date range (e.g., "2023-01-01 to 2023-01-31")
128+
if strings.Contains(timeFrame, " to ") {
129+
parts := strings.Split(timeFrame, " to ")
130+
if len(parts) != 2 {
131+
return time.Time{}, time.Time{}, errors.New("invalid date range format. Expected 'start to end'")
132+
}
133+
134+
var err error
135+
startTime, err = parseDateString(strings.TrimSpace(parts[0]))
136+
if err != nil {
137+
return time.Time{}, time.Time{}, err
138+
}
139+
140+
endTime, err = parseDateString(strings.TrimSpace(parts[1]))
141+
if err != nil {
142+
return time.Time{}, time.Time{}, err
143+
}
144+
145+
// If end time doesn't have a time component, set it to end of day
146+
if endTime.Hour() == 0 && endTime.Minute() == 0 && endTime.Second() == 0 {
147+
endTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 23, 59, 59, 999999999, endTime.Location())
148+
}
149+
150+
return startTime, endTime, nil
151+
}
152+
153+
// Try to parse as a single date (e.g., "2023-01-01")
154+
parsedDate, err := parseDateString(timeFrame)
155+
if err == nil {
156+
startTime = parsedDate
157+
endTime = time.Date(parsedDate.Year(), parsedDate.Month(), parsedDate.Day(), 23, 59, 59, 999999999, parsedDate.Location())
158+
return startTime, endTime, nil
159+
}
160+
161+
return time.Time{}, time.Time{}, errors.New("could not parse time frame")
162+
}
163+
164+
// parseRelativeTime parses a relative time string (e.g., "3h", "7d") into a time.Duration
165+
func parseRelativeTime(relTime string) (time.Duration, error) {
166+
// Extract the number and unit
167+
re := regexp.MustCompile(`^(\d+)([smhdwMy])$`)
168+
matches := re.FindStringSubmatch(relTime)
169+
if len(matches) != 3 {
170+
return 0, errors.New("invalid relative time format. Expected format: '3h', '7d', etc.")
171+
}
172+
173+
value, err := strconv.Atoi(matches[1])
174+
if err != nil {
175+
return 0, err
176+
}
177+
178+
unit := matches[2]
179+
180+
// Convert to duration
181+
switch unit {
182+
case "s": // seconds
183+
return time.Duration(value) * time.Second, nil
184+
case "m": // minutes
185+
return time.Duration(value) * time.Minute, nil
186+
case "h": // hours
187+
return time.Duration(value) * time.Hour, nil
188+
case "d": // days
189+
return time.Duration(value) * 24 * time.Hour, nil
190+
case "w": // weeks
191+
return time.Duration(value) * 7 * 24 * time.Hour, nil
192+
case "M": // months (approximate)
193+
return time.Duration(value) * 30 * 24 * time.Hour, nil
194+
case "y": // years (approximate)
195+
return time.Duration(value) * 365 * 24 * time.Hour, nil
196+
default:
197+
return 0, errors.New("unknown time unit. Valid units: s, m, h, d, w, M, y")
198+
}
199+
}
200+
201+
// parseDateString attempts to parse a date string in various formats
202+
func parseDateString(dateStr string) (time.Time, error) {
203+
// Try standard formats
204+
formats := []string{
205+
"2006-01-02",
206+
"2006-01-02T15:04:05Z",
207+
"2006-01-02 15:04:05",
208+
"2006/01/02",
209+
"01/02/2006",
210+
"2-Jan-2006",
211+
"Jan 2, 2006",
212+
}
213+
214+
for _, format := range formats {
215+
t, err := time.Parse(format, dateStr)
216+
if err == nil {
217+
return t, nil
218+
}
219+
}
220+
221+
return time.Time{}, errors.New("could not parse date string")
222+
}

0 commit comments

Comments
 (0)