using dy.net.model.dto; using dy.net.model.entity; using dy.net.service; using dy.net.utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using dy.net.storage; namespace dy.net.Controllers { [Route("api/[controller]")] [ApiController] public class VideoController : ControllerBase { private readonly DouyinVideoService douyinVideoService; private readonly DouyinCommonService douyinCommonService; private readonly MediaStorageRouter _storageRouter; public VideoController(DouyinVideoService dyCollectVideoService, DouyinCommonService douyinCommonService, MediaStorageRouter storageRouter) { this.douyinVideoService = dyCollectVideoService; this.douyinCommonService = douyinCommonService; _storageRouter = storageRouter; } /// /// 分页查询收藏视频 /// /// [Authorize] [HttpPost("paged")] public async Task GetPagedAsync(DouyinVideoPageRequestDto dto) { var (list, totalCount) = await douyinVideoService.GetPagedAsync(dto); return ApiResult.Success(new { data = list, total = totalCount, pageIndex = dto.PageIndex, pageSize = dto.PageSize }); } /// /// 查询统计数据 /// /// [Authorize] [HttpGet("statics")] public async Task GetStaticsAsync() { return ApiResult.Success(await douyinVideoService.GetStatics()); } /// /// 播放视频 /// /// /// [AllowAnonymous] [HttpGet("play/{vid}")] public async Task StreamVideo([FromRoute] string vid) { try { var viedo = await douyinVideoService.GetById(vid); if (viedo == null) { return ApiResult.Fail($"视频不存在:{vid}"); } return await PlayVideoAsync(viedo); } catch (Exception ex) { return ApiResult.Fail($"视频加载失败:{ex.Message}"); } } /// /// 抖音视频播放接口(优化版) /// 解决内存泄漏、资源释放不及时、鲁棒性不足等问题 /// /// 视频实体(修正拼写错误:viedo -> video) /// 视频流响应 public async Task PlayVideoAsync(DouyinVideo video) { // 空值校验 if (video == null) { return ApiResult.Fail("视频信息不能为空"); } if (video.StorageType.IsRemote()) return await PlayRemoteVideoAsync(video); // 1. 获取完整物理路径并校验 string videoFullPath = video.VideoSavePath; if (string.IsNullOrWhiteSpace(videoFullPath)) { return ApiResult.Fail("视频保存路径不能为空"); } // 安全校验:防止路径遍历攻击 try { videoFullPath = Path.GetFullPath(videoFullPath); } catch { return ApiResult.Fail("视频路径格式非法"); } // 2. 验证文件是否存在 if (!System.IO.File.Exists(videoFullPath)) { return ApiResult.Fail($"视频文件不存在:{videoFullPath}"); } try { // 3. 获取文件信息(使用using确保FileInfo资源释放) var fileInfo = new FileInfo(videoFullPath); long fileSize = fileInfo.Length; // 校验空文件 if (fileSize == 0) { return ApiResult.Fail($"视频文件为空:{videoFullPath}"); } string contentType = GetContentType(videoFullPath); // 4. 处理分片请求(Range) if (Request.Headers.ContainsKey("Range")) { return await HandleRangeRequestAsync(videoFullPath, fileSize, contentType); } else { // 完整文件请求(启用分片处理,兼容前端断点续传) return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true); } } catch (UnauthorizedAccessException) { return ApiResult.Fail($"没有权限访问视频文件:{videoFullPath}"); } catch (IOException ex) { // 记录日志(建议添加日志框架,如Serilog/NLog) // _logger.LogError(ex, "读取视频文件失败:{Path}", videoFullPath); return ApiResult.Fail($"读取视频文件失败:{ex.Message}"); } catch (Exception ex) { // _logger.LogError(ex, "视频播放接口异常:{Path}", videoFullPath); return ApiResult.Fail($"服务器内部错误:{ex.Message}"); } } private async Task PlayRemoteVideoAsync(DouyinVideo video) { if (string.IsNullOrWhiteSpace(video.VideoSavePath)) return ApiResult.Fail("远端视频路径不能为空"); long? from = null; long? to = null; var range = Request.Headers.Range.ToString(); if (!string.IsNullOrWhiteSpace(range) && range.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) { var parts = range[6..].Split('-', 2); if (long.TryParse(parts[0], out var parsedFrom)) from = parsedFrom; if (parts.Length > 1 && long.TryParse(parts[1], out var parsedTo)) to = parsedTo; } await using var remote = await _storageRouter.Resolve(video.StorageType) .OpenReadAsync(video.VideoSavePath, from, to, HttpContext.RequestAborted); Response.StatusCode = remote.StatusCode; Response.ContentType = remote.ContentType ?? GetContentType(video.VideoSavePath); Response.Headers.AcceptRanges = "bytes"; if (remote.ContentLength.HasValue) Response.ContentLength = remote.ContentLength.Value; if (!string.IsNullOrWhiteSpace(remote.ContentRange)) Response.Headers.ContentRange = remote.ContentRange; await remote.Stream.CopyToAsync(Response.Body, 81920, HttpContext.RequestAborted); await Response.Body.FlushAsync(HttpContext.RequestAborted); return new EmptyResult(); } /// /// 处理分片请求(Range),异步安全处理流 /// /// 文件路径 /// 文件总大小 /// 内容类型 /// 分片响应 private async Task HandleRangeRequestAsync(string filePath, long fileSize, string contentType) { string rangeHeader = Request.Headers["Range"].ToString(); // 安全解析Range头 if (!rangeHeader.StartsWith("bytes=")) { // 416 - 请求的范围无法满足 Response.StatusCode = StatusCodes.Status416RequestedRangeNotSatisfiable; Response.Headers.Add("Content-Range", $"bytes */{fileSize}"); return new EmptyResult(); } // 解析起始位置 string[] rangeParts = rangeHeader.Split('=')[1].Split('-'); if (!long.TryParse(rangeParts[0], out long start) || start < 0 || start >= fileSize) { Response.StatusCode = StatusCodes.Status416RequestedRangeNotSatisfiable; Response.Headers.Add("Content-Range", $"bytes */{fileSize}"); return new EmptyResult(); } // 计算分片结束位置(每片2MB,可配置) long end = Math.Min(start + 1024 * 1024 * 2, fileSize - 1); long chunkSize = end - start + 1; // 设置206分片响应头 Response.StatusCode = StatusCodes.Status206PartialContent; Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileSize}"); Response.Headers.Add("Accept-Ranges", "bytes"); Response.Headers.Add("Content-Length", chunkSize.ToString()); Response.ContentType = contentType; // 核心改进:使用异步流 + using确保释放(通过管道直接写入响应流) await using var fileStream = new FileStream( filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 8192, // 增大缓冲区提升性能,减少IO次数 FileOptions.Asynchronous | FileOptions.SequentialScan); // 顺序扫描优化 // 定位到分片起始位置 fileStream.Seek(start, SeekOrigin.Begin); // 直接写入响应流,避免FileStreamResult的延迟释放问题 var buffer = new byte[8192]; long remainingBytes = chunkSize; while (remainingBytes > 0) { int bytesRead = await fileStream.ReadAsync(buffer, 0, (int)Math.Min(remainingBytes, buffer.Length)); if (bytesRead == 0) break; await Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead)); remainingBytes -= bytesRead; } await Response.Body.FlushAsync(); return new EmptyResult(); } /// /// 播放视频 /// /// /// /// [HttpGet("/share/{vid}/{k}")] [AllowAnonymous] public async Task Share([FromRoute] string vid, [FromRoute] string k) { try { var viedo = await douyinVideoService.GetById(vid); if (viedo == null) { return ApiResult.Fail($"视频不存在:{vid}"); } var expectedKey = (viedo.FileHash + viedo.AuthorId).Md5(); if (expectedKey != k) { return ApiResult.Fail($"视频地址无效"); } return await PlayVideoAsync(viedo); } catch (Exception ex) { return ApiResult.Fail($"视频加载失败:{ex.Message}"); } } /// /// 辅助方法:根据文件名获取 MIME 类型(确保前端正确识别视频格式) /// private static string GetContentType(string filename) { string extension = Path.GetExtension(filename).ToLowerInvariant(); return extension switch { ".mp4" => "video/mp4", _ => "application/octet-stream" // 默认二进制流 }; } /// /// 重新下载 /// /// /// [Authorize] [HttpPost("redown")] public async Task ReDownload(ReDownViedoDto dto) { if (dto == null) { return ApiResult.Fail("参数错误"); } else { var result = await douyinVideoService.ReDownloadViedoAsync(dto); if (result) { return ApiResult.Success(true); } else { return ApiResult.Fail("错误"); } } } /// /// 批量删除 /// /// /// [Authorize] [HttpPost("vdelete/batch")] public async Task BathRealDelete(ReDownViedoDto dto) { var result = await douyinVideoService.RealDeleteVideos(dto.Ids); if (result) { return ApiResult.Success(true); } else { return ApiResult.Fail("错误"); } } /// /// 删除视频-不再下载 /// /// /// [Authorize] [HttpGet("vdelete/{vid}")] public async Task DeleteVideo([FromRoute] string vid) { if (string.IsNullOrWhiteSpace(vid)) { return ApiResult.Fail("参数错误"); } else { var res = await douyinVideoService.RealDeleteVideos(new List { vid }); if (res) { return ApiResult.Success("删除成功"); } else { return ApiResult.Fail("删除失败"); } } } /// /// 查询已删除视频列表 /// /// [Authorize] [HttpGet("vdelete/get")] public async Task GetDeleteVideo() { return ApiResult.Success(await douyinCommonService.GetDouyinDeleteVideos()); } /// /// 根据博主id删除博主所有视频 /// /// /// [Authorize] [HttpGet("vdelete/byauthor/{uperUid}")] public async Task DeleteByAuthor([FromRoute] string uperUid) { var videos = await douyinVideoService.GetByAuthorId(uperUid); if (videos != null && videos.Any()) { var res = await douyinVideoService.RealDeleteVideos(videos.Select(x => x.Id).ToList()); if (res) { return ApiResult.Success("删除成功"); } else { return ApiResult.Fail("删除失败"); } } return ApiResult.Fail("未找到该博主视频"); } //private async Task<(bool flowControl, IActionResult value)> BatchDeleteVideos(List videos) //{ // return (flowControl: true, value: null); //} /// /// 查询最新N条数据 /// /// /// [HttpGet("top{top}")] public async Task GetLastSyncTop([FromRoute] int top = 5) { return ApiResult.Success(await douyinVideoService.GetLastSyncTop(top)); } /// /// 删除无效视频记录 /// /// [Authorize] [HttpGet("removeInvalid")] public async Task RemoveInvalidVideo() { var data = await douyinVideoService.DeleteInvalidVideo(); return Ok(data); } ///// ///// 所有视频重新生成nfo文件 ///// ///// //[HttpGet("renfo")] //public async Task ReCreateNfo() //{ // var videos = await douyinVideoService.GetAllAsync(); // if (videos == null || videos.Count == 0) // { // return ApiResult.Success("暂无视频数据需要生成NFO文件"); // } // _ = Task.Run(async () => // { // try // { // var totalCount = videos.Count; // foreach (var video in videos) // { // try // { // NfoFileGenerator.GenerateVideoNfoFile(video); // Serilog.Log.Debug($"刮削视频(Path:{video.VideoSavePath})生成NFO成功!"); // await Task.Delay(50); // } // catch (Exception singleEx) // { // Serilog.Log.Error($"刮削视频(Path:{video.VideoSavePath})生成NFO失败:{singleEx.Message}"); // } // } // } // catch (Exception ex) // { // Serilog.Log.Error($"NFO文件生成任务执行异常:{ex.Message}\n{ex.StackTrace}"); // } // }); // return ApiResult.Success(); //} /// /// 获取7天视频同步趋势数据(曲线图) /// /// 7天图表数据列表 [HttpGet("chart/{day}")] public async Task Chart([FromRoute] int day = 7) { try { var chartData = await douyinVideoService.GetChartData(day); return ApiResult.Success(chartData); } catch (Exception ex) { return ApiResult.Fail(ex.Message); } } [Authorize] [HttpGet("/Move")] public async Task Move() { await douyinVideoService.HandOldFolderVideos(); return ApiResult.Success(DateTime.Now); } } }