Compare commits

...

40 Commits

Author SHA1 Message Date
Zhe Fang
32ba453264 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 16:30:16 -05:00
Zhe Fang
d4902329bb chores: update readme 2025-12-31 16:30:14 -05:00
Zhe Fang
83aee8948b Update README.CN.md 2025-12-31 16:13:48 -05:00
Zhe Fang
1f9fab3228 Update README.CN.md 2025-12-31 16:10:42 -05:00
Zhe Fang
7a3a659dfc Update README.CN.md 2025-12-31 16:09:17 -05:00
Zhe Fang
a14afd3eb5 Update README.md 2025-12-31 16:01:37 -05:00
Zhe Fang
c2af7f3186 Update README.CN.md 2025-12-31 16:00:16 -05:00
Zhe Fang
cd026dd2bf Update README.CN.md 2025-12-31 15:56:08 -05:00
Zhe Fang
4bc1a9975d Update README.md 2025-12-31 15:52:05 -05:00
Zhe Fang
07eecf0930 Update README.md 2025-12-31 15:49:22 -05:00
Zhe Fang
35fba5abb0 chores 2025-12-31 15:44:58 -05:00
Zhe Fang
03ef231a3f chores 2025-12-31 15:39:44 -05:00
Zhe Fang
f41879f4e5 chores: add pic 2025-12-31 15:38:18 -05:00
Zhe Fang
bda7510ed6 chores: update readme 2025-12-31 14:28:54 -05:00
Zhe Fang
5ec8c7c61f chores: bump to 1.2.236 2025-12-31 13:32:45 -05:00
Zhe Fang
7e6bd9dade fix: _scrobbleStopwatch 2025-12-31 10:55:56 -05:00
Zhe Fang
56244cb793 fix: scrollable timer 2025-12-31 10:47:43 -05:00
Zhe Fang
cb5f70ab55 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 10:12:29 -05:00
Zhe Fang
3cc018bb1f chores: bump to 1.2.234.0 2025-12-31 10:12:28 -05:00
Zhe Fang
c517d2b008 Update README.md 2025-12-31 10:08:58 -05:00
Zhe Fang
e79f2a0223 Update README.CN.md 2025-12-31 10:07:53 -05:00
Zhe Fang
39122b9147 Update README.CN.md 2025-12-31 10:07:01 -05:00
Zhe Fang
accbdc1806 Update README.md 2025-12-31 10:06:01 -05:00
Zhe Fang
de014d1ad7 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 09:52:12 -05:00
Zhe Fang
cc2ce5f8cf chores: i18n 2025-12-31 09:52:10 -05:00
Zhe Fang
2a2d80436e Update README.CN.md 2025-12-31 09:34:29 -05:00
Zhe Fang
ce3f79f35c Update README.md 2025-12-31 09:33:01 -05:00
Zhe Fang
12e6000cb3 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:43:44 -05:00
Zhe Fang
c1dc684411 fix: scrobble timer is still running when music is paused 2025-12-31 08:43:43 -05:00
Zhe Fang
69ea2cb495 Update README.md 2025-12-31 08:21:33 -05:00
Zhe Fang
e2ee03c4be Update README.CN.md 2025-12-31 08:21:03 -05:00
Zhe Fang
c6fe33d6ae Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:12:12 -05:00
Zhe Fang
7744e145fa chores: i18n 2025-12-31 08:12:10 -05:00
Zhe Fang
0284b1de81 Update README.CN.md 2025-12-30 21:46:32 -05:00
Zhe Fang
108c2cd34b Update README.md 2025-12-30 21:45:56 -05:00
Zhe Fang
390e30f7f5 chores: bump to v1.2.232.0 2025-12-30 20:54:40 -05:00
Zhe Fang
900774668d chores: i18n 2025-12-30 20:09:02 -05:00
Zhe Fang
6ca2d1f897 chores 2025-12-30 19:49:54 -05:00
Zhe Fang
164bd077b8 fix: app crash when audio device was not found 2025-12-30 14:30:10 -05:00
Zhe Fang
8ec71fcfb7 chores: update StatsDashboardControl 2025-12-30 14:04:08 -05:00
65 changed files with 1209 additions and 969 deletions

View File

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

View File

@@ -76,6 +76,7 @@
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
<converter:PathToImageConverter x:Key="PathToImageConverter" />
<converter:DoubleToDecimalConverter x:Key="DoubleToDecimalConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />

View File

@@ -1,8 +1,6 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.FileSystemService;
@@ -17,36 +15,42 @@ using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching; // 关键:用于线程调度
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.Windows.Globalization;
using Microsoft.Windows.AppLifecycle; // 关键App生命周期管理
using Serilog;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3
{
public partial class App : Application
{
private Window? m_window;
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
private static Mutex? _instanceMutex;
private readonly string _appKey = Windows.ApplicationModel.Package.Current.Id.FamilyName;
public App()
{
this.InitializeComponent();
// Must be done before InitializeComponent
if (!TryHandleSingleInstance())
{
// 如果移交成功直接退出当前进程
Environment.Exit(0);
return;
}
EnsureSingleInstance();
this.InitializeComponent();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories();
@@ -54,28 +58,81 @@ namespace BetterLyrics.WinUI3
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
// 注册全局异常捕获
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void EnsureSingleInstance()
/// <summary>
/// 处理单实例逻辑。
/// 返回 true 表示我是主实例,继续运行。
/// 返回 false 表示我是第二个实例,已通知主实例,我应该退出。
/// </summary>
private bool TryHandleSingleInstance()
{
_instanceMutex = new Mutex(true, Constants.App.AppName, out bool createdNew);
// 尝试查找或注册当前实例
var mainInstance = AppInstance.FindOrRegisterForKey(_appKey);
if (!createdNew)
// 如果当前实例就是注册的那个主实例
if (mainInstance.IsCurrent)
{
User32.MessageBox(HWND.NULL, new ResourceLoader().GetString("TryRunMultipleInstance"), null, User32.MB_FLAGS.MB_APPLMODAL);
Environment.Exit(0);
// 监听 "Activated" 事件。
// 当第二个实例启动并重定向过来时,这个事件会被触发。
mainInstance.Activated += OnMainInstanceActivated;
return true;
}
else
{
// 我不是主实例,我是后来者。
// 获取当前实例的激活参数(比如是通过文件双击打开的,这里能拿到文件路径)
var args = AppInstance.GetCurrent().GetActivatedEventArgs();
// 将激活请求重定向给主实例
// 注意:这里是同步等待,确保发送成功后再退出
try
{
mainInstance.RedirectActivationToAsync(args).AsTask().Wait();
}
catch (Exception)
{
// 即使重定向失败,作为第二个实例也应该退出
}
return false;
}
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
/// <summary>
/// 当第二个实例试图启动时,主实例会收到此回调
/// </summary>
private void OnMainInstanceActivated(object? sender, AppActivationArguments e)
{
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
// 这个事件是在后台线程触发的,必须切回 UI 线程操作窗口
m_window?.DispatcherQueue.TryEnqueue(() =>
{
HandleActivation();
});
}
/// <summary>
/// 唤醒逻辑
/// </summary>
private void HandleActivation()
{
WindowHook.OpenOrShowWindow<LyricsWindowSwitchWindow>();
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
// 初始化数据库
await EnsureDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
// 开始后台扫描任务
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{
if (item.LastSyncTime == null)
@@ -85,8 +142,10 @@ namespace BetterLyrics.WinUI3
}
fileSystemService.StartAllFolderTimers();
WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 初始化托盘
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 根据设置打开歌词窗口
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -102,12 +161,101 @@ namespace BetterLyrics.WinUI3
}
}
}
// 根据设置自动打开主界面
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
}
}
private async Task EnsureDatabasesAsync()
{
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
var fileCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
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
{
await initAction();
}
catch (Exception ex)
{
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})");
}
}
}
private async Task ShowErrorDialogAsync(string title, string content)
{
// 这里假设 m_window 已经存在。如果没有显示主窗口,这个弹窗可能无法显示。
// 在 App 启动极早期的错误,可能需要退化为 Log 或者 System.Diagnostics.Process.Start 打开记事本报错
if (m_window != null)
{
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();
});
}
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
@@ -115,14 +263,19 @@ namespace BetterLyrics.WinUI3
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
// 数据库工厂
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}"))
// 日志
.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
@@ -135,6 +288,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<ILocalizationService, LocalizationService>()
.AddSingleton<IFileSystemService, FileSystemService>()
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()
.AddSingleton<PlaybackSettingsControlViewModel>()
@@ -167,7 +321,8 @@ namespace BetterLyrics.WinUI3
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
{
_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
// FirstChance 异常非常多(比如内部 try-catch 也会触发),通常建议只在 Debug 模式记录,或者过滤特定类型
// _logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
@@ -180,4 +335,4 @@ namespace BetterLyrics.WinUI3
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -87,6 +87,10 @@
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
@@ -97,7 +101,6 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SMBLibrary" Version="1.5.5.1" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
@@ -130,6 +133,10 @@
<ItemGroup>
<TrimmerRootAssembly Include="FlaUI.UIA3" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Abstractions" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Relational" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Sqlite" />
<TrimmerRootAssembly Include="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
@@ -217,6 +224,9 @@
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

View File

@@ -25,5 +25,6 @@
public const string MoeKoeMusic = "cn.MoeKoe.Music";
public const string MoeKoeMusicAlternative = "electron.app.MoeKoe Music";
public const string Listen1 = "com.listen1.listen1";
public const string OriginalSoundHQPlayer = "SennpaiStudio.528762A6196EF_z79ft30j24epr!App";
}
}

View File

@@ -24,5 +24,6 @@
public const string SaltPlayerForWindowsSteam = "Salt Player for Windows (Steam)";
public const string MoeKoeMusic = "MoeKoe Music";
public const string Listen1 = "Listen 1";
public const string OriginalSoundHQPlayer = "Original Sound HQ Player";
}
}

View File

@@ -190,6 +190,12 @@
</dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageSettingsPlayHistory" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button x:Uid="SettingsPageExportPlayHistoryButton" Command="{x:Bind ViewModel.ExportPlayHistoryCommand}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -71,7 +71,7 @@
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay}" />
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay, Converter={StaticResource DoubleToDecimalConverter}}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Bottom"
@@ -194,47 +194,25 @@
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PlayerStatDisplayItem">
<Grid Margin="0,4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
FontSize="13"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind PlayerName}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
<Grid
Grid.Row="1"
Height="4"
Margin="0,4,0,0"
CornerRadius="2">
<Rectangle
Fill="{ThemeResource SystemControlBackgroundBaseLowBrush}"
RadiusX="2"
RadiusY="2" />
<Rectangle
Width="{x:Bind DisplayWidth}"
HorizontalAlignment="Left"
Fill="{ThemeResource AccentFillColorDefaultBrush}"
RadiusX="2"
RadiusY="2" />
</Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
FontSize="13"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind PlayerName}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>

View File

@@ -0,0 +1,33 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Converter
{
public partial class DoubleToDecimalConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value == null) return string.Empty;
if (double.TryParse(value.ToString(), out double number))
{
int decimalPlaces = 2;
if (parameter != null && int.TryParse(parameter.ToString(), out int parsedParams))
{
decimalPlaces = parsedParams;
}
return number.ToString($"F{decimalPlaces}");
}
return value.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Helper
@@ -29,6 +30,18 @@ namespace BetterLyrics.WinUI3.Helper
return Encoding.GetEncoding(encoding);
}
public static async Task CopyFileAsync(string sourcePath, string destinationPath)
{
var dir = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await sourceStream.CopyToAsync(destinationStream);
}
}
public static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();

View File

@@ -33,6 +33,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string SaltPlayerForWindowsLogoPath => Path.Combine(AssetsFolder, "SaltPlayerForWindows.png");
public static string MoeKoeMusicLogoPath => Path.Combine(AssetsFolder, "MoeKoeMusic.png");
public static string Listen1LogoPath => Path.Combine(AssetsFolder, "Listen1.png");
public static string OriginalSoundHQPlayerLogoPath => Path.Combine(AssetsFolder, "OriginalSoundHQPlayer.png");
public static string UnknownPlayerLogoPath => Path.Combine(AssetsFolder, "Question.png");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");

View File

@@ -55,6 +55,7 @@ namespace BetterLyrics.WinUI3.Helper
PlayerId.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerId.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerId.Listen1 => PlayerName.Listen1,
PlayerId.OriginalSoundHQPlayer => PlayerName.OriginalSoundHQPlayer,
_ => id,
};
@@ -83,6 +84,7 @@ namespace BetterLyrics.WinUI3.Helper
PlayerId.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerId.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerId.Listen1 => PathHelper.Listen1LogoPath,
PlayerId.OriginalSoundHQPlayer => PathHelper.OriginalSoundHQPlayerLogoPath,
_ => PathHelper.UnknownPlayerLogoPath,
};
}

View File

@@ -17,12 +17,20 @@ namespace BetterLyrics.WinUI3.Hooks
static SystemVolumeHook()
{
_deviceEnumerator = new MMDeviceEnumerator();
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (_defaultDevice != null)
try
{
_defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
_deviceEnumerator = new MMDeviceEnumerator();
// 找不到设备会抛出异常,在这里截获它
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (_defaultDevice != null)
{
_defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
}
}
catch (Exception ex)
{
_defaultDevice = null;
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Db
{
public partial class FilesIndexDbContext : DbContext
{
public FilesIndexDbContext(DbContextOptions<FilesIndexDbContext> options) : base(options) { }
public DbSet<FilesIndexItem> FilesIndex { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Db
{
public partial class PlayHistoryDbContext : DbContext
{
public PlayHistoryDbContext(DbContextOptions<PlayHistoryDbContext> options) : base(options) { }
public DbSet<PlayHistoryItem> PlayHistory { get; set; }
}
}

View File

@@ -136,7 +136,7 @@ namespace BetterLyrics.WinUI3.Models
SetFromTrack(track);
}
public ExtendedTrack(FileCacheEntity? entity, Stream? stream = null) : base()
public ExtendedTrack(FilesIndexItem? entity, Stream? stream = null) : base()
{
if (entity == null) return;

View File

@@ -1,57 +0,0 @@
using SQLite;
using System;
namespace BetterLyrics.WinUI3.Models
{
[Table("FileCache")]
public class FileCacheEntity
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
// 【新增】关键字段!
// 关联到 MediaFolder.Id。
// 作用:
// 1. 区分不同配置(即使两个配置连的是同一个 SMB但在 APP 里视为不同源)。
// 2. 删除配置时,可以由 MediaFolderId 快速级联删除所有缓存。
[Indexed]
public string MediaFolderId { get; set; }
// 【修改】从 ParentPath 改为 ParentUri
// 存储父文件夹的标准 URI (smb://host/share/parent)
// 根目录文件的 ParentUri 可以为空,或者等于 MediaFolder 的 Base Uri
[Indexed]
public string? ParentUri { get; set; }
// 【核心】标准化的完整 URI (smb://host/share/folder/file.ext)
// 确保它是 URL 编码过且格式统一的
[Indexed(Unique = true)]
public string Uri { get; set; }
public string FileName { get; set; } = "";
public bool IsDirectory { get; set; }
// 记录文件大小,同步时用来对比文件是否变化
public long FileSize { get; set; }
// 记录修改时间,同步时对比使用
public DateTime? LastModified { get; set; }
// ------ 元数据部分 (保持不变) ------
public string Title { get; set; } = "";
public string Artists { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
public double SampleRate { get; set; }
public int BitDepth { get; set; }
public int Duration { get; set; }
public string AudioFormatName { get; set; } = "";
public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";
public string? EmbeddedLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public bool IsMetadataParsed { get; set; }
}
}

View File

@@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models
{
[Index(nameof(MediaFolderId))] // 普通索引
[Index(nameof(ParentUri))] // 普通索引
[Index(nameof(Uri), IsUnique = true)] // 唯一索引
public class FilesIndexItem
{
[Key] // 主键
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 明确指定为自增 (Identity)
public int Id { get; set; }
// 关联到 MediaFolder.Id
// 注意:作为索引列,必须限制长度,否则 SQL Server 会报错 (索引最大900字节)
[MaxLength(450)]
public string MediaFolderId { get; set; }
// 存储父文件夹的标准 URI
// 允许为空
[MaxLength(450)]
public string? ParentUri { get; set; }
// 唯一索引列
// 必须限制长度。450字符 * 2字节/字符 = 900字节 (正好卡在 SQL Server 限制内)
[Required]
[MaxLength(450)]
public string Uri { get; set; }
public string FileName { get; set; } = "";
public bool IsDirectory { get; set; }
public long FileSize { get; set; }
public DateTime? LastModified { get; set; }
// 下面的元数据字段通常不需要索引,可以使用 MaxLength 稍微优化空间,
// 或者直接留空(默认为 nvarchar(max)
public string Title { get; set; } = "";
public string Artists { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
public double SampleRate { get; set; }
public int BitDepth { get; set; }
public int Duration { get; set; }
[MaxLength(50)] // 格式名称通常很短,限制一下是个好习惯
public string AudioFormatName { get; set; } = "";
[MaxLength(20)]
public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";
// 歌词可能会很长,保留默认的 nvarchar(max) 即可
public string? EmbeddedLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public bool IsMetadataParsed { get; set; }
}
}

View File

@@ -1,25 +1,39 @@
using SQLite;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models
{
[Table("PlayHistory")]
[Index(nameof(Title))]
[Index(nameof(Artist))]
[Index(nameof(StartedAt))] // 用于按时间排序查询(如:最近播放)
[Index(nameof(PlayerId))]
public class PlayHistoryItem
{
[PrimaryKey, AutoIncrement]
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // AutoIncrement
public int Id { get; set; }
[Indexed] public string Title { get; set; }
[Indexed] public string Artist { get; set; }
public string Album { get; set; }
// 注意:作为索引列,必须加 MaxLength。
// 如果不加,默认为 nvarchar(max)SQL Server 无法对其建立高效索引。
[MaxLength(450)]
public string Title { get; set; } = "";
[MaxLength(450)]
public string Artist { get; set; } = "";
// Album 没有索引,可以不限制长度,或者为了规范也限制一下
public string Album { get; set; } = "";
public DateTime StartedAt { get; set; }
[Indexed] public DateTime StartedAt { get; set; }
public double DurationPlayedMs { get; set; }
public double TotalDurationMs { get; set; }
[Indexed]
public string PlayerId { get; set; }
// PlayerId 通常是个 GUID 或者短字符串,给 100 长度通常足够了,节省索引空间
[MaxLength(100)]
public string PlayerId { get; set; } = "";
}
}
}

View File

@@ -10,7 +10,6 @@ namespace BetterLyrics.WinUI3.Models
public string PlayerId { get; set; }
public int PlayCount { get; set; }
public double DisplayWidth { get; set; }
public string PlayerName => PlayerIdHelper.GetDisplayName(PlayerId);
}
}

View File

@@ -90,7 +90,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FileCacheEntity? bestMatch = null;
FilesIndexItem? bestMatch = null;
foreach (var item in allFiles)
{

View File

@@ -1,15 +1,16 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml.Controls;
using SQLite;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -28,7 +29,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
private readonly ILocalizationService _localizationService;
private readonly ILogger<FileSystemService> _logger;
private readonly SQLiteAsyncConnection _db;
private readonly IDbContextFactory<FilesIndexDbContext> _contextFactory;
private bool _isInitialized = false;
// 定时器字典
@@ -36,54 +38,37 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
// 当前正在执行的扫描任务字典
private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeScanTokens = new();
private static readonly SemaphoreSlim _dbLock = new(1, 1);
private static readonly SemaphoreSlim _folderScanLock = new(1, 1);
public FileSystemService(ISettingsService settingsService, ILocalizationService localizationService, ILogger<FileSystemService> logger)
public FileSystemService(
ISettingsService settingsService,
ILocalizationService localizationService,
ILogger<FileSystemService> logger,
IDbContextFactory<FilesIndexDbContext> contextFactory)
{
_logger = logger;
_localizationService = localizationService;
_settingsService = settingsService;
_db = new SQLiteAsyncConnection(PathHelper.FilesIndexPath);
_contextFactory = contextFactory;
}
public async Task InitializeAsync()
public async Task<List<FilesIndexItem>> GetFilesAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId, bool forceRefresh = false)
{
if (_isInitialized) return;
string queryParentUri = parentFolder == null ? "" : parentFolder.Uri;
if (parentFolder == null && !forceRefresh) forceRefresh = true;
await _db.CreateTableAsync<FileCacheEntity>();
using var context = await _contextFactory.CreateDbContextAsync();
_isInitialized = true;
}
public async Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false)
{
await InitializeAsync();
string queryParentUri;
if (parentFolder == null)
{
if (!forceRefresh) forceRefresh = true;
queryParentUri = "";
}
else
{
queryParentUri = parentFolder.Uri;
}
List<FileCacheEntity> cachedEntities = new List<FileCacheEntity>();
if (parentFolder != null)
{
cachedEntities = await _db.Table<FileCacheEntity>()
.Where(x => x.MediaFolderId == configId && x.ParentUri == queryParentUri)
.ToListAsync();
}
var cachedEntities = await context.FilesIndex
.AsNoTracking() // 读操作不追踪,提升性能
.Where(x => x.MediaFolderId == configId && x.ParentUri == queryParentUri)
.ToListAsync();
bool needSync = forceRefresh || cachedEntities.Count == 0;
if (needSync)
{
// SyncAsync 内部自己管理 Context
cachedEntities = await SyncAsync(provider, parentFolder, configId);
}
@@ -91,17 +76,11 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
/// <summary>
/// 从远端/本地同步文件至数据库,该阶段不会解析文件全部元数据。
/// <para/>
/// 如果某个已有文件被修改或有新文件被添加,会预留空位,等待后续填充(通常交给 <see cref="ScanMediaFolderAsync"/> 完成)
/// 从远端/本地同步文件至数据库
/// </summary>
/// <param name="provider"></param>
/// <param name="parentFolder"></param>
/// <param name="configId"></param>
/// <returns></returns>
private async Task<List<FileCacheEntity>> SyncAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId)
private async Task<List<FilesIndexItem>> SyncAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId)
{
List<FileCacheEntity> remoteItems;
List<FilesIndexItem> remoteItems;
try
{
remoteItems = await provider.GetFilesAsync(parentFolder);
@@ -116,80 +95,79 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
string targetParentUri = "";
if (remoteItems.Count > 0)
{
targetParentUri = remoteItems[0].ParentUri ?? "";
}
else if (parentFolder != null)
{
targetParentUri = parentFolder.Uri;
}
else
{
return [];
}
try
{
await _db.RunInTransactionAsync(conn =>
using var context = await _contextFactory.CreateDbContextAsync();
// 开启事务 (EF Core 也能管理事务)
using var transaction = await context.Database.BeginTransactionAsync();
// 1. 获取数据库中现有的该目录下的文件
var dbItems = await context.FilesIndex
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToListAsync();
var dbMap = dbItems.ToDictionary(x => x.Uri, x => x);
// 2. 远端数据去重(防止 Provider 返回重复 Uri
var remoteDistinct = remoteItems
.GroupBy(x => x.Uri)
.Select(g => g.First())
.ToList();
var remoteUris = new HashSet<string>();
// 3. 处理 新增 和 更新
foreach (var remote in remoteDistinct)
{
var dbItems = conn.Table<FileCacheEntity>()
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToList();
remoteUris.Add(remote.Uri);
var dbMap = dbItems.ToDictionary(x => x.Uri, x => x);
var remoteMap = remoteItems
.GroupBy(x => x.Uri)
.Select(g => g.First())
.ToDictionary(x => x.Uri, x => x);
var toInsert = new List<FileCacheEntity>();
var toUpdate = new List<FileCacheEntity>();
var toDelete = new List<FileCacheEntity>();
foreach (var remote in remoteItems)
if (dbMap.TryGetValue(remote.Uri, out var existing))
{
if (dbMap.TryGetValue(remote.Uri, out var existing))
{
bool isChanged = existing.FileSize != remote.FileSize ||
existing.LastModified != remote.LastModified;
// 检查是否变更
bool isChanged = existing.FileSize != remote.FileSize ||
existing.LastModified != remote.LastModified;
if (isChanged)
{
existing.FileSize = remote.FileSize;
existing.LastModified = remote.LastModified;
existing.IsMetadataParsed = false; // 标记为未解析,下次会重新读取元数据
toUpdate.Add(existing);
}
else
{
// 数据库里原有的 Title, Artist, LocalAlbumArtPath 都会被完美保留
}
}
else
if (isChanged)
{
toInsert.Add(remote);
existing.FileSize = remote.FileSize;
existing.LastModified = remote.LastModified;
existing.IsMetadataParsed = false; // 标记重新解析
// EF Core 自动追踪 existing 的变化,无需手动 Update
}
}
foreach (var dbItem in dbItems)
else
{
if (!remoteMap.ContainsKey(dbItem.Uri))
{
toDelete.Add(dbItem);
}
// 新增
// 注意:如果 Id 是自增的,不要手动赋值 Id除非是 Guid
context.FilesIndex.Add(remote);
}
}
if (toInsert.Count > 0) conn.InsertAll(toInsert);
if (toUpdate.Count > 0) conn.UpdateAll(toUpdate);
if (toDelete.Count > 0)
// 4. 处理 删除 (数据库有,远端没有)
foreach (var dbItem in dbItems)
{
if (!remoteUris.Contains(dbItem.Uri))
{
foreach (var item in toDelete) conn.Delete(item);
context.FilesIndex.Remove(dbItem);
}
});
}
var finalItems = await _db.Table<FileCacheEntity>()
await context.SaveChangesAsync();
await transaction.CommitAsync();
// 5. 返回最新数据
// 这里的 dbItems 已经被 Update 更新了内存状态,但 Remove 的还在列表里Add 的不在列表里
// 所以最稳妥的是重新查一次,或者手动维护列表。为了准确性,重新查询 (AsNoTracking)
var finalItems = await context.FilesIndex
.AsNoTracking()
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToListAsync();
@@ -204,37 +182,34 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
}
public async Task UpdateMetadataAsync(FileCacheEntity entity)
public async Task UpdateMetadataAsync(FilesIndexItem entity)
{
// 现在的实体已经包含了完整信息,直接 Update 即可
// 我们只需要确保 Where 子句用的是主键或者 Uri
using var context = await _contextFactory.CreateDbContextAsync();
// 简化版 SQL直接用 ORM 的 Update
// 但因为 entity 对象可能包含一些不应该被覆盖的旧数据(如果多线程操作),
// 手写 SQL 只更新 Metadata 字段更安全。
string sql = @"
UPDATE FileCache
SET
Title = ?, Artists = ?, Album = ?,
Year = ?, Bitrate = ?, SampleRate = ?, BitDepth = ?,
Duration = ?, AudioFormatName = ?, AudioFormatShortName = ?, Encoder = ?,
EmbeddedLyrics = ?, LocalAlbumArtPath = ?,
IsMetadataParsed = 1
WHERE Id = ?"; // 推荐用 Id (主键) 最快,如果没有 Id 则用 Uri
await _db.ExecuteAsync(sql,
entity.Title, entity.Artists, entity.Album,
entity.Year, entity.Bitrate, entity.SampleRate, entity.BitDepth,
entity.Duration, entity.AudioFormatName, entity.AudioFormatShortName, entity.Encoder,
entity.EmbeddedLyrics, entity.LocalAlbumArtPath,
entity.Id // WHERE Id = ?
);
// 使用 EF Core 7.0+ 的 ExecuteUpdateAsync 高效更新
// 这会直接生成 UPDATE SQL不经过内存加载性能极高
await context.FilesIndex
.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.Album, entity.Album)
.SetProperty(p => p.Year, entity.Year)
.SetProperty(p => p.Bitrate, entity.Bitrate)
.SetProperty(p => p.SampleRate, entity.SampleRate)
.SetProperty(p => p.BitDepth, entity.BitDepth)
.SetProperty(p => p.Duration, entity.Duration)
.SetProperty(p => p.AudioFormatName, entity.AudioFormatName)
.SetProperty(p => p.AudioFormatShortName, entity.AudioFormatShortName)
.SetProperty(p => p.Encoder, entity.Encoder)
.SetProperty(p => p.EmbeddedLyrics, entity.EmbeddedLyrics)
.SetProperty(p => p.LocalAlbumArtPath, entity.LocalAlbumArtPath)
.SetProperty(p => p.IsMetadataParsed, true)
);
}
public async Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity)
public async Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FilesIndexItem entity)
{
// 直接传递实体给 Provider
return await provider.OpenReadAsync(entity);
}
@@ -258,7 +233,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (_activeScanTokens.TryGetValue(folder.Id, out var activeScanCts))
{
activeScanCts.Cancel();
// 强制终止正在扫描的操作
}
try
@@ -272,17 +246,16 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
});
await InitializeAsync();
using var context = await _contextFactory.CreateDbContextAsync();
await _dbLock.WaitAsync();
try
await context.FilesIndex
.Where(x => x.MediaFolderId == folder.Id)
.ExecuteDeleteAsync();
// VACUUM 是 SQLite 特有的命令
if (context.Database.IsSqlite())
{
await _db.ExecuteAsync("DELETE FROM FileCache WHERE MediaFolderId = ?", folder.Id);
await _db.ExecuteAsync("VACUUM");
}
finally
{
_dbLock.Release();
await context.Database.ExecuteSqlRawAsync("VACUUM");
}
}
finally
@@ -325,8 +298,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
_dispatcherQueue.TryEnqueue(() => folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceConnecting"));
await InitializeAsync();
using var fs = folder.CreateFileSystem();
if (fs == null || !await fs.ConnectAsync())
{
@@ -340,8 +311,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
_dispatcherQueue.TryEnqueue(() => folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceFetchingFileList"));
var filesToProcess = new List<FileCacheEntity>();
var foldersToScan = new Queue<FileCacheEntity?>();
var filesToProcess = new List<FilesIndexItem>();
var foldersToScan = new Queue<FilesIndexItem?>();
foldersToScan.Enqueue(null); // 根目录
while (foldersToScan.Count > 0)
@@ -349,7 +320,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (scanCts.Token.IsCancellationRequested) return;
var currentParent = foldersToScan.Dequeue();
var items = await GetFilesAsync(fs, currentParent, folder.Id, forceRefresh: true);
foreach (var item in items)
@@ -414,10 +384,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (track.Duration > 0)
{
// 保存封面
string? artPath = await SaveAlbumArtToDiskAsync(track);
// 填充实体
item.Title = track.Title;
item.Artists = track.Artist;
item.Album = track.Album;
@@ -429,7 +397,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
item.AudioFormatName = track.AudioFormatName;
item.AudioFormatShortName = track.AudioFormatShortName;
item.Encoder = track.Encoder;
item.EmbeddedLyrics = track.RawLyrics; // 内嵌歌词
item.EmbeddedLyrics = track.RawLyrics;
item.LocalAlbumArtPath = artPath;
item.IsMetadataParsed = true;
}
@@ -441,7 +409,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
using var reader = new StreamReader(stream);
string content = await reader.ReadToEndAsync();
item.EmbeddedLyrics = content;
item.IsMetadataParsed = true;
}
@@ -449,15 +416,10 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (item.IsMetadataParsed)
{
await _dbLock.WaitAsync(token);
try
{
await UpdateMetadataAsync(item);
}
finally
{
_dbLock.Release();
}
// 更新操作:直接调用 UpdateMetadataAsync
// 此时不需要 _dbLock因为 UpdateMetadataAsync 内部会 CreateDbContextAsync
// 而 _folderScanLock 已经保证了当前文件夹扫描的独占性
await UpdateMetadataAsync(item);
}
}
catch (Exception ex)
@@ -488,7 +450,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
finally
{
_folderScanLock.Release();
_activeScanTokens.TryRemove(folder.Id, out _);
_dispatcherQueue.TryEnqueue(() =>
@@ -499,23 +460,22 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
}
public async Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
public async Task<List<FilesIndexItem>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
{
await InitializeAsync();
if (enabledConfigIds == null || !enabledConfigIds.Any())
{
return new List<FileCacheEntity>();
return new List<FilesIndexItem>();
}
var idList = enabledConfigIds.ToList();
// SQL 逻辑: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
var results = await _db.Table<FileCacheEntity>()
using var context = await _contextFactory.CreateDbContextAsync();
// SQL: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
return await context.FilesIndex
.AsNoTracking()
.Where(x => x.IsMetadataParsed && idList.Contains(x.MediaFolderId))
.ToListAsync();
return results;
}
public void StartAllFolderTimers()
@@ -575,11 +535,11 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}, newCts.Token);
}
// 参数为 string parentUri表示哪个文件夹的内容变了
public event EventHandler<string>? FolderUpdated;
private async Task<string?> SaveAlbumArtToDiskAsync(ExtendedTrack track)
{
// 代码未变,纯 IO 操作
var picData = track.AlbumArtByteArray;
if (picData == null || picData.Length == 0) return null;
@@ -632,6 +592,5 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
}
}
}
}

View File

@@ -10,12 +10,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
public interface IFileSystemService
{
/// <summary>
/// 初始化(连接)数据库
/// </summary>
/// <returns></returns>
Task InitializeAsync();
/// <summary>
/// 从数据库拉取文件(必要时需要从远端/本地同步至数据库)
/// </summary>
@@ -24,7 +18,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// <param name="configId"></param>
/// <param name="forceRefresh">强制需要从远端/本地同步至数据库</param>
/// <returns></returns>
Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false);
Task<List<FilesIndexItem>> GetFilesAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId, bool forceRefresh = false);
/// <summary>
/// 打开文件(通过远端/本地流)
@@ -32,14 +26,14 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// <param name="provider"></param>
/// <param name="entity"></param>
/// <returns></returns>
Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity);
Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FilesIndexItem entity);
/// <summary>
/// 更新数据库(单个文件)
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
Task UpdateMetadataAsync(FileCacheEntity entity);
Task UpdateMetadataAsync(FilesIndexItem entity);
/// <summary>
/// 从数据库删除
@@ -60,7 +54,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// </summary>
/// <param name="enabledConfigIds"></param>
/// <returns></returns>
Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds);
Task<List<FilesIndexItem>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds);
void StartAllFolderTimers();

View File

@@ -14,13 +14,13 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// </summary>
/// <param name="parentFolder"></param>
/// <returns></returns>
Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null);
Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null);
/// <summary>
/// 打开流
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
Task<Stream?> OpenReadAsync(FileCacheEntity file);
Task<Stream?> OpenReadAsync(FilesIndexItem file);
Task DisconnectAsync();
}
}

View File

@@ -42,22 +42,14 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public async Task<bool> ConnectAsync()
{
try
{
if (_client.IsConnected) return true;
await _client.AutoConnect(); // AutoConnect 会自动尝试 FTP/FTPS
return _client.IsConnected;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"FTP连接失败: {ex.Message}");
return false;
}
if (_client.IsConnected) return true;
await _client.AutoConnect(); // AutoConnect 会自动尝试 FTP/FTPS
return _client.IsConnected;
}
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FileCacheEntity>();
var result = new List<FilesIndexItem>();
string targetServerPath;
Uri parentUri;
@@ -104,7 +96,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
Path = item.FullName
};
result.Add(new FileCacheEntity
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
// 如果是根目录扫描ParentUri 用 Config 的;否则用传入文件夹的
@@ -130,7 +122,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
return result;
}
public async Task<Stream?> OpenReadAsync(FileCacheEntity file)
public async Task<Stream?> OpenReadAsync(FilesIndexItem file)
{
if (file == null) return null;

View File

@@ -20,12 +20,20 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public Task<bool> ConnectAsync()
{
return Task.FromResult(Directory.Exists(_rootLocalPath));
var isExisted = Directory.Exists(_rootLocalPath);
if (isExisted)
{
return Task.FromResult(true);
}
else
{
throw new FileNotFoundException(null, _rootLocalPath);
}
}
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FileCacheEntity>();
var result = new List<FilesIndexItem>();
string targetPath;
string parentUriString;
@@ -70,7 +78,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
size = fi.Length;
}
result.Add(new FileCacheEntity
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id, // 关联配置 ID
@@ -94,7 +102,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
return await Task.FromResult(result);
}
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
public async Task<Stream?> OpenReadAsync(FilesIndexItem entity)
{
if (entity == null) return null;

View File

@@ -42,29 +42,22 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public async Task<bool> ConnectAsync()
{
try
{
_client = new SMB2Client();
_client = new SMB2Client();
// 连接主机
bool connected = _client.Connect(_config.UriHost, SMBTransportType.DirectTCPTransport);
if (!connected) return false;
// 连接主机
bool connected = _client.Connect(_config.UriHost, SMBTransportType.DirectTCPTransport);
if (!connected) return false;
// 登录
var status = _client.Login(string.Empty, _config.UserName, _config.Password);
if (status != NTStatus.STATUS_SUCCESS) return false;
// 登录
var status = _client.Login(string.Empty, _config.UserName, _config.Password);
if (status != NTStatus.STATUS_SUCCESS) return false;
// 连接共享目录 (TreeConnect)
// SMBLibrary 必须先连接到 Share后续所有文件操作都是基于这个 Share 的相对路径
if (string.IsNullOrEmpty(_shareName)) return false;
// 连接共享目录 (TreeConnect)
// SMBLibrary 必须先连接到 Share后续所有文件操作都是基于这个 Share 的相对路径
if (string.IsNullOrEmpty(_shareName)) return false;
_fileStore = _client.TreeConnect(_shareName, out status);
return status == NTStatus.STATUS_SUCCESS;
}
catch (Exception)
{
return false;
}
_fileStore = _client.TreeConnect(_shareName, out status);
return status == NTStatus.STATUS_SUCCESS;
}
/// <summary>
@@ -74,9 +67,9 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
/// 传入要列出的文件夹实体。
/// 如果传入 null则默认列出 MediaFolder 配置的根目录。
/// </param>
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FileCacheEntity>();
var result = new List<FilesIndexItem>();
if (_fileStore == null) return result;
string smbPath = GetPathRelativeToShare(parentFolder);
@@ -128,7 +121,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
var baseUri = new Uri(parentUriString);
var newUri = new Uri(baseUri, item.FileName);
result.Add(new FileCacheEntity
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
@@ -155,7 +148,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
/// 打开文件流
/// </summary>
/// <param name="file">只需要传入文件实体即可</param>
public async Task<Stream?> OpenReadAsync(FileCacheEntity file)
public async Task<Stream?> OpenReadAsync(FilesIndexItem file)
{
if (_fileStore == null || file == null) return null;
@@ -181,7 +174,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
_client?.Disconnect();
}
private string GetPathRelativeToShare(FileCacheEntity? entity)
private string GetPathRelativeToShare(FilesIndexItem? entity)
{
Uri targetUri;

View File

@@ -36,22 +36,13 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public async Task<bool> ConnectAsync()
{
try
{
// 测试连接Propfind 请求配置的根路径
// GetStandardUri 已经包含了用户设置的路径
var result = await _client.Propfind(_config.GetStandardUri().AbsoluteUri);
return result.IsSuccessful;
}
catch
{
return false;
}
var result = await _client.Propfind(_config.GetStandardUri().AbsoluteUri);
return result.IsSuccessful;
}
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var list = new List<FileCacheEntity>();
var list = new List<FilesIndexItem>();
Uri targetUri;
if (parentFolder == null)
@@ -98,7 +89,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
if (string.IsNullOrEmpty(extension) || !FileHelper.AllSupportedExtensions.Contains(extension)) continue;
}
list.Add(new FileCacheEntity
list.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
@@ -118,7 +109,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
return list;
}
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
public async Task<Stream?> OpenReadAsync(FilesIndexItem entity)
{
if (entity == null) return null;

View File

@@ -279,7 +279,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
int maxScore = 0;
FileCacheEntity? bestFileEntity = null;
FilesIndexItem? bestFileEntity = null;
MediaFolder? bestFolderConfig = null;
var lyricsSearchResult = new LyricsSearchResult();
@@ -345,7 +345,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FileCacheEntity? bestFile = null;
FilesIndexItem? bestFile = null;
int maxScore = 0;
foreach (var item in allFiles)

View File

@@ -203,6 +203,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
return;
}
@@ -213,6 +214,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
}
else
@@ -239,7 +241,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
@@ -254,6 +255,15 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_ => false,
};
}
if (CurrentIsPlaying)
{
_scrobbleStopwatch.Start();
}
else
{
_scrobbleStopwatch.Stop();
}
}));
}
@@ -333,7 +343,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_scrobbleStopwatch.Elapsed.TotalMilliseconds >= (lastSong.DurationMs / 2))
{
// 写入本地播放记录
var playHistoryItem = CurrentSongInfo.ToPlayHistoryItem(_scrobbleStopwatch.Elapsed.TotalMilliseconds);
var playHistoryItem = lastSong.ToPlayHistoryItem(_scrobbleStopwatch.Elapsed.TotalMilliseconds);
if (playHistoryItem != null)
{
// 后台
@@ -469,6 +479,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
_discordService.Disable();

View File

@@ -9,7 +9,6 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
{
public interface IPlayHistoryService
{
Task InitializeAsync();
Task AddLogAsync(PlayHistoryItem item);
Task<List<PlayHistoryItem>> GetRecentLogsAsync(int limit = 50);
Task<List<PlayHistoryItem>> GetLogsByDateRangeAsync(DateTime start, DateTime end);

View File

@@ -1,158 +1,155 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Models.Stats;
using SQLite;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.PlayHistoryService
{
public class PlayHistoryService : IPlayHistoryService
{
private SQLiteAsyncConnection _db;
private readonly string _dbPath;
private readonly IDbContextFactory<PlayHistoryDbContext> _contextFactory;
public PlayHistoryService()
public PlayHistoryService(IDbContextFactory<PlayHistoryDbContext> contextFactory)
{
_dbPath = PathHelper.PlayHistoryPath;
_contextFactory = contextFactory;
}
public async Task InitializeAsync()
{
if (_db != null) return;
_db = new SQLiteAsyncConnection(_dbPath);
await _db.CreateTableAsync<PlayHistoryItem>();
}
/// <summary>
/// 添加一条播放记录
/// </summary>
public async Task AddLogAsync(PlayHistoryItem item)
{
await InitializeAsync();
// 再次确保这里是 UTC 时间,方便跨时区统计
using var context = await _contextFactory.CreateDbContextAsync();
// 确保 UTC
if (item.StartedAt.Kind != DateTimeKind.Utc)
{
item.StartedAt = item.StartedAt.ToUniversalTime();
}
await _db.InsertAsync(item);
context.PlayHistory.Add(item);
await context.SaveChangesAsync();
}
/// <summary>
/// 获取最近的播放记录 (用于“最近播放”列表)
/// </summary>
public async Task<List<PlayHistoryItem>> GetRecentLogsAsync(int limit = 50)
{
await InitializeAsync();
return await _db.Table<PlayHistoryItem>()
.OrderByDescending(x => x.StartedAt)
.Take(limit)
.ToListAsync();
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking() // 读操作,不需要追踪状态,提升性能
.OrderByDescending(x => x.StartedAt)
.Take(limit)
.ToListAsync();
}
/// <summary>
/// 获取特定时间段的所有原始记录 (用于生成复杂的图表,如 Hourly Heatmap)
/// </summary>
public async Task<List<PlayHistoryItem>> GetLogsByDateRangeAsync(DateTime start, DateTime end)
{
await InitializeAsync();
return await _db.Table<PlayHistoryItem>()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.ToListAsync();
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.ToListAsync();
}
/// <summary>
/// 统计时间段内 Top N 歌曲
/// </summary>
public async Task<List<SongPlayCount>> GetTopSongsAsync(DateTime start, DateTime end, int limit = 10)
{
await InitializeAsync();
using var context = await _contextFactory.CreateDbContextAsync();
// SQLite 语法: Group By Title 和 Artist
string query = @"
SELECT Title, Artist, COUNT(*) as PlayCount
FROM PlayHistory
WHERE StartedAt >= ? AND StartedAt <= ?
GROUP BY Title, Artist
ORDER BY PlayCount DESC
LIMIT ?";
return await _db.QueryAsync<SongPlayCount>(query, start, end, limit);
// EF Core 会自动将这个 LINQ 翻译成高效的 GROUP BY SQL
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => new { x.Title, x.Artist }) // 组合分组
.Select(g => new SongPlayCount
{
Title = g.Key.Title,
Artist = g.Key.Artist,
PlayCount = g.Count()
})
.OrderByDescending(x => x.PlayCount)
.Take(limit)
.ToListAsync();
}
/// <summary>
/// 统计时间段内 Top N 歌手
/// </summary>
public async Task<List<ArtistPlayCount>> GetTopArtistsAsync(DateTime start, DateTime end, int limit = 10)
{
await InitializeAsync();
using var context = await _contextFactory.CreateDbContextAsync();
// 同时统计播放次数和总播放时长(秒)
string query = @"
SELECT Artist, COUNT(*) as PlayCount, SUM(DurationPlayedMs)/1000.0 as TotalDurationSeconds
FROM PlayHistory
WHERE StartedAt >= ? AND StartedAt <= ?
GROUP BY Artist
ORDER BY PlayCount DESC
LIMIT ?";
return await _db.QueryAsync<ArtistPlayCount>(query, start, end, limit);
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => x.Artist)
.Select(g => new ArtistPlayCount
{
Artist = g.Key,
PlayCount = g.Count(),
// 注意SQLite 存储 double 精度,这里求和后转秒
TotalDurationSeconds = g.Sum(x => x.DurationPlayedMs) / 1000.0
})
.OrderByDescending(x => x.PlayCount)
.Take(limit)
.ToListAsync();
}
/// <summary>
/// 获取总听歌时长
/// </summary>
public async Task<TimeSpan> GetTotalListeningDurationAsync(DateTime start, DateTime end)
{
await InitializeAsync();
using var context = await _contextFactory.CreateDbContextAsync();
var result = await _db.ExecuteScalarAsync<double>(
"SELECT SUM(DurationPlayedMs) FROM PlayHistory WHERE StartedAt >= ? AND StartedAt <= ?",
start, end);
var totalMs = await context.PlayHistory
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.SumAsync(x => Math.Min(x.DurationPlayedMs, x.TotalDurationMs)); // 防止超过歌曲本身时长
return TimeSpan.FromMilliseconds(result);
return TimeSpan.FromMilliseconds(totalMs);
}
/// <summary>
/// 获取播放器来源分布
/// </summary>
public async Task<List<PlayerStats>> GetPlayerDistributionAsync(DateTime start, DateTime end)
{
await InitializeAsync();
using var context = await _contextFactory.CreateDbContextAsync();
string query = @"
SELECT PlayerId, COUNT(*) as Count
FROM PlayHistory
WHERE StartedAt >= ? AND StartedAt <= ?
GROUP BY PlayerId
ORDER BY Count DESC";
return await _db.QueryAsync<PlayerStats>(query, start, end);
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => x.PlayerId)
.Select(g => new PlayerStats
{
PlayerId = g.Key,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.ToListAsync();
}
public async Task DeleteLogAsync(int id)
{
await InitializeAsync();
await _db.DeleteAsync<PlayHistoryItem>(id);
using var context = await _contextFactory.CreateDbContextAsync();
// EF Core 删除需要先查询,或者使用 ExecuteDeleteAsync (EF Core 7+)
// 写法 1 (传统):
// var item = await context.PlayHistory.FindAsync(id);
// if (item != null) { context.PlayHistory.Remove(item); await context.SaveChangesAsync(); }
// 写法 2 (EF Core 7.0+ 高效写法,直接生成 DELETE SQL):
await context.PlayHistory
.Where(x => x.Id == id)
.ExecuteDeleteAsync();
}
public async Task ClearHistoryAsync()
{
await InitializeAsync();
await _db.DeleteAllAsync<PlayHistoryItem>();
using var context = await _contextFactory.CreateDbContextAsync();
// 高效清空表
await context.PlayHistory.ExecuteDeleteAsync();
}
public async Task GenerateTestDataAsync(int count = 100)
{
// 这里的逻辑稍微重构了一下,使用批量插入提升性能
var random = new Random();
// === 1. 扩充的数据池 (涵盖多语言、多风格) ===
var presetSongs = new List<(string Title, string Artist, string Album)>
{
// --- 欧美流行 ---
("Anti-Hero", "Taylor Swift", "Midnights"),
("Cruel Summer", "Taylor Swift", "Lover"),
("Blank Space", "Taylor Swift", "1989"),
@@ -165,8 +162,6 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
("Bad Guy", "Billie Eilish", "When We All Fall Asleep, Where Do We Go?"),
("Flowers", "Miley Cyrus", "Endless Summer Vacation"),
("Stay", "The Kid LAROI & Justin Bieber", "F*ck Love 3: Over You"),
// --- 华语流行 ---
("七里香", "周杰伦", "七里香"),
("晴天", "周杰伦", "叶惠美"),
("一路向北", "周杰伦", "11月的肖邦"),
@@ -180,8 +175,6 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
("泡沫", "G.E.M. 邓紫棋", "Xposed"),
("因为爱情", "王菲 & 陈奕迅", "Stranger Under My Skin"),
("红豆", "王菲", "唱游"),
// --- 摇滚/经典 ---
("Bohemian Rhapsody", "Queen", "A Night at the Opera"),
("Don't Stop Me Now", "Queen", "Jazz"),
("Numb", "Linkin Park", "Meteora"),
@@ -190,8 +183,6 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
("Viva La Vida", "Coldplay", "Viva La Vida"),
("Smells Like Teen Spirit", "Nirvana", "Nevermind"),
("Hotel California", "Eagles", "Hotel California"),
// --- 日韩/二次元 ---
("Lemon", "米津玄師", "Lemon"),
("Kick Back", "米津玄師", "KICK BACK"),
("アイドル", "YOASOBI", "アイドル"),
@@ -201,64 +192,49 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
("Butter", "BTS", "Butter"),
("How You Like That", "BLACKPINK", "The Album"),
("Ditto", "NewJeans", "OMG"),
// --- 电子/纯音乐 ---
("Get Lucky", "Daft Punk", "Random Access Memories"),
("The Nights", "Avicii", "The Days / Nights"),
("Summer", "Calvin Harris", "Motion"),
};
// 模拟播放器分布:假设 Spotify 和 MusicBee 用得最多
var playerIds = new[] {
"Spotify", "Spotify", "Spotify", // 权重高
"MusicBee", "MusicBee",
"QQMusic",
"NeteaseCloudMusic",
"AppleMusic"
};
var playerIds = new[] { "Spotify", "Spotify", "Spotify", "MusicBee", "MusicBee", "QQMusic", "NeteaseCloudMusic", "AppleMusic" };
int addedCount = 0;
var batchList = new List<PlayHistoryItem>();
// 我们用 while 循环,直到凑够 count 条有效的记录为止
while (addedCount < count)
// 我们尝试生成 count 条有效数据
// 为了防止死循环,加个硬上限
int attempts = 0;
while (batchList.Count < count && attempts < count * 5)
{
// 随机挑一首歌
attempts++;
var song = presetSongs[random.Next(presetSongs.Count)];
// 随机挑一个播放器
var playerId = playerIds[random.Next(playerIds.Length)];
// 生成时间:过去 60 天内均匀分布
var daysBack = random.Next(0, 60);
var daysBack = random.Next(0, 365);
var hoursBack = random.Next(0, 24);
var minutesBack = random.Next(0, 60);
// 加一点随机秒数,防止时间完全重叠
var secondsBack = random.Next(0, 60);
var startedAt = DateTime.Now
var startedAt = DateTime.UtcNow // 直接用 UTC
.AddDays(-daysBack)
.AddHours(-hoursBack)
.AddMinutes(-minutesBack)
.AddSeconds(-secondsBack);
// 歌曲总时长 (3分钟 - 5分钟)
var totalDurationMs = random.Next(180, 300) * 1000.0;
// 模拟听歌习惯:
// 70% 的概率是听完的 (0.9 - 1.0)
// 20% 的概率是切歌 (0.3 - 0.8)
// 10% 的概率是刚听就切了 (0.05 - 0.3)
double playedRatio;
double roll = random.NextDouble();
if (roll > 0.3) playedRatio = 0.9 + (random.NextDouble() * 0.1); // 听完
else if (roll > 0.1) playedRatio = 0.3 + (random.NextDouble() * 0.5); // 听一半
else playedRatio = 0.05 + (random.NextDouble() * 0.25); // 秒切
if (roll > 0.3) playedRatio = 0.9 + (random.NextDouble() * 0.1);
else if (roll > 0.1) playedRatio = 0.3 + (random.NextDouble() * 0.5);
else playedRatio = 0.05 + (random.NextDouble() * 0.25);
var playedDurationMs = totalDurationMs * playedRatio;
// 只有听了一半以上的才算作记录
if (playedDurationMs >= (totalDurationMs / 2))
{
var item = new PlayHistoryItem
batchList.Add(new PlayHistoryItem
{
Title = song.Title,
Artist = song.Artist,
@@ -267,13 +243,16 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
StartedAt = startedAt,
TotalDurationMs = totalDurationMs,
DurationPlayedMs = playedDurationMs
};
await AddLogAsync(item);
addedCount++; // 只有成功写入才计数
});
}
}
}
if (batchList.Count > 0)
{
using var context = await _contextFactory.CreateDbContextAsync();
await context.PlayHistory.AddRangeAsync(batchList);
await context.SaveChangesAsync();
}
}
}
}
}

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>كل الموسيقى</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>البيانات قيد المعالجة. يمكنك التحقق من التقدم المفصل في الإعدادات.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>مزامنة مكتبة الوسائط جارية...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>توجد مشكلة في مزامنة مكتبة الوسائط</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>مسح قائمة الانتظار</value>
@@ -700,7 +703,7 @@
<value>كمية الضبابية (Blur)</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>يشمل ملفات السجل وذاكرة التخزين المؤقت للكلمات عبر الشبكة</value>
<value>يتضمن ملفات السجلات، وذاكرة التخزين المؤقت للكلمات عبر الإنترنت</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>ذاكرة التخزين المؤقت (Cache)</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>الخروج من البرنامج عند إغلاق نافذة الكلمات</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>تصدير تاريخ اللعب</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>تصدير الإعدادات</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>مدير الإعدادات</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>تاريخ اللعب</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>تصفح مركز مشاركة الموارد عبر الإنترنت</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Alle Musikstücke</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Die Daten werden gerade verarbeitet. Sie können den genauen Fortschritt in den Einstellungen überprüfen.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Synchronisierung der Medienbibliothek läuft...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Es gibt ein Problem mit der Synchronisierung der Medienbibliothek</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Warteschlange leeren</value>
@@ -700,7 +703,7 @@
<value>Unschärfe-Stärke</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Einschließlich Protokolldateien, Online-Songtext-Cache</value>
<value>Enthält Protokolldateien, Online-Liedtext-Cache</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>App beenden, wenn Songtext-Fenster geschlossen wird</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Spielverlauf exportieren</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Einstellungen exportieren</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Einstellungsmanager</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Geschichte spielen</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Online Share Hub durchsuchen</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>All Music</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>The data is being processed. You can check the detailed progress in the settings.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Media Library sync in progress...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>There is a problem with Media Library sync</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Clear queue</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Exit app when lyrics window is closed</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Export play history</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Export Settings</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Settings Manager</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Play History</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Browse Online Share Hub</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Toda la música</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Los datos se están procesando. Puedes consultar el progreso detallado en los ajustes.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sincronización de la biblioteca multimedia en curso...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Hay un problema con la sincronización de la biblioteca multimedia</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Borrar cola</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Salir de la app al cerrar la ventana de letras</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exportar historial de jugadas</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exportar configuración</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestor de configuración</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Historia del juego</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Explorar Centro de recursos en línea</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Toute la musique</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Les données sont en cours de traitement. Vous pouvez vérifier l'état d'avancement détaillé dans les paramètres.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Synchronisation de la médiathèque en cours...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Il y a un problème avec la synchronisation de la médiathèque</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Vider la file d'attente</value>
@@ -700,7 +703,7 @@
<value>Quantité de flou</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Comprend les fichiers journaux, le cache des paroles en ligne</value>
<value>Inclut les fichiers journaux, le cache des paroles en ligne</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Quitter l'app à la fermeture de la fenêtre des paroles</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exporter l'historique des jeux</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exporter les paramètres</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestionnaire de paramètres</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Histoire du jeu</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Parcourir le Hub de partage en ligne</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>सभी गाने</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>डेटा प्रोसेस हो रहा है। आप सेटिंग्स में डिटेल्ड प्रोग्रेस देख सकते हैं।</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>मीडिया लाइब्रेरी सिंक चल रहा है...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>मीडिया लाइब्रेरी सिंक में कोई समस्या है</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>प्लेइंग कतार साफ़ करें</value>
@@ -700,7 +703,7 @@
<value>धुंधलापन मात्रा</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>लॉग फ़ाइलें, नेटवर्क बोल कैश शामिल हैं</value>
<value>लॉग फ़ाइलें, ऑनलाइन लिरिक्स कैश शामिल हैं</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>कैश</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>बोल विंडो बंद होने पर बाहर निकलें</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>प्ले इतिहास निर्यात करें</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>सेटिंग्स निर्यात करें</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>सेटिंग्स प्रबंधक</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>प्ले इतिहास</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>ऑनलाइन संसाधन शेयरिंग हब ब्राउज़ करें</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Semua Musik</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Data sedang diproses. Anda dapat memeriksa kemajuan terperinci dalam pengaturan.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sinkronisasi Perpustakaan Media sedang berlangsung...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Ada masalah dengan sinkronisasi Perpustakaan Media</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Bersihkan antrean putar</value>
@@ -700,7 +703,7 @@
<value>Tingkat Keburaman</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Termasuk file log, cache lirik jaringan</value>
<value>Termasuk file log, cache lirik online</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Keluar dari program saat jendela lirik ditutup</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Riwayat permainan ekspor</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Ekspor Pengaturan</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Manajer Pengaturan</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Riwayat Bermain</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Jelajahi Hub Berbagi Sumber Daya Online</value>
</data>

View File

@@ -169,7 +169,7 @@
<value>LX Music サーバーに接続できません。「設定」-「再生ソース」-「LX Music」-「LX Music サーバー」に移動し、リンクが正しく入力されているか確認してください</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>キャッシュをクリーニング中...</value>
<value>キャッシュをクリ中...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>接続に失敗しました</value>
@@ -181,19 +181,19 @@
<value>ファイルリストを取得しています...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
<value>解析中...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>キャッシュクリーン準備中</value>
<value>キャッシュクリアを準備中...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>準備完了</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>ルートディレクトリのパスが検出された。フルディスクインデックスにはメディア以外のファイルが多数含まれている可能性があり、スキャンに時間がかかりすぎる。特定のサブディレクトリを指定することを推奨する。</value>
<value>ルートディレクトリが指定されました。フルディスクインデックス作成には大量の非メディアファイルが含まれる可能性があり、スキャンに時間がかかる恐れがあります。特定のサブディレクトリを指定することをお勧めします。</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>スキャンを待っています...</value>
<value>スキャン準備中...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>全画面モード</value>
@@ -259,13 +259,13 @@
<value>歌詞の言語</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌詞提供</value>
<value>歌詞提供</value>
</data>
<data name="LyricsPageLyricsSearch.Text" xml:space="preserve">
<value>歌詞検索</value>
</data>
<data name="LyricsPageLyricsSettings.Text" xml:space="preserve">
<value>歌詞ウィンドウのショートカット</value>
<value>歌詞ウィンドウのショートカット設定</value>
</data>
<data name="LyricsPageMatchPercentage.Header" xml:space="preserve">
<value>一致率</value>
@@ -289,7 +289,7 @@
<value>翻訳提供元</value>
</data>
<data name="LyricsPageTransliterationProviderPrefix.Header" xml:space="preserve">
<value>翻訳提供元</value>
<value>ルビ提供元</value>
</data>
<data name="LyricsParseError" xml:space="preserve">
<value>歌詞の解析に失敗しました</value>
@@ -307,7 +307,7 @@
<value>曲の長さ</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>* 変更は保存後すぐに反映され、マッピング情報とターゲットの歌詞を使用して後続の曲の歌詞が取得されます。 「Instrumental」としてマークすると、「Instrumental」プレースホルダ歌詞に直接戻ります。 リセットすると元のデータの検索が復元されます。</value>
<value>* 変更は保存後すぐに反映され、以降はマッピング情報に基づいた歌詞取得が行われます。「インストゥルメンタル」としてマークすると、専用のプレースホルダ歌詞に切り替わります。リセットを実行すると元のデータ取得状態に戻ります。</value>
</data>
<data name="LyricsSearchControlIgnoreCache.Header" xml:space="preserve">
<value>検索時にキャッシュを無視する</value>
@@ -316,7 +316,7 @@
<value>マッピング先</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>インストとしてマーク</value>
<value>インストゥルメンタルとしてマーク</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>見つかりません</value>
@@ -367,10 +367,10 @@
<value>さらにモードを追加するには「設定」に移動します</value>
</data>
<data name="LyricsWindowSwitchWindowTitle" xml:space="preserve">
<value>歌詞ウィンドウ切替</value>
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>アルバムアートエリアのみ表示</value>
<value>アルバムアートのみ表示</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>歌詞のみ表示</value>
@@ -394,7 +394,7 @@
<value>ローカルフォルダー</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>名</value>
<value>名</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>今すぐ同期</value>
@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>すべてのミュージック</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>データ処理中です。詳細な進捗状況は設定で確認できます。</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>メディアライブラリの同期中...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>メディアライブラリの同期に問題が発生しました</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>キューをクリア</value>
@@ -520,22 +523,22 @@
<value>ミュージックギャラリー - BetterLyrics</value>
</data>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>自動同期周波数</value>
<value>自動同期の頻度</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>一切なし</value>
<value>なし</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>毎日</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>15分ごと</value>
<value>15分間隔</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>1時間ごと</value>
<value>1時間間隔</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>6時間ごと</value>
<value>6時間間隔</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>狭い表示モード</value>
@@ -553,13 +556,13 @@
<value>個人情報保護方針</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>ブラウズ</value>
<value>参照</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>名称</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>空のままにしておくと、自動的にデフォルトの名前が生成され。</value>
<value>空のままにすると、デフォルトの名前が自動生成されます。</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>パスワード</value>
@@ -568,7 +571,7 @@
<value>パス</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>指定されたフォルダのパスが見つかりません</value>
<value>指定されたパスが見つかりません</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>パスが必要</value>
@@ -610,7 +613,7 @@
<value>デザイン参考</value>
</data>
<data name="SettingsPage3DLyrics.Header" xml:space="preserve">
<value>[実験的] 深度エフェクト</value>
<value>[実験的] 奥行きのエフェクト</value>
</data>
<data name="SettingsPage3DLyricsDepth.Header" xml:space="preserve">
<value>深度</value>
@@ -685,10 +688,10 @@
<value>起動時にデフォルトの歌詞ウィンドウを自動的に開く</value>
</data>
<data name="SettingsPageAutoOpenMusicGalleryWindow.Header" xml:space="preserve">
<value>起動時にギャラリーウィンドウを開く</value>
<value>起動時にミュージックギャラリーウィンドウを開く</value>
</data>
<data name="SettingsPageAutoPlayWhenOpenMusicGalleryWindow.Header" xml:space="preserve">
<value>ギャラリーウィンドウを開いたときに自動的に再生を再開</value>
<value>ミュージックギャラリーを開いたときに自動的に再生を再開する</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動起動</value>
@@ -709,7 +712,7 @@
<value>中央揃え</value>
</data>
<data name="SettingsPageCheckShortcut.Content" xml:space="preserve">
<value>キー割り当ての確認</value>
<value>ショートカットキーの確認</value>
</data>
<data name="SettingsPageChinese.Header" xml:space="preserve">
<value>ピンインルビ</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>歌詞ウィンドウを閉じたときにアプリを終了する</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>再生履歴をエクスポート</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>設定をエクスポート</value>
</data>
@@ -880,7 +886,7 @@
<value>定期チェックで最前面表示を強制維持します</value>
</data>
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>常に上部に強制的に表示</value>
<value>常にトップに強制的に表示</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>現在の歌詞に文字単位の情報がない場合でも、文字単位の歌詞シミュレーションをします</value>
@@ -1015,7 +1021,7 @@
<value>現在の行の位置</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞エフェクト</value>
<value>歌詞エフェクト</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超極太</value>
@@ -1102,7 +1108,7 @@
<value>順次:以下のリストの優先順位に従って検索し、最初に見つかった結果を返します。ベストマッチ:有効なすべてのソースを検索し、一致スコアが最も高い結果を自動的に選択します</value>
</data>
<data name="SettingsPageLyricsSearchType.Header" xml:space="preserve">
<value>歌詞検索方法</value>
<value>歌詞検索方法</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>中太</value>
@@ -1111,7 +1117,7 @@
<value>準細</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞スタイル</value>
<value>歌詞スタイル</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>極細</value>
@@ -1132,13 +1138,13 @@
<value>歌詞ウィンドウマネージャー</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>歌詞ウィンドウ状態切り替えショートカット</value>
<value>歌詞ウィンドウスイッチャーのショートカットキー</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>この値を調整すると、順次検索とベストマッチ検索の結果に影響しますが、手動歌詞検索インターフェイスの検索結果には影響しません</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>一致度の最低ライン</value>
<value>最小一致しきい値</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアライブラリ</value>
@@ -1210,7 +1216,7 @@
<value>歌詞のルビ</value>
</data>
<data name="SettingsPagePhoneticText.Header" xml:space="preserve">
<value>ルビのサイズ</value>
<value>ルビ</value>
</data>
<data name="SettingsPagePinToTaskbar.Header" xml:space="preserve">
<value>タスクバーにピン留め</value>
@@ -1237,10 +1243,10 @@
<value>"soundcloud.com" で "Cut to the Feeling" を再生</value>
</data>
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>再生/一時停止のショートカット</value>
<value>再生/一時停止のショートカットキー</value>
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>前の曲へのショートカット</value>
<value>前の曲へのショートカットキー</value>
</data>
<data name="SettingsPagePureLayer.Header" xml:space="preserve">
<value>単色のレイヤー</value>
@@ -1308,11 +1314,14 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>設定マネージャー</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>再生履歴</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>オンライン共有センターを閲覧する</value>
</data>
<data name="SettingsPageShortcut.Text" xml:space="preserve">
<value>ショートカット</value>
<value>ショートカットキー</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>ホットキーの登録に失敗しました</value>
@@ -1327,7 +1336,7 @@
<value>アーティストを表示</value>
</data>
<data name="SettingsPageShowHideHotKey.Header" xml:space="preserve">
<value>歌詞ウィンドウの表示/非表示ショートカット</value>
<value>歌詞ウィンドウの表示/非表示ショートカットキー</value>
</data>
<data name="SettingsPageShowInSwitchers.Description" xml:space="preserve">
<value>例: Alt + Tab、タスクバー</value>
@@ -1384,7 +1393,7 @@
<value>スタートアップ</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>統計</value>
<value>統計データ</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>ミュージックギャラリーウィンドウを閉じたときに再生を停止する</value>
@@ -1459,7 +1468,7 @@
<value>標準モード</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>情報源</value>
<value>再生ソース</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>今月</value>
@@ -1483,13 +1492,13 @@
<value>トップトラック</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>トップ・ソース</value>
<value>よく使う再生ソース</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>総所要時間</value>
<value>総再生時間</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>演奏曲目</value>
<value>再生された曲の数</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>終了</value>
@@ -1513,7 +1522,7 @@
<value>設定を開く</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>歌詞ウィンドウ切り替え</value>
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="TaskbarMode" xml:space="preserve">
<value>タスクバー モード</value>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>모든 음악</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>데이터가 처리 중입니다. 설정에서 자세한 진행 상황을 확인할 수 있습니다.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>미디어 라이브러리 동기화 진행 중...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>미디어 라이브러리 동기화에 문제가 있습니다.</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>재생 대기열 비우기</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>가사 창을 닫을 때 앱 종료</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>플레이 기록 내보내기</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>설정 내보내기</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>설정 관리자</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>플레이 기록</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>온라인 공유 허브 탐색</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Semua Muzik</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Data sedang diproses. Anda boleh menyemak kemajuan terperinci dalam tetapan.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Penyegerakan Pustaka Media sedang dijalankan...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Terdapat masalah dengan penyegerakan Pustaka Media</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Kosongkan baris gilir main</value>
@@ -700,7 +703,7 @@
<value>Jumlah Kabur</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Termasuk fail log, cache lirik rangkaian</value>
<value>Termasuk fail log, cache lirik dalam talian</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Keluar dari program apabila tetingkap lirik ditutup</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Eksport sejarah main</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Eksport Tetapan</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Pengurus Tetapan</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Sejarah Main</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Layari Hab Perkongsian Sumber Dalam Talian</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Todas as Músicas</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Os dados estão a ser processados. Pode verificar o progresso detalhado nas definições.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sincronização da biblioteca multimédia em curso...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Existe um problema com a sincronização da Biblioteca Multimédia</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Limpar fila de reprodução</value>
@@ -700,7 +703,7 @@
<value>Quantidade de Desfoque</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Inclui ficheiros de registo e cache de letras da rede</value>
<value>Inclui ficheiros de registo, cache de letras online</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Sair do programa ao fechar a janela de letras</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exportar histórico de jogos</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exportar Definições</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestor de Definições</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>História do jogo</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Explorar o Centro de Partilha de Recursos Online</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Вся музыка</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Данные находятся в процессе обработки. Подробную информацию о ходе обработки можно посмотреть в настройках.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Выполняется синхронизация медиатеки...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Возникла проблема с синхронизацией медиатеки</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Очистить очередь воспроизведения</value>
@@ -700,7 +703,7 @@
<value>Степень размытия</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Включает лог-файлы, кэш онлайн-текстов</value>
<value>Включая файлы журналов, кэш онлайновых текстов песен</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Кэш</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Выходить из программы при закрытии окна текста</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Экспорт истории игр</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Экспорт настроек</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Менеджер настроек</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>История игры</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Просмотреть онлайн-центр обмена</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>เพลงทั้งหมด</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>กำลังประมวลผลข้อมูลอยู่ คุณสามารถตรวจสอบความคืบหน้าโดยละเอียดได้ในการตั้งค่า</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>กำลังซิงค์คลังสื่อ...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>มีปัญหาเกี่ยวกับการซิงค์ไลบรารีมีเดีย</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>ล้างคิวการเล่น</value>
@@ -700,7 +703,7 @@
<value>ปริมาณความเบลอ</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>รวมไฟล์บันทึก, แคชเนื้อเพลงจากเครือข่าย</value>
<value>รวมถึงไฟล์บันทึก, แคชเนื้อเพลงออนไลน์</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>แคช</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>ออกจากโปรแกรมเมื่อปิดหน้าต่างเนื้อเพลง</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>ส่งออกประวัติการเล่น</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>ส่งออกการตั้งค่า</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>ตัวจัดการการตั้งค่า</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>ประวัติการเล่น</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>เรียกดูศูนย์แบ่งปันทรัพยากรออนไลน์</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Tất cả bài hát</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>Dữ liệu đang được xử lý. Bạn có thể kiểm tra tiến độ chi tiết trong phần cài đặt.</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Đang đồng bộ thư viện phương tiện...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Có vấn đề với việc đồng bộ hóa Thư viện Phương tiện.</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Xóa hàng đợi phát</value>
@@ -700,7 +703,7 @@
<value>Độ mờ</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Bao gồm tệp nhật ký, bộ nhớ đệm lời bài hát mạng</value>
<value>Gồm các tệp nhật ký bộ nhớ đệm lời bài hát trực tuyến.</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Bộ nhớ đệm</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Thoát chương trình khi đóng cửa sổ lời bài hát</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Xuất lịch sử phát lại</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Xuất cài đặt</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Trình quản lý cài đặt</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Lịch sử chơi</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Duyệt trung tâm chia sẻ tài nguyên trực tuyến</value>
</data>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有音乐</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>数据正在处理中。您可以在设置中查看详细进度。</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>正在同步媒体库...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>媒体库同步出现问题</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>清除播放队列</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>关闭歌词窗口时退出程序</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>导出播放记录</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>导出设置</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>设置管理器</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>播放记录</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>浏览在线资源共享中心</value>
</data>
@@ -1489,7 +1498,7 @@
<value>总时长</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>播放的曲目</value>
<value>播放的曲目</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>退出程序</value>

View File

@@ -414,8 +414,11 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有音樂</value>
</data>
<data name="MusicGalleryPageDataLoading.Message" xml:space="preserve">
<value>資料正在處理中。您可以在設定中檢查詳細進度。</value>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>媒體庫同步中...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>媒體庫同步有問題</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>清除播放佇列</value>
@@ -855,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>關閉歌詞視窗時結束程式</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>匯出播放記錄</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>匯出設定</value>
</data>
@@ -1308,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>設定管理器</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>播放記錄</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>瀏覽線上資源共享中心</value>
</data>

View File

@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml.Controls;
using System;
using System.IO;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
@@ -74,6 +75,19 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
[RelayCommand]
private async Task ExportPlayHistoryAsync()
{
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>();
if (folder != null)
{
var dest = Path.Combine(folder.Path, $"BetterLyrics_Play_History_Export_{DateTime.Now:yyyyMMdd_HHmmss}.db");
await FileHelper.CopyFileAsync(PathHelper.PlayHistoryPath, dest);
ToastHelper.ShowToast("ExportSettingsSuccess", null, InfoBarSeverity.Success);
}
}
[RelayCommand]
private void ClearCacheFiles()
{

View File

@@ -92,7 +92,8 @@ namespace BetterLyrics.WinUI3.ViewModels
public SongsTabInfo? SelectedSongsTabInfo => AppSettings.StarredPlaylists.ElementAtOrDefault(SelectedSongsTabInfoIndex);
[ObservableProperty] public partial bool IsDataLoading { get; set; } = false;
[ObservableProperty] public partial bool IsDataSyncing { get; set; } = false;
[ObservableProperty] public partial bool IsDataSyncError { get; set; } = false;
[ObservableProperty] public partial ExtendedTrack TrackRightTapped { get; set; } = new();
@@ -121,6 +122,7 @@ namespace BetterLyrics.WinUI3.ViewModels
RefreshSongs();
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
_mediaPlayer.MediaOpened += MediaPlayer_MediaOpened;
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
@@ -138,6 +140,11 @@ namespace BetterLyrics.WinUI3.ViewModels
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
IsDataSyncError = AppSettings.LocalMediaFolders.Any(x => x.StatusSeverity == InfoBarSeverity.Error);
}
private void TrackPlayingQueue_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.DecodedAbsoluteUri)];
@@ -495,7 +502,7 @@ namespace BetterLyrics.WinUI3.ViewModels
await _currentProvider.ConnectAsync();
var fileCacheStub = new FileCacheEntity
var fileCacheStub = new FilesIndexItem
{
Uri = PlayingTrack.Uri
};
@@ -656,7 +663,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else if (message.PropertyName == nameof(MediaFolder.IsProcessing))
{
IsDataLoading = message.NewValue;
IsDataSyncing = message.NewValue;
}
}
}

View File

@@ -104,16 +104,10 @@ namespace BetterLyrics.WinUI3.ViewModels
foreach (var item in stats.OrderByDescending(x => x.Count))
{
double maxBarWidth = 150.0;
double calculatedWidth = (item.Count / maxCount) * maxBarWidth;
if (calculatedWidth < 2 && item.Count > 0) calculatedWidth = 2;
PlayerStats.Add(new PlayerStatDisplayItem
{
PlayerId = item.PlayerId,
PlayCount = item.Count,
DisplayWidth = calculatedWidth
});
}
}

View File

@@ -247,7 +247,7 @@
</Flyout>
</Grid.Tag>
<StackPanel Grid.Row="1" Spacing="6">
<StackPanel Grid.Row="0" Spacing="6">
<AutoSuggestBox
x:Name="SongSearchBox"
@@ -306,10 +306,17 @@
</StackPanel>
<InfoBar
x:Uid="MusicGalleryPageDataLoading"
Grid.Row="2"
x:Uid="MusicGalleryPageDataSync"
Grid.Row="1"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsDataLoading, Mode=OneWay}" />
IsOpen="{x:Bind ViewModel.IsDataSyncing, Mode=OneWay}" />
<InfoBar
x:Uid="MusicGalleryPageDataSyncError"
Grid.Row="1"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsDataSyncError, Mode=OneWay}"
Severity="Error" />
<SemanticZoom Grid.Row="2">
<SemanticZoom.ZoomedInView>

BIN
Promotion/banner_fade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View File

@@ -1,235 +1,183 @@
![](Promotion/banner.png)
<div align=center>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="96">
</div>
<h2 align=center>
BetterLyrics
</h2>
<h4 align="center">
🤩 一款优雅且高度自定义的歌词/播放器应用,基于 WinUI3/Win2D 构建
</h4>
[**中文**](README.CN.md) | [**English**](README.md)
<div align="center">
[使用指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [隐私政策](PrivacyPolicy.CN.md) | [服务协议](TermsofService.CN.md)
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">
<h1>BetterLyrics</h1>
<h4>
🤩 一款优雅且高度自定义的歌词可视化与全能音乐播放应用 <br>
基于 WinUI3 / Win2D 构建
</h4>
<div>
<img src="https://img.shields.io/badge/Language-C%23-purple" alt="C#">
<img src="https://img.shields.io/badge/Framework-WinUI%203-blue" alt="WinUI 3">
<img src="https://img.shields.io/badge/License-GPL_v3.0-blue" alt="License">
<a href="https://github.com/jayfunc/BetterLyrics/stargazers"><img src="https://img.shields.io/github/stars/jayfunc/BetterLyrics" alt="Stars"></a>
<a href="https://crowdin.com/project/betterlyrics"><img src="https://badges.crowdin.net/betterlyrics/localized.svg" alt="Crowdin"></a>
</div>
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
<div align=center>
![Static Badge](https://img.shields.io/badge/Language-C%23-purple)
![Static Badge](https://img.shields.io/badge/License-GPL_v3.0-blue)
![Static Badge](https://img.shields.io/badge/IDE-Visual%20Studio-purple)
![Static Badge](https://img.shields.io/badge/Framework-WinUI%203-blue)
<br>
</div>
<div align=center>
[![GitHub Repo stars](https://img.shields.io/github/stars/jayfunc/BetterLyrics)](https://github.com/jayfunc/BetterLyrics/stargazers)
[![Crowdin](https://badges.crowdin.net/betterlyrics/localized.svg)](https://crowdin.com/project/betterlyrics)
</div>
<div align=center>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics)
</div>
## 🔥 精选推荐与社区
<div align="center">
<mark>**_💞 BetterLyrics 的发展离不开每一位贡献者、反馈者和用户的全力支持。_**</mark>
| HelloGitHub 推荐 | 少数派 SSPAI 推荐 | 🤖 AI 问答 |
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**阅读评测文章**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
<mark>**_项目持续活跃开发中可能会遇到未知问题。_**</mark>
**交流群:** [QQ 群 (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>
## ✍️ 协助翻译
## 🧪 下载与安装
找不到你的语言?有更好的翻译?没关系!😆 访问 [此处](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file) 查看如何贡献翻译!
<div align="center">
## 🎉 该项目入选少数派推荐文章!
| Microsoft Store (推荐) | 手动安装 |
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>无限期免费试用(功能与付费版一致) | [**📦 最新版本 (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>[查看安装指南](https://www.cnblogs.com/jayfunc/p/19212078) |
文章链接:[BetterLyrics - 一款专为 Windows 打造的沉浸式流畅歌词显示软件](https://sspai.com/post/101028)
[📖 用户指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [🔒 隐私政策](PrivacyPolicy.CN.md) | [⚖️ 服务条款](TermsofService.CN.md)
## 🔈 反馈交流群
</div>
[QQ 群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) | [Discord Server](https://discord.gg/5yAQPnyCKv) | [Telegram Group](https://t.me/+svhSLZ7awPsxNGY1)
## 🌟 核心功能
## 🌟 特色功能
- 🎨 **绝美视觉与 UI**
- **优雅设计:** 基于 WinUI3 & Win2D 的流畅、高度个性化风格。
- **沉浸特效:** 支持流体背景、3D/扇形歌词、雪花粒子等多种效果。
- **深度定制:** 按需配置动画、字体和行为逻辑,打造你的专属播放器。
- 🌠 **精美的用户界面**
- 流畅、高度自定义的样式、动画、动效
- 沉浸式流体背景
- 透视/扇形歌词
- 雪花效果
- 多种歌词滚动函数
- ...
- ↔️ **强大的歌词翻译**
- 本地机器翻译 (支持 30 多种语言)
- 自动读取本地音乐文件内嵌歌词
- 🧩 **多种歌词源**
- 💾 本地源
- 音乐文件 (内嵌歌词)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) 文件 (传统格式、增强格式)
- [.eslrc](https://github.com/ESLyric/release) 文件
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) 文件
- ☁️ 在线源
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
- <details><summary>⚠️ Apple Music (需要额外配置)</summary>
- 🎧 **多功能播放与连接**
- **内置播放器:** 支持播放 **本地硬盘** 文件或通过 **网络协议** (SMB, WebDAV) 流式播放。
- **外部集成:** 可视化来自 Spotify, Apple Music, 网易云音乐及 [其他多种播放器](https://github.com/jayfunc/BetterLyrics/wiki/使用指南#已知支持的音乐播放器配置指南) 的音乐。
- 浏览器打开 Apple Music打开开发者工具。刷新网页回到开发者工具窗口筛选出 Fetch/XHR选择一个请求在请求标头中找到 media-user-token 并复制其值。
- 打开 BetterLyrics 转到播放源设置。在 Media-User-Token (for Apple Music) 中粘贴复制的值并点按右侧对勾
- 🎶 **支持众多音乐播放器**
- 🌐 **强大的歌词系统**
- **离线翻译:** 注重隐私的本地机器翻译(支持 30+ 种语言)
- **全面源支持:** 支持 .lrc (标准/增强), .eslrc, .ttml, 内嵌标签以及在线源QQ 音乐, 网易云, LRCLIB
- **Apple Music** 支持歌词获取(需配置 Token
- 点击 [此处](https://github.com/jayfunc/BetterLyrics/wiki/使用指南#已知支持的音乐播放器配置指南) 查看详细信息
- 🪟 **全场景显示模式**
- **标准模式:** 全屏沉浸式体验。
- **停靠模式:** 贴附于屏幕边缘的精致侧边栏。
- **桌面悬浮:** 悬浮于所有应用之上的歌词挂件。
- 🪟 **多种显示模式**
- **标准模式**
- 标准的歌词窗口样式,沉浸式的音乐歌词体验。
- **停靠模式**
- 停靠在屏幕上/下边缘的轻量歌词窗口,工作休闲互不打扰。
- **桌面模式**
- 悬浮在所有应用上层,不能被选中,但能直击你的使用需求。
- **更多模式...**
- 等你来发现...
- 🧠 **智能化行为**
- 根据歌曲播放状态自动显隐歌词窗口
- 🧠 **智能行为**
- 音乐暂停时自动隐藏。
## 🖼️ 屏幕截图
## 🖼️ 软件截图
![](Screenshots/fs2.png)
![](Screenshots/std.png)
![](Screenshots/narrow.png)
![](Screenshots/Snipaste_2025-10-31_19-23-17.png)
![](Screenshots/Snipaste_2025-10-31_19-27-34.png)
![](Screenshots/dock.png)
![](Screenshots/desktop.png)
<div align="center">
> ⚠️ 由于 GIF 格式帧率限制,效果仅作展示。请以实机效果为准。
| 标准视图 | 侧边栏模式 |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
![](Screenshots/PixPin_2025-10-24_18-13-44.gif)
![](Screenshots/PixPin_2025-10-24_18-17-17.gif)
| 歌词视觉特效 | 多模式共存 |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
## 📹 演示
| 全屏模式 | 全屏模式 |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
在 [哔哩哔哩](https://www.bilibili.com/video/BV1QRstz1EGt/) 上观看于 2025 年 10 月 21 日上传的演示视频。
| 音乐库 | 播放统计 |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
## 🧪 即刻体验
</div>
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/zh-cn%20dark.svg" width="200"/>
</a>
## 📹 演示视频
**无限期**免费试用版和付费版**无任何区别**
> 观看我们在 Bilibili 发布的演示视频(上传于 2025 年 10 月 21 日):[点击观看](https://www.bilibili.com/video/BV1QRstz1EGt/)
如果喜欢该软件,请考虑 [捐赠](#-捐赠) 或在 **Microsoft Store** 购买,感谢您的支持! 🥰
## ✍️ 贡献与构建
无法从 Microsoft Store 下载?尝试以下方法:
- [从 Microsoft Store 外部下载](https://www.cnblogs.com/jayfunc/p/19212083)
- 转至 [最新发布页](https://github.com/jayfunc/BetterLyrics/releases/latest) 并从 `Assets`(资产)列表下载 `.zip` 文件。(安装方法参考 [此文档](https://www.cnblogs.com/jayfunc/p/19212078)。)
**协助翻译:** 找不到你的语言?[点此开始翻译](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file)。
## 🏗️ 构建
**从源码构建:**
> 构建前,请确保已替换 `Constants` 文件夹下的 `DiscordTemplate.cs` 和 `LastFM.cs`。
在构建之前确保:
- 替换文件 `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate``BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate.cs`.
- 替换文件 `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFMTemplate``BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFM.cs`
## 🤑 赞助与捐赠
## 🤑 捐赠
如果你喜欢 BetterLyrics请考虑支持它。你的支持有助于项目持续发展
如果你喜欢本应用,请考虑捐赠支持开发者。这将有助于本应用的长远发展。
<div align="center">
通过以下途径捐赠:
- [PayPal](https://paypal.me/zhefangpay)
- [Buy Me a Coffee](https://buymeacoffee.com/founchoo)
- [爱发电](https://afdian.com/a/jayfunc)
- <details><summary>支付宝</summary>
![](Donate/Alipay.jpg)
</detais>
| 网页平台 | 支付宝 (扫码) | 微信 (扫码) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
- <details><summary>微信</summary>
![](Donate/WeChatReward.png)
</details>
**[查看完整赞助者名单 (Hall of Fame)](SPONSORS.md)**
本项目的持续发展离不开大家的支持。**[查看完整鸣谢名单](SPONSORS.md)**
</div>
## 📄 许可证
## ⭐ Star 历史趋势
本项目采用 GNU 通用公共许可证 v3.0 授权。详情请参阅 [LICENSE](https://github.com/jayfunc/BetterLyrics/blob/dev/LICENSE) 文件。
<div align="center">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date" width="100%">
</div>
## 💖 感
## 📄 许可与致
部分功能及代码引用或修改自公开资料库,包括但不限于下述开源项目/包、教程等,在此一并感谢
本项目采用 **GNU General Public License v3.0** 许可证
<details>
<summary><b>💖 特别致谢、引用与灵感</b></summary>
<br>
**依赖与引用:**
| 项目/包 | 描述 |
| :--- | :--- |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | QQ、网易、酷狗在线歌词源提供歌词抓取、解密、解析等一系列方法 |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB 歌词 API |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music 歌词抓取Python 实现) |
| [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) | 从音乐文件提取图片 |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | 提供有关窗口的开箱即用的 Win32 API |
| [TagLib#](https://github.com/mono/taglib-sharp) | 读取音乐文件内嵌的原始歌词内容 |
| [Vanara](https://github.com/dahall/Vanara) | 提供开箱即用的 Win32 API |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | 离线翻译核心 |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | 动态流体背景 |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | 频谱图 |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | 为 WinUI3 提供众多开箱即用的功能 |
| ... | ... |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | QQ、网易、酷狗歌词获取与解密 |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB 歌词 API 提供方 |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music 歌词获取 |
| [Audio Tools Library (ATL)](https://github.com/Zeugma440/atldotnet) | 从音乐文件提取图片 |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | 简化 Win32 API 的窗口访问 |
| [TagLib#](https://github.com/mono/taglib-sharp) | 读取原始歌词内容 |
| [Vanara](https://github.com/dahall/Vanara) | Win32 API 封装库 |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | 离线歌词翻译支持 |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | 动态流体背景实现 |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | WinUI 3 开发辅助工具 |
点按 [此处](https://github.com/jayfunc/BetterLyrics/network/dependencies) 查看所有依赖
查看 [完整依赖列表](https://github.com/jayfunc/BetterLyrics/network/dependencies)。
### 教程、博客等
<br>
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
## 💡 灵感来源
部分设计思路参考自下述插件/软件(不含间接或直接引用、修改的代码,仅作为设计思路指导方向)。
**💡 灵感来源:**
部分设计理念参考了以下插件/软件(仅作为设计思路参考,不涉及代码引用):
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## ⭐ 星标记录
</details>
<div style="display: flex; justify-content: space-around; align-items: flex-start;">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date" width="100%" >
## 💭 分享到社交媒体
<details>
<summary><b>点击展开</b></summary>
<br>
<div align="center">
<img src="https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light" width="48%">
<img src="https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics" width="48%">
</div>
</details>
## 🤗 欢迎反馈问题、提交代码
<br>
如果发现 Bug 请在 Issues 内提出,同时也欢迎任何想法、建议。
## ⚠️ 免责声明
本项目按“原样”提供,不提供任何形式的担保。
所有歌词、字体、图标及其他第三方资源均为其各自版权所有者的财产。
本项目作者不主张对这些资源的所有权。
本项目为非商业用途,不得用于侵犯任何权利。
用户有责任确保其使用符合适用的法律和许可协议。
## 💭 社交媒体分享
![BetterLyrics](https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
![BetterLyrics](https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics)
<div align="center">
<mark><i>本项目正处于积极开发阶段;可能会出现意外问题。</i></mark><br>
<sub>免责声明:本项目“按原样”提供。所有第三方资源归其各自所有者所有。</sub>
</div>

302
README.md
View File

@@ -1,243 +1,183 @@
![](Promotion/banner.png)
<div align=center>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="96">
</div>
<h2 align=center>
BetterLyrics
</h2>
<h4 align="center">
🤩 An elegant and deeply customizable lyrics & player app, built with WinUI3/Win2D
</h4>
[**中文**](README.CN.md) | [**English**](README.md)
<div align="center">
[User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [Privacy Policy](PrivacyPolicy.md) | [Terms of Service](TermsofService.md)
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">
<h1>BetterLyrics</h1>
<h4>
🤩 An elegant and deeply customizable lyrics visualizer & versatile music player <br>
Built with WinUI3 / Win2D
</h4>
<div>
<img src="https://img.shields.io/badge/Language-C%23-purple" alt="C#">
<img src="https://img.shields.io/badge/Framework-WinUI%203-blue" alt="WinUI 3">
<img src="https://img.shields.io/badge/License-GPL_v3.0-blue" alt="License">
<a href="https://github.com/jayfunc/BetterLyrics/stargazers"><img src="https://img.shields.io/github/stars/jayfunc/BetterLyrics" alt="Stars"></a>
<a href="https://crowdin.com/project/betterlyrics"><img src="https://badges.crowdin.net/betterlyrics/localized.svg" alt="Crowdin"></a>
</div>
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
<div align=center>
![Static Badge](https://img.shields.io/badge/Language-C%23-purple)
![Static Badge](https://img.shields.io/badge/License-GPL_v3.0-blue)
![Static Badge](https://img.shields.io/badge/IDE-Visual%20Studio-purple)
![Static Badge](https://img.shields.io/badge/Framework-WinUI%203-blue)
<br>
</div>
<div align=center>
[![GitHub Repo stars](https://img.shields.io/github/stars/jayfunc/BetterLyrics)](https://github.com/jayfunc/BetterLyrics/stargazers)
[![Crowdin](https://badges.crowdin.net/betterlyrics/localized.svg)](https://crowdin.com/project/betterlyrics)
</div>
<div align=center>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics)
</div>
## 🔥 Featured & Community
<div align="center">
<mark>**_💞 BetterLyrics is made possible by all its contributors, bug reporters and users._**</mark>
| Featured by HelloGitHub | Featured by SSPAI | 🤖 Ask AI |
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**Read the Review Article**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
**Chat Groups:** [QQ Group (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>
## 🧪 Download & Install
<div align="center">
**_[中文版 README 请点按此处](https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md)_**
| Microsoft Store (Recommended) | Manual Install |
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>Unlimited free trial (Same as paid) | [**📦 Latest Release (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>See [Installation Guide](https://jayfunc.blog/blog/how-to-install-zip) |
<mark>**_This project is under active development; unexpected issues may occur._**</mark>
[📖 User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [🔒 Privacy Policy](PrivacyPolicy.md) | [⚖️ Terms of Service](TermsofService.md)
</div>
## ✍️ Help us translate into your language
## 🌟 Highlighted Features
Cannot find your language? Or have better translations? Don't worry! Start translating and becoming one of the contributors! 😆 See [here](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file) for more info on how to contribute.
- 🎨 **Stunning Visuals & UI**
- **Elegant Design:** Smooth, highly personalized style powered by WinUI3 & Win2D.
- **Immersive Effects:** Fluid backgrounds, 3D/Fan-shaped lyrics, snowflake particles, and more.
- **Deep Customization:** Configure animations, fonts, and behaviors to your taste.
## 🎉 This project was featured by SSPAI!
- 🎧 **Versatile Playback & Connectivity**
- **Built-in Player:** Play from **Local Drives** or stream via **Network Protocols** (SMB, WebDAV).
- **External Integration:** Visualizes music from Spotify, Apple Music, NetEase, and [many others](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide#known-supported-music-players-configuration-guide).
Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028).
- 🌐 **Advanced Lyrics System**
- **Offline Translation:** Privacy-focused local machine translation (30+ languages).
- **Comprehensive Sources:** .lrc (Standard/Enhanced), .eslrc, .ttml, embedded tags, and online sources (QQ Music, NetEase, LRCLIB).
- **Apple Music:** Supports lyrics fetching (Requires token configuration).
## 🔈 Feedback and chat group
- 🪟 **Display Modes for Every Scenario**
- **Standard:** Full immersive experience.
- **Docked:** A sleek bar attached to your screen edge.
- **Desktop Overlay:** Lyrics floating above all apps.
[QQ 群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) | [Discord Server](https://discord.gg/5yAQPnyCKv) | [Telegram Group](https://t.me/+svhSLZ7awPsxNGY1)
## 🌟 Highlighted features
- 🌠 **Pleasing User Interface**
- Smooth and highly personalized style, animations and effects
- Immersive fluid background
- Perspective/fan-shaped lyrics
- Snowflake effect
- Multiple lyrics scrolling functions
- ... (and more)
- ↔️ **Strong Lyrics Translation**
- Offline machine translation (supporting 30+ languages)
- Auto-reading local lyrics files for embedded translation
- 🧩 **Various Lyrics Source**
- 💾 Local storage
- Music files (with embedded lyrics)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) files (with both core format and enhanced format)
- [.eslrc](https://github.com/ESLyric/release) files
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) files
- ☁️ Online lyrics providers
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
- <details><summary>⚠️ Apple Music (additional config needed)</summary>
- Open the Apple Music web app and the Developer Tools window. Refresh the page. Return to the Developer Tools window, select Fetch/XHR, select a request, find the Media-User-Token header in the request header, and copy its value.
- Open BetterLyrics and go to the Playback Source settings. Enter the copied value in the Media-User-Token (for Apple Music) setting and click the accept icon on the right-hand side.
- 🎶 **Multiple Music Players Supported**
- Check it out [here](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide#known-supported-music-players-configuration-guide) for detailed info
- 🪟 **Multiple Display Modes**
- **Standard Mode**
- Enjoy an immersive listening journey with rich lyrics, animations and beautifully dynamic backgrounds
- **Docked Mode**
- A smart animated lyrics bar docked to your screen edge
- **Desktop Mode**
- Enjoy immersive lyrics floating above your apps
- **And More...**
- Waiting for you to discover...
- 🧠 **Smart Behaviors**
- Auto hide when music paused
- Auto-hides when music pauses.
## 🖼️ Screenshots
![](Screenshots/fs2.png)
![](Screenshots/std.png)
![](Screenshots/narrow.png)
![](Screenshots/Snipaste_2025-10-31_19-23-17.png)
![](Screenshots/Snipaste_2025-10-31_19-27-34.png)
![](Screenshots/dock.png)
![](Screenshots/desktop.png)
<div align="center">
> ⚠️ Due to GIF format and frame rate limitations, the displayed effect is for preview only. Please refer to the actual device for the actual effect.
| Standard View | Narrow Mode |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
![](Screenshots/PixPin_2025-10-24_18-13-44.gif)
![](Screenshots/PixPin_2025-10-24_18-17-17.gif)
| Lyrics Visual Effects | Coexisting Modes |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
| Fullscreen Mode | Fullscreen Mode |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
| Music Gallery | Playback Statistics |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
</div>
## 📹 Demonstration
Watch our demo video (uploaded on 21 Oct 2025) on Bilibili [here](https://www.bilibili.com/video/BV1QRstz1EGt/).
> Watch our demo video (uploaded on 21 Oct 2025) on Bilibili [here](https://www.bilibili.com/video/BV1QRstz1EGt/).
## 🧪 Try it now
## ✍️ Contribute & Build
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
**Help us translate:** Cannot find your language? [Start translating here](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file).
**Unlimited** free trail or purchase (there is **no difference** between free and paid version).
If you find it useful, please consider [donating](#-donations) or purchasing it in **Microsoft Store**, I'll appreciate it! 🥰
Having trouble downloading and installing from the MS Store? Try the following options:
- [Download from outside Microsoft Store](https://jayfunc.blog/blog/download-from-outside-ms-store)
- Go to [latest release](https://github.com/jayfunc/BetterLyrics/releases/latest) and download `.zip` file from `Assets`. (See [this doc](https://jayfunc.blog/blog/how-to-install-zip) for how to install it.)
## 🏗️ Build
Before you build, make sure that you have already:
- Replaced `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate` with `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate.cs`.
- Replaced `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFMTemplate` with `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFM.cs`.
**Build from source:**
> Before building, ensure you have replaced `DiscordTemplate.cs` and `LastFM.cs` in the `Constants` folder.
## 🤑 Donations
If you like this project, please consider supporting it by donating. Your support will help keep the project alive and encourage further development.
If you like BetterLyrics, please consider supporting it. Your support helps keep the project alive!
You can donate via:
- [PayPal](https://paypal.me/zhefangpay)
- [Buy Me a Coffee](https://buymeacoffee.com/founchoo)
- [爱发电](https://afdian.com/a/jayfunc)
- <details><summary>支付宝</summary>
![](Donate/Alipay.jpg)
</detais>
<div align="center">
- <details><summary>微信</summary>
![](Donate/WeChatReward.png)
</details>
| Web Platforms | Alipay (QR) | WeChat (QR) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
This project is made possible by the generous support of our users. **[View the full Hall of Fame](SPONSORS.md)**
**[View the full Hall of Fame (Sponsors)](SPONSORS.md)**
## 📄 License
</div>
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](https://github.com/jayfunc/BetterLyrics/blob/dev/LICENSE) file for details.
## ⭐ Star History
## 💖 Many thanks to
<div align="center">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date" width="100%">
</div>
Some functions and code are referenced or modified from public repositories, including but not limited to the following open source projects/packages, tutorials, etc., and we would like to express our gratitude to them here.
## 📄 License & Credits
This project is licensed under the **GNU General Public License v3.0**.
<details>
<summary><b>💖 Special Thanks, Credits & Inspiration</b></summary>
<br>
**Dependencies & References:**
| Projects/Packages | Description |
| :--- | :--- |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | Provide lyrics fetch, decryption, and parsing for QQ, Netease, and Kugou sources |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | Lyrics fetch/decrypt for QQ, Netease, Kugou |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB lyrics API provider |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music lyrics fetch using Python |
| [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) | Used for extracting pictures from music files |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | Provide easy ways to access the Win32 API regarding windowing |
| [TagLib#](https://github.com/mono/taglib-sharp) | Used for reading the original lyrics content |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music lyrics fetch |
| [Audio Tools Library (ATL)](https://github.com/Zeugma440/atldotnet) | Picture extraction from music files |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | Win32 API windowing access |
| [TagLib#](https://github.com/mono/taglib-sharp) | Reading original lyrics content |
| [Vanara](https://github.com/dahall/Vanara) | Win32 API wrapper |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | Provide the ability for offline lyrics translation |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | Dynamic fluid background implementation |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | Audio visualization reference |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | Provide many out-of-the-box features for building WinUI 3 applications |
| ... | ... |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | Offline lyrics translation |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | Dynamic fluid background |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | WinUI 3 helpers |
See all the dependencies [here](https://github.com/jayfunc/BetterLyrics/network/dependencies).
### Tutorials/Blogs/etc.
See [dependencies](https://github.com/jayfunc/BetterLyrics/network/dependencies) for full list.
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
<br>
## 💡 Inspired by
Some design ideas are referenced from the following plugins/software (excluding code that is indirectly or directly referenced or modified, and is only used as a guide for design ideas).
**💡 Inspired by:**
Some design ideas are referenced from the following projects (design inspiration only):
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## ⭐ Star history
</details>
<div style="display: flex; justify-content: space-around; align-items: flex-start;">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date" width="100%" >
## 💭 Share on Social Media
<details>
<summary><b>Click to expand</b></summary>
<br>
<div align="center">
<img src="https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light" width="48%">
<img src="https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics" width="48%">
</div>
</details>
## 🤗 Any issues and PRs are welcome
<br>
If you find a bug, please file it in issues, or if you have any ideas, feel free to share them here.
## ⚠️ Disclaimer
This project is provided "as is" without warranty of any kind.
All lyrics, fonts, icons, and other third-party resources are the property of their respective copyright holders.
The author of this project does not claim ownership of such resources.
This project is non-commercial and should not be used to infringe any rights.
Users are responsible for ensuring their own use complies with applicable laws and licenses.
## 💭 Share it on social media
![BetterLyrics](https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
![BetterLyrics](https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics)
<div align="center">
<mark><i>This project is under active development; unexpected issues may occur.</i></mark><br>
<sub>Disclaimer: This project is provided "as is". All third-party resources belong to their respective owners.</sub>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

BIN
Screenshots/all-in-one.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

BIN
Screenshots/effect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
Screenshots/fs3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

BIN
Screenshots/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB