PlazaSharp/libplaza/Main.cs

542 lines
21 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 <see cref="Nightwave"/> Plaza API
/// </summary>
namespace libplaza {
#region API models
/// <summary>
/// A transitional class used internally by <see cref="JsonConvert.DeserializeObject{T}(string)"/>
/// </summary>
public class PlazaPlayback {
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string artist;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string title;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string album;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int length;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int position;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string artwork;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string artwork_s;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int likes;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int hates;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
}
/// <summary>
/// A transitional class used internally by <see cref="JsonConvert.DeserializeObject{T}(string)"/>
/// </summary>
public class PlazaJson {
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public bool maintenance;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int listeners;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public long updated_at;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
[JsonProperty]
public PlazaPlayback playback;
}
/// <summary>
/// A transitional class used internally by <see cref="JsonConvert.DeserializeObject{T}(string)"/>
/// </summary>
public class PlazaUser {
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int id;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string username;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string email;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int role;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string created_at;
}
/// <summary>
/// A transitional class used internally by <see cref="JsonConvert.DeserializeObject{T}(string)"/>
/// </summary>
public class PlazaVote {
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string status;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int[] ratings;
}
/// <summary>
/// A transitional class used internally by <see cref="JsonConvert.DeserializeObject{T}(string)"/>
/// </summary>
public class PlazaGetVote {
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public int rate;
}
/// <summary>
/// A transitional class used internally by <see cref="JsonConvert.DeserializeObject{T}(string)"/>
/// </summary>
public class PlazaLogoutResult {
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string status;
}
/// <summary>
/// A transitional class used internally by <see cref="JsonConvert.DeserializeObject{T}(string)"/>
/// </summary>
public class PlazaLogonResult {
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string status;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string username;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string email;
/// <summary>
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
/// </summary>
public string token;
}
#endregion
#region Exceptions
/// <summary>
/// An <see cref="Exception"/> object indicating that the <see cref="Nightwave"/> 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 <see cref="Nightwave"/> API.
/// </summary>
internal class PlazaUnavailableException : Exception { }
#endregion
#region Type Definitions
/// <summary>
///Represents the current broadcast information returned by a <see cref="Nightwave"/> API object.
/// </summary>
public class Status {
/// <summary>
/// Indicates whether the <see cref="Nightwave"/> API is in maintenance mode
/// </summary>
public bool InMaintenance;
/// <summary>
/// Indicates whether an exception occurred while querying the <see cref="Nightwave"/> API
/// </summary>
public bool FaultOccurred;
/// <summary>
/// Contains the currently-playing track's title
/// </summary>
public string Title;
/// <summary>
/// Contains the currently-playing track's creating artist
/// </summary>
public string Artist;
/// <summary>
/// Contains the currently-playing track's album, if any
/// </summary>
public string Album;
/// <summary>
/// Contains a relative URI to the artwork for the currently-playing track
/// </summary>
public string ArtworkUri;
/// <summary>
/// Contains an integer value of the elapsed seconds for the current track
/// </summary>
public int Elapsed;
/// <summary>
/// Contains an integer value of the duration in seconds of the current track
/// </summary>
public int Duration;
/// <summary>
/// Contains a 64-bit integer value of the epoch timestamp when the broadcast information was updated
/// </summary>
public long Since;
/// <summary>
/// Contains an integer value of the time difference between the local computer and the <see cref="Nightwave"/> API server
/// </summary>
public int ServerTimeOffset;
/// <summary>
/// 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>
/// Contains an integer value of the number of likes the currently-playing track has received
/// </summary>
public int Likes;
/// <summary>
/// Contains an integer value of the number of dislikes the currently-playing track has received
/// </summary>
public int Dislikes;
/// <summary>
/// Contains an integer value of the number of people currently listening to the broadcast
/// </summary>
public int Listeners;
}
/// <summary>
/// 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>
/// <see cref="Nightwave"/> is the primary class by which the Nightwave Plaza API can be used.
/// </summary>
public class Nightwave : IDisposable {
/// <summary>
/// <see cref="MinRefreshTime"/> represents the minimum number of seconds that must pass before the cached <see cref="Status"/> may be invalidated
/// </summary>
public int MinRefreshTime;
private long tsLastRefresh=0;
private static HttpClient _hcl=new HttpClient();
private Status _bc;
/// <summary>
/// Creates a new <see cref="Nightwave"/> instance.
/// </summary>
/// <param name="baseuri">Specify an alternate URI for the API - useful if testing a dev server.</param>
/// <example>
/// To create a <see cref="Nightwave"/> API object targeting the production Nightwave Plaza API:
/// <code>
/// <see cref="Nightwave"/> nw = new <see cref="Nightwave"/>();
/// </code>
/// To create a <see cref="Nightwave"/> API object targeting a local server (eg., during development):
/// <code><see cref="Nightwave"/> nwd = new <see cref="Nightwave"/>("http://localhost:8000");</code>
/// </example>
public Nightwave(string baseuri = "https://api.plaza.one/") {
MinRefreshTime = 10;
_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>
/// <see cref="Nightwave"/> object deconstructor. Simply a wrapper to ensure that <see cref="Dispose"/> is called.
/// </summary>
~Nightwave() =>
Dispose();
/// <summary>
/// De-initialises the <see cref="Nightwave"/> API object.
/// <para>Currently, this is equivalent to synchronously logging out the current session.</para>
/// </summary>
public void Dispose() =>
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>
/// <see cref="Nightwave"/> n = new Nightwave();
/// <see cref="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>
/// <see cref="Nightwave"/> n = new Nightwave();
/// <see cref="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 <see cref="Status"/> object returned being fully fresh.
/// The <see cref="Status"/> object being returned may be a cached object UNLESS the <paramref name="Force"/> parameter is true
/// OR the <see cref="Status"/> object is older than the number of seconds stored by <see cref="MinRefreshTime"/>
/// OR <see cref="Status.CalculatedElapsed"/> is equal to or greater than <see cref="Status.Duration"/>.
/// </remarks>
/// <param name="Force">If true, the API will always be queried, and no caching will be performed.</param>
/// <returns>A populated <see cref="Status"/> 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 <see cref="Nightwave"/> 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());
return r.status == "success";
}
/// <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 <see cref="Nightwave"/> 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() =>
await GetUser() != null;
/// <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());
return r.id <= 0 ? null : r.username;
}
/// <summary>
/// Performs a login request to the <see cref="Nightwave"/> 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 <see cref="Nightwave"/> 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 <see cref="Nightwave"/> 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");
}
}
}