Compare commits
No commits in common. "9274796729a56290d788106e0ed5327643103e4a" and "69e2e22e47c452151ba686ed6528b10a145b3227" have entirely different histories.
9274796729
...
69e2e22e47
13 changed files with 46 additions and 343 deletions
|
@ -77,9 +77,5 @@ func handleConfigOverrides(config *model.Config) error {
|
||||||
if env, has := os.LookupEnv("ARIMELODY_DISCORD_CLIENT_ID"); has { config.Discord.ClientID = env }
|
if env, has := os.LookupEnv("ARIMELODY_DISCORD_CLIENT_ID"); has { config.Discord.ClientID = env }
|
||||||
if env, has := os.LookupEnv("ARIMELODY_DISCORD_SECRET"); has { config.Discord.Secret = env }
|
if env, has := os.LookupEnv("ARIMELODY_DISCORD_SECRET"); has { config.Discord.Secret = env }
|
||||||
|
|
||||||
if env, has := os.LookupEnv("ARIMELODY_TWITCH_BROADCASTER"); has { config.Twitch.Broadcaster = env }
|
|
||||||
if env, has := os.LookupEnv("ARIMELODY_TWITCH_CLIENT_ID"); has { config.Twitch.ClientID = env }
|
|
||||||
if env, has := os.LookupEnv("ARIMELODY_TWITCH_SECRET"); has { config.Twitch.Secret = env }
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TWITCH_API_BASE = "https://api.twitch.tv/helix/"
|
|
||||||
|
|
||||||
func TwitchSetup(app *model.AppState) error {
|
|
||||||
app.Twitch = &model.TwitchState{}
|
|
||||||
err := RefreshTwitchToken(app)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func RefreshTwitchToken(app *model.AppState) error {
|
|
||||||
if app.Twitch != nil && app.Twitch.Token != nil && time.Now().UTC().After(app.Twitch.Token.ExpiresAt) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
requestUrl, _ := url.Parse("https://id.twitch.tv/oauth2/token")
|
|
||||||
req, _ := http.NewRequest(http.MethodPost, requestUrl.String(), bytes.NewBuffer([]byte(url.Values{
|
|
||||||
"client_id": []string{ app.Config.Twitch.ClientID },
|
|
||||||
"client_secret": []string{ app.Config.Twitch.Secret },
|
|
||||||
"grant_type": []string{ "client_credentials" },
|
|
||||||
}.Encode())))
|
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type TwitchOAuthToken struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
}
|
|
||||||
oauthResponse := TwitchOAuthToken{}
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&oauthResponse)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Twitch.Token = &model.TwitchOAuthToken{
|
|
||||||
AccessToken: oauthResponse.AccessToken,
|
|
||||||
ExpiresAt: time.Now().UTC().Add(time.Second * time.Duration(oauthResponse.ExpiresIn)).UTC(),
|
|
||||||
TokenType: oauthResponse.TokenType,
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastStreamState *model.TwitchStreamInfo
|
|
||||||
var lastStreamStateAt time.Time
|
|
||||||
|
|
||||||
func GetTwitchStatus(app *model.AppState, broadcaster string) (*model.TwitchStreamInfo, error) {
|
|
||||||
if lastStreamState != nil && time.Now().UTC().Before(lastStreamStateAt.Add(time.Minute)) {
|
|
||||||
return lastStreamState, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print("MAKING COSTLY REQUEST TO TWITCH.TV API...\n")
|
|
||||||
|
|
||||||
requestUrl, _ := url.Parse(TWITCH_API_BASE + "streams")
|
|
||||||
requestUrl.RawQuery = url.Values{
|
|
||||||
"user_login": []string{ broadcaster },
|
|
||||||
}.Encode()
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, requestUrl.String(), nil)
|
|
||||||
req.Header.Set("Client-Id", app.Config.Twitch.ClientID)
|
|
||||||
req.Header.Set("Authorization", "Bearer " + app.Twitch.Token.AccessToken)
|
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamsResponse struct {
|
|
||||||
Data []model.TwitchStreamInfo `json:"data"`
|
|
||||||
}
|
|
||||||
streamInfo := StreamsResponse{}
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&streamInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(streamInfo.Data) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lastStreamState = &streamInfo.Data[0]
|
|
||||||
lastStreamStateAt = time.Now().UTC()
|
|
||||||
return lastStreamState, nil
|
|
||||||
}
|
|
47
main.go
47
main.go
|
@ -22,6 +22,7 @@ import (
|
||||||
"arimelody-web/cursor"
|
"arimelody-web/cursor"
|
||||||
"arimelody-web/log"
|
"arimelody-web/log"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
|
"arimelody-web/templates"
|
||||||
"arimelody-web/view"
|
"arimelody-web/view"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
@ -39,7 +40,6 @@ func main() {
|
||||||
|
|
||||||
app := model.AppState{
|
app := model.AppState{
|
||||||
Config: controller.GetConfig(),
|
Config: controller.GetConfig(),
|
||||||
Twitch: nil,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialise database connection
|
// initialise database connection
|
||||||
|
@ -460,11 +460,6 @@ func main() {
|
||||||
// handle DB migrations
|
// handle DB migrations
|
||||||
controller.CheckDBVersionAndMigrate(app.DB)
|
controller.CheckDBVersionAndMigrate(app.DB)
|
||||||
|
|
||||||
err = controller.TwitchSetup(&app)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initial invite code
|
// initial invite code
|
||||||
accountsCount := 0
|
accountsCount := 0
|
||||||
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
||||||
|
@ -516,13 +511,49 @@ func createServeMux(app *model.AppState) *http.ServeMux {
|
||||||
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
|
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
|
||||||
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
|
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
|
||||||
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
|
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
|
||||||
mux.Handle("/uploads/", http.StripPrefix("/uploads", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
|
mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
|
||||||
mux.Handle("/cursor-ws", cursor.Handler(app))
|
mux.Handle("/cursor-ws", cursor.Handler(app))
|
||||||
mux.Handle("/", view.IndexHandler(app))
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodHead {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
||||||
|
err := templates.IndexTemplate.Execute(w, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
staticHandler("public").ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func staticHandler(directory string) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
||||||
|
|
||||||
|
// does the file exist?
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// is thjs a directory? (forbidden)
|
||||||
|
if info.IsDir() {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var PoweredByStrings = []string{
|
var PoweredByStrings = []string{
|
||||||
"nerd rage",
|
"nerd rage",
|
||||||
"estrogen",
|
"estrogen",
|
||||||
|
|
|
@ -21,12 +21,6 @@ type (
|
||||||
Secret string `toml:"secret"`
|
Secret string `toml:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
TwitchConfig struct {
|
|
||||||
Broadcaster string `toml:"broadcaster"`
|
|
||||||
ClientID string `toml:"client_id"`
|
|
||||||
Secret string `toml:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Config struct {
|
Config struct {
|
||||||
BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."`
|
BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."`
|
||||||
Host string `toml:"host"`
|
Host string `toml:"host"`
|
||||||
|
@ -35,13 +29,11 @@ type (
|
||||||
TrustedProxies []string `toml:"trusted_proxies"`
|
TrustedProxies []string `toml:"trusted_proxies"`
|
||||||
DB DBConfig `toml:"db"`
|
DB DBConfig `toml:"db"`
|
||||||
Discord DiscordConfig `toml:"discord"`
|
Discord DiscordConfig `toml:"discord"`
|
||||||
Twitch TwitchConfig `toml:"twitch"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppState struct {
|
AppState struct {
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
Config Config
|
Config Config
|
||||||
Log log.Logger
|
Log log.Logger
|
||||||
Twitch *TwitchState
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
TwitchOAuthToken struct {
|
|
||||||
AccessToken string
|
|
||||||
ExpiresAt time.Time
|
|
||||||
TokenType string
|
|
||||||
}
|
|
||||||
|
|
||||||
TwitchState struct {
|
|
||||||
Token *TwitchOAuthToken
|
|
||||||
}
|
|
||||||
|
|
||||||
TwitchStreamInfo struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
UserLogin string `json:"user_login"`
|
|
||||||
UserName string `json:"user_name"`
|
|
||||||
GameID string `json:"game_id"`
|
|
||||||
GameName string `json:"game_name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
ViewerCount int `json:"viewer_count"`
|
|
||||||
StartedAt string `json:"started_at"`
|
|
||||||
Language string `json:"language"`
|
|
||||||
ThumbnailURL string `json:"thumbnail_url"`
|
|
||||||
TagIDs []string `json:"tag_ids"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
IsMature bool `json:"is_mature"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (info *TwitchStreamInfo) Thumbnail(width int, height int) string {
|
|
||||||
res := strings.Replace(info.ThumbnailURL, "{width}", fmt.Sprintf("%d", width), 1)
|
|
||||||
res = strings.Replace(res, "{height}", fmt.Sprintf("%d", height), 1)
|
|
||||||
return res
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@
|
||||||
--secondary: #f8e05b;
|
--secondary: #f8e05b;
|
||||||
--tertiary: #f788fe;
|
--tertiary: #f788fe;
|
||||||
--links: #5eb2ff;
|
--links: #5eb2ff;
|
||||||
--live: #fd3737;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ nav {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
img#header-icon {
|
img#header-icon {
|
||||||
|
@ -35,19 +36,19 @@ img#header-icon {
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-text {
|
#header-text {
|
||||||
|
width: 11em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-text h1 {
|
#header-text h1 {
|
||||||
width: fit-content;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-text h2 {
|
#header-text h2 {
|
||||||
width: fit-content;
|
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -222,84 +222,3 @@ div#web-buttons {
|
||||||
transform: translate(-2px, -2px);
|
transform: translate(-2px, -2px);
|
||||||
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
|
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
#live-banner {
|
|
||||||
margin: 1em 0 2em 0;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--primary);
|
|
||||||
box-shadow: 0 0 8px var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#live-banner h2 {
|
|
||||||
margin: 0 0 .4em 0;
|
|
||||||
color: var(--on-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
#live-banner p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-highlight {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-preview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-preview div:first-of-type {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-thumbnail {
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-button {
|
|
||||||
margin: .2em;
|
|
||||||
padding: .4em .5em;
|
|
||||||
display: inline-block;
|
|
||||||
color: var(--primary);
|
|
||||||
border: 1px solid var(--primary);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: color .1s linear, background-color .1s linear, box-shadow .1s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-button:hover {
|
|
||||||
color: var(--background);
|
|
||||||
background-color: var(--primary);
|
|
||||||
box-shadow: 0 0 8px var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .3em;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-game {
|
|
||||||
overflow: hidden;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-game .live-game-prefix {
|
|
||||||
opacity: .8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-title {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-viewers {
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
package view
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/controller"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"arimelody-web/templates"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IndexHandler(app *model.AppState) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodHead {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type IndexData struct {
|
|
||||||
TwitchStatus *model.TwitchStreamInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
var twitchStatus *model.TwitchStreamInfo = nil
|
|
||||||
if len(app.Config.Twitch.Broadcaster) > 0 {
|
|
||||||
twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
|
||||||
err := templates.IndexTemplate.Execute(w, IndexData{
|
|
||||||
TwitchStatus: twitchStatus,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to render index page: %v\n", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
StaticHandler("public").ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package view
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StaticHandler(directory string) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
|
||||||
|
|
||||||
// does the file exist?
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// is thjs a directory? (forbidden)
|
|
||||||
if info.IsDir() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,12 +2,7 @@
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<small>
|
<small><em>*made with <span aria-label="love">♥</span> by ari, 2025*</em></small>
|
||||||
<em>
|
|
||||||
*made with <span aria-label="love">♥</span> by ari, 2025*
|
|
||||||
<a href="https://git.arimelody.me/ari/arimelody.me" target="_blank">source</a>
|
|
||||||
</em>
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="/music" preload="mouseover">music</a>
|
<a href="/music" preload="mouseover">music</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://git.arimelody.me/ari/arimelody.me" target="_blank">source</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<!-- coming later! -->
|
<!-- coming later! -->
|
||||||
<span title="coming later!">blog</span>
|
<span title="coming later!">blog</span>
|
||||||
|
|
|
@ -22,23 +22,6 @@
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<main>
|
<main>
|
||||||
{{if .TwitchStatus}}
|
|
||||||
<div id="live-banner">
|
|
||||||
<h2>ari is <span class="live-highlight">LIVE</span> right now!</h2>
|
|
||||||
<div class="live-preview">
|
|
||||||
<div>
|
|
||||||
<img src="{{.TwitchStatus.Thumbnail 144 81}}" alt="livestream thumbnail" class="live-thumbnail">
|
|
||||||
<a href="https://twitch.tv/{{.TwitchStatus.UserName}}" class="live-button">join in!</a>
|
|
||||||
</div>
|
|
||||||
<div class="live-info">
|
|
||||||
<p class="live-game"><span class="live-game-prefix">streaming:</span> {{.TwitchStatus.GameName}}</p>
|
|
||||||
<p class="live-title">{{.TwitchStatus.Title}}</p>
|
|
||||||
<p class="live-viewers">{{.TwitchStatus.ViewerCount}} viewers</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<h1 class="typeout">
|
<h1 class="typeout">
|
||||||
# hello, world!
|
# hello, world!
|
||||||
</h1>
|
</h1>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue