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 }