vivaplusdl/webview.go
Sebastiaan de Schaetzen 91be8274c4
All checks were successful
Build / build (push) Successful in 3m11s
UI is largely working
2025-03-16 18:11:34 +01:00

283 lines
7.6 KiB
Go

package main
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
"github.com/donseba/go-htmx"
)
type App struct {
db *mysqlite.Db
htmx *htmx.HTMX
}
func serveWebview(db *mysqlite.Db) {
addr := "localhost:8081"
app := &App{
db: db,
htmx: htmx.New(),
}
log.Printf("Listening on %s", addr)
http.Handle("/static/", http.StripPrefix("/static/", staticFileServer()))
http.HandleFunc("/video/", app.serveVideo)
http.HandleFunc("/stream/", app.serveStream)
http.HandleFunc("/mark-watched/", app.handleMarkWatched)
http.HandleFunc("/mark-unwatched/", app.handleMarkUnwatched)
http.HandleFunc("/", app.serveIndex)
err := http.ListenAndServe(addr, nil)
if err != nil {
panic(err)
}
}
func (a *App) renderTemplate(w http.ResponseWriter, name string, viewmodel interface{}) {
err := renderTemplate(w, "index", viewmodel)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (a *App) getViewModelForVideo(id int) (*VideoInfoVM, error) {
// Query the database for the requested video
var title, description, thumbnail string
var watchState *string
err := a.db.Query(`SELECT title, description, thumbnail, watch_state FROM videos WHERE id = ? LIMIT 1`).Bind(id).ScanSingle(&title, &description, &thumbnail, &watchState)
if err != nil {
return nil, err
}
// Find the previous video ID based on upload date and episode number
var previousID int
err = a.db.Query(`
SELECT id FROM videos
WHERE (upload_date < (SELECT upload_date FROM videos WHERE id = ?))
OR (upload_date = (SELECT upload_date FROM videos WHERE id = ?) AND episode < (SELECT episode FROM videos WHERE id = ?))
ORDER BY upload_date DESC, episode DESC
LIMIT 1
`).Bind(id, id, id).ScanSingle(&previousID)
if err != nil {
// If there's no previous video, set previousID to 0 or -1 to indicate no previous video
previousID = 0
}
// Determine if the video has been watched
isWatched := watchState != nil && *watchState == "watched"
return &VideoInfoVM{
ID: id,
PreviousID: previousID,
Title: title,
Description: description,
Thumbnail: thumbnail,
IsWatched: isWatched,
}, nil
}
func (a *App) getViewModelForUnwachtedVideo() (*VideoInfoVM, error) {
// Query the database for the oldest unwatched episode
var id int
err := a.db.Query(`SELECT id FROM videos WHERE (watch_state IS NULL OR watch_state != 'watched') ORDER BY upload_date, episode LIMIT 1`).ScanSingle(&id)
if err != nil {
return nil, err
}
return a.getViewModelForVideo(id)
}
func (a *App) serveIndex(w http.ResponseWriter, r *http.Request) {
var vm *VideoInfoVM
var err error
// Check if a specific video ID was requested
videoIDParam := r.URL.Query().Get("video")
if videoIDParam != "" {
videoID, parseErr := strconv.Atoi(videoIDParam)
if parseErr == nil {
vm, err = a.getViewModelForVideo(videoID)
} else {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
} else {
// Fall back to the default behavior - get the oldest unwatched video
vm, err = a.getViewModelForUnwachtedVideo()
}
if err != nil {
log.Printf("Failed to find video: %v", err)
http.Error(w, "Failed to find video", http.StatusInternalServerError)
return
}
a.renderTemplate(w, "index", vm)
}
func (a *App) serveVideo(w http.ResponseWriter, r *http.Request) {
// Parse the video ID from the URL
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) < 3 {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
videoIDStr := pathParts[2]
videoID, err := strconv.Atoi(videoIDStr)
if err != nil {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
// Query the database for the video with the given ID
var title, description string
err = a.db.Query(`SELECT title, description FROM videos WHERE id = ?`).Bind(videoID).ScanSingle(&title, &description)
if err != nil {
log.Printf("Failed to find video: %v", err)
http.Error(w, "Video not found", http.StatusNotFound)
return
}
vm := VideoPlayerVM{
ID: videoID,
Title: title,
Description: description,
}
a.renderTemplate(w, "video", vm)
}
func (a *App) serveStream(w http.ResponseWriter, r *http.Request) {
// Parse the video ID from the URL
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) < 3 {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
videoIDStr := pathParts[2]
videoID, err := strconv.Atoi(videoIDStr)
if err != nil {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
// Get video metadata from database
var year, episode int
err = a.db.Query(`SELECT year, episode FROM videos WHERE id = ?`).Bind(videoID).ScanSingle(&year, &episode)
if err != nil {
http.Error(w, "Video not found", http.StatusNotFound)
return
}
// Determine the video file path
downloadDir := os.Getenv("VIVAPLUS_DESTINATION")
if downloadDir == "" {
downloadDir = "./downloads"
}
seasonDir := filepath.Join(downloadDir, fmt.Sprintf("Season %d", year))
videoFilename := fmt.Sprintf("S%dE%03d.mp4", year, episode)
videoPath := filepath.Join(seasonDir, videoFilename)
// Check if the file exists
log.Printf("Streaming video: %s", videoPath)
_, err = os.Stat(videoPath)
if os.IsNotExist(err) {
http.Error(w, "Video file not found", http.StatusNotFound)
return
}
// Serve the file
http.ServeFile(w, r, videoPath)
}
func (a *App) handleMarkWatched(w http.ResponseWriter, r *http.Request) {
// Only handle POST requests
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse the video ID from the URL
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) < 3 {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
videoIDStr := pathParts[2]
videoID, err := strconv.Atoi(videoIDStr)
if err != nil {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
// Update the watch state in the database
err = a.db.Query(`UPDATE videos SET watch_state = NULL WHERE id = ?`).Bind(videoID).Exec()
if err != nil {
http.Error(w, "Failed to update watch state", http.StatusInternalServerError)
return
}
h := a.htmx.NewHandler(w, r)
if h.RenderPartial() {
vm, err := a.getViewModelForVideo(videoID)
if err != nil {
http.Error(w, "Failed to find video", http.StatusInternalServerError)
return
}
a.renderTemplate(w, "frag-index", vm)
} else {
// Redirect back to the homepage
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
func (a *App) handleMarkUnwatched(w http.ResponseWriter, r *http.Request) {
// Only handle POST requests
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse the video ID from the URL
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) < 3 {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
videoIDStr := pathParts[2]
videoID, err := strconv.Atoi(videoIDStr)
if err != nil {
http.Error(w, "Invalid video ID", http.StatusBadRequest)
return
}
// Update the watch state in the database
err = a.db.Query(`UPDATE videos SET watch_state = 'unwatched' WHERE id = ?`).Bind(videoID).Exec()
if err != nil {
http.Error(w, "Failed to update watch state", http.StatusInternalServerError)
return
}
h := a.htmx.NewHandler(w, r)
if h.RenderPartial() {
vm, err := a.getViewModelForVideo(videoID)
if err != nil {
http.Error(w, "Failed to find video", http.StatusInternalServerError)
return
}
a.renderTemplate(w, "frag-index", vm)
} else {
// Redirect back to the homepage
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}