feat: 视频缩略图生成、最近文件面板与前端视图重构
- 新增 VideoThumbnailService,基于 ffmpeg 截取视频缩略图,ffprobe 提取时长
- 新增 ManagedThumbnailMap 模型及多数据库迁移,存储缩略图元数据
- 新增 /api/thumbnails/{id} 缩略图流端点
- 新增最近添加/最近播放 API 与前端面板,支持列表/网格双视图切换
- FileRecordDto 扩展 thumbnailUrl、videoDuration、lastPlayedAt 字段
- 前端新增文件库 Tab 导航、卡片网格视图、视频海报与时长信息栏
- 添加文件库目录不再同步全量扫描,改为后台异步自动扫描
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
<Content Include="www\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\tools\ffmpeg\bin\ffmpeg.exe" Link="tools\ffmpeg\bin\ffmpeg.exe" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\tools\ffmpeg\bin\ffprobe.exe" Link="tools\ffmpeg\bin\ffprobe.exe" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -12,6 +12,7 @@ using FileShare_Services.Services.FileLibrary;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FileShare_PC
|
||||
{
|
||||
@@ -71,9 +72,13 @@ namespace FileShare_PC
|
||||
services.AddSingleton<PcGlobalTokenService>();
|
||||
services.AddSingleton<IAuthService, PcAuthService>();
|
||||
services.AddSingleton<IPcAuthEndpointService, PcAuthEndpointService>();
|
||||
services.AddSingleton(new ThumbnailStorageOptions());
|
||||
services.AddSingleton<IVideoThumbnailService>(sp =>
|
||||
new VideoThumbnailService(sp.GetRequiredService<ThumbnailStorageOptions>()));
|
||||
services.AddScoped<IFileLibraryService, FileLibraryService>();
|
||||
services.AddScoped<IFileLibraryEndpointService, FileLibraryEndpointService>();
|
||||
services.AddScoped<IFileStreamService, FileStreamService>();
|
||||
services.AddScoped<IThumbnailStreamService, ThumbnailStreamService>();
|
||||
|
||||
// ---- 端点注册 ----
|
||||
var endpointBuilder = new ServiceEndpointBuilder();
|
||||
|
||||
@@ -554,7 +554,8 @@ namespace FileShare_PC.Views
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await TryHandleLocalMediaStreamAsync(context))
|
||||
if (await TryHandleLocalMediaStreamAsync(context)
|
||||
|| await TryHandleLocalThumbnailAsync(context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -691,6 +692,69 @@ namespace FileShare_PC.Views
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> TryHandleLocalThumbnailAsync(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
var segments = (request.Url?.AbsolutePath ?? string.Empty)
|
||||
.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
if (segments.Length != 3
|
||||
|| !string.Equals(segments[0], "api", StringComparison.OrdinalIgnoreCase)
|
||||
|| !string.Equals(segments[1], "thumbnails", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AddLocalMediaHeaders(response);
|
||||
|
||||
if (!string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
|
||||
response.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!int.TryParse(segments[2], out var id) || id <= 0)
|
||||
{
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
response.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
var thumbnailService = scope.ServiceProvider.GetService<IThumbnailStreamService>();
|
||||
var thumbnail = thumbnailService is null ? null : await thumbnailService.GetThumbnailAsync(id);
|
||||
if (thumbnail is null || !File.Exists(thumbnail.FilePath))
|
||||
{
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
response.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(thumbnail.FilePath);
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
response.ContentType = thumbnail.ContentType;
|
||||
response.ContentLength64 = fileInfo.Length;
|
||||
response.Headers["Cache-Control"] = "public, max-age=3600";
|
||||
response.Headers["Content-Disposition"] =
|
||||
$"inline; filename=\"{Uri.EscapeDataString(thumbnail.FileName)}\"";
|
||||
response.Headers["Last-Modified"] = thumbnail.LastModified.ToUniversalTime().ToString("R");
|
||||
|
||||
if (string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase)
|
||||
|| fileInfo.Length == 0)
|
||||
{
|
||||
response.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
await using var input = File.Open(thumbnail.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
await input.CopyToAsync(response.OutputStream);
|
||||
response.OutputStream.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为媒体流响应添加 CORS 和 Range 请求头,允许浏览器跨域访问和分段请求。
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user