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