Skip to content

Commit

Permalink
在不登录的时候使用普通api
Browse files Browse the repository at this point in the history
更新匹配算法
  • Loading branch information
wwh1004 committed Aug 24, 2019
1 parent 75298de commit eb61600
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 80 deletions.
15 changes: 15 additions & 0 deletions NLyric/Audio/Track.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace NLyric.Audio {
public class Track : ITrackOrAlbum {
Expand All @@ -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<string> {
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);
}
}
}
}
6 changes: 3 additions & 3 deletions NLyric/NLyric.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>NLyric</AssemblyName>
<Title>NLyric</Title>
<Product>NLyric</Product>
<Copyright>Copyright © 2019 Wwh</Copyright>
<AssemblyVersion>2.2.0.0</AssemblyVersion>
<FileVersion>2.2.0.0</FileVersion>
<AssemblyVersion>2.2.5.0</AssemblyVersion>
<FileVersion>2.2.5.0</FileVersion>
<OutputPath>..\bin\$(Configuration)</OutputPath>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.1;net472</TargetFrameworks>
Expand Down
60 changes: 34 additions & 26 deletions NLyric/NLyricImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -445,6 +445,7 @@ private static TSource MatchByUser<TSource, TTarget>(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;
Expand All @@ -453,41 +454,48 @@ private static TSource MatchByUser<TSource, TTarget>(TSource[] sources, TTarget
private static TSource MatchByUser<TSource, TTarget>(TSource[] sources, TTarget target, bool fuzzy) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {
Dictionary<TSource, double> 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<TSource, double>();
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<TSource, double>(nameSimilarities)).ToArray(), target, nameSimilarities) : null;
// fuzzy为true时是第二次搜索了,再让用户再次手动从搜索结果中选择,自动匹配失败的原因可能是 Settings.Match.MinimumSimilarity 设置太大了
return Select(sources.Where(t => nameSimilarities[t] > _matchSettings.MinimumSimilarity).OrderByDescending(t => t, new DictionaryComparer<TSource, double>(nameSimilarities)).ToArray(), target, nameSimilarities);
}

private static TSource Match<TSource, TTarget>(TSource[] sources, TTarget target, Dictionary<TSource, double> nameSimilarities, out bool isExact) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {
private static TSource MatchExactly<TSource, TTarget>(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;
}

Expand Down
160 changes: 129 additions & 31 deletions NLyric/Ncm/CloudMusic.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<bool> LoginAsync(string account, string password) {
Dictionary<string, string> queries;
bool isPhone;
bool isOk;

queries = new Dictionary<string, string>();
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<NcmTrack[]> SearchTrackAsync(Track track, int limit, bool withArtists) {
Expand All @@ -39,19 +41,25 @@ public static async Task<NcmTrack[]> 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<string, string> {
{ "keywords", string.Join(" ", keywords) },
{ "type", "1" },
{ "limit", limit.ToString() }
});
if (_isLoggedIn) {
(isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary<string, string> {
{ "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"];
if (json is null)
throw new ArgumentException($"\"{string.Join(" ", keywords)}\" 中有关键词被屏蔽");
if ((int)json["songCount"] == 0)
return Array.Empty<NcmTrack>();
return ((JArray)json["songs"]).Select(t => ParseTrack(t, false)).ToArray();
return json["songs"].Select(t => ParseTrack(t, false)).ToArray();
}

public static async Task<NcmAlbum[]> SearchAlbumAsync(Album album, int limit, bool withArtists) {
Expand All @@ -68,31 +76,45 @@ public static async Task<NcmAlbum[]> 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<string, string> {
{ "keywords", string.Join(" ", keywords) },
{ "type", "10" },
{ "limit", limit.ToString() }
});
if (_isLoggedIn) {
(isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary<string, string> {
{ "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"];
if (json is null)
throw new ArgumentException($"\"{string.Join(" ", keywords)}\" 中有关键词被屏蔽");
if ((int)json["albumCount"] == 0)
return Array.Empty<NcmAlbum>();
return ((JArray)json["albums"]).Select(t => ParseAlbum(t)).ToArray();
return json["albums"].Select(t => ParseAlbum(t)).ToArray();
}

public static async Task<NcmTrack[]> GetTracksAsync(int albumId) {
bool isOk;
JObject json;
if (_isLoggedIn) {
bool isOk;
JObject json;

(isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Album, new Dictionary<string, string> {
{ "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<string, string> {
{ "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<NcmLyric> GetLyricAsync(int trackId) {
Expand All @@ -103,9 +125,15 @@ public static async Task<NcmLyric> GetLyricAsync(int trackId) {
Lrc translatedLrc;
int translatedVersion;

(isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Lyric, new Dictionary<string, string> {
{ "id", trackId.ToString() }
});
if (_isLoggedIn) {
(isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Lyric, new Dictionary<string, string> {
{ "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)
Expand All @@ -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) {
Expand All @@ -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";

/// <summary>
/// 搜索类型
/// </summary>
public enum SearchType {
Track = 1,
Album = 10
}

public static async Task<JObject> SearchAsync(IEnumerable<string> 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<JObject> 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<JObject> 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;
}
}
}
}
}
Loading

0 comments on commit eb61600

Please sign in to comment.