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