diff --git a/FileShare-EFCore/Database/AppDataContext.cs b/FileShare-EFCore/Database/AppDataContext.cs index 79a7fd4..9b10a90 100644 --- a/FileShare-EFCore/Database/AppDataContext.cs +++ b/FileShare-EFCore/Database/AppDataContext.cs @@ -25,6 +25,7 @@ namespace FileShare_EFCore.Database /// 文件库文件记录数据 public DbSet ManagedFileRecords => Set(); + /// 文件缩略图映射数据 public DbSet ManagedThumbnailMaps => Set(); /// diff --git a/FileShare-EFCore/Models/ManagedFileRecord.cs b/FileShare-EFCore/Models/ManagedFileRecord.cs index 779eb93..1f37e8f 100644 --- a/FileShare-EFCore/Models/ManagedFileRecord.cs +++ b/FileShare-EFCore/Models/ManagedFileRecord.cs @@ -90,6 +90,7 @@ namespace FileShare_EFCore.Models /// 所属根目录。 public ManagedLibraryRoot? LibraryRoot { get; set; } + /// 缩略图映射记录。 public ManagedThumbnailMap? Thumbnail { get; set; } } } diff --git a/FileShare-EFCore/Models/ManagedLibraryRoot.cs b/FileShare-EFCore/Models/ManagedLibraryRoot.cs index 80ba977..8d73fd2 100644 --- a/FileShare-EFCore/Models/ManagedLibraryRoot.cs +++ b/FileShare-EFCore/Models/ManagedLibraryRoot.cs @@ -63,6 +63,7 @@ namespace FileShare_EFCore.Models /// 文件记录。 public List Files { get; set; } = new(); + /// 缩略图记录。 public List Thumbnails { get; set; } = new(); } } diff --git a/FileShare-EFCore/Models/ManagedThumbnailMap.cs b/FileShare-EFCore/Models/ManagedThumbnailMap.cs index 9420340..b65835e 100644 --- a/FileShare-EFCore/Models/ManagedThumbnailMap.cs +++ b/FileShare-EFCore/Models/ManagedThumbnailMap.cs @@ -4,34 +4,45 @@ using System.ComponentModel.DataAnnotations.Schema; namespace FileShare_EFCore.Models { + /// + /// 文件缩略图映射记录,存储视频缩略图的文件路径与内容类型。 + /// [Comment("文件缩略图映射记录")] [Table("managed-thumbnail-map")] public class ManagedThumbnailMap { + /// 主键 ID。 [Key] [Column("id")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } + /// 所属根目录 ID。 [Column("library-root-id")] public int LibraryRootId { get; set; } + /// 缩略图相对于缩略图存储根目录的路径。 [Column("relative-path")] [MaxLength(1024)] public string RelativePath { get; set; } = string.Empty; + /// 缩略图的 MIME 类型。 [Column("content-type")] [MaxLength(100)] public string ContentType { get; set; } = "image/jpeg"; + /// 创建时间 UTC。 [Column("created-at")] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + /// 更新时间 UTC。 [Column("updated-at")] public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + /// 所属根目录。 public ManagedLibraryRoot? LibraryRoot { get; set; } + /// 引用了此缩略图的文件记录。 public List Files { get; set; } = new(); } } diff --git a/FileShare-PC/Views/MainWindow.axaml.cs b/FileShare-PC/Views/MainWindow.axaml.cs index c77ebd3..5459ff8 100644 --- a/FileShare-PC/Views/MainWindow.axaml.cs +++ b/FileShare-PC/Views/MainWindow.axaml.cs @@ -692,6 +692,11 @@ namespace FileShare_PC.Views return true; } + /// + /// 尝试处理本地缩略图请求,匹配 /api/thumbnails/{id} 路径并返回缩略图文件流。 + /// + /// HTTP 监听器上下文。 + /// 如果请求路径匹配缩略图端点则返回 true,否则返回 false。 private async Task TryHandleLocalThumbnailAsync(HttpListenerContext context) { var request = context.Request; diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs index 994b432..ed8a515 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs @@ -85,8 +85,18 @@ namespace FileShare_Services.Services.FileLibrary /// API 响应。 Task BrowseDirectoryAsync(BrowseDirectoryRequest request); + /// + /// 获取最近添加或最近播放的文件列表。 + /// + /// 包含类型和数量的请求。 + /// API 响应。 Task GetRecentFilesAsync(RecentFilesRequest request); + /// + /// 标记文件为已播放,更新最近播放时间。 + /// + /// 包含文件 ID 的请求。 + /// API 响应。 Task MarkFilePlayedAsync(MarkFilePlayedRequest request); } } diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs index a4e5d6f..485b473 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs @@ -98,8 +98,20 @@ namespace FileShare_Services.Services.FileLibrary /// 目录浏览响应,包含子目录和文件列表。 Task BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default); + /// + /// 获取最近添加或最近播放的文件列表。 + /// + /// 筛选类型,"added" 按创建时间排序,"played" 按最近播放时间排序。 + /// 返回数量,范围 1-48,默认 12。 + /// 取消令牌。 + /// 文件记录 DTO 列表。 Task> GetRecentFilesAsync(string type, int count = 12, CancellationToken cancellationToken = default); + /// + /// 将指定文件的最近播放时间更新为当前 UTC 时间。 + /// + /// 文件记录 ID。 + /// 取消令牌。 Task MarkFilePlayedAsync(int id, CancellationToken cancellationToken = default); } } diff --git a/FileShare-Services/Services/FileLibrary/IVideoThumbnailService.cs b/FileShare-Services/Services/FileLibrary/IVideoThumbnailService.cs index 14872b5..cdd932f 100644 --- a/FileShare-Services/Services/FileLibrary/IVideoThumbnailService.cs +++ b/FileShare-Services/Services/FileLibrary/IVideoThumbnailService.cs @@ -1,11 +1,39 @@ namespace FileShare_Services.Services.FileLibrary { + /// + /// 视频缩略图生成服务接口,负责从视频文件中截取缩略图并获取时长信息。 + /// public interface IVideoThumbnailService { + /// + /// 为指定视频文件生成缩略图,如果已存在则直接返回。 + /// + /// 文件库根目录 ID。 + /// 视频文件的绝对路径。 + /// 取消令牌。 + /// 生成的缩略图信息,失败时返回 null。 Task GenerateThumbnailAsync(int libraryRootId, string videoPath, CancellationToken ct = default); + + /// + /// 将相对路径转换为缩略图存储目录下的绝对路径,并校验路径安全性。 + /// + /// 相对路径。 + /// 绝对路径。 + /// 当路径越权访问缩略图存储根目录之外的位置时抛出。 string GetAbsolutePath(string relativePath); + + /// + /// 获取视频文件的总时长。 + /// + /// 视频文件的绝对路径。 + /// 视频时长(秒),获取失败时返回 null。 double? GetVideoDuration(string videoPath); } + /// + /// 缩略图生成结果记录。 + /// + /// 缩略图相对于存储根目录的路径。 + /// 缩略图的 MIME 类型。 public sealed record GeneratedThumbnail(string RelativePath, string ContentType); } diff --git a/FileShare-Services/Services/FileLibrary/ThumbnailStorageOptions.cs b/FileShare-Services/Services/FileLibrary/ThumbnailStorageOptions.cs index 6ccf26c..c3d9474 100644 --- a/FileShare-Services/Services/FileLibrary/ThumbnailStorageOptions.cs +++ b/FileShare-Services/Services/FileLibrary/ThumbnailStorageOptions.cs @@ -1,9 +1,17 @@ namespace FileShare_Services.Services.FileLibrary { + /// + /// 缩略图存储与服务配置选项。 + /// public sealed class ThumbnailStorageOptions { + /// 缩略图文件存储根目录路径。 public string RootPath { get; set; } = Path.Combine(AppContext.BaseDirectory, "thumbnails"); + + /// ffmpeg 可执行文件路径。 public string FfmpegPath { get; set; } = Path.Combine("tools", "ffmpeg", "bin", "ffmpeg.exe"); + + /// ffprobe 可执行文件路径。 public string FfprobePath { get; set; } = Path.Combine("tools", "ffmpeg", "bin", "ffprobe.exe"); } } diff --git a/FileShare-Services/Services/FileLibrary/ThumbnailStreamService.cs b/FileShare-Services/Services/FileLibrary/ThumbnailStreamService.cs index 7fa8077..39de82e 100644 --- a/FileShare-Services/Services/FileLibrary/ThumbnailStreamService.cs +++ b/FileShare-Services/Services/FileLibrary/ThumbnailStreamService.cs @@ -4,13 +4,26 @@ using Microsoft.EntityFrameworkCore; namespace FileShare_Services.Services.FileLibrary { + /// + /// 缩略图流服务接口,根据缩略图 ID 返回文件流响应。 + /// public interface IThumbnailStreamService { + /// + /// 根据缩略图记录 ID 获取缩略图文件流响应。 + /// + /// 缩略图记录 ID。 + /// 取消令牌。 + /// 文件流响应,不存在或文件丢失时返回 null。 Task GetThumbnailAsync(int id, CancellationToken cancellationToken = default); } + /// + /// 缩略图流服务实现,从数据库查找缩略图映射并返回对应的物理文件流。 + /// public sealed class ThumbnailStreamService(AppDataContext db, IVideoThumbnailService thumbnails) : IThumbnailStreamService { + /// public async Task GetThumbnailAsync(int id, CancellationToken cancellationToken = default) { var thumbnail = await db.ManagedThumbnailMaps diff --git a/FileShare-Services/Services/FileLibrary/VideoThumbnailService.cs b/FileShare-Services/Services/FileLibrary/VideoThumbnailService.cs index d1ce836..ad4e056 100644 --- a/FileShare-Services/Services/FileLibrary/VideoThumbnailService.cs +++ b/FileShare-Services/Services/FileLibrary/VideoThumbnailService.cs @@ -4,12 +4,22 @@ using System.Text; namespace FileShare_Services.Services.FileLibrary { + /// + /// 视频缩略图服务实现,使用 ffmpeg 截取视频帧作为缩略图,使用 ffprobe 提取视频时长。 + /// public sealed class VideoThumbnailService : IVideoThumbnailService { + /// 缩略图文件存储目录的绝对路径。 private readonly string _thumbnailDir; + /// ffmpeg 可执行文件路径。 private readonly string _ffmpegPath; + /// ffprobe 可执行文件路径。 private readonly string _ffprobePath; + /// + /// 初始化视频缩略图服务,解析并验证配置路径,创建缩略图存储目录。 + /// + /// 缩略图存储配置选项。 public VideoThumbnailService(ThumbnailStorageOptions options) { _thumbnailDir = Path.IsPathRooted(options.RootPath) @@ -20,6 +30,7 @@ namespace FileShare_Services.Services.FileLibrary Directory.CreateDirectory(_thumbnailDir); } + /// public Task GenerateThumbnailAsync(int libraryRootId, string videoPath, CancellationToken ct = default) { var hash = ComputeHash(videoPath); @@ -69,6 +80,7 @@ namespace FileShare_Services.Services.FileLibrary } } + /// public string GetAbsolutePath(string relativePath) { var root = Path.GetFullPath(_thumbnailDir); @@ -82,6 +94,11 @@ namespace FileShare_Services.Services.FileLibrary return fullPath; } + /// + /// 解析可执行文件路径,支持绝对路径和相对于应用程序基目录的相对路径。 + /// + /// 配置中的原始可执行文件路径。 + /// 可用的绝对路径。 private static string ResolveExecutablePath(string executablePath) { if (Path.IsPathRooted(executablePath)) @@ -95,6 +112,7 @@ namespace FileShare_Services.Services.FileLibrary : executablePath; } + /// public double? GetVideoDuration(string videoPath) { try @@ -130,6 +148,11 @@ namespace FileShare_Services.Services.FileLibrary } } + /// + /// 计算视频文件路径的 MD5 哈希值,用于生成唯一的缩略图文件名。 + /// + /// 视频文件的绝对路径。 + /// 小写的十六进制 MD5 哈希字符串。 public string ComputeHash(string videoPath) => Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(videoPath))).ToLowerInvariant(); }