mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 10:54:55 +08:00
Compare commits
14 Commits
v1.2.243.0
...
3947050d6f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3947050d6f | ||
|
|
707d85bc75 | ||
|
|
78bafb8508 | ||
|
|
b4d24c5570 | ||
|
|
83c9f9806d | ||
|
|
adde74afb0 | ||
|
|
67b4d4e409 | ||
|
|
8d7fbe63c5 | ||
|
|
5037b92913 | ||
|
|
c1ee7a6779 | ||
|
|
7ddfd1118b | ||
|
|
97f20decf2 | ||
|
|
81eb4e1c96 | ||
|
|
00ee4a051c |
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="37412.BetterLyrics"
|
||||
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
|
||||
Version="1.2.243.0" />
|
||||
Version="1.2.245.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.GSMTCService;
|
||||
using BetterLyrics.WinUI3.Services.LastFMService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using BetterLyrics.WinUI3.Services.LyricsCacheService;
|
||||
using BetterLyrics.WinUI3.Services.LyricsSearchService;
|
||||
using BetterLyrics.WinUI3.Services.PlayHistoryService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Services.SMTCService;
|
||||
using BetterLyrics.WinUI3.Services.SongSearchMapService;
|
||||
using BetterLyrics.WinUI3.Services.TranslationService;
|
||||
using BetterLyrics.WinUI3.Services.TransliterationService;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
@@ -126,13 +128,25 @@ namespace BetterLyrics.WinUI3
|
||||
|
||||
protected override async void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
// 初始化数据库
|
||||
await EnsureDatabasesAsync();
|
||||
await InitDatabasesAsync();
|
||||
|
||||
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
|
||||
|
||||
// Migrate MappedSongSearchQueries
|
||||
var songSearchMapService = Ioc.Default.GetRequiredService<ISongSearchMapService>();
|
||||
var obsoleteSongSearchMap = settingsService.AppSettings.MappedSongSearchQueries;
|
||||
if (obsoleteSongSearchMap.Count > 0)
|
||||
{
|
||||
foreach (var item in obsoleteSongSearchMap)
|
||||
{
|
||||
await songSearchMapService.SaveMappingAsync(item);
|
||||
}
|
||||
obsoleteSongSearchMap.Clear();
|
||||
}
|
||||
|
||||
// Start scan tasks in background
|
||||
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
|
||||
|
||||
// 开始后台扫描任务
|
||||
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (item.LastSyncTime == null)
|
||||
@@ -142,10 +156,10 @@ namespace BetterLyrics.WinUI3
|
||||
}
|
||||
fileSystemService.StartAllFolderTimers();
|
||||
|
||||
// 初始化托盘
|
||||
// Init system tray
|
||||
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
|
||||
|
||||
// 根据设置打开歌词窗口
|
||||
// Open lyrics window if set
|
||||
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
|
||||
{
|
||||
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
|
||||
@@ -162,97 +176,39 @@ namespace BetterLyrics.WinUI3
|
||||
}
|
||||
}
|
||||
|
||||
// 根据设置自动打开主界面
|
||||
// Open music gallery if set
|
||||
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
|
||||
{
|
||||
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureDatabasesAsync()
|
||||
private async Task InitDatabasesAsync()
|
||||
{
|
||||
// Init databases
|
||||
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
|
||||
var fileCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
|
||||
var songSearchMapFactory = Ioc.Default.GetRequiredService<IDbContextFactory<SongSearchMapDbContext>>();
|
||||
var filesIndexFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
|
||||
var lyricsCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<LyricsCacheDbContext>>();
|
||||
|
||||
await SafeInitDatabaseAsync(
|
||||
"PlayHistory",
|
||||
PathHelper.PlayHistoryPath,
|
||||
async () =>
|
||||
{
|
||||
using var db = await playHistoryFactory.CreateDbContextAsync();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
},
|
||||
isCritical: true
|
||||
);
|
||||
|
||||
await SafeInitDatabaseAsync(
|
||||
"FileCache",
|
||||
PathHelper.FilesIndexPath,
|
||||
async () =>
|
||||
{
|
||||
using var db = await fileCacheFactory.CreateDbContextAsync();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
},
|
||||
isCritical: false
|
||||
);
|
||||
}
|
||||
|
||||
private async Task SafeInitDatabaseAsync(string dbName, string dbPath, Func<Task> initAction, bool isCritical)
|
||||
{
|
||||
try
|
||||
using (var playHistoryDb = await playHistoryFactory.CreateDbContextAsync())
|
||||
{
|
||||
await initAction();
|
||||
await playHistoryDb.Database.EnsureCreatedAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
using (var songSearchMapDb = await songSearchMapFactory.CreateDbContextAsync())
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DB Error] {dbName} init failed: {ex.Message}");
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(dbPath))
|
||||
{
|
||||
// 尝试清理连接池
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
if (isCritical)
|
||||
{
|
||||
var backupPath = dbPath + ".bak_" + DateTime.Now.ToString("yyyyMMddHHmmss");
|
||||
File.Move(dbPath, backupPath, true);
|
||||
await ShowErrorDialogAsync("Database Recovery", $"Database {dbName} is damaged, the old database has been backed up to {backupPath}, and the program will create a new database.");
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Delete(dbPath);
|
||||
}
|
||||
}
|
||||
await initAction();
|
||||
System.Diagnostics.Debug.WriteLine($"[DB Info] {dbName} recovered successfully.");
|
||||
}
|
||||
catch (Exception fatalEx)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[] : {fatalEx.Message}");
|
||||
await ShowErrorDialogAsync("Fatal Error", $"{dbName} recovery failed, please delete the file at {dbPath} and try again by restarting the program. ({fatalEx.Message})");
|
||||
}
|
||||
await songSearchMapDb.Database.EnsureCreatedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowErrorDialogAsync(string title, string content)
|
||||
{
|
||||
// 这里假设 m_window 已经存在。如果没有显示主窗口,这个弹窗可能无法显示。
|
||||
// 在 App 启动极早期的错误,可能需要退化为 Log 或者 System.Diagnostics.Process.Start 打开记事本报错
|
||||
if (m_window != null)
|
||||
using (var filesIndexDb = await filesIndexFactory.CreateDbContextAsync())
|
||||
{
|
||||
m_window.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
|
||||
{
|
||||
Title = title,
|
||||
Content = content,
|
||||
CloseButtonText = "OK",
|
||||
XamlRoot = m_window.Content?.XamlRoot // 确保 Content 不为空
|
||||
};
|
||||
if (dialog.XamlRoot != null) await dialog.ShowAsync();
|
||||
});
|
||||
await filesIndexDb.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
using (var lyricsCacheDb = await lyricsCacheFactory.CreateDbContextAsync())
|
||||
{
|
||||
await lyricsCacheDb.Database.EnsureCreatedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +225,8 @@ namespace BetterLyrics.WinUI3
|
||||
// 数据库工厂
|
||||
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
|
||||
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}"))
|
||||
.AddDbContextFactory<LyricsCacheDbContext>(options => options.UseSqlite($"Data Source={PathHelper.LyricsCachePath}"))
|
||||
.AddDbContextFactory<SongSearchMapDbContext>(options => options.UseSqlite($"Data Source={PathHelper.SongSearchMapPath}"))
|
||||
|
||||
// 日志
|
||||
.AddLogging(loggingBuilder =>
|
||||
@@ -290,6 +248,8 @@ namespace BetterLyrics.WinUI3
|
||||
.AddSingleton<ILocalizationService, LocalizationService>()
|
||||
.AddSingleton<IFileSystemService, FileSystemService>()
|
||||
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
|
||||
.AddSingleton<ILyricsCacheService, LyricsCacheService>()
|
||||
.AddSingleton<ISongSearchMapService, SongSearchMapService>()
|
||||
|
||||
// ViewModels
|
||||
.AddSingleton<AppSettingsControlViewModel>()
|
||||
|
||||
@@ -160,7 +160,8 @@
|
||||
<RichTextBlock>
|
||||
<Paragraph>
|
||||
<Run x:Uid="SetingsPageContributors" />
|
||||
<Run Text="(Code)" />
|
||||
<Run Text="-" />
|
||||
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Code" />
|
||||
</Paragraph>
|
||||
</RichTextBlock>
|
||||
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
|
||||
@@ -178,7 +179,8 @@
|
||||
<RichTextBlock>
|
||||
<Paragraph>
|
||||
<Run x:Uid="SetingsPageContributors" />
|
||||
<Run Text="(Translator)" />
|
||||
<Run Text="-" />
|
||||
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Translator" />
|
||||
</Paragraph>
|
||||
</RichTextBlock>
|
||||
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
|
||||
@@ -193,13 +195,19 @@
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock x:Uid="SettingsPagePatrons" />
|
||||
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
|
||||
<uc:PatronControl Date="Jan 3, 2026" PatronName="**轩" />
|
||||
<uc:PatronControl Date="Dec 13, 2025" PatronName="<Anonymous>" />
|
||||
<uc:PatronControl Date="Dec 3, 2025" PatronName="YE" />
|
||||
<uc:PatronControl Date="Dec 2, 2025" PatronName="<Anonymous>" />
|
||||
<uc:PatronControl Date="Nov 23, 2025" PatronName="**玄" />
|
||||
<uc:PatronControl Date="Nov 21, 2025" PatronName="**智" />
|
||||
<uc:PatronControl Date="Nov 17, 2025" PatronName="*鹤" />
|
||||
<uc:PatronControl Date="Nov 17, 2025" PatronName="SuHeAndZl" />
|
||||
<uc:PatronControl Date="Nov 2, 2025" PatronName="借过" />
|
||||
<uc:PatronControl Date="Aug 28, 2025" PatronName="**华" />
|
||||
<TextBlock x:Uid="SettingsPageUserWhoPurchased" Margin="12,8" />
|
||||
<TextBlock
|
||||
x:Uid="SettingsPageUserWhoPurchased"
|
||||
Margin="12,8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</dev:WrapPanel>
|
||||
</StackPanel>
|
||||
</dev:SettingsCard>
|
||||
@@ -208,7 +216,10 @@
|
||||
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock x:Uid="SetingsPageSpecialThanks" />
|
||||
<TextBlock x:Uid="SettingsPageYouNowUsing" Margin="0,8" />
|
||||
<TextBlock
|
||||
x:Uid="SettingsPageYouNowUsing"
|
||||
Margin="0,8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</dev:SettingsCard>
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.UI;
|
||||
using System.Numerics;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Controls
|
||||
{
|
||||
@@ -398,6 +399,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
strokeColor: _albumArtThemeColors.StrokeFontColor,
|
||||
bgColor: _albumArtThemeColors.BgFontColor,
|
||||
fgColor: _albumArtThemeColors.FgFontColor,
|
||||
currentProgressMs: _songPositionWithOffset.TotalMilliseconds,
|
||||
getPlaybackState: (lineIndex) =>
|
||||
{
|
||||
if (_renderLyricsLines == null) return new LinePlaybackState();
|
||||
@@ -433,19 +435,19 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
//args.DrawingSession.DrawText(
|
||||
// $"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
|
||||
// $"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
|
||||
// $"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
|
||||
// $"Playing line (idx): {_playingLineIndex}\n" +
|
||||
// $"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
|
||||
// $"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
|
||||
// $"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
|
||||
// $"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0)}\n" +
|
||||
// $"Y offset: {_canvasYScrollTransition.Value}\n" +
|
||||
// $"User scroll offset: {_mouseYScrollTransition.Value}",
|
||||
// new Vector2(0, 0), Colors.Red);
|
||||
#if DEBUG && false
|
||||
args.DrawingSession.DrawText(
|
||||
$"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
|
||||
$"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
|
||||
$"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
|
||||
$"Playing line (idx): {_playingLineIndex}\n" +
|
||||
$"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
|
||||
$"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
|
||||
$"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
|
||||
$"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_gsmtcService.CurrentSongInfo.DurationMs)}\n" +
|
||||
$"Y offset: {_canvasYScrollTransition.Value}\n" +
|
||||
$"User scroll offset: {_mouseYScrollTransition.Value}",
|
||||
new Vector2(0, 0), Colors.Red);
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -475,7 +477,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
#region UpdatePlayingLineIndex
|
||||
|
||||
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, lyricsData);
|
||||
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, _renderLyricsLines);
|
||||
bool isPlayingLineChanged = newPlayingIndex != _playingLineIndex;
|
||||
_playingLineIndex = newPlayingIndex;
|
||||
|
||||
@@ -536,7 +538,8 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
_isMouseScrolling,
|
||||
_isLayoutChanged,
|
||||
isPlayingLineChanged,
|
||||
_isMouseScrollingChanged
|
||||
_isMouseScrollingChanged,
|
||||
_songPositionWithOffset.TotalMilliseconds
|
||||
);
|
||||
|
||||
_isMouseScrollingChanged = false;
|
||||
@@ -667,7 +670,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
private void UpdateRenderLyricsLines()
|
||||
{
|
||||
_renderLyricsLines = null;
|
||||
_renderLyricsLines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
|
||||
var lines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
|
||||
{
|
||||
LyricsSyllables = x.LyricsSyllables,
|
||||
StartMs = x.StartMs,
|
||||
@@ -676,6 +679,11 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
OriginalText = x.OriginalText,
|
||||
TranslatedText = x.TranslatedText
|
||||
}).ToList();
|
||||
if (lines != null)
|
||||
{
|
||||
LyricsLayoutManager.CalculateLanes(lines);
|
||||
}
|
||||
_renderLyricsLines = lines;
|
||||
}
|
||||
|
||||
private async Task ReloadCoverBackgroundResourcesAsync()
|
||||
|
||||
@@ -97,19 +97,6 @@
|
||||
Style="{StaticResource GhostButtonStyle}" />
|
||||
</Grid>
|
||||
|
||||
<RichTextBlock
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap">
|
||||
<Paragraph>
|
||||
<Run Text="*" />
|
||||
<Run x:Uid="ArtistsSplitHint" />
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Run Text="; , / ; 、 ," />
|
||||
</Paragraph>
|
||||
</RichTextBlock>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -169,7 +156,7 @@
|
||||
<Grid Grid.Column="1">
|
||||
<ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LyricsSearchResult">
|
||||
<DataTemplate x:DataType="models:LyricsCacheItem">
|
||||
<ListViewItem IsEnabled="{x:Bind IsFound}">
|
||||
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
|
||||
<local:PropertyRow
|
||||
@@ -180,17 +167,13 @@
|
||||
<!-- Lyrics search result -->
|
||||
<StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind Artist, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsPageMatchPercentage"
|
||||
Unit="%"
|
||||
Value="{x:Bind MatchPercentage, Mode=OneWay}" />
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsPageCachePath"
|
||||
Link="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<!-- NOT FOUND -->
|
||||
<TextBlock
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DisplayArtists, Mode=OneWay}" />
|
||||
Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
<Grid Margin="12,8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{x:Bind PatronName, Mode=OneWay}" />
|
||||
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Date, Mode=OneWay}" />
|
||||
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind PatronName, Mode=OneWay}" />
|
||||
<TextBlock Foreground="{ThemeResource TextFillColorTertiaryBrush}" Text="{x:Bind Date, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -326,7 +326,7 @@
|
||||
HorizontalContentAlignment="Left">
|
||||
<StackPanel Spacing="6">
|
||||
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
@@ -339,7 +339,7 @@
|
||||
HorizontalContentAlignment="Left">
|
||||
<StackPanel Spacing="6">
|
||||
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Artist, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
|
||||
@@ -354,10 +354,6 @@
|
||||
x:Uid="LyricsPageMatchPercentage"
|
||||
Unit="%"
|
||||
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsPageCachePath"
|
||||
Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
|
||||
ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}" />
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
|
||||
@@ -8,21 +8,6 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
extension(LyricsSearchProvider provider)
|
||||
{
|
||||
public string GetCacheDirectory() => provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
|
||||
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
|
||||
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
|
||||
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
|
||||
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
|
||||
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
|
||||
LyricsSearchProvider.LocalMusicFile => PathHelper.LocalMusicCacheDirectory,
|
||||
LyricsSearchProvider.LocalLrcFile => PathHelper.LocalLrcCacheDirectory,
|
||||
LyricsSearchProvider.LocalEslrcFile => PathHelper.LocalEslrcCacheDirectory,
|
||||
LyricsSearchProvider.LocalTtmlFile => PathHelper.LocalTtmlCacheDirectory,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(provider)),
|
||||
};
|
||||
|
||||
public LyricsFormat GetLyricsFormat() => provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
@@ -9,7 +12,7 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
Title = "N/A",
|
||||
Album = "N/A",
|
||||
Artists = ["N/A"],
|
||||
Artist = "N/A",
|
||||
};
|
||||
|
||||
extension(SongInfo songInfo)
|
||||
@@ -20,9 +23,9 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
return songInfo;
|
||||
}
|
||||
|
||||
public SongInfo WithArtist(string[] value)
|
||||
public SongInfo WithArtist(string value)
|
||||
{
|
||||
songInfo.Artists = value;
|
||||
songInfo.Artist = value;
|
||||
return songInfo;
|
||||
}
|
||||
|
||||
@@ -39,7 +42,7 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
return new PlayHistoryItem
|
||||
{
|
||||
Title = songInfo.Title,
|
||||
Artist = songInfo.DisplayArtists,
|
||||
Artist = songInfo.Artist,
|
||||
Album = songInfo.Album,
|
||||
PlayerId = songInfo.PlayerId ?? "N/A",
|
||||
TotalDurationMs = songInfo.DurationMs,
|
||||
@@ -47,6 +50,23 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
StartedAt = DateTime.FromBinary(songInfo.StartedAt)
|
||||
};
|
||||
}
|
||||
|
||||
public string GetCacheKey()
|
||||
{
|
||||
string title = songInfo.Title?.Trim() ?? "";
|
||||
string album = songInfo.Album?.Trim() ?? "";
|
||||
|
||||
string artists = songInfo.Artist?.Trim() ?? "";
|
||||
|
||||
long seconds = (long)Math.Round(songInfo.Duration);
|
||||
string durationPart = seconds.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
string rawKey = $"{title}|{artists}|{album}|{durationPart}";
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawKey));
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,22 +53,6 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static LyricsSearchResult? ReadLyricsCache(SongInfo songInfo, LyricsSearchProvider lyricsSearchProvider)
|
||||
{
|
||||
var cacheFilePath = Path.Combine(
|
||||
lyricsSearchProvider.GetCacheDirectory(),
|
||||
SanitizeFileName($"{songInfo.ToFileName()}.json"));
|
||||
|
||||
if (File.Exists(cacheFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(cacheFilePath);
|
||||
var data = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LyricsSearchResult);
|
||||
data?.SelfPath = cacheFilePath;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
|
||||
{
|
||||
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
|
||||
@@ -79,19 +63,9 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void WriteLyricsCache(SongInfo songInfo, LyricsSearchResult lyricsSearchResult)
|
||||
{
|
||||
var cacheFilePath = Path.Combine(
|
||||
lyricsSearchResult.Provider.GetCacheDirectory(),
|
||||
SanitizeFileName($"{songInfo.ToFileName()}.json"));
|
||||
lyricsSearchResult.SelfPath = cacheFilePath;
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(lyricsSearchResult, SourceGenerationContext.Default.LyricsSearchResult);
|
||||
File.WriteAllText(cacheFilePath, json);
|
||||
}
|
||||
|
||||
public static void WriteAlbumArtCache(SongInfo songInfo, byte[] img, string format, string cacheFolderPath)
|
||||
{
|
||||
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.DisplayArtists} - {songInfo.Album}{format}"));
|
||||
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.Artist} - {songInfo.Album}{format}"));
|
||||
File.WriteAllBytes(cacheFilePath, img);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
// JaroWinkler 适合短字符串匹配
|
||||
private static readonly JaroWinkler _algo = new();
|
||||
|
||||
public static int CalculateScore(SongInfo local, LyricsSearchResult remote)
|
||||
public static int CalculateScore(SongInfo local, LyricsCacheItem remote)
|
||||
{
|
||||
if (local == null || remote == null) return 0;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
if (localHasMetadata && remoteHasMetadata)
|
||||
{
|
||||
double titleScore = GetStringSimilarity(local.Title, remote.Title);
|
||||
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists);
|
||||
double artistScore = GetStringSimilarity(local.Artist, remote.Artist);
|
||||
double albumScore = GetStringSimilarity(local.Album, remote.Album);
|
||||
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration);
|
||||
|
||||
@@ -41,12 +41,12 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
else
|
||||
{
|
||||
string? localQuery = localHasMetadata
|
||||
? $"{local.Title} {string.Join(" ", local.Artists ?? [])}"
|
||||
? $"{local.Title} {local.Artist}"
|
||||
: Path.GetFileNameWithoutExtension(local.LinkedFileName);
|
||||
|
||||
string remoteQuery = remoteHasMetadata
|
||||
? $"{remote.Title} {string.Join(" ", remote.Artists ?? [])}"
|
||||
: Path.GetFileNameWithoutExtension(remote.Reference);
|
||||
string? remoteQuery = remoteHasMetadata
|
||||
? $"{remote.Title} {remote.Artist}"
|
||||
: null;
|
||||
|
||||
string fp1 = CreateSortedFingerprint(localQuery);
|
||||
string fp2 = CreateSortedFingerprint(remoteQuery);
|
||||
|
||||
@@ -40,16 +40,6 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
|
||||
|
||||
public static string LyricsCacheDirectory => Path.Combine(CacheFolder, "lyrics");
|
||||
public static string LrcLibLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "lrclib");
|
||||
public static string NeteaseLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "netease");
|
||||
public static string QQLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "qq");
|
||||
public static string KugouLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "kugou");
|
||||
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "amll-ttml-db");
|
||||
public static string AppleMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "apple-music");
|
||||
public static string LocalMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-music");
|
||||
public static string LocalLrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-lrc");
|
||||
public static string LocalEslrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-eslrc");
|
||||
public static string LocalTtmlCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-ttml");
|
||||
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.jsonl");
|
||||
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
|
||||
|
||||
@@ -60,24 +50,14 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u");
|
||||
public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db");
|
||||
public static string FilesIndexPath => Path.Combine(LocalFolder, "files-index.db");
|
||||
public static string SongSearchMapPath => Path.Combine(LocalFolder, "song-search-map.db");
|
||||
public static string LyricsCachePath => Path.Combine(LyricsCacheDirectory, "lyrics-cache.db");
|
||||
|
||||
public static void EnsureDirectories()
|
||||
{
|
||||
Directory.CreateDirectory(SettingsDirectory);
|
||||
|
||||
Directory.CreateDirectory(LogDirectory);
|
||||
|
||||
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
|
||||
Directory.CreateDirectory(QQLyricsCacheDirectory);
|
||||
Directory.CreateDirectory(KugouLyricsCacheDirectory);
|
||||
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
|
||||
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
|
||||
Directory.CreateDirectory(AppleMusicCacheDirectory);
|
||||
Directory.CreateDirectory(LocalMusicCacheDirectory);
|
||||
Directory.CreateDirectory(LocalLrcCacheDirectory);
|
||||
Directory.CreateDirectory(LocalEslrcCacheDirectory);
|
||||
Directory.CreateDirectory(LocalTtmlCacheDirectory);
|
||||
|
||||
Directory.CreateDirectory(LyricsCacheDirectory);
|
||||
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
|
||||
Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
IList<RenderLyricsLine>? lines,
|
||||
int startIndex,
|
||||
int endIndex,
|
||||
int playingLineIndex,
|
||||
int primaryPlayingLineIndex,
|
||||
double canvasHeight,
|
||||
double targetYScrollOffset,
|
||||
double playingLineTopOffsetFactor,
|
||||
@@ -29,13 +29,14 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
TimeSpan elapsedTime,
|
||||
bool isMouseScrolling,
|
||||
bool isLayoutChanged,
|
||||
bool isPlayingLineChanged,
|
||||
bool isMouseScrollingChanged
|
||||
bool isPrimaryPlayingLineChanged,
|
||||
bool isMouseScrollingChanged,
|
||||
double currentProgressMs
|
||||
)
|
||||
{
|
||||
if (lines == null) return;
|
||||
|
||||
var currentPlayingLine = lines.ElementAtOrDefault(playingLineIndex);
|
||||
var currentPlayingLine = lines.ElementAtOrDefault(primaryPlayingLineIndex);
|
||||
if (currentPlayingLine == null) return;
|
||||
|
||||
var phoneticOpacity = lyricsStyle.PhoneticLyricsOpacity / 100.0;
|
||||
@@ -47,13 +48,17 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
var line = lines.ElementAtOrDefault(i);
|
||||
if (line == null) continue;
|
||||
|
||||
if (isLayoutChanged || isPlayingLineChanged || isMouseScrollingChanged)
|
||||
bool isSecondaryLinePlaying = currentProgressMs >= line.StartMs && currentProgressMs <= line.EndMs;
|
||||
if (i == primaryPlayingLineIndex) isSecondaryLinePlaying = true;
|
||||
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
|
||||
line.IsPlayingLastFrame = isSecondaryLinePlaying;
|
||||
|
||||
if (isLayoutChanged || isPrimaryPlayingLineChanged || isMouseScrollingChanged || isSecondaryLinePlayingChanged)
|
||||
{
|
||||
int lineCountDelta = i - playingLineIndex;
|
||||
int absLineCountDelta = Math.Abs(lineCountDelta);
|
||||
int lineCountDelta = i - primaryPlayingLineIndex;
|
||||
double distanceFromPlayingLine = Math.Abs(line.OriginalPosition.Y - currentPlayingLine.OriginalPosition.Y);
|
||||
|
||||
double distanceFactor = 0;
|
||||
double distanceFactor;
|
||||
if (lineCountDelta < 0)
|
||||
{
|
||||
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * playingLineTopOffsetFactor), 0, 1);
|
||||
@@ -88,45 +93,53 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
|
||||
line.BlurAmountTransition.SetDuration(yScrollDuration);
|
||||
line.BlurAmountTransition.SetDelay(yScrollDelay);
|
||||
line.BlurAmountTransition.StartTransition(isMouseScrolling ? 0 : (lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0));
|
||||
line.BlurAmountTransition.StartTransition(
|
||||
(isMouseScrolling || isSecondaryLinePlaying) ? 0 :
|
||||
(lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0));
|
||||
|
||||
line.ScaleTransition.SetDuration(yScrollDuration);
|
||||
line.ScaleTransition.SetDelay(yScrollDelay);
|
||||
line.ScaleTransition.StartTransition(
|
||||
lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
|
||||
isSecondaryLinePlaying ? _highlightedScale :
|
||||
(lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
|
||||
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
|
||||
_highlightedScale);
|
||||
_highlightedScale));
|
||||
|
||||
line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.PhoneticOpacityTransition.StartTransition(
|
||||
isSecondaryLinePlaying ? phoneticOpacity :
|
||||
CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
// 原文不透明度(已播放)
|
||||
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.PlayedOriginalOpacityTransition.StartTransition(
|
||||
isSecondaryLinePlaying ? 1.0 :
|
||||
CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
// 原文不透明度(未播放)
|
||||
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.UnplayedOriginalOpacityTransition.StartTransition(
|
||||
isSecondaryLinePlaying ? originalOpacity :
|
||||
CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.TranslatedOpacityTransition.StartTransition(
|
||||
isSecondaryLinePlaying ? translatedOpacity :
|
||||
CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
line.ColorTransition.SetDuration(yScrollDuration);
|
||||
line.ColorTransition.SetDelay(yScrollDelay);
|
||||
line.ColorTransition.StartTransition(absLineCountDelta == 0 ? fgColor : bgColor);
|
||||
line.ColorTransition.StartTransition(isSecondaryLinePlaying ? fgColor : bgColor);
|
||||
|
||||
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
|
||||
line.AngleTransition.SetDuration(yScrollDuration);
|
||||
line.AngleTransition.SetDelay(yScrollDelay);
|
||||
line.AngleTransition.StartTransition(
|
||||
(lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ?
|
||||
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) :
|
||||
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
|
||||
0);
|
||||
|
||||
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
|
||||
|
||||
@@ -187,6 +187,37 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
return lines.Last().BottomRightPosition.Y;
|
||||
}
|
||||
|
||||
public static void CalculateLanes(IList<RenderLyricsLine>? lines, int toleranceMs = 50)
|
||||
{
|
||||
if (lines == null) return;
|
||||
var lanesEndMs = new List<int> { 0 };
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var start = line.StartMs;
|
||||
var end = line.EndMs;
|
||||
|
||||
int assignedLane = -1;
|
||||
for (int i = 0; i < lanesEndMs.Count; i++)
|
||||
{
|
||||
if (lanesEndMs[i] <= start + toleranceMs)
|
||||
{
|
||||
assignedLane = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (assignedLane == -1)
|
||||
{
|
||||
assignedLane = lanesEndMs.Count;
|
||||
lanesEndMs.Add(0);
|
||||
}
|
||||
|
||||
lanesEndMs[assignedLane] = end ?? 0;
|
||||
line.LaneIndex = assignedLane;
|
||||
}
|
||||
}
|
||||
|
||||
public static int FindMouseHoverLineIndex(
|
||||
IList<RenderLyricsLine>? lines,
|
||||
bool isMouseInLyricsArea,
|
||||
|
||||
@@ -13,30 +13,53 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
_lastFoundIndex = 0;
|
||||
}
|
||||
|
||||
public int GetCurrentLineIndex(double currentTimeMs, LyricsData? lyricsData)
|
||||
public int GetCurrentLineIndex(double currentTimeMs, IList<RenderLyricsLine>? lines)
|
||||
{
|
||||
if (lyricsData == null || lyricsData.LyricsLines.Count == 0) return 0;
|
||||
var lines = lyricsData.LyricsLines;
|
||||
if (lines == null || lines.Count == 0) return 0;
|
||||
|
||||
// Cache hit
|
||||
if (IsTimeInLine(currentTimeMs, lines, _lastFoundIndex)) return _lastFoundIndex;
|
||||
if (_lastFoundIndex + 1 < lines.Count && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex + 1))
|
||||
if (_lastFoundIndex >= 0 && _lastFoundIndex < lines.Count)
|
||||
{
|
||||
_lastFoundIndex++;
|
||||
return _lastFoundIndex;
|
||||
var lastLine = lines[_lastFoundIndex];
|
||||
if (lastLine.LaneIndex == 0 && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex))
|
||||
{
|
||||
return _lastFoundIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss
|
||||
int bestCandidateIndex = -1;
|
||||
int bestCandidateLane = int.MaxValue;
|
||||
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
if (IsTimeInLine(currentTimeMs, lines, i))
|
||||
{
|
||||
_lastFoundIndex = i;
|
||||
return i;
|
||||
var currentLine = lines[i];
|
||||
int currentLane = currentLine.LaneIndex;
|
||||
|
||||
if (currentLane == 0)
|
||||
{
|
||||
_lastFoundIndex = i;
|
||||
return i;
|
||||
}
|
||||
|
||||
if (currentLane < bestCandidateLane)
|
||||
{
|
||||
bestCandidateIndex = i;
|
||||
bestCandidateLane = currentLane;
|
||||
}
|
||||
}
|
||||
else if (lines[i].StartMs > currentTimeMs + 1000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Default
|
||||
if (bestCandidateIndex != -1)
|
||||
{
|
||||
_lastFoundIndex = bestCandidateIndex;
|
||||
return bestCandidateIndex;
|
||||
}
|
||||
|
||||
return Math.Min(_lastFoundIndex, lines.Count - 1);
|
||||
}
|
||||
|
||||
@@ -140,7 +163,7 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
return state;
|
||||
}
|
||||
|
||||
private bool IsTimeInLine(double time, IList<LyricsLine> lines, int index)
|
||||
private bool IsTimeInLine(double time, IList<RenderLyricsLine> lines, int index)
|
||||
{
|
||||
if (index < 0 || index >= lines.Count) return false;
|
||||
var line = lines[index];
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.Db
|
||||
{
|
||||
public partial class LyricsCacheDbContext : DbContext
|
||||
{
|
||||
public LyricsCacheDbContext(DbContextOptions<LyricsCacheDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<LyricsCacheItem> LyricsCache { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.Db
|
||||
{
|
||||
public partial class SongSearchMapDbContext : DbContext
|
||||
{
|
||||
public DbSet<MappedSongSearchQuery> SongSearchMap { get; set; }
|
||||
|
||||
public SongSearchMapDbContext(DbContextOptions<SongSearchMapDbContext> options) : base(options) { }
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
this.Uri = entity.Uri;
|
||||
|
||||
this.Title = entity.Title;
|
||||
this.Artist = entity.Artists;
|
||||
this.Artist = entity.Artist;
|
||||
this.Album = entity.Album;
|
||||
this.Year = entity.Year;
|
||||
this.Bitrate = entity.Bitrate;
|
||||
|
||||
@@ -10,25 +10,19 @@ namespace BetterLyrics.WinUI3.Models
|
||||
[Index(nameof(Uri), IsUnique = true)] // 唯一索引
|
||||
public class FilesIndexItem
|
||||
{
|
||||
[Key] // 主键
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 明确指定为自增 (Identity)
|
||||
public int Id { get; set; }
|
||||
[Key][DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
|
||||
|
||||
// 关联到 MediaFolder.Id
|
||||
// 注意:作为索引列,必须限制长度,否则 SQL Server 会报错 (索引最大900字节)
|
||||
[MaxLength(450)]
|
||||
public string MediaFolderId { get; set; }
|
||||
[MaxLength(450)] public string MediaFolderId { get; set; }
|
||||
|
||||
// 存储父文件夹的标准 URI
|
||||
// 允许为空
|
||||
[MaxLength(450)]
|
||||
public string? ParentUri { get; set; }
|
||||
[MaxLength(450)] public string? ParentUri { get; set; }
|
||||
|
||||
// 唯一索引列
|
||||
// 必须限制长度。450字符 * 2字节/字符 = 900字节 (正好卡在 SQL Server 限制内)
|
||||
[Required]
|
||||
[MaxLength(450)]
|
||||
public string Uri { get; set; }
|
||||
[Required][MaxLength(450)] public string Uri { get; set; }
|
||||
|
||||
public string FileName { get; set; } = "";
|
||||
|
||||
@@ -41,7 +35,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
// 下面的元数据字段通常不需要索引,可以使用 MaxLength 稍微优化空间,
|
||||
// 或者直接留空(默认为 nvarchar(max))
|
||||
public string Title { get; set; } = "";
|
||||
public string Artists { get; set; } = "";
|
||||
[Column("Artists")] public string Artist { get; set; } = "";
|
||||
public string Album { get; set; } = "";
|
||||
public int? Year { get; set; }
|
||||
public int Bitrate { get; set; }
|
||||
@@ -49,11 +43,9 @@ namespace BetterLyrics.WinUI3.Models
|
||||
public int BitDepth { get; set; }
|
||||
public int Duration { get; set; }
|
||||
|
||||
[MaxLength(50)] // 格式名称通常很短,限制一下是个好习惯
|
||||
public string AudioFormatName { get; set; } = "";
|
||||
[MaxLength(50)] public string AudioFormatName { get; set; } = "";
|
||||
|
||||
[MaxLength(20)]
|
||||
public string AudioFormatShortName { get; set; } = "";
|
||||
[MaxLength(20)] public string AudioFormatShortName { get; set; } = "";
|
||||
|
||||
public string Encoder { get; set; } = "";
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NTextCat.Commons;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public partial class LyricsSearchResult : ObservableObject, ICloneable
|
||||
[Table("LyricsCache")]
|
||||
// 建立联合索引,确保同一个 Provider 下,同一个 Hash 只有一条记录
|
||||
[Index(nameof(CacheKey), nameof(Provider), IsUnique = false)]
|
||||
public partial class LyricsCacheItem : ObservableObject, ICloneable
|
||||
{
|
||||
[Key][DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
|
||||
|
||||
[MaxLength(64)][Required] public string CacheKey { get; set; }
|
||||
|
||||
public LyricsSearchProvider Provider { get; set; }
|
||||
[ObservableProperty] public partial TranslationSearchProvider? TranslationProvider { get; set; }
|
||||
[ObservableProperty] public partial TransliterationSearchProvider? TransliterationProvider { get; set; }
|
||||
@@ -25,33 +35,38 @@ namespace BetterLyrics.WinUI3.Models
|
||||
/// </summary>
|
||||
public string? Transliteration { get; set; }
|
||||
|
||||
[MaxLength(255)]
|
||||
public string? Title { get; set; }
|
||||
public string[]? Artists { get; set; }
|
||||
[MaxLength(255)]
|
||||
public string? Artist { get; set; }
|
||||
[MaxLength(255)]
|
||||
public string? Album { get; set; }
|
||||
public double? Duration { get; set; }
|
||||
[ObservableProperty] public partial int MatchPercentage { get; set; } = -1;
|
||||
[ObservableProperty] public partial string Reference { get; set; } = "about:blank";
|
||||
|
||||
public string? SelfPath { get; set; }
|
||||
[NotMapped][JsonIgnore] public bool IsFound => !string.IsNullOrEmpty(Raw);
|
||||
|
||||
[JsonIgnore] public bool IsFound => !string.IsNullOrEmpty(Raw);
|
||||
|
||||
[JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
|
||||
|
||||
[JsonIgnore] public string? DisplayArtists => Artists?.Join("; ");
|
||||
[NotMapped][JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
return new LyricsSearchResult()
|
||||
return new LyricsCacheItem()
|
||||
{
|
||||
Album = this.Album,
|
||||
Duration = this.Duration,
|
||||
Provider = this.Provider,
|
||||
TranslationProvider = this.TranslationProvider,
|
||||
TransliterationProvider = this.TransliterationProvider,
|
||||
|
||||
Raw = this.Raw,
|
||||
Translation = this.Translation,
|
||||
Transliteration = this.Transliteration,
|
||||
|
||||
Title = this.Title,
|
||||
Artists = this.Artists,
|
||||
Artist = this.Artist,
|
||||
Album = this.Album,
|
||||
Duration = this.Duration,
|
||||
|
||||
MatchPercentage = this.MatchPercentage,
|
||||
Provider = this.Provider,
|
||||
Reference = this.Reference
|
||||
};
|
||||
}
|
||||
@@ -59,7 +74,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
public void CopyFromSongInfo(SongInfo songInfo)
|
||||
{
|
||||
Title = songInfo.Title;
|
||||
Artists = songInfo.Artists;
|
||||
Artist = songInfo.Artist;
|
||||
Album = songInfo.Album;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public partial class MappedSongSearchQuery : ObservableRecipient
|
||||
[Table("SongSearchMap")]
|
||||
[Index(nameof(OriginalTitle), nameof(OriginalArtist), nameof(OriginalAlbum))]
|
||||
public partial class MappedSongSearchQuery : ObservableRecipient, ICloneable
|
||||
{
|
||||
[Key][DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; }
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalTitle { get; set; } = string.Empty;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalArtist { get; set; } = string.Empty;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalAlbum { get; set; } = string.Empty;
|
||||
@@ -17,7 +25,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchProvider? LyricsSearchProvider { get; set; }
|
||||
|
||||
public MappedSongSearchQuery Clone()
|
||||
public object Clone()
|
||||
{
|
||||
return new MappedSongSearchQuery
|
||||
{
|
||||
|
||||
@@ -57,6 +57,16 @@ namespace BetterLyrics.WinUI3.Models
|
||||
public CanvasGeometry? TranslatedCanvasGeometry { get; private set; }
|
||||
public CanvasGeometry? PhoneticCanvasGeometry { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 轨道索引 (0 = 主轨道, 1 = 第一副轨道, etc.)
|
||||
/// 用于布局计算时的堆叠逻辑
|
||||
/// </summary>
|
||||
public int LaneIndex { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// 是否为背景人声/和声
|
||||
/// </summary>
|
||||
public bool IsPlayingLastFrame { get; set; } = false;
|
||||
|
||||
public RenderLyricsLine()
|
||||
{
|
||||
AngleTransition = new(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using BetterLyrics.WinUI3.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.Settings
|
||||
{
|
||||
@@ -14,7 +15,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaFolder> LocalMediaFolders { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
|
||||
[Obsolete][ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsWindowStatus> WindowBoundsRecords { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<SongsTabInfo> StarredPlaylists { get; set; } = [];
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
public partial string Album { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string[] Artists { get; set; }
|
||||
public partial string Artist { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double DurationMs { get; set; }
|
||||
@@ -32,8 +32,6 @@ namespace BetterLyrics.WinUI3.Models
|
||||
|
||||
public double Duration => DurationMs / 1000;
|
||||
|
||||
public string DisplayArtists => Artists.Join(ATL.Settings.DisplayValueSeparator.ToString());
|
||||
|
||||
public SongInfo() { }
|
||||
|
||||
public object Clone()
|
||||
@@ -41,7 +39,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
return new SongInfo()
|
||||
{
|
||||
Title = this.Title,
|
||||
Artists = this.Artists,
|
||||
Artist = this.Artist,
|
||||
Album = this.Album,
|
||||
DurationMs = this.DurationMs,
|
||||
PlayerId = this.PlayerId,
|
||||
@@ -55,7 +53,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
return
|
||||
$"Title: {Title}, " +
|
||||
$"Artist: {DisplayArtists}, " +
|
||||
$"Artist: {Artist}, " +
|
||||
$"Album: {Album}, " +
|
||||
$"Duration: {Duration} sec, " +
|
||||
$"Plauer ID: {PlayerId}, " +
|
||||
@@ -65,7 +63,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
|
||||
public string ToFileName()
|
||||
{
|
||||
return $"{DisplayArtists} - {Title} - {Album} - {Duration}";
|
||||
return $"{Artist} - {Title} - {Album} - {Duration}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,24 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
startIndex += text.Length;
|
||||
}
|
||||
|
||||
if (syllables.Count > 1)
|
||||
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)
|
||||
{
|
||||
lrcLines.Add(new LyricsLine
|
||||
{
|
||||
StartMs = syllables[0].StartMs,
|
||||
EndMs = lineEndMs,
|
||||
OriginalText = string.Concat(syllables.Select(s => s.Text)),
|
||||
LyricsSyllables = syllables
|
||||
});
|
||||
@@ -56,17 +69,19 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
var bracketMatches = bracketRegex.Matches(line);
|
||||
|
||||
string content = line;
|
||||
int? lineStartTime = null;
|
||||
int lineStartMs;
|
||||
if (bracketMatches.Count > 0)
|
||||
{
|
||||
var match = bracketMatches[0];
|
||||
int min = int.Parse(match.Groups[1].Value);
|
||||
int sec = int.Parse(match.Groups[2].Value);
|
||||
int ms = int.Parse(match.Groups[4].Value.PadRight(3, '0'));
|
||||
lineStartTime = min * 60_000 + sec * 1000 + ms;
|
||||
lineStartMs = min * 60_000 + sec * 1000 + ms;
|
||||
|
||||
content = bracketRegex!.Replace(line, "").Trim();
|
||||
if (content == "//") content = "";
|
||||
lrcLines.Add(new LyricsLine { StartMs = lineStartTime.Value, OriginalText = content });
|
||||
|
||||
lrcLines.Add(new LyricsLine { StartMs = lineStartMs, OriginalText = content });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
{
|
||||
public partial class LyricsParser
|
||||
{
|
||||
private readonly XNamespace _ttml = "http://www.w3.org/ns/ttml#metadata";
|
||||
|
||||
private void ParseTtml(string raw)
|
||||
{
|
||||
try
|
||||
@@ -19,120 +22,146 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
|
||||
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
|
||||
if (body == null) return;
|
||||
|
||||
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
|
||||
|
||||
foreach (var p in ps)
|
||||
{
|
||||
// 句级时间
|
||||
string? pBegin = p.Attribute("begin")?.Value;
|
||||
string? pEnd = p.Attribute("end")?.Value;
|
||||
int pStartMs = ParseTtmlTime(pBegin);
|
||||
int pEndMs = ParseTtmlTime(pEnd);
|
||||
ParseTtmlSegment(
|
||||
container: p,
|
||||
originalDest: originalLines,
|
||||
transDest: translationLines,
|
||||
romanDest: romanLines,
|
||||
isBackground: false
|
||||
);
|
||||
|
||||
// 只获取一级span
|
||||
var spans = p.Elements()
|
||||
.Where(s => s.Name.LocalName == "span")
|
||||
.ToList();
|
||||
var bgSpans = p.Elements().Where(s => s.Attribute(_ttml + "role")?.Value == "x-bg");
|
||||
|
||||
var originalTextSpans = spans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == null)
|
||||
.ToList();
|
||||
|
||||
// 处理原文span后的空白
|
||||
for (int i = 0; i < originalTextSpans.Count; i++)
|
||||
foreach (var bgSpan in bgSpans)
|
||||
{
|
||||
var span = originalTextSpans[i];
|
||||
var nextNode = span.NodesAfterSelf().FirstOrDefault();
|
||||
if (nextNode is XText textNode)
|
||||
{
|
||||
span.Value += textNode.Value;
|
||||
}
|
||||
// 把 span 当作一个容器,再调一次通用解析方法
|
||||
ParseTtmlSegment(
|
||||
container: bgSpan,
|
||||
originalDest: originalLines,
|
||||
transDest: translationLines,
|
||||
romanDest: romanLines,
|
||||
isBackground: true
|
||||
);
|
||||
}
|
||||
// 拼接空白字符后的原文
|
||||
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
|
||||
|
||||
var originalCharTimings = new List<LyricsSyllable>();
|
||||
int originalStartIndex = 0;
|
||||
foreach (var span in originalTextSpans)
|
||||
{
|
||||
string? sBegin = span.Attribute("begin")?.Value;
|
||||
string? sEnd = span.Attribute("end")?.Value;
|
||||
int sStartMs = ParseTtmlTime(sBegin);
|
||||
int sEndMs = ParseTtmlTime(sEnd);
|
||||
originalCharTimings.Add(new LyricsSyllable
|
||||
{
|
||||
StartMs = sStartMs,
|
||||
EndMs = sEndMs,
|
||||
StartIndex = originalStartIndex,
|
||||
Text = span.Value
|
||||
});
|
||||
originalStartIndex += span.Value.Length;
|
||||
}
|
||||
if (originalTextSpans.Count == 0)
|
||||
{
|
||||
originalText = p.Value;
|
||||
}
|
||||
|
||||
originalLines.Add(new LyricsLine
|
||||
{
|
||||
StartMs = pStartMs,
|
||||
EndMs = pEndMs,
|
||||
OriginalText = originalText,
|
||||
LyricsSyllables = originalCharTimings,
|
||||
});
|
||||
|
||||
// 解析 x-role
|
||||
ParseTtmlXRole(spans, translationLines, "x-translation", pStartMs, pEndMs);
|
||||
ParseTtmlXRole(spans, romanLines, "x-roman", pStartMs, pEndMs);
|
||||
}
|
||||
|
||||
_lyricsDataArr.Add(new LyricsData(originalLines));
|
||||
|
||||
if (translationLines.Count > 0)
|
||||
{
|
||||
_lyricsDataArr.Add(new LyricsData(translationLines));
|
||||
}
|
||||
|
||||
if (romanLines.Count > 0)
|
||||
{
|
||||
_lyricsDataArr.Add(new LyricsData(romanLines) { LanguageCode = PhoneticHelper.RomanCode });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 解析失败,忽略
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void ParseTtmlXRole(List<XElement> sourceSpans, List<LyricsLine> saveLyricsLines, string xRole, int pStartMs, int? pEndMs)
|
||||
private void ParseTtmlSegment(
|
||||
XElement container,
|
||||
List<LyricsLine> originalDest,
|
||||
List<LyricsLine> transDest,
|
||||
List<LyricsLine> romanDest,
|
||||
bool isBackground)
|
||||
{
|
||||
var textSpans = sourceSpans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == xRole)
|
||||
int containerStartMs = ParseTtmlTime(container.Attribute("begin")?.Value);
|
||||
int containerEndMs = ParseTtmlTime(container.Attribute("end")?.Value);
|
||||
|
||||
var contentSpans = container.Elements()
|
||||
.Where(s => s.Name.LocalName == "span")
|
||||
.Where(s =>
|
||||
{
|
||||
var role = s.Attribute(_ttml + "role")?.Value;
|
||||
return role == null;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
string text = string.Concat(textSpans.Select(s => s.Value));
|
||||
var charTimings = new List<LyricsSyllable>();
|
||||
int startIndex = 0;
|
||||
foreach (var span in textSpans)
|
||||
for (int i = 0; i < contentSpans.Count; i++)
|
||||
{
|
||||
string? sBegin = span.Attribute("begin")?.Value;
|
||||
string? sEnd = span.Attribute("end")?.Value;
|
||||
int sStartMs = ParseTtmlTime(sBegin);
|
||||
int sEndMs = ParseTtmlTime(sEnd);
|
||||
charTimings.Add(new LyricsSyllable
|
||||
var span = contentSpans[i];
|
||||
var nextNode = span.NodesAfterSelf().FirstOrDefault();
|
||||
if (nextNode is XText textNode)
|
||||
{
|
||||
span.Value += textNode.Value;
|
||||
}
|
||||
}
|
||||
|
||||
var syllables = new List<LyricsSyllable>();
|
||||
int startIndex = 0;
|
||||
var sbText = new System.Text.StringBuilder();
|
||||
|
||||
foreach (var span in contentSpans)
|
||||
{
|
||||
int sStartMs = ParseTtmlTime(span.Attribute("begin")?.Value);
|
||||
int sEndMs = ParseTtmlTime(span.Attribute("end")?.Value);
|
||||
string text = span.Value;
|
||||
|
||||
syllables.Add(new LyricsSyllable
|
||||
{
|
||||
StartMs = sStartMs,
|
||||
EndMs = sEndMs,
|
||||
StartIndex = startIndex,
|
||||
Text = span.Value
|
||||
Text = text
|
||||
});
|
||||
startIndex += span.Value.Length;
|
||||
|
||||
sbText.Append(text);
|
||||
startIndex += text.Length;
|
||||
}
|
||||
if (textSpans.Count > 0)
|
||||
|
||||
string fullOriginalText = sbText.ToString();
|
||||
|
||||
if (contentSpans.Count == 0)
|
||||
{
|
||||
saveLyricsLines.Add(new LyricsLine
|
||||
fullOriginalText = container.Value;
|
||||
}
|
||||
|
||||
originalDest.Add(new LyricsLine
|
||||
{
|
||||
StartMs = containerStartMs,
|
||||
EndMs = containerEndMs,
|
||||
OriginalText = fullOriginalText,
|
||||
LyricsSyllables = syllables
|
||||
});
|
||||
|
||||
var transSpan = container.Elements()
|
||||
.FirstOrDefault(s => s.Attribute(_ttml + "role")?.Value == "x-translation");
|
||||
|
||||
AddAuxiliaryLine(transDest, transSpan, containerStartMs, containerEndMs);
|
||||
|
||||
var romanSpan = container.Elements()
|
||||
.FirstOrDefault(s => s.Attribute(_ttml + "role")?.Value == "x-roman");
|
||||
|
||||
AddAuxiliaryLine(romanDest, romanSpan, containerStartMs, containerEndMs);
|
||||
}
|
||||
|
||||
private void AddAuxiliaryLine(List<LyricsLine> destList, XElement? span, int startMs, int endMs)
|
||||
{
|
||||
if (span != null)
|
||||
{
|
||||
string text = span.Value;
|
||||
|
||||
destList.Add(new LyricsLine
|
||||
{
|
||||
StartMs = pStartMs,
|
||||
EndMs = pEndMs,
|
||||
OriginalText = text,
|
||||
LyricsSyllables = charTimings,
|
||||
StartMs = startMs,
|
||||
EndMs = endMs,
|
||||
OriginalText = text
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
destList.Add(new LyricsLine
|
||||
{
|
||||
StartMs = startMs,
|
||||
EndMs = endMs,
|
||||
OriginalText = ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
{
|
||||
}
|
||||
|
||||
public List<LyricsData> Parse(LyricsSearchResult? lyricsSearchResult)
|
||||
public List<LyricsData> Parse(LyricsCacheItem? lyricsSearchResult)
|
||||
{
|
||||
_logger.LogInformation("LyricsParser.Parse");
|
||||
_lyricsDataArr = [];
|
||||
@@ -75,7 +75,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
ITranslationService translationService,
|
||||
ITransliterationService transliterationService,
|
||||
TranslationSettings settings,
|
||||
LyricsSearchResult? lyricsSearchResult,
|
||||
LyricsCacheItem? lyricsSearchResult,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
@@ -180,7 +180,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
return (main, transliterationSearchProvider, translationSearchProvider);
|
||||
}
|
||||
|
||||
private void LoadTranslation(LyricsSearchResult? lyricsSearchResult)
|
||||
private void LoadTranslation(LyricsCacheItem? lyricsSearchResult)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Translation))
|
||||
{
|
||||
@@ -197,7 +197,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadTransliteration(LyricsSearchResult? lyricsSearchResult)
|
||||
private void LoadTransliteration(LyricsCacheItem? lyricsSearchResult)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Transliteration))
|
||||
{
|
||||
|
||||
@@ -111,14 +111,14 @@ namespace BetterLyrics.WinUI3.Providers
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<LyricsSearchResult> SearchSongInfoAsync(Models.SongInfo songInfo)
|
||||
public async Task<LyricsCacheItem> SearchSongInfoAsync(Models.SongInfo songInfo)
|
||||
{
|
||||
LyricsSearchResult lyricsSearchResult = new()
|
||||
LyricsCacheItem lyricsSearchResult = new()
|
||||
{
|
||||
Provider = Enums.LyricsSearchProvider.AppleMusic
|
||||
};
|
||||
|
||||
var query = $"{songInfo.DisplayArtists} {songInfo.Title}";
|
||||
var query = $"{songInfo.Artist} {songInfo.Title}";
|
||||
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/search";
|
||||
var url = apiUrl + $"?term={WebUtility.UrlEncode(query)}&types=songs&limit=1&l={_language}";
|
||||
var resp = await _client.GetStringAsync(url);
|
||||
@@ -133,7 +133,7 @@ namespace BetterLyrics.WinUI3.Providers
|
||||
var attr = song.GetProperty("attributes");
|
||||
|
||||
lyricsSearchResult.Title = attr.GetProperty("name").ToString();
|
||||
lyricsSearchResult.Artists = attr.GetProperty("artistName").ToString().SplitByCommonSplitter();
|
||||
lyricsSearchResult.Artist = attr.GetProperty("artistName").ToString();
|
||||
lyricsSearchResult.Album = attr.GetProperty("albumName").ToString();
|
||||
lyricsSearchResult.Duration = attr.GetProperty("durationInMillis").GetInt32() / 1000.0;
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
Color strokeColor,
|
||||
Color bgColor,
|
||||
Color fgColor,
|
||||
double currentProgressMs,
|
||||
Func<int, LinePlaybackState> getPlaybackState)
|
||||
{
|
||||
using (var opacityLayer = ds.CreateLayer((float)lyricsOpacity))
|
||||
@@ -70,6 +71,7 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
strokeColor,
|
||||
bgColor,
|
||||
fgColor,
|
||||
currentProgressMs,
|
||||
getPlaybackState);
|
||||
}
|
||||
|
||||
@@ -101,6 +103,7 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
strokeColor,
|
||||
bgColor,
|
||||
fgColor,
|
||||
currentProgressMs,
|
||||
getPlaybackState);
|
||||
}
|
||||
}
|
||||
@@ -125,6 +128,7 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
Color strokeColor,
|
||||
Color bgColor,
|
||||
Color fgColor,
|
||||
double currentProgressMs,
|
||||
Func<int, LinePlaybackState> getPlaybackState)
|
||||
{
|
||||
if (lines == null) return;
|
||||
@@ -162,10 +166,12 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
|
||||
using (var textOnlyLayer = RenderBaseTextLayer(control, line, styleSettings.LyricsFontStrokeWidth, strokeColor, line.ColorTransition.Value))
|
||||
{
|
||||
if (i == playingLineIndex)
|
||||
bool isPlaying = currentProgressMs >= line.StartMs && currentProgressMs <= line.EndMs;
|
||||
if (i == playingLineIndex) isPlaying = true;
|
||||
|
||||
if (isPlaying)
|
||||
{
|
||||
var state = getPlaybackState(i);
|
||||
|
||||
_playingRenderer.Draw(control, ds, textOnlyLayer, line, state, bgColor, fgColor, effectSettings);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
|
||||
@@ -28,15 +29,7 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
|
||||
if (line.OriginalCanvasTextLayout != null)
|
||||
{
|
||||
double opacity;
|
||||
if (line.PlayedOriginalOpacityTransition.StartValue > line.UnplayedOriginalOpacityTransition.StartValue)
|
||||
{
|
||||
opacity = line.PlayedOriginalOpacityTransition.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
opacity = line.UnplayedOriginalOpacityTransition.Value;
|
||||
}
|
||||
double opacity = Math.Max(line.PlayedOriginalOpacityTransition.Value, line.UnplayedOriginalOpacityTransition.Value);
|
||||
DrawPart(ds, textOnlyLayer,
|
||||
line.OriginalCanvasTextLayout,
|
||||
line.OriginalPosition,
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Serialization
|
||||
[JsonSerializable(typeof(CutletDockerResponse))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(AppSettings))]
|
||||
[JsonSerializable(typeof(LyricsSearchResult))]
|
||||
[JsonSerializable(typeof(LyricsCacheItem))]
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
internal partial class SourceGenerationContext : JsonSerializerContext { }
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
var ext = Path.GetExtension(item.FileName).ToLower();
|
||||
if (!FileHelper.MusicExtensions.Contains(ext)) continue;
|
||||
|
||||
bool isMetadataMatch = (item.Title == songInfo.Title && item.Artists == songInfo.DisplayArtists);
|
||||
bool isMetadataMatch = (item.Title == songInfo.Title && item.Artist == songInfo.Artist);
|
||||
|
||||
bool isFilenameMatch = StringHelper.IsSwitchableNormalizedMatch(
|
||||
Path.GetFileNameWithoutExtension(item.FileName),
|
||||
songInfo.DisplayArtists,
|
||||
songInfo.Artist,
|
||||
songInfo.Title
|
||||
);
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
try
|
||||
{
|
||||
string format = ".jpg";
|
||||
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(songInfo.DisplayArtists, songInfo.Album, format, PathHelper.iTunesAlbumArtCacheDirectory);
|
||||
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(songInfo.Artist, songInfo.Album, format, PathHelper.iTunesAlbumArtCacheDirectory);
|
||||
|
||||
if (cachedAlbumArt != null)
|
||||
{
|
||||
@@ -146,7 +146,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
}
|
||||
|
||||
// Build the iTunes API URL
|
||||
string url = $"{Constants.iTunes.QueryPrefix}term=" + WebUtility.UrlEncode($"{songInfo.Artists} {songInfo.Album}").Replace("%20", "+") + "&country=" + countryCode + "&entity=album&media=music&limit=1";
|
||||
string url = $"{Constants.iTunes.QueryPrefix}term=" + WebUtility.UrlEncode($"{songInfo.Artist} {songInfo.Album}").Replace("%20", "+") + "&country=" + countryCode + "&entity=album&media=music&limit=1";
|
||||
|
||||
// Make a request to the API
|
||||
using HttpResponseMessage response = await _iTunesHttpClinet.GetAsync(url);
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace BetterLyrics.WinUI3.Services.DiscordService
|
||||
SmallImageKey = "logo"
|
||||
},
|
||||
Details = songInfo.Title,
|
||||
State = songInfo.DisplayArtists,
|
||||
State = songInfo.Artist,
|
||||
Timestamps = Timestamps.FromTimeSpan(songInfo.Duration)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
|
||||
.Where(x => x.Id == entity.Id) // 优先用 Id
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(p => p.Title, entity.Title)
|
||||
.SetProperty(p => p.Artists, entity.Artists)
|
||||
.SetProperty(p => p.Artist, entity.Artist)
|
||||
.SetProperty(p => p.Album, entity.Album)
|
||||
.SetProperty(p => p.Year, entity.Year)
|
||||
.SetProperty(p => p.Bitrate, entity.Bitrate)
|
||||
@@ -385,7 +385,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
|
||||
string? artPath = await SaveAlbumArtToDiskAsync(track);
|
||||
|
||||
item.Title = track.Title;
|
||||
item.Artists = track.Artist;
|
||||
item.Artist = track.Artist;
|
||||
item.Album = track.Album;
|
||||
item.Year = track.Year;
|
||||
item.Bitrate = track.Bitrate;
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsData? CurrentLyricsData { get; private set; }
|
||||
|
||||
[ObservableProperty] public partial LyricsSearchResult? CurrentLyricsSearchResult { get; private set; }
|
||||
[ObservableProperty] public partial LyricsCacheItem? CurrentLyricsSearchResult { get; private set; }
|
||||
|
||||
private async Task RefreshLyricsAsync(CancellationToken token)
|
||||
{
|
||||
|
||||
@@ -110,9 +110,6 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
|
||||
|
||||
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
|
||||
|
||||
_settingsService.AppSettings.MappedSongSearchQueries.CollectionChanged += MappedSongSearchQueries_CollectionChanged;
|
||||
_settingsService.AppSettings.MappedSongSearchQueries.ItemPropertyChanged += MappedSongSearchQueries_ItemPropertyChanged;
|
||||
|
||||
InitMediaManager();
|
||||
}
|
||||
|
||||
@@ -151,16 +148,6 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
|
||||
}
|
||||
}
|
||||
|
||||
private void MappedSongSearchQueries_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
|
||||
{
|
||||
UpdateLyrics();
|
||||
}
|
||||
|
||||
private void MappedSongSearchQueries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateLyrics();
|
||||
}
|
||||
|
||||
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateAlbumArt();
|
||||
@@ -344,7 +331,7 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
|
||||
CurrentSongInfo = new()
|
||||
{
|
||||
Title = fixedTitle,
|
||||
Artists = fixedArtist.SplitByCommonSplitter(),
|
||||
Artist = fixedArtist,
|
||||
Album = fixedAlbum,
|
||||
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
|
||||
PlayerId = sessionId,
|
||||
|
||||
@@ -40,6 +40,6 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
|
||||
|
||||
AlbumArtThemeColors CalculateAlbumArtThemeColors(LyricsWindowStatus lyricsWindowStatus, Color backdropAccentColor);
|
||||
|
||||
LyricsSearchResult? CurrentLyricsSearchResult { get; }
|
||||
LyricsCacheItem? CurrentLyricsSearchResult { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
|
||||
await _client.Track.ScrobbleAsync(new Hqub.Lastfm.Entities.Scrobble
|
||||
{
|
||||
Track = songInfo.Title,
|
||||
Artist = songInfo.DisplayArtists,
|
||||
Artist = songInfo.Artist,
|
||||
Date = DateTime.Now,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.LyricsCacheService
|
||||
{
|
||||
public interface ILyricsCacheService
|
||||
{
|
||||
Task<LyricsCacheItem?> GetLyricsAsync(SongInfo songInfo, LyricsSearchProvider provider);
|
||||
Task SaveLyricsAsync(SongInfo songInfo, LyricsCacheItem result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Db;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.LyricsCacheService
|
||||
{
|
||||
public class LyricsCacheService : ILyricsCacheService
|
||||
{
|
||||
private readonly IDbContextFactory<LyricsCacheDbContext> _contextFactory;
|
||||
|
||||
public LyricsCacheService(IDbContextFactory<LyricsCacheDbContext> contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read cache from DB
|
||||
/// </summary>
|
||||
public async Task<LyricsCacheItem?> GetLyricsAsync(SongInfo songInfo, LyricsSearchProvider provider)
|
||||
{
|
||||
using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
string key = songInfo.GetCacheKey();
|
||||
|
||||
var existingItem = await context.LyricsCache
|
||||
.FirstOrDefaultAsync(x => x.CacheKey == key && x.Provider == provider);
|
||||
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write cache to DB
|
||||
/// </summary>
|
||||
public async Task SaveLyricsAsync(SongInfo songInfo, LyricsCacheItem result)
|
||||
{
|
||||
using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
string key = songInfo.GetCacheKey();
|
||||
|
||||
var existingItem = await context.LyricsCache
|
||||
.FirstOrDefaultAsync(x => x.CacheKey == key && x.Provider == result.Provider);
|
||||
|
||||
if (existingItem == null)
|
||||
{
|
||||
var newItem = (LyricsCacheItem)result.Clone();
|
||||
newItem.CacheKey = key;
|
||||
|
||||
await context.LyricsCache.AddAsync(newItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No need to handle this case
|
||||
return;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
{
|
||||
public interface ILyricsSearchService
|
||||
{
|
||||
Task<LyricsSearchResult?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token);
|
||||
Task<LyricsCacheItem?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token);
|
||||
|
||||
Task<List<LyricsSearchResult>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token);
|
||||
Task<List<LyricsCacheItem>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Providers;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.LyricsCacheService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Services.SongSearchMapService;
|
||||
using Lyricify.Lyrics.Helpers;
|
||||
using Lyricify.Lyrics.Searchers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -30,12 +32,22 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly ILyricsCacheService _lyricsCacheService;
|
||||
private readonly ISongSearchMapService _songSearchMapService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LyricsSearchService(ISettingsService settingsService, IFileSystemService fileSystemService, ILogger<LyricsSearchService> logger)
|
||||
public LyricsSearchService(
|
||||
ISettingsService settingsService,
|
||||
IFileSystemService fileSystemService,
|
||||
ILyricsCacheService lyricsCacheService,
|
||||
ISongSearchMapService songSearchMapService,
|
||||
ILogger<LyricsSearchService> logger
|
||||
)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_lyricsCacheService = lyricsCacheService;
|
||||
_songSearchMapService = songSearchMapService;
|
||||
_logger = logger;
|
||||
|
||||
_lrcLibHttpClient = new();
|
||||
@@ -91,32 +103,30 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LyricsSearchResult?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token)
|
||||
public async Task<LyricsCacheItem?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token)
|
||||
{
|
||||
if (lyricsSearchType == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lyricsSearchResult = new LyricsSearchResult();
|
||||
var lyricsSearchResult = new LyricsCacheItem();
|
||||
//lyricsSearchResult.Raw = File.ReadAllText("C:\\Users\\Zhe\\Desktop\\星河回响 (Tech Demo).lrc");
|
||||
//return lyricsSearchResult;
|
||||
|
||||
string overridenTitle = songInfo.Title;
|
||||
string[] overridenArtists = songInfo.Artists;
|
||||
string overridenArtist = songInfo.Artist;
|
||||
string overridenAlbum = songInfo.Album;
|
||||
|
||||
_logger.LogInformation("SearchSmartlyAsync {SongInfo}", songInfo);
|
||||
|
||||
// 先检查该曲目是否已被用户映射
|
||||
var found = _settingsService.AppSettings.MappedSongSearchQueries
|
||||
.FirstOrDefault(x =>
|
||||
x.OriginalTitle == overridenTitle &&
|
||||
x.OriginalArtist == overridenArtists.Join(ATL.Settings.DisplayValueSeparator.ToString()) &&
|
||||
x.OriginalAlbum == overridenAlbum);
|
||||
var found = await _songSearchMapService.GetMappingAsync(overridenTitle, overridenArtist, overridenAlbum);
|
||||
|
||||
if (found != null)
|
||||
{
|
||||
overridenTitle = found.MappedTitle;
|
||||
overridenArtists = found.MappedArtist.Split(ATL.Settings.DisplayValueSeparator);
|
||||
overridenArtist = found.MappedArtist;
|
||||
overridenAlbum = found.MappedAlbum;
|
||||
|
||||
_logger.LogInformation("Found mapped song search query: {MappedSongSearchQuery}", found);
|
||||
@@ -125,7 +135,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
if (pureMusic)
|
||||
{
|
||||
lyricsSearchResult.Title = overridenTitle;
|
||||
lyricsSearchResult.Artists = overridenArtists;
|
||||
lyricsSearchResult.Artist = overridenArtist;
|
||||
lyricsSearchResult.Album = overridenAlbum;
|
||||
lyricsSearchResult.Raw = "[99:00.000]🎶🎶🎶";
|
||||
return lyricsSearchResult;
|
||||
@@ -137,13 +147,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
return await SearchSingleAsync(
|
||||
((SongInfo)songInfo.Clone())
|
||||
.WithTitle(overridenTitle)
|
||||
.WithArtist(overridenArtists)
|
||||
.WithArtist(overridenArtist)
|
||||
.WithAlbum(overridenAlbum),
|
||||
targetProvider.Value, checkCache, token);
|
||||
}
|
||||
}
|
||||
|
||||
List<LyricsSearchResult> lyricsSearchResults = [];
|
||||
List<LyricsCacheItem> lyricsSearchResults = [];
|
||||
|
||||
var mediaSourceProviderInfo = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == songInfo.PlayerId);
|
||||
if (mediaSourceProviderInfo != null)
|
||||
@@ -159,7 +169,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
lyricsSearchResult = await SearchSingleAsync(
|
||||
((SongInfo)songInfo.Clone())
|
||||
.WithTitle(overridenTitle)
|
||||
.WithArtist(overridenArtists)
|
||||
.WithArtist(overridenArtist)
|
||||
.WithAlbum(overridenAlbum),
|
||||
provider.Provider, checkCache, token);
|
||||
|
||||
@@ -176,7 +186,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
case LyricsSearchType.Sequential:
|
||||
return lyricsSearchResult;
|
||||
case LyricsSearchType.BestMatch:
|
||||
lyricsSearchResults.Add((LyricsSearchResult)lyricsSearchResult.Clone());
|
||||
lyricsSearchResults.Add((LyricsCacheItem)lyricsSearchResult.Clone());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -193,10 +203,10 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<LyricsSearchResult>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token)
|
||||
public async Task<List<LyricsCacheItem>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token)
|
||||
{
|
||||
_logger.LogInformation("SearchAllAsync {SongInfo}", songInfo);
|
||||
var results = new List<LyricsSearchResult>();
|
||||
var results = new List<LyricsCacheItem>();
|
||||
foreach (var provider in Enum.GetValues<LyricsSearchProvider>())
|
||||
{
|
||||
var searchResult = await SearchSingleAsync(songInfo, provider, checkCache, token);
|
||||
@@ -205,9 +215,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
return results;
|
||||
}
|
||||
|
||||
private async Task<LyricsSearchResult> SearchSingleAsync(SongInfo songInfo, LyricsSearchProvider provider, bool checkCache, CancellationToken token)
|
||||
private async Task<LyricsCacheItem> SearchSingleAsync(SongInfo songInfo, LyricsSearchProvider provider, bool checkCache, CancellationToken token)
|
||||
{
|
||||
var lyricsSearchResult = new LyricsSearchResult
|
||||
var lyricsSearchResult = new LyricsCacheItem
|
||||
{
|
||||
Provider = provider,
|
||||
};
|
||||
@@ -217,7 +227,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
// Check cache first if allowed
|
||||
if (checkCache && provider.IsRemote())
|
||||
{
|
||||
var cached = FileHelper.ReadLyricsCache(songInfo, provider);
|
||||
var cached = await _lyricsCacheService.GetLyricsAsync(songInfo, provider);
|
||||
if (cached != null)
|
||||
{
|
||||
lyricsSearchResult = cached;
|
||||
@@ -269,20 +279,20 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
|
||||
if (provider.IsRemote())
|
||||
{
|
||||
FileHelper.WriteLyricsCache(songInfo, lyricsSearchResult);
|
||||
await _lyricsCacheService.SaveLyricsAsync(songInfo, lyricsSearchResult);
|
||||
}
|
||||
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private async Task<LyricsSearchResult> SearchFile(SongInfo songInfo, LyricsFormat format)
|
||||
private async Task<LyricsCacheItem> SearchFile(SongInfo songInfo, LyricsFormat format)
|
||||
{
|
||||
int maxScore = 0;
|
||||
|
||||
FilesIndexItem? bestFileEntity = null;
|
||||
MediaFolder? bestFolderConfig = null;
|
||||
|
||||
var lyricsSearchResult = new LyricsSearchResult();
|
||||
var lyricsSearchResult = new LyricsCacheItem();
|
||||
if (format.ToLyricsSearchProvider() is LyricsSearchProvider lyricsSearchProvider)
|
||||
{
|
||||
lyricsSearchResult.Provider = lyricsSearchProvider;
|
||||
@@ -305,7 +315,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
{
|
||||
if (item.FileName.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FileName });
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsCacheItem { Reference = item.FileName });
|
||||
|
||||
if (score > maxScore)
|
||||
{
|
||||
@@ -328,9 +338,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private async Task<LyricsSearchResult> SearchEmbedded(SongInfo songInfo)
|
||||
private async Task<LyricsCacheItem> SearchEmbedded(SongInfo songInfo)
|
||||
{
|
||||
var lyricsSearchResult = new LyricsSearchResult
|
||||
var lyricsSearchResult = new LyricsCacheItem
|
||||
{
|
||||
Provider = LyricsSearchProvider.LocalMusicFile,
|
||||
};
|
||||
@@ -352,10 +362,10 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.EmbeddedLyrics)) continue;
|
||||
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsCacheItem
|
||||
{
|
||||
Title = item.Title,
|
||||
Artists = item.Artists?.Split(ATL.Settings.DisplayValueSeparator),
|
||||
Artist = item.Artist,
|
||||
Album = item.Album,
|
||||
Duration = item.Duration
|
||||
});
|
||||
@@ -370,7 +380,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
if (bestFile != null && maxScore > 0)
|
||||
{
|
||||
lyricsSearchResult.Title = bestFile.Title;
|
||||
lyricsSearchResult.Artists = bestFile.Artists?.Split(ATL.Settings.DisplayValueSeparator);
|
||||
lyricsSearchResult.Artist = bestFile.Artist;
|
||||
lyricsSearchResult.Album = bestFile.Album;
|
||||
lyricsSearchResult.Duration = bestFile.Duration;
|
||||
|
||||
@@ -382,9 +392,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private async Task<LyricsSearchResult> SearchAmllTtmlDbAsync(SongInfo songInfo)
|
||||
private async Task<LyricsCacheItem> SearchAmllTtmlDbAsync(SongInfo songInfo)
|
||||
{
|
||||
var lyricsSearchResult = new LyricsSearchResult
|
||||
var lyricsSearchResult = new LyricsCacheItem
|
||||
{
|
||||
Provider = LyricsSearchProvider.AmllTtmlDb,
|
||||
};
|
||||
@@ -398,7 +408,6 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
}
|
||||
}
|
||||
|
||||
int bestScore = 0;
|
||||
string? rawLyricFile = null;
|
||||
await foreach (var line in File.ReadLinesAsync(PathHelper.AmllTtmlDbIndexPath))
|
||||
{
|
||||
@@ -412,7 +421,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
continue;
|
||||
|
||||
string? title = null;
|
||||
string[]? artists = null;
|
||||
string? artist = null;
|
||||
string? album = null;
|
||||
|
||||
foreach (var meta in metadataArr.EnumerateArray())
|
||||
@@ -424,34 +433,32 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
if (key == "musicName" && valueArr.GetArrayLength() > 0)
|
||||
title = valueArr[0].GetString();
|
||||
if (key == "artists" && valueArr.GetArrayLength() > 0)
|
||||
artists = valueArr.EnumerateArray().Select(x => x.GetString() ?? "").ToArray();
|
||||
artist = string.Join(" / ", valueArr.EnumerateArray());
|
||||
if (key == "album" && valueArr.GetArrayLength() > 0)
|
||||
album = valueArr[0].GetString();
|
||||
}
|
||||
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsCacheItem
|
||||
{
|
||||
Title = title,
|
||||
Artists = artists,
|
||||
Artist = artist,
|
||||
Album = album,
|
||||
});
|
||||
if (score > bestScore)
|
||||
if (score > lyricsSearchResult.MatchPercentage)
|
||||
{
|
||||
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
|
||||
{
|
||||
rawLyricFile = rawLyricFileProp.GetString();
|
||||
lyricsSearchResult.Title = title;
|
||||
lyricsSearchResult.Artists = artists;
|
||||
lyricsSearchResult.Artist = artist;
|
||||
lyricsSearchResult.Album = album;
|
||||
bestScore = score;
|
||||
lyricsSearchResult.MatchPercentage = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawLyricFile))
|
||||
{
|
||||
return lyricsSearchResult;
|
||||
@@ -478,9 +485,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private async Task<LyricsSearchResult> SearchLrcLibAsync(SongInfo songInfo)
|
||||
private async Task<LyricsCacheItem> SearchLrcLibAsync(SongInfo songInfo)
|
||||
{
|
||||
var lyricsSearchResult = new LyricsSearchResult
|
||||
var lyricsSearchResult = new LyricsCacheItem
|
||||
{
|
||||
Provider = LyricsSearchProvider.LrcLib,
|
||||
};
|
||||
@@ -489,7 +496,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
var url =
|
||||
$"https://lrclib.net/api/search?" +
|
||||
$"track_name={Uri.EscapeDataString(songInfo.Title)}&" +
|
||||
$"artist_name={Uri.EscapeDataString(songInfo.DisplayArtists)}&" +
|
||||
$"artist_name={Uri.EscapeDataString(songInfo.Artist)}&" +
|
||||
$"&album_name={Uri.EscapeDataString(songInfo.Album)}" +
|
||||
$"&durationMs={Uri.EscapeDataString(songInfo.DurationMs.ToString())}";
|
||||
|
||||
@@ -524,7 +531,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
|
||||
lyricsSearchResult.Raw = original;
|
||||
lyricsSearchResult.Title = searchedTitle;
|
||||
lyricsSearchResult.Artists = searchedArtist?.SplitByCommonSplitter();
|
||||
lyricsSearchResult.Artist = searchedArtist;
|
||||
lyricsSearchResult.Album = searchedAlbum;
|
||||
lyricsSearchResult.Duration = searchedDuration;
|
||||
|
||||
@@ -535,9 +542,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private static async Task<LyricsSearchResult> SearchQQNeteaseKugouAsync(SongInfo songInfo, Searchers searcher)
|
||||
private static async Task<LyricsCacheItem> SearchQQNeteaseKugouAsync(SongInfo songInfo, Searchers searcher)
|
||||
{
|
||||
var lyricsSearchResult = new LyricsSearchResult();
|
||||
var lyricsSearchResult = new LyricsCacheItem();
|
||||
|
||||
switch (searcher)
|
||||
{
|
||||
@@ -560,11 +567,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
|
||||
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIdHelper.IsNeteaseFamily(songInfo.PlayerId))
|
||||
{
|
||||
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
|
||||
result = new NeteaseSearchResult(songInfo.Title, [songInfo.Artist], songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
|
||||
}
|
||||
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerId.QQMusic)
|
||||
{
|
||||
result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
|
||||
result = new QQMusicSearchResult(songInfo.Title, [songInfo.Artist], songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -572,8 +579,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
{
|
||||
DurationMs = (int)songInfo.DurationMs,
|
||||
Album = songInfo.Album,
|
||||
AlbumArtists = songInfo.Artists.ToList(),
|
||||
Artists = songInfo.Artists.ToList(),
|
||||
Artist = songInfo.Artist,
|
||||
Title = songInfo.Title,
|
||||
}, searcher, Lyricify.Lyrics.Searchers.Helpers.CompareHelper.MatchType.NoMatch);
|
||||
}
|
||||
@@ -633,7 +639,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
}
|
||||
|
||||
lyricsSearchResult.Title = result?.Title;
|
||||
lyricsSearchResult.Artists = result?.Artists;
|
||||
lyricsSearchResult.Artist = string.Join(" / ", result?.Artists ?? []);
|
||||
lyricsSearchResult.Album = result?.Album;
|
||||
lyricsSearchResult.Duration = result?.DurationMs / 1000;
|
||||
|
||||
@@ -642,9 +648,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private async Task<LyricsSearchResult> SearchAppleMusicAsync(SongInfo songInfo)
|
||||
private async Task<LyricsCacheItem> SearchAppleMusicAsync(SongInfo songInfo)
|
||||
{
|
||||
LyricsSearchResult lyricsSearchResult = new()
|
||||
LyricsCacheItem lyricsSearchResult = new()
|
||||
{
|
||||
Provider = LyricsSearchProvider.AppleMusic
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.SongSearchMapService
|
||||
{
|
||||
public interface ISongSearchMapService
|
||||
{
|
||||
Task SaveMappingAsync(MappedSongSearchQuery mapping);
|
||||
Task<MappedSongSearchQuery?> GetMappingAsync(string title, string artist, string album);
|
||||
Task DeleteMappingAsync(MappedSongSearchQuery mapping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Db;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.SongSearchMapService
|
||||
{
|
||||
public class SongSearchMapService : ISongSearchMapService
|
||||
{
|
||||
private readonly IDbContextFactory<SongSearchMapDbContext> _contextFactory;
|
||||
|
||||
public SongSearchMapService(IDbContextFactory<SongSearchMapDbContext> contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public async Task SaveMappingAsync(MappedSongSearchQuery mapping)
|
||||
{
|
||||
using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
var existing = await context.SongSearchMap
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.OriginalTitle == mapping.OriginalTitle &&
|
||||
x.OriginalArtist == mapping.OriginalArtist &&
|
||||
x.OriginalAlbum == mapping.OriginalAlbum);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.MappedTitle = mapping.MappedTitle;
|
||||
existing.MappedArtist = mapping.MappedArtist;
|
||||
existing.MappedAlbum = mapping.MappedAlbum;
|
||||
|
||||
existing.IsMarkedAsPureMusic = mapping.IsMarkedAsPureMusic;
|
||||
existing.LyricsSearchProvider = mapping.LyricsSearchProvider;
|
||||
|
||||
context.SongSearchMap.Update(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newItem = (MappedSongSearchQuery)mapping.Clone();
|
||||
await context.SongSearchMap.AddAsync(newItem);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<MappedSongSearchQuery?> GetMappingAsync(string title, string artist, string album)
|
||||
{
|
||||
using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
return await context.SongSearchMap
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.OriginalTitle == title &&
|
||||
x.OriginalArtist == artist &&
|
||||
x.OriginalAlbum == album);
|
||||
}
|
||||
|
||||
public async Task DeleteMappingAsync(MappedSongSearchQuery mapping)
|
||||
{
|
||||
using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
var target = await context.SongSearchMap
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.OriginalTitle == mapping.OriginalTitle &&
|
||||
x.OriginalArtist == mapping.OriginalArtist &&
|
||||
x.OriginalAlbum == mapping.OriginalAlbum);
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
context.SongSearchMap.Remove(target);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -92,14 +92,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
private void ClearCacheFiles()
|
||||
{
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.LogDirectory);
|
||||
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.LyricsCacheDirectory);
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.AmllTtmlDbLyricsCacheDirectory);
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.KugouLyricsCacheDirectory);
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.LrcLibLyricsCacheDirectory);
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.NeteaseLyricsCacheDirectory);
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.QQLyricsCacheDirectory);
|
||||
|
||||
DirectoryHelper.DeleteAllFiles(PathHelper.iTunesAlbumArtCacheDirectory);
|
||||
|
||||
ToastHelper.ShowToast("ActionCompleted", null, InfoBarSeverity.Success);
|
||||
|
||||
@@ -3,9 +3,10 @@ using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Parsers.LyricsParser;
|
||||
using BetterLyrics.WinUI3.Services.LyricsSearchService;
|
||||
using BetterLyrics.WinUI3.Services.GSMTCService;
|
||||
using BetterLyrics.WinUI3.Services.LyricsSearchService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Services.SongSearchMapService;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -22,6 +23,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
private readonly ILyricsSearchService _lyricsSearchService;
|
||||
private readonly IGSMTCService _gsmtcService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISongSearchMapService _songSearchMapService;
|
||||
|
||||
private LatestOnlyTaskRunner _lyricsSearchRunner = new();
|
||||
|
||||
@@ -29,10 +31,10 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
public partial AppSettings AppSettings { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<LyricsSearchResult> LyricsSearchResults { get; set; } = [];
|
||||
public partial ObservableCollection<LyricsCacheItem> LyricsSearchResults { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial LyricsSearchResult? SelectedLyricsSearchResult { get; set; }
|
||||
public partial LyricsCacheItem? SelectedLyricsSearchResult { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<LyricsData>? LyricsDataArr { get; set; }
|
||||
@@ -43,59 +45,53 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
[ObservableProperty]
|
||||
public partial bool IsSearching { get; set; } = false;
|
||||
|
||||
public LyricsSearchControlViewModel(ILyricsSearchService lyricsSearchService, IGSMTCService gsmtcService, ISettingsService settingsService)
|
||||
public LyricsSearchControlViewModel(
|
||||
ILyricsSearchService lyricsSearchService,
|
||||
IGSMTCService gsmtcService,
|
||||
ISettingsService settingsService,
|
||||
ISongSearchMapService songSearchMapService
|
||||
)
|
||||
{
|
||||
_lyricsSearchService = lyricsSearchService;
|
||||
_gsmtcService = gsmtcService;
|
||||
_settingsService = settingsService;
|
||||
_songSearchMapService = songSearchMapService;
|
||||
|
||||
AppSettings = _settingsService.AppSettings;
|
||||
|
||||
InitMappedSongSearchQuery();
|
||||
_ = InitMappedSongSearchQueryAsync();
|
||||
}
|
||||
|
||||
private void InitMappedSongSearchQuery()
|
||||
private async Task InitMappedSongSearchQueryAsync()
|
||||
{
|
||||
LyricsSearchResults.Clear();
|
||||
LyricsDataArr = null;
|
||||
if (_gsmtcService.CurrentSongInfo != null)
|
||||
{
|
||||
var found = GetMappedSongSearchQueryFromSettings();
|
||||
var found = await _songSearchMapService.GetMappingAsync(
|
||||
_gsmtcService.CurrentSongInfo.Title,
|
||||
_gsmtcService.CurrentSongInfo.Artist,
|
||||
_gsmtcService.CurrentSongInfo.Album);
|
||||
|
||||
if (found == null)
|
||||
{
|
||||
MappedSongSearchQuery = new MappedSongSearchQuery
|
||||
{
|
||||
OriginalTitle = _gsmtcService.CurrentSongInfo.Title,
|
||||
OriginalArtist = _gsmtcService.CurrentSongInfo.DisplayArtists,
|
||||
OriginalArtist = _gsmtcService.CurrentSongInfo.Artist,
|
||||
OriginalAlbum = _gsmtcService.CurrentSongInfo.Album,
|
||||
MappedTitle = _gsmtcService.CurrentSongInfo.Title,
|
||||
MappedArtist = _gsmtcService.CurrentSongInfo.DisplayArtists,
|
||||
MappedArtist = _gsmtcService.CurrentSongInfo.Artist,
|
||||
MappedAlbum = _gsmtcService.CurrentSongInfo.Album,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
MappedSongSearchQuery = found.Clone();
|
||||
MappedSongSearchQuery = (MappedSongSearchQuery)found.Clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MappedSongSearchQuery? GetMappedSongSearchQueryFromSettings()
|
||||
{
|
||||
if (_gsmtcService.CurrentSongInfo == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var found = AppSettings.MappedSongSearchQueries
|
||||
.FirstOrDefault(x =>
|
||||
x.OriginalTitle == _gsmtcService.CurrentSongInfo.Title &&
|
||||
x.OriginalArtist == _gsmtcService.CurrentSongInfo.DisplayArtists &&
|
||||
x.OriginalAlbum == _gsmtcService.CurrentSongInfo.Album);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
public void PlayLyricsLine(LyricsLine? value)
|
||||
{
|
||||
if (value?.StartMs == null)
|
||||
@@ -123,7 +119,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
var result = await _lyricsSearchService.SearchAllAsync(
|
||||
((SongInfo)_gsmtcService.CurrentSongInfo.Clone())
|
||||
.WithTitle(MappedSongSearchQuery.MappedTitle)
|
||||
.WithArtist(MappedSongSearchQuery.MappedArtist.SplitByCommonSplitter())
|
||||
.WithArtist(MappedSongSearchQuery.MappedArtist)
|
||||
.WithAlbum(MappedSongSearchQuery.MappedAlbum),
|
||||
!_settingsService.AppSettings.GeneralSettings.IgnoreCacheWhenSearching,
|
||||
token);
|
||||
@@ -134,32 +130,27 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (MappedSongSearchQuery == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = GetMappedSongSearchQueryFromSettings();
|
||||
if (existing != null)
|
||||
{
|
||||
AppSettings.MappedSongSearchQueries.Remove(existing);
|
||||
}
|
||||
AppSettings.MappedSongSearchQueries.Add(MappedSongSearchQuery);
|
||||
MappedSongSearchQuery = MappedSongSearchQuery.Clone();
|
||||
await _songSearchMapService.SaveMappingAsync(MappedSongSearchQuery);
|
||||
MappedSongSearchQuery = (MappedSongSearchQuery)MappedSongSearchQuery.Clone();
|
||||
_gsmtcService.UpdateLyrics();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Reset()
|
||||
private async Task ResetAsync()
|
||||
{
|
||||
var existing = GetMappedSongSearchQueryFromSettings();
|
||||
if (existing != null)
|
||||
{
|
||||
AppSettings.MappedSongSearchQueries.Remove(existing);
|
||||
}
|
||||
InitMappedSongSearchQuery();
|
||||
if (MappedSongSearchQuery == null) return;
|
||||
|
||||
await _songSearchMapService.DeleteMappingAsync(MappedSongSearchQuery);
|
||||
await InitMappedSongSearchQueryAsync();
|
||||
SelectedLyricsSearchResult = null;
|
||||
_gsmtcService.UpdateLyrics();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -180,7 +171,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
MappedSongSearchQuery?.MappedAlbum = MappedSongSearchQuery?.OriginalAlbum ?? string.Empty;
|
||||
}
|
||||
|
||||
partial void OnSelectedLyricsSearchResultChanged(LyricsSearchResult? value)
|
||||
partial void OnSelectedLyricsSearchResultChanged(LyricsCacheItem? value)
|
||||
{
|
||||
MappedSongSearchQuery?.LyricsSearchProvider = value?.Provider;
|
||||
if (value?.Raw != null)
|
||||
@@ -200,7 +191,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
|
||||
{
|
||||
InitMappedSongSearchQuery();
|
||||
_ = InitMappedSongSearchQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
var albumFontSize = albumArtLayoutSettings.IsAutoSongInfoFontSize ? lyricsLayoutMetrics.AlbumNameSize : albumArtLayoutSettings.SongInfoFontSize * 0.8;
|
||||
|
||||
RenderTextBlock(TitleTextBlock, _gsmtcService.CurrentSongInfo.Title, titleFontSize);
|
||||
RenderTextBlock(ArtistsTextBlock, _gsmtcService.CurrentSongInfo.DisplayArtists, artistsFontSize);
|
||||
RenderTextBlock(ArtistsTextBlock, _gsmtcService.CurrentSongInfo.Artist, artistsFontSize);
|
||||
RenderTextBlock(AlbumTextBlock, _gsmtcService.CurrentSongInfo.Album, albumFontSize);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ Special thanks to the following people for their support!
|
||||
|
||||
| Date / 日期 | Name / 昵称 |
|
||||
| :--- | :--- |
|
||||
| Jan 3, 2026 | \*\*轩 |
|
||||
| Dec 13, 2025 | \<Anonymous\> |
|
||||
| Dec 3, 2025 | YE |
|
||||
| Dec 2, 2025 | \<Anonymous\> |
|
||||
| Nov 23, 2025 | \*\*玄 |
|
||||
| Nov 21, 2025 | \*\*智 |
|
||||
| Nov 17, 2025 | \*鹤 |
|
||||
| Nov 17, 2025 | SuHeAndZl |
|
||||
| Nov 2, 2025 | 借过 |
|
||||
| Aug 28, 2025 | \*\*华 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user