This commit is contained in:
parent
c36a533d17
commit
91be8274c4
3
go.mod
3
go.mod
@ -5,7 +5,8 @@ go 1.24
|
|||||||
toolchain go1.24.0
|
toolchain go1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.6.0
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.7.0
|
||||||
|
github.com/donseba/go-htmx v1.12.0
|
||||||
github.com/playwright-community/playwright-go v0.4902.0
|
github.com/playwright-community/playwright-go v0.4902.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
6
go.sum
6
go.sum
@ -1,10 +1,12 @@
|
|||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.6.0 h1:Tqo9jnPXBDzIiMh+CrrFd5epITbG8UWqVfpLk21R/C0=
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.7.0 h1:gq75Ce7QTQ5Rj5fzS/6eeOA/enyV0oDMVt5mejwX14Y=
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.6.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.7.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
|
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
|
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
|
||||||
|
github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||||
|
@ -12,9 +12,9 @@ func staticFileServer() http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderTemplate(wr io.Writer, templateName string, viewModel any) error {
|
func renderTemplate(wr io.Writer, templateName string, viewModel any) error {
|
||||||
templates, err := template.ParseGlob("web/*.gohtml")
|
templates, err := template.ParseGlob("web/*.go.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return templates.ExecuteTemplate(wr, templateName+".gohtml", viewModel)
|
return templates.ExecuteTemplate(wr, templateName+".go.html", viewModel)
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@ package main
|
|||||||
|
|
||||||
type VideoInfoVM struct {
|
type VideoInfoVM struct {
|
||||||
ID int
|
ID int
|
||||||
|
PreviousID int
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
Thumbnail string
|
Thumbnail string
|
||||||
|
IsWatched bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoPlayerVM struct {
|
type VideoPlayerVM struct {
|
||||||
|
8
web/frag-header.go.html
Normal file
8
web/frag-header.go.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{{define "frag-header"}}
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Viva++</title>
|
||||||
|
<link href="/static/style.css" rel="stylesheet">
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
|
</head>
|
||||||
|
{{end}}
|
32
web/frag-index.go.html
Normal file
32
web/frag-index.go.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{{- /*gotype: vivaplusdl.VideoInfoVM*/}}
|
||||||
|
{{define "frag-index"}}
|
||||||
|
<div class="video-box">
|
||||||
|
<div class="thumbnail-container">
|
||||||
|
<a href="/video/{{.ID}}">
|
||||||
|
<img src="{{.Thumbnail}}" alt="Thumbnail" class="video-thumbnail" />
|
||||||
|
<div class="play-button">
|
||||||
|
<span class="play-symbol">▶</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="video-info">
|
||||||
|
<h2 class="video-title">{{.Title}}</h2>
|
||||||
|
<p class="video-description">{{.Description}}</p>
|
||||||
|
<div class="video-actions">
|
||||||
|
<a href="/video/{{.ID}}" class="watch-button">Watch Now</a>
|
||||||
|
<form hx-boost="true" method="post" action="{{if .IsWatched}}/mark-unwatched/{{.ID}}{{else}}/mark-watched/{{.ID}}{{end}}">
|
||||||
|
<button type="submit" class="mark-watched">
|
||||||
|
{{if .IsWatched}}
|
||||||
|
Mark as Unwatched
|
||||||
|
{{else}}
|
||||||
|
Mark as Watched
|
||||||
|
{{end}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{if ne .PreviousID 0}}
|
||||||
|
<a href="/?video={{.PreviousID}}" class="mark-watched">Show Previous</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
11
web/index.go.html
Normal file
11
web/index.go.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{- /*gotype: vivaplusdl.VideoInfoVM*/}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{template "frag-header" .}}
|
||||||
|
<body>
|
||||||
|
<h1>Viva++</h1>
|
||||||
|
<div class="video-container">
|
||||||
|
{{template "frag-index" .}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,28 +0,0 @@
|
|||||||
{{- /*gotype: vivaplusdl.VideoInfoVM*/}}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Viva++</title>
|
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Viva++</h1>
|
|
||||||
<div class="video-container">
|
|
||||||
<div class="video-box">
|
|
||||||
<div class="thumbnail-container">
|
|
||||||
<a href="/video/{{.ID}}">
|
|
||||||
<img src="{{.Thumbnail}}" alt="Thumbnail" class="video-thumbnail" />
|
|
||||||
<div class="play-button">
|
|
||||||
<span class="play-symbol">▶</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="video-info">
|
|
||||||
<h2 class="video-title">{{.Title}}</h2>
|
|
||||||
<p class="video-description">{{.Description}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,11 +1,7 @@
|
|||||||
{{- /*gotype: vivaplusdl.VideoPlayerVM*/}}
|
{{- /*gotype: vivaplusdl.VideoPlayerVM*/}}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
{{template "frag-header" .}}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>{{.Title}} - Viva++</title>
|
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Viva++</h1>
|
<h1>Viva++</h1>
|
||||||
<div class="back-button-container">
|
<div class="back-button-container">
|
369
webview.go
369
webview.go
@ -10,17 +10,29 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||||
|
"github.com/donseba/go-htmx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
db *mysqlite.Db
|
||||||
|
htmx *htmx.HTMX
|
||||||
|
}
|
||||||
|
|
||||||
func serveWebview(db *mysqlite.Db) {
|
func serveWebview(db *mysqlite.Db) {
|
||||||
addr := "localhost:8081"
|
addr := "localhost:8081"
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
db: db,
|
||||||
|
htmx: htmx.New(),
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("Listening on %s", addr)
|
log.Printf("Listening on %s", addr)
|
||||||
http.Handle("/static/", http.StripPrefix("/static/", staticFileServer()))
|
http.Handle("/static/", http.StripPrefix("/static/", staticFileServer()))
|
||||||
http.HandleFunc("/video/", serveVideo(db))
|
http.HandleFunc("/video/", app.serveVideo)
|
||||||
http.HandleFunc("/stream/", serveStream(db))
|
http.HandleFunc("/stream/", app.serveStream)
|
||||||
http.HandleFunc("/", serveIndex(db))
|
http.HandleFunc("/mark-watched/", app.handleMarkWatched)
|
||||||
// http.HandleFunc("/video/", handleVideoActions(db))
|
http.HandleFunc("/mark-unwatched/", app.handleMarkUnwatched)
|
||||||
|
http.HandleFunc("/", app.serveIndex)
|
||||||
|
|
||||||
err := http.ListenAndServe(addr, nil)
|
err := http.ListenAndServe(addr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -28,152 +40,243 @@ func serveWebview(db *mysqlite.Db) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveIndex(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
func (a *App) renderTemplate(w http.ResponseWriter, name string, viewmodel interface{}) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
err := renderTemplate(w, "index", viewmodel)
|
||||||
// Query the database for the oldest unwatched episode
|
if err != nil {
|
||||||
var id int
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
var title, description, thumbnail string
|
|
||||||
err := db.Query(`SELECT id, title, description, thumbnail FROM videos WHERE (watch_state IS NULL OR watch_state != 'watched') ORDER BY upload_date, episode LIMIT 1`).ScanSingle(&id, &title, &description, &thumbnail)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "No unwatched episodes found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vm := VideoInfoVM{
|
|
||||||
ID: id,
|
|
||||||
Title: title,
|
|
||||||
Description: description,
|
|
||||||
Thumbnail: thumbnail,
|
|
||||||
}
|
|
||||||
err = renderTemplate(w, "index", vm)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveVideo(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
func (a *App) getViewModelForVideo(id int) (*VideoInfoVM, error) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
// Query the database for the requested video
|
||||||
if r.Method != http.MethodGet {
|
var title, description, thumbnail string
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
var watchState *string
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the video ID from the URL
|
// Find the previous video ID based on upload date and episode number
|
||||||
pathParts := strings.Split(r.URL.Path, "/")
|
var previousID int
|
||||||
if len(pathParts) < 3 {
|
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)
|
http.Error(w, "Invalid video ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to the default behavior - get the oldest unwatched video
|
||||||
|
vm, err = a.getViewModelForUnwachtedVideo()
|
||||||
|
}
|
||||||
|
|
||||||
videoIDStr := pathParts[2]
|
if err != nil {
|
||||||
videoID, err := strconv.Atoi(videoIDStr)
|
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 {
|
if err != nil {
|
||||||
http.Error(w, "Invalid video ID", http.StatusBadRequest)
|
http.Error(w, "Failed to find video", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
a.renderTemplate(w, "frag-index", vm)
|
||||||
// Query the database for the video with the given ID
|
} else {
|
||||||
var title, description string
|
// Redirect back to the homepage
|
||||||
err = db.Query(`SELECT title, description FROM videos WHERE id = ?`).Bind(videoID).ScanSingle(&title, &description)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Video not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vm := VideoPlayerVM{
|
|
||||||
ID: videoID,
|
|
||||||
Title: title,
|
|
||||||
Description: description,
|
|
||||||
}
|
|
||||||
err = renderTemplate(w, "video", vm)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveStream(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleMarkUnwatched(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
// Only handle POST requests
|
||||||
// Parse the video ID from the URL
|
if r.Method != http.MethodPost {
|
||||||
pathParts := strings.Split(r.URL.Path, "/")
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
if len(pathParts) < 3 {
|
return
|
||||||
http.Error(w, "Invalid video ID", http.StatusBadRequest)
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
videoIDStr := pathParts[2]
|
// Parse the video ID from the URL
|
||||||
videoID, err := strconv.Atoi(videoIDStr)
|
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 {
|
if err != nil {
|
||||||
http.Error(w, "Invalid video ID", http.StatusBadRequest)
|
http.Error(w, "Failed to find video", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
a.renderTemplate(w, "frag-index", vm)
|
||||||
// Get video metadata from database
|
} else {
|
||||||
var year, episode int
|
// Redirect back to the homepage
|
||||||
err = db.Query(`SELECT year, episode FROM videos WHERE id = ?`).Bind(videoID).ScanSingle(&year, &episode)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
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 handleVideoActions(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// // Only handle POST requests for actions
|
|
||||||
// if r.Method != http.MethodPost {
|
|
||||||
// http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pathParts := strings.Split(r.URL.Path, "/")
|
|
||||||
// if len(pathParts) < 4 {
|
|
||||||
// http.Error(w, "Invalid URL format", http.StatusBadRequest)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// videoIDStr := pathParts[2]
|
|
||||||
// action := pathParts[3]
|
|
||||||
// videoID, err := strconv.Atoi(videoIDStr)
|
|
||||||
// if err != nil {
|
|
||||||
// http.Error(w, "Invalid video ID", http.StatusBadRequest)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// switch action {
|
|
||||||
// case "mark-watched":
|
|
||||||
// err = db.Query(`UPDATE videos SET watch_state = 'watched' WHERE id = ?`).Bind(videoID).Exec()
|
|
||||||
// if err != nil {
|
|
||||||
// http.Error(w, "Failed to update watch state", http.StatusInternalServerError)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// // Redirect back to the homepage
|
|
||||||
// http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
||||||
// default:
|
|
||||||
// http.Error(w, "Unknown action", http.StatusBadRequest)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user