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"` } 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 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) var errNext, errPrevious error for row := range a.db.Query("SELECT id, thumbnail 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) 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) // } //}