b328e82eb2
- 新增视频预览:VideoPlayer 组件、VideoController/DyCollectVideoService/Repository、VideoPageRequestDTO 对应后端改动 - 新增数据看板:workplace/statics.vue、RecordTable、相关 store/http/axios 改动 - 新增下载配置:DownloadConfigService、ConfigController、AppSet.vue、AppConfig - 其他:DyHttpClientService / 同步 Job(Collect, Favorit) / Cookie 表 / CommonService / ServiceExtension 的配套调整 - 仓库卫生:.gitignore 追加 feiniu 过滤;运维脚本归档到 docs/scripts/
347 lines
13 KiB
C#
347 lines
13 KiB
C#
using dy.net.dto;
|
||
using dy.net.service;
|
||
using dy.net.utils;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Newtonsoft.Json;
|
||
using System.Drawing.Printing;
|
||
|
||
namespace dy.net.Controllers
|
||
{
|
||
[Route("api/[controller]")]
|
||
[ApiController]
|
||
public class VideoController : ControllerBase
|
||
{
|
||
private readonly DyCollectVideoService dyCollectVideoService;
|
||
|
||
public VideoController(DyCollectVideoService dyCollectVideoService)
|
||
{
|
||
this.dyCollectVideoService = dyCollectVideoService;
|
||
}
|
||
/// <summary>
|
||
/// 分页查询收藏视频
|
||
/// </summary>
|
||
/// <param name="dto"></param>
|
||
[HttpPost("paged")]
|
||
public async Task<IActionResult> GetPagedAsync(VideoPageRequestDTO dto)
|
||
{
|
||
// 调试日志:输出接收到的排序参数
|
||
Console.WriteLine($"排序参数 - SortField: {dto.SortField}, SortOrder: {dto.SortOrder}");
|
||
|
||
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Tags, dto.Author, dto.ViedoType, dto.Dates, dto.IsDeleted, dto.SortField, dto.SortOrder);
|
||
return Ok(new
|
||
{
|
||
code = 0,
|
||
data = new
|
||
{
|
||
data = list,
|
||
total=totalCount,
|
||
pageIndex=dto.PageIndex,
|
||
pageSize=dto.PageSize
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有视频类型列表(用于下拉框)
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
[HttpGet("categories")]
|
||
public async Task<IActionResult> GetAllCategories()
|
||
{
|
||
var categories = await dyCollectVideoService.GetAllCategoriesAsync();
|
||
return Ok(new
|
||
{
|
||
code = 0,
|
||
data = categories
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询统计数据
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
[HttpGet("statics")]
|
||
public async Task<IActionResult> GetStaticsAsync()
|
||
{
|
||
var data = await dyCollectVideoService.GetStatics();
|
||
return Ok(new
|
||
{
|
||
code = 0,
|
||
data
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理数据库中文件不存在的视频记录
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
[Authorize]
|
||
[HttpPost("cleanup")]
|
||
public async Task<IActionResult> CleanupMissingFilesAsync()
|
||
{
|
||
try
|
||
{
|
||
Serilog.Log.Information("开始清理文件不存在的视频记录");
|
||
var deletedCount = await dyCollectVideoService.CleanupMissingFilesAsync();
|
||
Serilog.Log.Information($"清理完成,删除了 {deletedCount} 条记录");
|
||
return Ok(new
|
||
{
|
||
code = 0,
|
||
data = new
|
||
{
|
||
deletedCount = deletedCount,
|
||
message = $"清理完成,删除了 {deletedCount} 条文件不存在的记录"
|
||
}
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Serilog.Log.Error(ex, "清理文件不存在的记录失败");
|
||
return StatusCode(500, new { code = -1, error = "清理失败:" + ex.Message });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除指定作者的所有视频数据
|
||
/// </summary>
|
||
/// <param name="authorName">作者名称</param>
|
||
/// <returns></returns>
|
||
[Authorize]
|
||
[HttpPost("delete-author")]
|
||
public async Task<IActionResult> DeleteAuthorVideosAsync([FromBody] DeleteAuthorRequest request)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(request?.AuthorName))
|
||
{
|
||
return BadRequest(new { code = -1, error = "作者名称不能为空" });
|
||
}
|
||
|
||
Serilog.Log.Information($"开始删除作者 {request.AuthorName} 的视频数据");
|
||
var deletedCount = await dyCollectVideoService.DeleteAuthorVideosAsync(request.AuthorName);
|
||
Serilog.Log.Information($"删除完成,删除了 {deletedCount} 条记录");
|
||
|
||
return Ok(new
|
||
{
|
||
code = 0,
|
||
data = new
|
||
{
|
||
deletedCount = deletedCount,
|
||
message = $"已删除作者 {request.AuthorName} 的 {deletedCount} 条视频记录"
|
||
}
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Serilog.Log.Error(ex, $"删除作者视频失败: {request?.AuthorName}");
|
||
return StatusCode(500, new { code = -1, error = "删除失败:" + ex.Message });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除作者请求DTO
|
||
/// </summary>
|
||
public class DeleteAuthorRequest
|
||
{
|
||
public string AuthorName { get; set; }
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 获取视频流(支持Range请求,用于视频播放)
|
||
/// </summary>
|
||
/// <param name="id">视频ID</param>
|
||
/// <returns></returns>
|
||
[Authorize]
|
||
[HttpGet("stream/{id}")]
|
||
public async Task<IActionResult> GetVideoStream(string id)
|
||
{
|
||
try
|
||
{
|
||
var video = await dyCollectVideoService.GetByIdAsync(id);
|
||
if (video == null || string.IsNullOrWhiteSpace(video.VideoSavePath))
|
||
{
|
||
return NotFound(new { code = -1, error = "视频不存在" });
|
||
}
|
||
|
||
if (!System.IO.File.Exists(video.VideoSavePath))
|
||
{
|
||
return NotFound(new { code = -1, error = "视频文件不存在" });
|
||
}
|
||
|
||
var fileInfo = new FileInfo(video.VideoSavePath);
|
||
var contentType = GetContentType(video.VideoSavePath);
|
||
|
||
// 记录日志,便于调试
|
||
var rangeHeader = Request.Headers["Range"].ToString();
|
||
Serilog.Log.Debug($"视频流请求: ID={id}, 路径={video.VideoSavePath}, Content-Type={contentType}, Range={rangeHeader}");
|
||
|
||
var fileStream = new FileStream(video.VideoSavePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||
|
||
// 使用ASP.NET Core内置的Range支持
|
||
Response.Headers.Add("Accept-Ranges", "bytes");
|
||
|
||
// 添加缓存控制头,确保浏览器可以缓存视频元数据
|
||
// 但允许 Range 请求,这样浏览器可以快速开始播放
|
||
Response.Headers.Add("Cache-Control", "public, max-age=3600");
|
||
|
||
// 添加 CORS 头(如果需要)
|
||
Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||
Response.Headers.Add("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
||
Response.Headers.Add("Access-Control-Allow-Headers", "Range");
|
||
Response.Headers.Add("Access-Control-Expose-Headers", "Content-Range, Accept-Ranges, Content-Length");
|
||
|
||
return File(fileStream, contentType, enableRangeProcessing: true);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Serilog.Log.Error(ex, $"获取视频流失败: {id}");
|
||
return StatusCode(500, new { code = -1, error = "获取视频流失败" });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取视频封面图片
|
||
/// </summary>
|
||
/// <param name="id">视频ID</param>
|
||
/// <returns></returns>
|
||
[Authorize]
|
||
[HttpGet("cover/{id}")]
|
||
public async Task<IActionResult> GetVideoCover(string id)
|
||
{
|
||
try
|
||
{
|
||
var video = await dyCollectVideoService.GetByIdAsync(id);
|
||
if (video == null)
|
||
{
|
||
return GetDefaultCoverImage();
|
||
}
|
||
|
||
// 优先使用本地保存的封面
|
||
if (!string.IsNullOrWhiteSpace(video.VideoCoverSavePath) && System.IO.File.Exists(video.VideoCoverSavePath))
|
||
{
|
||
var fileInfo = new FileInfo(video.VideoCoverSavePath);
|
||
var contentType = GetImageContentType(video.VideoCoverSavePath);
|
||
var fileStream = System.IO.File.OpenRead(video.VideoCoverSavePath);
|
||
return File(fileStream, contentType);
|
||
}
|
||
|
||
// 如果没有本地封面,返回默认图片
|
||
return GetDefaultCoverImage();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Serilog.Log.Error(ex, $"获取视频封面失败: {id}");
|
||
return GetDefaultCoverImage();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回默认封面图片(SVG 占位图)
|
||
/// </summary>
|
||
private IActionResult GetDefaultCoverImage()
|
||
{
|
||
var svgContent = @"<svg width=""400"" height=""225"" xmlns=""http://www.w3.org/2000/svg"">
|
||
<rect width=""400"" height=""225"" fill=""#f0f0f0""/>
|
||
<text x=""50%"" y=""50%"" font-family=""Arial, sans-serif"" font-size=""16"" fill=""#999"" text-anchor=""middle"" dy="".3em"">暂无封面</text>
|
||
</svg>";
|
||
var svgBytes = System.Text.Encoding.UTF8.GetBytes(svgContent);
|
||
return File(svgBytes, "image/svg+xml");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据文件扩展名获取图片 Content-Type
|
||
/// </summary>
|
||
private string GetImageContentType(string fileName)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(fileName))
|
||
{
|
||
return "image/jpeg";
|
||
}
|
||
|
||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||
return extension switch
|
||
{
|
||
".jpg" or ".jpeg" => "image/jpeg",
|
||
".png" => "image/png",
|
||
".gif" => "image/gif",
|
||
".webp" => "image/webp",
|
||
".svg" => "image/svg+xml",
|
||
".bmp" => "image/bmp",
|
||
_ => "image/jpeg"
|
||
};
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 根据文件扩展名获取Content-Type
|
||
/// </summary>
|
||
private string GetContentType(string fileName)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(fileName))
|
||
{
|
||
Serilog.Log.Warning($"文件名为空,使用默认 video/mp4");
|
||
return "video/mp4"; // 抖音视频默认是 MP4
|
||
}
|
||
|
||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||
|
||
// 如果没有扩展名,尝试从文件名推断(抖音视频格式通常是 mp4)
|
||
if (string.IsNullOrEmpty(extension))
|
||
{
|
||
Serilog.Log.Warning($"文件 {fileName} 没有扩展名,使用默认 video/mp4");
|
||
return "video/mp4";
|
||
}
|
||
|
||
var contentType = extension switch
|
||
{
|
||
".mp4" => "video/mp4",
|
||
".webm" => "video/webm",
|
||
".ogg" => "video/ogg",
|
||
".mov" => "video/quicktime",
|
||
".avi" => "video/x-msvideo",
|
||
".mkv" => "video/x-matroska",
|
||
".flv" => "video/x-flv",
|
||
".wmv" => "video/x-ms-wmv",
|
||
".3gp" => "video/3gpp",
|
||
".m3u8" => "application/x-mpegURL", // HLS 流
|
||
_ => null
|
||
};
|
||
|
||
// 如果无法识别扩展名,默认使用 video/mp4(抖音视频通常是 MP4)
|
||
if (contentType == null)
|
||
{
|
||
Serilog.Log.Warning($"无法识别的视频扩展名: {extension},文件: {fileName},使用默认 video/mp4");
|
||
return "video/mp4";
|
||
}
|
||
|
||
return contentType;
|
||
}
|
||
|
||
[AllowAnonymous]
|
||
[HttpGet]
|
||
public IActionResult Index()
|
||
{
|
||
List<MyClass> myClasses = new List<MyClass>();
|
||
foreach (var drive in DriveInfo.GetDrives())
|
||
{
|
||
myClasses.Add(new MyClass { name=drive.Name, x1= drive.TotalSize, x2= drive.TotalFreeSpace });
|
||
Serilog.Log.Debug($"Drive: {drive.Name}, Total Size: {drive.TotalSize}, Free Space: {drive.TotalFreeSpace}");
|
||
}
|
||
//var disk=DiskInfoHelper.GetDockerHostTotalDiskSpaceGB();
|
||
return Ok(JsonConvert.SerializeObject(myClasses)+"\r\n"+JsonConvert.SerializeObject(myClasses.Sum(x=>x.x1)+"\r\n"+ JsonConvert.SerializeObject(myClasses.Sum(x => x.x2))));
|
||
}
|
||
|
||
|
||
public class MyClass
|
||
{
|
||
public string name { get; set; }
|
||
|
||
public long x1 { get; set; }
|
||
|
||
public long x2 { get; set; }
|
||
}
|
||
}
|
||
}
|