feat(concerts): show artwork-themed event details in app

This commit is contained in:
zarzet committed 2026-09-25 12:00:28 +07:00
1 parent ab58cdbb0c
commit abb8dc4a42
18 files changed
+1047 -23

No files matched your search

@@ -185,6 +185,7 @@ impl Backend {
"album" => "getAlbum",
"playlist" => "getPlaylist",
"artist" => "getArtist",
"concert" => "getConcert",
_ => {
return Err(ResolverError::Failed(format!(
"unsupported provider resource type: {kind}"
@@ -393,6 +394,7 @@ fn response(kind: &str, value: &Value, check: &Check<'_>) -> Result<Value, Resol
check().map_err(ResolverError::Cancelled)?;
let cover = text(value, "cover_url");
Ok(match kind {
"concert" => json!({"concert": value}),
// Move normalized values into the envelope. json! serializes borrowed
// expressions, duplicating every field of an already-built track list.
"track" => Map::from_iter([("track".into(), track(value, "", 0))]).into(),
@@ -21,11 +21,19 @@ function getArtist(id) {
return {id, name: "Example Artist", image_url: "https://example.invalid/portrait.jpg",
concerts: id === "bad-concerts" ? {} : [{id: "event-1", location: "Example City",
venue: "Example Hall", startAt: "2026-10-07T01:00:00Z", timeZone: "America/New_York",
url: "https://example.invalid/events/1"}],
url: "https://example.invalid/events/1", detailId: "event-1"}],
headerLogo: "https://example.invalid/logo.png", albumsNext: "artist-page-2", albums: []};
}
function handleUrl() { return {type: "artist", artist: getArtist("artist-1")}; }
registerExtension({getAlbum: collection, getPlaylist: collection, getArtist, handleUrl});
function getConcert(id) {
return {id, artistName: "Example Artist", title: "Example Show", venue: "Example Hall",
address: "123 Example Street", startAt: "2026-10-07T01:00:00Z",
endAt: "2026-10-07T04:00:00Z", timeZone: "America/New_York",
ticketUrl: "https://example.invalid/tickets/1", mapUrl: "https://example.invalid/maps/1",
coverUrl: "https://example.invalid/artist.jpg", attribution: "Example Events",
setList: {id: "list-1", name: "Concert Set List", coverUrl: "https://example.invalid/list.jpg"}};
}
registerExtension({getAlbum: collection, getPlaylist: collection, getArtist, getConcert, handleUrl});
"#;
fn fixture() -> (tempfile::TempDir, Backend) {
@@ -54,6 +62,30 @@ fn fixture() -> (tempfile::TempDir, Backend) {
(root, backend)
}
#[test]
fn concert_details_preserve_generic_actions_and_set_list() {
let (_root, backend) = fixture();
let result: Value = serde_json::from_str(
&backend
.get_provider_metadata_json(ID, "concert", "event-1", &|| Ok(()))
.unwrap(),
)
.unwrap();
let detail = &result["concert"];
assert_eq!(detail["id"], "event-1");
assert_eq!(detail["artist_name"], "Example Artist");
assert_eq!(detail["start_at"], "2026-10-07T01:00:00Z");
assert_eq!(detail["end_at"], "2026-10-07T04:00:00Z");
assert_eq!(detail["ticket_url"], "https://example.invalid/tickets/1");
assert_eq!(detail["map_url"], "https://example.invalid/maps/1");
assert_eq!(detail["set_list"]["id"], "list-1");
assert_eq!(
detail["set_list"]["cover_url"],
"https://example.invalid/list.jpg"
);
assert_eq!(detail["attribution"], "Example Events");
}
#[test]
fn response_envelopes_preserve_json_and_cancellation() {
let value = json!({
@@ -214,8 +214,8 @@ impl ExtensionManager {
let manifest = &entry.manifest;
let requirement =
match method {
"getTrack" | "getAlbum" | "getPlaylist" | "getArtist" | "searchTracks"
| "enrichTrack" => (!manifest.has_type("metadata_provider"))
"getTrack" | "getAlbum" | "getPlaylist" | "getArtist" | "getConcert"
| "searchTracks" | "enrichTrack" => (!manifest.has_type("metadata_provider"))
.then_some("is not a metadata provider"),
"checkAvailability" => (!manifest.has_type("download_provider"))
.then_some("is not a download provider"),
@@ -313,6 +313,7 @@ impl ExtensionManager {
"getAlbum" => "album",
"getPlaylist" => "playlist",
"getArtist" => "artist",
"getConcert" => "concert",
"handleUrl" => "URL handle result",
"fetchLyrics" => "lyrics result",
"checkAvailability" => "availability result",
+13 -1
View File
@@ -208,7 +208,18 @@
function concert(value) {
return fields(value, [["id","s"],["location","s"],["venue","s"],
["start_at","s","startAt"],["time_zone","s","timeZone"],
["url","s"]], ["id","location","start_at"]);
["url","s"],["detail_id","s","detailId"]], ["id","location","start_at"]);
}
function concertDetail(value) {
const result = fields(value, [["id","s"],["artist_name","s","artistName"],
["title","s"],["cover_url","s","coverUrl"],["venue","s"],["address","s"],
["start_at","s","startAt"],["end_at","s","endAt"],["time_zone","s","timeZone"],
["ticket_url","s","ticketUrl"],["map_url","s","mapUrl"],["url","s"],
["attribution","s"]], ["id"]);
const setList = first(object(value), ["set_list","setList"]);
if (!empty(setList)) result.set_list = fields(setList, [["id","s"],["name","s"],
["cover_url","s","coverUrl"],["url","s"]], ["id","name"]);
return result;
}
function decryption(value) {
if (empty(value)) return null;
@@ -274,6 +285,7 @@
}
const parsers = {
getTrack: track, enrichTrack: track, getAlbum: album, getPlaylist: album, getArtist: artist,
getConcert: concertDetail,
searchTracks: search, customSearch: value => array(value, track), handleUrl: url,
checkAvailability: value => fields(value, [["available","b"],["reason","s"],["track_id","s","trackId"],
["skip_fallback","b","skipFallback"],["prepared_context","m","preparedContext"]], ["available"]),