Files
lampac/mods/Music/Providers/Discovery/VkTopChart/VkTopChartDiscoveryProvider.cs
T

184 lines
5.7 KiB
C#

using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
namespace Music;
public class VkTopChartDiscoveryProvider : IMusicDiscoveryProvider
{
static readonly HttpClient httpClient = MusicHttp.CreateClient("vktop200");
static readonly TimeSpan cacheTtl = TimeSpan.FromHours(1);
const string providerId = "vktop200";
const string baseUrl = "https://www.top200chart.ru/";
const string userAgent = "LampacNextgenMusic/0.1 (https://github.com/lampac-nextgen/lampac)";
public string Id => providerId;
public string Name => "VK Top 200";
public bool Enabled => true;
static VkTopChartDiscoveryProvider()
{
httpClient.Timeout = TimeSpan.FromSeconds(20);
}
public async Task<List<MusicBrowseSection>> GetHomeSectionsAsync(int limit, CancellationToken cancellationToken = default)
{
var feed = await GetTracksAsync(cancellationToken);
if (feed.Count == 0)
return new List<MusicBrowseSection>();
return new List<MusicBrowseSection>
{
new()
{
id = "browse:vk_top_tracks",
title = "Топ треки VK",
type = "track",
source_provider = Id,
has_more = feed.Count > limit,
tracks = feed.Take(limit).ToList()
}
};
}
public async Task<MusicBrowseSection> GetSectionAsync(string sectionId, int limit, CancellationToken cancellationToken = default)
{
if (!string.Equals(sectionId, "browse:vk_top_tracks", StringComparison.OrdinalIgnoreCase))
return null;
var feed = await GetTracksAsync(cancellationToken);
if (feed.Count == 0)
return null;
return new MusicBrowseSection
{
id = sectionId,
title = "Топ треки VK",
type = "track",
source_provider = Id,
has_more = false,
tracks = feed.Take(Math.Max(limit, 1)).ToList()
};
}
async Task<List<MusicTrack>> GetTracksAsync(CancellationToken cancellationToken)
{
return await MusicMetadataCacheService.GetOrCreateAsync(
providerId,
"browse",
"top200:tracks:v2",
cacheTtl,
() => LoadTracksAsync(cancellationToken),
cancellationToken
) ?? new List<MusicTrack>();
}
async Task<List<MusicTrack>> LoadTracksAsync(CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, baseUrl);
request.Headers.TryAddWithoutValidation("User-Agent", userAgent);
request.Headers.TryAddWithoutValidation("Accept", "text/html,application/xhtml+xml");
using var response = await httpClient.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
return new List<MusicTrack>();
var html = await response.Content.ReadAsStringAsync(cancellationToken);
var json = ExtractTracksJsonArray(html);
if (json == null)
return new List<MusicTrack>();
return json
.OfType<JsonObject>()
.Select(ParseTrack)
.Where(i => i != null)
.ToList();
}
static JsonArray ExtractTracksJsonArray(string html)
{
if (string.IsNullOrWhiteSpace(html))
return null;
const string marker = "\\\"tracks\\\":[";
int markerIndex = html.IndexOf(marker, StringComparison.Ordinal);
if (markerIndex < 0)
return null;
int start = html.IndexOf('[', markerIndex);
if (start < 0)
return null;
int depth = 0;
for (int i = start; i < html.Length; i++)
{
char c = html[i];
if (c == '[')
{
depth++;
continue;
}
if (c == ']')
{
depth--;
if (depth == 0)
{
string arrayJson = html.Substring(start, i - start + 1)
.Replace("\\\"", "\"")
.Replace("\\\\", "\\");
return JsonNode.Parse(arrayJson) as JsonArray;
}
}
}
return null;
}
static MusicTrack ParseTrack(JsonObject item)
{
if (item == null)
return null;
string title = item["title"]?.GetValue<string>()?.Trim();
string artist = item["artist"]?.GetValue<string>()?.Trim();
string sourceId = item["id"]?.GetValue<int?>()?.ToString();
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(artist))
return null;
string trackId = string.IsNullOrWhiteSpace(sourceId)
? $"vkchart:{StableId($"{artist}::{title}")}"
: $"vkchart:{sourceId}";
string cover = item["cover_url"]?.GetValue<string>();
int? position = item["position"]?.GetValue<int?>();
return new MusicTrack
{
id = trackId,
title = title,
artist_name = artist,
album_title = "VK Top 200",
track_number = position,
images = string.IsNullOrWhiteSpace(cover)
? new List<MusicImage>()
: new List<MusicImage> { new() { url = cover, width = 640, height = 640 } },
provider_refs = new List<MusicProviderRef>
{
new() { provider = providerId, external_id = sourceId ?? trackId }
}
};
}
static string StableId(string value)
{
var bytes = MD5.HashData(Encoding.UTF8.GetBytes(value ?? string.Empty));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}