完善酷狗权益识别并修复音乐列表空行
兼容本地酷狗 API 的多种权益响应结构,严格区分会员等级、有效期与音质解锁条件。统一过滤无效及重复歌曲,修复搜索、队列、历史记录和页面中的空行显示。
This commit is contained in:
@@ -115,7 +115,7 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
{
|
||||
RefreshedAt = refreshedAt,
|
||||
IsCached = true,
|
||||
IsStale = DateTimeOffset.UtcNow - refreshedAt >= TimeSpan.FromHours(12)
|
||||
IsStale = session.LoginState.IsStale || DateTimeOffset.UtcNow - refreshedAt >= TimeSpan.FromHours(12)
|
||||
};
|
||||
_initialized = true;
|
||||
return;
|
||||
@@ -153,7 +153,10 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
{
|
||||
lock (_refreshSync)
|
||||
{
|
||||
if (!force && LoginState.LoggedIn && !LoginState.IsStale) return Task.FromResult(LoginState);
|
||||
if (!force && LoginState.LoggedIn && !LoginState.IsStale && LoginState.Entitlements.Count > 0)
|
||||
{
|
||||
return Task.FromResult(LoginState);
|
||||
}
|
||||
if (!force && _lastRefreshFailure is { } failedAt && DateTimeOffset.UtcNow - failedAt < TimeSpan.FromMinutes(2))
|
||||
{
|
||||
return Task.FromResult(LoginState with { IsCached = true, IsStale = true });
|
||||
@@ -169,9 +172,17 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
await InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (_account is null) return LoginState;
|
||||
await ValidateAccountAsync(_account, cancellationToken).ConfigureAwait(false);
|
||||
LoginState = EnrichLoginState(ToLoginState(_account));
|
||||
var entitlements = await LoadEntitlementsAsync(_account, cancellationToken).ConfigureAwait(false);
|
||||
if (entitlements.Count > 0) LoginState = LoginState with { Entitlements = entitlements };
|
||||
var refreshedState = EnrichLoginState(ToLoginState(_account));
|
||||
var entitlementResult = await LoadEntitlementsAsync(_account, cancellationToken).ConfigureAwait(false);
|
||||
if (_localApi is not null && !entitlementResult.Succeeded)
|
||||
{
|
||||
_lastRefreshFailure = DateTimeOffset.UtcNow;
|
||||
LoginState = LoginState with { IsCached = true, IsStale = true };
|
||||
return LoginState;
|
||||
}
|
||||
LoginState = _localApi is null
|
||||
? refreshedState
|
||||
: refreshedState with { Entitlements = entitlementResult.Grants };
|
||||
_lastRefreshFailure = null;
|
||||
await SaveSessionAsync(_account, LoginState, cancellationToken).ConfigureAwait(false);
|
||||
return LoginState;
|
||||
@@ -290,10 +301,8 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
Uri.EscapeDataString(keywords.Trim()) +
|
||||
$"&page=1&pagesize={Math.Clamp(limit, 1, 100)}&userid=-1&platform=WebFilter&filter=2&iscorrection=1&privilege_filter=0";
|
||||
var result = await GetPublicJsonAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
var songs = KugouApiClient.Array(result["data"]?["lists"])
|
||||
.Select(MapSong)
|
||||
.Where(song => !string.IsNullOrWhiteSpace(song.Id))
|
||||
.ToArray();
|
||||
var songs = MusicSongRules.FilterDisplayable(
|
||||
KugouApiClient.Array(result["data"]?["lists"]).Select(MapSong));
|
||||
return new MusicSearchResult(songs, [], []);
|
||||
}
|
||||
|
||||
@@ -393,7 +402,7 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
account,
|
||||
"everydayrec.service.kugou.com",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return SongArray(response.Json).Select(MapSong).Where(song => !string.IsNullOrWhiteSpace(song.Id)).Take(100).ToArray();
|
||||
return MusicSongRules.FilterDisplayable(SongArray(response.Json).Select(MapSong)).Take(100).ToArray();
|
||||
}
|
||||
catch (KugouApiException exception) when (exception.AuthenticationFailure)
|
||||
{
|
||||
@@ -950,7 +959,10 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
private async Task<KugouAccountCredential> RequireAccountAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (LoginState.IsStale) await RefreshLoginStateAsync(false, cancellationToken).ConfigureAwait(false);
|
||||
if (LoginState.LoggedIn && (LoginState.IsStale || LoginState.Entitlements.Count == 0))
|
||||
{
|
||||
await RefreshLoginStateAsync(false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
return _account ?? throw new InvalidOperationException("请先登录酷狗音乐账户。");
|
||||
}
|
||||
|
||||
@@ -986,9 +998,13 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
private async Task CommitAccountAsync(KugouAccountCredential account, bool save, CancellationToken cancellationToken)
|
||||
{
|
||||
_account = account with { Version = 2 };
|
||||
LoginState = EnrichLoginState(ToLoginState(_account));
|
||||
var entitlements = await LoadEntitlementsAsync(_account, cancellationToken).ConfigureAwait(false);
|
||||
if (entitlements.Count > 0) LoginState = LoginState with { Entitlements = entitlements };
|
||||
var refreshedState = EnrichLoginState(ToLoginState(_account));
|
||||
var entitlementResult = await LoadEntitlementsAsync(_account, cancellationToken).ConfigureAwait(false);
|
||||
LoginState = _localApi is null
|
||||
? refreshedState
|
||||
: entitlementResult.Succeeded
|
||||
? refreshedState with { Entitlements = entitlementResult.Grants }
|
||||
: refreshedState with { Entitlements = [], IsCached = true, IsStale = true };
|
||||
InvalidateAccountCaches();
|
||||
if (save)
|
||||
{
|
||||
@@ -1149,7 +1165,7 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
withinPage = 0;
|
||||
page++;
|
||||
}
|
||||
return collected.Take(count).ToArray();
|
||||
return MusicSongRules.FilterDisplayable(collected).Take(count).ToArray();
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<MusicSong>> LoadCollectionTracksAsync(
|
||||
@@ -1189,7 +1205,7 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
if (nodes.Count < pageSize) break;
|
||||
begin += nodes.Count;
|
||||
}
|
||||
return collected.Take(count).ToArray();
|
||||
return MusicSongRules.FilterDisplayable(collected).Take(count).ToArray();
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<MusicSong>> GetRankTracksAsync(
|
||||
@@ -1213,7 +1229,7 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
withinPage = 0;
|
||||
page++;
|
||||
}
|
||||
return collected.Take(count).ToArray();
|
||||
return MusicSongRules.FilterDisplayable(collected).Take(count).ToArray();
|
||||
}
|
||||
|
||||
private async Task AddSongsToAccountPlaylistAsync(
|
||||
@@ -1462,14 +1478,15 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
DateTimeOffset.UtcNow), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<MusicEntitlementGrant>> LoadEntitlementsAsync(
|
||||
private async Task<KugouEntitlementLoadResult> LoadEntitlementsAsync(
|
||||
KugouAccountCredential account,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_localApi is null) return [];
|
||||
if (_localApi is null) return new(false, []);
|
||||
try
|
||||
{
|
||||
var cookie = $"token={account.Token};userid={account.UserId}";
|
||||
if (!string.IsNullOrWhiteSpace(account.VipLevel)) cookie += $";vip_type={account.VipLevel}";
|
||||
if (!string.IsNullOrWhiteSpace(account.VipToken)) cookie += $";vip_token={account.VipToken}";
|
||||
if (!string.IsNullOrWhiteSpace(account.T1)) cookie += $";t1={account.T1}";
|
||||
var response = await _localApi.SendApiAsync(
|
||||
@@ -1477,30 +1494,11 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
method: HttpMethod.Post,
|
||||
body: new JsonObject { ["cookie"] = cookie },
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var grants = new List<MusicEntitlementGrant>();
|
||||
var index = 0;
|
||||
foreach (var node in KugouApiClient.Array(response["data"]?["busi_vip"] ?? response["busi_vip"]))
|
||||
{
|
||||
var product = KugouApiClient.Text(node, "product_type") ?? KugouApiClient.Text(node, "vip_type") ?? $"group-{index}";
|
||||
var groupCode = KugouApiClient.Text(node, "busi_type") ?? $"group-{index}";
|
||||
var groupName = index switch
|
||||
{
|
||||
0 => "概念版",
|
||||
1 => "畅听版",
|
||||
_ => groupCode
|
||||
};
|
||||
var enabled = KugouApiClient.Integer(node, "is_vip") == 1;
|
||||
var expiryText = KugouApiClient.Text(node, "vip_end_time") ?? KugouApiClient.Text(node, "end_time");
|
||||
DateTimeOffset? expiresAt = DateTimeOffset.TryParse(expiryText, out var parsedExpiry) ? parsedExpiry : null;
|
||||
grants.Add(new MusicEntitlementGrant(
|
||||
product,
|
||||
$"{groupName} {product}",
|
||||
groupCode,
|
||||
enabled,
|
||||
expiresAt));
|
||||
index++;
|
||||
}
|
||||
return grants;
|
||||
if (!KugouEntitlementParser.IsSuccessfulResponse(response)) return new(false, []);
|
||||
var grants = KugouEntitlementParser.Parse(response);
|
||||
return new(true, grants.Count == 0
|
||||
? [new MusicEntitlementGrant("none", "酷狗会员", "membership", false)]
|
||||
: grants);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -1508,7 +1506,7 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
return new(false, []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1533,20 +1531,35 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<MusicQualityOption> BuildQualityOptions(MusicLoginState state)
|
||||
internal static IReadOnlyList<MusicQualityOption> BuildQualityOptions(MusicLoginState state)
|
||||
{
|
||||
var vip = state.Entitlements.Any(item => item.Enabled &&
|
||||
(item.Group == "membership" || item.PlatformCode.Contains("vip", StringComparison.OrdinalIgnoreCase)));
|
||||
var active = state.Entitlements.Where(IsActiveEntitlement).ToArray();
|
||||
var svip = active.Any(IsSvipEntitlement);
|
||||
var vip = svip || active.Any(IsVipEntitlement);
|
||||
return
|
||||
[
|
||||
new(MusicPlaybackQuality.Master, "蝰蛇母带", "FLAC", 1000, "酷狗 SVIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗会员权益"),
|
||||
new(MusicPlaybackQuality.HiRes, "Hi-Res", "FLAC", 900, "酷狗 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗会员权益"),
|
||||
new(MusicPlaybackQuality.Lossless, "无损音质", "FLAC", 800, "酷狗 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗会员权益"),
|
||||
new(MusicPlaybackQuality.Master, "蝰蛇母带", "FLAC", 1000, "酷狗 SVIP", svip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, svip ? null : "需要酷狗 SVIP 权益"),
|
||||
new(MusicPlaybackQuality.HiRes, "Hi-Res", "FLAC", 900, "酷狗 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗 VIP 权益"),
|
||||
new(MusicPlaybackQuality.Lossless, "无损音质", "FLAC", 800, "酷狗 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗 VIP 权益"),
|
||||
new(MusicPlaybackQuality.High, "高品音质", "MP3", 320, "", MusicQualityAvailability.Available),
|
||||
new(MusicPlaybackQuality.Standard, "标准音质", "MP3", 128, "", MusicQualityAvailability.Available)
|
||||
];
|
||||
}
|
||||
|
||||
private static bool IsActiveEntitlement(MusicEntitlementGrant item)
|
||||
=> item.Enabled && (item.ExpiresAt is null || item.ExpiresAt > DateTimeOffset.UtcNow);
|
||||
|
||||
private static bool IsSvipEntitlement(MusicEntitlementGrant item)
|
||||
=> item.PlatformCode.Equals("svip", StringComparison.OrdinalIgnoreCase) ||
|
||||
item.PlatformCode is "2" or "6";
|
||||
|
||||
private static bool IsVipEntitlement(MusicEntitlementGrant item)
|
||||
=> IsSvipEntitlement(item) ||
|
||||
item.PlatformCode.Equals("vip", StringComparison.OrdinalIgnoreCase) ||
|
||||
item.PlatformCode.Equals("dvip", StringComparison.OrdinalIgnoreCase) ||
|
||||
item.PlatformCode.Equals("qvip", StringComparison.OrdinalIgnoreCase) ||
|
||||
item.PlatformCode == "1";
|
||||
|
||||
private sealed record KugouSongData(string Hash, long AlbumId, long MixSongId, long FileId);
|
||||
|
||||
private sealed record KugouPlaybackCandidate(MusicPlaybackQuality Quality, string Value, string Hash, int Bitrate);
|
||||
|
||||
Reference in New Issue
Block a user