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