vivaplusdl/webview.go

341 lines
10 KiB
Go

package main
import (
"cmp"
"fmt"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
"github.com/donseba/go-htmx"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
)
type App struct {
db *mysqlite.Db
htmx *htmx.HTMX
}
func serveWebview(db *mysqlite.Db) {
addr := "0.0.0.0:8081"
app := &App{
db: db,
}
log.Printf("Listening on %s", addr)
router := gin.Default()
router.StaticFile("/", "static/index.html")
router.GET("/stream/:id", app.serveStream)
api := router.Group("/api")
api.GET("/homepage", app.homePage)
err := router.Run(addr)
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
//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)
//}
}
type VideoModel struct {
ID int `json:"id"`
Thumbnail string `json:"thumbnail"`
URL string `json:"url"`
Title string `json:"title"`
}
type HomePageModel struct {
Current VideoModel `json:"currentVideo"`
Next []VideoModel `json:"nextVideos"`
Previous []VideoModel `json:"previousVideos"`
}
func (a *App) homePage(c *gin.Context) {
model := HomePageModel{}
errCurrent := a.db.Query("SELECT id, thumbnail, title FROM videos WHERE (watch_state IS NULL OR watch_state != 'watched') ORDER BY upload_date, episode LIMIT 1").
ScanSingle(&model.Current.ID, &model.Current.Thumbnail, &model.Current.Title)
var errNext, errPrevious error
for row := range a.db.Query("SELECT id, thumbnail, title FROM videos WHERE (watch_state IS NULL OR watch_state != 'watched') ORDER BY upload_date, episode LIMIT 3 OFFSET 1").Range(&errNext) {
video := VideoModel{}
err := row.Scan(&video.ID, &video.Thumbnail, &video.Title)
if err != nil {
log.Printf("Failed to scan next video: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve next videos"})
return
}
video.URL = fmt.Sprintf("/stream/%d", video.ID)
model.Next = append(model.Next, video)
}
if err := cmp.Or(errCurrent, errNext, errPrevious); err != nil {
log.Printf("Failed to retrieve videos: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve videos"})
return
}
model.Current.URL = fmt.Sprintf("/stream/%d", model.Current.ID)
c.JSON(200, model)
}
//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(c *gin.Context) {
// Parse the video ID from the URL
idStr := c.Param("id")
videoID, err := strconv.Atoi(idStr)
if err != nil {
log.Printf("Failed to parse video ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid video ID"})
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 {
log.Printf("Failed to find video: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "Video not found"})
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) {
log.Printf("Video file not found: %s", videoPath)
c.JSON(http.StatusNotFound, gin.H{"error": "Video file not found"})
return
}
// Serve the file
w := c.Writer
r := c.Request
w.Header().Set("Content-Type", "video/mp4")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", videoFilename))
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
log.Printf("Serving video file: %s", videoPath)
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)
// }
//}