From 561882f302b9950c7c4526b56e9483057656c359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E7=85=8C?= Date: Sun, 18 Aug 2019 22:37:27 +0800 Subject: [PATCH] 1. Update null check 2. Rename 3. Update cache rule 4. Add "TraditionalToSimplified" function 5. Update UX --- NLyric/Audio/Album.cs | 8 +- NLyric/Audio/Track.cs | 6 +- NLyric/Caches/AlbumCache.cs | 2 +- NLyric/Caches/Extensions.cs | 20 +-- NLyric/Caches/LyricCache.cs | 2 +- NLyric/Caches/TrackCache.cs | 6 +- NLyric/ChineseConverter.cs | 38 +++++ NLyric/Crc32.cs | 2 +- NLyric/DictionaryComparer.cs | 2 +- NLyric/Logger.cs | 12 +- NLyric/Lyrics/Lrc.cs | 36 ++-- NLyric/NLyric.csproj | 12 +- NLyric/{CliWorker.cs => NLyricImpl.cs} | 158 ++++++++++-------- NLyric/Ncm/NcmApi.cs | 24 ++- NLyric/Program.cs | 4 +- ...ncodedCollection.cs => QueryCollection.cs} | 2 +- NLyric/Settings.json | 1 + NLyric/Settings/AllSettings.cs | 8 +- NLyric/Settings/LyricSettings.cs | 2 + NLyric/StringHelper.cs | 14 +- NLyric/System/Cli/ArgumentAttribute.cs | 15 ++ NLyric/System/Cli/CommandLine.cs | 21 ++- .../System/Extensions/HttpClientExtensions.cs | 48 ++---- NLyric/System/Extensions/HttpExtensions.cs | 13 ++ NLyric/The163KeyHelper.cs | 2 +- NLyric/TraditionalToSimplified.map | Bin 0 -> 12112 bytes 26 files changed, 273 insertions(+), 185 deletions(-) create mode 100644 NLyric/ChineseConverter.cs rename NLyric/{CliWorker.cs => NLyricImpl.cs} (86%) rename NLyric/{FormUrlEncodedCollection.cs => QueryCollection.cs} (65%) create mode 100644 NLyric/System/Extensions/HttpExtensions.cs create mode 100644 NLyric/TraditionalToSimplified.map diff --git a/NLyric/Audio/Album.cs b/NLyric/Audio/Album.cs index 9f2bc1c..5fe03aa 100644 --- a/NLyric/Audio/Album.cs +++ b/NLyric/Audio/Album.cs @@ -16,9 +16,9 @@ public class Album : ITrackOrAlbum { public int? Year => _year; public Album(string name, string[] artists, int? trackCount, int? year) { - if (name == null) + if (name is null) throw new ArgumentNullException(nameof(name)); - if (artists == null) + if (artists is null) throw new ArgumentNullException(nameof(artists)); _name = name; @@ -33,7 +33,7 @@ public Album(string name, string[] artists, int? trackCount, int? year) { /// /// 为空时,是否从 获取艺术家 public Album(ATL.Track track, bool getArtistsFromTrack) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); if (!HasAlbumInfo(track)) throw new ArgumentException(nameof(track) + " 中不存在专辑信息"); @@ -52,7 +52,7 @@ public Album(ATL.Track track, bool getArtistsFromTrack) { } public static bool HasAlbumInfo(ATL.Track track) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); return !string.IsNullOrWhiteSpace(track.Album); diff --git a/NLyric/Audio/Track.cs b/NLyric/Audio/Track.cs index cb43bc4..a05dde1 100644 --- a/NLyric/Audio/Track.cs +++ b/NLyric/Audio/Track.cs @@ -10,9 +10,9 @@ public class Track : ITrackOrAlbum { public string[] Artists => _artists; public Track(string name, string[] artists) { - if (name == null) + if (name is null) throw new ArgumentNullException(nameof(name)); - if (artists == null) + if (artists is null) throw new ArgumentNullException(nameof(artists)); _name = name; @@ -20,7 +20,7 @@ public Track(string name, string[] artists) { } public Track(ATL.Track track) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); _name = track.Title.GetSafeString(); diff --git a/NLyric/Caches/AlbumCache.cs b/NLyric/Caches/AlbumCache.cs index 56de5c1..38055f4 100644 --- a/NLyric/Caches/AlbumCache.cs +++ b/NLyric/Caches/AlbumCache.cs @@ -17,7 +17,7 @@ public AlbumCache(Album album, int id) : this(album.Name, id) { } public AlbumCache(string name, int id) { - if (name == null) + if (name is null) throw new ArgumentNullException(nameof(name)); Name = name; diff --git a/NLyric/Caches/Extensions.cs b/NLyric/Caches/Extensions.cs index 44ea764..db78a3a 100644 --- a/NLyric/Caches/Extensions.cs +++ b/NLyric/Caches/Extensions.cs @@ -6,25 +6,25 @@ namespace NLyric.Caches { public static class Extensions { public static AlbumCache Match(this IEnumerable caches, Album album) { - if (album == null) + if (album is null) throw new ArgumentNullException(nameof(album)); return caches.FirstOrDefault(t => IsMatched(t, album)); } public static TrackCache Match(this IEnumerable caches, Track track, Album album) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); - if (album == null) + if (album is null) throw new ArgumentNullException(nameof(album)); return caches.FirstOrDefault(t => IsMatched(t, track, album)); } public static TrackCache Match(this IEnumerable caches, Track track, string fileName) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); - if (fileName == null) + if (fileName is null) throw new ArgumentNullException(nameof(fileName)); return caches.FirstOrDefault(t => IsMatched(t, track, fileName)); @@ -35,25 +35,25 @@ public static LyricCache Match(this IEnumerable caches, int id) { } public static bool IsMatched(this AlbumCache cache, Album album) { - if (album == null) + if (album is null) throw new ArgumentNullException(nameof(album)); return cache.Name == album.Name; } public static bool IsMatched(this TrackCache cache, Track track, Album album) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); - if (album == null) + if (album is null) throw new ArgumentNullException(nameof(album)); return cache.Name == track.Name && cache.AlbumName == album.Name && cache.Artists.SequenceEqual(track.Artists); } public static bool IsMatched(this TrackCache cache, Track track, string fileName) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); - if (fileName == null) + if (fileName is null) throw new ArgumentNullException(nameof(fileName)); return cache.Name == track.Name && cache.FileName == fileName && cache.Artists.SequenceEqual(track.Artists); diff --git a/NLyric/Caches/LyricCache.cs b/NLyric/Caches/LyricCache.cs index 8e427a8..0b5e81b 100644 --- a/NLyric/Caches/LyricCache.cs +++ b/NLyric/Caches/LyricCache.cs @@ -25,7 +25,7 @@ public LyricCache(NcmLyric lyric, string checkSum) : this(lyric.Id, lyric.IsAbso } public LyricCache(int id, bool isAbsoluteMusic, int rawVersion, int translatedVersion, string checkSum) { - if (checkSum == null) + if (checkSum is null) throw new ArgumentNullException(nameof(checkSum)); Id = id; diff --git a/NLyric/Caches/TrackCache.cs b/NLyric/Caches/TrackCache.cs index 3c433c0..ffbd561 100644 --- a/NLyric/Caches/TrackCache.cs +++ b/NLyric/Caches/TrackCache.cs @@ -26,11 +26,11 @@ public TrackCache(Track track, string fileName, int id) : this(track.Name, track } public TrackCache(string name, string[] artists, string albumName, string fileName, int id) { - if (name == null) + if (name is null) throw new ArgumentNullException(nameof(name)); - if (artists == null) + if (artists is null) throw new ArgumentNullException(nameof(artists)); - if (albumName == null && fileName == null) + if (albumName is null && fileName is null) throw new ArgumentException($"{nameof(albumName)} 和 {nameof(fileName)} 不能同时为 null"); Name = name; diff --git a/NLyric/ChineseConverter.cs b/NLyric/ChineseConverter.cs new file mode 100644 index 0000000..43736f5 --- /dev/null +++ b/NLyric/ChineseConverter.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; + +namespace NLyric { + internal static class ChineseConverter { + private static readonly Dictionary _traditionalToSimplifiedMap; + + static ChineseConverter() { + Assembly assembly; + + assembly = Assembly.GetExecutingAssembly(); + using (Stream stream = assembly.GetManifestResourceStream("NLyric.TraditionalToSimplified.map")) + using (BinaryReader reader = new BinaryReader(stream)) { + int count; + + count = (int)stream.Length / 4; + _traditionalToSimplifiedMap = new Dictionary(count); + for (int i = 0; i < count; i++) + _traditionalToSimplifiedMap.Add((char)reader.ReadUInt16(), (char)reader.ReadUInt16()); + } + } + + public static string TraditionalToSimplified(string s) { + if (s is null) + return null; + + StringBuilder sb; + + sb = new StringBuilder(s); + for (int i = 0; i < sb.Length; i++) + if (_traditionalToSimplifiedMap.TryGetValue(sb[i], out char c)) + sb[i] = c; + return sb.ToString(); + } + } +} diff --git a/NLyric/Crc32.cs b/NLyric/Crc32.cs index 9e44344..c829157 100644 --- a/NLyric/Crc32.cs +++ b/NLyric/Crc32.cs @@ -24,7 +24,7 @@ static Crc32() { } public static uint Compute(byte[] data) { - if (data == null) + if (data is null) throw new ArgumentNullException(nameof(data)); uint crc32; diff --git a/NLyric/DictionaryComparer.cs b/NLyric/DictionaryComparer.cs index 154aeb2..f603259 100644 --- a/NLyric/DictionaryComparer.cs +++ b/NLyric/DictionaryComparer.cs @@ -6,7 +6,7 @@ internal sealed class DictionaryComparer : IComparer where T private readonly Dictionary _dictionary; public DictionaryComparer(Dictionary dictionary) { - if (dictionary == null) + if (dictionary is null) throw new ArgumentNullException(nameof(dictionary)); _dictionary = dictionary; diff --git a/NLyric/Logger.cs b/NLyric/Logger.cs index b80f5f4..6dd877a 100644 --- a/NLyric/Logger.cs +++ b/NLyric/Logger.cs @@ -12,13 +12,11 @@ private Logger() { } public void LogNewLine() { - lock (_syncRoot) - Console.WriteLine(); + LogInfo(string.Empty, ConsoleColor.Gray); } public void LogInfo(string value) { - lock (_syncRoot) - Console.WriteLine(value); + LogInfo(value, ConsoleColor.Gray); } public void LogWarning(string value) { @@ -41,14 +39,14 @@ public void LogInfo(string value, ConsoleColor color) { } public void LogException(Exception value) { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); LogError(ExceptionToString(value)); } private static string ExceptionToString(Exception exception) { - if (exception == null) + if (exception is null) throw new ArgumentNullException(nameof(exception)); StringBuilder sb; @@ -65,7 +63,7 @@ private static void DumpException(Exception exception, StringBuilder sb) { sb.AppendLine("StackTrace: " + Environment.NewLine + exception.StackTrace); sb.AppendLine("TargetSite: " + Environment.NewLine + exception.TargetSite.ToString()); sb.AppendLine("----------------------------------------"); - if (exception.InnerException != null) + if (!(exception.InnerException is null)) DumpException(exception.InnerException, sb); } } diff --git a/NLyric/Lyrics/Lrc.cs b/NLyric/Lyrics/Lrc.cs index fa1acc7..70d16bb 100644 --- a/NLyric/Lyrics/Lrc.cs +++ b/NLyric/Lyrics/Lrc.cs @@ -16,12 +16,12 @@ public sealed class Lrc { private string _album; private string _by; private TimeSpan? _offset; - private readonly Dictionary _lyrics = new Dictionary(); + private Dictionary _lyrics = new Dictionary(); public string Title { get => _title; set { - if (value == null) { + if (value is null) { _title = value; return; } @@ -33,7 +33,7 @@ public string Title { public string Artist { get => _artist; set { - if (value == null) { + if (value is null) { _artist = value; return; } @@ -45,7 +45,7 @@ public string Artist { public string Album { get => _album; set { - if (value == null) { + if (value is null) { _album = value; return; } @@ -57,7 +57,7 @@ public string Album { public string By { get => _by; set { - if (value == null) { + if (value is null) { _by = value; return; } @@ -68,10 +68,18 @@ public string By { public TimeSpan? Offset { get => _offset; - set => _offset = (value == null || value.Value.Ticks == 0) ? null : value; + set => _offset = (value is null || value.Value.Ticks == 0) ? null : value; } - public Dictionary Lyrics => _lyrics; + public Dictionary Lyrics { + get => _lyrics; + set { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + _lyrics = value; + } + } public static Lrc Parse(string text) { if (string.IsNullOrEmpty(text)) @@ -83,7 +91,7 @@ public static Lrc Parse(string text) { using (StringReader reader = new StringReader(text)) { string line; - while ((line = reader.ReadLine()) != null) + while (!((line = reader.ReadLine()) is null)) if (!TryParseLine(line.Trim(), lrc)) throw new FormatException(); } @@ -100,7 +108,7 @@ public static Lrc UnsafeParse(string text) { using (StringReader reader = new StringReader(text)) { string line; - while ((line = reader.ReadLine()) != null) + while (!((line = reader.ReadLine()) is null)) TryParseLine(line.Trim(), lrc); } return lrc; @@ -164,15 +172,15 @@ public override string ToString() { StringBuilder sb; sb = new StringBuilder(); - if (_title != null) + if (!(_title is null)) AppendLine(sb, TI, _title); - if (_artist != null) + if (!(_artist is null)) AppendLine(sb, AR, _artist); - if (_album != null) + if (!(_album is null)) AppendLine(sb, AL, _album); - if (_by != null) + if (!(_by is null)) AppendLine(sb, BY, _by); - if (_offset != null) + if (!(_offset is null)) AppendLine(sb, OFFSET, ((long)_offset.Value.TotalMilliseconds).ToString()); foreach (KeyValuePair lyric in _lyrics) sb.AppendLine($"[{TimeSpanToLyricString(lyric.Key)}]{lyric.Value}"); diff --git a/NLyric/NLyric.csproj b/NLyric/NLyric.csproj index a2e090c..77bd82b 100644 --- a/NLyric/NLyric.csproj +++ b/NLyric/NLyric.csproj @@ -1,16 +1,22 @@ - + NLyric NLyric NLyric Copyright © 2019 Wwh - 2.0.2.0 - 2.0.2.0 + 2.1.0.0 + 2.1.0.0 Exe netcoreapp2.1;net472 NLyric 7.3 + + + + + + diff --git a/NLyric/CliWorker.cs b/NLyric/NLyricImpl.cs similarity index 86% rename from NLyric/CliWorker.cs rename to NLyric/NLyricImpl.cs index b1cc286..8e02080 100644 --- a/NLyric/CliWorker.cs +++ b/NLyric/NLyricImpl.cs @@ -12,7 +12,7 @@ using NLyric.Settings; namespace NLyric { - internal static partial class CliWorker { + internal static class NLyricImpl { private static readonly SearchSettings _searchSettings = AllSettings.Default.Search; private static readonly FuzzySettings _fuzzySettings = AllSettings.Default.Fuzzy; private static readonly MatchSettings _matchSettings = AllSettings.Default.Match; @@ -23,12 +23,14 @@ internal static partial class CliWorker { // AlbumId -> Tracks private static readonly Dictionary _cachedNcmLyrics = new Dictionary(); // TrackId -> Lyric + private static string _allCachesPath; private static AllCaches _allCaches; public static async Task ExecuteAsync(Arguments arguments) { Logger.Instance.LogInfo("程序会自动过滤相似度为0的结果与歌词未被收集的结果!!!", ConsoleColor.Green); Logger.Instance.LogNewLine(); - LoadLocalCaches(arguments.Directory); + _allCachesPath = Path.Combine(arguments.Directory, ".nlyric"); + LoadLocalCaches(); foreach (string audioPath in Directory.EnumerateFiles(arguments.Directory, "*", SearchOption.AllDirectories)) { string lrcPath; int? trackId; @@ -39,14 +41,14 @@ public static async Task ExecuteAsync(Arguments arguments) { Logger.Instance.LogInfo($"开始搜索文件\"{Path.GetFileName(audioPath)}\"的歌词。"); trackId = await TryGetMusicId(audioPath); // 同时尝试通过163Key和专辑获取歌曲信息 - if (trackId == null) + if (trackId is null) Logger.Instance.LogWarning($"无法找到文件\"{Path.GetFileName(audioPath)}\"的网易云音乐ID!"); else await WriteLrcAsync(trackId.Value, lrcPath); Logger.Instance.LogNewLine(); Logger.Instance.LogNewLine(); } - SaveLocalCaches(arguments.Directory); + SaveLocalCaches(); } private static bool CanSkip(string audioPath, string lrcPath) { @@ -80,7 +82,7 @@ private static bool IsAudioFile(string extension) { try { // 歌曲无163Key,通过自己的算法匹配 ncmTrack = await MapToAsync(track, album, audioPath); - if (ncmTrack != null) { + if (!(ncmTrack is null)) { trackId = ncmTrack.Id; Logger.Instance.LogInfo($"已获取文件\"{Path.GetFileName(audioPath)}\"的网易云音乐ID: {trackId}。"); } @@ -110,16 +112,18 @@ private static async Task WriteLrcAsync(int trackId, string lrcPath) { } if (hasLrcFile) { // 如果歌词存在,判断是否需要覆盖或更新 - if (lyricCache != null && lyricCache.CheckSum == lyricCheckSum) { + if (!(lyricCache is null) && lyricCache.CheckSum == lyricCheckSum) { // 歌词由NLyric创建 if (ncmLyric.RawVersion <= lyricCache.RawVersion && ncmLyric.TranslatedVersion <= lyricCache.TranslatedVersion) { // 是最新版本 - Logger.Instance.LogInfo("本地歌词已是最新版本,正在跳过。", ConsoleColor.Green); + Logger.Instance.LogInfo("本地歌词已是最新版本,正在跳过。", ConsoleColor.Yellow); return; } else { // 不是最新版本 - if (!_lyricSettings.AutoUpdate) { + if (_lyricSettings.AutoUpdate) + Logger.Instance.LogInfo("本地歌词不是最新版本,正在更新。", ConsoleColor.Green); + else { Logger.Instance.LogInfo("本地歌词不是最新版本但是自动更新被禁止,正在跳过。", ConsoleColor.Yellow); return; } @@ -128,13 +132,13 @@ private static async Task WriteLrcAsync(int trackId, string lrcPath) { else { // 歌词非NLyric创建 if (!_lyricSettings.Overwriting) { - Logger.Instance.LogInfo("本地歌词已存在并且非NLyric创建,正在跳过。", ConsoleColor.Cyan); + Logger.Instance.LogInfo("本地歌词已存在并且非NLyric创建,正在跳过。", ConsoleColor.Yellow); return; } } } lrc = ToLrc(ncmLyric); - if (lrc != null) { + if (!(lrc is null)) { string lyric; lyric = lrc.ToString(); @@ -153,9 +157,9 @@ private static async Task WriteLrcAsync(int trackId, string lrcPath) { /// /// private static async Task MapToAsync(Track track, Album album, string audioPath) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); - if (audioPath == null) + if (audioPath is null) throw new ArgumentNullException(nameof(audioPath)); string fileName; @@ -164,9 +168,9 @@ private static async Task MapToAsync(Track track, Album album, string NcmTrack ncmTrack; fileName = Path.GetFileName(audioPath); - trackCache = album == null ? _allCaches.TrackCaches.Match(track, fileName) : _allCaches.TrackCaches.Match(track, album); + trackCache = album is null ? _allCaches.TrackCaches.Match(track, fileName) : _allCaches.TrackCaches.Match(track, album); // 有专辑信息就用专辑信息,没有专辑信息就用文件名 - if (trackCache != null) + if (!(trackCache is null)) return new NcmTrack(track, trackCache.Id); // 先尝试从缓存获取歌曲 if (The163KeyHelper.TryGetMusicId(audioPath, out trackId)) { @@ -177,21 +181,21 @@ private static async Task MapToAsync(Track track, Album album, string NcmAlbum ncmAlbum; ncmAlbum = null; - if (album != null) { + if (!(album is null)) { // 存在专辑信息,尝试获取网易云音乐上对应的专辑 AlbumCache albumCache; albumCache = _allCaches.AlbumCaches.Match(album); - if (albumCache != null) + if (!(albumCache is null)) ncmAlbum = new NcmAlbum(album, albumCache.Id); // 先尝试从缓存获取专辑 - if (ncmAlbum == null) { + if (ncmAlbum is null) { ncmAlbum = await MapToAsync(album); - if (ncmAlbum != null) + if (!(ncmAlbum is null)) UpdateCache(album, ncmAlbum.Id); } } - if (ncmAlbum == null) { + if (ncmAlbum is null) { // 没有对应的专辑信息,使用无专辑匹配 ncmTrack = await MapToAsync(track); } @@ -202,16 +206,16 @@ private static async Task MapToAsync(Track track, Album album, string ncmTracks = (await GetTracksAsync(ncmAlbum)).Where(t => ComputeSimilarity(t.Name, track.Name, false) != 0).ToArray(); // 获取网易云音乐上专辑收录的歌曲 ncmTrack = MatchByUser(ncmTracks, track); - if (ncmTrack == null) + if (ncmTrack is null) // 网易云音乐上的专辑可能没收录这个歌曲,不清楚为什么,但是确实存在这个情况,比如专辑id:3094396 ncmTrack = await MapToAsync(track); } } - if (ncmTrack == null) + if (ncmTrack is null) Logger.Instance.LogWarning("歌曲匹配失败!"); else { Logger.Instance.LogInfo("歌曲匹配成功!"); - if (album == null) + if (album is null) UpdateCache(track, fileName, ncmTrack.Id); else UpdateCache(track, album, ncmTrack.Id); @@ -241,7 +245,7 @@ private static async Task GetTracksAsync(NcmAlbum ncmAlbum) { /// /// private static async Task MapToAsync(Track track) { - if (track == null) + if (track is null) throw new ArgumentNullException(nameof(track)); NcmTrack ncmTrack; @@ -249,7 +253,7 @@ private static async Task MapToAsync(Track track) { Logger.Instance.LogInfo($"开始搜索歌曲\"{track}\"。"); Logger.Instance.LogWarning("正在尝试带艺术家搜索,结果可能将过少!"); ncmTrack = await MapToAsync(track, true); - if (ncmTrack == null && _fuzzySettings.TryIgnoringArtists) { + if (ncmTrack is null && _fuzzySettings.TryIgnoringArtists) { Logger.Instance.LogWarning("正在尝试忽略艺术家搜索,结果可能将不精确!"); ncmTrack = await MapToAsync(track, false); } @@ -262,7 +266,7 @@ private static async Task MapToAsync(Track track) { /// /// private static async Task MapToAsync(Album album) { - if (album == null) + if (album is null) throw new ArgumentNullException(nameof(album)); string replacedAlbumName; @@ -274,11 +278,11 @@ private static async Task MapToAsync(Album album) { Logger.Instance.LogInfo($"开始搜索专辑\"{album}\"。"); Logger.Instance.LogWarning("正在尝试带艺术家搜索,结果可能将过少!"); ncmAlbum = await MapToAsync(album, true); - if (ncmAlbum == null && _fuzzySettings.TryIgnoringArtists) { + if (ncmAlbum is null && _fuzzySettings.TryIgnoringArtists) { Logger.Instance.LogWarning("正在尝试忽略艺术家搜索,结果可能将不精确!"); ncmAlbum = await MapToAsync(album, false); } - if (ncmAlbum == null) { + if (ncmAlbum is null) { Logger.Instance.LogWarning("专辑匹配失败!"); _cachedNcmAlbums[replacedAlbumName] = null; return null; @@ -321,13 +325,10 @@ private static async Task MapToAsync(Album album, bool withArtists) { #endregion #region local cache - private static void LoadLocalCaches(string directoryPath) { - string cachePath; - - cachePath = Path.Combine(directoryPath, ".nlyric"); - if (File.Exists(cachePath)) { - _allCaches = JsonConvert.DeserializeObject(File.ReadAllText(cachePath)); - Logger.Instance.LogInfo($"搜索缓存\"{cachePath}\"已被加载。"); + private static void LoadLocalCaches() { + if (File.Exists(_allCachesPath)) { + _allCaches = JsonConvert.DeserializeObject(File.ReadAllText(_allCachesPath)); + Logger.Instance.LogInfo($"搜索缓存\"{_allCachesPath}\"加载成功。"); } else _allCaches = new AllCaches() { @@ -337,15 +338,16 @@ private static void LoadLocalCaches(string directoryPath) { }; } - private static void SaveLocalCaches(string directoryPath) { - string cachePath; - - cachePath = Path.Combine(directoryPath, ".nlyric"); + private static void SaveLocalCaches() { _allCaches.AlbumCaches.Sort((x, y) => x.Name.CompareTo(y.Name)); _allCaches.TrackCaches.Sort((x, y) => x.Name.CompareTo(y.Name)); _allCaches.LyricCaches.Sort((x, y) => x.Id.CompareTo(y.Id)); + SaveLocalCachesCore(_allCachesPath); + Logger.Instance.LogInfo($"搜索缓存\"{_allCachesPath}\"已被保存。"); + } + + private static void SaveLocalCachesCore(string cachePath) { File.WriteAllText(cachePath, FormatJson(JsonConvert.SerializeObject(_allCaches))); - Logger.Instance.LogInfo($"搜索缓存\"{cachePath}\"已被保存。"); } private static string FormatJson(string json) { @@ -362,24 +364,30 @@ private static void UpdateCache(Album album, int id) { AlbumCache cache; cache = _allCaches.AlbumCaches.Match(album); - if (cache == null) - _allCaches.AlbumCaches.Add(new AlbumCache(album, id)); + if (!(cache is null)) + return; + _allCaches.AlbumCaches.Add(new AlbumCache(album, id)); + OnCacheUpdated(); } private static void UpdateCache(Track track, Album album, int id) { TrackCache cache; cache = _allCaches.TrackCaches.Match(track, album); - if (cache == null) - _allCaches.TrackCaches.Add(new TrackCache(track, album, id)); + if (!(cache is null)) + return; + _allCaches.TrackCaches.Add(new TrackCache(track, album, id)); + OnCacheUpdated(); } private static void UpdateCache(Track track, string fileName, int id) { TrackCache cache; cache = _allCaches.TrackCaches.Match(track, fileName); - if (cache == null) - _allCaches.TrackCaches.Add(new TrackCache(track, fileName, id)); + if (!(cache is null)) + return; + _allCaches.TrackCaches.Add(new TrackCache(track, fileName, id)); + OnCacheUpdated(); } private static void UpdateCache(NcmLyric lyric, string checkSum) { @@ -389,6 +397,11 @@ private static void UpdateCache(NcmLyric lyric, string checkSum) { if (index != -1) _allCaches.LyricCaches.RemoveAt(index); _allCaches.LyricCaches.Add(new LyricCache(lyric, checkSum)); + OnCacheUpdated(); + } + + private static void OnCacheUpdated() { + SaveLocalCachesCore(_allCachesPath); } #endregion @@ -399,7 +412,7 @@ private static TSource MatchByUser(TSource[] sources, TTarget if (sources.Length == 0) return null; result = MatchByUser(sources, target, false); - if (result == null && _fuzzySettings.TryIgnoringExtraInfo) + if (result is null && _fuzzySettings.TryIgnoringExtraInfo) result = MatchByUser(sources, target, true); return result; } @@ -415,8 +428,8 @@ private static TSource MatchByUser(TSource[] sources, TTarget foreach (TSource source in sources) nameSimilarities[source] = ComputeSimilarity(source.Name, target.Name, fuzzy); result = Match(sources, target, nameSimilarities, out isExact); - if (result != null && (isExact || Confirm("不完全相似,是否使用自动匹配结果?"))) - // 自动匹配成功,如果是完全匹配,不需要用户再次确认,反正由用户再次确认 + if (isExact) + // 自动匹配成功,如果是完全匹配,不需要用户再次确认 return result; return fuzzy ? Select(sources.Where(t => nameSimilarities[t] > _matchSettings.MinimumSimilarityUser).OrderByDescending(t => t, new DictionaryComparer(nameSimilarities)).ToArray(), target, nameSimilarities) : null; // fuzzy为true时是第二次搜索了,再让用户再次手动从搜索结果中选择,自动匹配失败的原因可能是 Settings.Match.MinimumSimilarity 设置太大了 @@ -482,7 +495,7 @@ private static TSource Select(TSource[] sources, TTarget targe } Logger.Instance.LogWarning("输入有误,请重新输入!"); } while (true); - if (result != null) + if (!(result is null)) Logger.Instance.LogInfo("已选择:" + result.ToString()); return result; @@ -493,23 +506,6 @@ string TrackOrAlbumToString(ITrackOrAlbum trackOrAlbum) { } } - private static bool Confirm(string text) { - Logger.Instance.LogInfo(text); - Logger.Instance.LogInfo("请手动输入Yes或No。"); - do { - string userInput; - - userInput = Console.ReadLine().Trim().ToUpperInvariant(); - switch (userInput) { - case "YES": - return true; - case "NO": - return false; - } - Logger.Instance.LogWarning("输入有误,请重新输入!"); - } while (true); - } - private static double ComputeSimilarity(string x, string y, bool fuzzy) { x = x.ReplaceEx(); y = y.ReplaceEx(); @@ -543,20 +539,24 @@ private static Lrc ToLrc(NcmLyric lyric) { Logger.Instance.LogWarning("当前歌曲是纯音乐无歌词!"); return null; } + if (!(lyric.Raw is null)) + NormalizeLyric(lyric.Raw, false); + if (!(lyric.Translated is null)) + NormalizeLyric(lyric.Translated, _lyricSettings.SimplifyTranslated); foreach (string mode in _lyricSettings.Modes) { switch (mode.ToUpperInvariant()) { case "MERGED": - if (lyric.Raw == null || lyric.Translated == null) + if (lyric.Raw is null || lyric.Translated is null) continue; Logger.Instance.LogInfo("已获取混合歌词。"); return MergeLyric(lyric.Raw, lyric.Translated); case "RAW": - if (lyric.Raw == null) + if (lyric.Raw is null) continue; Logger.Instance.LogInfo("已获取原始歌词。"); return lyric.Raw; case "TRANSLATED": - if (lyric.Translated == null) + if (lyric.Translated is null) continue; Logger.Instance.LogInfo("已获取翻译歌词。"); return lyric.Translated; @@ -568,12 +568,22 @@ private static Lrc ToLrc(NcmLyric lyric) { return null; } - private static Lrc MergeLyric(Lrc rawLrc, Lrc translatedLrc) { - if (rawLrc == null) - throw new ArgumentNullException(nameof(rawLrc)); - if (translatedLrc == null) - throw new ArgumentNullException(nameof(translatedLrc)); + private static void NormalizeLyric(Lrc lrc, bool simplify) { + Dictionary newLyrics; + newLyrics = new Dictionary(lrc.Lyrics.Count); + foreach (KeyValuePair lyric in lrc.Lyrics) { + string value; + + value = lyric.Value.Trim('/', ' '); + if (simplify) + value = ChineseConverter.TraditionalToSimplified(value); + newLyrics.Add(lyric.Key, value); + } + lrc.Lyrics = newLyrics; + } + + private static Lrc MergeLyric(Lrc rawLrc, Lrc translatedLrc) { Lrc mergedLrc; mergedLrc = new Lrc { diff --git a/NLyric/Ncm/NcmApi.cs b/NLyric/Ncm/NcmApi.cs index 1cfff0b..a38c092 100644 --- a/NLyric/Ncm/NcmApi.cs +++ b/NLyric/Ncm/NcmApi.cs @@ -24,17 +24,19 @@ public enum SearchType { } public static async Task SearchAsync(IEnumerable keywords, SearchType type, int limit) { - FormUrlEncodedCollection parameters; + QueryCollection queries; - parameters = new FormUrlEncodedCollection { + queries = new QueryCollection { { "s", string.Join(" ", keywords) }, { "type", ((int)type).ToString() }, { "limit", limit.ToString() } }; using (HttpClient client = new HttpClient()) - using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Get, SEARCH_URL, parameters, null)) { + using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Get, SEARCH_URL, queries, null)) { JObject json; + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(); json = JObject.Parse(await response.Content.ReadAsStringAsync()); if ((int)json["code"] != 200) throw new HttpRequestException(); @@ -43,15 +45,17 @@ public static async Task SearchAsync(IEnumerable keywords, Searc } public static async Task GetAlbumAsync(int id) { - FormUrlEncodedCollection parameters; + QueryCollection queries; - parameters = new FormUrlEncodedCollection { + queries = new QueryCollection { { "id", id.ToString() } }; using (HttpClient client = new HttpClient()) - using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Get, ALBUM_URL, parameters, null)) { + using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Get, ALBUM_URL, queries, null)) { JObject json; + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(); json = JObject.Parse(await response.Content.ReadAsStringAsync()); if ((int)json["code"] != 200) throw new HttpRequestException(); @@ -60,17 +64,19 @@ public static async Task GetAlbumAsync(int id) { } public static async Task GetLyricAsync(int id) { - FormUrlEncodedCollection parameters; + QueryCollection queries; - parameters = new FormUrlEncodedCollection { + queries = new QueryCollection { { "id", id.ToString() }, { "lv", "-1" }, { "tv", "-1" } }; using (HttpClient client = new HttpClient()) - using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Get, LYRIC_URL, parameters, null)) { + using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Get, LYRIC_URL, queries, null)) { JObject json; + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(); json = JObject.Parse(await response.Content.ReadAsStringAsync()); if ((int)json["code"] != 200) throw new HttpRequestException(); diff --git a/NLyric/Program.cs b/NLyric/Program.cs index 03907d3..fb53eb8 100644 --- a/NLyric/Program.cs +++ b/NLyric/Program.cs @@ -8,7 +8,7 @@ namespace NLyric { public static class Program { private static void Main(string[] args) { - if (args == null || args.Length == 0) { + if (args is null || args.Length == 0) { CommandLine.ShowUsage(); return; } @@ -25,7 +25,7 @@ private static void Main(string[] args) { return; } AllSettings.Default = JsonConvert.DeserializeObject(File.ReadAllText("Settings.json")); - CliWorker.ExecuteAsync(arguments).GetAwaiter().GetResult(); + NLyricImpl.ExecuteAsync(arguments).GetAwaiter().GetResult(); Logger.Instance.LogInfo("完成", ConsoleColor.Green); #if DEBUG Console.ReadKey(true); diff --git a/NLyric/FormUrlEncodedCollection.cs b/NLyric/QueryCollection.cs similarity index 65% rename from NLyric/FormUrlEncodedCollection.cs rename to NLyric/QueryCollection.cs index e0e78cc..749aee2 100644 --- a/NLyric/FormUrlEncodedCollection.cs +++ b/NLyric/QueryCollection.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; namespace NLyric { - internal sealed class FormUrlEncodedCollection : List> { + internal sealed class QueryCollection : List> { public void Add(string key, string value) { Add(new KeyValuePair(key, value)); } diff --git a/NLyric/Settings.json b/NLyric/Settings.json index 1b465ee..4ab2274 100644 --- a/NLyric/Settings.json +++ b/NLyric/Settings.json @@ -68,6 +68,7 @@ "Raw", "Translated" ], // 歌词模式,依次尝试每一个模式直到成功,Merged表示混合未翻译和翻译后歌词,Raw表示未翻译的歌词,Translated表示翻译后的歌词 + "SimplifyTranslated": true, // 部分翻译后的歌词是繁体的,这个选项可以简体化翻译后的歌词 "AutoUpdate": true, // 是否自动更新由NLyric创建的歌词 "Overwriting": false // 是否覆盖非NLyric创建的歌词 } diff --git a/NLyric/Settings/AllSettings.cs b/NLyric/Settings/AllSettings.cs index 61879fe..c061aa3 100644 --- a/NLyric/Settings/AllSettings.cs +++ b/NLyric/Settings/AllSettings.cs @@ -6,15 +6,17 @@ internal sealed class AllSettings { public static AllSettings Default { get { - if (_default == null) + if (_default is null) throw new InvalidOperationException(); + return _default; } set { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); - if (_default != null) + if (!(_default is null)) throw new InvalidOperationException(); + _default = value; } } diff --git a/NLyric/Settings/LyricSettings.cs b/NLyric/Settings/LyricSettings.cs index a763a1e..64ff713 100644 --- a/NLyric/Settings/LyricSettings.cs +++ b/NLyric/Settings/LyricSettings.cs @@ -2,6 +2,8 @@ namespace NLyric.Settings { internal sealed class LyricSettings { public string[] Modes { get; set; } + public bool SimplifyTranslated { get; set; } + public bool AutoUpdate { get; set; } public bool Overwriting { get; set; } diff --git a/NLyric/StringHelper.cs b/NLyric/StringHelper.cs index 59df501..4bb0301 100644 --- a/NLyric/StringHelper.cs +++ b/NLyric/StringHelper.cs @@ -16,7 +16,7 @@ internal static class StringHelper { /// /// public static string GetSafeString(this string value) { - return value == null ? string.Empty : value.Trim(); + return value is null ? string.Empty : value.Trim(); } /// @@ -25,7 +25,7 @@ public static string GetSafeString(this string value) { /// /// public static string ReplaceEx(this string value) { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); return value.ToHalfWidth().WholeWordReplace().CharReplace(); @@ -37,7 +37,7 @@ public static string ReplaceEx(this string value) { /// /// public static string WholeWordReplace(this string value) { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); if (value.Length == 0) @@ -54,7 +54,7 @@ public static string WholeWordReplace(this string value) { /// /// public static string CharReplace(this string value) { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); StringBuilder buffer; @@ -75,7 +75,7 @@ public static string CharReplace(this string value) { /// /// public static string Fuzzy(this string value) { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); int fuzzyStartIndex; @@ -97,7 +97,7 @@ public static string Fuzzy(this string value) { /// /// public static string[] SplitEx(this string value) { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); return value.Split(_searchSettings.Separators, StringSplitOptions.RemoveEmptyEntries); @@ -109,7 +109,7 @@ public static string[] SplitEx(this string value) { /// /// public static string ToHalfWidth(this string value) { - if (value == null) + if (value is null) throw new ArgumentNullException(nameof(value)); char[] chars; diff --git a/NLyric/System/Cli/ArgumentAttribute.cs b/NLyric/System/Cli/ArgumentAttribute.cs index 3c1dc46..4cf61c4 100644 --- a/NLyric/System/Cli/ArgumentAttribute.cs +++ b/NLyric/System/Cli/ArgumentAttribute.cs @@ -10,23 +10,38 @@ internal sealed class ArgumentAttribute : Attribute { private string _type; private string _description; + /// + /// 参数名 + /// public string Name => _name; + /// + /// 是否为必选参数 + /// public bool IsRequired { get => _isRequired; set => _isRequired = value; } + /// + /// 默认值,当 时, 必须为 。 + /// public object DefaultValue { get => _defaultValue; set => _defaultValue = value; } + /// + /// 参数类型,用于 显示类型来简单描述参数。若应用到返回类型为 的属性上, 必须为 。 + /// public string Type { get => _type; set => _type = value; } + /// + /// 参数介绍,用于 具体描述参数。 + /// public string Description { get => _description; set => _description = value; diff --git a/NLyric/System/Cli/CommandLine.cs b/NLyric/System/Cli/CommandLine.cs index f483a59..1fa70d1 100644 --- a/NLyric/System/Cli/CommandLine.cs +++ b/NLyric/System/Cli/CommandLine.cs @@ -6,7 +6,7 @@ namespace System.Cli { internal static class CommandLine { public static T Parse(string[] args) where T : new() { - if (args == null) + if (args is null) throw new ArgumentNullException(nameof(args)); T result; @@ -17,7 +17,7 @@ internal static class CommandLine { } public static bool TryParse(string[] args, out T result) where T : new() { - if (args == null) { + if (args is null) { result = default; return false; } @@ -102,7 +102,7 @@ public static bool ShowUsage() { if (!VerifyProperty(propertyInfo, out attribute)) return false; - if (attribute == null) + if (attribute is null) continue; argumentInfos.Add(new ArgumentInfo(attribute, propertyInfo)); } @@ -135,7 +135,7 @@ private static bool TryGetArgumentInfos(Type type, out Dictionary SendAsync(this HttpClient client, HttpMe return client.SendAsync(method, url, null, null); } - public static Task SendAsync(this HttpClient client, HttpMethod method, string url, IEnumerable> parameters, IEnumerable> headers) { - return client.SendAsync(method, url, parameters, headers, (byte[])null, "application/x-www-form-urlencoded"); + public static Task SendAsync(this HttpClient client, HttpMethod method, string url, IEnumerable> queries, IEnumerable> headers) { + return client.SendAsync(method, url, queries, headers, (byte[])null, "application/x-www-form-urlencoded"); } - public static Task SendAsync(this HttpClient client, HttpMethod method, string url, IEnumerable> parameters, IEnumerable> headers, string content, string contentType) { - return client.SendAsync(method, url, parameters, headers, content == null ? null : Encoding.UTF8.GetBytes(content), contentType); + public static Task SendAsync(this HttpClient client, HttpMethod method, string url, IEnumerable> queries, IEnumerable> headers, string content, string contentType) { + return client.SendAsync(method, url, queries, headers, content is null ? null : Encoding.UTF8.GetBytes(content), contentType); } - public static Task SendAsync(this HttpClient client, HttpMethod method, string url, IEnumerable> parameters, IEnumerable> headers, byte[] content, string contentType) { - if (client == null) + public static Task SendAsync(this HttpClient client, HttpMethod method, string url, IEnumerable> queries, IEnumerable> headers, byte[] content, string contentType) { + if (client is null) throw new ArgumentNullException(nameof(client)); - if (method == null) + if (method is null) throw new ArgumentNullException(nameof(method)); if (string.IsNullOrEmpty(url)) throw new ArgumentNullException(nameof(url)); if (string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); - if (method != HttpMethod.Get && parameters != null && content != null) - throw new NotSupportedException(); UriBuilder uriBuilder; HttpRequestMessage request; uriBuilder = new UriBuilder(url); - if (parameters != null && method == HttpMethod.Get) { + if (!(queries is null)) { string query; - query = FormToString(parameters); + query = queries.ToQueryString(); if (!string.IsNullOrEmpty(query)) if (string.IsNullOrEmpty(uriBuilder.Query)) uriBuilder.Query = query; @@ -46,29 +43,16 @@ public static Task SendAsync(this HttpClient client, HttpMe uriBuilder.Query += "&" + query; } request = new HttpRequestMessage(method, uriBuilder.Uri); - if (content != null) + if (!(content is null)) request.Content = new ByteArrayContent(content); - else if (parameters != null && method != HttpMethod.Get) - request.Content = new FormUrlEncodedContent(parameters); - if (request.Content != null) + else if (!(queries is null) && method != HttpMethod.Get) + request.Content = new FormUrlEncodedContent(queries); + if (!(request.Content is null)) request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); - if (headers != null) - UpdateHeaders(request.Headers, headers); + if (!(headers is null)) + foreach (KeyValuePair header in headers) + request.Headers.TryAddWithoutValidation(header.Key, header.Value); return client.SendAsync(request); } - - private static string FormToString(IEnumerable> values) { - return string.Join("&", values.Select(t => t.Key + "=" + Uri.EscapeDataString(t.Value))); - } - - private static void UpdateHeaders(HttpRequestHeaders requestHeaders, IEnumerable> headers) { - if (requestHeaders == null) - throw new ArgumentNullException(nameof(requestHeaders)); - if (headers == null) - throw new ArgumentNullException(nameof(headers)); - - foreach (KeyValuePair item in headers) - requestHeaders.TryAddWithoutValidation(item.Key, item.Value); - } } } diff --git a/NLyric/System/Extensions/HttpExtensions.cs b/NLyric/System/Extensions/HttpExtensions.cs new file mode 100644 index 0000000..02640da --- /dev/null +++ b/NLyric/System/Extensions/HttpExtensions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Extensions { + internal static class HttpExtensions { + public static string ToQueryString(this IEnumerable> queries) { + if (queries is null) + throw new ArgumentNullException(nameof(queries)); + + return string.Join("&", queries.Select(t => Uri.EscapeDataString(t.Key) + "=" + Uri.EscapeDataString(t.Value))); + } + } +} diff --git a/NLyric/The163KeyHelper.cs b/NLyric/The163KeyHelper.cs index 699046d..6fd4f93 100644 --- a/NLyric/The163KeyHelper.cs +++ b/NLyric/The163KeyHelper.cs @@ -38,7 +38,7 @@ public static bool TryGetMusicId(string filePath, out int trackId) { byt163Key = null; break; } - if (byt163Key == null) { + if (byt163Key is null) { trackId = 0; return false; } diff --git a/NLyric/TraditionalToSimplified.map b/NLyric/TraditionalToSimplified.map new file mode 100644 index 0000000000000000000000000000000000000000..54c8efe75a4e8ac1d067e23d9e2c798e447e1f77 GIT binary patch literal 12112 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 literal 0 HcmV?d00001