All checks were successful
Build / build (push) Successful in 3m11s
283 lines
7.6 KiB
Go
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)
|
|
}
|
|
}
|