diff --git a/.gitignore b/.gitignore
index 3042c1b..1b7ae6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 *obj/
 *bin/
-*.vs/
\ No newline at end of file
+*.vs/
+
+run-tests.ps1
\ No newline at end of file
diff --git a/CSharp-Music-API.sln b/CSharp-Music-API.sln
deleted file mode 100644
index d1afc74..0000000
--- a/CSharp-Music-API.sln
+++ /dev/null
@@ -1,28 +0,0 @@
-﻿
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicAPI", "MusicAPI\MusicAPI.csproj", "{A35F2761-72E1-43B9-B276-6B3D9F02D770}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicAPI.Tests", "MusicAPI.Tests\MusicAPI.Tests.csproj", "{DC7A2932-4F0A-4C55-9084-D3744FA93245}"
-EndProject
-Global
-	GlobalSection(SolutionConfigurationPlatforms) = preSolution
-		Debug|Any CPU = Debug|Any CPU
-		Release|Any CPU = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(SolutionProperties) = preSolution
-		HideSolutionNode = FALSE
-	EndGlobalSection
-	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{A35F2761-72E1-43B9-B276-6B3D9F02D770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{A35F2761-72E1-43B9-B276-6B3D9F02D770}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{A35F2761-72E1-43B9-B276-6B3D9F02D770}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{A35F2761-72E1-43B9-B276-6B3D9F02D770}.Release|Any CPU.Build.0 = Release|Any CPU
-		{DC7A2932-4F0A-4C55-9084-D3744FA93245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{DC7A2932-4F0A-4C55-9084-D3744FA93245}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{DC7A2932-4F0A-4C55-9084-D3744FA93245}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{DC7A2932-4F0A-4C55-9084-D3744FA93245}.Release|Any CPU.Build.0 = Release|Any CPU
-	EndGlobalSection
-EndGlobal
diff --git a/MusicAPI.Tests/MusicAPI.Tests.csproj b/MusicAPI.Tests/MusicAPI.Tests.csproj
deleted file mode 100644
index cc3e643..0000000
--- a/MusicAPI.Tests/MusicAPI.Tests.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <Nullable>enable</Nullable>
-
-    <IsPackable>false</IsPackable>
-    <IsTestProject>true</IsTestProject>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <PackageReference Include="coverlet.collector" Version="6.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
-    <PackageReference Include="NUnit" Version="3.14.0" />
-    <PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Using Include="NUnit.Framework" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\MusicAPI\MusicAPI.csproj" />
-  </ItemGroup>
-
-</Project>
diff --git a/MusicAPI.Tests/UnitTest1.cs b/MusicAPI.Tests/UnitTest1.cs
deleted file mode 100644
index 31b3fd0..0000000
--- a/MusicAPI.Tests/UnitTest1.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System.Reflection;
-using System;
-
-namespace MusicAPI.Tests;
-
-public class Tests
-{
-	[SetUp]
-	public void Setup()
-	{
-	}
-
-	[Test]
-	public async Task Test()
-	{
-		//声明API实例
-		//var api = new NeteaseAPI();
-		//var api = new TencentAPI();
-		var api = new SpotifyAPI("", "");
-
-		//需要增加（修改）的Headers
-		//api.Headers = new Dictionary<string, string> { { "Addition", "12345" } /*, { "Cookie", "Yours" }*/ };
-
-		//搜索音乐
-		var search = await api.Search("secret base～君がくれたもの～", limit: 30);
-		var song = search![0]!;
-		Console.WriteLine(song);
-
-		//获取音乐信息
-		var songInfo = await api.GetSong(song!.Id);
-		Console.WriteLine(songInfo);
-
-		////获取音乐资源
-		//var songResource = await api.GetSongResource(song!.Id);
-		//Console.WriteLine(songResource);
-
-		////获取音乐歌词
-		//var songLyric = await api.GetLyric(song!.Id);
-		//Console.WriteLine(songLyric);
-
-		////获取音乐头图
-		//var songPicture = await api.GetPicture(song!.Id, 300);
-		//Console.WriteLine(songPicture);
-
-		return;
-	}
-}
\ No newline at end of file
diff --git a/MusicAPI/MusicAPI.csproj b/MusicAPI/MusicAPI.csproj
deleted file mode 100644
index 3b5b170..0000000
--- a/MusicAPI/MusicAPI.csproj
+++ /dev/null
@@ -1,13 +0,0 @@
-﻿<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <Nullable>enable</Nullable>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
-  </ItemGroup>
-
-</Project>
diff --git a/MusicAPI/MusicAPI.sln b/MusicAPI/MusicAPI.sln
deleted file mode 100644
index 2fc400e..0000000
--- a/MusicAPI/MusicAPI.sln
+++ /dev/null
@@ -1,25 +0,0 @@
-﻿
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.11.35222.181
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicAPI", "MusicAPI.csproj", "{E29CB4E9-9F0B-4254-8550-122D3E458E4E}"
-EndProject
-Global
-	GlobalSection(SolutionConfigurationPlatforms) = preSolution
-		Debug|Any CPU = Debug|Any CPU
-		Release|Any CPU = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{E29CB4E9-9F0B-4254-8550-122D3E458E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{E29CB4E9-9F0B-4254-8550-122D3E458E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{E29CB4E9-9F0B-4254-8550-122D3E458E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{E29CB4E9-9F0B-4254-8550-122D3E458E4E}.Release|Any CPU.Build.0 = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(SolutionProperties) = preSolution
-		HideSolutionNode = FALSE
-	EndGlobalSection
-	GlobalSection(ExtensibilityGlobals) = postSolution
-		SolutionGuid = {3F1E8A83-A476-4824-925E-C3DC2C651188}
-	EndGlobalSection
-EndGlobal
diff --git a/MusicAPI/src/HttpManager.cs b/MusicAPI/src/HttpManager.cs
deleted file mode 100644
index 6d34d2f..0000000
--- a/MusicAPI/src/HttpManager.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-﻿using Newtonsoft.Json;
-using System.Net;
-using System.Numerics;
-using System.Security.Cryptography;
-using System.Text;
-using System.Web;
-
-namespace MusicAPI;
-
-public static class HttpManager
-{
-	public static async Task<string?> RequestAsync(this HttpClient client, string method, string? url, Dictionary<string, string>? parameters, Dictionary<string, string> headers)
-	{
-		string responseBody = string.Empty;
-		if (string.IsNullOrWhiteSpace(url))
-			return string.Empty;
-		
-
-		switch (method) {
-			case "POST": {
-				if (parameters == null || parameters.Count == 0)
-					return string.Empty;
-				using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url)) {
-					request.Content = new FormUrlEncodedContent((IEnumerable<KeyValuePair<string, string>>)parameters);
-					foreach (var header in headers) {
-						//Console.WriteLine($"{header.Key}: {header.Value}");
-						request.Headers.TryAddWithoutValidation(header.Key, header.Value);
-					}
-					HttpResponseMessage response = await client.SendAsync(request);
-					responseBody = await response.Content.ReadAsStringAsync();
-				}
-				break;
-			}
-
-			case "GET": {
-				var urlWithQuery = url;
-				if (parameters != null && parameters.Count > 0) {
-					var queryString = HttpUtility.ParseQueryString(string.Empty);
-					foreach (var parameter in parameters) {
-						queryString[parameter.Key] = parameter.Value;
-					}
-					urlWithQuery = $"{urlWithQuery}?{queryString.ToString()}";
-				}
-				
-				//Console.WriteLine(urlWithQuery);
-
-				using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlWithQuery)) {
-					foreach (var header in headers) {
-						//Console.WriteLine($"{header.Key}: {header.Value}");
-						request.Headers.TryAddWithoutValidation(header.Key, header.Value);
-					}
-					HttpResponseMessage response = await client.SendAsync(request);
-					responseBody = await response.Content.ReadAsStringAsync();
-				}
-				break;
-			}
-		}
-
-		return responseBody;
-	}
-
-
-	public static string GetRandomIP()
-	{
-		Random random = new Random();
-		int ip = random.Next(1884815360, 1884890111);
-		return new IPAddress(ip).ToString();
-	}
-}
\ No newline at end of file
diff --git a/MusicAPI/src/Models/Universal.cs b/MusicAPI/src/Models/Universal.cs
deleted file mode 100644
index 21490a1..0000000
--- a/MusicAPI/src/Models/Universal.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Newtonsoft.Json;
-
-namespace MusicAPI;
-
-
-public record Song
-{
-  public required string    Id { get; set; }
-  public required string    Name { get; set; }
-  public required List<string> Artists { get; set; }
-  public required string    AlbumName { get; set; }
-  public required string    LyricId { get; set; }
-  public string?            Picture { get; set; } = string.Empty;
-}
-
-public record SongResource
-{
-  public required string    Url { get; set; }
-  public required string    Type { get; set; }
-  public required int       Br { get; set; }
-  public double             Size { get; set; }
-  public double             Duration { get; set; }
-}
-
-public record Artist
-{
-  public required string    Name { get; set; }
-  public string?            Id { get; set; }
-}
-
-public record Album
-{
-  public required string    Name { get; set; }
-  public required string    Pic { get; set; }
-}
-
-public record Lyric
-{
-  public required bool      HasTimeInfo { get; set; }
-  public required string    OriginalLyric { get; set; }
-  public string?            TranslatedLyric { get; set; }
-}
\ No newline at end of file
diff --git a/MusicAPI/src/NeteaseAPI.cs b/MusicAPI/src/NeteaseAPI.cs
deleted file mode 100644
index 65fe266..0000000
--- a/MusicAPI/src/NeteaseAPI.cs
+++ /dev/null
@@ -1,367 +0,0 @@
-﻿using Newtonsoft.Json;
-using System.Numerics;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace MusicAPI;
-public class NeteaseEncryptedResult
-{
-	public required string url { get; set; }
-	public required Dictionary<string, string> body { get; set; }
-}
-
-public class NeteaseAPI
-{
-	~NeteaseAPI()
-	{
-		if (HttpClient != null)
-			HttpClient.Dispose();
-	}
-	private HttpClient HttpClient = new();
-
-
-	public Dictionary<string, string> Headers = new();
-
-	private Dictionary<string, string> GetHeaders()
-	{
-		//string cookieString = string.Join("; ", Cookies.Select(cookie => $"{cookie.Key}={cookie.Value}"));
-		Dictionary<string, string> keyValues = new Dictionary<string, string> {
-						{ "Referer", "https://music.163.com/" },
-						{ "Cookie", "appver=8.2.30; os=iPhone OS; osver=15.0; EVNSM=1.0.0; buildver=2206; channel=distribution; machineid=iPhone13.3" },
-						{ "User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CloudMusic/0.1.1 NeteaseMusic/8.2.30" },
-						{ "X-Real-IP", HttpManager.GetRandomIP() },
-						{ "Accept", "*/*" },
-						{ "Accept-Language", "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4" },
-						{ "Connection", "keep-alive" },
-						{ "Content-Type", "application/x-www-form-urlencoded" }
-					};
-
-		foreach (var header in Headers)
-		{
-			keyValues.TryAdd(header.Key, header.Value);
-			if (keyValues.ContainsKey(header.Key))
-				keyValues[header.Key] = header.Value;
-			//Console.WriteLine($"{keyValues[header.Key]}: {keyValues[header.Value]}");
-		}
-		return keyValues;
-	}
-
-	public async Task<List<Song>?> Search(string keyword, int type = 1, int limit = 30, int page = 1)
-	{
-		Dictionary<string, object> apiRequest = new Dictionary<string, object> {
-				{ "url", "http://music.163.com/api/cloudsearch/pc" },
-				{ "body", new Dictionary<string, object> {
-						{ "s", string.Concat(keyword.Select(c => $@"\u{(int)c:X4}")).ToLower() },
-						{ "type", type },
-						{ "limit", limit },
-						{ "total", "true" },
-						{ "offset", (page > 0 && limit > 0) ? (page - 1) * limit : 0 }
-					}
-				}
-			};
-		NeteaseEncryptedResult encryptedRequest = NeteaseCrypto.NetEaseAESCBC(apiRequest);
-
-
-		string? jsonResult = await HttpClient.RequestAsync("POST", encryptedRequest.url, encryptedRequest.body, GetHeaders());
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		List<Song> searchResult = new();
-		foreach (dynamic? jsonData in json!.result!.songs)
-		{
-			if (jsonData == null)
-				continue;
-			var artists = new List<string>();
-
-			foreach (dynamic? artist in jsonData!.ar)
-			{
-				artists.Add((string)artist.name);
-			}
-			Song singleResult = new Song
-			{
-				Id = jsonData!.id,
-				Name = jsonData!.name,
-				Artists = artists,
-				AlbumName = jsonData!.al!.name,
-				LyricId = jsonData!.id
-			};
-			searchResult.Add(singleResult);
-		}
-		;
-
-		// singleResult["id"] = jsonData!.id;
-		// singleResult["name"] = jsonData!.name;
-		// singleResult["artist"] = artistResult;
-		// singleResult["album"] = jsonData!.al!.name;
-		// singleResult["pic_id"] = jsonData!.al!.pic;
-		// singleResult["url_id"] = jsonData!.id;
-		// singleResult["lrc_id"] = jsonData!.id;
-
-		return searchResult;
-	}
-
-	public async Task<Song?> GetSong(string id)
-	{
-		Dictionary<string, object> apiRequest = new Dictionary<string, object> {
-				{ "url", "http://music.163.com/api/v3/song/detail/" },
-				{ "body", new Dictionary<string, object> {
-						{ "c", $"[{{\"id\":'{id}',\"v\":0}}]" }
-					}
-				}
-			};
-		NeteaseEncryptedResult encryptedRequest = NeteaseCrypto.NetEaseAESCBC(apiRequest);
-
-		string? jsonResult = await HttpClient.RequestAsync("POST", encryptedRequest.url, encryptedRequest.body, GetHeaders());
-		//Console.WriteLine(jsonResult);
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		foreach (dynamic? jsonData in json!.songs)
-		{
-			if (jsonData == null)
-				continue;
-
-			List<string> artists = new List<string>();
-
-			foreach (dynamic? artist in jsonData!.ar) {
-				if (artist.name == null)
-					continue;
-				artists.Add((string)artist.name ?? "");
-			}
-			
-			string? url = jsonData!.al!.picUrl;
-			return new Song {
-				Id = jsonData!.id,
-				Name = jsonData!.name,
-				Artists = artists,
-				AlbumName = jsonData!.al!.name,
-				LyricId = jsonData!.id,
-				Picture = string.IsNullOrWhiteSpace(url) ? string.Empty : url
-			};
-		}
-		return null;
-	}
-
-	public async Task<SongResource?> GetSongResource(string id, int br = 320)
-	{
-		Dictionary<string, object> apiRequest = new Dictionary<string, object> {
-				{ "url", "http://music.163.com/api/song/enhance/player/url" },
-				{ "body", new Dictionary<string, object> {
-						{ "ids", $"[{id}]" },
-						{ "br", br * 1000 }
-					}
-				}
-			};
-		NeteaseEncryptedResult encryptedRequest = NeteaseCrypto.NetEaseAESCBC(apiRequest);
-
-		string? jsonResult = await HttpClient.RequestAsync("POST", encryptedRequest.url, encryptedRequest.body, GetHeaders());
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		var data = json!.data[0];
-		return new SongResource
-		{
-			Url = data!.url,
-			Type = data!.type,
-			Br = (int)(data!.br / 1000.0),
-			Size = data!.size,
-			Duration = (double)(data!.time / 1000.0)
-		};
-
-	}
-
-	public async Task<Lyric?> GetLyric(string id)
-	{
-		Dictionary<string, object> apiRequest = new Dictionary<string, object> {
-				{ "url", "http://music.163.com/api/song/lyric" },
-				{ "body", new Dictionary<string, object> {
-						{ "id", id },
-						{ "os", "linux" },
-						{ "lv", -1 },
-						{ "kv", -1 },
-						{ "tv", -1 }
-					}
-				}
-			};
-		NeteaseEncryptedResult encryptedRequest = NeteaseCrypto.NetEaseAESCBC(apiRequest);
-
-		string? jsonResult = await HttpClient.RequestAsync("POST", encryptedRequest.url, encryptedRequest.body, GetHeaders());
-		//Console.WriteLine(jsonResult);
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-		Regex regex = new Regex(@"\[(\d+:\d+.*)\]", RegexOptions.Compiled);
-		return new Lyric
-		{
-			HasTimeInfo = regex.IsMatch(json!.lrc!.lyric!.ToString()),
-			OriginalLyric = json!.lrc!.lyric!.ToString(),
-			TranslatedLyric = json!.tlyric?.lyric?.ToString() ?? string.Empty
-		};
-	}
-
-	public async Task<string?> GetPicture(string id, int px = 400) {
-		Song? songInfo = await GetSong(id);
-		if (songInfo == null)
-			return string.Empty;
-		string? url = songInfo.Picture;
-		if (string.IsNullOrWhiteSpace(url))
-			return string.Empty;
-
-		return $"{url}?param={px}y{px}";
-	}
-}
-
-
-public static class NeteaseCrypto
-{
-	private static readonly string Modulus = "157794750267131502212476817800345498121872783333389747424011531025366277535262539913701806290766479189477533597854989606803194253978660329941980786072432806427833685472618792592200595694346872951301770580765135349259590167490536138082469680638514416594216629258349130257685001248172188325316586707301643237607";
-	private static readonly string PubKey = "65537";
-	private static readonly string Nonce = "0CoJUm6Qyw8W8jud";
-	private static readonly string IV = "0102030405060708";
-
-	public static NeteaseEncryptedResult NetEaseAESCBC(Dictionary<string, object> api)
-	{
-		string skey = GetRandomHex(16) ?? "B3v3kH4vRPWRJFfH";
-
-		string body = JsonConvert.SerializeObject(api["body"]);
-		body = EncryptAES(body.Replace("\\\\", "\\"), Nonce, IV);
-		body = EncryptAES(body, skey, IV);
-
-		string encryptedSkey = SecretKey(skey, PubKey, Modulus);
-
-		NeteaseEncryptedResult encryptedResult = new NeteaseEncryptedResult
-		{
-			url = api["url"].ToString()!.Replace("/api/", "/weapi/"),
-			body = new Dictionary<string, string> {
-				{ "params", body },
-				{ "encSecKey", encryptedSkey.ToLower() }
-			}
-		};
-
-		return encryptedResult;
-	}
-
-	private static string GetRandomHex(int length)
-	{
-		using (var rng = RandomNumberGenerator.Create())
-		{
-			byte[] bytes = new byte[length / 2];
-			rng.GetBytes(bytes);
-			return BitConverter.ToString(bytes).Replace("-", "").ToLower();
-		}
-	}
-
-	private static string EncryptAES(string plainText, string key, string iv)
-	{
-		byte[] keyBytes = Encoding.UTF8.GetBytes(key);
-		byte[] ivBytes = Encoding.UTF8.GetBytes(iv);
-
-		if (keyBytes.Length != 16)
-			throw new ArgumentException("AES-128-CBC的KEY必须为16字节长");
-		if (ivBytes.Length != 16)
-			throw new ArgumentException("AES-128-CBC的IV必须为16字节长");
-
-		using (Aes aes = Aes.Create())
-		{
-			aes.Key = keyBytes;
-			aes.IV = ivBytes;
-			aes.Mode = CipherMode.CBC;
-			aes.Padding = PaddingMode.PKCS7;
-
-			ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
-			byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
-			using (MemoryStream ms = new MemoryStream())
-			{
-				using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
-				{
-					cs.Write(plainBytes, 0, plainBytes.Length);
-					cs.FlushFinalBlock();
-				}
-				byte[] encryptedBytes = ms.ToArray();
-				return Convert.ToBase64String(encryptedBytes);
-			}
-		}
-	}
-
-	private static string SecretKey(string skey, string pubkey, string modulus)
-	{
-		skey = ReverseString(skey);
-		skey = bchexdec(str2hex(skey));
-		skey = bcpowmod(skey, pubkey, modulus);
-
-		BigInteger bigIntValue = BigInteger.Parse(skey);
-		skey = bigIntValue.ToString("X").ToLower();
-		if (skey.Length == 257)
-			skey = skey.Substring(1);
-
-		skey = skey.PadLeft(256, '0');
-		return skey;
-	}
-
-	private static string ReverseString(string input)
-	{
-		char[] charArray = input.ToCharArray();
-		Array.Reverse(charArray);
-		return new string(charArray);
-	}
-
-	private static string bchexdec(string hex)
-	{
-		BigInteger result = 0;
-		int length = hex.Length;
-		for (int i = 0; i < length; i++)
-		{
-			char hexChar = hex[i];
-			BigInteger digitValue = HexCharToValue(hexChar);
-			BigInteger power = BigInteger.Pow(16, length - i - 1);
-			result += digitValue * power;
-		}
-
-		return result.ToString();
-	}
-
-	private static string str2hex(string input)
-	{
-		StringBuilder hexBuilder = new StringBuilder();
-		foreach (char c in input)
-		{
-			string hexCode = ((int)c).ToString("X2");
-			hexBuilder.Append(hexCode);
-		}
-		return hexBuilder.ToString();
-	}
-
-	private static string bcpowmod(string input, string pubkey, string modulus)
-	{
-		BigInteger skeyBigInt = BigInteger.Parse(input);
-		BigInteger pubkeyBigInt = BigInteger.Parse(pubkey);
-		BigInteger modulusBigInt = BigInteger.Parse(modulus);
-		BigInteger result = BigInteger.ModPow(skeyBigInt, pubkeyBigInt, modulusBigInt);
-
-		return result.ToString();
-	}
-
-	private static BigInteger HexCharToValue(char hexChar)
-	{
-		if (hexChar >= '0' && hexChar <= '9')
-		{
-			return hexChar - '0';
-		}
-		else if (hexChar >= 'a' && hexChar <= 'f')
-		{
-			return 10 + hexChar - 'a';
-		}
-		else if (hexChar >= 'A' && hexChar <= 'F')
-		{
-			return 10 + hexChar - 'A';
-		}
-		else
-		{
-			throw new ArgumentException($"无效HEX字符: {hexChar}");
-		}
-	}
-}
\ No newline at end of file
diff --git a/MusicAPI/src/SpotifyAPI.cs b/MusicAPI/src/SpotifyAPI.cs
deleted file mode 100644
index 8e1be3d..0000000
--- a/MusicAPI/src/SpotifyAPI.cs
+++ /dev/null
@@ -1,152 +0,0 @@
-﻿using Newtonsoft.Json;
-
-namespace MusicAPI;
-public class SpotifyAPI {
-	//https://developer.spotify.com/dashboard
-	public SpotifyAPI(string ClientID, string ClientSecret) {
-		this.ClientID = ClientID;
-		this.ClientSecret = ClientSecret;
-	}
-
-	~SpotifyAPI() {
-		if (HttpClient != null)
-			HttpClient.Dispose();
-	}
-	private HttpClient HttpClient = new();
-	private string ClientID, ClientSecret;
-	private string? Token = string.Empty;
-	private long expiredTime = -1;
-
-	public Dictionary<string, string> Headers = new();
-
-	private Dictionary<string, string> GetHeaders() {
-		Dictionary<string, string> keyValues = new Dictionary<string, string> {
-						{ "Referer", "https://developer.spotify.com" },
-						{ "User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36" },
-						{ "Accept", "*/*" },
-						{ "Accept-Language", "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4" },
-						{ "Connection", "keep-alive" },
-						{ "Content-Type", "application/x-www-form-urlencoded" },
-						{ "Authorization", this.Token ?? "" }
-					};
-
-		return keyValues;
-	}
-
-	public async Task<List<Song>?> Search(string keyword, int type = 1, int limit = 30, int page = 1) {
-		if (!(await this.GetToken()))
-			return null;
-
-		Dictionary<string, string> requestParams = new Dictionary<string, string> {
-				{ "q", keyword },
-				{ "type", "track" },
-				{ "limit", $"{limit}" }
-		};
-
-		string? jsonResult = await HttpClient.RequestAsync("GET", "https://api.spotify.com/v1/search", requestParams, GetHeaders());
-
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		//Console.WriteLine("1");
-		List<Song> searchResult = new();
-		foreach (dynamic? jsonData in json!.tracks!.items) {
-			if (jsonData == null)
-				continue;
-			var artists = new List<string>();
-
-			foreach (dynamic? artist in jsonData!.artists) {
-				if (artist == null)
-					continue;
-				artists.Add((string)artist.name);
-			}
-
-			Song singleResult = new Song
-			{
-				Id = jsonData!.id,
-				Name = jsonData!.name,
-				Artists = artists,
-				AlbumName = jsonData!.album!.name,
-				LyricId = jsonData!.id
-			};
-			searchResult.Add(singleResult);
-		};
-
-		return searchResult;
-	}
-
-	public async Task<Song?> GetSong(string id) {
-		if (!(await this.GetToken()))
-			return null;
-
-		//Console.WriteLine(id);
-		Dictionary<string, string> requestParams = new();
-		string? jsonResult = await HttpClient.RequestAsync("GET", $"https://api.spotify.com/v1/tracks/{id}", requestParams, GetHeaders());
-
-		//Console.WriteLine(jsonResult);
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		Dictionary<string, object> albumImage = new();
-		List<Dictionary<string, object>> albumImages = new();
-		List<string> artists = new();
-
-		foreach (dynamic? artist in json!.artists) {
-			if (artist.name == null)
-				continue;
-			artists.Add((string)artist.name ?? "");
-		}
-
-		foreach (dynamic? image in json!.album!.images) {
-			if (image?.url == null)
-				continue;
-			albumImage = new();
-			albumImage.Add("url", (string)image!.url!);
-			albumImage.Add("height", (int)image!.height!);
-			albumImage.Add("width", (int)image!.width!);
-			albumImages.Add(albumImage);
-		}
-
-		string? pid = (albumImages.Count > 0) ? JsonConvert.SerializeObject(albumImages) : string.Empty;
-		return new Song {
-			Id = json!.id,
-			Name = json!.name,
-			Artists = artists,
-			AlbumName = json!.album!.name,
-			LyricId = json!.id,
-			Picture = string.IsNullOrWhiteSpace(pid) ? string.Empty : pid
-		};
-	}
-
-	private async Task<bool> GetToken() {
-		long timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds();
-		if (expiredTime > timeStamp + 1)
-			return true;
-
-		Dictionary<string, string> requestParams = new Dictionary<string, string> {
-				{ "grant_type", "client_credentials" },
-				{ "client_id", this.ClientID },
-				{ "client_secret", this.ClientSecret }
-		};
-
-		string? result = await HttpClient.RequestAsync("POST", "https://accounts.spotify.com/api/token", requestParams, GetHeaders());
-		if (string.IsNullOrWhiteSpace(result))
-			return false;
-
-		dynamic? json = JsonConvert.DeserializeObject(result ?? "{}");
-		if (json == null)
-			return false;
-
-		string? access_token = json?.access_token ?? string.Empty, token_type = json?.token_type ?? string.Empty;
-		int expires_in = json?.expires_in ?? -1;
-		if (expires_in <= 0 || string.IsNullOrWhiteSpace(access_token) || string.IsNullOrWhiteSpace(token_type))
-			return false;
-
-		expiredTime = timeStamp + expires_in;
-		//Bearer 1POdFZRZbvb...qqillRxMr2z'
-		this.Token = $"{token_type} {access_token}";
-		return true;
-	}
-}
\ No newline at end of file
diff --git a/MusicAPI/src/TencentAPI.cs b/MusicAPI/src/TencentAPI.cs
deleted file mode 100644
index 59b858c..0000000
--- a/MusicAPI/src/TencentAPI.cs
+++ /dev/null
@@ -1,303 +0,0 @@
-﻿using Newtonsoft.Json;
-using System.Text.RegularExpressions;
-
-namespace MusicAPI;
-public class TencentAPI {
-	~TencentAPI() {
-		if (HttpClient != null)
-			HttpClient.Dispose();
-	}
-	private HttpClient HttpClient = new();
-
-
-	public Dictionary<string, string> Headers = new();
-
-	private Dictionary<string, string> GetHeaders() {
-		Dictionary<string, string> keyValues = new Dictionary<string, string> {
-						{ "Referer", "http://y.qq.com" },
-						{ "Cookie", "pgv_pvi=22038528; pgv_si=s3156287488; pgv_pvid=5535248600; yplayer_open=1; ts_last=y.qq.com/portal/player.html; ts_uid=4847550686; yq_index=0; qqmusic_fromtag=66; player_exist=1" },
-						{ "User-Agent", "QQ%E9%9F%B3%E4%B9%90/54409 CFNetwork/901.1 Darwin/17.6.0 (x86_64)" },
-						{ "Accept", "*/*" },
-						{ "Accept-Language", "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4" },
-						{ "Connection", "keep-alive" },
-						{ "Content-Type", "application/x-www-form-urlencoded" }
-					};
-
-		foreach (var header in Headers) {
-			keyValues.TryAdd(header.Key, header.Value);
-			if (keyValues.ContainsKey(header.Key))
-				keyValues[header.Key] = header.Value;
-			//Console.WriteLine($"{keyValues[header.Key]}: {keyValues[header.Value]}");
-		}
-		return keyValues;
-	}
-
-	public async Task<List<Song>?> Search(string keyword, int type = 1, int limit = 30, int page = 1) {
-		Dictionary<string, string> requestParams = new Dictionary<string, string> {
-						{ "w", keyword },
-						{ "p", page.ToString() },
-						{ "n", limit.ToString() },
-						{ "aggr", "1" },
-						{ "lossless", "1" },
-						{ "cr", "1" },
-						{ "new_json", "1" },
-						{ "format", "json" }
-		};
-
-		string? jsonResult = await HttpClient.RequestAsync("GET", "https://c.y.qq.com/soso/fcgi-bin/client_search_cp", requestParams, GetHeaders());
-
-		//Console.WriteLine(jsonResult);
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		//Console.WriteLine("1");
-		List<Song> searchResult = new();
-		foreach (dynamic? jsonData in json!.data!.song!.list) {
-			if (jsonData == null)
-				continue;
-			var artists = new List<string>();
-
-			foreach (dynamic? artist in jsonData!.singer) {
-				if (artist == null)
-					continue;
-				artists.Add((string)artist.name);
-			}
-
-			Song singleResult = new Song
-			{
-				Id = jsonData!.mid,
-				Name = jsonData!.name,
-				Artists = artists,
-				AlbumName = jsonData!.album!.title,
-				LyricId = jsonData!.mid
-			};
-			searchResult.Add(singleResult);
-		};
-
-		return searchResult;
-
-	}
-
-	public async Task<Song?> GetSong(string id) {
-		Dictionary<string, string> requestParams = new Dictionary<string, string> {
-				{ "format", "json" },
-				{ "platform", "yqq" },
-				{ "songmid", id }
-		};
-
-		string? jsonResult = await HttpClient.RequestAsync("GET", "https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg", requestParams, GetHeaders());
-
-		//Console.WriteLine(jsonResult);
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		foreach (dynamic? jsonData in json!.data) {
-			if (jsonData == null)
-				continue;
-
-			List<string> artists = new List<string>();
-
-			foreach (dynamic? artist in jsonData!.singer) {
-				if (artist.name == null)
-					continue;
-				artists.Add((string)artist.name ?? "");
-			}
-
-			string? pid = jsonData!.album!.pmid;
-			return new Song {
-				Id = jsonData!.mid,
-				Name = jsonData!.name,
-				Artists = artists,
-				AlbumName = jsonData!.album!.title,
-				LyricId = jsonData!.mid,
-				Picture = string.IsNullOrWhiteSpace(pid) ? string.Empty : pid
-			};
-		}
-
-		return null;
-	}
-
-	public async Task<SongResource?> GetSongResource(string id, int br = 320) {
-		Dictionary<string, string> requestParams = new Dictionary<string, string> {
-				{ "format", "json" },
-				{ "platform", "yqq" },
-				{ "songmid", id }
-		};
-
-		string? jsonResult = await HttpClient.RequestAsync("GET", "https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg", requestParams, GetHeaders());
-		//Console.WriteLine(jsonResult);
-		dynamic? json = JsonConvert.DeserializeObject(jsonResult ?? "{}");
-		if (string.IsNullOrWhiteSpace(jsonResult) || json == null)
-			return null;
-
-		Dictionary<string, object>? songResource = await TencentUrl(jsonResult!, br);
-		if (songResource == null)
-			return null;
-
-		return new SongResource {
-			Size = (double)((int)songResource["size"] / 1024.0 / 1024.0),
-			Br = br,
-			Duration = 0.0,
-			Type = string.IsNullOrWhiteSpace((string)songResource["fileName"]) ? string.Empty : (
-				((string)songResource["fileName"]).EndsWith("mp3", StringComparison.OrdinalIgnoreCase) ? "mp3" : (
-					((string)songResource["fileName"]).EndsWith("m4a", StringComparison.OrdinalIgnoreCase) ? "m4a" : (
-						((string)songResource["fileName"]).EndsWith("flac", StringComparison.OrdinalIgnoreCase) ? "flac" : string.Empty
-					)
-				)
-			),
-			Url = string.IsNullOrWhiteSpace((string)songResource["url"]) ? string.Empty : (string)songResource["url"]
-		};
-	}
-
-	public async Task<Lyric?> GetLyric(string id) {
-		Dictionary<string, string> requestParams = new Dictionary<string, string> {
-				{ "songmid", id },
-				{ "g_tk", "5381" }
-		};
-
-		string? jsonResult = await HttpClient.RequestAsync("GET", "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg", requestParams, GetHeaders());
-		//Console.WriteLine(jsonResult);
-		if (string.IsNullOrWhiteSpace(jsonResult))
-			return null;
-
-		Match match = Regex.Match(jsonResult!, @"MusicJsonCallback\((.*?)\)");
-		if (!match.Success)
-			return null;
-
-		string? content = match.Groups[1].Value;
-		dynamic? json = JsonConvert.DeserializeObject(content ?? "{}");
-		if (json == null)
-			return null;
-
-		string? originalLyric = json!.lyric!.ToString(), translatedLyric = json!.trans?.ToString();
-		if (string.IsNullOrWhiteSpace(originalLyric))
-			return null;
-
-		originalLyric = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(originalLyric));
-
-		Regex regex = new Regex(@"\[(\d+:\d+.*)\]", RegexOptions.Compiled);
-		return new Lyric {
-			HasTimeInfo = regex.IsMatch(originalLyric),
-			OriginalLyric = originalLyric,
-			TranslatedLyric = String.IsNullOrWhiteSpace(translatedLyric) ? string.Empty : System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(translatedLyric))
-		};
-	}
-
-	public async Task<string?> GetPicture(string id, int px = 300) {
-		Song? songInfo = await GetSong(id);
-		if (songInfo == null)
-			return string.Empty;
-		string? pid = songInfo.Picture;
-		if (string.IsNullOrWhiteSpace(pid))
-			return string.Empty;
-
-		return $"https://y.qq.com/music/photo_new/T002R{FindClosestValue(px)}x{FindClosestValue(px)}M000{pid}.jpg?max_age=2592000";
-	}
-
-	private static int FindClosestValue(int target) {
-		int[] array = { 58, 68, 300, 500 };
-		int left = 0;
-		int right = array.Length - 1;
-		int closestValue = array[0];
-
-		while (left <= right) {
-			int mid = left + (right - left) / 2;
-
-			if (array[mid] == target)
-				return array[mid];
-
-			if (Math.Abs(array[mid] - target) < Math.Abs(closestValue - target))
-				closestValue = array[mid];
-
-			if (array[mid] < target)
-				left = mid + 1;
-			else
-				right = mid - 1;
-		}
-
-		return closestValue;
-	}
-
-	private async Task<Dictionary<string, object>?> TencentUrl(string result, int br) {
-		var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(result);
-		var random = new Random();
-		var guid = random.Next(0, 100000) * 1000000L + random.Next(0, 100000);
-
-		var types = new List<Tuple<string, int, string, string>>(){
-			new Tuple<string, int, string, string>("size_flac", 999, "F000", "flac"),
-			new Tuple<string, int, string, string>("size_320mp3", 320, "M800", "mp3"),
-			new Tuple<string, int, string, string>("size_192aac", 192, "C600", "m4a"),
-			new Tuple<string, int, string, string>("size_128mp3", 128, "M500", "mp3"),
-			new Tuple<string, int, string, string>("size_96aac", 96, "C400", "m4a"),
-			new Tuple<string, int, string, string>("size_48aac", 48, "C200", "m4a"),
-			new Tuple<string, int, string, string>("size_24aac", 24, "C100", "m4a")
-		};
-
-		var match = Regex.Match(GetHeaders()["Cookie"], @"uin=(\d+)");
-		string uin = match.Success ? match.Groups[1].Value : "0";
-
-		var songData = (data!["data"] as Newtonsoft.Json.Linq.JArray)?[0];
-		if (songData == null)
-			return null;
-
-		var songmid = songData["mid"]!.ToString();
-		var songtype = songData["type"]!.ToString();
-		var mediaMid = songData["file"]!["media_mid"]!.ToString();
-
-
-		var payload = new {
-			req_0 = new {
-				module = "vkey.GetVkeyServer",
-				method = "CgiGetVkey",
-				param = new {
-					guid = guid.ToString(),
-					songmid = new List<string> { songmid },
-					filename = types.Select(type => type.Item3 + mediaMid + "." + type.Item4).ToList(),
-					songtype = new List<int> { int.Parse(songtype) },
-					uin = uin,
-					loginflag = 1,
-					platform = "20"
-				}
-			}
-		};
-
-		var requestParams = new Dictionary<string, string> {
-			{ "format", "json" },
-			{ "platform", "yqq.json" },
-			{ "needNewCode", "0" },
-			{ "data", JsonConvert.SerializeObject(payload) }
-		};
-
-		string? responseContent = await HttpClient.RequestAsync("GET", "https://u.y.qq.com/cgi-bin/musicu.fcg", requestParams, GetHeaders());
-		if (string.IsNullOrWhiteSpace(responseContent))
-			return null;
-
-		var jsonResponse = JsonConvert.DeserializeObject<Dictionary<string, object>>(responseContent);
-		var vkeys = jsonResponse?["req_0"] as Newtonsoft.Json.Linq.JObject;
-		var midurlinfo = vkeys?["data"]?["midurlinfo"] as Newtonsoft.Json.Linq.JArray;
-
-		if (midurlinfo == null)
-			return null;
-
-		Dictionary<string, object> songResource = new Dictionary<string, object> {
-			{ "url", string.Empty },
-			{ "fileName", string.Empty },
-			{ "size", 0 }
-		};
-
-
-		foreach (var (type, index) in types.Select((value, index) => (value, index))) {
-			var vkey = midurlinfo[index]?["vkey"]?.ToString();
-			if (!string.IsNullOrEmpty(vkey) && int.Parse(songData["file"]![type.Item1]!.ToString()) <= br) {
-				songResource["url"] = vkeys!["data"]!["sip"]![0]!.ToString() + midurlinfo[index]!["purl"]!.ToString();
-				songResource["fileName"] = midurlinfo[index]!["filename"]!.ToString();
-				songResource["size"] = songData["file"]![type.Item1]!.ToString();
-				break;
-			}
-		}
-
-		return songResource;
-	}
-}
\ No newline at end of file
diff --git a/NET-MusicAPI.Tests/NET-MusicAPI.Tests.csproj b/NET-MusicAPI.Tests/NET-MusicAPI.Tests.csproj
new file mode 100644
index 0000000..5404280
--- /dev/null
+++ b/NET-MusicAPI.Tests/NET-MusicAPI.Tests.csproj
@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net10.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <LangVersion>preview</LangVersion>
+
+    <IsPackable>false</IsPackable>
+    <IsTestProject>true</IsTestProject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="coverlet.collector" Version="6.0.4">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
+    <PackageReference Include="NUnit" Version="4.4.0" />
+    <PackageReference Include="NUnit.Analyzers" Version="4.11.2">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Using Include="NUnit.Framework" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\NET-MusicAPI\NET-MusicAPI.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/NET-MusicAPI.Tests/src/NeteaseApiTests.cs b/NET-MusicAPI.Tests/src/NeteaseApiTests.cs
new file mode 100644
index 0000000..01752f5
--- /dev/null
+++ b/NET-MusicAPI.Tests/src/NeteaseApiTests.cs
@@ -0,0 +1,152 @@
+using Microsoft.Extensions.DependencyInjection;
+using MusicAPI.Extensions;
+using MusicAPI.Abstractions;
+
+namespace MusicAPI.Tests;
+
+[TestFixture]
+public sealed class NeteaseApiTests
+{
+    private ServiceProvider? serviceProvider;
+    private IMusicApi? musicApi;
+
+    private const string TestKeyword = "星茶会";
+    private const int TestSearchLimit = 5;
+    private const int TestBitrate = 320;
+    private const int TestPictureSize = 300;
+
+    [SetUp]
+    public void Setup()
+    {
+        serviceProvider = new ServiceCollection().AddNeteaseApi().BuildServiceProvider();
+        musicApi = serviceProvider.GetRequiredService<IMusicApi>();
+    }
+
+    [TearDown]
+    public void TearDown()
+    {
+        serviceProvider?.Dispose();
+    }
+
+    [Test]
+    public async Task SearchAsyncWithValidKeywordShouldReturnResults()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(SearchAsyncWithValidKeywordShouldReturnResults)}");
+
+        var results = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(results, Is.Not.Null);
+        Assert.That(results, Is.Not.Empty);
+        Assert.That(results!.Count, Is.LessThanOrEqualTo(TestSearchLimit));
+
+        var firstSong = results.First();
+        Assert.That(firstSong.Id, Is.Not.Empty);
+        Assert.That(firstSong.Name, Is.Not.Empty);
+        Assert.That(firstSong.Artists, Is.Not.Empty);
+        Assert.That(firstSong.AlbumName, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Found song: {firstSong.Name} by {string.Join(", ", firstSong.Artists)}");
+    }
+
+    [Test]
+    public async Task GetSongAsyncWithValidIdShouldReturnDetails()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongAsyncWithValidIdShouldReturnDetails)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var song = await musicApi.GetSongAsync(songId);
+        Assert.That(song, Is.Not.Null);
+        Assert.That(song!.Id, Is.EqualTo(songId));
+        Assert.That(song.Name, Is.Not.Empty);
+        Assert.That(song.Artists, Is.Not.Empty);
+        Assert.That(song.Picture, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Song details: {song.Name}");
+        TestContext.Out.WriteLine($"Picture URL: {song.Picture}");
+    }
+
+    [Test]
+    public async Task GetSongResourceAsyncWithValidIdShouldReturnResource()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongResourceAsyncWithValidIdShouldReturnResource)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        foreach (var song in searchResults!.Take(TestSearchLimit))
+        {
+            TestContext.Out.WriteLine($"Trying song: {song.Name} by {string.Join(", ", song.Artists)} (ID: {song.Id})");
+
+            var resource = await musicApi.GetSongResourceAsync(song.Id, br: TestBitrate);
+            if (resource != null && !string.IsNullOrWhiteSpace(resource.Url))
+            {
+                TestContext.Out.WriteLine($" Resource URL: {resource.Url}");
+                TestContext.Out.WriteLine($" Type: {resource.Type}, Bitrate: {resource.Br}kbps");
+                Assert.That(resource.Type, Is.Not.Empty);
+                Assert.That(resource.Br, Is.GreaterThan(0));
+                Assert.Pass($"Successfully retrieved song resource for: {song.Name}");
+                return;
+            }
+
+            TestContext.Out.WriteLine($" Resource unavailable for: {song.Name}");
+        }
+
+        TestContext.Out.WriteLine("All songs unavailable - expected for some songs due to licensing");
+        Assert.Pass("Resource unavailable for all tested songs (expected for some songs)");
+    }
+
+    [Test]
+    public async Task GetLyricAsyncWithValidIdShouldReturnLyric()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetLyricAsyncWithValidIdShouldReturnLyric)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var lyric = await musicApi.GetLyricAsync(songId);
+        Assert.That(lyric, Is.Not.Null);
+        Assert.That(lyric!.OriginalLyric, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Has time info: {lyric.HasTimeInfo}");
+        TestContext.Out.WriteLine($"Original lyric length: {lyric.OriginalLyric.Length}");
+        if (!string.IsNullOrWhiteSpace(lyric.TranslatedLyric))
+        {
+            TestContext.Out.WriteLine($"Translated lyric length: {lyric.TranslatedLyric.Length}");
+        }
+    }
+
+    [Test]
+    public async Task GetPictureAsyncWithValidIdShouldReturnUrl()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetPictureAsyncWithValidIdShouldReturnUrl)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var pictureUrl = await musicApi.GetPictureAsync(songId, px: TestPictureSize);
+        Assert.That(pictureUrl, Is.Not.Null.And.Not.Empty);
+        Assert.That(pictureUrl, Does.Contain($"param={TestPictureSize}y{TestPictureSize}"));
+
+        TestContext.Out.WriteLine($"Picture URL: {pictureUrl}");
+    }
+
+    [Test]
+    public void SearchAsyncWithNullKeywordShouldThrowArgumentNullException()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(SearchAsyncWithNullKeywordShouldThrowArgumentNullException)}");
+
+        Assert.ThrowsAsync<ArgumentNullException>(async () => await musicApi!.SearchAsync(null!));
+    }
+
+    [Test]
+    public void GetSongAsyncWithNullIdShouldThrowArgumentNullException()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongAsyncWithNullIdShouldThrowArgumentNullException)}");
+
+        Assert.ThrowsAsync<ArgumentNullException>(async () => await musicApi!.GetSongAsync(null!));
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI.Tests/src/SpotifyApiTests.cs b/NET-MusicAPI.Tests/src/SpotifyApiTests.cs
new file mode 100644
index 0000000..6609304
--- /dev/null
+++ b/NET-MusicAPI.Tests/src/SpotifyApiTests.cs
@@ -0,0 +1,141 @@
+using Microsoft.Extensions.DependencyInjection;
+using MusicAPI.Extensions;
+using MusicAPI.Abstractions;
+
+namespace MusicAPI.Tests;
+
+[TestFixture]
+public sealed class SpotifyApiTests
+{
+    private ServiceProvider? serviceProvider;
+    private IMusicApi? musicApi;
+
+    private const string TestKeyword = "星茶会";
+    private const int TestSearchLimit = 5;
+
+    [SetUp]
+    public void Setup()
+    {
+        serviceProvider = new ServiceCollection().AddSpotifyApi().BuildServiceProvider();
+        musicApi = serviceProvider.GetRequiredService<IMusicApi>();
+    }
+
+    [TearDown]
+    public void TearDown()
+    {
+        serviceProvider?.Dispose();
+    }
+
+    [Test]
+    public async Task SearchAsyncWithValidKeywordShouldReturnResults()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(SearchAsyncWithValidKeywordShouldReturnResults)}");
+
+        var results = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(results, Is.Not.Null);
+        Assert.That(results, Is.Not.Empty);
+        Assert.That(results!.Count, Is.LessThanOrEqualTo(TestSearchLimit));
+
+        var firstSong = results.First();
+        Assert.That(firstSong.Id, Is.Not.Empty);
+        Assert.That(firstSong.Name, Is.Not.Empty);
+        Assert.That(firstSong.Artists, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Found song: {firstSong.Name}");
+        TestContext.Out.WriteLine($"Artists: {string.Join(", ", firstSong.Artists)}");
+        TestContext.Out.WriteLine($"Album: {firstSong.AlbumName}");
+        TestContext.Out.WriteLine($"Spotify ID: {firstSong.Id}");
+    }
+
+    [Test]
+    public async Task GetSongAsyncWithValidIdShouldReturnDetails()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongAsyncWithValidIdShouldReturnDetails)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var song = await musicApi.GetSongAsync(songId);
+
+        Assert.That(song, Is.Not.Null);
+        Assert.That(song!.Id, Is.EqualTo(songId));
+        Assert.That(song.Name, Is.Not.Empty);
+        Assert.That(song.Artists, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Song details: {song.Name}");
+        TestContext.Out.WriteLine($"Artists: {string.Join(", ", song.Artists)}");
+        TestContext.Out.WriteLine($"Album: {song.AlbumName}");
+        TestContext.Out.WriteLine($"Cover: {song.Picture}");
+    }
+
+    [Test]
+    public async Task GetSongResourceAsyncShouldReturnYouTubeSearchUrl()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongResourceAsyncShouldReturnYouTubeSearchUrl)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var song = searchResults!.First();
+        var resource = await musicApi.GetSongResourceAsync(songId);
+
+        Assert.That(song, Is.Not.Null);
+        Assert.That(resource, Is.Not.Null);
+        Assert.That(resource!.Url, Is.Not.Empty);
+        Assert.That(resource.Url, Does.Contain("youtube.com/results?search_query="));
+        Assert.That(resource.Type, Is.EqualTo("youtube-search"));
+
+        TestContext.Out.WriteLine($"Song: {song.Name} - {string.Join(", ", song.Artists)}");
+        TestContext.Out.WriteLine($"YouTube search URL: {resource.Url}");
+        TestContext.Out.WriteLine($"Type: {resource.Type}");
+        TestContext.Out.WriteLine($"Duration: {resource.Duration}s");
+    }
+
+    [Test]
+    public async Task GetPictureAsyncWithValidIdShouldReturnUrl()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetPictureAsyncWithValidIdShouldReturnUrl)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var pictureUrl = await musicApi.GetPictureAsync(songId);
+
+        Assert.That(pictureUrl, Is.Not.Null.And.Not.Empty);
+        TestContext.Out.WriteLine($"Cover URL: {pictureUrl}");
+    }
+
+    [Test]
+    public async Task GetLyricAsyncShouldReturnNull()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetLyricAsyncShouldReturnNull)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var lyric = await musicApi.GetLyricAsync(songId);
+
+        Assert.That(lyric, Is.Null);
+        TestContext.Out.WriteLine("Lyrics feature not implemented (expected)");
+    }
+
+    [Test]
+    public void SearchAsyncWithNullKeywordShouldThrowArgumentNullException()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(SearchAsyncWithNullKeywordShouldThrowArgumentNullException)}");
+
+        Assert.ThrowsAsync<ArgumentNullException>(async () => await musicApi!.SearchAsync(null!));
+    }
+
+    [Test]
+    public void GetSongAsyncWithNullIdShouldThrowArgumentNullException()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongAsyncWithNullIdShouldThrowArgumentNullException)}");
+
+        Assert.ThrowsAsync<ArgumentNullException>(async () => await musicApi!.GetSongAsync(null!));
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI.Tests/src/TencentApiTests.cs b/NET-MusicAPI.Tests/src/TencentApiTests.cs
new file mode 100644
index 0000000..d488226
--- /dev/null
+++ b/NET-MusicAPI.Tests/src/TencentApiTests.cs
@@ -0,0 +1,152 @@
+using Microsoft.Extensions.DependencyInjection;
+using MusicAPI.Extensions;
+using MusicAPI.Abstractions;
+
+namespace MusicAPI.Tests;
+
+[TestFixture]
+public sealed class TencentApiTests
+{
+    private ServiceProvider? serviceProvider;
+    private IMusicApi? musicApi;
+
+    private const string TestKeyword = "星茶会";
+    private const int TestSearchLimit = 5;
+    private const int TestBitrate = 320;
+    private const int TestPictureSize = 300;
+
+    [SetUp]
+    public void Setup()
+    {
+        serviceProvider = new ServiceCollection().AddTencentApi().BuildServiceProvider();
+        musicApi = serviceProvider.GetRequiredService<IMusicApi>();
+    }
+
+    [TearDown]
+    public void TearDown()
+    {
+        serviceProvider?.Dispose();
+    }
+
+    [Test]
+    public async Task SearchAsyncWithValidKeywordShouldReturnResults()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(SearchAsyncWithValidKeywordShouldReturnResults)}");
+
+        var results = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(results, Is.Not.Null);
+        Assert.That(results, Is.Not.Empty);
+        Assert.That(results!.Count, Is.LessThanOrEqualTo(TestSearchLimit));
+
+        var firstSong = results.First();
+        Assert.That(firstSong.Id, Is.Not.Empty);
+        Assert.That(firstSong.Name, Is.Not.Empty);
+        Assert.That(firstSong.Artists, Is.Not.Empty);
+        Assert.That(firstSong.AlbumName, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Found song: {firstSong.Name} by {string.Join(", ", firstSong.Artists)}");
+    }
+
+    [Test]
+    public async Task GetSongAsyncWithValidIdShouldReturnDetails()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongAsyncWithValidIdShouldReturnDetails)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var song = await musicApi.GetSongAsync(songId);
+        Assert.That(song, Is.Not.Null);
+        Assert.That(song!.Id, Is.EqualTo(songId));
+        Assert.That(song.Name, Is.Not.Empty);
+        Assert.That(song.Artists, Is.Not.Empty);
+        Assert.That(song.AlbumName, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Song details: {song.Name}");
+        TestContext.Out.WriteLine($"Picture URL: {song.Picture}");
+    }
+
+    [Test]
+    public async Task GetSongResourceAsyncWithValidIdShouldReturnResource()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongResourceAsyncWithValidIdShouldReturnResource)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        foreach (var song in searchResults!.Take(TestSearchLimit))
+        {
+            TestContext.Out.WriteLine($"Trying song: {song.Name} by {string.Join(", ", song.Artists)} (ID: {song.Id})");
+
+            var resource = await musicApi.GetSongResourceAsync(song.Id, br: TestBitrate);
+            if (resource != null && !string.IsNullOrWhiteSpace(resource.Url))
+            {
+                TestContext.Out.WriteLine($" Resource URL: {resource.Url}");
+                TestContext.Out.WriteLine($" Type: {resource.Type}, Bitrate: {resource.Br}kbps");
+                Assert.That(resource.Type, Is.Not.Empty);
+                Assert.That(resource.Br, Is.GreaterThan(0));
+                Assert.Pass($"Successfully retrieved song resource for: {song.Name}");
+                return;
+            }
+
+            TestContext.Out.WriteLine($" Resource unavailable for: {song.Name}");
+        }
+
+        TestContext.Out.WriteLine("All songs unavailable - expected for some songs due to licensing");
+        Assert.Pass("Resource unavailable for all tested songs (expected for some songs)");
+    }
+
+    [Test]
+    public async Task GetLyricAsyncWithValidIdShouldReturnLyric()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetLyricAsyncWithValidIdShouldReturnLyric)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var lyric = await musicApi.GetLyricAsync(songId);
+        Assert.That(lyric, Is.Not.Null);
+        Assert.That(lyric!.OriginalLyric, Is.Not.Empty);
+
+        TestContext.Out.WriteLine($"Has time info: {lyric.HasTimeInfo}");
+        TestContext.Out.WriteLine($"Original lyric length: {lyric.OriginalLyric.Length}");
+        if (!string.IsNullOrWhiteSpace(lyric.TranslatedLyric))
+        {
+            TestContext.Out.WriteLine($"Translated lyric length: {lyric.TranslatedLyric.Length}");
+        }
+    }
+
+    [Test]
+    public async Task GetPictureAsyncWithValidIdShouldReturnUrl()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetPictureAsyncWithValidIdShouldReturnUrl)}");
+
+        var searchResults = await musicApi!.SearchAsync(TestKeyword, limit: TestSearchLimit);
+        Assert.That(searchResults, Is.Not.Null.And.Not.Empty);
+
+        var songId = searchResults!.First().Id;
+        var pictureUrl = await musicApi.GetPictureAsync(songId, px: TestPictureSize);
+        Assert.That(pictureUrl, Is.Not.Null.And.Not.Empty);
+        Assert.That(pictureUrl, Does.Contain($"{TestPictureSize}x{TestPictureSize}"));
+
+        TestContext.Out.WriteLine($"Picture URL: {pictureUrl}");
+    }
+
+    [Test]
+    public void SearchAsyncWithNullKeywordShouldThrowArgumentNullException()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(SearchAsyncWithNullKeywordShouldThrowArgumentNullException)}");
+
+        Assert.ThrowsAsync<ArgumentNullException>(async () => await musicApi!.SearchAsync(null!));
+    }
+
+    [Test]
+    public void GetSongAsyncWithNullIdShouldThrowArgumentNullException()
+    {
+        TestContext.Out.WriteLine($"[Test] {nameof(GetSongAsyncWithNullIdShouldThrowArgumentNullException)}");
+
+        Assert.ThrowsAsync<ArgumentNullException>(async () => await musicApi!.GetSongAsync(null!));
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI.sln b/NET-MusicAPI.sln
new file mode 100644
index 0000000..3107597
--- /dev/null
+++ b/NET-MusicAPI.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NET-MusicAPI", "NET-MusicAPI\NET-MusicAPI.csproj", "{D93547FB-794A-4A78-95B3-A24DA13D8161}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NET-MusicAPI.Tests", "NET-MusicAPI.Tests\NET-MusicAPI.Tests.csproj", "{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release|Any CPU = Release|Any CPU
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Debug|x64.Build.0 = Debug|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Debug|x86.Build.0 = Debug|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Release|x64.ActiveCfg = Release|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Release|x64.Build.0 = Release|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Release|x86.ActiveCfg = Release|Any CPU
+		{D93547FB-794A-4A78-95B3-A24DA13D8161}.Release|x86.Build.0 = Release|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Debug|x64.Build.0 = Debug|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Debug|x86.Build.0 = Debug|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Release|Any CPU.Build.0 = Release|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Release|x64.ActiveCfg = Release|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Release|x64.Build.0 = Release|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Release|x86.ActiveCfg = Release|Any CPU
+		{43D68BC9-34BD-4694-8CD9-ACB7A1B2E843}.Release|x86.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal
diff --git a/NET-MusicAPI/NET-MusicAPI.csproj b/NET-MusicAPI/NET-MusicAPI.csproj
new file mode 100644
index 0000000..4155ddd
--- /dev/null
+++ b/NET-MusicAPI/NET-MusicAPI.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net10.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <LangVersion>preview</LangVersion>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
+    <PackageReference Include="SpotifyAPI.Web" Version="7.2.1" />
+    <PackageReference Include="System.Text.Json" Version="10.0.0" />
+  </ItemGroup>
+
+</Project>
diff --git a/NET-MusicAPI/src/Abstractions/IMusicApi.cs b/NET-MusicAPI/src/Abstractions/IMusicApi.cs
new file mode 100644
index 0000000..887c382
--- /dev/null
+++ b/NET-MusicAPI/src/Abstractions/IMusicApi.cs
@@ -0,0 +1,54 @@
+namespace MusicAPI.Abstractions;
+
+public interface IMusicApi
+{
+    /// <summary>
+    /// Gets or sets custom HTTP headers for requests
+    /// </summary>
+    IReadOnlyDictionary<string, string>? CustomHeaders { get; set; }
+
+    /// <summary>
+    /// Search for songs by keyword
+    /// </summary>
+    /// <param name="keyword">Search keyword</param>
+    /// <param name="type">Search type (default: 1)</param>
+    /// <param name="limit">Maximum number of results (default: 30)</param>
+    /// <param name="page">Page number (default: 1)</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    /// <returns>List of songs matching the search criteria</returns>
+    Task<IReadOnlyList<Song>?> SearchAsync(string keyword, int type = 1, int limit = 30, int page = 1, CancellationToken cancellationToken = default);
+
+    /// <summary>
+    /// Get detailed information about a song
+    /// </summary>
+    /// <param name="id">Song identifier</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    /// <returns>Song details or null if not found</returns>
+    Task<Song?> GetSongAsync(string id, CancellationToken cancellationToken = default);
+
+    /// <summary>
+    /// Get streaming resource for a song
+    /// </summary>
+    /// <param name="id">Song identifier</param>
+    /// <param name="br">Bitrate in kbps (default: 320)</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    /// <returns>Song resource or null if not available</returns>
+    Task<SongResource?> GetSongResourceAsync(string id, int br = 320, CancellationToken cancellationToken = default);
+
+    /// <summary>
+    /// Get lyrics for a song
+    /// </summary>
+    /// <param name="id">Song identifier</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    /// <returns>Lyric information or null if not available</returns>
+    Task<Lyric?> GetLyricAsync(string id, CancellationToken cancellationToken = default);
+
+    /// <summary>
+    /// Get picture URL for a song
+    /// </summary>
+    /// <param name="id">Song identifier</param>
+    /// <param name="px">Picture size in pixels (default: 300)</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    /// <returns>Picture URL or null if not available</returns>
+    Task<string?> GetPictureAsync(string id, int px = 300, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Extensions/HttpClientExtensions.cs b/NET-MusicAPI/src/Extensions/HttpClientExtensions.cs
new file mode 100644
index 0000000..1bbff9a
--- /dev/null
+++ b/NET-MusicAPI/src/Extensions/HttpClientExtensions.cs
@@ -0,0 +1,72 @@
+using System.Net;
+using System.Web;
+
+namespace MusicAPI;
+
+public static class HttpClientExtensions
+{
+    /// <summary>
+    /// Sends an HTTP request with custom headers and parameters
+    /// </summary>
+    public static async Task<string?> SendMusicApiRequestAsync(this HttpClient client, HttpMethod method, string? url, IReadOnlyDictionary<string, string>? parameters, IReadOnlyDictionary<string, string> headers, CancellationToken cancellationToken = default)
+    {
+        if (string.IsNullOrWhiteSpace(url))
+        {
+            return string.Empty;
+        }
+
+        using var request = CreateRequest(method, url, parameters);
+
+        foreach (var (key, value) in headers)
+        {
+            request.Headers.TryAddWithoutValidation(key, value);
+        }
+
+        using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
+        response.EnsureSuccessStatusCode();
+
+        return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+    }
+
+    private static HttpRequestMessage CreateRequest(HttpMethod method, string url, IReadOnlyDictionary<string, string>? parameters)
+    {
+        if (method == HttpMethod.Post)
+        {
+            var request = new HttpRequestMessage(HttpMethod.Post, url);
+            if (parameters?.Any() ?? false)
+            {
+                request.Content = new FormUrlEncodedContent(parameters);
+            }
+            return request;
+        }
+
+        // GET request
+        var urlWithQuery = BuildUrlWithQuery(url, parameters);
+        return new HttpRequestMessage(HttpMethod.Get, urlWithQuery);
+    }
+
+    private static string BuildUrlWithQuery(string url, IReadOnlyDictionary<string, string>? parameters)
+    {
+        if (!(parameters?.Any() ?? false))
+        {
+            return url;
+        }
+
+        var queryString = HttpUtility.ParseQueryString(string.Empty);
+        foreach (var (key, value) in parameters)
+        {
+            queryString[key] = value;
+        }
+
+        return $"{url}?{queryString}";
+    }
+
+    /// <summary>
+    /// Generates a random IP address in the range 112.80.0.0 - 112.95.255.255
+    /// </summary>
+    public static string GenerateRandomIp()
+    {
+        var ip = Random.Shared.Next(1884815360, 1884890111);
+        return new IPAddress(ip).ToString();
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Extensions/ServiceCollectionExtensions.cs b/NET-MusicAPI/src/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..316e976
--- /dev/null
+++ b/NET-MusicAPI/src/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,93 @@
+using Microsoft.Extensions.DependencyInjection;
+using MusicAPI.Netease;
+using MusicAPI.Tencent;
+using MusicAPI.Spotify;
+using MusicAPI.Abstractions;
+
+namespace MusicAPI.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+    /// <summary>
+    /// Adds Netease Cloud Music API services to the service collection
+    /// </summary>
+    public static IServiceCollection AddNeteaseApi(this IServiceCollection services)
+    {
+        ArgumentNullException.ThrowIfNull(services);
+
+        services.AddHttpClient<IMusicApi, NeteaseApi>(client =>
+        {
+            client.Timeout = TimeSpan.FromSeconds(8);
+        });
+
+        return services;
+    }
+
+    /// <summary>
+    /// Adds Netease Cloud Music API as a singleton with self-managed HttpClient
+    /// </summary>
+    public static IServiceCollection AddNeteaseApiSingleton(this IServiceCollection services)
+    {
+        ArgumentNullException.ThrowIfNull(services);
+
+        services.AddSingleton<IMusicApi>(sp => new NeteaseApi());
+
+        return services;
+    }
+
+    /// <summary>
+    /// Adds Tencent QQ Music API services to the service collection
+    /// </summary>
+    public static IServiceCollection AddTencentApi(this IServiceCollection services)
+    {
+        ArgumentNullException.ThrowIfNull(services);
+
+        services.AddHttpClient<IMusicApi, TencentApi>(client =>
+        {
+            client.Timeout = TimeSpan.FromSeconds(8);
+        });
+
+        return services;
+    }
+
+    /// <summary>
+    /// Adds Tencent QQ Music API as a singleton with self-managed HttpClient
+    /// </summary>
+    public static IServiceCollection AddTencentApiSingleton(this IServiceCollection services)
+    {
+        ArgumentNullException.ThrowIfNull(services);
+
+        services.AddSingleton<IMusicApi>(sp => new TencentApi());
+
+        return services;
+    }
+
+    /// <summary>
+    /// Adds Spotify Music API services (Spotify metadata + YouTube audio search)
+    /// </summary>
+    [Obsolete("Limited functionality due to Spotify's strict API restrictions.")]
+    public static IServiceCollection AddSpotifyApi(this IServiceCollection services)
+    {
+        ArgumentNullException.ThrowIfNull(services);
+
+        services.AddHttpClient<IMusicApi, SpotifyApi>(client =>
+        {
+            client.Timeout = TimeSpan.FromSeconds(10);
+        });
+
+        return services;
+    }
+
+    /// <summary>
+    /// Adds Spotify Music API as a singleton
+    /// </summary>
+    [Obsolete("Limited functionality due to Spotify's strict API restrictions.")]
+    public static IServiceCollection AddSpotifyApiSingleton(this IServiceCollection services)
+    {
+        ArgumentNullException.ThrowIfNull(services);
+
+        services.AddSingleton<IMusicApi>(sp => new SpotifyApi());
+
+        return services;
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Models/Universal.cs b/NET-MusicAPI/src/Models/Universal.cs
new file mode 100644
index 0000000..7d527dc
--- /dev/null
+++ b/NET-MusicAPI/src/Models/Universal.cs
@@ -0,0 +1,55 @@
+
+namespace MusicAPI;
+
+/// <summary>
+/// Represents a music song with metadata
+/// </summary>
+public sealed record Song
+{
+    public required string Id { get; init; }
+    public required string Name { get; init; }
+    public required IReadOnlyList<string> Artists { get; init; }
+    public required string AlbumName { get; init; }
+    public required string LyricId { get; init; }
+    public string Picture { get; init; } = string.Empty;
+}
+
+/// <summary>
+/// Represents a song resource with streaming information
+/// </summary>
+public sealed record SongResource
+{
+    public required string Url { get; init; }
+    public required string Type { get; init; }
+    public required int Br { get; init; }
+    public double Size { get; init; }
+    public double Duration { get; init; }
+}
+
+/// <summary>
+/// Represents an artist
+/// </summary>
+public sealed record Artist
+{
+    public required string Name { get; init; }
+    public string? Id { get; init; }
+}
+
+/// <summary>
+/// Represents an album with cover art
+/// </summary>
+public sealed record Album
+{
+    public required string Name { get; init; }
+    public required string Pic { get; init; }
+}
+
+/// <summary>
+/// Represents song lyrics with optional translation
+/// </summary>
+public sealed record Lyric
+{
+    public required bool HasTimeInfo { get; init; }
+    public required string OriginalLyric { get; init; }
+    public string TranslatedLyric { get; init; } = string.Empty;
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Netease/NeteaseApi.cs b/NET-MusicAPI/src/Netease/NeteaseApi.cs
new file mode 100644
index 0000000..501db38
--- /dev/null
+++ b/NET-MusicAPI/src/Netease/NeteaseApi.cs
@@ -0,0 +1,266 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+using MusicAPI.Abstractions;
+
+namespace MusicAPI.Netease;
+
+public sealed partial class NeteaseApi : IMusicApi, IDisposable
+{
+    private readonly HttpClient httpClient;
+    private readonly bool disposedHttpClient;
+    private volatile bool disposed;
+
+    [GeneratedRegex(@"\[(\d+:\d+.*)\]", RegexOptions.Compiled)]
+    private static partial Regex TimeInfoRegex();
+
+    public IReadOnlyDictionary<string, string>? CustomHeaders { get; set; }
+
+    public NeteaseApi() : this(null)
+    {
+    }
+
+    public NeteaseApi(HttpClient? client)
+    {
+        if (client == null)
+        {
+            this.httpClient = new HttpClient
+            {
+                Timeout = TimeSpan.FromSeconds(8)
+            };
+            this.disposedHttpClient = true;
+        }
+        else
+        {
+            this.httpClient = client;
+            this.disposedHttpClient = false;
+        }
+    }
+
+    private Dictionary<string, string> BuildHeaders()
+    {
+        var headers = new Dictionary<string, string>
+        {
+            ["Referer"] = "https://music.163.com/",
+            ["Cookie"] = "appver=8.2.30; os=iPhone OS; osver=15.0; EVNSM=1.0.0; buildver=2206; channel=distribution; machineid=iPhone13.3",
+            ["User-Agent"] = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CloudMusic/0.1.1 NeteaseMusic/8.2.30",
+            ["X-Real-IP"] = HttpClientExtensions.GenerateRandomIp(),
+            ["Accept"] = "*/*",
+            ["Accept-Language"] = "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4",
+            ["Connection"] = "keep-alive",
+            ["Content-Type"] = "application/x-www-form-urlencoded"
+        };
+
+        if (CustomHeaders != null)
+        {
+            foreach (var (key, value) in CustomHeaders)
+            {
+                headers[key] = value;
+            }
+        }
+
+        return headers;
+    }
+
+    public async Task<IReadOnlyList<Song>?> SearchAsync(string keyword, int type = 1, int limit = 30, int page = 1, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(keyword);
+
+        var requestBody = new
+        {
+            s = string.Concat(keyword.Select(c => $@"\u{(int)c:x4}")),
+            type,
+            limit,
+            total = "true",
+            offset = page > 0 && limit > 0 ? (page - 1) * limit : 0
+        };
+
+        var encrypted = NeteaseCryptoService.EncryptRequest("http://music.163.com/api/cloudsearch/pc", requestBody);
+        var response = await httpClient.SendMusicApiRequestAsync(HttpMethod.Post, encrypted.Url, encrypted.Body, BuildHeaders(), cancellationToken).ConfigureAwait(false);
+        if (string.IsNullOrWhiteSpace(response))
+        {
+            return null;
+        }
+
+        var json = JsonNode.Parse(response);
+        var songs = json?["result"]?["songs"]?.AsArray();
+        if (songs == null)
+        {
+            return null;
+        }
+
+        var results = new List<Song>(songs.Count);
+
+        foreach (var songNode in songs)
+        {
+            if (songNode != null)
+            {
+                var artists = songNode["ar"]?.AsArray()
+                .Select(a => a?["name"]?.GetValue<string>())
+                .Where(name => !string.IsNullOrWhiteSpace(name))
+                .Cast<string>()
+                .ToList() ?? [];
+
+                var song = new Song
+                {
+                    Id = songNode["id"]?.GetValue<long>().ToString() ?? string.Empty,
+                    Name = songNode["name"]?.GetValue<string>() ?? string.Empty,
+                    Artists = artists,
+                    AlbumName = songNode["al"]?["name"]?.GetValue<string>() ?? string.Empty,
+                    LyricId = songNode["id"]?.GetValue<long>().ToString() ?? string.Empty
+                };
+
+                results.Add(song);
+            }
+        }
+
+        return results;
+    }
+
+    public async Task<Song?> GetSongAsync(string id, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var requestBody = new
+        {
+            c = $"[{{\"id\":'{id}',\"v\":0}}]"
+        };
+
+        var encrypted = NeteaseCryptoService.EncryptRequest("http://music.163.com/api/v3/song/detail/", requestBody);
+
+        var response = await httpClient.SendMusicApiRequestAsync(HttpMethod.Post, encrypted.Url, encrypted.Body, BuildHeaders(), cancellationToken).ConfigureAwait(false);
+        if (string.IsNullOrWhiteSpace(response))
+        {
+            return null;
+        }
+
+        var json = JsonNode.Parse(response);
+        var songNode = json?["songs"]?[0];
+        if (songNode == null)
+        {
+            return null;
+        }
+
+        var artists = songNode["ar"]?.AsArray()
+            .Select(a => a?["name"]?.GetValue<string>())
+            .Where(name => !string.IsNullOrWhiteSpace(name))
+            .Cast<string>()
+            .ToList() ?? [];
+
+        return new Song
+        {
+            Id = songNode["id"]?.GetValue<long>().ToString() ?? string.Empty,
+            Name = songNode["name"]?.GetValue<string>() ?? string.Empty,
+            Artists = artists,
+            AlbumName = songNode["al"]?["name"]?.GetValue<string>() ?? string.Empty,
+            LyricId = songNode["id"]?.GetValue<long>().ToString() ?? string.Empty,
+            Picture = songNode["al"]?["picUrl"]?.GetValue<string>() ?? string.Empty
+        };
+    }
+
+    public async Task<SongResource?> GetSongResourceAsync(string id, int br = 320, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var requestBody = new
+        {
+            ids = $"[{id}]",
+            br = br * 1000
+        };
+
+        var encrypted = NeteaseCryptoService.EncryptRequest("http://music.163.com/api/song/enhance/player/url", requestBody);
+
+        var response = await httpClient.SendMusicApiRequestAsync(HttpMethod.Post, encrypted.Url, encrypted.Body, BuildHeaders(), cancellationToken).ConfigureAwait(false);
+        if (string.IsNullOrWhiteSpace(response))
+        {
+            return null;
+        }
+
+        var json = JsonNode.Parse(response);
+        var data = json?["data"]?[0];
+        if (data == null)
+        {
+            return null;
+        }
+
+        return new SongResource
+        {
+            Url = data["url"]?.GetValue<string>() ?? string.Empty,
+            Type = data["type"]?.GetValue<string>() ?? string.Empty,
+            Br = (int)(data["br"]?.GetValue<long>() ?? 0) / 1000,
+            Size = (data["size"]?.GetValue<long>() ?? 0) / 1024.0 / 1024.0,
+            Duration = (data["time"]?.GetValue<long>() ?? 0) / 1000.0
+        };
+    }
+
+    public async Task<Lyric?> GetLyricAsync(string id, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var requestBody = new
+        {
+            id,
+            os = "linux",
+            lv = -1,
+            kv = -1,
+            tv = -1
+        };
+
+        var encrypted = NeteaseCryptoService.EncryptRequest("http://music.163.com/api/song/lyric", requestBody);
+
+        var response = await httpClient.SendMusicApiRequestAsync(HttpMethod.Post, encrypted.Url, encrypted.Body, BuildHeaders(), cancellationToken).ConfigureAwait(false);
+        if (string.IsNullOrWhiteSpace(response))
+        {
+            return null;
+        }
+
+        var json = JsonNode.Parse(response);
+        var originalLyric = json?["lrc"]?["lyric"]?.GetValue<string>() ?? string.Empty;
+        var translatedLyric = json?["tlyric"]?["lyric"]?.GetValue<string>() ?? string.Empty;
+        if (string.IsNullOrWhiteSpace(originalLyric))
+        {
+            return null;
+        }
+
+        return new Lyric
+        {
+            HasTimeInfo = TimeInfoRegex().IsMatch(originalLyric),
+            OriginalLyric = originalLyric,
+            TranslatedLyric = translatedLyric
+        };
+    }
+
+    public async Task<string?> GetPictureAsync(string id, int px = 300, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        var song = await GetSongAsync(id, cancellationToken).ConfigureAwait(false);
+
+        if (song == null || string.IsNullOrWhiteSpace(song.Picture))
+        {
+            return string.Empty;
+        }
+
+        return $"{song.Picture}?param={px}y{px}";
+    }
+
+    public void Dispose()
+    {
+        if (disposed)
+        {
+            return;
+        }
+
+        disposed = true;
+
+        if (disposedHttpClient)
+        {
+            httpClient?.Dispose();
+        }
+
+        GC.SuppressFinalize(this);
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Netease/NeteaseCryptoService.cs b/NET-MusicAPI/src/Netease/NeteaseCryptoService.cs
new file mode 100644
index 0000000..51433fa
--- /dev/null
+++ b/NET-MusicAPI/src/Netease/NeteaseCryptoService.cs
@@ -0,0 +1,141 @@
+using System.Text;
+using System.Numerics;
+using System.Text.Json;
+using System.Security.Cryptography;
+
+namespace MusicAPI.Netease;
+
+internal static class NeteaseCryptoService
+{
+    private const string Modulus = "157794750267131502212476817800345498121872783333389747424011531025366277535262539913701806290766479189477533597854989606803194253978660329941980786072432806427833685472618792592200595694346872951301770580765135349259590167490536138082469680638514416594216629258349130257685001248172188325316586707301643237607";
+    private const string PubKey = "65537";
+    private const string Nonce = "0CoJUm6Qyw8W8jud";
+    private const string IV = "0102030405060708";
+
+    /// <summary>
+    /// Encrypts API request using AES-CBC encryption
+    /// </summary>
+    public static NeteaseEncryptedResult EncryptRequest(string url, object body)
+    {
+        var skey = GenerateRandomHex(16);
+
+        var bodyJson = JsonSerializer.Serialize(body);
+        var encryptedBody = EncryptAes(bodyJson.Replace("\\\\", "\\"), Nonce, IV);
+        encryptedBody = EncryptAes(encryptedBody, skey, IV);
+
+        var encryptedSkey = EncryptSecretKey(skey, PubKey, Modulus);
+
+        return new NeteaseEncryptedResult(url.Replace("/api/", "/weapi/"), new Dictionary<string, string>
+        {
+            ["params"] = encryptedBody,
+            ["encSecKey"] = encryptedSkey.ToLowerInvariant()
+        });
+    }
+
+    private static string GenerateRandomHex(int length)
+    {
+        Span<byte> bytes = stackalloc byte[length / 2];
+        RandomNumberGenerator.Fill(bytes);
+        return Convert.ToHexString(bytes).ToLowerInvariant();
+    }
+
+    private static string EncryptAes(string plainText, string key, string iv)
+    {
+        ArgumentException.ThrowIfNullOrWhiteSpace(plainText);
+        ArgumentException.ThrowIfNullOrWhiteSpace(key);
+        ArgumentException.ThrowIfNullOrWhiteSpace(iv);
+
+        var keyBytes = Encoding.UTF8.GetBytes(key);
+        var ivBytes = Encoding.UTF8.GetBytes(iv);
+
+        if (keyBytes.Length != 16)
+        {
+            throw new ArgumentException("AES-128-CBC key must be 16 bytes", nameof(key));
+        }
+
+        if (ivBytes.Length != 16)
+        {
+            throw new ArgumentException("AES-128-CBC IV must be 16 bytes", nameof(iv));
+        }
+
+        using var aes = Aes.Create();
+        aes.Key = keyBytes;
+        aes.IV = ivBytes;
+        aes.Mode = CipherMode.CBC;
+        aes.Padding = PaddingMode.PKCS7;
+
+        using var encryptor = aes.CreateEncryptor();
+        var plainBytes = Encoding.UTF8.GetBytes(plainText);
+        var encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
+
+        return Convert.ToBase64String(encryptedBytes);
+    }
+
+    private static string EncryptSecretKey(string skey, string pubkey, string modulus)
+    {
+        var reversedKey = ReverseString(skey);
+        var hexKey = ConvertToHex(reversedKey);
+        var decimalKey = HexToDecimal(hexKey);
+        var encrypted = ModularExponentiation(decimalKey, pubkey, modulus);
+
+        var result = BigInteger.Parse(encrypted).ToString("X").ToLowerInvariant();
+
+        // Remove leading character if length is 257
+        if (result.Length == 257)
+        {
+            result = result[1..];
+        }
+
+        return result.PadLeft(256, '0');
+    }
+
+    private static string ReverseString(string input)
+    {
+        Span<char> chars = stackalloc char[input.Length];
+        input.AsSpan().CopyTo(chars);
+        chars.Reverse();
+        return new string(chars);
+    }
+
+    private static string ConvertToHex(string input)
+    {
+        var builder = new StringBuilder(input.Length * 2);
+        foreach (var c in input)
+        {
+            builder.Append(((int)c).ToString("X2"));
+        }
+        return builder.ToString();
+    }
+
+    private static string HexToDecimal(string hex)
+    {
+        BigInteger result = 0;
+        var length = hex.Length;
+
+        for (var i = 0; i < length; i++)
+        {
+            var digitValue = HexCharToValue(hex[i]);
+            var power = BigInteger.Pow(16, length - i - 1);
+            result += digitValue * power;
+        }
+
+        return result.ToString();
+    }
+
+    private static string ModularExponentiation(string baseValue, string exponent, string modulus)
+    {
+        var baseInt = BigInteger.Parse(baseValue);
+        var expInt = BigInteger.Parse(exponent);
+        var modInt = BigInteger.Parse(modulus);
+
+        return BigInteger.ModPow(baseInt, expInt, modInt).ToString();
+    }
+
+    private static BigInteger HexCharToValue(char hexChar) => hexChar switch
+    {
+        >= '0' and <= '9' => hexChar - '0',
+        >= 'a' and <= 'f' => 10 + hexChar - 'a',
+        >= 'A' and <= 'F' => 10 + hexChar - 'A',
+        _ => throw new ArgumentException($"Invalid hex character: {hexChar}", nameof(hexChar))
+    };
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Netease/NeteaseEncryptedResult.cs b/NET-MusicAPI/src/Netease/NeteaseEncryptedResult.cs
new file mode 100644
index 0000000..f98fc40
--- /dev/null
+++ b/NET-MusicAPI/src/Netease/NeteaseEncryptedResult.cs
@@ -0,0 +1,10 @@
+namespace MusicAPI.Netease;
+
+internal sealed record NeteaseEncryptedResult(string Url, IReadOnlyDictionary<string, string> Body)
+{
+    public override string ToString()
+    {
+        var bodyParams = string.Join("&", Body.Select(kvp => $"{kvp.Key}={kvp.Value}"));
+        return $"{Url}?{bodyParams}";
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Spotify/SpotifyApi.cs b/NET-MusicAPI/src/Spotify/SpotifyApi.cs
new file mode 100644
index 0000000..794842b
--- /dev/null
+++ b/NET-MusicAPI/src/Spotify/SpotifyApi.cs
@@ -0,0 +1,202 @@
+using System.Web;
+using SpotifyAPI.Web;
+using MusicAPI.Abstractions;
+
+namespace MusicAPI.Spotify;
+
+/// <summary>
+/// Spotify + YouTube downloader implementation
+/// Gets metadata from Spotify and generates YouTube search URLs
+/// Note: This returns YouTube search URLs, not direct download links
+/// </summary>
+[Obsolete("Limited functionality due to Spotify's strict API restrictions.")]
+public sealed class SpotifyApi : IMusicApi, IDisposable
+{
+    private readonly string? spotifyClientId;
+    private readonly string? spotifyClientSecret;
+    private SpotifyClient? spotifyClient;
+    private readonly SemaphoreSlim authLock = new(1, 1);
+    private volatile bool disposed;
+
+    public IReadOnlyDictionary<string, string>? CustomHeaders { get; set; }
+
+    public SpotifyApi(string? spotifyClientId = null, string? spotifyClientSecret = null)
+    {
+        this.spotifyClientId = spotifyClientId;
+        this.spotifyClientSecret = spotifyClientSecret;
+    }
+
+    public SpotifyApi(HttpClient httpClient, string? spotifyClientId = null, string? spotifyClientSecret = null) : this(spotifyClientId, spotifyClientSecret)
+    {
+        // SpotifyAPI.Web manages its own HttpClient
+    }
+
+    public async Task<IReadOnlyList<Song>?> SearchAsync(string keyword, int type = 1, int limit = 30, int page = 1, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(keyword);
+
+        var client = await GetSpotifyClientAsync(cancellationToken).ConfigureAwait(false);
+        var offset = (page - 1) * limit;
+
+        var searchRequest = new SearchRequest(SearchRequest.Types.Track, keyword)
+        {
+            Limit = limit,
+            Offset = offset
+        };
+
+        var searchResponse = await client.Search.Item(searchRequest, cancellationToken).ConfigureAwait(false);
+        if (searchResponse?.Tracks?.Items == null || searchResponse.Tracks.Items.Count == 0)
+        {
+            return null;
+        }
+
+        var results = new List<Song>();
+        foreach (var track in searchResponse.Tracks.Items)
+        {
+            if (track == null)
+            {
+                continue;
+            }
+
+            results.Add(new Song
+            {
+                Id = track.Id,
+                Name = track.Name,
+                Artists = track.Artists.Select(a => a.Name).ToList(),
+                AlbumName = track.Album.Name,
+                Picture = track.Album.Images.FirstOrDefault()?.Url ?? string.Empty,
+                LyricId = string.Empty
+            });
+        }
+
+        return results;
+    }
+
+    public async Task<Song?> GetSongAsync(string id, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var client = await GetSpotifyClientAsync(cancellationToken).ConfigureAwait(false);
+        var track = await client.Tracks.Get(id, cancellationToken).ConfigureAwait(false);
+
+        if (track == null)
+        {
+            return null;
+        }
+
+        return new Song
+        {
+            Id = track.Id,
+            Name = track.Name,
+            Artists = track.Artists.Select(a => a.Name).ToList(),
+            AlbumName = track.Album.Name,
+            Picture = track.Album.Images.FirstOrDefault()?.Url ?? string.Empty,
+            LyricId = string.Empty
+        };
+    }
+
+    public async Task<SongResource?> GetSongResourceAsync(string id, int br = 320, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        // Get Spotify metadata
+        var client = await GetSpotifyClientAsync(cancellationToken).ConfigureAwait(false);
+        var track = await client.Tracks.Get(id, cancellationToken).ConfigureAwait(false);
+        if (track == null)
+        {
+            return null;
+        }
+
+        var artists = string.Join(" ", track.Artists.Select(a => a.Name));
+        var searchQuery = $"{artists} - {track.Name}";
+
+        // Generate YouTube search URL
+        var youtubeSearchUrl = GenerateYouTubeSearchUrl(searchQuery);
+
+        return new SongResource
+        {
+            Url = youtubeSearchUrl,
+            Type = "youtube-search",
+            Br = 128, // YouTube typically 128-256kbps
+            Size = 0,
+            Duration = track.DurationMs / 1000
+        };
+    }
+
+    public Task<Lyric?> GetLyricAsync(string id, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        // TODO:
+        // Spotify Web API doesn't provide lyrics
+        // Could integrate with Genius/Musixmatch like spotdl does
+        return Task.FromResult<Lyric?>(null);
+    }
+
+    public async Task<string?> GetPictureAsync(string id, int px = 300, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var song = await GetSongAsync(id, cancellationToken).ConfigureAwait(false);
+        return song?.Picture;
+    }
+
+    public void Dispose()
+    {
+        if (disposed)
+        {
+            return;
+        }
+        disposed = true;
+
+        authLock.Dispose();
+        GC.SuppressFinalize(this);
+    }
+
+    private async Task<SpotifyClient> GetSpotifyClientAsync(CancellationToken cancellationToken = default)
+    {
+        if (spotifyClient != null)
+        {
+            return spotifyClient;
+        }
+
+        await authLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+        try
+        {
+            if (spotifyClient != null)
+            {
+                return spotifyClient;
+            }
+
+            var id = spotifyClientId ?? CustomHeaders?.GetValueOrDefault("SpotifyClientId") ?? Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID");
+            var secret = spotifyClientSecret ?? CustomHeaders?.GetValueOrDefault("SpotifyClientSecret") ?? Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET");
+
+            if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret))
+            {
+                throw new InvalidOperationException("Spotify Client ID and Secret are required.");
+            }
+
+            var config = SpotifyClientConfig.CreateDefault();
+            var request = new ClientCredentialsRequest(id, secret);
+            var response = await new OAuthClient(config).RequestToken(request, cancellationToken).ConfigureAwait(false);
+
+            spotifyClient = new SpotifyClient(config.WithToken(response.AccessToken));
+            return spotifyClient;
+        }
+        finally
+        {
+            authLock.Release();
+        }
+    }
+
+    private static string GenerateYouTubeSearchUrl(string query)
+    {
+        var encodedQuery = HttpUtility.UrlEncode(query);
+        return $"https://www.youtube.com/results?search_query={encodedQuery}";
+    }
+}
\ No newline at end of file
diff --git a/NET-MusicAPI/src/Tencent/TencentApi.cs b/NET-MusicAPI/src/Tencent/TencentApi.cs
new file mode 100644
index 0000000..584da1c
--- /dev/null
+++ b/NET-MusicAPI/src/Tencent/TencentApi.cs
@@ -0,0 +1,364 @@
+using System.Web;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+using MusicAPI.Abstractions;
+
+namespace MusicAPI.Tencent;
+
+public sealed partial class TencentApi : IMusicApi, IDisposable
+{
+    private readonly HttpClient httpClient;
+    private readonly bool disposedHttpClient;
+    private volatile bool disposed;
+
+    [GeneratedRegex(@"\[(\d+:\d+.*)\]", RegexOptions.Compiled)]
+    private static partial Regex TimeInfoRegex();
+
+    public IReadOnlyDictionary<string, string>? CustomHeaders { get; set; }
+
+    public TencentApi() : this(null)
+    {
+    }
+
+    public TencentApi(HttpClient? client)
+    {
+        if (client == null)
+        {
+            this.httpClient = new HttpClient
+            {
+                Timeout = TimeSpan.FromSeconds(8)
+            };
+            this.disposedHttpClient = true;
+        }
+        else
+        {
+            this.httpClient = client;
+            this.disposedHttpClient = false;
+        }
+    }
+
+    private Dictionary<string, string> BuildHeaders()
+    {
+        var headers = new Dictionary<string, string>
+        {
+            ["Referer"] = "http://y.qq.com",
+            ["Cookie"] = "pgv_pvi=22038528; pgv_si=s3156287488; pgv_pvid=5535248600; yplayer_open=1; ts_last=y.qq.com/portal/player.html; ts_uid=4847550686; yq_index=0; qqmusic_fromtag=66; player_exist=1",
+            ["User-Agent"] = "QQ%E9%9F%B3%E4%B9%90/54409 CFNetwork/901.1 Darwin/17.6.0 (x86_64)",
+            ["Accept"] = "*/*",
+            ["Accept-Language"] = "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4",
+            ["Connection"] = "keep-alive",
+            ["Content-Type"] = "application/x-www-form-urlencoded"
+        };
+
+        if (CustomHeaders != null)
+        {
+            foreach (var (key, value) in CustomHeaders)
+            {
+                headers[key] = value;
+            }
+        }
+
+        return headers;
+    }
+
+    public async Task<IReadOnlyList<Song>?> SearchAsync(string keyword, int type = 1, int limit = 30, int page = 1, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(keyword);
+
+        var url = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp";
+        var parameters = new Dictionary<string, string>
+        {
+            ["format"] = "json",
+            ["p"] = page.ToString(),
+            ["n"] = limit.ToString(),
+            ["w"] = keyword,
+            ["aggr"] = "1",
+            ["lossless"] = "1",
+            ["cr"] = "1",
+            ["new_json"] = "1"
+        };
+
+        var response = await httpClient.SendMusicApiRequestAsync(HttpMethod.Get, url, parameters, BuildHeaders(), cancellationToken).ConfigureAwait(false);
+        if (string.IsNullOrWhiteSpace(response))
+        {
+            return null;
+        }
+
+        var json = JsonNode.Parse(response);
+        var songs = json?["data"]?["song"]?["list"]?.AsArray();
+        if (songs == null)
+        {
+            return null;
+        }
+
+        var results = new List<Song>(songs.Count);
+
+        foreach (var songNode in songs)
+        {
+            if (songNode != null)
+            {
+                var artists = songNode["singer"]?.AsArray()
+                    .Select(a => a?["name"]?.GetValue<string>())
+                    .Where(name => !string.IsNullOrWhiteSpace(name))
+                    .Cast<string>()
+                    .ToList() ?? [];
+
+                var song = new Song
+                {
+                    Id = songNode["mid"]?.GetValue<string>() ?? string.Empty,
+                    Name = songNode["name"]?.GetValue<string>() ?? string.Empty,
+                    Artists = artists,
+                    AlbumName = songNode["album"]?["title"]?.GetValue<string>() ?? string.Empty,
+                    LyricId = songNode["mid"]?.GetValue<string>() ?? string.Empty
+                };
+
+                results.Add(song);
+            }
+        }
+
+        return results;
+    }
+
+    public async Task<Song?> GetSongAsync(string id, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var url = "https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg";
+        var parameters = new Dictionary<string, string>
+        {
+            ["songmid"] = id,
+            ["platform"] = "yqq",
+            ["format"] = "json"
+        };
+
+        var response = await httpClient.SendMusicApiRequestAsync(HttpMethod.Get, url, parameters, BuildHeaders(), cancellationToken).ConfigureAwait(false);
+        if (string.IsNullOrWhiteSpace(response))
+        {
+            return null;
+        }
+
+        var json = JsonNode.Parse(response);
+        var songNode = json?["data"]?[0];
+        if (songNode == null)
+        {
+            return null;
+        }
+
+        var artists = songNode["singer"]?.AsArray()
+            .Select(a => a?["name"]?.GetValue<string>())
+            .Where(name => !string.IsNullOrWhiteSpace(name))
+            .Cast<string>()
+            .ToList() ?? [];
+
+        var albumMid = songNode["album"]?["mid"]?.GetValue<string>() ?? string.Empty;
+
+        return new Song
+        {
+            Id = songNode["mid"]?.GetValue<string>() ?? string.Empty,
+            Name = songNode["name"]?.GetValue<string>() ?? string.Empty,
+            Artists = artists,
+            AlbumName = songNode["album"]?["title"]?.GetValue<string>() ?? string.Empty,
+            LyricId = songNode["mid"]?.GetValue<string>() ?? string.Empty,
+            Picture = !string.IsNullOrEmpty(albumMid) ? $"https://y.gtimg.cn/music/photo_new/T002R300x300M000{albumMid}.jpg" : string.Empty
+        };
+    }
+
+    public async Task<SongResource?> GetSongResourceAsync(string id, int br = 320, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var guid = "10000";
+        var uin = "0";
+
+        var qualityMap = new[]
+        {
+            new { Br = 999, Prefix = "F000", Ext = ".flac", Name = "flac" },
+            new { Br = 320, Prefix = "M800", Ext = ".mp3", Name = "320" },
+            new { Br = 192, Prefix = "C600", Ext = ".m4a", Name = "aac_192" },
+            new { Br = 128, Prefix = "M500", Ext = ".mp3", Name = "128" },
+            new { Br = 96, Prefix = "C400", Ext = ".m4a", Name = "aac_96" },
+            new { Br = 48, Prefix = "C200", Ext = ".m4a", Name = "aac_48" }
+        };
+
+        // Build request payload for single quality level
+        foreach (var quality in qualityMap.Where(q => q.Br <= br).OrderByDescending(q => q.Br))
+        {
+            // Reference: https://github.com/Suxiaoqinx/tencent_url
+            // !!! IMPORTANT: filename format is {prefix}{songmid}{songmid}{ext}
+            var filename = $"{quality.Prefix}{id}{id}{quality.Ext}";
+
+            var payload = new
+            {
+                req_1 = new
+                {
+                    module = "vkey.GetVkeyServer",
+                    method = "CgiGetVkey",
+                    param = new
+                    {
+                        filename = new[] { filename },
+                        guid = guid,
+                        songmid = new[] { id },
+                        songtype = new[] { 0 },
+                        uin = uin,
+                        loginflag = 1,
+                        platform = "20"
+                    }
+                },
+                loginUin = uin,
+                comm = new
+                {
+                    uin = uin,
+                    format = "json",
+                    ct = 24,
+                    cv = 0
+                }
+            };
+
+            var vkeyUrl = "https://u.y.qq.com/cgi-bin/musicu.fcg";
+            var jsonPayload = JsonSerializer.Serialize(payload);
+
+            var headers = BuildHeaders();
+            headers["Content-Type"] = "application/json";
+
+            using var request = new HttpRequestMessage(HttpMethod.Post, vkeyUrl)
+            {
+                Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json")
+            };
+
+            foreach (var (key, value) in headers)
+            {
+                request.Headers.TryAddWithoutValidation(key, value);
+            }
+
+            using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+            if (!response.IsSuccessStatusCode)
+            {
+                continue;
+            }
+
+            var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+            if (string.IsNullOrWhiteSpace(responseContent))
+            {
+                continue;
+            }
+
+            var vkeyJson = JsonNode.Parse(responseContent);
+            var midurlinfo = vkeyJson?["req_1"]?["data"]?["midurlinfo"]?.AsArray();
+            var sip = vkeyJson?["req_1"]?["data"]?["sip"]?.AsArray();
+
+            if (midurlinfo == null || midurlinfo.Count == 0 || sip == null || sip.Count == 0)
+            {
+                continue;
+            }
+
+            var purl = midurlinfo[0]?["purl"]?.GetValue<string>();
+            if (string.IsNullOrEmpty(purl))
+            {
+                // This quality not available, try next one
+                continue;
+            }
+
+            var sipUrl = sip[1]?.GetValue<string>() ?? sip[0]?.GetValue<string>() ?? string.Empty;
+            var fullUrl = (sipUrl + purl).Replace("http://", "https://");
+
+            return new SongResource
+            {
+                Url = fullUrl,
+                Type = quality.Ext.TrimStart('.'),
+                Br = quality.Br,
+                Size = 0,
+                Duration = 0
+            };
+        }
+
+        return null;
+    }
+
+    public async Task<Lyric?> GetLyricAsync(string id, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+        ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
+        var url = "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg";
+        var parameters = new Dictionary<string, string>
+        {
+            ["songmid"] = id,
+            ["g_tk"] = "5381"
+        };
+
+        var response = await httpClient.SendMusicApiRequestAsync(HttpMethod.Get, url, parameters, BuildHeaders(), cancellationToken).ConfigureAwait(false);
+        if (string.IsNullOrWhiteSpace(response))
+        {
+            return null;
+        }
+
+        // Remove JSONP wrapper: MusicJsonCallback(...)
+        var jsonStr = response.Trim();
+        if (jsonStr.StartsWith("MusicJsonCallback("))
+        {
+            jsonStr = jsonStr[18..^1];
+        }
+
+        var json = JsonNode.Parse(jsonStr);
+        var lyricBase64 = json?["lyric"]?.GetValue<string>();
+        var transBase64 = json?["trans"]?.GetValue<string>();
+
+        if (string.IsNullOrWhiteSpace(lyricBase64))
+        {
+            return null;
+        }
+
+        var originalLyric = DecodeBase64AndHtmlEntities(lyricBase64);
+        var translatedLyric = !string.IsNullOrWhiteSpace(transBase64) ? DecodeBase64AndHtmlEntities(transBase64) : string.Empty;
+
+        return new Lyric
+        {
+            HasTimeInfo = TimeInfoRegex().IsMatch(originalLyric),
+            OriginalLyric = originalLyric,
+            TranslatedLyric = translatedLyric
+        };
+    }
+
+    public async Task<string?> GetPictureAsync(string id, int px = 300, CancellationToken cancellationToken = default)
+    {
+        ObjectDisposedException.ThrowIf(disposed, this);
+
+        var song = await GetSongAsync(id, cancellationToken).ConfigureAwait(false);
+        if (song == null || string.IsNullOrWhiteSpace(song.Picture))
+        {
+            return string.Empty;
+        }
+
+        return song.Picture.Replace("300x300", $"{px}x{px}");
+    }
+
+    private static string DecodeBase64AndHtmlEntities(string base64String)
+    {
+        var bytes = Convert.FromBase64String(base64String);
+        var text = Encoding.UTF8.GetString(bytes);
+
+        return HttpUtility.HtmlDecode(text);
+    }
+
+    public void Dispose()
+    {
+        if (disposed)
+        {
+            return;
+        }
+
+        disposed = true;
+
+        if (disposedHttpClient)
+        {
+            httpClient?.Dispose();
+        }
+
+        GC.SuppressFinalize(this);
+    }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 6496354..621998a 100644
--- a/README.md
+++ b/README.md
@@ -1,75 +1,210 @@
 # NET-MusicAPI
 
-## 环境要求
-- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) (≥13.0.3)
-- .NET 8.0+ (测试环境)
+[![.NET](https://img.shields.io/badge/.NET-10.0-blue.svg)](https://dotnet.microsoft.com/)
+[![C#](https://img.shields.io/badge/C%23-12-green.svg)](https://docs.microsoft.com/en-us/dotnet/csharp/)
+[![License](https://img.shields.io/badge/license-MIT-orange.svg)](LICENSE)
+
+现代化的 .NET 音乐 API 库，支持网易云音乐、QQ 音乐和 Spotify。使用 .NET 10 和 C# 12 最新特性构建，提供简洁优雅的 API 接口。
+
+## ✨ 支持平台
+
+- ✅ **网易云音乐 (Netease Cloud Music)** - 完整支持
+- ✅ **QQ 音乐 (Tencent Music)** - 完整支持
+- ⚠️ **Spotify** - 暂时暂停（受限于 API 限制）
+
+## 🚀 快速开始
+
+### 安装
 
-## 快速开始
 ```powershell
-# 克隆本项目
+# 克隆项目
 git clone https://github.com/ELDment/NET-MusicAPI.git
 cd NET-MusicAPI
 
-# 构建类库
+# 构建项目
 dotnet build -c Release
 
-# 运行测试模块
-dotnet test
+# 运行测试
+dotnet test -c Release
 ```
 
-## 使用实例
+### 基本使用
+
+#### 独立使用
+
 ```csharp
-// using MusicAPI;
+using MusicAPI.Netease;
 
-public class Program {
-    static async Task Main(string[] args) {
-        // 声明平台API实例
-        var api = new NeteaseAPI();
-        //var api = new TencentAPI();
+using var api = new NeteaseApi();
 
-        // 设置Headers
-        api.Headers = new Dictionary<string, string> { { "special", "xxx" } /*, { "Cookie", "xxx" }*/ };
+// 搜索歌曲
+var songs = await api.SearchAsync("坏女孩", limit: 10);
+foreach (var song in songs!)
+{
+    Console.WriteLine($"{song.Name} - {string.Join(", ", song.Artists)}");
+}
 
-        // 搜索歌曲
-        var search = await api.Search("Avid", limit: 5);
-        var song = search[0]!;
-        Console.WriteLine(song);
+// 获取歌曲详情
+var songDetail = await api.GetSongAsync(songs.First().Id);
+Console.WriteLine($"歌曲：{songDetail!.Name}");
+Console.WriteLine($"封面：{songDetail.Picture}");
+
+// 获取歌曲资源（播放链接）
+var resource = await api.GetSongResourceAsync(songs.First().Id, br: 320);
+if (!string.IsNullOrEmpty(resource?.Url))
+{
+    Console.WriteLine($"播放链接：{resource.Url}");
+    Console.WriteLine($"比特率：{resource.Br}kbps");
+}
 
-        // 获取歌曲信息
-        var songInfo = await api.GetSong(song!.Id);
-        Console.WriteLine(songInfo);
+// 获取歌词
+var lyric = await api.GetLyricAsync(songs.First().Id);
+Console.WriteLine($"原文歌词：{lyric!.OriginalLyric}");
+if (!string.IsNullOrEmpty(lyric.TranslatedLyric))
+{
+    Console.WriteLine($"翻译歌词：{lyric.TranslatedLyric}");
+}
 
-        // 获取歌曲资源
-        var songResource = await api.GetSongResource(song!.Id);
-        Console.WriteLine(songResource);
+// 获取封面图片
+var pictureUrl = await api.GetPictureAsync(songs.First().Id, px: 300);
+Console.WriteLine($"封面链接：{pictureUrl}");
+```
 
-        // 获取歌词
-        var songLyric = await api.GetLyric(song!.Id);
-        Console.WriteLine(songLyric);
+#### Spotify 配置
 
-        // 获取歌曲头图
-        var songPicture = await api.GetPicture(song!.Id, 520);
-        Console.WriteLine(songPicture);
+⚠️ *由于严格的API限制，暂时暂停接口支持*
 
-        return;
+```csharp
+// 方式 1: 构造函数传参
+using var api = new SpotifyApi("...", "...");
+
+// 方式 2: 环境变量
+Environment.SetEnvironmentVariable("SPOTIFY_CLIENT_ID", "...");
+Environment.SetEnvironmentVariable("SPOTIFY_CLIENT_SECRET", "...");
+using var api = new SpotifyApi();
+
+// 方式 3: 自定义 Headers
+using var api = new SpotifyApi();
+api.CustomHeaders = new Dictionary<string, string>
+{
+    ["SpotifyClientId"] = "...",
+    ["SpotifyClientSecret"] = "..."
+};
+```
+
+#### 依赖注入（ASP.NET Core）
+
+```csharp
+using MusicAPI.Extensions;
+using MusicAPI.Abstractions;
+
+// 注册服务
+// 网易云
+builder.Services.AddNeteaseApi();
+
+// QQ（腾讯）音乐
+builder.Services.AddTencentApi();
+
+// Spotify
+builder.Services.AddSpotifyApi();
+
+// 在控制器或服务中使用
+public class MusicController : ControllerBase
+{
+    private readonly IMusicApi musicApi;
+
+    public MusicController(IMusicApi musicApi)
+    {
+        this.musicApi = musicApi;
+    }
+
+    [HttpGet("search")]
+    public async Task<IActionResult> Search(string keyword, int limit = 10)
+    {
+        var results = await musicApi.SearchAsync(keyword, limit: limit);
+        return Ok(results);
+    }
+
+    [HttpGet("song/{id}")]
+    public async Task<IActionResult> GetSong(string id)
+    {
+        var song = await musicApi.GetSongAsync(id);
+        return Ok(song);
     }
 }
 ```
 
-## 更多用法
- - [网易云的Headers设置](https://github.com/metowolf/Meting/wiki/special-for-netease)
+### 高级用法
 
-## 相关项目
- - [metowolf/Meting](https://github.com/metowolf/Meting)
- - [ELDment/Meting-Fixed](https://github.com/ELDment/Meting-Fixed)
+#### 自定义请求头
 
-## 贡献
-- **[samyycX](https://github.com/samyycX)**
+```csharp
+using var api = new NeteaseApi();
 
-## 关键词
+// 设置自定义 Headers
+api.CustomHeaders = new Dictionary<string, string>
+{
+    ["User-Agent"] = "...",
+    ["Cookie"] = "..."
+};
+
+var songs = await api.SearchAsync("星光就在前方");
+```
+
+#### 使用外部 HttpClient
+
+```csharp
+// 如果需要自己管理 HttpClient
+var httpClient = new HttpClient
+{
+    Timeout = TimeSpan.FromSeconds(60)
+};
+
+using var api = new NeteaseApi(httpClient);
+var songs = await api.SearchAsync("裙摆与向日葵花");
+```
+
+## 🧪 测试
+
+### 运行单元测试
+
+```powershell
+# 运行所有测试
+dotnet test
+
+# 运行测试并显示详细输出
+dotnet test --logger "console;verbosity=detailed"
+
+# 运行特定测试
+dotnet test --filter "FullyQualifiedName~NeteaseApiTests"
 ```
-网易云音乐[搜索|直链|歌词|封面|详情|专辑|歌单|歌手]API
-QQ音乐[搜索|直链|歌词|封面|详情|专辑|歌单|歌手]API
-Netease [Search|URL|Stream|Lyrics|Cover|Details|Album|Playlist|Artist] API
-QQ Music [Search|URL|Stream|Lyrics|Cover|Details|Album|Playlist|Artist] API
+
+### 使用测试脚本
+
+```powershell
+.\run-tests.ps1
+
+# 测试特定平台
+.\run-tests.ps1 -Platform netease
+.\run-tests.ps1 -Platform tencent
+.\run-tests.ps1 -Platform spotify
 ```
+
+## 🤝 贡献
+
+### 贡献者
+
+- **[electronix](https://github.com/samyycX)** - 项目原始贡献者
+
+## 📄 许可证
+
+本项目采用 [MIT](LICENSE) 许可证
+
+## 🔗 相关项目
+
+- [metowolf/Meting](https://github.com/metowolf/Meting)
+- [ELDment/Meting-Fixed](https://github.com/ELDment/Meting-Fixed)
+
+---
+
+**关键词**: .NET Music API | C# Music Library | Netease Cloud Music | Tencent QQ Music | Spotify
diff --git a/run-tests.ps1.example b/run-tests.ps1.example
new file mode 100644
index 0000000..bc7941b
--- /dev/null
+++ b/run-tests.ps1.example
@@ -0,0 +1,42 @@
+# Test Runner for NET-MusicAPI
+# Copy this file to run-tests.ps1 and fill in your credentials
+
+param(
+    [Parameter(Mandatory=$false)]
+    [Alias("p")]
+    [ValidateSet("netease", "tencent", "spotify", "")]
+    [string]$Platform = ""
+)
+
+# Get your Spotify credentials from: https://developer.spotify.com/dashboard
+$env:SPOTIFY_CLIENT_ID = "..."
+$env:SPOTIFY_CLIENT_SECRET = "..."
+
+Write-Host "=== NET-MusicAPI Tests ===" -ForegroundColor Yellow
+Write-Host "Spotify Client ID: $env:SPOTIFY_CLIENT_ID" -ForegroundColor Cyan
+Write-Host "Spotify Client Secret: $env:SPOTIFY_CLIENT_SECRET" -ForegroundColor Cyan
+Write-Host ""
+
+# Determine test filter
+if ($Platform -ne "") {
+    $testClass = switch ($Platform.ToLower()) {
+        "netease" { "NeteaseApiTests" }
+        "tencent" { "TencentApiTests" }
+        "spotify" { "SpotifyApiTests" }
+    }
+    Write-Host "Running tests for platform: $Platform ($testClass)" -ForegroundColor Green
+    Write-Host ""
+    
+    # Run tests with filter
+    Set-Location NET-MusicAPI.Tests
+    dotnet test --filter "FullyQualifiedName~$testClass" --logger "console;verbosity=detailed"
+    Set-Location ..
+} else {
+    Write-Host "Running all tests" -ForegroundColor Green
+    Write-Host ""
+    
+    # Run all tests
+    Set-Location NET-MusicAPI.Tests
+    dotnet test --logger "console;verbosity=detailed"
+    Set-Location ..
+}
\ No newline at end of file
