refactor: Common components are moved to the Common library.

This commit is contained in:
real-zony
2022-10-06 13:02:20 +08:00
parent ecab0e0f5c
commit 740e8f4c63
64 changed files with 84 additions and 150 deletions

View File

@@ -0,0 +1,22 @@
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 歌词数据下载器,用于匹配并下载歌曲的歌词。
/// </summary>
public interface ILyricDownloader
{
/// <summary>
/// 下载歌词数据。
/// </summary>
/// <param name="songName">歌曲的名称。</param>
/// <param name="artist">歌曲的作者。</param>
/// <param name="duration">歌曲的时长。</param>
/// <returns>歌曲的歌词数据对象。</returns>
ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
/// <summary>
/// 下载器的名称。
/// </summary>
string DownloaderName { get; }
}
}

View File

@@ -0,0 +1,23 @@
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 构建 <see cref="LyricItemCollection"/> 对象的工厂。
/// </summary>
public interface ILyricItemCollectionFactory
{
/// <summary>
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
/// </summary>
/// <param name="sourceLyric">原始歌词数据。</param>
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
LyricItemCollection Build(string sourceLyric);
/// <summary>
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
/// </summary>
/// <param name="sourceLyric">原始歌词数据。</param>
/// <param name="translationLyric">翻译歌词数据。</param>
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
LyricItemCollection Build(string sourceLyric, string translationLyric);
}
}

View File

@@ -0,0 +1,7 @@
namespace ZonyLrcTools.Common.Lyrics
{
public interface ILyricTextResolver
{
LyricItemCollection Resolve(string lyricText);
}
}

View File

@@ -0,0 +1,23 @@
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 定义了程序默认提供的歌词下载器。
/// </summary>
public static class InternalLyricDownloaderNames
{
/// <summary>
/// 网易云音乐歌词下载器。
/// </summary>
public const string NetEase = nameof(NetEase);
/// <summary>
/// QQ 音乐歌词下载器。
/// </summary>
public const string QQ = nameof(QQ);
/// <summary>
/// 酷狗音乐歌词下载器。
/// </summary>
public const string KuGou = nameof(KuGou);
}
}

View File

@@ -0,0 +1,58 @@
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 歌词下载器的基类,定义了歌词下载器的常规逻辑。
/// </summary>
public abstract class LyricDownloader : ILyricDownloader, ITransientDependency
{
public abstract string DownloaderName { get; }
/// <summary>
/// 歌词数据下载的核心逻辑。
/// </summary>
/// <param name="songName">歌曲名称。</param>
/// <param name="artist">歌曲作者/艺术家。</param>
/// <param name="duration">歌曲的时长。</param>
/// <returns>下载完成的歌曲数据。</returns>
public virtual async ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist, long? duration = null)
{
var args = new LyricDownloaderArgs(songName, artist, duration ?? 0);
await ValidateAsync(args);
var downloadDataBytes = await DownloadDataAsync(args);
return await GenerateLyricAsync(downloadDataBytes, args);
}
/// <summary>
/// 通用的验证逻辑,验证基本参数是否正确。
/// </summary>
/// <param name="args">歌词下载时需要的参数信息。</param>
protected virtual ValueTask ValidateAsync(LyricDownloaderArgs args)
{
if (string.IsNullOrEmpty(args.SongName))
{
throw new ErrorCodeException(ErrorCodes.SongNameIsNull, attachObj: args);
}
if (string.IsNullOrEmpty(args.SongName) && string.IsNullOrEmpty(args.Artist))
{
throw new ErrorCodeException(ErrorCodes.SongNameAndArtistIsNull, attachObj: args);
}
return ValueTask.CompletedTask;
}
/// <summary>
/// 根据指定的歌曲参数,下载歌词数据。
/// </summary>
protected abstract ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args);
/// <summary>
/// 根据指定的歌词二进制数据,生成歌词数据。
/// </summary>
/// <param name="data">歌词的原始二进制数据。</param>
protected abstract ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args);
}
}

View File

@@ -0,0 +1,18 @@
namespace ZonyLrcTools.Common.Lyrics
{
public class LyricDownloaderArgs
{
public string SongName { get; set; }
public string Artist { get; set; }
public long Duration { get; set; }
public LyricDownloaderArgs(string songName, string artist, long duration)
{
SongName = songName;
Artist = artist;
Duration = duration;
}
}
}

View File

@@ -0,0 +1,128 @@
using System.Text.RegularExpressions;
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 歌词的行对象,是 <see cref="LyricItemCollection"/> 的最小单位。。
/// </summary>
public class LyricItem : IComparable<LyricItem>
{
/// <summary>
/// 原始时间轴,格式类似于 [01:55.12]。
/// </summary>
public string OriginalTimeline => $"[{Minute:00}:{Second:00.00}]";
/// <summary>
/// 歌词文本数据。
/// </summary>
public string LyricText { get; }
/// <summary>
/// 歌词所在的时间(分)。
/// </summary>
public int Minute { get; }
/// <summary>
/// 歌词所在的时间(秒)。
/// </summary>
public double Second { get; }
/// <summary>
/// 排序分数,用于一组歌词当中的排序权重。<br/>
/// </summary>
public double SortScore => Minute * 60 + Second;
/// <summary>
/// 构建新的 <see cref="LyricItem"/> 对象。
/// </summary>
/// <param name="lyricText">原始的 Lyric 歌词。</param>
public LyricItem(string lyricText)
{
var timeline = new Regex(@"\[\d+:\d+.\d+\]").Match(lyricText)
.Value.Replace("]", string.Empty)
.Replace("[", string.Empty)
.Split(':');
if (int.TryParse(timeline[0], out var minute)) Minute = minute;
if (double.TryParse(timeline[1], out var second)) Second = second;
LyricText = new Regex(@"(?<=\[\d+:\d+.\d+\]).+").Match(lyricText).Value;
}
/// <summary>
/// 构造新的 <see cref="LyricItem"/> 对象。
/// </summary>
/// <param name="minute">歌词所在的时间(分)。</param>
/// <param name="second">歌词所在的时间(秒)。</param>
/// <param name="lyricText">歌词文本数据。</param>
public LyricItem(int minute, double second, string lyricText)
{
Minute = minute;
Second = second;
LyricText = lyricText;
}
public int CompareTo(LyricItem other)
{
if (SortScore > other.SortScore)
{
return 1;
}
if (SortScore < other.SortScore)
{
return -1;
}
return 0;
}
public static bool operator >(LyricItem left, LyricItem right)
{
return left.SortScore > right.SortScore;
}
public static bool operator <(LyricItem left, LyricItem right)
{
return left.SortScore < right.SortScore;
}
public static bool operator ==(LyricItem left, LyricItem right)
{
return (int?)left?.SortScore == (int?)right?.SortScore;
}
public static bool operator !=(LyricItem item1, LyricItem item2)
{
return !(item1 == item2);
}
public static LyricItem operator +(LyricItem src, LyricItem dist)
{
return new LyricItem(src.Minute, src.Second, $"{src.LyricText} {dist.LyricText}");
}
protected bool Equals(LyricItem other)
{
return LyricText == other.LyricText && Minute == other.Minute && Second.Equals(other.Second);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((LyricItem)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(LyricText, Minute, Second);
}
/// <summary>
/// 获得歌词行的标准文本。
/// </summary>
public override string ToString() => $"[{Minute:00}:{Second:00.00}]{LyricText}";
}
}

View File

@@ -0,0 +1,113 @@
using System.Text;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Extensions;
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 歌词数据,包含多条歌词行对象(<see cref="LyricItem"/>)。
/// </summary>
public class LyricItemCollection : List<LyricItem>
{
/// <summary>
/// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。
/// </summary>
public bool IsPruneMusic => Count == 0;
public GlobalLyricsConfigOptions Options { get; private set; }
public LyricItemCollection(GlobalLyricsConfigOptions options)
{
Options = options;
}
public static LyricItemCollection operator +(LyricItemCollection left, LyricItemCollection right)
{
if (right.IsPruneMusic)
{
return left;
}
var option = left.Options;
var newCollection = new LyricItemCollection(option);
var indexDiff = left.Count - right.Count;
if (!option.IsOneLine)
{
left.ForEach(item => newCollection.Add(item));
right.ForEach(item => newCollection.Add(item));
newCollection.Sort();
return newCollection;
}
// 如果索引相等,直接根据索引快速匹配构建。
if (indexDiff == 0)
{
newCollection.AddRange(left.Select((t, index) => t + right[index]));
return newCollection;
}
// 首先按照时间轴进行合并。
var leftMarkDict = BuildMarkDictionary(left);
var rightMarkDict = BuildMarkDictionary(right);
for (var leftIndex = 0; leftIndex < left.Count; leftIndex++)
{
var rightItem = right.Find(lyric => Math.Abs(lyric.SortScore - left[leftIndex].SortScore) < 0.001);
if (rightItem != null)
{
newCollection.Add(left[leftIndex] + rightItem);
var rightIndex = right.FindIndex(item => item == rightItem);
rightMarkDict[rightIndex] = true;
}
else
{
newCollection.Add(left[leftIndex]);
}
leftMarkDict[leftIndex] = true;
}
// 遍历未处理的歌词项,将其添加到返回集合当中。
var leftWaitProcessIndex = leftMarkDict
.Where(item => !item.Value)
.Select(pair => pair.Key);
var rightWaitProcessIndex = rightMarkDict
.Where(item => !item.Value)
.Select(pair => pair.Key);
leftWaitProcessIndex.Foreach(index => newCollection.Add(left[index]));
rightWaitProcessIndex.Foreach(index => newCollection.Add(right[index]));
newCollection.Sort();
return newCollection;
}
/// <summary>
/// 根据歌词集合构建一个索引状态字典。
/// </summary>
/// <remarks>
/// 这个索引字典用于标识每个索引的歌词是否被处理,为 True 则为已处理,为 False 为未处理。
/// </remarks>
/// <param name="items">等待构建的歌词集合实例。</param>
private static Dictionary<int, bool> BuildMarkDictionary(LyricItemCollection items)
{
return items
.Select((item, index) => new { index, item })
.ToDictionary(item => item.index, item => false);
}
public override string ToString()
{
var lyricBuilder = new StringBuilder();
ForEach(lyric => lyricBuilder.Append(lyric).Append(Options.LineBreak));
return lyricBuilder.ToString().TrimEnd(Options.LineBreak);
}
public byte[] GetUtf8Bytes()
{
return Encoding.UTF8.GetBytes(ToString());
}
}
}

View File

@@ -0,0 +1,68 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// <see cref="ILyricItemCollectionFactory"/> 的默认实现。
/// </summary>
public class LyricItemCollectionFactory : ILyricItemCollectionFactory, ITransientDependency
{
private readonly GlobalOptions _options;
public LyricItemCollectionFactory(IOptions<GlobalOptions> options)
{
_options = options.Value;
}
public LyricItemCollection Build(string sourceLyric)
{
var lyric = new LyricItemCollection(_options.Provider.Lyric.Config);
if (string.IsNullOrEmpty(sourceLyric))
{
return lyric;
}
InternalBuildLyricObject(lyric, sourceLyric);
return lyric;
}
public LyricItemCollection Build(string sourceLyric, string translationLyric)
{
var lyric = new LyricItemCollection(_options.Provider.Lyric.Config);
if (string.IsNullOrEmpty(sourceLyric))
{
return lyric;
}
lyric = InternalBuildLyricObject(lyric, sourceLyric);
if (_options.Provider.Lyric.Config.IsEnableTranslation && !string.IsNullOrEmpty(translationLyric))
{
var translatedLyric = InternalBuildLyricObject(new LyricItemCollection(_options.Provider.Lyric.Config), translationLyric);
if (_options.Provider.Lyric.Config.IsOnlyOutputTranslation)
{
return translatedLyric;
}
return lyric + translatedLyric;
}
return lyric;
}
private LyricItemCollection InternalBuildLyricObject(LyricItemCollection lyric, string sourceText)
{
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
foreach (Match match in regex.Matches(sourceText))
{
lyric.Add(new LyricItem(match.Value));
}
return lyric;
}
}
}

View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class GetLyricAccessKeyRequest
{
[JsonProperty("ver")] public int UnknownParameters1 { get; }
[JsonProperty("man")] public string UnknownParameters2 { get; }
[JsonProperty("client")] public string UnknownParameters3 { get; }
[JsonProperty("hash")] public string FileHash { get; }
public GetLyricAccessKeyRequest(string fileHash)
{
UnknownParameters1 = 1;
UnknownParameters2 = "yes";
UnknownParameters3 = "mobi";
FileHash = fileHash;
}
}
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class GetLyricAccessKeyResponse
{
[JsonProperty("status")] public int Status { get; set; }
[JsonProperty("errcode")] public int ErrorCode { get; set; }
[JsonProperty("candidates")] public List<GetLyricAccessKeyDataObject> AccessKeyDataObjects { get; set; }
}
public class GetLyricAccessKeyDataObject
{
[JsonProperty("accesskey")] public string AccessKey { get; set; }
[JsonProperty("id")] public string Id { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class GetLyricRequest
{
[JsonProperty("ver")] public int UnknownParameters1 { get; }
[JsonProperty("client")] public string UnknownParameters2 { get; }
[JsonProperty("fmt")] public string UnknownParameters3 { get; }
[JsonProperty("charset")] public string UnknownParameters4 { get; }
[JsonProperty("id")] public string Id { get; }
[JsonProperty("accesskey")] public string AccessKey { get; }
public GetLyricRequest(string id, string accessKey)
{
UnknownParameters1 = 1;
UnknownParameters2 = "iphone";
UnknownParameters3 = "lrc";
UnknownParameters4 = "utf8";
Id = id;
AccessKey = accessKey;
}
}
public class GetLyricResponse
{
}
}

View File

@@ -0,0 +1,28 @@
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class SongSearchRequest
{
[JsonProperty("filter")] public int Filter { get; }
[JsonProperty("platform")] public string Platform { get; }
[JsonProperty("keyword")] public string Keyword { get; }
[JsonProperty("pagesize")] public int PageSize { get; }
[JsonProperty("page")] public int Page { get; }
public SongSearchRequest(string musicName, string artistName, int pageSize = 30)
{
Filter = 2;
Platform = "WebFilter";
Keyword = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
PageSize = pageSize;
Page = 1;
}
}
}

View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class SongSearchResponse
{
[JsonProperty("status")] public int Status { get; set; }
[JsonProperty("data")] public SongSearchResponseInnerData Data { get; set; }
[JsonProperty("error_code")] public int ErrorCode { get; set; }
[JsonProperty("error_msg")] public string ErrorMessage { get; set; }
}
public class SongSearchResponseInnerData
{
[JsonProperty("lists")] public List<SongSearchResponseSongDetail> List { get; set; }
}
public class SongSearchResponseSongDetail
{
public string FileHash { get; set; }
}
}

View File

@@ -0,0 +1,71 @@
using System.Text;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
{
public class KuGourLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.KuGou;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
private readonly GlobalOptions _options;
private const string KuGouSearchMusicUrl = @"https://songsearch.kugou.com/song_search_v2";
private const string KuGouGetLyricAccessKeyUrl = @"http://lyrics.kugou.com/search";
private const string KuGouGetLyricUrl = @"http://lyrics.kugou.com/download";
public KuGourLyricDownloader(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory,
IOptions<GlobalOptions> options)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
_options = options.Value;
}
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(KuGouSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth));
ValidateSongSearchResponse(searchResult, args);
// 获得特殊的 AccessToken 与 Id真正请求歌词数据。
var accessKeyResponse = await _warpHttpClient.GetAsync<GetLyricAccessKeyResponse>(KuGouGetLyricAccessKeyUrl,
new GetLyricAccessKeyRequest(searchResult.Data.List[0].FileHash));
var accessKeyObject = accessKeyResponse.AccessKeyDataObjects[0];
var lyricResponse = await _warpHttpClient.GetAsync(KuGouGetLyricUrl,
new GetLyricRequest(accessKeyObject.Id, accessKeyObject.AccessKey));
return Encoding.UTF8.GetBytes(lyricResponse);
}
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args)
{
await ValueTask.CompletedTask;
var lyricJsonObj = JObject.Parse(Encoding.UTF8.GetString(data));
if (lyricJsonObj.SelectToken("$.status").Value<int>() != 200)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
var lyricText = Encoding.UTF8.GetString(Convert.FromBase64String(lyricJsonObj.SelectToken("$.content").Value<string>()));
return _lyricItemCollectionFactory.Build(lyricText);
}
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricDownloaderArgs args)
{
if (response.ErrorCode != 0 && response.Status != 1 || response.Data.List == null)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
}
}
}

View File

@@ -0,0 +1,38 @@
using Newtonsoft.Json;
// ReSharper disable InconsistentNaming
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
public class GetLyricRequest
{
public GetLyricRequest(long songId)
{
OS = "ios";
Id = songId;
Lv = Kv = Tv = Rv = -1;
}
/// <summary>
/// 请求的操作系统。
/// </summary>
[JsonProperty("os")]
public string OS { get; }
/// <summary>
/// 歌曲的 SID 值。
/// </summary>
[JsonProperty("id")]
public long Id { get; }
[JsonProperty("lv")] public int Lv { get; }
[JsonProperty("kv")] public int Kv { get; }
[JsonProperty("tv")] public int Tv { get; }
[JsonProperty("rv")] public int Rv { get; set; }
[JsonProperty("crypto")] public string Protocol { get; set; } = "api";
}
}

View File

@@ -0,0 +1,51 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
public class GetLyricResponse
{
/// <summary>
/// 原始的歌词。
/// </summary>
[JsonProperty("lrc")]
public InnerLyric OriginalLyric { get; set; }
/// <summary>
/// 卡拉 OK 歌词。
/// </summary>
[JsonProperty("klyric")]
public InnerLyric KaraokeLyric { get; set; }
/// <summary>
/// 如果存在翻译歌词,则本项内容为翻译歌词。
/// </summary>
[JsonProperty("tlyric")]
public InnerLyric TranslationLyric { get; set; }
/// <summary>
/// 如果存在罗马音歌词,则本项内容为罗马音歌词。
/// </summary>
[JsonProperty("romalrc")]
public InnerLyric RomaLyric { get; set; }
/// <summary>
/// 状态码。
/// </summary>
[JsonProperty("code")]
public string StatusCode { get; set; }
}
/// <summary>
/// 歌词 JSON 类型
/// </summary>
public class InnerLyric
{
[JsonProperty("version")] public string Version { get; set; }
/// <summary>
/// 具体的歌词数据。
/// </summary>
[JsonProperty("lyric")]
public string Text { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
public class GetSongDetailsRequest
{
public GetSongDetailsRequest(int songId)
{
SongId = songId;
SongIds = $"%5B{songId}%5D";
}
[JsonProperty("id")] public int SongId { get; }
[JsonProperty("ids")] public string SongIds { get; }
}
}

View File

@@ -0,0 +1,67 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
public class SongSearchRequest
{
/// <summary>
/// CSRF 标识,一般为空即可,接口不会进行校验。
/// </summary>
[JsonProperty("csrf_token")]
public string CsrfToken { get; set; }
/// <summary>
/// 需要搜索的内容,一般是歌曲名 + 歌手的格式。
/// </summary>
[JsonProperty("s")]
public string SearchKey { get; set; }
/// <summary>
/// 页偏移量。
/// </summary>
[JsonProperty("offset")]
public int Offset { get; set; }
/// <summary>
/// 搜索类型。
/// </summary>
[JsonProperty("type")]
public int Type { get; set; }
/// <summary>
/// 是否获取全部的搜索结果。
/// </summary>
[JsonProperty("total")]
public bool IsTotal { get; set; }
/// <summary>
/// 每页的最大结果容量。
/// </summary>
[JsonProperty("limit")]
public int Limit { get; set; }
[JsonProperty("crypto")] public string Crypto { get; set; } = "weapi";
public SongSearchRequest()
{
CsrfToken = string.Empty;
Type = 1;
Offset = 0;
IsTotal = true;
Limit = 10;
}
public SongSearchRequest(string musicName, string artistName, int limit = 10) : this()
{
// Remove all the brackets and the content inside them.
var regex = new Regex(@"\([^)]*\)");
musicName = regex.Replace(musicName, string.Empty);
SearchKey = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
Limit = limit;
}
}
}

View File

@@ -0,0 +1,91 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
public class SongSearchResponse
{
[JsonProperty("result")] public InnerListItemModel Items { get; set; }
[JsonProperty("code")] public int StatusCode { get; set; }
public int GetFirstMatchSongId(string songName, long? duration)
{
var perfectMatch = Items.SongItems.FirstOrDefault(x => x.Name == songName);
if (perfectMatch != null)
{
return perfectMatch.Id;
}
if (duration is null or 0)
{
return Items.SongItems.First().Id;
}
return Items.SongItems.OrderBy(t => Math.Abs(t.Duration - duration.Value)).First().Id;
}
}
public class InnerListItemModel
{
[JsonProperty("songs")] public IList<SongModel> SongItems { get; set; }
[JsonProperty("songCount")] public int SongCount { get; set; }
}
public class SongModel
{
/// <summary>
/// 歌曲的名称。
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// 歌曲的 Sid (Song Id)。
/// </summary>
[JsonProperty("id")]
public int Id { get; set; }
/// <summary>
/// 歌曲的演唱者。
/// </summary>
[JsonProperty("artists")]
public IList<SongArtistModel> Artists { get; set; }
/// <summary>
/// 歌曲的专辑信息。
/// </summary>
[JsonProperty("album")]
public SongAlbumModel Album { get; set; }
/// <summary>
/// 歌曲的实际长度。
/// </summary>
[JsonProperty("duration")]
public long Duration { get; set; }
}
public class SongArtistModel
{
/// <summary>
/// 歌手/艺术家的名称。
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
}
public class SongAlbumModel
{
/// <summary>
/// 专辑的名称。
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// 专辑图像的 Url 地址。
/// </summary>
[JsonProperty("img1v1Url")]
public string PictureUrl { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
public static class SongSearchResponseStatusCode
{
public const int Success = 200;
}
}

View File

@@ -0,0 +1,93 @@
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.NetEase.JsonModel;
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
{
public class NetEaseLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.NetEase;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
private readonly GlobalOptions _options;
private const string NetEaseSearchMusicUrl = @"https://music.163.com/api/search/get/web";
private const string NetEaseGetLyricUrl = @"https://music.163.com/api/song/lyric";
private const string NetEaseRequestReferer = @"https://music.163.com";
private const string NetEaseRequestContentType = @"application/x-www-form-urlencoded";
public NetEaseLyricDownloader(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory,
IOptions<GlobalOptions> options)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
_options = options.Value;
}
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
NetEaseSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
true,
msg =>
{
msg.Headers.Referrer = new Uri(NetEaseRequestReferer);
if (msg.Content != null)
{
msg.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(NetEaseRequestContentType);
}
});
ValidateSongSearchResponse(searchResult, args);
var lyricResponse = await _warpHttpClient.GetAsync(
NetEaseGetLyricUrl,
new GetLyricRequest(searchResult.GetFirstMatchSongId(args.SongName, args.Duration)),
msg => msg.Headers.Referrer = new Uri(NetEaseRequestReferer));
return Encoding.UTF8.GetBytes(lyricResponse);
}
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args)
{
await ValueTask.CompletedTask;
var json = JsonConvert.DeserializeObject<GetLyricResponse>(Encoding.UTF8.GetString(data));
if (json?.OriginalLyric == null || string.IsNullOrEmpty(json.OriginalLyric.Text))
{
return new LyricItemCollection(null);
}
if (json.OriginalLyric.Text.Contains("纯音乐,请欣赏"))
{
return new LyricItemCollection(null);
}
return _lyricItemCollectionFactory.Build(
json.OriginalLyric?.Text,
json.TranslationLyric?.Text);
}
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricDownloaderArgs args)
{
if (response?.StatusCode != SongSearchResponseStatusCode.Success)
{
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
}
if (response.Items?.SongCount <= 0)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
}
}
}

View File

@@ -0,0 +1,33 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
{
public class GetLyricRequest
{
[JsonProperty("nobase64")] public int IsNoBase64Encoding { get; set; }
[JsonProperty("songmid")] public string SongId { get; set; }
[JsonProperty("platform")] public string ClientPlatform { get; set; }
[JsonProperty("inCharset")] public string InCharset { get; set; }
[JsonProperty("outCharset")] public string OutCharset { get; set; }
[JsonProperty("g_tk")] public int Gtk { get; set; }
protected GetLyricRequest()
{
}
public GetLyricRequest(string songId)
{
IsNoBase64Encoding = 1;
SongId = songId;
ClientPlatform = "yqq";
InCharset = "utf8";
OutCharset = "utf-8";
Gtk = 5381;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
{
public class SongSearchRequest
{
[JsonProperty("format")]
public string Format { get; protected set; }
[JsonProperty("inCharset")]
public string InCharset { get; protected set; }
[JsonProperty("outCharset")]
public string OutCharset { get; protected set; }
[JsonProperty("platform")]
public string Platform { get; protected set; }
[JsonProperty("key")]
public string Keyword { get; protected set; }
protected SongSearchRequest()
{
Format = "json";
InCharset = OutCharset = "utf-8";
Platform = "yqq.json";
}
public SongSearchRequest(string musicName, string artistName) : this()
{
Keyword = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
}
}
}

View File

@@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
{
public class SongSearchResponse
{
[JsonProperty("code")] public int StatusCode { get; set; }
[JsonProperty("data")] public QQMusicInnerDataModel Data { get; set; }
}
public class QQMusicInnerDataModel
{
[JsonProperty("song")] public QQMusicInnerSongModel Song { get; set; }
}
public class QQMusicInnerSongModel
{
[JsonProperty("itemlist")] public List<QQMusicInnerSongItem> SongItems { get; set; }
}
public class QQMusicInnerSongItem
{
[JsonProperty("mid")] public string SongId { get; set; }
}
}

View File

@@ -0,0 +1,82 @@
using System.Text;
using System.Web;
using Newtonsoft.Json.Linq;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel;
namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic
{
public class QQLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.QQ;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
// private const string QQSearchMusicUrl = @"https://c.y.qq.com/soso/fcgi-bin/client_search_cp";
private const string QQSearchMusicUrl = @"https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg";
private const string QQGetLyricUrl = @"https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg";
private const string QQMusicRequestReferer = @"https://y.qq.com/";
public QQLyricDownloader(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
}
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(
QQSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist));
ValidateSongSearchResponse(searchResult, args);
var lyricJsonString = await _warpHttpClient.GetAsync(QQGetLyricUrl,
new GetLyricRequest(searchResult.Data.Song.SongItems.FirstOrDefault()?.SongId),
op => op.Headers.Referrer = new Uri(QQMusicRequestReferer));
return Encoding.UTF8.GetBytes(lyricJsonString);
}
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args)
{
await ValueTask.CompletedTask;
var lyricJsonString = Encoding.UTF8.GetString(data);
lyricJsonString = lyricJsonString.Replace(@"MusicJsonCallback(", string.Empty).TrimEnd(')');
if (lyricJsonString.Contains("\"code\":-1901"))
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
if (lyricJsonString.Contains("此歌曲为没有填词的纯音乐,请您欣赏"))
{
return _lyricItemCollectionFactory.Build(null);
}
var lyricJsonObj = JObject.Parse(lyricJsonString);
var sourceLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.lyric").Value<string>()));
var translateLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.trans").Value<string>()));
return _lyricItemCollectionFactory.Build(sourceLyric, translateLyric);
}
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricDownloaderArgs args)
{
if (response is not { StatusCode: 0 } || response.Data.Song.SongItems == null)
{
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
}
if (response.Data.Song.SongItems.Count <= 0)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
}
}
}