PlazaSharp/libplaza/Main.cs

343 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
/// <summary>
/// Classes and other definitions relating to the <c>Nightwave</c> Plaza API
/// </summary>
namespace libplaza {
#region API models
public class PlazaPlayback {
public string artist;
public string title;
public string album;
public int length;
public int position;
public string artwork;
public string artwork_s;
public int likes;
public int hates;
}
public class PlazaJson {
public bool maintenance;
public int listeners;
public long updated_at;
[JsonProperty]
public PlazaPlayback playback;
}
public class PlazaUser {
public int id;
public string username;
public string email;
public int role;
public string created_at;
}
public class PlazaVote {
public string status;
public int[] ratings;
}
public class PlazaGetVote {
public int rate;
}
public class PlazaLogoutResult {
public string status;
}
public class PlazaLogonResult {
public string status;
public string username;
public string email;
public string token;
}
#endregion
#region Exceptions
/// <summary>
/// An <see cref="Exception"/> object indicating that the <c>Nightwave</c> API is under maintenance
/// </summary>
internal class PlazaInMaintenanceException : Exception { }
/// <summary>
/// An <see cref="Exception"/> object indicating that an error or exception occurred when communicating with the <c>Nightwave</c> API.
/// </summary>
internal class PlazaUnavailableException : Exception { }
#endregion
#region Type Definitions
/// <summary>
/// A <c>Status</c> object represents the current broadcast information returned by a <c>Nightwave</c> API object.
/// </summary>
public class Status {
/// <summary><c>InMaintenance</c> indicates whether the <c>Nightwave</c> API is in maintenance mode</summary>
public bool InMaintenance;
/// <summary><c>FaultOccurred</c> indicates whether an exception occurred while querying the <c>Nightwave</c> API</summary>
public bool FaultOccurred;
/// <summary><c>Title</c> contains the currently-playing track's title</summary>
public string Title;
/// <summary><c>Artist</c> contains the currently-playing track's creating artist</summary>
public string Artist;
/// <summary><c>Album</c> contains the currently-playing track's album, if any</summary>
public string Album;
/// <summary><c>ArtworkUri</c> contains a relative URI to the artwork for the currently-playing track</summary>
public string ArtworkUri;
/// <summary><c>Elapsed</c> contains an integer value of the elapsed seconds for the current track</summary>
public int Elapsed;
/// <summary><c>Duration</c> contains an integer value of the duration in seconds of the current track</summary>
public int Duration;
/// <summary><c>Since</c> contains a 64-bit integer value of the epoch timestamp when the broadcast information was updated</summary>
public long Since;
/// <summary><c>ServerTimeOffset</c> contains an integer value of the time difference between the local computer and the <c>Nightwave</c> API server</summary>
public int ServerTimeOffset;
/// <summary><c>CalculatedElapsed</c> returns a dynamically-generated integer value of the elapsed seconds, taking time offsets and time since <see cref="Since"/></summary>
public int CalculatedElapsed =>
(int)(DateTimeOffset.Now.ToUnixTimeSeconds() - ServerTimeOffset - Since) + Elapsed;
/// <summary><c>Likes</c> contains an integer value of the number of likes the currently-playing track has received</summary>
public int Likes;
/// <summary><c>Dislikes</c> contains an integer value of the number of dislikes the currently-playing track has received</summary>
public int Dislikes;
/// <summary><c>Listeners</c> contains an integer value of the number of people currently listening to the broadcast</summary>
public int Listeners;
}
/// <summary>
/// <c>Vote</c> represents the user's disposition towards the currently-playing track.
/// </summary>
public enum Vote {
/// <summary>To <c>Dislike</c> is to indicate the user's disposition towards this track is one of displeasure; that the user does not appreciate hearing this track, perhaps even that the user wishes not to hear this track.</summary>
Dislike = -1,
/// <summary>To be of <c>Neutral</c> disposition is to indicate the user has no feelings towards this track, or that the user does have feelings, but not strong enough to be worth expressing</summary>
Neutral,
/// <summary>To <c>Like</c> is to indicate the user's disposition towards this track is one of enjoyment or pleasure; that the user appreciated this track and/or wishes to indicate such.</summary>
Like,
/// <summary>To <c>Favourite</c> is functionally equivalent to <see cref="Like"/>, with the additional effect of adding the track to the user's favourites list.</summary>
Favourite
}
#endregion
/// <summary>
/// <c>Nightwave</c> is the primary class by which the Nightwave Plaza API can be used.
/// </summary>
public class Nightwave {
/// <summary><c>MinRefreshTime</c> represents the minimum number of seconds that must pass before the cached <c>Status</c> may be invalidated</summary>
public int MinRefreshTime=10;
private long tsLastRefresh=0;
private static HttpClient _hcl=new HttpClient();
private Status _bc;
/// <summary>
/// Creates a new <c>Nightwave</c> instance.
/// </summary>
/// <param name="baseuri">Specify an alternate URI for the API - useful if testing a dev server.</param>
/// <example>
/// To create a <c>Nightwave</c> API object targeting the production Nightwave Plaza API:
/// <code>
/// Nightwave nw = new Nightwave();
/// </code>
/// To create a <c>Nightwave</c> API object targeting a local server (eg., during development):
/// <code>Nightwave nwd = new Nightwave("http://localhost:8000");</code>
/// </example>
public Nightwave(string baseuri = "https://api.plaza.one/") {
_hcl.BaseAddress = new Uri(baseuri);
_hcl.Timeout = TimeSpan.FromSeconds(2);
_hcl.DefaultRequestHeaders.Accept.Clear();
_hcl.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
}
/// <summary>
/// De-initialises the <c>Nightwave</c> API object.
/// <para>Currently, this is equivalent to synchronously logging out the current session.</para>
/// </summary>
~Nightwave() {
_ = Task.Run(async () => await Logout());
}
/// <summary>
/// Retrieves the current Nightwave Plaza radio broadcast information, if possible
/// </summary>
/// <example>
/// If Nightwave Plaza is broadcasting normally:
/// <code>
/// Nightwave n = new Nightwave();
/// Status s = await n.Status();
/// Console.WriteLine(s.Title);
/// </code>
/// The Console output would show "リサフランク420 / 現代のコンピュー", if that track were playing.
/// In the event that an HTTP request fails or Nightwave Plaza is in maintenance mode, the appropriate flag will be set
/// <code>
/// Nightwave n = new Nightwave();
/// Status s = await n.Status();
/// if (s.InMaintenance) {
/// Console.WriteLine("In maintenance!");
/// return;
/// } else if (s.FaultOccurred) {
/// Console.WriteLine("A fault occurred!");
/// return;
/// }
/// Console.WriteLine(s.Title);
/// </code>
/// The Console output would show "In maintenance!" or "A fault occurred!", depending on the conditions.
/// </example>
/// <remarks>
/// This method has inherent caching, and thus will not necessarily result in the <c>Status</c> object returned being fully fresh.
/// The <c>Status</c> object being returned may be a cached object UNLESS the <paramref name="Force"/> parameter is true
/// OR the <c>Status</c> object is older than the number of seconds stored by <see cref="MinRefreshTime"/>
/// OR <c>Status.CalculatedElapsed</c> is equal to or greater than <c>Status.Duration</c>.
/// </remarks>
/// <param name="Force">If true, the API will always be queried, and no caching will be performed.</param>
/// <returns>A populated <c>Status</c> object describing the current broadcast details.</returns>
public async Task<Status> Status(bool Force = false) {
//An API call will ONLY be made if Force is set, if the cached data is "stale",
// or if the currently-playing track's elapsed time is more than the track duration.
if(Force
|| (tsLastRefresh + MinRefreshTime < DateTimeOffset.Now.ToUnixTimeSeconds())
|| _bc.CalculatedElapsed < _bc.Duration) {
await Update();
tsLastRefresh = DateTimeOffset.Now.ToUnixTimeSeconds();
}
return _bc;
}
/// <summary>
/// A basic wrapper around <see cref="GetNightwaveAsync"/> which provides basic error handling.
/// </summary>
/// <returns>If called with <c>await</c>, returns nothing. If called without, returns a <c>Task</c> object for the method call</returns>
public async Task Update() {
try {
Status _=await GetNightwaveAsync();
_.InMaintenance = false;
_.FaultOccurred = false;
_bc = _;
} catch(PlazaInMaintenanceException) {
_bc.InMaintenance = true;
_bc.FaultOccurred = false;
} catch(Exception) { //TODO make this more reliable?
_bc.FaultOccurred = true;
}
}
/// <summary>
/// Communicates with the <c>Nightwave</c> API to retrieve the current broadcast information.
/// </summary>
/// <returns>Returns a <c>Status</c> object representing the current broadcast information.</returns>
/// <exception cref="PlazaUnavailableException">May be thrown in event of a non-success (HTTP/1.1 2XX) return code</exception>
/// <exception cref="PlazaInMaintenanceException">May be thrown in event that the API indicates it is in maintenance</exception>
public async Task<Status> GetNightwaveAsync() {
Status status;
HttpResponseMessage _r=await _hcl.GetAsync("/status");
if(!_r.IsSuccessStatusCode)
throw new PlazaUnavailableException();
PlazaJson j=JsonConvert.DeserializeObject<PlazaJson>(await _r.Content.ReadAsStringAsync());
if(j.maintenance)
throw new PlazaInMaintenanceException();
status = new Status {
Title = j.playback.title,
Album = j.playback.album,
Artist = j.playback.artist,
Elapsed = j.playback.position,
Duration = j.playback.length,
Since = j.updated_at,
Likes = j.playback.likes,
Dislikes = j.playback.hates,
Listeners = j.listeners,
ServerTimeOffset = (int)(DateTimeOffset.Now.ToUnixTimeSeconds() - _r.Headers.Date.Value.ToUnixTimeSeconds()),
ArtworkUri = $"https://plaza.one/{j.playback.artwork}"
};
return status;
}
/// <summary>
/// Fetches the user's <see cref="Vote"/> for the currently-playing track.
/// </summary>
/// <returns>A <see cref="Vote"/> object indicating the user's disposition towards the track. If the vote could not be returned, this will be <c>null</c></returns>
public async Task<Vote?> Vote() {
if(!await CheckSession())
return null;
HttpResponseMessage _r=await _hcl.GetAsync("/vote");
if(!_r.IsSuccessStatusCode)
return null;
PlazaGetVote r=JsonConvert.DeserializeObject<PlazaGetVote>(await _r.Content.ReadAsStringAsync());
return (Vote)r.rate;
}
/// <summary>
/// Casts the given <see cref="Vote"/> specified in <paramref name="v"/> as the user's vote on the current track.
/// </summary>
/// <param name="v">A <see cref="Vote"/> object indicating the user's disposition towards the current track</param>
/// <returns>A <c>bool</c>ean value indicating the success of the vote operation</returns>
public async Task<bool> Vote(Vote v) {
if(!await CheckSession())
return false;
HttpResponseMessage _r=await _hcl.PostAsync("/vote", new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("rate", ((int)v).ToString())}));
if(!_r.IsSuccessStatusCode)
return false;
PlazaVote r=JsonConvert.DeserializeObject<PlazaVote>(await _r.Content.ReadAsStringAsync());
if(r.status != "success")
return false;
return true;
}
/// <summary>
/// An overload method to <see cref="CheckSession"/> to first set the <c>X-Access-Token</c> header to <paramref name="str"/>
/// </summary>
/// <param name="str">A valid session token for the <c>Nightwave</c> API</param>
/// <returns>A <c>bool</c>ean value indicating whether the token given in <paramref name="str"/> was able to perform an authenticated API request</returns>
public async Task<bool> CheckSession(string str) {
_ = _hcl.DefaultRequestHeaders.Remove("X-Access-Token");
_hcl.DefaultRequestHeaders.Add("X-Access-Token", str);
return await CheckSession();
}
/// <summary>
/// Performs a basic read-only API request to determine whether the session token, if present, is valid.
/// </summary>
/// <returns>A <c>bool</c>ean value indicating whether the request succeeded</returns>
public async Task<bool> CheckSession() {
if(await GetUser() != null)
return true;
return false;
}
/// <summary>
/// Performs a basic read-only API request to fetch the currently-authenticated user's profile details.
/// </summary>
/// <returns>If successful, returns a <c>string</c> containing the authenticated user's username. If unsuccessful, will return null.</returns>
public async Task<string> GetUser() {
HttpResponseMessage _r=await _hcl.GetAsync("/user");
if(!_r.IsSuccessStatusCode)
return null;
PlazaUser r=JsonConvert.DeserializeObject<PlazaUser>(await _r.Content.ReadAsStringAsync());
if(r.id <= 0)
return null;
return r.username;
}
/// <summary>
/// Performs a login request to the <c>Nightwave</c> API with the given <paramref name="u">username</paramref> and <paramref name="p">password</paramref>.
/// </summary>
/// <param name="u">A <c>string</c> containing the user's username</param>
/// <param name="p">A <c>string</c> containing the user's password</param>
/// <returns>A <c>bool</c>ean value indicating whether the login attempt succeeded</returns>
/// <remarks>
/// Functionally, this method is a wrapper for <see cref="Login(string)"/>, performing the function of generating a Basic authentication string
/// </remarks>
public async Task<bool> Login(string u, string p) =>
await Login(Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{u}:{p}")));
/// <summary>
/// Performs a login request to the <c>Nightwave</c> API with the given HTTP Basic authentication <paramref name="str">string</paramref>.
/// </summary>
/// <param name="str">A valid HTTP Basic authentication string (<c>md5(username:password)</c>)</param>
/// <returns>A <c>bool</c>ean value indicating whether the login attempt succeeded</returns>
public async Task<bool> Login(string str) {
_hcl.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", str);
HttpResponseMessage _r=await _hcl.PostAsync("/user/login", null);
if(!_r.IsSuccessStatusCode)
return false;
PlazaLogonResult r=JsonConvert.DeserializeObject<PlazaLogonResult>(await _r.Content.ReadAsStringAsync());
if(r.status != "success")
return false;
_hcl.DefaultRequestHeaders.Add("X-Access-Token", r.token);
_ = _hcl.DefaultRequestHeaders.Remove("Authorization");
return true;
}
/// <summary>
/// Performs a logout request to the <c>Nightwave</c> API.
/// </summary>
/// <returns>If called with <c>await</c>, returns nothing. Otherwise, returns a <c>Task</c> object representing the method call</returns>
public async Task Logout() {
//Any API call errors are assumed to indicate an invalid session
// Since the goal of a logout function is to not have a valid session, this is a good thing.
if(await CheckSession())
_ = await _hcl.PostAsync("/user/logout", null);
_ = _hcl.DefaultRequestHeaders.Remove("Authorization");
_ = _hcl.DefaultRequestHeaders.Remove("X-Access-Token");
}
}
}