tangled
alpha
login
or
join now
cherry.computer
/
website
My personal site
cherry.computer
htmx
tailwind
axum
askama
0
fork
atom
overview
issues
pulls
pipelines
feat: push scrobbles rather than pull them via SSE
cherry.computer
1 year ago
0f0fe58c
a04492e3
verified
This commit was signed with the committer's
known signature
.
cherry.computer
SSH Key Fingerprint:
SHA256:SIA77Ll0IpMb8Xd3RtaGT+PBIGIePhJJg5W2r6Td7cc=
+97
-19
8 changed files
expand all
collapse all
unified
split
frontend
package-lock.json
package.json
src
ts
htmx.ts
main.ts
server
Cargo.lock
Cargo.toml
src
main.rs
templates
index.html
+11
frontend/package-lock.json
···
9
"version": "1.0.0",
10
"license": "MIT",
11
"dependencies": {
0
12
"htmx.org": "^2.0.4",
13
"normalize.css": "^8.0.1",
14
"three": "^0.172.0"
···
1732
"node": ">=8"
1733
}
1734
},
0
0
0
0
0
1735
"node_modules/htmx.org": {
1736
"version": "2.0.4",
1737
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
···
3413
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
3414
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
3415
"dev": true
0
0
0
0
0
3416
},
3417
"htmx.org": {
3418
"version": "2.0.4",
···
9
"version": "1.0.0",
10
"license": "MIT",
11
"dependencies": {
12
+
"htmx-ext-sse": "^2.2.2",
13
"htmx.org": "^2.0.4",
14
"normalize.css": "^8.0.1",
15
"three": "^0.172.0"
···
1733
"node": ">=8"
1734
}
1735
},
1736
+
"node_modules/htmx-ext-sse": {
1737
+
"version": "2.2.2",
1738
+
"resolved": "https://registry.npmjs.org/htmx-ext-sse/-/htmx-ext-sse-2.2.2.tgz",
1739
+
"integrity": "sha512-MTnKkBzA2t4sI8gOXrRiPaceTlkUbrw3+3qOy1BfuBNIPBalsJiT4qxUGd6W48ggOkfe2akOnB8uxICJKw+Dsg=="
1740
+
},
1741
"node_modules/htmx.org": {
1742
"version": "2.0.4",
1743
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
···
3419
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
3420
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
3421
"dev": true
3422
+
},
3423
+
"htmx-ext-sse": {
3424
+
"version": "2.2.2",
3425
+
"resolved": "https://registry.npmjs.org/htmx-ext-sse/-/htmx-ext-sse-2.2.2.tgz",
3426
+
"integrity": "sha512-MTnKkBzA2t4sI8gOXrRiPaceTlkUbrw3+3qOy1BfuBNIPBalsJiT4qxUGd6W48ggOkfe2akOnB8uxICJKw+Dsg=="
3427
},
3428
"htmx.org": {
3429
"version": "2.0.4",
+1
frontend/package.json
···
36
},
37
"dependencies": {
38
"htmx.org": "^2.0.4",
0
39
"normalize.css": "^8.0.1",
40
"three": "^0.172.0"
41
},
···
36
},
37
"dependencies": {
38
"htmx.org": "^2.0.4",
39
+
"htmx-ext-sse": "^2.2.2",
40
"normalize.css": "^8.0.1",
41
"three": "^0.172.0"
42
},
+9
frontend/src/ts/htmx.ts
···
0
0
0
0
0
0
0
0
0
···
1
+
import htmx from "htmx.org";
2
+
3
+
declare global {
4
+
interface Window {
5
+
htmx: typeof htmx;
6
+
}
7
+
}
8
+
9
+
window.htmx = htmx;
+3
-1
frontend/src/ts/main.ts
···
1
import "../css/main.css";
2
3
-
import "htmx.org";
0
0
4
5
import "./header";
6
import "./logo";
···
1
import "../css/main.css";
2
3
+
// need to load htmx as a global variable before importing extensions
4
+
import "./htmx";
5
+
import "htmx-ext-sse";
6
7
import "./header";
8
import "./logo";
+35
server/Cargo.lock
···
95
]
96
97
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
98
name = "atomic-waker"
99
version = "1.1.2"
100
source = "registry+https://github.com/rust-lang/crates.io-index"
···
854
dependencies = [
855
"anyhow",
856
"askama",
0
857
"axum",
858
"reqwest",
859
"serde",
860
"tokio",
0
861
"tower",
862
"tower-http",
863
"tracing",
···
1469
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
1470
dependencies = [
1471
"rustls",
0
0
0
0
0
0
0
0
0
0
0
1472
"tokio",
1473
]
1474
···
95
]
96
97
[[package]]
98
+
name = "async-stream"
99
+
version = "0.3.6"
100
+
source = "registry+https://github.com/rust-lang/crates.io-index"
101
+
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
102
+
dependencies = [
103
+
"async-stream-impl",
104
+
"futures-core",
105
+
"pin-project-lite",
106
+
]
107
+
108
+
[[package]]
109
+
name = "async-stream-impl"
110
+
version = "0.3.6"
111
+
source = "registry+https://github.com/rust-lang/crates.io-index"
112
+
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
113
+
dependencies = [
114
+
"proc-macro2",
115
+
"quote",
116
+
"syn",
117
+
]
118
+
119
+
[[package]]
120
name = "atomic-waker"
121
version = "1.1.2"
122
source = "registry+https://github.com/rust-lang/crates.io-index"
···
876
dependencies = [
877
"anyhow",
878
"askama",
879
+
"async-stream",
880
"axum",
881
"reqwest",
882
"serde",
883
"tokio",
884
+
"tokio-stream",
885
"tower",
886
"tower-http",
887
"tracing",
···
1493
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
1494
dependencies = [
1495
"rustls",
1496
+
"tokio",
1497
+
]
1498
+
1499
+
[[package]]
1500
+
name = "tokio-stream"
1501
+
version = "0.1.17"
1502
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1503
+
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
1504
+
dependencies = [
1505
+
"futures-core",
1506
+
"pin-project-lite",
1507
"tokio",
1508
]
1509
+2
server/Cargo.toml
···
8
[dependencies]
9
anyhow = "1.0.57"
10
askama = { version = "0.13.0", git="https://github.com/rinja-rs/askama.git" }
0
11
axum = "0.8.1"
12
reqwest = { version = "0.12.12", features = ["json"] }
13
serde = { version = "1.0.217", features = ["derive"] }
14
tokio = { version = "1.18.2", features = ["full"] }
0
15
tower = "0.5.2"
16
tower-http = { version = "0.6.2", features = ["compression-full", "fs", "trace", "set-header"] }
17
tracing = "0.1.34"
···
8
[dependencies]
9
anyhow = "1.0.57"
10
askama = { version = "0.13.0", git="https://github.com/rinja-rs/askama.git" }
11
+
async-stream = "0.3.6"
12
axum = "0.8.1"
13
reqwest = { version = "0.12.12", features = ["json"] }
14
serde = { version = "1.0.217", features = ["derive"] }
15
tokio = { version = "1.18.2", features = ["full"] }
16
+
tokio-stream = "0.1.17"
17
tower = "0.5.2"
18
tower-http = { version = "0.6.2", features = ["compression-full", "fs", "trace", "set-header"] }
19
tracing = "0.1.34"
+33
-11
server/src/main.rs
···
2
mod scrobble;
3
mod scrobble_monitor;
4
5
-
use std::{env, net::SocketAddr};
6
7
use crate::index::get_index;
8
use crate::scrobble_monitor::ScrobbleMonitor;
9
10
use askama::Template;
0
11
use axum::{
12
http::{HeaderName, HeaderValue, StatusCode},
13
-
response::{Html, IntoResponse},
14
routing::{get, get_service},
15
Extension, Router,
16
};
0
0
17
use tower::ServiceBuilder;
18
use tower_http::{
19
compression::CompressionLayer, services::ServeDir, set_header::SetResponseHeaderLayer,
···
57
})
58
}
59
60
-
async fn get_scrobble(Extension(mut monitor): Extension<ScrobbleMonitor>) -> impl IntoResponse {
61
-
let template = monitor.get_scrobble().await.map_err(|err| {
62
-
tracing::error!("failed to get data from last.fm: {err:?}");
63
-
StatusCode::BAD_GATEWAY
64
-
})?;
65
-
template.render().map(Html).map_err(|err| {
66
-
tracing::error!("failed to render scrobble: {err:?}");
67
-
StatusCode::INTERNAL_SERVER_ERROR
68
-
})
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
69
}
···
2
mod scrobble;
3
mod scrobble_monitor;
4
5
+
use std::{convert::Infallible, env, net::SocketAddr, time::Duration};
6
7
use crate::index::get_index;
8
use crate::scrobble_monitor::ScrobbleMonitor;
9
10
use askama::Template;
11
+
use async_stream::stream;
12
use axum::{
13
http::{HeaderName, HeaderValue, StatusCode},
14
+
response::{sse, Html, IntoResponse, Sse},
15
routing::{get, get_service},
16
Extension, Router,
17
};
18
+
use tokio::time::{self, MissedTickBehavior};
19
+
use tokio_stream::Stream;
20
use tower::ServiceBuilder;
21
use tower_http::{
22
compression::CompressionLayer, services::ServeDir, set_header::SetResponseHeaderLayer,
···
60
})
61
}
62
63
+
async fn get_scrobble(
64
+
Extension(mut monitor): Extension<ScrobbleMonitor>,
65
+
) -> Sse<impl Stream<Item = Result<sse::Event, Infallible>>> {
66
+
let stream = stream! {
67
+
let mut interval = time::interval(Duration::from_secs(30));
68
+
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
69
+
interval.tick().await;
70
+
loop {
71
+
interval.tick().await;
72
+
let template = match monitor.get_scrobble().await {
73
+
Ok(template) => template,
74
+
Err(err) => {
75
+
tracing::error!("failed to get data from last.fm: {err:?}");
76
+
continue;
77
+
}
78
+
};
79
+
let data = match template.render() {
80
+
Ok(data) => data,
81
+
Err(err) => {
82
+
tracing::error!("failed to render scrobble: {err:?}");
83
+
break;
84
+
}
85
+
};
86
+
yield Ok(sse::Event::default().event("scrobble").data(data));
87
+
}
88
+
};
89
+
90
+
Sse::new(stream)
91
}
+3
-7
server/templates/index.html
···
88
</div>
89
<div
90
class="scrobble-bar"
91
-
hx-get="/scrobbles"
92
-
hx-trigger=
93
-
{%- if let Some(_) = scrobble -%}
94
-
"every 30s"
95
-
{%- else -%}
96
-
"load, every 30s"
97
-
{%- endif -%}
98
>
99
{%- match scrobble %} {%- when Some with (ScrobblesTemplate {intro, now_playing, image, srcset}) %}
100
{% include "scrobble.html" %}
···
88
</div>
89
<div
90
class="scrobble-bar"
91
+
hx-ext="sse"
92
+
sse-connect="/scrobbles"
93
+
sse-swap="scrobble"
0
0
0
0
94
>
95
{%- match scrobble %} {%- when Some with (ScrobblesTemplate {intro, now_playing, image, srcset}) %}
96
{% include "scrobble.html" %}