diff --git a/DisCatSharp.Lavalink/Enums/LavalinkSearchType.cs b/DisCatSharp.Lavalink/Enums/LavalinkSearchType.cs index f3c1d089e..1676a80fb 100644 --- a/DisCatSharp.Lavalink/Enums/LavalinkSearchType.cs +++ b/DisCatSharp.Lavalink/Enums/LavalinkSearchType.cs @@ -1,44 +1,54 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. namespace DisCatSharp.Lavalink; /// /// The lavalink search type. /// public enum LavalinkSearchType { /// /// Search on SoundCloud /// SoundCloud, /// /// Search on Youtube. /// Youtube, /// /// Provide Lavalink with a plain URL. /// - Plain + Plain, + + /// + /// Search on Spotify. + /// + Spotify, + + /// + /// Search on Apple Music + /// + AppleMusic } diff --git a/DisCatSharp.Lavalink/LavalinkRestClient.cs b/DisCatSharp.Lavalink/LavalinkRestClient.cs index f09721122..95095e2db 100644 --- a/DisCatSharp.Lavalink/LavalinkRestClient.cs +++ b/DisCatSharp.Lavalink/LavalinkRestClient.cs @@ -1,426 +1,428 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Net; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Lavalink.Entities; using DisCatSharp.Net; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Lavalink; /// /// Represents a class for Lavalink REST calls. /// public sealed class LavalinkRestClient { /// /// Gets the REST connection endpoint for this client. /// public ConnectionEndpoint RestEndpoint { get; private set; } private HttpClient _http; private readonly ILogger _logger; private readonly Lazy _dcsVersionString = new(() => { var a = typeof(DiscordClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) return iv.InformationalVersion; var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) vs = $"{vs}, CI build {v.Revision}"; return vs; }); /// /// Creates a new Lavalink REST client. /// /// The REST server endpoint to connect to. /// The password for the remote server. public LavalinkRestClient(ConnectionEndpoint restEndpoint, string password) { this.RestEndpoint = restEndpoint; this.ConfigureHttpHandling(password); } /// /// Initializes a new instance of the class. /// /// The config. /// The client. internal LavalinkRestClient(LavalinkConfiguration config, BaseDiscordClient client) { this.RestEndpoint = config.RestEndpoint; this._logger = client.Logger; this.ConfigureHttpHandling(config.Password, client); } /// /// Gets the version of the Lavalink server. /// /// public Task GetVersionAsync() { var versionUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.VERSION}"); return this.InternalGetVersionAsync(versionUri); } #region Track_Loading /// /// Searches for specified terms. /// /// What to search for. /// What platform will search for. /// A collection of tracks matching the criteria. public Task GetTracksAsync(string searchQuery, LavalinkSearchType type = LavalinkSearchType.Youtube) { var prefix = type switch { LavalinkSearchType.Youtube => "ytsearch:", LavalinkSearchType.SoundCloud => "scsearch:", LavalinkSearchType.Plain => "", + LavalinkSearchType.Spotify => "spsearch:", + LavalinkSearchType.AppleMusic => "amsearch:", _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; var str = WebUtility.UrlEncode(prefix + searchQuery); var tracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.LOAD_TRACKS}?identifier={str}"); return this.InternalResolveTracksAsync(tracksUri); } /// /// Loads tracks from specified URL. /// /// URL to load tracks from. /// A collection of tracks from the URL. public Task GetTracksAsync(Uri uri) { var str = WebUtility.UrlEncode(uri.ToString()); var tracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.LOAD_TRACKS}?identifier={str}"); return this.InternalResolveTracksAsync(tracksUri); } /// /// Loads tracks from a local file. /// /// File to load tracks from. /// A collection of tracks from the file. public Task GetTracksAsync(FileInfo file) { var str = WebUtility.UrlEncode(file.FullName); var tracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.LOAD_TRACKS}?identifier={str}"); return this.InternalResolveTracksAsync(tracksUri); } /// /// Decodes a base64 track string into a Lavalink track object. /// /// The base64 track string. /// public Task DecodeTrackAsync(string trackString) { var str = WebUtility.UrlEncode(trackString); var decodeTrackUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.DECODE_TRACK}?track={str}"); return this.InternalDecodeTrackAsync(decodeTrackUri); } /// /// Decodes an array of base64 track strings into Lavalink track objects. /// /// The array of base64 track strings. /// public Task> DecodeTracksAsync(string[] trackStrings) { var decodeTracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.DECODE_TRACKS}"); return this.InternalDecodeTracksAsync(decodeTracksUri, trackStrings); } /// /// Decodes a list of base64 track strings into Lavalink track objects. /// /// The list of base64 track strings. /// public Task> DecodeTracksAsync(List trackStrings) { var decodeTracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.DECODE_TRACKS}"); return this.InternalDecodeTracksAsync(decodeTracksUri, trackStrings.ToArray()); } #endregion #region Route_Planner /// /// Retrieves statistics from the route planner. /// /// The status () details. public Task GetRoutePlannerStatusAsync() { var routeStatusUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.ROUTE_PLANNER}{Endpoints.STATUS}"); return this.InternalGetRoutePlannerStatusAsync(routeStatusUri); } /// /// Unmarks a failed route planner IP Address. /// /// The IP address name to unmark. /// public Task FreeAddressAsync(string address) { var routeFreeAddressUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.ROUTE_PLANNER}{Endpoints.FREE_ADDRESS}"); return this.InternalFreeAddressAsync(routeFreeAddressUri, address); } /// /// Unmarks all failed route planner IP Addresses. /// /// public Task FreeAllAddressesAsync() { var routeFreeAllAddressesUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.ROUTE_PLANNER}{Endpoints.FREE_ALL}"); return this.InternalFreeAllAddressesAsync(routeFreeAllAddressesUri); } #endregion /// /// get version async. /// /// The uri. /// A Task. internal async Task InternalGetVersionAsync(Uri uri) { using var req = await this._http.GetAsync(uri).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var json = await sr.ReadToEndAsync().ConfigureAwait(false); return json; } #region Internal_Track_Loading /// /// resolve tracks async. /// /// The uri. /// A Task. internal async Task InternalResolveTracksAsync(Uri uri) { // this function returns a Lavalink 3-like dataset regardless of input data version var json = "[]"; using (var req = await this._http.GetAsync(uri).ConfigureAwait(false)) using (var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var sr = new StreamReader(res, Utilities.UTF8)) json = await sr.ReadToEndAsync().ConfigureAwait(false); var jdata = JToken.Parse(json); if (jdata is JArray jarr) { // Lavalink 2.x var tracks = new List(jarr.Count); foreach (var jt in jarr) { var track = jt["info"].ToObject(); track.TrackString = jt["track"].ToString(); tracks.Add(track); } return new LavalinkLoadResult { PlaylistInfo = default, LoadResultType = tracks.Count == 0 ? LavalinkLoadResultType.LoadFailed : LavalinkLoadResultType.TrackLoaded, Tracks = tracks }; } else if (jdata is JObject jo) { // Lavalink 3.x jarr = jo["tracks"] as JArray; var loadInfo = jo.ToObject(); var tracks = new List(jarr.Count); foreach (var jt in jarr) { var track = jt["info"].ToObject(); track.TrackString = jt["track"].ToString(); tracks.Add(track); } loadInfo.Tracks = new ReadOnlyCollection(tracks); return loadInfo; } else return null; } /// /// decode track async. /// /// The uri. /// A Task. internal async Task InternalDecodeTrackAsync(Uri uri) { using var req = await this._http.GetAsync(uri).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var json = await sr.ReadToEndAsync().ConfigureAwait(false); if (!req.IsSuccessStatusCode) { var jsonError = JObject.Parse(json); this._logger?.LogError(LavalinkEvents.LavalinkDecodeError, "Unable to decode track strings: {0}", jsonError["message"]); return null; } var track = JsonConvert.DeserializeObject(json); return track; } /// /// decode tracks async. /// /// The uri. /// The ids. /// A Task. internal async Task> InternalDecodeTracksAsync(Uri uri, string[] ids) { var jsonOut = JsonConvert.SerializeObject(ids); var content = new StringContent(jsonOut, Utilities.UTF8, "application/json"); using var req = await this._http.PostAsync(uri, content).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var jsonIn = await sr.ReadToEndAsync().ConfigureAwait(false); if (!req.IsSuccessStatusCode) { var jsonError = JObject.Parse(jsonIn); this._logger?.LogError(LavalinkEvents.LavalinkDecodeError, "Unable to decode track strings", jsonError["message"]); return null; } var jarr = JToken.Parse(jsonIn) as JArray; var decodedTracks = new LavalinkTrack[jarr.Count]; for (var i = 0; i < decodedTracks.Length; i++) { decodedTracks[i] = JsonConvert.DeserializeObject(jarr[i]["info"].ToString()); decodedTracks[i].TrackString = jarr[i]["track"].ToString(); } var decodedTrackList = new ReadOnlyCollection(decodedTracks); return decodedTrackList; } #endregion #region Internal_Route_Planner /// /// get route planner status async. /// /// The uri. /// A Task. internal async Task InternalGetRoutePlannerStatusAsync(Uri uri) { using var req = await this._http.GetAsync(uri).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var json = await sr.ReadToEndAsync().ConfigureAwait(false); var status = JsonConvert.DeserializeObject(json); return status; } /// /// free address async. /// /// The uri. /// The address. /// A Task. internal async Task InternalFreeAddressAsync(Uri uri, string address) { var payload = new StringContent(address, Utilities.UTF8, "application/json"); using var req = await this._http.PostAsync(uri, payload).ConfigureAwait(false); if (req.StatusCode == HttpStatusCode.InternalServerError) this._logger?.LogWarning(LavalinkEvents.LavalinkRestError, "Request to {0} returned an internal server error - your server route planner configuration is likely incorrect", uri); } /// /// free all addresses async. /// /// The uri. /// A Task. internal async Task InternalFreeAllAddressesAsync(Uri uri) { var httpReq = new HttpRequestMessage(HttpMethod.Post, uri); using var req = await this._http.SendAsync(httpReq).ConfigureAwait(false); if (req.StatusCode == HttpStatusCode.InternalServerError) this._logger?.LogWarning(LavalinkEvents.LavalinkRestError, "Request to {0} returned an internal server error - your server route planner configuration is likely incorrect", uri); } #endregion /// /// Configures the http handling. /// /// The password. /// The client. private void ConfigureHttpHandling(string password, BaseDiscordClient client = null) { var httphandler = new HttpClientHandler { UseCookies = false, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseProxy = client != null && client.Configuration.Proxy != null }; if (httphandler.UseProxy) // because mono doesn't implement this properly httphandler.Proxy = client.Configuration.Proxy; this._http = new HttpClient(httphandler); this._http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", $"DisCatSharp.LavaLink/{this._dcsVersionString}"); this._http.DefaultRequestHeaders.TryAddWithoutValidation("Client-Name", $"DisCatSharp"); this._http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", password); } }