Skip to content

Commit 8b55aa9

Browse files
committed
Parse performers (sometimes), also clean up description and improve some CSS
1 parent 9ba15a9 commit 8b55aa9

File tree

4 files changed

+116
-49
lines changed

4 files changed

+116
-49
lines changed

rust/src/main.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ async fn main() {
4747
let client = reqwest::Client::new();
4848
let concerts = wigmore::get_api(&client).await;
4949

50-
// TODO: Fetch all concerts, not just the first 30. (Don't want to spam Wigmore's servers too
50+
// TODO: Fetch all concerts, not just the first N. (Don't want to spam Wigmore's servers too
5151
// much)
52-
let mut full_concerts = stream::iter(&concerts[..30])
52+
let mut full_concerts = stream::iter(&concerts[..40])
5353
.map(|concert| wigmore::get_concert(&concert, &client))
5454
.buffer_unordered(10)
5555
.collect::<Vec<Option<core::Concert>>>()

rust/src/wigmore.rs

+42-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use crate::core;
22
use chrono::{DateTime, Utc};
33
use futures::future::join_all;
4+
use html_escape::decode_html_entities;
45
use scraper::{Html, Selector};
56
use serde::{Deserialize, Serialize};
6-
use html_escape::decode_html_entities;
77

88
#[derive(Debug, Serialize, Deserialize)]
99
pub struct WigmoreFrontPageConcert {
@@ -125,7 +125,7 @@ fn parse_concert_json(
125125
let opt_cycle = piece["cycle"].as_str().map(decode_html_entities);
126126
let opt_piece_name = piece["title"].as_str().map(decode_html_entities);
127127
let opt_title = match (opt_cycle, opt_piece_name) {
128-
(Some(cycle), Some(piece_name)) => Some(format!("{}, {}", cycle, piece_name)),
128+
(Some(cycle), Some(piece_name)) => Some(format!("{}: {}", cycle, piece_name)),
129129
(Some(cycle), None) => Some(cycle.to_string()),
130130
(None, Some(piece_name)) => Some(piece_name.to_string()),
131131
_ => None,
@@ -150,7 +150,44 @@ fn parse_concert_json(
150150
.to_lowercase();
151151
let is_wigmore_u35 = booking_text.contains("tickets for under 35s available");
152152

153-
// TODO: Parse artists
153+
// Parse artists
154+
let mut performers = Vec::new();
155+
let opt_credits = json["data"]["page"]["credits"].as_array();
156+
match opt_credits {
157+
None => eprintln!("No performer credits found for concert at {}", fp_entry.url),
158+
Some(credits) => {
159+
for credit in credits {
160+
let opt_artist_name = credit["artist"]["title"].as_str().map(decode_html_entities);
161+
let opt_role = credit["role"].as_str().map(decode_html_entities);
162+
match (opt_artist_name, opt_role) {
163+
(Some(artist_name), Some(role)) => performers.push(core::Performer {
164+
name: artist_name.to_string(),
165+
instrument: Some(role.to_string()),
166+
}),
167+
_ => (),
168+
}
169+
}
170+
}
171+
}
172+
173+
fn clean_up_description(s: &str) -> String {
174+
// Split paragraphs
175+
let s = decode_html_entities(s)
176+
.to_string()
177+
.replace("</p><p>", "\n")
178+
.replace("<p>", "")
179+
.replace("</p>", "");
180+
// Then remove any remaining HTML tags, it just seems excessive
181+
// to keep them in the description
182+
let fragments = scraper::Html::parse_fragment(&s);
183+
let mut cleaned = String::new();
184+
for node in fragments.tree {
185+
if let scraper::Node::Text(text_node) = node {
186+
cleaned.push_str(&text_node.text);
187+
}
188+
}
189+
cleaned
190+
}
154191

155192
core::Concert {
156193
datetime: fp_entry.datetime,
@@ -159,13 +196,13 @@ fn parse_concert_json(
159196
subtitle: fp_entry.subtitle.clone(),
160197
description: json["data"]["page"]["overviewText"]
161198
.as_str()
162-
.map(|s| decode_html_entities(s).to_string()),
199+
.map(clean_up_description),
163200
programme_pdf_url: json["data"]["page"]["programmeDocument"]["url"]
164201
.as_str()
165202
.map(|s| s.to_string()),
166203
venue: "Wigmore Hall".to_string(),
167204
is_wigmore_u35,
168-
performers: Vec::new(),
205+
performers,
169206
pieces,
170207
}
171208
}

src/lib/Details.svelte

+72-41
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,67 @@
66

77
<div class="details">
88
{#if selectedConcert !== null}
9-
<h3>{selectedConcert.title}</h3>
10-
{#if selectedConcert.subtitle}
11-
<h4>{selectedConcert.subtitle}</h4>
12-
{/if}
13-
<p>
14-
{selectedConcert.datetime.toLocaleString()} at {selectedConcert.venue}
15-
</p>
9+
<div id="selected">
10+
<h2>
11+
{selectedConcert.title}
12+
{#if selectedConcert.subtitle}
13+
— {selectedConcert.subtitle}
14+
{/if}
15+
</h2>
16+
<p>
17+
{selectedConcert.datetime.toLocaleString()} at {selectedConcert.venue}
18+
</p>
1619

17-
<p>
18-
<a href={selectedConcert.url}>Link to concert</a>
19-
{#if selectedConcert.programme_pdf_url}
20-
| <a href={selectedConcert.programme_pdf_url}
21-
>Link to programme (PDF)</a
22-
>
23-
{/if}
24-
</p>
20+
<p>
21+
<a href={selectedConcert.url}>Link to concert</a>
22+
{#if selectedConcert.programme_pdf_url}
23+
| <a href={selectedConcert.programme_pdf_url}
24+
>Link to programme (PDF)</a
25+
>
26+
{/if}
27+
</p>
2528

26-
{#if selectedConcert.performers.length === 0}
27-
<p>No performers listed. (This is a placeholder!)</p>
28-
{:else}
29-
<h4>Performer{selectedConcert.performers.length > 1 ? "s" : ""}</h4>
30-
{#each selectedConcert.performers as performer}
31-
<p>{performer}</p>
32-
{/each}
33-
{/if}
29+
<h3>Performer(s)</h3>
30+
{#if selectedConcert.performers.length === 0}
31+
None listed.
32+
{:else}
33+
<div class="two-col-grid">
34+
{#each selectedConcert.performers as performer}
35+
<span>{performer.name}</span>
36+
<span>{performer.instrument ? performer.instrument : ""}</span>
37+
{/each}
38+
</div>
39+
{/if}
3440

35-
{#if selectedConcert.pieces.length === 0}
36-
<p>No programme provided.</p>
37-
{:else}
38-
<h4>Programme</h4>
39-
<div class="programme">
40-
{#each selectedConcert.pieces as piece}
41-
<span>{piece.composer}</span><span>{@html piece.title}</span>
42-
{/each}
43-
</div>
44-
{/if}
41+
<h3>Programme</h3>
42+
{#if selectedConcert.pieces.length === 0}
43+
None provided.
44+
{:else}
45+
<div class="two-col-grid">
46+
{#each selectedConcert.pieces as piece}
47+
<span>{piece.composer}</span><span
48+
>{@html piece.title}</span
49+
>
50+
{/each}
51+
</div>
52+
{/if}
4553

46-
{#if selectedConcert.description}
47-
<h4>Description</h4>
48-
<p>{@html selectedConcert.description}</p>
49-
{/if}
54+
<h3>Description</h3>
55+
{#if selectedConcert.description}
56+
<div id="description">
57+
{#each selectedConcert.description.split("\n") as paragraph}
58+
<p>{paragraph}</p>
59+
{/each}
60+
</div>
61+
{:else}
62+
None provided.
63+
{/if}
64+
</div>
5065
{:else}
51-
<p>No concert selected</p>
66+
<div id="none-selected">
67+
<h2>No concert selected</h2>
68+
<p>Select a concert from the list on the left to view details :)</p>
69+
</div>
5270
{/if}
5371
</div>
5472

@@ -65,17 +83,30 @@
6583
border-radius: 10px;
6684
}
6785
68-
.details > *:first-child {
86+
#selected > *:first-child {
6987
margin-top: 0;
7088
}
7189
72-
.details > *:last-child {
90+
#selected > *:last-child {
7391
margin-bottom: 0;
7492
}
7593
76-
div.programme {
94+
h3 {
95+
margin-bottom: 5px;
96+
}
97+
98+
div#none-selected {
99+
display: grid;
100+
place-items: center;
101+
}
102+
103+
div.two-col-grid {
77104
display: grid;
78105
grid-template-columns: max-content 1fr;
79106
gap: 4px 30px;
80107
}
108+
109+
div#description > *:first-child {
110+
margin-top: 0;
111+
}
81112
</style>

src/lib/Overview.svelte

-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
{/if}
5656
<p>{concert.datetime.toLocaleString()}</p>
5757
<p>{concert.venue}</p>
58-
<p>{concert.performers.join(", ")}</p>
5958
</button>
6059
{/each}
6160
</div>

0 commit comments

Comments
 (0)