diff --git a/api/stream.go b/api/stream.go index 0238b2f7c..e7086600e 100644 --- a/api/stream.go +++ b/api/stream.go @@ -50,6 +50,7 @@ func configGinStreamRestRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) { thumbs.GET(":fid", routes.getThumbs) thumbs.GET("/live", routes.getLiveThumbs) thumbs.GET("/vod", routes.getVODThumbs) + thumbs.POST("/", routes.putCustomLiveThumbnail) // TODO: change to admin only endpoint } } { @@ -177,8 +178,15 @@ func (r streamRoutes) getLiveThumbs(c *gin.Context) { tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) streamId := strconv.Itoa(int(tumLiveContext.Stream.ID)) - path := pathprovider.LiveThumbnail(streamId) - c.File(path) + + file, err := r.DaoWrapper.FileDao.GetThumbnail(tumLiveContext.Stream.ID, model.FILETYPE_THUMB_LG_CAM_PRES) + if err != nil { + + path := pathprovider.LiveThumbnail(streamId) + c.File(path) + } + c.File(file.Path) + } func (r streamRoutes) getSubtitles(c *gin.Context) { @@ -894,3 +902,61 @@ func (r streamRoutes) updateChatEnabled(c *gin.Context) { return } } + +func (r streamRoutes) putCustomLiveThumbnail(c *gin.Context) { + tumLiveContext := c.MustGet("TUMLiveContext").(tools.TUMLiveContext) + streamID := tumLiveContext.Stream.ID + course := tumLiveContext.Course + file, err := c.FormFile("file") + if err != nil { + //c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file"}) + _ = c.AbortWithError(http.StatusBadRequest, tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "Invalid file", + Err: err, + }) + return + } + + filename := file.Filename + fileUuid := uuid.NewV1() + + filesFolder := filepath.Join( + tools.Cfg.Paths.Mass, + fmt.Sprintf("%s.%d.%s", course.Name, course.Year, course.TeachingTerm), + "files") + + path := filepath.Join( + filesFolder, + fmt.Sprintf("%s%s", fileUuid, filepath.Ext(filename))) + + //tempFilePath := pathprovider.LiveThumbnail(strconv.Itoa(int(streamID))) + if err := c.SaveUploadedFile(file, path); err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "Failed to save file", + Err: err, + }) + return + } + + thumb := model.File{ + StreamID: streamID, + Path: path, + Filename: file.Filename, + Type: model.FILETYPE_THUMB_LG_CAM_PRES, + CourseName: course.Name, + } + + if err := r.DaoWrapper.FileDao.SetThumbnail(streamID, thumb); err != nil { + + _ = c.AbortWithError(http.StatusInternalServerError, tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "Failed to set thumbnail", + Err: err, + }) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Thumbnail uploaded successfully"}) +} diff --git a/dao/file.go b/dao/file.go index 4f31790a5..1a3f2a4d6 100644 --- a/dao/file.go +++ b/dao/file.go @@ -14,6 +14,7 @@ type FileDao interface { DeleteFile(id uint) error CountVoDFiles() (int64, error) SetThumbnail(streamId uint, thumb model.File) error + GetThumbnail(streamId uint, fileType model.FileType) (f model.File, err error) } type fileDao struct { @@ -56,3 +57,8 @@ func (d fileDao) SetThumbnail(streamId uint, thumb model.File) error { return tx.Create(&thumb).Error }) } + +func (d fileDao) GetThumbnail(streamId uint, fileType model.FileType) (f model.File, err error) { + err = DB.Where("stream_id = ? AND type = ?", streamId, fileType).First(&f).Error + return +} diff --git a/model/file.go b/model/file.go index 824b3300c..2b8d6c984 100644 --- a/model/file.go +++ b/model/file.go @@ -22,15 +22,17 @@ const ( FILETYPE_THUMB_LG_CAM FILETYPE_THUMB_LG_PRES FILETYPE_THUMB_LG_CAM_PRES // generated from CAM and PRES, preferred over the others + FILETYPE_THUMB_CUSTOM ) type File struct { gorm.Model - StreamID uint `gorm:"not null"` - Path string `gorm:"not null"` - Filename string - Type FileType `gorm:"not null; default: 1"` + StreamID uint `gorm:"not null"` + Path string `gorm:"not null"` + Filename string + Type FileType `gorm:"not null; default: 1"` + CourseName string `gorm:"default: null"` } func (f File) GetDownloadFileName() string { @@ -68,7 +70,7 @@ func (f File) GetVodTypeByName() string { } func (f File) IsThumb() bool { - return f.Type == FILETYPE_THUMB_CAM || f.Type == FILETYPE_THUMB_PRES || f.Type == FILETYPE_THUMB_COMB + return f.Type == FILETYPE_THUMB_CAM || f.Type == FILETYPE_THUMB_PRES || f.Type == FILETYPE_THUMB_COMB || f.Type == FILETYPE_THUMB_CUSTOM } func (f File) IsURL() bool { diff --git a/model/stream.go b/model/stream.go index ab44a7a6c..6104d1c48 100755 --- a/model/stream.go +++ b/model/stream.go @@ -18,43 +18,44 @@ import ( type Stream struct { gorm.Model - Name string `gorm:"index:,class:FULLTEXT"` - Description string `gorm:"type:text;index:,class:FULLTEXT"` - CourseID uint - Start time.Time `gorm:"not null"` - End time.Time `gorm:"not null"` - ChatEnabled bool `gorm:"default:null"` - RoomName string - RoomCode string - EventTypeName string - TUMOnlineEventID uint - SeriesIdentifier string `gorm:"default:null"` - StreamKey string `gorm:"not null"` - PlaylistUrl string - PlaylistUrlPRES string - PlaylistUrlCAM string - LiveNow bool `gorm:"not null"` - LiveNowTimestamp time.Time `gorm:"default:null;column:live_now_timestamp"` - Recording bool - Premiere bool `gorm:"default:null"` - Ended bool `gorm:"default:null"` - Chats []Chat - Stats []Stat - Units []StreamUnit - VodViews uint `gorm:"default:0"` // todo: remove me before next semester - StartOffset uint `gorm:"default:null"` - EndOffset uint `gorm:"default:null"` - LectureHallID uint `gorm:"default:null"` - Silences []Silence - Files []File `gorm:"foreignKey:StreamID"` - ThumbInterval uint32 `gorm:"default:null"` - StreamName string - Duration sql.NullInt32 `gorm:"default:null"` - StreamWorkers []Worker `gorm:"many2many:stream_workers;"` - StreamProgresses []StreamProgress `gorm:"foreignKey:StreamID"` - VideoSections []VideoSection - TranscodingProgresses []TranscodingProgress `gorm:"foreignKey:StreamID"` - Private bool `gorm:"not null;default:false"` + Name string `gorm:"index:,class:FULLTEXT"` + Description string `gorm:"type:text;index:,class:FULLTEXT"` + CourseID uint + Start time.Time `gorm:"not null"` + End time.Time `gorm:"not null"` + ChatEnabled bool `gorm:"default:null"` + CustomThumbnailEnabled bool `gorm:"default:false"` + RoomName string + RoomCode string + EventTypeName string + TUMOnlineEventID uint + SeriesIdentifier string `gorm:"default:null"` + StreamKey string `gorm:"not null"` + PlaylistUrl string + PlaylistUrlPRES string + PlaylistUrlCAM string + LiveNow bool `gorm:"not null"` + LiveNowTimestamp time.Time `gorm:"default:null;column:live_now_timestamp"` + Recording bool + Premiere bool `gorm:"default:null"` + Ended bool `gorm:"default:null"` + Chats []Chat + Stats []Stat + Units []StreamUnit + VodViews uint `gorm:"default:0"` // todo: remove me before next semester + StartOffset uint `gorm:"default:null"` + EndOffset uint `gorm:"default:null"` + LectureHallID uint `gorm:"default:null"` + Silences []Silence + Files []File `gorm:"foreignKey:StreamID"` + ThumbInterval uint32 `gorm:"default:null"` + StreamName string + Duration sql.NullInt32 `gorm:"default:null"` + StreamWorkers []Worker `gorm:"many2many:stream_workers;"` + StreamProgresses []StreamProgress `gorm:"foreignKey:StreamID"` + VideoSections []VideoSection + TranscodingProgresses []TranscodingProgress `gorm:"foreignKey:StreamID"` + Private bool `gorm:"not null;default:false"` Watched bool `gorm:"-"` // Used to determine if stream is watched when loaded for a specific user. } @@ -337,30 +338,31 @@ func (s Stream) GetJson(lhs []LectureHall, course Course) gin.H { } return gin.H{ - "lectureId": s.Model.ID, - "courseId": s.CourseID, - "seriesIdentifier": s.SeriesIdentifier, - "name": s.Name, - "description": s.Description, - "lectureHallId": s.LectureHallID, - "lectureHallName": lhName, - "streamKey": s.StreamKey, - "isLiveNow": s.LiveNow, - "isRecording": s.Recording, - "isConverting": s.IsConverting(), - "transcodingProgresses": s.TranscodingProgresses, - "isPast": s.IsPast(), - "hasStats": s.Stats != nil, - "files": files, - "color": s.Color(), - "start": s.Start, - "end": s.End, - "isChatEnabled": s.ChatEnabled, - "courseSlug": course.Slug, - "private": s.Private, - "downloadableVods": s.GetVodFiles(), - "isCopying": false, - "videoSections": videoSections, + "lectureId": s.Model.ID, + "courseId": s.CourseID, + "seriesIdentifier": s.SeriesIdentifier, + "name": s.Name, + "description": s.Description, + "lectureHallId": s.LectureHallID, + "lectureHallName": lhName, + "streamKey": s.StreamKey, + "isLiveNow": s.LiveNow, + "isRecording": s.Recording, + "isConverting": s.IsConverting(), + "transcodingProgresses": s.TranscodingProgresses, + "isPast": s.IsPast(), + "hasStats": s.Stats != nil, + "files": files, + "color": s.Color(), + "start": s.Start, + "end": s.End, + "isChatEnabled": s.ChatEnabled, + "isCustomThumbnailEnabled": s.CustomThumbnailEnabled, + "courseSlug": course.Slug, + "private": s.Private, + "downloadableVods": s.GetVodFiles(), + "isCopying": false, + "videoSections": videoSections, } } diff --git a/web/template/partial/course/manage/lecture-management-card.gohtml b/web/template/partial/course/manage/lecture-management-card.gohtml index 2b66b8865..c8155ff08 100644 --- a/web/template/partial/course/manage/lecture-management-card.gohtml +++ b/web/template/partial/course/manage/lecture-management-card.gohtml @@ -374,6 +374,40 @@ Chat Enabled + +
+ +
+ Drop thumbnail here. +
+ Thumbnail Preview + +{{/* */}} +
+{{/*
*/}} + +{{/*
*/}} + + + + +
- +