This commit is contained in:
		
							
								
								
									
										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
 | 
					 | 
				
			||||||
// 		}
 | 
					 | 
				
			||||||
// 	}
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user