Compare commits

..

7 Commits

Author SHA1 Message Date
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
53 changed files with 900 additions and 587 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.232.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

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,11 @@
<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.Data.SqlClient" Version="6.1.3" />
<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 +102,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 +134,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 +225,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

@@ -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

@@ -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

@@ -333,7 +333,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)
{
// 后台

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 => x.DurationPlayedMs); // 直接在数据库层面求和
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

@@ -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>
@@ -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

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -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 />
</data>
<data name="SettingsPageExportSettingsButton.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>
@@ -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>

View File

@@ -53,6 +53,10 @@ BetterLyrics
找不到你的语言?有更好的翻译?没关系!😆 访问 [此处](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file) 查看如何贡献翻译!
## 🎉 该项目已被 HelloGithub 推荐!
<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="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 🎉 该项目入选少数派推荐文章!
文章链接:[BetterLyrics - 一款专为 Windows 打造的沉浸式流畅歌词显示软件](https://sspai.com/post/101028)。

View File

@@ -59,6 +59,10 @@ BetterLyrics
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.
## 🎉 This project was recommended by HelloGithub!
<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="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 🎉 This project was featured by SSPAI!
Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028).