Decompiled source of WKMusic v1.0.1

WKMusic.dll

Decompiled 2 months ago
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using DG.Tweening;
using DG.Tweening.Core;
using DG.Tweening.Plugins.Options;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("WKMusic")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+57e91829fcde28c0bc52c8f4a67e18ff9a2b5a5b")]
[assembly: AssemblyProduct("WKMusic")]
[assembly: AssemblyTitle("WKMusic")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
	internal static class IsExternalInit
	{
	}
}
namespace WKMusic
{
	internal static class BrowserSetup
	{
		public sealed record SetupResult(string ClientId, string ClientSecret, SpotifyToken Token);

		private sealed class FlowState
		{
			public string? CredId;

			public string? CredSecret;

			public string? OAuthState;

			public Func<string, string, string, Task<SpotifyToken>>? ExchangeCode;
		}

		private static readonly HttpClient _http = new HttpClient();

		private static readonly string[] Scopes = new string[3] { "user-read-playback-state", "user-modify-playback-state", "user-read-currently-playing" };

		private const string BaseStyle = "\n<style>\n  body{font-family:'Segoe UI',sans-serif;background:#121212;color:#fff;display:flex;\n       align-items:center;justify-content:center;min-height:100vh;margin:0}\n  .card{background:#1e1e1e;border-radius:12px;padding:36px 40px;width:400px;\n        box-shadow:0 8px 32px #0008}\n  h1{margin:0 0 6px;font-size:1.4rem}\n  .sub{color:#888;font-size:.9rem;margin:0 0 24px}\n  .err{color:#e74c3c;font-size:.85rem;margin:0 0 14px}\n  label{display:block;font-size:.85rem;color:#aaa;margin-bottom:4px}\n  input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;\n        border:1px solid #333;background:#2a2a2a;color:#fff;font-size:.95rem;margin-bottom:16px}\n  input:focus{outline:none;border-color:#1db954}\n  button{width:100%;padding:11px;background:#1db954;color:#000;font-weight:700;\n         border:none;border-radius:6px;cursor:pointer;font-size:1rem}\n  button:hover{background:#1ed760}\n  .hint{font-size:.8rem;color:#666;margin-top:18px;line-height:1.6}\n  a{color:#1db954}\n  code{background:#2a2a2a;padding:1px 5px;border-radius:3px;font-size:.78rem}\n</style>";

		public static async Task<SetupResult?> RunAsync(string workerUrl, Func<string, string, string, Task<SpotifyToken>> exchangeCode, string initialClientId = "", string initialClientSecret = "", CancellationToken ct = default(CancellationToken))
		{
			int port = FindFreePort();
			string redirectUri = workerUrl + "/callback";
			string credId = NullIfEmpty(initialClientId);
			string credSecret = NullIfEmpty(initialClientSecret);
			TaskCompletionSource<SetupResult?> tcs = new TaskCompletionSource<SetupResult>();
			ct.Register(delegate
			{
				tcs.TrySetResult(null);
			});
			HttpListener listener = new HttpListener();
			listener.Prefixes.Add($"http://localhost:{port}/");
			listener.Start();
			FlowState state = new FlowState
			{
				CredId = credId,
				CredSecret = credSecret,
				ExchangeCode = exchangeCode
			};
			Task.Run(async delegate
			{
				try
				{
					while (!tcs.Task.IsCompleted)
					{
						HttpListenerContext ctx;
						try
						{
							ctx = await listener.GetContextAsync();
						}
						catch
						{
							break;
						}
						Task.Run(() => Handle(ctx, port, redirectUri, state, tcs));
					}
				}
				catch
				{
				}
				finally
				{
					listener.Stop();
				}
			});
			OpenBrowser($"http://localhost:{port}/");
			Plugin.Logger.LogInfo((object)$"WKMusic: setup → http://localhost:{port}/");
			return await tcs.Task;
		}

		private static async Task Handle(HttpListenerContext ctx, int port, string redirectUri, FlowState s, TaskCompletionSource<SetupResult?> tcs)
		{
			HttpListenerRequest request = ctx.Request;
			HttpListenerResponse res = ctx.Response;
			string absolutePath = request.Url.AbsolutePath;
			try
			{
				if (request.HttpMethod == "GET" && absolutePath == "/")
				{
					if (s.CredId == null || s.CredSecret == null)
					{
						await WriteHtml(res, PageCredentials());
						return;
					}
					s.OAuthState = $"{Guid.NewGuid():N}:{port}";
					Redirect(res, BuildAuthUrl(s.CredId, s.OAuthState, redirectUri));
				}
				else if (request.HttpMethod == "POST" && absolutePath == "/submit")
				{
					Dictionary<string, string> obj = await ReadForm(request);
					obj.TryGetValue("clientId", out var id2);
					obj.TryGetValue("clientSecret", out var secret2);
					id2 = id2?.Trim() ?? "";
					secret2 = secret2?.Trim() ?? "";
					string text = ValidateCredentials(id2, secret2);
					if (text == null)
					{
						text = await VerifyWithSpotifyAsync(id2, secret2);
					}
					string text2 = text;
					if (text2 != null)
					{
						await WriteHtml(res, PageCredentials(id2, text2));
						return;
					}
					s.CredId = id2;
					s.CredSecret = secret2;
					s.OAuthState = $"{Guid.NewGuid():N}:{port}";
					Redirect(res, BuildAuthUrl(s.CredId, s.OAuthState, redirectUri));
				}
				else if (request.HttpMethod == "GET" && absolutePath == "/callback")
				{
					NameValueCollection queryString = request.QueryString;
					string text3 = queryString["code"];
					string text4 = queryString["state"];
					string text5 = queryString["error"];
					if (!string.IsNullOrEmpty(text5))
					{
						await WriteHtml(res, PageError("Spotify error: " + text5));
						tcs.TrySetResult(null);
					}
					else if (text4 != s.OAuthState)
					{
						await WriteHtml(res, PageError("State mismatch — please try again."));
						tcs.TrySetResult(null);
					}
					else if (!string.IsNullOrEmpty(text3) && s.CredId != null && s.CredSecret != null && s.ExchangeCode != null)
					{
						try
						{
							SpotifyToken token = await s.ExchangeCode(s.CredId, s.CredSecret, text3);
							await WriteHtml(res, PageSuccess());
							tcs.TrySetResult(new SetupResult(s.CredId, s.CredSecret, token));
						}
						catch (Exception ex)
						{
							await WriteHtml(res, PageError("Token exchange failed: " + ex.Message));
							tcs.TrySetResult(null);
						}
					}
					else
					{
						await WriteHtml(res, PageError("Missing authorization code."));
						tcs.TrySetResult(null);
					}
				}
				else
				{
					res.StatusCode = 404;
					res.Close();
				}
			}
			catch (Exception ex2)
			{
				Plugin.Logger.LogWarning((object)("WKMusic: BrowserSetup handler error: " + ex2.Message));
				try
				{
					res.Abort();
				}
				catch
				{
				}
			}
		}

		private static string? ValidateCredentials(string id, string secret)
		{
			if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret))
			{
				return "Both fields are required.";
			}
			return null;
		}

		private static async Task<string?> VerifyWithSpotifyAsync(string clientId, string clientSecret)
		{
			_ = 1;
			try
			{
				FormUrlEncodedContent content = new FormUrlEncodedContent(new Dictionary<string, string>
				{
					["grant_type"] = "client_credentials",
					["client_id"] = clientId,
					["client_secret"] = clientSecret
				});
				HttpResponseMessage httpResponseMessage = await _http.PostAsync("https://accounts.spotify.com/api/token", content);
				if (httpResponseMessage.IsSuccessStatusCode)
				{
					return null;
				}
				JToken obj = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync())["error"];
				string text = ((obj != null) ? Extensions.Value<string>((IEnumerable<JToken>)obj) : null) ?? "";
				return (text == "invalid_client") ? "Invalid Client ID or Client Secret — check your Spotify Dashboard." : ("Spotify returned: " + text);
			}
			catch
			{
				return null;
			}
		}

		private static string BuildAuthUrl(string clientId, string state, string redirectUri)
		{
			string text = string.Join("&", new Dictionary<string, string>
			{
				["client_id"] = clientId,
				["response_type"] = "code",
				["redirect_uri"] = redirectUri,
				["scope"] = string.Join(" ", Scopes),
				["state"] = state
			}.Select((KeyValuePair<string, string> kv) => Uri.EscapeDataString(kv.Key) + "=" + Uri.EscapeDataString(kv.Value)));
			return "https://accounts.spotify.com/authorize?" + text;
		}

		private static async Task WriteHtml(HttpListenerResponse res, string html)
		{
			byte[] bytes = Encoding.UTF8.GetBytes(html);
			res.ContentType = "text/html; charset=utf-8";
			res.ContentLength64 = bytes.Length;
			await res.OutputStream.WriteAsync(bytes, 0, bytes.Length);
			res.Close();
		}

		private static void Redirect(HttpListenerResponse res, string url)
		{
			res.StatusCode = 302;
			res.Headers["Location"] = url;
			res.Close();
		}

		private static async Task<Dictionary<string, string>> ReadForm(HttpListenerRequest req)
		{
			using StreamReader reader = new StreamReader(req.InputStream, Encoding.UTF8);
			string obj = await reader.ReadToEndAsync();
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			string[] array = obj.Split('&');
			foreach (string text in array)
			{
				int num = text.IndexOf('=');
				if (num >= 0)
				{
					string key = Uri.UnescapeDataString(text.Substring(0, num).Replace('+', ' '));
					string text2 = text;
					int num2 = num + 1;
					dictionary[key] = Uri.UnescapeDataString(text2.Substring(num2, text2.Length - num2).Replace('+', ' '));
				}
			}
			return dictionary;
		}

		private static int FindFreePort()
		{
			TcpListener tcpListener = new TcpListener(IPAddress.Loopback, 0);
			tcpListener.Start();
			int port = ((IPEndPoint)tcpListener.LocalEndpoint).Port;
			tcpListener.Stop();
			return port;
		}

		private static void OpenBrowser(string url)
		{
			try
			{
				Process.Start(new ProcessStartInfo(url)
				{
					UseShellExecute = true
				});
			}
			catch
			{
			}
		}

		private static string? NullIfEmpty(string s)
		{
			if (!string.IsNullOrWhiteSpace(s))
			{
				return s.Trim();
			}
			return null;
		}

		private static string PageCredentials(string initialId = "", string error = "")
		{
			return "<!DOCTYPE html>\n<html lang='en'><head><meta charset='utf-8'><title>WKMusic — Spotify Setup</title>\n<style>\n  body{font-family:'Segoe UI',sans-serif;background:#121212;color:#fff;display:flex;\n       align-items:center;justify-content:center;min-height:100vh;margin:0}\n  .card{background:#1e1e1e;border-radius:12px;padding:36px 40px;width:400px;\n        box-shadow:0 8px 32px #0008}\n  h1{margin:0 0 6px;font-size:1.4rem}\n  .sub{color:#888;font-size:.9rem;margin:0 0 24px}\n  .err{color:#e74c3c;font-size:.85rem;margin:0 0 14px}\n  label{display:block;font-size:.85rem;color:#aaa;margin-bottom:4px}\n  input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;\n        border:1px solid #333;background:#2a2a2a;color:#fff;font-size:.95rem;margin-bottom:16px}\n  input:focus{outline:none;border-color:#1db954}\n  button{width:100%;padding:11px;background:#1db954;color:#000;font-weight:700;\n         border:none;border-radius:6px;cursor:pointer;font-size:1rem}\n  button:hover{background:#1ed760}\n  .hint{font-size:.8rem;color:#666;margin-top:18px;line-height:1.6}\n  a{color:#1db954}\n  code{background:#2a2a2a;padding:1px 5px;border-radius:3px;font-size:.78rem}\n</style></head>\n<body><div class='card'>\n  <h1>\ud83c\udfb5 WKMusic — Spotify Setup</h1>\n  <p class='sub'>One-time setup. Token is saved locally.</p>\n  " + (string.IsNullOrEmpty(error) ? "" : ("<p class='err'>" + error + "</p>")) + "\n  <form method='POST' action='/submit'>\n    <label>Client ID</label>\n    <input name='clientId' value='" + initialId + "' placeholder='32-character hex string' autofocus required>\n    <label>Client Secret</label>\n    <input name='clientSecret' type='password' placeholder='32-character hex string' required>\n    <button type='submit'>Connect Spotify →</button>\n  </form>\n  <p class='hint'>\n    Get your credentials at <a href='https://developer.spotify.com/dashboard' target='_blank'>developer.spotify.com/dashboard</a>.<br>\n    Add <code>https://wkmusic.eventeventeventevent1.workers.dev/callback</code> as a Redirect URI.\n  </p>\n</div></body></html>";
		}

		private static string PageSuccess()
		{
			return "<!DOCTYPE html>\n<html lang='en'><head><meta charset='utf-8'><title>WKMusic — Connected</title>\n<style>\n  body{font-family:'Segoe UI',sans-serif;background:#121212;color:#fff;display:flex;\n       align-items:center;justify-content:center;min-height:100vh;margin:0}\n  .card{background:#1e1e1e;border-radius:12px;padding:36px 40px;width:400px;\n        box-shadow:0 8px 32px #0008}\n  h1{margin:0 0 6px;font-size:1.4rem}\n  .sub{color:#888;font-size:.9rem;margin:0 0 24px}\n  .err{color:#e74c3c;font-size:.85rem;margin:0 0 14px}\n  label{display:block;font-size:.85rem;color:#aaa;margin-bottom:4px}\n  input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;\n        border:1px solid #333;background:#2a2a2a;color:#fff;font-size:.95rem;margin-bottom:16px}\n  input:focus{outline:none;border-color:#1db954}\n  button{width:100%;padding:11px;background:#1db954;color:#000;font-weight:700;\n         border:none;border-radius:6px;cursor:pointer;font-size:1rem}\n  button:hover{background:#1ed760}\n  .hint{font-size:.8rem;color:#666;margin-top:18px;line-height:1.6}\n  a{color:#1db954}\n  code{background:#2a2a2a;padding:1px 5px;border-radius:3px;font-size:.78rem}\n</style>\n<style>.card{text-align:center} .icon{font-size:3rem;margin-bottom:12px} h1{color:#1db954}</style>\n</head><body><div class='card'>\n  <div class='icon'>\ud83c\udfb5</div>\n  <h1>Connected!</h1>\n  <p class='sub' style='margin:0'>Spotify is linked. You can close this tab and return to the game.</p>\n</div></body></html>";
		}

		private static string PageError(string message)
		{
			return "<!DOCTYPE html>\n<html lang='en'><head><meta charset='utf-8'><title>WKMusic — Error</title>\n<style>\n  body{font-family:'Segoe UI',sans-serif;background:#121212;color:#fff;display:flex;\n       align-items:center;justify-content:center;min-height:100vh;margin:0}\n  .card{background:#1e1e1e;border-radius:12px;padding:36px 40px;width:400px;\n        box-shadow:0 8px 32px #0008}\n  h1{margin:0 0 6px;font-size:1.4rem}\n  .sub{color:#888;font-size:.9rem;margin:0 0 24px}\n  .err{color:#e74c3c;font-size:.85rem;margin:0 0 14px}\n  label{display:block;font-size:.85rem;color:#aaa;margin-bottom:4px}\n  input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;\n        border:1px solid #333;background:#2a2a2a;color:#fff;font-size:.95rem;margin-bottom:16px}\n  input:focus{outline:none;border-color:#1db954}\n  button{width:100%;padding:11px;background:#1db954;color:#000;font-weight:700;\n         border:none;border-radius:6px;cursor:pointer;font-size:1rem}\n  button:hover{background:#1ed760}\n  .hint{font-size:.8rem;color:#666;margin-top:18px;line-height:1.6}\n  a{color:#1db954}\n  code{background:#2a2a2a;padding:1px 5px;border-radius:3px;font-size:.78rem}\n</style>\n<style>.card{text-align:center} h1{color:#e74c3c}</style>\n</head><body><div class='card'>\n  <h1>⚠ Error</h1>\n  <p class='sub' style='margin:0'>" + message + "</p>\n</div></body></html>";
		}
	}
	public class CredentialsStorage
	{
		public sealed record SpotifyCredentials(string ClientId, string ClientSecret);

		private static class NativeMethods
		{
			[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
			public struct CREDENTIAL
			{
				public uint Flags;

				public uint Type;

				public string TargetName;

				public string? Comment;

				public long LastWritten;

				public uint CredentialBlobSize;

				public IntPtr CredentialBlob;

				public uint Persist;

				public uint AttributeCount;

				public IntPtr Attributes;

				public string? TargetAlias;

				public string? UserName;
			}

			public const uint CRED_TYPE_GENERIC = 1u;

			public const uint CRED_PERSIST_LOCAL_MACHINE = 2u;

			[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern bool CredWrite(ref CREDENTIAL credential, uint flags);

			[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern bool CredRead(string target, uint type, uint flags, out IntPtr credential);

			[DllImport("advapi32.dll")]
			public static extern void CredFree(IntPtr buffer);

			[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
			public static extern bool CredDelete(string target, uint type, uint flags);
		}

		private const string CredentialName = "WKMusic/SpotifyCredentials";

		public void Save(string clientId, string clientSecret)
		{
			string s = JsonConvert.SerializeObject((object)new
			{
				ClientId = clientId,
				ClientSecret = clientSecret
			});
			byte[] bytes = Encoding.UTF8.GetBytes(s);
			NativeMethods.CREDENTIAL cREDENTIAL = default(NativeMethods.CREDENTIAL);
			cREDENTIAL.Type = 1u;
			cREDENTIAL.TargetName = "WKMusic/SpotifyCredentials";
			cREDENTIAL.CredentialBlob = Marshal.AllocHGlobal(bytes.Length);
			cREDENTIAL.CredentialBlobSize = (uint)bytes.Length;
			cREDENTIAL.Persist = 2u;
			cREDENTIAL.UserName = "wkmusic";
			NativeMethods.CREDENTIAL credential = cREDENTIAL;
			Marshal.Copy(bytes, 0, credential.CredentialBlob, bytes.Length);
			try
			{
				if (!NativeMethods.CredWrite(ref credential, 0u))
				{
					throw new Exception($"CredWrite failed: {Marshal.GetLastWin32Error()}");
				}
			}
			finally
			{
				Marshal.FreeHGlobal(credential.CredentialBlob);
			}
		}

		public SpotifyCredentials? Load()
		{
			if (!NativeMethods.CredRead("WKMusic/SpotifyCredentials", 1u, 0u, out var credential))
			{
				return null;
			}
			try
			{
				NativeMethods.CREDENTIAL cREDENTIAL = Marshal.PtrToStructure<NativeMethods.CREDENTIAL>(credential);
				byte[] array = new byte[cREDENTIAL.CredentialBlobSize];
				Marshal.Copy(cREDENTIAL.CredentialBlob, array, 0, array.Length);
				var anon = JsonConvert.DeserializeAnonymousType(Encoding.UTF8.GetString(array), new
				{
					ClientId = "",
					ClientSecret = ""
				});
				if (anon == null || string.IsNullOrWhiteSpace(anon.ClientId))
				{
					return null;
				}
				return new SpotifyCredentials(anon.ClientId, anon.ClientSecret);
			}
			catch
			{
				Clear();
				return null;
			}
			finally
			{
				NativeMethods.CredFree(credential);
			}
		}

		public void Clear()
		{
			NativeMethods.CredDelete("WKMusic/SpotifyCredentials", 1u, 0u);
		}
	}
	public interface IMusicClient
	{
		Task InitializeAsync(CancellationToken ct = default(CancellationToken));

		Task<PlaybackState> GetPlaybackStateAsync(CancellationToken ct = default(CancellationToken));
	}
	internal static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "dev.wkmusic";

		public const string PLUGIN_NAME = "WKMusic";

		public const string PLUGIN_VERSION = "1.0.0";
	}
	[BepInPlugin("dev.wkmusic", "WKMusic", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource Logger = null;

		internal static Plugin Instance = null;

		internal volatile PlaybackState State = PlaybackState.Empty;

		internal volatile bool Initialized;

		internal volatile bool Initializing;

		internal DateTime StateUpdatedAtUtc = DateTime.UtcNow;

		internal volatile byte[]? PendingCoverBytes;

		internal static float PlayerScale = 0.8f;

		internal static float CoverSize = 64f;

		internal static float CoverOpacity = 0.6f;

		private void Awake()
		{
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Expected O, but got Unknown
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Expected O, but got Unknown
			//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0103: Expected O, but got Unknown
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			Instance = this;
			Logger = ((BaseUnityPlugin)this).Logger;
			string workerUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Spotify", "WorkerUrl", "https://wkmusic.eventeventeventevent1.workers.dev", "Cloudflare Worker URL; change only if you host your own").Value;
			PlayerScale = ((BaseUnityPlugin)this).Config.Bind<float>("HUD", "PlayerScale", 0.8f, new ConfigDescription("Overall player scale (affects font size and layout dimensions)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 3f), Array.Empty<object>())).Value;
			CoverSize = ((BaseUnityPlugin)this).Config.Bind<float>("HUD", "CoverSize", 64f, new ConfigDescription("Album cover size in pixels", (AcceptableValueBase)(object)new AcceptableValueRange<float>(20f, 300f), Array.Empty<object>())).Value;
			CoverOpacity = ((BaseUnityPlugin)this).Config.Bind<float>("HUD", "CoverOpacity", 0.6f, new ConfigDescription("Album cover opacity (0 = invisible, 1 = fully opaque)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())).Value;
			CredentialsStorage credStorage = new CredentialsStorage();
			CredentialsStorage.SpotifyCredentials saved = credStorage.Load();
			Task.Run(async delegate
			{
				Initializing = true;
				try
				{
					TokenStorage tokenStorage = new TokenStorage();
					string clientId;
					string clientSecret;
					if ((object)saved == null)
					{
						Logger.LogInfo((object)"WKMusic: credentials missing, opening browser setup...");
						BrowserSetup.SetupResult setupResult = await BrowserSetup.RunAsync(workerUrl, delegate(string id, string secret, string code)
						{
							SpotifyAuth spotifyAuth2 = new SpotifyAuth(workerUrl, secret);
							string redirectUri2 = workerUrl + "/callback";
							return spotifyAuth2.ExchangeCodeAsync(id, code, redirectUri2, default(CancellationToken));
						});
						if ((object)setupResult == null)
						{
							Logger.LogWarning((object)"WKMusic: setup cancelled.");
							return;
						}
						credStorage.Save(setupResult.ClientId, setupResult.ClientSecret);
						tokenStorage.Save(setupResult.Token);
						clientId = setupResult.ClientId;
						clientSecret = setupResult.ClientSecret;
					}
					else
					{
						clientId = saved.ClientId;
						clientSecret = saved.ClientSecret;
					}
					SpotifyClient client = new SpotifyClient(new SpotifyAuth(workerUrl, clientSecret), tokenStorage, clientId);
					try
					{
						await client.InitializeAsync();
					}
					catch (SpotifyAuthException ex) when ((object)saved != null && ex.Error == "invalid_client")
					{
						Logger.LogWarning((object)"WKMusic: saved Spotify credentials were rejected; clearing them and reopening setup.");
						tokenStorage.Clear();
						credStorage.Clear();
						BrowserSetup.SetupResult setupResult2 = await BrowserSetup.RunAsync(workerUrl, delegate(string id, string secret, string code)
						{
							SpotifyAuth spotifyAuth = new SpotifyAuth(workerUrl, secret);
							string redirectUri = workerUrl + "/callback";
							return spotifyAuth.ExchangeCodeAsync(id, code, redirectUri, default(CancellationToken));
						}, clientId);
						if ((object)setupResult2 == null)
						{
							Logger.LogWarning((object)"WKMusic: setup cancelled.");
							return;
						}
						credStorage.Save(setupResult2.ClientId, setupResult2.ClientSecret);
						tokenStorage.Save(setupResult2.Token);
						client = new SpotifyClient(new SpotifyAuth(workerUrl, setupResult2.ClientSecret), tokenStorage, setupResult2.ClientId);
						await client.InitializeAsync();
					}
					Initialized = true;
					Logger.LogInfo((object)"WKMusic: Spotify connected.");
					StartPolling(client);
				}
				catch (Exception ex2)
				{
					Logger.LogError((object)("WKMusic: init failed — " + ex2.GetType().Name + ": " + ex2.Message));
					Logger.LogError((object)(ex2.StackTrace ?? ""));
				}
				finally
				{
					Initializing = false;
				}
			});
			DOTween.Init((bool?)null, (bool?)null, (LogBehaviour?)null);
			new Harmony("dev.wkmusic").PatchAll();
		}

		private void StartPolling(IMusicClient client)
		{
			IMusicClient client2 = client;
			HttpClient http = new HttpClient();
			string lastCoverToken = null;
			Task.Run(async delegate
			{
				while (true)
				{
					int delayMs;
					try
					{
						PlaybackState state = await client2.GetPlaybackStateAsync();
						State = state;
						StateUpdatedAtUtc = DateTime.UtcNow;
						delayMs = 1000;
						TrackInfo track = State.Track;
						string text = (((object)track == null) ? null : ((track.CoverBytes != null) ? ("embedded:" + track.Id) : track.CoverUrl));
						if (text != lastCoverToken)
						{
							lastCoverToken = text;
							byte[] pendingCoverBytes;
							if (track?.CoverBytes != null)
							{
								pendingCoverBytes = track.CoverBytes;
							}
							else
							{
								byte[] array = (((object)track == null || track.CoverUrl == null) ? Array.Empty<byte>() : (await http.GetByteArrayAsync(track.CoverUrl)));
								pendingCoverBytes = array;
							}
							PendingCoverBytes = pendingCoverBytes;
						}
					}
					catch (SpotifyRateLimitException ex)
					{
						int num = Math.Max(1000, (int)Math.Ceiling(ex.RetryAfter.TotalMilliseconds));
						delayMs = ((num <= 30000) ? num : num);
						string statusMessage = ((num > 30000) ? ("Spotify rate limited (" + FormatDelay(delayMs) + ")") : "Spotify rate limited");
						State = State with
						{
							StatusMessage = statusMessage
						};
						StateUpdatedAtUtc = DateTime.UtcNow;
						Logger.LogWarning((object)($"WKMusic: Spotify rate limited polling; retrying in {delayMs} ms. " + string.Format("Retry-After={0}, requested={1} ms.", ex.RawRetryAfter ?? "missing", num)));
					}
					catch (Exception ex2)
					{
						delayMs = 5000;
						Logger.LogWarning((object)("WKMusic: poll error - " + ex2.GetType().Name + ": " + ex2.Message + "\n" + ex2.StackTrace));
					}
					await Task.Delay(delayMs);
				}
			});
		}

		internal static string Truncate(string s, int max)
		{
			if (s.Length > max)
			{
				return s.Substring(0, max - 3) + "...";
			}
			return s;
		}

		private static string FormatDelay(int delayMs)
		{
			TimeSpan timeSpan = TimeSpan.FromMilliseconds(delayMs);
			if (timeSpan.TotalHours >= 1.0)
			{
				return $"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m";
			}
			if (timeSpan.TotalMinutes >= 1.0)
			{
				return $"{(int)timeSpan.TotalMinutes}m {timeSpan.Seconds}s";
			}
			return $"{timeSpan.Seconds}s";
		}
	}
	[HarmonyPatch(typeof(CL_UIManager), "Awake")]
	internal static class Patch_UIManager_Awake
	{
		private const float RightMargin = 10f;

		private const float TextWidth = 320f;

		private const float CoverGap = 10f;

		private static void Postfix(CL_UIManager __instance)
		{
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_0111: Unknown result type (might be due to invalid IL or missing references)
			//IL_013f: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Logger.LogInfo((object)"WKMusic: CL_UIManager.Awake fired.");
			Transform canvas = __instance.canvas;
			TMP_Text timer = __instance.timer;
			if ((Object)(object)canvas == (Object)null || (Object)(object)timer == (Object)null)
			{
				Plugin.Logger.LogWarning((object)$"WKMusic: canvas={canvas}, timer={timer} - aborting.");
				return;
			}
			float playerScale = Plugin.PlayerScale;
			float coverSize = Plugin.CoverSize;
			float coverRight = 10f + 320f * playerScale + 10f;
			GameObject val = new GameObject("WKMusic_HUD");
			val.transform.SetParent(canvas, false);
			RectTransform obj = val.AddComponent<RectTransform>();
			Vector2 val2 = default(Vector2);
			((Vector2)(ref val2))..ctor(1f, 1f);
			obj.anchorMax = val2;
			obj.anchorMin = val2;
			obj.pivot = new Vector2(1f, 1f);
			obj.anchoredPosition = Vector2.zero;
			obj.sizeDelta = Vector2.zero;
			Patch_UIManager_Update.HudGroup = val.AddComponent<CanvasGroup>();
			Transform transform = val.transform;
			Patch_UIManager_Update.CoverA = CreateCover(transform, coverSize, coverRight);
			Patch_UIManager_Update.CoverB = CreateCover(transform, coverSize, coverRight);
			Patch_UIManager_Update.RestoreCover();
			Patch_UIManager_Update.TrackLabel = CreateLabel(transform, timer, "WKMusic_Track", new Vector2(-10f, -10f), playerScale, (TextAlignmentOptions)258, 42f * playerScale);
			Patch_UIManager_Update.ProgressLabel = CreateLabel(transform, timer, "WKMusic_Progress", new Vector2(-10f, -62f * playerScale), playerScale, (TextAlignmentOptions)257);
			Patch_UIManager_Update.ResetCache();
			if (!Plugin.Instance.Initialized)
			{
				Patch_UIManager_Update.TrackLabel.text = "WKMusic: loading...";
			}
			Plugin.Logger.LogInfo((object)"WKMusic: HUD created.");
		}

		private static RawImage CreateCover(Transform canvas, float size, float coverRight)
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = new GameObject("WKMusic_Cover");
			val.transform.SetParent(canvas, false);
			val.SetActive(false);
			RawImage obj = val.AddComponent<RawImage>();
			RectTransform rectTransform = ((Graphic)obj).rectTransform;
			rectTransform.anchorMin = new Vector2(1f, 1f);
			rectTransform.anchorMax = new Vector2(1f, 1f);
			rectTransform.pivot = new Vector2(1f, 1f);
			rectTransform.anchoredPosition = new Vector2(0f - coverRight, -10f);
			rectTransform.sizeDelta = new Vector2(size, size);
			return obj;
		}

		private static TMP_Text CreateLabel(Transform canvas, TMP_Text reference, string name, Vector2 offset, float scale = 1f, TextAlignmentOptions alignment = 257, float height = 30f)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = new GameObject(name);
			val.transform.SetParent(canvas, false);
			TextMeshProUGUI obj = val.AddComponent<TextMeshProUGUI>();
			((TMP_Text)obj).font = reference.font;
			((TMP_Text)obj).fontSize = reference.fontSize * 0.85f * scale;
			((Graphic)obj).color = ((Graphic)reference).color;
			((TMP_Text)obj).alignment = alignment;
			((TMP_Text)obj).enableWordWrapping = false;
			((TMP_Text)obj).overflowMode = (TextOverflowModes)0;
			((TMP_Text)obj).richText = true;
			RectTransform rectTransform = ((TMP_Text)obj).rectTransform;
			rectTransform.anchorMin = new Vector2(1f, 1f);
			rectTransform.anchorMax = new Vector2(1f, 1f);
			rectTransform.pivot = new Vector2(1f, 1f);
			rectTransform.anchoredPosition = offset;
			rectTransform.sizeDelta = new Vector2(320f * scale, height);
			return (TMP_Text)(object)obj;
		}
	}
	[HarmonyPatch(typeof(CL_UIManager), "Update")]
	internal static class Patch_UIManager_Update
	{
		internal static TMP_Text? TrackLabel;

		internal static TMP_Text? ProgressLabel;

		internal static CanvasGroup? HudGroup;

		internal static RawImage? CoverA;

		internal static RawImage? CoverB;

		private static bool _aIsFront = true;

		private static Texture2D? _coverTexA;

		private static Texture2D? _coverTexB;

		private static byte[]? _lastCoverBytes;

		private static bool _wasPaused;

		private static string? _lastTrackText;

		private static float _trackAlpha = 1f;

		private static RawImage? Front
		{
			get
			{
				if (!_aIsFront)
				{
					return CoverB;
				}
				return CoverA;
			}
		}

		private static RawImage? Back
		{
			get
			{
				if (!_aIsFront)
				{
					return CoverA;
				}
				return CoverB;
			}
		}

		internal static void ResetCache()
		{
			if ((Object)(object)TrackLabel != (Object)null)
			{
				TrackLabel.text = "";
			}
			if ((Object)(object)ProgressLabel != (Object)null)
			{
				ProgressLabel.text = "";
			}
			_wasPaused = false;
			_lastTrackText = null;
			_trackAlpha = 1f;
			DOTween.Kill((object)"coverA", false);
			DOTween.Kill((object)"coverB", false);
			_aIsFront = true;
		}

		internal static void RestoreCover()
		{
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			if (_lastCoverBytes == null)
			{
				return;
			}
			RawImage front = Front;
			if (!((Object)(object)front == (Object)null))
			{
				SetLayerTexture(front, ref _aIsFront ? ref _coverTexA : ref _coverTexB, _lastCoverBytes);
				((Graphic)front).color = new Color(1f, 1f, 1f, Plugin.CoverOpacity);
				((Component)front).gameObject.SetActive(true);
				RawImage back = Back;
				if ((Object)(object)back != (Object)null)
				{
					((Component)back).gameObject.SetActive(false);
				}
			}
		}

		private static void SetLayerTexture(RawImage layer, ref Texture2D? texSlot, byte[] bytes)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			if ((Object)(object)texSlot != (Object)null)
			{
				Object.Destroy((Object)(object)texSlot);
			}
			texSlot = new Texture2D(2, 2, (TextureFormat)4, false, false);
			if (ImageConversion.LoadImage(texSlot, bytes))
			{
				((Texture)texSlot).wrapMode = (TextureWrapMode)1;
				texSlot.Apply();
			}
			layer.texture = (Texture)(object)texSlot;
		}

		internal static void ApplyCover(byte[] bytes)
		{
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0104: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0144: Expected O, but got Unknown
			RawImage back = Back;
			if ((Object)(object)back == (Object)null)
			{
				return;
			}
			DOTween.Kill((object)"coverA", false);
			DOTween.Kill((object)"coverB", false);
			if (_aIsFront)
			{
				SetLayerTexture(back, ref _coverTexB, bytes);
			}
			else
			{
				SetLayerTexture(back, ref _coverTexA, bytes);
			}
			((Graphic)back).color = new Color(1f, 1f, 1f, 0f);
			((Component)back).gameObject.SetActive(true);
			((Component)back).transform.SetAsLastSibling();
			RawImage front = Front;
			TweenSettingsExtensions.SetId<Tweener>(DOVirtual.Float(0f, Plugin.CoverOpacity, 0.6f, (TweenCallback<float>)delegate(float v)
			{
				//IL_0024: Unknown result type (might be due to invalid IL or missing references)
				if ((Object)(object)back != (Object)null)
				{
					((Graphic)back).color = new Color(1f, 1f, 1f, v);
				}
			}), "coverB");
			if ((Object)(object)front != (Object)null && ((Component)front).gameObject.activeSelf)
			{
				TweenSettingsExtensions.OnComplete<Tweener>(TweenSettingsExtensions.SetId<Tweener>(DOVirtual.Float(((Graphic)front).color.a, 0f, 0.6f, (TweenCallback<float>)delegate(float v)
				{
					//IL_0024: Unknown result type (might be due to invalid IL or missing references)
					if ((Object)(object)front != (Object)null)
					{
						((Graphic)front).color = new Color(1f, 1f, 1f, v);
					}
				}), "coverA"), (TweenCallback)delegate
				{
					if ((Object)(object)front != (Object)null)
					{
						((Component)front).gameObject.SetActive(false);
					}
				});
			}
			_aIsFront = !_aIsFront;
		}

		internal static void HideCover()
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Expected O, but got Unknown
			DOTween.Kill((object)"coverA", false);
			DOTween.Kill((object)"coverB", false);
			RawImage front = Front;
			if ((Object)(object)front == (Object)null || !((Component)front).gameObject.activeSelf)
			{
				return;
			}
			TweenSettingsExtensions.OnComplete<Tweener>(TweenSettingsExtensions.SetId<Tweener>(DOVirtual.Float(((Graphic)front).color.a, 0f, 0.5f, (TweenCallback<float>)delegate(float v)
			{
				//IL_0024: Unknown result type (might be due to invalid IL or missing references)
				if ((Object)(object)front != (Object)null)
				{
					((Graphic)front).color = new Color(1f, 1f, 1f, v);
				}
			}), "coverA"), (TweenCallback)delegate
			{
				if ((Object)(object)front != (Object)null)
				{
					((Component)front).gameObject.SetActive(false);
				}
			});
		}

		private static void SetTrackLabel(string newText)
		{
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)TrackLabel == (Object)null))
			{
				if (_lastTrackText != newText)
				{
					_lastTrackText = newText;
					TrackLabel.text = newText;
					_trackAlpha = 0f;
				}
				if (_trackAlpha < 1f)
				{
					_trackAlpha = Mathf.Min(1f, _trackAlpha + Time.deltaTime * 2.5f);
					Color color = ((Graphic)TrackLabel).color;
					((Graphic)TrackLabel).color = new Color(color.r, color.g, color.b, _trackAlpha);
				}
			}
		}

		private static void Postfix()
		{
			if ((Object)(object)TrackLabel == (Object)null || (Object)(object)ProgressLabel == (Object)null)
			{
				return;
			}
			Plugin instance = Plugin.Instance;
			byte[] pendingCoverBytes = instance.PendingCoverBytes;
			if (pendingCoverBytes != null)
			{
				instance.PendingCoverBytes = null;
				if (pendingCoverBytes.Length == 0)
				{
					_lastCoverBytes = null;
					HideCover();
				}
				else
				{
					_lastCoverBytes = pendingCoverBytes;
					ApplyCover(pendingCoverBytes);
				}
			}
			if (instance.Initializing)
			{
				SetTrackLabel("WKMusic: authorizing...");
				ProgressLabel.text = "";
				return;
			}
			if (!instance.Initialized)
			{
				SetTrackLabel("");
				ProgressLabel.text = "";
				return;
			}
			PlaybackState state = instance.State;
			if (!string.IsNullOrEmpty(state.StatusMessage))
			{
				SetTrackLabel(state.StatusMessage);
				ProgressLabel.text = "";
				return;
			}
			if ((Object)(object)HudGroup != (Object)null)
			{
				bool flag = state.Track != null && !state.IsPlaying;
				if (flag != _wasPaused)
				{
					_wasPaused = flag;
					DOVirtual.Float(HudGroup.alpha, flag ? 0.3f : 1f, 0.5f, (TweenCallback<float>)delegate(float v)
					{
						HudGroup.alpha = v;
					});
					TweenSettingsExtensions.SetEase<TweenerCore<Vector3, Vector3, VectorOptions>>(ShortcutExtensions.DOScale(((Component)HudGroup).transform, flag ? 0.75f : 1f, 0.5f), (Ease)9);
				}
			}
			if ((object)state.Track == null)
			{
				SetTrackLabel("Nothing playing");
				ProgressLabel.text = "";
				return;
			}
			TrackInfo track = state.Track;
			int value = (state.IsPlaying ? (state.ProgressMs + (int)(DateTime.UtcNow - instance.StateUpdatedAtUtc).TotalMilliseconds) : state.ProgressMs);
			value = Math.Clamp(value, 0, track.DurationMs);
			TimeSpan timeSpan = TimeSpan.FromMilliseconds(value);
			TimeSpan timeSpan2 = TimeSpan.FromMilliseconds(track.DurationMs);
			int num = Math.Clamp((int)Math.Round(((track.DurationMs > 0) ? ((double)value / (double)track.DurationMs) : 0.0) * 50.0), 0, 50);
			string arg = new string('-', num) + "|<color=#303030>" + new string('-', 50 - num) + "</color>";
			SetTrackLabel(Plugin.Truncate(track.Title, 40) + "\n<line-height=65%><color=#888888><size=80%>" + Plugin.Truncate(track.Artist, 40) + "</size></color>");
			ProgressLabel.text = $"<mspace=0.45em>{timeSpan:m\\:ss}</mspace>  <mspace=0.22em>{arg}</mspace>  <mspace=0.45em>{timeSpan2:m\\:ss}</mspace>";
		}
	}
	public class SpotifyAuthException : Exception
	{
		public string? Error { get; }

		public string ResponseBody { get; }

		public SpotifyAuthException(string message, string? error, string responseBody)
		{
			Error = error;
			ResponseBody = responseBody;
			base..ctor(message);
		}
	}
	public class SpotifyAuth
	{
		private const string TokenUrl = "https://accounts.spotify.com/api/token";

		private readonly HttpClient _http = http ?? new HttpClient();

		private readonly string _workerUrl = workerUrl.TrimEnd('/');

		private readonly string _clientSecret;

		public SpotifyAuth(string workerUrl, string clientSecret, HttpClient? http = null)
		{
			_clientSecret = clientSecret;
			base..ctor();
		}

		public async Task<SpotifyToken> AuthorizeAsync(string clientId, CancellationToken ct = default(CancellationToken))
		{
			string redirectUri = _workerUrl + "/callback";
			return ((await BrowserSetup.RunAsync(_workerUrl, (string id, string _, string code) => ExchangeCodeAsync(id, code, redirectUri, ct), clientId, "", ct)) ?? throw new OperationCanceledException("Spotify authorization cancelled.")).Token;
		}

		public async Task<SpotifyToken> RefreshAsync(string clientId, SpotifyToken token, CancellationToken ct = default(CancellationToken))
		{
			Dictionary<string, string> nameValueCollection = new Dictionary<string, string>
			{
				["grant_type"] = "refresh_token",
				["refresh_token"] = token.RefreshToken,
				["client_id"] = clientId,
				["client_secret"] = _clientSecret
			};
			HttpResponseMessage response = await _http.PostAsync("https://accounts.spotify.com/api/token", new FormUrlEncodedContent(nameValueCollection), ct);
			string text = await response.Content.ReadAsStringAsync();
			if (!response.IsSuccessStatusCode)
			{
				string error = TryReadSpotifyError(text);
				throw new SpotifyAuthException("Token refresh failed: " + text, error, text);
			}
			JObject val = JObject.Parse(text);
			string accessToken = Extensions.Value<string>((IEnumerable<JToken>)val["access_token"]);
			JToken obj = val["refresh_token"];
			return new SpotifyToken(accessToken, ((obj != null) ? Extensions.Value<string>((IEnumerable<JToken>)obj) : null) ?? token.RefreshToken, Extensions.Value<int>((IEnumerable<JToken>)val["expires_in"]), DateTime.UtcNow);
		}

		internal async Task<SpotifyToken> ExchangeCodeAsync(string clientId, string code, string redirectUri, CancellationToken ct)
		{
			Dictionary<string, string> nameValueCollection = new Dictionary<string, string>
			{
				["grant_type"] = "authorization_code",
				["code"] = code,
				["redirect_uri"] = redirectUri,
				["client_id"] = clientId,
				["client_secret"] = _clientSecret
			};
			HttpResponseMessage response = await _http.PostAsync("https://accounts.spotify.com/api/token", new FormUrlEncodedContent(nameValueCollection), ct);
			string text = await response.Content.ReadAsStringAsync();
			if (!response.IsSuccessStatusCode)
			{
				string error = TryReadSpotifyError(text);
				throw new SpotifyAuthException("Token exchange failed: " + text, error, text);
			}
			JObject val = JObject.Parse(text);
			string accessToken = Extensions.Value<string>((IEnumerable<JToken>)val["access_token"]);
			JToken obj = val["refresh_token"];
			return new SpotifyToken(accessToken, ((obj != null) ? Extensions.Value<string>((IEnumerable<JToken>)obj) : null) ?? string.Empty, Extensions.Value<int>((IEnumerable<JToken>)val["expires_in"]), DateTime.UtcNow);
		}

		private static string? TryReadSpotifyError(string json)
		{
			try
			{
				JToken obj = JObject.Parse(json)["error"];
				return (obj != null) ? Extensions.Value<string>((IEnumerable<JToken>)obj) : null;
			}
			catch
			{
				return null;
			}
		}
	}
	public class SpotifyRateLimitException : Exception
	{
		public TimeSpan RetryAfter { get; }

		public string? RawRetryAfter { get; }

		public SpotifyRateLimitException(TimeSpan retryAfter, string? rawRetryAfter)
		{
			RetryAfter = retryAfter;
			RawRetryAfter = rawRetryAfter;
			base..ctor($"Spotify rate limit reached. Retry after {retryAfter.TotalSeconds:0.#}s.");
		}
	}
	public class SpotifyClient : IMusicClient
	{
		[CompilerGenerated]
		private SpotifyAuth <auth>P;

		[CompilerGenerated]
		private TokenStorage <storage>P;

		private const string ApiBase = "https://api.spotify.com/v1";

		private readonly HttpClient _http;

		private readonly string _clientId;

		private SpotifyToken? _token;

		public SpotifyClient(SpotifyAuth auth, TokenStorage storage, string clientId, HttpClient? http = null)
		{
			<auth>P = auth;
			<storage>P = storage;
			_http = http ?? new HttpClient();
			_clientId = clientId;
			base..ctor();
		}

		public async Task InitializeAsync(CancellationToken ct = default(CancellationToken))
		{
			_token = <storage>P.Load();
			if ((object)_token == null)
			{
				_token = await <auth>P.AuthorizeAsync(_clientId, ct);
				<storage>P.Save(_token);
			}
			else if (_token.IsExpired)
			{
				await ForceRefreshAsync(ct);
			}
		}

		public async Task<PlaybackState> GetPlaybackStateAsync(CancellationToken ct = default(CancellationToken))
		{
			string text = await GetAsync("/me/player", ct);
			if (text == null)
			{
				return PlaybackState.Empty;
			}
			JObject val = JObject.Parse(text);
			JToken val2 = val["item"];
			if (val2 == null || (int)val2.Type == 10)
			{
				return PlaybackState.Empty;
			}
			TrackInfo track = ParseTrack(val2);
			bool isPlaying = Extensions.Value<bool>((IEnumerable<JToken>)val["is_playing"]);
			int progressMs = Extensions.Value<int>((IEnumerable<JToken>)val["progress_ms"]);
			return new PlaybackState(track, isPlaying, progressMs);
		}

		public Task PlayAsync(CancellationToken ct = default(CancellationToken))
		{
			return PutAsync("/me/player/play", ct);
		}

		public Task PauseAsync(CancellationToken ct = default(CancellationToken))
		{
			return PutAsync("/me/player/pause", ct);
		}

		public Task NextAsync(CancellationToken ct = default(CancellationToken))
		{
			return PostAsync("/me/player/next", ct);
		}

		public Task PreviousAsync(CancellationToken ct = default(CancellationToken))
		{
			return PostAsync("/me/player/previous", ct);
		}

		public Task SeekAsync(int positionMs, CancellationToken ct = default(CancellationToken))
		{
			return PutAsync($"/me/player/seek?position_ms={positionMs}", ct);
		}

		public Task SetVolumeAsync(int volumePercent, CancellationToken ct = default(CancellationToken))
		{
			return PutAsync($"/me/player/volume?volume_percent={Math.Clamp(volumePercent, 0, 100)}", ct);
		}

		private async Task<string?> GetAsync(string path, CancellationToken ct)
		{
			await EnsureTokenValidAsync(ct);
			HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.spotify.com/v1" + path);
			httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
			HttpResponseMessage httpResponseMessage = await _http.SendAsync(httpRequestMessage, ct);
			if (httpResponseMessage.StatusCode == HttpStatusCode.NoContent)
			{
				return null;
			}
			if (httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized)
			{
				await ForceRefreshAsync(ct);
				return await GetAsync(path, ct);
			}
			EnsureNotRateLimited(httpResponseMessage);
			httpResponseMessage.EnsureSuccessStatusCode();
			return await httpResponseMessage.Content.ReadAsStringAsync();
		}

		private async Task PutAsync(string path, CancellationToken ct)
		{
			await EnsureTokenValidAsync(ct);
			HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, "https://api.spotify.com/v1" + path);
			httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
			httpRequestMessage.Content = new StringContent("", Encoding.UTF8, "application/json");
			HttpResponseMessage httpResponseMessage = await _http.SendAsync(httpRequestMessage, ct);
			if (httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized)
			{
				await ForceRefreshAsync(ct);
				await PutAsync(path, ct);
			}
			else if (httpResponseMessage.StatusCode != HttpStatusCode.NoContent)
			{
				EnsureNotRateLimited(httpResponseMessage);
				httpResponseMessage.EnsureSuccessStatusCode();
			}
		}

		private async Task PostAsync(string path, CancellationToken ct)
		{
			await EnsureTokenValidAsync(ct);
			HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://api.spotify.com/v1" + path);
			httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
			httpRequestMessage.Content = new StringContent("", Encoding.UTF8, "application/json");
			HttpResponseMessage httpResponseMessage = await _http.SendAsync(httpRequestMessage, ct);
			if (httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized)
			{
				await ForceRefreshAsync(ct);
				await PostAsync(path, ct);
			}
			else if (httpResponseMessage.StatusCode != HttpStatusCode.NoContent)
			{
				EnsureNotRateLimited(httpResponseMessage);
				httpResponseMessage.EnsureSuccessStatusCode();
			}
		}

		private async Task EnsureTokenValidAsync(CancellationToken ct)
		{
			if ((object)_token == null)
			{
				throw new InvalidOperationException("Client not initialized. Call InitializeAsync first.");
			}
			if (_token.IsExpired)
			{
				await ForceRefreshAsync(ct);
			}
		}

		private async Task ForceRefreshAsync(CancellationToken ct)
		{
			try
			{
				_token = await <auth>P.RefreshAsync(_clientId, _token, ct);
			}
			catch (SpotifyAuthException ex) when (ex.Error == "invalid_grant")
			{
				<storage>P.Clear();
				_token = await <auth>P.AuthorizeAsync(_clientId, ct);
			}
			<storage>P.Save(_token);
		}

		private static void EnsureNotRateLimited(HttpResponseMessage response)
		{
			if (response.StatusCode != HttpStatusCode.TooManyRequests)
			{
				return;
			}
			response.Headers.TryGetValues("Retry-After", out IEnumerable<string> values);
			string rawRetryAfter = values?.FirstOrDefault();
			TimeSpan timeSpan = response.Headers.RetryAfter?.Delta ?? (response.Headers.RetryAfter?.Date - DateTimeOffset.UtcNow) ?? TimeSpan.FromSeconds(10.0);
			if (timeSpan < TimeSpan.FromSeconds(1.0))
			{
				timeSpan = TimeSpan.FromSeconds(1.0);
			}
			throw new SpotifyRateLimitException(timeSpan, rawRetryAfter);
		}

		private static TrackInfo ParseTrack(JToken item)
		{
			string id = Extensions.Value<string>((IEnumerable<JToken>)item[(object)"id"]);
			string title = Extensions.Value<string>((IEnumerable<JToken>)item[(object)"name"]);
			IEnumerable<string> values = ((IEnumerable<JToken>)item[(object)"artists"]).Select((JToken a) => Extensions.Value<string>((IEnumerable<JToken>)a[(object)"name"]));
			string artist = string.Join(", ", values);
			string album = Extensions.Value<string>((IEnumerable<JToken>)item[(object)"album"][(object)"name"]);
			int durationMs = Extensions.Value<int>((IEnumerable<JToken>)item[(object)"duration_ms"]);
			JToken obj = item[(object)"album"][(object)"images"];
			object obj2;
			if (obj == null)
			{
				obj2 = null;
			}
			else
			{
				JToken? obj3 = ((IEnumerable<JToken>)obj).FirstOrDefault();
				if (obj3 == null)
				{
					obj2 = null;
				}
				else
				{
					JToken obj4 = obj3[(object)"url"];
					obj2 = ((obj4 != null) ? Extensions.Value<string>((IEnumerable<JToken>)obj4) : null);
				}
			}
			string coverUrl = (string)obj2;
			return new TrackInfo(id, title, artist, album, coverUrl, durationMs);
		}
	}
	public record SpotifyToken(string AccessToken, string RefreshToken, int ExpiresIn, DateTime ObtainedAt)
	{
		public bool IsExpired => DateTime.UtcNow >= ObtainedAt.AddSeconds(ExpiresIn - 30);
	}
	public record TrackInfo(string Id, string Title, string Artist, string Album, string? CoverUrl, int DurationMs, byte[]? CoverBytes = null);
	public record PlaybackState(TrackInfo? Track, bool IsPlaying, int ProgressMs, string? StatusMessage = null)
	{
		public static PlaybackState Empty => new PlaybackState(null, IsPlaying: false, 0);
	}
	public class TokenStorage
	{
		private class StoredToken
		{
			public string AccessToken { get; set; } = "";


			public string RefreshToken { get; set; } = "";


			public int ExpiresIn { get; set; }

			public DateTime ObtainedAt { get; set; }

			public StoredToken()
			{
			}

			public StoredToken(string access, string refresh, int expiresIn, DateTime obtainedAt)
			{
				AccessToken = access;
				RefreshToken = refresh;
				ExpiresIn = expiresIn;
				ObtainedAt = obtainedAt;
			}
		}

		private static class NativeMethods
		{
			[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
			public struct CREDENTIAL
			{
				public uint Flags;

				public uint Type;

				public string TargetName;

				public string? Comment;

				public long LastWritten;

				public uint CredentialBlobSize;

				public IntPtr CredentialBlob;

				public uint Persist;

				public uint AttributeCount;

				public IntPtr Attributes;

				public string? TargetAlias;

				public string? UserName;
			}

			public const uint CRED_TYPE_GENERIC = 1u;

			public const uint CRED_PERSIST_LOCAL_MACHINE = 2u;

			[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern bool CredWrite(ref CREDENTIAL credential, uint flags);

			[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern bool CredRead(string target, uint type, uint flags, out IntPtr credential);

			[DllImport("advapi32.dll")]
			public static extern void CredFree(IntPtr buffer);

			[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
			public static extern bool CredDelete(string target, uint type, uint flags);
		}

		private readonly string CredentialName;

		public TokenStorage(string credentialName = "WKMusic/SpotifyToken")
		{
			CredentialName = credentialName;
			base..ctor();
		}

		public void Save(SpotifyToken token)
		{
			string s = JsonConvert.SerializeObject((object)new StoredToken(token.AccessToken, token.RefreshToken, token.ExpiresIn, token.ObtainedAt));
			byte[] bytes = Encoding.UTF8.GetBytes(s);
			NativeMethods.CREDENTIAL cREDENTIAL = default(NativeMethods.CREDENTIAL);
			cREDENTIAL.Type = 1u;
			cREDENTIAL.TargetName = CredentialName;
			cREDENTIAL.CredentialBlob = Marshal.AllocHGlobal(bytes.Length);
			cREDENTIAL.CredentialBlobSize = (uint)bytes.Length;
			cREDENTIAL.Persist = 2u;
			cREDENTIAL.UserName = "spotify";
			NativeMethods.CREDENTIAL credential = cREDENTIAL;
			Marshal.Copy(bytes, 0, credential.CredentialBlob, bytes.Length);
			try
			{
				if (!NativeMethods.CredWrite(ref credential, 0u))
				{
					throw new Exception($"CredWrite failed: {Marshal.GetLastWin32Error()}");
				}
			}
			finally
			{
				Marshal.FreeHGlobal(credential.CredentialBlob);
			}
		}

		public SpotifyToken? Load()
		{
			if (!NativeMethods.CredRead(CredentialName, 1u, 0u, out var credential))
			{
				return null;
			}
			try
			{
				NativeMethods.CREDENTIAL cREDENTIAL = Marshal.PtrToStructure<NativeMethods.CREDENTIAL>(credential);
				byte[] array = new byte[cREDENTIAL.CredentialBlobSize];
				Marshal.Copy(cREDENTIAL.CredentialBlob, array, 0, array.Length);
				StoredToken storedToken = JsonConvert.DeserializeObject<StoredToken>(Encoding.UTF8.GetString(array));
				if (storedToken == null)
				{
					return null;
				}
				return new SpotifyToken(storedToken.AccessToken, storedToken.RefreshToken, storedToken.ExpiresIn, storedToken.ObtainedAt);
			}
			catch
			{
				Clear();
				return null;
			}
			finally
			{
				NativeMethods.CredFree(credential);
			}
		}

		public void Clear()
		{
			NativeMethods.CredDelete(CredentialName, 1u, 0u);
		}
	}
}