Improved playlist handling

This commit is contained in:
Carl Pearson
2024-10-11 06:25:25 -06:00
parent 4b81dd46f2
commit 037e6279e6
15 changed files with 302 additions and 169 deletions

View File

@@ -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

26
database/database.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

12
handlers/init.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -12,7 +12,6 @@ import (
var log *logrus.Logger
func initLogger() {
log = logrus.New()
log.SetOutput(os.Stdout)
log.SetLevel(logrus.DebugLevel)

14
main.go
View File

@@ -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)

View File

@@ -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()

34
originals/originals.go Normal file
View File

@@ -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
}

26
playlists/model.go Normal file
View File

@@ -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
}

1
playlists/playlists.go Normal file
View File

@@ -0,0 +1 @@
package playlists

View File

@@ -12,45 +12,19 @@
</head>
<body>
<h1>Playlist</h1>
<h1>{{.playlist.Title}}</h1>
<h2>Playlist</h2>
<div class="video-list">
{{range .originals}}
<div class="video-card">
<div class="video-title">
{{if or (eq .Status "download completed") (eq .Status "transcoding") (eq .Status "completed")}}
<a href="/video/{{.ID}}">{{.Title}}</a>
{{else}}
{{.Title}}
{{end}}
</div>
<div class="video-info">{{.Artist}}</div>
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></div>
<div class="video-info">{{.Status}}</div>
<div class="video-info">
{{if .Audio}}
Audio
{{end}}
{{if .Video}}
Video
{{end}}
</div>
<div class="video-options">
{{if eq .Status "completed"}}
<form action="/video/{{.ID}}/process" method="post" style="display:inline;">
<button type="submit">Reprocess</button>
</form>
{{else if eq .Status "failed"}}
<form action="/video/{{.ID}}/restart" method="post" style="display:inline;">
<button type="submit">Restart</button>
</form>
{{else if eq .Status "downloading"}}
{{end}}
<form action="/video/{{.ID}}/delete" method="post" style="display:inline;">
<button type="submit">Delete</button>
</form>
</div>
</div>
{{range .unwatched}}
{{template "playlist-video-card-html" .}}
{{end}}
</div>
<h2>Watched</h2>
<div class="video-list">
{{range .watched}}
{{template "playlist-video-card-html" .}}
{{end}}
</div>

56
templates/video_card.html Normal file
View File

@@ -0,0 +1,56 @@
{{define "playlist-video-card-html"}}
<div class="video-card">
<div class="video-title">
{{if or (eq .Status "download completed") (eq .Status "transcoding") (eq .Status "completed")}}
<a href="/video/{{.ID}}">{{.Title}}</a>
{{else}}
{{.Title}}
{{end}}
</div>
<div class="video-info">{{.Artist}}</div>
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></div>
<div class="video-info">{{.Status}}</div>
<div class="video-info">
{{if .Audio}}
Audio
{{end}}
{{if .Video}}
Video
{{end}}
</div>
<div class="video-options">
{{if or (eq .Status "completed") (eq .Status "not started")}}
<form action="/video/{{.ID}}/toggle_watched" method="post" style="display:inline;">
<button type="submit">
{{ if .Watched }}
Not Watched
{{ else }}
Watched
{{ end }}
</button>
</form>
{{end}}
{{if eq .Status "completed"}}
<form action="/video/{{.ID}}/process" method="post" style="display:inline;">
<button type="submit">Reprocess</button>
</form>
{{else if eq .Status "failed"}}
<form action="/video/{{.ID}}/restart" method="post" style="display:inline;">
<button type="submit">Restart</button>
</form>
{{end}}
{{if eq .Status "not started"}}
<form action="/video/{{.ID}}/restart" method="post" style="display:inline;">
<button type="submit">Start</button>
</form>
{{end}}
<form action="/video/{{.ID}}/delete" method="post" style="display:inline;">
<button type="submit">Delete</button>
</form>
</div>
</div>
{{end}}
{{define "playlist-video-card-css"}}
<!-- <link rel="stylesheet" href="/static/style/footer.css"> -->
{{end}}

View File

@@ -65,17 +65,8 @@
{{.Title}}
{{end}}
</div>
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></div>
<div class="video-options">
{{if eq .Status "completed"}}
<form action="/p/{{.ID}}/process" method="post" style="display:inline;">
<button type="submit">Reprocess</button>
</form>
{{else if eq .Status "failed"}}
<form action="/p/{{.ID}}/restart" method="post" style="display:inline;">
<button type="submit">Restart</button>
</form>
{{else if eq .Status "downloading"}}
{{end}}
<form action="/p/{{.ID}}/delete" method="post" style="display:inline;">
<button type="submit">Delete</button>
</form>

View File

@@ -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").