mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-09-06 05:36:53 +00:00
refactor: Common components are moved to the Common library.
This commit is contained in:
21
src/ZonyLrcTools.Common/Album/IAlbumDownloader.cs
Normal file
21
src/ZonyLrcTools.Common/Album/IAlbumDownloader.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace ZonyLrcTools.Common.Album
|
||||
{
|
||||
/// <summary>
|
||||
/// 专辑封面下载器,用于匹配并下载歌曲的专辑封面。
|
||||
/// </summary>
|
||||
public interface IAlbumDownloader
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载器的名称。
|
||||
/// </summary>
|
||||
string DownloaderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 下载专辑封面。
|
||||
/// </summary>
|
||||
/// <param name="songName">歌曲的名称。</param>
|
||||
/// <param name="artist">歌曲的作者。</param>
|
||||
/// <returns>专辑封面的图像数据。</returns>
|
||||
ValueTask<byte[]> DownloadAsync(string songName, string artist);
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
namespace ZonyLrcTools.Common.Album
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义了程序默认提供的专辑图像下载器。
|
||||
/// </summary>
|
||||
public static class InternalAlbumDownloaderNames
|
||||
{
|
||||
/// <summary>
|
||||
/// 网易云音乐专辑图像下载器。
|
||||
/// </summary>
|
||||
public const string NetEase = nameof(NetEase);
|
||||
|
||||
/// <summary>
|
||||
/// QQ 音乐专辑图像下载器。
|
||||
/// </summary>
|
||||
public const string QQ = nameof(QQ);
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||
using ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel;
|
||||
|
||||
namespace ZonyLrcTools.Common.Album.NetEase
|
||||
{
|
||||
public class NetEaseAlbumDownloader : IAlbumDownloader, ITransientDependency
|
||||
{
|
||||
public string DownloaderName => InternalAlbumDownloaderNames.NetEase;
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly Action<HttpRequestMessage> _defaultOption;
|
||||
|
||||
private const string SearchMusicApi = @"https://music.163.com/api/search/get/web";
|
||||
private const string GetMusicInfoApi = @"https://music.163.com/api/song/detail";
|
||||
private const string DefaultReferer = @"https://music.163.com";
|
||||
|
||||
public NetEaseAlbumDownloader(IWarpHttpClient warpHttpClient)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_defaultOption = message =>
|
||||
{
|
||||
message.Headers.Referrer = new Uri(DefaultReferer);
|
||||
|
||||
if (message.Content != null)
|
||||
{
|
||||
message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> DownloadAsync(string songName, string artist)
|
||||
{
|
||||
var requestParameter = new SongSearchRequest(songName, artist);
|
||||
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
|
||||
SearchMusicApi,
|
||||
requestParameter,
|
||||
true,
|
||||
_defaultOption);
|
||||
|
||||
if (searchResult is not { StatusCode: 200 } || searchResult.Items?.SongCount <= 0)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong);
|
||||
}
|
||||
|
||||
var songDetailJsonStr = await _warpHttpClient.GetAsync(
|
||||
GetMusicInfoApi,
|
||||
new GetSongDetailsRequest(searchResult.GetFirstMatchSongId(songName, null)),
|
||||
_defaultOption);
|
||||
|
||||
var url = JObject.Parse(songDetailJsonStr).SelectToken("$.songs[0].album.picUrl")?.Value<string>();
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal);
|
||||
}
|
||||
|
||||
return await new HttpClient().GetByteArrayAsync(new Uri(url));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
using System.Net.Http.Headers;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||
using ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel;
|
||||
|
||||
namespace ZonyLrcTools.Common.Album.QQMusic
|
||||
{
|
||||
public class QQMusicAlbumDownloader : IAlbumDownloader, ITransientDependency
|
||||
{
|
||||
public string DownloaderName => InternalAlbumDownloaderNames.QQ;
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
|
||||
private readonly Action<HttpRequestMessage> _defaultOption = message =>
|
||||
{
|
||||
message.Headers.Referrer = new Uri(DefaultReferer);
|
||||
|
||||
if (message.Content != null)
|
||||
{
|
||||
message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
|
||||
}
|
||||
};
|
||||
|
||||
private const string SearchApi = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp";
|
||||
private const string DefaultReferer = "https://y.qq.com";
|
||||
|
||||
public QQMusicAlbumDownloader(IWarpHttpClient warpHttpClient)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> DownloadAsync(string songName, string artist)
|
||||
{
|
||||
var requestParameter = new SongSearchRequest(songName, artist);
|
||||
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(
|
||||
SearchApi,
|
||||
requestParameter, _defaultOption);
|
||||
|
||||
return new byte[] { 0x1, 0x2 };
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志记录相关的扩展方法。
|
||||
/// </summary>
|
||||
public static class LoggerHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 <see cref="LogLevel.Warning"/> 级别打印错误日志,并记录异常堆栈。
|
||||
/// </summary>
|
||||
/// <param name="logger">日志记录器实例。</param>
|
||||
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</param>
|
||||
/// <param name="e">异常实例,可为空。</param>
|
||||
public static void LogWarningWithErrorCode(this ILogger logger, int errorCode, Exception e = null)
|
||||
{
|
||||
logger.LogWarning($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="LogLevel.Warning"/> 级别打印错误日志,并记录异常堆栈。
|
||||
/// </summary>
|
||||
/// <param name="logger">日志记录器的实例。</param>
|
||||
/// <param name="exception">错误码异常实例。</param>
|
||||
public static void LogWarningInfo(this ILogger logger, ErrorCodeException exception)
|
||||
{
|
||||
if (exception.ErrorCode < 50000)
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"错误代码: {exception.ErrorCode},信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}");
|
||||
sb.Append($"\n附加信息:\n {JsonConvert.SerializeObject(exception.AttachObject)}");
|
||||
logger.LogWarning(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="LogLevel.Information"/> 级别打印歌曲下载成功信息。
|
||||
/// </summary>
|
||||
/// <param name="logger">日志记录器的实例。</param>
|
||||
/// <param name="musicInfo">需要打印的歌曲信息。</param>
|
||||
public static void LogSuccessful(this ILogger logger, MusicInfo musicInfo)
|
||||
{
|
||||
logger.LogInformation($"歌曲名: {musicInfo.Name}, 艺术家: {musicInfo.Artist}, 下载成功.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串处理相关的工具方法。
|
||||
/// </summary>
|
||||
public static class StringHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 截断指定字符串末尾的匹配字串。
|
||||
/// </summary>
|
||||
/// <param name="string">待截断的字符串。</param>
|
||||
/// <param name="trimEndStr">需要在末尾截断的字符串。</param>
|
||||
/// <returns>截断成功的字符串实例。</returns>
|
||||
public static string TrimEnd(this string @string, string trimEndStr)
|
||||
{
|
||||
if (@string.EndsWith(trimEndStr, StringComparison.Ordinal))
|
||||
{
|
||||
return @string.Substring(0, @string.Length - trimEndStr.Length);
|
||||
}
|
||||
|
||||
return @string;
|
||||
}
|
||||
}
|
||||
}
|
65
src/ZonyLrcTools.Common/Infrastructure/IO/FileScanner.cs
Normal file
65
src/ZonyLrcTools.Common/Infrastructure/IO/FileScanner.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Common.Infrastructure.Extensions;
|
||||
|
||||
namespace ZonyLrcTools.Common.Infrastructure.IO
|
||||
{
|
||||
public class FileScanner : IFileScanner, ITransientDependency
|
||||
{
|
||||
public ILogger<FileScanner> Logger { get; set; }
|
||||
|
||||
public FileScanner()
|
||||
{
|
||||
Logger = NullLogger<FileScanner>.Instance;
|
||||
}
|
||||
|
||||
public Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions)
|
||||
{
|
||||
if (extensions == null || !extensions.Any())
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.FileSuffixIsEmpty);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.DirectoryNotExist);
|
||||
}
|
||||
|
||||
var files = new List<FileScannerResult>();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
var tempResult = new ConcurrentBag<string>();
|
||||
SearchFile(tempResult, path, extension);
|
||||
|
||||
files.Add(new FileScannerResult(
|
||||
Path.GetExtension(extension) ?? throw new ErrorCodeException(ErrorCodes.UnableToGetTheFileExtension),
|
||||
tempResult.ToList()));
|
||||
}
|
||||
|
||||
return Task.FromResult(files);
|
||||
}
|
||||
|
||||
private void SearchFile(ConcurrentBag<string> files, string folder, string extension)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(folder, extension))
|
||||
{
|
||||
files.Add(file);
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.GetDirectories(folder))
|
||||
{
|
||||
SearchFile(files, directory, extension);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogWarningWithErrorCode(ErrorCodes.ScanFileError, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
namespace ZonyLrcTools.Common.Infrastructure.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件扫描结果对象。
|
||||
/// </summary>
|
||||
public class FileScannerResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前路径对应的扩展名。
|
||||
/// </summary>
|
||||
public string ExtensionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前扩展名下面的所有文件路径集合。
|
||||
/// </summary>
|
||||
public List<string> FilePaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="FileScannerResult"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="extensionName">当前路径对应的扩展名。</param>
|
||||
/// <param name="filePaths">当前扩展名下面的所有文件路径集合。</param>
|
||||
public FileScannerResult(string extensionName, List<string> filePaths)
|
||||
{
|
||||
ExtensionName = extensionName;
|
||||
FilePaths = filePaths;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
namespace ZonyLrcTools.Common.Infrastructure.IO
|
||||
{
|
||||
public static class FileStreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 将字节数据通过缓冲区的形式,写入到文件当中。
|
||||
/// </summary>
|
||||
/// <param name="fileStream">需要写入数据的文件流。</param>
|
||||
/// <param name="data">等待写入的数据。</param>
|
||||
/// <param name="bufferSize">缓冲区大小。</param>
|
||||
public static async Task WriteBytesToFileAsync(this FileStream fileStream, byte[] data, int bufferSize = 1024)
|
||||
{
|
||||
await using (fileStream)
|
||||
{
|
||||
var count = data.Length / 1024;
|
||||
var modCount = data.Length % 1024;
|
||||
if (count <= 0)
|
||||
{
|
||||
await fileStream.WriteAsync(data, 0, modCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
await fileStream.WriteAsync(data, i * 1024, 1024);
|
||||
}
|
||||
|
||||
if (modCount != 0)
|
||||
{
|
||||
await fileStream.WriteAsync(data, count * 1024, modCount);
|
||||
}
|
||||
}
|
||||
|
||||
await fileStream.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
src/ZonyLrcTools.Common/Infrastructure/IO/IFileScanner.cs
Normal file
15
src/ZonyLrcTools.Common/Infrastructure/IO/IFileScanner.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace ZonyLrcTools.Common.Infrastructure.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// 音乐文件扫描器,用于扫描音乐文件。
|
||||
/// </summary>
|
||||
public interface IFileScanner
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描指定路径下面的歌曲文件。
|
||||
/// </summary>
|
||||
/// <param name="path">等待扫描的路径。</param>
|
||||
/// <param name="extensions">需要搜索的歌曲后缀名。</param>
|
||||
Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions);
|
||||
}
|
||||
}
|
88
src/ZonyLrcTools.Common/Infrastructure/Threading/WarpTask.cs
Normal file
88
src/ZonyLrcTools.Common/Infrastructure/Threading/WarpTask.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
namespace ZonyLrcTools.Common.Infrastructure.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// 针对 Task 的包装类,基于信号量 <see cref="SemaphoreSlim"/> 限定并行度。
|
||||
/// </summary>
|
||||
public class WarpTask : IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private readonly int _maxDegreeOfParallelism;
|
||||
|
||||
public WarpTask(int maxDegreeOfParallelism)
|
||||
{
|
||||
if (maxDegreeOfParallelism <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
|
||||
}
|
||||
|
||||
_maxDegreeOfParallelism = maxDegreeOfParallelism;
|
||||
_semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
|
||||
}
|
||||
|
||||
public async Task RunAsync(Func<Task> taskFactory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token))
|
||||
{
|
||||
await _semaphore.WaitAsync(cts.Token);
|
||||
try
|
||||
{
|
||||
await taskFactory().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> RunAsync<T>(Func<Task<T>> taskFactory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token))
|
||||
{
|
||||
await _semaphore.WaitAsync(cts.Token);
|
||||
try
|
||||
{
|
||||
return await taskFactory().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposedValue = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_cts.Cancel();
|
||||
for (int i = 0; i < _maxDegreeOfParallelism; i++)
|
||||
{
|
||||
_semaphore.WaitAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_semaphore.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~WarpTask()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
22
src/ZonyLrcTools.Common/Lyrics/ILyricDownloader.cs
Normal file
22
src/ZonyLrcTools.Common/Lyrics/ILyricDownloader.cs
Normal 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; }
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
7
src/ZonyLrcTools.Common/Lyrics/ILyricTextResolver.cs
Normal file
7
src/ZonyLrcTools.Common/Lyrics/ILyricTextResolver.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ZonyLrcTools.Common.Lyrics
|
||||
{
|
||||
public interface ILyricTextResolver
|
||||
{
|
||||
LyricItemCollection Resolve(string lyricText);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
58
src/ZonyLrcTools.Common/Lyrics/LyricDownloader.cs
Normal file
58
src/ZonyLrcTools.Common/Lyrics/LyricDownloader.cs
Normal 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);
|
||||
}
|
||||
}
|
18
src/ZonyLrcTools.Common/Lyrics/LyricDownloaderArgs.cs
Normal file
18
src/ZonyLrcTools.Common/Lyrics/LyricDownloaderArgs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
128
src/ZonyLrcTools.Common/Lyrics/LyricItem.cs
Normal file
128
src/ZonyLrcTools.Common/Lyrics/LyricItem.cs
Normal 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}";
|
||||
}
|
||||
}
|
113
src/ZonyLrcTools.Common/Lyrics/LyricItemCollection.cs
Normal file
113
src/ZonyLrcTools.Common/Lyrics/LyricItemCollection.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
68
src/ZonyLrcTools.Common/Lyrics/LyricItemCollectionFactory.cs
Normal file
68
src/ZonyLrcTools.Common/Lyrics/LyricItemCollectionFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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";
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
||||
{
|
||||
public static class SongSearchResponseStatusCode
|
||||
{
|
||||
public const int Success = 200;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
src/ZonyLrcTools.Common/MusicDecryption/DecryptionResult.cs
Normal file
14
src/ZonyLrcTools.Common/MusicDecryption/DecryptionResult.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace ZonyLrcTools.Common.MusicDecryption
|
||||
{
|
||||
public class DecryptionResult
|
||||
{
|
||||
public byte[] Data { get; protected set; }
|
||||
|
||||
public Dictionary<string, object> ExtensionObjects { get; set; }
|
||||
|
||||
public DecryptionResult(byte[] data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
}
|
15
src/ZonyLrcTools.Common/MusicDecryption/IMusicDecryptor.cs
Normal file
15
src/ZonyLrcTools.Common/MusicDecryption/IMusicDecryptor.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace ZonyLrcTools.Common.MusicDecryption
|
||||
{
|
||||
/// <summary>
|
||||
/// 音乐解密器,用于将加密的歌曲数据,转换为可识别的歌曲格式。
|
||||
/// </summary>
|
||||
public interface IMusicDecryptor
|
||||
{
|
||||
/// <summary>
|
||||
/// 将加密数据转换为可识别的歌曲格式。
|
||||
/// </summary>
|
||||
/// <param name="sourceBytes">源加密的歌曲数据。</param>
|
||||
/// <returns>解密完成的歌曲数据。</returns>
|
||||
Task<DecryptionResult> ConvertMusic(byte[] sourceBytes);
|
||||
}
|
||||
}
|
180
src/ZonyLrcTools.Common/MusicDecryption/NcmMusicDecryptor.cs
Normal file
180
src/ZonyLrcTools.Common/MusicDecryption/NcmMusicDecryptor.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
|
||||
namespace ZonyLrcTools.Common.MusicDecryption
|
||||
{
|
||||
/// <summary>
|
||||
/// NCM 音乐转换器,用于将 NCM 格式的音乐转换为可播放的格式。
|
||||
/// </summary>
|
||||
public class NcmMusicDecryptor : IMusicDecryptor, ITransientDependency
|
||||
{
|
||||
protected readonly byte[] AesCoreKey = { 0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57 };
|
||||
protected readonly byte[] AesModifyKey = { 0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28 };
|
||||
|
||||
public async Task<DecryptionResult> ConvertMusic(byte[] sourceBytes)
|
||||
{
|
||||
var stream = new MemoryStream(sourceBytes);
|
||||
var streamReader = new BinaryReader(stream);
|
||||
|
||||
var lengthBytes = new byte[4];
|
||||
lengthBytes = streamReader.ReadBytes(4);
|
||||
if (BitConverter.ToInt32(lengthBytes) != 0x4e455443)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
lengthBytes = streamReader.ReadBytes(4);
|
||||
if (BitConverter.ToInt32(lengthBytes) != 0x4d414446)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
stream.Seek(2, SeekOrigin.Current);
|
||||
stream.Read(lengthBytes);
|
||||
|
||||
var keyBytes = new byte[BitConverter.ToInt32(lengthBytes)];
|
||||
stream.Read(keyBytes);
|
||||
|
||||
// 对已经加密的数据进行异或操作。
|
||||
for (int i = 0; i < keyBytes.Length; i++)
|
||||
{
|
||||
keyBytes[i] ^= 0x64;
|
||||
}
|
||||
|
||||
var coreKeyBytes = GetBytesByOffset(DecryptAes128Ecb(AesCoreKey, keyBytes), 17);
|
||||
|
||||
var modifyDataBytes = new byte[streamReader.ReadInt32()];
|
||||
stream.Read(modifyDataBytes);
|
||||
for (int i = 0; i < modifyDataBytes.Length; i++)
|
||||
{
|
||||
modifyDataBytes[i] ^= 0x63;
|
||||
}
|
||||
|
||||
var decryptBase64Bytes = Convert.FromBase64String(Encoding.UTF8.GetString(GetBytesByOffset(modifyDataBytes, 22)));
|
||||
var decryptModifyData = DecryptAes128Ecb(AesModifyKey, decryptBase64Bytes);
|
||||
|
||||
var musicInfoJson = JObject.Parse(Encoding.UTF8.GetString(GetBytesByOffset(decryptModifyData, 6)));
|
||||
|
||||
// CRC 校验
|
||||
stream.Seek(4, SeekOrigin.Current);
|
||||
stream.Seek(5, SeekOrigin.Current);
|
||||
|
||||
GetAlbumImageBytes(stream, streamReader);
|
||||
|
||||
var sBox = BuildKeyBox(coreKeyBytes);
|
||||
return new DecryptionResult(GetMusicBytes(sBox, stream).ToArray())
|
||||
{
|
||||
ExtensionObjects = new Dictionary<string, object>
|
||||
{
|
||||
{ "JSON", musicInfoJson }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] GetBytesByOffset(byte[] srcBytes, int offset = 0)
|
||||
{
|
||||
var resultBytes = new byte[srcBytes.Length - offset];
|
||||
Array.Copy(srcBytes, offset, resultBytes, 0, srcBytes.Length - offset);
|
||||
return resultBytes;
|
||||
}
|
||||
|
||||
private byte[] DecryptAes128Ecb(byte[] keyBytes, byte[] data)
|
||||
{
|
||||
var aes = Aes.Create();
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Mode = CipherMode.ECB;
|
||||
using var decryptor = aes.CreateDecryptor(keyBytes, null);
|
||||
var result = decryptor.TransformFinalBlock(data, 0, data.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RC4 加密,生成 KeyBox。
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
private byte[] BuildKeyBox(byte[] key)
|
||||
{
|
||||
byte[] box = new byte[256];
|
||||
for (int i = 0; i < 256; ++i)
|
||||
{
|
||||
box[i] = (byte)i;
|
||||
}
|
||||
|
||||
byte keyLength = (byte)key.Length;
|
||||
byte c;
|
||||
byte lastByte = 0;
|
||||
byte keyOffset = 0;
|
||||
byte swap;
|
||||
|
||||
for (int i = 0; i < 256; ++i)
|
||||
{
|
||||
swap = box[i];
|
||||
c = (byte)((swap + lastByte + key[keyOffset++]) & 0xff);
|
||||
|
||||
if (keyOffset >= keyLength)
|
||||
{
|
||||
keyOffset = 0;
|
||||
}
|
||||
|
||||
box[i] = box[c];
|
||||
box[c] = swap;
|
||||
lastByte = c;
|
||||
}
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获得歌曲的专辑图像信息。
|
||||
/// </summary>
|
||||
/// <param name="stream">原始文件流。</param>
|
||||
/// <param name="streamReader">二进制读取器。</param>
|
||||
private byte[] GetAlbumImageBytes(Stream stream, BinaryReader streamReader)
|
||||
{
|
||||
var imgLength = streamReader.ReadInt32();
|
||||
|
||||
if (imgLength <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var imgBuffer = streamReader.ReadBytes(imgLength);
|
||||
|
||||
return imgBuffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获得歌曲的完整数据。
|
||||
/// </summary>
|
||||
/// <param name="sBox"></param>
|
||||
/// <param name="stream">原始文件流。</param>
|
||||
private MemoryStream GetMusicBytes(byte[] sBox, Stream stream)
|
||||
{
|
||||
var n = 0x8000;
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var tb = new byte[n];
|
||||
var result = stream.Read(tb);
|
||||
if (result <= 0) break;
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var j = (byte)((i + 1) & 0xff);
|
||||
tb[i] ^= sBox[sBox[j] + sBox[(sBox[j] + j) & 0xff] & 0xff];
|
||||
}
|
||||
|
||||
memoryStream.Write(tb);
|
||||
}
|
||||
|
||||
memoryStream.Flush();
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
}
|
||||
}
|
46
src/ZonyLrcTools.Common/MusicInfo.cs
Normal file
46
src/ZonyLrcTools.Common/MusicInfo.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace ZonyLrcTools.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌曲信息的承载类,携带歌曲的相关数据。
|
||||
/// </summary>
|
||||
public class MusicInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌曲对应的物理文件路径。
|
||||
/// </summary>
|
||||
public string FilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的实际歌曲长度。
|
||||
/// </summary>
|
||||
public long? TotalTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的作者。
|
||||
/// </summary>
|
||||
public string Artist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否下载成功?
|
||||
/// </summary>
|
||||
public bool IsSuccessful { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 构建一个新的 <see cref="MusicInfo"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="filePath">歌曲对应的物理文件路径。</param>
|
||||
/// <param name="name">歌曲的名称。</param>
|
||||
/// <param name="artist">歌曲的作者。</param>
|
||||
public MusicInfo(string filePath, string name, string artist)
|
||||
{
|
||||
FilePath = filePath;
|
||||
Name = name;
|
||||
Artist = artist;
|
||||
}
|
||||
}
|
||||
}
|
39
src/ZonyLrcTools.Common/TagInfo/BlockWordDictionary.cs
Normal file
39
src/ZonyLrcTools.Common/TagInfo/BlockWordDictionary.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Common.Configuration;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
/// <inheritdoc cref="ZonyLrcTools.Cli.Infrastructure.Tag.IBlockWordDictionary" />
|
||||
public class BlockWordDictionary : IBlockWordDictionary, ISingletonDependency
|
||||
{
|
||||
private readonly GlobalOptions _options;
|
||||
|
||||
private readonly Lazy<Dictionary<string, string>> _wordsDictionary;
|
||||
|
||||
public BlockWordDictionary(IOptions<GlobalOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
|
||||
_wordsDictionary = new Lazy<Dictionary<string, string>>(() =>
|
||||
{
|
||||
var jsonData = File.ReadAllText(_options.Provider.Tag.BlockWord.FilePath);
|
||||
return JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonData);
|
||||
});
|
||||
}
|
||||
|
||||
public string GetValue(string key)
|
||||
{
|
||||
if (_wordsDictionary.Value.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
85
src/ZonyLrcTools.Common/TagInfo/DefaultTagLoader.cs
Normal file
85
src/ZonyLrcTools.Common/TagInfo/DefaultTagLoader.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZonyLrcTools.Common;
|
||||
using ZonyLrcTools.Common.Configuration;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认的标签加载器 <see cref="ITagLoader"/> 实现。
|
||||
/// </summary>
|
||||
public class DefaultTagLoader : ITagLoader, ISingletonDependency
|
||||
{
|
||||
protected readonly IEnumerable<ITagInfoProvider> TagInfoProviders;
|
||||
protected readonly IBlockWordDictionary BlockWordDictionary;
|
||||
protected readonly ILogger<DefaultTagLoader> Logger;
|
||||
|
||||
protected GlobalOptions Options;
|
||||
|
||||
private readonly IEnumerable<ITagInfoProvider> _sortedTagInfoProviders;
|
||||
|
||||
public DefaultTagLoader(IEnumerable<ITagInfoProvider> tagInfoProviders,
|
||||
IBlockWordDictionary blockWordDictionary,
|
||||
IOptions<GlobalOptions> options,
|
||||
ILogger<DefaultTagLoader> logger)
|
||||
{
|
||||
TagInfoProviders = tagInfoProviders;
|
||||
BlockWordDictionary = blockWordDictionary;
|
||||
Logger = logger;
|
||||
Options = options.Value;
|
||||
|
||||
_sortedTagInfoProviders = GetTagInfoProviders();
|
||||
}
|
||||
|
||||
public virtual async ValueTask<MusicInfo> LoadTagAsync(string filePath)
|
||||
{
|
||||
foreach (var provider in _sortedTagInfoProviders)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await provider.LoadAsync(filePath);
|
||||
if (info != null && !string.IsNullOrEmpty(info.Name))
|
||||
{
|
||||
HandleBlockWord(info);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogWarning($"{filePath} 没有找到正确的标签信息,请考虑调整正则表达式。");
|
||||
return null;
|
||||
}
|
||||
|
||||
private IEnumerable<ITagInfoProvider> GetTagInfoProviders()
|
||||
{
|
||||
if (!TagInfoProviders.Any())
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.LoadTagInfoProviderError);
|
||||
}
|
||||
|
||||
return Options.Provider.Tag.Plugin
|
||||
.Where(x => x.Priority != -1)
|
||||
.OrderBy(x => x.Priority)
|
||||
.Join(TagInfoProviders, x => x.Name, y => y.Name, (x, y) => y);
|
||||
}
|
||||
|
||||
protected void HandleBlockWord(MusicInfo info)
|
||||
{
|
||||
if (Options.Provider.Tag.BlockWord.IsEnable)
|
||||
{
|
||||
info.Name = BlockWordDictionary.GetValue(info.Name) ?? info.Name;
|
||||
info.Artist = BlockWordDictionary.GetValue(info.Name) ?? info.Artist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
src/ZonyLrcTools.Common/TagInfo/FileNameTagInfoProvider.cs
Normal file
46
src/ZonyLrcTools.Common/TagInfo/FileNameTagInfoProvider.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZonyLrcTools.Common;
|
||||
using ZonyLrcTools.Common.Configuration;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于正则表达式的标签解析器,从文件名当中解析歌曲的标签信息。
|
||||
/// </summary>
|
||||
public class FileNameTagInfoProvider : ITagInfoProvider, ISingletonDependency
|
||||
{
|
||||
public string Name => ConstantName;
|
||||
public const string ConstantName = "FileName";
|
||||
public const string RegularExpressionsOption = "regularExpressions";
|
||||
|
||||
private readonly GlobalOptions _options;
|
||||
|
||||
public FileNameTagInfoProvider(IOptions<GlobalOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async ValueTask<MusicInfo> LoadAsync(string filePath)
|
||||
{
|
||||
await ValueTask.CompletedTask;
|
||||
|
||||
var regex = _options.Provider.Tag.Plugin
|
||||
.First(t => t.Name == ConstantName)
|
||||
.Extensions[RegularExpressionsOption];
|
||||
|
||||
var match = Regex.Match(Path.GetFileNameWithoutExtension(filePath), regex);
|
||||
|
||||
if (match.Groups.Count != 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MusicInfo(filePath, match.Groups["name"].Value, match.Groups["artist"].Value);
|
||||
}
|
||||
}
|
||||
}
|
18
src/ZonyLrcTools.Common/TagInfo/IBlockWordDictionary.cs
Normal file
18
src/ZonyLrcTools.Common/TagInfo/IBlockWordDictionary.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// 屏蔽词字典。
|
||||
/// </summary>
|
||||
public interface IBlockWordDictionary
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 <paramref name="key"/> 获得屏蔽词结果。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 例: 原歌曲的 "fuckking" ,在网易云实际为 "***kking"。
|
||||
/// </remarks>
|
||||
/// <param name="key">原始单词。</param>
|
||||
/// <returns>原始单词对应的屏蔽词。</returns>
|
||||
string GetValue(string key);
|
||||
}
|
||||
}
|
23
src/ZonyLrcTools.Common/TagInfo/ITagInfoProvider.cs
Normal file
23
src/ZonyLrcTools.Common/TagInfo/ITagInfoProvider.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Threading.Tasks;
|
||||
using ZonyLrcTools.Common;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// 具体的标签解析器,执行真正的标签解析逻辑。
|
||||
/// </summary>
|
||||
public interface ITagInfoProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 标签解析器的唯一标识。
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 加载歌曲文件的标签信息。
|
||||
/// </summary>
|
||||
/// <param name="filePath">歌曲文件的路径。</param>
|
||||
/// <returns>加载完成的歌曲信息。</returns>
|
||||
ValueTask<MusicInfo> LoadAsync(string filePath);
|
||||
}
|
||||
}
|
18
src/ZonyLrcTools.Common/TagInfo/ITagLoader.cs
Normal file
18
src/ZonyLrcTools.Common/TagInfo/ITagLoader.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using ZonyLrcTools.Common;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// 标签加载器,用于加载文件的音乐标签信息。
|
||||
/// </summary>
|
||||
public interface ITagLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载歌曲文件的标签信息。
|
||||
/// </summary>
|
||||
/// <param name="filePath">歌曲文件的路径。</param>
|
||||
/// <returns>加载完成的歌曲信息。</returns>
|
||||
ValueTask<MusicInfo> LoadTagAsync(string filePath);
|
||||
}
|
||||
}
|
46
src/ZonyLrcTools.Common/TagInfo/TaglibTagInfoProvider.cs
Normal file
46
src/ZonyLrcTools.Common/TagInfo/TaglibTagInfoProvider.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ZonyLrcTools.Common;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于 TagLib 的标签信息解析器。
|
||||
/// </summary>
|
||||
public class TaglibTagInfoProvider : ITagInfoProvider, ISingletonDependency
|
||||
{
|
||||
public string Name => ConstantName;
|
||||
public const string ConstantName = "Taglib";
|
||||
|
||||
public async ValueTask<MusicInfo> LoadAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = TagLib.File.Create(filePath);
|
||||
|
||||
var songName = file.Tag.Title;
|
||||
var songArtist = file.Tag.FirstPerformer;
|
||||
|
||||
if (!string.IsNullOrEmpty(file.Tag.FirstAlbumArtist))
|
||||
{
|
||||
songArtist = file.Tag.FirstAlbumArtist;
|
||||
}
|
||||
|
||||
await ValueTask.CompletedTask;
|
||||
|
||||
if (songName == null && songArtist == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MusicInfo(filePath, songName, songArtist);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.TagInfoProviderLoadInfoFailed, ex.Message, filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
src/ZonyLrcTools.Common/Updater/DefaultUpdater.cs
Normal file
59
src/ZonyLrcTools.Common/Updater/DefaultUpdater.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||
using ZonyLrcTools.Common.Updater.JsonModel;
|
||||
|
||||
namespace ZonyLrcTools.Common.Updater;
|
||||
|
||||
public class DefaultUpdater : ISingletonDependency
|
||||
{
|
||||
public const string UpdateUrl = "https://api.zony.me/lrc-tools/update";
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly ILogger<DefaultUpdater> _logger;
|
||||
|
||||
public DefaultUpdater(IWarpHttpClient warpHttpClient,
|
||||
ILogger<DefaultUpdater> logger)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task CheckUpdateAsync()
|
||||
{
|
||||
var response = await _warpHttpClient.GetAsync<NewVersionResponse>(UpdateUrl);
|
||||
if (response == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
if (response.NewVersion <= currentVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var importantItem = response.Items.FirstOrDefault(x => x.ItemType == NewVersionItemType.Important);
|
||||
if (importantItem != null)
|
||||
{
|
||||
_logger.LogWarning($"发现了新版本,请点击下面的链接进行更新:{importantItem.Url}");
|
||||
_logger.LogWarning($"最新版本号:{response.NewVersion},当前版本号: ${currentVersion}");
|
||||
_logger.LogWarning($"更新内容:{response.NewVersionDescription}");
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
Process.Start("explorer.exe", importantItem.Url);
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Process.Start("open", importantItem.Url);
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Process.Start("xdg-open", importantItem.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
namespace ZonyLrcTools.Common.Updater.JsonModel;
|
||||
|
||||
public class NewVersionItem
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public NewVersionItemType ItemType { get; set; }
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace ZonyLrcTools.Common.Updater.JsonModel;
|
||||
|
||||
public enum NewVersionItemType
|
||||
{
|
||||
Important,
|
||||
NormalBinaryFile
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
namespace ZonyLrcTools.Common.Updater.JsonModel;
|
||||
|
||||
public class NewVersionResponse
|
||||
{
|
||||
public Version NewVersion { get; set; }
|
||||
|
||||
public string NewVersionDescription { get; set; }
|
||||
|
||||
public List<NewVersionItem> Items { get; set; }
|
||||
|
||||
public DateTime UpdateTime { get; set; }
|
||||
}
|
@@ -12,6 +12,11 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
|
||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Lyrics\Providers" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
Reference in New Issue
Block a user