using System.Collections.Immutable; 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 LyricsDownloader : ILyricsDownloader, ISingletonDependency { private readonly IEnumerable _lyricsProviders; private readonly IWarpLogger _logger; private readonly GlobalOptions _options; public IEnumerable AvailableProviders => new Lazy>(() => { return _options.Provider.Lyric.Plugin .Where(op => op.Priority != -1) .OrderBy(op => op.Priority) .Join(_lyricsProviders, op => op.Name, loader => loader.DownloaderName, (_, loader) => loader); }).Value; public LyricsDownloader(IEnumerable lyricsProviders, IOptions options, IWarpLogger logger) { _lyricsProviders = lyricsProviders; _logger = logger; _options = options.Value; } public async Task DownloadAsync(List needDownloadMusicInfos, int parallelCount = 1, Func? callback = null, 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) continue; _logger.LogSuccessful(info); break; } if (callback != null) await callback(info); }, cancellationToken), cancellationToken)); await Task.WhenAll(downloadTasks); var successfulCount = needDownloadMusicInfos.Count(m => m is { IsSuccessful: true, IsPruneMusic: false }); var skippedCount = needDownloadMusicInfos.Count(m => m is { IsSuccessful: true, IsPruneMusic: true }); var failedCount = needDownloadMusicInfos.Count(m => m.IsSuccessful == false); await _logger.InfoAsync($"歌词数据下载完成,成功: {successfulCount} 跳过(纯音乐): {skippedCount} 失败{failedCount}。"); await LogFailedSongFilesInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"歌词下载失败列表_{DateTime.Now:yyyyMMddHHmmss}.txt"), 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; info.IsPruneMusic = 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; } } // Convert UTF-8 to selected encoding. private byte[] Utf8ToSelectedEncoding(LyricsItemCollection lyrics) { var supportEncodings = Encoding.GetEncodings(); // If the encoding is UTF-8 BOM, add the BOM to the front of the lyrics. if (_options.Provider.Lyric.Config.FileEncoding == "utf-8-bom") { return Encoding.UTF8.GetPreamble().Concat(lyrics.GetUtf8Bytes()).ToArray(); } 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()); } private async Task LogFailedSongFilesInfo(string outFilePath, IEnumerable musicInfos) { var failedSongList = musicInfos.Where(m => m.IsSuccessful == false).ToImmutableList(); if (!failedSongList.Any()) { return; } var failedSongFiles = new StringBuilder(); failedSongFiles.AppendLine("歌曲名,歌手,路径"); foreach (var failedSongFile in failedSongList) { failedSongFiles.AppendLine($"{failedSongFile.Name},{failedSongFile.Artist},{failedSongFile.FilePath}"); } await File.WriteAllTextAsync(outFilePath, failedSongFiles.ToString()); } }