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: