Skip to content

Upload Custom Thumbnail for a Lecture #1479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions api/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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"})
}
6 changes: 6 additions & 0 deletions dao/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
12 changes: 7 additions & 5 deletions model/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
124 changes: 63 additions & 61 deletions model/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down Expand Up @@ -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,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,40 @@
<span class="ml-3 text-sm font-medium text-3">Chat Enabled</span>
</label>
</section>
</div>
<section x-data="{upload: false, thumbnailSrc: ''}">
<label class="relative inline-flex items-center cursor-pointer left-3" >
<input type="checkbox" name="isCustomThumbnailEnabled" class="sr-only peer" x-bind-change-set="changeSet" @click="upload =! upload"/>
<div
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-600
dark:peer-focus:ring-indigo-600 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full
peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px]
after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5
after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-indigo-600" ></div>
<span class="ml-3 text-sm font-medium text-3">Upload Custom Thumbnail</span>
</label>
<div
@drop.prevent="(e) => onCustomThumbnailUpload(e)"
@dragover.prevent=""
class="tl-textarea grow mt-4 p-6 border-2 border-dashed border-gray-300 rounded-lg"
placeholder="Drop thumbnail here."
autocomplete="off"
x-show="upload">
Drop thumbnail here.
</div>
<img :src="thumbnailSrc" alt="Thumbnail Preview" class="mt-4" x-show="thumbnailSrc">
<button @click="clearThumbnail" class="mt-2 px-4 py-2 bg-red-500 text-white rounded" x-show="thumbnailSrc">Clear Thumbnail</button>
{{/* </label>*/}}
</section>
{{/* <article x-data="{ id: $id('text-input') }"*/}}
{{/* class="w-full" >*/}}

{{/* </article>*/}}

<!--input type="file" id="thumbnailUpload" class="hidden" accept="image/*" @change="onAttachmentFileDrop"-->



<div>
<button :disabled="isSaving" @click="discardEdit();"
class="px-8 py-3 text-2 text-white rounded bg-indigo-500/70 hover:bg-indigo-500/90 dark:bg-indigo-500/10 disabled:opacity-20 dark:hover:bg-indigo-500/20 mr-4">
Expand All @@ -386,7 +420,7 @@
x-text="isSaving ? 'loading ...' : (uiEditMode === admin.UIEditMode.single ? 'Save Lecture' : 'Save Series')"
></button>
</div>
</div>

</div>

<template x-if="lecture.isCopying">
Expand Down
20 changes: 20 additions & 0 deletions web/ts/api/admin-lecture-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface UpdateLectureMetaRequest {
description?: string;
lectureHallId?: number;
isChatEnabled?: boolean;
isCustomThumbnailEnabled?: boolean;
}

export class LectureFile {
Expand Down Expand Up @@ -167,6 +168,7 @@ export interface Lecture {
hasStats: boolean;
isChatEnabled: boolean;
isConverting: boolean;
isCustomThumbnailEnabled: boolean;
isLiveNow: boolean;
isPast: boolean;
isRecording: boolean;
Expand Down Expand Up @@ -265,6 +267,15 @@ export const AdminLectureList = {
);
}

/* if (request.isCustomThumbnailEnabled !== undefined) {
promises.push(
put(`/api/stream/${lectureId}/customThumbnail/enabled`, {
lectureId,
thumbnailFile: request.thumbnailFile,
}),
);
}*/

const errors = (await Promise.all(promises)).filter((res) => res.status !== StatusCodes.OK);
if (errors.length > 0) {
console.error(errors);
Expand Down Expand Up @@ -371,6 +382,15 @@ export const AdminLectureList = {
) => {
return await uploadFile(`/api/stream/${lectureId}/files?type=file`, file, listener);
},
uploadThumbnailFile: async (
courseId: number,
lectureId: number,
file: File,
listener: PostFormDataListener = {},
) => {
return await uploadFile(`/api/stream/${lectureId}/thumbs/`, file, listener);

},

/**
* Upload a url as attachment for a lecture
Expand Down
22 changes: 22 additions & 0 deletions web/ts/data-store/admin-lecture-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,26 @@ export class AdminLectureListProvider extends StreamableMapProvider<number, Lect
) {
await AdminLectureList.uploadVideo(courseId, lectureId, videoType, file, listener);
}

async uploadThumbnail(courseId: number, lectureId: number, file: File) {

const res = await AdminLectureList.uploadThumbnailFile(courseId, lectureId, file);
const newFile = new LectureFile({
id: JSON.parse(res.responseText),
fileType: 2,
friendlyName: file.name,
});

this.data[courseId] = (await this.getData(courseId)).map((s) => {
if (s.lectureId === lectureId) {
return {
...s,
files: [...s.files, newFile],
};
}
return s;
});
await this.triggerUpdate(courseId);

}
}
Loading
Loading