Compare commits

...

23 Commits

Author SHA1 Message Date
Zhe Fang
2b3169e5f6 Update check-hash-collision.js 2026-01-10 13:32:08 -05:00
Zhe Fang
9d2e245a99 Rename releases-to-discord.yml to release-to-discord.yml 2026-01-10 13:25:17 -05:00
Zhe Fang
0513c3a128 Add GitHub Actions workflow for plugin registry check 2026-01-10 13:22:32 -05:00
Zhe Fang
5082c4c245 Create check-hash-collision.js 2026-01-10 13:20:59 -05:00
Zhe Fang
659b4d0e60 Create plugins-registry.json 2026-01-10 13:19:39 -05:00
Zhe Fang
85f67c2ec6 Merge pull request #186 from jayfunc/plugin
feat: plugin system
2026-01-10 12:43:34 -05:00
Zhe Fang
bdbeb391ea Merge branch 'dev' into plugin 2026-01-10 12:43:27 -05:00
Zhe Fang
3357e7aaf4 chores: bump to 256 2026-01-10 11:46:26 -05:00
Zhe Fang
e43461d624 feat: float and glow effect now can be adapted to auto word-by-word effect 2026-01-10 11:29:22 -05:00
Zhe Fang
3e1907ad8c fix: lyrics parser (line endtime) 2026-01-10 10:06:11 -05:00
Zhe Fang
74eeffc8a6 fix: fan shape lyrics effect 2026-01-10 09:45:23 -05:00
Zhe Fang
c32eb3b877 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-10 07:38:18 -05:00
Zhe Fang
047e53b830 fix: lyrics parser 2026-01-10 07:38:17 -05:00
Zhe Fang
fdb7bd16f6 plugin 2026-01-10 06:21:57 -05:00
Zhe Fang
4e33444d8e Update README.md 2026-01-09 19:26:31 -05:00
Zhe Fang
8fc67711b6 Simplify language selection in README.CN.md
Removed redundant link from Chinese README.
2026-01-09 19:26:16 -05:00
Zhe Fang
28757d9880 chores: bump to 253 2026-01-09 18:09:45 -05:00
Zhe Fang
e5fb04f577 fix: ValueTransition init 2026-01-09 17:54:50 -05:00
Zhe Fang
9d03c8f688 chores: i18n 2026-01-09 17:39:35 -05:00
Zhe Fang
094fe7b7a1 fix: lyrics animation gpu usage 2026-01-09 17:31:07 -05:00
Zhe Fang
bc32a3f34c feat: add all time filter for stats 2026-01-09 15:27:00 -05:00
Zhe Fang
b23d3c280f fix: not found and loading lyrics placeholder was not displayed 2026-01-09 14:48:12 -05:00
Zhe Fang
2738d45b69 fix: fill Duration (via searching NCM) for amll-ttml-db lyrics source; improve matching system 2026-01-09 13:50:46 -05:00
61 changed files with 761 additions and 257 deletions

View File

@@ -0,0 +1,20 @@
name: Plugin Registry Check
on:
pull_request:
paths:
- 'Community/plugins-registry.json'
jobs:
check-collision:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run Hash Collision Check
run: node Community/scripts/check-hash-collision.js

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,16 @@
using BetterLyrics.Core.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces
{
public interface ILyricsProvider
{
string Id { get; }
string Name { get; }
string Author { get; }
Task<LyricsSearchResult> GetLyricsAsync(string title, string artist, string album, double duration);
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Models
{
public record LyricsSearchResult(
string? Title,
string? Artist,
string? Album,
double? Duration,
string Raw,
string? Translation = null,
string? Transliteration = null,
string? Reference = null);
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.Core\BetterLyrics.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,59 @@
using BetterLyrics.Core.Interfaces;
using BetterLyrics.Core.Models;
namespace BetterLyrics.Plugins.Demo
{
public class DemoLyricsProvider : ILyricsProvider
{
public string Id => "f7acc86b-6e3d-42c3-a9a9-8c05c5339412";
public string Name => "Demo Plugin";
public string Author => "jayfunc";
public async Task<LyricsSearchResult> GetLyricsAsync(string title, string artist, string album, double duration)
{
await Task.Delay(300);
string searchedTitle = "Demo Song";
string searchedArtist = "Demo Artist";
string searchedAlbum = "Demo Album";
double searchedDuration = 25.0;
string searchedRaw =
$"[00:00.00]Welcome to use Demo Plugin\n" +
$"[00:05.00]Playing: {title} now\n" +
$"[00:10.00]Artist: {artist}\n" +
$"[00:15.00]Album: {album}\n" +
$"[00:20.00]Duration: {duration}\n" +
$"[00:25.00]This is a test lyrics source...";
string searchedTranslation =
$"[00:00.00]欢迎使用演示插件\n" +
$"[00:05.00]当前正在播放:{title}\n" +
$"[00:10.00]歌手:{artist}\n" +
$"[00:15.00]专辑:{album}\n" +
$"[00:20.00]时长:{duration}\n" +
$"[00:25.00]这是一个测试歌词源...";
string searchedTransliteration =
$"[00:00.00]ˈwɛlkəm tuː juːz ˈdɛmoʊ ˈplʌgɪn\n" +
$"[00:05.00]ˈpleɪɪŋ: {title} naʊ\n" +
$"[00:10.00]ˈɑːrtɪst: {artist}\n" +
$"[00:15.00]ˈælbəm: {album}\n" +
$"[00:20.00]dʊˈreɪʃən: {duration}\n" +
$"[00:25.00]ðɪs ɪz ə tɛst ˈlɪrɪks sɔːrs...";
string searchedReference = "https://path.to.lyrics/if.the.lyrics.was.originally.fetched.from.web";
return new LyricsSearchResult(
searchedTitle,
searchedArtist,
searchedAlbum,
searchedDuration,
searchedRaw,
searchedTranslation,
searchedTransliteration,
searchedReference);
}
}
}

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.2.249.0" />
Version="1.2.256.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -10,6 +10,7 @@ using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
@@ -248,6 +249,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
.AddSingleton<ILyricsCacheService, LyricsCacheService>()
.AddSingleton<ISongSearchMapService, SongSearchMapService>()
.AddSingleton<IPluginService, PluginService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()

View File

@@ -96,6 +96,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
@@ -121,6 +125,7 @@
<PackageReference Include="z440.atl.core" Version="7.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BetterLyrics.Core\BetterLyrics.Core.csproj" />
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
</ItemGroup>

View File

@@ -74,6 +74,7 @@
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlAllTime" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>

View File

@@ -14,5 +14,6 @@ namespace BetterLyrics.WinUI3.Enums
LocalEslrcFile,
LocalTtmlFile,
AppleMusic,
Plugin = 999,
}
}

View File

@@ -7,6 +7,7 @@
ThisMonth,
ThisQuarter,
ThisYear,
AllTime,
Custom
}
}

View File

@@ -13,5 +13,6 @@
LocalEslrcFile,
LocalTtmlFile,
LibreTranslate,
Plugin = 999,
}
}

View File

@@ -13,6 +13,7 @@
LocalEslrcFile,
LocalTtmlFile,
BetterLyrics,
CutletDocker
CutletDocker,
Plugin = 999,
}
}

View File

@@ -1,5 +1,6 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Services.LocalizationService;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -18,14 +19,27 @@ namespace BetterLyrics.WinUI3.Extensions
new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
PrimaryText = "● ● ●",
PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds }],
IsPrimaryHasRealSyllableInfo = true,
},
],
LanguageCode = "N/A",
};
}
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
PrimaryText = "N/A",
PrimarySyllables = [new BaseLyrics { Text = "N/A", StartMs = 0, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds }],
}]);
}
public void SetTranslatedText(LyricsData translationData, int toleranceMs = 50)
{
foreach (var line in lyricsData.LyricsLines)

View File

@@ -36,6 +36,12 @@ namespace BetterLyrics.WinUI3.Extensions
return songInfo;
}
public SongInfo WithSongId(string value)
{
songInfo.SongId = value;
return songInfo;
}
public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs)
{
if (songInfo == null) return null;

View File

@@ -10,27 +10,27 @@ namespace BetterLyrics.WinUI3.Helper
{
public static partial class MetadataComparer
{
private const double WeightTitle = 0.40;
private const double WeightArtist = 0.40;
private const double WeightTitle = 0.30;
private const double WeightArtist = 0.30;
private const double WeightAlbum = 0.10;
private const double WeightDuration = 0.10;
private const double WeightDuration = 0.30;
// JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new();
public static int CalculateScore(SongInfo songInfo, LyricsCacheItem remote)
{
return CalculateScore(songInfo, remote.Title, remote.Artist, remote.Album, remote.Duration * 1000);
return CalculateScore(songInfo, remote.Title, remote.Artist, remote.Album, remote.Duration);
}
public static int CalculateScore(SongInfo songInfo, FilesIndexItem local)
{
return CalculateScore(songInfo, local.Title, local.Artist, local.Album, local.Duration * 1000, local.FileName);
return CalculateScore(songInfo, local.Title, local.Artist, local.Album, local.Duration, local.FileName);
}
public static int CalculateScore(
SongInfo songInfo,
string? compareTitle, string? compareArtist, string? compareAlbum, double? compareDurationMs, string? compareFileName = null)
string? compareTitle, string? compareArtist, string? compareAlbum, double? compareDuration, string? compareFileName = null)
{
double totalScore = 0;
@@ -42,7 +42,7 @@ namespace BetterLyrics.WinUI3.Helper
double titleScore = GetStringSimilarity(songInfo.Title, compareTitle);
double artistScore = GetStringSimilarity(songInfo.Artist, compareArtist);
double albumScore = GetStringSimilarity(songInfo.Album, compareAlbum);
double durationScore = GetDurationSimilarity(songInfo.DurationMs, compareDurationMs);
double durationScore = GetDurationSimilarity(songInfo.Duration, compareDuration);
totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) +
@@ -94,19 +94,18 @@ namespace BetterLyrics.WinUI3.Helper
return _algo.Similarity(s1, s2);
}
private static double GetDurationSimilarity(double localMs, double? remoteSeconds)
private static double GetDurationSimilarity(double localSeconds, double? remoteSeconds)
{
if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配
double localSeconds = localMs / 1000.0;
double diff = Math.Abs(localSeconds - remoteSeconds.Value);
// 差距 <= 3100% 相似
// 差距 >= 200% 相似
// 差距 <= 1 100 % 相似
// 差距 >= 10 0 % 相似
// 中间线性插值
const double PerfectTolerance = 3.0;
const double MaxTolerance = 20.0;
const double PerfectTolerance = 1.0;
const double MaxTolerance = 10.0;
if (diff <= PerfectTolerance) return 1.0;
if (diff >= MaxTolerance) return 0.0;

View File

@@ -40,17 +40,6 @@ namespace BetterLyrics.WinUI3.Helper
_targetValue = initialValue;
_totalDurationForAutoSplit = defaultTotalDuration;
if (interpolator == null)
{
// 默认缓动
SetEasingType(Enums.EasingType.EaseInOutQuad);
}
else
{
_easingType = null;
_interpolator = interpolator;
}
if (interpolator != null)
{
_interpolator = interpolator;

View File

@@ -36,38 +36,79 @@ namespace BetterLyrics.WinUI3.Logic
double currentPositionMs
)
{
if (lines == null) return;
if (lines == null || lines.Count == 0) return;
var currentPlayingLine = lines.ElementAtOrDefault(primaryPlayingLineIndex);
if (currentPlayingLine == null) return;
if (primaryPlayingLineIndex < 0 || primaryPlayingLineIndex >= lines.Count) return;
var primaryPlayingLine = lines[primaryPlayingLineIndex];
var phoneticOpacity = lyricsStyle.PhoneticLyricsOpacity / 100.0;
var originalOpacity = lyricsStyle.OriginalLyricsOpacity / 100.0;
var translatedOpacity = lyricsStyle.TranslatedLyricsOpacity / 100.0;
for (int i = startIndex; i <= endIndex + 1; i++)
double topHeightFactor = canvasHeight * playingLineTopOffsetFactor;
double bottomHeightFactor = canvasHeight * (1 - playingLineTopOffsetFactor);
double scrollTopDurationSec = lyricsEffect.LyricsScrollTopDuration / 1000.0;
double scrollTopDelaySec = lyricsEffect.LyricsScrollTopDelay / 1000.0;
double scrollBottomDurationSec = lyricsEffect.LyricsScrollBottomDuration / 1000.0;
double scrollBottomDelaySec = lyricsEffect.LyricsScrollBottomDelay / 1000.0;
double canvasTransDuration = canvasYScrollTransition.DurationSeconds;
bool isBlurEnabled = lyricsEffect.IsLyricsBlurEffectEnabled;
bool isOutOfSightEnabled = lyricsEffect.IsLyricsOutOfSightEffectEnabled;
bool isFanEnabled = lyricsEffect.IsFanLyricsEnabled;
double fanAngleRad = Math.PI * (lyricsEffect.FanLyricsAngle / 180.0);
bool isGlowEnabled = lyricsEffect.IsLyricsGlowEffectEnabled;
bool isFloatEnabled = lyricsEffect.IsLyricsFloatAnimationEnabled;
bool isScaleEnabled = lyricsEffect.IsLyricsScaleEffectEnabled;
int safeStart = Math.Max(0, startIndex);
int safeEnd = Math.Min(lines.Count - 1, endIndex + 1);
for (int i = safeStart; i <= safeEnd; i++)
{
var line = lines.ElementAtOrDefault(i);
if (line == null) continue;
var line = lines[i];
var lineHeight = line.PrimaryLineHeight;
if (lineHeight == null || lineHeight <= 0) continue;
bool isWordAnimationEnabled = lyricsEffect.WordByWordEffectMode switch
{
Enums.WordByWordEffectMode.Auto => line.IsPrimaryHasRealSyllableInfo,
Enums.WordByWordEffectMode.Always => true,
Enums.WordByWordEffectMode.Never => false,
_ => line.IsPrimaryHasRealSyllableInfo
};
double targetCharFloat = lyricsEffect.IsLyricsFloatAnimationAmountAutoAdjust
? lineHeight.Value * 0.1
: lyricsEffect.LyricsFloatAnimationAmount;
double targetCharGlow = lyricsEffect.IsLyricsGlowEffectAmountAutoAdjust
? lineHeight.Value * 0.2
: lyricsEffect.LyricsGlowEffectAmount;
double targetCharScale = lyricsEffect.IsLyricsScaleEffectAmountAutoAdjust
? 1.15
: lyricsEffect.LyricsScaleEffectAmount / 100.0;
var maxAnimationDurationMs = Math.Max(line.EndMs ?? 0 - currentPositionMs, 0);
bool isSecondaryLinePlaying = line.GetIsPlaying(currentPositionMs);
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
line.IsPlayingLastFrame = isSecondaryLinePlaying;
// 行动画
if (isLayoutChanged || isPrimaryPlayingLineChanged || isMouseScrollingChanged || isSecondaryLinePlayingChanged)
if (isLayoutChanged || isPrimaryPlayingLineChanged || isMouseScrollingChanged)
{
int lineCountDelta = i - primaryPlayingLineIndex;
double distanceFromPlayingLine = Math.Abs(line.PrimaryPosition.Y - currentPlayingLine.PrimaryPosition.Y);
double distanceFromPlayingLine = Math.Abs(line.PrimaryPosition.Y - primaryPlayingLine.PrimaryPosition.Y);
double distanceFactor;
if (lineCountDelta < 0)
{
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * playingLineTopOffsetFactor), 0, 1);
distanceFactor = Math.Clamp(distanceFromPlayingLine / topHeightFactor, 0, 1);
}
else
{
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * (1 - playingLineTopOffsetFactor)), 0, 1);
distanceFactor = Math.Clamp(distanceFromPlayingLine / bottomHeightFactor, 0, 1);
}
double yScrollDuration;
@@ -76,34 +117,34 @@ namespace BetterLyrics.WinUI3.Logic
if (lineCountDelta < 0)
{
yScrollDuration =
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollTopDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollTopDelay / 1000.0;
canvasTransDuration +
distanceFactor * (scrollTopDurationSec - canvasTransDuration);
yScrollDelay = distanceFactor * scrollTopDelaySec;
}
else if (lineCountDelta == 0)
{
yScrollDuration = canvasYScrollTransition.DurationSeconds;
yScrollDuration = canvasTransDuration;
yScrollDelay = 0;
}
else
{
yScrollDuration =
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollBottomDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollBottomDelay / 1000.0;
canvasTransDuration +
distanceFactor * (scrollBottomDurationSec - canvasTransDuration);
yScrollDelay = distanceFactor * scrollBottomDelaySec;
}
line.BlurAmountTransition.SetDuration(yScrollDuration);
line.BlurAmountTransition.SetDelay(yScrollDelay);
line.BlurAmountTransition.Start(
(isMouseScrolling || isSecondaryLinePlaying) ? 0 :
(lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0));
(isBlurEnabled ? (5 * distanceFactor) : 0));
line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.Start(
isSecondaryLinePlaying ? _highlightedScale :
(lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
(isOutOfSightEnabled ?
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
_highlightedScale));
@@ -140,8 +181,8 @@ namespace BetterLyrics.WinUI3.Logic
line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.Start(
(lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ?
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
(isFanEnabled && !isMouseScrolling) ?
fanAngleRad * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
0);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
@@ -152,128 +193,100 @@ namespace BetterLyrics.WinUI3.Logic
line.YOffsetTransition.Start(targetYScrollOffset);
}
var maxAnimationDurationMs = Math.Max(line.EndMs - currentPositionMs, 0);
// 字符动画
foreach (var renderChar in line.PrimaryRenderChars)
if (isWordAnimationEnabled)
{
renderChar.ProgressPlayed = renderChar.GetPlayProgress(currentPositionMs);
bool isCharPlaying = renderChar.GetIsPlaying(currentPositionMs);
bool isCharPlayingChanged = renderChar.IsPlayingLastFrame != isCharPlaying;
if (isSecondaryLinePlayingChanged || isCharPlayingChanged)
if (isSecondaryLinePlayingChanged)
{
if (lyricsEffect.IsLyricsGlowEffectEnabled)
// 辉光动画
if (isGlowEnabled && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LineStartToCurrentChar
&& isSecondaryLinePlaying)
{
double targetGlow = lyricsEffect.IsLyricsGlowEffectAmountAutoAdjust ? renderChar.LayoutRect.Height * 0.2 : lyricsEffect.LyricsGlowEffectAmount;
switch (lyricsEffect.LyricsGlowEffectScope)
foreach (var renderChar in line.PrimaryRenderChars)
{
case Enums.LyricsEffectScope.LineStartToCurrentChar:
if (isSecondaryLinePlayingChanged && isSecondaryLinePlaying)
{
var stepInOutDuration = Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs) / 2.0 / 1000.0;
var stepLastingDuration = Math.Max(maxAnimationDurationMs / 1000.0 - stepInOutDuration * 2, 0);
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetGlow, stepInOutDuration),
new Models.Keyframe<double>(targetGlow, stepLastingDuration),
new Models.Keyframe<double>(0, stepInOutDuration)
);
}
break;
default:
break;
var stepInOutDuration = Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs) / 2.0 / 1000.0;
var stepLastingDuration = Math.Max(maxAnimationDurationMs / 1000.0 - stepInOutDuration * 2, 0);
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetCharGlow, stepInOutDuration),
new Models.Keyframe<double>(targetCharGlow, stepLastingDuration),
new Models.Keyframe<double>(0, stepInOutDuration)
);
}
}
if (lyricsEffect.IsLyricsFloatAnimationEnabled)
// 浮动动画
if (isFloatEnabled)
{
double targetFloat =
lyricsEffect.IsLyricsFloatAnimationAmountAutoAdjust ? renderChar.LayoutRect.Height * 0.1 : lyricsEffect.LyricsFloatAnimationAmount;
if (isSecondaryLinePlayingChanged)
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetFloat : 0);
renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetCharFloat : 0);
}
if (isCharPlayingChanged)
}
}
// 字符动画
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.ProgressPlayed = renderChar.GetPlayProgress(currentPositionMs);
bool isCharPlaying = renderChar.GetIsPlaying(currentPositionMs);
bool isCharPlayingChanged = renderChar.IsPlayingLastFrame != isCharPlaying;
if (isCharPlayingChanged)
{
if (isFloatEnabled)
{
renderChar.FloatTransition.SetDurationMs(Math.Min(lyricsEffect.LyricsFloatAnimationDuration, maxAnimationDurationMs));
renderChar.FloatTransition.Start(0);
}
}
if (isCharPlayingChanged)
{
renderChar.IsPlayingLastFrame = isCharPlaying;
}
}
}
// 音节动画
foreach (var syllable in line.PrimaryRenderSyllables)
{
bool isSyllablePlaying = syllable.GetIsPlaying(currentPositionMs);
bool isSyllablePlayingChanged = syllable.IsPlayingLastFrame != isSyllablePlaying;
if (isSyllablePlayingChanged)
// 音节动画
foreach (var syllable in line.PrimaryRenderSyllables)
{
var syllableHeight = syllable.ChildrenRenderLyricsChars.FirstOrDefault()?.LayoutRect.Height ?? 0;
bool isSyllablePlaying = syllable.GetIsPlaying(currentPositionMs);
bool isSyllablePlayingChanged = syllable.IsPlayingLastFrame != isSyllablePlaying;
if (lyricsEffect.IsLyricsScaleEffectEnabled)
if (isSyllablePlayingChanged)
{
double targetScale =
lyricsEffect.IsLyricsScaleEffectAmountAutoAdjust ? 1.15 : lyricsEffect.LyricsScaleEffectAmount / 100.0;
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
if (isScaleEnabled && isSyllablePlaying)
{
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration)
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
if (isSyllablePlaying)
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.ScaleTransition.Start(
new Models.Keyframe<double>(targetScale, stepDuration),
new Models.Keyframe<double>(targetCharScale, stepDuration),
new Models.Keyframe<double>(1.0, stepDuration)
);
}
}
}
}
if (lyricsEffect.IsLyricsGlowEffectEnabled)
{
double targetGlow = lyricsEffect.IsLyricsGlowEffectAmountAutoAdjust ? syllableHeight * 0.2 : lyricsEffect.LyricsGlowEffectAmount;
switch (lyricsEffect.LyricsGlowEffectScope)
if (isGlowEnabled && isSyllablePlaying && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LongDurationSyllable
&& syllable.DurationMs >= lyricsEffect.LyricsGlowEffectLongSyllableDuration)
{
case Enums.LyricsEffectScope.LongDurationSyllable:
if (syllable.DurationMs >= lyricsEffect.LyricsGlowEffectLongSyllableDuration)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
if (isSyllablePlaying)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetGlow, stepDuration),
new Models.Keyframe<double>(0, stepDuration)
);
}
}
}
break;
default:
break;
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetCharGlow, stepDuration),
new Models.Keyframe<double>(0, stepDuration)
);
}
}
syllable.IsPlayingLastFrame = isSyllablePlaying;
}
syllable.IsPlayingLastFrame = isSyllablePlaying;
}
}
// 更新动画
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.Update(elapsedTime);
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.Update(elapsedTime);
}
}
line.Update(elapsedTime);

View File

@@ -215,7 +215,7 @@ namespace BetterLyrics.WinUI3.Logic
lanesEndMs.Add(0);
}
lanesEndMs[assignedLane] = end;
lanesEndMs[assignedLane] = end ?? 0;
line.LaneIndex = assignedLane;
}
}

View File

@@ -74,7 +74,7 @@ namespace BetterLyrics.WinUI3.Logic
if (line == null) return state;
double lineEndMs = line.EndMs;
double lineEndMs = line.EndMs ?? 0;
// 还没到
if (currentTimeMs < line.StartMs) return state;
@@ -91,7 +91,7 @@ namespace BetterLyrics.WinUI3.Logic
switch (wordByWordEffectMode)
{
case WordByWordEffectMode.Auto:
if (line.PrimaryRenderSyllables.Count > 1)
if (line.IsPrimaryHasRealSyllableInfo)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
@@ -106,7 +106,7 @@ namespace BetterLyrics.WinUI3.Logic
state.SyllableProgress = 1f;
return state;
case WordByWordEffectMode.Always:
if (line.PrimaryRenderSyllables.Count > 1)
if (line.IsPrimaryHasRealSyllableInfo)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
@@ -129,8 +129,7 @@ namespace BetterLyrics.WinUI3.Logic
var timing = line.PrimaryRenderSyllables[i];
var nextTiming = (i + 1 < count) ? line.PrimaryRenderSyllables[i + 1] : null;
//double timingEndMs = timing.EndMs ?? nextTiming?.StartMs ?? lineEndMs;
double timingEndMs = timing.EndMs;
double timingEndMs = timing.EndMs ?? 0;
// 在当前字范围内
if (time >= timing.StartMs && time <= timingEndMs)

View File

@@ -47,10 +47,15 @@ namespace BetterLyrics.WinUI3.Models
[NotMapped][JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
[MaxLength(128)]
public string? PluginId { get; set; }
public object Clone()
{
return new LyricsCacheItem()
{
PluginId = this.PluginId,
Provider = this.Provider,
TranslationProvider = this.TranslationProvider,
TransliterationProvider = this.TransliterationProvider,

View File

@@ -7,8 +7,8 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
public class BaseLyrics
{
public int StartMs { get; set; }
public int EndMs { get; set; }
public int DurationMs => EndMs - StartMs;
public int? EndMs { get; set; } = null;
public int DurationMs => Math.Max((EndMs ?? 0) - StartMs, 0);
public string Text { get; set; } = "";
public int Length => Text.Length;

View File

@@ -9,8 +9,6 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
{
public class LyricsData
{
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
public List<LyricsLine> LyricsLines { get; set; } = [];
public string? LanguageCode
{
@@ -29,15 +27,5 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
LyricsLines = lyricsLines;
}
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
PrimaryText = _localizationService.GetLocalizedString("LyricsNotFound"),
}]);
}
}
}

View File

@@ -19,6 +19,11 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
public string SecondaryText { get; set; } = "";
public string TertiaryText { get; set; } = "";
public new string Text => PrimaryText;
public new int StartIndex = 0;
public bool IsPrimaryHasRealSyllableInfo { get; set; } = false;
public LyricsLine()
{
for (int charStartIndex = 0; charStartIndex < PrimaryText.Length; charStartIndex++)

View File

@@ -74,6 +74,10 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
/// </summary>
public int LaneIndex { get; set; } = 0;
public double? PrimaryLineHeight => PrimaryRenderChars.FirstOrDefault()?.LayoutRect.Height;
public bool IsPrimaryHasRealSyllableInfo { get; set; }
public RenderLyricsLine(LyricsLine lyricsLine) : base(lyricsLine)
{
AngleTransition = new(
@@ -128,6 +132,7 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
PrimaryText = lyricsLine.PrimaryText;
SecondaryText = lyricsLine.SecondaryText;
PrimaryRenderSyllables = lyricsLine.PrimarySyllables.Select(x => new RenderLyricsSyllable(x)).ToList();
IsPrimaryHasRealSyllableInfo = lyricsLine.IsPrimaryHasRealSyllableInfo;
}
public void UpdateCenterPosition(double maxWidth, TextAlignmentType type)

View File

@@ -40,26 +40,14 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
startIndex += text.Length;
}
int lineEndMs = 0;
if (syllables.Count > 0)
{
var lastSyllable = syllables[syllables.Count - 1];
if (string.IsNullOrWhiteSpace(lastSyllable.Text))
{
lineEndMs = lastSyllable.StartMs;
syllables.RemoveAt(syllables.Count - 1);
}
}
if (syllables.Count > 0)
if (syllables.Count > 1)
{
lrcLines.Add(new LyricsLine
{
StartMs = syllables[0].StartMs,
EndMs = lineEndMs,
PrimaryText = string.Concat(syllables.Select(s => s.Text)),
PrimarySyllables = syllables
PrimarySyllables = syllables,
IsPrimaryHasRealSyllableInfo = true
});
}
else
@@ -81,7 +69,13 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
content = bracketRegex!.Replace(line, "").Trim();
if (content == "//") content = "";
lrcLines.Add(new LyricsLine { StartMs = lineStartMs, PrimaryText = content });
var lyricsLine = new LyricsLine
{
StartMs = lineStartMs,
PrimaryText = content,
IsPrimaryHasRealSyllableInfo = false
};
lrcLines.Add(lyricsLine);
}
}
}
@@ -125,5 +119,6 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
}
}
}
}
}

View File

@@ -21,9 +21,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
var lineWrite = new LyricsLine
{
StartMs = lineRead.StartTime ?? 0,
EndMs = lineRead.EndTime ?? (nextLineRead?.StartTime ?? 0),
PrimaryText = lineRead.Text,
PrimarySyllables = [],
IsPrimaryHasRealSyllableInfo = true,
};
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;

View File

@@ -127,7 +127,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
StartMs = containerStartMs,
EndMs = containerEndMs,
PrimaryText = fullOriginalText,
PrimarySyllables = syllables
PrimarySyllables = syllables,
IsPrimaryHasRealSyllableInfo = true,
});
var transSpan = container.Elements()
@@ -151,7 +152,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
StartMs = startMs,
EndMs = endMs,
PrimaryText = text
PrimaryText = text,
IsPrimaryHasRealSyllableInfo = false,
});
}
else
@@ -160,7 +162,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
StartMs = startMs,
EndMs = endMs,
PrimaryText = ""
PrimaryText = "",
IsPrimaryHasRealSyllableInfo = false,
});
}
}

View File

@@ -69,6 +69,9 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
LoadTransliteration(lyricsSearchResult);
GenerateTransliterationLyricsData();
EnsureEndMs(lyricsSearchResult?.Duration);
EnsureSyllables();
return _lyricsDataArr;
}
@@ -271,5 +274,84 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
}
}
private void EnsureEndMs(double? duration)
{
foreach (var lyricsData in _lyricsDataArr)
{
var lines = lyricsData.LyricsLines;
// 计算结束时间
for (int i = 0; i < lines.Count; i++)
{
// 计算行结束时间
if (lines[i].EndMs == null)
{
if (i + 1 < lines.Count)
{
lines[i].EndMs = lines[i + 1].StartMs;
}
else
{
lines[i].EndMs = (int)(duration ?? 0) * 1000;
}
}
// 计算音节结束时间
for (int j = 0; j < lines[i].PrimarySyllables.Count; j++)
{
var syllable = lines[i].PrimarySyllables[j];
if (syllable.EndMs == null)
{
if (j < lines[i].PrimarySyllables.Count - 1)
{
syllable.EndMs = lines[i].PrimarySyllables[j + 1].StartMs;
}
else
{
syllable.EndMs = lines[i].EndMs;
}
}
}
}
}
}
/// <summary>
/// Invoke this after <see cref="EnsureEndMs"/>
/// </summary>
private void EnsureSyllables()
{
foreach (var lyricsData in _lyricsDataArr)
{
if (lyricsData == null) continue;
var lines = lyricsData.LyricsLines;
if (lines == null) continue;
foreach (var line in lines)
{
if (line == null) continue;
if (line.IsPrimaryHasRealSyllableInfo) continue;
if (line.PrimarySyllables.Count > 0) continue;
var content = line.PrimaryText;
var length = content.Length;
if (length == 0) continue;
var avgSyllableDuration = line.DurationMs / length;
if (avgSyllableDuration == 0) continue;
for (int j = 0; j < length; j++)
{
line.PrimarySyllables.Add(new BaseLyrics
{
Text = content[j].ToString(),
StartIndex = j,
StartMs = line.StartMs + avgSyllableDuration * j,
EndMs = line.StartMs + avgSyllableDuration * (j + 1),
});
}
}
}
}
}
}

View File

@@ -26,12 +26,12 @@ namespace BetterLyrics.WinUI3.Renderer
Color fgColor,
LyricsEffectSettings settings)
{
DrawPhonetic(ds, textOnlyLayer, line);
DrawOriginalText(control, ds, textOnlyLayer, line, playbackState, bgColor, fgColor, settings);
DrawTranslated(ds, textOnlyLayer, line);
DrawTertiaryText(ds, textOnlyLayer, line);
DrawPrimaryText(control, ds, textOnlyLayer, line, playbackState, bgColor, fgColor, settings);
DrawSecondaryText(ds, textOnlyLayer, line);
}
private void DrawPhonetic(CanvasDrawingSession ds, ICanvasImage source, RenderLyricsLine line)
private void DrawTertiaryText(CanvasDrawingSession ds, ICanvasImage source, RenderLyricsLine line)
{
if (line.TertiaryTextLayout == null) return;
@@ -65,7 +65,7 @@ namespace BetterLyrics.WinUI3.Renderer
});
}
private void DrawTranslated(CanvasDrawingSession ds, ICanvasImage source, RenderLyricsLine line)
private void DrawSecondaryText(CanvasDrawingSession ds, ICanvasImage source, RenderLyricsLine line)
{
if (line.SecondaryTextLayout == null) return;
@@ -99,7 +99,7 @@ namespace BetterLyrics.WinUI3.Renderer
});
}
private void DrawOriginalText(
private void DrawPrimaryText(
ICanvasResourceCreator resourceCreator,
CanvasDrawingSession ds,
ICanvasImage source,

View File

@@ -22,7 +22,6 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
{
_logger.LogInformation("RefreshLyricsAsync");
CurrentLyricsSearchResult = null;
CurrentLyricsData = LyricsData.GetLoadingPlaceholder();
if (CurrentSongInfo != SongInfoExtensions.Placeholder)

View File

@@ -11,6 +11,7 @@ using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService;
@@ -19,6 +20,7 @@ using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
@@ -206,7 +208,6 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
private void InitMediaManager()
{
_mediaManager.Start();
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSession(x.Value.Id));
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;

View File

@@ -55,6 +55,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsCacheService
existingItem.Title = result.Title;
existingItem.Artist = result.Artist;
existingItem.Album = result.Album;
existingItem.Duration = result.Duration;
existingItem.TransliterationProvider = result.TransliterationProvider;
existingItem.TranslationProvider = result.TranslationProvider;

View File

@@ -1,5 +1,7 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.Core.Interfaces;
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
@@ -9,6 +11,7 @@ using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Providers;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
using Lyricify.Lyrics.Helpers;
@@ -29,12 +32,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
private readonly HttpClient _amllTtmlDbHttpClient;
private readonly HttpClient _lrcLibHttpClient;
private readonly AppleMusic _appleMusic;
private readonly Providers.AppleMusic _appleMusic;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private readonly ILyricsCacheService _lyricsCacheService;
private readonly ISongSearchMapService _songSearchMapService;
private readonly IPluginService _pluginService;
private readonly ILogger _logger;
public LyricsSearchService(
@@ -42,6 +46,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
IFileSystemService fileSystemService,
ILyricsCacheService lyricsCacheService,
ISongSearchMapService songSearchMapService,
IPluginService pluginService,
ILogger<LyricsSearchService> logger
)
{
@@ -49,15 +54,16 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
_fileSystemService = fileSystemService;
_lyricsCacheService = lyricsCacheService;
_songSearchMapService = songSearchMapService;
_pluginService = pluginService;
_logger = logger;
_lrcLibHttpClient = new();
_lrcLibHttpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{Constants.App.AppName} {MetadataHelper.AppVersion} ({Constants.Link.BetterLyricsGitHub})"
$"{Constants.App.AppName} {MetadataHelper.AppVersion} ({Link.BetterLyricsGitHub})"
);
_amllTtmlDbHttpClient = new();
_appleMusic = new AppleMusic();
_appleMusic = new Providers.AppleMusic();
}
private static bool IsAmllTtmlDbIndexInvalid()
@@ -206,11 +212,26 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
_logger.LogInformation("SearchAllAsync {SongInfo}", songInfo);
var results = new List<LyricsCacheItem>();
foreach (var provider in Enum.GetValues<LyricsSearchProvider>())
{
if (provider == LyricsSearchProvider.Plugin) continue;
var searchResult = await SearchSingleAsync(songInfo, provider, checkCache, token);
results.Add(searchResult);
}
if (_pluginService.Providers.Any())
{
foreach (var plugin in _pluginService.Providers)
{
if (token.IsCancellationRequested) break;
var pluginResult = await SearchPluginAsync(songInfo, plugin, token);
results.Add(pluginResult);
}
}
return results;
}
@@ -402,6 +423,8 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
string? rawLyricFile = null;
string? bestNcmMusicId = null;
await foreach (var line in File.ReadLinesAsync(PathHelper.AmllTtmlDbIndexPath))
{
if (string.IsNullOrWhiteSpace(line))
@@ -416,6 +439,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
string? title = null;
string? artist = null;
string? album = null;
string? ncmMusicId = null;
foreach (var meta in metadataArr.EnumerateArray())
{
@@ -429,6 +453,8 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
artist = string.Join(" / ", valueArr.EnumerateArray());
if (key == "album" && valueArr.GetArrayLength() > 0)
album = valueArr[0].GetString();
if (key == "ncmMusicId" && valueArr.GetArrayLength() > 0)
ncmMusicId = valueArr[0].GetString();
}
int score = MetadataComparer.CalculateScore(songInfo, new LyricsCacheItem
@@ -436,12 +462,12 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
Title = title,
Artist = artist,
Album = album,
Duration = 0,
});
if (score > lyricsSearchResult.MatchPercentage)
{
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
{
bestNcmMusicId = ncmMusicId;
rawLyricFile = rawLyricFileProp.GetString();
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
@@ -458,19 +484,28 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
// 下载歌词内容
var url = $"{_settingsService.AppSettings.GeneralSettings.AmllTtmlDbBaseUrl}/{Constants.AmllTTmlDB.QueryPrefix}/{rawLyricFile}";
var url = $"{_settingsService.AppSettings.GeneralSettings.AmllTtmlDbBaseUrl}/{AmllTTmlDB.QueryPrefix}/{rawLyricFile}";
lyricsSearchResult.Reference = url;
try
{
// 下载写入歌词
using var response = await _amllTtmlDbHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return lyricsSearchResult;
}
string lyrics = await response.Content.ReadAsStringAsync();
lyricsSearchResult.Raw = lyrics;
// 反查时长
if (bestNcmMusicId != null && lyricsSearchResult.Duration == null)
{
var tmp = await SearchQQNeteaseKugouAsync(
((SongInfo)songInfo.Clone()).WithSongId($"{ExtendedGenreFiled.NetEaseCloudMusicTrackID}{bestNcmMusicId}"),
Searchers.Netease);
lyricsSearchResult.Duration = tmp.Duration;
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
}
}
catch
{
@@ -633,7 +668,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
lyricsSearchResult.Title = result?.Title;
lyricsSearchResult.Artist = string.Join(" / ", result?.Artists ?? []);
lyricsSearchResult.Artist = result?.Artist;
lyricsSearchResult.Album = result?.Album;
lyricsSearchResult.Duration = result?.DurationMs / 1000;
@@ -658,5 +693,41 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsCacheItem> SearchPluginAsync(SongInfo songInfo, ILyricsProvider plugin, CancellationToken token)
{
var cacheItem = new LyricsCacheItem
{
Provider = LyricsSearchProvider.Plugin,
PluginId = plugin.Id,
};
try
{
var result = await plugin.GetLyricsAsync(songInfo.Title, songInfo.Artist, songInfo.Album, songInfo.Duration);
if (result != null && !string.IsNullOrEmpty(result.Raw))
{
cacheItem.Title = result.Title;
cacheItem.Artist = result.Artist;
cacheItem.Album = result.Album;
cacheItem.Duration = result.Duration;
cacheItem.Raw = result.Raw;
cacheItem.Translation = result.Translation;
cacheItem.Transliteration = result.Transliteration;
cacheItem.Reference = result.Reference ?? "about:blank";
cacheItem.MatchPercentage = MetadataComparer.CalculateScore(songInfo, cacheItem);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Plugin {PluginName} failed to search", plugin.Name);
}
return cacheItem;
}
}
}

View File

@@ -0,0 +1,14 @@
using BetterLyrics.Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Services.PluginService
{
public interface IPluginService
{
IReadOnlyList<ILyricsProvider> Providers { get; }
void LoadPlugins();
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
namespace BetterLyrics.WinUI3.Services.PluginService
{
public class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
}
}

View File

@@ -0,0 +1,21 @@
using BetterLyrics.Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Services.PluginService
{
public class PluginService : IPluginService
{
private List<ILyricsProvider> _providers = new();
public IReadOnlyList<ILyricsProvider> Providers => _providers;
public void LoadPlugins()
{
// 在涉及加载程序集的地方:
// var context = new PluginLoadContext(pluginPath);
// 它是本文件夹下的 internal 或者是 public 类,直接用即可。
}
}
}

View File

@@ -244,7 +244,7 @@
<value>مثال: http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>المدة</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>جارٍ تحميل الكلمات...</value>
@@ -1138,16 +1138,16 @@
<value>اختصار تبديل حالة نافذة الكلمات</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>استراتيجية الرسوم المتحركة كلمة بكلمة</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>دائماً</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>السيارات</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>أبداً</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>سيؤثر ضبط هذه القيمة على نتائج البحث المتسلسل والبحث بأفضل تطابق، ولكنه لن يؤثر على نتائج البحث في واجهة البحث اليدوي عن الكلمات</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>النشاط بالساعة</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>كل الوقت</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>مخصص</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>z. B. http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Dauer</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Songtext wird geladen...</value>
@@ -1138,16 +1138,16 @@
<value>Tastenkürzel für Songtext-Fensterstatuswechsel</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Wort-für-Wort-Animationsstrategie</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Immer</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Niemals</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Das Anpassen dieses Wertes beeinflusst die sequenzielle Suche und die Suche nach der besten Übereinstimmung, hat jedoch keinen Einfluss auf die Suchergebnisse in der manuellen Songtext-Suchoberfläche</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktivität nach Stunden</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Alle Zeiten</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Benutzerdefiniert</value>
</data>

View File

@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Activity by Hour</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>All Time</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Custom</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>ej. http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Duración</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Cargando letra...</value>
@@ -1138,16 +1138,16 @@
<value>Atajo de cambio de estado de ventana de letras</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Estrategia de animación palabra por palabra</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Siempre</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Nunca</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Ajustar este valor afectará a la búsqueda secuencial y a los resultados de la búsqueda de mejor coincidencia, pero no afectará a los resultados de búsqueda en la interfaz de búsqueda manual de letras</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Actividad por horas</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Todos los tiempos</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>A medida</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>ex : http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Durée de l'accord</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Chargement des paroles...</value>
@@ -1138,16 +1138,16 @@
<value>Raccourci changement état fenêtre paroles</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Stratégie d'animation mot à mot</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Toujours</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Jamais</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>L'ajustement de cette valeur affectera les résultats de la recherche séquentielle et de la meilleure correspondance, mais n'affectera pas les résultats de la recherche manuelle</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Activité par heure</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Tout le temps</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Sur mesure</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>उदाहरण http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>समयांतराल</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>बोल लोड हो रहे हैं...</value>
@@ -1138,16 +1138,16 @@
<value>बोल विंडो स्थिति स्विच शॉर्टकट</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>शब्द-दर-शब्द एनिमेशन रणनीति</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>हमेशा</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>स्वतः</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>कभी नहीं</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>इस मान को समायोजित करने से अनुक्रमिक खोज और सर्वोत्तम मिलान खोज परिणाम प्रभावित होंगे, लेकिन मैनुअल बोल खोज इंटरफ़ेस में खोज परिणाम प्रभावित नहीं होंगे</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>घंटे के हिसाब से गतिविधि</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>अब तक के सारे</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>कस्टम</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>Contoh: http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Durasi</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Memuat lirik...</value>
@@ -1138,16 +1138,16 @@
<value>Pintasan Pengalih Status Jendela Lirik</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Strategi Animasi Kata per Kata</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Selalu</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>Otomatis</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Tidak pernah</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Menyesuaikan nilai ini akan memengaruhi hasil pencarian berurutan dan pencarian kecocokan terbaik, tetapi tidak akan memengaruhi hasil pencarian di antarmuka pencarian manual lirik</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktivitas per Jam</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Semua Waktu</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Kustom</value>
</data>

View File

@@ -1051,7 +1051,7 @@
<value>カスタム</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>フォントファミリー</value>
<value>フォント</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>フォントサイズ</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>アクティブ時間帯</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>全期間</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>カスタム</value>
</data>
@@ -1492,7 +1495,7 @@
<value>最多アクティブ</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>再生記録中...</value>
<value>再生履歴記録中...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>再生ソース</value>

View File

@@ -244,7 +244,7 @@
<value>예: http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>기간</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>가사 불러오는 중...</value>
@@ -1138,16 +1138,16 @@
<value>가사 창 상태 전환 단축키</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>단어별 애니메이션 전략</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>항상</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>자동</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>절대로</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>이 값을 조정하면 순차 검색 및 최적 일치 검색 결과에 영향을 미치지만 수동 가사 검색 인터페이스의 검색 결과에는 영향을 미치지 않습니다</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>시간별 활동</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>모든 시간</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>사용자 지정</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>Contoh: http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Durasi</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Memuatkan lirik...</value>
@@ -1138,16 +1138,16 @@
<value>Pintasan Tukar Status Tetingkap Lirik</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Strategi Animasi Perkataan demi Perkataan</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Sentiasa</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>Automatik</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Tak pernah</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Melaraskan nilai ini akan mempengaruhi hasil carian jujukan dan carian padanan terbaik, tetapi tidak akan mempengaruhi hasil carian dalam antara muka carian lirik manual</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktiviti mengikut Jam</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Semua</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Tersuai</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>Exemplo: http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Duração</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>A carregar a letra...</value>
@@ -1138,16 +1138,16 @@
<value>Atalho de Alternância de Estado da Janela</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Estratégia de animação palavra a palavra</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Sempre</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>Automóvel</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Nunca</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Ajustar este valor afetará os resultados da pesquisa sequencial e de melhor correspondência, mas não afetará os resultados na interface de pesquisa manual de letras</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Atividade por hora</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Todo o tempo</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Personalizado</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>например, http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Продолжительность</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Загрузка текста...</value>
@@ -1138,16 +1138,16 @@
<value>Горячая клавиша переключения состояния окна</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Стратегия словесной анимации</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Всегда</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>Авто</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Никогда</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Настройка этого значения повлияет на результаты последовательного поиска и поиска лучшего совпадения, но не повлияет на результаты в интерфейсе ручного поиска</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Активность по часам</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Все время</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Пользовательское</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>ตัวอย่าง http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>ระยะเวลา</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>กำลังโหลดเนื้อเพลง...</value>
@@ -1138,16 +1138,16 @@
<value>ทางลัดสลับสถานะหน้าต่างเนื้อเพลง</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>กลยุทธ์การสร้างแอนิเมชันแบบคำต่อคำ</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>เสมอ</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>ออโต้</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>ไม่เคย</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>การปรับค่านี้จะมีผลกับผลการค้นหาแบบตามลำดับและแบบตรงกันที่สุด แต่จะไม่มีผลกับผลการค้นหาในหน้าค้นหาเนื้อเพลงด้วยตนเอง</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>กิจกรรมตามชั่วโมง</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>ตลอดเวลา</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>กำหนดเอง</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>Ví dụ: http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>Thời gian</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Đang tải lời bài hát...</value>
@@ -1138,16 +1138,16 @@
<value>Phím tắt chuyển trạng thái cửa sổ lời bài hát</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>Chiến lược hoạt hình từng từ</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>Luôn luôn</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>Tự động</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>Không bao giờ</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Điều chỉnh giá trị này sẽ ảnh hưởng đến kết quả tìm kiếm tuần tự và khớp nhất, nhưng sẽ không ảnh hưởng đến kết quả tìm kiếm trong giao diện tìm kiếm lời bài hát thủ công</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Hoạt động theo giờ</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>Mọi thời đại</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Tùy chỉnh</value>
</data>

View File

@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>活跃时段</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>全部时间</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>自定义</value>
</data>

View File

@@ -244,7 +244,7 @@
<value>例如 http://localhost:5000</value>
</data>
<data name="LyricsEffectSettingsControlAnimationDuration.Header" xml:space="preserve">
<value>Duration</value>
<value>時間長度</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>載入歌詞中...</value>
@@ -1138,16 +1138,16 @@
<value>歌詞視窗狀態切換快速鍵</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
<value>逐字動畫策略</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
<value>永遠</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
<value>自動</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
<value>從不</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>調整此值將影響順序搜尋和最佳符合搜尋結果,但不會影響手動歌詞搜尋介面中的搜尋結果</value>
@@ -1479,6 +1479,9 @@
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>每小時的活動</value>
</data>
<data name="StatsDashboardControlAllTime.Content" xml:space="preserve">
<value>所有時間</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>自訂</value>
</data>

View File

@@ -186,6 +186,9 @@ namespace BetterLyrics.WinUI3.ViewModels
case StatsRange.ThisYear:
startLocal = new DateTime(nowLocal.Year, 1, 1);
break;
case StatsRange.AllTime:
startLocal = DateTime.MinValue;
break;
}
CustomStartDate = startLocal.Date;

View File

@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.36105.23
# Visual Studio Version 18
VisualStudioVersion = 18.1.11312.151
MinimumVisualStudioVersion = 10.0.40219.1
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "BetterLyrics.WinUI3 (Package)", "BetterLyrics.WinUI3\BetterLyrics.WinUI3 (Package)\BetterLyrics.WinUI3 (Package).wapproj", "{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}"
EndProject
@@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impressionist", "Impression
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorThief.WinUI3", "ColorThief.WinUI3\ColorThief.WinUI3.csproj", "{8F2FE667-2D91-428E-0630-05E6330F9625}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Core", "BetterLyrics.Core\BetterLyrics.Core.csproj", "{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Plugins.Demo", "BetterLyrics.Plugins.Demo\BetterLyrics.Plugins.Demo.csproj", "{87D235CA-4311-4766-8186-AD9B193DFABC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -75,6 +79,30 @@ Global
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x64.Build.0 = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.ActiveCfg = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.Build.0 = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|ARM64.Build.0 = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x64.ActiveCfg = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x64.Build.0 = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x86.ActiveCfg = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x86.Build.0 = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|ARM64.ActiveCfg = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|ARM64.Build.0 = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x64.ActiveCfg = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x64.Build.0 = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x86.ActiveCfg = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x86.Build.0 = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Debug|ARM64.Build.0 = Debug|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Debug|x64.ActiveCfg = Debug|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Debug|x64.Build.0 = Debug|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Debug|x86.ActiveCfg = Debug|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Debug|x86.Build.0 = Debug|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|ARM64.ActiveCfg = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|ARM64.Build.0 = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|x64.ActiveCfg = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|x64.Build.0 = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|x86.ActiveCfg = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,40 @@
// Community/scripts/check-hash-collision.js
const fs = require('fs');
const path = require('path');
const registryPath = path.join(__dirname, '../plugins-registry.json');
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
function getStableId(pluginId) {
let hash = 23;
for (let i = 0; i < pluginId.length; i++) {
hash = (hash * 31 + pluginId.charCodeAt(i)) | 0;
}
return Math.abs(hash) + 1000;
}
const seenIds = new Set();
let hasError = false;
console.log("🔍 Starting to check for plugin ID conflicts...");
registry.forEach(item => {
const stableId = getStableId(item.id);
console.log(`Checking [${item.id}] -> Hash: ${stableId}`);
if (seenIds.has(stableId)) {
console.error(`⛔ Fatel error! Conflict detected!`);
console.error(`The hash value (${stableId}) calculated from the plugin ID [${item.id}] is duplicated with an existing plugin.`);
hasError = true;
}
seenIds.add(stableId);
});
if (hasError) {
console.log("⛔ Check failed, please change the plugin ID.");
process.exit(1);
} else {
console.log("✅ The check passed; no conflicts were found.");
process.exit(0);
}

View File

@@ -1,4 +1,4 @@
[**中文**](README.CN.md) | [**English**](README.md)
**中文** | [**English**](README.md)
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">

View File

@@ -1,4 +1,4 @@
[**中文**](README.CN.md) | [**English**](README.md)
[**中文**](README.CN.md) | **English**
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">