799c6dfa4b17e0816a6a107f32bb731835ece760
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/.gitignore b/.gitignore
2index 82009254414ac175f140ffd4d0b10fbd5ef42930..456dd41db6df8483997d7c8d2af356a33cca2051 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -1,17 +1,6 @@
6-# Binaries for programs and plugins
7-*.exe
8-*.exe~
9-*.dll
10-*.so
11-*.dylib
12-
13 # Test binary, built with `go test -c`
14 *.test
15
16-# Output of the go coverage tool, specifically when used with LiteIDE
17-*.out
18-
19-debug
20 # editor temp files
21 *.swp
22 *.swo
23@@ -20,13 +9,11 @@ debug
24 # Local env files
25 .env
26
27-# Build artifacts and cache
28-**/bin
29-/dist/
30+# Build artifacts and tools
31+bin/
32+dist/
33+
34+# Static web assets
35 assets/vendor/
36
37-# Visual studio code configuration
38 .vscode/
39-
40-# secrets for local use that cannot be encrypted
41-k8s/local/patches/core-http-config.yaml
42diff --git a/assets/load.sh b/assets/load.sh
43index 62f2a9dc5da1f4fadf0bcbd881172743bdaccc6e..a07d9c1ea29912567b6a4d358477c21c35bbdde0 100755
44--- a/assets/load.sh
45+++ b/assets/load.sh
46@@ -2,9 +2,9 @@ set -e
47
48
49 mkdir -p "assets/vendor/fonts"
50-curl -o "assets/vendor/bootstrap.min.css" https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css
51-curl -o "assets/vendor/bootstrap.bundle.min.js" https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js
52-curl -o "assets/vendor/bootstrap-icons.min.css" https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css
53-curl -o "assets/vendor/fonts/bootstrap-icons.woff2" "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a="
54-curl -o "assets/vendor/fuse.js" https://cdn.jsdelivr.net/npm/fuse.js@7.0.0
55-curl -o "assets/vendor/datastar.js" https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.8/bundles/datastar.js
56+curl -sSfo "assets/vendor/bootstrap.min.css" https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css
57+curl -sSfo "assets/vendor/bootstrap.bundle.min.js" https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js
58+curl -sSfo "assets/vendor/bootstrap-icons.min.css" https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css
59+curl -sSfo "assets/vendor/fonts/bootstrap-icons.woff2" "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a="
60+curl -sSfo "assets/vendor/fuse.js" https://cdn.jsdelivr.net/npm/fuse.js@7.0.0
61+curl -sSfo "assets/vendor/datastar.js" https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.8/bundles/datastar.js
62diff --git a/db/videos.go b/db/videos.go
63index f2054f71de15009e6c9d0411626a2dbce7469691..04a3c733966ef274402def8ad22ea62898e82bd4 100644
64--- a/db/videos.go
65+++ b/db/videos.go
66@@ -14,6 +14,7 @@ func (db *postgresDB) GetNewVideos(ctx context.Context, sortDesc bool) ([]models
67 , title
68 , published_timestamp
69 , is_short
70+ , is_live
71 , duration
72 , progress
73 , is_discarded
74@@ -48,6 +49,7 @@ func (db *postgresDB) GetNewVideos(ctx context.Context, sortDesc bool) ([]models
75 &video.Title,
76 &video.PublishedTime,
77 &video.IsShort,
78+ &video.IsLive,
79 &video.DurationSeconds,
80 &video.ProgressSeconds,
81 &video.IsDiscarded,
82@@ -78,6 +80,7 @@ func (db *postgresDB) GetWatchedVideos(
83 , published_timestamp
84 , watch_timestamp
85 , is_short
86+ , is_live
87 , duration
88 , progress
89 , is_discarded
90@@ -114,6 +117,7 @@ func (db *postgresDB) GetWatchedVideos(
91 &video.PublishedTime,
92 &video.WatchTime,
93 &video.IsShort,
94+ &video.IsLive,
95 &video.DurationSeconds,
96 &video.ProgressSeconds,
97 &video.IsDiscarded,
98@@ -156,9 +160,10 @@ func (db *postgresDB) AddVideo(ctx context.Context, video models.Video, channelI
99 , published_timestamp
100 , duration
101 , is_short
102+ , is_live
103 , channel_id
104 , is_discarded
105- ) VALUES ($1, $2, $3, $4, $5, $6, $7)
106+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
107 ON CONFLICT DO NOTHING
108 `
109
110@@ -170,6 +175,7 @@ func (db *postgresDB) AddVideo(ctx context.Context, video models.Video, channelI
111 video.PublishedTime,
112 video.DurationSeconds,
113 video.IsShort,
114+ video.IsLive,
115 channelID,
116 isDiscarded,
117 )
118@@ -219,6 +225,7 @@ func (db *postgresDB) SetVideoProgress(ctx context.Context, videoID string, prog
119 , title
120 , published_timestamp
121 , is_short
122+ , is_live
123 , duration
124 , progress
125 , channels.name
126@@ -234,6 +241,7 @@ func (db *postgresDB) SetVideoProgress(ctx context.Context, videoID string, prog
127 &video.Title,
128 &video.PublishedTime,
129 &video.IsShort,
130+ &video.IsLive,
131 &video.DurationSeconds,
132 &video.ProgressSeconds,
133 &video.ChannelName,
134@@ -254,6 +262,7 @@ func (db *postgresDB) GetVideo(ctx context.Context, videoID string) (*models.Vid
135 , title
136 , published_timestamp
137 , is_short
138+ , is_live
139 , duration
140 , progress
141 , watch_timestamp
142@@ -276,6 +285,7 @@ func (db *postgresDB) GetVideo(ctx context.Context, videoID string) (*models.Vid
143 &video.Title,
144 &video.PublishedTime,
145 &video.IsShort,
146+ &video.IsLive,
147 &video.DurationSeconds,
148 &video.ProgressSeconds,
149 &video.WatchTime,
150diff --git a/lib/clients/youtube/get_durations.go b/lib/clients/youtube/get_durations.go
151index bad0c3c1eec49432f17c50ee3a22c622feaf1d9e..c604921f0a4c88594931f481836b35766e1d7d16 100644
152--- a/lib/clients/youtube/get_durations.go
153+++ b/lib/clients/youtube/get_durations.go
154@@ -19,17 +19,23 @@ type APIVideoListResponse struct {
155 }
156
157 type APIVideo struct {
158- ID string `json:"id"`
159- Snippet APISnippet `json:"snippet"`
160- ContentDetails APIContentDetails `json:"contentDetails"`
161- Player APIPlayer `json:"player"`
162+ ID string `json:"id"`
163+ Snippet APISnippet `json:"snippet"`
164+ ContentDetails APIContentDetails `json:"contentDetails"`
165+ Player APIPlayer `json:"player"`
166+ LiveStreamingDetails *APILiveStreamingDetails `json:"liveStreamingDetails"`
167 }
168
169 type APISnippet struct {
170- PublishedAt string `json:"publishedAt"`
171- ChannelID string `json:"channelId"`
172- Title string `json:"title"`
173- ChannelName string `json:"channelTitle"`
174+ PublishedAt string `json:"publishedAt"`
175+ ChannelID string `json:"channelId"`
176+ Title string `json:"title"`
177+ ChannelName string `json:"channelTitle"`
178+ LiveBroadcastContent string `json:"liveBroadcastContent"`
179+}
180+
181+type APILiveStreamingDetails struct {
182+ ActualEndTime string `json:"actualEndTime"`
183 }
184
185 type APIPlayer struct {
186@@ -63,7 +69,7 @@ func (c *youTubeClient) GetVideoDurations(ctx context.Context, videos map[string
187 videoIDs := ids.String()
188 query := url.Values{}
189 query.Add("id", videoIDs)
190- query.Add("part", "contentDetails")
191+ query.Add("part", "contentDetails,liveStreamingDetails")
192 query.Add("key", c.apiKey)
193
194 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "", nil)
195@@ -115,6 +121,7 @@ func (c *youTubeClient) GetVideoDurations(ctx context.Context, videos map[string
196 return fmt.Errorf("failed to parse video duration [%v]: %w", v.ContentDetails.Duration, err)
197 }
198 videos[v.ID].DurationSeconds = int(math.Round(duration.Seconds()))
199+ videos[v.ID].IsLive = v.LiveStreamingDetails != nil && v.LiveStreamingDetails.ActualEndTime == ""
200 }
201
202 return nil
203diff --git a/lib/clients/youtube/get_video_metadata.go b/lib/clients/youtube/get_video_metadata.go
204index 46f8488c8abf59a657afea055536820f6b73984d..39fa0e1f886092b588b266138743e354de4ef88c 100644
205--- a/lib/clients/youtube/get_video_metadata.go
206+++ b/lib/clients/youtube/get_video_metadata.go
207@@ -16,7 +16,7 @@ import (
208 func (c *youTubeClient) GetVideoMetadata(ctx context.Context, videoID string) (*models.Video, error) {
209 query := url.Values{}
210 query.Add("id", videoID)
211- query.Add("part", "snippet,contentDetails,player")
212+ query.Add("part", "snippet,contentDetails,player,liveStreamingDetails")
213 query.Add("key", c.apiKey)
214
215 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "", nil)
216@@ -85,5 +85,7 @@ func (c *youTubeClient) GetVideoMetadata(ctx context.Context, videoID string) (*
217 return nil, err
218 }
219
220+ video.IsLive = videoData.LiveStreamingDetails != nil && videoData.LiveStreamingDetails.ActualEndTime == ""
221+
222 return video, nil
223 }
224diff --git a/migrations/000009_add_is_live.down.sql b/migrations/000009_add_is_live.down.sql
225new file mode 100644
226index 0000000000000000000000000000000000000000..923c4a5ced2cb8440397555cc4a97360745e5734
227--- /dev/null
228+++ b/migrations/000009_add_is_live.down.sql
229@@ -0,0 +1 @@
230+ALTER TABLE videos DROP COLUMN is_live;
231diff --git a/migrations/000009_add_is_live.up.sql b/migrations/000009_add_is_live.up.sql
232new file mode 100644
233index 0000000000000000000000000000000000000000..f00579b55925f55c1e2ccaf2dbbb4be9e946afac
234--- /dev/null
235+++ b/migrations/000009_add_is_live.up.sql
236@@ -0,0 +1 @@
237+ALTER TABLE videos ADD COLUMN is_live BOOLEAN DEFAULT false;
238diff --git a/models/video.go b/models/video.go
239index 45e92040a39dc163eea9fa2a4eb19004e886b89d..73fb15b7edea7324797e80a49df04b2bfd0d0ecf 100644
240--- a/models/video.go
241+++ b/models/video.go
242@@ -26,6 +26,8 @@ type Video struct {
243 ProgressSeconds int `json:"progress"`
244 // IsShort indicates if a video is a YouTube short
245 IsShort bool `json:"short"`
246+ // IsLive indicates if the video is a currently active live stream
247+ IsLive bool `json:"is_live"`
248 // IsDiscarded indicates if the video was discarded from the feed without watching
249 // This happens to shorts videos for channels that have shorts disabled
250 IsDiscarded bool `json:"is_discarded"`
251diff --git a/pages/new_videos.templ b/pages/new_videos.templ
252index 6b2f3fe61d8b95479f3c20b136e63c86afe407ca..1984d007575ad7e6c8b428ea89e56403b80a6d47 100644
253--- a/pages/new_videos.templ
254+++ b/pages/new_videos.templ
255@@ -49,14 +49,23 @@ templ thumbnail(video models.Video) {
256 style="width: 100%; aspect-ratio: 16/9; object-fit: cover;"
257 />
258 }
259- <span
260- focusable="false"
261- class="text-body bg-body bg-opacity-75 px-1 py-1 rounded"
262- style="pointer-events: none; z-index: 2; position: absolute; bottom: 0.2rem; right: 0.2rem;"
263- fill="white"
264- >
265- { video.Duration() }
266- </span>
267+ if video.IsLive {
268+ <span
269+ focusable="false"
270+ class="text-white bg-danger px-1 py-1 rounded fw-bold"
271+ style="pointer-events: none; z-index: 2; position: absolute; bottom: 0.2rem; right: 0.2rem;"
272+ >
273+ LIVE
274+ </span>
275+ } else {
276+ <span
277+ focusable="false"
278+ class="text-body bg-body bg-opacity-75 px-1 py-1 rounded"
279+ style="pointer-events: none; z-index: 2; position: absolute; bottom: 0.2rem; right: 0.2rem;"
280+ >
281+ { video.Duration() }
282+ </span>
283+ }
284 </a>
285 @ProgressBar(video)
286 }
287diff --git a/pages/new_videos_templ.go b/pages/new_videos_templ.go
288index 239551f3a1ff51b441a79dd6a495099ea9a887ee..0f60c5e7292e0e8332b0dded741bd3be6623c5f3 100644
289--- a/pages/new_videos_templ.go
290+++ b/pages/new_videos_templ.go
291@@ -200,20 +200,31 @@ func thumbnail(video models.Video) templ.Component {
292 return templ_7745c5c3_Err
293 }
294 }