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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
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.6.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.7.0 h1:gq75Ce7QTQ5Rj5fzS/6eeOA/enyV0oDMVt5mejwX14Y=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
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 {
|
||||
templates, err := template.ParseGlob("web/*.gohtml")
|
||||
templates, err := template.ParseGlob("web/*.go.html")
|
||||
if err != nil {
|
||||
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 {
|
||||
ID int
|
||||
PreviousID int
|
||||
Title string
|
||||
Description string
|
||||
Thumbnail string
|
||||
IsWatched bool
|
||||
}
|
||||
|
||||
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*/}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{.Title}} - Viva++</title>
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
</head>
|
||||
{{template "frag-header" .}}
|
||||
<body>
|
||||
<h1>Viva++</h1>
|
||||
<div class="back-button-container">
|
245
webview.go
245
webview.go
@ -10,17 +10,29 @@ import (
|
||||
"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/", serveVideo(db))
|
||||
http.HandleFunc("/stream/", serveStream(db))
|
||||
http.HandleFunc("/", serveIndex(db))
|
||||
// http.HandleFunc("/video/", handleVideoActions(db))
|
||||
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 {
|
||||
@ -28,37 +40,87 @@ func serveWebview(db *mysqlite.Db) {
|
||||
}
|
||||
}
|
||||
|
||||
func serveIndex(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Query the database for the oldest unwatched episode
|
||||
var id int
|
||||
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)
|
||||
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 serveVideo(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
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 {
|
||||
@ -75,8 +137,9 @@ func serveVideo(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Query the database for the video with the given ID
|
||||
var title, description string
|
||||
err = db.Query(`SELECT title, description FROM videos WHERE id = ?`).Bind(videoID).ScanSingle(&title, &description)
|
||||
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
|
||||
}
|
||||
@ -86,15 +149,10 @@ func serveVideo(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
||||
Title: title,
|
||||
Description: description,
|
||||
}
|
||||
err = renderTemplate(w, "video", vm)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
a.renderTemplate(w, "video", vm)
|
||||
}
|
||||
|
||||
func serveStream(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
@ -111,7 +169,7 @@ func serveStream(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get video metadata from database
|
||||
var year, episode int
|
||||
err = db.Query(`SELECT year, episode FROM videos WHERE id = ?`).Bind(videoID).ScanSingle(&year, &episode)
|
||||
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
|
||||
@ -137,43 +195,88 @@ func serveStream(db *mysqlite.Db) func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 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 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
|
||||
// }
|
||||
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
|
||||
}
|
||||
|
||||
// pathParts := strings.Split(r.URL.Path, "/")
|
||||
// if len(pathParts) < 4 {
|
||||
// http.Error(w, "Invalid URL format", http.StatusBadRequest)
|
||||
// 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]
|
||||
// action := pathParts[3]
|
||||
// videoID, err := strconv.Atoi(videoIDStr)
|
||||
// if err != nil {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user