From 037e6279e6cc912007fee43324a329defd0bac83 Mon Sep 17 00:00:00 2001 From: Carl Pearson Date: Fri, 11 Oct 2024 06:25:25 -0600 Subject: [PATCH] Improved playlist handling --- Dockerfile | 4 ++ database/database.go | 26 +++++++++ handlers.go | 110 ++++++++++++++++++++++--------------- handlers/init.go | 12 ++++ handlers/toggle_watched.go | 36 ++++++++++++ logger.go | 1 - main.go | 14 ++++- models.go | 69 +---------------------- originals/originals.go | 34 ++++++++++++ playlists/model.go | 26 +++++++++ playlists/playlists.go | 1 + templates/playlist.html | 48 ++++------------ templates/video_card.html | 56 +++++++++++++++++++ templates/videos.html | 11 +--- workers.go | 23 ++++---- 15 files changed, 302 insertions(+), 169 deletions(-) create mode 100644 database/database.go create mode 100644 handlers/init.go create mode 100644 handlers/toggle_watched.go create mode 100644 originals/originals.go create mode 100644 playlists/model.go create mode 100644 playlists/playlists.go create mode 100644 templates/video_card.html diff --git a/Dockerfile b/Dockerfile index 243b09e..b7abedb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,11 @@ RUN wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux && chmod +x /usr/local/bin/yt-dlp ADD *.go /src/. +ADD database /src/database +ADD handlers /src/handlers ADD media /src/media +ADD originals /src/originals +Add playlists /src/playlists ADD go.mod /src/. RUN cd /src && go mod tidy diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..d89ea13 --- /dev/null +++ b/database/database.go @@ -0,0 +1,26 @@ +package database + +import ( + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +var db *gorm.DB +var log *logrus.Logger + +func Init(d *gorm.DB, logger *logrus.Logger) error { + db = d + log = logger.WithFields(logrus.Fields{ + "component": "database", + }).Logger + return nil +} + +func Fini() {} + +func Get() *gorm.DB { + if db == nil { + panic("didn't call database.Init(...)") + } + return db +} diff --git a/handlers.go b/handlers.go index 3229032..5eacf84 100644 --- a/handlers.go +++ b/handlers.go @@ -12,6 +12,8 @@ import ( "strings" "time" "ytdlp-site/media" + "ytdlp-site/originals" + "ytdlp-site/playlists" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" @@ -141,20 +143,21 @@ func downloadPostHandler(c echo.Context) error { } if isPlaylistUrl(url) { - playlist := Playlist{ + playlist := playlists.Playlist{ URL: url, UserID: userID, Audio: audioOnly, Video: !audioOnly, + Status: playlists.StatusNotStarted, } db.Create(&playlist) go startPlaylist(playlist.ID, url, audioOnly) } else { - original := Original{ + original := originals.Original{ URL: url, UserID: userID, - Status: Pending, + Status: originals.StatusNotStarted, Audio: audioOnly, Video: !audioOnly, } @@ -548,7 +551,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { log.Debugf("startDownload audioOnly=%t", audioOnly) // metadata phase - SetOriginalStatus(originalID, Metadata) + originals.SetStatus(db, originalID, originals.StatusMetadata) var origMeta Meta var err error if audioOnly { @@ -558,29 +561,29 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { } if err != nil { log.Errorln("couldn't retrieve metadata:", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } log.Debugf("original metadata %v", origMeta) - err = db.Model(&Original{}).Where("id = ?", originalID).Updates(map[string]interface{}{ + err = db.Model(&originals.Original{}).Where("id = ?", originalID).Updates(map[string]interface{}{ "title": origMeta.title, "artist": origMeta.artist, }).Error if err != nil { log.Errorln("couldn't store metadata:", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } // download original - SetOriginalStatus(originalID, Downloading) + originals.SetStatus(db, originalID, originals.StatusDownloading) // create temporary directory // do this in the data directory since /tmp is sometimes a different filesystem tempDir, err := os.MkdirTemp(getDataDir(), "dl") if err != nil { log.Errorln("Error creating temporary directory:", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } defer os.RemoveAll(tempDir) @@ -601,7 +604,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { err = cmd.Run() if err != nil { log.Errorln("yt-dlp failed") - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } @@ -609,7 +612,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { dirEnts, err := os.ReadDir(tempDir) if err != nil { log.Errorln("Error reading directory:", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } dlFilename := "" @@ -622,7 +625,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { } if dlFilename == "" { log.Errorln("couldn't find a downloaded file") - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) } // move to data directory @@ -632,7 +635,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { err = os.Rename(srcPath, dlFilepath) if err != nil { log.Errorln("rename downloaded media error", srcPath, "->", dlFilepath, ":", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } @@ -640,7 +643,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { mediaMeta, err := getAudioMeta(dlFilepath) if err != nil { log.Errorln("couldn't get audio file metadata", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } @@ -654,14 +657,14 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { fmt.Println("create Audio", audio) if db.Create(&audio).Error != nil { fmt.Println("Couldn't create audio entry", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } } else { mediaMeta, err := getVideoMeta(dlFilepath) if err != nil { log.Errorln("couldn't get video file metadata", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } @@ -678,12 +681,12 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { log.Debugln("create Video", video) if db.Create(&video).Error != nil { log.Errorln("Couldn't create video entry", err) - SetOriginalStatus(originalID, Failed) + originals.SetStatus(db, originalID, originals.StatusFailed) return } } - SetOriginalStatus(originalID, DownloadCompleted) + originals.SetStatus(db, originalID, originals.StatusDownloadCompleted) processOriginal(originalID) } @@ -691,22 +694,22 @@ func startPlaylist(id uint, url string, audioOnly bool) { // retrieve playlist metadata pl, err := getYtdlpPlaylist(url) if err != nil { - SetPlaylistStatus(id, Failed) + playlists.SetStatus(db, id, playlists.StatusFailed) return } - err = db.Model(&Playlist{}).Where("id = ?", id).Updates(map[string]interface{}{ + err = db.Model(&playlists.Playlist{}).Where("id = ?", id).Updates(map[string]interface{}{ "title": pl.Title, }).Error if err != nil { - SetPlaylistStatus(id, Failed) + playlists.SetStatus(db, id, playlists.StatusFailed) return } for _, entry := range pl.Entries { - original := Original{ + original := originals.Original{ Title: entry.Title, URL: entry.URL, - Status: StatusNotStarted, + Status: originals.StatusNotStarted, Video: !audioOnly, Audio: audioOnly, Playlist: true, @@ -714,19 +717,19 @@ func startPlaylist(id uint, url string, audioOnly bool) { } err = db.Create(&original).Error if err != nil { - SetPlaylistStatus(id, Failed) + playlists.SetStatus(db, id, playlists.StatusFailed) return } } - SetPlaylistStatus(id, Completed) + playlists.SetStatus(db, id, playlists.StatusCompleted) } func videosHandler(c echo.Context) error { userID := c.Get("user_id").(uint) - var origs []Original + var origs []originals.Original db.Where("user_id = ?", userID).Find(&origs) - var playlists []Playlist + var playlists []playlists.Playlist db.Where("user_id = ?", userID).Find(&playlists) return c.Render(http.StatusOK, "videos.html", @@ -798,7 +801,7 @@ func makeNiceFilename(input string) string { func videoHandler(c echo.Context) error { id, _ := strconv.Atoi(c.Param("id")) - var orig Original + var orig originals.Original if err := db.First(&orig, id).Error; err != nil { return c.Redirect(http.StatusSeeOther, "/videos") } @@ -874,16 +877,20 @@ func videoRestartHandler(c echo.Context) error { id, _ := strconv.Atoi(c.Param("id")) // FIXME: rewrite this as an update - var orig Original + var orig originals.Original if err := db.First(&orig, id).Error; err != nil { return c.Redirect(http.StatusSeeOther, "/videos") } - orig.Status = Pending + orig.Status = originals.StatusNotStarted db.Save(&orig) go startDownload(uint(id), orig.URL, orig.Audio) - return c.Redirect(http.StatusSeeOther, "/videos") + referrer := c.Request().Referer() + if referrer == "" { + referrer = "/videos" + } + return c.Redirect(http.StatusSeeOther, referrer) } func deleteTranscodes(originalID uint) { @@ -934,7 +941,7 @@ func deleteAudiosWithSource(originalID uint, source string) { } func deleteOriginal(id uint) error { - var orig Original + var orig originals.Original if err := db.First(&orig, id).Error; err != nil { return err } @@ -1091,7 +1098,7 @@ func processHandler(c echo.Context) error { deleteAudiosWithSource(uint(id), "transcode") deleteTranscodedVideos(uint(id)) - err := SetOriginalStatus(uint(id), DownloadCompleted) + err := originals.SetStatus(db, uint(id), originals.StatusDownloadCompleted) if err != nil { log.Errorf("error while setting original %d status: %v", id, err) } @@ -1102,21 +1109,38 @@ func processHandler(c echo.Context) error { } func playlistHandler(c echo.Context) error { - id := c.Param("id") - var originals []Original + var playlist playlists.Playlist + err := db.Where(id).First(&playlist).Error + if err != nil { + return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) + } - err := db.Where("playlist = ?", true). + var origs []originals.Original + var watchedOrigs []originals.Original + + err = db.Where("playlist = ?", true). Where("playlist_id = ?", id). - Find(&originals).Error + Where("watched = ?", false). + Find(&origs).Error + if err != nil { + return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) + } + + err = db.Where("playlist = ?", true). + Where("playlist_id = ?", id). + Where("watched = ?", true). + Find(&watchedOrigs).Error if err != nil { return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) } return c.Render(http.StatusOK, "playlist.html", map[string]interface{}{ - "originals": originals, + "playlist": playlist, + "unwatched": origs, + "watched": watchedOrigs, "Footer": makeFooter(), }) } @@ -1125,16 +1149,16 @@ func deletePlaylistHandler(c echo.Context) error { id := c.Param("id") // delete all originals - var originals []Original - err := db.Model(&Original{}). + var origs []originals.Original + err := db.Model(&originals.Original{}). Where("playlist = ?", true). Where("playlist_id = ?", id). - Find(&originals).Error + Find(&origs).Error if err != nil { log.Errorln(err) } - for _, original := range originals { + for _, original := range origs { err := deleteOriginal(original.ID) if err != nil { log.Errorln(err) @@ -1142,7 +1166,7 @@ func deletePlaylistHandler(c echo.Context) error { } // delete playlist entry - err = db.Delete(&Playlist{}, id).Error + err = db.Delete(&playlists.Playlist{}, id).Error if err != nil { log.Errorln(err) } diff --git a/handlers/init.go b/handlers/init.go new file mode 100644 index 0000000..16608b0 --- /dev/null +++ b/handlers/init.go @@ -0,0 +1,12 @@ +package handlers + +import "github.com/sirupsen/logrus" + +var log *logrus.Logger + +func Init(logger *logrus.Logger) error { + log = logger.WithFields(logrus.Fields{ + "component": "handlers", + }).Logger + return nil +} diff --git a/handlers/toggle_watched.go b/handlers/toggle_watched.go new file mode 100644 index 0000000..405f463 --- /dev/null +++ b/handlers/toggle_watched.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/labstack/echo/v4" + "gorm.io/gorm" + + "ytdlp-site/database" + "ytdlp-site/originals" +) + +func ToggleWatched(c echo.Context) error { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + + db := database.Get() + + result := db.Model(&originals.Original{}). + Where("id = ?", id). + Update("watched", gorm.Expr("NOT watched")) + + if result.Error != nil { + log.Errorln(result.Error) + } + + if result.RowsAffected == 0 { + log.Errorln(gorm.ErrRecordNotFound) + } + + referrer := c.Request().Referer() + if referrer == "" { + referrer = "/videos" + } + return c.Redirect(http.StatusSeeOther, referrer) +} diff --git a/logger.go b/logger.go index 6574b04..064e510 100644 --- a/logger.go +++ b/logger.go @@ -12,7 +12,6 @@ import ( var log *logrus.Logger func initLogger() { - log = logrus.New() log.SetOutput(os.Stdout) log.SetLevel(logrus.DebugLevel) diff --git a/main.go b/main.go index e540ba6..8f7f112 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,11 @@ import ( "os" "path/filepath" "time" + "ytdlp-site/database" + "ytdlp-site/handlers" "ytdlp-site/media" + "ytdlp-site/originals" + "ytdlp-site/playlists" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" @@ -46,6 +50,8 @@ func main() { log.Infof("GitSHA: %s", getGitSHA()) log.Infof("BuildDate: %s", getBuildDate()) + handlers.Init(log) + gormLogger := logger.New( golog.New(os.Stdout, "\r\n", golog.LstdFlags), // io writer logger.Config{ @@ -80,7 +86,12 @@ func main() { sqlDB.SetMaxOpenConns(1) // Migrate the schema - db.AutoMigrate(&Original{}, &Playlist{}, &media.Video{}, &media.Audio{}, &User{}, &TempURL{}, &Transcode{}) + db.AutoMigrate(&originals.Original{}, &playlists.Playlist{}, &media.Video{}, + &media.Audio{}, &User{}, &TempURL{}, &Transcode{}) + + database.Init(db, log) + defer database.Fini() + go PeriodicCleanup() // create a user @@ -124,6 +135,7 @@ func main() { e.POST("/video/:id/delete", deleteOriginalHandler, authMiddleware) e.GET("/temp/:token", tempHandler) e.POST("/video/:id/process", processHandler, authMiddleware) + e.POST("/video/:id/toggle_watched", handlers.ToggleWatched, authMiddleware) e.POST("/delete_video/:id", deleteVideoHandler, authMiddleware) e.POST("/delete_audio/:id", deleteAudioHandler, authMiddleware) e.POST("/transcode_to_video/:id", transcodeToVideoHandler, authMiddleware) diff --git a/models.go b/models.go index 63a0a15..d021d37 100644 --- a/models.go +++ b/models.go @@ -5,40 +5,13 @@ import ( "fmt" "sync" "time" + "ytdlp-site/originals" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) -type OriginalStatus string - -const ( - StatusNotStarted OriginalStatus = "not started" - Pending OriginalStatus = "pending" - Metadata OriginalStatus = "metadata" - Downloading OriginalStatus = "downloading" - DownloadCompleted OriginalStatus = "download completed" - Transcoding OriginalStatus = "transcoding" - Completed OriginalStatus = "completed" - Failed OriginalStatus = "failed" -) - -type Original struct { - gorm.Model - UserID uint - URL string - Title string - Artist string - Status OriginalStatus - Audio bool // video download requested - Video bool // audio download requested - Watched bool - - Playlist bool // part of a playlist - PlaylistID uint // Playlist.ID (if part of a playlist) -} - type Transcode struct { gorm.Model Status string // "pending", "running", "failed" @@ -58,16 +31,6 @@ type Transcode struct { Rate uint } -type Playlist struct { - gorm.Model - UserID uint - URL string - Title string - Status OriginalStatus - Audio bool - Video bool -} - type User struct { gorm.Model Username string `gorm:"unique"` @@ -102,18 +65,8 @@ func CreateUser(db *gorm.DB, username, password string) error { return nil } -func SetOriginalStatus(id uint, status OriginalStatus) error { - return db.Model(&Original{}).Where("id = ?", id).Update("status", status).Error -} - -func SetPlaylistStatus(id uint, status OriginalStatus) error { - return db.Model(&Playlist{}).Where("id = ?", id).Update("status", status).Error -} - -func NewDownloadManager() *DownloadManager { - return &DownloadManager{ - downloads: make(map[uint]*DownloadStatus), - } +func SetOriginalStatus(id uint, status originals.Status) error { + return db.Model(&originals.Original{}).Where("id = ?", id).Update("status", status).Error } func (dm *DownloadManager) UpdateStatus(id uint, progress float64, status string, err string) { @@ -127,22 +80,6 @@ func (dm *DownloadManager) UpdateStatus(id uint, progress float64, status string dm.downloads[id].Error = err } -func (dm *DownloadManager) GetStatus(id uint) (DownloadStatus, bool) { - dm.mutex.RLock() - defer dm.mutex.RUnlock() - status, exists := dm.downloads[id] - if !exists { - return DownloadStatus{}, false - } - return *status, true -} - -func (dm *DownloadManager) RemoveStatus(id uint) { - dm.mutex.Lock() - defer dm.mutex.Unlock() - delete(dm.downloads, id) -} - func generateToken() string { uuidObj := uuid.Must(uuid.NewV7()) return uuidObj.String() diff --git a/originals/originals.go b/originals/originals.go new file mode 100644 index 0000000..b63a14f --- /dev/null +++ b/originals/originals.go @@ -0,0 +1,34 @@ +package originals + +import "gorm.io/gorm" + +type Status string + +const ( + StatusNotStarted Status = "not started" + StatusMetadata Status = "metadata" + StatusDownloading Status = "downloading" + StatusDownloadCompleted Status = "download completed" + StatusTranscoding Status = "transcoding" + StatusCompleted Status = "completed" + StatusFailed Status = "failed" +) + +type Original struct { + gorm.Model + UserID uint + URL string + Title string + Artist string + Status Status + Audio bool // video download requested + Video bool // audio download requested + Watched bool + + Playlist bool // part of a playlist + PlaylistID uint // Playlist.ID (if part of a playlist) +} + +func SetStatus(db *gorm.DB, id uint, status Status) error { + return db.Model(&Original{}).Where("id = ?", id).Update("status", status).Error +} diff --git a/playlists/model.go b/playlists/model.go new file mode 100644 index 0000000..fa42363 --- /dev/null +++ b/playlists/model.go @@ -0,0 +1,26 @@ +package playlists + +import "gorm.io/gorm" + +type Status string + +type Playlist struct { + gorm.Model + UserID uint + URL string + Title string + Status Status + Audio bool + Video bool +} + +const ( + StatusNotStarted Status = "not started" + StatusDownloading Status = "downloading" + StatusCompleted Status = "completed" + StatusFailed Status = "failed" +) + +func SetStatus(db *gorm.DB, id uint, status Status) error { + return db.Model(&Playlist{}).Where("id = ?", id).Update("status", status).Error +} diff --git a/playlists/playlists.go b/playlists/playlists.go new file mode 100644 index 0000000..af225fc --- /dev/null +++ b/playlists/playlists.go @@ -0,0 +1 @@ +package playlists diff --git a/templates/playlist.html b/templates/playlist.html index 5538570..099c2e9 100644 --- a/templates/playlist.html +++ b/templates/playlist.html @@ -12,45 +12,19 @@ -

Playlist

+

{{.playlist.Title}}

+

Playlist

- {{range .originals}} -
-
- {{if or (eq .Status "download completed") (eq .Status "transcoding") (eq .Status "completed")}} - {{.Title}} - {{else}} - {{.Title}} - {{end}} -
-
{{.Artist}}
- -
{{.Status}}
-
- {{if .Audio}} - Audio - {{end}} - {{if .Video}} - Video - {{end}} -
-
- {{if eq .Status "completed"}} -
- -
- {{else if eq .Status "failed"}} -
- -
- {{else if eq .Status "downloading"}} - {{end}} -
- -
-
-
+ {{range .unwatched}} + {{template "playlist-video-card-html" .}} + {{end}} +
+ +

Watched

+
+ {{range .watched}} + {{template "playlist-video-card-html" .}} {{end}}
diff --git a/templates/video_card.html b/templates/video_card.html new file mode 100644 index 0000000..301138f --- /dev/null +++ b/templates/video_card.html @@ -0,0 +1,56 @@ +{{define "playlist-video-card-html"}} +
+
+ {{if or (eq .Status "download completed") (eq .Status "transcoding") (eq .Status "completed")}} + {{.Title}} + {{else}} + {{.Title}} + {{end}} +
+
{{.Artist}}
+ +
{{.Status}}
+
+ {{if .Audio}} + Audio + {{end}} + {{if .Video}} + Video + {{end}} +
+
+ {{if or (eq .Status "completed") (eq .Status "not started")}} +
+ +
+ {{end}} + {{if eq .Status "completed"}} +
+ +
+ {{else if eq .Status "failed"}} +
+ +
+ {{end}} + {{if eq .Status "not started"}} +
+ +
+ {{end}} +
+ +
+
+
+{{end}} + +{{define "playlist-video-card-css"}} + +{{end}} \ No newline at end of file diff --git a/templates/videos.html b/templates/videos.html index b95a834..c99911f 100644 --- a/templates/videos.html +++ b/templates/videos.html @@ -65,17 +65,8 @@ {{.Title}} {{end}} +
{{.URL}}
- {{if eq .Status "completed"}} -
- -
- {{else if eq .Status "failed"}} -
- -
- {{else if eq .Status "downloading"}} - {{end}}
diff --git a/workers.go b/workers.go index 6cedbd9..c02fa8e 100644 --- a/workers.go +++ b/workers.go @@ -9,6 +9,7 @@ import ( "strings" "time" "ytdlp-site/media" + "ytdlp-site/originals" "github.com/google/uuid" "gorm.io/gorm" @@ -67,7 +68,7 @@ func videoToVideo(transID uint, height uint, srcFilepath string) { // look up original var trans Transcode db.First(&trans, transID) - var orig Original + var orig originals.Original db.First(&orig, "id = ?", trans.OriginalID) // create video record @@ -129,7 +130,7 @@ func videoToAudio(transID uint, kbps uint, videoFilepath string) { // look up original var trans Transcode db.First(&trans, "id = ?", transID) - var orig Original + var orig originals.Original db.First(&orig, "id = ?", trans.OriginalID) // create audio record @@ -187,7 +188,7 @@ func audioToAudio(transID uint, kbps uint, srcFilepath string) { // look up original var trans Transcode db.First(&trans, "id = ?", transID) - var orig Original + var orig originals.Original db.First(&orig, "id = ?", trans.OriginalID) // create audio record @@ -225,29 +226,29 @@ func transcodePending() { var originalsToUpdate []uint // find any originals with a transcode job and mark them as transcoding - db.Model(&Original{}). + db.Model(&originals.Original{}). Select("id"). Where("id IN (?)", db.Model(&Transcode{}). Select("original_id"), ). Find(&originalsToUpdate) - db.Model(&Original{}). + db.Model(&originals.Original{}). Where("id IN ?", originalsToUpdate). - Update("status", Transcoding) + Update("status", originals.StatusTranscoding) // originals marked transcoding that don't have a transcode job -> complete - db.Model(&Original{}). + db.Model(&originals.Original{}). Select("id"). Where("status = ? AND id NOT IN (?)", - Transcoding, + originals.StatusTranscoding, db.Model(&Transcode{}). Select("original_id"), ). Find(&originalsToUpdate) - db.Model(&Original{}). - Where("id IN ? AND status = ?", originalsToUpdate, Transcoding). - Update("status", Completed) + db.Model(&originals.Original{}). + Where("id IN ? AND status = ?", originalsToUpdate, originals.StatusTranscoding). + Update("status", originals.StatusCompleted) var trans Transcode err := db.Where("status = ?", "pending").