feat: Reinitialize the Repository.

重新初始化仓库。
This commit is contained in:
Zony
2021-05-07 10:26:26 +08:00
parent a754f419b2
commit 8e1e61764c
78 changed files with 3329 additions and 22 deletions

View File

@@ -0,0 +1,196 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure;
using ZonyLrcTools.Cli.Infrastructure.Album;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Extensions;
using ZonyLrcTools.Cli.Infrastructure.IO;
using ZonyLrcTools.Cli.Infrastructure.Lyric;
using ZonyLrcTools.Cli.Infrastructure.Tag;
using ZonyLrcTools.Cli.Infrastructure.Threading;
namespace ZonyLrcTools.Cli.Commands
{
[Command("download", Description = "下载歌词文件或专辑图像。")]
public class DownloadCommand : ToolCommandBase
{
private readonly ILogger<DownloadCommand> _logger;
private readonly IFileScanner _fileScanner;
private readonly ITagLoader _tagLoader;
private readonly IEnumerable<ILyricDownloader> _lyricDownloaderList;
private readonly IEnumerable<IAlbumDownloader> _albumDownloaderList;
private readonly ToolOptions _options;
public DownloadCommand(ILogger<DownloadCommand> logger,
IFileScanner fileScanner,
IOptions<ToolOptions> options,
ITagLoader tagLoader,
IEnumerable<ILyricDownloader> lyricDownloaderList,
IEnumerable<IAlbumDownloader> albumDownloaderList)
{
_logger = logger;
_fileScanner = fileScanner;
_tagLoader = tagLoader;
_lyricDownloaderList = lyricDownloaderList;
_albumDownloaderList = albumDownloaderList;
_options = options.Value;
}
#region > Options <
[Option("-d|--dir", CommandOptionType.SingleValue, Description = "指定需要扫描的目录。")]
[DirectoryExists]
public string Directory { get; set; }
[Option("-l|--lyric", CommandOptionType.NoValue, Description = "指定程序需要下载歌词文件。")]
public bool DownloadLyric { get; set; }
[Option("-a|--album", CommandOptionType.NoValue, Description = "指定程序需要下载专辑图像。")]
public bool DownloadAlbum { get; set; }
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
public int ParallelNumber { get; set; } = 2;
#endregion
public override List<string> CreateArgs() => new();
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
var files = await ScanMusicFilesAsync();
var musicInfos = await LoadMusicInfoAsync(files);
if (DownloadLyric)
{
await DownloadLyricFilesAsync(musicInfos);
}
if (DownloadAlbum)
{
await DownloadAlbumAsync(musicInfos);
}
return 0;
}
private async Task<List<string>> ScanMusicFilesAsync()
{
var files = (await _fileScanner.ScanAsync(Directory, _options.SupportFileExtensions.Split(';')))
.SelectMany(t => t.FilePaths)
.ToList();
if (files.Count == 0)
{
_logger.LogError("没有找到任何音乐文件。");
throw new ErrorCodeException(ErrorCodes.NoFilesWereScanned);
}
_logger.LogInformation($"已经扫描到了 {files.Count} 个音乐文件。");
return files;
}
private async Task<ImmutableList<MusicInfo>> LoadMusicInfoAsync(IReadOnlyCollection<string> files)
{
_logger.LogInformation("开始加载音乐文件的标签信息...");
var warpTask = new WarpTask(ParallelNumber);
var warpTaskList = files.Select(file => warpTask.RunAsync(() => Task.Run(async () => await _tagLoader.LoadTagAsync(file))));
var result = await Task.WhenAll(warpTaskList);
_logger.LogInformation($"已成功加载 {files.Count} 个音乐文件的标签信息。");
return result.ToImmutableList();
}
#region > <
private async ValueTask DownloadLyricFilesAsync(ImmutableList<MusicInfo> musicInfos)
{
_logger.LogInformation("开始下载歌词文件数据...");
var downloader = _lyricDownloaderList.FirstOrDefault(d => d.DownloaderName == InternalLyricDownloaderNames.NetEase);
var warpTask = new WarpTask(ParallelNumber);
var warpTaskList = musicInfos.Select(info =>
warpTask.RunAsync(() => Task.Run(async () => await DownloadLyricTaskLogicAsync(downloader, info))));
await Task.WhenAll(warpTaskList);
_logger.LogInformation($"歌词数据下载完成,成功: {musicInfos.Count} 失败{0}。");
}
private async Task DownloadLyricTaskLogicAsync(ILyricDownloader downloader, MusicInfo info)
{
try
{
var lyric = await downloader.DownloadAsync(info.Name, info.Artist);
var filePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!, $"{Path.GetFileNameWithoutExtension(info.FilePath)}.lrc");
if (File.Exists(filePath))
{
return;
}
if (lyric.IsPruneMusic)
{
return;
}
await using var stream = new FileStream(filePath, FileMode.Create);
await using var sw = new StreamWriter(stream);
await sw.WriteAsync(lyric.ToString());
await sw.FlushAsync();
}
catch (ErrorCodeException ex)
{
_logger.LogWarningInfo(ex);
}
}
#endregion
#region > <
private async ValueTask DownloadAlbumAsync(ImmutableList<MusicInfo> musicInfos)
{
_logger.LogInformation("开始下载专辑图像数据...");
var downloader = _albumDownloaderList.FirstOrDefault(d => d.DownloaderName == InternalAlbumDownloaderNames.NetEase);
var warpTask = new WarpTask(ParallelNumber);
var warpTaskList = musicInfos.Select(info =>
warpTask.RunAsync(() => Task.Run(async () => await DownloadAlbumTaskLogicAsync(downloader, info))));
await Task.WhenAll(warpTaskList);
_logger.LogInformation($"专辑数据下载完成,成功: {musicInfos.Count} 失败{0}。");
}
private async Task DownloadAlbumTaskLogicAsync(IAlbumDownloader downloader, MusicInfo info)
{
try
{
var album = await downloader.DownloadAsync(info.Name, info.Artist);
var filePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!, $"{Path.GetFileNameWithoutExtension(info.FilePath)}.png");
if (File.Exists(filePath) || album.Length <= 0)
{
return;
}
await new FileStream(filePath, FileMode.Create).WriteBytesToFileAsync(album, 1024);
}
catch (ErrorCodeException ex)
{
_logger.LogWarningInfo(ex);
}
}
#endregion
}
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure.IO;
namespace ZonyLrcTools.Cli.Commands
{
[Command("scan", Description = "扫描指定目录下符合条件的音乐文件数量。")]
public class ScanCommand : ToolCommandBase
{
private readonly IFileScanner _fileScanner;
private readonly ToolOptions _options;
private readonly ILogger<ScanCommand> _logger;
public ScanCommand(IFileScanner fileScanner,
IOptions<ToolOptions> options,
ILogger<ScanCommand> logger)
{
_fileScanner = fileScanner;
_logger = logger;
_options = options.Value;
}
[Option("-d|--dir", CommandOptionType.SingleValue, Description = "指定需要扫描的目录。")]
[DirectoryExists]
public string DirectoryPath { get; set; }
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
var result = await _fileScanner.ScanAsync(
DirectoryPath,
_options.SupportFileExtensions.Split(';'));
_logger.LogInformation($"目录扫描完成,共扫描到 {result.Sum(f => f.FilePaths.Count)} 个音乐文件。");
return 0;
}
public override List<string> CreateArgs()
{
return new();
}
}
}

View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Extensions;
namespace ZonyLrcTools.Cli.Commands
{
[Command("lyric-tool")]
[Subcommand(typeof(ScanCommand), typeof(DownloadCommand))]
public class ToolCommand : ToolCommandBase
{
public override List<string> CreateArgs()
{
return new();
}
public static async Task<int> Main(string[] args)
{
ConfigureLogger();
ConfigureErrorMessage();
try
{
return await BuildHostedService(args);
}
catch (Exception ex)
{
return HandleException(ex);
}
finally
{
Log.CloseAndFlush();
}
}
#region > <
private static void ConfigureErrorMessage() => ErrorCodeHelper.LoadErrorMessage();
private static void ConfigureLogger()
{
Log.Logger = new LoggerConfiguration()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.Console())
.WriteTo.Logger(lc =>
{
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning)
.WriteTo.Async(c => c.File("Logs/warnings.txt"));
})
.WriteTo.Logger(lc =>
{
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Error)
.WriteTo.Async(c => c.File("Logs/errors.txt"));
})
.CreateLogger();
}
private static Task<int> BuildHostedService(string[] args)
{
return new HostBuilder()
.ConfigureLogging(builder => builder.AddSerilog())
.ConfigureHostConfiguration(builder =>
{
builder
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
})
.ConfigureServices((_, services) =>
{
services.AddSingleton(PhysicalConsole.Singleton);
services.BeginAutoDependencyInject<ToolCommand>();
services.ConfigureConfiguration();
services.ConfigureToolService();
})
.RunCommandLineApplicationAsync<ToolCommand>(args);
}
private static int HandleException(Exception ex)
{
switch (ex)
{
case ErrorCodeException exception:
Log.Logger.Error($"出现了未处理的异常,错误代码: {exception.ErrorCode},错误信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}\n调用栈:\n{exception.StackTrace}");
Environment.Exit(exception.ErrorCode);
return exception.ErrorCode;
case Exception unknownException:
Log.Logger.Error($"出现了未处理的异常: {unknownException.Message}\n调用栈:\n{unknownException.StackTrace}");
Environment.Exit(-1);
return 1;
default:
return 1;
}
}
#endregion
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
namespace ZonyLrcTools.Cli.Commands
{
[HelpOption("--help|-h", Description = "欢迎使用 ZonyLrcToolsX Command Line Interface。")]
public abstract class ToolCommandBase
{
public abstract List<string> CreateArgs();
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
{
return Task.FromResult(0);
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
namespace ZonyLrcTools.Cli.Commands
{
/// <summary>
/// 工具类相关命令。
/// </summary>
[Command("util", Description = "提供常用的工具类功能。")]
public class UtilityCommand : ToolCommandBase
{
public override List<string> CreateArgs() => new();
protected override Task<int> OnExecuteAsync(CommandLineApplication app)
{
return base.OnExecuteAsync(app);
}
}
}

View File

@@ -0,0 +1,29 @@
using ZonyLrcTools.Cli.Infrastructure.Lyric;
using ZonyLrcTools.Cli.Infrastructure.Network;
using ZonyLrcTools.Cli.Infrastructure.Tag;
namespace ZonyLrcTools.Cli.Config
{
public class ToolOptions
{
/// <summary>
/// 支持的音乐文件后缀集合,以 ; 进行分隔。
/// </summary>
public string SupportFileExtensions { get; set; }
/// <summary>
/// 歌词下载相关的配置信息。
/// </summary>
public LyricItemCollectionOption LyricOption { get; set; }
/// <summary>
/// 标签加载器的加载配置项。
/// </summary>
public TagInfoProviderOptions TagInfoProviderOptions { get; set; }
/// <summary>
/// 网络代理相关的配置信息。
/// </summary>
public NetworkOptions NetworkOptions { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.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);
}
}

View File

@@ -0,0 +1,18 @@
namespace ZonyLrcTools.Cli.Infrastructure.Album
{
/// <summary>
/// 定义了程序默认提供的专辑图像下载器。
/// </summary>
public static class InternalAlbumDownloaderNames
{
/// <summary>
/// 网易云音乐专辑图像下载器。
/// </summary>
public const string NetEase = nameof(NetEase);
/// <summary>
/// QQ 音乐专辑图像下载器。
/// </summary>
public const string QQ = nameof(QQ);
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.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.GetFirstSongId()),
_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));
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.Album.QQMusic
{
public class QQMusicAlbumDownloader : IAlbumDownloader, ITransientDependency
{
public string DownloaderName => InternalAlbumDownloaderNames.QQ;
private readonly IWarpHttpClient _warpHttpClient;
private readonly Action<HttpRequestMessage> _defaultOption;
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;
_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.GetAsync<SongSearchResponse>(
SearchApi,
requestParameter);
return null;
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Cli.Infrastructure.Extensions;
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
{
public static class AutoDependencyInjectExtensions
{
/// <summary>
/// 开始进行自动依赖注入。
/// </summary>
/// <remarks>
/// 会根据实现了 <see cref="ITransientDependency"/> 或 <see cref="ISingletonDependency"/> 的接口进行自动注入。
/// </remarks>
/// <param name="services">服务定义集合。</param>
/// <typeparam name="TAssemblyType">需要注入的任意类型。</typeparam>
public static IServiceCollection BeginAutoDependencyInject<TAssemblyType>(this IServiceCollection services)
{
var allTypes = typeof(TAssemblyType).Assembly
.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
.ToArray();
var transientTypes = allTypes.Where(t => typeof(ITransientDependency).IsAssignableFrom(t));
var singletonTypes = allTypes.Where(t => typeof(ISingletonDependency).IsAssignableFrom(t));
transientTypes.Foreach(t =>
{
foreach (var exposedService in GetDefaultExposedTypes(t))
{
services.Add(CreateServiceDescriptor(t, exposedService, ServiceLifetime.Transient));
}
});
singletonTypes.Foreach(t =>
{
foreach (var exposedService in GetDefaultExposedTypes(t))
{
services.Add(CreateServiceDescriptor(t, exposedService, ServiceLifetime.Singleton));
}
});
return services;
}
public static List<Type> GetDefaultExposedTypes(Type type)
{
var serviceTypes = new List<Type>();
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
{
var interfaceName = interfaceType.Name;
if (interfaceName.StartsWith("I"))
{
interfaceName = interfaceName.Substring(1, interfaceName.Length - 1);
}
if (type.Name.EndsWith(interfaceName))
{
serviceTypes.Add(interfaceType);
serviceTypes.Add(type);
}
}
return serviceTypes;
}
public static ServiceDescriptor CreateServiceDescriptor(Type implementationType,
Type exposingServiceType,
ServiceLifetime lifetime)
{
return new ServiceDescriptor(exposingServiceType, implementationType, lifetime);
}
}
}

View File

@@ -0,0 +1,9 @@
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
{
/// <summary>
/// 继承了本接口的类都会以单例的形式注入到 IoC 容器当中。
/// </summary>
public interface ISingletonDependency
{
}
}

View File

@@ -0,0 +1,9 @@
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
{
/// <summary>
/// 继承了本接口的类都会以瞬时的形式注入到 IoC 容器当中。
/// </summary>
public interface ITransientDependency
{
}
}

View File

@@ -0,0 +1,57 @@
using System.IO;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
{
/// <summary>
/// Service 注入的扩展方法。
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 配置工具会用到的服务。
/// </summary>
public static IServiceCollection ConfigureToolService(this IServiceCollection services)
{
if (services == null)
{
return null;
}
services.AddHttpClient(DefaultWarpHttpClient.HttpClientNameConstant)
.ConfigurePrimaryHttpMessageHandler(provider =>
{
var option = provider.GetRequiredService<IOptions<ToolOptions>>().Value;
return new HttpClientHandler
{
UseProxy = option.NetworkOptions.Enable,
Proxy = new WebProxy($"{option.NetworkOptions.ProxyIp}:{option.NetworkOptions.ProxyPort}")
};
});
return services;
}
/// <summary>
/// 配置工具关联的配置信息(<see cref="IConfiguration"/>)。
/// </summary>
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
services.Configure<ToolOptions>(configuration.GetSection("ToolOption"));
return services;
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
{
/// <summary>
/// 带错误码的异常实现。
/// </summary>
public class ErrorCodeException : Exception
{
public int ErrorCode { get; }
public object AttachObject { get; }
/// <summary>
/// 构建一个新的 <see cref="ErrorCodeException"/> 对象。
/// </summary>
/// <param name="errorCode">错误码,参考 <see cref="ErrorCodes"/> 类的定义。</param>
/// <param name="message">错误信息。</param>
/// <param name="attachObj">附加的对象数据。</param>
public ErrorCodeException(int errorCode, string message = null, object attachObj = null) : base(message)
{
ErrorCode = errorCode;
AttachObject = attachObj;
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
{
/// <summary>
/// 错误码相关的帮助类。
/// </summary>
public static class ErrorCodeHelper
{
public static Dictionary<int, string> ErrorMessages { get; }
static ErrorCodeHelper()
{
ErrorMessages = new Dictionary<int, string>();
}
/// <summary>
/// 从 err_msg.json 文件加载错误信息。
/// </summary>
public static void LoadErrorMessage()
{
// 防止重复加载。
if (ErrorMessages.Count != 0)
{
return;
}
var jsonPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "error_msg.json");
using var jsonReader = new JsonTextReader(File.OpenText(jsonPath));
var jsonObj = JObject.Load(jsonReader);
var errors = jsonObj.SelectTokens("$.Error.*");
var warnings = jsonObj.SelectTokens("$.Warning.*");
errors.Union(warnings).Select(m => m.Parent).OfType<JProperty>().ToList()
.ForEach(m => ErrorMessages.Add(int.Parse(m.Name), m.Value.Value<string>()));
}
public static string GetMessage(int errorCode) => ErrorMessages[errorCode];
}
}

View File

@@ -0,0 +1,86 @@
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
{
/// <summary>
/// 错误码。
/// </summary>
public static class ErrorCodes
{
#region > <
/// <summary>
/// 文本: 待搜索的后缀不能为空。
/// </summary>
public const int FileSuffixIsEmpty = 10001;
/// <summary>
/// 文本: 需要扫描的目录不存在,请确认路径是否正确。。
/// </summary>
public const int DirectoryNotExist = 10002;
/// <summary>
/// 文本: 不能获取文件的后缀信息。
/// </summary>
public const int UnableToGetTheFileExtension = 10003;
/// <summary>
/// 文本: 没有扫描到任何音乐文件。
/// </summary>
public const int NoFilesWereScanned = 10004;
#endregion
#region > <
/// <summary>
/// 文本: 扫描文件时出现了错误。
/// </summary>
public const int ScanFileError = 50001;
/// <summary>
/// 文本: 歌曲名称或歌手名称均为空,无法进行搜索。
/// </summary>
public const int SongNameAndArtistIsNull = 50002;
/// <summary>
/// 文本: 歌曲名称不能为空,无法进行搜索。
/// </summary>
public const int SongNameIsNull = 50003;
/// <summary>
/// 文本: 下载器没有搜索到对应的歌曲信息。
/// </summary>
public const int NoMatchingSong = 50004;
/// <summary>
/// 文本: 下载请求的返回值不合法,可能是服务端故障。
/// </summary>
public const int TheReturnValueIsIllegal = 50005;
/// <summary>
/// 文本: 标签信息读取器为空,无法解析音乐 Tag 信息。
/// </summary>
public const int LoadTagInfoProviderError = 50006;
/// <summary>
/// 文本: TagLib 标签读取器出现了预期之外的异常。
/// </summary>
public const int TagInfoProviderLoadInfoFailed = 50007;
/// <summary>
/// 文本: 服务接口限制,无法进行请求,请尝试使用代理服务器。
/// </summary>
public const int ServiceUnavailable = 50008;
/// <summary>
/// 文本: 对目标服务器执行 Http 请求失败。
/// </summary>
public const int HttpRequestFailed = 50009;
/// <summary>
/// 文本: Http 请求的结果反序列化为 Json 失败。
/// </summary>
public const int HttpResponseConvertJsonFailed = 50010;
#endregion
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
{
/// <summary>
/// Linq 相关的扩展方法。
/// </summary>
public static class LinqHelper
{
/// <summary>
/// 使用 Lambda 的形式遍历指定的迭代器。
/// </summary>
/// <param name="items">等待遍历的迭代器实例。</param>
/// <param name="action">遍历时需要执行的操作。</param>
public static void Foreach<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Text;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
{
/// <summary>
/// 日志记录相关的扩展方法。
/// </summary>
public static class LoggerExtensions
{
/// <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());
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace ZonyLrcTools.Cli.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;
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Extensions;
namespace ZonyLrcTools.Cli.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);
}
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
namespace ZonyLrcTools.Cli.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;
}
}
}

View File

@@ -0,0 +1,41 @@
using System.IO;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.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();
}
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.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);
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <summary>
/// 歌词数据下载器,用于匹配并下载歌曲的歌词。
/// </summary>
public interface ILyricDownloader
{
/// <summary>
/// 下载歌词数据。
/// </summary>
/// <param name="songName">歌曲的名称。</param>
/// <param name="artist">歌曲的作者。</param>
/// <returns>歌曲的歌词数据对象。</returns>
ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist);
/// <summary>
/// 下载器的名称。
/// </summary>
string DownloaderName { get; }
}
}

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <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,30 @@
using System.Threading.Tasks;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou
{
public class KuGourLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.KuGou;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
public KuGourLyricDownloader(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
}
protected override ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
throw new System.NotImplementedException();
}
protected override ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data)
{
throw new System.NotImplementedException();
}
}
}

View File

@@ -0,0 +1,23 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <summary>
/// 换行符格式定义。
/// </summary>
public static class LineBreakType
{
/// <summary>
/// Windows 系统。
/// </summary>
public const string Windows = "\r\n";
/// <summary>
/// macOS 系统。
/// </summary>
public const string MacOs = "\r";
/// <summary>
/// UNIX 系统(Linux)。
/// </summary>
public const string Unix = "\n";
}
}

View File

@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <summary>
/// 歌词下载器的基类,定义了歌词下载器的常规逻辑。
/// </summary>
public abstract class LyricDownloader : ILyricDownloader, ITransientDependency
{
public abstract string DownloaderName { get; }
public virtual async ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist)
{
var args = new LyricDownloaderArgs(songName, artist);
await ValidateAsync(args);
var downloadDataBytes = await DownloadDataAsync(args);
return await GenerateLyricAsync(downloadDataBytes);
}
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;
}
protected abstract ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args);
protected abstract ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data);
}
}

View File

@@ -0,0 +1,15 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
public class LyricDownloaderArgs
{
public string SongName { get; set; }
public string Artist { get; set; }
public LyricDownloaderArgs(string songName, string artist)
{
SongName = songName;
Artist = artist;
}
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.Text.RegularExpressions;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <summary>
/// 每一行歌词的对象。
/// </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);
}
public override string ToString() => $"[{Minute:00}:{Second:00.00}]{LyricText}";
}
}

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ZonyLrcTools.Cli.Infrastructure.Extensions;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <summary>
/// 歌词数据,包含多条歌词对象(<see cref="LyricItem"/>)。
/// </summary>
public class LyricItemCollection : List<LyricItem>
{
/// <summary>
/// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。
/// </summary>
public bool IsPruneMusic => Count == 0;
public LyricItemCollectionOption Option { get; private set; }
public LyricItemCollection(LyricItemCollectionOption option)
{
Option = option;
}
public static LyricItemCollection operator +(LyricItemCollection left, LyricItemCollection right)
{
if (right.IsPruneMusic)
{
return left;
}
var option = left.Option;
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("\r\n"));
return lyricBuilder.ToString().TrimEnd("\r\n");
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
public class LyricItemCollectionFactory : ILyricItemCollectionFactory, ITransientDependency
{
private readonly ToolOptions _options;
public LyricItemCollectionFactory(IOptions<ToolOptions> options)
{
_options = options.Value;
}
public LyricItemCollection Build(string sourceLyric, string translateLyric = null)
{
var items = new LyricItemCollection(_options.LyricOption);
if (string.IsNullOrEmpty(sourceLyric))
{
return items;
}
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
foreach (Match match in regex.Matches(sourceLyric))
{
items.Add(new LyricItem(match.Value));
}
return items;
}
}
}

View File

@@ -0,0 +1,17 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
public class LyricItemCollectionOption
{
/// <summary>
/// 双语歌词是否合并为一行。
/// </summary>
public bool IsOneLine { get; set; } = false;
/// <summary>
/// 换行符格式,取值来自 <see cref="LineBreakType"/> 常量类。
/// </summary>
public string LineBreak { get; set; } = LineBreakType.Windows;
public static readonly LyricItemCollectionOption NullInstance = new();
}
}

View File

@@ -0,0 +1,34 @@
using Newtonsoft.Json;
// ReSharper disable InconsistentNaming
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
{
public class GetLyricRequest
{
public GetLyricRequest(long songId)
{
OS = "osx";
Id = songId;
Lv = Kv = Tv = -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; }
}
}

View File

@@ -0,0 +1,45 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.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("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,19 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.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,59 @@
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.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; }
public SongSearchRequest()
{
CsrfToken = string.Empty;
Type = 1;
Offset = 0;
IsTotal = true;
Limit = 5;
}
public SongSearchRequest(string musicName, string artistName) : this()
{
SearchKey = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
}
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
{
public class SongSearchResponse
{
[JsonProperty("result")] public InnerListItemModel Items { get; set; }
[JsonProperty("code")] public int StatusCode { get; set; }
public int GetFirstSongId()
{
return Items.SongItems[0].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; }
}
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.Cli.Infrastructure.Lyric.NetEase.JsonModel
{
public static class SongSearchResponseStatusCode
{
public const int Success = 200;
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase
{
public class NetEaseLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.NetEase;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
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)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
}
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
NetEaseSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist),
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.GetFirstSongId()),
msg => msg.Headers.Referrer = new Uri(NetEaseRequestReferer));
return Encoding.UTF8.GetBytes(lyricResponse);
}
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data)
{
await ValueTask.CompletedTask;
var json = JsonConvert.DeserializeObject<GetLyricResponse>(Encoding.UTF8.GetString(data));
if (json?.OriginalLyric == null)
{
return new LyricItemCollection(LyricItemCollectionOption.NullInstance);
}
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,80 @@
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel
{
public class SongSearchRequest
{
[JsonProperty("ct")] public int UnknownParameter1 { get; set; }
[JsonProperty("qqmusic_ver")] public int ClientVersion { get; set; }
[JsonProperty("new_json")] public int UnknownParameter2 { get; set; }
[JsonProperty("remoteplace")] public string RemotePlace { get; set; }
[JsonProperty("t")] public int UnknownParameter3 { get; set; }
[JsonProperty("aggr")] public int UnknownParameter4 { get; set; }
[JsonProperty("cr")] public int UnknownParameter5 { get; set; }
[JsonProperty("catZhida")] public int UnknownParameter6 { get; set; }
[JsonProperty("lossless")] public int LossLess { get; set; }
[JsonProperty("flag_qc")] public int UnknownParameter7 { get; set; }
[JsonProperty("p")] public int Page { get; set; }
[JsonProperty("n")] public int Limit { get; set; }
[JsonProperty("w")] public string Keyword { get; set; }
[JsonProperty("g_tk")] public int UnknownParameter8 { get; set; }
[JsonProperty("hostUin")] public int UnknownParameter9 { get; set; }
[JsonProperty("format")] public string ResultFormat { get; set; }
[JsonProperty("inCharset")] public string InCharset { get; set; }
[JsonProperty("outCharset")] public string OutCharset { get; set; }
[JsonProperty("notice")] public int UnknownParameter10 { get; set; }
[JsonProperty("platform")] public string Platform { get; set; }
[JsonProperty("needNewCode")] public int UnknownParameter11 { get; set; }
public SongSearchRequest()
{
UnknownParameter1 = 24;
ClientVersion = 1298;
UnknownParameter2 = 1;
RemotePlace = "txt.yqq.song";
UnknownParameter3 = 0;
UnknownParameter4 = 1;
UnknownParameter5 = 1;
UnknownParameter6 = 1;
LossLess = 0;
UnknownParameter7 = 0;
Page = 1;
Limit = 5;
UnknownParameter8 = 5381;
UnknownParameter9 = 0;
ResultFormat = "json";
InCharset = "utf8";
OutCharset = "utf8";
UnknownParameter10 = 0;
Platform = "yqq";
UnknownParameter11 = 0;
}
public SongSearchRequest(string musicName, string artistName) : this()
{
Keyword = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.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("list")]
public List<QQMusicInnerSongItem> SongItems { get; set; }
}
public class QQMusicInnerSongItem
{
[JsonProperty("mid")]
public string SongId { get; set; }
}
public class AlbumInfo
{
[JsonProperty("id")]
public long Id { get; set; }
}
}

View File

@@ -0,0 +1,30 @@
using System.Threading.Tasks;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic
{
public class QQLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.QQ;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
public QQLyricDownloader(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
}
protected override ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
throw new System.NotImplementedException();
}
protected override ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data)
{
throw new System.NotImplementedException();
}
}
}

View File

@@ -0,0 +1,18 @@
namespace ZonyLrcTools.Cli.Infrastructure
{
public class MusicInfo
{
public string FilePath { get; }
public string Name { get; }
public string Artist { get; }
public MusicInfo(string filePath, string name, string artist)
{
FilePath = filePath;
Name = name;
Artist = artist;
}
}
}

View File

@@ -0,0 +1,151 @@
using System;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Network
{
public class DefaultWarpHttpClient : IWarpHttpClient, ITransientDependency
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<DefaultWarpHttpClient> _logger;
public const string HttpClientNameConstant = "WarpClient";
public DefaultWarpHttpClient(IHttpClientFactory httpClientFactory,
ILogger<DefaultWarpHttpClient> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async ValueTask<string> PostAsync(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null)
{
var parametersStr = isQueryStringParam ? BuildQueryString(parameters) : BuildJsonBodyString(parameters);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
requestMessage.Content = new StringContent(parametersStr);
requestOption?.Invoke(requestMessage);
using var responseMessage = await BuildHttpClient().SendAsync(requestMessage);
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
return responseMessage.StatusCode switch
{
HttpStatusCode.OK => responseContentString,
HttpStatusCode.ServiceUnavailable => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable),
_ => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable, attachObj: new {parametersStr, responseContentString})
};
}
public async ValueTask<TResponse> PostAsync<TResponse>(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null)
{
var responseString = await PostAsync(url, parameters, isQueryStringParam, requestOption);
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new {parameters, responseString});
try
{
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseString);
if (responseObj != null) return responseObj;
throw throwException;
}
catch (JsonSerializationException)
{
throw throwException;
}
}
public async ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
{
var requestParamsStr = BuildQueryString(parameters);
var requestMsg = new HttpRequestMessage(HttpMethod.Get, new Uri($"{url}?{requestParamsStr}"));
requestOption?.Invoke(requestMsg);
using (var responseMsg = await BuildHttpClient().SendAsync(requestMsg))
{
var responseContent = await responseMsg.Content.ReadAsStringAsync();
return responseMsg.StatusCode switch
{
HttpStatusCode.OK => responseContent,
HttpStatusCode.ServiceUnavailable => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable),
_ => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable, attachObj: new {requestParamsStr, responseContent})
};
}
}
public async ValueTask<TResponse> GetAsync<TResponse>(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
{
var responseStr = await GetAsync(url, parameters, requestOption);
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new {parameters, responseStr});
try
{
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseStr);
if (responseObj != null) return responseObj;
throw throwException;
}
catch (JsonSerializationException)
{
throw throwException;
}
}
protected virtual HttpClient BuildHttpClient()
{
return _httpClientFactory.CreateClient(HttpClientNameConstant);
}
private string BuildQueryString(object parameters)
{
if (parameters == null)
{
return string.Empty;
}
var type = parameters.GetType();
if (type == typeof(string))
{
return parameters as string;
}
var properties = type.GetProperties();
var paramBuilder = new StringBuilder();
foreach (var propertyInfo in properties)
{
var jsonProperty = propertyInfo.GetCustomAttribute<JsonPropertyAttribute>();
var propertyName = jsonProperty != null ? jsonProperty.PropertyName : propertyInfo.Name;
paramBuilder.Append($"{propertyName}={propertyInfo.GetValue(parameters)}&");
}
return paramBuilder.ToString().TrimEnd('&');
}
private string BuildJsonBodyString(object parameters)
{
if (parameters == null) return string.Empty;
if (parameters is string result) return result;
return JsonConvert.SerializeObject(parameters);
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Network
{
/// <summary>
/// 基于 <see cref="IHttpClientFactory"/> 封装的 HTTP 请求客户端。
/// </summary>
public interface IWarpHttpClient
{
/// <summary>
/// 根据指定的配置执行 POST 请求,并以 <see cref="string"/> 作为返回值。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="isQueryStringParam">是否以 QueryString 形式携带参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> PostAsync(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null);
/// <summary>
/// 根据指定的配置执行 POST 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="isQueryStringParam">是否以 QueryString 形式携带参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> PostAsync<TResponse>(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null);
/// <summary>
/// 根据指定的配置执行 GET 请求,并以 <see cref="string"/> 作为返回值。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
/// <summary>
/// 根据指定的配置执行 GET 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> GetAsync<TResponse>(
string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
}
}

View File

@@ -0,0 +1,23 @@
namespace ZonyLrcTools.Cli.Infrastructure.Network
{
/// <summary>
/// 工具网络相关的设定。
/// </summary>
public class NetworkOptions
{
/// <summary>
/// 是否启用了网络代理功能。
/// </summary>
public bool Enable { get; set; }
/// <summary>
/// 代理服务器的 Ip。
/// </summary>
public string ProxyIp { get; set; }
/// <summary>
/// 代理服务器的 端口。
/// </summary>
public int ProxyPort { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
public class DefaultTagLoader : ITagLoader, ITransientDependency
{
protected readonly IEnumerable<ITagInfoProvider> TagInfoProviders;
public DefaultTagLoader(IEnumerable<ITagInfoProvider> tagInfoProviders)
{
TagInfoProviders = tagInfoProviders;
}
public virtual async ValueTask<MusicInfo> LoadTagAsync(string filePath)
{
if (!TagInfoProviders.Any())
{
throw new ErrorCodeException(ErrorCodes.LoadTagInfoProviderError);
}
foreach (var provider in TagInfoProviders.OrderBy(p => p.Priority))
{
var info = await provider.LoadAsync(filePath);
if (info != null)
{
return info;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
public class FileNameTagInfoProvider : ITagInfoProvider, ITransientDependency
{
public int Priority => 2;
public string Name => ConstantName;
public const string ConstantName = "FileName";
private readonly ToolOptions Options;
public FileNameTagInfoProvider(IOptions<ToolOptions> options)
{
Options = options.Value;
}
public async ValueTask<MusicInfo> LoadAsync(string filePath)
{
await ValueTask.CompletedTask;
var match = Regex.Match(Path.GetFileNameWithoutExtension(filePath), Options.TagInfoProviderOptions.FileNameRegularExpressions);
if (match.Groups.Count != 3)
{
return null;
}
return new MusicInfo(filePath, match.Groups["name"].Value, match.Groups["artist"].Value);
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
public interface ITagInfoProvider
{
int Priority { get; }
string Name { get; }
ValueTask<MusicInfo> LoadAsync(string filePath);
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
public interface ITagLoader
{
/// <summary>
/// 加载歌曲的标签信息。
/// </summary>
/// <param name="filePath">歌曲文件的路径。</param>
/// <returns>加载完成的歌曲信息。</returns>
ValueTask<MusicInfo> LoadTagAsync(string filePath);
}
}

View File

@@ -0,0 +1,10 @@
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
public class TagInfoProviderOptions
{
/// <summary>
/// 用于从文件名当中提取歌曲名、歌手名。
/// </summary>
public string FileNameRegularExpressions { get; set; }
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Threading.Tasks;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
public class TaglibTagInfoProvider : ITagInfoProvider, ITransientDependency
{
public int Priority => 1;
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);
}
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Threading
{
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);
}
}
}

View File

@@ -0,0 +1,20 @@
{
"Error": {
"10001": "待搜索的后缀不能为空。",
"10002": "需要扫描的目录不存在,请确认路径是否正确。",
"10003": "不能获取文件的后缀信息。",
"10004": "没有扫描到任何音乐文件。"
},
"Warning": {
"50001": "扫描文件时出现了错误。",
"50002": "歌曲名称或歌手名称均为空,无法进行搜索。",
"50003": "歌曲名称不能为空,无法进行搜索。",
"50004": "下载器没有搜索到对应的歌曲信息。",
"50005": "下载请求的返回值不合法,可能是服务端故障。",
"50006": "标签信息读取器为空,无法解析音乐 Tag 信息。",
"50007": "TagLib 标签读取器出现了预期之外的异常。",
"50008": "服务接口限制,无法进行请求,请尝试使用代理服务器。",
"50009": "对目标服务器执行 Http 请求失败。",
"50010": "Http 请求的结果反序列化为 Json 失败。"
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.1.0" />
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Refit" Version="6.0.38" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.38" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.0.38" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="TagLibSharp" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="Resources\error_msg.json" />
<Content Include="Resources\error_msg.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="Infrastructure\Network\Exceptions" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ToolOption": {
"SupportFileExtensions": "*.mp3;*.flac",
"NetworkOptions": {
"Enable": true,
"ProxyIp": "127.0.0.1",
"ProxyPort": 4780
},
"TagInfoProviderOptions": {
"FileNameRegularExpressions": "(?'artist'.+)\\s-\\s(?'name'.+)"
}
}
}

19
src/ZonyLrcTools.Cli/publish.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
read -r -p "请输入版本号:" Version
Platforms=('win-x64' 'linux-x64' 'osx-x64')
if ! [ -d './TempFiles' ];
then
mkdir ./TempFiles
fi
rm -rf ./TempFiles/*
for platform in "${Platforms[@]}"
do
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
zip -r -j ./bin/Release/net5.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$Version".zip ./bin/Release/net5.0/"$platform"/publish/
mv ./bin/Release/net5.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$Version".zip ./TempFiles
done