using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace libplaza {
#region API models
///
/// A transitional class used internally by
///
public class PlazaPlayback {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string artist;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string title;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string album;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int length;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int position;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string artwork_src;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string artwork_sm_src;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int reactions;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
//public int hates;
}
///
/// A transitional class used internally by
///
public class PlazaJson {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public bool maintenance;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int listeners;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public long updated_at;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
[JsonProperty]
public PlazaPlayback song;
}
///
/// A transitional class used internally by
///
public class PlazaUser {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int id;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string username;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string email;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int role;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string created_at;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public PlazaCurrentVote current_like;
}
///
/// A transitional class used internally by
///
public class PlazaVote {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string result;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int reactions;
}
///
/// A transitional class used internally by
///
public class PlazaGetVote {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public int reaction;
}
///
/// A transitional class used internally by
///
public class PlazaCurrentVote {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public bool like_id;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public PlazaLikedSong song;
}
///
/// A transitional class used internally by
///
public class PlazaLikedSong {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string artist;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string artwork_src;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string title;
}
///
/// A transitional class used internally by
///
public class PlazaLogoutResult {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string status;
}
///
/// A transitional class used internally by
///
public class PlazaLogonResult {
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string result;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string username;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string email;
///
/// A representation of a JSON object attribute, used transitionally while processing a deserialized JSON payload
///
public string token;
}
#endregion
#region Exceptions
///
/// An object indicating that the API is under maintenance
///
internal class PlazaInMaintenanceException : Exception { }
///
/// An object indicating that an error or exception occurred when communicating with the API.
///
internal class PlazaUnavailableException : Exception { }
///
/// A object indicating that, while an error occurred, it's one that we can silently retry.
///
internal class PlazaQuietlyUnavailableException : PlazaUnavailableException { }
#endregion
#region Type Definitions
///
///Represents the current broadcast information returned by a API object.
///
public class Status {
///
/// Indicates whether the API is in maintenance mode
///
public bool InMaintenance;
///
/// Indicates whether an exception occurred while querying the API
///
public bool FaultOccurred;
///
/// Contains the currently-playing track's title
///
public string Title;
///
/// Contains the currently-playing track's creating artist
///
public string Artist;
///
/// Contains the currently-playing track's album, if any
///
public string Album;
///
/// Contains a relative URI to the artwork for the currently-playing track
///
public string ArtworkUri;
///
/// Contains an integer value of the elapsed seconds for the current track
///
public int Elapsed;
///
/// Contains an integer value of the duration in seconds of the current track
///
public int Duration;
///
/// Contains a 64-bit integer value of the epoch timestamp when the broadcast information was updated
///
public long Since;
///
/// Contains an integer value of the time difference between the local computer and the API server
///
public int ServerTimeOffset;
///
/// Returns a dynamically-generated integer value of the elapsed seconds, taking time offsets and time since
///
public int CalculatedElapsed =>
(int)(DateTimeOffset.Now.ToUnixTimeSeconds() - ServerTimeOffset - Since) + Elapsed;
///
/// Contains an integer value of the number of likes the currently-playing track has received
///
public int Likes;
///
/// Contains an integer value of the number of dislikes the currently-playing track has received
///
//public int Dislikes;
///
/// Contains an integer value of the number of people currently listening to the broadcast
///
public int Listeners;
}
///
/// Represents the user's disposition towards the currently-playing track.
///
public enum Vote {
///
/// To Dislike 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.
/// This field was removed at some point, because it "no longer makes any sense" - I disagree.
///
//Dislike = -1,
///
/// To be of Neutral 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
///
Neutral,
///
/// To Like 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.
///
Like,
///
/// To Favourite is functionally equivalent to , with the additional effect of
/// adding the track to the user's favourites list.
///
Favourite
}
#endregion
#region Nightwave
///
/// is the primary class by which the Nightwave Plaza API can be used.
///
public class Nightwave : IDisposable {
///
/// represents the minimum number of seconds that must pass before the cached may be invalidated
///
public int MinRefreshTime;
private long tsLastRefresh=0;
private static HttpClient _hcl=new HttpClient();
private Status _bc;
///
/// Creates a new instance.
///
/// Specify an alternate URI for the API - useful if testing a dev server.
///
/// To create a API object targeting the production Nightwave Plaza API:
///
/// nw = new ();
///
/// To create a API object targeting a local server (eg., during development):
/// nwd = new ("http://localhost:8000");
///
public Nightwave(string baseuri = "https://api.plaza.one/") {
MinRefreshTime = 15;
_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"));
}
///
/// object deconstructor. Simply a wrapper to ensure that is called.
///
~Nightwave() =>
Dispose();
///
/// De-initialises the API object.
/// Currently, this is equivalent to synchronously logging out the current session.
///
public void Dispose() =>
Task.Run(async () => await Logout());
///
/// Retrieves the current Nightwave Plaza radio broadcast information, if possible
///
///
/// If Nightwave Plaza is broadcasting normally:
///
/// n = new Nightwave();
/// s = await n.Status();
/// Console.WriteLine(s.Title);
///
/// 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
///
/// n = new Nightwave();
/// 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);
///
/// The Console output would show "In maintenance!" or "A fault occurred!", depending on the conditions.
///
///
/// This method has inherent caching, and thus will not necessarily result in the object returned being fully fresh.
/// The object being returned may be a cached object UNLESS the parameter is true
/// OR the object is older than the number of seconds stored by
/// OR is equal to or greater than .
///
/// If true, the API will always be queried, and no caching will be performed.
/// A populated object describing the current broadcast details.
public async Task 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;
}
///
/// A basic wrapper around which provides basic error handling.
///
/// If called with await, returns nothing. If called without, returns a Task object for the method call
public async Task Update() {
try {
Status _=await GetNightwaveAsync();
_.InMaintenance = false;
_.FaultOccurred = false;
_bc = _;
} catch(PlazaInMaintenanceException) {
_bc.InMaintenance = true;
_bc.FaultOccurred = false;
} catch(PlazaQuietlyUnavailableException) {
_bc.FaultOccurred = false;
} catch(PlazaUnavailableException) {
_bc.FaultOccurred = true;
} catch(Exception) { //TODO make this more reliable?
_bc.FaultOccurred = true;
}
}
///
/// Communicates with the API to retrieve the current broadcast information.
///
/// Returns a Status object representing the current broadcast information.
/// May be thrown in event of a non-success (HTTP/1.1 2XX) return code
/// May be thrown in event that the API indicates it is in maintenance
public async Task GetNightwaveAsync() {
Status status;
HttpResponseMessage _r=await _hcl.GetAsync("/status");
if(_r.StatusCode == HttpStatusCode.TooManyRequests)
throw new PlazaQuietlyUnavailableException();
if(!_r.IsSuccessStatusCode)
throw new PlazaUnavailableException();
PlazaJson j=JsonConvert.DeserializeObject(await _r.Content.ReadAsStringAsync());
if(j.maintenance)
throw new PlazaInMaintenanceException();
// Previously this calculated ServerTimeOffset as Now - Headers.Date.Value.ToUnixTimeSeconds
status = new Status {
Title = j.song.title,
Album = j.song.album,
Artist = j.song.artist,
Elapsed = j.song.position,
Duration = j.song.length,
Since = j.updated_at,
Likes = j.song.reactions,
Listeners = j.listeners,
ServerTimeOffset = (int)(DateTimeOffset.Now.ToUnixTimeSeconds() - j.updated_at),
ArtworkUri = j.song.artwork_src
};
return status;
}
///
/// Fetches the user's for the currently-playing track.
///
/// A object indicating the user's disposition towards the track. If the vote could not be returned, this will be null
public async Task Vote() {
if(!await CheckSession())
return null;
PlazaUser r=await GetUser();
if(r.current_like == null)
return libplaza.Vote.Neutral;
if(r.current_like.like_id)
return libplaza.Vote.Favourite;
return libplaza.Vote.Like;
}
///
/// Casts the given specified in as the user's vote on the current track.
///
/// A object indicating the user's disposition towards the current track
/// A boolean value indicating the success of the vote operation
public async Task Vote(Vote v) {
if(!await CheckSession())
return false;
HttpResponseMessage _r=await _hcl.PostAsync("/reactions", new StringContent(
$"{{\"reaction\":{(int)v}}}", Encoding.UTF8, "application/json"));
if(!_r.IsSuccessStatusCode)
return false;
PlazaVote r=JsonConvert.DeserializeObject(await _r.Content.ReadAsStringAsync());
return r.result == "success";
}
///
/// An overload method to to first set the X-Access-Token header to
///
/// A valid session token for the API
/// A boolean value indicating whether the token given in was able to perform an authenticated API request
public async Task CheckSession(string str) {
_ = _hcl.DefaultRequestHeaders.Remove("X-Access-Token");
_hcl.DefaultRequestHeaders.Add("X-Access-Token", str);
return await CheckSession();
}
///
/// Performs a basic read-only API request to determine whether the session token, if present, is valid.
///
/// A boolean value indicating whether the request succeeded
public async Task CheckSession() =>
await GetUsername() != null;
///
/// Performs a basic read-only API request to fetch the profile information for the current user
///
/// If successful, returns a containing the user's profile information. If unsuccessful, returns null.
public async Task GetUser() {
HttpResponseMessage _r=await _hcl.GetAsync("/user");
if(!_r.IsSuccessStatusCode)
return null;
PlazaUser r=JsonConvert.DeserializeObject(await _r.Content.ReadAsStringAsync());
return r;
}
///
/// Performs a basic read-only API request to fetch the currently-authenticated user's profile details.
///
/// If successful, returns a string containing the authenticated user's username. If unsuccessful, will return null.
public async Task GetUsername() {
PlazaUser r=await GetUser();
return (r==null || r.username==null || r.username?.Length <= 0) ? null : r.username;
}
///
/// Performs a login request to the API with the given username and password.
///
/// A string containing the user's username
/// A string containing the user's password
/// A boolean value indicating whether the login attempt succeeded
///
/// Functionally, this method is a wrapper for , performing the function of generating a Basic authentication string
///
public async Task Login(string u, string p) {
_hcl.DefaultRequestHeaders.Add("Authorization", "Bearer 1");
HttpResponseMessage _r=await _hcl.PostAsync("/user/auth", new StringContent(
$"{{\"username\":\"{u}\",\"password\":\"{p}\"}}", Encoding.UTF8, "application/json"));
_ = _hcl.DefaultRequestHeaders.Remove("Authorization");
if(_r.StatusCode == HttpStatusCode.TooManyRequests)
return false;
if(!_r.IsSuccessStatusCode)
return false;
PlazaLogonResult r=JsonConvert.DeserializeObject(await _r.Content.ReadAsStringAsync());
if(r.result != "ok")
return false;
_hcl.DefaultRequestHeaders.Add("Authorization", $"Bearer {r.token}");
return true;
}
///
/// Performs a login request to the API with the given HTTP Basic authentication string.
///
/// A valid HTTP Basic authentication string (md5(username:password))
/// A boolean value indicating whether the login attempt succeeded
public async Task 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(await _r.Content.ReadAsStringAsync());
if(r.result != "success")
return false;
_hcl.DefaultRequestHeaders.Add("X-Access-Token", r.token);
_ = _hcl.DefaultRequestHeaders.Remove("Authorization");
return true;
}
///
/// Performs a logout request to the API.
///
/// If called with await, returns nothing. Otherwise, returns a Task object representing the method call
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");
}
}
#endregion
}