using System.ComponentModel;
namespace Blog.Models;
///
/// What Rvrb.Stats.snapshot/0 answers with: the rvrb bot's room as it is right now, and
/// what it has played since the plays table started filling up. Decoded from the Erlang term in
/// .
///
public sealed record RvrbSnapshot(
DateTimeOffset GeneratedAt,
RvrbLive? Live,
RvrbTotals Totals,
IReadOnlyList PlaysPerDay,
IReadOnlyList TopDjs,
IReadOnlyList TopTracks,
IReadOnlyList TopArtists,
IReadOnlyList RecentPlays);
///
/// The room as the bot's websocket currently sees it. Null in a snapshot means the bot is not
/// connected to RVRB — the historic half is still there, since it only needs the database.
///
public sealed record RvrbLive(
string? ChannelId,
RvrbCurrentTrack? CurrentTrack,
int Dopes,
int Stars,
bool AutoDoped,
bool AutoStarred,
int QueuedTracks,
int KnownBots,
IReadOnlyList Djs,
TimeSpan Lap);
/// The track playing right now.
public sealed record RvrbCurrentTrack(
string? SpotifyTrackId,
string? Name,
IReadOnlyList ArtistNames,
TimeSpan? Duration,
string? AlbumArt,
TimeSpan? Elapsed,
TimeSpan? Remaining)
{
///
/// How far through the track we are, 0 to 1, or null when either half of that is unknown —
/// a track that arrived without a duration, or one already playing when the bot connected.
///
public double? Progress => Duration is { TotalMilliseconds: > 0 } total && Elapsed is { } elapsed
? Math.Clamp(elapsed.TotalMilliseconds / total.TotalMilliseconds, 0, 1)
: null;
}
/// One slot in the DJ queue, with the wait until that DJ next plays.
public sealed record RvrbDjSlot(
string RvrbId,
string? Name,
TimeSpan AverageTrack,
int PlayCount,
bool Measured,
bool Current,
TimeSpan Wait);
/// Room-wide counts.
public sealed record RvrbTotals(
long Plays,
long Votes,
long Dopes,
long Stars,
long Users,
long Djs,
long Tracks,
long Artists,
DateTimeOffset? FirstPlayAt,
DateTimeOffset? LastPlayAt);
/// Plays on one day. Days nothing was played on are present, with a count of zero.
public sealed record RvrbDay(DateOnly Date, int Plays);
/// A DJ on the leaderboard, and what their plays earned.
public sealed record RvrbDj(string Name, string? UserName, int Plays, int Dopes, int Stars, int Score);
/// A track on the leaderboard.
public sealed record RvrbTrack(
string TrackName,
IReadOnlyList ArtistNames,
int Plays,
int Dopes,
int Stars,
int Score);
/// An artist on the leaderboard.
public sealed record RvrbArtist(string ArtistName, int Plays, int Dopes, int Stars, int Score);
/// One played track in the recent history.
public sealed record RvrbPlay(
DateTimeOffset PlayedAt,
string? Dj,
string TrackName,
IReadOnlyList ArtistNames,
TimeSpan? Duration,
int Dopes,
int Stars,
int Score);
///
/// The result of asking the bot for a snapshot: either one, or why there isn't one. A bot that is
/// down is the normal thing a status page is there to report, so it is a value here rather than an
/// exception the page has to catch.
///
// Nothing here is ever mutated after it is built, and saying so is what lets HybridCache hand the
// same instance to every request instead of serializing a copy per reader.
[ImmutableObject(true)]
public sealed record RvrbStatus(DateTimeOffset FetchedAt, RvrbSnapshot? Snapshot, string? Error)
{
public static RvrbStatus Reachable(RvrbSnapshot snapshot) =>
new(DateTimeOffset.UtcNow, snapshot, null);
public static RvrbStatus Unreachable(string error) => new(DateTimeOffset.UtcNow, null, error);
}