| 1 |
@page "/rvrb" |
| 2 |
<PageTitle>rvrb bot</PageTitle> |
| 3 |
<PageScript Src="./Components/Pages/Rvrb.razor.js"></PageScript> |
| 4 |
|
| 5 |
<main> |
| 6 |
<h1>rvrb bot</h1> |
| 7 |
|
| 8 |
<p class="flex-spread"> |
| 9 |
<span class="@StatusClass">@StatusLabel</span> |
| 10 |
<label class="noselect"> |
| 11 |
Auto-refresh |
| 12 |
<input type="checkbox" id="autoRefresh"> |
| 13 |
</label> |
| 14 |
</p> |
| 15 |
|
| 16 |
@if (Snapshot is null) |
| 17 |
{ |
| 18 |
<Panel Legend="Unreachable"> |
| 19 |
<p> |
| 20 |
Nothing answered on the bot's node, so there are no stats to show. |
| 21 |
</p> |
| 22 |
@if (Status.Error is { } error) |
| 23 |
{ |
| 24 |
@* A failure on the far side arrives as an Erlang term, stack trace and all, which |
| 25 |
is worth keeping but not worth putting in front of everyone. *@ |
| 26 |
<details> |
| 27 |
<summary class="status-down">What went wrong</summary> |
| 28 |
<pre>@error</pre> |
| 29 |
</details> |
| 30 |
} |
| 31 |
</Panel> |
| 32 |
} |
| 33 |
else |
| 34 |
{ |
| 35 |
<Panel Legend="Now playing"> |
| 36 |
@if (Snapshot.Live is null) |
| 37 |
{ |
| 38 |
<p>The bot isn't connected to RVRB right now, so nothing is playing.</p> |
| 39 |
} |
| 40 |
else if (Snapshot.Live.CurrentTrack is not { } track) |
| 41 |
{ |
| 42 |
<p>Connected, but no track has played since the bot joined.</p> |
| 43 |
} |
| 44 |
else |
| 45 |
{ |
| 46 |
<div class="now-playing"> |
| 47 |
@if (track.AlbumArt is { } art) |
| 48 |
{ |
| 49 |
<img class="album-art" src="@art" alt="" width="128" height="128"/> |
| 50 |
} |
| 51 |
<div> |
| 52 |
<h3>@track.Name</h3> |
| 53 |
<p>@string.Join(", ", track.ArtistNames)</p> |
| 54 |
@* A bar needs both halves to mean anything: a track that came without a |
| 55 |
duration, or one already playing when the bot connected, gets neither. *@ |
| 56 |
@if (track.Duration is { } length && track.Elapsed is not null) |
| 57 |
{ |
| 58 |
<p> |
| 59 |
<progress id="trackProgress" max="1000" |
| 60 |
value="@((int)((track.Progress ?? 0) * 1000))" |
| 61 |
data-elapsed-ms="@((long)track.Elapsed.Value.TotalMilliseconds)" |
| 62 |
data-duration-ms="@((long)length.TotalMilliseconds)" |
| 63 |
data-age-ms="@((long)Age.TotalMilliseconds)"></progress> |
| 64 |
<span id="trackElapsed">@Format.Duration(track.Elapsed)</span> |
| 65 |
/ |
| 66 |
<span>@Format.Duration(length)</span> |
| 67 |
</p> |
| 68 |
} |
| 69 |
<p> |
| 70 |
👍 @Snapshot.Live.Dopes |
| 71 |
🔖 @Snapshot.Live.Stars |
| 72 |
@if (Snapshot.Live.AutoDoped) |
| 73 |
{ |
| 74 |
<span class="status-up"> — the bot doped this one</span> |
| 75 |
} |
| 76 |
@if (Snapshot.Live.AutoStarred) |
| 77 |
{ |
| 78 |
<span class="status-up"> — and starred it</span> |
| 79 |
} |
| 80 |
</p> |
| 81 |
</div> |
| 82 |
</div> |
| 83 |
} |
| 84 |
</Panel> |
| 85 |
|
| 86 |
@if (Snapshot.Live is { Djs.Count: > 0 } live) |
| 87 |
{ |
| 88 |
<Panel Legend="@($"DJ queue — one lap ≈ {Format.Duration(live.Lap)}")"> |
| 89 |
<div class="table-scroll"> |
| 90 |
<table> |
| 91 |
<thead> |
| 92 |
<tr> |
| 93 |
<th>DJ</th> |
| 94 |
<th>Avg track length</th> |
| 95 |
<th>Next play in</th> |
| 96 |
</tr> |
| 97 |
</thead> |
| 98 |
<tbody> |
| 99 |
@foreach (var dj in live.Djs) |
| 100 |
{ |
| 101 |
<tr> |
| 102 |
<td>@(dj.Current ? "▶ " : "")@(dj.Name ?? "someone")</td> |
| 103 |
<td> |
| 104 |
≈ @Format.Duration(dj.AverageTrack) |
| 105 |
@if (!dj.Measured) |
| 106 |
{ |
| 107 |
<span class="muted"> (assumed)</span> |
| 108 |
} |
| 109 |
</td> |
| 110 |
<td>≈ @Format.Duration(dj.Wait)</td> |
| 111 |
</tr> |
| 112 |
} |
| 113 |
</tbody> |
| 114 |
</table> |
| 115 |
</div> |
| 116 |
@if (live.QueuedTracks > 0) |
| 117 |
{ |
| 118 |
<p>@live.QueuedTracks @(live.QueuedTracks == 1 ? "track" : "tracks") queued for the bot's own turn.</p> |
| 119 |
} |
| 120 |
</Panel> |
| 121 |
} |
| 122 |
|
| 123 |
<Panel Legend="Totals"> |
| 124 |
<dl class="stats"> |
| 125 |
<div><dt>Plays</dt><dd>@Snapshot.Totals.Plays</dd></div> |
| 126 |
<div><dt>Tracks</dt><dd>@Snapshot.Totals.Tracks</dd></div> |
| 127 |
<div><dt>Artists</dt><dd>@Snapshot.Totals.Artists</dd></div> |
| 128 |
<div><dt>DJs</dt><dd>@Snapshot.Totals.Djs</dd></div> |
| 129 |
<div><dt>Users seen</dt><dd>@Snapshot.Totals.Users</dd></div> |
| 130 |
<div><dt>Dopes</dt><dd>@Snapshot.Totals.Dopes</dd></div> |
| 131 |
<div><dt>Stars</dt><dd>@Snapshot.Totals.Stars</dd></div> |
| 132 |
</dl> |
| 133 |
@if (Snapshot.Totals.FirstPlayAt is { } first) |
| 134 |
{ |
| 135 |
<p>Recording plays since <Timestamp At="first"/>.</p> |
| 136 |
} |
| 137 |
</Panel> |
| 138 |
|
| 139 |
<Panel Legend="@($"Plays per day — last {Snapshot.PlaysPerDay.Count} days")"> |
| 140 |
<ol class="bars"> |
| 141 |
@foreach (var day in Snapshot.PlaysPerDay) |
| 142 |
{ |
| 143 |
<li title="@BarTitle(day)"> |
| 144 |
<span class="bar" style="block-size: @(BarHeight(day.Plays))%"></span> |
| 145 |
<span class="bar-label">@day.Plays</span> |
| 146 |
</li> |
| 147 |
} |
| 148 |
</ol> |
| 149 |
</Panel> |
| 150 |
|
| 151 |
<Panel Legend="Top DJs"> |
| 152 |
<div class="table-scroll"> |
| 153 |
<table> |
| 154 |
<thead> |
| 155 |
<tr> |
| 156 |
<th>DJ</th> |
| 157 |
<th>Plays</th> |
| 158 |
<th>👍</th> |
| 159 |
<th>🔖</th> |
| 160 |
<th>Score</th> |
| 161 |
</tr> |
| 162 |
</thead> |
| 163 |
<tbody> |
| 164 |
@foreach (var dj in Snapshot.TopDjs) |
| 165 |
{ |
| 166 |
<tr> |
| 167 |
<td>@dj.Name</td> |
| 168 |
<td>@dj.Plays</td> |
| 169 |
<td>@dj.Dopes</td> |
| 170 |
<td>@dj.Stars</td> |
| 171 |
<td>@dj.Score</td> |
| 172 |
</tr> |
| 173 |
} |
| 174 |
</tbody> |
| 175 |
</table> |
| 176 |
</div> |
| 177 |
</Panel> |
| 178 |
|
| 179 |
<Panel Legend="Top tracks"> |
| 180 |
<div class="table-scroll"> |
| 181 |
<table> |
| 182 |
<thead> |
| 183 |
<tr> |
| 184 |
<th>Track</th> |
| 185 |
<th>Artist</th> |
| 186 |
<th>Plays</th> |
| 187 |
<th>Score</th> |
| 188 |
</tr> |
| 189 |
</thead> |
| 190 |
<tbody> |
| 191 |
@foreach (var track in Snapshot.TopTracks) |
| 192 |
{ |
| 193 |
<tr> |
| 194 |
<td>@track.TrackName</td> |
| 195 |
<td>@string.Join(", ", track.ArtistNames)</td> |
| 196 |
<td>@track.Plays</td> |
| 197 |
<td>@track.Score</td> |
| 198 |
</tr> |
| 199 |
} |
| 200 |
</tbody> |
| 201 |
</table> |
| 202 |
</div> |
| 203 |
</Panel> |
| 204 |
|
| 205 |
<Panel Legend="Top artists"> |
| 206 |
<div class="table-scroll"> |
| 207 |
<table> |
| 208 |
<thead> |
| 209 |
<tr> |
| 210 |
<th>Artist</th> |
| 211 |
<th>Plays</th> |
| 212 |
<th>Score</th> |
| 213 |
</tr> |
| 214 |
</thead> |
| 215 |
<tbody> |
| 216 |
@foreach (var artist in Snapshot.TopArtists) |
| 217 |
{ |
| 218 |
<tr> |
| 219 |
<td>@artist.ArtistName</td> |
| 220 |
<td>@artist.Plays</td> |
| 221 |
<td>@artist.Score</td> |
| 222 |
</tr> |
| 223 |
} |
| 224 |
</tbody> |
| 225 |
</table> |
| 226 |
</div> |
| 227 |
</Panel> |
| 228 |
|
| 229 |
<Panel Legend="Recently played"> |
| 230 |
<div class="table-scroll"> |
| 231 |
<table> |
| 232 |
<thead> |
| 233 |
<tr> |
| 234 |
<th>When</th> |
| 235 |
<th>DJ</th> |
| 236 |
<th>Track</th> |
| 237 |
<th>Artist</th> |
| 238 |
<th>Score</th> |
| 239 |
</tr> |
| 240 |
</thead> |
| 241 |
<tbody> |
| 242 |
@foreach (var play in Snapshot.RecentPlays) |
| 243 |
{ |
| 244 |
<tr> |
| 245 |
<td><Timestamp At="play.PlayedAt"/></td> |
| 246 |
<td>@play.Dj</td> |
| 247 |
<td>@play.TrackName</td> |
| 248 |
<td>@string.Join(", ", play.ArtistNames)</td> |
| 249 |
<td>@play.Score</td> |
| 250 |
</tr> |
| 251 |
} |
| 252 |
</tbody> |
| 253 |
</table> |
| 254 |
</div> |
| 255 |
</Panel> |
| 256 |
|
| 257 |
<p class="muted"> |
| 258 |
Read from <code>@Options.Node</code> over Erlang distribution with |
| 259 |
<a href="https://github.com/Besselking/BeamSharp">BeamSharp</a>: this site joins the |
| 260 |
cluster as a hidden node and calls <code>Rvrb.Stats.snapshot/0</code> the way any other |
| 261 |
BEAM node would. Snapshot taken <Timestamp At="Snapshot.GeneratedAt"/>. |
| 262 |
</p> |
| 263 |
} |
| 264 |
</main> |