Initial commit
This commit is contained in:
33
.gitea/workflows/docker_build.yaml
Normal file
33
.gitea/workflows/docker_build.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Build Docker images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "*" ]
|
||||
|
||||
jobs:
|
||||
acls:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24.3'
|
||||
|
||||
- uses: ko-build/setup-ko@v0.8
|
||||
with:
|
||||
version: v0.18.0
|
||||
env:
|
||||
KO_DOCKER_REPO: git.prettysunflower.moe/prettysunflower
|
||||
|
||||
- env:
|
||||
auth_token: ${{ secrets.HUB_TOKEN }}
|
||||
run: |
|
||||
echo "${auth_token}" | ko login git.prettysunflower.moe --username ${{ vars.HUB_USERNAME }} --password-stdin
|
||||
ko build -B
|
45
.gitea/workflows/static_docker_build.yaml
Normal file
45
.gitea/workflows/static_docker_build.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Build Docker images for static folder
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "*" ]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: static
|
||||
|
||||
jobs:
|
||||
acls:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: git.prettysunflower.moe/prettysunflower/website-static
|
||||
-
|
||||
name: Login to Gitea Container Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.prettysunflower.moe
|
||||
username: ${{ vars.HUB_USERNAME }}
|
||||
password: ${{ secrets.HUB_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: "{{defaultContext}}:static"
|
||||
push: ${{ gitea.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
build/
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/prettysunflower-website.iml" filepath="$PROJECT_DIR$/.idea/prettysunflower-website.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
9
.idea/prettysunflower-website.iml
generated
Normal file
9
.idea/prettysunflower-website.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
11
.ko.yaml
Normal file
11
.ko.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
defaultBaseImage: cgr.dev/chainguard/static
|
||||
defaultPlatforms:
|
||||
- linux/arm64
|
||||
- linux/amd64
|
||||
- linux/arm/v7
|
||||
|
||||
builds:
|
||||
- id: website
|
||||
main: .
|
||||
ldflags:
|
||||
- -s -w
|
28
keys/init.go
Normal file
28
keys/init.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package keys
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func InitHttpHandlers() {
|
||||
http.HandleFunc("/ssh/", sshKey)
|
||||
http.HandleFunc("/age/", ageKey)
|
||||
http.HandleFunc("/gpg/", gpgKey)
|
||||
http.HandleFunc("/gpg/koumbit/", gpgKey)
|
||||
}
|
||||
|
||||
func sshKey(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = io.WriteString(w, SSH_KEY)
|
||||
}
|
||||
|
||||
func ageKey(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = io.WriteString(w, AGE_KEY)
|
||||
}
|
||||
|
||||
func gpgKey(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = io.WriteString(w, GPG_KEY)
|
||||
}
|
28
keys/keys.go
Normal file
28
keys/keys.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package keys
|
||||
|
||||
const SSH_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKK/ydi3HD1cHP40hlYP3EU+h55rAj+nHkLhzcClHStj me@prettysunflower.moe"
|
||||
const AGE_KEY = "age1r0tjhg6uexyj0p7fp0ftv5h7r7e3ptzkk2797pznfvrvsm576u0s37yyaw"
|
||||
const GPG_KEY = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: 13EE 61FF 762C 86C7 FC92 71A4 ED9D 604F 0092 91F4
|
||||
Comment: Remilia Da Costa Faro <remilia@koumbit.org>
|
||||
Comment: Remilia Da Costa Faro <remilia@remilia.ch>
|
||||
|
||||
xjMEZ3w3SBYJKwYBBAHaRw8BAQdAcIpRU4WrTXDfljp/1PSE1fgPDAbOG/Fj/Dqb
|
||||
hLYx1orNK1JlbWlsaWEgRGEgQ29zdGEgRmFybyA8cmVtaWxpYUBrb3VtYml0Lm9y
|
||||
Zz7CjwQTFggANwUJBaOagAIbAwQLCQgHBRUICQoLBRYCAwEAFiEEE+5h/3Yshsf8
|
||||
knGk7Z1gTwCSkfQFAmf+RF0ACgkQ7Z1gTwCSkfT4OQD+KetHdN1L/M7TmFZ+Nbhz
|
||||
a5XBOmeYT3vpq8aKg0lcG+EA/RVwKvvzgqTzd1zBmgP7gqvFaFXZMlo0VVfsSrCJ
|
||||
9uANwo8EExYIADcWIQQT7mH/diyGx/yScaTtnWBPAJKR9AUCZ3w3SAUJBaOagAIb
|
||||
AwQLCQgHBRUICQoLBRYCAwEAAAoJEO2dYE8AkpH0nFgBAPW5fDQxAH8/Pr8ByvAs
|
||||
CNoCqPIKqvre3U6+JgvsQ/gFAP0arX0+IOjpfFiXCvnYEWMiL2RLi9T45DHrMN1V
|
||||
n341Bs0qUmVtaWxpYSBEYSBDb3N0YSBGYXJvIDxyZW1pbGlhQHJlbWlsaWEuY2g+
|
||||
wpwEExYKAEQCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AWIQQT
|
||||
7mH/diyGx/yScaTtnWBPAJKR9AUCZ/5EXQIZAQAKCRDtnWBPAJKR9N/jAP9C9Lci
|
||||
uRCwd9WxWbXlQBCdZI8h/0GFlnkOdY4O5nPzLQD/dz2raMl7pp9H7KL5r3ashOYo
|
||||
wwdGr1H1EEYyDFR9UAnOOARnfDdIEgorBgEEAZdVAQUBAQdAzUl/nTzragxZUHQ3
|
||||
HmmT0XfGgaNWKXuS1FAOI3KP6jkDAQgHwn4EGBYIACYWIQQT7mH/diyGx/yScaTt
|
||||
nWBPAJKR9AUCZ3w3SAUJBaOagAIbDAAKCRDtnWBPAJKR9MEaAP9TBEiI63CLyIr1
|
||||
6qhFwlEsiPOQS/hIYOoHJG1OPUZMfQEA3IvzRpZzFIb/pR4VvF+Zpddm1GD8yh0r
|
||||
IvOz2WQApgI=
|
||||
=mMXG
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
18
main.go
Normal file
18
main.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"prettysunflower-website/keys"
|
||||
"prettysunflower-website/pages"
|
||||
"prettysunflower-website/radio"
|
||||
"prettysunflower-website/static"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pages.InitHttpHandlers()
|
||||
radio.InitHttpHandlers()
|
||||
static.InitHttpHandlers()
|
||||
keys.InitHttpHandlers()
|
||||
|
||||
_ = http.ListenAndServe(":3334", nil)
|
||||
}
|
38
pages/email_autoconfig.go
Normal file
38
pages/email_autoconfig.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type EmailAutoconfigTemplateData struct {
|
||||
Domain string
|
||||
DisplayName string
|
||||
ShortDisplayName string
|
||||
ImapServer string
|
||||
PopServer string
|
||||
SmtpServer string
|
||||
}
|
||||
|
||||
func emailAutoconfig(w http.ResponseWriter, r *http.Request) {
|
||||
templateFile := "templates/email_autoconfig.tmpl"
|
||||
files, err := template.New(templateFile).ParseFS(content, templateFile)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = files.ExecuteTemplate(w, "email_autoconfig.tmpl", EmailAutoconfigTemplateData{
|
||||
Domain: "prettysunflower.moe",
|
||||
DisplayName: "prettysunflower's mail server",
|
||||
ShortDisplayName: "prettysunflower",
|
||||
ImapServer: "mail.prettysunflower.moe",
|
||||
PopServer: "mail.prettysunflower.moe",
|
||||
SmtpServer: "mail.prettysunflower.moe",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
32
pages/init.go
Normal file
32
pages/init.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var content embed.FS
|
||||
|
||||
func InitHttpHandlers() {
|
||||
http.HandleFunc("/{$}", homepage)
|
||||
http.HandleFunc("/.well-known/autoconfig/mail/config-v1.1.xml", emailAutoconfig)
|
||||
http.HandleFunc("/mail/config-v1.1.xml", emailAutoconfig)
|
||||
}
|
||||
|
||||
func homepage(w http.ResponseWriter, r *http.Request) {
|
||||
templateFile := "templates/homepage.tmpl"
|
||||
files, err := template.New(templateFile).ParseFS(content, templateFile)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = files.ExecuteTemplate(w, "homepage.tmpl", nil)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
28
pages/templates/email_autoconfig.tmpl
Normal file
28
pages/templates/email_autoconfig.tmpl
Normal file
@@ -0,0 +1,28 @@
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="{{ .Domain }}">
|
||||
<domain>{{ .Domain }}</domain>
|
||||
<displayName>{{ .DisplayName }}</displayName>
|
||||
<displayShortName>{{ .ShortDisplayName }}</displayShortName>
|
||||
<incomingServer type="imap">
|
||||
<hostname>{{ .ImapServer }}</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<incomingServer type="pop3">
|
||||
<hostname>{{ .PopServer }}</hostname>
|
||||
<port>995</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>{{ .SmtpServer }}</hostname>
|
||||
<port>587</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</outgoingServer>
|
||||
</emailProvider>
|
||||
</clientConfig>
|
78
pages/templates/homepage.tmpl
Normal file
78
pages/templates/homepage.tmpl
Normal file
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>prettysunflower</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/static/css/style.css">
|
||||
</head>
|
||||
<body class="page-index">
|
||||
<header>
|
||||
<div>
|
||||
<img src="https://kakigoori.dev/5819581e-42b9-41a7-bab6-c3f99de30934/auto" alt="Avatar of prettysunflower">
|
||||
|
||||
<div>
|
||||
<h1>prettysunflower</h1>
|
||||
<p>
|
||||
Nyallo! We're Remilia, Xeon, and Takeno!
|
||||
</p>
|
||||
<p>
|
||||
We're a system of 3, we're software and website developers at
|
||||
the <a href="https://koumbit.org">Réseau Koumbit</a>, we're Touhou and Factorio players,
|
||||
and we're your local trans women/enby/plural person wishing you a good day!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="columns">
|
||||
<div>
|
||||
<h2>Our pronouns 🏳️⚧</h2>
|
||||
|
||||
<p>
|
||||
<strong>Remilia:</strong> <a href="https://pronouns.within.lgbt/they/them/their/theirs/themself">they/them</a> and <a href="https://pronouns.within.lgbt/she/her/her/hers/herself">she/her</a><br>
|
||||
<strong>Xeon:</strong> <a href="https://pronouns.within.lgbt/they/them/their/theirs/themselves">they/them</a><br>
|
||||
<strong>Takeno:</strong> <a href="https://pronouns.within.lgbt/she/her/her/hers/herself">she/her</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>The ways to contact us</h2>
|
||||
<p>
|
||||
<a href="mailto:me@prettysunflower.moe">me@prettysunflower.moe</a><br>
|
||||
<a href="https://bsky.app/profile/prettysunflower.moe">Bluesky</a><br>
|
||||
<a href="https://akkoma.prettysunflower.moe">Fediverse</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Our code</h2>
|
||||
<p>
|
||||
<a href="https://git.prettysunflower.moe">Gitea</a><br>
|
||||
<a href="https://github.com/prettysunflower">GitHub</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="main-projects">
|
||||
<h2>Our main projects</h2>
|
||||
|
||||
<div>
|
||||
<img src="https://kakigoori.dev/bcf6fdd9-3855-4f4d-9b5a-0791e14e29c7/height/200/auto">
|
||||
|
||||
<div>
|
||||
<h3>Kakigoori</h3>
|
||||
<p>
|
||||
Kakigoori is an picture distribution system to publish images on the web.
|
||||
Upload it once, and Kakigoori will create versions of the image
|
||||
optimized for the web (AVIF, WebP).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
15
radio/init.go
Normal file
15
radio/init.go
Normal 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
130
radio/playlist.go
Normal 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
|
||||
}
|
||||
}
|
56
radio/templates/trains.tmpl
Normal file
56
radio/templates/trains.tmpl
Normal 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
34
radio/trains.go
Normal 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
|
||||
}
|
||||
}
|
4
static/Caddyfile
Normal file
4
static/Caddyfile
Normal file
@@ -0,0 +1,4 @@
|
||||
:8001 {
|
||||
root * /srv/
|
||||
file_server
|
||||
}
|
4
static/Dockerfile
Normal file
4
static/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM caddy:latest
|
||||
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY static/ /srv/
|
5
static/noStatic.go
Normal file
5
static/noStatic.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build !serveStatic
|
||||
|
||||
package static
|
||||
|
||||
func InitHttpHandlers() {}
|
16
static/serveStatic.go
Normal file
16
static/serveStatic.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build serveStatic
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
func InitHttpHandlers() {
|
||||
fs := http.FileServerFS(staticFS)
|
||||
http.Handle("/static/", http.StripPrefix("/static", fs))
|
||||
}
|
8
static/static/css/body.scss
Normal file
8
static/static/css/body.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
@use "fonts";
|
||||
@use "colors";
|
||||
|
||||
body {
|
||||
font-family: fonts.$font-stack;
|
||||
background-color: colors.$background-color;
|
||||
margin: 0;
|
||||
}
|
1
static/static/css/colors.scss
Normal file
1
static/static/css/colors.scss
Normal file
@@ -0,0 +1 @@
|
||||
$background-color: oklch(0.97 0.0261 90.1);
|
3
static/static/css/fonts.scss
Normal file
3
static/static/css/fonts.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap');
|
||||
|
||||
$font-stack: "Open Sans", apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
48
static/static/css/index.scss
Normal file
48
static/static/css/index.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
@use "layout";
|
||||
|
||||
.page-index {
|
||||
header {
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
background-image: url('https://kakigoori.dev/c152e805-b859-4ad0-817c-4d671e5f15ad/auto');
|
||||
background-size: cover;
|
||||
background-position: top center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
width: min(80%, 800px);
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
@include layout.light-hr;
|
||||
}
|
||||
|
||||
.columns {
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.main-projects {
|
||||
& > div {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
10
static/static/css/layout.scss
Normal file
10
static/static/css/layout.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.columns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
@mixin light-hr($height: .5px, $color: oklch(75% 0 0deg)) {
|
||||
border: $height solid $color;
|
||||
}
|
60
static/static/css/radio.scss
Normal file
60
static/static/css/radio.scss
Normal file
@@ -0,0 +1,60 @@
|
||||
@use "colors";
|
||||
|
||||
.page-radio-trains {
|
||||
.video-zone {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
|
||||
#video-player {
|
||||
aspect-ratio: 16 / 9;
|
||||
max-width: 80%;
|
||||
max-height: 50vh;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding: 0 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
& > div {
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: oklch(from colors.$background-color calc(l - 0.05) c h);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: .5em;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
102
static/static/css/style.css
Normal file
102
static/static/css/style.css
Normal file
@@ -0,0 +1,102 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap");
|
||||
.columns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background-color: oklch(97% 0.0261 90.1deg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-index header {
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
background-image: url("https://kakigoori.dev/c152e805-b859-4ad0-817c-4d671e5f15ad/auto");
|
||||
background-size: cover;
|
||||
background-position: top center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.page-index header > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
width: min(80%, 800px);
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.page-index header img {
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.page-index hr {
|
||||
border: 0.5px solid oklch(75% 0 0deg);
|
||||
}
|
||||
.page-index .columns h2 {
|
||||
text-align: center;
|
||||
}
|
||||
.page-index .main-projects > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.page-radio-trains .video-zone {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.page-radio-trains .video-zone #video-player {
|
||||
aspect-ratio: 16/9;
|
||||
max-width: 80%;
|
||||
max-height: 50vh;
|
||||
width: 100%;
|
||||
}
|
||||
.page-radio-trains h1 {
|
||||
padding: 0 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.page-radio-trains main {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
.page-radio-trains main > div img {
|
||||
width: 100%;
|
||||
}
|
||||
.page-radio-trains main > div.active {
|
||||
background-color: oklch(from oklch(97% 0.0261 90.1deg) calc(l - 0.05) c h);
|
||||
}
|
||||
.page-radio-trains main > div p {
|
||||
margin: 0.5em;
|
||||
}
|
||||
.page-radio-trains main > div p a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
@media screen and (max-width: 992px) {
|
||||
.page-radio-trains main {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.page-radio-trains main {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 576px) {
|
||||
.page-radio-trains main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
1
static/static/css/style.css.map
Normal file
1
static/static/css/style.css.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["fonts.scss","layout.scss","body.scss","colors.scss","index.scss","radio.scss"],"names":[],"mappings":"AAAQ;ACAR;EACI;EACA;EACA;EACA;;;ACDJ;EACI,aFFS;EEGT,kBCLe;EDMf;;;AEHA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAIR;EHxBA;;AG6BI;EACI;;AAKJ;EACI;;;ACzCR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGI;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAEA;EACI;EACA;;AAKZ;EAzBJ;IA0BQ;;;AAGJ;EA7BJ;IA8BQ;;;AAGJ;EAjCJ;IAkCQ","file":"style.css"}
|
4
static/static/css/style.scss
Normal file
4
static/static/css/style.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
@use "layout";
|
||||
@use "body";
|
||||
@use "index";
|
||||
@use "radio";
|
Reference in New Issue
Block a user