mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-07-01 20:30:41 +00:00
refactor: Refactor lyrics download logic and abstract a new logger.
This commit is contained in:
parent
3e27e18098
commit
7d17fc0b97
@ -1,12 +1,9 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using McMaster.Extensions.CommandLineUtils;
|
using McMaster.Extensions.CommandLineUtils;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
||||||
using ZonyLrcTools.Common;
|
using ZonyLrcTools.Common;
|
||||||
@ -15,6 +12,7 @@ using ZonyLrcTools.Common.Configuration;
|
|||||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Extensions;
|
using ZonyLrcTools.Common.Infrastructure.Extensions;
|
||||||
using ZonyLrcTools.Common.Infrastructure.IO;
|
using ZonyLrcTools.Common.Infrastructure.IO;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Logging;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Threading;
|
using ZonyLrcTools.Common.Infrastructure.Threading;
|
||||||
using ZonyLrcTools.Common.Lyrics;
|
using ZonyLrcTools.Common.Lyrics;
|
||||||
using File = System.IO.File;
|
using File = System.IO.File;
|
||||||
@ -24,26 +22,25 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
[Command("download", Description = "下载歌词文件或专辑图像。")]
|
[Command("download", Description = "下载歌词文件或专辑图像。")]
|
||||||
public class DownloadCommand : ToolCommandBase
|
public class DownloadCommand : ToolCommandBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<DownloadCommand> _logger;
|
private readonly ILyricsDownloader _lyricsDownloader;
|
||||||
private readonly IFileScanner _fileScanner;
|
private readonly IFileScanner _fileScanner;
|
||||||
private readonly ITagLoader _tagLoader;
|
|
||||||
private readonly IEnumerable<ILyricsProvider> _lyricDownloaderList;
|
|
||||||
private readonly IEnumerable<IAlbumDownloader> _albumDownloaderList;
|
private readonly IEnumerable<IAlbumDownloader> _albumDownloaderList;
|
||||||
|
private readonly ITagLoader _tagLoader;
|
||||||
|
private readonly IWarpLogger _logger;
|
||||||
|
|
||||||
private readonly GlobalOptions _options;
|
private readonly GlobalOptions _options;
|
||||||
|
|
||||||
public DownloadCommand(ILogger<DownloadCommand> logger,
|
public DownloadCommand(IFileScanner fileScanner,
|
||||||
IFileScanner fileScanner,
|
|
||||||
IOptions<GlobalOptions> options,
|
IOptions<GlobalOptions> options,
|
||||||
|
IEnumerable<IAlbumDownloader> albumDownloaderList,
|
||||||
ITagLoader tagLoader,
|
ITagLoader tagLoader,
|
||||||
IEnumerable<ILyricsProvider> lyricDownloaderList,
|
ILyricsDownloader lyricsDownloader, IWarpLogger logger)
|
||||||
IEnumerable<IAlbumDownloader> albumDownloaderList)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_fileScanner = fileScanner;
|
_fileScanner = fileScanner;
|
||||||
_tagLoader = tagLoader;
|
|
||||||
_lyricDownloaderList = lyricDownloaderList;
|
|
||||||
_albumDownloaderList = albumDownloaderList;
|
_albumDownloaderList = albumDownloaderList;
|
||||||
|
_tagLoader = tagLoader;
|
||||||
|
_lyricsDownloader = lyricsDownloader;
|
||||||
|
_logger = logger;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,18 +59,14 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
|
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
|
||||||
public int ParallelNumber { get; set; } = 2;
|
public int ParallelNumber { get; set; } = 2;
|
||||||
|
|
||||||
[Option] public string ErrorMessage { get; set; } = Path.Combine(Directory.GetCurrentDirectory(), "error.log");
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||||
{
|
{
|
||||||
if (DownloadLyric)
|
if (DownloadLyric)
|
||||||
{
|
{
|
||||||
await DownloadLyricFilesAsync(
|
var musicInfos = await LoadMusicInfoAsync(RemoveExistLyricFiles(await ScanMusicFilesAsync()));
|
||||||
await LoadMusicInfoAsync(
|
await _lyricsDownloader.DownloadAsync(musicInfos.ToList(), ParallelNumber);
|
||||||
RemoveExistLyricFiles(
|
|
||||||
await ScanMusicFilesAsync())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DownloadAlbum)
|
if (DownloadAlbum)
|
||||||
@ -94,11 +87,11 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
|
|
||||||
if (files.Count == 0)
|
if (files.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogError("没有找到任何音乐文件。");
|
await _logger.ErrorAsync("没有找到任何音乐文件。");
|
||||||
throw new ErrorCodeException(ErrorCodes.NoFilesWereScanned);
|
throw new ErrorCodeException(ErrorCodes.NoFilesWereScanned);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"已经扫描到了 {files.Count} 个音乐文件。");
|
await _logger.InfoAsync($"已经扫描到了 {files.Count} 个音乐文件。");
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
@ -118,7 +111,7 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogWarning($"已经存在歌词文件 {path},跳过。");
|
_logger.WarnAsync($"已经存在歌词文件 {path},跳过。").GetAwaiter().GetResult();
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
@ -126,7 +119,7 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
|
|
||||||
private async Task<ImmutableList<MusicInfo>> LoadMusicInfoAsync(IReadOnlyCollection<string> files)
|
private async Task<ImmutableList<MusicInfo>> LoadMusicInfoAsync(IReadOnlyCollection<string> files)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("开始加载音乐文件的标签信息...");
|
await _logger.InfoAsync("开始加载音乐文件的标签信息...");
|
||||||
|
|
||||||
var warpTask = new WarpTask(ParallelNumber);
|
var warpTask = new WarpTask(ParallelNumber);
|
||||||
var warpTaskList = files.Select(file => warpTask.RunAsync(() => Task.Run(async () => await _tagLoader.LoadTagAsync(file))));
|
var warpTaskList = files.Select(file => warpTask.RunAsync(() => Task.Run(async () => await _tagLoader.LoadTagAsync(file))));
|
||||||
@ -138,111 +131,16 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
// Load music total time info.
|
// Load music total time info.
|
||||||
// result.Foreach(m => { m.TotalTime = (long?)new AudioFileReader(m.FilePath).TotalTime.TotalMilliseconds; });
|
// result.Foreach(m => { m.TotalTime = (long?)new AudioFileReader(m.FilePath).TotalTime.TotalMilliseconds; });
|
||||||
|
|
||||||
_logger.LogInformation($"已成功加载 {files.Count} 个音乐文件的标签信息。");
|
await _logger.InfoAsync($"已成功加载 {files.Count} 个音乐文件的标签信息。");
|
||||||
|
|
||||||
return result.ToImmutableList();
|
return result.ToImmutableList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<ILyricsProvider> GetLyricDownloaderList()
|
|
||||||
{
|
|
||||||
var downloader = _options.Provider.Lyric.Plugin
|
|
||||||
.Where(op => op.Priority != -1)
|
|
||||||
.OrderBy(op => op.Priority)
|
|
||||||
.Join(_lyricDownloaderList,
|
|
||||||
op => op.Name,
|
|
||||||
loader => loader.DownloaderName,
|
|
||||||
(op, loader) => loader);
|
|
||||||
|
|
||||||
return downloader;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region > Lyric download logic <
|
|
||||||
|
|
||||||
private async ValueTask DownloadLyricFilesAsync(ImmutableList<MusicInfo> musicInfos)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("开始下载歌词文件数据...");
|
|
||||||
|
|
||||||
var downloaderList = GetLyricDownloaderList();
|
|
||||||
var warpTask = new WarpTask(ParallelNumber);
|
|
||||||
var warpTaskList = musicInfos.Select(info =>
|
|
||||||
warpTask.RunAsync(() => Task.Run(async () => await DownloadLyricTaskLogicAsync(downloaderList, info))));
|
|
||||||
|
|
||||||
await Task.WhenAll(warpTaskList);
|
|
||||||
|
|
||||||
_logger.LogInformation($"歌词数据下载完成,成功: {musicInfos.Count(m => m.IsSuccessful)} 失败{musicInfos.Count(m => m.IsSuccessful == false)}。");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DownloadLyricTaskLogicAsync(IEnumerable<ILyricsProvider> downloaderList, MusicInfo info)
|
|
||||||
{
|
|
||||||
async Task InternalDownloadLogicAsync(ILyricsProvider downloader)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var lyric = await downloader.DownloadAsync(info.Name, info.Artist, info.TotalTime);
|
|
||||||
var lyricFilePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!,
|
|
||||||
$"{Path.GetFileNameWithoutExtension(info.FilePath)}.lrc");
|
|
||||||
|
|
||||||
if (File.Exists(lyricFilePath))
|
|
||||||
{
|
|
||||||
File.Delete(lyricFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
info.IsSuccessful = true;
|
|
||||||
|
|
||||||
if (lyric.IsPruneMusic)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var stream = new FileStream(lyricFilePath, FileMode.Create);
|
|
||||||
await using var sw = new BinaryWriter(stream);
|
|
||||||
|
|
||||||
sw.Write(EncodingConvert(lyric));
|
|
||||||
await stream.FlushAsync();
|
|
||||||
}
|
|
||||||
catch (ErrorCodeException ex)
|
|
||||||
{
|
|
||||||
info.IsSuccessful = ex.ErrorCode == ErrorCodes.NoMatchingSong;
|
|
||||||
|
|
||||||
_logger.LogWarningInfo(ex);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError($"下载歌词文件时发生错误:{ex.Message},歌曲名: {info.Name},歌手: {info.Artist}。");
|
|
||||||
info.IsSuccessful = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var downloader in downloaderList)
|
|
||||||
{
|
|
||||||
await InternalDownloadLogicAsync(downloader);
|
|
||||||
|
|
||||||
if (info.IsSuccessful)
|
|
||||||
{
|
|
||||||
_logger.LogSuccessful(info);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] EncodingConvert(LyricItemCollection lyric)
|
|
||||||
{
|
|
||||||
var supportEncodings = Encoding.GetEncodings();
|
|
||||||
if (supportEncodings.All(x => x.Name != _options.Provider.Lyric.Config.FileEncoding))
|
|
||||||
{
|
|
||||||
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(_options.Provider.Lyric.Config.FileEncoding), lyric.GetUtf8Bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region > Ablum image download logic <
|
#region > Ablum image download logic <
|
||||||
|
|
||||||
private async ValueTask DownloadAlbumAsync(ImmutableList<MusicInfo> musicInfos)
|
private async ValueTask DownloadAlbumAsync(ImmutableList<MusicInfo> musicInfos)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("开始下载专辑图像数据...");
|
await _logger.InfoAsync("开始下载专辑图像数据...");
|
||||||
|
|
||||||
var downloader = _albumDownloaderList.FirstOrDefault(d => d.DownloaderName == InternalAlbumDownloaderNames.NetEase);
|
var downloader = _albumDownloaderList.FirstOrDefault(d => d.DownloaderName == InternalAlbumDownloaderNames.NetEase);
|
||||||
var warpTask = new WarpTask(ParallelNumber);
|
var warpTask = new WarpTask(ParallelNumber);
|
||||||
@ -251,7 +149,7 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
|
|
||||||
await Task.WhenAll(warpTaskList);
|
await Task.WhenAll(warpTaskList);
|
||||||
|
|
||||||
_logger.LogInformation($"专辑数据下载完成,成功: {musicInfos.Count(m => m.IsSuccessful)} 失败{musicInfos.Count(m => m.IsSuccessful == false)}。");
|
await _logger.InfoAsync($"专辑数据下载完成,成功: {musicInfos.Count(m => m.IsSuccessful)} 失败{musicInfos.Count(m => m.IsSuccessful == false)}。");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DownloadAlbumTaskLogicAsync(IAlbumDownloader downloader, MusicInfo info)
|
private async Task DownloadAlbumTaskLogicAsync(IAlbumDownloader downloader, MusicInfo info)
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Logging;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Cli.Infrastructure.Logging;
|
||||||
|
|
||||||
|
public class SerilogWarpLogger : IWarpLogger, ITransientDependency
|
||||||
|
{
|
||||||
|
private readonly ILogger<SerilogWarpLogger> _logger;
|
||||||
|
|
||||||
|
public SerilogWarpLogger(ILogger<SerilogWarpLogger> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DebugAsync(string message, Exception exception = null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(message, exception);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InfoAsync(string message, Exception exception = null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(message, exception);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WarnAsync(string message, Exception exception = null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(message, exception);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ErrorAsync(string message, Exception exception = null)
|
||||||
|
{
|
||||||
|
_logger.LogError(message, exception);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ using System.Text;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Logging;
|
||||||
|
|
||||||
namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
||||||
{
|
{
|
||||||
@ -16,9 +17,9 @@ namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
|||||||
/// <param name="logger">日志记录器实例。</param>
|
/// <param name="logger">日志记录器实例。</param>
|
||||||
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</param>
|
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</param>
|
||||||
/// <param name="e">异常实例,可为空。</param>
|
/// <param name="e">异常实例,可为空。</param>
|
||||||
public static void LogWarningWithErrorCode(this ILogger logger, int errorCode, Exception e = null)
|
public static void LogWarningWithErrorCode(this IWarpLogger logger, int errorCode, Exception e = null)
|
||||||
{
|
{
|
||||||
logger.LogWarning($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}");
|
logger.WarnAsync($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -26,7 +27,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">日志记录器的实例。</param>
|
/// <param name="logger">日志记录器的实例。</param>
|
||||||
/// <param name="exception">错误码异常实例。</param>
|
/// <param name="exception">错误码异常实例。</param>
|
||||||
public static void LogWarningInfo(this ILogger logger, ErrorCodeException exception)
|
public static void LogWarningInfo(this IWarpLogger logger, ErrorCodeException exception)
|
||||||
{
|
{
|
||||||
if (exception.ErrorCode < 50000)
|
if (exception.ErrorCode < 50000)
|
||||||
{
|
{
|
||||||
@ -36,7 +37,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
|||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append($"错误代码: {exception.ErrorCode},信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}");
|
sb.Append($"错误代码: {exception.ErrorCode},信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}");
|
||||||
sb.Append($"\n附加信息:\n {JsonConvert.SerializeObject(exception.AttachObject)}");
|
sb.Append($"\n附加信息:\n {JsonConvert.SerializeObject(exception.AttachObject)}");
|
||||||
logger.LogWarning(sb.ToString());
|
logger.WarnAsync(sb.ToString()).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -44,9 +45,9 @@ namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">日志记录器的实例。</param>
|
/// <param name="logger">日志记录器的实例。</param>
|
||||||
/// <param name="musicInfo">需要打印的歌曲信息。</param>
|
/// <param name="musicInfo">需要打印的歌曲信息。</param>
|
||||||
public static void LogSuccessful(this ILogger logger, MusicInfo musicInfo)
|
public static void LogSuccessful(this IWarpLogger logger, MusicInfo musicInfo)
|
||||||
{
|
{
|
||||||
logger.LogInformation($"歌曲名: {musicInfo.Name}, 艺术家: {musicInfo.Artist}, 下载成功.");
|
logger.InfoAsync($"歌曲名: {musicInfo.Name}, 艺术家: {musicInfo.Artist}, 下载成功.").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,16 +4,17 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Extensions;
|
using ZonyLrcTools.Common.Infrastructure.Extensions;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Logging;
|
||||||
|
|
||||||
namespace ZonyLrcTools.Common.Infrastructure.IO
|
namespace ZonyLrcTools.Common.Infrastructure.IO
|
||||||
{
|
{
|
||||||
public class FileScanner : IFileScanner, ITransientDependency
|
public class FileScanner : IFileScanner, ITransientDependency
|
||||||
{
|
{
|
||||||
public ILogger<FileScanner> Logger { get; set; }
|
private readonly IWarpLogger _logger;
|
||||||
|
|
||||||
public FileScanner()
|
public FileScanner(IWarpLogger logger)
|
||||||
{
|
{
|
||||||
Logger = NullLogger<FileScanner>.Instance;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions)
|
public Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions)
|
||||||
@ -58,7 +59,7 @@ namespace ZonyLrcTools.Common.Infrastructure.IO
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.LogWarningWithErrorCode(ErrorCodes.ScanFileError, e);
|
_logger.LogWarningWithErrorCode(ErrorCodes.ScanFileError, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace ZonyLrcTools.Common.Infrastructure.Logging;
|
||||||
|
|
||||||
|
public interface IWarpLogger
|
||||||
|
{
|
||||||
|
Task DebugAsync(string message, Exception? exception = null);
|
||||||
|
Task InfoAsync(string message, Exception? exception = null);
|
||||||
|
Task WarnAsync(string message, Exception? exception = null);
|
||||||
|
Task ErrorAsync(string message, Exception? exception = null);
|
||||||
|
}
|
127
src/ZonyLrcTools.Common/Lyrics/DefaultLyricsDownloader.cs
Normal file
127
src/ZonyLrcTools.Common/Lyrics/DefaultLyricsDownloader.cs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using ZonyLrcTools.Common.Configuration;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Extensions;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Logging;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Threading;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Common.Lyrics;
|
||||||
|
|
||||||
|
public class DefaultLyricsDownloader : ILyricsDownloader, ISingletonDependency
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<ILyricsProvider> _lyricDownloaderList;
|
||||||
|
private readonly GlobalOptions _options;
|
||||||
|
private readonly IWarpLogger _logger;
|
||||||
|
|
||||||
|
public IEnumerable<ILyricsProvider> AvailableProviders => new Lazy<IEnumerable<ILyricsProvider>>(() =>
|
||||||
|
{
|
||||||
|
return _options.Provider.Lyric.Plugin
|
||||||
|
.Where(op => op.Priority != -1)
|
||||||
|
.OrderBy(op => op.Priority)
|
||||||
|
.Join(_lyricDownloaderList,
|
||||||
|
op => op.Name,
|
||||||
|
loader => loader.DownloaderName,
|
||||||
|
(op, loader) => loader);
|
||||||
|
}).Value;
|
||||||
|
|
||||||
|
public DefaultLyricsDownloader(IEnumerable<ILyricsProvider> lyricDownloaderList,
|
||||||
|
IOptions<GlobalOptions> options,
|
||||||
|
IWarpLogger logger)
|
||||||
|
{
|
||||||
|
_lyricDownloaderList = lyricDownloaderList;
|
||||||
|
_logger = logger;
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MusicInfo>> DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
|
||||||
|
int parallelCount = 2,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _logger.InfoAsync("开始下载歌词文件数据...");
|
||||||
|
if (parallelCount <= 0)
|
||||||
|
{
|
||||||
|
parallelCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var warpTask = new WarpTask(parallelCount);
|
||||||
|
var downloadTasks = needDownloadMusicInfos.Select(info =>
|
||||||
|
warpTask.RunAsync(() =>
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// Try to download lyrics from all available providers.
|
||||||
|
foreach (var lyricsProvider in AvailableProviders)
|
||||||
|
{
|
||||||
|
await DownloadAndWriteLyricsAsync(lyricsProvider, info);
|
||||||
|
|
||||||
|
if (info.IsSuccessful)
|
||||||
|
{
|
||||||
|
_logger.LogSuccessful(info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, cancellationToken), cancellationToken));
|
||||||
|
|
||||||
|
await Task.WhenAll(downloadTasks);
|
||||||
|
|
||||||
|
await _logger.InfoAsync($"歌词数据下载完成,成功: {needDownloadMusicInfos.Count(m => m.IsSuccessful)} 失败{needDownloadMusicInfos.Count(m => m.IsSuccessful == false)}。");
|
||||||
|
|
||||||
|
return needDownloadMusicInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DownloadAndWriteLyricsAsync(ILyricsProvider provider, MusicInfo info)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lyrics = await provider.DownloadAsync(info.Name, info.Artist);
|
||||||
|
|
||||||
|
if (lyrics.IsPruneMusic)
|
||||||
|
{
|
||||||
|
info.IsSuccessful = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newLyricsFilePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!,
|
||||||
|
$"{Path.GetFileNameWithoutExtension(info.FilePath)}.lrc");
|
||||||
|
|
||||||
|
if (File.Exists(newLyricsFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(newLyricsFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write lyrics to file.
|
||||||
|
await using (var fileStream = new FileStream(newLyricsFilePath, FileMode.CreateNew, FileAccess.Write))
|
||||||
|
{
|
||||||
|
await using (var binaryWriter = new BinaryWriter(fileStream, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
binaryWriter.Write(Utf8ToSelectedEncoding(lyrics));
|
||||||
|
binaryWriter.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.IsSuccessful = true;
|
||||||
|
}
|
||||||
|
catch (ErrorCodeException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarningInfo(ex);
|
||||||
|
info.IsSuccessful = false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await _logger.ErrorAsync($"下载歌词文件时发生错误:{ex.Message},歌曲名: {info.Name},歌手: {info.Artist}。");
|
||||||
|
info.IsSuccessful = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] Utf8ToSelectedEncoding(LyricsItemCollection lyrics)
|
||||||
|
{
|
||||||
|
var supportEncodings = Encoding.GetEncodings();
|
||||||
|
if (supportEncodings.All(x => x.Name != _options.Provider.Lyric.Config.FileEncoding))
|
||||||
|
{
|
||||||
|
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(_options.Provider.Lyric.Config.FileEncoding), lyrics.GetUtf8Bytes());
|
||||||
|
}
|
||||||
|
}
|
@ -2,5 +2,9 @@
|
|||||||
|
|
||||||
public interface ILyricsDownloader
|
public interface ILyricsDownloader
|
||||||
{
|
{
|
||||||
|
Task<List<MusicInfo>> DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
|
||||||
|
int parallelCount = 2,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
IEnumerable<ILyricsProvider> AvailableProviders { get; }
|
||||||
}
|
}
|
@ -1,23 +1,23 @@
|
|||||||
namespace ZonyLrcTools.Common.Lyrics
|
namespace ZonyLrcTools.Common.Lyrics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构建 <see cref="LyricItemCollection"/> 对象的工厂。
|
/// 构建 <see cref="LyricsItemCollection"/> 对象的工厂。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ILyricsItemCollectionFactory
|
public interface ILyricsItemCollectionFactory
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
|
/// 根据指定的歌曲数据构建新的 <see cref="LyricsItemCollection"/> 实例。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sourceLyric">原始歌词数据。</param>
|
/// <param name="sourceLyric">原始歌词数据。</param>
|
||||||
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
|
/// <returns>构建完成的 <see cref="LyricsItemCollection"/> 对象。</returns>
|
||||||
LyricItemCollection Build(string sourceLyric);
|
LyricsItemCollection Build(string sourceLyric);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
|
/// 根据指定的歌曲数据构建新的 <see cref="LyricsItemCollection"/> 实例。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sourceLyric">原始歌词数据。</param>
|
/// <param name="sourceLyric">原始歌词数据。</param>
|
||||||
/// <param name="translationLyric">翻译歌词数据。</param>
|
/// <param name="translationLyric">翻译歌词数据。</param>
|
||||||
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
|
/// <returns>构建完成的 <see cref="LyricsItemCollection"/> 对象。</returns>
|
||||||
LyricItemCollection Build(string sourceLyric, string translationLyric);
|
LyricsItemCollection Build(string sourceLyric, string translationLyric);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// <param name="artist">歌曲的作者。</param>
|
/// <param name="artist">歌曲的作者。</param>
|
||||||
/// <param name="duration">歌曲的时长。</param>
|
/// <param name="duration">歌曲的时长。</param>
|
||||||
/// <returns>歌曲的歌词数据对象。</returns>
|
/// <returns>歌曲的歌词数据对象。</returns>
|
||||||
ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
|
ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 下载器的名称。
|
/// 下载器的名称。
|
||||||
|
@ -2,6 +2,6 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
{
|
{
|
||||||
public interface ILyricsTextResolver
|
public interface ILyricsTextResolver
|
||||||
{
|
{
|
||||||
LyricItemCollection Resolve(string lyricText);
|
LyricsItemCollection Resolve(string lyricText);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
|
|||||||
namespace ZonyLrcTools.Common.Lyrics
|
namespace ZonyLrcTools.Common.Lyrics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌词的行对象,是 <see cref="LyricItemCollection"/> 的最小单位。。
|
/// 歌词的行对象,是 <see cref="LyricsItemCollection"/> 的最小单位。。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LyricsItem : IComparable<LyricsItem>
|
public class LyricsItem : IComparable<LyricsItem>
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌词数据,包含多条歌词行对象(<see cref="LyricsItem"/>)。
|
/// 歌词数据,包含多条歌词行对象(<see cref="LyricsItem"/>)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LyricItemCollection : List<LyricsItem>
|
public class LyricsItemCollection : List<LyricsItem>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。
|
/// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。
|
||||||
@ -16,12 +16,12 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
|
|
||||||
public GlobalLyricsConfigOptions Options { get; private set; }
|
public GlobalLyricsConfigOptions Options { get; private set; }
|
||||||
|
|
||||||
public LyricItemCollection(GlobalLyricsConfigOptions options)
|
public LyricsItemCollection(GlobalLyricsConfigOptions options)
|
||||||
{
|
{
|
||||||
Options = options;
|
Options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LyricItemCollection operator +(LyricItemCollection left, LyricItemCollection right)
|
public static LyricsItemCollection operator +(LyricsItemCollection left, LyricsItemCollection right)
|
||||||
{
|
{
|
||||||
if (right.IsPruneMusic)
|
if (right.IsPruneMusic)
|
||||||
{
|
{
|
||||||
@ -29,7 +29,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
}
|
}
|
||||||
|
|
||||||
var option = left.Options;
|
var option = left.Options;
|
||||||
var newCollection = new LyricItemCollection(option);
|
var newCollection = new LyricsItemCollection(option);
|
||||||
var indexDiff = left.Count - right.Count;
|
var indexDiff = left.Count - right.Count;
|
||||||
if (!option.IsOneLine)
|
if (!option.IsOneLine)
|
||||||
{
|
{
|
||||||
@ -91,7 +91,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// 这个索引字典用于标识每个索引的歌词是否被处理,为 True 则为已处理,为 False 为未处理。
|
/// 这个索引字典用于标识每个索引的歌词是否被处理,为 True 则为已处理,为 False 为未处理。
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="items">等待构建的歌词集合实例。</param>
|
/// <param name="items">等待构建的歌词集合实例。</param>
|
||||||
private static Dictionary<int, bool> BuildMarkDictionary(LyricItemCollection items)
|
private static Dictionary<int, bool> BuildMarkDictionary(LyricsItemCollection items)
|
||||||
{
|
{
|
||||||
return items
|
return items
|
||||||
.Select((item, index) => new { index, item })
|
.Select((item, index) => new { index, item })
|
@ -17,9 +17,9 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LyricItemCollection Build(string sourceLyric)
|
public LyricsItemCollection Build(string sourceLyric)
|
||||||
{
|
{
|
||||||
var lyric = new LyricItemCollection(_options.Provider.Lyric.Config);
|
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
||||||
if (string.IsNullOrEmpty(sourceLyric))
|
if (string.IsNullOrEmpty(sourceLyric))
|
||||||
{
|
{
|
||||||
return lyric;
|
return lyric;
|
||||||
@ -30,9 +30,9 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
return lyric;
|
return lyric;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LyricItemCollection Build(string sourceLyric, string translationLyric)
|
public LyricsItemCollection Build(string sourceLyric, string translationLyric)
|
||||||
{
|
{
|
||||||
var lyric = new LyricItemCollection(_options.Provider.Lyric.Config);
|
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
||||||
if (string.IsNullOrEmpty(sourceLyric))
|
if (string.IsNullOrEmpty(sourceLyric))
|
||||||
{
|
{
|
||||||
return lyric;
|
return lyric;
|
||||||
@ -42,7 +42,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
|
|
||||||
if (_options.Provider.Lyric.Config.IsEnableTranslation && !string.IsNullOrEmpty(translationLyric))
|
if (_options.Provider.Lyric.Config.IsEnableTranslation && !string.IsNullOrEmpty(translationLyric))
|
||||||
{
|
{
|
||||||
var translatedLyric = InternalBuildLyricObject(new LyricItemCollection(_options.Provider.Lyric.Config), translationLyric);
|
var translatedLyric = InternalBuildLyricObject(new LyricsItemCollection(_options.Provider.Lyric.Config), translationLyric);
|
||||||
if (_options.Provider.Lyric.Config.IsOnlyOutputTranslation)
|
if (_options.Provider.Lyric.Config.IsOnlyOutputTranslation)
|
||||||
{
|
{
|
||||||
return translatedLyric;
|
return translatedLyric;
|
||||||
@ -54,15 +54,15 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
return lyric;
|
return lyric;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LyricItemCollection InternalBuildLyricObject(LyricItemCollection lyric, string sourceText)
|
private LyricsItemCollection InternalBuildLyricObject(LyricsItemCollection lyrics, string sourceText)
|
||||||
{
|
{
|
||||||
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
|
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
|
||||||
foreach (Match match in regex.Matches(sourceText))
|
foreach (Match match in regex.Matches(sourceText))
|
||||||
{
|
{
|
||||||
lyric.Add(new LyricsItem(match.Value));
|
lyrics.Add(new LyricsItem(match.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return lyric;
|
return lyrics;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,7 +17,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// <param name="artist">歌曲作者/艺术家。</param>
|
/// <param name="artist">歌曲作者/艺术家。</param>
|
||||||
/// <param name="duration">歌曲的时长。</param>
|
/// <param name="duration">歌曲的时长。</param>
|
||||||
/// <returns>下载完成的歌曲数据。</returns>
|
/// <returns>下载完成的歌曲数据。</returns>
|
||||||
public virtual async ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist, long? duration = null)
|
public virtual async ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null)
|
||||||
{
|
{
|
||||||
var args = new LyricsProviderArgs(songName, artist, duration ?? 0);
|
var args = new LyricsProviderArgs(songName, artist, duration ?? 0);
|
||||||
await ValidateAsync(args);
|
await ValidateAsync(args);
|
||||||
@ -53,6 +53,6 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// 根据指定的歌词二进制数据,生成歌词数据。
|
/// 根据指定的歌词二进制数据,生成歌词数据。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">歌词的原始二进制数据。</param>
|
/// <param name="data">歌词的原始二进制数据。</param>
|
||||||
protected abstract ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args);
|
protected abstract ValueTask<LyricsItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -47,7 +47,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
|
|||||||
return Encoding.UTF8.GetBytes(lyricResponse);
|
return Encoding.UTF8.GetBytes(lyricResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args)
|
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
var lyricJsonObj = JObject.Parse(Encoding.UTF8.GetString(data));
|
var lyricJsonObj = JObject.Parse(Encoding.UTF8.GetString(data));
|
||||||
|
@ -57,19 +57,19 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
|
|||||||
return Encoding.UTF8.GetBytes(lyricResponse);
|
return Encoding.UTF8.GetBytes(lyricResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args)
|
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
|
|
||||||
var json = JsonConvert.DeserializeObject<GetLyricResponse>(Encoding.UTF8.GetString(data));
|
var json = JsonConvert.DeserializeObject<GetLyricResponse>(Encoding.UTF8.GetString(data));
|
||||||
if (json?.OriginalLyric == null || string.IsNullOrEmpty(json.OriginalLyric.Text))
|
if (json?.OriginalLyric == null || string.IsNullOrEmpty(json.OriginalLyric.Text))
|
||||||
{
|
{
|
||||||
return new LyricItemCollection(null);
|
return new LyricsItemCollection(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.OriginalLyric.Text.Contains("纯音乐,请欣赏"))
|
if (json.OriginalLyric.Text.Contains("纯音乐,请欣赏"))
|
||||||
{
|
{
|
||||||
return new LyricItemCollection(null);
|
return new LyricsItemCollection(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _lyricsItemCollectionFactory.Build(
|
return _lyricsItemCollectionFactory.Build(
|
||||||
|
@ -42,7 +42,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic
|
|||||||
return Encoding.UTF8.GetBytes(lyricJsonString);
|
return Encoding.UTF8.GetBytes(lyricJsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args)
|
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(byte[] data, LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ namespace ZonyLrcTools.Tests.Infrastructure.Lyric
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void LyricCollectionLineBreak_Test()
|
public void LyricCollectionLineBreak_Test()
|
||||||
{
|
{
|
||||||
var lyricObject = new LyricItemCollection(new GlobalLyricsConfigOptions
|
var lyricObject = new LyricsItemCollection(new GlobalLyricsConfigOptions
|
||||||
{
|
{
|
||||||
IsOneLine = false,
|
IsOneLine = false,
|
||||||
LineBreak = LineBreakType.MacOs
|
LineBreak = LineBreakType.MacOs
|
||||||
|
Loading…
x
Reference in New Issue
Block a user