mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 19:22:14 +08:00
chores: fix SMB and local file system, add auto-sync, improve lyruics search, album art search, local music gallery load speed (after 1st time)
This commit is contained in:
@@ -75,6 +75,7 @@
|
||||
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
|
||||
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
|
||||
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
|
||||
<converter:PathToImageConverter x:Key="PathToImageConverter" />
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
|
||||
|
||||
@@ -7,7 +7,6 @@ using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
|
||||
using BetterLyrics.WinUI3.Services.DiscordService;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.LastFMService;
|
||||
using BetterLyrics.WinUI3.Services.LibWatcherService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using BetterLyrics.WinUI3.Services.LyricsSearchService;
|
||||
using BetterLyrics.WinUI3.Services.MediaSessionsService;
|
||||
@@ -75,6 +74,9 @@ namespace BetterLyrics.WinUI3
|
||||
{
|
||||
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
|
||||
|
||||
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
|
||||
fileSystemService.StartAllFolderTimers();
|
||||
|
||||
WindowHook.OpenOrShowWindow<SystemTrayWindow>();
|
||||
|
||||
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
|
||||
@@ -118,7 +120,6 @@ namespace BetterLyrics.WinUI3
|
||||
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
|
||||
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
|
||||
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
|
||||
.AddSingleton<ILibWatcherService, LibWatcherService>()
|
||||
.AddSingleton<ITranslationService, TranslationService>()
|
||||
.AddSingleton<ITransliterationService, TransliterationService>()
|
||||
.AddSingleton<ILastFMService, LastFMService>()
|
||||
|
||||
@@ -240,8 +240,6 @@
|
||||
<ProgressBar
|
||||
VerticalAlignment="Top"
|
||||
IsIndeterminate="True"
|
||||
ShowError="False"
|
||||
ShowPaused="False"
|
||||
Visibility="{x:Bind ViewModel.IsSearching, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<Grid Grid.Column="2">
|
||||
|
||||
@@ -50,37 +50,73 @@
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:MediaFolder">
|
||||
<dev:SettingsExpander Description="{x:Bind ConnectionSummary, Mode=OneWay}">
|
||||
<dev:SettingsExpander Description="{x:Bind ConnectionSummary, Mode=OneWay}" IsExpanded="True">
|
||||
|
||||
<dev:SettingsExpander.HeaderIcon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="{x:Bind SourceType, Converter={StaticResource FileSourceTypeToIconConverter}, Mode=OneWay}" />
|
||||
</dev:SettingsExpander.HeaderIcon>
|
||||
|
||||
<dev:SettingsExpander.Header>
|
||||
<HyperlinkButton
|
||||
Padding="0"
|
||||
Click="LocalFolderHyperlinkButton_Click"
|
||||
Content="{x:Bind Path, Mode=OneWay}"
|
||||
Tag="{x:Bind Path, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ConnectionSummary}" />
|
||||
Content="{x:Bind ConnectionSummary, Mode=OneWay}" />
|
||||
</dev:SettingsExpander.Header>
|
||||
|
||||
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
|
||||
|
||||
<dev:SettingsExpander.Items>
|
||||
<dev:SettingsCard>
|
||||
<dev:SettingsCard.Header>
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsPageRemovePath"
|
||||
Padding="0"
|
||||
Click="SettingsPageRemovePathButton_Click"
|
||||
Tag="{Binding}" />
|
||||
</dev:SettingsCard.Header>
|
||||
<dev:SettingsCard x:Uid="MediaSettingsControlLastSyncTime" Description="{x:Bind LastSyncTime.ToString(), Mode=OneWay, TargetNullValue=N/A}">
|
||||
<Button
|
||||
x:Uid="MediaSettingsControlSyncNow"
|
||||
Click="SyncNowButton_Click"
|
||||
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageMusicLibRealTimeWatch" IsEnabled="{Binding IsLocal, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{Binding IsRealTimeWatchEnabled, Mode=TwoWay}" />
|
||||
<dev:SettingsCard x:Uid="MusicSettingsControlAutoSyncInterval">
|
||||
<ComboBox IsEnabled="{x:Bind IsEnabled, Mode=OneWay}" SelectedIndex="{x:Bind ScanInterval, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
||||
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalDisabled" />
|
||||
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEveryFifteenMin" />
|
||||
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEveryHour" />
|
||||
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEverySixHrs" />
|
||||
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEveryDay" />
|
||||
</ComboBox>
|
||||
</dev:SettingsCard>
|
||||
<dev:SettingsCard>
|
||||
<Button x:Uid="SettingsPageRemovePath" Click="SettingsPageRemovePathButton_Click" />
|
||||
</dev:SettingsCard>
|
||||
</dev:SettingsExpander.Items>
|
||||
|
||||
<dev:SettingsExpander.ItemsFooter>
|
||||
<StackPanel>
|
||||
<!-- Index info -->
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind IsIndexing, Mode=OneWay}"
|
||||
Message="{x:Bind IndexingStatusText, Mode=OneWay}" />
|
||||
<ProgressBar Visibility="{x:Bind IsIndexing, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" Value="{x:Bind IndexingProgress, Mode=OneWay}">
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind IndexingProgress, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="0">
|
||||
<interactivity:ChangePropertyAction PropertyName="IsIndeterminate" Value="True" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind IndexingProgress, Mode=OneWay}"
|
||||
ComparisonCondition="NotEqual"
|
||||
Value="0">
|
||||
<interactivity:ChangePropertyAction PropertyName="IsIndeterminate" Value="False" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</ProgressBar>
|
||||
<!-- Clean up info -->
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind IsCleaningUp, Mode=OneWay}"
|
||||
Message="{x:Bind CleaningUpStatusText, Mode=OneWay}" />
|
||||
<ProgressBar IsIndeterminate="True" Visibility="{x:Bind IsCleaningUp, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</dev:SettingsExpander.ItemsFooter>
|
||||
|
||||
</dev:SettingsExpander>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
|
||||
@@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.System;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
@@ -22,18 +23,23 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private void SettingsPageRemovePathButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.RemoveFolderAsync((MediaFolder)(sender as HyperlinkButton)!.Tag);
|
||||
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
|
||||
ViewModel.RemoveFolder(folder);
|
||||
}
|
||||
|
||||
private async void LocalFolderHyperlinkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is HyperlinkButton button && button.Tag is string uriStr)
|
||||
{
|
||||
if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri))
|
||||
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
|
||||
if (Uri.TryCreate(folder.UriString, UriKind.Absolute, out var uri))
|
||||
{
|
||||
await Launcher.LaunchUriAsync(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncNowButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
|
||||
ViewModel.SyncFolder(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,53 +40,49 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private string GetScheme()
|
||||
{
|
||||
string scheme = string.Empty;
|
||||
switch (_protocolType.ToUpper())
|
||||
{
|
||||
case "SMB":
|
||||
scheme = "smb";
|
||||
break;
|
||||
case "FTP":
|
||||
scheme = "ftp";
|
||||
break;
|
||||
case "WEBDAV":
|
||||
scheme = "https";
|
||||
break;
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
public MediaFolder GetConfig()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(HostBox.Text))
|
||||
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlServerAddressRequired"));
|
||||
|
||||
string name = $"{_protocolType} - {HostBox.Text}";
|
||||
|
||||
Enum.TryParse(_protocolType, true, out FileSourceType sourceType);
|
||||
|
||||
string scheme = GetScheme();
|
||||
|
||||
var folder = new MediaFolder
|
||||
{
|
||||
Name = name,
|
||||
Path = HostBox.Text, // <20><><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP/Host
|
||||
Port = (int)PortBox.Value,
|
||||
UserName = UserBox.Text,
|
||||
Password = PwdBox.Password, // <20><> PasswordBox <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>
|
||||
SourceType = sourceType,
|
||||
IsRealTimeWatchEnabled = false
|
||||
|
||||
UriScheme = scheme,
|
||||
UriHost = HostBox.Text.Trim(), // ȥ<><C8A5><EFBFBD><EFBFBD>β<EFBFBD>ո<EFBFBD>
|
||||
UriPort = (int)PortBox.Value,
|
||||
|
||||
UriPath = PathBox.Text.Trim(),
|
||||
|
||||
UserName = UserBox.Text.Trim(),
|
||||
Password = PwdBox.Password,
|
||||
};
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><E2B4A6>·<EFBFBD><C2B7><EFBFBD><EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA>"Զ<><D4B6>·<EFBFBD><C2B7>"ƴ<>ӵ<EFBFBD> Path <20><EFBFBD><EFA3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>ֶδ<D6B6>
|
||||
// Ϊ<>˼<CBBC><F2B5A5A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѭ<EFBFBD><D1AD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD>壺
|
||||
// <20><><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD><EFBFBD><EFBFBD>ټ<EFBFBD>һ<EFBFBD><D2BB> RemotePath <20>ֶΣ<D6B6><CEA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
|
||||
// *<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD>壬<EFBFBD><E5A3AC><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD>
|
||||
// Path <20>ֶδ洢<CEB4><E6B4A2>ʽ<EFBFBD><CABD> "192.168.1.5/Music"
|
||||
|
||||
var rawPath = PathBox.Text.Trim().TrimStart('/', '\\'); // ȥ<><C8A5><EFBFBD><EFBFBD>ͷ<EFBFBD><CDB7>б<EFBFBD><D0B1>
|
||||
if (!string.IsNullOrEmpty(rawPath))
|
||||
{
|
||||
// <20><EFBFBD>·<EFBFBD><C2B7>ƴ<EFBFBD><C6B4><EFBFBD><EFBFBD>
|
||||
if (sourceType == FileSourceType.SMB)
|
||||
{
|
||||
// SMBLibrary <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD> Host <20>ֿ<EFBFBD><D6BF><EFBFBD>ShareName <20>ֿ<EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP <20><><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFA3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA> ShareName ƴ<>ں<EFBFBD><DABA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֶ<EFBFBD>
|
||||
// Ϊ<>˷<EFBFBD><CBB7>㣬<EFBFBD><E3A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP <20><> ShareName ƴ<><C6B4>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Path
|
||||
// <20><><EFBFBD><EFBFBD>: 192.168.1.5/Music
|
||||
folder.Path = $"{HostBox.Text}/{rawPath}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// FTP/WebDAV: 192.168.1.5/pub/music
|
||||
folder.Path = $"{HostBox.Text}/{rawPath}";
|
||||
}
|
||||
}
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,20 +17,16 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
using (var ms = new MemoryStream(byteArray))
|
||||
{
|
||||
var stream = ms.AsRandomAccessStream();
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
|
||||
bitmapImage.SetSource(stream);
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return PathHelper.AlbumArtPlaceholderPath;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return PathHelper.AlbumArtPlaceholderPath;
|
||||
return new BitmapImage(new Uri(PathHelper.AlbumArtPlaceholderPath));
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class PathToImageConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
string targetPath = PathHelper.AlbumArtPlaceholderPath;
|
||||
if (value is string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
targetPath = path;
|
||||
}
|
||||
}
|
||||
return new BitmapImage(new Uri(targetPath));
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Enums
|
||||
{
|
||||
public enum AutoScanInterval
|
||||
{
|
||||
Disabled,
|
||||
Every15Minutes,
|
||||
EveryHour,
|
||||
Every6Hours,
|
||||
Daily
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Ude;
|
||||
|
||||
@@ -86,5 +88,15 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
".wav", ".aiff", ".aif", ".pcm", ".cda", ".dsf", ".dff", ".au", ".snd",
|
||||
".mid", ".midi", ".mod", ".xm", ".it", ".s3m"
|
||||
};
|
||||
|
||||
public static readonly string[] LyricExtensions =
|
||||
Enum.GetValues(typeof(LyricsSearchProvider)).Cast<LyricsSearchProvider>()
|
||||
.Where(x => x.IsLocal())
|
||||
.Select(x => x.GetLyricsFormat())
|
||||
.Where(x => x != LyricsFormat.NotSpecified)
|
||||
.Select(x => x.ToFileExtension())
|
||||
.ToArray();
|
||||
|
||||
public static readonly HashSet<string> AllSupportedExtensions = new(MusicExtensions.Union(LyricExtensions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,10 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
|
||||
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes");
|
||||
public static string LocalAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "local");
|
||||
|
||||
public static string PlayQueuePath => Path.Combine(CacheFolder, "play-queue.m3u");
|
||||
public static string FilesCachePath => Path.Combine(CacheFolder, "files_cache.db");
|
||||
|
||||
public static void EnsureDirectories()
|
||||
{
|
||||
@@ -75,6 +77,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
Directory.CreateDirectory(LocalTtmlCacheDirectory);
|
||||
|
||||
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
|
||||
Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
private readonly Stream _stream;
|
||||
private readonly bool _closeStreamOnDispose;
|
||||
|
||||
public StreamFileAbstraction(string path, Stream stream, bool closeStreamOnDispose = false)
|
||||
public StreamFileAbstraction(string path, Stream? stream, bool closeStreamOnDispose = false)
|
||||
{
|
||||
_name = Path.GetFileName(path);
|
||||
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
@@ -1,28 +1,216 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class ExtendedTrack : ATL.Track
|
||||
public class ExtendedTrack
|
||||
{
|
||||
public new string Path { get; private set; } = "";
|
||||
public string RawLyrics { get; set; } = "";
|
||||
public string ParentFolderName => Directory.GetParent(Path)?.Name ?? "";
|
||||
public string ParentFolderPath => Directory.GetParent(Path)?.FullName ?? "";
|
||||
public string FileName => System.IO.Path.GetFileName(Path);
|
||||
// 标准 URI (file:///..., smb://..., http://...)
|
||||
public string Uri { get; private set; } = "";
|
||||
|
||||
// 对于本地文件,返回 C:\Music\Song.mp3
|
||||
// 对于远程文件,返回解码后的路径部分 /Music/Song.mp3
|
||||
public string UriPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Uri)) return "";
|
||||
try
|
||||
{
|
||||
var u = new System.Uri(Uri);
|
||||
return u.IsFile ? u.LocalPath : System.Net.WebUtility.UrlDecode(u.AbsolutePath);
|
||||
}
|
||||
catch { return Uri; }
|
||||
}
|
||||
}
|
||||
|
||||
public string? RawLyrics { get; set; }
|
||||
public string? LocalAlbumArtPath { get; set; }
|
||||
public byte[]? AlbumArtByteArray { get; set; }
|
||||
|
||||
public string ParentFolderName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Uri)) return "";
|
||||
try
|
||||
{
|
||||
// 使用 Uri Segments 安全获取倒数第二层 (文件夹名)
|
||||
// Segments 示例: "/", "Music/", "Artist/", "Song.mp3"
|
||||
var u = new System.Uri(Uri);
|
||||
if (u.Segments.Length > 1)
|
||||
{
|
||||
// 取倒数第二个 segment (如果是文件)
|
||||
// 注意处理末尾斜杠
|
||||
string folder = u.Segments[u.Segments.Length - 2];
|
||||
return System.Net.WebUtility.UrlDecode(folder.TrimEnd('/', '\\'));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ParentFolderPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Uri)) return "";
|
||||
try
|
||||
{
|
||||
var u = new System.Uri(Uri);
|
||||
if (u.IsFile)
|
||||
{
|
||||
// 本地文件:返回目录路径 C:\Music
|
||||
return System.IO.Path.GetDirectoryName(u.LocalPath) ?? "";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 远程文件:返回去掉文件名的 URI
|
||||
// new Uri(u, ".") 表示当前目录
|
||||
return new System.Uri(u, ".").AbsoluteUri;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Uri)) return "";
|
||||
try
|
||||
{
|
||||
var u = new System.Uri(Uri);
|
||||
if (u.IsFile) return System.IO.Path.GetFileName(u.LocalPath);
|
||||
|
||||
// 远程文件:获取 AbsolutePath 的最后一段并解码
|
||||
// 例如: /Music/My%20Song.mp3 -> My Song.mp3
|
||||
string rawName = System.IO.Path.GetFileName(u.AbsolutePath);
|
||||
return System.Net.WebUtility.UrlDecode(rawName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return System.IO.Path.GetFileName(Uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Title { get; set; } = "";
|
||||
public string Artist { 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 ExtendedTrack() : base() { }
|
||||
|
||||
public ExtendedTrack(string path) : base(path)
|
||||
public ExtendedTrack(string uriString) : base()
|
||||
{
|
||||
Path = path;
|
||||
Uri = uriString;
|
||||
|
||||
string atlPath = uriString;
|
||||
try
|
||||
{
|
||||
var u = new Uri(uriString);
|
||||
if (u.IsFile) atlPath = u.LocalPath;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 用于本地文件
|
||||
var track = new Track(atlPath);
|
||||
SetFromTrack(track);
|
||||
}
|
||||
|
||||
public ExtendedTrack(string path, Stream stream) : base(stream, System.IO.Path.GetExtension(path))
|
||||
public ExtendedTrack(FileCacheEntity? entity, Stream? stream = null) : base()
|
||||
{
|
||||
Path = path;
|
||||
SetRawLyrics(new StreamFileAbstraction(path, stream));
|
||||
if (entity == null) return;
|
||||
|
||||
this.Uri = entity.Uri;
|
||||
|
||||
this.Title = entity.Title;
|
||||
this.Artist = entity.Artists;
|
||||
this.Album = entity.Album;
|
||||
this.Year = entity.Year;
|
||||
this.Bitrate = entity.Bitrate;
|
||||
this.SampleRate = entity.SampleRate;
|
||||
this.BitDepth = entity.BitDepth;
|
||||
|
||||
this.Duration = entity.Duration;
|
||||
|
||||
this.AudioFormatName = entity.AudioFormatName;
|
||||
this.AudioFormatShortName = entity.AudioFormatShortName;
|
||||
|
||||
this.Encoder = entity.Encoder;
|
||||
|
||||
this.RawLyrics = entity.EmbeddedLyrics;
|
||||
this.LocalAlbumArtPath = entity.LocalAlbumArtPath;
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
var track = new Track(stream, Path.GetExtension(FileName));
|
||||
SetFromTrack(track);
|
||||
SetRawLyrics(new StreamFileAbstraction(Uri, stream));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFromTrack(Track? track)
|
||||
{
|
||||
if (track == null) return;
|
||||
|
||||
this.Title = track.Title;
|
||||
this.Artist = track.Artist;
|
||||
this.Album = track.Album;
|
||||
this.Year = track.Year;
|
||||
this.Bitrate = track.Bitrate;
|
||||
this.SampleRate = track.SampleRate;
|
||||
this.BitDepth = track.BitDepth;
|
||||
|
||||
this.Duration = track.Duration;
|
||||
|
||||
this.AudioFormatName = track.AudioFormat.Name;
|
||||
this.AudioFormatShortName = track.AudioFormat.ShortName;
|
||||
|
||||
this.Encoder = track.Encoder;
|
||||
|
||||
this.AlbumArtByteArray = null;
|
||||
|
||||
if (track.EmbeddedPictures != null && track.EmbeddedPictures.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var validPics = track.EmbeddedPictures.Where(p => p != null).ToList();
|
||||
|
||||
if (validPics.Count > 0)
|
||||
{
|
||||
var cover = validPics.FirstOrDefault(p => p.PicType == PictureInfo.PIC_TYPE.Front);
|
||||
|
||||
if (cover == null)
|
||||
{
|
||||
cover = validPics.First();
|
||||
}
|
||||
|
||||
this.AlbumArtByteArray = cover.PictureData;
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
private void SetRawLyrics(StreamFileAbstraction streamFileAbstraction)
|
||||
|
||||
@@ -1,22 +1,57 @@
|
||||
using SQLite;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
[Table("FileCache")]
|
||||
public class FileCacheEntity : UnifiedFileItem
|
||||
public class FileCacheEntity
|
||||
{
|
||||
[PrimaryKey, AutoIncrement]
|
||||
public int Id { get; set; }
|
||||
|
||||
// 【新增】关键字段!
|
||||
// 关联到 MediaFolder.Id。
|
||||
// 作用:
|
||||
// 1. 区分不同配置(即使两个配置连的是同一个 SMB,但在 APP 里视为不同源)。
|
||||
// 2. 删除配置时,可以由 MediaFolderId 快速级联删除所有缓存。
|
||||
[Indexed]
|
||||
public string ParentPath { get; set; }
|
||||
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 FullPath { get; set; }
|
||||
public string Uri { get; set; }
|
||||
|
||||
public string Title { 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; } // 建议明确单位,例如 DurationMs
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
@@ -15,32 +14,85 @@ namespace BetterLyrics.WinUI3.Models
|
||||
[ObservableProperty] public partial string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients][NotifyPropertyChangedFor(nameof(ConnectionSummary))] public partial string Path { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedRecipients]
|
||||
[NotifyPropertyChangedFor(nameof(IsLocal))]
|
||||
[NotifyPropertyChangedFor(nameof(ConnectionSummary))]
|
||||
[NotifyPropertyChangedFor(nameof(UriString))]
|
||||
public partial FileSourceType SourceType { get; set; } = FileSourceType.Local;
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Name { get; set; }
|
||||
|
||||
[ObservableProperty] public partial string UserName { get; set; }
|
||||
// 连接属性
|
||||
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial string UserName { get; set; }
|
||||
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial string UriScheme { get; set; }
|
||||
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial string UriHost { get; set; }
|
||||
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial int UriPort { get; set; } = -1;
|
||||
|
||||
[ObservableProperty] public partial int Port { get; set; } = 80;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedRecipients]
|
||||
[NotifyPropertyChangedFor(nameof(ConnectionSummary))]
|
||||
[NotifyPropertyChangedFor(nameof(UriString))]
|
||||
public partial string UriPath { get; set; }
|
||||
|
||||
[JsonIgnore] public string Password { get; set; }
|
||||
|
||||
[JsonIgnore] public bool IsLocal => SourceType == FileSourceType.Local;
|
||||
|
||||
[JsonIgnore][ObservableProperty] public partial bool IsIndexing { get; set; } = false;
|
||||
[JsonIgnore][ObservableProperty] public partial double IndexingProgress { get; set; } = 0;
|
||||
[JsonIgnore][ObservableProperty] public partial string IndexingStatusText { get; set; } = "";
|
||||
|
||||
[JsonIgnore][ObservableProperty] public partial bool IsCleaningUp { get; set; } = false;
|
||||
[JsonIgnore][ObservableProperty] public partial string CleaningUpStatusText { get; set; } = "";
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DateTime? LastSyncTime { get; set; }
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AutoScanInterval ScanInterval { get; set; } = AutoScanInterval.Disabled;
|
||||
|
||||
public Uri GetStandardUri()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsLocal)
|
||||
{
|
||||
return new Uri(UriPath);
|
||||
}
|
||||
|
||||
var builder = new UriBuilder
|
||||
{
|
||||
Scheme = UriScheme ?? "file",
|
||||
Host = UriHost,
|
||||
Port = UriPort,
|
||||
UserName = UserName
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(UriPath))
|
||||
{
|
||||
string cleanPath = UriPath.Replace("\\", "/");
|
||||
if (!cleanPath.StartsWith("/")) cleanPath = "/" + cleanPath;
|
||||
builder.Path = cleanPath;
|
||||
}
|
||||
|
||||
return builder.Uri;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new Uri("about:blank");
|
||||
}
|
||||
}
|
||||
|
||||
// 例:smb://user@host:445/share/path
|
||||
[JsonIgnore]
|
||||
public string UriString => GetStandardUri().AbsoluteUri;
|
||||
|
||||
[JsonIgnore]
|
||||
public string ConnectionSummary
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsLocal) return Path;
|
||||
return $"{SourceType} - {Path} {(string.IsNullOrEmpty(UserName) ? "" : $"({UserName})")}";
|
||||
if (IsLocal) return UriPath;
|
||||
return $"{UriScheme}://{UriHost}{(UriPort > 0 ? ":" + UriPort : "")}/{UriPath?.TrimStart('/', '\\')} {(string.IsNullOrEmpty(UserName) ? "" : $"({UserName})")}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +102,8 @@ namespace BetterLyrics.WinUI3.Models
|
||||
|
||||
public MediaFolder(string path)
|
||||
{
|
||||
Path = path;
|
||||
UriPath = path;
|
||||
SourceType = FileSourceType.Local;
|
||||
}
|
||||
|
||||
public IUnifiedFileSystem? CreateFileSystem()
|
||||
@@ -63,12 +116,13 @@ namespace BetterLyrics.WinUI3.Models
|
||||
|
||||
return SourceType switch
|
||||
{
|
||||
FileSourceType.Local => new LocalFileSystem(Path),
|
||||
FileSourceType.SMB => new SMBFileSystem(Path, UserName, Password),
|
||||
FileSourceType.FTP => new FTPFileSystem(Path, UserName, Password, Port, Path),
|
||||
FileSourceType.WebDav => new WebDavFileSystem(Path, UserName, Password, Port, Path),
|
||||
FileSourceType.Local => new LocalFileSystem(this),
|
||||
FileSourceType.SMB => new SMBFileSystem(this),
|
||||
FileSourceType.FTP => new FTPFileSystem(this),
|
||||
FileSourceType.WebDav => new WebDavFileSystem(this),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class UnifiedFileItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FullPath { get; set; }
|
||||
public bool IsFolder { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
@@ -22,11 +23,13 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
private readonly HttpClient _iTunesHttpClinet;
|
||||
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public AlbumArtSearchService(ISettingsService settingsService, ILogger<AlbumArtSearchService> logger)
|
||||
public AlbumArtSearchService(ISettingsService settingsService, IFileSystemService fileSystemService, ILogger<AlbumArtSearchService> logger)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_logger = logger;
|
||||
_iTunesHttpClinet = new();
|
||||
}
|
||||
@@ -77,70 +80,53 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
|
||||
private async Task<byte[]?> SearchFile(SongInfo songInfo)
|
||||
{
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (!folder.IsEnabled) continue;
|
||||
var enabledIds = _settingsService.AppSettings.LocalMediaFolders
|
||||
.Where(f => f.IsEnabled)
|
||||
.Select(f => f.Id)
|
||||
.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null) continue;
|
||||
if (!await fs.ConnectAsync()) continue;
|
||||
if (enabledIds.Count == 0) return null;
|
||||
|
||||
// 递归扫描
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue(""); // 根目录
|
||||
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
var items = await fs.GetFilesAsync(currentPath);
|
||||
FileCacheEntity? bestMatch = null;
|
||||
|
||||
foreach (var item in items)
|
||||
foreach (var item in allFiles)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
|
||||
continue;
|
||||
}
|
||||
var ext = Path.GetExtension(item.FileName).ToLower();
|
||||
if (!FileHelper.MusicExtensions.Contains(ext)) continue;
|
||||
|
||||
var ext = Path.GetExtension(item.Name).ToLower();
|
||||
if (FileHelper.MusicExtensions.Contains(ext))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = await fs.OpenReadAsync(item.FullPath))
|
||||
{
|
||||
var track = new ExtendedTrack(item.FullPath, stream);
|
||||
bool isMetadataMatch = (item.Title == songInfo.Title && item.Artists == songInfo.DisplayArtists);
|
||||
|
||||
bool isMetadataMatch = (track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists);
|
||||
bool isFilenameMatch = StringHelper.IsSwitchableNormalizedMatch(
|
||||
Path.GetFileNameWithoutExtension(item.Name),
|
||||
Path.GetFileNameWithoutExtension(item.FileName),
|
||||
songInfo.DisplayArtists,
|
||||
songInfo.Title
|
||||
);
|
||||
|
||||
if (isMetadataMatch || isFilenameMatch)
|
||||
{
|
||||
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
|
||||
if (bytes != null && bytes.Length > 0)
|
||||
bestMatch = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch == null || string.IsNullOrEmpty(bestMatch.LocalAlbumArtPath))
|
||||
{
|
||||
return bytes;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
try
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
if (File.Exists(bestMatch.LocalAlbumArtPath))
|
||||
{
|
||||
return await File.ReadAllBytesAsync(bestMatch.LocalAlbumArtPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"读取本地缓存失败: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,85 +1,606 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
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.Extensions.Logging;
|
||||
using SQLite;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.FileSystemService
|
||||
{
|
||||
public class FileSystemService : IFileSystemService
|
||||
public partial class FileSystemService : BaseViewModel, IFileSystemService,
|
||||
IRecipient<PropertyChangedMessage<AutoScanInterval>>,
|
||||
IRecipient<PropertyChangedMessage<bool>>
|
||||
{
|
||||
private readonly IUnifiedFileSystem _provider;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILogger<FileSystemService> _logger;
|
||||
|
||||
private readonly SQLiteAsyncConnection _db;
|
||||
private bool _isInitialized = false;
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, CancellationTokenSource> _folderTimerTokens = new();
|
||||
private static readonly SemaphoreSlim _dbLock = new(1, 1);
|
||||
private static readonly SemaphoreSlim _folderScanLock = new(1, 1);
|
||||
|
||||
public FileSystemService(IUnifiedFileSystem provider)
|
||||
public FileSystemService(ISettingsService settingsService, ILocalizationService localizationService, ILogger<FileSystemService> logger)
|
||||
{
|
||||
_provider = provider;
|
||||
|
||||
var dbPath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "files_cache.db");
|
||||
_db = new SQLiteAsyncConnection(dbPath);
|
||||
_logger = logger;
|
||||
_localizationService = localizationService;
|
||||
_settingsService = settingsService;
|
||||
_db = new SQLiteAsyncConnection(PathHelper.FilesCachePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化(连接)数据库
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
await _provider.ConnectAsync();
|
||||
|
||||
await _db.CreateTableAsync<FileCacheEntity>();
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false)
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
var cachedEntities = await _db.Table<FileCacheEntity>()
|
||||
.Where(x => x.ParentPath == relativePath)
|
||||
.ToListAsync();
|
||||
|
||||
var result = cachedEntities.Select(x => new UnifiedFileItem
|
||||
string queryParentUri;
|
||||
if (parentFolder == null)
|
||||
{
|
||||
Name = x.Name,
|
||||
FullPath = x.FullPath,
|
||||
IsFolder = x.IsFolder,
|
||||
}).ToList();
|
||||
|
||||
_ = SyncInBackground(relativePath);
|
||||
return result;
|
||||
if (!forceRefresh) forceRefresh = true;
|
||||
queryParentUri = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
queryParentUri = parentFolder.Uri;
|
||||
}
|
||||
|
||||
private async Task SyncInBackground(string relativePath)
|
||||
List<FileCacheEntity> cachedEntities = new List<FileCacheEntity>();
|
||||
|
||||
if (parentFolder != null)
|
||||
{
|
||||
var remoteItems = await _provider.GetFilesAsync(relativePath);
|
||||
cachedEntities = await _db.Table<FileCacheEntity>()
|
||||
.Where(x => x.MediaFolderId == configId && x.ParentUri == queryParentUri)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
var newEntities = remoteItems.Select(item => new FileCacheEntity
|
||||
bool needSync = forceRefresh || cachedEntities.Count == 0;
|
||||
|
||||
if (needSync)
|
||||
{
|
||||
ParentPath = relativePath,
|
||||
cachedEntities = await SyncAsync(provider, parentFolder, configId);
|
||||
}
|
||||
|
||||
FullPath = item.FullPath,
|
||||
Name = item.Name,
|
||||
IsFolder = item.IsFolder,
|
||||
});
|
||||
return cachedEntities;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
List<FileCacheEntity> remoteItems;
|
||||
try
|
||||
{
|
||||
remoteItems = await provider.GetFilesAsync(parentFolder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Network sync error: {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (remoteItems == null) return [];
|
||||
|
||||
string targetParentUri = "";
|
||||
if (remoteItems.Count > 0)
|
||||
{
|
||||
targetParentUri = remoteItems[0].ParentUri ?? "";
|
||||
}
|
||||
else if (parentFolder != null)
|
||||
{
|
||||
targetParentUri = parentFolder.Uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _db.RunInTransactionAsync(conn =>
|
||||
{
|
||||
conn.Execute("DELETE FROM FileCache WHERE ParentPath = ?", relativePath);
|
||||
conn.InsertAll(newEntities);
|
||||
var dbItems = conn.Table<FileCacheEntity>()
|
||||
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
|
||||
.ToList();
|
||||
|
||||
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))
|
||||
{
|
||||
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
|
||||
{
|
||||
toInsert.Add(remote);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dbItem in dbItems)
|
||||
{
|
||||
if (!remoteMap.ContainsKey(dbItem.Uri))
|
||||
{
|
||||
toDelete.Add(dbItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (toInsert.Count > 0) conn.InsertAll(toInsert);
|
||||
if (toUpdate.Count > 0) conn.UpdateAll(toUpdate);
|
||||
if (toDelete.Count > 0)
|
||||
{
|
||||
foreach (var item in toDelete) conn.Delete(item);
|
||||
}
|
||||
});
|
||||
|
||||
FolderUpdated?.Invoke(this, relativePath);
|
||||
}
|
||||
var finalItems = await _db.Table<FileCacheEntity>()
|
||||
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<Stream> OpenFileAsync(UnifiedFileItem item)
|
||||
FolderUpdated?.Invoke(this, targetParentUri);
|
||||
|
||||
return finalItems;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return await _provider.OpenReadAsync(item.FullPath);
|
||||
System.Diagnostics.Debug.WriteLine($"Database sync error: {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateMetadataAsync(FileCacheEntity entity)
|
||||
{
|
||||
// 现在的实体已经包含了完整信息,直接 Update 即可
|
||||
// 我们只需要确保 Where 子句用的是主键或者 Uri
|
||||
|
||||
// 简化版 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 = ?
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity)
|
||||
{
|
||||
// 直接传递实体给 Provider
|
||||
return await provider.OpenReadAsync(entity);
|
||||
}
|
||||
|
||||
public async Task DeleteCacheForMediaFolderAsync(MediaFolder folder)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
|
||||
folder.IsCleaningUp = true;
|
||||
});
|
||||
|
||||
if (_folderTimerTokens.TryRemove(folder.Id, out var cts))
|
||||
{
|
||||
cts.Cancel();
|
||||
cts.Dispose();
|
||||
_logger.LogInformation("DeleteCacheForMediaFolderAsync: {}", "cts.Dispose();");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _folderScanLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
await _dbLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await _db.ExecuteAsync("DELETE FROM FileCache WHERE MediaFolderId = ?", folder.Id);
|
||||
await _db.ExecuteAsync("VACUUM");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dbLock.Release();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_folderScanLock.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("DeleteCacheForMediaFolderAsync: {}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.CleaningUpStatusText = "";
|
||||
folder.IsCleaningUp = false;
|
||||
folder.LastSyncTime = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ScanMediaFolderAsync(MediaFolder folder, CancellationToken token = default)
|
||||
{
|
||||
if (folder == null || !folder.IsEnabled) return;
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.IsIndexing = true;
|
||||
folder.IndexingProgress = 0;
|
||||
folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceWaitingForScan");
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await _folderScanLock.WaitAsync(token);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceConnecting"));
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null || !await fs.ConnectAsync())
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceConnectFailed"));
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceFetchingFileList"));
|
||||
|
||||
var filesToProcess = new List<FileCacheEntity>();
|
||||
var foldersToScan = new Queue<FileCacheEntity?>();
|
||||
foldersToScan.Enqueue(null); // 根目录
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
var currentParent = foldersToScan.Dequeue();
|
||||
|
||||
var items = await GetFilesAsync(fs, currentParent, folder.Id, forceRefresh: true);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.IsDirectory)
|
||||
{
|
||||
foldersToScan.Enqueue(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ext = Path.GetExtension(item.FileName).ToLower();
|
||||
if (FileHelper.AllSupportedExtensions.Contains(ext))
|
||||
{
|
||||
filesToProcess.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int total = filesToProcess.Count;
|
||||
int current = 0;
|
||||
|
||||
foreach (var item in filesToProcess)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
current++;
|
||||
|
||||
if (current % 10 == 0 || current == total)
|
||||
{
|
||||
double progress = (double)current / total * 100;
|
||||
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
folder.IndexingProgress = progress;
|
||||
folder.IndexingStatusText = $"{_localizationService.GetLocalizedString("FileSystemServiceParsing")} {current}/{total}";
|
||||
});
|
||||
}
|
||||
|
||||
if (item.IsMetadataParsed) continue;
|
||||
|
||||
var ext = Path.GetExtension(item.FileName).ToLower();
|
||||
|
||||
try
|
||||
{
|
||||
if (FileHelper.MusicExtensions.Contains(ext))
|
||||
{
|
||||
using var originalStream = await OpenFileAsync(fs, item);
|
||||
if (originalStream == null) continue;
|
||||
|
||||
ExtendedTrack track;
|
||||
if (originalStream.CanSeek)
|
||||
{
|
||||
track = new ExtendedTrack(item, originalStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var memStream = new MemoryStream();
|
||||
await originalStream.CopyToAsync(memStream, token);
|
||||
memStream.Position = 0;
|
||||
track = new ExtendedTrack(item, memStream);
|
||||
}
|
||||
|
||||
if (track.Duration > 0)
|
||||
{
|
||||
// 保存封面
|
||||
string? artPath = await SaveAlbumArtToDiskAsync(track);
|
||||
|
||||
// 填充实体
|
||||
item.Title = track.Title;
|
||||
item.Artists = track.Artist;
|
||||
item.Album = track.Album;
|
||||
item.Year = track.Year;
|
||||
item.Bitrate = track.Bitrate;
|
||||
item.SampleRate = track.SampleRate;
|
||||
item.BitDepth = track.BitDepth;
|
||||
item.Duration = track.Duration;
|
||||
item.AudioFormatName = track.AudioFormatName;
|
||||
item.AudioFormatShortName = track.AudioFormatShortName;
|
||||
item.Encoder = track.Encoder;
|
||||
item.EmbeddedLyrics = track.RawLyrics; // 内嵌歌词
|
||||
item.LocalAlbumArtPath = artPath;
|
||||
item.IsMetadataParsed = true;
|
||||
}
|
||||
}
|
||||
else if (FileHelper.LyricExtensions.Contains(ext))
|
||||
{
|
||||
using var stream = await OpenFileAsync(fs, item);
|
||||
if (stream != null)
|
||||
{
|
||||
using var reader = new StreamReader(stream);
|
||||
string content = await reader.ReadToEndAsync();
|
||||
|
||||
item.EmbeddedLyrics = content;
|
||||
item.IsMetadataParsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.IsMetadataParsed)
|
||||
{
|
||||
await _dbLock.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
await UpdateMetadataAsync(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dbLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("ScanMediaFolderAsync: {}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.LastSyncTime = DateTime.Now;
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_folderScanLock.Release();
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.IsIndexing = false;
|
||||
folder.IndexingStatusText = "";
|
||||
folder.IndexingProgress = 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
if (enabledConfigIds == null || !enabledConfigIds.Any())
|
||||
{
|
||||
return new List<FileCacheEntity>();
|
||||
}
|
||||
|
||||
var idList = enabledConfigIds.ToList();
|
||||
|
||||
// SQL 逻辑: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
|
||||
var results = await _db.Table<FileCacheEntity>()
|
||||
.Where(x => x.IsMetadataParsed && idList.Contains(x.MediaFolderId))
|
||||
.ToListAsync();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void StartAllFolderTimers()
|
||||
{
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (folder.IsEnabled)
|
||||
{
|
||||
UpdateFolderTimer(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFolderTimer(MediaFolder folder)
|
||||
{
|
||||
if (_folderTimerTokens.TryRemove(folder.Id, out var oldCts))
|
||||
{
|
||||
oldCts.Cancel();
|
||||
oldCts.Dispose();
|
||||
}
|
||||
|
||||
if (!folder.IsEnabled || folder.ScanInterval == AutoScanInterval.Disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newCts = new CancellationTokenSource();
|
||||
_folderTimerTokens[folder.Id] = newCts;
|
||||
|
||||
TimeSpan period = folder.ScanInterval switch
|
||||
{
|
||||
AutoScanInterval.Every15Minutes => TimeSpan.FromMinutes(15),
|
||||
AutoScanInterval.EveryHour => TimeSpan.FromHours(1),
|
||||
AutoScanInterval.Every6Hours => TimeSpan.FromHours(6),
|
||||
AutoScanInterval.Daily => TimeSpan.FromDays(1),
|
||||
_ => TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var timer = new PeriodicTimer(period);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(newCts.Token))
|
||||
{
|
||||
await ScanMediaFolderAsync(folder, newCts.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"文件夹 {folder.Name} 定时扫描出错: {ex.Message}");
|
||||
}
|
||||
}, newCts.Token);
|
||||
}
|
||||
|
||||
// 参数为 string parentUri,表示哪个文件夹的内容变了
|
||||
public event EventHandler<string>? FolderUpdated;
|
||||
|
||||
private async Task<string?> SaveAlbumArtToDiskAsync(ExtendedTrack track)
|
||||
{
|
||||
var picData = track.AlbumArtByteArray;
|
||||
if (picData == null || picData.Length == 0) return null;
|
||||
|
||||
try
|
||||
{
|
||||
string hash = ComputeHashForBytes(picData);
|
||||
string safeName = hash + ".jpg";
|
||||
|
||||
string localPath = Path.Combine(PathHelper.LocalAlbumArtCacheDirectory, safeName);
|
||||
|
||||
if (File.Exists(localPath)) return localPath;
|
||||
|
||||
await File.WriteAllBytesAsync(localPath, picData);
|
||||
|
||||
return localPath;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string ComputeHashForBytes(byte[] data)
|
||||
{
|
||||
using (var md5 = System.Security.Cryptography.MD5.Create())
|
||||
{
|
||||
var hashBytes = md5.ComputeHash(data);
|
||||
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<AutoScanInterval> message)
|
||||
{
|
||||
if (message.Sender is MediaFolder mediaFolder)
|
||||
{
|
||||
if (message.PropertyName == nameof(MediaFolder.ScanInterval))
|
||||
{
|
||||
UpdateFolderTimer(mediaFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<bool> message)
|
||||
{
|
||||
if (message.Sender is MediaFolder mediaFolder)
|
||||
{
|
||||
if (message.PropertyName == nameof(MediaFolder.IsEnabled))
|
||||
{
|
||||
UpdateFolderTimer(mediaFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.FileSystemService
|
||||
@@ -9,8 +11,54 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
|
||||
public interface IFileSystemService
|
||||
{
|
||||
Task InitializeAsync();
|
||||
Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath);
|
||||
Task<Stream> OpenFileAsync(UnifiedFileItem item);
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库拉取文件(必要时需要从远端/本地同步至数据库)
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="parentFolder"></param>
|
||||
/// <param name="configId"></param>
|
||||
/// <param name="forceRefresh">强制需要从远端/本地同步至数据库</param>
|
||||
/// <returns></returns>
|
||||
Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false);
|
||||
|
||||
/// <summary>
|
||||
/// 打开文件(通过远端/本地流)
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// 更新数据库(单个文件)
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
Task UpdateMetadataAsync(FileCacheEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库删除
|
||||
/// </summary>
|
||||
/// <param name="folder"></param>
|
||||
/// <returns></returns>
|
||||
Task DeleteCacheForMediaFolderAsync(MediaFolder folder);
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库拉取文件(必要时需要从远端/本地同步至数据库)。对于需要解析的文件,打开流填充元数据并回写至数据库。
|
||||
/// </summary>
|
||||
/// <param name="folder"></param>
|
||||
/// <returns></returns>
|
||||
Task ScanMediaFolderAsync(MediaFolder folder, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库拉取
|
||||
/// </summary>
|
||||
/// <param name="enabledConfigIds"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds);
|
||||
|
||||
void StartAllFolderTimers();
|
||||
|
||||
event EventHandler<string> FolderUpdated;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,18 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
|
||||
public interface IUnifiedFileSystem : IDisposable
|
||||
{
|
||||
Task<bool> ConnectAsync();
|
||||
Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath);
|
||||
Task<Stream> OpenReadAsync(string fullPath);
|
||||
/// <summary>
|
||||
/// 从流拉取
|
||||
/// </summary>
|
||||
/// <param name="parentFolder"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null);
|
||||
/// <summary>
|
||||
/// 打开流
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
Task<Stream?> OpenReadAsync(FileCacheEntity file);
|
||||
Task DisconnectAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using FluentFTP;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -11,43 +11,142 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
public partial class FTPFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private readonly AsyncFtpClient _client;
|
||||
private readonly string _rootPath; // 服务器上的根路径 (例如 /pub/music)
|
||||
private readonly MediaFolder _config;
|
||||
|
||||
public FTPFileSystem(string host, string user, string pass, int port, string remotePath)
|
||||
public FTPFileSystem(MediaFolder config)
|
||||
{
|
||||
// 如果 path 是 "192.168.1.5/Music",我们需要把 /Music 拆出来
|
||||
// 但为了简单,假设 host 仅仅是 IP,remotePath 才是路径
|
||||
_rootPath = remotePath ?? "/";
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
|
||||
var config = new FtpConfig { ConnectTimeout = 5000 };
|
||||
_client = new AsyncFtpClient(host, user ?? "anonymous", pass ?? "", port > 0 ? port : 21, config);
|
||||
// 初始化 FluentFTP 配置
|
||||
var ftpConfig = new FtpConfig
|
||||
{
|
||||
ConnectTimeout = 5000,
|
||||
// 根据需要配置编码,防止中文乱码
|
||||
// Encoding = System.Text.Encoding.GetEncoding("GB2312")
|
||||
};
|
||||
|
||||
// FluentFTP 构造函数接收主机、用户、密码、端口
|
||||
// 端口如果为 -1 (MediaFolder 默认值),则让 FluentFTP 使用默认 21
|
||||
int port = _config.UriPort > 0 ? _config.UriPort : 0;
|
||||
|
||||
_client = new AsyncFtpClient(
|
||||
_config.UriHost,
|
||||
_config.UserName ?? "anonymous",
|
||||
_config.Password ?? "",
|
||||
port,
|
||||
ftpConfig
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.AutoConnect();
|
||||
return _client.IsConnected;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
catch
|
||||
{
|
||||
string targetPath = Path.Combine(_rootPath, relativePath).Replace("\\", "/");
|
||||
|
||||
var items = await _client.GetListing(targetPath);
|
||||
return items.Select(i => new UnifiedFileItem
|
||||
{
|
||||
Name = i.Name,
|
||||
FullPath = i.FullName,
|
||||
IsFolder = i.Type == FtpObjectType.Directory,
|
||||
}).ToList();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
return await _client.OpenRead(fullPath);
|
||||
var result = new List<FileCacheEntity>();
|
||||
|
||||
// 1. 确定目标服务器路径
|
||||
string targetServerPath;
|
||||
Uri parentUri;
|
||||
|
||||
if (parentFolder == null)
|
||||
{
|
||||
// 根目录:从配置中提取路径 (例如 /Music)
|
||||
// GetStandardUri().AbsolutePath 会返回带前导斜杠的路径
|
||||
var rootUri = _config.GetStandardUri();
|
||||
targetServerPath = rootUri.AbsolutePath; // "/Music"
|
||||
parentUri = rootUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 子目录:将标准 URI 转换为 FTP 服务器路径
|
||||
targetServerPath = GetServerPathFromUri(parentFolder.Uri);
|
||||
parentUri = new Uri(parentFolder.Uri);
|
||||
}
|
||||
|
||||
// 确保路径合法性 (FluentFTP 喜欢 Unix 风格斜杠)
|
||||
targetServerPath = targetServerPath.Replace("\\", "/");
|
||||
if (string.IsNullOrEmpty(targetServerPath)) targetServerPath = "/";
|
||||
|
||||
// 2. 获取列表
|
||||
var items = await _client.GetListing(targetServerPath);
|
||||
|
||||
// 3. 准备 Base URI 用于拼接子项
|
||||
// FTP URI 基础部分: ftp://host:port
|
||||
string baseUriStr = $"{parentUri.Scheme}://{parentUri.Host}";
|
||||
if (parentUri.Port > 0) baseUriStr += $":{parentUri.Port}";
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
// 排除 . 和 ..
|
||||
if (item.Name == "." || item.Name == "..") continue;
|
||||
|
||||
// 构建完整的标准 URI
|
||||
// item.FullName 是服务器上的绝对路径 (例如 /Music/Song.mp3)
|
||||
// 我们需要把它拼成 ftp://host:port/Music/Song.mp3
|
||||
// 注意:Path.Combine 在 Windows 上可能会用反斜杠,这里手动拼接更安全
|
||||
|
||||
string itemFullPath = item.FullName.StartsWith("/") ? item.FullName : "/" + item.FullName;
|
||||
string standardUri = baseUriStr + itemFullPath; // Uri 构造函数会自动处理编码
|
||||
|
||||
result.Add(new FileCacheEntity
|
||||
{
|
||||
MediaFolderId = _config.Id,
|
||||
|
||||
// 记录父级 URI
|
||||
// 如果 parentFolder 为空,则父级是 Config 的根 URI
|
||||
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
|
||||
|
||||
Uri = standardUri, // 标准化 URI
|
||||
|
||||
FileName = item.Name,
|
||||
IsDirectory = item.Type == FtpObjectType.Directory,
|
||||
|
||||
FileSize = item.Size,
|
||||
LastModified = item.Modified
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
|
||||
{
|
||||
if (entity == null) return null;
|
||||
|
||||
// 从标准 URI 还原回 FTP 服务器路径
|
||||
string serverPath = GetServerPathFromUri(entity.Uri);
|
||||
|
||||
return await _client.OpenRead(serverPath);
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync() => await _client.Disconnect();
|
||||
public void Dispose() => _client?.Dispose();
|
||||
|
||||
// =========================================================
|
||||
// ★ 私有辅助方法:URI -> FTP Path
|
||||
// =========================================================
|
||||
private string GetServerPathFromUri(string uriString)
|
||||
{
|
||||
// 输入: ftp://192.168.1.5:21/Music/Song.mp3
|
||||
// 输出: /Music/Song.mp3
|
||||
|
||||
var uri = new Uri(uriString);
|
||||
|
||||
// Uri.AbsolutePath 自动包含了路径部分 (例如 /Music/Song.mp3)
|
||||
// 并且会自动进行 URL Decode (比如 %20 -> 空格)
|
||||
// 这正是 FluentFTP 需要的格式
|
||||
return uri.AbsolutePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,25 +8,42 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
{
|
||||
public partial class LocalFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private readonly string _rootPath;
|
||||
private readonly MediaFolder _config;
|
||||
private readonly string _rootLocalPath;
|
||||
|
||||
public LocalFileSystem(string rootPath)
|
||||
public LocalFileSystem(MediaFolder config)
|
||||
{
|
||||
_rootPath = rootPath;
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_rootLocalPath = config.UriPath;
|
||||
}
|
||||
|
||||
public Task<bool> ConnectAsync()
|
||||
{
|
||||
return Task.FromResult(Directory.Exists(_rootPath));
|
||||
return Task.FromResult(Directory.Exists(_rootLocalPath));
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
var result = new List<UnifiedFileItem>();
|
||||
var result = new List<FileCacheEntity>();
|
||||
|
||||
var targetPath = string.IsNullOrWhiteSpace(relativePath)
|
||||
? _rootPath
|
||||
: Path.Combine(_rootPath, relativePath);
|
||||
string targetPath;
|
||||
string parentUriString;
|
||||
|
||||
try
|
||||
{
|
||||
if (parentFolder == null)
|
||||
{
|
||||
// 根目录
|
||||
targetPath = _rootLocalPath;
|
||||
parentUriString = _config.GetStandardUri().AbsoluteUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 子目录:从标准 URI (file:///...) 还原为本地路径 (C:\...)
|
||||
var uri = new Uri(parentFolder.Uri);
|
||||
targetPath = uri.LocalPath;
|
||||
parentUriString = parentFolder.Uri;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(targetPath)) return result;
|
||||
|
||||
@@ -34,20 +51,53 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
|
||||
foreach (var item in dirInfo.GetFileSystemInfos())
|
||||
{
|
||||
// 生成标准 URI 作为唯一 ID
|
||||
// new Uri("C:\Path\File") 会自动生成 file:///C:/Path/File
|
||||
var itemUri = new Uri(item.FullName).AbsoluteUri;
|
||||
|
||||
bool isDir = (item.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
||||
result.Add(new UnifiedFileItem
|
||||
long size = 0;
|
||||
|
||||
// DirectoryInfo 没有 Length 属性,只有 FileInfo 有
|
||||
if (!isDir && item is FileInfo fi)
|
||||
{
|
||||
Name = item.Name,
|
||||
FullPath = item.FullName,
|
||||
IsFolder = isDir,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
size = fi.Length;
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
result.Add(new FileCacheEntity
|
||||
{
|
||||
return new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
MediaFolderId = _config.Id, // 关联配置 ID
|
||||
|
||||
ParentUri = parentUriString, // 记录父级 URI
|
||||
|
||||
Uri = itemUri, // 标准化 URI (file:///...)
|
||||
|
||||
FileName = item.Name,
|
||||
IsDirectory = isDir,
|
||||
|
||||
FileSize = size,
|
||||
LastModified = item.LastWriteTime
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Local scan error: {ex.Message}");
|
||||
}
|
||||
|
||||
return await Task.FromResult(result);
|
||||
}
|
||||
|
||||
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
|
||||
{
|
||||
if (entity == null) return null;
|
||||
|
||||
// 将标准 URI (file:///C:/...) 还原为本地路径 (C:\...)
|
||||
string localPath = new Uri(entity.Uri).LocalPath;
|
||||
|
||||
// 使用 FileShare.Read 允许其他程序同时读取
|
||||
// 使用 useAsync: true 优化异步读写性能
|
||||
return new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync() => await Task.CompletedTask;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using SMBLibrary;
|
||||
using SMBLibrary.Client;
|
||||
using System;
|
||||
@@ -12,106 +11,167 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
{
|
||||
public partial class SMBFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private SMB2Client _client;
|
||||
private ISMBFileStore _fileStore;
|
||||
private SMB2Client? _client;
|
||||
private ISMBFileStore? _fileStore;
|
||||
|
||||
private readonly string _ip;
|
||||
private readonly string _shareName;
|
||||
private readonly string _pathInsideShare; // 共享里的子路径
|
||||
private readonly string _username;
|
||||
private readonly string _password;
|
||||
// 保存配置对象的引用,它是我们的“真理来源”
|
||||
private readonly MediaFolder _config;
|
||||
|
||||
// fullPathInput 例如: "192.168.1.5/Music/Pop"
|
||||
public SMBFileSystem(string fullPathInput, string user, string pass)
|
||||
// 缓存解析出来的 Share 名称,因为 TreeConnect 要用
|
||||
private string _shareName;
|
||||
|
||||
public SMBFileSystem(MediaFolder config)
|
||||
{
|
||||
_username = user;
|
||||
_password = pass;
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
|
||||
// 解析路径:分离 IP 和 共享名
|
||||
var parts = fullPathInput.Replace("\\", "/").Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
// 在构造时就解析好 Share 名称,避免后续重复解析
|
||||
// 假设 URI 是 smb://host/ShareName/Folder/Sub
|
||||
// 我们需要提取 "ShareName"
|
||||
var uri = _config.GetStandardUri();
|
||||
|
||||
if (parts.Length >= 1) _ip = parts[0];
|
||||
if (parts.Length >= 2) _shareName = parts[1];
|
||||
|
||||
// 剩下的部分重新拼起来作为子路径
|
||||
if (parts.Length > 2)
|
||||
_pathInsideShare = string.Join("\\", parts.Skip(2));
|
||||
// Segments[0] 是 "/", Segments[1] 是 "ShareName/"
|
||||
if (uri.Segments.Length > 1)
|
||||
{
|
||||
_shareName = uri.Segments[1].TrimEnd('/');
|
||||
}
|
||||
else
|
||||
_pathInsideShare = "";
|
||||
{
|
||||
// 如果没有 ShareName,这在 SMB 中通常是不合法的,但在根目录下可能发生
|
||||
_shareName = "";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_client = new SMB2Client();
|
||||
bool connected = _client.Connect(_ip, SMBTransportType.DirectTCPTransport);
|
||||
|
||||
// 1. 连接主机
|
||||
bool connected = _client.Connect(_config.UriHost, SMBTransportType.DirectTCPTransport);
|
||||
if (!connected) return false;
|
||||
|
||||
var status = _client.Login(string.Empty, _username, _password);
|
||||
// 2. 登录
|
||||
var status = _client.Login(string.Empty, _config.UserName, _config.Password);
|
||||
if (status != NTStatus.STATUS_SUCCESS) return false;
|
||||
|
||||
// 连接具体的共享文件夹
|
||||
if (string.IsNullOrEmpty(_shareName)) return true; // 只连了服务器,没连共享
|
||||
// 3. 连接共享目录 (TreeConnect)
|
||||
// 注意:SMBLibrary 必须先连接到 Share,后续所有文件操作都是基于这个 Share 的相对路径
|
||||
if (string.IsNullOrEmpty(_shareName)) return false;
|
||||
|
||||
_fileStore = _client.TreeConnect(_shareName, out status);
|
||||
return status == NTStatus.STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
catch (Exception)
|
||||
{
|
||||
var result = new List<UnifiedFileItem>();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件列表
|
||||
/// </summary>
|
||||
/// <param name="parentFolder">
|
||||
/// 传入要列出的文件夹实体。
|
||||
/// 如果传入 null,则默认列出 MediaFolder 配置的根目录。
|
||||
/// </param>
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
var result = new List<FileCacheEntity>();
|
||||
if (_fileStore == null) return result;
|
||||
|
||||
// 拼接完整路径: Root里面的子路径 + 传入的相对路径
|
||||
string queryPath = Path.Combine(_pathInsideShare, relativePath).Replace("/", "\\").TrimStart('\\');
|
||||
string smbPath = GetPathRelativeToShare(parentFolder);
|
||||
|
||||
// 打开目录
|
||||
var statusRet = _fileStore.CreateFile(out object handle, out FileStatus status, queryPath,
|
||||
var statusRet = _fileStore.CreateFile(out object handle, out FileStatus status, smbPath,
|
||||
AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read,
|
||||
CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
|
||||
|
||||
if (statusRet != NTStatus.STATUS_SUCCESS) return result;
|
||||
|
||||
// 确保 parentUriString 总是以 / 结尾,方便后续拼接
|
||||
string parentUriString = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri;
|
||||
|
||||
List<QueryDirectoryFileInformation> fileInfo;
|
||||
|
||||
do
|
||||
{
|
||||
statusRet = _fileStore.QueryDirectory(out fileInfo, handle, "*", FileInformationClass.FileDirectoryInformation);
|
||||
|
||||
List<FileDirectoryInformation> list = fileInfo.Select(x => (FileDirectoryInformation)x).ToList();
|
||||
foreach (var item in list)
|
||||
// 【安全检查】如果查询失败或者没有更多文件,fileInfo 可能是 null,直接跳出
|
||||
if (statusRet != NTStatus.STATUS_SUCCESS && statusRet != NTStatus.STATUS_NO_MORE_FILES)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果是 NO_MORE_FILES 但 fileInfo 依然有残留数据(极少见),或者是 SUCCESS
|
||||
if (fileInfo != null)
|
||||
{
|
||||
foreach (var item in fileInfo.Cast<FileDirectoryInformation>())
|
||||
{
|
||||
// 排除当前目录和父目录
|
||||
if (item.FileName == "." || item.FileName == "..") continue;
|
||||
|
||||
result.Add(new UnifiedFileItem
|
||||
// ==================================================
|
||||
// ★ 修正后的 URI 构建逻辑
|
||||
// ==================================================
|
||||
|
||||
// 方法 A (推荐): 使用 Uri 构造函数自动合并
|
||||
// 1. 确保 Base Uri 以 / 结尾 (否则 "folder" + "file" 会变成 "file" 替换掉 "folder")
|
||||
if (!parentUriString.EndsWith("/")) parentUriString += "/";
|
||||
var baseUri = new Uri(parentUriString);
|
||||
|
||||
// 2. 直接利用 Uri 的构造函数处理相对路径
|
||||
// new Uri(baseUri, "filename") 会自动处理编码和斜杠
|
||||
// 注意:如果 item.FileName 包含特殊字符,Uri 类会自动帮我们编码
|
||||
var newUri = new Uri(baseUri, item.FileName);
|
||||
|
||||
// 如果你还是想用 UriBuilder (手动控制更强),请用下面这行代替上面:
|
||||
/*
|
||||
var builder = new UriBuilder(baseUri);
|
||||
// 关键:先 Unescape 解码,变回原始字符串,再拼接,最后赋值给 builder 让它重新编码
|
||||
string cleanBasePath = Uri.UnescapeDataString(baseUri.AbsolutePath);
|
||||
builder.Path = Path.Combine(cleanBasePath, item.FileName).Replace("\\", "/");
|
||||
var newUri = builder.Uri;
|
||||
*/
|
||||
|
||||
result.Add(new FileCacheEntity
|
||||
{
|
||||
Name = item.FileName,
|
||||
FullPath = Path.Combine(queryPath, item.FileName),
|
||||
IsFolder = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory,
|
||||
MediaFolderId = _config.Id,
|
||||
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri, // 保持原始父级 URI (不带末尾斜杠的)
|
||||
|
||||
Uri = newUri.AbsoluteUri, // 使用修正后的 URI
|
||||
|
||||
FileName = item.FileName,
|
||||
IsDirectory = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory,
|
||||
FileSize = item.AllocationSize,
|
||||
LastModified = item.ChangeTime
|
||||
});
|
||||
}
|
||||
|
||||
if (statusRet == NTStatus.STATUS_NO_MORE_FILES)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (statusRet != NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
// Log
|
||||
break;
|
||||
}
|
||||
if (statusRet == NTStatus.STATUS_NO_MORE_FILES) break;
|
||||
|
||||
} while (statusRet == NTStatus.STATUS_SUCCESS);
|
||||
|
||||
_fileStore.CloseFile(handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
/// <summary>
|
||||
/// 打开文件流
|
||||
/// </summary>
|
||||
/// <param name="file">只需要传入文件实体即可</param>
|
||||
public async Task<Stream?> OpenReadAsync(FileCacheEntity file)
|
||||
{
|
||||
var ret = _fileStore.CreateFile(out object handle, out FileStatus status, fullPath,
|
||||
if (_fileStore == null || file == null) return null;
|
||||
|
||||
// ★ 核心简化:直接把对象扔进去,获取路径
|
||||
string smbPath = GetPathRelativeToShare(file);
|
||||
|
||||
var ret = _fileStore.CreateFile(out object handle, out FileStatus status, smbPath,
|
||||
AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, 0, ShareAccess.Read, CreateDisposition.FILE_OPEN, 0, null);
|
||||
|
||||
if (ret != NTStatus.STATUS_SUCCESS) throw new IOException($"SMB Open Error: {ret}");
|
||||
if (ret != NTStatus.STATUS_SUCCESS)
|
||||
throw new IOException($"SMB Open Error: {ret}");
|
||||
|
||||
return new SMBReadOnlyStream(_fileStore, handle);
|
||||
}
|
||||
@@ -126,5 +186,47 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
{
|
||||
_client?.Disconnect();
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// ★ 私有魔法方法:处理所有令人头大的路径逻辑
|
||||
// =========================================================
|
||||
private string GetPathRelativeToShare(FileCacheEntity? entity)
|
||||
{
|
||||
Uri targetUri;
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
targetUri = _config.GetStandardUri();
|
||||
}
|
||||
else
|
||||
{
|
||||
targetUri = new Uri(entity.Uri);
|
||||
}
|
||||
|
||||
// 1. 获取绝对路径
|
||||
// ★★★ 关键修正:必须解码!把 %20 变回空格 ★★★
|
||||
// targetUri.AbsolutePath -> "/Share/My%20Music/Song.mp3"
|
||||
// Uri.UnescapeDataString -> "/Share/My Music/Song.mp3"
|
||||
string absolutePath = Uri.UnescapeDataString(targetUri.AbsolutePath);
|
||||
|
||||
// 2. 移除 ShareName 部分
|
||||
// 确保移除开头的 /
|
||||
string cleanPath = absolutePath.TrimStart('/');
|
||||
|
||||
// 找到 ShareName 后的第一个斜杠
|
||||
int slashIndex = cleanPath.IndexOf('/');
|
||||
|
||||
if (slashIndex == -1)
|
||||
{
|
||||
// 如果没有斜杠,说明就是 Share 根目录
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// 截取 Share 之后的部分
|
||||
string relativePath = cleanPath.Substring(slashIndex + 1);
|
||||
|
||||
// 3. 转换为 Windows 风格的反斜杠 (SMB 协议要求)
|
||||
return relativePath.Replace("/", "\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
private readonly ISMBFileStore _store;
|
||||
private readonly object _handle;
|
||||
private long _position;
|
||||
private long _length; // 新增:缓存文件长度
|
||||
private long _length;
|
||||
|
||||
// SMB 协议建议的最大读取块大小 (64KB 是最安全的通用值)
|
||||
private const int MaxReadChunkSize = 65536;
|
||||
|
||||
public SMBReadOnlyStream(ISMBFileStore store, object handle)
|
||||
{
|
||||
@@ -25,18 +28,15 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果获取失败,这是一个严重问题,意味着无法 Seek 到末尾
|
||||
// 暂时设为 0,但后续读取可能会出问题
|
||||
_length = 0;
|
||||
_length = 0; // 这是一个风险点,但为了不 crash 先设为 0
|
||||
System.Diagnostics.Debug.WriteLine($"SMB GetLength Error: {status}");
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => true;
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => _length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
@@ -45,30 +45,49 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// 保护:如果位置已经超过文件末尾,直接返回 0 (EOF)
|
||||
if (_position >= _length) return 0;
|
||||
|
||||
// 保护:防止读取越界 (请求读取量不能超过剩余量)
|
||||
long remaining = _length - _position;
|
||||
int bytesToRequest = (int)Math.Min(count, remaining);
|
||||
int totalBytesRead = 0;
|
||||
int remainingRequest = count;
|
||||
|
||||
// 为了安全,保留对 remaining 的检查是必须的
|
||||
if (bytesToRequest <= 0) return 0;
|
||||
// 循环读取,直到读完请求的数量,或者文件结束
|
||||
while (remainingRequest > 0)
|
||||
{
|
||||
// 计算剩余文件长度
|
||||
long remainingFile = _length - _position;
|
||||
if (remainingFile <= 0) break; // 已到末尾
|
||||
|
||||
var status = _store.ReadFile(out byte[] data, _handle, _position, bytesToRequest);
|
||||
// 计算本次 SMB 请求的大小 (取三者最小值:请求剩余量、文件剩余量、SMB最大块限制)
|
||||
int bytesToReadThisChunk = (int)Math.Min(Math.Min(remainingRequest, remainingFile), MaxReadChunkSize);
|
||||
|
||||
if (status == NTStatus.STATUS_END_OF_FILE) return 0;
|
||||
// 发送 SMB 请求
|
||||
var status = _store.ReadFile(out byte[] data, _handle, _position, bytesToReadThisChunk);
|
||||
|
||||
// 处理结果
|
||||
if (status == NTStatus.STATUS_END_OF_FILE) break;
|
||||
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
throw new IOException($"SMB Read failed. Status: {status} (Pos: {_position}, Req: {bytesToRequest})");
|
||||
// 遇到错误抛出详细信息
|
||||
throw new IOException($"SMB Read failed. Status: {status}, Position: {_position}, ChunkReq: {bytesToReadThisChunk}");
|
||||
}
|
||||
|
||||
if (data == null || data.Length == 0) return 0;
|
||||
if (data == null || data.Length == 0) break;
|
||||
|
||||
Array.Copy(data, 0, buffer, offset, data.Length);
|
||||
// 复制数据到输出 buffer
|
||||
Array.Copy(data, 0, buffer, offset + totalBytesRead, data.Length);
|
||||
|
||||
// 更新指针和计数器
|
||||
_position += data.Length;
|
||||
return data.Length;
|
||||
totalBytesRead += data.Length;
|
||||
remainingRequest -= data.Length;
|
||||
|
||||
// 如果实际读到的比请求的少,通常意味着提前到了 EOF,或者网络包较小
|
||||
// 这里选择继续循环尝试,直到读不够或者明确 EOF
|
||||
if (data.Length < bytesToReadThisChunk) break;
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
@@ -88,10 +107,9 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
break;
|
||||
}
|
||||
|
||||
// 允许 Seek 超过 EOF (标准 Stream 行为),但在 Read 时会返回 0
|
||||
if (newPos < 0)
|
||||
{
|
||||
throw new IOException("An attempt was made to move the file pointer before the beginning of the file.");
|
||||
throw new IOException("Seek before beginning.");
|
||||
}
|
||||
|
||||
_position = newPos;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -12,70 +11,129 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
|
||||
public partial class WebDavFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private readonly WebDavClient _client;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _rootPath;
|
||||
private readonly MediaFolder _config;
|
||||
private readonly Uri _baseAddress;
|
||||
|
||||
// host: http://192.168.1.5:5005
|
||||
// path: /music
|
||||
public WebDavFileSystem(string host, string user, string pass, int port, string path)
|
||||
public WebDavFileSystem(MediaFolder config)
|
||||
{
|
||||
if (!host.StartsWith("http")) host = $"http://{host}";
|
||||
if (port > 0) host = $"{host}:{port}";
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
|
||||
_baseUrl = host;
|
||||
_rootPath = path ?? "/";
|
||||
// 1. 构建 BaseAddress (只包含 http://host:port/)
|
||||
// MediaFolder.GetStandardUri() 返回的是带路径的完整 URI (http://host:port/path)
|
||||
// 我们需要提取出根用于初始化 WebDavClient
|
||||
var fullUri = _config.GetStandardUri();
|
||||
|
||||
// 提取 "http://host:port"
|
||||
_baseAddress = new Uri($"{fullUri.Scheme}://{fullUri.Authority}");
|
||||
|
||||
_client = new WebDavClient(new WebDavClientParams
|
||||
{
|
||||
BaseAddress = new Uri(_baseUrl),
|
||||
Credentials = new System.Net.NetworkCredential(user, pass)
|
||||
BaseAddress = _baseAddress,
|
||||
Credentials = new System.Net.NetworkCredential(_config.UserName, _config.Password)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
// WebDAV 无状态,Propfind 测试根目录连通性
|
||||
var result = await _client.Propfind(_rootPath);
|
||||
try
|
||||
{
|
||||
// 测试连接:Propfind 请求配置的根路径
|
||||
// GetStandardUri 已经包含了用户设置的路径
|
||||
var result = await _client.Propfind(_config.GetStandardUri().AbsoluteUri);
|
||||
return result.IsSuccessful;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
catch
|
||||
{
|
||||
var targetPath = Path.Combine(_rootPath, relativePath).Replace("\\", "/");
|
||||
var result = await _client.Propfind(targetPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
var list = new List<FileCacheEntity>();
|
||||
|
||||
// 1. 确定目标 URI
|
||||
Uri targetUri;
|
||||
if (parentFolder == null)
|
||||
{
|
||||
targetUri = _config.GetStandardUri();
|
||||
}
|
||||
else
|
||||
{
|
||||
targetUri = new Uri(parentFolder.Uri);
|
||||
}
|
||||
|
||||
// 2. 发送请求 (使用绝对 URI)
|
||||
// WebDavClient 允许传入绝对路径,它会自动处理
|
||||
var result = await _client.Propfind(targetUri.AbsoluteUri);
|
||||
|
||||
var list = new List<UnifiedFileItem>();
|
||||
if (result.IsSuccessful)
|
||||
{
|
||||
// 3. 准备父级 URI 字符串 (用于填充 Entity)
|
||||
// 确保以 / 结尾,方便后续逻辑判断或数据库查询
|
||||
string parentUriString = targetUri.AbsoluteUri;
|
||||
if (!parentUriString.EndsWith("/")) parentUriString += "/";
|
||||
|
||||
// WebDAV 可能会把文件夹自己作为结果返回,我们需要过滤它
|
||||
// 比较时忽略末尾斜杠
|
||||
string targetPathClean = targetUri.AbsolutePath.TrimEnd('/');
|
||||
|
||||
foreach (var res in result.Resources)
|
||||
{
|
||||
if (res == null || res.Uri == null) continue;
|
||||
// res.Uri 通常是相对路径,例如 "/dav/music/file.mp3"
|
||||
// 我们需要将其转换为绝对 URI
|
||||
var itemUri = new Uri(_baseAddress, res.Uri);
|
||||
|
||||
// ★ 过滤掉文件夹自身
|
||||
// 比较 AbsolutePath (例如 /dav/music vs /dav/music)
|
||||
if (itemUri.AbsolutePath.TrimEnd('/') == targetPathClean) continue;
|
||||
|
||||
// 获取文件名 (解码)
|
||||
// res.DisplayName 有时候是空的,这时候需要从 Uri 解析
|
||||
string name = res.DisplayName;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
// 取最后一段,忽略末尾斜杠
|
||||
name = itemUri.AbsolutePath.TrimEnd('/').Split('/').Last();
|
||||
name = System.Net.WebUtility.UrlDecode(name);
|
||||
}
|
||||
|
||||
// 排除掉文件夹自身 (WebDAV 通常会把当前请求的文件夹作为第一个结果返回)
|
||||
// 通过判断 URL 结尾是否一致来简单过滤,或者判断 IsCollection 且 Uri 相同
|
||||
// 这里简单处理:只要名字不为空
|
||||
var name = System.Net.WebUtility.UrlDecode(res.Uri.Split('/').LastOrDefault());
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
|
||||
// 如果名字和请求的目录名一样,可能是它自己,跳过 (这需要根据具体服务器响应调整)
|
||||
// 更稳妥的是比较 Uri
|
||||
|
||||
list.Add(new UnifiedFileItem
|
||||
list.Add(new FileCacheEntity
|
||||
{
|
||||
Name = name,
|
||||
FullPath = res.Uri.ToString(), // WebDAV 需要完整 URI
|
||||
IsFolder = res.IsCollection,
|
||||
MediaFolderId = _config.Id,
|
||||
|
||||
// 记录父级 URI (保持传入时的形式,或者统一标准)
|
||||
// 注意:对于 WebDAV,ParentUri 最好不带末尾斜杠,除非是根
|
||||
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
|
||||
|
||||
// ★ 存储完整的 http://... 标准 URI
|
||||
Uri = itemUri.AbsoluteUri,
|
||||
|
||||
FileName = name,
|
||||
IsDirectory = res.IsCollection,
|
||||
|
||||
// WebDAV 通常能提供这些信息
|
||||
FileSize = res.ContentLength ?? 0,
|
||||
LastModified = res.LastModifiedDate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
|
||||
{
|
||||
// WebDAV 获取流
|
||||
var res = await _client.GetRawFile(fullPath);
|
||||
if (!res.IsSuccessful) throw new IOException($"WebDAV Error: {res.StatusCode}");
|
||||
if (entity == null) return null;
|
||||
|
||||
// WebDAV 获取流,直接使用完整 URI
|
||||
var res = await _client.GetRawFile(entity.Uri);
|
||||
|
||||
if (!res.IsSuccessful)
|
||||
throw new IOException($"WebDAV Error {res.StatusCode}: {res.Description}");
|
||||
|
||||
return res.Stream;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Events;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.LibWatcherService
|
||||
{
|
||||
public interface ILibWatcherService
|
||||
{
|
||||
event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Events;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.LibWatcherService
|
||||
{
|
||||
public class LibWatcherService : BaseViewModel, IDisposable, ILibWatcherService
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
|
||||
|
||||
public LibWatcherService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
|
||||
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
|
||||
UpdateWatchers();
|
||||
}
|
||||
|
||||
private void LocalMediaFolders_ItemPropertyChanged(object? sender, Collections.ItemPropertyChangedEventArgs e)
|
||||
{
|
||||
UpdateWatchers();
|
||||
}
|
||||
|
||||
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateWatchers();
|
||||
}
|
||||
|
||||
public event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var watcher in _watchers.Values)
|
||||
{
|
||||
watcher.Dispose();
|
||||
}
|
||||
_watchers.Clear();
|
||||
}
|
||||
|
||||
private void UpdateWatchers()
|
||||
{
|
||||
var folders = _settingsService.AppSettings.LocalMediaFolders;
|
||||
// 移除不再监听的
|
||||
foreach (var key in _watchers.Keys.ToList())
|
||||
{
|
||||
if (!folders.Any(x => x.Path == key && x.IsEnabled && x.IsRealTimeWatchEnabled))
|
||||
{
|
||||
_watchers[key].Dispose();
|
||||
_watchers.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新的监听
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
if (!_watchers.ContainsKey(folder.Path) && Directory.Exists(folder.Path) && folder.IsEnabled && folder.IsRealTimeWatchEnabled)
|
||||
{
|
||||
var watcher = new FileSystemWatcher(folder.Path)
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
EnableRaisingEvents = true,
|
||||
};
|
||||
watcher.Created += (s, e) => OnChanged(folder.Path, e);
|
||||
watcher.Changed += (s, e) => OnChanged(folder.Path, e);
|
||||
watcher.Deleted += (s, e) => OnChanged(folder.Path, e);
|
||||
watcher.Renamed += (s, e) => OnChanged(folder.Path, e);
|
||||
_watchers[folder.Path] = watcher;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChanged(string folder, FileSystemEventArgs e)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
MusicLibraryFilesChanged?.Invoke(
|
||||
this,
|
||||
new LibChangedEventArgs(folder, e.FullPath, e.ChangeType)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Providers;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using Lyricify.Lyrics.Helpers;
|
||||
using Lyricify.Lyrics.Searchers;
|
||||
@@ -28,11 +29,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
private readonly AppleMusic _appleMusic;
|
||||
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LyricsSearchService(ISettingsService settingsService, ILogger<LyricsSearchService> logger)
|
||||
public LyricsSearchService(ISettingsService settingsService, IFileSystemService fileSystemService, ILogger<LyricsSearchService> logger)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_logger = logger;
|
||||
|
||||
_lrcLibHttpClient = new();
|
||||
@@ -276,92 +279,49 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
{
|
||||
int maxScore = 0;
|
||||
|
||||
MediaFolder? bestFolder = null;
|
||||
string? bestFilePath = null;
|
||||
FileCacheEntity? bestFileEntity = null;
|
||||
MediaFolder? bestFolderConfig = null;
|
||||
|
||||
var lyricsSearchResult = new LyricsSearchResult();
|
||||
|
||||
if (format.ToLyricsSearchProvider() is LyricsSearchProvider lyricsSearchProvider)
|
||||
{
|
||||
lyricsSearchResult.Provider = lyricsSearchProvider;
|
||||
}
|
||||
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (!folder.IsEnabled) continue;
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null) continue;
|
||||
if (!await fs.ConnectAsync()) continue;
|
||||
|
||||
// 递归扫描
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue(""); // 从根目录开始
|
||||
|
||||
string targetExt = format.ToFileExtension();
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
var items = await fs.GetFilesAsync(currentPath);
|
||||
var enabledFolders = _settingsService.AppSettings.LocalMediaFolders
|
||||
.Where(f => f.IsEnabled)
|
||||
.ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
|
||||
continue;
|
||||
}
|
||||
var enabledIds = enabledFolders.Select(f => f.Id).ToList();
|
||||
|
||||
if (item.Name.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
|
||||
if (enabledIds.Count == 0) return lyricsSearchResult;
|
||||
|
||||
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
|
||||
|
||||
foreach (var item in allFiles)
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FullPath });
|
||||
if (item.FileName.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FileName });
|
||||
|
||||
if (score > maxScore)
|
||||
{
|
||||
maxScore = score;
|
||||
bestFilePath = item.FullPath;
|
||||
bestFolder = folder;
|
||||
bestFileEntity = item;
|
||||
|
||||
bestFolderConfig = enabledFolders.FirstOrDefault(f => f.Id == item.MediaFolderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 日志记录...
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 如果找到了最佳匹配,读取内容
|
||||
if (bestFolder != null && bestFilePath != null)
|
||||
if (bestFileEntity != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 重新连接以读取文件 (因为之前的 fs 已经在 using 结束时释放)
|
||||
using var fs = bestFolder.CreateFileSystem();
|
||||
if (fs != null && await fs.ConnectAsync())
|
||||
{
|
||||
using var stream = await fs.OpenReadAsync(bestFilePath);
|
||||
lyricsSearchResult.Raw = bestFileEntity.EmbeddedLyrics;
|
||||
|
||||
// 使用 StreamReader 读取文本
|
||||
// 注意:这里简单使用 Default 编码,如果需要探测编码(FileHelper.GetEncoding),
|
||||
// 可能需要先读一部分字节来判断,或者使用带编码探测的库。
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string raw = await reader.ReadToEndAsync();
|
||||
|
||||
lyricsSearchResult.Reference = bestFilePath;
|
||||
lyricsSearchResult.Reference = bestFileEntity.Uri;
|
||||
lyricsSearchResult.MatchPercentage = maxScore;
|
||||
lyricsSearchResult.Raw = raw;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 读取失败处理
|
||||
}
|
||||
}
|
||||
|
||||
return lyricsSearchResult;
|
||||
@@ -369,107 +329,52 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
|
||||
private async Task<LyricsSearchResult> SearchEmbedded(SongInfo songInfo)
|
||||
{
|
||||
int bestScore = 0;
|
||||
string? bestFilePath = null;
|
||||
string? bestRaw = null;
|
||||
|
||||
// 用于最后回填 Metadata
|
||||
string? bestTitle = null;
|
||||
string[]? bestArtists = null;
|
||||
string? bestAlbum = null;
|
||||
double bestDuration = 0;
|
||||
|
||||
var lyricsSearchResult = new LyricsSearchResult
|
||||
{
|
||||
Provider = LyricsSearchProvider.LocalMusicFile,
|
||||
};
|
||||
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (!folder.IsEnabled) continue;
|
||||
var enabledIds = _settingsService.AppSettings.LocalMediaFolders
|
||||
.Where(f => f.IsEnabled)
|
||||
.Select(f => f.Id)
|
||||
.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null) continue;
|
||||
if (!await fs.ConnectAsync()) continue;
|
||||
if (enabledIds.Count == 0) return lyricsSearchResult;
|
||||
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue("");
|
||||
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
var items = await fs.GetFilesAsync(currentPath);
|
||||
FileCacheEntity? bestFile = null;
|
||||
int maxScore = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
foreach (var item in allFiles)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrEmpty(item.EmbeddedLyrics)) continue;
|
||||
|
||||
var ext = Path.GetExtension(item.Name).ToLower();
|
||||
if (FileHelper.MusicExtensions.Contains(ext))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = await fs.OpenReadAsync(item.FullPath);
|
||||
|
||||
var track = new ExtendedTrack(item.FullPath, stream);
|
||||
var raw = track.RawLyrics;
|
||||
|
||||
if (!string.IsNullOrEmpty(raw))
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
|
||||
{
|
||||
Title = track.Title,
|
||||
Artists = track.Artist?.Split(ATL.Settings.DisplayValueSeparator),
|
||||
Album = track.Album,
|
||||
Duration = track.Duration,
|
||||
Reference = item.FullPath,
|
||||
Title = item.Title,
|
||||
Artists = item.Artists?.Split(ATL.Settings.DisplayValueSeparator),
|
||||
Album = item.Album,
|
||||
Duration = item.Duration
|
||||
});
|
||||
|
||||
if (score > bestScore)
|
||||
if (score > maxScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestFilePath = item.FullPath;
|
||||
bestRaw = raw;
|
||||
|
||||
// 缓存当前最佳的元数据,避免最后还需要重新打开文件读一次
|
||||
bestTitle = track.Title;
|
||||
bestArtists = track.Artist?.Split(ATL.Settings.DisplayValueSeparator);
|
||||
bestAlbum = track.Album;
|
||||
bestDuration = track.Duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 单个文件解析失败忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 文件夹扫描失败忽略
|
||||
maxScore = score;
|
||||
bestFile = item;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestFilePath != null)
|
||||
if (bestFile != null && maxScore > 0)
|
||||
{
|
||||
// 直接使用缓存的数据,不需要 new Track(bestFile) 了
|
||||
lyricsSearchResult.Title = bestTitle;
|
||||
lyricsSearchResult.Artists = bestArtists;
|
||||
lyricsSearchResult.Album = bestAlbum;
|
||||
lyricsSearchResult.Duration = bestDuration;
|
||||
lyricsSearchResult.Title = bestFile.Title;
|
||||
lyricsSearchResult.Artists = bestFile.Artists?.Split(ATL.Settings.DisplayValueSeparator);
|
||||
lyricsSearchResult.Album = bestFile.Album;
|
||||
lyricsSearchResult.Duration = bestFile.Duration;
|
||||
|
||||
lyricsSearchResult.Raw = bestRaw;
|
||||
lyricsSearchResult.Reference = bestFilePath;
|
||||
lyricsSearchResult.MatchPercentage = bestScore;
|
||||
lyricsSearchResult.Raw = bestFile.EmbeddedLyrics;
|
||||
lyricsSearchResult.Reference = bestFile.Uri;
|
||||
lyricsSearchResult.MatchPercentage = maxScore;
|
||||
}
|
||||
|
||||
return lyricsSearchResult;
|
||||
|
||||
@@ -9,6 +9,7 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -11,7 +11,6 @@ using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
|
||||
using BetterLyrics.WinUI3.Services.DiscordService;
|
||||
using BetterLyrics.WinUI3.Services.LibWatcherService;
|
||||
using BetterLyrics.WinUI3.Services.LyricsSearchService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Services.TranslationService;
|
||||
@@ -27,6 +26,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Text.Json;
|
||||
@@ -41,7 +41,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
public partial class MediaSessionsService : BaseViewModel, IMediaSessionsService,
|
||||
IRecipient<PropertyChangedMessage<bool>>,
|
||||
IRecipient<PropertyChangedMessage<string>>,
|
||||
IRecipient<PropertyChangedMessage<ChineseRomanization>>
|
||||
IRecipient<PropertyChangedMessage<ChineseRomanization>>,
|
||||
IRecipient<PropertyChangedMessage<DateTime?>>
|
||||
{
|
||||
private EventSourceReader? _sse = null;
|
||||
private readonly MediaManager _mediaManager = new();
|
||||
@@ -52,7 +53,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
private readonly ITranslationService _translationService;
|
||||
private readonly ITransliterationService _transliterationService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ILibWatcherService _libWatcherService;
|
||||
private readonly IDiscordService _discordService;
|
||||
private readonly ILogger<MediaSessionsService> _logger;
|
||||
|
||||
@@ -71,7 +71,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
ISettingsService settingsService,
|
||||
IAlbumArtSearchService albumArtSearchService,
|
||||
ILyricsSearchService lyricsSearchService,
|
||||
ILibWatcherService libWatcherService,
|
||||
IDiscordService discordService,
|
||||
ITranslationService libreTranslateService,
|
||||
ITransliterationService transliterationService,
|
||||
@@ -80,7 +79,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
_settingsService = settingsService;
|
||||
_albumArtSearchService = albumArtSearchService;
|
||||
_lyrcsSearchService = lyricsSearchService;
|
||||
_libWatcherService = libWatcherService;
|
||||
_translationService = libreTranslateService;
|
||||
_transliterationService = transliterationService;
|
||||
_discordService = discordService;
|
||||
@@ -91,13 +89,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
_settingsService.AppSettings.MediaSourceProvidersInfo.ItemPropertyChanged += MediaSourceProvidersInfo_ItemPropertyChanged;
|
||||
|
||||
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
|
||||
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
|
||||
|
||||
_settingsService.AppSettings.MappedSongSearchQueries.CollectionChanged += MappedSongSearchQueries_CollectionChanged;
|
||||
_settingsService.AppSettings.MappedSongSearchQueries.ItemPropertyChanged += MappedSongSearchQueries_ItemPropertyChanged;
|
||||
|
||||
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
|
||||
|
||||
InitMediaManager();
|
||||
}
|
||||
|
||||
@@ -111,12 +106,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
UpdateLyrics();
|
||||
}
|
||||
|
||||
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
|
||||
{
|
||||
UpdateAlbumArt();
|
||||
UpdateLyrics();
|
||||
}
|
||||
|
||||
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateAlbumArt();
|
||||
@@ -144,12 +133,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
}
|
||||
}
|
||||
|
||||
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
|
||||
{
|
||||
UpdateAlbumArt();
|
||||
UpdateLyrics();
|
||||
}
|
||||
|
||||
private MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
|
||||
{
|
||||
var desiredSession = GetCurrentSession();
|
||||
@@ -693,6 +676,14 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
MediaManager_OnFocusedSessionChanged(null);
|
||||
}
|
||||
}
|
||||
else if (message.Sender is MediaFolder)
|
||||
{
|
||||
if (message.PropertyName == nameof(MediaFolder.IsEnabled))
|
||||
{
|
||||
UpdateAlbumArt();
|
||||
UpdateLyrics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<string> message)
|
||||
@@ -726,5 +717,16 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<DateTime?> message)
|
||||
{
|
||||
if (message.Sender is MediaFolder)
|
||||
{
|
||||
if (message.PropertyName == nameof(MediaFolder.LastSyncTime))
|
||||
{
|
||||
UpdateAlbumArt();
|
||||
UpdateLyrics();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>تعذر الاتصال بخادم موسيقى LX، يرجى الانتقال إلى الإعدادات - مصدر التشغيل - LX Music - خادم موسيقى LX للتحقق مما إذا تم إدخال الرابط بشكل صحيح</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>وضع ملء الشاشة</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>عرض مقسم</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>إضافة إلى قائمة التشغيل</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>مكتبة الموسيقى - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>وضع الشاشة الضيقة</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>مكتبة الوسائط المحلية</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>مراقبة تغييرات الملفات في الوقت الحقيقي</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>وضع الشاشة الضيقة</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Verbindung zum LX Music Server fehlgeschlagen. Bitte prüfen Sie unter Einstellungen - Wiedergabequelle - LX Music - LX Music Server, ob der Link korrekt ist</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Vollbildmodus</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Geteilte Ansicht</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Zur Wiedergabeliste hinzufügen</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Musikgalerie - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Schmaler Modus</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Lokale Medienbibliothek</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Echtzeit-Dateiüberwachung</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Schmaler Modus</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Unable to connect to LX Music Server. Please go to Settings - Playback Source - LX Music - LX Music Server to check if the link is entered correctly</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value>Cleaning cache...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value>Connection failed</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value>Connecting...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value>Fetching file list...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value>Parsing...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value>Waiting for scan...</value>
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Fullscreen Mode</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Split View</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value>Last Sync Time</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Add to playlist</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Music Gallery - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value>Auto-sync Frequency</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value>Never</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value>Every Day</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value>Every 15 Minutes</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value>Every Hour</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value>Every 6 Hours</value>
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Narrow Mode</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Local Media Library</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Real-time file monitoring</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Narrow Mode</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>No se puede conectar al servidor LX Music. Vaya a Configuración - Fuente de reproducción - LX Music - Servidor LX Music para verificar si el enlace es correcto</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Modo Pantalla Completa</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Vista dividida</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Añadir a lista de reproducción</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Galería de Música - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Modo Estrecho</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Biblioteca multimedia local</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Monitoreo de archivos en tiempo real</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Modo Estrecho</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Impossible de se connecter au serveur LX Music. Veuillez aller dans Paramètres - Source de lecture - LX Music - Serveur LX Music pour vérifier si le lien est correct</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Mode Plein écran</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Vue scindée</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Ajouter à la liste de lecture</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Galerie de musique - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Mode Étroit</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Bibliothèque multimédia locale</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Surveillance des fichiers en temps réel</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Mode Étroit</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>LX Music सर्वर से कनेक्ट नहीं हो सकता, कृपया सेटिंग्स - प्लेबैक स्रोत - LX Music - LX Music सर्वर पर जाएं और जांचें कि लिंक सही तरीके से दर्ज किया गया है या नहीं</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>फुलस्क्रीन मोड</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>विभाजित दृश्य</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>प्लेलिस्ट में जोड़ें</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>संगीत लाइब्रेरी - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>संकीर्ण मोड</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>स्थानीय मीडिया लाइब्रेरी</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>फ़ाइल परिवर्तनों की वास्तविक समय निगरानी</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>संकीर्ण मोड</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Tidak dapat terhubung ke server LX Music, silakan buka Pengaturan - Sumber Pemutaran - LX Music - Server LX Music untuk memeriksa apakah tautan telah dimasukkan dengan benar</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Mode Layar Penuh</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Tampilan Terpisah</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Tambahkan ke daftar putar</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Galeri Musik - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Mode Sempit</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Pustaka Media Lokal</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Pemantauan Perubahan File Secara Real-time</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Mode Sempit</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>LX Music サーバーに接続できません。「設定」-「再生ソース」-「LX Music」-「LX Music サーバー」に移動し、リンクが正しく入力されているか確認してください</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>全画面モード</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>分割表示</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>プレイリストに追加</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>ミュージックギャラリー - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>狭い表示モード</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>ローカルメディアライブラリ</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>リアルタイムのファイル監視</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>狭い表示モード</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>LX Music 서버에 연결할 수 없습니다. 설정 - 재생 소스 - LX Music - LX Music 서버에서 링크가 올바르게 입력되었는지 확인하세요</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>전체 화면 모드</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>분할 보기</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>재생 목록에 추가</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>음악 갤러리 - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>좁은 화면 모드</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>로컬 미디어 라이브러리</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>실시간 파일 감시</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>좁은 화면 모드</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Tidak dapat menyambung ke pelayan LX Music, sila pergi ke Tetapan - Sumber Main Balik - LX Music - Pelayan LX Music untuk menyemak sama ada pautan dimasukkan dengan betul</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Mod Skrin Penuh</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Paparan Pisah</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Tambah ke senarai main</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Galeri Muzik - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Mod Sempit</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Pustaka Media Tempatan</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Pemantauan Perubahan Fail Masa Nyata</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Mod Sempit</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Não foi possível ligar ao servidor LX Music. Aceda a Definições - Fonte de Reprodução - LX Music - Servidor LX Music para verificar se a ligação foi introduzida corretamente</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Modo de Ecrã Inteiro</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Vista Dividida</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Adicionar à lista de reprodução</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Galeria de Música - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Modo Estreito</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Biblioteca Multimédia Local</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Monitorização de Alterações de Ficheiros em Tempo Real</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Modo Estreito</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Не удалось подключиться к серверу LX Music. Перейдите в Настройки - Источник воспроизведения - LX Music - Сервер LX Music, чтобы проверить правильность ссылки</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Полноэкранный режим</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Разделенный вид</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Добавить в плейлист</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Музыкальная галерея - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Узкий режим</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Локальная медиатека</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Следить за изменениями файлов в реальном времени</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Узкий режим</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ LX Music โปรดไปที่ การตั้งค่า - แหล่งเล่นเพลง - LX Music - เซิร์ฟเวอร์ LX Music เพื่อตรวจสอบว่าป้อนลิงก์ถูกต้องหรือไม่</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>โหมดเต็มหน้าจอ</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>มุมมองแยก</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>เพิ่มลงในเพลย์ลิสต์</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>คลังเพลง - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>โหมดหน้าแคบ</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>ไลบรารีสื่อในเครื่อง</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>เฝ้าดูการเปลี่ยนแปลงไฟล์แบบเรียลไทม์</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>โหมดหน้าแคบ</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>Không thể kết nối với máy chủ LX Music, vui lòng vào Cài đặt - Nguồn phát - LX Music - Máy chủ LX Music để kiểm tra xem liên kết đã được nhập chính xác chưa</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>Chế độ toàn màn hình</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>Chế độ xem chia tách</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>Thêm vào danh sách phát</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>Thư viện nhạc - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>Chế độ hẹp</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Thư viện phương tiện cục bộ</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>Lắng nghe thay đổi tệp thời gian thực</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>Chế độ hẹp</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>无法连接到 LX 音乐服务器,请转到设置 - 播放源 - LX Music - LX 音乐服务器以检查是否正确输入链接</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value>清理缓存中...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value>连接失败</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value>连接中...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value>获取文件列表中...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value>解析中...</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value>等待扫描中...</value>
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>全屏模式</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>拆分视图</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value>上次同步时间</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value>立即同步</value>
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>添加到播放列表</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>音乐库 - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value>自动同步频率</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value>从不</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value>每天</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value>每 15 分钟</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value>每小时</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value>每 6 小时</value>
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>窄屏模式</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>本地媒体库</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>实时监听文件变化</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>窄屏模式</value>
|
||||
</data>
|
||||
|
||||
@@ -165,6 +165,24 @@
|
||||
<data name="FailToStartLXMusicServer" xml:space="preserve">
|
||||
<value>無法連線到 LX 音樂伺服器,請轉到設定 - 播放來源 - LX Music - LX 音樂伺服器以檢查是否正確輸入連結</value>
|
||||
</data>
|
||||
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceConnecting" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceParsing" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="FullscreenMode" xml:space="preserve">
|
||||
<value>全螢幕模式</value>
|
||||
</data>
|
||||
@@ -357,6 +375,12 @@
|
||||
<data name="MainPageSplitView.Content" xml:space="preserve">
|
||||
<value>分割檢視</value>
|
||||
</data>
|
||||
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
|
||||
<value>加入播放清單</value>
|
||||
</data>
|
||||
@@ -471,6 +495,24 @@
|
||||
<data name="MusicGalleryPageTitle" xml:space="preserve">
|
||||
<value>音樂庫 - BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="NarrowMode" xml:space="preserve">
|
||||
<value>窄屏模式</value>
|
||||
</data>
|
||||
@@ -1089,9 +1131,6 @@
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>本機媒體櫃</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
|
||||
<value>即時監聽檔案變化</value>
|
||||
</data>
|
||||
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
|
||||
<value>窄屏模式</value>
|
||||
</data>
|
||||
|
||||
@@ -3,6 +3,7 @@ using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
@@ -13,7 +14,9 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static Vanara.PInvoke.Shell32;
|
||||
|
||||
namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
@@ -21,14 +24,16 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AppSettings AppSettings { get; set; }
|
||||
[ObservableProperty] public partial AppSettings AppSettings { get; set; }
|
||||
|
||||
public MediaSettingsControlViewModel(ISettingsService settingsService, ILocalizationService localizationService)
|
||||
public MediaSettingsControlViewModel(ISettingsService settingsService, ILocalizationService localizationService, IFileSystemService fileSystemService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
_settingsService = settingsService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
AppSettings = _settingsService.AppSettings;
|
||||
}
|
||||
|
||||
@@ -36,16 +41,16 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
|
||||
if (AppSettings.LocalMediaFolders.Any(x => Path.GetFullPath(x.Path).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
|
||||
if (AppSettings.LocalMediaFolders.Any(x => Path.GetFullPath(x.UriPath).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
ToastHelper.ShowToast("SettingsPagePathExistedInfo", null, InfoBarSeverity.Warning);
|
||||
}
|
||||
else if (AppSettings.LocalMediaFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
|
||||
else if (AppSettings.LocalMediaFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.UriPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// 添加的文件夹是现有文件夹的子文件夹
|
||||
ToastHelper.ShowToast("SettingsPagePathBeIncludedInfo", null, InfoBarSeverity.Warning);
|
||||
}
|
||||
else if (AppSettings.LocalMediaFolders.Any(item => Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
|
||||
else if (AppSettings.LocalMediaFolders.Any(item => Path.GetFullPath(item.UriPath).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
|
||||
)
|
||||
{
|
||||
// 添加的文件夹是现有文件夹的父文件夹
|
||||
@@ -53,13 +58,29 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
}
|
||||
else
|
||||
{
|
||||
AppSettings.LocalMediaFolders.Add(new MediaFolder(path));
|
||||
var tempFolder = new MediaFolder(path);
|
||||
AppSettings.LocalMediaFolders.Add(tempFolder);
|
||||
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(tempFolder));
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveFolderAsync(MediaFolder folder)
|
||||
public void RemoveFolder(MediaFolder folder)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await _fileSystemService.DeleteCacheForMediaFolderAsync(folder);
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
AppSettings.LocalMediaFolders.Remove(folder);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void SyncFolder(MediaFolder folder)
|
||||
{
|
||||
if (folder.IsIndexing) return;
|
||||
|
||||
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(folder, CancellationToken.None));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -91,8 +112,8 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
var configControl = (RemoteServerConfigControl)dialog.Content;
|
||||
|
||||
try
|
||||
{
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
e.Cancel = true;
|
||||
|
||||
dialog.IsPrimaryButtonEnabled = false;
|
||||
@@ -101,16 +122,28 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
|
||||
var tempFolder = configControl.GetConfig();
|
||||
|
||||
var provider = tempFolder.CreateFileSystem();
|
||||
bool isConnected = await Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var provider = tempFolder.CreateFileSystem();
|
||||
if (provider == null) return false;
|
||||
|
||||
bool isConnected = provider != null && await provider.ConnectAsync();
|
||||
return await provider.ConnectAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowErrorTip(configControl, ex.Message);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isConnected)
|
||||
{
|
||||
await provider!.DisconnectAsync();
|
||||
|
||||
PasswordVaultHelper.Save(Constants.App.AppName, tempFolder.VaultKey, tempFolder.Password);
|
||||
AppSettings.LocalMediaFolders.Add(tempFolder);
|
||||
PasswordVaultHelper.Save(Constants.App.AppName, tempFolder.VaultKey, tempFolder.Password);
|
||||
|
||||
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(tempFolder));
|
||||
|
||||
e.Cancel = false;
|
||||
}
|
||||
@@ -118,17 +151,12 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
ShowErrorTip(configControl, _localizationService.GetLocalizedString("SettingsPageServerTestFailedInfo"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowErrorTip(configControl, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
dialog.IsPrimaryButtonEnabled = true;
|
||||
configControl.IsEnabled = true;
|
||||
configControl.SetProgressBarVisibility(Visibility.Collapsed);
|
||||
}
|
||||
|
||||
deferral.Complete();
|
||||
};
|
||||
|
||||
await dialog.ShowAsync();
|
||||
@@ -136,8 +164,6 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
|
||||
private void ShowErrorTip(RemoteServerConfigControl control, string message)
|
||||
{
|
||||
// 你可以在 RemoteServerConfigControl 里加一个 InfoBar 用来显示错误
|
||||
// 假设你在 UserControl 里公开了一个 ShowError 方法
|
||||
control.ShowError(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,20 @@ using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.LibWatcherService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -26,14 +28,17 @@ using Windows.Media;
|
||||
using Windows.Media.Core;
|
||||
using Windows.Media.Playback;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
public partial class MusicGalleryPageViewModel : BaseViewModel
|
||||
public partial class MusicGalleryPageViewModel : BaseViewModel,
|
||||
IRecipient<PropertyChangedMessage<DateTime?>>,
|
||||
IRecipient<PropertyChangedMessage<bool>>
|
||||
{
|
||||
private readonly ILibWatcherService _libWatcherService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
private readonly MediaPlayer _mediaPlayer = new();
|
||||
private readonly MediaTimelineController _timelineController = new();
|
||||
@@ -41,6 +46,10 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
|
||||
private readonly DispatcherQueueTimer _refreshSongsTimer;
|
||||
|
||||
private IRandomAccessStream? _currentStream;
|
||||
private Stream? _currentNetStream;
|
||||
private IUnifiedFileSystem? _currentProvider;
|
||||
|
||||
// All songs
|
||||
private List<ExtendedTrack> _tracks = [];
|
||||
// Songs in current playlist
|
||||
@@ -85,18 +94,22 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
|
||||
public SongsTabInfo? SelectedSongsTabInfo => SongsTabInfoList.ElementAtOrDefault(SelectedSongsTabInfoIndex);
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsDataLoading { get; set; } = false;
|
||||
[ObservableProperty] public partial bool IsDataLoading { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ExtendedTrack TrackRightTapped { get; set; } = new();
|
||||
[ObservableProperty] public partial ExtendedTrack TrackRightTapped { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SongSearchQuery { get; set; } = string.Empty;
|
||||
|
||||
public MusicGalleryPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService, ILocalizationService localizationService)
|
||||
public MusicGalleryPageViewModel(
|
||||
ISettingsService settingsService,
|
||||
ILocalizationService localizationService,
|
||||
IFileSystemService fileSystemService
|
||||
)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
_refreshSongsTimer = _dispatcherQueue.CreateTimer();
|
||||
|
||||
_settingsService = settingsService;
|
||||
@@ -110,7 +123,6 @@ 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;
|
||||
@@ -126,19 +138,11 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
_smtc.IsPreviousEnabled = true;
|
||||
_smtc.ButtonPressed += Smtc_ButtonPressed;
|
||||
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
|
||||
|
||||
_libWatcherService = libWatcherService;
|
||||
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
|
||||
}
|
||||
|
||||
private void TrackPlayingQueue_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.Path)];
|
||||
}
|
||||
|
||||
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
|
||||
{
|
||||
RefreshSongs();
|
||||
AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.UriPath)];
|
||||
}
|
||||
|
||||
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
@@ -264,11 +268,6 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, Events.LibChangedEventArgs e)
|
||||
{
|
||||
RefreshSongs();
|
||||
}
|
||||
|
||||
public void CancelRefreshSongs()
|
||||
{
|
||||
}
|
||||
@@ -278,102 +277,36 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
_refreshSongsTimer.Debounce(() =>
|
||||
{
|
||||
IsDataLoading = true;
|
||||
_tracks.Clear();
|
||||
|
||||
Task.Run(async () =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (!folder.IsEnabled) continue;
|
||||
var enabledFolderIds = _settingsService.AppSettings.LocalMediaFolders
|
||||
.Where(f => f.IsEnabled)
|
||||
.Select(f => f.Id)
|
||||
.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 创建底层的驱动 (FTP/SMB/Local)
|
||||
// 注意:这里我们使用 using 确保用完销毁连接
|
||||
using var rawFs = folder.CreateFileSystem();
|
||||
if (rawFs == null) continue;
|
||||
var cachedFiles = await _fileSystemService.GetParsedFilesAsync(enabledFolderIds);
|
||||
|
||||
// 2. 【关键】将驱动包装进 Service
|
||||
// 这样你就拥有了:缓存能力 + 后台静默更新能力
|
||||
// 建议:如果 DatabaseManager 是单例,最好将其注入进去,这里直接 new 为了演示方便
|
||||
var fileService = new FileSystemService(rawFs);
|
||||
var newTrackList = cachedFiles
|
||||
.Select(x => new ExtendedTrack(x))
|
||||
.ToList();
|
||||
|
||||
// 初始化数据库连接 (如果没有在构造函数里做)
|
||||
await fileService.InitializeAsync();
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_tracks = newTrackList;
|
||||
|
||||
// 递归扫描队列
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue(""); // 从根目录开始
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
|
||||
// 3. 【提速】这里改用 Service 获取文件列表
|
||||
// 第一次运行会走网络,第二次运行直接读本地 SQLite,毫秒级响应
|
||||
var items = await fileService.GetFilesAsync(currentPath);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
// 文件夹:加入队列继续递归
|
||||
foldersToScan.Enqueue(item.FullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(item.Name).ToLower();
|
||||
if (FileHelper.MusicExtensions.Contains(ext))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 4. 读取文件流 (解析 ID3 信息)
|
||||
// 注意:这里目前还是瓶颈,因为每次都要读文件头
|
||||
// 优化方向:将 Title/Artist 也存入 SQLite,跳过这一步
|
||||
using (var stream = await fileService.OpenFileAsync(item))
|
||||
{
|
||||
// 这里的 item 是 UnifiedFileItem (Model),正是 Service 返回的类型
|
||||
ExtendedTrack track = new ExtendedTrack(item.FullPath, stream);
|
||||
|
||||
if (track.Duration > 0)
|
||||
{
|
||||
// 读取专辑图到内存 (因为流马上要关闭)
|
||||
_ = track.EmbeddedPictures;
|
||||
_tracks.Add(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Error loading track {item.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Folder scan error ({folder.Name}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Global scan error: {ex.Message}");
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
// 应用过滤器
|
||||
ApplyPlaylist();
|
||||
ApplySongSearchQuery();
|
||||
|
||||
IsLocalMediaNotFound = !_filteredTracks.Any();
|
||||
|
||||
ApplySongOrderType();
|
||||
|
||||
IsDataLoading = false;
|
||||
});
|
||||
});
|
||||
}, Constants.Time.DebounceTimeout);
|
||||
}, Time.DebounceTimeout);
|
||||
}
|
||||
|
||||
public void ApplyPlaylist()
|
||||
@@ -404,7 +337,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var m3uFileContent = File.ReadAllText(path);
|
||||
_playlistTracks = _tracks.Where(t => m3uFileContent.Contains(t.Path)).ToList();
|
||||
_playlistTracks = _tracks.Where(t => m3uFileContent.Contains(t.UriPath)).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -494,28 +427,109 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
_timelineController.Pause();
|
||||
_mediaPlayer.Source = null;
|
||||
|
||||
_currentStream?.Dispose();
|
||||
_currentNetStream?.Dispose();
|
||||
_currentStream = null;
|
||||
_currentNetStream = null;
|
||||
|
||||
if (playQueueItem == null)
|
||||
{
|
||||
_smtc.IsEnabled = false;
|
||||
_smtc.DisplayUpdater.ClearAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayingTrack = playQueueItem.Track;
|
||||
|
||||
var updater = _smtc.DisplayUpdater;
|
||||
updater.ClearAll();
|
||||
|
||||
_smtc.IsEnabled = true;
|
||||
_mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(PlayingTrack.Path));
|
||||
|
||||
var storageFile = await StorageFile.GetFileFromPathAsync(PlayingTrack.Path);
|
||||
try
|
||||
{
|
||||
// ★ 1. 查找对应的 MediaFolder 配置
|
||||
// 现在的 PlayingTrack.Uri 是标准的完整 URI (例如 smb://host/share/file.mp3)
|
||||
// 我们通过对比前缀来找到它属于哪个 MediaFolder
|
||||
var targetFolder = _settingsService.AppSettings.LocalMediaFolders.FirstOrDefault(f =>
|
||||
PlayingTrack.Uri.StartsWith(f.GetStandardUri().AbsoluteUri, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (targetFolder == null)
|
||||
{
|
||||
throw new Exception($"找不到文件 {PlayingTrack.FileName} 对应的存储配置。请检查服务器设置是否已启用。");
|
||||
}
|
||||
|
||||
// ★ 2. 创建 Provider 并连接
|
||||
_currentProvider = targetFolder.CreateFileSystem();
|
||||
if (_currentProvider == null) return;
|
||||
|
||||
await _currentProvider.ConnectAsync();
|
||||
|
||||
// ★ 3. 构造实体对象进行读取
|
||||
// FileSystemService.OpenFileAsync 现在只需要 entity.Uri 就能工作
|
||||
var fileCacheStub = new FileCacheEntity
|
||||
{
|
||||
Uri = PlayingTrack.Uri
|
||||
};
|
||||
|
||||
_currentNetStream = await _fileSystemService.OpenFileAsync(_currentProvider, fileCacheStub);
|
||||
|
||||
_currentStream = _currentNetStream.AsRandomAccessStream();
|
||||
|
||||
// 获取 MIME 类型 (使用 FileName 或 Uri 都可以)
|
||||
string contentType = GetMimeType(PlayingTrack.FileName);
|
||||
var mediaSource = MediaSource.CreateFromStream(_currentStream, contentType);
|
||||
|
||||
_mediaPlayer.Source = mediaSource;
|
||||
|
||||
// --- SMTC 更新逻辑 (基本保持不变) ---
|
||||
var updater = _smtc.DisplayUpdater;
|
||||
updater.Type = MediaPlaybackType.Music;
|
||||
|
||||
updater.MusicProperties.Title = PlayingTrack.Title ?? PlayingTrack.FileName;
|
||||
updater.MusicProperties.Artist = PlayingTrack.Artist ?? "Unknown Artist";
|
||||
updater.MusicProperties.AlbumTitle = PlayingTrack.Album ?? "";
|
||||
|
||||
updater.MusicProperties.Genres.Clear();
|
||||
// 注意:这里改用 FileName 获取文件名,因为 UriPath 已被移除
|
||||
updater.MusicProperties.Genres.Add($"{ExtendedGenreFiled.FileName}{Path.GetFileNameWithoutExtension(PlayingTrack.FileName)}");
|
||||
|
||||
await updater.CopyFromFileAsync(MediaPlaybackType.Music, storageFile);
|
||||
updater.AppMediaId = Package.Current.Id.FullName;
|
||||
updater.MusicProperties.AlbumTitle = PlayingTrack.Album;
|
||||
updater.MusicProperties.Genres.Add($"{ExtendedGenreFiled.FileName}{Path.GetFileNameWithoutExtension(PlayingTrack.Path)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(PlayingTrack.LocalAlbumArtPath) && File.Exists(PlayingTrack.LocalAlbumArtPath))
|
||||
{
|
||||
var storageFile = await StorageFile.GetFileFromPathAsync(PlayingTrack.LocalAlbumArtPath);
|
||||
updater.Thumbnail = RandomAccessStreamReference.CreateFromFile(storageFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
updater.Thumbnail = null;
|
||||
}
|
||||
|
||||
updater.Update();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 建议:播放失败时弹个 Toast 或者在 UI 上显示错误
|
||||
System.Diagnostics.Debug.WriteLine($"PlayTrackAsync Error: {ex.Message}");
|
||||
|
||||
// 自动跳过或停止
|
||||
_timelineController.Pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetMimeType(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).ToLower();
|
||||
return ext switch
|
||||
{
|
||||
".mp3" => "audio/mpeg",
|
||||
".flac" => "audio/flac",
|
||||
".wav" => "audio/wav",
|
||||
".m4a" => "audio/mp4",
|
||||
".aac" => "audio/aac",
|
||||
".ogg" => "audio/ogg",
|
||||
".wma" => "audio/x-ms-wma",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
partial void OnSongOrderTypeChanged(CommonSongProperty value)
|
||||
@@ -582,5 +596,28 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
await PlayTrackAtAsync(-1);
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<DateTime?> message)
|
||||
{
|
||||
if (message.Sender is MediaFolder)
|
||||
{
|
||||
if (message.PropertyName == nameof(MediaFolder.LastSyncTime))
|
||||
{
|
||||
RefreshSongs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<bool> message)
|
||||
{
|
||||
if (message.Sender is MediaFolder)
|
||||
{
|
||||
if (message.PropertyName == nameof(MediaFolder.IsEnabled))
|
||||
{
|
||||
RefreshSongs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
</controls:OpacityMaskView.OpacityMask>
|
||||
<Image Source="{x:Bind ViewModel.TrackRightTapped.EmbeddedPictures, Mode=OneWay, Converter={StaticResource PictureInfosToImageSourceConverter}}" Stretch="Uniform" />
|
||||
<Image Source="{x:Bind ViewModel.TrackRightTapped.LocalAlbumArtPath, Mode=OneWay, Converter={StaticResource PathToImageConverter}}" Stretch="Uniform" />
|
||||
</controls:OpacityMaskView>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
@@ -76,12 +76,12 @@
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoBitrate" Value="{x:Bind ViewModel.TrackRightTapped.Bitrate, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoSampleRate" Value="{x:Bind ViewModel.TrackRightTapped.SampleRate, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoBitDepth" Value="{x:Bind ViewModel.TrackRightTapped.BitDepth, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoFormat" Value="{x:Bind ViewModel.TrackRightTapped.AudioFormat.Name, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoFormat" Value="{x:Bind ViewModel.TrackRightTapped.AudioFormatName, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoEncoder" Value="{x:Bind ViewModel.TrackRightTapped.Encoder, Mode=OneWay}" />
|
||||
<uc:PropertyRow
|
||||
x:Uid="MusicGalleryPageFileInfoPath"
|
||||
Link="{x:Bind ViewModel.TrackRightTapped.Path, Mode=OneWay}"
|
||||
Value="{x:Bind ViewModel.TrackRightTapped.Path, Mode=OneWay}" />
|
||||
Link="{x:Bind ViewModel.TrackRightTapped.Uri, Mode=OneWay}"
|
||||
Value="{x:Bind ViewModel.TrackRightTapped.Uri, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoLyrics" Value="{x:Bind ViewModel.TrackRightTapped.RawLyrics, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -402,7 +402,7 @@
|
||||
MaxWidth="48"
|
||||
MaxHeight="48"
|
||||
CornerRadius="4">
|
||||
<Image Source="{x:Bind EmbeddedPictures, Mode=OneWay, Converter={StaticResource PictureInfosToImageSourceConverter}}" Stretch="Uniform" />
|
||||
<Image Source="{x:Bind LocalAlbumArtPath, Mode=OneWay, Converter={StaticResource PathToImageConverter}}" Stretch="Uniform" />
|
||||
</Grid>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
@@ -414,7 +414,7 @@
|
||||
<TextBlock
|
||||
Margin="4,2"
|
||||
FontSize="12"
|
||||
Text="{x:Bind AudioFormat.ShortName}" />
|
||||
Text="{x:Bind AudioFormatShortName}" />
|
||||
</Grid>
|
||||
<HyperlinkButton Padding="0" Click="ArtistHyperlibkButton_Click">
|
||||
<TextBlock Text="{x:Bind Artist}" TextWrapping="Wrap" />
|
||||
@@ -443,12 +443,12 @@
|
||||
Text="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<!-- 歌曲时长 -->
|
||||
<!-- 路径 -->
|
||||
<HyperlinkButton
|
||||
Grid.Column="5"
|
||||
VerticalAlignment="Center"
|
||||
Click="PathHyperlibkButton_Click"
|
||||
Content="{x:Bind Path, Converter={StaticResource PathToParentFolderConverter}}" />
|
||||
Content="{x:Bind ParentFolderName}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="6"
|
||||
@@ -721,7 +721,6 @@
|
||||
<labs:Shimmer Grid.Row="4" CornerRadius="6" />
|
||||
<labs:Shimmer Grid.Row="6" CornerRadius="6" />
|
||||
</Grid>
|
||||
<ProgressRing IsActive="{x:Bind ViewModel.IsDataLoading, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
|
||||
private async void SongPathHyperlinkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LauncherHelper.SelectAndShowFile(((ExtendedTrack)((HyperlinkButton)sender).DataContext).Path);
|
||||
await LauncherHelper.SelectAndShowFile(((ExtendedTrack)((HyperlinkButton)sender).DataContext).UriPath);
|
||||
}
|
||||
|
||||
private async void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
@@ -236,7 +236,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var content = File.ReadAllText(path);
|
||||
foreach (var item in ViewModel.SelectedTracks.Select(x => x.Path).ToList())
|
||||
foreach (var item in ViewModel.SelectedTracks.Select(x => x.UriPath).ToList())
|
||||
{
|
||||
if (!content.Contains(item))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user