diff --git a/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs b/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs
index 47c9179..7b30270 100644
--- a/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs
+++ b/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs
@@ -5,7 +5,6 @@ using ZonyLrcTools.Common.Album;
using ZonyLrcTools.Common.Lyrics;
// ReSharper disable UnusedAutoPropertyAccessor.Global
-
// ReSharper disable MemberCanBePrivate.Global
namespace ZonyLrcTools.Cli.Commands.SubCommand
diff --git a/src/ZonyLrcTools.Cli/config.yaml b/src/ZonyLrcTools.Cli/config.yaml
index 2592637..397f31a 100644
--- a/src/ZonyLrcTools.Cli/config.yaml
+++ b/src/ZonyLrcTools.Cli/config.yaml
@@ -39,10 +39,13 @@ globalOption:
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
- name: QQ # 基于 QQ 音乐的歌词下载器。
priority: 2
- # depth: 10 # 暂时不支持。
+ # depth: 10 # 暂时不支持。
- name: KuGou # 基于酷狗音乐的歌词下载器。
priority: 3
depth: 10
+ - name: KuWo # 基于酷我音乐的歌词下载器。
+ priority: 4
+ depth: 10
# 歌词下载的一些共有配置参数。
config:
isOneLine: true # 双语歌词是否合并为一行展示。
diff --git a/src/ZonyLrcTools.Common/Infrastructure/Exceptions/ErrorCodeException.cs b/src/ZonyLrcTools.Common/Infrastructure/Exceptions/ErrorCodeException.cs
index 2e99df8..4303429 100644
--- a/src/ZonyLrcTools.Common/Infrastructure/Exceptions/ErrorCodeException.cs
+++ b/src/ZonyLrcTools.Common/Infrastructure/Exceptions/ErrorCodeException.cs
@@ -15,7 +15,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
/// 错误码,参考 类的定义。
/// 错误信息。
/// 附加的对象数据。
- public ErrorCodeException(int errorCode, string message = null, object attachObj = null) : base(message)
+ public ErrorCodeException(int errorCode, string? message = null, object attachObj = null) : base(message)
{
ErrorCode = errorCode;
AttachObject = attachObj;
diff --git a/src/ZonyLrcTools.Common/Lyrics/InternalLyricsProviderNames.cs b/src/ZonyLrcTools.Common/Lyrics/InternalLyricsProviderNames.cs
index 985057e..9f30373 100644
--- a/src/ZonyLrcTools.Common/Lyrics/InternalLyricsProviderNames.cs
+++ b/src/ZonyLrcTools.Common/Lyrics/InternalLyricsProviderNames.cs
@@ -19,5 +19,10 @@ namespace ZonyLrcTools.Common.Lyrics
/// 酷狗音乐歌词下载器。
///
public const string KuGou = nameof(KuGou);
+
+ ///
+ /// 酷我音乐歌词下载器。
+ ///
+ public const string KuWo = nameof(KuWo);
}
}
\ No newline at end of file
diff --git a/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/GetLyricsRequest.cs b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/GetLyricsRequest.cs
new file mode 100644
index 0000000..49f3117
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/GetLyricsRequest.cs
@@ -0,0 +1,13 @@
+using Newtonsoft.Json;
+
+namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
+
+public class GetLyricsRequest
+{
+ [JsonProperty("musicId")] public long MusicId { get; }
+
+ public GetLyricsRequest(long musicId)
+ {
+ MusicId = musicId;
+ }
+}
\ No newline at end of file
diff --git a/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/GetLyricsResponse.cs b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/GetLyricsResponse.cs
new file mode 100644
index 0000000..4b787bc
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/GetLyricsResponse.cs
@@ -0,0 +1,21 @@
+using Newtonsoft.Json;
+
+namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
+
+public class GetLyricsResponse
+{
+ [JsonProperty("status")] public int Status { get; set; }
+
+ [JsonProperty("lrclist")] public ICollection Lyrics { get; set; }
+
+ [JsonProperty("msg")] public string? ErrorMessage { get; set; }
+
+ [JsonProperty("msgs")] public string? ErrorMessage2 { get; set; }
+}
+
+public class GetLyricsItem
+{
+ [JsonProperty("lineLyric")] public string Text { get; set; }
+
+ [JsonProperty("time")] public string Position { get; set; }
+}
\ No newline at end of file
diff --git a/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/SongSearchRequest.cs b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/SongSearchRequest.cs
new file mode 100644
index 0000000..2d1c285
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/SongSearchRequest.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+
+namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
+
+public class SongSearchRequest
+{
+ [JsonProperty("key")] public string Keyword { get; set; }
+
+ [JsonProperty("pn")] public int PageNumber { get; }
+
+ [JsonProperty("rn")] public int PageSize { get; }
+
+ public SongSearchRequest(string name, string artist, int pageNumber = 1, int pageSize = 20)
+ {
+ Keyword = $"{name} {artist}";
+ PageNumber = pageNumber;
+ PageSize = pageSize;
+ }
+}
\ No newline at end of file
diff --git a/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/SongSearchResponse.cs b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/SongSearchResponse.cs
new file mode 100644
index 0000000..8002b19
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/JsonModel/SongSearchResponse.cs
@@ -0,0 +1,46 @@
+using Newtonsoft.Json;
+
+namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
+
+public class SongSearchResponse
+{
+ [JsonProperty("code")] public int Code { get; set; }
+
+ [JsonProperty("data")] public SongSearchResponseInnerData InnerData { get; set; }
+
+ [JsonProperty("msg")] public string? ErrorMessage { get; set; }
+
+ public long GetMatchedMusicId(string musicName, string artistName, long? duration)
+ {
+ var prefectMatch = InnerData.SongItems.FirstOrDefault(x => x.Name == musicName && x.Artist == artistName);
+ if (prefectMatch != null)
+ {
+ return prefectMatch.MusicId;
+ }
+
+ if (duration is null or 0)
+ {
+ return InnerData.SongItems.First().MusicId;
+ }
+
+ return InnerData.SongItems.OrderBy(t => Math.Abs(t.Duration - duration.Value)).First().MusicId;
+ }
+}
+
+public class SongSearchResponseInnerData
+{
+ [JsonProperty("total")] public string Total { get; set; }
+
+ [JsonProperty("list")] public ICollection SongItems { get; set; }
+}
+
+public class SongSearchResponseDetail
+{
+ [JsonProperty("artist")] public string? Artist { get; set; }
+
+ [JsonProperty("name")] public string? Name { get; set; }
+
+ [JsonProperty("rid")] public long MusicId { get; set; }
+
+ [JsonProperty("duration")] public long Duration { get; set; }
+}
\ No newline at end of file
diff --git a/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/KuWoLyricsProvider.cs b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/KuWoLyricsProvider.cs
new file mode 100644
index 0000000..13c582a
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Lyrics/Providers/KuWo/KuWoLyricsProvider.cs
@@ -0,0 +1,72 @@
+using System.Net.Http.Headers;
+using System.Text;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
+using ZonyLrcTools.Common.Configuration;
+using ZonyLrcTools.Common.Infrastructure.Exceptions;
+using ZonyLrcTools.Common.Infrastructure.Network;
+using ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
+
+namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo;
+
+public class KuWoLyricsProvider : LyricsProvider
+{
+ public override string DownloaderName => InternalLyricsProviderNames.KuWo;
+
+ private const string KuWoSearchMusicUrl = @"https://www.kuwo.cn/api/www/search/searchMusicBykeyWord";
+ private const string KuWoSearchLyricsUrl = @"https://m.kuwo.cn/newh5/singles/songinfoandlrc";
+ private const string KuWoDefaultToken = "ABCDE12345";
+
+ private readonly IWarpHttpClient _warpHttpClient;
+ private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
+ private readonly GlobalOptions _options;
+
+ private static readonly ProductInfoHeaderValue UserAgent = new("Chrome", "81.0.4044.138");
+
+ public KuWoLyricsProvider(IWarpHttpClient warpHttpClient,
+ ILyricsItemCollectionFactory lyricsItemCollectionFactory,
+ IOptions options)
+ {
+ _warpHttpClient = warpHttpClient;
+ _lyricsItemCollectionFactory = lyricsItemCollectionFactory;
+ _options = options.Value;
+ }
+
+ protected override async ValueTask DownloadDataAsync(LyricsProviderArgs args)
+ {
+ var songSearchResponse = await _warpHttpClient.GetAsync(KuWoSearchMusicUrl,
+ new SongSearchRequest(args.SongName, args.Artist, pageSize: _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
+ op =>
+ {
+ op.Headers.UserAgent.Add(UserAgent);
+ op.Headers.Referrer = new Uri("https://kuwo.cn");
+ op.Headers.Add("csrf", KuWoDefaultToken);
+ op.Headers.Add("Cookie", $"kw_token={KuWoDefaultToken}");
+ });
+
+ ValidateSongSearchResponse(songSearchResponse, args);
+
+ var songLyricsResponse = await _warpHttpClient.GetAsync(KuWoSearchLyricsUrl,
+ new GetLyricsRequest(songSearchResponse.GetMatchedMusicId(args.SongName, args.Artist, args.Duration)),
+ op =>
+ {
+ op.Headers.UserAgent.Add(UserAgent);
+ op.Headers.Referrer = new Uri("https://m.kuwo.cn/yinyue/");
+ });
+
+ return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(songLyricsResponse.Lyrics));
+ }
+
+ protected override ValueTask GenerateLyricAsync(byte[] data, LyricsProviderArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
+ {
+ if (response.Code != 200)
+ {
+ throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, response.ErrorMessage, args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/KuGouLyricDownloaderTests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/KuGouLyricProviderTests.cs
similarity index 81%
rename from tests/ZonyLrcTools.Tests/Infrastructure/Lyric/KuGouLyricDownloaderTests.cs
rename to tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/KuGouLyricProviderTests.cs
index 273661e..e302cee 100644
--- a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/KuGouLyricDownloaderTests.cs
+++ b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/KuGouLyricProviderTests.cs
@@ -5,13 +5,13 @@ using Shouldly;
using Xunit;
using ZonyLrcTools.Common.Lyrics;
-namespace ZonyLrcTools.Tests.Infrastructure.Lyric
+namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
{
- public class KuGouLyricDownloaderTests : TestBase
+ public class KuGouLyricProviderTests : TestBase
{
private readonly ILyricsProvider _lyricsProvider;
- public KuGouLyricDownloaderTests()
+ public KuGouLyricProviderTests()
{
_lyricsProvider = GetService>()
.FirstOrDefault(t => t.DownloaderName == InternalLyricsProviderNames.KuGou);
diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/KuWoLyricsProviderTests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/KuWoLyricsProviderTests.cs
new file mode 100644
index 0000000..d734897
--- /dev/null
+++ b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/KuWoLyricsProviderTests.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Shouldly;
+using Xunit;
+using ZonyLrcTools.Common.Lyrics;
+
+namespace ZonyLrcTools.Tests.Infrastructure.Lyrics;
+
+public class KuWoLyricsProviderTests : TestBase
+{
+ private readonly ILyricsProvider _kuwoLyricsProvider;
+
+ public KuWoLyricsProviderTests()
+ {
+ _kuwoLyricsProvider = GetService>()
+ .FirstOrDefault(t => t.DownloaderName == InternalLyricsProviderNames.KuWo);
+ }
+
+ [Fact]
+ public async Task DownloadAsync_Test()
+ {
+ var lyric = await _kuwoLyricsProvider.DownloadAsync("告白气球", "周杰伦");
+ lyric.ShouldNotBeNull();
+ lyric.IsPruneMusic.ShouldBeFalse();
+ }
+}
\ No newline at end of file
diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/LyricCollectionTests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/LyricCollectionTests.cs
similarity index 92%
rename from tests/ZonyLrcTools.Tests/Infrastructure/Lyric/LyricCollectionTests.cs
rename to tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/LyricCollectionTests.cs
index 3603c02..ff75e8d 100644
--- a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/LyricCollectionTests.cs
+++ b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/LyricCollectionTests.cs
@@ -3,7 +3,7 @@ using Xunit;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Lyrics;
-namespace ZonyLrcTools.Tests.Infrastructure.Lyric
+namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
{
public class LyricCollectionTests : TestBase
{
diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/NetEaseLyricDownloaderTests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/NetEaseLyricsProviderTests.cs
similarity index 95%
rename from tests/ZonyLrcTools.Tests/Infrastructure/Lyric/NetEaseLyricDownloaderTests.cs
rename to tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/NetEaseLyricsProviderTests.cs
index 670f95c..83402b6 100644
--- a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/NetEaseLyricDownloaderTests.cs
+++ b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/NetEaseLyricsProviderTests.cs
@@ -8,13 +8,13 @@ using Xunit;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Lyrics;
-namespace ZonyLrcTools.Tests.Infrastructure.Lyric
+namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
{
- public class NetEaseLyricDownloaderTests : TestBase
+ public class NetEaseLyricsProviderTests : TestBase
{
private readonly ILyricsProvider _lyricsProvider;
- public NetEaseLyricDownloaderTests()
+ public NetEaseLyricsProviderTests()
{
_lyricsProvider = GetService>()
.FirstOrDefault(t => t.DownloaderName == InternalLyricsProviderNames.NetEase);
diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/QQLyricDownloaderTests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/QQLyricsProviderTests.cs
similarity index 89%
rename from tests/ZonyLrcTools.Tests/Infrastructure/Lyric/QQLyricDownloaderTests.cs
rename to tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/QQLyricsProviderTests.cs
index 45e0ed6..d482404 100644
--- a/tests/ZonyLrcTools.Tests/Infrastructure/Lyric/QQLyricDownloaderTests.cs
+++ b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/QQLyricsProviderTests.cs
@@ -5,13 +5,13 @@ using Shouldly;
using Xunit;
using ZonyLrcTools.Common.Lyrics;
-namespace ZonyLrcTools.Tests.Infrastructure.Lyric
+namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
{
- public class QQLyricDownloaderTests : TestBase
+ public class QQLyricsProviderTests : TestBase
{
private readonly ILyricsProvider _lyricsProvider;
- public QQLyricDownloaderTests()
+ public QQLyricsProviderTests()
{
_lyricsProvider = GetService>()
.FirstOrDefault(t => t.DownloaderName == InternalLyricsProviderNames.QQ);
diff --git a/versions/release.bak.md b/versions/release.bak.md
index 1899d66..8a220d6 100644
--- a/versions/release.bak.md
+++ b/versions/release.bak.md
@@ -1,6 +1,7 @@
New Features:
- CLI 提供了一个更新检查功能,在程序启动的时候会检测最新的版本。
+- 支持从酷我音乐搜索歌词。
Breaking Changes: None
Enhancement: