Link tracks and artists on the rvrb page to Spotify
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-09-09 16:47 UTC
- commit
- 9d011c87380dc3b7dfa86d40fa5a6bc06528e451
- parent
- dfe928fa15
- tree
- browse at this commit
5 files changed +87 -8
| Blog/Components/Pages/Rvrb.razor | +15 -7 | |
| Blog/Components/_Shared/ArtistLinks.razor | added | +25 -0 |
| Blog/Components/_Shared/SpotifyLink.razor | added | +29 -0 |
| Blog/Models/RvrbSnapshot.cs | +12 -1 | |
| Blog/Services/RvrbSnapshotReader.cs | +6 -0 |
Blog/Components/Pages/Rvrb.razor +15 -7
| @@ -49,8 +49,10 @@ | ||
| 49 | 49 | <img class="album-art" src="@art" alt="" width="128" height="128"/> |
| 50 | 50 | } |
| 51 | 51 | <div> |
| 52 | - <h3></h3> | |
| 53 | - <p>(", ", track.ArtistNames)</p> | |
| 52 | + <h3> | |
| 53 | + <SpotifyLink Kind="track" Id="@track.SpotifyTrackId" Name="@track.Name"/> | |
| 54 | + </h3> | |
| 55 | + <p><ArtistLinks Names="track.ArtistNames" Ids="track.SpotifyArtistIds"/></p> | |
| 54 | 56 | @* A bar needs both halves to mean anything: a track that came without a |
| 55 | 57 | duration, or one already playing when the bot connected, gets neither. *@ |
| 56 | 58 | (track.Duration is { } length && track.Elapsed is not null) |
| @@ -192,8 +194,10 @@ | ||
| 192 | 194 | (var track in Snapshot.TopTracks) |
| 193 | 195 | { |
| 194 | 196 | <tr> |
| 195 | - <td></td> | |
| 196 | - <td>(", ", track.ArtistNames)</td> | |
| 197 | + <td> | |
| 198 | + <SpotifyLink Kind="track" Id="@track.SpotifyTrackId" Name="@track.TrackName"/> | |
| 199 | + </td> | |
| 200 | + <td><ArtistLinks Names="track.ArtistNames" Ids="track.SpotifyArtistIds"/></td> | |
| 197 | 201 | <td></td> |
| 198 | 202 | <td></td> |
| 199 | 203 | </tr> |
| @@ -217,7 +221,9 @@ | ||
| 217 | 221 | (var artist in Snapshot.TopArtists) |
| 218 | 222 | { |
| 219 | 223 | <tr> |
| 220 | - <td></td> | |
| 224 | + <td> | |
| 225 | + <SpotifyLink Kind="artist" Id="@artist.SpotifyArtistId" Name="@artist.ArtistName"/> | |
| 226 | + </td> | |
| 221 | 227 | <td></td> |
| 222 | 228 | <td></td> |
| 223 | 229 | </tr> |
| @@ -245,8 +251,10 @@ | ||
| 245 | 251 | <tr> |
| 246 | 252 | <td><Timestamp At="play.PlayedAt"/></td> |
| 247 | 253 | <td></td> |
| 248 | - <td></td> | |
| 249 | - <td>(", ", play.ArtistNames)</td> | |
| 254 | + <td> | |
| 255 | + <SpotifyLink Kind="track" Id="@play.SpotifyTrackId" Name="@play.TrackName"/> | |
| 256 | + </td> | |
| 257 | + <td><ArtistLinks Names="play.ArtistNames" Ids="play.SpotifyArtistIds"/></td> | |
| 250 | 258 | <td></td> |
| 251 | 259 | </tr> |
| 252 | 260 | } |
Blog/Components/_Shared/ArtistLinks.razor +25 -0
| @@ -0,0 +1,25 @@ | ||
| 1 | +@* A track's artists, comma-separated, each linked to Spotify where we have its id. *@ | |
| 2 | +@* Names and ids arrive positionally parallel from the bot, so an id is matched to a name *@ | |
| 3 | +@* by index - a shorter id list (a play recorded before they were stored) just means the *@ | |
| 4 | +@* trailing names go unlinked. *@ | |
| 5 | + | |
| 6 | + (var i = 0; i < Names.Count; i++) | |
| 7 | +{ | |
| 8 | + if (i > 0) | |
| 9 | + { | |
| 10 | + <text>, </text> | |
| 11 | + } | |
| 12 | + | |
| 13 | + <SpotifyLink Kind="artist" Id="@IdAt(i)" Name="@Names[i]"/> | |
| 14 | +} | |
| 15 | + | |
| 16 | + { | |
| 17 | + [Parameter] | |
| 18 | + [EditorRequired] | |
| 19 | + public required IReadOnlyList<string> Names { get; set; } | |
| 20 | + | |
| 21 | + [Parameter] | |
| 22 | + public IReadOnlyList<string> Ids { get; set; } = []; | |
| 23 | + | |
| 24 | + private string? IdAt(int index) => index < Ids.Count ? Ids[index] : null; | |
| 25 | +} | |
Blog/Components/_Shared/SpotifyLink.razor +29 -0
| @@ -0,0 +1,29 @@ | ||
| 1 | +@* A name that links to Spotify when we know its id, and is plain text when we don't. *@ | |
| 2 | +@* Plays recorded before an id was stored, or ones RVRB passed through without one, are *@ | |
| 3 | +@* ordinary here rather than exceptional - hence one component instead of an @ per table. *@ | |
| 4 | + | |
| 5 | + (string.IsNullOrEmpty(Id)) | |
| 6 | +{ | |
| 7 | + | |
| 8 | +} | |
| 9 | +else | |
| 10 | +{ | |
| 11 | + <a href="@Url"></a> | |
| 12 | +} | |
| 13 | + | |
| 14 | + { | |
| 15 | + /// <summary>What the id identifies: <c>track</c>, <c>artist</c>, <c>album</c>.</summary> | |
| 16 | + [Parameter] | |
| 17 | + [EditorRequired] | |
| 18 | + public required string Kind { get; set; } | |
| 19 | + | |
| 20 | + /// <summary>The Spotify id. Null or empty renders the name unlinked.</summary> | |
| 21 | + [Parameter] | |
| 22 | + public string? Id { get; set; } | |
| 23 | + | |
| 24 | + [Parameter] | |
| 25 | + [EditorRequired] | |
| 26 | + public required string Name { get; set; } | |
| 27 | + | |
| 28 | + private string Url => $"https://open.spotify.com/{Kind}/{Uri.EscapeDataString(Id!)}"; | |
| 29 | +} | |
Blog/Models/RvrbSnapshot.cs +12 -1
| @@ -38,6 +38,7 @@ public sealed record RvrbCurrentTrack( | ||
| 38 | 38 | string? SpotifyTrackId, |
| 39 | 39 | string? Name, |
| 40 | 40 | IReadOnlyList<string> ArtistNames, |
| 41 | + IReadOnlyList<string> SpotifyArtistIds, | |
| 41 | 42 | TimeSpan? Duration, |
| 42 | 43 | string? AlbumArt, |
| 43 | 44 | TimeSpan? Elapsed, |
| @@ -85,13 +86,21 @@ public sealed record RvrbDj(string Name, string? UserName, int Plays, int Dopes, | ||
| 85 | 86 | public sealed record RvrbTrack( |
| 86 | 87 | string TrackName, |
| 87 | 88 | IReadOnlyList<string> ArtistNames, |
| 89 | + string? SpotifyTrackId, | |
| 90 | + IReadOnlyList<string> SpotifyArtistIds, | |
| 88 | 91 | int Plays, |
| 89 | 92 | int Dopes, |
| 90 | 93 | int Stars, |
| 91 | 94 | int Score); |
| 92 | 95 | |
| 93 | 96 | /// <summary>An artist on the leaderboard.</summary> |
| 94 | -public sealed record RvrbArtist(string ArtistName, int Plays, int Dopes, int Stars, int Score); | |
| 97 | +public sealed record RvrbArtist( | |
| 98 | + string ArtistName, | |
| 99 | + string? SpotifyArtistId, | |
| 100 | + int Plays, | |
| 101 | + int Dopes, | |
| 102 | + int Stars, | |
| 103 | + int Score); | |
| 95 | 104 | |
| 96 | 105 | /// <summary>One played track in the recent history.</summary> |
| 97 | 106 | public sealed record RvrbPlay( |
| @@ -99,6 +108,8 @@ public sealed record RvrbPlay( | ||
| 99 | 108 | string? Dj, |
| 100 | 109 | string TrackName, |
| 101 | 110 | IReadOnlyList<string> ArtistNames, |
| 111 | + string? SpotifyTrackId, | |
| 112 | + IReadOnlyList<string> SpotifyArtistIds, | |
| 102 | 113 | TimeSpan? Duration, |
| 103 | 114 | int Dopes, |
| 104 | 115 | int Stars, |
Blog/Services/RvrbSnapshotReader.cs +6 -0
| @@ -51,6 +51,7 @@ internal static class RvrbSnapshotReader | ||
| 51 | 51 | SpotifyTrackId: Text(track.Get("spotify_track_id")), |
| 52 | 52 | Name: Text(track.Get("name")), |
| 53 | 53 | ArtistNames: ReadStrings(track.Get("artist_names")), |
| 54 | + SpotifyArtistIds: ReadStrings(track.Get("spotify_artist_ids")), | |
| 54 | 55 | Duration: Duration(track.Get("duration_ms")), |
| 55 | 56 | AlbumArt: Text(track.Get("album_art")), |
| 56 | 57 | Elapsed: Duration(track.Get("elapsed_ms")), |
| @@ -94,6 +95,8 @@ internal static class RvrbSnapshotReader | ||
| 94 | 95 | private static RvrbTrack ReadTrack(ErlMap track) => new( |
| 95 | 96 | TrackName: Text(track.Get("track_name")) ?? "unknown track", |
| 96 | 97 | ArtistNames: ReadStrings(track.Get("artist_names")), |
| 98 | + SpotifyTrackId: Text(track.Get("spotify_track_id")), | |
| 99 | + SpotifyArtistIds: ReadStrings(track.Get("spotify_artist_ids")), | |
| 97 | 100 | Plays: (int)Int(track.Get("plays")), |
| 98 | 101 | Dopes: (int)Int(track.Get("dopes")), |
| 99 | 102 | Stars: (int)Int(track.Get("stars")), |
| @@ -101,6 +104,7 @@ internal static class RvrbSnapshotReader | ||
| 101 | 104 | |
| 102 | 105 | private static RvrbArtist ReadArtist(ErlMap artist) => new( |
| 103 | 106 | ArtistName: Text(artist.Get("artist_name")) ?? "unknown artist", |
| 107 | + SpotifyArtistId: Text(artist.Get("spotify_artist_id")), | |
| 104 | 108 | Plays: (int)Int(artist.Get("plays")), |
| 105 | 109 | Dopes: (int)Int(artist.Get("dopes")), |
| 106 | 110 | Stars: (int)Int(artist.Get("stars")), |
| @@ -111,6 +115,8 @@ internal static class RvrbSnapshotReader | ||
| 111 | 115 | Dj: Text(play.Get("dj")), |
| 112 | 116 | TrackName: Text(play.Get("track_name")) ?? "unknown track", |
| 113 | 117 | ArtistNames: ReadStrings(play.Get("artist_names")), |
| 118 | + SpotifyTrackId: Text(play.Get("spotify_track_id")), | |
| 119 | + SpotifyArtistIds: ReadStrings(play.Get("spotify_artist_ids")), | |
| 114 | 120 | Duration: Duration(play.Get("duration_ms")), |
| 115 | 121 | Dopes: (int)Int(play.Get("dopes")), |
| 116 | 122 | Stars: (int)Int(play.Get("stars")), |