Initial commit

This commit is contained in:
Carl Pearson
2024-09-06 10:47:41 -06:00
commit 1720727c9e
15 changed files with 721 additions and 0 deletions

236
handlers.go Normal file
View File

@@ -0,0 +1,236 @@
package main
import (
"bytes"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
func registerHandler(c echo.Context) error {
return c.Render(http.StatusOK, "register.html", nil)
}
func registerPostHandler(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
err := CreateUser(db, username, password)
if err != nil {
return c.String(http.StatusInternalServerError, "Error creating user")
}
return c.Redirect(http.StatusSeeOther, "/login")
}
func loginHandler(c echo.Context) error {
return c.Render(http.StatusOK, "login.html", nil)
}
func homeHandler(c echo.Context) error {
return c.Render(http.StatusOK, "home.html", nil)
}
func loginPostHandler(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
var user User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
return c.String(http.StatusUnauthorized, "Invalid credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return c.String(http.StatusUnauthorized, "Invalid credentials")
}
session, err := store.Get(c.Request(), "session")
if err != nil {
return c.String(http.StatusInternalServerError, "Unable to retrieve session")
}
session.Values["user_id"] = user.ID
err = session.Save(c.Request(), c.Response().Writer)
if err != nil {
return c.String(http.StatusInternalServerError, "Unable to save session")
}
session, _ = store.Get(c.Request(), "session")
_, ok := session.Values["user_id"]
if !ok {
return c.String(http.StatusInternalServerError, "user_id was not saved as expected")
}
fmt.Println("loginPostHandler: redirect to /download")
return c.Redirect(http.StatusSeeOther, "/download")
}
func logoutHandler(c echo.Context) error {
session, _ := store.Get(c.Request(), "session")
session.Values["user_id"] = nil
session.Save(c.Request(), c.Response().Writer)
return c.Redirect(http.StatusSeeOther, "/login")
}
func downloadHandler(c echo.Context) error {
return c.Render(http.StatusOK, "download.html", nil)
}
func downloadPostHandler(c echo.Context) error {
url := c.FormValue("url")
userID := c.Get("user_id").(uint)
video := Video{URL: url, UserID: userID, Status: "pending"}
db.Create(&video)
go startDownload(video.ID, url)
return c.Redirect(http.StatusSeeOther, "/videos")
}
type Meta struct {
title string
ext string
}
func getMeta(url string) (Meta, error) {
cmd := exec.Command("yt-dlp", "--simulate", "--print", "%(title)s.%(ext)s", url)
var stdout bytes.Buffer
cmd.Stdout = &stdout
err := cmd.Run()
if err != nil {
fmt.Println("getTitle error:", err)
return Meta{}, err
} else {
isDot := func(r rune) bool {
return r == '.'
}
fields := strings.FieldsFunc(strings.TrimSpace(stdout.String()), isDot)
if len(fields) < 2 {
return Meta{}, errors.New("couldn't parse ytdlp output")
}
return Meta{
title: strings.Join(fields[:len(fields)-1], "."),
ext: fields[len(fields)-1],
}, nil
}
}
func startDownload(videoID uint, videoURL string) {
db.Model(&Video{}).Where("id = ?", videoID).Update("status", "downloading")
meta, err := getMeta(videoURL)
if err != nil {
db.Model(&Video{}).Where("id = ?", videoID).Update("status", "failed")
return
}
fmt.Println("set video title:", meta.title)
db.Model(&Video{}).Where("id = ?", videoID).Update("title", meta.title)
videoFilename := fmt.Sprintf("%d-%s.%s", videoID, meta.title, meta.ext)
videoFilepath := filepath.Join(getDownloadDir(), "video", videoFilename)
cmd := exec.Command("yt-dlp", "-o", videoFilepath, videoURL)
err = cmd.Run()
if err != nil {
db.Model(&Video{}).Where("id = ?", videoID).Update("status", "failed")
return
}
audioFilename := fmt.Sprintf("%d-%s.mp3", videoID, meta.title)
audioFilepath := filepath.Join(getDownloadDir(), "audio", audioFilename)
audioDir := filepath.Dir(audioFilepath)
fmt.Println("Create", audioDir)
err = os.MkdirAll(audioDir, 0700)
if err != nil {
fmt.Println("Error: couldn't create", audioDir)
db.Model(&Video{}).Where("id = ?", videoID).Update("status", "failed")
return
}
ffmpeg := "ffmpeg"
ffmpegArgs := []string{"-i", videoFilepath, "-vn", "-acodec",
"mp3", "-b:a", "192k", audioFilepath}
fmt.Println(ffmpeg, ffmpegArgs)
cmd = exec.Command(ffmpeg, ffmpegArgs...)
err = cmd.Run()
if err != nil {
fmt.Println("Error: convert to audio file", videoFilepath, "->", audioFilepath)
db.Model(&Video{}).Where("id = ?", videoID).Update("status", "failed")
return
}
// FIXME: ensure expected files exist
db.Model(&Video{}).Where("id = ?", videoID).Updates(map[string]interface{}{
"video_filename": videoFilename,
"audio_filename": audioFilename,
"status": "completed",
})
}
func videosHandler(c echo.Context) error {
userID := c.Get("user_id").(uint)
var videos []Video
db.Where("user_id = ?", userID).Find(&videos)
return c.Render(http.StatusOK, "videos.html", map[string]interface{}{"videos": videos})
}
func videoCancelHandler(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
var video Video
if err := db.First(&video, id).Error; err != nil {
return c.Redirect(http.StatusSeeOther, "/videos")
}
// Cancel the download (this is a simplified version, you might need to implement a more robust cancellation mechanism)
video.Status = "cancelled"
db.Save(&video)
return c.Redirect(http.StatusSeeOther, "/videos")
}
func videoRestartHandler(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
var video Video
if err := db.First(&video, id).Error; err != nil {
return c.Redirect(http.StatusSeeOther, "/videos")
}
video.Status = "pending"
db.Save(&video)
go startDownload(uint(id), video.URL)
return c.Redirect(http.StatusSeeOther, "/videos")
}
func videoDeleteHandler(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
var video Video
if err := db.First(&video, id).Error; err != nil {
return c.Redirect(http.StatusSeeOther, "/videos")
}
// Delete the file
if video.VideoFilename != "" {
os.Remove(filepath.Join(getDownloadDir(), "video", video.VideoFilename))
}
if video.AudioFilename != "" {
os.Remove(filepath.Join(getDownloadDir(), "audio", video.AudioFilename))
}
// Delete from database
db.Delete(&video)
return c.Redirect(http.StatusSeeOther, "/videos")
}