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: add context field to media
cherry.computer
3 months ago
3f881d6b
83fabd7e
verified
This commit was signed with the committer's
known signature
.
cherry.computer
SSH Key Fingerprint:
SHA256:SIA77Ll0IpMb8Xd3RtaGT+PBIGIePhJJg5W2r6Td7cc=
+58
-5
5 changed files
expand all
collapse all
unified
split
server
src
scrapers
apple_music.rs
backloggd.rs
letterboxd.rs
scrapers.rs
templates
index.html
+1
server/src/scrapers.rs
···
6
6
pub struct Media {
7
7
pub name: String,
8
8
pub image: String,
9
9
+
pub context: String,
9
10
}
+8
server/src/scrapers/apple_music.rs
···
37
37
}
38
38
39
39
#[derive(Deserialize, Debug, Clone)]
40
40
+
#[serde(rename_all = "camelCase")]
40
41
struct AppleMusicTrackAttributes {
41
42
name: String,
43
43
+
album_name: String,
44
44
+
artist_name: String,
42
45
artwork: AppleMusicTrackArtwork,
43
46
}
44
47
···
98
101
.replace("{w}", dimensions)
99
102
.replace("{h}", dimensions);
100
103
104
104
+
let artist = &track.attributes.artist_name;
105
105
+
let album = &track.attributes.album_name;
106
106
+
let context = format!("{artist} — {album}");
107
107
+
101
108
Ok(Media {
102
109
name: track.attributes.name.clone(),
103
110
image: artwork_url,
111
111
+
context,
104
112
})
105
113
}
106
114
+15
-1
server/src/scrapers/backloggd.rs
···
12
12
static NAME_SEL: LazyLock<Selector> =
13
13
LazyLock::new(|| Selector::parse(".game-name a").unwrap());
14
14
static IMAGE_SEL: LazyLock<Selector> = LazyLock::new(|| Selector::parse(".card-img").unwrap());
15
15
+
static PLATFORM_SEL: LazyLock<Selector> =
16
16
+
LazyLock::new(|| Selector::parse(".journal-platform").unwrap());
15
17
16
18
let html = reqwest::get("https://backloggd.com/u/cherryfunk/journal")
17
19
.await
···
40
42
.attr("src")
41
43
.context("image element didn't have src attribute")?
42
44
.to_owned();
45
45
+
let platform = first_entry
46
46
+
.select(&PLATFORM_SEL)
47
47
+
.next()
48
48
+
.context("couldn't find platform element")?
49
49
+
.text()
50
50
+
.next()
51
51
+
.context("platform element didn't have any text")?
52
52
+
.to_owned();
43
53
44
44
-
Ok(Media { name, image })
54
54
+
Ok(Media {
55
55
+
name,
56
56
+
image,
57
57
+
context: platform,
58
58
+
})
45
59
}
46
60
47
61
#[once(time = 300, option = false)]
+30
-3
server/src/scrapers/letterboxd.rs
···
16
16
struct Extracted {
17
17
name: String,
18
18
image_url: Url,
19
19
+
rating: Option<u8>,
19
20
}
20
21
21
22
pub async fn fetch() -> anyhow::Result<Media> {
···
28
29
.text()
29
30
.await
30
31
.context("failed to get HTML text")?;
31
31
-
let Extracted { name, image_url } = parse_html(&html)?;
32
32
+
let Extracted {
33
33
+
name,
34
34
+
image_url,
35
35
+
rating,
36
36
+
} = parse_html(&html)?;
32
37
33
38
let image_url_data: ImageUrlMetadata = client
34
39
.get(image_url.clone())
35
40
.send()
36
41
.await
37
37
-
.with_context(|| format!("failed to fetch image metadata from URL {}", image_url))?
42
42
+
.with_context(|| format!("failed to fetch image metadata from URL {image_url}"))?
38
43
.json()
39
44
.await
40
45
.context("failed to parse image metadata")?;
46
46
+
let formatted_rating = match rating {
47
47
+
Some(rating) => format!(
48
48
+
"{} {}",
49
49
+
f32::from(rating) / 2.0,
50
50
+
if rating == 2 { "star" } else { "stars" }
51
51
+
),
52
52
+
None => "no rating".to_owned(),
53
53
+
};
41
54
42
55
Ok(Media {
43
56
name,
44
57
image: image_url_data.url,
58
58
+
context: formatted_rating,
45
59
})
46
60
}
47
61
···
51
65
static NAME_SEL: LazyLock<Selector> = LazyLock::new(|| Selector::parse(".name").unwrap());
52
66
static POSTER_COMPONENT_SEL: LazyLock<Selector> =
53
67
LazyLock::new(|| Selector::parse(".react-component:has(> .poster)").unwrap());
68
68
+
static RATING_SEL: LazyLock<Selector> = LazyLock::new(|| Selector::parse(".rating").unwrap());
54
69
55
70
let document = Html::parse_document(html);
56
71
···
70
85
.select(&POSTER_COMPONENT_SEL)
71
86
.next()
72
87
.context("couldn't find post component")?;
88
88
+
let rating = first_entry
89
89
+
.select(&RATING_SEL)
90
90
+
.next()
91
91
+
.context("couldn't find rating component")?
92
92
+
.value()
93
93
+
.classes()
94
94
+
.find_map(|class| class.strip_prefix("rated-"))
95
95
+
.and_then(|rating| rating.parse().ok());
73
96
74
97
let image_url = build_image_url(poster_component)?;
75
98
76
76
-
Ok(Extracted { name, image_url })
99
99
+
Ok(Extracted {
100
100
+
name,
101
101
+
image_url,
102
102
+
rating,
103
103
+
})
77
104
}
78
105
79
106
fn build_image_url(poster_component: ElementRef) -> anyhow::Result<Url> {
+4
-1
server/templates/index.html
···
25
25
src="{{ media.image }}"
26
26
alt="Cover art for {{ media.name }}"
27
27
/>
28
28
-
<p class="self-center text-2xl text-white">{{ media.name }}</p>
28
28
+
<div class="flex flex-col self-center">
29
29
+
<p class="text-2xl text-white">{{ media.name }}</p>
30
30
+
<p class="text-xl text-gray-700 italic">{{ media.context }}</p>
31
31
+
</div>
29
32
</div>
30
33
{%- endfor %}
31
34
</div>