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);
}
}