324 lines
8.8 KiB
Go
324 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"ytdlp-site/config"
|
|
"ytdlp-site/ffmpeg"
|
|
"ytdlp-site/media"
|
|
"ytdlp-site/originals"
|
|
"ytdlp-site/transcodes"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
maxConcurrent = 2
|
|
)
|
|
|
|
var sem = make(chan struct{}, maxConcurrent)
|
|
|
|
func ensureDirFor(path string) error {
|
|
dir := filepath.Dir(path)
|
|
log.Debugln("Create", dir)
|
|
return os.MkdirAll(dir, 0700)
|
|
}
|
|
|
|
func videoToVideo(sem chan struct{}, transID uint, srcFilepath string) {
|
|
sem <- struct{}{} // Acquire semaphore
|
|
defer func() { <-sem }() // release semaphore
|
|
|
|
var trans transcodes.Transcode
|
|
db.First(&trans, "id = ?", transID)
|
|
originals.SetStatus(trans.OriginalID, originals.StatusTranscoding)
|
|
|
|
// determine destination path
|
|
dstFilename := uuid.Must(uuid.NewV7()).String()
|
|
dstFilename = fmt.Sprintf("%s.mp4", dstFilename)
|
|
dstFilepath := filepath.Join(config.GetDataDir(), dstFilename)
|
|
|
|
err := ensureDirFor(dstFilepath)
|
|
if err != nil {
|
|
fmt.Println("Error: couldn't create dir for ", dstFilepath, err)
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", trans.ID).Update("status", "failed")
|
|
return
|
|
}
|
|
|
|
// FIXME: ignoring any requested audio bitrate
|
|
// determine audio bitrate
|
|
var audioBitrate uint = 160
|
|
if trans.Height <= 240 {
|
|
audioBitrate = 64
|
|
} else if trans.Height <= 540 {
|
|
audioBitrate = 96
|
|
} else if trans.Height < 720 {
|
|
audioBitrate = 128
|
|
}
|
|
|
|
// start ffmpeg
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", trans.ID).Update("status", "running")
|
|
var vf string
|
|
if trans.FPS > 0 {
|
|
vf = fmt.Sprintf("scale=-2:%d,fps=%f", trans.Height, trans.FPS)
|
|
} else {
|
|
vf = fmt.Sprintf("scale=-2:%d", trans.Height)
|
|
}
|
|
stdout, stderr, err := ffmpeg.Ffmpeg("-i", srcFilepath,
|
|
"-vf", vf, "-c:v", "libx264",
|
|
"-crf", "23", "-preset", "fast", "-c:a", "aac", "-b:a", fmt.Sprintf("%dk", audioBitrate),
|
|
dstFilepath)
|
|
if err != nil {
|
|
fmt.Println("Error: convert to video file", srcFilepath, "->", dstFilepath, string(stdout), string(stderr))
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", trans.ID).Update("status", "failed")
|
|
return
|
|
}
|
|
|
|
// look up original
|
|
var orig originals.Original
|
|
db.First(&orig, "id = ?", trans.OriginalID)
|
|
|
|
// create video record
|
|
video := media.Video{
|
|
VideoFile: media.VideoFile{
|
|
MediaFile: media.MediaFile{
|
|
Filename: dstFilename,
|
|
},
|
|
},
|
|
OriginalID: orig.ID, Source: "transcode",
|
|
}
|
|
|
|
fileSize, err := getSize(dstFilepath)
|
|
if err == nil {
|
|
video.Size = fileSize
|
|
}
|
|
length, err := getLength(dstFilepath)
|
|
if err == nil {
|
|
video.Length = length
|
|
}
|
|
|
|
meta, err := getVideoMeta(dstFilepath)
|
|
fmt.Println("meta for", dstFilepath, meta)
|
|
if err == nil {
|
|
video.Width = meta.width
|
|
video.Height = meta.height
|
|
video.FPS = meta.fps
|
|
}
|
|
|
|
db.Create(&video)
|
|
|
|
// complete transcode
|
|
db.Delete(&trans)
|
|
originals.SetStatusTranscodingOrCompleted(trans.OriginalID)
|
|
}
|
|
|
|
func videoToAudio(sem chan struct{}, transID uint, videoFilepath string) {
|
|
sem <- struct{}{} // Acquire semaphore
|
|
defer func() { <-sem }() // release semaphore
|
|
|
|
var trans transcodes.Transcode
|
|
db.First(&trans, "id = ?", transID)
|
|
originals.SetStatus(trans.OriginalID, originals.StatusTranscoding)
|
|
|
|
// determine destination path
|
|
audioFilename := uuid.Must(uuid.NewV7()).String()
|
|
audioFilename = fmt.Sprintf("%s.mp3", audioFilename)
|
|
audioFilepath := filepath.Join(config.GetDataDir(), audioFilename)
|
|
|
|
// ensure destination directory
|
|
err := ensureDirFor(audioFilepath)
|
|
if err != nil {
|
|
fmt.Println("Error: couldn't create dir for ", audioFilepath, err)
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", transID).Update("status", "failed")
|
|
return
|
|
}
|
|
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", transID).Update("status", "running")
|
|
_, _, err = ffmpeg.Ffmpeg("-i", videoFilepath, "-vn", "-acodec",
|
|
"mp3", "-b:a",
|
|
fmt.Sprintf("%dk", trans.Kbps),
|
|
audioFilepath)
|
|
if err != nil {
|
|
fmt.Println("Error: convert to audio file", videoFilepath, "->", audioFilepath)
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", transID).Update("status", "failed")
|
|
return
|
|
}
|
|
|
|
// look up original
|
|
|
|
var orig originals.Original
|
|
db.First(&orig, "id = ?", trans.OriginalID)
|
|
|
|
// create audio record
|
|
audio := media.Audio{
|
|
MediaFile: media.MediaFile{
|
|
Filename: audioFilename,
|
|
},
|
|
OriginalID: orig.ID,
|
|
Bps: trans.Kbps * 1000,
|
|
Source: "transcode",
|
|
}
|
|
|
|
fileSize, err := getSize(audioFilepath)
|
|
if err == nil {
|
|
audio.Size = fileSize
|
|
}
|
|
length, err := getLength(audioFilepath)
|
|
if err == nil {
|
|
audio.Length = length
|
|
}
|
|
|
|
db.Create(&audio)
|
|
|
|
// complete transcode
|
|
db.Delete(&trans)
|
|
originals.SetStatusTranscodingOrCompleted(trans.OriginalID)
|
|
}
|
|
|
|
func audioToAudio(sem chan struct{}, transID uint, srcFilepath string) {
|
|
sem <- struct{}{} // Acquire semaphore
|
|
defer func() { <-sem }() // release semaphore
|
|
|
|
var trans transcodes.Transcode
|
|
db.First(&trans, "id = ?", transID)
|
|
|
|
originals.SetStatus(trans.OriginalID, originals.StatusTranscoding)
|
|
|
|
// determine destination path
|
|
dstFilename := uuid.Must(uuid.NewV7()).String()
|
|
dstFilename = fmt.Sprintf("%s.mp3", dstFilename)
|
|
dstFilepath := filepath.Join(config.GetDataDir(), dstFilename)
|
|
|
|
// ensure destination directory
|
|
err := ensureDirFor(dstFilepath)
|
|
if err != nil {
|
|
fmt.Println("Error: couldn't create dir for ", dstFilepath, err)
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", transID).Update("status", "failed")
|
|
return
|
|
}
|
|
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", transID).Update("status", "running")
|
|
_, _, err = ffmpeg.Ffmpeg("-i", srcFilepath, "-vn", "-acodec",
|
|
"mp3", "-b:a",
|
|
fmt.Sprintf("%dk", trans.Kbps),
|
|
dstFilepath)
|
|
if err != nil {
|
|
fmt.Println("Error: convert to audio file", srcFilepath, "->", dstFilepath)
|
|
db.Model(&transcodes.Transcode{}).Where("id = ?", transID).Update("status", "failed")
|
|
return
|
|
}
|
|
|
|
// look up original
|
|
var orig originals.Original
|
|
db.First(&orig, "id = ?", trans.OriginalID)
|
|
|
|
// create audio record
|
|
audio := media.Audio{
|
|
MediaFile: media.MediaFile{
|
|
Filename: dstFilename,
|
|
},
|
|
OriginalID: orig.ID,
|
|
Bps: trans.Kbps * 1000,
|
|
Source: "transcode",
|
|
}
|
|
|
|
fileSize, err := getSize(dstFilepath)
|
|
if err == nil {
|
|
audio.Size = fileSize
|
|
}
|
|
length, err := getLength(dstFilepath)
|
|
if err == nil {
|
|
audio.Length = length
|
|
}
|
|
|
|
db.Create(&audio)
|
|
|
|
// complete transcode
|
|
db.Delete(&trans)
|
|
originals.SetStatusTranscodingOrCompleted(trans.OriginalID)
|
|
}
|
|
|
|
func cleanupTranscodes() {
|
|
log.Traceln("cleanupTranscode")
|
|
|
|
// any running jobs here got stuck or dead in the midde, so reset them
|
|
db.Model(&transcodes.Transcode{}).Where("status = ?", "running").Update("status", "pending")
|
|
|
|
// find any originals with a transcode job -> transcoding
|
|
var originalsToUpdate []uint
|
|
db.Model(&originals.Original{}).
|
|
Select("id").
|
|
Where("id IN (?)",
|
|
db.Model(&transcodes.Transcode{}).
|
|
Select("original_id"),
|
|
).
|
|
Find(&originalsToUpdate)
|
|
db.Model(&originals.Original{}).
|
|
Where("id IN ?", originalsToUpdate).
|
|
Update("status", originals.StatusTranscoding)
|
|
|
|
// originals marked transcoding that don't have a transcode job -> complete
|
|
db.Model(&originals.Original{}).
|
|
Select("id").
|
|
Where("status = ? AND id NOT IN (?)",
|
|
originals.StatusTranscoding,
|
|
db.Model(&transcodes.Transcode{}).
|
|
Select("original_id"),
|
|
).
|
|
Find(&originalsToUpdate)
|
|
db.Model(&originals.Original{}).
|
|
Where("id IN ? AND status = ?", originalsToUpdate, originals.StatusTranscoding).
|
|
Update("status", originals.StatusCompleted)
|
|
|
|
// start any existing transcode jobs
|
|
for {
|
|
var trans transcodes.Transcode
|
|
err := db.Where("status = ?", "pending").
|
|
Order("CASE " +
|
|
"WHEN dst_kind = 'video' AND height = 540 THEN 0 " +
|
|
"WHEN dst_kind = 'audio' AND rate = 96 THEN 0 " +
|
|
"ELSE 1 END").First(&trans).Error
|
|
// err := db.First(&trans, "status = ?", "pending").Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
log.Traceln("no pending transcode jobs")
|
|
break // no more pending jobs
|
|
}
|
|
|
|
if trans.SrcKind == "video" {
|
|
|
|
var srcVideo media.Video
|
|
err = db.First(&srcVideo, "id = ?", trans.SrcID).Error
|
|
if err != nil {
|
|
fmt.Println("no such source video for video Transcode", trans)
|
|
db.Delete(&trans)
|
|
continue
|
|
}
|
|
srcFilepath := filepath.Join(config.GetDataDir(), srcVideo.Filename)
|
|
|
|
if trans.DstKind == "video" {
|
|
go videoToVideo(sem, trans.ID, srcFilepath)
|
|
} else if trans.DstKind == "audio" {
|
|
go videoToAudio(sem, trans.ID, srcFilepath)
|
|
} else {
|
|
fmt.Println("unexpected src/dst kinds for Transcode", trans)
|
|
db.Delete(&trans)
|
|
}
|
|
} else if trans.SrcKind == "audio" {
|
|
var srcAudio media.Audio
|
|
err = db.First(&srcAudio, "id = ?", trans.SrcID).Error
|
|
if err != nil {
|
|
log.Errorln("no such source audio for audio Transcode", trans)
|
|
db.Delete(&trans)
|
|
continue
|
|
}
|
|
srcFilepath := filepath.Join(config.GetDataDir(), srcAudio.Filename)
|
|
go audioToAudio(sem, trans.ID, srcFilepath)
|
|
} else {
|
|
fmt.Println("unexpected src kind for Transcode", trans)
|
|
db.Delete(&trans)
|
|
}
|
|
}
|
|
|
|
}
|