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