mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 19:08:33 +08:00
364 lines
14 KiB
C#
364 lines
14 KiB
C#
// 2025/6/23 by Zhe Fang
|
|
|
|
using ATL;
|
|
using BetterLyrics.WinUI3.Enums;
|
|
using BetterLyrics.WinUI3.Helper;
|
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
|
using Lyricify.Lyrics.Providers.Web.Kugou;
|
|
using Lyricify.Lyrics.Searchers;
|
|
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Windows.Storage;
|
|
|
|
namespace BetterLyrics.WinUI3.Services
|
|
{
|
|
public class LyricsSearchService : ILyricsSearchService
|
|
{
|
|
private readonly HttpClient _amllTtmlDbHttpClient;
|
|
private readonly HttpClient _lrcLibHttpClient;
|
|
|
|
private readonly ISettingsService _settingsService;
|
|
private readonly ILogger _logger;
|
|
|
|
public LyricsSearchService(ISettingsService settingsService)
|
|
{
|
|
_settingsService = settingsService;
|
|
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsSearchService>>();
|
|
|
|
_lrcLibHttpClient = new();
|
|
_lrcLibHttpClient.DefaultRequestHeaders.Add(
|
|
"User-Agent",
|
|
$"{MetadataHelper.AppName} {MetadataHelper.AppVersion} ({MetadataHelper.GithubUrl})"
|
|
);
|
|
_amllTtmlDbHttpClient = new();
|
|
}
|
|
|
|
private static bool IsAmllTtmlDbIndexInvalid()
|
|
{
|
|
bool existed = File.Exists(PathHelper.AmllTtmlDbIndexPath);
|
|
|
|
if (!existed)
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
long currentTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
string lastUpdatedStr = File.ReadAllText(PathHelper.AmllTtmlDbLastUpdatedPath);
|
|
long lastUpdated = Convert.ToInt64(lastUpdatedStr);
|
|
return currentTs - lastUpdated > 1 * 24 * 60 * 60;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> DownloadAmllTtmlDbIndexAsync()
|
|
{
|
|
const string url = "https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/metadata/raw-lyrics-index.jsonl";
|
|
try
|
|
{
|
|
using var response = await _amllTtmlDbHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
|
if (!response.IsSuccessStatusCode) return false;
|
|
|
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
|
await using var fs = new FileStream(
|
|
PathHelper.AmllTtmlDbIndexPath,
|
|
FileMode.Create,
|
|
FileAccess.Write,
|
|
FileShare.None
|
|
);
|
|
await stream.CopyToAsync(fs);
|
|
|
|
long currentTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
File.WriteAllText(PathHelper.AmllTtmlDbLastUpdatedPath, currentTs.ToString());
|
|
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<(string?, LyricsSearchProvider?)> SearchAsync(string title, string artist, string album, double durationMs, CancellationToken token)
|
|
{
|
|
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
|
|
|
|
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
|
|
{
|
|
if (!provider.IsEnabled)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string? cachedLyrics;
|
|
LyricsFormat lyricsFormat = provider.Provider.GetLyricsFormat();
|
|
|
|
// Check cache first
|
|
if (provider.Provider.IsRemote())
|
|
{
|
|
cachedLyrics = FileHelper.ReadLyricsCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
|
|
if (!string.IsNullOrWhiteSpace(cachedLyrics))
|
|
{
|
|
return (cachedLyrics, provider.Provider);
|
|
}
|
|
}
|
|
|
|
string? searchedLyrics = null;
|
|
|
|
if (provider.Provider.IsLocal())
|
|
{
|
|
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
|
|
{
|
|
searchedLyrics = SearchEmbedded(title, artist);
|
|
}
|
|
else
|
|
{
|
|
searchedLyrics = await SearchFile(title, artist, lyricsFormat);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (provider.Provider)
|
|
{
|
|
case LyricsSearchProvider.LrcLib:
|
|
searchedLyrics = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000));
|
|
break;
|
|
case LyricsSearchProvider.QQ:
|
|
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.QQMusic);
|
|
break;
|
|
case LyricsSearchProvider.Kugou:
|
|
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Kugou);
|
|
break;
|
|
case LyricsSearchProvider.Netease:
|
|
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Netease);
|
|
break;
|
|
case LyricsSearchProvider.AmllTtmlDb:
|
|
searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
if (!string.IsNullOrWhiteSpace(searchedLyrics))
|
|
{
|
|
if (provider.Provider.IsRemote())
|
|
{
|
|
FileHelper.WriteLyricsCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
|
|
}
|
|
|
|
return (searchedLyrics, provider.Provider);
|
|
}
|
|
}
|
|
|
|
return (null, null);
|
|
}
|
|
|
|
private async Task<string?> SearchFile(string title, string artist, LyricsFormat format)
|
|
{
|
|
foreach (var folder in _settingsService.LocalMediaFolders)
|
|
{
|
|
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
|
{
|
|
foreach (var file in Directory.GetFiles(folder.Path, $"*{format.ToFileExtension()}", SearchOption.AllDirectories))
|
|
{
|
|
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
|
|
{
|
|
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
|
|
if (raw != null)
|
|
{
|
|
return raw;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private string? SearchEmbedded(string title, string artist)
|
|
{
|
|
foreach (var folder in _settingsService.LocalMediaFolders)
|
|
{
|
|
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
|
{
|
|
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
|
|
{
|
|
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
|
|
{
|
|
try
|
|
{
|
|
var plain = TagLib.File.Create(file).Tag.Lyrics;
|
|
if (plain != null && plain != string.Empty)
|
|
{
|
|
return plain;
|
|
}
|
|
}
|
|
catch (Exception) { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async Task<string?> SearchAmllTtmlDbAsync(string title, string artist)
|
|
{
|
|
if (IsAmllTtmlDbIndexInvalid())
|
|
{
|
|
var downloadOk = await DownloadAmllTtmlDbIndexAsync();
|
|
if (!downloadOk)
|
|
return null;
|
|
}
|
|
|
|
string? rawLyricFile = null;
|
|
await foreach (var line in File.ReadLinesAsync(PathHelper.AmllTtmlDbIndexPath))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
continue;
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(line);
|
|
var root = doc.RootElement;
|
|
if (!root.TryGetProperty("metadata", out var metadataArr))
|
|
continue;
|
|
string? musicName = null;
|
|
string? artists = null;
|
|
foreach (var meta in metadataArr.EnumerateArray())
|
|
{
|
|
if (meta.GetArrayLength() != 2)
|
|
continue;
|
|
var key = meta[0].GetString();
|
|
var valueArr = meta[1];
|
|
if (key == "musicName" && valueArr.GetArrayLength() > 0)
|
|
musicName = valueArr[0].GetString();
|
|
if (key == "artists" && valueArr.GetArrayLength() > 0)
|
|
artists = valueArr[0].GetString();
|
|
}
|
|
if (musicName == null || artists == null)
|
|
continue;
|
|
|
|
if (FileHelper.IsSwitchableNormalizedMatch($"{artists} - {musicName}", title, artist))
|
|
{
|
|
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
|
|
{
|
|
rawLyricFile = rawLyricFileProp.GetString();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(rawLyricFile))
|
|
return null;
|
|
|
|
// 下载歌词内容
|
|
var url = $"https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/raw-lyrics/{rawLyricFile}";
|
|
try
|
|
{
|
|
var response = await _amllTtmlDbHttpClient.GetAsync(url);
|
|
if (!response.IsSuccessStatusCode)
|
|
return null;
|
|
return await response.Content.ReadAsStringAsync();
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private async Task<string?> SearchLrcLibAsync(string title, string artist, string album, int duration)
|
|
{
|
|
// Build API query URL
|
|
var url =
|
|
$"https://lrclib.net/api/search?" +
|
|
$"track_name={Uri.EscapeDataString(title)}&" +
|
|
$"artist_name={Uri.EscapeDataString(artist)}&" +
|
|
$"&album_name={Uri.EscapeDataString(album)}" +
|
|
$"&durationMs={Uri.EscapeDataString(duration.ToString())}";
|
|
|
|
var response = await _lrcLibHttpClient.GetAsync(url);
|
|
if (!response.IsSuccessStatusCode)
|
|
return null;
|
|
|
|
var json = await response.Content.ReadAsStringAsync();
|
|
|
|
var jArr = JsonSerializer.Deserialize(
|
|
json,
|
|
Serialization.SourceGenerationContext.Default.JsonElement
|
|
);
|
|
if (jArr.ValueKind == JsonValueKind.Array && jArr.GetArrayLength() > 0)
|
|
{
|
|
var first = jArr[0];
|
|
var syncedLyrics = first.GetProperty("syncedLyrics").GetString();
|
|
var result = string.IsNullOrWhiteSpace(syncedLyrics) ? null : syncedLyrics;
|
|
if (!string.IsNullOrWhiteSpace(result))
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static async Task<string?> SearchQQNeteaseKugouAsync(string title, string artist, string album, int durationMs, Searchers searchers)
|
|
{
|
|
var result = await SearchersHelper.GetSearcher(searchers).SearchForResult(
|
|
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
|
|
{
|
|
DurationMs = durationMs,
|
|
Album = album,
|
|
Artists = [artist],
|
|
Title = title,
|
|
}
|
|
);
|
|
|
|
if (result is QQMusicSearchResult qqResult)
|
|
{
|
|
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.QQMusicApi.GetLyricsAsync(qqResult.Id);
|
|
var original = response?.Lyrics;
|
|
var translated = response?.Trans;
|
|
if (!string.IsNullOrEmpty(translated))
|
|
{
|
|
FileHelper.WriteLyricsCache(
|
|
title,
|
|
artist,
|
|
translated,
|
|
LyricsFormat.Lrc,
|
|
PathHelper.QQTranslationCacheDirectory
|
|
);
|
|
}
|
|
return original;
|
|
}
|
|
else if (result is NeteaseSearchResult neteaseResult)
|
|
{
|
|
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.NeteaseApi.GetLyric(neteaseResult.Id);
|
|
return response?.Lrc.Lyric;
|
|
}
|
|
else if (result is KugouSearchResult kugouResult)
|
|
{
|
|
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.KugouApi.GetSearchLyrics(hash: kugouResult.Hash);
|
|
if (response?.Candidates.FirstOrDefault() is SearchLyricsResponse.Candidate candidate)
|
|
{
|
|
return Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyrics(
|
|
candidate.Id,
|
|
candidate.AccessKey
|
|
);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|