Merge feature/merge-upstream: Modal 小屏 + 视频播放 + Docker Alpine(冲突以 feature 为准)
部署到群晖 / deploy (push) Failing after 3h9m39s
部署到群晖 / deploy (push) Failing after 3h9m39s
# Conflicts: # Dockerfile # Dockerfile-arm # dto/VideoPageRequestDTO.cs # model/AppConfig.cs # repository/DyCollectVideoRepository.cs # service/CommonService.cs # service/DyCollectVideoService.cs # service/DyHttpClientService.cs
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
using ClockSnowFlake;
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using dy.net.dto;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
@@ -17,10 +17,10 @@ namespace dy.net.Controllers
|
||||
{
|
||||
private readonly IWebHostEnvironment webHostEnvironment;
|
||||
|
||||
private readonly UserService _userService;
|
||||
public AuthController(UserService userService, IWebHostEnvironment webHostEnvironment )
|
||||
private readonly AdminUserService _userService;
|
||||
public AuthController(AdminUserService userService, IWebHostEnvironment webHostEnvironment)
|
||||
{
|
||||
_userService=userService;
|
||||
_userService = userService;
|
||||
this.webHostEnvironment = webHostEnvironment;
|
||||
}
|
||||
|
||||
@@ -35,66 +35,21 @@ namespace dy.net.Controllers
|
||||
public async Task<IActionResult> UpdatePwd(UpdatePwdRequest user)
|
||||
{
|
||||
var (code, erro) = await _userService.UpdatePwd(user);
|
||||
return Ok(new { code, erro });
|
||||
return ApiResult.Success("", erro);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// 获取头像
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetUserAvatar()
|
||||
{
|
||||
var user = await _userService.GetUser();
|
||||
return Ok(new { code = 0, error = "", data = new { user?.Avatar, user?.Id,user?.UserName } });
|
||||
return ApiResult.Success(new { user?.Avatar, user?.Id, user?.UserName });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改用户头像
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> UpdateUserAvatar(IFormFile file)
|
||||
{
|
||||
if (file != null && file.Length > 0)
|
||||
{
|
||||
long maxFileSize = 5 * 1024 * 1024; // 限制文件大小为5MB
|
||||
if (file.Length > maxFileSize)
|
||||
{
|
||||
return Ok(new { code = -1, erro = "文件最大只能上传5M" });
|
||||
}
|
||||
var fileName = $"{IdGener.GetGuid()}_{file.FileName}";
|
||||
var filePath = webHostEnvironment.IsProduction() ?
|
||||
Path.Combine(Md5Util.UPLOAD_PATH_PRO, fileName) : Path.Combine(Md5Util.UPLOAD_PATH_DEV, fileName);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
|
||||
try
|
||||
{
|
||||
// 问了节约空间,删除文件夹下的所有文件
|
||||
string[] files = Directory.GetFiles(webHostEnvironment.IsProduction() ? Md5Util.UPLOAD_PATH_PRO : Md5Util.UPLOAD_PATH_DEV);
|
||||
foreach (string mfile in files)
|
||||
{
|
||||
if (!mfile.Contains(fileName))
|
||||
System.IO.File.Delete(mfile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error($"delete file error ,{ex.Message}");
|
||||
}
|
||||
var update = await _userService.UpdateAvatar( fileName);
|
||||
|
||||
return Ok(new { code = update ? 0 : -1, erro = update ? "" : "上传失败", data = update ? fileName : "" });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(new { code = -1, erro = "空文件" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -108,7 +63,7 @@ namespace dy.net.Controllers
|
||||
{
|
||||
if (loginUserInfo == null)
|
||||
{
|
||||
return Ok(new { code = -1, erro = "参数不能为空" });
|
||||
return ApiResult.Fail("参数不能为空");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -116,7 +71,7 @@ namespace dy.net.Controllers
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Ok(new { code = -1, erro = "用户名或密码不正确" });
|
||||
return ApiResult.Fail("用户名或密码不正确");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -124,30 +79,29 @@ namespace dy.net.Controllers
|
||||
{
|
||||
|
||||
var tokenString = GenerateJwtToken(user.UserName);
|
||||
|
||||
return Ok(new { code = 0, erro = "", token = tokenString, expires = 24 * 60 * 60 * 1000 });
|
||||
return Ok(new { code = 0, erro = "", token = tokenString, expires = 24 * 60 * 60 * 1000, data = user.UserName });
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(new { code = -1, erro = "用户名或密码不正确" });
|
||||
return ApiResult.Fail("用户名或密码不正确");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string GenerateJwtToken(string username)
|
||||
private static string GenerateJwtToken(string username)
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, username),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
new Claim(ClaimTypes.Name, username),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
var k = Md5Util.JWT_TOKEN_KEY;
|
||||
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(k));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var expires = DateTime.Now.AddDays(1);
|
||||
var expires = DateTime.Now.AddDays(7);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: IdGener.GetLong().ToString(),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class CateController : ControllerBase
|
||||
{
|
||||
private readonly DouyinCollectCateService _douyinCollectCateService;
|
||||
private readonly DouyinQuartzJobService _douyinQuartzJobService;
|
||||
|
||||
public CateController(DouyinCollectCateService douyinCollectCateService, DouyinQuartzJobService douyinQuartzJobService)
|
||||
{
|
||||
this._douyinCollectCateService = douyinCollectCateService;
|
||||
_douyinQuartzJobService = douyinQuartzJobService;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询
|
||||
/// </summary>
|
||||
/// <returns>分页结果</returns>
|
||||
[HttpPost("paged")]
|
||||
public async Task<IActionResult> GetPagedAsync(
|
||||
DouyinCollectCateRequestDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.cookieId))
|
||||
{
|
||||
return ApiResult.Success(new { });
|
||||
}
|
||||
var (list, totalCount) = await _douyinCollectCateService.GetPagedAsync(dto);
|
||||
|
||||
return ApiResult.Success(new
|
||||
{
|
||||
data = list,
|
||||
total = totalCount,
|
||||
pageIndex = dto.PageIndex,
|
||||
pageSize = dto.PageSize
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改关注同步状态
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("BatchSave")]
|
||||
public async Task<IActionResult> BatchSave(List<DouyinCollectCateSwitchDto> dto)
|
||||
{
|
||||
|
||||
if (dto.Any(x => !DouyinFileNameHelper.IsValidWithoutSpecialChars(x.SaveFolder)))
|
||||
{
|
||||
return ApiResult.Fail("有部分文件名不符合要求,请检查");
|
||||
}
|
||||
|
||||
var result = await _douyinCollectCateService.BatchSwitchSync(dto);
|
||||
return ApiResult.SuccOrFail(result, result);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+398
-83
@@ -1,110 +1,306 @@
|
||||
using ClockSnowFlake;
|
||||
using dy.net.dto;
|
||||
using dy.net.model;
|
||||
using dy.net.extension;
|
||||
using dy.net.model.dto;
|
||||
using dy.net.model.entity;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using jzc.http.lib;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using static Dm.net.buffer.ByteArrayBuffer;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ConfigController : ControllerBase
|
||||
{
|
||||
private readonly DyCookieService dyCookieService;
|
||||
private readonly DouyinCookieService dyCookieService;
|
||||
|
||||
private readonly CommonService commonService;
|
||||
private readonly QuartzJobService quartzJobService;
|
||||
private readonly DyQrCodeService _qrCodeService;
|
||||
private readonly DouyinCommonService commonService;
|
||||
private readonly DouyinQuartzJobService quartzJobService;
|
||||
private readonly DouyinFollowService douyinFollowService;
|
||||
private readonly DouyinCookieService douyinCookieService;
|
||||
private readonly DouyinHttpClientService httpClientService;
|
||||
|
||||
public ConfigController(DyCookieService dyCookieService, CommonService commonService, QuartzJobService quartzJobService, DyQrCodeService qrCodeService)
|
||||
public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService, DouyinQuartzJobService quartzJobService, DouyinFollowService douyinFollowService, DouyinCookieService douyinCookieService, DouyinHttpClientService httpClientService)
|
||||
{
|
||||
this.dyCookieService = dyCookieService;
|
||||
this.commonService = commonService;
|
||||
this.quartzJobService = quartzJobService;
|
||||
_qrCodeService = qrCodeService;
|
||||
this.douyinFollowService = douyinFollowService;
|
||||
this.douyinCookieService = douyinCookieService;
|
||||
this.httpClientService = httpClientService;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 导出配置
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("exportConf")]
|
||||
public async Task<IActionResult> ExportConf()
|
||||
{
|
||||
|
||||
// 1. 组装导出数据
|
||||
var dto = new AppConfigImportDto()
|
||||
{
|
||||
follows = await douyinFollowService.GetHandFollows(),
|
||||
conf = commonService.GetConfig(),
|
||||
cookies = await douyinCookieService.GetAllAsync()
|
||||
};
|
||||
|
||||
return ApiResult.Success(dto);
|
||||
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("importConf")]
|
||||
public async Task<IActionResult> ImportConf(AppConfigImportDto dto)
|
||||
{
|
||||
if (dto == null)
|
||||
return ApiResult.Fail("json数据为空");
|
||||
|
||||
await HandleFollowsImport(dto.follows);
|
||||
await HandleConfigImport(dto.conf);
|
||||
await HandleCookiesImport(dto.cookies);
|
||||
|
||||
return ApiResult.Success();
|
||||
}
|
||||
|
||||
private async Task HandleFollowsImport(List<DouyinFollowed> follows)
|
||||
{
|
||||
if (follows?.Count > 0)
|
||||
{
|
||||
var added = await douyinFollowService.AddHandFollows(follows);
|
||||
if (added)
|
||||
Serilog.Log.Debug("关注列表导入成功");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleConfigImport(AppConfig conf)
|
||||
{
|
||||
if (conf == null) return;
|
||||
|
||||
if (conf.BatchCount > 30)
|
||||
{
|
||||
Serilog.Log.Debug("对不起,为了项目能长久稳定运行,还是最大不要超过30吧。。。");
|
||||
conf.BatchCount = 30;
|
||||
}
|
||||
|
||||
var updated = await commonService.UpdateConfig(conf);
|
||||
if (updated)
|
||||
Serilog.Log.Debug("系统配置导入成功");
|
||||
}
|
||||
|
||||
private async Task HandleCookiesImport(List<DouyinCookie> cookies)
|
||||
{
|
||||
if (cookies?.Count > 0)
|
||||
{
|
||||
var imported = await douyinCookieService.ImportCookies(cookies);
|
||||
if (imported)
|
||||
Serilog.Log.Debug("抖音Cookie配置导入成功");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 分页查询
|
||||
/// </summary>
|
||||
/// <returns>分页结果(视频列表和总数)</returns>
|
||||
/// <returns>分页结果</returns>
|
||||
[HttpPost("paged")]
|
||||
public async Task<IActionResult> GetPagedAsync(
|
||||
PageRequestDto dto)
|
||||
{
|
||||
var (list, totalCount) = await dyCookieService.GetPagedAsync(dto.PageIndex, dto.PageSize);
|
||||
return Ok(new
|
||||
|
||||
return ApiResult.Success(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
data = list,
|
||||
total=totalCount,
|
||||
pageIndex= dto.PageIndex,
|
||||
pageSize= dto.PageSize
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
/// <summary>
|
||||
/// 新增用户Cookie
|
||||
/// </summary>
|
||||
[HttpPost("add")]
|
||||
public async Task<IActionResult> AddAsync([FromBody] DyUserCookies dyUserCookies)
|
||||
{
|
||||
if(dyUserCookies.UpSecUserIdsJson!=null)
|
||||
{
|
||||
dyUserCookies.UpSecUserIds = JsonConvert.SerializeObject( dyUserCookies.UpSecUserIdsJson);
|
||||
}
|
||||
var result = await dyCookieService.Add(dyUserCookies);
|
||||
if (result)
|
||||
{
|
||||
await ReStartJob();
|
||||
return Ok(new { code = 0 });
|
||||
}
|
||||
return BadRequest(new { code=-1, message = "添加失败" });
|
||||
data = list,
|
||||
total = totalCount,
|
||||
pageIndex = dto.PageIndex,
|
||||
pageSize = dto.PageSize
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 查询所有用户Cookie
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("list")]
|
||||
public async Task<IActionResult> GetAllList()
|
||||
{
|
||||
var follows = await douyinFollowService.GetGroupByCookieAsync();
|
||||
return ApiResult.Success(follows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否已经初始化了
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("isInit")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> IsInit()
|
||||
{
|
||||
var init = await dyCookieService.IsInit();
|
||||
return ApiResult.Success(init);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 配合工具自动设置cookie
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("FastResetCookie")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> FastResetCookie([FromBody] DouyinCookieResetDto dto)
|
||||
{
|
||||
var cookieValid = await httpClientService.CheckCookie(new DouyinCookie { Cookies = dto.cookie });
|
||||
if (!cookieValid)
|
||||
return ApiResult.Fail("Cookie无效或已过期,请按照文档提示重新获取有效Cookie,不要使用插件获取cookie");
|
||||
var result = await dyCookieService.FastResetCookie(dto.id, dto.cookie);
|
||||
|
||||
return result ? ApiResult.Success() : ApiResult.Fail("cookie设置失败");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配合工具自动设置cookie,获取当前设置的所有Cookie
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Cookies")]
|
||||
public async Task<IActionResult> GetAllCookies()
|
||||
{
|
||||
var cookies = await dyCookieService.GetAllAsync();
|
||||
if (cookies != null)
|
||||
return ApiResult.Success(cookies.Select(x => new { id= x.Id,name= x.UserName, status=x.StatusMsg }));
|
||||
return ApiResult.Success();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 非docker初始化
|
||||
/// </summary>
|
||||
[HttpPost("deskinit")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> DeskInitAsync([FromBody] DouyinCookie dyUserCookies)
|
||||
{
|
||||
// 1. 基础赋值
|
||||
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||
|
||||
// 2. 路径权限校验
|
||||
var (Success, Message) = ValidatePaths(dyUserCookies);
|
||||
if (!Success)
|
||||
return ApiResult.Fail(Message);
|
||||
|
||||
RemoveCookieLineString(dyUserCookies);
|
||||
|
||||
// 3. Cookie 有效性校验
|
||||
var cookieValid = await httpClientService.CheckCookie(dyUserCookies);
|
||||
if (!cookieValid)
|
||||
return ApiResult.Fail("Cookie无效或已过期,请按照文档提示重新获取有效Cookie,不要使用插件获取cookie");
|
||||
|
||||
dyUserCookies.StatusCode = 0;
|
||||
dyUserCookies.StatusMsg = "正常";
|
||||
// 4. 保存到数据库
|
||||
var saved = await dyCookieService.Add(dyUserCookies);
|
||||
return saved ? ApiResult.Success() : ApiResult.Fail("添加失败");
|
||||
}
|
||||
|
||||
private static (bool Success, string Message) ValidatePaths(DouyinCookie cookie)
|
||||
{
|
||||
var pathsToCheck = new Dictionary<string, string>
|
||||
{
|
||||
{ "收藏存储路径", cookie.SavePath },
|
||||
{ "喜欢视频存储路径", cookie.FavSavePath },
|
||||
{ "上传视频存储路径", cookie.UpSavePath },
|
||||
// { "图片存储路径", cookie.ImgSavePath } // 可随时启用
|
||||
};
|
||||
|
||||
foreach (var (label, path) in pathsToCheck)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
if (!DouyinFileUtils.HasDirectoryReadWritePermission(path))
|
||||
{
|
||||
return (false, $"请在飞牛应用设置里面将 {path} 添加读写权限({label})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cookie.SavePath))
|
||||
{
|
||||
return (false, "收藏存储路径不能为空");
|
||||
}
|
||||
|
||||
return (true, string.Empty);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 快速开启或停止
|
||||
/// </summary>
|
||||
[HttpPost("switch")]
|
||||
public async Task<IActionResult> SwitchAsync([FromBody] DouyinCookieSwitchDto dto)
|
||||
{
|
||||
var result = await dyCookieService.Switch(dto);
|
||||
if (result)
|
||||
{
|
||||
ReStartJob();
|
||||
return ApiResult.Success();
|
||||
}
|
||||
return ApiResult.Fail("添加失败");
|
||||
}
|
||||
/// <summary>
|
||||
/// 更新用户Cookie
|
||||
/// </summary>
|
||||
[HttpPost("update")]
|
||||
public async Task<IActionResult> UpdateAsync([FromBody] DyUserCookies dyUserCookies)
|
||||
public async Task<IActionResult> AddOrUpdateAsync([FromBody] DouyinCookie dyUserCookies)
|
||||
{
|
||||
RemoveCookieLineString(dyUserCookies);
|
||||
|
||||
if (dyUserCookies.UpSecUserIdsJson != null )
|
||||
var checkCk = await httpClientService.CheckCookie(dyUserCookies);
|
||||
if (!checkCk)
|
||||
{
|
||||
dyUserCookies.UpSecUserIds = JsonConvert.SerializeObject(dyUserCookies.UpSecUserIdsJson);
|
||||
return ApiResult.Fail("Cookie无效或已过期,请按照文档提示重新获取有效Cookie,不要使用插件获取cookie");
|
||||
}
|
||||
|
||||
dyUserCookies.StatusCode = 0;
|
||||
dyUserCookies.StatusMsg = "正常";
|
||||
if (dyUserCookies.Id == "0")
|
||||
{
|
||||
dyUserCookies.Id=IdGener.GetLong().ToString();
|
||||
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||
var result = await dyCookieService.Add(dyUserCookies);
|
||||
if (result)
|
||||
{
|
||||
await ReStartJob();
|
||||
return Ok(new { code = 0 });
|
||||
ReStartJob();
|
||||
return ApiResult.Success();
|
||||
}
|
||||
return BadRequest(new { code = -1, message = "添加失败" });
|
||||
return ApiResult.Fail("添加失败");
|
||||
}
|
||||
else {
|
||||
var result = await dyCookieService.UpdateAsync(dyUserCookies);
|
||||
else
|
||||
{
|
||||
var result = await dyCookieService.UpdateCookieAsync(dyUserCookies);
|
||||
if (result)
|
||||
{
|
||||
await ReStartJob();
|
||||
return Ok(new { code = 0 });
|
||||
ReStartJob();
|
||||
return ApiResult.Success();
|
||||
}
|
||||
return BadRequest(new { code = -1, message = "更新失败" });
|
||||
return ApiResult.Fail("更新失败");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void RemoveCookieLineString(DouyinCookie dyUserCookies)
|
||||
{
|
||||
if (dyUserCookies != null && !string.IsNullOrWhiteSpace(dyUserCookies.Cookies))
|
||||
{
|
||||
var s = dyUserCookies.Cookies.Replace("\\r\\n", "\r\n");
|
||||
var ss = s.Trim(new char[] { '\r', '\n' });
|
||||
dyUserCookies.Cookies = ss;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,28 +309,36 @@ namespace dy.net.Controllers
|
||||
[HttpGet("delete")]
|
||||
public async Task<IActionResult> DeleteAsync(string id)
|
||||
{
|
||||
var count = await dyCookieService.DeleteByIdsAsync(new List<string> { id});
|
||||
if (count > 0) {
|
||||
await ReStartJob();
|
||||
var count = await dyCookieService.DeleteByIdsAsync(new List<string> { id });
|
||||
if (count > 0)
|
||||
{
|
||||
ReStartJob();
|
||||
}
|
||||
return Ok(new { code = 0, deletedCount = count });
|
||||
return ApiResult.Success(count);
|
||||
}
|
||||
|
||||
[HttpGet("GetConfig")]
|
||||
public IActionResult GetConfig()
|
||||
{
|
||||
var data = commonService.GetConfig();
|
||||
return Ok(new { code = 0, data = data });
|
||||
var data = commonService.GetConfig();
|
||||
return ApiResult.Success(data);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("UpdateConfig")]
|
||||
public async Task<IActionResult> UpdateConfig(AppConfig config)
|
||||
{
|
||||
var data = await commonService.UpdateConfig(config);
|
||||
if (data) {
|
||||
await ReStartJob();
|
||||
var update = await commonService.UpdateConfig(config);
|
||||
if (update)
|
||||
{
|
||||
if (config.OnlySyncNew)
|
||||
{
|
||||
Serilog.Log.Debug("仅同步新视频配置已生效,后续所有类型的视频同步将只会读取最近一页约20条数据");
|
||||
}
|
||||
|
||||
ReStartJob();
|
||||
}
|
||||
return Ok(new { code = 0, data = data });
|
||||
return ApiResult.Success(update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -142,12 +346,13 @@ namespace dy.net.Controllers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("ExecuteJobNow")]
|
||||
[Authorize]
|
||||
//[Authorize]
|
||||
public async Task<IActionResult> ExecuteJobNow()
|
||||
{
|
||||
var config = commonService.GetConfig();
|
||||
await quartzJobService.StartJob(config.Cron);
|
||||
return Ok(new { code = 0 , error = "" });
|
||||
var config = commonService.GetConfig();
|
||||
if (config != null)
|
||||
await quartzJobService.InitOrReStartAllJobs(config.Cron.ToString());
|
||||
return ApiResult.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -278,22 +483,132 @@ namespace dy.net.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReStartJob() {
|
||||
var config= commonService.GetConfig();
|
||||
|
||||
//var cookies = await dyCookieService.GetAllCookies();
|
||||
//重置同步状态
|
||||
//foreach (var cookie in cookies)
|
||||
//{
|
||||
// cookie.CollHasSyncd = 0;
|
||||
// cookie.FavHasSyncd = 0;
|
||||
// cookie.UperSyncd = 0;
|
||||
// await dyCookieService.UpdateAsync(cookie);
|
||||
//}
|
||||
|
||||
if (config!=null)
|
||||
quartzJobService.StartJob(config.Cron);
|
||||
private void ReStartJob()
|
||||
{
|
||||
var config = commonService.GetConfig();
|
||||
if (config != null)
|
||||
quartzJobService.InitOrReStartAllJobs(config.Cron.ToString());
|
||||
//避免前端等待
|
||||
}
|
||||
/// <summary>
|
||||
/// 镜像标签
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("mytag")]
|
||||
public async Task<IActionResult> GetMyTag()
|
||||
{
|
||||
var deploy = Appsettings.Get("deploy");
|
||||
var tag = Appsettings.Get("tagName");
|
||||
tag = deploy == "fn" ? "fn_" + Appsettings.Get("fnVersion") : tag;
|
||||
deploy = deploy == "fn" ? "fnos" : "docker";
|
||||
return ApiResult.Success(new { tag, deploy });
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("checktag")]
|
||||
public async Task<IActionResult> CheckTag()
|
||||
{
|
||||
|
||||
var deploy = Appsettings.Get("deploy");
|
||||
if (string.IsNullOrWhiteSpace(deploy))
|
||||
{
|
||||
return await GetDockerTagVersions();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (deploy == "fn")//飞牛
|
||||
{
|
||||
return ApiResult.Success(new List<string> { "fn_" + Appsettings.Get("fnVersion") });
|
||||
}
|
||||
else
|
||||
{
|
||||
return await GetDockerTagVersions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static async Task<IActionResult> GetDockerTagVersions()
|
||||
{
|
||||
var data = await JianZhiChuHttpHelper.GetTenImage(Appsettings.Get("tagName"));
|
||||
if (data.IsSuccessStatusCode)
|
||||
{
|
||||
var content = await data.Content.ReadAsStringAsync();
|
||||
|
||||
var tagData = JsonConvert.DeserializeObject<DouyinApiResponse<List<string>>>(content);
|
||||
if (tagData != null && tagData.Data != null && tagData.Data.Count > 0)
|
||||
return ApiResult.Success(tagData.Data);
|
||||
return ApiResult.Fail();
|
||||
}
|
||||
else
|
||||
{
|
||||
return ApiResult.Fail("请求失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询mp3目录下有没有音频文件
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("mp3List")]
|
||||
public async Task<IActionResult> GetExistMps()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "mp3");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ServiceExtension.FnDataFolder))
|
||||
{
|
||||
path = ServiceExtension.FnDataFolder;
|
||||
}
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
var allowedExtensions = new HashSet<string> { ".mp3", ".wav" };
|
||||
|
||||
var customMusics = Directory.GetFiles(path)
|
||||
.Where(filePath =>
|
||||
allowedExtensions.Contains(Path.GetExtension(filePath).ToLowerInvariant()) &&
|
||||
Path.GetFileNameWithoutExtension(filePath) != "silent_10")
|
||||
.ToList();
|
||||
|
||||
var fileNames = customMusics.Select(f => new { filename = Path.GetFileName(f) }).Where(x => x.filename != "silent_10.mp3").ToList();
|
||||
return ApiResult.Success(fileNames);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ApiResult.Fail("没有找到默认音频文件");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放音频流
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("getmp3")]
|
||||
public async Task<IActionResult> GetMp3([FromQuery] string name)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "mp3", name);
|
||||
if (!string.IsNullOrWhiteSpace(ServiceExtension.FnDataFolder))
|
||||
{
|
||||
path = Path.Combine(ServiceExtension.FnDataFolder, name);
|
||||
}
|
||||
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
//返回mp3文件流
|
||||
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
return new FileStreamResult(fileStream, "audio/mpeg")
|
||||
{
|
||||
FileDownloadName = name
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return ApiResult.Fail("文件不存在");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
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;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class FollowController : ControllerBase
|
||||
{
|
||||
private readonly DouyinFollowService _douyinFollowService;
|
||||
private readonly DouyinQuartzJobService _douyinQuartzJobService;
|
||||
|
||||
public FollowController(DouyinFollowService douyinFollowService, DouyinQuartzJobService douyinQuartzJobService)
|
||||
{
|
||||
this._douyinFollowService = douyinFollowService;
|
||||
_douyinQuartzJobService = douyinQuartzJobService;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询
|
||||
/// </summary>
|
||||
/// <returns>分页结果</returns>
|
||||
[HttpPost("paged")]
|
||||
public async Task<IActionResult> GetPagedAsync(
|
||||
FollowRequestDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.MySelfId))
|
||||
{
|
||||
return ApiResult.Success(new { });
|
||||
}
|
||||
var (list, totalCount) = await _douyinFollowService.GetPagedAsync(dto);
|
||||
|
||||
return ApiResult.Success(new
|
||||
{
|
||||
data = list,
|
||||
total = totalCount,
|
||||
pageIndex = dto.PageIndex,
|
||||
pageSize = dto.PageSize
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// 重新同步-单次
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("sync")]
|
||||
public async Task<IActionResult> SyncFollowList()
|
||||
{
|
||||
//后台异步
|
||||
_douyinQuartzJobService.StartFollowJobOnceAsync();
|
||||
await Task.Delay(1000);
|
||||
return ApiResult.Success();
|
||||
}
|
||||
[HttpPost("add")]
|
||||
public async Task<IActionResult> AddFollow(DouyinFollowed followed)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(followed.mySelfId))
|
||||
{
|
||||
return ApiResult.Fail("请先配置抖音授权信息,抖音授权配置里面填写你的uid");
|
||||
}
|
||||
var res = await _douyinFollowService.AddAsync(followed);
|
||||
return ApiResult.SuccOrFail(res, "", res ? "" : "添加失败,或者已存在相同secuid和uid");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改关注同步状态
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("openOrCloseSync")]
|
||||
public async Task<IActionResult> OpenOrCloseSync(FollowUpdateDto dto)
|
||||
{
|
||||
|
||||
if (dto.OpenSync)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(dto.SavePath))
|
||||
{
|
||||
if (!DouyinFileNameHelper.IsValidWithoutSpecialChars(dto.SavePath))
|
||||
{
|
||||
return ApiResult.Fail("请输入有效文件夹名称(字母数字中文简体)");
|
||||
}
|
||||
|
||||
if (dto.SavePath.Length > 20)
|
||||
{
|
||||
return ApiResult.Fail("请输入有效文件夹名称(最长20)");
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = await _douyinFollowService.OpenOrCloseSync(dto);
|
||||
return ApiResult.SuccOrFail(result, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改关注全量同步状态
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("openOrCloseFullSync")]
|
||||
public async Task<IActionResult> OpenOrCloseFullSync(FollowUpdateDto dto)
|
||||
{
|
||||
return await OpenOrCloseSync(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除关注对象
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("delete")]
|
||||
public async Task<IActionResult> DeleteFollow(FollowUpdateDto dto)
|
||||
{
|
||||
var result = await _douyinFollowService.DeleteFollow(dto);
|
||||
return ApiResult.SuccOrFail(result, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
@@ -9,19 +9,21 @@ namespace dy.net.Controllers
|
||||
public class LogsController : ControllerBase
|
||||
{
|
||||
private readonly IWebHostEnvironment webHostEnvironment;
|
||||
private readonly LogInfoService logInfoService;
|
||||
|
||||
public LogsController(IWebHostEnvironment webHostEnvironment)
|
||||
public LogsController(IWebHostEnvironment webHostEnvironment, LogInfoService logInfoService)
|
||||
{
|
||||
this.webHostEnvironment = webHostEnvironment;
|
||||
this.logInfoService = logInfoService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetLog(string type, string date)
|
||||
[HttpGet("/api/logs/GetLog/{type}/{date}")]
|
||||
public async Task<IActionResult> GetLog([FromRoute] string type, [FromRoute] string date)
|
||||
{
|
||||
var filePath = Path.Combine(webHostEnvironment.IsDevelopment() ? Directory.GetCurrentDirectory() : AppDomain.CurrentDomain.BaseDirectory, "logs", $"log-{type}-{date}.txt");
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
{
|
||||
var msg = $"Log file log-{type}-{date}.txt not found.";
|
||||
var msg = $"{date},没有发现{type}的日志";
|
||||
//Serilog.Log.Error(msg);
|
||||
return Ok(msg);
|
||||
}
|
||||
@@ -35,5 +37,57 @@ namespace dy.net.Controllers
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近10天的日志文件列表
|
||||
/// </summary>
|
||||
[HttpGet("/api/logs/list")]
|
||||
public IActionResult GetLogFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
var _logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||
var files = logInfoService.GetLogFiles(_logDirectory);
|
||||
return ApiResult.Success(files);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiResult.Fail("获取日志列表失败," + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取日志文件内容流
|
||||
/// </summary>
|
||||
[HttpGet("/api/logs/content")]
|
||||
public IActionResult GetLogContent([FromQuery] LogContentRequest request)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
try
|
||||
{
|
||||
var _logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||
var stream = logInfoService.GetLogFileStream(_logDirectory, request.Type, request.Date);
|
||||
// 返回文件流,指定MIME类型为文本
|
||||
return File(stream, "text/plain", $"log-{request.Type}-{request.Date}.txt");
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { message = ex.Message });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { message = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { message = "读取日志文件失败", error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+382
-239
@@ -1,10 +1,9 @@
|
||||
using dy.net.dto;
|
||||
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 Newtonsoft.Json;
|
||||
using System.Drawing.Printing;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
@@ -12,33 +11,29 @@ namespace dy.net.Controllers
|
||||
[ApiController]
|
||||
public class VideoController : ControllerBase
|
||||
{
|
||||
private readonly DyCollectVideoService dyCollectVideoService;
|
||||
private readonly DouyinVideoService douyinVideoService;
|
||||
private readonly DouyinCommonService douyinCommonService;
|
||||
|
||||
public VideoController(DyCollectVideoService dyCollectVideoService)
|
||||
public VideoController(DouyinVideoService dyCollectVideoService, DouyinCommonService douyinCommonService)
|
||||
{
|
||||
this.dyCollectVideoService = dyCollectVideoService;
|
||||
this.douyinVideoService = dyCollectVideoService;
|
||||
this.douyinCommonService = douyinCommonService;
|
||||
}
|
||||
/// <summary>
|
||||
/// 分页查询收藏视频
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
[Authorize]
|
||||
[HttpPost("paged")]
|
||||
public async Task<IActionResult> GetPagedAsync(VideoPageRequestDTO dto)
|
||||
public async Task<IActionResult> GetPagedAsync(DouyinVideoPageRequestDto 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
|
||||
var (list, totalCount) = await douyinVideoService.GetPagedAsync(dto);
|
||||
return ApiResult.Success(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
data = list,
|
||||
total=totalCount,
|
||||
pageIndex=dto.PageIndex,
|
||||
pageSize=dto.PageSize
|
||||
}
|
||||
data = list,
|
||||
total = totalCount,
|
||||
pageIndex = dto.PageIndex,
|
||||
pageSize = dto.PageSize
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,286 +56,434 @@ namespace dy.net.Controllers
|
||||
/// 查询统计数据
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("statics")]
|
||||
public async Task<IActionResult> GetStaticsAsync()
|
||||
{
|
||||
var data = await dyCollectVideoService.GetStatics();
|
||||
return Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data
|
||||
});
|
||||
return ApiResult.Success(await douyinVideoService.GetStatics());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理数据库中文件不存在的视频记录
|
||||
/// 播放视频
|
||||
/// </summary>
|
||||
/// <param name="vid"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("cleanup")]
|
||||
public async Task<IActionResult> CleanupMissingFilesAsync()
|
||||
[AllowAnonymous]
|
||||
[HttpGet("play/{vid}")]
|
||||
public async Task<IActionResult> StreamVideo([FromRoute] string vid)
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Information("开始清理文件不存在的视频记录");
|
||||
var deletedCount = await dyCollectVideoService.CleanupMissingFilesAsync();
|
||||
Serilog.Log.Information($"清理完成,删除了 {deletedCount} 条记录");
|
||||
return Ok(new
|
||||
var viedo = await douyinVideoService.GetById(vid);
|
||||
|
||||
if (viedo == null)
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
deletedCount = deletedCount,
|
||||
message = $"清理完成,删除了 {deletedCount} 条文件不存在的记录"
|
||||
}
|
||||
});
|
||||
return ApiResult.Fail($"视频不存在:{vid}");
|
||||
}
|
||||
return await PlayVideoAsync(viedo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "清理文件不存在的记录失败");
|
||||
return StatusCode(500, new { code = -1, error = "清理失败:" + ex.Message });
|
||||
return ApiResult.Fail($"视频加载失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 抖音视频播放接口(优化版)
|
||||
/// 解决内存泄漏、资源释放不及时、鲁棒性不足等问题
|
||||
/// </summary>
|
||||
/// <param name="video">视频实体(修正拼写错误:viedo -> video)</param>
|
||||
/// <returns>视频流响应</returns>
|
||||
public async Task<IActionResult> PlayVideoAsync(DouyinVideo video)
|
||||
{
|
||||
// 空值校验
|
||||
if (video == null)
|
||||
{
|
||||
return ApiResult.Fail("视频信息不能为空");
|
||||
}
|
||||
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定作者的所有视频数据
|
||||
/// 处理分片请求(Range),异步安全处理流
|
||||
/// </summary>
|
||||
/// <param name="authorName">作者名称</param>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="fileSize">文件总大小</param>
|
||||
/// <param name="contentType">内容类型</param>
|
||||
/// <returns>分片响应</returns>
|
||||
private async Task<IActionResult> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放视频
|
||||
/// </summary>
|
||||
/// <param name="vid"></param>
|
||||
/// <param name="k"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("delete-author")]
|
||||
public async Task<IActionResult> DeleteAuthorVideosAsync([FromBody] DeleteAuthorRequest request)
|
||||
[HttpGet("/share/{vid}/{k}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Share([FromRoute] string vid, [FromRoute] string k)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request?.AuthorName))
|
||||
var viedo = await douyinVideoService.GetById(vid);
|
||||
|
||||
if (viedo == null)
|
||||
{
|
||||
return BadRequest(new { code = -1, error = "作者名称不能为空" });
|
||||
return ApiResult.Fail($"视频不存在:{vid}");
|
||||
}
|
||||
|
||||
Serilog.Log.Information($"开始删除作者 {request.AuthorName} 的视频数据");
|
||||
var deletedCount = await dyCollectVideoService.DeleteAuthorVideosAsync(request.AuthorName);
|
||||
Serilog.Log.Information($"删除完成,删除了 {deletedCount} 条记录");
|
||||
|
||||
return Ok(new
|
||||
var expectedKey = (viedo.FileHash + viedo.AuthorId).Md5();
|
||||
if (expectedKey != k)
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
deletedCount = deletedCount,
|
||||
message = $"已删除作者 {request.AuthorName} 的 {deletedCount} 条视频记录"
|
||||
}
|
||||
});
|
||||
return ApiResult.Fail($"视频地址无效");
|
||||
}
|
||||
|
||||
return await PlayVideoAsync(viedo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"删除作者视频失败: {request?.AuthorName}");
|
||||
return StatusCode(500, new { code = -1, error = "删除失败:" + ex.Message });
|
||||
return ApiResult.Fail($"视频加载失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除作者请求DTO
|
||||
/// 辅助方法:根据文件名获取 MIME 类型(确保前端正确识别视频格式)
|
||||
/// </summary>
|
||||
public class DeleteAuthorRequest
|
||||
private static string GetContentType(string filename)
|
||||
{
|
||||
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();
|
||||
string 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"
|
||||
".mp4" => "video/mp4",
|
||||
_ => "application/octet-stream" // 默认二进制流
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件扩展名获取Content-Type
|
||||
/// 重新下载
|
||||
/// </summary>
|
||||
private string GetContentType(string fileName)
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("redown")]
|
||||
public async Task<IActionResult> ReDownload(ReDownViedoDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
if (dto == null)
|
||||
{
|
||||
Serilog.Log.Warning($"文件名为空,使用默认 video/mp4");
|
||||
return "video/mp4"; // 抖音视频默认是 MP4
|
||||
return ApiResult.Fail("参数错误");
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
|
||||
// 如果没有扩展名,尝试从文件名推断(抖音视频格式通常是 mp4)
|
||||
if (string.IsNullOrEmpty(extension))
|
||||
else
|
||||
{
|
||||
Serilog.Log.Warning($"文件 {fileName} 没有扩展名,使用默认 video/mp4");
|
||||
return "video/mp4";
|
||||
var result = await douyinVideoService.ReDownloadViedoAsync(dto);
|
||||
if (result)
|
||||
{
|
||||
return ApiResult.Success(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ApiResult.Fail("错误");
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
/// <summary>
|
||||
/// 批量删除
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("vdelete/batch")]
|
||||
public async Task<IActionResult> BathRealDelete(ReDownViedoDto dto)
|
||||
{
|
||||
List<MyClass> myClasses = new List<MyClass>();
|
||||
foreach (var drive in DriveInfo.GetDrives())
|
||||
var result = await douyinVideoService.RealDeleteVideos(dto.Ids);
|
||||
if (result)
|
||||
{
|
||||
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}");
|
||||
return ApiResult.Success(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ApiResult.Fail("错误");
|
||||
}
|
||||
//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
|
||||
/// <summary>
|
||||
/// 删除视频-不再下载
|
||||
/// </summary>
|
||||
/// <param name="vid"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("vdelete/{vid}")]
|
||||
public async Task<IActionResult> DeleteVideo([FromRoute] string vid)
|
||||
{
|
||||
public string name { get; set; }
|
||||
if (string.IsNullOrWhiteSpace(vid))
|
||||
{
|
||||
return ApiResult.Fail("参数错误");
|
||||
}
|
||||
else
|
||||
{
|
||||
var res = await douyinVideoService.RealDeleteVideos(new List<string> { vid });
|
||||
if (res)
|
||||
{
|
||||
return ApiResult.Success("删除成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ApiResult.Fail("删除失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long x1 { get; set; }
|
||||
/// <summary>
|
||||
/// 查询已删除视频列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("vdelete/get")]
|
||||
public async Task<IActionResult> GetDeleteVideo()
|
||||
{
|
||||
return ApiResult.Success(await douyinCommonService.GetDouyinDeleteVideos());
|
||||
}
|
||||
/// <summary>
|
||||
/// 根据博主id删除博主所有视频
|
||||
/// </summary>
|
||||
/// <param name="uperUid"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("vdelete/byauthor/{uperUid}")]
|
||||
public async Task<IActionResult> 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("未找到该博主视频");
|
||||
}
|
||||
|
||||
public long x2 { get; set; }
|
||||
//private async Task<(bool flowControl, IActionResult value)> BatchDeleteVideos(List<DouyinVideo> videos)
|
||||
//{
|
||||
|
||||
// return (flowControl: true, value: null);
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 查询最新N条数据
|
||||
/// </summary>
|
||||
/// <param name="top"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("top{top}")]
|
||||
public async Task<IActionResult> GetLastSyncTop([FromRoute] int top = 5)
|
||||
{
|
||||
return ApiResult.Success(await douyinVideoService.GetLastSyncTop(top));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除无效视频记录
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("removeInvalid")]
|
||||
public async Task<IActionResult> RemoveInvalidVideo()
|
||||
{
|
||||
var data = await douyinVideoService.DeleteInvalidVideo();
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 所有视频重新生成nfo文件
|
||||
///// </summary>
|
||||
///// <returns></returns>
|
||||
//[HttpGet("renfo")]
|
||||
//public async Task<IActionResult> 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();
|
||||
//}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取7天视频同步趋势数据(曲线图)
|
||||
/// </summary>
|
||||
/// <returns>7天图表数据列表</returns>
|
||||
[HttpGet("chart/{day}")]
|
||||
public async Task<IActionResult> Chart([FromRoute] int day = 7)
|
||||
{
|
||||
try
|
||||
{
|
||||
var chartData = await douyinVideoService.GetChartData(day);
|
||||
return ApiResult.Success(chartData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/Move")]
|
||||
public async Task<IActionResult> Move()
|
||||
{
|
||||
await douyinVideoService.HandOldFolderVideos();
|
||||
|
||||
return ApiResult.Success(DateTime.Now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user