diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest
index 5767bf3..338aaf4 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest
@@ -12,7 +12,7 @@
+ Version="1.1.201.0" />
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml
index 89a9e84..e13d843 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml
@@ -65,7 +65,6 @@
-
@@ -75,6 +74,7 @@
+
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj
index b279d3e..e76e68e 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj
@@ -37,6 +37,7 @@
+
@@ -58,20 +59,21 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -85,6 +87,7 @@
+
@@ -95,6 +98,7 @@
+
@@ -339,6 +343,11 @@
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml
index 7ef65f4..964d0cd 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml
@@ -1,4 +1,3 @@
-
@@ -49,25 +49,32 @@
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}"
SelectionMode="None">
-
-
+
+
+
+ Content="{x:Bind Path, Mode=OneWay}"
+ Tag="{x:Bind Path, Mode=OneWay}"
+ ToolTipService.ToolTip="{x:Bind ConnectionSummary}" />
-
+
+
+
-
+
+
@@ -76,15 +83,55 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml.cs
index 4393667..a005ca6 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/MediaSettingsControl.xaml.cs
@@ -22,7 +22,7 @@ namespace BetterLyrics.WinUI3.Controls
private void SettingsPageRemovePathButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
- ViewModel.RemoveFolderAsync((LocalMediaFolder)(sender as HyperlinkButton)!.Tag);
+ ViewModel.RemoveFolderAsync((MediaFolder)(sender as HyperlinkButton)!.Tag);
}
private async void LocalFolderHyperlinkButton_Click(object sender, RoutedEventArgs e)
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/RemoteServerConfigControl.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/RemoteServerConfigControl.xaml
new file mode 100644
index 0000000..a210805
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/RemoteServerConfigControl.xaml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/RemoteServerConfigControl.xaml.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/RemoteServerConfigControl.xaml.cs
new file mode 100644
index 0000000..d2d886e
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/RemoteServerConfigControl.xaml.cs
@@ -0,0 +1,101 @@
+using BetterLyrics.WinUI3.Enums;
+using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Models;
+using DevWinUI;
+using Microsoft.UI.Xaml.Controls;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Windows.Storage;
+
+namespace BetterLyrics.WinUI3.Controls
+{
+ public sealed partial class RemoteServerConfigControl : UserControl
+ {
+ private readonly string _protocolType;
+
+ public RemoteServerConfigControl(string protocolType)
+ {
+ this.InitializeComponent();
+ _protocolType = protocolType;
+
+ SetupDefaults();
+ }
+
+ private void SetupDefaults()
+ {
+ switch (_protocolType.ToUpper())
+ {
+ case "SMB":
+ PortBox.Value = 445; // SMB 默认端口
+ PathBox.PlaceholderText = "SharedMusic";
+ break;
+ case "FTP":
+ PortBox.Value = 21; // FTP 默认端口
+ PathBox.PlaceholderText = "/pub/music";
+ break;
+ case "WEBDAV":
+ PortBox.Value = 80; // WebDAV 默认端口
+ PathBox.PlaceholderText = "/dav/music";
+ break;
+ }
+ }
+
+ public MediaFolder GetConfig()
+ {
+ if (string.IsNullOrWhiteSpace(HostBox.Text))
+ throw new ArgumentException("Server address is required.");
+
+ string name = $"{_protocolType} - {HostBox.Text}";
+
+ Enum.TryParse(_protocolType, true, out FileSourceType sourceType);
+
+ var folder = new MediaFolder
+ {
+ Name = name,
+ Path = HostBox.Text, // 这里 Path 存的是 IP/Host
+ Port = (int)PortBox.Value,
+ UserName = UserBox.Text,
+ Password = PwdBox.Password, // 从 PasswordBox 获取密码
+ SourceType = sourceType,
+ IsRealTimeWatchEnabled = false
+ };
+
+ // 特殊处理路径:
+ // 我们需要把"远程路径"拼接到 Path 里,或者用另一个字段存
+ // 为了简单,我们遵循上面的 MediaFolder 定义:
+ // 建议 MediaFolder 类里再加一个 RemotePath 字段,或者在这里把 Path 组合起来
+
+ // *修正建议*:如果不改 MediaFolder 定义,我们可以这样约定:
+ // Path 字段存储格式: "192.168.1.5/Music"
+
+ var rawPath = PathBox.Text.Trim().TrimStart('/', '\\'); // 去掉开头的斜杠
+ if (!string.IsNullOrEmpty(rawPath))
+ {
+ // 简单的路径拼接逻辑
+ if (sourceType == FileSourceType.SMB)
+ {
+ // SMBLibrary 的逻辑通常是 Host 分开,ShareName 分开
+ // 如果你把 IP 存在 Path 属性里,那你需要把 ShareName 拼在后面或者用新字段
+ // 为了方便,这里把 IP 和 ShareName 拼在一起存入 Path
+ // 比如: 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;
+ }
+
+ public void ShowError(string message)
+ {
+ ErrorInfoBar.Message = message;
+ ErrorInfoBar.IsOpen = true;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/FileSourceTypeToIconConverter.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/FileSourceTypeToIconConverter.cs
new file mode 100644
index 0000000..bfe596f
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/FileSourceTypeToIconConverter.cs
@@ -0,0 +1,30 @@
+锘縰sing BetterLyrics.WinUI3.Enums;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BetterLyrics.WinUI3.Converter
+{
+ public partial class FileSourceTypeToIconConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is FileSourceType type)
+ {
+ return type switch
+ {
+ FileSourceType.Local => new FontIcon { Glyph = "\uE8B7" }, // Folder
+ FileSourceType.SMB => new FontIcon { Glyph = "\uE839" }, // Network
+ FileSourceType.FTP => new FontIcon { Glyph = "\uE838" }, // Globe
+ FileSourceType.WebDav => new FontIcon { Glyph = "\uE753" }, // Cloud
+ _ => new FontIcon { Glyph = "\uE8B7" }
+ };
+ }
+ return new FontIcon { Glyph = "\uE8B7" };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/TrackToLyricsConverter.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/TrackToLyricsConverter.cs
deleted file mode 100644
index 70cf3b6..0000000
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/TrackToLyricsConverter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-锘縰sing ATL;
-using BetterLyrics.WinUI3.Extensions;
-using Microsoft.UI.Xaml.Data;
-using System;
-
-namespace BetterLyrics.WinUI3.Converter
-{
- public partial class TrackToLyricsConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, string language)
- {
- if (value is Track track)
- {
- return track.GetRawLyrics();
- }
- return "";
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, string language)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/FileSourceType.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/FileSourceType.cs
new file mode 100644
index 0000000..0de142b
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/FileSourceType.cs
@@ -0,0 +1,14 @@
+锘縰sing System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BetterLyrics.WinUI3.Enums
+{
+ public enum FileSourceType
+ {
+ Local,
+ SMB,
+ FTP,
+ WebDav
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Extensions/TrackExtensions.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Extensions/TrackExtensions.cs
deleted file mode 100644
index f1c82a0..0000000
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Extensions/TrackExtensions.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-锘縰sing ATL;
-using System.IO;
-
-namespace BetterLyrics.WinUI3.Extensions
-{
- public static class TrackExtensions
- {
- extension(Track track)
- {
- public string GetParentFolderName() => Directory.GetParent(track.Path)?.Name ?? "";
-
- public string GetParentFolderPath() => Directory.GetParent(track.Path)?.FullName ?? "";
-
- public string GetRawLyrics()
- {
- if (track.Path is string path)
- {
- try
- {
- return TagLib.File.Create(path).Tag.Lyrics;
- }
- catch (System.Exception)
- {
- return "";
- }
- }
- return "";
- }
-
- public string GetFileName() => Path.GetFileName(track.Path);
- }
- }
-}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/ExtendedTrack.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/ExtendedTrack.cs
new file mode 100644
index 0000000..6c1506b
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/ExtendedTrack.cs
@@ -0,0 +1,39 @@
+锘縰sing BetterLyrics.WinUI3.Models.FileSystem;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace BetterLyrics.WinUI3.Models
+{
+ public class ExtendedTrack : ATL.Track
+ {
+ 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);
+
+ public ExtendedTrack() : base() { }
+
+ public ExtendedTrack(string path) : base(path)
+ {
+ Path = path;
+ }
+
+ public ExtendedTrack(string path, Stream stream) : base(stream, System.IO.Path.GetExtension(path))
+ {
+ Path = path;
+ SetRawLyrics(new StreamFileAbstraction(path, stream));
+ }
+
+ private void SetRawLyrics(StreamFileAbstraction streamFileAbstraction)
+ {
+ try
+ {
+ RawLyrics = TagLib.File.Create(streamFileAbstraction).Tag.Lyrics;
+ }
+ catch (Exception) { }
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/FTPFileSystem.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/FTPFileSystem.cs
new file mode 100644
index 0000000..f0cfc47
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/FTPFileSystem.cs
@@ -0,0 +1,55 @@
+锘縰sing FluentFTP;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public partial class FTPFileSystem : IUnifiedFileSystem
+ {
+ private readonly AsyncFtpClient _client;
+ private readonly string _rootPath; // 鏈嶅姟鍣ㄤ笂鐨勬牴璺緞 (渚嬪 /pub/music)
+
+ public FTPFileSystem(string host, string user, string pass, int port, string remotePath)
+ {
+ // 濡傛灉 path 鏄 "192.168.1.5/Music"锛屾垜浠渶瑕佹妸 /Music 鎷嗗嚭鏉
+ // 浣嗕负浜嗙畝鍗曪紝鍋囪 host 浠呬粎鏄 IP锛宺emotePath 鎵嶆槸璺緞
+ _rootPath = remotePath ?? "/";
+
+ var config = new FtpConfig { ConnectTimeout = 5000 };
+ _client = new AsyncFtpClient(host, user ?? "anonymous", pass ?? "", port > 0 ? port : 21, config);
+ }
+
+ public async Task ConnectAsync()
+ {
+ await _client.AutoConnect();
+ return _client.IsConnected;
+ }
+
+ public async Task> GetFilesAsync(string relativePath)
+ {
+ 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,
+ Size = i.Size,
+ LastModified = i.Modified
+ }).ToList();
+ }
+
+ public async Task OpenReadAsync(string fullPath)
+ {
+ return await _client.OpenRead(fullPath);
+ }
+
+ public async Task DisconnectAsync() => await _client.Disconnect();
+ public void Dispose() => _client?.Dispose();
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/IUnifiedFileSystem.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/IUnifiedFileSystem.cs
new file mode 100644
index 0000000..065fd2a
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/IUnifiedFileSystem.cs
@@ -0,0 +1,17 @@
+锘縰sing System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Windows.Storage;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public interface IUnifiedFileSystem : IDisposable
+ {
+ Task ConnectAsync();
+ Task> GetFilesAsync(string relativePath);
+ Task OpenReadAsync(string fullPath);
+ Task DisconnectAsync();
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/LocalFileSystem.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/LocalFileSystem.cs
new file mode 100644
index 0000000..63307c0
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/LocalFileSystem.cs
@@ -0,0 +1,58 @@
+锘縰sing System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public partial class LocalFileSystem : IUnifiedFileSystem
+ {
+ private readonly string _rootPath;
+
+ public LocalFileSystem(string rootPath)
+ {
+ _rootPath = rootPath;
+ }
+
+ public Task ConnectAsync()
+ {
+ return Task.FromResult(Directory.Exists(_rootPath));
+ }
+
+ public async Task> GetFilesAsync(string relativePath)
+ {
+ var result = new List();
+
+ var targetPath = string.IsNullOrWhiteSpace(relativePath)
+ ? _rootPath
+ : Path.Combine(_rootPath, relativePath);
+
+ if (!Directory.Exists(targetPath)) return result;
+
+ var dirInfo = new DirectoryInfo(targetPath);
+
+ foreach (var item in dirInfo.GetFileSystemInfos())
+ {
+ bool isDir = (item.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
+ result.Add(new UnifiedFileItem
+ {
+ Name = item.Name,
+ FullPath = item.FullName,
+ IsFolder = isDir,
+ Size = isDir ? 0 : ((FileInfo)item).Length,
+ LastModified = item.LastWriteTime
+ });
+ }
+ return result;
+ }
+
+ public async Task OpenReadAsync(string fullPath)
+ {
+ return new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ }
+
+ public async Task DisconnectAsync() => await Task.CompletedTask;
+ public void Dispose() { }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/SMBFileSystem.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/SMBFileSystem.cs
new file mode 100644
index 0000000..f15fed4
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/SMBFileSystem.cs
@@ -0,0 +1,131 @@
+锘縰sing SMBLibrary;
+using SMBLibrary.Client;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public partial class SMBFileSystem : IUnifiedFileSystem
+ {
+ 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;
+
+ // fullPathInput 渚嬪: "192.168.1.5/Music/Pop"
+ public SMBFileSystem(string fullPathInput, string user, string pass)
+ {
+ _username = user;
+ _password = pass;
+
+ // 瑙f瀽璺緞锛氬垎绂 IP 鍜 鍏变韩鍚
+ var parts = fullPathInput.Replace("\\", "/").Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+
+ if (parts.Length >= 1) _ip = parts[0];
+ if (parts.Length >= 2) _shareName = parts[1];
+
+ // 鍓╀笅鐨勯儴鍒嗛噸鏂版嫾璧锋潵浣滀负瀛愯矾寰
+ if (parts.Length > 2)
+ _pathInsideShare = string.Join("\\", parts.Skip(2));
+ else
+ _pathInsideShare = "";
+ }
+
+ public async Task ConnectAsync()
+ {
+ _client = new SMB2Client();
+ bool connected = _client.Connect(_ip, SMBTransportType.DirectTCPTransport);
+ if (!connected) return false;
+
+ var status = _client.Login(string.Empty, _username, _password);
+ if (status != NTStatus.STATUS_SUCCESS) return false;
+
+ // 杩炴帴鍏蜂綋鐨勫叡浜枃浠跺す
+ if (string.IsNullOrEmpty(_shareName)) return true; // 鍙繛浜嗘湇鍔″櫒锛屾病杩炲叡浜
+
+ _fileStore = _client.TreeConnect(_shareName, out status);
+ return status == NTStatus.STATUS_SUCCESS;
+ }
+
+ public async Task> GetFilesAsync(string relativePath)
+ {
+ var result = new List();
+ if (_fileStore == null) return result;
+
+ // 鎷兼帴瀹屾暣璺緞: Root閲岄潰鐨勫瓙璺緞 + 浼犲叆鐨勭浉瀵硅矾寰
+ string queryPath = Path.Combine(_pathInsideShare, relativePath).Replace("/", "\\").TrimStart('\\');
+
+ // 鎵撳紑鐩綍
+ var statusRet = _fileStore.CreateFile(out object handle, out FileStatus status, queryPath,
+ AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read,
+ CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
+
+ if (statusRet != NTStatus.STATUS_SUCCESS) return result;
+
+ List fileInfo;
+ do
+ {
+ statusRet = _fileStore.QueryDirectory(out fileInfo, handle, "*", FileInformationClass.FileDirectoryInformation);
+
+ List list = fileInfo.Select(x => (FileDirectoryInformation)x).ToList();
+ foreach (var item in list)
+ {
+ // 鎺掗櫎褰撳墠鐩綍鍜岀埗鐩綍
+ if (item.FileName == "." || item.FileName == "..") continue;
+
+ result.Add(new UnifiedFileItem
+ {
+ Name = item.FileName,
+ FullPath = Path.Combine(queryPath, item.FileName),
+ IsFolder = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory,
+ Size = item.AllocationSize,
+ LastModified = item.LastWriteTime
+ });
+ }
+
+ if (statusRet == NTStatus.STATUS_NO_MORE_FILES)
+ {
+ break;
+ }
+
+ if (statusRet != NTStatus.STATUS_SUCCESS)
+ {
+ // Log
+ break;
+ }
+ } while (statusRet == NTStatus.STATUS_SUCCESS);
+
+ _fileStore.CloseFile(handle);
+ return result;
+ }
+
+ public async Task OpenReadAsync(string fullPath)
+ {
+ var ret = _fileStore.CreateFile(out object handle, out FileStatus status, fullPath,
+ 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}");
+
+ return new SMBReadOnlyStream(_fileStore, handle);
+ }
+
+ public async Task DisconnectAsync()
+ {
+ _client?.Disconnect();
+ await Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ _client?.Disconnect();
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/SMBReadOnlyStream.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/SMBReadOnlyStream.cs
new file mode 100644
index 0000000..e663281
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/SMBReadOnlyStream.cs
@@ -0,0 +1,116 @@
+锘縰sing SMBLibrary;
+using SMBLibrary.Client;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public partial class SMBReadOnlyStream : Stream
+ {
+ private readonly ISMBFileStore _store;
+ private readonly object _handle;
+ private long _position;
+ private long _length; // 鏂板锛氱紦瀛樻枃浠堕暱搴
+
+ public SMBReadOnlyStream(ISMBFileStore store, object handle)
+ {
+ _store = store;
+ _handle = handle;
+ _position = 0;
+
+ var status = _store.GetFileInformation(out FileInformation result, handle, FileInformationClass.FileStandardInformation);
+ if (status == NTStatus.STATUS_SUCCESS && result is FileStandardInformation info)
+ {
+ _length = info.EndOfFile;
+ }
+ else
+ {
+ // 濡傛灉鑾峰彇澶辫触锛岃繖鏄竴涓弗閲嶉棶棰橈紝鎰忓懗鐫鏃犳硶 Seek 鍒版湯灏
+ // 鏆傛椂璁句负 0锛屼絾鍚庣画璇诲彇鍙兘浼氬嚭闂
+ _length = 0;
+ }
+ }
+
+ 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;
+ set => _position = value;
+ }
+
+ 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);
+
+ // 涓轰簡瀹夊叏锛屼繚鐣欏 remaining 鐨勬鏌ユ槸蹇呴』鐨
+ if (bytesToRequest <= 0) return 0;
+
+ var status = _store.ReadFile(out byte[] data, _handle, _position, bytesToRequest);
+
+ if (status == NTStatus.STATUS_END_OF_FILE) return 0;
+
+ if (status != NTStatus.STATUS_SUCCESS)
+ {
+ throw new IOException($"SMB Read failed. Status: {status} (Pos: {_position}, Req: {bytesToRequest})");
+ }
+
+ if (data == null || data.Length == 0) return 0;
+
+ Array.Copy(data, 0, buffer, offset, data.Length);
+ _position += data.Length;
+ return data.Length;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ long newPos = _position;
+
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ newPos = offset;
+ break;
+ case SeekOrigin.Current:
+ newPos = _position + offset;
+ break;
+ case SeekOrigin.End:
+ newPos = _length + offset;
+ 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.");
+ }
+
+ _position = newPos;
+ return _position;
+ }
+
+ public override void SetLength(long value) => throw new NotSupportedException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+ public override void Flush() { }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ {
+ try { _store.CloseFile(_handle); } catch { }
+ }
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/StreamFileAbstraction.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/StreamFileAbstraction.cs
new file mode 100644
index 0000000..264f764
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/StreamFileAbstraction.cs
@@ -0,0 +1,45 @@
+锘縰sing System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public class StreamFileAbstraction : TagLib.File.IFileAbstraction
+ {
+ private readonly string _name;
+ private readonly Stream _stream;
+ private readonly bool _closeStreamOnDispose;
+
+ public StreamFileAbstraction(string path, Stream stream, bool closeStreamOnDispose = false)
+ {
+ _name = Path.GetFileName(path);
+ _stream = stream ?? throw new ArgumentNullException(nameof(stream));
+ _closeStreamOnDispose = closeStreamOnDispose;
+ }
+
+ public string Name => _name;
+
+ public Stream ReadStream => _stream;
+
+ public Stream WriteStream
+ {
+ get
+ {
+ if (_stream.CanWrite)
+ {
+ return _stream;
+ }
+ throw new InvalidOperationException("The underlying stream is read-only. Tag saving is not supported for this source.");
+ }
+ }
+
+ public void CloseStream(Stream stream)
+ {
+ if (_closeStreamOnDispose)
+ {
+ stream?.Dispose();
+ }
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/UnifiedFileItem.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/UnifiedFileItem.cs
new file mode 100644
index 0000000..8e7259c
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/UnifiedFileItem.cs
@@ -0,0 +1,15 @@
+锘縰sing System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public class UnifiedFileItem
+ {
+ public string Name { get; set; }
+ public string FullPath { get; set; }
+ public long Size { get; set; }
+ public bool IsFolder { get; set; }
+ public DateTime? LastModified { get; set; }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/WebDavFileSystem.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/WebDavFileSystem.cs
new file mode 100644
index 0000000..9e02054
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileSystem/WebDavFileSystem.cs
@@ -0,0 +1,87 @@
+锘縰sing System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WebDav;
+
+namespace BetterLyrics.WinUI3.Models.FileSystem
+{
+ public partial class WebDavFileSystem : IUnifiedFileSystem
+ {
+ private readonly WebDavClient _client;
+ private readonly string _baseUrl;
+ private readonly string _rootPath;
+
+ // host: http://192.168.1.5:5005
+ // path: /music
+ public WebDavFileSystem(string host, string user, string pass, int port, string path)
+ {
+ if (!host.StartsWith("http")) host = $"http://{host}";
+ if (port > 0) host = $"{host}:{port}";
+
+ _baseUrl = host;
+ _rootPath = path ?? "/";
+
+ _client = new WebDavClient(new WebDavClientParams
+ {
+ BaseAddress = new Uri(_baseUrl),
+ Credentials = new System.Net.NetworkCredential(user, pass)
+ });
+ }
+
+ public async Task ConnectAsync()
+ {
+ // WebDAV 鏃犵姸鎬侊紝Propfind 娴嬭瘯鏍圭洰褰曡繛閫氭
+ var result = await _client.Propfind(_rootPath);
+ return result.IsSuccessful;
+ }
+
+ public async Task> GetFilesAsync(string relativePath)
+ {
+ var targetPath = Path.Combine(_rootPath, relativePath).Replace("\\", "/");
+ var result = await _client.Propfind(targetPath);
+
+ var list = new List();
+ if (result.IsSuccessful)
+ {
+ foreach (var res in result.Resources)
+ {
+ if (res == null || res.Uri == null) continue;
+
+ // 鎺掗櫎鎺夋枃浠跺す鑷韩 (WebDAV 閫氬父浼氭妸褰撳墠璇锋眰鐨勬枃浠跺す浣滀负绗竴涓粨鏋滆繑鍥)
+ // 閫氳繃鍒ゆ柇 URL 缁撳熬鏄惁涓鑷存潵绠鍗曡繃婊わ紝鎴栬呭垽鏂 IsCollection 涓 Uri 鐩稿悓
+ // 杩欓噷绠鍗曞鐞嗭細鍙鍚嶅瓧涓嶄负绌
+ var name = System.Net.WebUtility.UrlDecode(res.Uri.Split('/').LastOrDefault());
+ if (string.IsNullOrEmpty(name)) continue;
+
+ // 濡傛灉鍚嶅瓧鍜岃姹傜殑鐩綍鍚嶄竴鏍凤紝鍙兘鏄畠鑷繁锛岃烦杩 (杩欓渶瑕佹牴鎹叿浣撴湇鍔″櫒鍝嶅簲璋冩暣)
+ // 鏇寸ǔ濡ョ殑鏄瘮杈 Uri
+
+ list.Add(new UnifiedFileItem
+ {
+ Name = name,
+ FullPath = res.Uri.ToString(), // WebDAV 闇瑕佸畬鏁 URI
+ IsFolder = res.IsCollection,
+ Size = res.ContentLength ?? 0,
+ LastModified = res.LastModifiedDate
+ });
+ }
+ }
+ return list;
+ }
+
+ public async Task OpenReadAsync(string fullPath)
+ {
+ // WebDAV 鑾峰彇娴
+ var res = await _client.GetRawFile(fullPath);
+ if (!res.IsSuccessful) throw new IOException($"WebDAV Error: {res.StatusCode}");
+ return res.Stream;
+ }
+
+ public async Task DisconnectAsync() => await Task.CompletedTask;
+
+ public void Dispose() => _client?.Dispose();
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LocalLyricsFolder.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LocalLyricsFolder.cs
deleted file mode 100644
index ab51d19..0000000
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LocalLyricsFolder.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-锘// 2025/6/23 by Zhe Fang
-
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace BetterLyrics.WinUI3.Models
-{
- public partial class LocalMediaFolder : ObservableRecipient
- {
- [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
- [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
- [ObservableProperty][NotifyPropertyChangedRecipients] public partial string Path { get; set; }
-
- public LocalMediaFolder() { }
-
- public LocalMediaFolder(string path)
- {
- Path = path;
- }
- }
-}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MediaFolder.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MediaFolder.cs
new file mode 100644
index 0000000..33c1e69
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MediaFolder.cs
@@ -0,0 +1,73 @@
+锘// 2025/6/23 by Zhe Fang
+
+using BetterLyrics.WinUI3.Enums;
+using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Models.FileSystem;
+using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+using System.Text.Json.Serialization;
+
+namespace BetterLyrics.WinUI3.Models
+{
+ public partial class MediaFolder : ObservableRecipient
+ {
+ [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))]
+ public partial FileSourceType SourceType { get; set; } = FileSourceType.Local;
+
+ [ObservableProperty][NotifyPropertyChangedRecipients] public partial string Name { get; set; }
+
+ [ObservableProperty] public partial string UserName { get; set; }
+
+ [ObservableProperty] public partial int Port { get; set; } = -1;
+
+ [JsonIgnore] public string Password { get; set; }
+
+ [JsonIgnore] public bool IsLocal => SourceType == FileSourceType.Local;
+
+ [JsonIgnore]
+ public string ConnectionSummary
+ {
+ get
+ {
+ if (IsLocal) return Path;
+ return $"{SourceType} - {Path} {(string.IsNullOrEmpty(UserName) ? "" : $"({UserName})")}";
+ }
+ }
+
+ [JsonIgnore] public string VaultKey => $"{Id}-{UserName}";
+
+ public MediaFolder() { }
+
+ public MediaFolder(string path)
+ {
+ Path = path;
+ }
+
+ public IUnifiedFileSystem? CreateFileSystem()
+ {
+ if (!IsEnabled) return null;
+ if (string.IsNullOrEmpty(Password) && !IsLocal)
+ {
+ Password = PasswordVaultHelper.Get(Constants.App.AppName, VaultKey) ?? "";
+ }
+
+ 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),
+ _ => throw new NotImplementedException()
+ };
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/PlayQueueItem.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/PlayQueueItem.cs
index 56c4ce3..5473924 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/PlayQueueItem.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/PlayQueueItem.cs
@@ -4,9 +4,9 @@ namespace BetterLyrics.WinUI3.Models
{
public class PlayQueueItem
{
- public Track Track { get; set; }
+ public ExtendedTrack Track { get; set; }
- public PlayQueueItem(Track track)
+ public PlayQueueItem(ExtendedTrack track)
{
Track = track;
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Settings/AppSettings.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Settings/AppSettings.cs
index ff143a9..89e2824 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Settings/AppSettings.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Settings/AppSettings.cs
@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial MusicGallerySettings MusicGallerySettings { get; set; } = new MusicGallerySettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AdvancedSettings AdvancedSettings { get; set; } = new AdvancedSettings();
- [ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection LocalMediaFolders { get; set; } = [];
+ [ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection LocalMediaFolders { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection MediaSourceProvidersInfo { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection MappedSongSearchQueries { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection WindowBoundsRecords { get; set; } = [];
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs
index fed2e82..afb4421 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs
@@ -49,7 +49,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
- result = SearchFile(songInfo)?.AsBuffer();
+ result = (await SearchFile(songInfo))?.AsBuffer();
break;
case AlbumArtSearchProvider.SMTC:
result = bufferFromSMTC;
@@ -77,29 +77,73 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
return null;
}
- private byte[]? SearchFile(SongInfo songInfo)
+ private async Task SearchFile(SongInfo songInfo)
{
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
- if (Directory.Exists(folder.Path) && folder.IsEnabled)
+ if (!folder.IsEnabled) continue;
+
+ try
{
- foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
+ using var fs = folder.CreateFileSystem();
+ if (fs == null) continue;
+ if (!await fs.ConnectAsync()) continue;
+
+ // 閫掑綊鎵弿
+ var foldersToScan = new Queue();
+ foldersToScan.Enqueue(""); // 鏍圭洰褰
+
+ while (foldersToScan.Count > 0)
{
- if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
+ var currentPath = foldersToScan.Dequeue();
+ var items = await fs.GetFilesAsync(currentPath);
+
+ foreach (var item in items)
{
- Track track = new(file);
- if ((track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists) || StringHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), songInfo.DisplayArtists, songInfo.Title))
+ if (item.IsFolder)
{
- var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
- if (bytes != null)
+ foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
+ 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 = (track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists);
+ bool isFilenameMatch = StringHelper.IsSwitchableNormalizedMatch(
+ Path.GetFileNameWithoutExtension(item.Name),
+ songInfo.DisplayArtists,
+ songInfo.Title
+ );
+
+ if (isMetadataMatch || isFilenameMatch)
+ {
+ var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
+ if (bytes != null && bytes.Length > 0)
+ {
+ return bytes;
+ }
+ }
+ }
+ }
+ catch
{
- return bytes;
}
}
}
}
}
+ catch
+ {
+ }
}
+
return null;
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/LyricsSearchService/LyricsSearchService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/LyricsSearchService/LyricsSearchService.cs
index d754cc5..14df860 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/LyricsSearchService/LyricsSearchService.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/LyricsSearchService/LyricsSearchService.cs
@@ -242,7 +242,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
lyricsSearchResult = await SearchAmllTtmlDbAsync(songInfo);
break;
case LyricsSearchProvider.LocalMusicFile:
- lyricsSearchResult = SearchEmbedded(songInfo);
+ lyricsSearchResult = await SearchEmbedded(songInfo);
break;
case LyricsSearchProvider.LocalLrcFile:
case LyricsSearchProvider.LocalEslrcFile:
@@ -277,7 +277,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private async Task SearchFile(SongInfo songInfo, LyricsFormat format)
{
int maxScore = 0;
- string? bestFile = null;
+
+ MediaFolder? bestFolder = null;
+ string? bestFilePath = null;
var lyricsSearchResult = new LyricsSearchResult();
@@ -288,47 +290,97 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
- if (Directory.Exists(folder.Path) && folder.IsEnabled)
+ if (!folder.IsEnabled) continue;
+
+ try
{
- try
+ using var fs = folder.CreateFileSystem();
+ if (fs == null) continue;
+ if (!await fs.ConnectAsync()) continue;
+
+ // 閫掑綊鎵弿
+ var foldersToScan = new Queue();
+ foldersToScan.Enqueue(""); // 浠庢牴鐩綍寮濮
+
+ string targetExt = format.ToFileExtension();
+
+ while (foldersToScan.Count > 0)
{
- foreach (var file in DirectoryHelper.GetAllFiles(folder.Path, $"*{format.ToFileExtension()}"))
+ var currentPath = foldersToScan.Dequeue();
+ var items = await fs.GetFilesAsync(currentPath);
+
+ foreach (var item in items)
{
- int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = file });
- if (score > maxScore)
+ if (item.IsFolder)
{
- bestFile = file;
- maxScore = score;
+ foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
+ continue;
+ }
+
+ if (item.Name.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
+ {
+ int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FullPath });
+
+ if (score > maxScore)
+ {
+ maxScore = score;
+ bestFilePath = item.FullPath;
+ bestFolder = folder;
+ }
}
}
}
- catch (Exception)
- {
- }
+ }
+ catch (Exception ex)
+ {
+ // 鏃ュ織璁板綍...
}
}
- if (bestFile != null)
+ // 4. 濡傛灉鎵惧埌浜嗘渶浣冲尮閰嶏紝璇诲彇鍐呭
+ if (bestFolder != null && bestFilePath != null)
{
- lyricsSearchResult.Reference = bestFile;
- lyricsSearchResult.MatchPercentage = maxScore;
-
- string? raw = await File.ReadAllTextAsync(bestFile, FileHelper.GetEncoding(bestFile));
- if (raw != null)
+ try
{
- lyricsSearchResult.Raw = raw;
+ // 閲嶆柊杩炴帴浠ヨ鍙栨枃浠 (鍥犱负涔嬪墠鐨 fs 宸茬粡鍦 using 缁撴潫鏃堕噴鏀)
+ using var fs = bestFolder.CreateFileSystem();
+ if (fs != null && await fs.ConnectAsync())
+ {
+ using var stream = await fs.OpenReadAsync(bestFilePath);
+
+ // 浣跨敤 StreamReader 璇诲彇鏂囨湰
+ // 娉ㄦ剰锛氳繖閲岀畝鍗曚娇鐢 Default 缂栫爜锛屽鏋滈渶瑕佹帰娴嬬紪鐮(FileHelper.GetEncoding)锛
+ // 鍙兘闇瑕佸厛璇讳竴閮ㄥ垎瀛楄妭鏉ュ垽鏂紝鎴栬呬娇鐢ㄥ甫缂栫爜鎺㈡祴鐨勫簱銆
+ using var reader = new StreamReader(stream);
+
+ string raw = await reader.ReadToEndAsync();
+
+ lyricsSearchResult.Reference = bestFilePath;
+ lyricsSearchResult.MatchPercentage = maxScore;
+ lyricsSearchResult.Raw = raw;
+ }
+ }
+ catch (Exception)
+ {
+ // 璇诲彇澶辫触澶勭悊
}
}
return lyricsSearchResult;
}
- private LyricsSearchResult SearchEmbedded(SongInfo songInfo)
+ private async Task SearchEmbedded(SongInfo songInfo)
{
int bestScore = 0;
- string? bestFile = null;
+ 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,
@@ -336,49 +388,89 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
- if (Directory.Exists(folder.Path) && folder.IsEnabled)
+ if (!folder.IsEnabled) continue;
+
+ try
{
- foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
+ using var fs = folder.CreateFileSystem();
+ if (fs == null) continue;
+ if (!await fs.ConnectAsync()) continue;
+
+ var foldersToScan = new Queue();
+ foldersToScan.Enqueue("");
+
+ while (foldersToScan.Count > 0)
{
- if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
+ var currentPath = foldersToScan.Dequeue();
+ var items = await fs.GetFilesAsync(currentPath);
+
+ foreach (var item in items)
{
- var track = new Track(file);
- var raw = track.GetRawLyrics();
-
- if (!string.IsNullOrEmpty(raw))
+ if (item.IsFolder)
{
- int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
- {
- Title = track.Title,
- Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator),
- Album = track.Album,
- Duration = track.Duration,
- Reference = file,
- });
+ foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
+ continue;
+ }
- if (score > bestScore)
+ var ext = Path.GetExtension(item.Name).ToLower();
+ if (FileHelper.MusicExtensions.Contains(ext))
+ {
+ try
{
- bestScore = score;
- bestFile = file;
- bestRaw = raw;
+ 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,
+ });
+
+ if (score > bestScore)
+ {
+ bestScore = score;
+ bestFilePath = item.FullPath;
+ bestRaw = raw;
+
+ // 缂撳瓨褰撳墠鏈浣崇殑鍏冩暟鎹紝閬垮厤鏈鍚庤繕闇瑕侀噸鏂版墦寮鏂囦欢璇讳竴娆
+ bestTitle = track.Title;
+ bestArtists = track.Artist?.Split(ATL.Settings.DisplayValueSeparator);
+ bestAlbum = track.Album;
+ bestDuration = track.Duration;
+ }
+ }
+ }
+ catch
+ {
+ // 鍗曚釜鏂囦欢瑙f瀽澶辫触蹇界暐
}
}
}
}
}
+ catch
+ {
+ // 鏂囦欢澶规壂鎻忓け璐ュ拷鐣
+ }
}
- if (bestFile != null)
+ if (bestFilePath != null)
{
- var track = new Track(bestFile);
-
- lyricsSearchResult.Title = track.Title;
- lyricsSearchResult.Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator);
- lyricsSearchResult.Album = track.Album;
- lyricsSearchResult.Duration = track.Duration;
+ // 鐩存帴浣跨敤缂撳瓨鐨勬暟鎹紝涓嶉渶瑕 new Track(bestFile) 浜
+ lyricsSearchResult.Title = bestTitle;
+ lyricsSearchResult.Artists = bestArtists;
+ lyricsSearchResult.Album = bestAlbum;
+ lyricsSearchResult.Duration = bestDuration;
lyricsSearchResult.Raw = bestRaw;
- lyricsSearchResult.Reference = bestFile;
+ lyricsSearchResult.Reference = bestFilePath;
lyricsSearchResult.MatchPercentage = bestScore;
}
@@ -560,13 +652,14 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
ISearchResult? result;
- if (searcher == Searchers.Netease && songInfo.SongId != null)
+
+ if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIDHelper.IsNeteaseFamily(songInfo.PlayerId))
{
- result = new NeteaseSearchResult("", [], "", [], 0, songInfo.SongId);
+ result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
}
- else if (searcher == Searchers.QQMusic && songInfo.SongId != null)
+ else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerID.QQMusic)
{
- result = new QQMusicSearchResult("", [], "", [], 0, songInfo.SongId, "");
+ result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
}
else
{
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
index a386c9d..d3850c8 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
@@ -120,6 +120,9 @@
Operation completed
+
+ Add
+
Local music files
@@ -546,6 +549,21 @@
Privacy policy
+
+ Password
+
+
+ Path
+
+
+ Port
+
+
+ Server Address
+
+
+ Username
+
Romaji
@@ -594,9 +612,6 @@
Adapt to environmental color
-
- Add a folder
-
Add
@@ -1044,6 +1059,9 @@
Enable monitoring for new playback sources
+
+ Local Folder
+
Log record
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw
index d6c05e3..dc7ee89 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw
@@ -120,6 +120,9 @@
鎿嶄綔銇屽畬浜嗐仐銇俱仐銇
+
+ 杩藉姞
+
銉兗銈儷闊虫ソ銉曘偂銈ゃ儷
@@ -546,6 +549,21 @@
銉椼儵銈ゃ儛銈枫兗銉濄儶銈枫兗
+
+ 銉戙偣銉兗銉
+
+
+ 銉戙偣
+
+
+ 銉濄兗銉
+
+
+ 銈点兗銉愩兗銈€儔銉偣
+
+
+ 銉︺兗銈躲兗鍚
+
銉兗銉炪兂
@@ -594,9 +612,6 @@
鐠板銇壊銇仼蹇溿仐銇俱仐銈囥亞
-
- 銉曘偐銉儉銉笺倰杩藉姞銇椼伨銇
-
杩藉姞
@@ -1044,6 +1059,9 @@
鏂般仐銇勫啀鐢熴偨銉笺偣銇洠瑕栥倰鏈夊姽銇仐銇俱仚
+
+ 銉兗銈儷 銉曘偐銉儉
+
銉偘銉偝銉笺儔
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw
index c65a650..9259fdf 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw
@@ -120,6 +120,9 @@
鞛戩爠 鞕勲
+
+ 於旉皜頃橂嫟
+
搿滌滑 鞚岇晠 韺岇澕
@@ -546,6 +549,21 @@
臧滌澑鞝曤炒 氤错樃鞝曥眳
+
+ 牍勲皜氩堩樃
+
+
+ 旮
+
+
+ 頃惮
+
+
+ IP 欤检唽
+
+
+ 靷毄鞛 鞚措
+
搿滊
@@ -594,9 +612,6 @@
頇橁步 靸夓儊鞐 鞝侅潙頃橃劯鞖
-
- 韽措崝毳 於旉皜頃橃嫮鞁滌槫
-
於旉皜頃橂嫟
@@ -1044,6 +1059,9 @@
靸堧鞖 鞛儩 靻岇姢鞐 雽頃 氇媹韯半鞚 頇滌劚頇旐晿鞁嫓鞓
+
+ 搿滌滑 韽措崝
+
搿滉犯 霠堨綌霌
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
index 736da5b..0f09f0b 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
@@ -120,6 +120,9 @@
鎿嶄綔瀹屾垚
+
+ 娣诲姞
+
鏈湴闊充箰鏂囦欢
@@ -546,6 +549,21 @@
闅愮鏀跨瓥
+
+ 瀵嗙爜
+
+
+ 璺緞
+
+
+ 绔彛
+
+
+ 鏈嶅姟鍣ㄥ湴鍧
+
+
+ 鐢ㄦ埛鍚
+
缃楅┈闊
@@ -594,9 +612,6 @@
閫傚簲鐜鑹插僵
-
- 娣诲姞鏂囦欢澶
-
娣诲姞
@@ -1044,6 +1059,9 @@
鍚敤瀵规柊鎾斁婧愮殑鐩戝惉
+
+ 鏈湴鏂囦欢澶
+
鏃ュ織璁板綍
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
index d1634da..cda5e2f 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
@@ -120,6 +120,9 @@
鎿嶄綔瀹屾垚
+
+ 娣诲姞
+
鏈湴闊虫▊鏂囦欢
@@ -546,6 +549,21 @@
闅辩鏀跨瓥
+
+ 瀵嗙⒓
+
+
+ 璺緫
+
+
+ 閫f帴鍩
+
+
+ 浼烘湇鍣ㄤ綅鍧
+
+
+ 浣跨敤鑰呭悕绋
+
缇呴Μ闊
@@ -594,9 +612,6 @@
閬╂噳鐠板鑹插僵
-
- 鏂板璩囨枡澶
-
娣诲姞
@@ -1044,6 +1059,9 @@
鍟熺敤灏嶆柊鎾斁渚嗘簮鐨勭洠鑱
+
+ 鏈湴璩囨枡澶
+
鏃ヨ獙瑷橀寗
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MediaSettingsControlViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MediaSettingsControlViewModel.cs
index 3610eef..579dc6e 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MediaSettingsControlViewModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MediaSettingsControlViewModel.cs
@@ -1,4 +1,7 @@
-锘縰sing BetterLyrics.WinUI3.Helper;
+锘縰sing BetterLyrics.WinUI3.Controls;
+using BetterLyrics.WinUI3.Enums;
+using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.ResourceService;
@@ -12,6 +15,7 @@ using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Windows.Foundation;
namespace BetterLyrics.WinUI3.ViewModels
{
@@ -30,17 +34,6 @@ namespace BetterLyrics.WinUI3.ViewModels
AppSettings = _settingsService.AppSettings;
}
- [RelayCommand]
- private async Task SelectAndAddFolderAsync(UIElement sender)
- {
- var folder = await PickerHelper.PickSingleFolderAsync();
-
- if (folder != null)
- {
- AddFolderAsync(folder.Path);
- }
- }
-
private void AddFolderAsync(string path)
{
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
@@ -62,13 +55,93 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
- AppSettings.LocalMediaFolders.Add(new LocalMediaFolder(path));
+ AppSettings.LocalMediaFolders.Add(new MediaFolder(path));
}
}
- public void RemoveFolderAsync(LocalMediaFolder folder)
+ public void RemoveFolderAsync(MediaFolder folder)
{
AppSettings.LocalMediaFolders.Remove(folder);
}
+
+ [RelayCommand]
+ private async Task SelectAndAddFolderAsync(UIElement sender)
+ {
+ var folder = await PickerHelper.PickSingleFolderAsync();
+
+ if (folder != null)
+ {
+ AddFolderAsync(folder.Path);
+ }
+ }
+
+ [RelayCommand]
+ private async Task AddRemoteSourceAsync(string protocolType)
+ {
+ var dialog = new ContentDialog
+ {
+ XamlRoot = WindowHook.GetWindow()?.Content.XamlRoot,
+ Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
+ Title = protocolType,
+ PrimaryButtonText = _resourceService.GetLocalizedString("Add"),
+ CloseButtonText = _resourceService.GetLocalizedString("Cancel"),
+ DefaultButton = ContentDialogButton.Primary,
+ Content = new RemoteServerConfigControl(protocolType)
+ };
+
+ dialog.PrimaryButtonClick += async (s, e) =>
+ {
+ var configControl = (RemoteServerConfigControl)dialog.Content;
+
+ try
+ {
+ e.Cancel = true;
+
+ dialog.IsPrimaryButtonEnabled = false;
+ configControl.IsEnabled = false;
+ dialog.Title = $"Connecting to {protocolType}...";
+
+ var tempFolder = configControl.GetConfig();
+
+ var provider = tempFolder.CreateFileSystem();
+
+ bool isConnected = provider != null && await provider.ConnectAsync();
+
+ if (isConnected)
+ {
+ await provider!.DisconnectAsync();
+
+ PasswordVaultHelper.Save(Constants.App.AppName, tempFolder.VaultKey, tempFolder.Password);
+ AppSettings.LocalMediaFolders.Add(tempFolder);
+
+ e.Cancel = false;
+ }
+ else
+ {
+ ShowErrorTip(configControl, "Connection failed. Check IP/Port.");
+ }
+ }
+ catch (Exception ex)
+ {
+ ShowErrorTip(configControl, $"Error: {ex.Message}");
+ }
+ finally
+ {
+ dialog.IsPrimaryButtonEnabled = true;
+ configControl.IsEnabled = true;
+ dialog.Title = protocolType;
+ }
+ };
+
+ await dialog.ShowAsync();
+ }
+
+ private void ShowErrorTip(RemoteServerConfigControl control, string message)
+ {
+ // 浣犲彲浠ュ湪 RemoteServerConfigControl 閲屽姞涓涓 InfoBar 鐢ㄦ潵鏄剧ず閿欒
+ // 鍋囪浣犲湪 UserControl 閲屽叕寮浜嗕竴涓 ShowError 鏂规硶
+ control.ShowError(message);
+ }
+
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MusicGalleryPageViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MusicGalleryPageViewModel.cs
index 8b4b2ed..928ae28 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MusicGalleryPageViewModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MusicGalleryPageViewModel.cs
@@ -43,11 +43,11 @@ namespace BetterLyrics.WinUI3.ViewModels
private readonly DispatcherQueueTimer _refreshSongsTimer;
// All songs
- private List