diff --git a/NLyric/Audio/Track.cs b/NLyric/Audio/Track.cs index a05dde1..a667701 100644 --- a/NLyric/Audio/Track.cs +++ b/NLyric/Audio/Track.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace NLyric.Audio { public class Track : ITrackOrAlbum { @@ -25,10 +26,24 @@ public Track(ATL.Track track) { _name = track.Title.GetSafeString(); _artists = track.Artist.GetSafeString().SplitEx(); + Array.Sort(_artists, StringComparer.Instance); } public override string ToString() { return "Name:" + _name + " | Artists:" + string.Join(",", _artists); } + + private sealed class StringComparer : IComparer { + private static readonly StringComparer _instance = new StringComparer(); + + public static StringComparer Instance => _instance; + + private StringComparer() { + } + + public int Compare(string x, string y) { + return string.CompareOrdinal(x, y); + } + } } } diff --git a/NLyric/NLyric.csproj b/NLyric/NLyric.csproj index 57e4cb1..ccc20c6 100644 --- a/NLyric/NLyric.csproj +++ b/NLyric/NLyric.csproj @@ -1,11 +1,11 @@ - + NLyric NLyric NLyric Copyright © 2019 Wwh - 2.2.0.0 - 2.2.0.0 + 2.2.5.0 + 2.2.5.0 ..\bin\$(Configuration) Exe netcoreapp2.1;net472 diff --git a/NLyric/NLyricImpl.cs b/NLyric/NLyricImpl.cs index ab57189..671aba7 100644 --- a/NLyric/NLyricImpl.cs +++ b/NLyric/NLyricImpl.cs @@ -69,7 +69,6 @@ private static async Task LoginIfNeedAsync() { password = Console.ReadLine(); if (await CloudMusic.LoginAsync(account, password)) { Logger.Instance.LogInfo("登录成功", ConsoleColor.Green); - Logger.Instance.LogNewLine(); break; } else { @@ -82,6 +81,7 @@ private static async Task LoginIfNeedAsync() { else Logger.Instance.LogWarning("输入有误,请重新输入!"); } while (true); + Logger.Instance.LogNewLine(); } private static bool CanSkip(string audioPath, string lrcPath) { @@ -445,6 +445,7 @@ private static TSource MatchByUser(TSource[] sources, TTarget if (sources.Length == 0) return null; result = MatchByUser(sources, target, false); + if (result is null && _fuzzySettings.TryIgnoringExtraInfo) result = MatchByUser(sources, target, true); return result; @@ -453,41 +454,48 @@ private static TSource MatchByUser(TSource[] sources, TTarget private static TSource MatchByUser(TSource[] sources, TTarget target, bool fuzzy) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum { Dictionary nameSimilarities; TSource result; - bool isExact; if (sources.Length == 0) return null; + result = MatchExactly(sources, target, fuzzy); + if (!fuzzy || !(result is null)) + // 不是fuzzy模式或者result不为空,可以直接返回结果,不需要用户选择了 + return result; nameSimilarities = new Dictionary(); foreach (TSource source in sources) nameSimilarities[source] = ComputeSimilarity(source.Name, target.Name, fuzzy); - result = Match(sources, target, nameSimilarities, out isExact); - 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 设置太大了 + return Select(sources.Where(t => nameSimilarities[t] > _matchSettings.MinimumSimilarity).OrderByDescending(t => t, new DictionaryComparer(nameSimilarities)).ToArray(), target, nameSimilarities); } - private static TSource Match(TSource[] sources, TTarget target, Dictionary nameSimilarities, out bool isExact) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum { + private static TSource MatchExactly(TSource[] sources, TTarget target, bool fuzzy) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum { foreach (TSource source in sources) { - double nameSimilarity; - - nameSimilarity = nameSimilarities[source]; - if (nameSimilarity < _matchSettings.MinimumSimilarityAuto) - continue; - foreach (string ncmArtist in source.Artists) - foreach (string artist in target.Artists) - if (ComputeSimilarity(ncmArtist, artist, false) >= _matchSettings.MinimumSimilarityAuto) { - Logger.Instance.LogInfo( - "自动匹配结果:" + Environment.NewLine + - "网易云音乐:" + source.ToString() + Environment.NewLine + - "本地:" + target.ToString() + Environment.NewLine + - "相似度:" + nameSimilarity.ToString()); - isExact = nameSimilarity == 1; - return source; - } + string x; + string y; + + x = source.Name; + y = target.Name; + if (fuzzy) { + x = x.Fuzzy(); + y = y.Fuzzy(); + } + if (x != y) + goto not_equal; + if (source.Artists.Length != target.Artists.Length) + goto not_equal; + for (int i = 0; i < source.Artists.Length; i++) { + x = source.Artists[i]; + y = target.Artists[i]; + if (fuzzy) { + x = x.Fuzzy(); + y = y.Fuzzy(); + } + if (x != y) + goto not_equal; + } + return source; + not_equal: + continue; } - isExact = false; return null; } diff --git a/NLyric/Ncm/CloudMusic.cs b/NLyric/Ncm/CloudMusic.cs index dad3ff4..7d5957b 100644 --- a/NLyric/Ncm/CloudMusic.cs +++ b/NLyric/Ncm/CloudMusic.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Extensions; using System.Linq; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using NeteaseCloudMusicApi; @@ -11,18 +13,18 @@ namespace NLyric.Ncm { public static class CloudMusic { private static readonly CloudMusicApi _api = new CloudMusicApi(); + private static bool _isLoggedIn; public static async Task LoginAsync(string account, string password) { Dictionary queries; bool isPhone; - bool isOk; queries = new Dictionary(); isPhone = Regex.Match(account, "^[0-9]+$").Success; queries[isPhone ? "phone" : "email"] = account; queries["password"] = password; - (isOk, _) = await _api.RequestAsync(isPhone ? CloudMusicApiProviders.LoginCellphone : CloudMusicApiProviders.Login, queries); - return isOk; + (_isLoggedIn, _) = await _api.RequestAsync(isPhone ? CloudMusicApiProviders.LoginCellphone : CloudMusicApiProviders.Login, queries); + return _isLoggedIn; } public static async Task SearchTrackAsync(Track track, int limit, bool withArtists) { @@ -39,11 +41,17 @@ public static async Task SearchTrackAsync(Track track, int limit, bo throw new ArgumentException("歌曲信息无效"); for (int i = 0; i < keywords.Count; i++) keywords[i] = keywords[i].WholeWordReplace(); - (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary { - { "keywords", string.Join(" ", keywords) }, - { "type", "1" }, - { "limit", limit.ToString() } - }); + if (_isLoggedIn) { + (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary { + { "keywords", string.Join(" ", keywords) }, + { "type", "1" }, + { "limit", limit.ToString() } + }); + } + else { + json = await NormalApi.SearchAsync(keywords, NormalApi.SearchType.Track, limit); + isOk = true; + } if (!isOk) throw new ApplicationException(nameof(CloudMusicApiProviders.Search) + " API错误"); json = (JObject)json["result"]; @@ -51,7 +59,7 @@ public static async Task SearchTrackAsync(Track track, int limit, bo throw new ArgumentException($"\"{string.Join(" ", keywords)}\" 中有关键词被屏蔽"); if ((int)json["songCount"] == 0) return Array.Empty(); - return ((JArray)json["songs"]).Select(t => ParseTrack(t, false)).ToArray(); + return json["songs"].Select(t => ParseTrack(t, false)).ToArray(); } public static async Task SearchAlbumAsync(Album album, int limit, bool withArtists) { @@ -68,11 +76,17 @@ public static async Task SearchAlbumAsync(Album album, int limit, bo throw new ArgumentException("专辑信息无效"); for (int i = 0; i < keywords.Count; i++) keywords[i] = keywords[i].WholeWordReplace(); - (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary { - { "keywords", string.Join(" ", keywords) }, - { "type", "10" }, - { "limit", limit.ToString() } - }); + if (_isLoggedIn) { + (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary { + { "keywords", string.Join(" ", keywords) }, + { "type", "10" }, + { "limit", limit.ToString() } + }); + } + else { + json = await NormalApi.SearchAsync(keywords, NormalApi.SearchType.Album, limit); + isOk = true; + } if (!isOk) throw new ApplicationException(nameof(CloudMusicApiProviders.Search) + " API错误"); json = (JObject)json["result"]; @@ -80,19 +94,27 @@ public static async Task SearchAlbumAsync(Album album, int limit, bo throw new ArgumentException($"\"{string.Join(" ", keywords)}\" 中有关键词被屏蔽"); if ((int)json["albumCount"] == 0) return Array.Empty(); - return ((JArray)json["albums"]).Select(t => ParseAlbum(t)).ToArray(); + return json["albums"].Select(t => ParseAlbum(t)).ToArray(); } public static async Task GetTracksAsync(int albumId) { - bool isOk; - JObject json; + if (_isLoggedIn) { + bool isOk; + JObject json; - (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Album, new Dictionary { - { "id", albumId.ToString() } - }); - if (!isOk) - throw new ApplicationException(nameof(CloudMusicApiProviders.Album) + " API错误"); - return ((JArray)json["songs"]).Select(t => ParseTrack(t, true)).ToArray(); + (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Album, new Dictionary { + { "id", albumId.ToString() } + }); + if (!isOk) + throw new ApplicationException(nameof(CloudMusicApiProviders.Album) + " API错误"); + return json["songs"].Select(t => ParseTrack(t, true)).ToArray(); + } + else { + JObject json; + + json = await NormalApi.GetAlbumAsync(albumId); + return json["album"]["songs"].Select(t => ParseTrack(t, false)).ToArray(); + } } public static async Task GetLyricAsync(int trackId) { @@ -103,9 +125,15 @@ public static async Task GetLyricAsync(int trackId) { Lrc translatedLrc; int translatedVersion; - (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Lyric, new Dictionary { - { "id", trackId.ToString() } - }); + if (_isLoggedIn) { + (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Lyric, new Dictionary { + { "id", trackId.ToString() } + }); + } + else { + json = await NormalApi.GetLyricAsync(trackId); + isOk = true; + } if (!isOk) throw new ApplicationException(nameof(CloudMusicApiProviders.Lyric) + " API错误"); if ((bool?)json["uncollected"] == true) @@ -123,22 +151,22 @@ private static NcmAlbum ParseAlbum(JToken json) { Album album; NcmAlbum ncmAlbum; - album = new Album((string)json["name"], ParseNames((JArray)json["artists"]), (int)json["size"], TimeStampToDateTime((long)json["publishTime"]).Year); + album = new Album((string)json["name"], ParseNames(json["artists"]), (int)json["size"], TimeStampToDateTime((long)json["publishTime"]).Year); ncmAlbum = new NcmAlbum(album, (int)json["id"]); return ncmAlbum; } - private static NcmTrack ParseTrack(JToken json, bool fromAlbum) { + private static NcmTrack ParseTrack(JToken json, bool isShortName) { Track track; NcmTrack ncmTrack; - track = new Track((string)json["name"], ParseNames((JArray)json[fromAlbum ? "ar" : "artists"])); + track = new Track((string)json["name"], ParseNames(json[isShortName ? "ar" : "artists"])); ncmTrack = new NcmTrack(track, (int)json["id"]); return ncmTrack; } - private static string[] ParseNames(JArray array) { - return array.Select(t => (string)t["name"]).ToArray(); + private static string[] ParseNames(JToken json) { + return json.Select(t => (string)t["name"]).ToArray(); } private static (Lrc, int) ParseLyric(JToken json) { @@ -155,5 +183,75 @@ private static (Lrc, int) ParseLyric(JToken json) { private static DateTime TimeStampToDateTime(long timeStamp) { return new DateTime(1970, 1, 1).AddMilliseconds(timeStamp); } + + internal static class NormalApi { + private const string SEARCH_URL = "http://music.163.com/api/search/pc"; + private const string ALBUM_URL = "http://music.163.com/api/album"; + private const string LYRIC_URL = "http://music.163.com/api/song/lyric"; + + /// + /// 搜索类型 + /// + public enum SearchType { + Track = 1, + Album = 10 + } + + public static async Task SearchAsync(IEnumerable keywords, SearchType type, int limit) { + QueryCollection queries; + + 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, 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(); + return json; + } + } + + public static async Task GetAlbumAsync(int id) { + using (HttpClient client = new HttpClient()) + using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Get, ALBUM_URL + "/" + id.ToString())) { + JObject json; + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(); + json = JObject.Parse(await response.Content.ReadAsStringAsync()); + if ((int)json["code"] != 200) + throw new HttpRequestException(); + return json; + } + } + + public static async Task GetLyricAsync(int id) { + QueryCollection queries; + + 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, 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(); + return json; + } + } + } } } diff --git a/NLyric/Program.cs b/NLyric/Program.cs index fb53eb8..8b2180c 100644 --- a/NLyric/Program.cs +++ b/NLyric/Program.cs @@ -2,12 +2,13 @@ using System.Cli; using System.IO; using System.Reflection; +using System.Threading.Tasks; using Newtonsoft.Json; using NLyric.Settings; namespace NLyric { public static class Program { - private static void Main(string[] args) { + private static async Task Main(string[] args) { if (args is null || args.Length == 0) { CommandLine.ShowUsage(); return; @@ -25,7 +26,7 @@ private static void Main(string[] args) { return; } AllSettings.Default = JsonConvert.DeserializeObject(File.ReadAllText("Settings.json")); - NLyricImpl.ExecuteAsync(arguments).GetAwaiter().GetResult(); + await NLyricImpl.ExecuteAsync(arguments); Logger.Instance.LogInfo("完成", ConsoleColor.Green); #if DEBUG Console.ReadKey(true); diff --git a/NLyric/Settings.json b/NLyric/Settings.json index 9f0037d..623c947 100644 --- a/NLyric/Settings.json +++ b/NLyric/Settings.json @@ -32,8 +32,7 @@ ] // Feat.的各种写法 }, "Match": { // 匹配设置,在搜索到歌曲信息之后,程序会通过自己的算法再次确认是否匹配 - "MinimumSimilarityAuto": 0.9, // 自动匹配时要求的最小相似度,0~1 - "MinimumSimilarityUser": 0.5, // 用户匹配时的最小相似度,小于设定值的将不予显示,0~1 + "MinimumSimilarity": 0.65, // 匹配时的最小相似度,小于设定值的将不予显示,0~1 "CharReplace": { "\u00B7": "\u002e", "\u0387": "\u002e", @@ -44,7 +43,7 @@ "\u22C5": "\u002e", "\u30FB": "\u002e", "\uFF65": "\u002e", - // · -> . + // . "\uFF0A": "\u002A", // * "\uFF01": "\u0021", diff --git a/NLyric/Settings/MatchSettings.cs b/NLyric/Settings/MatchSettings.cs index 05ba1e2..7a75cd0 100644 --- a/NLyric/Settings/MatchSettings.cs +++ b/NLyric/Settings/MatchSettings.cs @@ -3,26 +3,15 @@ namespace NLyric.Settings { internal sealed class MatchSettings { - private double _minimumSimilarityAuto; - private double _minimumSimilarityUser; + private double _minimumSimilarity; - public double MinimumSimilarityAuto { - get => _minimumSimilarityAuto; + public double MinimumSimilarity { + get => _minimumSimilarity; set { if (value < 0 || value > 1) throw new ArgumentOutOfRangeException(nameof(value)); - _minimumSimilarityAuto = value; - } - } - - public double MinimumSimilarityUser { - get => _minimumSimilarityUser; - set { - if (value < 0 || value > 1) - throw new ArgumentOutOfRangeException(nameof(value)); - - _minimumSimilarityUser = value; + _minimumSimilarity = value; } } diff --git a/NLyric/System/Extensions/HttpClientExtensions.cs b/NLyric/System/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..1586344 --- /dev/null +++ b/NLyric/System/Extensions/HttpClientExtensions.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace System.Extensions { + internal static class HttpClientExtensions { + public static Task SendAsync(this HttpClient client, HttpMethod method, string url) { + return client.SendAsync(method, url, null, null); + } + + 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> 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> queries, IEnumerable> headers, byte[] content, string contentType) { + if (client is null) + throw new ArgumentNullException(nameof(client)); + 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)); + + UriBuilder uriBuilder; + HttpRequestMessage request; + + uriBuilder = new UriBuilder(url); + if (!(queries is null)) { + string query; + + query = queries.ToQueryString(); + if (!string.IsNullOrEmpty(query)) + if (string.IsNullOrEmpty(uriBuilder.Query)) + uriBuilder.Query = query; + else + uriBuilder.Query += "&" + query; + } + request = new HttpRequestMessage(method, uriBuilder.Uri); + if (!(content is null)) + request.Content = new ByteArrayContent(content); + 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 is null)) + foreach (KeyValuePair header in headers) + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + return client.SendAsync(request); + } + } +} 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))); + } + } +}