diff options
| -rw-r--r-- | Dockerfile | 1 | ||||
| -rw-r--r-- | api.go | 58 | ||||
| -rw-r--r-- | docker-compose.yaml | 2 | ||||
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | home.go | 43 | ||||
| -rw-r--r-- | main.go | 90 | ||||
| -rw-r--r-- | templates/base.html | 12 | ||||
| -rw-r--r-- | templates/index.html | 3 | ||||
| -rw-r--r-- | templates/watch.html | 89 | ||||
| -rw-r--r-- | util.go | 25 | ||||
| -rw-r--r-- | watch.go | 34 | 
12 files changed, 243 insertions, 121 deletions
@@ -18,6 +18,7 @@ FROM alpine:latest  WORKDIR /app  COPY --from=builder /app/mintube . +COPY --from=builder /app/README.md .  COPY --from=builder /app/templates ./templates  COPY --from=builder /app/static ./static @@ -0,0 +1,58 @@ +package main + +import ( +	"fmt" +	"io" +	"log" +	"net/http" +) + +const youtubeAPI = "https://www.googleapis.com/youtube/v3/" + +/* function to make an api request */ +func apiRequest(w http.ResponseWriter, r *http.Request, endpoint string, videoIdParam string, part string) { +	videoID := r.URL.Query().Get("id") +	if videoID == "" { +		msg := "Missing ?id=VIDEO_ID parameter" +		http.Error(w, msg, http.StatusBadRequest) +		return +	} + +	if apiKey == "" { +		msg := "API_KEY environment variable not set" +		http.Error(w, msg, http.StatusInternalServerError) +		log.Println(msg) +		return +	} + +	url := fmt.Sprintf("%s?part=%s&%s=%s&key=%s", youtubeAPI + endpoint, part, videoIdParam, videoID, apiKey) +	resp, err := http.Get(url) +	if err != nil { +		msg := "Failed to fetch video info: " + err.Error() +		http.Error(w, msg, http.StatusInternalServerError) +		log.Println(msg) +		return +	} +	defer resp.Body.Close() + +	if resp.StatusCode != http.StatusOK { +		msg := "YouTube API error: " + resp.Status +		http.Error(w, msg, http.StatusBadGateway) +		log.Println(msg) +		return +	} + +	w.Header().Set("Content-Type", "application/json") +	io.Copy(w, resp.Body) +} + +/* routes */ +func setupRoutesAPI() { +	http.HandleFunc("/api/details", func(w http.ResponseWriter, r *http.Request) { +		apiRequest(w, r, "videos", "id", "snippet,statistics,topicDetails") +	}) + +	http.HandleFunc("/api/comments", func(w http.ResponseWriter, r *http.Request) { +		apiRequest(w, r, "commentThreads", "videoId", "snippet,replies&maxResults=100") +	}) +} diff --git a/docker-compose.yaml b/docker-compose.yaml index bd98f85..3ad6b28 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,4 +6,4 @@ services:        - 7008:8080 # TOOB      # Add api key here, or in .env file      #environment: -    #  - API_KEY: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +    #  API_KEY: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -2,4 +2,7 @@ module tjkeller.xyz/mintube  go 1.23.7 -require github.com/joho/godotenv v1.5.1 +require ( +	github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b +	github.com/joho/godotenv v1.5.1 +) @@ -1,2 +1,4 @@ +github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk= +github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=  github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=  github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= @@ -0,0 +1,43 @@ +package main + +import ( +	"html/template" +	"io/ioutil" +	"log" +	"net/http" +	"github.com/gomarkdown/markdown" +) + +var templateIndexFiles = []string{ "templates/base.html", "templates/index.html" } +var templateIndex = template.Must(template.ParseFiles(templateIndexFiles...)) +var contentHTML template.HTML // just display README.md + +type IndexTemplateData struct { +	Content template.HTML +} + +func loadReadme() { +	mdBytes, err := ioutil.ReadFile("README.md") +	if err != nil { +		log.Fatalf("Failed to read markdown file: %v", err) +	} +	htmlBytes := markdown.ToHTML(mdBytes, nil, nil) +	contentHTML = template.HTML(htmlBytes) +} + +func renderIndexTemplate(w http.ResponseWriter) { +	if debug { +		reloadTemplate(&templateIndex, templateIndexFiles...) +		loadReadme() +	} +	err := templateIndex.Execute(w, IndexTemplateData{ +		Content: contentHTML, +	}) +	if err != nil { +		templateError(err, w) +	} +} + +func setupHome() { +	loadReadme() +} @@ -1,9 +1,6 @@  package main  import ( -	"fmt" -	"html/template" -	"io"  	"log"  	"net/http"  	"os" @@ -11,86 +8,35 @@ import (  	"github.com/joho/godotenv"  ) -const youtubeAPI = "https://www.googleapis.com/youtube/v3/" -var tmpl = template.Must(template.ParseFiles("templates/watch.html")) +var debug = false +var apiKey = "" -func apiRequest(w http.ResponseWriter, r *http.Request, endpoint string, videoIdParam string, part string) { -	videoID := r.URL.Query().Get("id") -	if videoID == "" { -		msg := "Missing ?id=VIDEO_ID parameter" -		http.Error(w, msg, http.StatusBadRequest) -		return -	} - -	apiKey := os.Getenv("API_KEY") -	if apiKey == "" { -		msg := "API_KEY environment variable not set" -		http.Error(w, msg, http.StatusInternalServerError) -		log.Println(msg) -		return -	} - -	//url := fmt.Sprintf("%s?part=snippet&id=%s&key=%s", youtubeAPI, videoID, apiKey) -	url := fmt.Sprintf("%s?part=%s&%s=%s&key=%s", youtubeAPI + endpoint, part, videoIdParam, videoID, apiKey) -	resp, err := http.Get(url) -	if err != nil { -		msg := "Failed to fetch video info: " + err.Error() -		http.Error(w, msg, http.StatusInternalServerError) -		log.Println(msg) -		return -	} -	defer resp.Body.Close() - -	if resp.StatusCode != http.StatusOK { -		msg := "YouTube API error: " + resp.Status -		http.Error(w, msg, http.StatusBadGateway) -		log.Println(msg) -		return -	} - -	w.Header().Set("Content-Type", "application/json") -	io.Copy(w, resp.Body) -} - -func videoDetailsHandler(w http.ResponseWriter, r *http.Request) { -	apiRequest(w, r, "videos", "id", "snippet,statistics,topicDetails") -} - -func commentThreadsHandler(w http.ResponseWriter, r *http.Request) { -	apiRequest(w, r, "commentThreads", "videoId", "snippet,replies&maxResults=100") -} - -func handler(w http.ResponseWriter, r *http.Request) { -	id := strings.Trim(r.URL.Path, "/") -	//if path == "" { -	// TODO landing page -	//} -	if id == "watch" { -		id = r.URL.Query().Get("v") -	} -	data := struct { -		Id string -	}{ -		Id: id, -	} -	err := tmpl.Execute(w, data) -	if err != nil { -		msg := "Template execution error" -		http.Error(w, msg, http.StatusInternalServerError) -		log.Println(msg) +/* root handler */ +func rootHandler(w http.ResponseWriter, r *http.Request) { +	path := strings.Trim(r.URL.Path, "/") +	if path == "" { +		//http.ServeFile(w, r, "static/index.html") +		renderIndexTemplate(w) +	} else { +		renderWatchTemplate(w, path)  	}  } +/* main */  func main() {  	// load .env file if it exists  	godotenv.Load() +	debug = os.Getenv("DEBUG") != "" +	apiKey = os.Getenv("API_KEY")  	// setup routes +	setupRoutesAPI() +	setupRoutesWatch() +	setupHome()  	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) -	http.HandleFunc("/api/details", videoDetailsHandler) -	http.HandleFunc("/api/comments", commentThreadsHandler) +	http.HandleFunc("/", rootHandler) -	http.HandleFunc("/", handler) +	// start http server  	log.Println("Listening on http://localhost:8080")  	err := http.ListenAndServe(":8080", nil)  	if err != nil { diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..7c1f8eb --- /dev/null +++ b/templates/base.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +	<meta charset="UTF-8"> +	<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> +	<link rel="stylesheet" href="/static/style.css"> +	{{ block "scripts" . }}{{ end }} +</head> +<body> +	{{ block "content" . }}{{ end }} +</body> +</html> diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..c4ade88 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,3 @@ +{{ define "content" }} +	{{ .Content }} +{{ end }} diff --git a/templates/watch.html b/templates/watch.html index bae485d..be4be41 100644 --- a/templates/watch.html +++ b/templates/watch.html @@ -1,48 +1,43 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -	<meta charset="UTF-8"> -	<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> -	<link rel="stylesheet" href="/static/style.css"> -	<script src="/static/index.js" defer></script> -	<script src="https://www.youtube.com/iframe_api"></script> -</head> -<body> -	<iframe -		id="player" -		src="https://www.youtube-nocookie.com/embed/{{ .Id }}?enablejsapi=1&autoplay=1" -		frameborder="0" -		allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" -		referrerpolicy="no-referrer" -		allowfullscreen -	> -	</iframe> -	<h1 id="title"></h1> -	<button id="toggle-details">Show Details</button> -	<div id="details" style="display:none"> -		<span><a id="details-channel"></a>   👍 <span id="details-likes"></span></span> -		<br> -		<span><span id="details-views"></span> Views   <span id="details-date"></span></span> -		<br> -		<h3>Description:</h3> -		<span id="details-desc">No description has been added to this video.</span> -		<br> -		<h4>Tags:</h4> -		<div id="details-tags"></div> -	</div> +{{ define "scripts" }} +<script src="/static/index.js" defer></script> +<script src="https://www.youtube.com/iframe_api"></script> +{{ end }} + +{{ define "content" }} +<iframe +	id="player" +	src="https://www.youtube-nocookie.com/embed/{{ .Id }}?enablejsapi=1&autoplay=1" +	frameborder="0" +	allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" +	referrerpolicy="no-referrer" +	allowfullscreen +> +</iframe> +<h1 id="title"></h1> +<button id="toggle-details">Show Details</button> +<div id="details" style="display:none"> +	<span><a id="details-channel"></a>   👍 <span id="details-likes"></span></span>  	<br> -	<div id="comments" style="display:none"> -		<h3>Comments:</h3> -		<template id="template-comment"> -			<div class="comment"> -				<a class="author"></a> -				<span class="date"></span> -				<span class="date modified"></span> -				<div class="body"></div> -				<span>👍 <span class="likes"></span></span> -				<div class="replies"></div> -			</div> -		</template> -	</div> -</body> -</html> +	<span><span id="details-views"></span> Views   <span id="details-date"></span></span> +	<br> +	<h3>Description:</h3> +	<span id="details-desc">No description has been added to this video.</span> +	<br> +	<h4>Tags:</h4> +	<div id="details-tags"></div> +</div> +<br> +<div id="comments" style="display:none"> +	<h3>Comments:</h3> +	<template id="template-comment"> +		<div class="comment"> +			<a class="author"></a> +			<span class="date"></span> +			<span class="date modified"></span> +			<div class="body"></div> +			<span>👍 <span class="likes"></span></span> +			<div class="replies"></div> +		</div> +	</template> +</div> +{{ end }} @@ -0,0 +1,25 @@ +package main + +import ( +	"fmt" +	"html/template" +	"log" +	"net/http" +) + +func reloadTemplate(t **template.Template, files ...string) error { +	tmpl, err := template.ParseFiles(files...) +	if err != nil { +		msg := fmt.Sprintf("Template load error: %v", err) +		log.Println(msg) +	} else { +		*t = tmpl +	} +	return err +} + +func templateError(err error, w http.ResponseWriter) { +	msg := "Template execution error" +	http.Error(w, msg, http.StatusInternalServerError) +	log.Println(msg) +} diff --git a/watch.go b/watch.go new file mode 100644 index 0000000..8352e62 --- /dev/null +++ b/watch.go @@ -0,0 +1,34 @@ +package main + +import ( +	"html/template" +	"net/http" +) + +var templateWatchFiles = []string{ "templates/base.html", "templates/watch.html" } +var templateWatch = template.Must(template.ParseFiles(templateWatchFiles...)) + +/* render template */ +type WatchTemplateData struct { +	Id string +} + +func renderWatchTemplate(w http.ResponseWriter, id string) { +	if debug { +		reloadTemplate(&templateWatch, templateWatchFiles...) +	} +	err := templateWatch.Execute(w, WatchTemplateData{ +		Id: id, +	}) +	if err != nil { +		templateError(err, w) +	} +} + +/* routes */ +func setupRoutesWatch() { +	http.HandleFunc("/watch", func(w http.ResponseWriter, r *http.Request) { +		id := r.URL.Query().Get("v") +		renderWatchTemplate(w, id) +	}) +}  | 
