diff --git a/Downloader/LyricsDownloader.cs b/Downloader/LyricsDownloader.cs new file mode 100644 index 0000000..db8e8f9 --- /dev/null +++ b/Downloader/LyricsDownloader.cs @@ -0,0 +1,100 @@ +using Lyrics.Models; +using NeteaseCloudMusicApi; +using Newtonsoft.Json.Linq; +using System.Text.RegularExpressions; + +namespace Lyrics.Downloader; + +public class LyricsDownloader +{ + private readonly CloudMusicApi _cloudMusicApi; + + public LyricsDownloader(CloudMusicApi cloudMusicApi) + { + _cloudMusicApi = cloudMusicApi; + } + + public async Task DownloadLyricAndWriteFileAsync(int songId) + { + if (songId <= 0) + { + throw new ArgumentException("SongId invalid.", nameof(songId)); + } + + // Find local .lrc file. + if (File.Exists($"Lyrics/{songId}.lrc")) + { + Console.WriteLine($"Lyric file {songId}.lrc already exists."); + return; + } + + await Task.Delay(TimeSpan.FromMilliseconds(new Random().Next(500, 1500))); + + // Download lyric by id at Netease Cloud Music. + string? lyricString = await GetLyricAsync(songId); + + if (string.IsNullOrEmpty(lyricString)) + { + throw new Exception("Can't find lyric."); + } + + if (lyricString.Contains("纯音乐,请欣赏") + // 沒有時間資訊 + || !Regex.IsMatch(lyricString, @"\[\d{2}:\d{2}.\d{1,5}\]") + // 小於6行 + || lyricString.Split('\n').Length < 6) + { + throw new Exception("Found an invalid lyric."); + } + + await File.WriteAllTextAsync($"Lyrics/{songId}.lrc", lyricString, System.Text.Encoding.UTF8); + Console.WriteLine($"Write new lyric file {songId}.lrc."); + } + + public async Task<(int songId, string songName)> GetSongIdAsync(ISong song, int offset = 0) + { + if (string.IsNullOrEmpty(song.Title)) + { + throw new ArgumentException("Song Title invalid"); + } + + (bool isOk, JObject json) = await _cloudMusicApi.RequestAsync(CloudMusicApiProviders.Search, + new Dictionary { + { "keywords", song.Title}, + { "type", 1 }, + { "limit", 1 }, + { "offset", offset } + }); + if (!isOk || null == json) + { + Console.Error.WriteLine($"API response ${json?["code"] ?? "error"} while getting song id."); + return default; + } + + json = (JObject)json["result"]; + return null == json + || json["songs"] is not IEnumerable result + ? default + : result.Select(t => ((int)t["id"], (string)t["name"])) + .FirstOrDefault(); + } + + public async Task GetLyricAsync(int songId) + { + (bool isOk, JObject json) = await _cloudMusicApi.RequestAsync(CloudMusicApiProviders.Lyric, + new Dictionary { + { "id", songId } + }); + if (!isOk || null == json) + { + Console.Error.WriteLine($"API response ${json?["code"] ?? "error"} while getting lyric."); + return null; + } + + return (bool?)json["uncollected"] != true + && (bool?)json["nolyric"] != true + ? json["lrc"]["lyric"].ToString().Trim() + : null; + } + +} diff --git a/Lyrics.csproj b/Lyrics.csproj index f902796..c09ee91 100644 --- a/Lyrics.csproj +++ b/Lyrics.csproj @@ -8,12 +8,9 @@ - - - - - - + + + diff --git a/PrepareLyrics.cs b/PrepareLyrics.cs deleted file mode 100644 index bc0e44b..0000000 --- a/PrepareLyrics.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Lyrics.Models; -using System.Text.Json; - -internal partial class Program -{ - static async Task ReadJsonFilesAsync() - { - JsonSerializerOptions option = new() - { - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true - }; - - try - { - await ReadPlaylistsAsync(); - await ReadLyricsAsync(); - } - catch (JsonException) - { - Console.WriteLine("Failed to read the file."); - Environment.Exit(13); // ERROR_INVALID_DATA - } - catch (NotSupportedException) - { - Console.WriteLine("Failed to read the file."); - Environment.Exit(13); // ERROR_INVALID_DATA - } - - async Task ReadPlaylistsAsync() - { - string[] jsoncFiles = Directory.GetFiles("Playlists", "*list.jsonc", SearchOption.AllDirectories); - foreach (var file in jsoncFiles) - { - Console.WriteLine($"Reading {file}..."); - using FileStream fs = File.OpenRead(file); - List temp = await JsonSerializer.DeserializeAsync>(fs, option) ?? new(); - Console.WriteLine($"Loaded {temp.Count} songs."); - Songs.AddRange(temp); - } - - Console.WriteLine($"Total: Loaded {Songs.Count} songs."); - } - - async Task ReadLyricsAsync() - { - string path = "Lyrics.json"; - if (!File.Exists(path)) - { - using StreamWriter fs = File.CreateText(path); - await fs.WriteLineAsync("[]"); - Console.WriteLine($"Create {path} because file is not exists."); - return; - } - - Console.WriteLine($"Reading {path}..."); - - using FileStream fs2 = File.OpenRead(path); - List temp2 = await JsonSerializer.DeserializeAsync>(fs2, option) ?? new(); - Console.WriteLine($"Loaded {temp2.Count} lyrics."); - Lyrics.AddRange(temp2); - } - } - - static void ProcessLyricsFromENV(List lyricFromENV) - { - foreach (var item in lyricFromENV) - { - ILyric? match = Lyrics.Find(p => p.VideoId == item.VideoId - && p.StartTime == item.StartTime); - if (null != match) - { - match.Offset = item.Offset; - //Lyrics.Insert(0, old); - } - } - } - - static void RemoveExcludeSongs(List<(string VideoId, int StartTime)> excludeSongs) - { - var hashSet = excludeSongs.ToHashSet(); - var count = Songs.RemoveAll(p => hashSet.Contains((p.VideoId, p.StartTime))); - excludeSongs.Where(p => p.StartTime == -1) - .ToList() - .ForEach((excludeVideoId) => - { - count += Songs.RemoveAll(p => p.VideoId == excludeVideoId.VideoId); - }); - Console.WriteLine($"Exclude {count} songs from exclude list."); - } - - static void RemoveSongsContainSpecifiedTitle(List excludeTitles) - { - var count = Songs.RemoveAll(p => excludeTitles.Where(p1 => p.Title.Contains(p1, StringComparison.OrdinalIgnoreCase)) - .Any()); - Console.WriteLine($"Exclude {count} songs from specified title."); - } - - static List RemoveLyricsNotContainsInSongs() - { - var songsHashSet = Songs.Select(p => (p.VideoId, p.StartTime)) - .ToHashSet(); - - var removed = new List(); - for (int i = 0; i < Lyrics.Count; i++) - { - ILyric lyric = Lyrics[i]; - if (!songsHashSet.Contains((lyric.VideoId, lyric.StartTime))) - { - removed.Add(Lyrics[i]); - Lyrics.RemoveAt(i); - i--; - } - } - - Console.WriteLine($"Remove {removed.Count} lyrics because of not contains in playlists."); - return removed; - } - - /// - /// Remove duplicate lyrics based on VideoId and StartTime. The first one will be used if duplicates. - /// - /// - public static List RemoveDuplicatesLyrics() - { - var set = new HashSet<(string, int)>(); - var removed = new List(); - for (int i = 0; i < Lyrics.Count; i++) - { - ILyric lyric = Lyrics[i]; - if (!set.Contains((lyric.VideoId, lyric.StartTime))) - { - set.Add((lyric.VideoId, lyric.StartTime)); - } - else - { - removed.Add(Lyrics[i]); - Lyrics.RemoveAt(i); - i--; - } - } - Console.WriteLine($"Remove {removed.Count} lyrics because of duplicates."); - Console.WriteLine($"Finally get {Lyrics.Count} lyrics."); - return removed; - } - -} diff --git a/ProcessNewSongs.cs b/ProcessNewSongs.cs deleted file mode 100644 index bfdc555..0000000 --- a/ProcessNewSongs.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Lyrics.Models; -using NeteaseCloudMusicApi; -using Newtonsoft.Json.Linq; -using System.Text.RegularExpressions; - -internal partial class Program -{ - static async Task ProcessNewSongs(CloudMusicApi api, List diffList, List removed) - { - Random random = new(); - HashSet failedIds = new(); - - for (int i = 0; i < diffList.Count; i++) - { - ISong? song = diffList[i]; - song.Title = Regex.Replace(song.Title, @"[(「【\(\[].*[)」】\]\)]", "") - .Split('/')[0] - .Split('/')[0] - .Trim(); - try - { - // -1: Init - // 0: Disable manually - int songId = -1; - string songName = string.Empty; - - // Find lyric id at local. - ILyric? existLyric = removed.Find(p => p.Title.ToLower() == song.Title.ToLower()) - ?? Lyrics.Find(p => p.Title.ToLower() == song.Title.ToLower()); - - if (null != existLyric) - { - (songId, songName) = (existLyric.LyricId, existLyric.Title); - } - else - // Find lyric id at Netease Cloud Music. - { - (songId, songName) = await GetSongIdAsync(api, song); - - // Can't find song from internet. - if (songId == 0) - { - Console.Error.WriteLine($"Can't find song. {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); - songId = -1; - } - } - - if (failedIds.Contains(songId)) - songId = -songId; - - if (songId > 0) - { - try - { - await DownloadLyricAndWriteFileAsync(api, songId); - } - catch (Exception e) - { - Console.Error.WriteLine($"{e.Message} {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); - Console.Error.WriteLine("Try with second song match."); - - var (songId2, songName2) = await GetSongIdAsync(api, song, 1); - - // Can't find song from internet. - if (songId2 == 0) - { - Console.Error.WriteLine($"Can't find second song match. {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); - songId = -songId; - } - else - { - try - { - await DownloadLyricAndWriteFileAsync(api, songId2); - (songId, songName) = (songId2, songName2); - } - catch (Exception e2) - { - Console.Error.WriteLine($"{e2.Message} {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); - Console.Error.WriteLine("Failed again with second song match."); - songId = -songId; - } - } - } - - if (songId < -1) failedIds.Add(-songId); - } - - Lyrics.Add(new Lyric() - { - VideoId = song.VideoId, - StartTime = song.StartTime, - LyricId = songId, - Title = Regex.Replace(songName ?? "", @"[「【\(\[].*[」】\]\)]", "").Trim(), - Offset = 0 - }); - - Console.WriteLine($"Get lyric {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}, {songId}, {songName}"); - } - catch (Newtonsoft.Json.JsonException e) - { - Console.Error.WriteLine(e); - } - finally - { - await Task.Delay(TimeSpan.FromMilliseconds(random.Next(500, 3000))); - } - } - } - - static async Task DownloadLyricAndWriteFileAsync(CloudMusicApi api, int songId) - { - if (songId <= 0) - { - throw new ArgumentException("SongId invalid.", nameof(songId)); - } - - // Find local .lrc file. - if (File.Exists($"Lyrics/{songId}.lrc")) - { - Console.WriteLine($"Lyric file {songId}.lrc already exists."); - return; - } - - await Task.Delay(TimeSpan.FromMilliseconds(new Random().Next(500, 1500))); - - // Download lyric by id at Netease Cloud Music. - string? lyricString = await GetLyricAsync(api, songId); - - if (string.IsNullOrEmpty(lyricString)) - { - throw new Exception("Can't find lyric."); - } - - if (lyricString.Contains("纯音乐,请欣赏") - // 沒有時間資訊 - || !Regex.IsMatch(lyricString, @"\[\d{2}:\d{2}.\d{1,5}\]") - // 小於6行 - || lyricString.Split('\n').Length < 6) - { - throw new Exception("Found an invalid lyric."); - } - - await File.WriteAllTextAsync($"Lyrics/{songId}.lrc", lyricString, System.Text.Encoding.UTF8); - Console.WriteLine($"Write new lyric file {songId}.lrc."); - } - - static async Task<(int songId, string songName)> GetSongIdAsync(CloudMusicApi api, ISong song, int offset = 0) - { - if (string.IsNullOrEmpty(song.Title)) - { - throw new ArgumentException("Song Title invalid"); - } - - (bool isOk, JObject json) = await api.RequestAsync(CloudMusicApiProviders.Search, - new Dictionary { - { "keywords", song.Title}, - { "type", 1 }, - { "limit", 1 }, - { "offset", offset } - }); - if (!isOk || null == json) - { - Console.Error.WriteLine($"API response ${json?["code"] ?? "error"} while getting song id."); - return default; - } - - json = (JObject)json["result"]; - if (null == json - || json["songs"] is not IEnumerable result - || !result.Any()) - { - return default; - } - - return result.Select(t => ((int)t["id"], (string)t["name"])).FirstOrDefault(); - } - - static async Task GetLyricAsync(CloudMusicApi api, int songId) - { - (bool isOk, JObject json) = await api.RequestAsync(CloudMusicApiProviders.Lyric, - new Dictionary { - { "id", songId } - }); - if (!isOk || null == json) - { - Console.Error.WriteLine($"API response ${json?["code"] ?? "error"} while getting lyric."); - return null; - } - - if ((bool?)json["uncollected"] == true - || (bool?)json["nolyric"] == true) - return null; - - return json["lrc"]["lyric"].ToString().Trim(); - } -} diff --git a/ProcessOldSongs.cs b/ProcessOldSongs.cs deleted file mode 100644 index faa9d50..0000000 --- a/ProcessOldSongs.cs +++ /dev/null @@ -1,54 +0,0 @@ -using NeteaseCloudMusicApi; - -internal partial class Program -{ - static async Task CheckOldSongs(CloudMusicApi api) - { - Console.WriteLine("Start to check old songs..."); - - // Download missing lyric files. - HashSet existsFiles = new DirectoryInfo("Lyrics").GetFiles() - .Select(p => p.Name) - .ToHashSet(); - HashSet failedFiles = new(); - - foreach (var lyric in Lyrics) - { - if (lyric.LyricId <= 0) continue; - var filename = lyric.LyricId + ".lrc"; - - if (existsFiles.Contains(filename) - || failedFiles.Contains(filename)) continue; - - try - { - await DownloadLyricAndWriteFileAsync(api, lyric.LyricId); - existsFiles.Add(filename); - } - catch (Exception e) - { - Console.Error.WriteLine($"Failed to download lyric {lyric.LyricId}: {e.Message}"); - failedFiles.Add(filename); - } - } - - // Delete lyric files which are not in used. - HashSet usedFiles = Lyrics.Where(p => p.LyricId >= 0) - .Select(p => p.LyricId + ".lrc") - .Distinct() - .ToHashSet(); - - foreach (var file in existsFiles) - { - if (!usedFiles.Any(p => p == file)) - { - File.Delete(Path.Combine("Lyrics", file)); - Console.WriteLine($"Delete {file} because it is not in used."); - } - } - - Console.WriteLine("Finish checking old songs."); - Console.WriteLine($"Exist files count: {new DirectoryInfo("Lyrics").GetFiles().Length}"); - Console.WriteLine($"Failed count: {failedFiles.Count}"); - } -} diff --git a/Processor/JsonFileProcessor.cs b/Processor/JsonFileProcessor.cs new file mode 100644 index 0000000..a5eeab0 --- /dev/null +++ b/Processor/JsonFileProcessor.cs @@ -0,0 +1,92 @@ +using Lyrics.Models; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; + +namespace Lyrics.Processor +{ + internal class JsonFileProcessor + { + private readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + public async Task<(List Songs, List Lyrics)> ReadJsonFilesAsync() + { + try + { + return (await ReadPlaylistsAsync(), await ReadLyricsAsync()); + } + catch (Exception e) + { + switch (e) + { + case JsonException: + case NotSupportedException: + Console.Error.WriteLine("Failed to read the file."); + Environment.Exit(13); // ERROR_INVALID_DATA + return default; + default: + throw; + } + } + } + + async Task> ReadPlaylistsAsync() + { + string[] jsoncFiles = Directory.GetFiles("Playlists", "*list.jsonc", SearchOption.AllDirectories); + List songs = new(); + foreach (var file in jsoncFiles) + { + Console.WriteLine($"Reading {file}..."); + using FileStream fs = File.OpenRead(file); + List temp = await JsonSerializer.DeserializeAsync>(fs, _jsonSerializerOptions) + ?? new(); + Console.WriteLine($"Loaded {temp.Count} songs."); + songs.AddRange(temp); + } + + Console.WriteLine($"Total: Loaded {songs.Count} songs."); + return songs; + } + + async Task> ReadLyricsAsync() + { + string path = "Lyrics.json"; + if (!File.Exists(path)) + { + using StreamWriter sw = File.CreateText(path); + await sw.WriteLineAsync("[]"); + Console.WriteLine($"Create {path} because file is not exists."); + return new(); + } + + Console.WriteLine($"Reading {path}..."); + + using FileStream fs = File.OpenRead(path); + List lyrics = await JsonSerializer.DeserializeAsync>(fs, _jsonSerializerOptions) + ?? new(); + Console.WriteLine($"Loaded {lyrics.Count} lyrics."); + + return lyrics; + } + + public static void WriteLyrics() + { + Console.WriteLine("Writing Lyrics.json..."); + File.WriteAllText( + "Lyrics.json", + JsonSerializer.Serialize( + Program.Lyrics.ToArray(), + options: new() + { + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), + WriteIndented = true, + }), + System.Text.Encoding.UTF8); + Console.WriteLine("Gracefully exit."); + } + } +} diff --git a/Processor/LyricsProcessor.cs b/Processor/LyricsProcessor.cs new file mode 100644 index 0000000..4f9b901 --- /dev/null +++ b/Processor/LyricsProcessor.cs @@ -0,0 +1,109 @@ +using Lyrics.Models; + +namespace Lyrics.Processor; + +internal class LyricsProcessor +{ + private readonly List _songs; + private readonly List _lyrics; + + public LyricsProcessor(ref List songs, ref List lyrics) + { + _songs = songs; + _lyrics = lyrics; + } + + public void ProcessLyricsFromENV(List lyricFromENV) + { + foreach (var item in lyricFromENV) + { + ILyric? match = _lyrics.Find(p => p.VideoId == item.VideoId + && p.StartTime == item.StartTime); + if (null != match) + { + match.Offset = item.Offset; + //_lyrics.Insert(0, old); + } + } + } + + public void RemoveExcludeSongs(List<(string VideoId, int StartTime)> excludeSongs) + { + var hashSet = excludeSongs.ToHashSet(); + var count = _songs.RemoveAll(p => hashSet.Contains((p.VideoId, p.StartTime))); + excludeSongs.Where(p => p.StartTime == -1) + .ToList() + .ForEach((excludeVideoId) + => count += _songs.RemoveAll(p => p.VideoId == excludeVideoId.VideoId)); + Console.WriteLine($"Exclude {count} songs from exclude list."); + } + + public void RemoveSongsContainSpecifiedTitle(List excludeTitles) + { + var count = _songs.RemoveAll(p => excludeTitles.Where(p1 => p.Title.Contains(p1, StringComparison.OrdinalIgnoreCase)) + .Any()); + Console.WriteLine($"Exclude {count} songs from specified title."); + } + + public List RemoveLyricsNotContainsInSongs() + { + var songsHashSet = _songs.Select(p => (p.VideoId, p.StartTime)) + .ToHashSet(); + + List removed = new(); + for (int i = 0; i < _lyrics.Count; i++) + { + ILyric lyric = _lyrics[i]; + if (!songsHashSet.Contains((lyric.VideoId, lyric.StartTime))) + { + removed.Add(_lyrics[i]); + _lyrics.RemoveAt(i); + i--; + } + } + + Console.WriteLine($"Remove {removed.Count} lyrics because of not contains in playlists."); + return removed; + } + + /// + /// Remove duplicate lyrics based on VideoId and StartTime. The first one will be used if duplicates. + /// + /// + public List RemoveDuplicatesLyrics() + { + HashSet<(string, int)> set = new(); + List removed = new(); + for (int i = 0; i < _lyrics.Count; i++) + { + ILyric lyric = _lyrics[i]; + if (!set.Contains((lyric.VideoId, lyric.StartTime))) + { + set.Add((lyric.VideoId, lyric.StartTime)); + } + else + { + removed.Add(_lyrics[i]); + _lyrics.RemoveAt(i); + i--; + } + } + Console.WriteLine($"Remove {removed.Count} lyrics because of duplicates."); + Console.WriteLine($"Finally get {_lyrics.Count} lyrics."); + return removed; + } + + /// + /// Filter out new songs that are not included in the lyrics. + /// + /// + public List FilterNewSongs() + { + if (Program.RETRY_FAILED_LYRICS) _lyrics.RemoveAll(p => p.LyricId < 0); + + HashSet lyricsHashSet = new(_lyrics.Select(p => p.VideoId + p.StartTime)); + + return _songs.Where(p => !lyricsHashSet.Contains(p.VideoId + p.StartTime)) + .ToList(); + } +} diff --git a/Processor/NewSongProcessor.cs b/Processor/NewSongProcessor.cs new file mode 100644 index 0000000..b714ab0 --- /dev/null +++ b/Processor/NewSongProcessor.cs @@ -0,0 +1,120 @@ +using Lyrics.Downloader; +using Lyrics.Models; +using System.Text.RegularExpressions; + +namespace Lyrics.Processor; + +internal class NewSongProcessor +{ + private readonly LyricsDownloader _lyricsDownloader; + private readonly List _lyrics; + + public NewSongProcessor(LyricsDownloader lyricsDownloader,ref List lyrics) + { + _lyricsDownloader = lyricsDownloader; + _lyrics = lyrics; + } + + internal async Task ProcessNewSongs(List diffList, List removed) + { + Random random = new(); + HashSet failedIds = new(); + + for (int i = 0; i < diffList.Count; i++) + { + ISong? song = diffList[i]; + song.Title = Regex.Replace(song.Title, @"[(「【\(\[].*[)」】\]\)]", "") + .Split('/')[0] + .Split('/')[0] + .Trim(); + try + { + // -1: Init + // 0: Disable manually + int songId = -1; + string songName = string.Empty; + + // Find lyric id at local. + ILyric? existLyric = removed.Find(p => p.Title.ToLower() == song.Title.ToLower()) + ?? _lyrics.Find(p => p.Title.ToLower() == song.Title.ToLower()); + + if (null != existLyric) + { + (songId, songName) = (existLyric.LyricId, existLyric.Title); + } + else + // Find lyric id at Netease Cloud Music. + { + (songId, songName) = await _lyricsDownloader.GetSongIdAsync(song); + + // Can't find song from internet. + if (songId == 0) + { + Console.Error.WriteLine($"Can't find song. {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); + songId = -1; + } + } + + if (failedIds.Contains(songId)) + songId = -songId; + + if (songId > 0) + { + try + { + await _lyricsDownloader.DownloadLyricAndWriteFileAsync(songId); + } + catch (Exception e) + { + Console.Error.WriteLine($"{e.Message} {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); + Console.Error.WriteLine("Try with second song match."); + + var (songId2, songName2) = await _lyricsDownloader.GetSongIdAsync(song, 1); + + // Can't find song from internet. + if (songId2 == 0) + { + Console.Error.WriteLine($"Can't find second song match. {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); + songId = -songId; + } + else + { + try + { + await _lyricsDownloader.DownloadLyricAndWriteFileAsync(songId2); + (songId, songName) = (songId2, songName2); + } + catch (Exception e2) + { + Console.Error.WriteLine($"{e2.Message} {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}"); + Console.Error.WriteLine("Failed again with second song match."); + songId = -songId; + } + } + } + + if (songId < -1) failedIds.Add(-songId); + } + + _lyrics.Add(new Lyric() + { + VideoId = song.VideoId, + StartTime = song.StartTime, + LyricId = songId, + Title = Regex.Replace(songName ?? "", @"[「【\(\[].*[」】\]\)]", "").Trim(), + Offset = 0 + }); + + Console.WriteLine($"Get lyric {i + 1}/{diffList.Count}: {song.VideoId}, {song.StartTime}, {songId}, {songName}"); + } + catch (Newtonsoft.Json.JsonException e) + { + Console.Error.WriteLine(e); + } + finally + { + await Task.Delay(TimeSpan.FromMilliseconds(random.Next(500, 3000))); + } + } + } +} diff --git a/Processor/OldSongProcessor.cs b/Processor/OldSongProcessor.cs new file mode 100644 index 0000000..ad2df09 --- /dev/null +++ b/Processor/OldSongProcessor.cs @@ -0,0 +1,76 @@ +using Lyrics.Downloader; +using Lyrics.Models; + +namespace Lyrics.Processor; + +internal class OldSongProcessor +{ + private readonly HashSet _existsFiles; + private readonly LyricsDownloader _lyricsDownloader; + private readonly List _lyrics; + + public OldSongProcessor(LyricsDownloader lyricsDownloader, ref List lyrics) + { + _existsFiles = new DirectoryInfo("Lyrics").GetFiles() + .Select(p => p.Name) + .ToHashSet(); + _lyricsDownloader = lyricsDownloader; + _lyrics = lyrics; + } + + public async Task ProcessOldSongs() + { + Console.WriteLine("Start to check old songs..."); + + await DownloadMissingLyrics(); + + RemoveLyricsNotInUsed(); + + Console.WriteLine("Finish checking old songs."); + Console.WriteLine($"Exist files count: {new DirectoryInfo("Lyrics").GetFiles().Length}"); + } + + async Task DownloadMissingLyrics() + { + // Download missing lyric files. + HashSet failedFiles = new(); + + foreach (var lyric in _lyrics) + { + if (lyric.LyricId <= 0) continue; + var filename = lyric.LyricId + ".lrc"; + + if (_existsFiles.Contains(filename) + || failedFiles.Contains(filename)) continue; + + try + { + await _lyricsDownloader.DownloadLyricAndWriteFileAsync(lyric.LyricId); + _existsFiles.Add(filename); + } + catch (Exception e) + { + Console.Error.WriteLine($"Failed to download lyric {lyric.LyricId}: {e.Message}"); + failedFiles.Add(filename); + } + } + Console.WriteLine($"Failed count: {failedFiles.Count}"); + } + + void RemoveLyricsNotInUsed() + { + HashSet usedFiles = _lyrics.Where(p => p.LyricId >= 0) + .Select(p => p.LyricId + ".lrc") + .Distinct() + .ToHashSet(); + + foreach (var file in _existsFiles) + { + if (!usedFiles.Any(p => p == file)) + { + File.Delete(Path.Combine("Lyrics", file)); + Console.WriteLine($"Delete {file} because it is not in used."); + } + } + } +} diff --git a/Program.cs b/Program.cs index 7968a6e..ad7da8e 100644 --- a/Program.cs +++ b/Program.cs @@ -1,15 +1,19 @@ using Lyrics; +using Lyrics.Downloader; using Lyrics.Models; +using Lyrics.Processor; using NeteaseCloudMusicApi; internal partial class Program { - public static readonly List Lyrics = new(); - public static readonly List Songs = new(); + private static List _lyrics = new(); + private static List _songs = new(); - public static bool RETRY_FAILED_LYRICS { get; set; } + public static bool RETRY_FAILED_LYRICS { get; private set; } - private static async Task Main() + public static List Lyrics => _lyrics; + + public static async Task Main() { Startup.Configure(out int MAX_COUNT, out bool _RETRY_FAILED_LYRICS, @@ -20,16 +24,19 @@ private static async Task Main() try { - await ReadJsonFilesAsync(); - ProcessLyricsFromENV(lyricsFromENV); - RemoveExcludeSongs(excludeSongs); - RemoveSongsContainSpecifiedTitle(excludeTitles); - List removed = RemoveLyricsNotContainsInSongs() - .Concat( - RemoveDuplicatesLyrics() - ).ToList(); + (_songs, _lyrics) = await new JsonFileProcessor().ReadJsonFilesAsync(); + + LyricsProcessor lyricsProcessor = new(ref _songs, ref _lyrics); + lyricsProcessor.ProcessLyricsFromENV(lyricsFromENV); + lyricsProcessor.RemoveExcludeSongs(excludeSongs); + lyricsProcessor.RemoveSongsContainSpecifiedTitle(excludeTitles); + List removed = lyricsProcessor.RemoveLyricsNotContainsInSongs() + .Concat( + lyricsProcessor.RemoveDuplicatesLyrics() + ).ToList(); - List diffList = FilterNewSongs(); + List diffList = lyricsProcessor.FilterNewSongs(); + Console.WriteLine($"Get {diffList.Count} new songs."); if (diffList.Count > MAX_COUNT) { @@ -39,13 +46,20 @@ private static async Task Main() } CloudMusicApi api = new(); - await CheckOldSongs(api); - await ProcessNewSongs(api, diffList, removed); + LyricsDownloader lyricsDownloader = new(api); + + OldSongProcessor oldSongProcessor = new(lyricsDownloader, ref _lyrics); + await oldSongProcessor.ProcessOldSongs(); + + NewSongProcessor newSongProcessor = new(lyricsDownloader, ref _lyrics); + await newSongProcessor.ProcessNewSongs(diffList, removed); + + // Lyrics.json is written when the program exits. } catch (Exception e) { - Console.WriteLine("Unhandled exception: " + e.Message); - Console.WriteLine(e.StackTrace); + Console.Error.WriteLine("Unhandled exception: " + e.Message); + Console.Error.WriteLine(e.StackTrace); Environment.Exit(-1); } finally @@ -53,16 +67,4 @@ private static async Task Main() Environment.Exit(0); } } - - /// - /// Filter out new songs that are not included in the lyrics. - /// - /// - static List FilterNewSongs() - { - if (RETRY_FAILED_LYRICS) Lyrics.RemoveAll(p => p.LyricId < 0); - - HashSet lyricsHashSet = new(Lyrics.Select(p => p.VideoId + p.StartTime)); - return Songs.Where(p => !lyricsHashSet.Contains(p.VideoId + p.StartTime)).ToList(); - } } diff --git a/Startup.cs b/Startup.cs index 35e53b7..ec74fdd 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,20 +1,25 @@ using Lyrics.Models; +using Lyrics.Processor; using Microsoft.Extensions.Configuration; -using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Unicode; namespace Lyrics; public static class Startup { + static void ProcessExit(object? sender, EventArgs e) + => JsonFileProcessor.WriteLyrics(); + public static void Configure(out int MAX_COUNT, out bool RETRY_FAILED_LYRICS, out List<(string, int)> excludeSongs, out List excludeTitles, out List lyricsFromENV) { - IOptions option = PrepareOptions(); + AppDomain.CurrentDomain.ProcessExit += ProcessExit; + Console.CancelKeyPress += ProcessExit; + + IOptions option = ReadOptions(); if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_COUNT"), out MAX_COUNT)) { @@ -41,13 +46,17 @@ public static void Configure(out int MAX_COUNT, lyricsFromENV = JsonSerializer.Deserialize>(lyricString) ?? new(); Console.WriteLine($"Get {lyricsFromENV.Count} lyrics from ENV."); } - catch (JsonException) + catch (Exception e) { - Console.WriteLine("Failed to parse lyric json from ENV."); - } - catch (NotSupportedException) - { - Console.WriteLine("Failed to parse lyric json from ENV."); + switch (e) + { + case JsonException: + case NotSupportedException: + Console.Error.WriteLine("Failed to parse lyric json from ENV."); + break; + default: + throw; + } } } @@ -56,30 +65,10 @@ public static void Configure(out int MAX_COUNT, Directory.CreateDirectory("Playlists"); File.Create("Lyrics/0.lrc").Close(); #endif - - AppDomain.CurrentDomain.ProcessExit += ProcessExit; - Console.CancelKeyPress += ProcessExit; - } - - static void ProcessExit(object? sender, EventArgs e) - { - Console.WriteLine("Writing Lyrics.json..."); - File.WriteAllText( - "Lyrics.json", - JsonSerializer.Serialize( - Program.Lyrics.ToArray(), - options: new() - { - Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), - WriteIndented = true, - }), - System.Text.Encoding.UTF8); - Console.WriteLine("Gracefully exit."); } - public static IOptions PrepareOptions() + private static IOptions ReadOptions() { - IOptions option = new Options(); try { IConfiguration configuration = new ConfigurationBuilder() @@ -90,21 +79,21 @@ public static IOptions PrepareOptions() .AddEnvironmentVariables() .Build(); - option = configuration.Get(); + IOptions option = configuration.Get(); if (null == option || null == option.ExcludeVideos) { throw new ApplicationException("Settings file is not valid."); } Console.WriteLine($"Get {option.ExcludeVideos.Length} exclude videos."); + return option; } catch (Exception e) { - Console.WriteLine(e.Message); - Console.WriteLine("ERROR_BAD_CONFIGURATION"); + Console.Error.WriteLine(e.Message); + Console.Error.WriteLine("ERROR_BAD_CONFIGURATION"); Environment.Exit(1610); // ERROR_BAD_CONFIGURATION + return default; } - - return option; } } \ No newline at end of file