Initial commit

This commit is contained in:
2025-05-22 18:08:15 +02:00
commit 364ffa15de
33 changed files with 900 additions and 0 deletions

15
radio/init.go Normal file
View File

@@ -0,0 +1,15 @@
package radio
import (
"embed"
"fmt"
"net/http"
)
//go:embed templates/*
var content embed.FS
func InitHttpHandlers() {
prefix := "/radio/"
http.HandleFunc(fmt.Sprint(prefix, "trains"), trains)
}

130
radio/playlist.go Normal file
View File

@@ -0,0 +1,130 @@
package radio
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
type YoutubeChannel struct {
Id string
Name string
}
type PlaylistItem struct {
Etag string
Id string
PublishedAt time.Time
Channel YoutubeChannel
VideoId string
Title string
Description string
ThumbnailUrl string
Position int
}
type PlaylistResponseSnippet struct {
PublishedAt string `json:"publishedAt"`
VideoOwnerChannelId string `json:"videoOwnerChannelId"`
VideoOwnerChannelTitle string `json:"videoOwnerChannelTitle"`
ResourceId PlaylistResponseResourceId `json:"resourceId"`
Title string `json:"title"`
Description string `json:"description"`
Position int `json:"position"`
}
type PlaylistResponseItem struct {
Etag string `json:"etag"`
Id string `json:"id"`
Snippet PlaylistResponseSnippet `json:"snippet"`
}
type PlaylistResponseResourceId struct {
VideoId string `json:"videoId"`
}
type PlaylistResponse struct {
Items []PlaylistResponseItem `json:"items"`
NextPageToken string `json:"nextPageToken"`
}
func requestYouTubePlaylist(playlistId string, pageToken string) ([]PlaylistItem, string) {
apiUrl, err := url.Parse("https://www.googleapis.com/youtube/v3/playlistItems")
if err != nil {
panic(err)
}
q := apiUrl.Query()
q.Add("key", os.Getenv("GOOGLE_API_KEY"))
q.Add("playlistId", playlistId)
q.Add("part", "id,snippet,contentDetails,status")
q.Add("maxResults", "50")
if pageToken != "" {
q.Add("pageToken", pageToken)
}
apiUrl.RawQuery = q.Encode()
response, err := http.Get(apiUrl.String())
if err != nil {
return nil, ""
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, ""
}
//type playlistResponse PlaylistResponse
var playlistItems PlaylistResponse
err = json.Unmarshal(body, &playlistItems)
if err != nil {
return nil, ""
}
var items []PlaylistItem
for _, item := range playlistItems.Items {
publishedAt, err := time.Parse(time.RFC3339, item.Snippet.PublishedAt)
if err != nil {
panic(err)
}
items = append(items, PlaylistItem{
Etag: item.Etag,
Id: item.Id,
PublishedAt: publishedAt,
VideoId: item.Snippet.ResourceId.VideoId,
Title: item.Snippet.Title,
Description: item.Snippet.Description,
ThumbnailUrl: fmt.Sprint("https://youtubethumbnails.prettysunflower.moe/", item.Snippet.ResourceId.VideoId),
Position: item.Snippet.Position,
Channel: YoutubeChannel{
Id: item.Snippet.VideoOwnerChannelId,
Name: item.Snippet.VideoOwnerChannelTitle,
},
})
}
return items, playlistItems.NextPageToken
}
func getPlaylistOfVideos(playlistId string) []PlaylistItem {
var playlistItems []PlaylistItem
nextPageToken := ""
for {
newItems, newNextPageToken := requestYouTubePlaylist(playlistId, nextPageToken)
playlistItems = append(playlistItems, newItems...)
if newNextPageToken == "" {
return playlistItems
}
nextPageToken = newNextPageToken
}
}

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>prettysunflower - The trains radio</title>
<link rel="stylesheet" type="text/css" href="/static/static/css/style.css">
</head>
<body class="page-radio-trains">
<div class="video-zone">
<video id="video-player" controls></video>
</div>
<h1>The trains radio</h1>
<main>
{{ range .PlaylistItems }}
<div class="playlist-item" data-videoid="{{ .VideoId }}">
<img src="{{ .ThumbnailUrl }}">
<p>
<a href="https://invidious.prettysunflower.moe/watch?v={{ .VideoId }}">
<strong>{{ .Title }}</strong><br>
{{ .Channel.Name }}
</a>
</p>
</div>
{{ end }}
</main>
<script>
const playlistItems = document.getElementsByClassName("playlist-item");
const videoBlock = document.querySelector(".video-zone video");
function loadVideo(videoId) {
videoBlock.src = `https://invidious.prettysunflower.moe/latest_version?local=true&id=${videoId}`;
videoBlock.currentTime = 0;
videoBlock.play();
}
let index = 0;
playlistItems[index].classList.add('active');
loadVideo(playlistItems[index].dataset.videoid);
videoBlock.addEventListener('ended', () => {
playlistItems[index].classList.remove('active');
index = index + 1;
if (index < playlistItems.length) {
playlistItems[index].classList.add('active');
loadVideo(playlistItems[index].dataset.videoid);
}
})
</script>
</body>
</html>

34
radio/trains.go Normal file
View File

@@ -0,0 +1,34 @@
package radio
import (
"html/template"
"math/rand"
"net/http"
)
type TrainsTemplateData struct {
PlaylistItems []PlaylistItem
}
func trains(w http.ResponseWriter, r *http.Request) {
trainsPlaylist := getPlaylistOfVideos("PLZoWUOSrw9RfbTAwQ1pTg3rZD-jZJHd9S")
rand.Shuffle(len(trainsPlaylist), func(i, j int) {
trainsPlaylist[i], trainsPlaylist[j] = trainsPlaylist[j], trainsPlaylist[i]
})
templateFile := "templates/trains.tmpl"
files, err := template.New(templateFile).ParseFS(content, templateFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = files.ExecuteTemplate(w, "trains.tmpl", TrainsTemplateData{
PlaylistItems: trainsPlaylist,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}