diff --git a/api/stream.go b/api/stream.go index 9058a25cf..6f2ee9cb0 100644 --- a/api/stream.go +++ b/api/stream.go @@ -359,26 +359,44 @@ func (r streamRoutes) getStream(c *gin.Context) { func (r streamRoutes) getStreamPlaylist(c *gin.Context) { type StreamPlaylistEntry struct { - StreamID uint `json:"streamId"` - CourseSlug string `json:"courseSlug"` - StreamName string `json:"streamName"` - LiveNow bool `json:"liveNow"` - Watched bool `json:"watched"` - Start time.Time `json:"start"` - CreatedAt time.Time `json:"createdAt"` + StreamID uint `json:"streamId"` + CourseSlug string `json:"courseSlug"` + StreamName string `json:"streamName"` + LiveNow bool `json:"liveNow"` + Watched bool `json:"watched"` + Start time.Time `json:"start"` + StreamProgress model.StreamProgress `json:"streamProgress"` + CreatedAt time.Time `json:"createdAt"` } tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) + + // Create mapping of stream id to progress for all progresses of user + var streamIDs []uint + for _, stream := range tumLiveContext.Course.Streams { + streamIDs = append(streamIDs, stream.ID) + } + streamProgresses := make(map[uint]model.StreamProgress) + res, err := r.LoadProgress(tumLiveContext.User.ID, streamIDs) + if err != nil { + logger.Error("Couldn't load progresses", "err", err) + } else { + for _, progress := range res { + streamProgresses[progress.StreamID] = progress + } + } + var result []StreamPlaylistEntry for _, stream := range tumLiveContext.Course.Streams { result = append(result, StreamPlaylistEntry{ - StreamID: stream.ID, - CourseSlug: tumLiveContext.Course.Slug, - StreamName: stream.GetName(), - LiveNow: stream.LiveNow, - Watched: stream.Watched, - Start: stream.Start, - CreatedAt: stream.CreatedAt, + StreamID: stream.ID, + CourseSlug: tumLiveContext.Course.Slug, + StreamName: stream.GetName(), + LiveNow: stream.LiveNow, + Watched: stream.Watched, + Start: stream.Start, + StreamProgress: streamProgresses[stream.ID], + CreatedAt: stream.CreatedAt, }) } diff --git a/api/users.go b/api/users.go index 2afc0e454..175a70eff 100644 --- a/api/users.go +++ b/api/users.go @@ -27,6 +27,7 @@ func configGinUsersRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) { router.POST("/api/users/settings/playbackSpeeds", routes.updatePlaybackSpeeds) router.POST("/api/users/settings/seekingTime", routes.updateSeekingTime) router.POST("/api/users/settings/customSpeeds", routes.updateCustomSpeeds) + router.POST("/api/users/settings/autoSkip", routes.updateAutoSkip) router.POST("/api/users/resetPassword", routes.resetPassword) @@ -723,6 +724,37 @@ func (r usersRoutes) updateSeekingTime(c *gin.Context) { } } +func (r usersRoutes) updateAutoSkip(c *gin.Context) { + u := c.MustGet("TUMLiveContext").(tools.TUMLiveContext).User + if u == nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusUnauthorized, + CustomMessage: "login required", + }) + return + } + var req struct{ Value model.AutoSkipSetting } + if err := c.BindJSON(&req); err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "can not bind body to request", + Err: err, + }) + return + } + + settingBytes, _ := json.Marshal(req.Value) + err := r.DaoWrapper.UsersDao.AddUserSetting(&model.UserSetting{UserID: u.ID, Type: model.AutoSkip, Value: string(settingBytes)}) + if err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "can not add user setting", + Err: err, + }) + return + } +} + func (r usersRoutes) exportPersonalData(c *gin.Context) { var resp personalData u := c.MustGet("TUMLiveContext").(tools.TUMLiveContext).User diff --git a/docs/static/tum-live-starter.sql b/docs/static/tum-live-starter.sql index 84f545ff6..2759ee343 100644 --- a/docs/static/tum-live-starter.sql +++ b/docs/static/tum-live-starter.sql @@ -352,6 +352,7 @@ CREATE TABLE `lecture_halls` ( LOCK TABLES `lecture_halls` WRITE; /*!40000 ALTER TABLE `lecture_halls` DISABLE KEYS */; +INSERT INTO `lecture_halls` VALUES (1,NULL,NULL,NULL,'HS001','Hörsaal 001',NULL,NULL,NULL,NULL,NULL,NULL,NULL); /*!40000 ALTER TABLE `lecture_halls` ENABLE KEYS */; UNLOCK TABLES; @@ -606,6 +607,7 @@ CREATE TABLE `silences` ( LOCK TABLES `silences` WRITE; /*!40000 ALTER TABLE `silences` DISABLE KEYS */; +INSERT INTO `silences` VALUES (1,'2024-01-14 21:16:37.000','2024-01-14 21:16:43.000',NULL,0,100,1),(2,'2024-01-14 21:17:00.000','2024-01-14 21:17:02.000',NULL,0,200,2); /*!40000 ALTER TABLE `silences` ENABLE KEYS */; UNLOCK TABLES; @@ -813,7 +815,7 @@ CREATE TABLE `streams` ( LOCK TABLES `streams` WRITE; /*!40000 ALTER TABLE `streams` DISABLE KEYS */; -INSERT INTO `streams` VALUES (1,'2022-04-18 13:45:58.657','2022-04-18 13:46:46.547',NULL,'VL 1: Was ist Bier?','',1,'2022-04-11 12:00:00.000','2022-04-11 13:00:00.000','','','',0,NULL,'c33dfc976efb410299e604b255db0127','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(2,'2022-04-18 13:46:25.841','2022-04-18 13:46:46.547',NULL,'VL 2: Wie mache ich Bier?','',1,'2022-04-18 12:00:00.000','2022-04-18 13:00:00.000','','','',0,NULL,'5815366e4010482687912588349bc5c0','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(4,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 3: Rückblick','',1,'2026-02-19 12:00:00.000','2026-02-19 13:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,0,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(7,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Livestream','',2,'2022-02-19 12:00:00.000','2022-02-19 13:00:00.000','','','',0,NULL,'','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',1,0,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(8,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Intro to Go','',3,'2022-02-19 12:00:00.000','2022-02-19 12:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL); +INSERT INTO `streams` VALUES (1,'2022-04-18 13:45:58.657','2022-04-18 13:46:46.547',NULL,'VL 1: Was ist Bier?','',1,'2022-04-11 12:00:00.000','2022-04-11 12:09:56.000','','','',0,NULL,'c33dfc976efb410299e604b255db0127','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(2,'2022-04-18 13:46:25.841','2022-04-18 13:46:46.547',NULL,'VL 2: Wie mache ich Bier?','',1,'2022-04-18 12:00:00.000','2022-04-18 12:09:56.000','','','',0,NULL,'5815366e4010482687912588349bc5c0','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(4,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 3: Rückblick','',1,'2026-02-19 12:00:00.000','2026-02-19 13:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,0,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(7,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Livestream','',2,'2022-02-19 12:00:00.000','2022-02-19 13:00:00.000','','','',0,NULL,'','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',1,0,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(8,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Intro to Go','',3,'2022-02-19 12:00:00.000','2022-02-19 12:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,1,0,'',NULL); /*!40000 ALTER TABLE `streams` ENABLE KEYS */; UNLOCK TABLES; diff --git a/model/stream.go b/model/stream.go index ba717b9ca..fc3572c71 100755 --- a/model/stream.go +++ b/model/stream.go @@ -410,3 +410,18 @@ func (s Stream) ToDTO() StreamDTO { Duration: duration, } } + +// FirstSilenceAsProgress returns the end of the first silence as a quotient of the length of the stream +func (s Stream) FirstSilenceAsProgress() float64 { + if len(s.Silences) == 0 { + return 0 + } + // Sanity check: first silence at beginning of stream + if s.Silences[0].Start != 0 { + return 0 + } + duration := s.End.Sub(s.Start).Seconds() + p := float64(s.Silences[0].End) / duration + + return p +} diff --git a/model/user.go b/model/user.go index 7de618b87..2cbc794b6 100755 --- a/model/user.go +++ b/model/user.go @@ -58,6 +58,7 @@ const ( CustomPlaybackSpeeds SeekingTime UserDefinedSpeeds + AutoSkip ) type UserSetting struct { @@ -197,6 +198,26 @@ func (u User) PreferredNameChangeAllowed() bool { return true } +// AutoSkipSetting wraps whether auto skip is enabled in JSON +type AutoSkipSetting struct { + Enabled bool `json:"enabled"` +} + +// GetAutoSkipEnabled returns whether the user has enabled auto skip +func (u User) GetAutoSkipEnabled() (AutoSkipSetting, error) { + for _, setting := range u.Settings { + if setting.Type == AutoSkip { + var a AutoSkipSetting + err := json.Unmarshal([]byte(setting.Value), &a) + if err != nil { + return AutoSkipSetting{Enabled: false}, err + } + return a, nil + } + } + return AutoSkipSetting{Enabled: false}, nil +} + type argonParams struct { memory uint32 iterations uint32 diff --git a/web/assets/css/home.css b/web/assets/css/home.css index 057a8f7b4..4a490579d 100644 --- a/web/assets/css/home.css +++ b/web/assets/css/home.css @@ -218,13 +218,20 @@ } .tum-live-menu .tum-live-menu-item { - @apply flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-800 my-2 w-full + @apply flex items-center py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-800 my-2 w-full } .tum-live-menu .tum-live-menu-item.active { @apply bg-gray-100 dark:bg-gray-800 } +.tum-live-menu .tum-live-menu-item .icon-wrapper { + width: 18px; + display: flex; + justify-content: center; + align-items: center; +} + .tum-live-course-list > header { @apply flex items-center justify-between mb-8 diff --git a/web/assets/css/watch.css b/web/assets/css/watch.css index 05b7ee1de..1e94242f0 100644 --- a/web/assets/css/watch.css +++ b/web/assets/css/watch.css @@ -1,5 +1,5 @@ .playlist-thumbnail { - @apply bg-gray-100 dark:bg-gray-800 rounded-lg mr-4 bg-cover transition-all duration-500 hover:shadow-xl dark:shadow-gray-900/75; + @apply relative bg-gray-100 dark:bg-gray-800 rounded-lg mr-4 bg-cover transition-all duration-500 hover:shadow-xl dark:shadow-gray-900/75; } /* Rendered, but not visible on the screen. Enables layout calculations. */ @@ -10,4 +10,4 @@ width: 100%; height: auto; overflow: visible; -} +} \ No newline at end of file diff --git a/web/template/home.gohtml b/web/template/home.gohtml index 9a8d3ad5d..5498eac7f 100644 --- a/web/template/home.gohtml +++ b/web/template/home.gohtml @@ -84,8 +84,8 @@ {{if or (eq $user.Role 1) (eq $user.Role 2) }} -
- +
+

Admin

@@ -95,8 +95,8 @@
-
- +
+

Settings

@@ -125,24 +125,24 @@ -
- +
+

Send Feedback

-
- +
+

Report problem

-
- +
+

Logout

diff --git a/web/template/partial/stream/playlist.gohtml b/web/template/partial/stream/playlist.gohtml index 065bd4d8e..001114dac 100644 --- a/web/template/partial/stream/playlist.gohtml +++ b/web/template/partial/stream/playlist.gohtml @@ -32,6 +32,15 @@
+
+
+ +
+

diff --git a/web/template/user-settings.gohtml b/web/template/user-settings.gohtml index b24214b21..b3857a061 100644 --- a/web/template/user-settings.gohtml +++ b/web/template/user-settings.gohtml @@ -139,6 +139,21 @@
+
+

Automatically Skip First Silence

+ + + +

Privacy & Data Protection

{ protected async fetcher(streamId: number): Promise { const result = await StreamPlaylist.get(streamId); - return result - .map((e) => { - e.startDate = new Date(e.start); - return e; - }) - .sort((a, b) => (a.startDate < b.startDate ? -1 : 1)); + return ( + result + .map((e) => { + e.startDate = new Date(e.start); + return e; + }) + // Convert stream progress to Typescript object + .map((e) => { + e.progress = new Progress(JSON.parse(JSON.stringify(e.streamProgress))); + return e; + }) + .sort((a, b) => (a.startDate < b.startDate ? -1 : 1)) + ); } } @@ -20,10 +28,12 @@ export type StreamPlaylistEntry = { liveNow: boolean; watched: boolean; start: string; + streamProgress: string; createdAt: string; - // Client Generated + // Client generated to package data with Typescript constructors startDate: Date; + progress: Progress; }; const StreamPlaylist = { diff --git a/web/ts/user-settings.ts b/web/ts/user-settings.ts index d283cf784..2180ca783 100644 --- a/web/ts/user-settings.ts +++ b/web/ts/user-settings.ts @@ -9,6 +9,7 @@ export enum UserSetting { PlaybackSpeeds = "playbackSpeeds", SeekingTime = "seekingTime", CustomSpeeds = "customSpeeds", + AutoSkip = "autoSkip", } export function updatePreference(t: UserSetting, value: string | boolean | number[]): Promise { diff --git a/web/watch.go b/web/watch.go index b966e7c12..f866d0f10 100644 --- a/web/watch.go +++ b/web/watch.go @@ -3,6 +3,7 @@ package web import ( "errors" "html/template" + "math" "net/http" "strconv" "strings" @@ -93,6 +94,17 @@ func (r mainRoutes) WatchPage(c *gin.Context) { } else if len(progress) > 0 { data.Progress = progress[0] } + + // Check if user wants to skip first silence + autoSkip, err := tumLiveContext.User.GetAutoSkipEnabled() + if err != nil { + logger.Error("Couldn't decode user setting", "err", err) + } else if autoSkip.Enabled { + // The length of the stream may mismatch with the length of the video if it is a self-stream + if tumLiveContext.Stream.LectureHallID != 0 { + data.Progress.Progress = math.Max(data.Progress.Progress, tumLiveContext.Stream.FirstSilenceAsProgress()) + } + } } if c.Query("restart") == "1" { c.Redirect(http.StatusFound, strings.Split(c.Request.RequestURI, "?")[0])