Compare commits

..

9 Commits

Author SHA1 Message Date
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
29 changed files with 464 additions and 129 deletions

View File

@@ -6,8 +6,4 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="Interfaces\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,7 +0,0 @@
namespace BetterLyrics.Core
{
public class Class1
{
}
}

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 <Identity
Name="37412.BetterLyrics" Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9" Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.2.253.0" /> Version="1.2.256.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <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.LyricsCacheService;
using BetterLyrics.WinUI3.Services.LyricsSearchService; using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService; using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService; using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.SongSearchMapService; using BetterLyrics.WinUI3.Services.SongSearchMapService;
@@ -248,6 +249,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<IPlayHistoryService, PlayHistoryService>() .AddSingleton<IPlayHistoryService, PlayHistoryService>()
.AddSingleton<ILyricsCacheService, LyricsCacheService>() .AddSingleton<ILyricsCacheService, LyricsCacheService>()
.AddSingleton<ISongSearchMapService, SongSearchMapService>() .AddSingleton<ISongSearchMapService, SongSearchMapService>()
.AddSingleton<IPluginService, PluginService>()
// ViewModels // ViewModels
.AddSingleton<AppSettingsControlViewModel>() .AddSingleton<AppSettingsControlViewModel>()

View File

@@ -96,6 +96,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" 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.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.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" /> <PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
@@ -121,6 +125,7 @@
<PackageReference Include="z440.atl.core" Version="7.9.0" /> <PackageReference Include="z440.atl.core" Version="7.9.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\BetterLyrics.Core\BetterLyrics.Core.csproj" />
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" /> <ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" /> <ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
</ItemGroup> </ItemGroup>

View File

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

View File

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

View File

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

View File

@@ -19,9 +19,10 @@ namespace BetterLyrics.WinUI3.Extensions
new LyricsLine new LyricsLine
{ {
StartMs = 0, StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds, EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
PrimaryText = "● ● ●", PrimaryText = "● ● ●",
PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds }], PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds }],
IsPrimaryHasRealSyllableInfo = true,
}, },
], ],
LanguageCode = "N/A", LanguageCode = "N/A",

View File

@@ -68,11 +68,17 @@ namespace BetterLyrics.WinUI3.Logic
for (int i = safeStart; i <= safeEnd; i++) for (int i = safeStart; i <= safeEnd; i++)
{ {
var line = lines[i]; var line = lines[i];
var lineHeight = line.PrimaryLineHeight; var lineHeight = line.PrimaryLineHeight;
if (lineHeight == null || lineHeight <= 0) continue; 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 double targetCharFloat = lyricsEffect.IsLyricsFloatAnimationAmountAutoAdjust
? lineHeight.Value * 0.1 ? lineHeight.Value * 0.1
: lyricsEffect.LyricsFloatAnimationAmount; : lyricsEffect.LyricsFloatAnimationAmount;
@@ -83,7 +89,7 @@ namespace BetterLyrics.WinUI3.Logic
? 1.15 ? 1.15
: lyricsEffect.LyricsScaleEffectAmount / 100.0; : lyricsEffect.LyricsScaleEffectAmount / 100.0;
var maxAnimationDurationMs = Math.Max(line.EndMs - currentPositionMs, 0); var maxAnimationDurationMs = Math.Max(line.EndMs ?? 0 - currentPositionMs, 0);
bool isSecondaryLinePlaying = line.GetIsPlaying(currentPositionMs); bool isSecondaryLinePlaying = line.GetIsPlaying(currentPositionMs);
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying; bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
@@ -176,7 +182,7 @@ namespace BetterLyrics.WinUI3.Logic
line.AngleTransition.SetDelay(yScrollDelay); line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.Start( line.AngleTransition.Start(
(isFanEnabled && !isMouseScrolling) ? (isFanEnabled && !isMouseScrolling) ?
Math.PI * (fanAngleRad / 180.0) * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) : fanAngleRad * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
0); 0);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType); line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
@@ -187,98 +193,100 @@ namespace BetterLyrics.WinUI3.Logic
line.YOffsetTransition.Start(targetYScrollOffset); line.YOffsetTransition.Start(targetYScrollOffset);
} }
if (isLayoutChanged || isSecondaryLinePlayingChanged) if (isWordAnimationEnabled)
{ {
// 辉光动画 if (isSecondaryLinePlayingChanged)
if (isGlowEnabled && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LineStartToCurrentChar
&& isSecondaryLinePlaying)
{ {
foreach (var renderChar in line.PrimaryRenderChars) // 辉光动画
if (isGlowEnabled && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LineStartToCurrentChar
&& isSecondaryLinePlaying)
{ {
var stepInOutDuration = Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs) / 2.0 / 1000.0; foreach (var renderChar in line.PrimaryRenderChars)
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 (isFloatEnabled)
{
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetCharFloat : 0);
}
}
}
// 字符动画
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);
}
renderChar.IsPlayingLastFrame = isCharPlaying;
}
}
// 音节动画
foreach (var syllable in line.PrimaryRenderSyllables)
{
bool isSyllablePlaying = syllable.GetIsPlaying(currentPositionMs);
bool isSyllablePlayingChanged = syllable.IsPlayingLastFrame != isSyllablePlaying;
if (isSyllablePlayingChanged)
{
if (isScaleEnabled && isSyllablePlaying)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{ {
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration) var stepInOutDuration = Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs) / 2.0 / 1000.0;
{ var stepLastingDuration = Math.Max(maxAnimationDurationMs / 1000.0 - stepInOutDuration * 2, 0);
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.ScaleTransition.Start(
new Models.Keyframe<double>(targetCharScale, stepDuration),
new Models.Keyframe<double>(1.0, stepDuration)
);
}
}
}
if (isGlowEnabled && isSyllablePlaying && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LongDurationSyllable
&& syllable.DurationMs >= lyricsEffect.LyricsGlowEffectLongSyllableDuration)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.GlowTransition.Start( renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetCharGlow, stepDuration), new Models.Keyframe<double>(targetCharGlow, stepInOutDuration),
new Models.Keyframe<double>(0, stepDuration) new Models.Keyframe<double>(targetCharGlow, stepLastingDuration),
new Models.Keyframe<double>(0, stepInOutDuration)
); );
} }
} }
syllable.IsPlayingLastFrame = isSyllablePlaying; // 浮动动画
if (isFloatEnabled)
{
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetCharFloat : 0);
}
}
} }
}
// 使动画步进一帧 // 字符动画
foreach (var renderChar in line.PrimaryRenderChars) foreach (var renderChar in line.PrimaryRenderChars)
{ {
renderChar.Update(elapsedTime); 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);
}
renderChar.IsPlayingLastFrame = isCharPlaying;
}
}
// 音节动画
foreach (var syllable in line.PrimaryRenderSyllables)
{
bool isSyllablePlaying = syllable.GetIsPlaying(currentPositionMs);
bool isSyllablePlayingChanged = syllable.IsPlayingLastFrame != isSyllablePlaying;
if (isSyllablePlayingChanged)
{
if (isScaleEnabled && isSyllablePlaying)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.ScaleTransition.Start(
new Models.Keyframe<double>(targetCharScale, stepDuration),
new Models.Keyframe<double>(1.0, stepDuration)
);
}
}
}
if (isGlowEnabled && isSyllablePlaying && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LongDurationSyllable
&& syllable.DurationMs >= lyricsEffect.LyricsGlowEffectLongSyllableDuration)
{
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;
}
}
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.Update(elapsedTime);
}
} }
line.Update(elapsedTime); line.Update(elapsedTime);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,26 +40,14 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
startIndex += text.Length; 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 > 1) if (syllables.Count > 1)
{ {
lrcLines.Add(new LyricsLine lrcLines.Add(new LyricsLine
{ {
StartMs = syllables[0].StartMs, StartMs = syllables[0].StartMs,
EndMs = lineEndMs,
PrimaryText = string.Concat(syllables.Select(s => s.Text)), PrimaryText = string.Concat(syllables.Select(s => s.Text)),
PrimarySyllables = syllables PrimarySyllables = syllables,
IsPrimaryHasRealSyllableInfo = true
}); });
} }
else else
@@ -81,7 +69,13 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
content = bracketRegex!.Replace(line, "").Trim(); content = bracketRegex!.Replace(line, "").Trim();
if (content == "//") content = ""; 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 var lineWrite = new LyricsLine
{ {
StartMs = lineRead.StartTime ?? 0, StartMs = lineRead.StartTime ?? 0,
EndMs = lineRead.EndTime ?? (nextLineRead?.StartTime ?? 0),
PrimaryText = lineRead.Text, PrimaryText = lineRead.Text,
PrimarySyllables = [], IsPrimaryHasRealSyllableInfo = true,
}; };
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables; var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;

View File

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

View File

@@ -69,6 +69,9 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
LoadTransliteration(lyricsSearchResult); LoadTransliteration(lyricsSearchResult);
GenerateTransliterationLyricsData(); GenerateTransliterationLyricsData();
EnsureEndMs(lyricsSearchResult?.Duration);
EnsureSyllables();
return _lyricsDataArr; 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

@@ -1,5 +1,6 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using BetterLyrics.Core.Interfaces;
using BetterLyrics.WinUI3.Constants; using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions; using BetterLyrics.WinUI3.Extensions;
@@ -10,6 +11,7 @@ using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Providers; using BetterLyrics.WinUI3.Providers;
using BetterLyrics.WinUI3.Services.FileSystemService; using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.LyricsCacheService; using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SongSearchMapService; using BetterLyrics.WinUI3.Services.SongSearchMapService;
using Lyricify.Lyrics.Helpers; using Lyricify.Lyrics.Helpers;
@@ -36,6 +38,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private readonly IFileSystemService _fileSystemService; private readonly IFileSystemService _fileSystemService;
private readonly ILyricsCacheService _lyricsCacheService; private readonly ILyricsCacheService _lyricsCacheService;
private readonly ISongSearchMapService _songSearchMapService; private readonly ISongSearchMapService _songSearchMapService;
private readonly IPluginService _pluginService;
private readonly ILogger _logger; private readonly ILogger _logger;
public LyricsSearchService( public LyricsSearchService(
@@ -43,6 +46,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
IFileSystemService fileSystemService, IFileSystemService fileSystemService,
ILyricsCacheService lyricsCacheService, ILyricsCacheService lyricsCacheService,
ISongSearchMapService songSearchMapService, ISongSearchMapService songSearchMapService,
IPluginService pluginService,
ILogger<LyricsSearchService> logger ILogger<LyricsSearchService> logger
) )
{ {
@@ -50,6 +54,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
_fileSystemService = fileSystemService; _fileSystemService = fileSystemService;
_lyricsCacheService = lyricsCacheService; _lyricsCacheService = lyricsCacheService;
_songSearchMapService = songSearchMapService; _songSearchMapService = songSearchMapService;
_pluginService = pluginService;
_logger = logger; _logger = logger;
_lrcLibHttpClient = new(); _lrcLibHttpClient = new();
@@ -207,11 +212,26 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{ {
_logger.LogInformation("SearchAllAsync {SongInfo}", songInfo); _logger.LogInformation("SearchAllAsync {SongInfo}", songInfo);
var results = new List<LyricsCacheItem>(); var results = new List<LyricsCacheItem>();
foreach (var provider in Enum.GetValues<LyricsSearchProvider>()) foreach (var provider in Enum.GetValues<LyricsSearchProvider>())
{ {
if (provider == LyricsSearchProvider.Plugin) continue;
var searchResult = await SearchSingleAsync(songInfo, provider, checkCache, token); var searchResult = await SearchSingleAsync(songInfo, provider, checkCache, token);
results.Add(searchResult); 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; return results;
} }
@@ -673,5 +693,41 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult; 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

@@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18 # Visual Studio Version 18
VisualStudioVersion = 18.1.11312.151 d18.0 VisualStudioVersion = 18.1.11312.151
MinimumVisualStudioVersion = 10.0.40219.1 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}" Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "BetterLyrics.WinUI3 (Package)", "BetterLyrics.WinUI3\BetterLyrics.WinUI3 (Package)\BetterLyrics.WinUI3 (Package).wapproj", "{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}"
EndProject EndProject
@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorThief.WinUI3", "ColorT
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Core", "BetterLyrics.Core\BetterLyrics.Core.csproj", "{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Core", "BetterLyrics.Core\BetterLyrics.Core.csproj", "{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Plugins.Demo", "BetterLyrics.Plugins.Demo\BetterLyrics.Plugins.Demo.csproj", "{87D235CA-4311-4766-8186-AD9B193DFABC}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64 Debug|ARM64 = Debug|ARM64
@@ -89,6 +91,18 @@ Global
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x86.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE