feat: initialise project structure with core encoding queue, database models, and web management API
Build / Build and Push (push) Successful in 50s
Build / Build and Push (push) Successful in 50s
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"goencode/internal/config"
|
||||
)
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
func Init(cfg *config.DatabaseConfig) error {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name)
|
||||
|
||||
var err error
|
||||
DB, err = sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set connection pool limits
|
||||
DB.SetMaxOpenConns(25)
|
||||
DB.SetMaxIdleConns(25)
|
||||
DB.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
if err = DB.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Connected to MariaDB successfully")
|
||||
|
||||
return runMigrations()
|
||||
}
|
||||
|
||||
func runMigrations() error {
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS watch_folders (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
folder_path VARCHAR(500) NOT NULL UNIQUE,
|
||||
media_type ENUM('video', 'audio') NOT NULL,
|
||||
target_resolution VARCHAR(20),
|
||||
custom_ffmpeg_flags TEXT,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS jobs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
media_type ENUM('video', 'audio') NOT NULL,
|
||||
status ENUM('pending', 'processing', 'failed') NOT NULL DEFAULT 'pending',
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
original_size BIGINT DEFAULT 0,
|
||||
target_resolution VARCHAR(20),
|
||||
ffmpeg_flags TEXT,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS job_reports (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
media_type ENUM('video', 'audio') NOT NULL,
|
||||
status ENUM('success', 'failed', 'skipped') NOT NULL,
|
||||
original_size BIGINT NOT NULL DEFAULT 0,
|
||||
encoded_size BIGINT NOT NULL DEFAULT 0,
|
||||
size_saved BIGINT NOT NULL DEFAULT 0,
|
||||
processing_time DECIMAL(10,2) DEFAULT 0,
|
||||
target_resolution VARCHAR(20),
|
||||
ffmpeg_flags TEXT,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS app_config (
|
||||
config_key VARCHAR(100) PRIMARY KEY,
|
||||
config_value TEXT
|
||||
)`,
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
if _, err := DB.Exec(q); err != nil {
|
||||
return fmt.Errorf("migration failed: %w\nQuery: %s", err, q)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Database schemas initialized")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type WatchFolder struct {
|
||||
ID int `json:"id"`
|
||||
FolderPath string `json:"folder_path"`
|
||||
MediaType string `json:"media_type"`
|
||||
TargetResolution string `json:"target_resolution"`
|
||||
CustomFFmpegFlags string `json:"custom_ffmpeg_flags"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
ID int `json:"id"`
|
||||
FilePath string `json:"file_path"`
|
||||
MediaType string `json:"media_type"`
|
||||
Status string `json:"status"` // pending, processing, failed
|
||||
Priority int `json:"priority"`
|
||||
OriginalSize int64 `json:"original_size"`
|
||||
TargetResolution string `json:"target_resolution"`
|
||||
FFmpegFlags string `json:"ffmpeg_flags"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type JobReport struct {
|
||||
ID int `json:"id"`
|
||||
FilePath string `json:"file_path"`
|
||||
MediaType string `json:"media_type"`
|
||||
Status string `json:"status"` // success, failed, skipped
|
||||
OriginalSize int64 `json:"original_size"`
|
||||
EncodedSize int64 `json:"encoded_size"`
|
||||
SizeSaved int64 `json:"size_saved"`
|
||||
ProcessingTime float64 `json:"processing_time"`
|
||||
TargetResolution string `json:"target_resolution"`
|
||||
FFmpegFlags string `json:"ffmpeg_flags"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// Watch Folders
|
||||
|
||||
func GetWatchFolders() ([]WatchFolder, error) {
|
||||
rows, err := DB.Query(`SELECT id, folder_path, media_type, target_resolution, custom_ffmpeg_flags, enabled, created_at, updated_at FROM watch_folders`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var folders []WatchFolder
|
||||
for rows.Next() {
|
||||
var f WatchFolder
|
||||
var targetRes, ffmpegFlags sql.NullString
|
||||
if err := rows.Scan(&f.ID, &f.FolderPath, &f.MediaType, &targetRes, &ffmpegFlags, &f.Enabled, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if targetRes.Valid {
|
||||
f.TargetResolution = targetRes.String
|
||||
}
|
||||
if ffmpegFlags.Valid {
|
||||
f.CustomFFmpegFlags = ffmpegFlags.String
|
||||
}
|
||||
folders = append(folders, f)
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func AddWatchFolder(f WatchFolder) error {
|
||||
_, err := DB.Exec(`INSERT INTO watch_folders (folder_path, media_type, target_resolution, custom_ffmpeg_flags, enabled) VALUES (?, ?, ?, ?, ?)`,
|
||||
f.FolderPath, f.MediaType, nullStr(f.TargetResolution), nullStr(f.CustomFFmpegFlags), f.Enabled)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteWatchFolder(id int) error {
|
||||
_, err := DB.Exec(`DELETE FROM watch_folders WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func nullStr(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{Valid: false}
|
||||
}
|
||||
return sql.NullString{String: s, Valid: true}
|
||||
}
|
||||
|
||||
// Jobs
|
||||
|
||||
func AddJob(filePath, mediaType string, priority int, targetRes, ffmpegFlags string, originalSize int64) error {
|
||||
_, err := DB.Exec(`INSERT INTO jobs (file_path, media_type, priority, target_resolution, ffmpeg_flags, original_size) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
filePath, mediaType, priority, nullStr(targetRes), nullStr(ffmpegFlags), originalSize)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPendingJobs() ([]Job, error) {
|
||||
rows, err := DB.Query(`SELECT id, file_path, media_type, status, priority, original_size, target_resolution, ffmpeg_flags, error_message, created_at, updated_at FROM jobs ORDER BY priority DESC, created_at ASC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var jobs []Job
|
||||
for rows.Next() {
|
||||
var j Job
|
||||
var targetRes, ffmpegFlags, errMsg sql.NullString
|
||||
if err := rows.Scan(&j.ID, &j.FilePath, &j.MediaType, &j.Status, &j.Priority, &j.OriginalSize, &targetRes, &ffmpegFlags, &errMsg, &j.CreatedAt, &j.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if targetRes.Valid { j.TargetResolution = targetRes.String }
|
||||
if ffmpegFlags.Valid { j.FFmpegFlags = ffmpegFlags.String }
|
||||
if errMsg.Valid { j.ErrorMessage = errMsg.String }
|
||||
jobs = append(jobs, j)
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func UpdateJobStatus(id int, status, errMsg string) error {
|
||||
_, err := DB.Exec(`UPDATE jobs SET status = ?, error_message = ? WHERE id = ?`, status, nullStr(errMsg), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func BumpJobPriority(id int) error {
|
||||
_, err := DB.Exec(`UPDATE jobs SET priority = priority + 1 WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteJob(id int) error {
|
||||
_, err := DB.Exec(`DELETE FROM jobs WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func MarkProcessingAsFailed() error {
|
||||
_, err := DB.Exec(`UPDATE jobs SET status = 'failed', error_message = 'Interrupted by server restart' WHERE status = 'processing'`)
|
||||
return err
|
||||
}
|
||||
|
||||
func IsFileAlreadyProcessedOrQueued(filePath string) (bool, error) {
|
||||
var count int
|
||||
err := DB.QueryRow(`SELECT COUNT(*) FROM jobs WHERE file_path = ?`, filePath).Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if count > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err = DB.QueryRow(`SELECT COUNT(*) FROM job_reports WHERE file_path = ?`, filePath).Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// Job Reports
|
||||
|
||||
func AddJobReport(j Job, status string, encodedSize int64, sizeSaved int64, processingTime float64) error {
|
||||
_, err := DB.Exec(`INSERT INTO job_reports (file_path, media_type, status, original_size, encoded_size, size_saved, processing_time, target_resolution, ffmpeg_flags, error_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
j.FilePath, j.MediaType, status, j.OriginalSize, encodedSize, sizeSaved, processingTime, nullStr(j.TargetResolution), nullStr(j.FFmpegFlags), nullStr(j.ErrorMessage))
|
||||
return err
|
||||
}
|
||||
|
||||
func GetJobReports(limit, offset int) ([]JobReport, int, error) {
|
||||
var total int
|
||||
if err := DB.QueryRow(`SELECT COUNT(*) FROM job_reports`).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
rows, err := DB.Query(`SELECT id, file_path, media_type, status, original_size, encoded_size, size_saved, processing_time, target_resolution, ffmpeg_flags, error_message, created_at FROM job_reports ORDER BY created_at DESC LIMIT ? OFFSET ?`, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var reports []JobReport
|
||||
for rows.Next() {
|
||||
var r JobReport
|
||||
var targetRes, ffmpegFlags, errMsg sql.NullString
|
||||
if err := rows.Scan(&r.ID, &r.FilePath, &r.MediaType, &r.Status, &r.OriginalSize, &r.EncodedSize, &r.SizeSaved, &r.ProcessingTime, &targetRes, &ffmpegFlags, &errMsg, &r.CreatedAt); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if targetRes.Valid { r.TargetResolution = targetRes.String }
|
||||
if ffmpegFlags.Valid { r.FFmpegFlags = ffmpegFlags.String }
|
||||
if errMsg.Valid { r.ErrorMessage = errMsg.String }
|
||||
reports = append(reports, r)
|
||||
}
|
||||
return reports, total, nil
|
||||
}
|
||||
|
||||
type DashboardStats struct {
|
||||
TotalSavedSpace int64
|
||||
FilesEncoded int
|
||||
QueueLength int
|
||||
}
|
||||
|
||||
func GetDashboardStats() (DashboardStats, error) {
|
||||
var stats DashboardStats
|
||||
|
||||
// Total saved space and files encoded
|
||||
err := DB.QueryRow(`
|
||||
SELECT
|
||||
COALESCE(SUM(size_saved), 0) as saved,
|
||||
COUNT(*) as count
|
||||
FROM job_reports
|
||||
WHERE status = 'success'
|
||||
`).Scan(&stats.TotalSavedSpace, &stats.FilesEncoded)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// Queue length
|
||||
err = DB.QueryRow(`SELECT COUNT(*) FROM jobs`).Scan(&stats.QueueLength)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return stats, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
Reference in New Issue
Block a user