Diff
1diff --git a/db/db.go b/db/db.go
2index c125121e19e6a3a41894f5c6d3b5652dd83f3096..8c1595eb9a42dce6544524e5c9ea326049ece9cd 100644
3--- a/db/db.go
4+++ b/db/db.go
5@@ -27,7 +27,7 @@ type DB interface {
6 // GetNewVideos returns a list of unwatched videos from all subscribed channels
7 GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
8 // GetWatchedVideos returns a list of all watched videos
9- GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
10+ GetWatchedVideos(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error)
11 // HasVideo returns true if the video with the given ID exists in the DB
12 HasVideo(ctx context.Context, videoID string) (bool, error)
13 // AddVideo adds a newly published video to the database
14diff --git a/db/videos.go b/db/videos.go
15index 8dfad98b2defa98feed633c2d807003b3fd59977..34b27f9416eaf5d3fc8f44ada4d945aeb9cf446a 100644
16--- a/db/videos.go
17+++ b/db/videos.go
18@@ -57,7 +57,9 @@ func (d *postgresDB) GetNewVideos(ctx context.Context, sortDesc bool) ([]models.
19 return videos, nil
20 }
21
22-func (d *postgresDB) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
23+func (d *postgresDB) GetWatchedVideos(
24+ ctx context.Context, sortDesc bool, limit int, offset int,
25+) ([]models.Video, error) {
26 query := `
27 SELECT
28 videos.id
29@@ -77,8 +79,9 @@ func (d *postgresDB) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]mod
30 if sortDesc {
31 query += " DESC"
32 }
33+ query += " LIMIT $1 OFFSET $2"
34
35- rows, err := d.db.QueryContext(ctx, query)
36+ rows, err := d.db.QueryContext(ctx, query, limit, offset)
37 if err != nil {
38 d.l.Error("Failed to query for watched videos", "call", "sql.QueryContext", "error", err)
39 return nil, err
40diff --git a/handler/handler.go b/handler/handler.go
41index ed66479b9fb36156f39c38fb2c1b8deac656fec0..0557c14080f60cb742810b5c4b38494a1dd04405 100644
42--- a/handler/handler.go
43+++ b/handler/handler.go
44@@ -15,7 +15,7 @@ type Handler interface {
45 UnsubscribeFromChannel(ctx context.Context, channelID string) error
46 ListChannels(ctx context.Context) ([]models.Channel, error)
47 GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
48- GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
49+ GetWatchedVideos(ctx context.Context, sortDesc bool, page int) ([]models.Video, error)
50 FetchVideos(ctx context.Context) error
51 MarkVideoAsWatched(ctx context.Context, videoID string) error
52 MarkVideoAsUnwatched(ctx context.Context, videoID string) error
53diff --git a/handler/videos.go b/handler/videos.go
54index 108e1bd29fd81675d33815da724238baef4c8485..856e994e3ed40a51bf03fd6ff339a1029e4eaf40 100644
55--- a/handler/videos.go
56+++ b/handler/videos.go
57@@ -20,8 +20,14 @@ func (h *handler) GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Vid
58 return h.db.GetNewVideos(ctx, sortDesc)
59 }
60
61-func (h *handler) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
62- return h.db.GetWatchedVideos(ctx, sortDesc)
63+const WatchedVideosPageSize = 100
64+
65+func (h *handler) GetWatchedVideos(ctx context.Context, sortDesc bool, page int) ([]models.Video, error) {
66+ if page < 1 {
67+ page = 1
68+ }
69+ offset := (page - 1) * WatchedVideosPageSize
70+ return h.db.GetWatchedVideos(ctx, sortDesc, WatchedVideosPageSize, offset)
71 }
72
73 func (h *handler) addVideosForChannel(ctx context.Context, parsedChannel *feedparser.Channel) {
74diff --git a/httpserver/ytrssil/videos.go b/httpserver/ytrssil/videos.go
75index 663abadd7e853c07f1aca97911382cb4a610766b..c81f088c8ca7017f434c9f9abccf781a159a2036 100644
76--- a/httpserver/ytrssil/videos.go
77+++ b/httpserver/ytrssil/videos.go
78@@ -17,7 +17,7 @@ func (srv *server) GetNewVideosJSON(c *gin.Context) {
79 }
80
81 func (srv *server) GetWatchedVideosJSON(c *gin.Context) {
82- videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), false)
83+ videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), false, 1)
84 if err != nil {
85 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
86 return
87diff --git a/httpserver/ytrssil/videos_pages.go b/httpserver/ytrssil/videos_pages.go
88index cf81b9d7bd090d5970059a86b2798e5fa7929bee..f4044398c5cec2aef7b232b107724d65b7f05fae 100644
89--- a/httpserver/ytrssil/videos_pages.go
90+++ b/httpserver/ytrssil/videos_pages.go
91@@ -2,6 +2,7 @@ package ytrssil
92
93 import (
94 "net/http"
95+ "strconv"
96
97 "github.com/gin-gonic/gin"
98
99@@ -22,7 +23,14 @@ func (srv server) NewVideosPage(c *gin.Context) {
100 }
101
102 func (srv server) WatchedVideosPage(c *gin.Context) {
103- videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), true)
104+ page := 1
105+ if pageParam := c.Query("page"); pageParam != "" {
106+ if parsedPage, err := strconv.Atoi(pageParam); err == nil && parsedPage > 0 {
107+ page = parsedPage
108+ }
109+ }
110+
111+ videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), true, page)
112 if err != nil {
113 returnErr(c, http.StatusInternalServerError, err)
114 return
115@@ -30,7 +38,7 @@ func (srv server) WatchedVideosPage(c *gin.Context) {
116
117 c.Render(http.StatusOK, pages.TemplRenderer{
118 Ctx: c.Request.Context(),
119- Component: pages.WatchedVideosPage(videos),
120+ Component: pages.WatchedVideosPage(videos, page),
121 })
122 }
123
124diff --git a/mocks/db/db.go b/mocks/db/db.go
125index 324291c1beaaa65ba316db256d5a0935b14870f7..3be205326db4a789e1dbf4df1e2fdb7ce507e574 100644
126--- a/mocks/db/db.go
127+++ b/mocks/db/db.go
128@@ -27,7 +27,7 @@ var _ db.DB = &DBMock{}
129 // GetNewVideosFunc: func(ctx context.Context, sortDesc bool) ([]models.Video, error) {
130 // panic("mock out the GetNewVideos method")
131 // },
132-// GetWatchedVideosFunc: func(ctx context.Context, sortDesc bool) ([]models.Video, error) {
133+// GetWatchedVideosFunc: func(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error) {
134 // panic("mock out the GetWatchedVideos method")
135 // },
136 // HasVideoFunc: func(ctx context.Context, videoID string) (bool, error) {
137@@ -62,7 +62,7 @@ type DBMock struct {
138 GetNewVideosFunc func(ctx context.Context, sortDesc bool) ([]models.Video, error)
139
140 // GetWatchedVideosFunc mocks the GetWatchedVideos method.
141- GetWatchedVideosFunc func(ctx context.Context, sortDesc bool) ([]models.Video, error)
142+ GetWatchedVideosFunc func(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error)
143
144 // HasVideoFunc mocks the HasVideo method.
145 HasVideoFunc func(ctx context.Context, videoID string) (bool, error)
146@@ -106,6 +106,10 @@ type DBMock struct {
147 Ctx context.Context
148 // SortDesc is the sortDesc argument value.
149 SortDesc bool
150+ // Limit is the limit argument value.
151+ Limit int
152+ // Offset is the offset argument value.
153+ Offset int
154 }
155 // HasVideo holds details about calls to the HasVideo method.
156 HasVideo []struct {
157@@ -240,21 +244,25 @@ func (mock *DBMock) GetNewVideosCalls() []struct {
158 }
159
160 // GetWatchedVideos calls GetWatchedVideosFunc.
161-func (mock *DBMock) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
162+func (mock *DBMock) GetWatchedVideos(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error) {
163 if mock.GetWatchedVideosFunc == nil {
164 panic("DBMock.GetWatchedVideosFunc: method is nil but DB.GetWatchedVideos was just called")
165 }
166 callInfo := struct {
167 Ctx context.Context
168 SortDesc bool
169+ Limit int
170+ Offset int
171 }{
172 Ctx: ctx,
173 SortDesc: sortDesc,
174+ Limit: limit,
175+ Offset: offset,
176 }
177 mock.lockGetWatchedVideos.Lock()
178 mock.calls.GetWatchedVideos = append(mock.calls.GetWatchedVideos, callInfo)
179 mock.lockGetWatchedVideos.Unlock()
180- return mock.GetWatchedVideosFunc(ctx, sortDesc)
181+ return mock.GetWatchedVideosFunc(ctx, sortDesc, limit, offset)
182 }
183
184 // GetWatchedVideosCalls gets all the calls that were made to GetWatchedVideos.
185@@ -264,10 +272,14 @@ func (mock *DBMock) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]mode
186 func (mock *DBMock) GetWatchedVideosCalls() []struct {
187 Ctx context.Context
188 SortDesc bool
189+ Limit int
190+ Offset int
191 } {
192 var calls []struct {
193 Ctx context.Context
194 SortDesc bool
195+ Limit int
196+ Offset int
197 }
198 mock.lockGetWatchedVideos.RLock()
199 calls = mock.calls.GetWatchedVideos
200diff --git a/pages/watched_videos.templ b/pages/watched_videos.templ
201index 60e667580608834f8437cc67a3788b5afd255ea9..963c4b29f8107b0785e7087b5d2383d6bf53ecc4 100644
202--- a/pages/watched_videos.templ
203+++ b/pages/watched_videos.templ
204@@ -35,7 +35,7 @@ templ watchedVideoCard(video models.Video) {
205 </div>
206 }
207
208-templ WatchedVideosPage(videos []models.Video) {
209+templ WatchedVideosPage(videos []models.Video, currentPage int) {
210 @BaseLayout("ytrssil - Watched Videos", "watched") {
211 <div class="row">
212 for _, video := range videos {
213@@ -44,5 +44,28 @@ templ WatchedVideosPage(videos []models.Video) {
214 </div>
215 }
216 </div>
217+ <div class="row mt-4 mb-4">
218+ <div class="col-12 d-flex justify-content-center align-items-center gap-3">
219+ if currentPage > 1 {
220+ <a href={ templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage-1)) } class="btn btn-primary">
221+ <i class="bi bi-arrow-left"></i> Previous
222+ </a>
223+ } else {
224+ <button class="btn btn-primary" disabled>
225+ <i class="bi bi-arrow-left"></i> Previous
226+ </button>
227+ }
228+ <span class="text-light">Page { fmt.Sprintf("%d", currentPage) }</span>
229+ if len(videos) == 100 {
230+ <a href={ templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage+1)) } class="btn btn-primary">
231+ Next <i class="bi bi-arrow-right"></i>
232+ </a>
233+ } else {
234+ <button class="btn btn-primary" disabled>
235+ Next <i class="bi bi-arrow-right"></i>
236+ </button>
237+ }
238+ </div>
239+ </div>
240 }
241 }
242diff --git a/pages/watched_videos_templ.go b/pages/watched_videos_templ.go
243index a5c5c218b81f9e5a5551d5436778b19ae1a28b24..322d8f923f7f9fdc3f23c392e650e47bb2148a6e 100644
244--- a/pages/watched_videos_templ.go
245+++ b/pages/watched_videos_templ.go
246@@ -116,7 +116,7 @@ func watchedVideoCard(video models.Video) templ.Component {
247 })
248 }
249
250-func WatchedVideosPage(videos []models.Video) templ.Component {
251+func WatchedVideosPage(videos []models.Video, currentPage int) templ.Component {
252 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
253 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
254 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
255@@ -193,7 +193,76 @@ func WatchedVideosPage(videos []models.Video) templ.Component {
256 return templ_7745c5c3_Err
257 }
258 }
259- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
260+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"row mt-4 mb-4\"><div class=\"col-12 d-flex justify-content-center align-items-center gap-3\">")
261+ if templ_7745c5c3_Err != nil {
262+ return templ_7745c5c3_Err
263+ }
264+ if currentPage > 1 {
265+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<a href=\"")
266+ if templ_7745c5c3_Err != nil {
267+ return templ_7745c5c3_Err
268+ }
269+ var templ_7745c5c3_Var11 templ.SafeURL
270+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage-1)))
271+ if templ_7745c5c3_Err != nil {
272+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 50, Col: 76}
273+ }
274+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
275+ if templ_7745c5c3_Err != nil {
276+ return templ_7745c5c3_Err
277+ }
278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" class=\"btn btn-primary\"><i class=\"bi bi-arrow-left\"></i> Previous</a> ")
279+ if templ_7745c5c3_Err != nil {
280+ return templ_7745c5c3_Err
281+ }
282+ } else {
283+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button class=\"btn btn-primary\" disabled><i class=\"bi bi-arrow-left\"></i> Previous</button> ")
284+ if templ_7745c5c3_Err != nil {
285+ return templ_7745c5c3_Err
286+ }
287+ }
288+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"text-light\">Page ")
289+ if templ_7745c5c3_Err != nil {
290+ return templ_7745c5c3_Err
291+ }
292+ var templ_7745c5c3_Var12 string
293+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage))
294+ if templ_7745c5c3_Err != nil {
295+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 58, Col: 66}
296+ }
297+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
298+ if templ_7745c5c3_Err != nil {
299+ return templ_7745c5c3_Err
300+ }
301+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span> ")
302+ if templ_7745c5c3_Err != nil {
303+ return templ_7745c5c3_Err
304+ }
305+ if len(videos) == 100 {
306+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<a href=\"")
307+ if templ_7745c5c3_Err != nil {
308+ return templ_7745c5c3_Err
309+ }
310+ var templ_7745c5c3_Var13 templ.SafeURL
311+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage+1)))
312+ if templ_7745c5c3_Err != nil {
313+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 60, Col: 76}
314+ }
315+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
316+ if templ_7745c5c3_Err != nil {
317+ return templ_7745c5c3_Err
318+ }
319+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"btn btn-primary\">Next <i class=\"bi bi-arrow-right\"></i></a>")
320+ if templ_7745c5c3_Err != nil {
321+ return templ_7745c5c3_Err
322+ }
323+ } else {
324+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button class=\"btn btn-primary\" disabled>Next <i class=\"bi bi-arrow-right\"></i></button>")
325+ if templ_7745c5c3_Err != nil {
326+ return templ_7745c5c3_Err
327+ }
328+ }
329+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div>")
330 if templ_7745c5c3_Err != nil {
331 return templ_7745c5c3_Err
332 }