all repos — breadsite @ 63b118288515ef1392f22813ffcd98b633ea7f05

Unnamed repository; edit this file 'description' to name the repository.

add snoot booping among other things
confusedbread confuseddbread@gmail.com
Thu, 03 Jul 2025 05:23:02 +0200
commit

63b118288515ef1392f22813ffcd98b633ea7f05

parent

3ffe747a9db18cbbd2fdded2e459fb1be13f70ab

M backend/listenBrainz.gobackend/listenBrainz.go

@@ -41,28 +41,6 @@ ReleaseMbid string `json:"release_mbid"`

ReleaseName string `json:"release_name"` } -type CoverArt struct { - Images []struct { - Types []string `json:"types"` - Front bool `json:"front"` - Back bool `json:"back"` - Edit int `json:"edit"` - Image string `json:"image"` - Comment string `json:"comment"` - Approved bool `json:"approved"` - Thumbnails struct { - // TODO: add interface here to allow for different size variations - Num250 string `json:"250"` - // Num500 string `json:"500"` - // Num1200 string `json:"1200"` - // Large string `json:"large"` - // Small string `json:"small"` - } `json:"thumbnails"` - ID int64 `json:"id"` - } `json:"images"` - Release string `json:"release"` -} - func getPlayingNow(userName string) (PlayingNow, error) { url := fmt.Sprintf("https://api.listenbrainz.org/1/user/%s/playing-now", url.QueryEscape(userName)) res, err := http.Get(url)

@@ -112,24 +90,19 @@

return metadata, nil } -func getCover(releaseMbid string) (CoverArt, error) { - url := fmt.Sprintf("https://coverartarchive.org/release/%s", url.QueryEscape(releaseMbid)) - res, err := http.Get(url) +func getCover(releaseMbid string) (string, error) { + client := http.Client{} + url := fmt.Sprintf("https://coverartarchive.org/release/%s/front-250", url.QueryEscape(releaseMbid)) + req , err := http.NewRequest("GET", url, nil) if err != nil { - return CoverArt{}, err + return "", err } - body, err := io.ReadAll(res.Body) - if err != nil { - log.Printf("body: %s\nerror: %s\n",url, err) - return CoverArt{}, err - } + req.Header = http.Header{ + "Sec-Fetch-Mode": {"navigate"}, + } + res, err := client.Do(req) - var coverArt CoverArt - if err := json.Unmarshal(body, &coverArt); err != nil { - log.Printf("url: %s\nbody: %s\nerror: %s\n",url, body, err) - return CoverArt{}, err - } - return coverArt, nil + return res.Request.URL.String(), nil }
M backend/main.gobackend/main.go

@@ -7,6 +7,7 @@ "net/http"

) type PlayingInfo struct { + playing bool artistName string recordingName string releaseName string

@@ -21,6 +22,14 @@ playingNow, err := getPlayingNow("confuseddbread")

if err != nil { panic(err) } + + if len(playingNow.Payload.Listens) == 0 { + playingInfo.playing = false + return playingInfo + } else { + playingInfo.playing = true + } + trackMetadata := playingNow.Payload.Listens[0].TrackMetadata playingInfo.artistName = trackMetadata.ArtistName playingInfo.recordingName = trackMetadata.TrackName

@@ -30,16 +39,21 @@ metadata, err := getMetadata(trackMetadata.ArtistName, trackMetadata.TrackName, trackMetadata.ReleaseName)

if err != nil { log.Fatalf("error: %s\n", err) } + if metadata.RecordingMbid == "" { + log.Println("no song metadata found") + return playingInfo + } playingInfo.recordingUrl = fmt.Sprintf("https://musicbrainz.org/recording/%s", metadata.RecordingMbid) coverArt, err := getCover(metadata.ReleaseMbid) - playingInfo.coverUrl = coverArt.Images[0].Image + playingInfo.coverUrl = coverArt return playingInfo } func main() { - http.HandleFunc("/", handler) - log.Println("http://localhost:8080") - log.Fatal(http.ListenAndServe(":8080", nil)) + http.HandleFunc("/now-playing", nowPlayingHandler) + http.HandleFunc("/boop", boopHandler) + log.Println("http://localhost:5050") + log.Fatal(http.ListenAndServe(":5050", nil)) }
D backend/net.go

@@ -1,28 +0,0 @@

-package main - -import ( - "fmt" - "log" - "net/http" - "reflect" -) - -func handler(w http.ResponseWriter, r *http.Request) { - playingInfo := getInfo() - - v := reflect.ValueOf(playingInfo) - k := v.Type() - for i := range v.NumField() { - log.Printf("%s: %s\n", k.Field(i).Name, v.Field(i)) - } - - fmt.Fprintf( - w, - "<p>%s</p><p>%s</p><p>%s</p><a href=\"%s\"><img src=\"%s\"></a>", - playingInfo.artistName, - playingInfo.recordingName, - playingInfo.releaseName, - playingInfo.recordingUrl, - playingInfo.coverUrl, - ) -}
A backend/nowPlaying.go

@@ -0,0 +1,74 @@

+package main + +import ( + "fmt" + "log" + "net/http" + "reflect" + "strings" + "time" +) + +type musicCache struct { + expires time.Time + content PlayingInfo +} + +var music_handler_Cache musicCache + +func music_cache_handler(music_handler_Cache *musicCache, expiry_time time.Duration) PlayingInfo { + e := music_handler_Cache.content == PlayingInfo{} + if time.Now().After(music_handler_Cache.expires) || e{ + log.Println("cache expired") + music_handler_Cache.content = getInfo() + music_handler_Cache.expires = time.Now().Add(expiry_time) + } + log.Println("returning cached result") + return music_handler_Cache.content +} + +func nowPlayingHandler(w http.ResponseWriter, r *http.Request) { + playingInfo := music_cache_handler(&music_handler_Cache, time.Second * 60) + + v := reflect.ValueOf(playingInfo) + k := v.Type() + for i := range v.NumField() { + log.Printf("%s: %s\n", k.Field(i).Name, v.Field(i)) + } + + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Headers", "*") + + var response string + + if playingInfo.playing { + response = fmt.Sprintf( + ` + <a class='nowPlaying' href=%s> + <h3>Now Playing:</h3> + <div class='songInfo'> + <img class='coverImg' src='%s' alt='%s'> + <div class='metadata'> + <p class='recordingName'>%s</p> + <p class='artistName'>~ %s</p> + <p class='releaseName'>%s</p> + </div> + </div> + </a> + `, + playingInfo.recordingUrl, + playingInfo.coverUrl, + strings.ReplaceAll( + fmt.Sprintf("%s - %s", playingInfo.recordingName, playingInfo.artistName), + `'`, + "", + ), + playingInfo.recordingName, + playingInfo.artistName, + playingInfo.releaseName, + ) + } else { + response = "<h3 class='nowPlaying offline'>nothing is playing...</h3>" + } + fmt.Fprintf(w, response) +}
A backend/snoot.go

@@ -0,0 +1,21 @@

+package main + +import ( + "fmt" + "net/http" +) + +type boopCache struct { + boops int +} + +var boopCounter boopCache + +func boopHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Headers", "*") + if r.Method == "POST" { + boopCounter.boops += 1 + } + fmt.Fprintf(w, `%v`, boopCounter.boops) +}
M src/main.rssrc/main.rs

@@ -2,6 +2,7 @@ use actix_web::{get, App, HttpServer, Result as AwResult};

use actix_files::Files; use maud::{DOCTYPE, html, Markup}; use std::io; +use chrono::Local; #[derive(Clone)] struct NavElement {

@@ -17,6 +18,8 @@ head {

meta charset="utf-8"; title { (page_title) } link rel="stylesheet" href="static/style.css"; + script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js" {} + script { "htmx.config.selfRequestsOnly = false" } } } }

@@ -47,20 +50,24 @@ html!(

header { (nav_bar(vec![ NavElement{ name: "breadsite".to_string(), href: "/".to_string(), class: String::from("navElement")}, - NavElement{ name: "test".to_string(), href: "/test".to_string(), class: String::from("navElement")}, + NavElement{ name: "boop".to_string(), href: "/boop".to_string(), class: String::from("navElement")}, NavElement{ name: ":333".to_string(), href: "/3".to_string(), class: String::from("navElement")}, ], current)) + + + div class="htmx-indicator nowPlaying loading" hx-get="http://localhost:5050/now-playing" hx-swap="outerHTML" hx-trigger="load" { ". . ." } } ) } -// TODOOO: pawer styling // TODOO: pawer 81x31 // TODOO: pawer links fn pawer() -> Markup { html! { pawer { - p { "paws...." } + @for _ in 1..101 { + p { "paws...." } + } } } }

@@ -68,20 +75,29 @@

// TODOOOO: about me // TODO: idea: heart rate monitor // TODO: idea: view counter -// TODO: idea: currently listening to // TODO: idea: svg -> neocat generator fn breadsite() -> Markup { html! { - h1 { "Hiiii" } + main { + h1 { "Hiiii" } + } } } -fn htmx_test() -> Markup { +fn boop() -> Markup { html! { - script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js" {} - button hx-get="/click" hx-swap="outerHTML" { "Click Me" } + main { + h1 { "Boop me" } + div class="boop timestamp" { (format!("boops since {}:",Local::now().format("%d/%m/%Y %H:%M UTC%Z"))) } + div #boop-counter hx-get="http://localhost:5050/boop" hx-trigger="load" {} + div class="boop stack" { + img class="unbooped" src="./static/neodog.png" {} + button class="boop" hx-post="http://localhost:5050/boop" hx-trigger="mousedown throttle:500ms" hx-swap="swap:500ms" hx-target="#boop-counter" {} + } + } } } + fn layout(page_title: &str, current: &str,site: Markup) -> Markup { html! {

@@ -101,17 +117,17 @@ (layout("breadsite", "/", breadsite()))

}) } -#[get("/test")] -async fn breadsite_test() -> AwResult<Markup> { +#[get("/boop")] +async fn breadsite_boop() -> AwResult<Markup> { Ok(html! { - (layout("test", "/test", breadsite())) + (layout("boop", "/boop", boop())) }) } #[get("/3")] async fn breadsite_3() -> AwResult<Markup> { Ok(html! { - (layout(":333", "/3", htmx_test())) + (layout(":333", "/3", breadsite())) }) }

@@ -119,7 +135,7 @@ #[actix_web::main]

async fn main() -> io::Result<()> { HttpServer::new(|| App::new() .service(breadsite_index) - .service(breadsite_test) + .service(breadsite_boop) .service(breadsite_3) .service(Files::new("/static", "./static")) )
M static/style.csssrc/static/style.css

@@ -328,28 +328,38 @@ }

:root { --text: var(--ctp-mocha-text); + --subtext: var(--ctp-mocha-subtext0); + --surface: var(--ctp-mocha-surface0); --background: var(--ctp-mocha-base); --mantle: var(--ctp-mocha-mantle); --crust: var(--ctp-mocha-crust); --link: var(--ctp-mocha-blue); - --nav-0: var(--ctp-mocha-yellow); - --nav-1: var(--ctp-mocha-red); - --nav-2: var(--ctp-mocha-pink); - --nav-3: var(--ctp-mocha-mauve); - --nav-4: var(--ctp-mocha-blue); + --nav-song: var(--ctp-mocha-blue); + + --nav-0: var(--ctp-mocha-peach); + --nav-1: var(--ctp-mocha-pink); + --nav-2: var(--ctp-mocha-mauve); + + --cover-size: 3.5rem; + --np-max-w: 32rem; + --np-max-h: 12rem; + + --nav-height: 4rem; + --nav-border: 0.6rem; + --nav-border-n: calc(var(--nav-border) * -1); + + --boop-animation: cubic-bezier(0,.39,1,-0.16); } * { font-family: 'Fredoka', sans-serif; + margin: 0; } body { background-color: var(--background); color: var(--text); - - margin: 0; - padding: 0; } a {

@@ -361,10 +371,37 @@ text-decoration: underline;

} } +header { + z-index: 10; + position: sticky; + top: 0; + padding: 0 !important; + + display: flex; + flex-direction: row; + justify-content: space-between; +} + +main { + padding: 1rem; +} + +pawer { + display: inline-block; + background: var(--crust); + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; + width: calc(100vw - 2rem); + padding: 1rem; +} + + + .navBar { display: flex; flex-direction: row; gap: 1rem; + z-index: 10; .navElement{ padding-top: 0.8rem;

@@ -389,7 +426,7 @@ color: var(--crust);

} #navElement-0 { - z-index:4; + z-index: 13; &:hover { height: 2.2rem !important; margin-bottom: 0 !important;

@@ -421,7 +458,7 @@ }

} #navElement-1 { - z-index:3; + z-index: 12; &:hover { height: 2.2rem !important; margin-bottom: 0 !important;

@@ -453,7 +490,7 @@ }

} #navElement-2 { - z-index:2; + z-index: 11; &:hover { height: 2.2rem !important; margin-bottom: 0 !important;

@@ -484,70 +521,110 @@ box-shadow: -2rem 0 var(--nav-2);

} } - #navElement-3 { - z-index:1; - &:hover { - height: 2.2rem !important; - margin-bottom: 0 !important; - position: relative; + .first { + margin-left: 1rem; + } +} + +.nowPlaying { + &.loading { + height: 2rem; + width: 2rem; + padding-left: 1rem; + margin-bottom: calc(5.7rem); + } - &::before { - content: ""; - position: absolute; - width: 100vw; - border: 0.12rem solid var(--nav-3); - bottom: 0; - right: 0.9rem; - } + &:hover { + text-decoration: none !important; + box-shadow: -0.2rem 0.2rem var(--nav-song); + background-color: var(--crust); + } + + + position: fixed; + top: 0; + right: 0; + + display: flex; + flex-direction: column; + gap: 0.2rem; + + padding: 0.5rem; + padding-right: 0.75rem; + + max-width: var(--np-max-w); + max-height: var(--np-max-h); + + color: var(--text); + text-wrap-mode: wrap; + overflow-wrap: anywhere; + overflow: hidden; + + border-bottom-left-radius: 1rem; + background: var(--mantle); - box-shadow: inset -0.2rem -0.2rem var(--nav-3), - -2rem 0 var(--crust); - background-color: var(--crust); + h3 { + font-size: 0.8rem; + } - &.current { - box-shadow: inset -0.2rem -0.2rem var(--nav-3), - -2rem 0 var(--nav-3); - } - } - &.current { - background-color: var(--nav-3); - box-shadow: -2rem 0 var(--nav-3); - } + .songInfo { + display: flex; + flex-direction: row; + gap: 0.5rem; } - #navElement-4 { - z-index:0; - &:hover { - height: 2.2rem !important; - margin-bottom: 0 !important; - position: relative; + p.recordingName { + font-size: 1.2rem; + } - &::before { - content: ""; - position: absolute; - width: 100vw; - border: 0.12rem solid var(--nav-4); - bottom: 0; - right: 0.9rem; - } + p.artistName { + font-size: 0.8rem; + } - box-shadow: inset -0.2rem -0.2rem var(--nav-4), - -2rem 0 var(--crust); - background-color: var(--crust); + p.releaseName { + color: var(--subtext) !important; + font-size: 0.8rem; + } - &.current { - box-shadow: inset -0.2rem -0.2rem var(--nav-4), - -2rem 0 var(--nav-4); - } - } - &.current { - background-color: var(--nav-4); - box-shadow: -2rem 0 var(--nav-4); - } + img.coverImg { + height: var(--cover-size); + width: var(--cover-size); + border-radius: 0.5rem; } +} - .first { - margin-left: 1rem; +button.boop, img.unbooped { + position: absolute; + height: 16rem; + width: 16rem; +} + +.boop.stack { + position: relative; + height: 16rem; + width: 16rem; +} + +img.unbooped { + left: 18px; + opacity: 1; +} +img.unbooped:has(~ button.boop:active){ + animation: fadeOut 0.5s forwards; + animation-timing-function: var(--boop-animation); + animation-iteration-count: 1; +} + +button.boop{ + background: url("neodog_boop_blep.png") no-repeat scroll 0 0 transparent; + border: none; + background-size: 16rem 16rem; + opacity: 0; + transition: transform 2s ease, opacity 2s ease 3s; + &:active{ + animation: fadeIn 0.5s forwards; + animation-timing-function: var(--boop-animation); + animation-iteration-count: 1; } }

@@ -559,3 +636,13 @@ * {

animation: spin 10s linear infinite; } } + +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@keyframes fadeOut { + 0% { opacity: 1; } + 100% { opacity: 0; } +}