tangled
alpha
login
or
join now
geesawra.industries
/
skeet.fm
3
fork
atom
Post your last.fm now playing to your Bluesky followers
3
fork
atom
overview
issues
pulls
pipelines
*: cache blobs, reorg code
geesawra.industries
5 months ago
80ac5584
e54bcb23
+147
-109
4 changed files
expand all
collapse all
unified
split
rustfmt.toml
src
lib.rs
model.rs
wrangler.toml
+1
rustfmt.toml
···
1
1
+
reorder_imports = true
+73
-109
src/lib.rs
···
1
1
-
use atrium_api::app;
2
1
use atrium_api::app::bsky::embed::external;
3
2
use atrium_api::app::bsky::feed;
4
3
use atrium_api::app::bsky::feed::post::RecordEmbedRefs;
5
4
use atrium_api::types;
6
5
use atrium_api::types::string::Datetime;
7
6
use bsky_sdk::BskyAgent;
8
8
-
use serde_derive::Deserialize;
9
9
-
use serde_derive::Serialize;
7
7
+
use std::sync::LazyLock;
10
8
use worker::*;
11
11
-
12
12
-
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
13
13
-
#[serde(rename_all = "camelCase")]
14
14
-
pub struct Root {
15
15
-
pub recenttracks: Recenttracks,
16
16
-
}
9
9
+
mod model;
17
10
18
18
-
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
19
19
-
#[serde(rename_all = "camelCase")]
20
20
-
pub struct Recenttracks {
21
21
-
pub track: Vec<Track>,
22
22
-
}
23
23
-
24
24
-
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
25
25
-
#[serde(rename_all = "camelCase")]
26
26
-
pub struct Track {
27
27
-
pub artist: Artist,
28
28
-
pub album: Album,
29
29
-
pub name: String,
30
30
-
pub image: Vec<Image>,
31
31
-
#[serde(rename = "@attr")]
32
32
-
pub attr: Option<Attr>,
33
33
-
pub url: String,
34
34
-
}
11
11
+
const KV_MAIN: &str = "skeetfm";
12
12
+
static KV_PREFIX: LazyLock<String> = LazyLock::new(|| KV_MAIN.to_string() + "_");
13
13
+
static KV_BLOB_CACHE: LazyLock<String> = LazyLock::new(|| KV_PREFIX.clone() + "blobcache");
35
14
36
36
-
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
37
37
-
#[serde(rename_all = "camelCase")]
38
38
-
pub struct Artist {
39
39
-
pub mbid: String,
40
40
-
#[serde(rename = "#text")]
41
41
-
pub text: String,
42
42
-
}
43
43
-
44
44
-
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
45
45
-
#[serde(rename_all = "camelCase")]
46
46
-
pub struct Album {
47
47
-
pub mbid: String,
48
48
-
#[serde(rename = "#text")]
49
49
-
pub text: String,
50
50
-
}
51
51
-
52
52
-
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
53
53
-
#[serde(rename_all = "camelCase")]
54
54
-
pub struct Attr {
55
55
-
pub nowplaying: String,
56
56
-
}
57
57
-
58
58
-
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
59
59
-
#[serde(rename_all = "camelCase")]
60
60
-
pub struct Image {
61
61
-
pub size: String,
62
62
-
#[serde(rename = "#text")]
63
63
-
pub text: String,
15
15
+
fn kv_key(s: String) -> String {
16
16
+
format!("{}{}", *KV_PREFIX, s)
64
17
}
65
18
66
19
fn now_playing_url(username: String, api_key: String) -> String {
···
69
22
)
70
23
}
71
24
72
72
-
struct NowPlaying {
73
73
-
artist: String,
74
74
-
title: String,
75
75
-
image: String,
76
76
-
url: String,
77
77
-
}
78
78
-
79
79
-
impl std::fmt::Display for NowPlaying {
80
80
-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81
81
-
write!(f, "{}, {}", self.title, self.artist)
82
82
-
}
83
83
-
}
84
84
-
85
85
-
async fn get_now_playing(u: String) -> Option<NowPlaying> {
86
86
-
let res: Root = reqwest::get(u).await.unwrap().json().await.unwrap();
25
25
+
async fn get_now_playing(u: String) -> Option<model::NowPlaying> {
26
26
+
let res: model::Root = reqwest::get(u).await.unwrap().json().await.unwrap();
87
27
88
28
for e in res.recenttracks.track {
89
29
match e.attr {
90
30
Some(a) => {
91
91
-
if a.nowplaying == "false" || a.nowplaying == "" {
31
31
+
if a.nowplaying == "false" || a.nowplaying.is_empty() {
92
32
continue;
93
33
}
94
34
95
35
let img = e.image.iter().find(|i| i.size == "extralarge");
96
96
-
return Some(NowPlaying {
36
36
+
return Some(model::NowPlaying {
97
37
artist: e.artist.text,
98
38
title: e.name,
99
39
url: e.url,
···
107
47
None
108
48
}
109
49
110
110
-
async fn embeds_if_any(
50
50
+
async fn cached_album_art(
51
51
+
env: &Env,
111
52
agent: &BskyAgent,
112
112
-
np: &NowPlaying,
113
113
-
) -> Option<types::Union<RecordEmbedRefs>> {
114
114
-
let img_data = reqwest::get(np.image.clone())
115
115
-
.await
116
116
-
.unwrap()
117
117
-
.bytes()
118
118
-
.await
119
119
-
.unwrap();
120
120
-
let img_ref = agent
121
121
-
.api
122
122
-
.com
123
123
-
.atproto
124
124
-
.repo
125
125
-
.upload_blob(img_data.into())
126
126
-
.await
127
127
-
.unwrap();
53
53
+
np: &model::NowPlaying,
54
54
+
) -> types::Union<RecordEmbedRefs> {
55
55
+
let kv = env.kv(&KV_BLOB_CACHE).unwrap();
128
56
129
129
-
let data = app::bsky::embed::external::ExternalData {
57
57
+
let cached_art: types::BlobRef = match kv.get(&np.to_string()).json().await.unwrap() {
58
58
+
Some(b) => b,
59
59
+
None => {
60
60
+
let img_data = reqwest::get(np.image.clone())
61
61
+
.await
62
62
+
.unwrap()
63
63
+
.bytes()
64
64
+
.await
65
65
+
.unwrap();
66
66
+
let blob = agent
67
67
+
.api
68
68
+
.com
69
69
+
.atproto
70
70
+
.repo
71
71
+
.upload_blob(img_data.into())
72
72
+
.await
73
73
+
.unwrap()
74
74
+
.blob
75
75
+
.clone();
76
76
+
kv.put(&np.to_string(), serde_json::to_string(&blob).unwrap())
77
77
+
.unwrap()
78
78
+
.execute()
79
79
+
.await
80
80
+
.unwrap();
81
81
+
82
82
+
blob
83
83
+
}
84
84
+
};
85
85
+
86
86
+
let data = external::ExternalData {
130
87
description: np.artist.clone(),
131
131
-
thumb: Some(img_ref.blob.clone()),
88
88
+
thumb: Some(cached_art.clone()),
132
89
title: np.title.clone(),
133
90
uri: np.url.clone(),
134
91
};
···
137
94
external: data.into(),
138
95
};
139
96
140
140
-
Some(types::Union::Refs(
141
141
-
RecordEmbedRefs::AppBskyEmbedExternalMain(Box::new(main.into())),
142
142
-
))
97
97
+
types::Union::Refs(RecordEmbedRefs::AppBskyEmbedExternalMain(Box::new(
98
98
+
main.into(),
99
99
+
)))
143
100
}
144
101
145
145
-
async fn post_now_playing(np: NowPlaying, user: String, pass: String, pds: String) {
102
102
+
async fn post_now_playing(
103
103
+
env: &Env,
104
104
+
np: model::NowPlaying,
105
105
+
user: String,
106
106
+
pass: String,
107
107
+
pds: String,
108
108
+
) {
146
109
let agent = BskyAgent::builder()
147
110
.config(bsky_sdk::agent::config::Config {
148
111
endpoint: pds,
···
157
120
agent
158
121
.create_record(feed::post::RecordData {
159
122
created_at: Datetime::now(),
160
160
-
embed: embeds_if_any(&agent, &np).await,
123
123
+
embed: Some(cached_album_art(env, &agent, &np).await),
161
124
entities: None,
162
125
facets: None,
163
126
labels: None,
···
195
158
}
196
159
}
197
160
198
198
-
const KEY_PREFIX: &'static str = "skeetfm_";
199
199
-
200
161
#[event(scheduled)]
201
201
-
async fn scheduled(_evt: ScheduledEvent, _env: Env, _ctx: ScheduleContext) {
162
162
+
async fn scheduled(_: ScheduledEvent, env: Env, _: ScheduleContext) {
202
163
console_error_panic_hook::set_once();
203
164
204
204
-
let vars = Vars::load(&_env);
165
165
+
let vars = Vars::load(&env);
205
166
206
167
let np_url = now_playing_url(vars.lastfm_username, vars.lastfm_api_key);
207
168
···
210
171
None => return,
211
172
};
212
173
213
213
-
let kv_key = format!("{}", np);
214
214
-
let kv = _env.kv("skeetfm").unwrap();
174
174
+
let key = kv_key(np.to_string());
175
175
+
let kv = env.kv(KV_MAIN).unwrap();
215
176
let res = kv
216
177
.list()
217
178
.limit(1)
218
218
-
.prefix(format!("{KEY_PREFIX}{}", kv_key.clone()))
179
179
+
.prefix(key.clone())
219
180
.execute()
220
181
.await
221
182
.unwrap();
···
225
186
226
187
let res = kv
227
188
.list()
228
228
-
.prefix(KEY_PREFIX.to_string())
189
189
+
.prefix(KV_PREFIX.to_string())
229
190
.execute()
230
191
.await
231
192
.unwrap();
···
234
195
kv.delete(&key.name).await.ok();
235
196
}
236
197
237
237
-
post_now_playing(np, vars.bsky_username, vars.bsky_password, vars.pds_address).await;
198
198
+
post_now_playing(
199
199
+
&env,
200
200
+
np,
201
201
+
vars.bsky_username,
202
202
+
vars.bsky_password,
203
203
+
vars.pds_address,
204
204
+
)
205
205
+
.await;
238
206
239
239
-
kv.put(&format!("{KEY_PREFIX}{}", kv_key.clone()), kv_key.clone())
240
240
-
.unwrap()
241
241
-
.execute()
242
242
-
.await
243
243
-
.unwrap();
207
207
+
kv.put(&key, "").unwrap().execute().await.unwrap();
244
208
}
+68
src/model.rs
···
1
1
+
use serde::{Deserialize, Serialize};
2
2
+
3
3
+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
4
4
+
#[serde(rename_all = "camelCase")]
5
5
+
pub struct Root {
6
6
+
pub recenttracks: Recenttracks,
7
7
+
}
8
8
+
9
9
+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
10
10
+
#[serde(rename_all = "camelCase")]
11
11
+
pub struct Recenttracks {
12
12
+
pub track: Vec<Track>,
13
13
+
}
14
14
+
15
15
+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
16
16
+
#[serde(rename_all = "camelCase")]
17
17
+
pub struct Track {
18
18
+
pub artist: Artist,
19
19
+
pub album: Album,
20
20
+
pub name: String,
21
21
+
pub image: Vec<Image>,
22
22
+
#[serde(rename = "@attr")]
23
23
+
pub attr: Option<Attr>,
24
24
+
pub url: String,
25
25
+
}
26
26
+
27
27
+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
28
28
+
#[serde(rename_all = "camelCase")]
29
29
+
pub struct Artist {
30
30
+
pub mbid: String,
31
31
+
#[serde(rename = "#text")]
32
32
+
pub text: String,
33
33
+
}
34
34
+
35
35
+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
36
36
+
#[serde(rename_all = "camelCase")]
37
37
+
pub struct Album {
38
38
+
pub mbid: String,
39
39
+
#[serde(rename = "#text")]
40
40
+
pub text: String,
41
41
+
}
42
42
+
43
43
+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
44
44
+
#[serde(rename_all = "camelCase")]
45
45
+
pub struct Attr {
46
46
+
pub nowplaying: String,
47
47
+
}
48
48
+
49
49
+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
50
50
+
#[serde(rename_all = "camelCase")]
51
51
+
pub struct Image {
52
52
+
pub size: String,
53
53
+
#[serde(rename = "#text")]
54
54
+
pub text: String,
55
55
+
}
56
56
+
57
57
+
pub struct NowPlaying {
58
58
+
pub artist: String,
59
59
+
pub title: String,
60
60
+
pub image: String,
61
61
+
pub url: String,
62
62
+
}
63
63
+
64
64
+
impl std::fmt::Display for NowPlaying {
65
65
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66
66
+
write!(f, "{}, {}", self.title, self.artist)
67
67
+
}
68
68
+
}
+5
wrangler.toml
···
15
15
binding = "skeetfm"
16
16
id = "1daf3d0198ef400e9ad03d8495955b03"
17
17
preview_id = "1daf3d0198ef400e9ad03d8495955b03"
18
18
+
19
19
+
[[kv_namespaces]]
20
20
+
binding = "skeetfm_blobcache"
21
21
+
id = "a60098e075e54b50814b2ae06f211c36"
22
22
+
preview_id = "a60098e075e54b50814b2ae06f211c36"