完成合集、短剧
This commit is contained in:
@@ -11,12 +11,12 @@ namespace dy.net.Controllers
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class CollectController : ControllerBase
|
||||
public class CateController : ControllerBase
|
||||
{
|
||||
private readonly DouyinCollectCateService _douyinCollectCateService;
|
||||
private readonly DouyinQuartzJobService _douyinQuartzJobService;
|
||||
|
||||
public CollectController(DouyinCollectCateService douyinCollectCateService, DouyinQuartzJobService douyinQuartzJobService)
|
||||
public CateController(DouyinCollectCateService douyinCollectCateService, DouyinQuartzJobService douyinQuartzJobService)
|
||||
{
|
||||
this._douyinCollectCateService = douyinCollectCateService;
|
||||
_douyinQuartzJobService = douyinQuartzJobService;
|
||||
@@ -197,10 +197,10 @@ namespace dy.net.Controllers
|
||||
return ApiResult.Fail($"请在飞牛应用设置里面将{dyUserCookies.UpSavePath}添加读写权限");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dyUserCookies.ImgSavePath) && !DouyinFileUtils.HasDirectoryReadWritePermission(dyUserCookies.ImgSavePath))
|
||||
{
|
||||
return ApiResult.Fail($"请在飞牛应用设置里面将{dyUserCookies.ImgSavePath}添加读写权限");
|
||||
}
|
||||
//if (!string.IsNullOrWhiteSpace(dyUserCookies.ImgSavePath) && !DouyinFileUtils.HasDirectoryReadWritePermission(dyUserCookies.ImgSavePath))
|
||||
//{
|
||||
// return ApiResult.Fail($"请在飞牛应用设置里面将{dyUserCookies.ImgSavePath}添加读写权限");
|
||||
//}
|
||||
|
||||
|
||||
var checkCk = await httpClientService.CheckCookie(dyUserCookies);
|
||||
|
||||
+217
-39
@@ -47,8 +47,7 @@ namespace dy.net.Controllers
|
||||
[HttpGet("statics")]
|
||||
public async Task<IActionResult> GetStaticsAsync()
|
||||
{
|
||||
var data = await douyinVideoService.GetStatics();
|
||||
return ApiResult.Success(data);
|
||||
return ApiResult.Success(await douyinVideoService.GetStatics());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,7 +67,7 @@ namespace dy.net.Controllers
|
||||
{
|
||||
return ApiResult.Fail($"视频不存在:{vid}");
|
||||
}
|
||||
return PlayViedo(viedo);
|
||||
return await PlayVideoAsync(viedo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -76,11 +75,85 @@ namespace dy.net.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult PlayViedo(DouyinVideo viedo)
|
||||
{
|
||||
//private IActionResult PlayViedo(DouyinVideo viedo)
|
||||
//{
|
||||
|
||||
// 1. 拼接完整物理路径(配置路径 + 文件名)
|
||||
string videoFullPath = viedo.VideoSavePath;
|
||||
// // 1. 拼接完整物理路径(配置路径 + 文件名)
|
||||
// string videoFullPath = viedo.VideoSavePath;
|
||||
|
||||
// // 2. 验证文件是否存在
|
||||
// if (!System.IO.File.Exists(videoFullPath))
|
||||
// {
|
||||
// return ApiResult.Fail($"视频文件不存在:{videoFullPath}");
|
||||
// }
|
||||
|
||||
// // 3. 获取文件信息(大小、类型)
|
||||
// var fileInfo = new FileInfo(videoFullPath);
|
||||
// long fileSize = fileInfo.Length;
|
||||
// string contentType = GetContentType(videoFullPath); // 自动识别视频 MIME 类型
|
||||
|
||||
// // 4. 处理分片请求(前端视频标签自动发起,支持断点续传)
|
||||
// if (Request.Headers.ContainsKey("Range") && long.TryParse(Request.Headers.Range.ToString().Split('=')[1].Split('-')[0], out long start))
|
||||
// {
|
||||
// // 分片起始位置(前端请求的起始字节)
|
||||
// long end = Math.Min(start + 1024 * 1024 * 2, fileSize - 1); // 每片 2MB(可调整)
|
||||
// long chunkSize = end - start + 1;
|
||||
|
||||
// // 5. 设置分片响应头
|
||||
// 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());
|
||||
|
||||
// // 6. 读取分片并返回流
|
||||
// var stream = new FileStream(
|
||||
// videoFullPath,
|
||||
// FileMode.Open,
|
||||
// FileAccess.Read,
|
||||
// FileShare.Read,
|
||||
// bufferSize: 4096,
|
||||
// useAsync: true);
|
||||
|
||||
// stream.Seek(start, SeekOrigin.Begin);
|
||||
// return new FileStreamResult(stream, contentType);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // 完整文件请求(兼容旧浏览器)
|
||||
// return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true);
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <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))
|
||||
@@ -88,36 +161,117 @@ namespace dy.net.Controllers
|
||||
return ApiResult.Fail($"视频文件不存在:{videoFullPath}");
|
||||
}
|
||||
|
||||
// 3. 获取文件信息(大小、类型)
|
||||
var fileInfo = new FileInfo(videoFullPath);
|
||||
long fileSize = fileInfo.Length;
|
||||
string contentType = GetContentType(videoFullPath); // 自动识别视频 MIME 类型
|
||||
|
||||
// 4. 处理分片请求(前端视频标签自动发起,支持断点续传)
|
||||
if (Request.Headers.ContainsKey("Range") && long.TryParse(Request.Headers.Range.ToString().Split('=')[1].Split('-')[0], out long start))
|
||||
try
|
||||
{
|
||||
// 分片起始位置(前端请求的起始字节)
|
||||
long end = Math.Min(start + 1024 * 1024 * 2, fileSize - 1); // 每片 2MB(可调整)
|
||||
long chunkSize = end - start + 1;
|
||||
// 3. 获取文件信息(使用using确保FileInfo资源释放)
|
||||
var fileInfo = new FileInfo(videoFullPath);
|
||||
long fileSize = fileInfo.Length;
|
||||
|
||||
// 5. 设置分片响应头
|
||||
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());
|
||||
// 校验空文件
|
||||
if (fileSize == 0)
|
||||
{
|
||||
return ApiResult.Fail($"视频文件为空:{videoFullPath}");
|
||||
}
|
||||
|
||||
// 6. 读取分片并返回流
|
||||
var stream = new FileStream(videoFullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
|
||||
stream.Seek(start, SeekOrigin.Begin);
|
||||
return new FileStreamResult(stream, contentType);
|
||||
string contentType = GetContentType(videoFullPath);
|
||||
|
||||
// 4. 处理分片请求(Range)
|
||||
if (Request.Headers.ContainsKey("Range"))
|
||||
{
|
||||
return await HandleRangeRequestAsync(videoFullPath, fileSize, contentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 完整文件请求(启用分片处理,兼容前端断点续传)
|
||||
return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// 完整文件请求(兼容旧浏览器)
|
||||
return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true);
|
||||
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="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>
|
||||
/// 播放视频
|
||||
@@ -144,7 +298,7 @@ namespace dy.net.Controllers
|
||||
return ApiResult.Fail($"视频地址无效");
|
||||
}
|
||||
|
||||
return PlayViedo(viedo);
|
||||
return await PlayVideoAsync(viedo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -155,16 +309,12 @@ namespace dy.net.Controllers
|
||||
/// <summary>
|
||||
/// 辅助方法:根据文件名获取 MIME 类型(确保前端正确识别视频格式)
|
||||
/// </summary>
|
||||
private string GetContentType(string filename)
|
||||
private static string GetContentType(string filename)
|
||||
{
|
||||
string extension = Path.GetExtension(filename).ToLowerInvariant();
|
||||
return extension switch
|
||||
{
|
||||
".mp4" => "video/mp4",
|
||||
".webm" => "video/webm",
|
||||
".ogg" => "video/ogg",
|
||||
".mov" => "video/quicktime",
|
||||
".avi" => "video/x-msvideo",
|
||||
_ => "application/octet-stream" // 默认二进制流
|
||||
};
|
||||
}
|
||||
@@ -277,7 +427,7 @@ namespace dy.net.Controllers
|
||||
|
||||
//private async Task<(bool flowControl, IActionResult value)> BatchDeleteVideos(List<DouyinVideo> videos)
|
||||
//{
|
||||
|
||||
|
||||
// return (flowControl: true, value: null);
|
||||
//}
|
||||
|
||||
@@ -287,7 +437,7 @@ namespace dy.net.Controllers
|
||||
/// <param name="top"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("top{top}")]
|
||||
public async Task<IActionResult> GetLastSyncTop([FromRoute]int top = 5)
|
||||
public async Task<IActionResult> GetLastSyncTop([FromRoute] int top = 5)
|
||||
{
|
||||
return ApiResult.Success(await douyinVideoService.GetLastSyncTop(top));
|
||||
}
|
||||
@@ -310,7 +460,7 @@ namespace dy.net.Controllers
|
||||
[HttpGet("renfo")]
|
||||
public async Task<IActionResult> ReCreateNfo()
|
||||
{
|
||||
var videos= await douyinVideoService.GetAllAsync();
|
||||
var videos = await douyinVideoService.GetAllAsync();
|
||||
if (videos == null || videos.Count == 0)
|
||||
{
|
||||
return ApiResult.Success("暂无视频数据需要生成NFO文件");
|
||||
@@ -330,7 +480,7 @@ namespace dy.net.Controllers
|
||||
}
|
||||
catch (Exception singleEx)
|
||||
{
|
||||
Serilog.Log.Error($"刮削视频(Path:{video.VideoSavePath})生成NFO失败:{singleEx.Message}");
|
||||
Serilog.Log.Error($"刮削视频(Path:{video.VideoSavePath})生成NFO失败:{singleEx.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,5 +492,33 @@ namespace dy.net.Controllers
|
||||
|
||||
return ApiResult.Success();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取7天视频同步趋势数据(曲线图)
|
||||
/// </summary>
|
||||
/// <returns>7天图表数据列表</returns>
|
||||
[HttpGet("chart")]
|
||||
public async Task<IActionResult> Chart()
|
||||
{
|
||||
try
|
||||
{
|
||||
var chartData = await douyinVideoService.GetChartData();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -220,12 +220,11 @@ namespace dy.net
|
||||
commonService.UpdateAllCookieSyncedToZero();
|
||||
// 初始化配置
|
||||
var config = commonService.InitConfig();
|
||||
if (!isDevelopment)
|
||||
//if (!isDevelopment)
|
||||
{
|
||||
// 启动定时任务
|
||||
var quartzJobService = services.GetRequiredService<DouyinQuartzJobService>();
|
||||
quartzJobService.InitOrReStartAllJobs(config?.Cron <= 0 ? "30" : config.Cron.ToString());
|
||||
DouyinHttpHelper.GetTenImage(Appsettings.Get("tagName"));//查询镜像版本
|
||||
}
|
||||
// 初始化Cookie
|
||||
var deploy= Appsettings.Get("deploy");
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<_PublishTargetUrl>E:\gitea\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
||||
<History>True|2026-01-19T02:19:44.8997385Z||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;True|2026-01-15T22:07:14.8243754+08:00||;True|2026-01-15T21:40:13.2613927+08:00||;True|2026-01-15T20:57:56.6291682+08:00||;True|2026-01-15T20:57:47.1363536+08:00||;True|2026-01-15T20:08:44.4710883+08:00||;True|2026-01-15T20:04:51.8284314+08:00||;False|2026-01-15T20:04:45.3258155+08:00||;True|2026-01-15T19:59:11.7907672+08:00||;True|2026-01-15T01:10:48.9066941+08:00||;True|2026-01-15T01:03:23.6216975+08:00||;True|2026-01-15T01:03:16.3364591+08:00||;False|2026-01-15T01:03:10.5654436+08:00||;True|2026-01-15T00:49:21.6559622+08:00||;True|2026-01-15T00:10:42.7288439+08:00||;True|2026-01-15T00:06:28.4241682+08:00||;True|2026-01-13T22:34:15.2372411+08:00||;True|2026-01-13T22:26:46.4522947+08:00||;True|2026-01-13T22:15:56.9735179+08:00||;True|2026-01-13T22:15:51.8250677+08:00||;False|2026-01-13T22:15:46.6608690+08:00||;True|2026-01-13T22:03:06.4683237+08:00||;False|2026-01-13T22:03:01.2036027+08:00||;True|2026-01-13T21:36:05.8776209+08:00||;True|2026-01-13T21:33:02.0766289+08:00||;False|2026-01-13T21:32:57.1976141+08:00||;True|2026-01-13T19:14:35.2519439+08:00||;True|2026-01-13T15:07:50.0836745+08:00||;False|2026-01-13T15:06:35.5929685+08:00||;True|2026-01-13T10:21:41.6012776+08:00||;True|2026-01-13T09:57:25.4788847+08:00||;True|2026-01-13T09:53:14.4900360+08:00||;True|2026-01-13T09:23:53.6465295+08:00||;True|2026-01-12T22:04:14.5886955+08:00||;False|2026-01-12T22:04:08.2008101+08:00||;True|2026-01-12T21:53:46.9947176+08:00||;True|2026-01-12T21:11:15.8358024+08:00||;True|2026-01-12T21:09:51.8663228+08:00||;True|2026-01-11T21:25:07.5052845+08:00||;False|2026-01-11T21:24:27.5744557+08:00||;True|2026-01-11T19:59:15.4734611+08:00||;False|2026-01-11T19:59:04.9543339+08:00||;True|2026-01-11T18:51:30.6649269+08:00||;True|2026-01-08T21:07:57.5094466+08:00||;True|2026-01-08T14:51:18.2266231+08:00||;True|2026-01-08T14:31:55.1137120+08:00||;True|2026-01-08T00:16:24.4451490+08:00||;True|2026-01-08T00:14:20.7430793+08:00||;True|2026-01-08T00:07:46.7228194+08:00||;True|2026-01-07T23:52:13.3290082+08:00||;True|2026-01-07T23:49:28.4838650+08:00||;True|2026-01-07T23:47:45.1936189+08:00||;True|2026-01-07T23:35:09.7818611+08:00||;True|2026-01-07T23:20:43.7462863+08:00||;True|2026-01-07T23:04:39.5140429+08:00||;False|2026-01-07T23:04:36.5367611+08:00||;True|2026-01-07T22:53:14.6013449+08:00||;True|2026-01-07T22:49:59.9549006+08:00||;True|2026-01-07T22:36:40.0254856+08:00||;False|2026-01-07T22:36:28.8275903+08:00||;True|2026-01-07T21:59:51.4355171+08:00||;True|2026-01-03T21:38:51.4307034+08:00||;True|2026-01-02T19:09:16.9668906+08:00||;False|2026-01-02T19:09:11.7496369+08:00||;True|2026-01-02T15:42:08.2215697+08:00||;True|2026-01-02T09:46:56.1654861+08:00||;True|2026-01-02T09:35:28.0211225+08:00||;True|2026-01-01T23:07:01.9022045+08:00||;False|2026-01-01T23:06:56.0537216+08:00||;True|2026-01-01T22:16:10.2974067+08:00||;True|2026-01-01T22:16:06.2123787+08:00||;False|2026-01-01T22:15:33.3626979+08:00||;True|2026-01-01T22:01:30.7161900+08:00||;True|2026-01-01T21:10:19.3664263+08:00||;True|2026-01-01T21:09:38.6071080+08:00||;True|2025-12-30T11:45:54.5543034+08:00||;True|2025-12-30T09:19:08.3178124+08:00||;True|2025-12-29T18:57:29.7032246+08:00||;True|2025-12-27T14:52:24.4780776+08:00||;False|2025-12-27T14:52:19.5635794+08:00||;True|2025-12-27T14:48:01.6252748+08:00||;False|2025-12-27T14:47:55.7976192+08:00||;True|2025-12-27T14:38:23.9723838+08:00||;True|2025-12-27T13:00:29.8583858+08:00||;True|2025-12-26T22:18:42.4015637+08:00||;True|2025-12-26T22:10:58.5274572+08:00||;True|2025-12-26T22:06:26.3129600+08:00||;True|2025-12-26T22:03:55.6718618+08:00||;False|2025-12-26T22:03:48.3809954+08:00||;True|2025-12-26T22:02:33.1840390+08:00||;</History>
|
||||
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
||||
<History>True|2026-01-21T17:37:53.6972395Z||;True|2026-01-22T01:34:14.2142463+08:00||;True|2026-01-22T01:32:50.9228911+08:00||;True|2026-01-22T01:29:43.8871986+08:00||;True|2026-01-22T01:29:40.7838709+08:00||;True|2026-01-22T01:27:34.4763970+08:00||;True|2026-01-22T01:26:55.0117367+08:00||;True|2026-01-22T01:23:19.4789587+08:00||;True|2026-01-19T10:19:44.8997385+08:00||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;True|2026-01-15T22:07:14.8243754+08:00||;True|2026-01-15T21:40:13.2613927+08:00||;True|2026-01-15T20:57:56.6291682+08:00||;True|2026-01-15T20:57:47.1363536+08:00||;True|2026-01-15T20:08:44.4710883+08:00||;True|2026-01-15T20:04:51.8284314+08:00||;False|2026-01-15T20:04:45.3258155+08:00||;True|2026-01-15T19:59:11.7907672+08:00||;True|2026-01-15T01:10:48.9066941+08:00||;True|2026-01-15T01:03:23.6216975+08:00||;True|2026-01-15T01:03:16.3364591+08:00||;False|2026-01-15T01:03:10.5654436+08:00||;True|2026-01-15T00:49:21.6559622+08:00||;True|2026-01-15T00:10:42.7288439+08:00||;True|2026-01-15T00:06:28.4241682+08:00||;True|2026-01-13T22:34:15.2372411+08:00||;True|2026-01-13T22:26:46.4522947+08:00||;True|2026-01-13T22:15:56.9735179+08:00||;True|2026-01-13T22:15:51.8250677+08:00||;False|2026-01-13T22:15:46.6608690+08:00||;True|2026-01-13T22:03:06.4683237+08:00||;False|2026-01-13T22:03:01.2036027+08:00||;True|2026-01-13T21:36:05.8776209+08:00||;True|2026-01-13T21:33:02.0766289+08:00||;False|2026-01-13T21:32:57.1976141+08:00||;True|2026-01-13T19:14:35.2519439+08:00||;True|2026-01-13T15:07:50.0836745+08:00||;False|2026-01-13T15:06:35.5929685+08:00||;True|2026-01-13T10:21:41.6012776+08:00||;True|2026-01-13T09:57:25.4788847+08:00||;True|2026-01-13T09:53:14.4900360+08:00||;True|2026-01-13T09:23:53.6465295+08:00||;True|2026-01-12T22:04:14.5886955+08:00||;False|2026-01-12T22:04:08.2008101+08:00||;True|2026-01-12T21:53:46.9947176+08:00||;True|2026-01-12T21:11:15.8358024+08:00||;True|2026-01-12T21:09:51.8663228+08:00||;True|2026-01-11T21:25:07.5052845+08:00||;False|2026-01-11T21:24:27.5744557+08:00||;True|2026-01-11T19:59:15.4734611+08:00||;False|2026-01-11T19:59:04.9543339+08:00||;True|2026-01-11T18:51:30.6649269+08:00||;True|2026-01-08T21:07:57.5094466+08:00||;True|2026-01-08T14:51:18.2266231+08:00||;True|2026-01-08T14:31:55.1137120+08:00||;True|2026-01-08T00:16:24.4451490+08:00||;True|2026-01-08T00:14:20.7430793+08:00||;True|2026-01-08T00:07:46.7228194+08:00||;True|2026-01-07T23:52:13.3290082+08:00||;True|2026-01-07T23:49:28.4838650+08:00||;True|2026-01-07T23:47:45.1936189+08:00||;True|2026-01-07T23:35:09.7818611+08:00||;True|2026-01-07T23:20:43.7462863+08:00||;True|2026-01-07T23:04:39.5140429+08:00||;False|2026-01-07T23:04:36.5367611+08:00||;True|2026-01-07T22:53:14.6013449+08:00||;True|2026-01-07T22:49:59.9549006+08:00||;True|2026-01-07T22:36:40.0254856+08:00||;False|2026-01-07T22:36:28.8275903+08:00||;True|2026-01-07T21:59:51.4355171+08:00||;True|2026-01-03T21:38:51.4307034+08:00||;True|2026-01-02T19:09:16.9668906+08:00||;False|2026-01-02T19:09:11.7496369+08:00||;True|2026-01-02T15:42:08.2215697+08:00||;True|2026-01-02T09:46:56.1654861+08:00||;True|2026-01-02T09:35:28.0211225+08:00||;True|2026-01-01T23:07:01.9022045+08:00||;False|2026-01-01T23:06:56.0537216+08:00||;True|2026-01-01T22:16:10.2974067+08:00||;True|2026-01-01T22:16:06.2123787+08:00||;False|2026-01-01T22:15:33.3626979+08:00||;True|2026-01-01T22:01:30.7161900+08:00||;True|2026-01-01T21:10:19.3664263+08:00||;True|2026-01-01T21:09:38.6071080+08:00||;True|2025-12-30T11:45:54.5543034+08:00||;True|2025-12-30T09:19:08.3178124+08:00||;True|2025-12-29T18:57:29.7032246+08:00||;True|2025-12-27T14:52:24.4780776+08:00||;False|2025-12-27T14:52:19.5635794+08:00||;True|2025-12-27T14:48:01.6252748+08:00||;False|2025-12-27T14:47:55.7976192+08:00||;</History>
|
||||
<LastFailureDetails />
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -36,25 +36,22 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取,
|
||||
|
||||
|
||||
|
||||
### 1.1 提取抖音 Cookie
|
||||
### 1.1 提取`Cookie`以及 `sec_uer_id`
|
||||
1. 打开 **抖音网页版** (https://www.douyin.com/) 并登录目标账号;
|
||||
2. 进入「我的收藏」页面,确保页面加载完成;
|
||||
3. 按 `F12` 打开浏览器「开发者工具」,切换到「Network (网络)」标签;
|
||||
4. 刷新页面,在搜索框中输入 `v1/web/aweme/listcollection` 筛选请求;
|
||||
5. 点击任意一条筛选结果,在右侧「Headers (标头)」中找到 `Cookie` 字段,**完整复制整段内容**(不可删减字符)。
|
||||
2. 进入个人主页,按下F12进入开发者模式、并切换到 `网络`(也可能叫`network`).
|
||||
3. 在筛选框中输入`/follow`.
|
||||
4. 点击自己头像边上的 `关注` 按钮、会弹出你的关注列表,然后在右侧网络请求里面会出现多个请求,随便选一个.
|
||||
4. 在请求的标签里面切换到`负载`(也可能叫`payload`)
|
||||
5. 找到sec_user_id,复制值即可
|
||||
6. 在请求的标签里面切换到`标头`(也可能叫`Headers`)
|
||||
7. 往下拉,直到出现`Cookie` ,复制完整的值,注意前后不要带换行符,很多人会多复制个换行符出来.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 1.2 提取你的`sec_user_id`
|
||||
- **个人 sec_user_id**(同步自己喜欢用):
|
||||
1.进入自己的抖音主页:
|
||||
- 方式:按 `F12` 点击`Network` 或`网络` 筛选器里面填`web/user/following/list` 然后 查看请求的 `payload` 或`负载` 即可找到`sec_user_id`;
|
||||
2.进入抖音主页后随便点一个自己的作品,然后你的名字,进入目标博主主页;
|
||||
方式:直接复制地址栏中 `user/` 到 `?from_tab_name` 中间部分内容即是博主的 `sec_user_id`;
|
||||
|
||||

|
||||
|
||||
### 1.3 提取 博主的`sec_user_id`以及博主的uid
|
||||
### 1.2 提取 博主的`sec_user_id`以及博主的uid
|
||||
- **对于想下载博主视频,但是又不想关注博主,需要用到**
|
||||
- 1.进入博主主页,按`F12` 点击`Network` 或`网络` 筛选器里面填`/web/aweme/post` 然后切到`预览`或`preview` 展开 json数据结果 找到`aweme_list` 然后随便点开其中一个子项 即可找到`author_user_id` 这便是博主的uid 。后续在关注列表中,需要手动添加非关注博主同步视频时将会要用到。
|
||||

|
||||
@@ -96,7 +93,7 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取,
|
||||
|
||||
| 镜像标签 | 架构 |
|
||||
| ----------------- | -------------- |
|
||||
| `beta_1.9.9` | x86_64 (amd64) |
|
||||
| `beta_2.0.0` | x86_64 (amd64) |
|
||||
| `arm_1.9.8` | ARM64 |
|
||||
|
||||
|
||||
@@ -114,7 +111,7 @@ docker run -d --restart=always \
|
||||
-v /opt/dysync/uper:/app/uper \
|
||||
-p 10101:10101 \
|
||||
--name dysync2026 \
|
||||
ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.9.9
|
||||
ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_2.0.0
|
||||
```
|
||||
|
||||
|
||||
@@ -129,7 +126,7 @@ version: '3.8'
|
||||
|
||||
services:
|
||||
dysync:
|
||||
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.9.9
|
||||
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_2.0.0
|
||||
container_name: dysync2026 # 容器名称
|
||||
restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止
|
||||
ports:
|
||||
|
||||
@@ -29,7 +29,7 @@ columns.value = [
|
||||
{ title: '收藏路径', dataIndex: 'savePath' },
|
||||
{ title: '喜欢路径', dataIndex: 'favSavePath' },
|
||||
{ title: '博主路径', dataIndex: 'upSavePath' },
|
||||
{ title: '图文视频', dataIndex: 'imgSavePath' },
|
||||
// { title: '图文视频', dataIndex: 'imgSavePath' },
|
||||
// { title: 'Cookie', dataIndex: 'cookies' },
|
||||
{ title: '状态', dataIndex: 'status', width: 180 },
|
||||
{ title: '操作', dataIndex: 'edit', width: 200 }, // 加宽操作列宽度
|
||||
@@ -54,11 +54,13 @@ type DataItem = {
|
||||
upSecUserIdsJson?: UpSecUserIdItem[];
|
||||
upSecUserIds?: string;
|
||||
upSavePath?: string;
|
||||
imgSavePath?: string;
|
||||
// imgSavePath?: string;
|
||||
useSinglePath?: boolean; // 新增:是否全部用一个地址
|
||||
useCollectFolder?: boolean;
|
||||
downMix?: boolean;
|
||||
downSeries?: boolean;
|
||||
// mixPath?: string;
|
||||
// seriesPath?: string;
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -117,11 +119,13 @@ const newCookie = (cookie?: DataItem) => {
|
||||
cookie.id = '0';
|
||||
cookie.upSecUserIdsJson = undefined;
|
||||
cookie.upSavePath = undefined;
|
||||
cookie.imgSavePath = undefined;
|
||||
// cookie.imgSavePath = undefined;
|
||||
cookie.useSinglePath = false; // 新增:默认不使用单一路径
|
||||
cookie.useCollectFolder = false; //是否按收藏夹来下载。
|
||||
cookie.downMix = false; //是否下载收藏夹的合集
|
||||
cookie.downSeries = false; //是否下载短剧
|
||||
// cookie.mixPath = undefined; //合集存储路径
|
||||
// cookie.seriesPath = undefined; //短剧存储路径
|
||||
return cookie;
|
||||
};
|
||||
|
||||
@@ -141,7 +145,7 @@ watch(
|
||||
if (useSinglePath && newSavePath) {
|
||||
form.favSavePath = newSavePath;
|
||||
form.upSavePath = newSavePath;
|
||||
form.imgSavePath = newSavePath;
|
||||
// form.imgSavePath = newSavePath;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -220,6 +224,7 @@ const deleted = (id: string) => {
|
||||
};
|
||||
|
||||
function edit(record: DataItem) {
|
||||
cookieId.value = record.id;
|
||||
editRecord.value = record;
|
||||
console.log(record);
|
||||
copyObject(form, record);
|
||||
@@ -316,6 +321,10 @@ onMounted(() => {
|
||||
const showDrawer = ref(false);
|
||||
// 抽屉类型(区分收藏夹/合集/短剧)
|
||||
type DrawerType = 'collect' | 'mix' | 'series';
|
||||
|
||||
const cookieId = ref('');
|
||||
const cateType = ref(5);
|
||||
|
||||
const drawerType = ref<DrawerType>('collect');
|
||||
// 抽屉滚动容器引用(用于监听触底分页)
|
||||
const drawerScrollRef = ref<HTMLDivElement | null>(null);
|
||||
@@ -332,28 +341,32 @@ const drawerPagination = reactive({
|
||||
|
||||
// ========== 定义抽屉数据项接口 ==========
|
||||
interface DrawerItem {
|
||||
Id: string; // 不显示
|
||||
Name: string; // 名称
|
||||
SaveFolder: string; // 保存文件夹
|
||||
Sync: boolean; // 是否同步
|
||||
CoverUrl: string; // 封面
|
||||
CookieId: string; // 不显示
|
||||
XId: string; // 不显示
|
||||
id: string; // 不显示
|
||||
name: string; // 名称
|
||||
saveFolder: string; // 保存文件夹
|
||||
sync: boolean; // 是否同步
|
||||
coverUrl: string; // 封面
|
||||
cookieId: string; // 不显示
|
||||
xId: string; // 不显示
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ========== 3个打开抽屉的方法 ==========
|
||||
const openCollectFolderSetModal = () => {
|
||||
drawerType.value = 'collect';
|
||||
cateType.value = 5;
|
||||
openCommonDrawer();
|
||||
};
|
||||
|
||||
const openMixDownSetModal = () => {
|
||||
drawerType.value = 'mix';
|
||||
cateType.value = 6;
|
||||
openCommonDrawer();
|
||||
};
|
||||
|
||||
const openSeriesDownSetModal = () => {
|
||||
drawerType.value = 'series';
|
||||
cateType.value = 7;
|
||||
openCommonDrawer();
|
||||
};
|
||||
|
||||
@@ -401,36 +414,32 @@ const handleDrawerScroll = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 加载抽屉数据(模拟接口请求,可替换为真实接口) ==========
|
||||
// ========== 加载抽屉数据(
|
||||
const loadDrawerData = () => {
|
||||
if (drawerPagination.loading || !drawerPagination.hasMore) return;
|
||||
|
||||
drawerPagination.loading = true;
|
||||
|
||||
// 模拟接口请求(实际项目中替换为真实API调用)
|
||||
setTimeout(() => {
|
||||
// 模拟数据(可根据drawerType返回不同类型数据)
|
||||
const mockData: DrawerItem[] = Array.from({ length: drawerPagination.pageSize }, (_, index) => ({
|
||||
Id: `${drawerPagination.current}-${index}`,
|
||||
Name: `${getDrawerTypeName()} ${(drawerPagination.current - 1) * drawerPagination.pageSize + index + 1}`,
|
||||
SaveFolder: `./${drawerType.value}/${(drawerPagination.current - 1) * drawerPagination.pageSize + index + 1}`,
|
||||
Sync: Math.random() > 0.5,
|
||||
CoverUrl: `https://picsum.photos/120/180?random=${
|
||||
// 改为竖向图片尺寸 120x180
|
||||
(drawerPagination.current - 1) * drawerPagination.pageSize + index + 1
|
||||
}`,
|
||||
CookieId: form.id || '0',
|
||||
XId: `X-${drawerPagination.current}-${index}`,
|
||||
}));
|
||||
|
||||
// 拼接数据列表
|
||||
drawerDataList.value = [...drawerDataList.value, ...mockData];
|
||||
// 更新分页状态
|
||||
drawerPagination.total = 100; // 模拟总条数
|
||||
drawerPagination.loading = false;
|
||||
// 判断是否还有更多数据
|
||||
drawerPagination.hasMore = drawerDataList.value.length < drawerPagination.total;
|
||||
}, 800);
|
||||
useApiStore()
|
||||
.CatePageList({
|
||||
cookieId: cookieId.value,
|
||||
cateType: cateType.value,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
drawerDataList.value = [...drawerDataList.value, ...res.data.data];
|
||||
drawerPagination.loading = false;
|
||||
// 更新分页状态
|
||||
drawerPagination.total = res.data.total;
|
||||
drawerPagination.loading = false;
|
||||
// 判断是否还有更多数据
|
||||
drawerPagination.hasMore = drawerDataList.value.length < drawerPagination.total;
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
drawerPagination.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 获取抽屉类型名称(用于页面展示) ==========
|
||||
@@ -443,15 +452,15 @@ const getDrawerTypeName = () => {
|
||||
case 'series':
|
||||
return '短剧';
|
||||
default:
|
||||
return '数据';
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 切换同步状态(可根据需求对接真实接口) ==========
|
||||
const toggleDrawerItemSync = (item: DrawerItem, index: number) => {
|
||||
if (drawerDataList.value[index]) {
|
||||
drawerDataList.value[index].Sync = !item.Sync;
|
||||
console.log(`切换${getDrawerTypeName()}【${item.Name}】的同步状态为:${!item.Sync}`);
|
||||
drawerDataList.value[index].sync = !item.sync;
|
||||
console.log(`切换${getDrawerTypeName()}【${item.name}】的同步状态为:${!item.sync}`);
|
||||
}
|
||||
};
|
||||
// ========== 关闭抽屉清理资源 ==========
|
||||
@@ -470,12 +479,20 @@ const saveDrawerData = () => {
|
||||
message.info('暂无需要保存的配置数据');
|
||||
return;
|
||||
}
|
||||
// 模拟保存逻辑(实际项目中可替换为真实接口,提交 drawerDataList.value 数据)
|
||||
drawerPagination.loading = true;
|
||||
setTimeout(() => {
|
||||
drawerPagination.loading = false;
|
||||
message.success(`${getDrawerTypeName()}配置保存成功`);
|
||||
}, 800);
|
||||
useApiStore()
|
||||
.BatchSaveCate(drawerDataList.value)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
showDrawer.value = false;
|
||||
message.success('保存成功');
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
drawerPagination.loading = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -504,14 +521,14 @@ const saveDrawerData = () => {
|
||||
<!-- 收藏的存储路径 -->
|
||||
<a-form-item label="收藏的存储路径" name="savePath">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<a-input v-model:value="form.savePath" style="width: 200px;" />
|
||||
<a-input v-model:value="form.savePath" class="form-item-div" />
|
||||
<a-alert message="不想同步收藏的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<!-- 新增:是否全部用一个地址开关 -->
|
||||
<a-form-item v-if="form.savePath&&form.savePath.length>0" label="是否统一存储路径" name="useSinglePath">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 200px;">
|
||||
<div class="form-item-div">
|
||||
<a-switch v-model:checked="form.useSinglePath" :checked-value="true" :un-checked-value="false" size="default" />
|
||||
<span style="margin-left:10px;">{{form.useSinglePath ?'是':'否'}}</span>
|
||||
</div>
|
||||
@@ -519,11 +536,28 @@ const saveDrawerData = () => {
|
||||
<a-alert message="开启后,所有视频都存储在收藏视频存储的路径,如果是容器部署:docker-compose配置,此时只需要映射一个路径' " :type="form.useSinglePath?'success':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 喜欢的存储路径 -->
|
||||
<a-form-item label="喜欢的存储路径" name="favSavePath">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<a-input v-model:value="form.favSavePath" :disabled="form.useSinglePath" placeholder="" class="form-item-div" />
|
||||
<a-alert message="不想同步喜欢的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 关注的存储路径 -->
|
||||
<a-form-item label="关注的存储路径" name="upSavePath">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<a-input v-model:value="form.upSavePath" :disabled="form.useSinglePath" placeholder="" class="form-item-div" />
|
||||
<a-alert message="不想同步关注列表博主的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.savePath&&form.savePath.length>0" label="下载收藏夹" name="useCollectFolder">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 200px;">
|
||||
<div class="form-item-div">
|
||||
<a-switch v-model:checked="form.useCollectFolder" :checked-value="true" :un-checked-value="false" size="default" />
|
||||
<span style="margin-left:10px;">{{ form.useCollectFolder ? '是' : '否' }}</span>
|
||||
|
||||
<a-button @click="openCollectFolderSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.useCollectFolder">
|
||||
<setting-outlined />
|
||||
</a-button>
|
||||
@@ -535,9 +569,11 @@ const saveDrawerData = () => {
|
||||
|
||||
<a-form-item v-if="form.savePath&&form.savePath.length>0" label="下载合集" name="downMix">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 200px;">
|
||||
<div class="form-item-div">
|
||||
<a-switch v-model:checked="form.downMix" :checked-value="true" :un-checked-value="false" size="default" />
|
||||
<span style="margin-left:10px;">{{ form.downMix ? '是' : '否' }}</span>
|
||||
<!-- <a-input v-model:value="form.mixPath" class="form-item-div-input" /> -->
|
||||
|
||||
<a-button @click="openMixDownSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.downMix"> <setting-outlined />
|
||||
</a-button>
|
||||
</div>
|
||||
@@ -548,9 +584,11 @@ const saveDrawerData = () => {
|
||||
|
||||
<a-form-item v-if="form.savePath&&form.savePath.length>0" label="下载短剧" name="downSeries">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 200px;">
|
||||
<div class="form-item-div">
|
||||
<a-switch v-model:checked="form.downSeries" :checked-value="true" :un-checked-value="false" size="default" />
|
||||
<span style="margin-left:10px;">{{ form.downSeries ? '是' : '否' }}</span>
|
||||
<!-- <a-input v-model:value="form.seriesPath" class="form-item-div-input" /> -->
|
||||
|
||||
<a-button @click="openSeriesDownSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.downSeries"> <setting-outlined />
|
||||
</a-button>
|
||||
</div>
|
||||
@@ -559,29 +597,13 @@ const saveDrawerData = () => {
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 喜欢的存储路径 -->
|
||||
<a-form-item label="喜欢的存储路径" name="favSavePath">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<a-input v-model:value="form.favSavePath" :disabled="form.useSinglePath" placeholder="" style="width: 200px;" />
|
||||
<a-alert message="不想同步喜欢的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 关注的存储路径 -->
|
||||
<a-form-item label="关注的存储路径" name="upSavePath">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<a-input v-model:value="form.upSavePath" :disabled="form.useSinglePath" placeholder="" style="width: 200px;" />
|
||||
<a-alert message="不想同步关注列表博主的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 图文的存储路径 -->
|
||||
<a-form-item label="图文的存储路径" name="imgSavePath">
|
||||
<!-- <a-form-item label="图文的存储路径" name="imgSavePath">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<a-input v-model:value="form.imgSavePath" :disabled="form.useSinglePath" style="width: 200px;" />
|
||||
<a-alert message="如果系统配置页面开启了同步图文视频,且开启了单独存储,则必填!!!" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form-item> -->
|
||||
|
||||
<!-- 同步状态开关 -->
|
||||
<a-form-item label="同步状态" name="status">
|
||||
@@ -614,35 +636,39 @@ const saveDrawerData = () => {
|
||||
|
||||
<!-- 卡片网格展示 - 优化布局(横向一行展示名称、保存路径) -->
|
||||
<a-card v-else :bordered="false" class="drawer-card-container grid-container">
|
||||
<a-card-grid v-for="(item, index) in drawerDataList" :key="item.Id" class="drawer-card-grid">
|
||||
<a-card-grid v-for="(item, index) in drawerDataList" :key="item.id" class="drawer-card-grid">
|
||||
<!-- 竖向封面(移除预览功能,仅保留基础展示) -->
|
||||
<div class="grid-cover vertical-cover" v-if="drawerType!='collect'">
|
||||
<a-image :preview="false" :src="item.CoverUrl" fallback="https://placeholder.picsum.photos/120/180" fit="cover" />
|
||||
<a-image :preview="false" :src="item.coverUrl" fit="cover" />
|
||||
</div>
|
||||
|
||||
<!-- 名称:横向一行展示(标签 + 内容 在同一行) -->
|
||||
<div class="grid-item horizontal-item name">
|
||||
<label class="drawer-label">名称:</label>
|
||||
<span>{{ item.Name || '未命名' }}</span>
|
||||
<span>{{ item.name || '未命名' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 保存文件夹:横向一行展示(标签 + 输入框 在同一行) -->
|
||||
<div class="grid-item horizontal-item save-folder">
|
||||
<label class="drawer-label">保存路径:</label>
|
||||
<a-input v-model:value="item.SaveFolder" size="small" placeholder="请输入保存路径" class="save-path-input" />
|
||||
<label class="drawer-label">保存:</label>
|
||||
<a-input v-model:value="item.saveFolder" size="small" placeholder="默认用名称作文件夹" class="save-path-input" />
|
||||
</div>
|
||||
<div class="grid-item horizontal-item name">
|
||||
<label class="drawer-label">集数:</label>
|
||||
<span>{{ item.total || '0' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 同步开关(保持原有逻辑,横向对齐) -->
|
||||
<div class="grid-item horizontal-item sync-switch">
|
||||
<label class="drawer-label">是否同步:</label>
|
||||
<a-switch :checked="item.Sync" @change="() => toggleDrawerItemSync(item, index)" size="small" />
|
||||
<label class="drawer-label">同步:</label>
|
||||
<a-switch :checked="item.sync" @change="() => toggleDrawerItemSync(item, index)" size="small" />
|
||||
</div>
|
||||
</a-card-grid>
|
||||
</a-card>
|
||||
|
||||
<!-- 无更多数据提示 -->
|
||||
<div v-if="!drawerPagination.hasMore && drawerDataList.length > 0" class="no-more-data">
|
||||
已加载全部{{ getDrawerTypeName() }}数据
|
||||
<!-- 已加载全部{{ getDrawerTypeName() }}数据 -->
|
||||
</div>
|
||||
|
||||
<!-- 加载下一页中 -->
|
||||
@@ -756,6 +782,13 @@ const saveDrawerData = () => {
|
||||
.cookie-content::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
.form-item-div {
|
||||
width: 300px;
|
||||
}
|
||||
.form-item-div-input {
|
||||
width: 150px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
/* Firefox 透明滚动条适配 */
|
||||
.cookie-content {
|
||||
scrollbar-width: thin;
|
||||
|
||||
@@ -22,7 +22,7 @@ type ConfigItem = {
|
||||
secUserId: string;
|
||||
status: number;
|
||||
upSavePath: string;
|
||||
imgSavePath: string;
|
||||
// imgSavePath: string;
|
||||
useSinglePath: boolean; // 非可选
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ const newConfig = (config?: ConfigItem): ConfigItem => {
|
||||
secUserId: '',
|
||||
status: 0,
|
||||
upSavePath: '',
|
||||
imgSavePath: '',
|
||||
// imgSavePath: '',
|
||||
useSinglePath: false,
|
||||
};
|
||||
};
|
||||
@@ -61,7 +61,7 @@ watch(
|
||||
if (useSinglePath && newSavePath) {
|
||||
form.value.favSavePath = newSavePath;
|
||||
form.value.upSavePath = newSavePath;
|
||||
form.value.imgSavePath = newSavePath;
|
||||
// form.value.imgSavePath = newSavePath;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -216,9 +216,9 @@ const manualCheckForm = (): { pass: boolean; msg: string } => {
|
||||
<a-input v-model:value="form.upSavePath" placeholder="关注视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="!form.useSinglePath" label="图文存储路径" name="imgSavePath">
|
||||
<!-- <a-form-item v-if="!form.useSinglePath" label="图文存储路径" name="imgSavePath">
|
||||
<a-input v-model:value="form.imgSavePath" placeholder="图文视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||
</a-form-item>
|
||||
</a-form-item> -->
|
||||
|
||||
<!-- 同步状态开关 -->
|
||||
<a-form-item label="同步状态" name="status">
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<span>开启后,将图片文件和音频文件合成为视频文件</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formState.DownImageVideo" has-feedback label="单独存储" name="ImageViedoSaveAlone" :wrapper-col="{ span: 20 }">
|
||||
<!-- <a-form-item v-if="formState.DownImageVideo" has-feedback label="单独存储" name="ImageViedoSaveAlone" :wrapper-col="{ span: 20 }">
|
||||
<a-switch v-model:checked="formState.ImageViedoSaveAlone" />
|
||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||
@@ -96,7 +96,7 @@
|
||||
开启后,图文视频统一存入抖音授权 Cookie 配置的目录,且需提前配置该存储路径。关闭后,则按类型分别存入对应文件夹(如收藏视频存入收藏视频目录)
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form-item> -->
|
||||
<a-form-item v-if="formState.DownImageVideo" has-feedback label="保留音频" name="DownMp3" :wrapper-col="{ span: 20 }">
|
||||
<a-switch v-model:checked="formState.DownMp3" />
|
||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||
@@ -304,7 +304,7 @@ interface FormState {
|
||||
LogKeepDay: number;
|
||||
DownImage: boolean;
|
||||
DownMp3: boolean;
|
||||
ImageViedoSaveAlone: boolean;
|
||||
//ImageViedoSaveAlone: boolean;
|
||||
FollowedTitleTemplate: string[];
|
||||
FollowedTitleSeparator: string;
|
||||
FullFollowedTitleTemplate: string;
|
||||
@@ -327,7 +327,7 @@ const formState: UnwrapRef<FormState> = reactive({
|
||||
DownImageVideo: false,
|
||||
DownMp3: false,
|
||||
DownImage: false,
|
||||
ImageViedoSaveAlone: true,
|
||||
// ImageViedoSaveAlone: true,
|
||||
FollowedTitleTemplate: [],
|
||||
FollowedTitleSeparator: '',
|
||||
FullFollowedTitleTemplate: '',
|
||||
@@ -403,7 +403,7 @@ const getConfig = () => {
|
||||
FollowedTitleTemplate: parsedTemplateArr,
|
||||
FollowedTitleSeparator: res.data.followedTitleSeparator || '',
|
||||
FullFollowedTitleTemplate: fullTemplate,
|
||||
ImageViedoSaveAlone: res.data.imageViedoSaveAlone,
|
||||
// ImageViedoSaveAlone: res.data.imageViedoSaveAlone,
|
||||
AutoDistinct: res.data.autoDistinct,
|
||||
PriorityLevel: res.data.priorityLevel,
|
||||
DownDynamicVideo: res.data.downDynamicVideo,
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
</a-modal>
|
||||
|
||||
<!-- 表格 - 增加复选框和操作列 -->
|
||||
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading" :row-selection="isBatchMode ? rowSelection : null" row-key="id">
|
||||
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading" :row-selection="isBatchMode ? rowSelection : null" row-key="id" :sorter="true">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'videoTitle'">
|
||||
<a class="video-title-link" :title="record.videoTitle || '无标题'" @click="handleVideoClick(record)" @mouseenter="handleTitleMouseEnter" @mouseleave="handleTitleMouseLeave">
|
||||
@@ -206,6 +206,11 @@ interface DataItem {
|
||||
isMergeVideo?: boolean;
|
||||
}
|
||||
|
||||
// 📌 新增:排序参数类型定义
|
||||
interface SortParam {
|
||||
field: string; // 排序字段
|
||||
order: 'ascend' | 'descend' | ''; // 排序方向:升序/降序/无
|
||||
}
|
||||
interface QuaryParam {
|
||||
dates?: string[];
|
||||
dates2?: string[];
|
||||
@@ -216,6 +221,8 @@ interface QuaryParam {
|
||||
viedoType: string;
|
||||
fileHash: string;
|
||||
authorId: string;
|
||||
sortField?: string; // 📌 新增:排序字段
|
||||
sortOrder?: string; // 📌 新增:排序方向(asc/desc)
|
||||
}
|
||||
|
||||
// 引入dayjs中文包
|
||||
@@ -226,6 +233,11 @@ dayjs.locale('zh-cn');
|
||||
// 批量操作相关状态
|
||||
const isBatchMode = ref(false); // 批量操作开关状态
|
||||
const selectedRowKeys = ref<string[]>([]); // 选中的行ID集合
|
||||
// 📌 新增:排序状态管理
|
||||
const sortParams = ref<SortParam>({
|
||||
field: 'syncTime', // 默认排序字段(发布时间)
|
||||
order: 'descend', // 默认降序(最新的在前)
|
||||
});
|
||||
|
||||
// 表格行选择器类型定义(对齐 Ant Design Vue 3.x 规范)
|
||||
interface CustomTableRowSelection<T> {
|
||||
@@ -255,7 +267,6 @@ const rowSelection = computed<CustomTableRowSelection<DataItem>>(() => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// 表格列配置(优化:临时注释 fixed: right 避免渲染冲突)
|
||||
const columns = ref([
|
||||
{
|
||||
title: '同步时间',
|
||||
@@ -268,6 +279,13 @@ const columns = ref([
|
||||
dataIndex: 'createTimeStr',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
sorter: true,
|
||||
sortOrder: sortParams.value.field === 'createTime' ? sortParams.value.order : null,
|
||||
onHeaderCell: () => ({
|
||||
onClick: () => {
|
||||
handleSortChange('createTime');
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '同步类型',
|
||||
@@ -280,6 +298,13 @@ const columns = ref([
|
||||
dataIndex: 'author',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sorter: true,
|
||||
sortOrder: sortParams.value.field === 'author' ? sortParams.value.order : null,
|
||||
onHeaderCell: () => ({
|
||||
onClick: () => {
|
||||
handleSortChange('author');
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '视频类型',
|
||||
@@ -304,10 +329,32 @@ const columns = ref([
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
// fixed: 'right', // 注释:避免固定列导致的重绘卡顿,如需使用可后续调试
|
||||
},
|
||||
]);
|
||||
|
||||
// 📌 新增:排序切换方法
|
||||
const handleSortChange = (field: string) => {
|
||||
// 如果点击的是当前排序字段,切换排序方向
|
||||
if (sortParams.value.field === field) {
|
||||
sortParams.value.order = sortParams.value.order === 'ascend' ? 'descend' : 'ascend';
|
||||
} else {
|
||||
// 如果是新的排序字段,默认降序
|
||||
sortParams.value.field = field;
|
||||
sortParams.value.order = 'descend';
|
||||
}
|
||||
|
||||
// 更新表格列的排序状态(刷新排序图标)
|
||||
columns.value.forEach((col) => {
|
||||
if (col.dataIndex === 'createTimeStr') {
|
||||
col.sortOrder = sortParams.value.order;
|
||||
} else {
|
||||
col.sortOrder = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 重新查询数据(传递排序参数)
|
||||
GetRecords();
|
||||
};
|
||||
// 监听批量操作开关状态变化,清空选中状态+强制表格重绘
|
||||
watch(isBatchMode, (isOpen) => {
|
||||
if (!isOpen) {
|
||||
@@ -347,6 +394,8 @@ const quaryData: UnwrapRef<QuaryParam> = reactive({
|
||||
viedoType: '*',
|
||||
authorId: '',
|
||||
fileHash: '',
|
||||
sortField: 'createTime', // 📌 默认排序字段
|
||||
sortOrder: 'desc', // 📌 默认降序
|
||||
});
|
||||
|
||||
// 分页配置
|
||||
@@ -438,6 +487,10 @@ const GetRecords = () => {
|
||||
if (value2.value) {
|
||||
quaryData.dates2 = value2.value.map((date) => date.format('YYYY-MM-DD')); // 修复:之前误写为value1
|
||||
}
|
||||
// 📌 关键:将前端排序状态转换为后端需要的参数
|
||||
quaryData.sortField = sortParams.value.field;
|
||||
// 转换排序方向(antd的ascend/descend 转 后端常用的asc/desc)
|
||||
quaryData.sortOrder = sortParams.value.order === 'ascend' ? 'asc' : 'desc';
|
||||
useApiStore()
|
||||
.VideoPageList(quaryData)
|
||||
.then((res) => {
|
||||
@@ -459,6 +512,45 @@ const GetRecords = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 📌 修改表格变化处理:支持分页时保留排序状态
|
||||
const handleTableChange = (paginationObj: any, filters: any, sorter: any) => {
|
||||
pagination.value.current = paginationObj.current;
|
||||
pagination.value.defaultPageSize = paginationObj.pageSize;
|
||||
|
||||
// 如果是排序变化(用户点击表头排序)
|
||||
if (sorter.field) {
|
||||
// 📌 处理不同列的字段映射
|
||||
if (sorter.field === 'createTimeStr') {
|
||||
sortParams.value.field = 'createTime'; // 映射到后端的createTime字段
|
||||
} else if (sorter.field === 'author') {
|
||||
sortParams.value.field = 'author'; // 博主列直接使用author字段
|
||||
} else {
|
||||
sortParams.value.field = sorter.field;
|
||||
}
|
||||
sortParams.value.order = sorter.order;
|
||||
|
||||
// 更新所有列的排序状态
|
||||
columns.value.forEach((col) => {
|
||||
if (col.dataIndex === sorter.field) {
|
||||
col.sortOrder = sorter.order;
|
||||
} else if (col.dataIndex === 'createTimeStr' && sorter.field === 'createTime') {
|
||||
col.sortOrder = sorter.order;
|
||||
} else if (col.dataIndex === 'author' && sorter.field === 'author') {
|
||||
col.sortOrder = sorter.order;
|
||||
} else {
|
||||
col.sortOrder = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 分页变化时清空选中状态
|
||||
if (isBatchMode.value) {
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
/** 立即同步 */
|
||||
const StartNow = () => {
|
||||
if (isSyncing.value) return;
|
||||
@@ -496,15 +588,15 @@ const datePicked2 = (_, dateArry: RangeValue) => {
|
||||
};
|
||||
|
||||
/** 表格分页/排序变化事件 */
|
||||
const handleTableChange = (paginationObj: any) => {
|
||||
pagination.value.current = paginationObj.current;
|
||||
pagination.value.defaultPageSize = paginationObj.pageSize;
|
||||
// 分页变化时清空选中状态(跨页不保留)
|
||||
if (isBatchMode.value) {
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
GetRecords();
|
||||
};
|
||||
// const handleTableChange = (paginationObj: any) => {
|
||||
// pagination.value.current = paginationObj.current;
|
||||
// pagination.value.defaultPageSize = paginationObj.pageSize;
|
||||
// // 分页变化时清空选中状态(跨页不保留)
|
||||
// if (isBatchMode.value) {
|
||||
// selectedRowKeys.value = [];
|
||||
// }
|
||||
// GetRecords();
|
||||
// };
|
||||
|
||||
/** 视频类型切换事件 */
|
||||
const onViedoTypeChanged = () => {
|
||||
@@ -1452,4 +1544,13 @@ onMounted(() => {
|
||||
height: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 📌 新增:博主列排序图标样式优化(和发布时间列保持一致) */
|
||||
:deep(.ant-table-column-title[data-column-key='author']) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.ant-table-column-title[data-column-key='author']:hover) {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,940 @@
|
||||
<template>
|
||||
<div class="stats-dashboard">
|
||||
<div class="dashboard-container">
|
||||
<!-- 核心统计概览 - 美化版 -->
|
||||
<section class="stats-overview">
|
||||
<!-- 总视频数卡片 -->
|
||||
<div class="stat-card primary-card main-card">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="stat-header">
|
||||
<div class="header-left">
|
||||
<span class="stat-meta">视频总数</span>
|
||||
<div class="stat-value">{{ totalVideos }}</div>
|
||||
</div>
|
||||
<div class="stat-icon video-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 细分项区域 - 新增合集、短剧统计项 -->
|
||||
<div class="stat-subitems">
|
||||
<div class="subitem" :title="`我喜欢的视频数: ${favoriteCount}`">
|
||||
<div class="subitem-icon like-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">我喜欢的</span>
|
||||
<span class="subitem-value">{{ favoriteCount }}</span>
|
||||
</div>
|
||||
|
||||
<div class="subitem" :title="`我收藏的视频数: ${collectCount}`">
|
||||
<div class="subitem-icon collect-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">我收藏的</span>
|
||||
<span class="subitem-value">{{ collectCount }}</span>
|
||||
</div>
|
||||
|
||||
<div class="subitem" :title="`我关注的视频数: ${followCount}`">
|
||||
<div class="subitem-icon follow-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="8.5" cy="7" r="4"></circle>
|
||||
<line x1="20" y1="8" x2="20" y2="14"></line>
|
||||
<line x1="23" y1="11" x2="17" y2="11"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">我关注的</span>
|
||||
<span class="subitem-value">{{ followCount }}</span>
|
||||
</div>
|
||||
|
||||
<div class="subitem" :title="`图文视频数: ${graphicVideoCount}`">
|
||||
<div class="subitem-icon graphic-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">图文视频</span>
|
||||
<span class="subitem-value">{{ graphicVideoCount }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 新增:合集数量 -->
|
||||
<div class="subitem" :title="`合集数量: ${mixCount}`">
|
||||
<div class="subitem-icon mix-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">合集数量</span>
|
||||
<span class="subitem-value">{{ mixCount }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 新增:短剧数量 -->
|
||||
<div class="subitem" :title="`短剧数量: ${seriesCount}`">
|
||||
<div class="subitem-icon series-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">短剧数量</span>
|
||||
<span class="subitem-value">{{ seriesCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 总占用空间卡片 -->
|
||||
<div class="stat-card secondary-card main-card">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="stat-header">
|
||||
<div class="header-left">
|
||||
<span class="stat-meta">空间总计</span>
|
||||
<div class="stat-value">{{ fileSizeTotal }} <span class="unit">G</span></div>
|
||||
</div>
|
||||
<div class="stat-icon size-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 12H2v8h20v-8z" />
|
||||
<path d="M6 18h.01" />
|
||||
<path d="M10 18h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 细分项区域 - 新增合集、短剧空间占用 -->
|
||||
<div class="stat-subitems">
|
||||
<div class="subitem" :title="`喜欢的视频占用: ${favoriteSize}G`">
|
||||
<div class="subitem-icon like-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">喜欢占用</span>
|
||||
<span class="subitem-value">{{ favoriteSize }} <span class="unit">G</span></span>
|
||||
</div>
|
||||
|
||||
<div class="subitem" :title="`收藏的视频占用: ${collectSize}G`">
|
||||
<div class="subitem-icon collect-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">收藏占用</span>
|
||||
<span class="subitem-value">{{ collectSize }} <span class="unit">G</span></span>
|
||||
</div>
|
||||
|
||||
<div class="subitem" :title="`关注的视频占用: ${followSize}G`">
|
||||
<div class="subitem-icon follow-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="8.5" cy="7" r="4"></circle>
|
||||
<line x1="20" y1="8" x2="20" y2="14"></line>
|
||||
<line x1="23" y1="11" x2="17" y2="11"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">关注占用</span>
|
||||
<span class="subitem-value">{{ followSize }} <span class="unit">G</span></span>
|
||||
</div>
|
||||
|
||||
<div class="subitem" :title="`图文视频占用: ${graphicVideoSize}G`">
|
||||
<div class="subitem-icon graphic-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">图文占用</span>
|
||||
<span class="subitem-value">{{ graphicVideoSize }} <span class="unit">G</span></span>
|
||||
</div>
|
||||
|
||||
<!-- 新增:合集空间占用 -->
|
||||
<div class="subitem" :title="`合集占用: ${videoMixSize}G`">
|
||||
<div class="subitem-icon mix-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">合集占用</span>
|
||||
<span class="subitem-value">{{ videoMixSize }} <span class="unit">G</span></span>
|
||||
</div>
|
||||
|
||||
<!-- 新增:短剧空间占用 -->
|
||||
<div class="subitem" :title="`短剧占用: ${videoSeriesSize}G`">
|
||||
<div class="subitem-icon series-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="subitem-meta">短剧占用</span>
|
||||
<span class="subitem-value">{{ videoSeriesSize }} <span class="unit">G</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 详细分类统计(保持不变) -->
|
||||
<section class="detailed-stats">
|
||||
<div class="stats-header">
|
||||
<div class="tab-controls">
|
||||
<a-badge :count="totalAuthors">
|
||||
<button class="tab-btn" :class="{ active: currentTab === 'author' }" @click="changeTab('author')">
|
||||
视频作者
|
||||
</button>
|
||||
</a-badge>
|
||||
<a-badge :count="categoryTotal">
|
||||
<button class="tab-btn" :class="{ active: currentTab === 'type' }" @click="changeTab('type')">
|
||||
视频分类
|
||||
</button>
|
||||
</a-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="stats-fade" mode="out-in">
|
||||
<!-- 作者统计 -->
|
||||
<div v-if="currentTab === 'author'" key="author-view" class="stats-content">
|
||||
<div class="authors-grid">
|
||||
<div class="author-card" v-for="(author, index) in authors" :key="index" @dblclick="handleDeleteItem(author)">
|
||||
<!-- 新增横向容器:包裹头像和作者信息 -->
|
||||
<div class="author-info-row">
|
||||
<div class="author-avatar">
|
||||
<img :src="author.icon" alt="作者头像" />
|
||||
</div>
|
||||
<div class="author-info">
|
||||
<h3 class="author-name">{{ author.name }}</h3>
|
||||
<p class="author-stats">同步数量: {{ author.count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条独立在横向容器下方,不与头像同行 -->
|
||||
<div class="author-progress">
|
||||
<div class="progress-bar" :style="{ width: `${(author.count / totalVideos) * 100}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分类统计 -->
|
||||
<div v-else key="category-view" class="stats-content">
|
||||
<div class="categories-grid">
|
||||
<div class="category-card" v-for="(category, index) in categories" :key="index" :style="{ '--category-color': category.color }">
|
||||
<div class="category-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 12 12" fill="white">
|
||||
<use :xlink:href="`#${category.icon}`"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="category-info">
|
||||
<h3 class="category-name">{{ category.name }}</h3>
|
||||
<p class="category-stats">作品数: {{ category.count }}</p>
|
||||
</div>
|
||||
<div class="category-percentage">
|
||||
{{ Math.round((category.count / totalVideos) * 100) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- SVG图标定义 -->
|
||||
<svg style="display: none;">
|
||||
<symbol id="cup" viewBox="0 0 12 12">
|
||||
<path d="M18 4h2v16h-2zM4 4h14v2H4zM4 8h10v2H4zM4 12h10v2H4zM4 16h6v2H4zM4 20h6v2H4z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
import { message, Spin, Empty, Tooltip, Modal, Form, FormInstance, Popconfirm } from 'ant-design-vue';
|
||||
// 类型接口
|
||||
interface Author {
|
||||
name: string;
|
||||
count: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
name: string;
|
||||
count: number;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const totalVideos = ref<number>(0);
|
||||
const totalAuthors = ref<number>(0);
|
||||
const categoryTotal = ref<number>(0);
|
||||
const fileSizeTotal = ref<string>('0.00');
|
||||
const totalDiskSize = ref<string>('0.00');
|
||||
|
||||
const favoriteCount = ref<number>(0);
|
||||
const collectCount = ref<number>(0);
|
||||
const followCount = ref<number>(0);
|
||||
const graphicVideoCount = ref<number>(0);
|
||||
// 新增:合集数量、短剧数量
|
||||
const mixCount = ref<number>(0);
|
||||
const seriesCount = ref<number>(0);
|
||||
|
||||
const favoriteSize = ref<string>('0.00');
|
||||
const collectSize = ref<string>('0.00');
|
||||
const followSize = ref<string>('0.00');
|
||||
const graphicVideoSize = ref<string>('0.00');
|
||||
// 新增:合集占用空间、短剧占用空间
|
||||
const videoMixSize = ref<string>('0.00');
|
||||
const videoSeriesSize = ref<string>('0.00');
|
||||
|
||||
const categories = ref<Category[]>([]);
|
||||
const authors = ref<Author[]>([]);
|
||||
const currentTab = ref<string>('author');
|
||||
const tabCount = ref<number>(0);
|
||||
|
||||
// 组件名称
|
||||
defineOptions({
|
||||
name: 'StatsDashboard',
|
||||
});
|
||||
|
||||
// 生成随机十六进制颜色的工具函数
|
||||
const generateRandomColor = () => {
|
||||
// 生成0-255的随机RGB值,转换为十六进制并补零
|
||||
const randomHex = () =>
|
||||
Math.floor(Math.random() * 256)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
return `#${randomHex()}${randomHex()}${randomHex()}`;
|
||||
};
|
||||
|
||||
// 加载数据和切换标签逻辑
|
||||
onMounted(() => {
|
||||
loadDashboardData();
|
||||
});
|
||||
|
||||
const changeTab = (e: any) => {
|
||||
currentTab.value = e;
|
||||
if (e == 'author') {
|
||||
tabCount.value = totalAuthors.value;
|
||||
} else {
|
||||
tabCount.value = categoryTotal.value;
|
||||
}
|
||||
};
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
const res = await useApiStore().VideoStatics();
|
||||
totalAuthors.value = res.data.authorCount;
|
||||
categoryTotal.value = res.data.categoryCount;
|
||||
totalVideos.value = res.data.videoCount;
|
||||
fileSizeTotal.value = res.data.videoSizeTotal || '0.00';
|
||||
totalDiskSize.value = res.data.totalDiskSize || '0.00';
|
||||
|
||||
favoriteCount.value = res.data.favoriteCount;
|
||||
collectCount.value = res.data.collectCount;
|
||||
followCount.value = res.data.followCount || 0;
|
||||
graphicVideoCount.value = res.data.graphicVideoCount || 0;
|
||||
// 新增:从接口获取合集、短剧数量
|
||||
mixCount.value = res.data.mixCount || 0;
|
||||
seriesCount.value = res.data.seriesCount || 0;
|
||||
|
||||
favoriteSize.value = res.data.videoFavoriteSize || '0.00';
|
||||
collectSize.value = res.data.videoCollectSize || '0.00';
|
||||
followSize.value = res.data.videoFollowSize || '0.00';
|
||||
graphicVideoSize.value = res.data.graphicVideoSize || '0.00';
|
||||
// 新增:从接口获取合集、短剧空间占用
|
||||
videoMixSize.value = res.data.videoMixSize || '0.00';
|
||||
videoSeriesSize.value = res.data.videoSeriesSize || '0.00';
|
||||
|
||||
categories.value = res.data.categories;
|
||||
authors.value = res.data.authors;
|
||||
|
||||
// 移除categoriessss相关逻辑,直接给分类设置固定图标cup(保证显示)
|
||||
// 动态为每个分类生成随机颜色,替代原有的colorArray
|
||||
categories.value.forEach((item) => {
|
||||
item.icon = 'cup'; // 固定使用已定义的cup图标,确保显示正常
|
||||
item.color = generateRandomColor(); // 随机生成颜色
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('加载仪表盘数据失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getRandomElements = (arr: any[], n: number) => {
|
||||
if (n <= 0) return [];
|
||||
if (n >= arr.length) return [...arr];
|
||||
return [...arr].sort(() => Math.random() - 0.5).slice(0, n);
|
||||
};
|
||||
|
||||
const handleDeleteItem = (item: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除博主「${item.name}」所有视频吗?删除后将无法恢复。`,
|
||||
okText: '确认删除',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
maskClosable: false,
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
useApiStore()
|
||||
.DeleteByAuthor(item.uperId)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
message.success('根据视频数量,需要时常不确定,可以稍后去日志查看...');
|
||||
resolve(true);
|
||||
} else {
|
||||
message.error('删除博主视频失败' + (res.message || '未知错误'));
|
||||
reject(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('删除博主视频异常', err);
|
||||
message.error('删除博主视频异常' + err);
|
||||
reject(false);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
/* 基础样式 */
|
||||
.stats-dashboard {
|
||||
min-height: 100vh;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 1700px) {
|
||||
.dashboard-container {
|
||||
max-width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 核心概览区域 - 调整网格布局适配新增的2个统计项 */
|
||||
.stats-overview {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 25px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* 主卡片样式 - 增强边框可见性 */
|
||||
.main-card {
|
||||
padding: 28px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* 白天模式添加明显边框 */
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* 卡片hover效果 */
|
||||
.main-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.12);
|
||||
/* hover时边框颜色加深 */
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* 卡片元数据 */
|
||||
.stat-meta {
|
||||
font-size: 15px;
|
||||
color: #666666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 卡片主数值 */
|
||||
.stat-value {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 单位样式 */
|
||||
.unit {
|
||||
font-size: 22px;
|
||||
color: #444444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 卡片图标 */
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(76, 175, 80, 0.15);
|
||||
color: #4caf50;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 空间卡片图标颜色 */
|
||||
.secondary-card .stat-icon {
|
||||
background-color: rgba(33, 150, 243, 0.15);
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
/* 卡片hover时图标缩放 */
|
||||
.main-card:hover .stat-icon {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
/* 细分项容器 - 增强分隔线,调整网格布局为3列适配6个统计项 */
|
||||
.stat-subitems {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr); /* 从2列改为3列,适配新增的2个统计项 */
|
||||
gap: 15px;
|
||||
padding-top: 20px;
|
||||
/* 白天模式使用明显的分隔线 */
|
||||
border-top: 1px solid #d0d0d0;
|
||||
}
|
||||
|
||||
/* 细分项样式 - 增强边框和背景 */
|
||||
.subitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
/* 白天模式添加白色背景和明显边框 */
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.2s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 细分项hover效果 - 增强边框和阴影 */
|
||||
.subitem:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.07);
|
||||
border-color: #c0c0c0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 细分项图标 */
|
||||
.subitem-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(233, 30, 99, 0.1);
|
||||
color: #e91e63;
|
||||
}
|
||||
|
||||
/* 收藏图标颜色 */
|
||||
.collect-icon {
|
||||
background-color: rgba(255, 152, 0, 0.1);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
/* 关注图标颜色 */
|
||||
.follow-icon {
|
||||
background-color: rgba(156, 39, 176, 0.1);
|
||||
color: #9c27b0;
|
||||
}
|
||||
|
||||
/* 图文图标颜色 */
|
||||
.graphic-icon {
|
||||
background-color: rgba(255, 159, 64, 0.1);
|
||||
color: #d9091a;
|
||||
}
|
||||
|
||||
/* 新增:合集图标样式 */
|
||||
.mix-icon {
|
||||
background-color: rgba(63, 81, 181, 0.1);
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
||||
/* 新增:短剧图标样式 */
|
||||
.series-icon {
|
||||
background-color: rgba(0, 188, 212, 0.1);
|
||||
color: #00bcd4;
|
||||
}
|
||||
|
||||
/* 细分项元数据 */
|
||||
.subitem-meta {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.3px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 细分项数值 */
|
||||
.subitem-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222222;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 细分项单位 */
|
||||
.subitem-value .unit {
|
||||
font-size: 12px;
|
||||
color: #555555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 卡片顶部主题边框 */
|
||||
.primary-card {
|
||||
border-top: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.secondary-card {
|
||||
border-top: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
/* 详细分类统计区域 */
|
||||
.detailed-stats {
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.tab-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #666666;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.authors-grid,
|
||||
.categories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.authors-grid,
|
||||
.categories-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.authors-grid,
|
||||
.categories-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 作者卡片样式 - 核心修改:头像+文字横向,进度条独立在下 */
|
||||
.author-card {
|
||||
display: flex;
|
||||
flex-direction: column; /* 整体纵向布局,容纳「头像+文字行」和「进度条行」 */
|
||||
gap: 10px; /* 头像文字行 与 进度条 之间的间距,可微调 */
|
||||
padding: 15px;
|
||||
background: #eeeeee;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.author-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 头像+文字 横向容器 */
|
||||
.author-info-row {
|
||||
display: flex;
|
||||
align-items: center; /* 头像与文字垂直居中 */
|
||||
gap: 12px; /* 头像与文字的间距,减少空白 */
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0; /* 防止头像被压缩 */
|
||||
}
|
||||
|
||||
.author-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 作者文字信息(名字+作品数) */
|
||||
.author-info {
|
||||
flex: 1; /* 占据头像右侧剩余空间 */
|
||||
display: flex;
|
||||
flex-direction: column; /* 名字在上,作品数在下,紧凑排列 */
|
||||
gap: 3px; /* 名字与作品数的间距,减少空白 */
|
||||
}
|
||||
|
||||
.author-name,
|
||||
.category-name {
|
||||
margin: 0; /* 移除默认外边距,消除多余空白 */
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.author-stats,
|
||||
.category-stats {
|
||||
margin: 5px 0px; /* 移除默认外边距 */
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
line-height: 1; /* 紧凑行高,减少垂直空白 */
|
||||
}
|
||||
|
||||
/* 进度条 - 独立成行,不与头像同行 */
|
||||
.author-progress {
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
width: 100%; /* 占满作者卡片宽度 */
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: #4caf50;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* 分类卡片样式 */
|
||||
.category-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: #eeeeee;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--category-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-percentage {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--category-color);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.stats-fade-enter-from,
|
||||
.stats-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.stats-fade-enter-active,
|
||||
.stats-fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 夜间模式样式 - 保持原有效果 */
|
||||
html.dark-mode .main-card {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
background-color: rgba(30, 30, 50, 0.9);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .main-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
html.dark-mode .stat-subitems {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
html.dark-mode .subitem {
|
||||
background: rgba(40, 40, 65, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
html.dark-mode .subitem:hover {
|
||||
background: rgba(40, 40, 65, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 夜间模式其他样式保持不变 */
|
||||
html.dark-mode .stats-dashboard {
|
||||
background-color: #1a1a2e;
|
||||
color: #eaeaea;
|
||||
}
|
||||
|
||||
html.dark-mode .stat-meta {
|
||||
color: #b0b0c3;
|
||||
}
|
||||
|
||||
html.dark-mode .stat-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .unit {
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
html.dark-mode .stat-icon {
|
||||
background-color: rgba(76, 175, 80, 0.25);
|
||||
}
|
||||
|
||||
html.dark-mode .secondary-card .stat-icon {
|
||||
background-color: rgba(33, 150, 243, 0.25);
|
||||
}
|
||||
|
||||
html.dark-mode .subitem-meta {
|
||||
color: #c0c0d3;
|
||||
}
|
||||
|
||||
html.dark-mode .subitem-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .subitem-value .unit {
|
||||
color: #b0b0c3;
|
||||
}
|
||||
|
||||
html.dark-mode .subitem-icon {
|
||||
background-color: rgba(233, 30, 99, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .collect-icon {
|
||||
background-color: rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .follow-icon {
|
||||
background-color: rgba(156, 39, 176, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .graphic-icon {
|
||||
background-color: rgba(255, 159, 64, 0.2);
|
||||
}
|
||||
|
||||
/* 新增:夜间模式下合集、短剧图标样式 */
|
||||
html.dark-mode .mix-icon {
|
||||
background-color: rgba(63, 81, 181, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .series-icon {
|
||||
background-color: rgba(0, 188, 212, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .detailed-stats {
|
||||
background: rgba(30, 30, 50, 0.8);
|
||||
}
|
||||
|
||||
html.dark-mode .author-card,
|
||||
html.dark-mode .category-card {
|
||||
background: rgba(40, 40, 65, 0.6);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
html.dark-mode .author-name,
|
||||
html.dark-mode .category-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
+484
-369
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,7 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
});
|
||||
}
|
||||
//视频统计
|
||||
async function VideoStatics() {
|
||||
return http.request<any, Response<any>>('/api/video/statics', 'get').then(r => {
|
||||
return r;
|
||||
@@ -102,6 +103,14 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
});
|
||||
}
|
||||
//视频曲线
|
||||
async function VideoChart() {
|
||||
return http.request<any, Response<any>>('/api/video/chart', 'get').then(r => {
|
||||
return r;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
//视频查询
|
||||
async function VideoPageList(param: object) {
|
||||
@@ -337,6 +346,22 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
});
|
||||
}
|
||||
|
||||
//合集、自定义收藏夹、短剧列表
|
||||
async function CatePageList(param: object) {
|
||||
return http.request<any, Response<any>>('/api/cate/paged', 'post_json', param).then(r => {
|
||||
return r;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
//批量修改 合集、自定义收藏夹、短剧
|
||||
async function BatchSaveCate(param: object) {
|
||||
return http.request<any, Response<any>>('/api/cate/BatchSave', 'post_json', param).then(r => {
|
||||
return r;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// // 音频文件上传接口
|
||||
// async function apiUploadAudio(formData: FormData, options?: { onUploadProgress?: (progressEvent: ProgressEvent) => void }) {
|
||||
@@ -362,6 +387,9 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
// }
|
||||
|
||||
return {
|
||||
VideoChart,
|
||||
BatchSaveCate,
|
||||
CatePageList,
|
||||
getVer,
|
||||
mp3List,
|
||||
BathRealDelete,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
@@ -106,6 +106,12 @@
|
||||
<HintPath>lib\dy.sync.lib.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||
<!--<Exec WorkingDirectory="$(SpaRoot)" Command="yarn install" />
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ActiveDebugProfile>tiny.ddns</ActiveDebugProfile>
|
||||
<NameOfLastUsedPublishProfile>E:\gitea\dysync\Properties\PublishProfiles\docker.pubxml</NameOfLastUsedPublishProfile>
|
||||
<ActiveDebugProfile>dy.net</ActiveDebugProfile>
|
||||
<NameOfLastUsedPublishProfile>E:\code\dysync\Properties\PublishProfiles\docker.pubxml</NameOfLastUsedPublishProfile>
|
||||
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -220,6 +220,7 @@ namespace dy.net.extension
|
||||
services.AddHttpClient(DouyinRequestParamManager.DY_HTTP_CLIENT, client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("User-Agent", DouyinRequestParamManager.DY_USER_AGENT);
|
||||
client.BaseAddress = new Uri("https://www.douyin.com");
|
||||
}).ConfigurePrimaryHttpMessageHandler(ignoreSslHandlerFactory);
|
||||
|
||||
// 抖音下载客户端(单独配置,保持原有特性)
|
||||
|
||||
@@ -5,105 +5,107 @@ using Serilog;
|
||||
using static Quartz.Logging.OperationName;
|
||||
|
||||
namespace dy.net.job
|
||||
{ /// <summary>
|
||||
/// 抖音任务依赖监听器(独立公共类)
|
||||
/// 作用:监听任务执行完成事件,触发下一个依赖任务,实现顺序执行
|
||||
/// </summary>
|
||||
public class DouyinJobDependencyListener : IJobListener
|
||||
{
|
||||
// 监听器名称(唯一标识,不可重复)
|
||||
public string Name => "DouyinJobDependencyListener";
|
||||
{
|
||||
|
||||
// /// <summary>
|
||||
///// 抖音任务依赖监听器(独立公共类)
|
||||
///// 作用:监听任务执行完成事件,触发下一个依赖任务,实现顺序执行
|
||||
///// </summary>
|
||||
// public class DouyinJobDependencyListener : IJobListener
|
||||
// {
|
||||
// // 监听器名称(唯一标识,不可重复)
|
||||
// public string Name => "DouyinJobDependencyListener";
|
||||
|
||||
/// <summary>
|
||||
/// 任务配置字典(从外部注入)
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, JobConfig> _jobConfigs;
|
||||
// /// <summary>
|
||||
// /// 任务配置字典(从外部注入)
|
||||
// /// </summary>
|
||||
// private readonly Dictionary<string, JobConfig> _jobConfigs;
|
||||
|
||||
/// <summary>
|
||||
/// 任务依赖关系(从外部注入,定义执行顺序)
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string> _jobDependency;
|
||||
// /// <summary>
|
||||
// /// 任务依赖关系(从外部注入,定义执行顺序)
|
||||
// /// </summary>
|
||||
// private readonly Dictionary<string, string> _jobDependency;
|
||||
|
||||
/// <summary>
|
||||
/// 任务服务(用于触发下一个任务,从外部注入)
|
||||
/// </summary>
|
||||
private readonly DouyinQuartzJobService _jobService;
|
||||
// /// <summary>
|
||||
// /// 任务服务(用于触发下一个任务,从外部注入)
|
||||
// /// </summary>
|
||||
// private readonly DouyinQuartzJobService _jobService;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数(依赖注入)
|
||||
/// </summary>
|
||||
/// <param name="jobConfigs">任务配置</param>
|
||||
/// <param name="jobDependency">任务依赖关系</param>
|
||||
/// <param name="jobService">任务服务</param>
|
||||
public DouyinJobDependencyListener(
|
||||
Dictionary<string, JobConfig> jobConfigs,
|
||||
Dictionary<string, string> jobDependency,
|
||||
DouyinQuartzJobService jobService)
|
||||
{
|
||||
_jobConfigs = jobConfigs ?? throw new ArgumentNullException(nameof(jobConfigs), "任务配置不能为空");
|
||||
_jobDependency = jobDependency ?? throw new ArgumentNullException(nameof(jobDependency), "任务依赖关系不能为空");
|
||||
_jobService = jobService ?? throw new ArgumentNullException(nameof(jobService), "任务服务不能为空");
|
||||
}
|
||||
// /// <summary>
|
||||
// /// 构造函数(依赖注入)
|
||||
// /// </summary>
|
||||
// /// <param name="jobConfigs">任务配置</param>
|
||||
// /// <param name="jobDependency">任务依赖关系</param>
|
||||
// /// <param name="jobService">任务服务</param>
|
||||
// public DouyinJobDependencyListener(
|
||||
// Dictionary<string, JobConfig> jobConfigs,
|
||||
// Dictionary<string, string> jobDependency,
|
||||
// DouyinQuartzJobService jobService)
|
||||
// {
|
||||
// _jobConfigs = jobConfigs ?? throw new ArgumentNullException(nameof(jobConfigs), "任务配置不能为空");
|
||||
// _jobDependency = jobDependency ?? throw new ArgumentNullException(nameof(jobDependency), "任务依赖关系不能为空");
|
||||
// _jobService = jobService ?? throw new ArgumentNullException(nameof(jobService), "任务服务不能为空");
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 任务执行前触发(无需处理)
|
||||
/// </summary>
|
||||
public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// 任务执行前触发(无需处理)
|
||||
// /// </summary>
|
||||
// public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// return Task.CompletedTask;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 任务被否决执行时触发(无需处理)
|
||||
/// </summary>
|
||||
public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// 任务被否决执行时触发(无需处理)
|
||||
// /// </summary>
|
||||
// public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// return Task.CompletedTask;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 任务执行完成后触发(核心逻辑:触发下一个依赖任务)
|
||||
/// </summary>
|
||||
public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var currentJobKey = context.JobDetail.Key;
|
||||
Log.Information("【任务监听】任务执行完成 - 任务名称: {JobName}, 执行状态: {Status}",
|
||||
currentJobKey.Name, jobException == null ? "成功" : "失败");
|
||||
// /// <summary>
|
||||
// /// 任务执行完成后触发(核心逻辑:触发下一个依赖任务)
|
||||
// /// </summary>
|
||||
// public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// var currentJobKey = context.JobDetail.Key;
|
||||
// Log.Information("【任务监听】任务执行完成 - 任务名称: {JobName}, 执行状态: {Status}",
|
||||
// currentJobKey.Name, jobException == null ? "成功" : "失败");
|
||||
|
||||
// 1. 若当前任务执行失败,终止后续依赖任务(避免无效执行)
|
||||
if (jobException != null)
|
||||
{
|
||||
Log.Error(jobException, "【任务监听】任务 {JobName} 执行失败,终止后续任务链条", currentJobKey.Name);
|
||||
return;
|
||||
}
|
||||
// // 1. 若当前任务执行失败,终止后续依赖任务(避免无效执行)
|
||||
// if (jobException != null)
|
||||
// {
|
||||
// Log.Error(jobException, "【任务监听】任务 {JobName} 执行失败,终止后续任务链条", currentJobKey.Name);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// 2. 根据当前任务的 JobKey,找到对应的配置 Key(如:dy.job.key.collect → collect)
|
||||
var currentConfigKey = _jobConfigs.FirstOrDefault(kv => kv.Value.JobKey == currentJobKey.Name).Key;
|
||||
if (string.IsNullOrEmpty(currentConfigKey))
|
||||
{
|
||||
Log.Warning("【任务监听】未找到任务 {JobName} 的配置信息,任务链条终止", currentJobKey.Name);
|
||||
return;
|
||||
}
|
||||
// // 2. 根据当前任务的 JobKey,找到对应的配置 Key(如:dy.job.key.collect → collect)
|
||||
// var currentConfigKey = _jobConfigs.FirstOrDefault(kv => kv.Value.JobKey == currentJobKey.Name).Key;
|
||||
// if (string.IsNullOrEmpty(currentConfigKey))
|
||||
// {
|
||||
// Log.Warning("【任务监听】未找到任务 {JobName} 的配置信息,任务链条终止", currentJobKey.Name);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// 3. 查找下一个依赖任务的配置 Key
|
||||
if (!_jobDependency.TryGetValue(currentConfigKey, out var nextConfigKey) || string.IsNullOrEmpty(nextConfigKey))
|
||||
{
|
||||
Log.Information("【任务监听】任务 {JobName} 是最后一个任务,本次任务链条执行完毕", currentJobKey.Name);
|
||||
return;
|
||||
}
|
||||
// // 3. 查找下一个依赖任务的配置 Key
|
||||
// if (!_jobDependency.TryGetValue(currentConfigKey, out var nextConfigKey) || string.IsNullOrEmpty(nextConfigKey))
|
||||
// {
|
||||
// Log.Information("【任务监听】任务 {JobName} 是最后一个任务,本次任务链条执行完毕", currentJobKey.Name);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// 4. 触发下一个任务(标记为「依赖触发」,立即执行)
|
||||
Log.Information("【任务监听】准备触发下一个任务: {NextJobName}(依赖触发)", nextConfigKey);
|
||||
var triggerSuccess = await _jobService.StartJobAsync(nextConfigKey, "", isDependencyTrigger: true);
|
||||
// // 4. 触发下一个任务(标记为「依赖触发」,立即执行)
|
||||
// Log.Information("【任务监听】准备触发下一个任务: {NextJobName}(依赖触发)", nextConfigKey);
|
||||
// var triggerSuccess = await _jobService.StartJobAsync(nextConfigKey, "", isDependencyTrigger: true);
|
||||
|
||||
if (triggerSuccess)
|
||||
{
|
||||
Log.Information("【任务监听】下一个任务 {NextJobName} 触发成功", nextConfigKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("【任务监听】下一个任务 {NextJobName} 触发失败,任务链条中断", nextConfigKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (triggerSuccess)
|
||||
// {
|
||||
// Log.Information("【任务监听】下一个任务 {NextJobName} 触发成功", nextConfigKey);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Log.Error("【任务监听】下一个任务 {NextJobName} 触发失败,任务链条中断", nextConfigKey);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
+20
-14
@@ -3,6 +3,7 @@ using dy.net.model.entity;
|
||||
using dy.net.model.response;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Serilog;
|
||||
using System;
|
||||
|
||||
namespace dy.net.job
|
||||
@@ -25,6 +26,11 @@ namespace dy.net.job
|
||||
}
|
||||
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.SavePath, "author");
|
||||
}
|
||||
|
||||
protected override async Task<DouyinVideoInfoResponse> FetchVideoData(DouyinCookie cookie, string cursor, DouyinFollowed followed, DouyinCollectCate cate)
|
||||
{
|
||||
return await douyinHttpClientService.SyncCollectVideos(cursor, count, cookie.Cookies);
|
||||
@@ -35,27 +41,27 @@ namespace dy.net.job
|
||||
return data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
|
||||
}
|
||||
|
||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount,DouyinFollowed followed)
|
||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount,DouyinFollowed followed,DouyinCollectCate cate)
|
||||
{
|
||||
if (syncCount > 0)
|
||||
{
|
||||
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||
cookie.CollHasSyncd = 1;
|
||||
await douyinCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],没有可以同步的新视频");
|
||||
}
|
||||
cookie.CollHasSyncd = 1;
|
||||
await douyinCookieService.UpdateAsync(cookie);
|
||||
Log.Debug($"[{VideoType}]-[{cookie.UserName}],本次成功同步{syncCount}条视频");
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed, DouyinCollectCate cate)
|
||||
{
|
||||
var (tag1, _, _) = GetVideoTags(item);
|
||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizeLinuxFileName(tag1, "", true);
|
||||
var folder = Path.Combine(cookie.SavePath, safeTag1, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId, true)}");
|
||||
string authorFolder;
|
||||
if (string.IsNullOrWhiteSpace(item.Author?.Nickname) && string.IsNullOrWhiteSpace(item.Author?.Uid))
|
||||
{
|
||||
authorFolder = "未知博主";
|
||||
}
|
||||
else
|
||||
{
|
||||
authorFolder = $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Author?.Nickname, item.Author?.Uid, true)}";
|
||||
}
|
||||
var folder = Path.Combine(cookie.SavePath, authorFolder, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId, true)}");
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
|
||||
|
||||
+19
-14
@@ -15,6 +15,11 @@ namespace dy.net.job
|
||||
|
||||
protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_favorite;
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.FavSavePath, "author");
|
||||
}
|
||||
|
||||
protected override async Task<List<DouyinCookie>> GetSyncCookies()
|
||||
{
|
||||
return await douyinCookieService.GetOpendCookiesAsync(x=> !string.IsNullOrWhiteSpace(x.FavSavePath)&&!string.IsNullOrWhiteSpace(x.SecUserId));
|
||||
@@ -31,25 +36,25 @@ namespace dy.net.job
|
||||
}
|
||||
|
||||
|
||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount, DouyinFollowed followed)
|
||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount, DouyinFollowed followed,DouyinCollectCate cate)
|
||||
{
|
||||
if (syncCount > 0)
|
||||
{
|
||||
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||
cookie.FavHasSyncd = 1;
|
||||
await douyinCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],没有可以同步的新视频");
|
||||
}
|
||||
cookie.FavHasSyncd = 1;
|
||||
await douyinCookieService.UpdateAsync(cookie);
|
||||
await base.HandleSyncCompletion(cookie, syncCount, followed, cate);
|
||||
}
|
||||
|
||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed,DouyinCollectCate cate)
|
||||
{
|
||||
var (tag1, _, _) = GetVideoTags(item);
|
||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizeLinuxFileName(tag1,"",true);
|
||||
var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId,true)}");
|
||||
string authorFolder;
|
||||
if (string.IsNullOrWhiteSpace(item.Author?.Nickname) && string.IsNullOrWhiteSpace(item.Author?.Uid))
|
||||
{
|
||||
authorFolder = "未知博主";
|
||||
}
|
||||
else
|
||||
{
|
||||
authorFolder = $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Author?.Nickname, item.Author?.Uid, true)}";
|
||||
}
|
||||
var folder = Path.Combine(cookie.FavSavePath, authorFolder, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId, true)}");
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
}
|
||||
|
||||
+269
-220
File diff suppressed because it is too large
Load Diff
@@ -15,14 +15,32 @@ namespace dy.net.job
|
||||
|
||||
protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_custom_collect;
|
||||
|
||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed, DouyinCollectCate cate)
|
||||
{
|
||||
if (cate != null)
|
||||
{
|
||||
var folder = Path.Combine(cookie.SavePath, DouyinFileNameHelper.SanitizeLinuxFileName(cate.SaveFolder, "", true), DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId, true));
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.CreateSaveFolder(cookie, item, config,followed,cate);
|
||||
}
|
||||
}
|
||||
protected override string GetAuthorAvatarBasePath(DouyinCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.SavePath, "author");
|
||||
}
|
||||
|
||||
protected override async Task<DouyinVideoInfoResponse> FetchVideoData(DouyinCookie cookie, string cursor, DouyinFollowed followed, DouyinCollectCate cate)
|
||||
{
|
||||
return await douyinHttpClientService.SyncCollectVideos(cursor, count, cookie.Cookies);
|
||||
return await douyinHttpClientService.SyncCollectVideosByCollectId(cursor,count,cookie.Cookies,cate.XId);
|
||||
}
|
||||
|
||||
protected override bool ShouldContinueSync(DouyinCookie cookie, DouyinVideoInfoResponse data, DouyinFollowed followed = null)
|
||||
{
|
||||
return data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
|
||||
return data != null && data.HasMore == 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.job
|
||||
{
|
||||
public class DouyinFollowedViedoSyncJob : DouyinBasicSyncJob
|
||||
public class DouyinFollowedSyncJob : DouyinBasicSyncJob
|
||||
{
|
||||
public DouyinFollowedViedoSyncJob(DouyinCookieService douyinCookieService, DouyinHttpClientService douyinHttpClientService, DouyinVideoService douyinVideoService, DouyinCommonService douyinCommonService, DouyinFollowService douyinFollowService, DouyinMergeVideoService douyinMergeVideoService, DouyinCollectCateService douyinCollectCateService) : base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService, douyinFollowService, douyinMergeVideoService, douyinCollectCateService)
|
||||
public DouyinFollowedSyncJob(DouyinCookieService douyinCookieService, DouyinHttpClientService douyinHttpClientService, DouyinVideoService douyinVideoService, DouyinCommonService douyinCommonService, DouyinFollowService douyinFollowService, DouyinMergeVideoService douyinMergeVideoService, DouyinCollectCateService douyinCollectCateService) : base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService, douyinFollowService, douyinMergeVideoService, douyinCollectCateService)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ namespace dy.net.job
|
||||
{
|
||||
return data != null && data.HasMore == 1 && cookie.UperSyncd == 0 && followed.FullSync;
|
||||
}
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.UpSavePath, "author");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关注用户特殊处理文件夹存储路径,用户可自定义保存路径
|
||||
@@ -41,6 +44,7 @@ namespace dy.net.job
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="followed"></param>
|
||||
/// <param name="cate"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <returns></returns>
|
||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed,DouyinCollectCate cate)
|
||||
@@ -49,7 +53,7 @@ namespace dy.net.job
|
||||
// 1. 优先获取有效的作者名称(遵循原有优先级:followed.UperName > item.Author.Nickname > 默认值)
|
||||
var rawAuthorName = followed?.UperName ?? item?.Author?.Nickname;
|
||||
var authorName = string.IsNullOrWhiteSpace(rawAuthorName)
|
||||
? "UnknownAuthor"
|
||||
? "未知博主"
|
||||
: DouyinFileNameHelper.SanitizeLinuxFileName(rawAuthorName, "", true);
|
||||
// 2. 确定最终文件夹路径(遵循原有优先级:followed.SavePath > authorName > 基础路径)
|
||||
var targetFolderName = !string.IsNullOrWhiteSpace(followed?.SavePath) ? followed.SavePath : authorName;
|
||||
@@ -58,17 +62,10 @@ namespace dy.net.job
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
#endregion
|
||||
|
||||
if (config.UperSaveTogether)
|
||||
{
|
||||
return folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sampleName = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId,true);
|
||||
var (existingName, _) = douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result;
|
||||
var fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName;
|
||||
return Path.Combine(folder, fileNameFolder);
|
||||
}
|
||||
var sampleName = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId, true);
|
||||
var (existingName, _) = douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName);
|
||||
var fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName;
|
||||
return Path.Combine(folder, fileNameFolder);
|
||||
}
|
||||
/// <summary>
|
||||
/// 关注的视频,生成文件名称
|
||||
@@ -76,8 +73,9 @@ namespace dy.net.job
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <param name="cate"></param>
|
||||
/// <returns></returns>
|
||||
protected override string GetVideoFileName(DouyinCookie cookie, Aweme item,AppConfig config)
|
||||
protected override string GetVideoFileName(DouyinCookie cookie, Aweme item,AppConfig config,DouyinCollectCate cate)
|
||||
{
|
||||
|
||||
string Format = "mp4";
|
||||
@@ -109,7 +107,7 @@ namespace dy.net.job
|
||||
if (config?.UperUseViedoTitle ?? false)//优先
|
||||
{
|
||||
var sampleName = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId);
|
||||
var (existingName, _) = douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result;
|
||||
var (existingName, _) = douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName);
|
||||
fileName = string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{Format}" : $"{existingName}.{Format}";
|
||||
}
|
||||
else
|
||||
@@ -146,47 +144,20 @@ namespace dy.net.job
|
||||
/// <param name="item"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <param name="imageType"></param>
|
||||
/// <param name="cate"></param>
|
||||
/// <returns></returns>
|
||||
protected override string GetNfoFileName(DouyinCookie cookie, Aweme item, AppConfig config, string imageType)
|
||||
protected override string GetNfoFileName(DouyinCookie cookie, Aweme item, AppConfig config, string imageType,DouyinCollectCate cate)
|
||||
{
|
||||
if (config.UperSaveTogether)
|
||||
{
|
||||
var videoFileName = GetVideoFileName(cookie, item,config);
|
||||
return $"{Path.GetFileNameWithoutExtension(videoFileName)}{imageType}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.GetNfoFileName(cookie, item, config, imageType);
|
||||
}
|
||||
return base.GetNfoFileName(cookie, item, config, imageType, cate);
|
||||
}
|
||||
|
||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount, DouyinFollowed followed)
|
||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount, DouyinFollowed followed, DouyinCollectCate cate)
|
||||
{
|
||||
if (syncCount > 0)
|
||||
{
|
||||
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||
cookie.UperSyncd = 1;
|
||||
await douyinCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}]-{(followed == null ? "" : $"{followed.UperName}")},没有可以同步的新视频");
|
||||
}
|
||||
cookie.UperSyncd = 1;
|
||||
await douyinCookieService.UpdateAsync(cookie);
|
||||
await base.HandleSyncCompletion(cookie, syncCount, followed, cate);
|
||||
}
|
||||
|
||||
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item)
|
||||
{
|
||||
var config = douyinCommonService.GetConfig();
|
||||
string simplifiedTitle = string.Empty;
|
||||
|
||||
if (config?.UperUseViedoTitle ?? false)
|
||||
{
|
||||
simplifiedTitle = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId);
|
||||
}
|
||||
return new VideoEntityDifferences
|
||||
{
|
||||
VideoTitleSimplify = simplifiedTitle
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,45 +3,25 @@ using dy.net.model.entity;
|
||||
using dy.net.model.response;
|
||||
using dy.net.service;
|
||||
using Quartz;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.job
|
||||
{
|
||||
[DisallowConcurrentExecution] // 禁止并发执行,确保同一时间只有一个实例在运行
|
||||
[DisallowConcurrentExecution]
|
||||
public class DouyinFollowsAndCollnectsSyncJob : IJob
|
||||
{
|
||||
// 服务依赖(统一下划线命名)
|
||||
private readonly DouyinCollectCateService _douyinCollectCateService;
|
||||
private readonly DouyinCookieService _dyCookieService;
|
||||
private readonly DouyinHttpClientService _douyinService;
|
||||
private readonly DouyinFollowService _followService;
|
||||
private readonly DouyinCommonService _douyinCommonService;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 抖音收藏夹服务
|
||||
/// </summary>
|
||||
protected readonly DouyinCollectCateService douyinCollectCateService;
|
||||
/// <summary>
|
||||
/// 抖音Cookie服务,用于获取和管理用户Cookie
|
||||
/// </summary>
|
||||
protected readonly DouyinCookieService _dyCookieService;
|
||||
|
||||
/// <summary>
|
||||
/// 抖音HTTP客户端服务,用于发送HTTP请求
|
||||
/// </summary>
|
||||
protected readonly DouyinHttpClientService _douyinService;
|
||||
|
||||
/// <summary>
|
||||
/// 抖音关注服务,用于管理关注数据
|
||||
/// </summary>
|
||||
protected readonly DouyinFollowService _followService;
|
||||
|
||||
protected readonly DouyinCommonService douyinCommonService;
|
||||
|
||||
public DouyinFollowsAndCollnectsSyncJob(DouyinCookieService dyCookieService, DouyinHttpClientService douyinService, DouyinFollowService followService, DouyinCommonService douyinCommonService, DouyinCollectCateService douyinCollectCateService)
|
||||
{
|
||||
_dyCookieService = dyCookieService;
|
||||
_douyinService = douyinService;
|
||||
_followService = followService;
|
||||
this.douyinCommonService = douyinCommonService;
|
||||
this.douyinCollectCateService = douyinCollectCateService;
|
||||
}
|
||||
|
||||
// 第一步:定义常量和枚举(建议放在单独的常量类中,此处为内联演示)
|
||||
// 常量定义
|
||||
private const string DEFAULT_FOLLOW_COUNT = "20";
|
||||
private const int INVALID_COOKIE_STATUS_CODE = 8;
|
||||
private const string LOG_TAG_COLLECT = "收藏夹同步";
|
||||
@@ -49,299 +29,234 @@ namespace dy.net.job
|
||||
private const string LOG_TAG_SERIES = "短剧列表同步";
|
||||
private const string LOG_TAG_FOLLOW = "关注列表同步";
|
||||
|
||||
/// <summary>
|
||||
/// 抖音数据同步任务执行方法(优化版)
|
||||
/// </summary>
|
||||
/// <param name="context">任务执行上下文</param>
|
||||
// 构造函数注入
|
||||
public DouyinFollowsAndCollnectsSyncJob(
|
||||
DouyinCookieService dyCookieService,
|
||||
DouyinHttpClientService douyinService,
|
||||
DouyinFollowService followService,
|
||||
DouyinCommonService douyinCommonService,
|
||||
DouyinCollectCateService douyinCollectCateService)
|
||||
{
|
||||
_dyCookieService = dyCookieService;
|
||||
_douyinService = douyinService;
|
||||
_followService = followService;
|
||||
_douyinCommonService = douyinCommonService;
|
||||
_douyinCollectCateService = douyinCollectCateService;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
// 1. 获取有效Cookie列表,提前做空值判断
|
||||
var cookies = await _dyCookieService.GetOpendCookiesAsync();
|
||||
if (cookies == null || !cookies.Any())
|
||||
{
|
||||
Serilog.Log.Debug("当前无可用的抖音Cookie,同步任务跳过");
|
||||
Log.Debug("当前无可用的抖音Cookie,同步任务跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取应用配置
|
||||
AppConfig conf = douyinCommonService.GetConfig();
|
||||
var conf = _douyinCommonService.GetConfig();
|
||||
|
||||
// 3. 遍历每个Cookie,执行各类数据同步
|
||||
foreach (var ck in cookies)
|
||||
{
|
||||
Serilog.Log.Information($"开始处理Cookie用户:[{ck.UserName}](ID:{ck.Id})");
|
||||
Log.Information($"开始处理Cookie用户:[{ck.UserName}](ID:{ck.Id})");
|
||||
|
||||
// 3.1 同步自定义收藏夹(UseCollectFolder=true时)
|
||||
// 调用通用方法,传入差异化参数(无委托,参数直观)
|
||||
if (ck.UseCollectFolder)
|
||||
{
|
||||
await SyncCollectDataAsync(
|
||||
cookie: ck,
|
||||
cateType: VideoTypeEnum.dy_custom_collect,
|
||||
dataFetchFunc: async (offset) => await _douyinService.SyncCollectFolderList(ck.Cookies, offset),
|
||||
entityConvertFunc: (collectItem) => new DouyinCollectCate
|
||||
await SyncCollectGenericAsync(ck, VideoTypeEnum.dy_custom_collect,
|
||||
(cookie, offset) => _douyinService.SyncCollectFolderList(cookie.Cookies, offset),
|
||||
item => new DouyinCollectCate
|
||||
{
|
||||
CookieId = ck.Id,
|
||||
CateType = VideoTypeEnum.dy_custom_collect,
|
||||
Name = collectItem.CollectsName,
|
||||
Name = item.CollectsName,
|
||||
Sync = false,
|
||||
XId = collectItem.CollectsId
|
||||
XId = item.CollectsId,
|
||||
Total = item.TotalNumber
|
||||
},
|
||||
hasMoreFunc: (data) => data?.HasMore ?? false,
|
||||
cursorFunc: (data) => data?.Cursor.ToString() ?? "0",
|
||||
dataListFunc: (data) => data?.CollectsList,
|
||||
logTag: LOG_TAG_COLLECT
|
||||
);
|
||||
data => data?.HasMore ?? false,
|
||||
data => data?.Cursor.ToString() ?? "0",
|
||||
data => data?.CollectsList,
|
||||
LOG_TAG_COLLECT);
|
||||
}
|
||||
|
||||
// 3.2 同步收藏夹合集(DownMix=true时)
|
||||
if (ck.DownMix)
|
||||
{
|
||||
await SyncCollectDataAsync(
|
||||
cookie: ck,
|
||||
cateType: VideoTypeEnum.dy_mix,
|
||||
dataFetchFunc: async (offset) => await _douyinService.SyncMixList(ck.Cookies, offset),
|
||||
entityConvertFunc: (mixItem) => new DouyinCollectCate
|
||||
await SyncCollectGenericAsync(ck, VideoTypeEnum.dy_mix,
|
||||
(cookie, offset) => _douyinService.SyncMixList(cookie.Cookies, offset),
|
||||
item => new DouyinCollectCate
|
||||
{
|
||||
CookieId = ck.Id,
|
||||
CateType = VideoTypeEnum.dy_mix,
|
||||
Name = mixItem.MixName,
|
||||
Name = item.MixName,
|
||||
Sync = false,
|
||||
XId = mixItem.MixId,
|
||||
CoverUrl = mixItem.CoverUrl?.UrlList?.FirstOrDefault() // 空值防护:避免CoverUrl为null
|
||||
XId = item.MixId,
|
||||
Total = item?.Statis?.UpdatedToEpisode ?? 0,
|
||||
CoverUrl = item.CoverUrl?.UrlList?.LastOrDefault()
|
||||
},
|
||||
hasMoreFunc: (data) => (data?.HasMore ?? 0) == 1,
|
||||
cursorFunc: (data) => data?.Cursor.ToString() ?? "0",
|
||||
dataListFunc: (data) => data?.MixInfos,
|
||||
logTag: LOG_TAG_MIX
|
||||
);
|
||||
data => (data?.HasMore ?? 0) == 1,
|
||||
data => data?.Cursor.ToString() ?? "0",
|
||||
data => data?.MixInfos,
|
||||
LOG_TAG_MIX);
|
||||
}
|
||||
|
||||
// 3.3 同步收藏夹短剧(DownSeries=true时)
|
||||
if (ck.DownSeries)
|
||||
{
|
||||
await SyncCollectDataAsync(
|
||||
cookie: ck,
|
||||
cateType: VideoTypeEnum.dy_series,
|
||||
dataFetchFunc: async (offset) => await _douyinService.SyncSeriesList(ck.Cookies, offset),
|
||||
entityConvertFunc: (seriesItem) => new DouyinCollectCate
|
||||
await SyncCollectGenericAsync(ck, VideoTypeEnum.dy_series,
|
||||
(cookie, offset) => _douyinService.SyncSeriesList(cookie.Cookies, offset),
|
||||
item => new DouyinCollectCate
|
||||
{
|
||||
CookieId = ck.Id,
|
||||
CateType = VideoTypeEnum.dy_series,
|
||||
Name = seriesItem.SeriesName,
|
||||
Name = item.SeriesName,
|
||||
Sync = false,
|
||||
XId = seriesItem.SeriesId,
|
||||
CoverUrl = seriesItem.CoverImage?.ImageUrlList?.FirstOrDefault() // 空值防护:避免CoverImage为null
|
||||
XId = item.SeriesId,
|
||||
Total = item?.Stats?.TotalEpisodeCount ?? 0,
|
||||
CoverUrl = item.CoverImage?.ImageUrlList?.FirstOrDefault()
|
||||
},
|
||||
hasMoreFunc: (data) => (data?.HasMore ?? 0) == 1,
|
||||
cursorFunc: (data) => data?.Cursor.ToString() ?? "0",
|
||||
dataListFunc: (data) => data?.SeriesList,
|
||||
logTag: LOG_TAG_SERIES
|
||||
);
|
||||
data => (data?.HasMore ?? 0) == 1,
|
||||
data => data?.Cursor.ToString() ?? "0",
|
||||
data => data?.SeriesList,
|
||||
LOG_TAG_SERIES);
|
||||
}
|
||||
|
||||
// 3.4 同步关注列表(单独处理,逻辑特殊不纳入通用方法)
|
||||
await SyncFollowListAsync(ck, conf);
|
||||
|
||||
Serilog.Log.Debug($"完成Cookie用户:[{ck.UserName}](ID:{ck.Id})的所有收藏夹信息、合集信息、关注列表信息、短剧信息同步");
|
||||
Log.Debug($"完成[{ck.UserName}] [列表]同步, 包括 [自定义收藏夹、关注、合集、短剧] ");
|
||||
}
|
||||
|
||||
// 4. 标记首次运行完成(仅执行一次)
|
||||
await douyinCommonService.SetConfigNotFirstRunning();
|
||||
await _douyinCommonService.SetConfigNotFirstRunning();
|
||||
}
|
||||
|
||||
#region 私有通用辅助方法
|
||||
#region 通用核心方法(无冗余,支持调试)
|
||||
/// <summary>
|
||||
/// 通用收藏类数据同步方法(泛型封装,消除冗余)
|
||||
/// 通用收藏类数据同步方法(无复杂委托,参数直观,便于调试)
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">抖音返回的分页数据类型</typeparam>
|
||||
/// <typeparam name="TItem">抖音返回的列表项类型</typeparam>
|
||||
/// <param name="cookie">当前Cookie信息</param>
|
||||
/// <param name="cateType">分类类型</param>
|
||||
/// <param name="dataFetchFunc">分页数据获取委托</param>
|
||||
/// <param name="entityConvertFunc">抖音项转换为DouyinCollectCate的委托</param>
|
||||
/// <param name="hasMoreFunc">判断是否还有更多数据的委托</param>
|
||||
/// <param name="cursorFunc">获取下一页游标值的委托</param>
|
||||
/// <param name="dataListFunc">从分页数据中提取列表的委托</param>
|
||||
/// <param name="logTag">日志标签</param>
|
||||
private async Task SyncCollectDataAsync<TData, TItem>(
|
||||
/// <typeparam name="TData">分页数据类型</typeparam>
|
||||
/// <typeparam name="TItem">列表项类型</typeparam>
|
||||
private async Task SyncCollectGenericAsync<TData, TItem>(
|
||||
DouyinCookie cookie,
|
||||
VideoTypeEnum cateType,
|
||||
Func<string, Task<TData>> dataFetchFunc,
|
||||
// 数据获取方法(最简委托,仅传必要参数)
|
||||
Func<DouyinCookie, string, Task<TData>> dataFetchFunc,
|
||||
// 实体转换方法(内联lambda,调试可直接看到转换逻辑)
|
||||
Func<TItem, DouyinCollectCate> entityConvertFunc,
|
||||
// 分页判断方法
|
||||
Func<TData, bool> hasMoreFunc,
|
||||
Func<TData, string> cursorFunc,
|
||||
Func<TData, List<TItem>> dataListFunc,
|
||||
// 游标获取方法
|
||||
Func<TData, string> getCursorFunc,
|
||||
// 列表提取方法
|
||||
Func<TData, List<TItem>> getDataListFunc,
|
||||
string logTag)
|
||||
{
|
||||
var collectDataList = new List<DouyinCollectCate>();
|
||||
string offset = "0";
|
||||
bool hasMore = true;
|
||||
List<DouyinCollectCate> collectDataList = new List<DouyinCollectCate>();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 分页获取抖音数据
|
||||
while (hasMore)
|
||||
{
|
||||
var pageData = await dataFetchFunc(offset);
|
||||
if (pageData == null)
|
||||
{
|
||||
//Serilog.Log.Warning($"[{cookie.UserName}] - {logTag}:获取分页数据为空,停止分页");
|
||||
break;
|
||||
}
|
||||
// 1. 获取分页数据(直接调用,调试可断点到具体Service方法)
|
||||
var pageData = await dataFetchFunc(cookie, offset);
|
||||
if (pageData == null) break;
|
||||
|
||||
// 2. 更新分页状态
|
||||
// 2. 更新分页状态(逻辑透明)
|
||||
hasMore = hasMoreFunc(pageData);
|
||||
offset = cursorFunc(pageData);
|
||||
offset = getCursorFunc(pageData);
|
||||
|
||||
// 3. 提取当前页列表数据并转换
|
||||
var currentPageItems = dataListFunc(pageData);
|
||||
if (currentPageItems != null && currentPageItems.Any())
|
||||
// 3. 提取并转换数据(内联逻辑,调试可看每一项转换结果)
|
||||
var currentItems = getDataListFunc(pageData);
|
||||
if (currentItems?.Any() == true)
|
||||
{
|
||||
var convertItems = currentPageItems.Select(entityConvertFunc).ToList();
|
||||
collectDataList.AddRange(convertItems);
|
||||
Serilog.Log.Debug($"[{cookie.UserName}] - {logTag}:获取到{currentPageItems.Count}条数据,累计{collectDataList.Count}条");
|
||||
collectDataList.AddRange(currentItems.Select(entityConvertFunc));
|
||||
Log.Debug($"[{cookie.UserName}] - {logTag}:获取{currentItems.Count}条,累计{collectDataList.Count}条");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调用Sync方法同步到数据库
|
||||
// 4. 同步到数据库
|
||||
if (collectDataList.Any())
|
||||
{
|
||||
var (add, update, delete, succ) = await douyinCollectCateService.Sync(
|
||||
collectDataList,
|
||||
cookie.Id,
|
||||
cateType);
|
||||
|
||||
Serilog.Log.Debug($"[{cookie.UserName}] - {logTag}:同步完成,新增{add}条,更新{update}条,删除{delete}条,是否成功:{succ}");
|
||||
var (add, update, delete, succ) = await _douyinCollectCateService.Sync(collectDataList, cookie.Id, cateType);
|
||||
Log.Debug($"[{cookie.UserName}] - {logTag}:同步完成 新增:{add} 更新:{update} 删除:{delete} 成功:{succ}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"[{cookie.UserName}] - {logTag}:无有效数据需要同步");
|
||||
Log.Debug($"[{cookie.UserName}] - {logTag}:无有效数据");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"[{cookie.UserName}] - {logTag}:分页同步失败,异常信息:{ex.Message}");
|
||||
Log.Error(ex, $"[{cookie.UserName}] - {logTag}:同步失败");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 关注列表单独同步方法(逻辑特殊,单独封装)
|
||||
/// </summary>
|
||||
/// <param name="cookie">当前Cookie信息</param>
|
||||
/// <param name="config">应用配置</param>
|
||||
#region 关注列表同步(逻辑独立,无重复)
|
||||
private async Task SyncFollowListAsync(DouyinCookie cookie, AppConfig config)
|
||||
{
|
||||
// 1. 校验SecUserId是否有效
|
||||
if (string.IsNullOrWhiteSpace(cookie.SecUserId))
|
||||
{
|
||||
Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:未设置SecUserId,跳过");
|
||||
Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:未设置SecUserId,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
var followList = new List<FollowingsItem>();
|
||||
string offset = "0";
|
||||
bool hasMore = true;
|
||||
int total = 0;
|
||||
List<FollowingsItem> followList = new List<FollowingsItem>();
|
||||
FollowErrorDto currentError = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 分页获取关注列表数据
|
||||
while (hasMore)
|
||||
{
|
||||
var (data, error) = await FetchFollowPageDataAsync(cookie, offset);
|
||||
|
||||
// 3. 处理错误信息
|
||||
if (error != null && error.StatusCode != 0)
|
||||
{
|
||||
Serilog.Log.Error($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:发生错误,状态码:{error.StatusCode},错误信息:{error.StatusMsg}");
|
||||
// 无效Cookie(未登录)直接停止分页
|
||||
if (error.StatusCode == INVALID_COOKIE_STATUS_CODE)
|
||||
var data = await _douyinService.SyncMyFollows(
|
||||
DEFAULT_FOLLOW_COUNT, offset, cookie.SecUserId, cookie.Cookies,
|
||||
async (err) =>
|
||||
{
|
||||
cookie.StatusMsg = "无效";
|
||||
cookie.StatusCode = INVALID_COOKIE_STATUS_CODE;
|
||||
currentError = err;
|
||||
cookie.StatusMsg = err.StatusCode == INVALID_COOKIE_STATUS_CODE ? "无效" : "正常";
|
||||
cookie.StatusCode = err.StatusCode;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 处理有效数据
|
||||
if (data != null)
|
||||
if (currentError != null && currentError.StatusCode != 0)
|
||||
{
|
||||
// 5. 更新MyUserId(若未设置)
|
||||
if (string.IsNullOrWhiteSpace(cookie.MyUserId))
|
||||
{
|
||||
cookie.MyUserId = data.MySelfUserId;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
|
||||
// 6. 更新分页状态和数据
|
||||
total = data.Total;
|
||||
hasMore = data.HasMore;
|
||||
offset = data.Offset.ToString();
|
||||
|
||||
if (data.Followings != null && data.Followings.Any())
|
||||
{
|
||||
followList.AddRange(data.Followings);
|
||||
Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:获取到{data.Followings.Count}条关注数据,累计{followList.Count}条");
|
||||
}
|
||||
|
||||
// 7. 非首次运行时,仅同步第一页数据
|
||||
if (!config.IsFirstRunning)
|
||||
{
|
||||
hasMore = false;
|
||||
//Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:非首次运行,停止分页获取");
|
||||
}
|
||||
Log.Error($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:错误 状态码:{currentError.StatusCode} 信息:{currentError.StatusMsg}");
|
||||
if (currentError.StatusCode == INVALID_COOKIE_STATUS_CODE) break;
|
||||
}
|
||||
else
|
||||
|
||||
if (data == null) break;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cookie.MyUserId))
|
||||
{
|
||||
Serilog.Log.Warning($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:获取分页数据为空,停止分页");
|
||||
break;
|
||||
cookie.MyUserId = data.MySelfUserId;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
|
||||
total = data.Total;
|
||||
hasMore = data.HasMore;
|
||||
offset = data.Offset.ToString();
|
||||
|
||||
if (data.Followings?.Any() == true)
|
||||
{
|
||||
followList.AddRange(data.Followings);
|
||||
Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:获取{data.Followings.Count}条,累计{followList.Count}条");
|
||||
}
|
||||
|
||||
if (!config.IsFirstRunning) hasMore = false;
|
||||
}
|
||||
|
||||
// 8. 同步关注列表到数据库
|
||||
if (followList.Any())
|
||||
{
|
||||
var (add, update, succ) = await _followService.Sync(followList, cookie);
|
||||
Serilog.Log.Information($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:同步完成,新增{add}条,更新{update}条,是否成功:{succ},总关注数:{total}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:无有效关注数据需要同步");
|
||||
Log.Information($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:同步完成 新增:{add} 更新:{update} 成功:{succ} 总关注数:{total}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:同步失败,异常信息:{ex.Message}");
|
||||
Log.Error(ex, $"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:同步失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页获取关注列表数据(封装回调逻辑,简化代码)
|
||||
/// </summary>
|
||||
/// <param name="cookie">当前Cookie信息</param>
|
||||
/// <param name="offset">分页偏移量</param>
|
||||
private async Task<(DouyinFollowInfoResponse Data, FollowErrorDto Error)> FetchFollowPageDataAsync(DouyinCookie cookie, string offset)
|
||||
{
|
||||
DouyinFollowInfoResponse resultData = null;
|
||||
FollowErrorDto resultError = null;
|
||||
resultData = await _douyinService.SyncMyFollows(
|
||||
DEFAULT_FOLLOW_COUNT,
|
||||
offset,
|
||||
cookie.SecUserId,
|
||||
cookie.Cookies,
|
||||
async (err) =>
|
||||
{
|
||||
resultError = err;
|
||||
// 更新Cookie状态(异步不阻塞)
|
||||
cookie.StatusMsg = err.StatusCode == INVALID_COOKIE_STATUS_CODE ? "无效" : "正常";
|
||||
cookie.StatusCode = err.StatusCode;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
});
|
||||
|
||||
// 若原方法是回调式无返回值,需调整封装逻辑
|
||||
return (resultData, resultError);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -13,19 +13,35 @@ namespace dy.net.job
|
||||
}
|
||||
|
||||
|
||||
protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_favorite;
|
||||
protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_mix;
|
||||
|
||||
|
||||
protected override async Task<DouyinVideoInfoResponse> FetchVideoData(DouyinCookie cookie, string cursor,DouyinFollowed followed,DouyinCollectCate cate)
|
||||
{
|
||||
return await douyinHttpClientService.SyncFavoriteVideos(count, cursor, cookie.SecUserId, cookie.Cookies);
|
||||
return await douyinHttpClientService.SyncMixViedosByMixId(cursor,count,cookie.Cookies,cate.XId);
|
||||
}
|
||||
|
||||
protected override bool ShouldContinueSync(DouyinCookie cookie, DouyinVideoInfoResponse data, DouyinFollowed followed=null)
|
||||
{
|
||||
return data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
|
||||
return data != null && data.HasMore == 1;
|
||||
}
|
||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed, DouyinCollectCate cate)
|
||||
{
|
||||
if (cate != null)
|
||||
{
|
||||
var folder = Path.Combine(cookie.SavePath,VideoType.GetDesc(), DouyinFileNameHelper.SanitizeLinuxFileName(cate.SaveFolder, cate.Name, true));
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.CreateSaveFolder(cookie, item, config, followed, cate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.SavePath, "author");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,5 +25,24 @@ namespace dy.net.job
|
||||
{
|
||||
return data != null && data.HasMore == 1;
|
||||
}
|
||||
|
||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed, DouyinCollectCate cate)
|
||||
{
|
||||
if (cate != null)
|
||||
{
|
||||
var folder = Path.Combine(cookie.SavePath, VideoType.GetDesc(), DouyinFileNameHelper.SanitizeLinuxFileName(cate.SaveFolder, cate.Name, true));
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.CreateSaveFolder(cookie, item, config, followed, cate);
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.SavePath, "author");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ namespace dy.net.model.dto
|
||||
|
||||
public string? Tag { get; set; }
|
||||
|
||||
public string? SortField { get; set; }
|
||||
|
||||
public string? SortOrder { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace dy.net.model.dto
|
||||
{
|
||||
public class VideoChartItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 日期,格式MM-DD(如01-21)
|
||||
/// </summary>
|
||||
public string Date { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 我喜欢的数量
|
||||
/// </summary>
|
||||
public int Favorite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 我收藏的数量
|
||||
/// </summary>
|
||||
public int Collect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 我关注的数量
|
||||
/// </summary>
|
||||
public int Follow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图文视频数量
|
||||
/// </summary>
|
||||
public int Graphic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合集数量
|
||||
/// </summary>
|
||||
public int Mix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 短剧数量
|
||||
/// </summary>
|
||||
public int Series { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,16 @@
|
||||
public string VideoFollowSize { get; set; }
|
||||
public string VideoFavoriteSize { get; set; }
|
||||
|
||||
public string VideoMixSize { get; set; }
|
||||
public string VideoSeriesSize { get; set; }
|
||||
|
||||
public long CollectCount { get; set; }
|
||||
public long FavoriteCount { get; set; }
|
||||
|
||||
public long FollowCount { get; set; }
|
||||
public long MixCount { get; set; }
|
||||
|
||||
public long SeriesCount { get; set; }
|
||||
|
||||
public long GraphicVideoCount { get; set; }
|
||||
public string GraphicVideoSize { get; set; }
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace dy.net.model.entity
|
||||
/// <summary>
|
||||
/// 是否博主视频直接放一个根目录,不另外按名字建文件夹
|
||||
/// </summary>
|
||||
public bool UperSaveTogether { get; set; }
|
||||
//public bool UperSaveTogether { get; set; }
|
||||
/// <summary>
|
||||
/// 是否下载图片视频
|
||||
/// </summary>
|
||||
@@ -68,7 +68,7 @@ namespace dy.net.model.entity
|
||||
/// <summary>
|
||||
/// 图文视频是否单独存放,否的话则按原类型存储位置存放,比如收藏夹、喜欢等
|
||||
/// </summary>
|
||||
public bool ImageViedoSaveAlone { get; set; }
|
||||
//public bool ImageViedoSaveAlone { get; set; }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -35,10 +35,12 @@ namespace dy.net.model.entity
|
||||
/// <summary>
|
||||
/// 封面
|
||||
/// </summary>
|
||||
[SugarColumn(Length =500,IsNullable =true)]
|
||||
public string CoverUrl { get; set; }
|
||||
/// <summary>
|
||||
/// 保存文件夹
|
||||
/// </summary>
|
||||
[SugarColumn(Length =500,IsNullable =true)]
|
||||
public string SaveFolder { get; set; }
|
||||
/// <summary>
|
||||
/// 是否开启同步
|
||||
@@ -52,10 +54,16 @@ namespace dy.net.model.entity
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
public DateTime UpdateTime { get; set; }
|
||||
[SugarColumn(IsNullable =true)]
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
/// <summary>
|
||||
/// 是否已完结
|
||||
/// </summary>
|
||||
public bool IsEnd { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总集数
|
||||
/// </summary>
|
||||
public int Total { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace dy.net.model.entity
|
||||
/// <summary>
|
||||
/// 用户抖音ID,对应我关注信息里面的myself_user_id
|
||||
/// </summary>
|
||||
[SugarColumn(Length =500,IsNullable =true)]
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string MyUserId { get; set; }
|
||||
/// <summary>
|
||||
/// 用户描述
|
||||
@@ -21,42 +21,42 @@ namespace dy.net.model.entity
|
||||
/// <summary>
|
||||
/// 用户抖音Cookie
|
||||
/// </summary>
|
||||
[SugarColumn(Length =-1,IsNullable =true)]
|
||||
[SugarColumn(Length = -1, IsNullable = true)]
|
||||
public string Cookies { get; set; }
|
||||
/// <summary>
|
||||
/// 存储路径(收藏视频的存储路径)
|
||||
/// </summary>
|
||||
[SugarColumn(Length =255,IsNullable =true)]
|
||||
[SugarColumn(Length = 255, IsNullable = true)]
|
||||
public string SavePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否按收藏夹文件夹来存储视频
|
||||
/// </summary>
|
||||
public bool UseCollectFolder { get; set; }
|
||||
public bool UseCollectFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否下载合集
|
||||
/// </summary>
|
||||
public bool DownMix { get; set; }
|
||||
public bool DownMix { get; set; }
|
||||
/// <summary>
|
||||
/// 是否下载短剧
|
||||
/// </summary>
|
||||
public bool DownSeries { get; set; }
|
||||
public bool DownSeries { get; set; }
|
||||
/// <summary>
|
||||
/// 1 开启同步 0 关闭同步
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 同步喜欢的视频需要sec_user_id
|
||||
/// </summary>
|
||||
[SugarColumn(Length =500,IsNullable =true)]
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string SecUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 喜欢的视频存储路径
|
||||
/// </summary>
|
||||
[SugarColumn(Length =500,IsNullable =true)]
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string FavSavePath { get; set; }
|
||||
|
||||
///// <summary>
|
||||
@@ -95,8 +95,8 @@ namespace dy.net.model.entity
|
||||
/// <summary>
|
||||
/// 图片视频存储路径
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string ImgSavePath { get; set; }
|
||||
//[SugarColumn(Length = 500, IsNullable = true)]
|
||||
//public string ImgSavePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 抖音返回的状态码,主要用于判断Cookie是否有效
|
||||
@@ -109,11 +109,23 @@ namespace dy.net.model.entity
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 100, IsNullable = true)]
|
||||
public string StatusMsg { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否统一一个路径(savepath)
|
||||
/// </summary>
|
||||
public bool useSinglePath { get; set; }
|
||||
[SugarColumn(Length = 100, IsNullable = true, ColumnName = "useSinglePath")]
|
||||
public bool UseSinglePath { get; set; } = true;//默认true
|
||||
|
||||
///// <summary>
|
||||
///// 合集存储路径
|
||||
///// </summary>
|
||||
//[SugarColumn(Length = 500, IsNullable = true)]
|
||||
//public string MixPath { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 短剧存储路径
|
||||
///// </summary>
|
||||
//[SugarColumn(Length = 500, IsNullable = true)]
|
||||
//public string SeriesPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,5 +165,11 @@ namespace dy.net.model.entity
|
||||
/// </summary>
|
||||
[SugarColumn(Length =200,IsNullable =true)]
|
||||
public string CateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义收藏夹、合集、短剧 绑定的XId
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 200, IsNullable = true)]
|
||||
public string CateXId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +169,11 @@ namespace dy.net.model.response
|
||||
//[JsonProperty("share_info")]
|
||||
//public DouyinShareInfo ShareInfo { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 统计信息
|
||||
///// </summary>
|
||||
//[JsonProperty("statis")]
|
||||
//public DouyinMixStatis Statis { get; set; }
|
||||
/// <summary>
|
||||
/// 统计信息
|
||||
/// </summary>
|
||||
[JsonProperty("statis")]
|
||||
public DouyinMixStatis Statis { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 状态信息
|
||||
|
||||
@@ -167,8 +167,8 @@ namespace dy.net.model.response
|
||||
//[JsonProperty("share_info")]
|
||||
//public DouyinDramaShareInfo ShareInfo { get; set; }
|
||||
|
||||
//[JsonProperty("stats")]
|
||||
//public DouyinDramaStats Stats { get; set; }
|
||||
[JsonProperty("stats")]
|
||||
public DouyinDramaStats Stats { get; set; }
|
||||
|
||||
//[JsonProperty("status")]
|
||||
//public DouyinDramaStatus DramaStatus { get; set; }
|
||||
@@ -1087,26 +1087,26 @@ namespace dy.net.model.response
|
||||
/// <summary>
|
||||
/// 抖音短剧统计信息
|
||||
/// </summary>
|
||||
//public class DouyinDramaStats
|
||||
//{
|
||||
// [JsonProperty("collect_vv")]
|
||||
// public int CollectViewCount { get; set; }
|
||||
public class DouyinDramaStats
|
||||
{
|
||||
[JsonProperty("collect_vv")]
|
||||
public int CollectViewCount { get; set; }
|
||||
|
||||
// [JsonProperty("current_episode")]
|
||||
// public int CurrentEpisode { get; set; }
|
||||
[JsonProperty("current_episode")]
|
||||
public int CurrentEpisode { get; set; }
|
||||
|
||||
// [JsonProperty("last_added_item_time")]
|
||||
// public long LastEpisodeAddTimestamp { get; set; }
|
||||
[JsonProperty("last_added_item_time")]
|
||||
public long LastEpisodeAddTimestamp { get; set; }
|
||||
|
||||
// [JsonProperty("play_vv")]
|
||||
// public long TotalPlayViewCount { get; set; }
|
||||
[JsonProperty("play_vv")]
|
||||
public long TotalPlayViewCount { get; set; }
|
||||
|
||||
// [JsonProperty("total_episode")]
|
||||
// public int TotalEpisodeCount { get; set; }
|
||||
[JsonProperty("total_episode")]
|
||||
public int TotalEpisodeCount { get; set; }
|
||||
|
||||
// [JsonProperty("updated_to_episode")]
|
||||
// public int LatestEpisodeCount { get; set; }
|
||||
//}
|
||||
[JsonProperty("updated_to_episode")]
|
||||
public int LatestEpisodeCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抖音短剧状态信息
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
namespace dy.net.model.response
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 视频列表
|
||||
@@ -245,8 +244,8 @@ namespace dy.net.model.response
|
||||
//[JsonProperty("media_type")]
|
||||
//public int MediaType { get; set; }
|
||||
|
||||
//[JsonProperty("mix_info")]
|
||||
//public MixInfo MixInfo { get; set; }
|
||||
[JsonProperty("mix_info")]
|
||||
public MixInfo MixInfo { get; set; }
|
||||
|
||||
[JsonProperty("music")]
|
||||
public Music Music { get; set; }
|
||||
@@ -619,59 +618,59 @@ namespace dy.net.model.response
|
||||
// public int Type { get; set; }
|
||||
//}
|
||||
|
||||
//public class MixInfo
|
||||
//{
|
||||
// [JsonProperty("cover_url")]
|
||||
// public ImageInfo CoverUrl { get; set; }
|
||||
public class MixInfo
|
||||
{
|
||||
[JsonProperty("cover_url")]
|
||||
public ImageInfo CoverUrl { get; set; }
|
||||
|
||||
// [JsonProperty("create_time")]
|
||||
// public long CreateTime { get; set; }
|
||||
//[JsonProperty("create_time")]
|
||||
//public long CreateTime { get; set; }
|
||||
|
||||
// [JsonProperty("desc")]
|
||||
// public string Desc { get; set; }
|
||||
//[JsonProperty("desc")]
|
||||
//public string Desc { get; set; }
|
||||
|
||||
// [JsonProperty("enable_ad")]
|
||||
// public int EnableAd { get; set; }
|
||||
//[JsonProperty("enable_ad")]
|
||||
//public int EnableAd { get; set; }
|
||||
|
||||
// [JsonProperty("extra")]
|
||||
// public string Extra { get; set; }
|
||||
//[JsonProperty("extra")]
|
||||
//public string Extra { get; set; }
|
||||
|
||||
// [JsonProperty("ids")]
|
||||
// public object Ids { get; set; }
|
||||
//[JsonProperty("ids")]
|
||||
//public object Ids { get; set; }
|
||||
|
||||
// [JsonProperty("is_iaa")]
|
||||
// public int IsIaa { get; set; }
|
||||
//[JsonProperty("is_iaa")]
|
||||
//public int IsIaa { get; set; }
|
||||
|
||||
// [JsonProperty("is_serial_mix")]
|
||||
// public int IsSerialMix { get; set; }
|
||||
//[JsonProperty("is_serial_mix")]
|
||||
//public int IsSerialMix { get; set; }
|
||||
|
||||
// [JsonProperty("mix_id")]
|
||||
// public string MixId { get; set; }
|
||||
//[JsonProperty("mix_id")]
|
||||
//public string MixId { get; set; }
|
||||
|
||||
// [JsonProperty("mix_name")]
|
||||
// public string MixName { get; set; }
|
||||
//[JsonProperty("mix_name")]
|
||||
//public string MixName { get; set; }
|
||||
|
||||
// [JsonProperty("mix_pic_type")]
|
||||
// public int MixPicType { get; set; }
|
||||
//[JsonProperty("mix_pic_type")]
|
||||
//public int MixPicType { get; set; }
|
||||
|
||||
// [JsonProperty("mix_type")]
|
||||
// public int MixType { get; set; }
|
||||
//[JsonProperty("mix_type")]
|
||||
//public int MixType { get; set; }
|
||||
|
||||
// [JsonProperty("share_info")]
|
||||
// public MixShareInfo ShareInfo { get; set; }
|
||||
//[JsonProperty("share_info")]
|
||||
//public MixShareInfo ShareInfo { get; set; }
|
||||
|
||||
// [JsonProperty("statis")]
|
||||
// public MixStatis Statis { get; set; }
|
||||
[JsonProperty("statis")]
|
||||
public MixStatis Statis { get; set; }
|
||||
|
||||
// [JsonProperty("status")]
|
||||
// public MixStatus Status { get; set; }
|
||||
//[JsonProperty("status")]
|
||||
//public MixStatus Status { get; set; }
|
||||
|
||||
// [JsonProperty("update_time")]
|
||||
// public long UpdateTime { get; set; }
|
||||
//[JsonProperty("update_time")]
|
||||
//public long UpdateTime { get; set; }
|
||||
|
||||
// [JsonProperty("watched_item")]
|
||||
// public string WatchedItem { get; set; }
|
||||
//}
|
||||
//[JsonProperty("watched_item")]
|
||||
//public string WatchedItem { get; set; }
|
||||
}
|
||||
|
||||
//public class MixShareInfo
|
||||
//{
|
||||
@@ -697,20 +696,20 @@ namespace dy.net.model.response
|
||||
// public string ShareWeiboDesc { get; set; }
|
||||
//}
|
||||
|
||||
//public class MixStatis
|
||||
//{
|
||||
// [JsonProperty("collect_vv")]
|
||||
// public int CollectVv { get; set; }
|
||||
public class MixStatis
|
||||
{
|
||||
[JsonProperty("collect_vv")]
|
||||
public int CollectVv { get; set; }
|
||||
|
||||
// [JsonProperty("current_episode")]
|
||||
// public int CurrentEpisode { get; set; }
|
||||
[JsonProperty("current_episode")]
|
||||
public int CurrentEpisode { get; set; }
|
||||
|
||||
// [JsonProperty("play_vv")]
|
||||
// public int PlayVv { get; set; }
|
||||
[JsonProperty("play_vv")]
|
||||
public int PlayVv { get; set; }
|
||||
|
||||
// [JsonProperty("updated_to_episode")]
|
||||
// public int UpdatedToEpisode { get; set; }
|
||||
//}
|
||||
[JsonProperty("updated_to_episode")]
|
||||
public int UpdatedToEpisode { get; set; }
|
||||
}
|
||||
|
||||
//public class MixStatus
|
||||
//{
|
||||
@@ -765,8 +764,8 @@ namespace dy.net.model.response
|
||||
//[JsonProperty("collect_stat")]
|
||||
//public int CollectStat { get; set; }
|
||||
|
||||
//[JsonProperty("cover_hd")]
|
||||
//public ImageInfo CoverHd { get; set; }
|
||||
[JsonProperty("cover_hd")]
|
||||
public ImageInfo CoverHd { get; set; }
|
||||
|
||||
//[JsonProperty("cover_large")]
|
||||
//public ImageInfo CoverLarge { get; set; }
|
||||
@@ -1285,4 +1284,5 @@ namespace dy.net.model.response
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using dy.net.model.dto;
|
||||
using dy.net.model.entity;
|
||||
using dy.net.utils;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace dy.net.repository
|
||||
@@ -103,8 +104,11 @@ namespace dy.net.repository
|
||||
// 用Dictionary查找,效率更高
|
||||
if (dbCateDict.TryGetValue(cate.XId, out var existCate))
|
||||
{
|
||||
// 对比字段是否有变化(根据实际业务字段调整)
|
||||
bool isChanged = !string.Equals(existCate.Name, cate.Name, StringComparison.Ordinal);
|
||||
// 对比字段是否有变化
|
||||
bool isChanged = !string.Equals(existCate.Name, cate.Name, StringComparison.Ordinal) ||
|
||||
!string.Equals(existCate.CoverUrl, cate.CoverUrl, StringComparison.Ordinal) ||
|
||||
!string.Equals(existCate.SaveFolder, cate.SaveFolder, StringComparison.Ordinal) ||
|
||||
existCate.Total != cate.Total;
|
||||
|
||||
if (isChanged)
|
||||
{
|
||||
@@ -131,7 +135,6 @@ namespace dy.net.repository
|
||||
updateCount = await Db.Updateable(toUpdateCates)
|
||||
.IgnoreColumns(x => new { x.Sync, x.CreateTime, x.CookieId })
|
||||
.IgnoreNullColumns() // 忽略空值字段
|
||||
.Where(c => c.Id == c.Id) // 批量更新默认按主键匹配,此处显式指定(也可省略,SqlSugar自动识别主键)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
@@ -154,12 +157,26 @@ namespace dy.net.repository
|
||||
// 异常时回滚事务(补充:避免Db.Ado未初始化事务导致的空引用)
|
||||
await Db.Ado?.RollbackTranAsync();
|
||||
isSuccess = false;
|
||||
Serilog.Log.Error($"同步收藏夹:类型-{cateType}异常,{ex.StackTrace}");
|
||||
Serilog.Log.Error($"同步{cateType.GetDesc()}时发生异常,{ex.StackTrace}");
|
||||
}
|
||||
|
||||
//Serilog.Log.Debug($"本次收藏夹{cateType}同步结果,新增:{addCount},更新:{updateCount},删除:{deleteCount}");
|
||||
// 8. 返回最终结果
|
||||
return (addCount, updateCount, deleteCount, isSuccess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 短剧、合集完结
|
||||
/// </summary>
|
||||
/// <param name="cate"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> UpdateCateEndStatus(DouyinCollectCate cate)
|
||||
{
|
||||
var videoCounts = await Db.Queryable<DouyinVideo>().Where(x => x.CateId == cate.Id && x.CateXId == cate.XId && x.ViedoType == cate.CateType).CountAsync();
|
||||
cate.IsEnd = videoCounts == cate.Total;
|
||||
|
||||
var update = await Db.Updateable(cate).UpdateColumns(x => new { x.IsEnd }).ExecuteCommandAsync();
|
||||
return update > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,14 @@ namespace dy.net.repository
|
||||
|
||||
|
||||
var totalCount = await where.CountAsync();
|
||||
var list = await where.OrderByDescending(x => x.SyncTime).Skip((dto.PageIndex - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync();
|
||||
List<DouyinVideo> list = new List<DouyinVideo>();
|
||||
if(string.IsNullOrWhiteSpace(dto.SortField))
|
||||
list = await where.OrderByDescending(x => x.SyncTime).Skip((dto.PageIndex - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync();
|
||||
else
|
||||
{
|
||||
|
||||
list = await where.OrderBy($"{dto.SortField} {dto.SortOrder}").Skip((dto.PageIndex - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync();
|
||||
}
|
||||
if (list.Any())
|
||||
{
|
||||
var users = await this.Db.Queryable<DouyinCookie>().ToListAsync();
|
||||
@@ -93,12 +100,12 @@ namespace dy.net.repository
|
||||
/// <param name="AuthorId"></param>
|
||||
/// <param name="ViedoNameSimplify"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
||||
public (string, string) GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
||||
{
|
||||
|
||||
var video = await this.Db.Queryable<DouyinVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.dy_follows)
|
||||
var video = this.Db.Queryable<DouyinVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.dy_follows)
|
||||
.Where(x => x.VideoTitleSimplify == ViedoNameSimplify)
|
||||
.OrderByDescending(x => x.CreateTime).FirstAsync();
|
||||
.OrderByDescending(x => x.CreateTime).First();
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
|
||||
@@ -11,18 +11,18 @@ namespace dy.net.service
|
||||
public class DouyinCollectCateService
|
||||
{
|
||||
|
||||
private readonly DouyinCollectCateRepository douyinCollectCateRepository;
|
||||
private readonly DouyinCollectCateRepository _douyinCollectCateRepository;
|
||||
|
||||
public DouyinCollectCateService(DouyinCollectCateRepository douyinCollectCateRepository)
|
||||
{
|
||||
douyinCollectCateRepository = douyinCollectCateRepository;
|
||||
_douyinCollectCateRepository = douyinCollectCateRepository;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<bool> AddAsync(DouyinCollectCate cate)
|
||||
{
|
||||
return await douyinCollectCateRepository.InsertAsync(cate);
|
||||
return await _douyinCollectCateRepository.InsertAsync(cate);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task<(List<DouyinCollectCate> list, int totalCount)> GetPagedAsync(DouyinCollectCateRequestDto dto)
|
||||
{
|
||||
return await douyinCollectCateRepository.GetPagedAsync(dto);
|
||||
return await _douyinCollectCateRepository.GetPagedAsync(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,7 +47,7 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task<(int add, int update,int delete, bool succ)> Sync(List<DouyinCollectCate> cates, string ckId,VideoTypeEnum cateType)
|
||||
{
|
||||
return await douyinCollectCateRepository.Sync(cates, ckId, cateType);
|
||||
return await _douyinCollectCateRepository.Sync(cates, ckId, cateType);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task<bool> BatchSwitchSync(List<DouyinCollectCateSwitchDto> dto)
|
||||
{
|
||||
return await douyinCollectCateRepository.SwitchBatchAsync(dto);
|
||||
return await _douyinCollectCateRepository.SwitchBatchAsync(dto);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,17 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task<List<DouyinCollectCate>> GetSyncCates(string cookieId, VideoTypeEnum cateType)
|
||||
{
|
||||
return await douyinCollectCateRepository.GetListAsync(x => x.CookieId == cookieId && x.CateType == cateType);
|
||||
return await _douyinCollectCateRepository.GetListAsync(x => x.CookieId == cookieId && x.CateType == cateType && x.Sync && x.Total > 0);
|
||||
}
|
||||
/// <summary>
|
||||
/// 完结
|
||||
/// </summary>
|
||||
/// <param name="cate"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public async Task<bool> UpdateCate2EndStatus(DouyinCollectCate cate)
|
||||
{
|
||||
return await _douyinCollectCateRepository.UpdateCateEndStatus(cate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,12 +49,11 @@ namespace dy.net.service
|
||||
Cron = 30,
|
||||
BatchCount = 18,
|
||||
LogKeepDay = 7,
|
||||
UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
|
||||
//UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
|
||||
UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名
|
||||
DownImageVideo = true,//默认下载图文视频
|
||||
DownMp3 = false,
|
||||
DownImage = false,
|
||||
ImageViedoSaveAlone = true,
|
||||
FollowedTitleTemplate = "",
|
||||
FullFollowedTitleTemplate = "",
|
||||
FollowedTitleSeparator = "",
|
||||
|
||||
@@ -53,14 +53,14 @@ namespace dy.net.service
|
||||
var cookie = new DouyinCookie
|
||||
{
|
||||
UserName = "douyin2026",
|
||||
Cookies = "-",
|
||||
SecUserId = "-",
|
||||
Cookies = "",
|
||||
SecUserId = "",
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Status = 0,
|
||||
SavePath = "/app/collect",
|
||||
FavSavePath = "/app/favorite",
|
||||
UpSavePath = "/app/uper",
|
||||
ImgSavePath="/app/images",
|
||||
//ImgSavePath="/app/images",
|
||||
CollHasSyncd = 0,
|
||||
FavHasSyncd = 0,
|
||||
UperSyncd = 0,
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace dy.net.service
|
||||
requestParameters["cursor"] = cursor; //页码
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
@@ -220,12 +220,13 @@ namespace dy.net.service
|
||||
|
||||
var requestParameters = DouyinRequestParamManager.DouyinFolderCollectParams;
|
||||
{
|
||||
count = "15";
|
||||
requestParameters["cursor"] = cursor;
|
||||
requestParameters["count"] = count;
|
||||
requestParameters["collects_id"] = collectsId;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -278,7 +279,7 @@ namespace dy.net.service
|
||||
requestParameters["cursor"] = cursor; //页码
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
@@ -342,11 +343,11 @@ namespace dy.net.service
|
||||
var requestParameters = DouyinRequestParamManager.DouyinMixVideoParams;
|
||||
{
|
||||
requestParameters["cursor"] = cursor;
|
||||
requestParameters["count"] = count;
|
||||
requestParameters["count"] = "15";
|
||||
requestParameters["mix_id"] = mixId;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -394,11 +395,11 @@ namespace dy.net.service
|
||||
var requestParameters = DouyinRequestParamManager.DouyinSeriesListParams;
|
||||
{
|
||||
// 添加动态参数
|
||||
requestParameters["count"] = "100";
|
||||
requestParameters["count"] = "15";//固定15,多了直接返回参数不合法
|
||||
requestParameters["cursor"] = cursor; //页码
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
@@ -466,7 +467,7 @@ namespace dy.net.service
|
||||
requestParameters["series_id"] = seriesId;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -538,7 +539,7 @@ namespace dy.net.service
|
||||
requestParameters["sec_user_id"] = secUserId;
|
||||
requestParameters["count"] = count;
|
||||
}
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -610,7 +611,7 @@ namespace dy.net.service
|
||||
requestParameters["sec_user_id"] = secUserId;
|
||||
requestParameters["count"] = count;
|
||||
}
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -683,7 +684,7 @@ namespace dy.net.service
|
||||
requestParameters["offset"] = offset;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using dy.net.job;
|
||||
using dy.net.model.dto;
|
||||
using dy.net.utils;
|
||||
using Quartz;
|
||||
using Quartz.Impl.Matchers;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.service
|
||||
@@ -19,19 +21,11 @@ namespace dy.net.service
|
||||
private const int DefaultCronStartDelaySeconds = 30;
|
||||
private const int DefaultSimpleStartDelaySeconds = 3;
|
||||
|
||||
// 任务顺序依赖配置(核心:定义执行顺序,供 Listener 使用)
|
||||
public Dictionary<string, string> JobDependency { get; } = new()
|
||||
{
|
||||
{"collect", "favorite"}, // collect 执行完 → 触发 favorite
|
||||
{"favorite", "followed"}, // favorite 执行完 → 触发 followed
|
||||
{"followed", null}, // followed 执行完 → 一轮任务结束
|
||||
};
|
||||
|
||||
// 任务配置信息(保持原有配置不变,改为 public 供 Listener 访问)
|
||||
public Dictionary<string, JobConfig> JobConfigs { get; } = new()
|
||||
// 任务配置信息(修复了series任务的Key重复问题,确保每个任务Key唯一)
|
||||
public static Dictionary<string, JobConfig> JobConfigs { get; } = new()
|
||||
{
|
||||
{
|
||||
"collect",
|
||||
VideoTypeEnum.dy_collects.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinCollectSyncJob),
|
||||
"dy.job.key.collect",
|
||||
@@ -39,7 +33,7 @@ namespace dy.net.service
|
||||
"抖音收藏同步任务")
|
||||
},
|
||||
{
|
||||
"favorite",
|
||||
VideoTypeEnum.dy_favorite.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinFavoritSyncJob),
|
||||
"dy.job.key.favorite",
|
||||
@@ -47,12 +41,12 @@ namespace dy.net.service
|
||||
"抖音点赞同步任务")
|
||||
},
|
||||
{
|
||||
"followed",
|
||||
VideoTypeEnum.dy_follows.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinFollowedViedoSyncJob),
|
||||
typeof(DouyinFollowedSyncJob),
|
||||
"dy.job.key.followed",
|
||||
"dy.trigger.key.followed",
|
||||
"抖音UP主作品同步任务")
|
||||
"抖音关注博主作品同步任务")
|
||||
},
|
||||
{
|
||||
"follow_user",
|
||||
@@ -60,7 +54,31 @@ namespace dy.net.service
|
||||
typeof(DouyinFollowsAndCollnectsSyncJob),
|
||||
"dy.job.key.follow_user",
|
||||
"dy.trigger.key.follow_user",
|
||||
"抖音关注同步任务")
|
||||
"抖音关注列表同步任务")
|
||||
},
|
||||
{
|
||||
VideoTypeEnum.dy_custom_collect.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinCollectCustomSyncJob),
|
||||
"dy.job.key.custom_collect",
|
||||
"dy.trigger.key.custom_collect",
|
||||
"抖音自定义收藏夹列表同步任务")
|
||||
},
|
||||
{
|
||||
VideoTypeEnum.dy_mix.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinMixSyncJob),
|
||||
"dy.job.key.mix",
|
||||
"dy.trigger.key.mix",
|
||||
"抖音收藏夹合集同步任务")
|
||||
},
|
||||
{
|
||||
VideoTypeEnum.dy_series.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinSeriesSyncJob),
|
||||
"dy.job.key.series",
|
||||
"dy.trigger.key.series",
|
||||
"抖音收藏夹短剧同步任务")
|
||||
},
|
||||
{
|
||||
"follow_user_once",
|
||||
@@ -78,9 +96,9 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动所有抖音相关定时任务(顺序执行模式)
|
||||
/// 启动所有抖音相关定时任务(所有任务独立执行)
|
||||
/// </summary>
|
||||
/// <param name="expression">Cron表达式或间隔分钟数(控制整个链条的执行频率)</param>
|
||||
/// <param name="expression">Cron表达式或间隔分钟数(所有任务使用相同的执行频率)</param>
|
||||
/// <returns>是否启动成功</returns>
|
||||
public async Task<bool> InitOrReStartAllJobs(string expression)
|
||||
{
|
||||
@@ -94,31 +112,37 @@ namespace dy.net.service
|
||||
{
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
|
||||
// 1. 注册独立的 JobListener(核心:注入配置和服务)
|
||||
await RegisterJobListener(scheduler);
|
||||
|
||||
// 2. 移除所有已存在的任务(避免重复调度)
|
||||
// 移除所有已存在的任务(避免重复调度)
|
||||
await RemoveAllExistingJobs(scheduler);
|
||||
|
||||
// 3. 只启动第一个任务(collect),后续任务由 Listener 自动触发
|
||||
var firstJobConfigKey = "collect";
|
||||
var startSuccess = await StartJobAsync(firstJobConfigKey, expression);
|
||||
// 遍历启动所有任务(独立执行,无顺序依赖)
|
||||
var allJobKeys = JobConfigs.Keys.Where(k => k != "follow_user_once").ToList(); // 排除单次执行的任务
|
||||
foreach (var jobKey in allJobKeys)
|
||||
{
|
||||
if (jobKey == "follow_user")
|
||||
{
|
||||
expression = "60";
|
||||
}
|
||||
var startSuccess = await StartJobAsync(jobKey, expression);
|
||||
if (startSuccess)
|
||||
{
|
||||
Log.Debug($"成功启动任务:{jobKey},执行频率:{expression}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"启动任务失败:{jobKey}");
|
||||
}
|
||||
}
|
||||
|
||||
if (startSuccess)
|
||||
{
|
||||
Log.Debug($"同步任务执行顺序:collect → favorite → followed-->默认每{expression}分钟执行一次...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"任务执行失败(任务 {firstJobConfigKey} 启动失败)");
|
||||
}
|
||||
//启动follow_user--这个与其他几个任务没有依赖关系,所以单独启动
|
||||
await StartJobAsync("follow_user", expression);
|
||||
return startSuccess;
|
||||
Log.Information($"共启动 {allJobKeys.Count} 个定时任务,执行频率:{expression}");
|
||||
|
||||
//await StartJobAsync(VideoTypeEnum.dy_custom_collect.GetDesc(), expression);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "【任务服务】初始化任务链条异常");
|
||||
Log.Error(ex, "【任务服务】初始化所有定时任务异常");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -131,27 +155,6 @@ namespace dy.net.service
|
||||
return await StartOneTimeJobAsync("follow_user_once");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册独立的 JobListener(核心步骤)
|
||||
/// </summary>
|
||||
private async Task RegisterJobListener(IScheduler scheduler)
|
||||
{
|
||||
// 创建独立的 Listener 实例,注入依赖(任务配置、依赖关系、当前服务)
|
||||
var dependencyListener = new DouyinJobDependencyListener(
|
||||
JobConfigs, // 任务配置
|
||||
JobDependency, // 依赖顺序
|
||||
this // 任务服务(用于触发下一个任务)
|
||||
);
|
||||
|
||||
// 注册 Listener:仅监听 DefaultJobGroup 分组的任务(精准匹配,避免影响其他任务)
|
||||
scheduler.ListenerManager.AddJobListener(
|
||||
dependencyListener,
|
||||
GroupMatcher<JobKey>.GroupEquals(DefaultJobGroup)
|
||||
);
|
||||
|
||||
Log.Information("【任务服务】JobListener 注册成功:{ListenerName}", dependencyListener.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除所有已存在的任务(避免重复调度)
|
||||
/// </summary>
|
||||
@@ -169,13 +172,12 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动指定定时任务(public 修饰,供 Listener 调用)
|
||||
/// 启动指定定时任务(独立执行,无依赖触发)
|
||||
/// </summary>
|
||||
/// <param name="configKey">任务配置Key(如:collect、favorite)</param>
|
||||
/// <param name="expression">定时表达式(依赖触发时传空)</param>
|
||||
/// <param name="isDependencyTrigger">是否为依赖触发(true=立即执行,false=定时执行)</param>
|
||||
/// <param name="expression">定时表达式(Cron或间隔分钟数)</param>
|
||||
/// <returns>是否启动成功</returns>
|
||||
public async Task<bool> StartJobAsync(string configKey, string expression, bool isDependencyTrigger = false)
|
||||
public async Task<bool> StartJobAsync(string configKey, string expression)
|
||||
{
|
||||
if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
{
|
||||
@@ -187,33 +189,26 @@ namespace dy.net.service
|
||||
{
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
|
||||
// 触发器Key:区分「定时触发」和「依赖触发」,避免冲突
|
||||
var triggerKey = new TriggerKey(
|
||||
$"{jobConfig.TriggerKey}_{(isDependencyTrigger ? "dependency" : "main")}",
|
||||
DefaultJobGroup
|
||||
);
|
||||
var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup);
|
||||
|
||||
// 移除已存在的任务(防止重复执行)
|
||||
await RemoveExistingJobAsync(scheduler, jobKey);
|
||||
|
||||
// 创建任务详情(添加禁止并发执行特性,避免顺序混乱)
|
||||
// 创建任务详情(保留禁止并发执行,避免同一任务重复运行)
|
||||
var jobDetail = JobBuilder.Create(jobConfig.JobType)
|
||||
.WithIdentity(jobKey)
|
||||
.WithDescription(jobConfig.Description)
|
||||
.DisallowConcurrentExecution() // 关键:禁止同一任务并发执行
|
||||
.DisallowConcurrentExecution() // 禁止同一任务并发执行
|
||||
.Build();
|
||||
|
||||
// 创建立触发器
|
||||
ITrigger trigger = isDependencyTrigger
|
||||
? CreateDependencyTrigger(triggerKey, jobConfig.Description) // 依赖触发:立即执行
|
||||
: CreateScheduledTrigger(triggerKey, expression, jobConfig.Description); // 定时触发:按表达式执行
|
||||
// 创建定时触发器(仅使用定时触发,移除依赖触发逻辑)
|
||||
ITrigger trigger = CreateScheduledTrigger(triggerKey, expression, jobConfig.Description);
|
||||
|
||||
// 调度任务
|
||||
await scheduler.ScheduleJob(jobDetail, trigger);
|
||||
Log.Information("【任务服务】启动任务成功 - 任务描述: {JobDescription}, 触发类型: {TriggerType}, 表达式: {Expression}",
|
||||
Log.Information("【任务服务】启动任务成功 - 任务描述: {JobDescription}, 执行频率: {Expression}",
|
||||
jobConfig.Description,
|
||||
isDependencyTrigger ? "依赖触发(立即执行)" : "定时触发",
|
||||
isDependencyTrigger ? "无" : expression);
|
||||
expression);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -225,7 +220,7 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动单次执行任务(保持原有逻辑不变)
|
||||
/// 启动单次执行任务
|
||||
/// </summary>
|
||||
private async Task<bool> StartOneTimeJobAsync(string configKey)
|
||||
{
|
||||
@@ -268,9 +263,9 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建「定时触发器」(按表达式执行,仅第一个任务使用)
|
||||
/// 创建定时触发器(支持Cron表达式或分钟间隔)
|
||||
/// </summary>
|
||||
private ITrigger CreateScheduledTrigger(TriggerKey triggerKey, string expression, string jobDescription)
|
||||
private static ITrigger CreateScheduledTrigger(TriggerKey triggerKey, string expression, string jobDescription)
|
||||
{
|
||||
// Cron表达式格式
|
||||
if (CronExpression.IsValidExpression(expression))
|
||||
@@ -312,21 +307,9 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建「依赖触发器」(立即执行,仅执行一次)
|
||||
/// 移除已存在的任务
|
||||
/// </summary>
|
||||
private ITrigger CreateDependencyTrigger(TriggerKey triggerKey, string jobDescription)
|
||||
{
|
||||
return TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithDescription($"{jobDescription} - 依赖触发(立即执行)")
|
||||
.StartNow() // 立即触发
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除已存在的任务(保持原有逻辑不变)
|
||||
/// </summary>
|
||||
private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
||||
private static async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
||||
{
|
||||
if (await scheduler.CheckExists(jobKey))
|
||||
{
|
||||
@@ -334,6 +317,5 @@ namespace dy.net.service
|
||||
await scheduler.DeleteJob(jobKey);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
//using dy.net.job;
|
||||
//using dy.net.model.dto;
|
||||
//using Quartz;
|
||||
//using Quartz.Impl.Matchers;
|
||||
//using Serilog;
|
||||
//using System;
|
||||
//using System.Threading.Tasks;
|
||||
|
||||
//namespace dy.net.service
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// 抖音相关定时任务服务
|
||||
// /// </summary>
|
||||
// public class DouyinQuartzJobService
|
||||
// {
|
||||
// private readonly ISchedulerFactory _schedulerFactory;
|
||||
// private const string DefaultJobGroup = "dysync.net";
|
||||
// private const int DefaultIntervalMinutes = 30;
|
||||
// private const int DefaultCronStartDelaySeconds = 30;
|
||||
// private const int DefaultSimpleStartDelaySeconds = 3;
|
||||
|
||||
// // 任务顺序依赖配置(核心:定义执行顺序,供 Listener 使用)
|
||||
// public Dictionary<string, string> JobDependency { get; } = new()
|
||||
// {
|
||||
// {"collect", "favorite"}, // collect 执行完 → 触发 favorite
|
||||
// {"favorite", "followed"}, // favorite 执行完 → 触发 followed
|
||||
// {"followed", null}, // followed 执行完 → 一轮任务结束
|
||||
// };
|
||||
|
||||
// // 任务配置信息(保持原有配置不变,改为 public 供 Listener 访问)
|
||||
// public Dictionary<string, JobConfig> JobConfigs { get; } = new()
|
||||
// {
|
||||
// {
|
||||
// "collect",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinCollectSyncJob),
|
||||
// "dy.job.key.collect",
|
||||
// "dy.trigger.key.collect",
|
||||
// "抖音收藏同步任务")
|
||||
// },
|
||||
// {
|
||||
// "favorite",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFavoritSyncJob),
|
||||
// "dy.job.key.favorite",
|
||||
// "dy.trigger.key.favorite",
|
||||
// "抖音点赞同步任务")
|
||||
// },
|
||||
// {
|
||||
// "followed",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFollowedSyncJob),
|
||||
// "dy.job.key.followed",
|
||||
// "dy.trigger.key.followed",
|
||||
// "抖音关注博主作品同步任务")
|
||||
// },
|
||||
// {
|
||||
// "follow_user",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFollowsAndCollnectsSyncJob),
|
||||
// "dy.job.key.follow_user",
|
||||
// "dy.trigger.key.follow_user",
|
||||
// "抖音关注列表同步任务")
|
||||
// },
|
||||
|
||||
// {
|
||||
// "custom_collect",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinCollectCustomSyncJob),
|
||||
// "dy.job.key.custom_collect",
|
||||
// "dy.trigger.key.custom_collect",
|
||||
// "抖音自定义收藏夹列表同步任务")
|
||||
// },
|
||||
|
||||
// {
|
||||
// "mix",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinMixSyncJob),
|
||||
// "dy.job.key.mix",
|
||||
// "dy.trigger.key.mix",
|
||||
// "抖音收藏夹合集同步任务")
|
||||
// },
|
||||
// {
|
||||
// "series",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinSeriesSyncJob),
|
||||
// "dy.job.key.mix",
|
||||
// "dy.trigger.key.mix",
|
||||
// "抖音收藏夹短剧同步任务")
|
||||
// },
|
||||
// {
|
||||
// "follow_user_once",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFollowsAndCollnectsSyncJob),
|
||||
// "dy.job.key.follow_user_once",
|
||||
// "dy.trigger.key.follow_user_once",
|
||||
// "抖音关注同步任务(单次执行)")
|
||||
// }
|
||||
// };
|
||||
|
||||
// public DouyinQuartzJobService(ISchedulerFactory schedulerFactory)
|
||||
// {
|
||||
// _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动所有抖音相关定时任务(顺序执行模式)
|
||||
// /// </summary>
|
||||
// /// <param name="expression">Cron表达式或间隔分钟数(控制整个链条的执行频率)</param>
|
||||
// /// <returns>是否启动成功</returns>
|
||||
// public async Task<bool> InitOrReStartAllJobs(string expression)
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(expression))
|
||||
// {
|
||||
// Log.Debug("定时任务表达式为空,使用默认配置({DefaultMinutes}分钟)", DefaultIntervalMinutes);
|
||||
// expression = DefaultIntervalMinutes.ToString();
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// var scheduler = await _schedulerFactory.GetScheduler();
|
||||
|
||||
// // 1. 注册独立的 JobListener(核心:注入配置和服务)
|
||||
// await RegisterJobListener(scheduler);
|
||||
|
||||
// // 2. 移除所有已存在的任务(避免重复调度)
|
||||
// await RemoveAllExistingJobs(scheduler);
|
||||
|
||||
// // 3. 只启动第一个任务(collect),后续任务由 Listener 自动触发
|
||||
// var firstJobConfigKey = "collect";
|
||||
// //var startSuccess = await StartJobAsync(firstJobConfigKey, expression);
|
||||
|
||||
// //if (startSuccess)
|
||||
// //{
|
||||
// // Log.Debug($"同步任务执行顺序:collect → favorite → followed-->默认每{expression}分钟执行一次...");
|
||||
// //}
|
||||
// //else
|
||||
// //{
|
||||
// // Log.Error($"任务执行失败(任务 {firstJobConfigKey} 启动失败)");
|
||||
// //}
|
||||
// //启动follow_user--这个与其他几个任务没有依赖关系,所以单独启动
|
||||
// //await StartJobAsync("collect", expression);
|
||||
// //await StartJobAsync("follow_user", expression);
|
||||
// //await StartJobAsync("follow_user", expression);
|
||||
// //await StartJobAsync("follow_user", expression);
|
||||
// //await StartJobAsync("custom_collect", expression);
|
||||
// //await StartJobAsync("mix", expression);
|
||||
// //await StartJobAsync("series", expression);
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Error(ex, "【任务服务】初始化任务链条异常");
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动关注同步任务(单次执行)
|
||||
// /// </summary>
|
||||
// public async Task<bool> StartFollowJobOnceAsync()
|
||||
// {
|
||||
// return await StartOneTimeJobAsync("follow_user_once");
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 注册独立的 JobListener(核心步骤)
|
||||
// /// </summary>
|
||||
// private async Task RegisterJobListener(IScheduler scheduler)
|
||||
// {
|
||||
// // 创建独立的 Listener 实例,注入依赖(任务配置、依赖关系、当前服务)
|
||||
// var dependencyListener = new DouyinJobDependencyListener(
|
||||
// JobConfigs, // 任务配置
|
||||
// JobDependency, // 依赖顺序
|
||||
// this // 任务服务(用于触发下一个任务)
|
||||
// );
|
||||
|
||||
// // 注册 Listener:仅监听 DefaultJobGroup 分组的任务(精准匹配,避免影响其他任务)
|
||||
// scheduler.ListenerManager.AddJobListener(
|
||||
// dependencyListener,
|
||||
// GroupMatcher<JobKey>.GroupEquals(DefaultJobGroup)
|
||||
// );
|
||||
|
||||
// Log.Information("【任务服务】JobListener 注册成功:{ListenerName}", dependencyListener.Name);
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 移除所有已存在的任务(避免重复调度)
|
||||
// /// </summary>
|
||||
// private async Task RemoveAllExistingJobs(IScheduler scheduler)
|
||||
// {
|
||||
// var jobKeys = JobConfigs.Values.Select(config => new JobKey(config.JobKey, DefaultJobGroup)).ToList();
|
||||
// foreach (var jobKey in jobKeys)
|
||||
// {
|
||||
// if (await scheduler.CheckExists(jobKey))
|
||||
// {
|
||||
// Log.Information("【任务服务】移除已存在的任务: {JobKey}", jobKey);
|
||||
// await scheduler.DeleteJob(jobKey);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动指定定时任务(public 修饰,供 Listener 调用)
|
||||
// /// </summary>
|
||||
// /// <param name="configKey">任务配置Key(如:collect、favorite)</param>
|
||||
// /// <param name="expression">定时表达式(依赖触发时传空)</param>
|
||||
// /// <param name="isDependencyTrigger">是否为依赖触发(true=立即执行,false=定时执行)</param>
|
||||
// /// <returns>是否启动成功</returns>
|
||||
// public async Task<bool> StartJobAsync(string configKey, string expression, bool isDependencyTrigger = false)
|
||||
// {
|
||||
// if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
// {
|
||||
// Log.Error("【任务服务】找不到任务配置: {ConfigKey}", configKey);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// var scheduler = await _schedulerFactory.GetScheduler();
|
||||
// var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
|
||||
// // 触发器Key:区分「定时触发」和「依赖触发」,避免冲突
|
||||
// var triggerKey = new TriggerKey(
|
||||
// $"{jobConfig.TriggerKey}_{(isDependencyTrigger ? "dependency" : "main")}",
|
||||
// DefaultJobGroup
|
||||
// );
|
||||
|
||||
// // 移除已存在的任务(防止重复执行)
|
||||
// await RemoveExistingJobAsync(scheduler, jobKey);
|
||||
|
||||
// // 创建任务详情(添加禁止并发执行特性,避免顺序混乱)
|
||||
// var jobDetail = JobBuilder.Create(jobConfig.JobType)
|
||||
// .WithIdentity(jobKey)
|
||||
// .WithDescription(jobConfig.Description)
|
||||
// .DisallowConcurrentExecution() // 关键:禁止同一任务并发执行
|
||||
// .Build();
|
||||
|
||||
// // 创建立触发器
|
||||
// ITrigger trigger = isDependencyTrigger
|
||||
// ? CreateDependencyTrigger(triggerKey, jobConfig.Description) // 依赖触发:立即执行
|
||||
// : CreateScheduledTrigger(triggerKey, expression, jobConfig.Description); // 定时触发:按表达式执行
|
||||
|
||||
// // 调度任务
|
||||
// await scheduler.ScheduleJob(jobDetail, trigger);
|
||||
// Log.Information("【任务服务】启动任务成功 - 任务描述: {JobDescription}, 触发类型: {TriggerType}, 表达式: {Expression}",
|
||||
// jobConfig.Description,
|
||||
// isDependencyTrigger ? "依赖触发(立即执行)" : "定时触发",
|
||||
// isDependencyTrigger ? "无" : expression);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Error(ex, "【任务服务】启动任务失败 - 任务描述: {JobDescription}", jobConfig.Description);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动单次执行任务(保持原有逻辑不变)
|
||||
// /// </summary>
|
||||
// private async Task<bool> StartOneTimeJobAsync(string configKey)
|
||||
// {
|
||||
// if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
// {
|
||||
// Log.Error("【任务服务】找不到任务配置: {ConfigKey}", configKey);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// var scheduler = await _schedulerFactory.GetScheduler();
|
||||
// var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
|
||||
// var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup);
|
||||
|
||||
// await RemoveExistingJobAsync(scheduler, jobKey);
|
||||
|
||||
// var jobDetail = JobBuilder.Create(jobConfig.JobType)
|
||||
// .WithIdentity(jobKey)
|
||||
// .WithDescription(jobConfig.Description)
|
||||
// .DisallowConcurrentExecution()
|
||||
// .Build();
|
||||
|
||||
// var trigger = TriggerBuilder.Create()
|
||||
// .WithIdentity(triggerKey)
|
||||
// .WithDescription($"{jobConfig.Description} - 单次执行")
|
||||
// .StartNow()
|
||||
// .Build();
|
||||
|
||||
// await scheduler.ScheduleJob(jobDetail, trigger);
|
||||
// Log.Information("【任务服务】启动单次任务成功 - 任务描述: {JobDescription}", jobConfig.Description);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Error(ex, "【任务服务】启动单次任务失败 - 任务描述: {JobDescription}", jobConfig.Description);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 创建「定时触发器」(按表达式执行,仅第一个任务使用)
|
||||
// /// </summary>
|
||||
// private ITrigger CreateScheduledTrigger(TriggerKey triggerKey, string expression, string jobDescription)
|
||||
// {
|
||||
// // Cron表达式格式
|
||||
// if (CronExpression.IsValidExpression(expression))
|
||||
// {
|
||||
// return TriggerBuilder.Create()
|
||||
// .WithIdentity(triggerKey)
|
||||
// .WithDescription($"{jobDescription} - Cron调度")
|
||||
// .WithCronSchedule(expression)
|
||||
// .StartAt(DateTime.Now.AddSeconds(DefaultCronStartDelaySeconds))
|
||||
// .Build();
|
||||
// }
|
||||
|
||||
// // 数字间隔格式(分钟)
|
||||
// if (int.TryParse(expression, out int intervalMinutes))
|
||||
// {
|
||||
// intervalMinutes = Math.Max(1, intervalMinutes); // 最小间隔1分钟
|
||||
// return TriggerBuilder.Create()
|
||||
// .WithIdentity(triggerKey)
|
||||
// .WithDescription($"{jobDescription} - 间隔{intervalMinutes}分钟调度")
|
||||
// .StartAt(DateTime.Now.AddSeconds(DefaultSimpleStartDelaySeconds))
|
||||
// .WithSimpleSchedule(x => x
|
||||
// .WithIntervalInMinutes(intervalMinutes)
|
||||
// .RepeatForever())
|
||||
// .Build();
|
||||
// }
|
||||
|
||||
// // 无效表达式,使用默认配置
|
||||
// Log.Warning("【任务服务】无效的任务表达式: {Expression},使用默认间隔{DefaultMinutes}分钟",
|
||||
// expression, DefaultIntervalMinutes);
|
||||
|
||||
// return TriggerBuilder.Create()
|
||||
// .WithIdentity(triggerKey)
|
||||
// .WithDescription($"{jobDescription} - 默认间隔调度")
|
||||
// .StartAt(DateTime.Now.AddSeconds(DefaultSimpleStartDelaySeconds))
|
||||
// .WithSimpleSchedule(x => x
|
||||
// .WithIntervalInMinutes(DefaultIntervalMinutes)
|
||||
// .RepeatForever())
|
||||
// .Build();
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 创建「依赖触发器」(立即执行,仅执行一次)
|
||||
// /// </summary>
|
||||
// private ITrigger CreateDependencyTrigger(TriggerKey triggerKey, string jobDescription)
|
||||
// {
|
||||
// return TriggerBuilder.Create()
|
||||
// .WithIdentity(triggerKey)
|
||||
// .WithDescription($"{jobDescription} - 依赖触发(立即执行)")
|
||||
// .StartNow() // 立即触发
|
||||
// .Build();
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 移除已存在的任务(保持原有逻辑不变)
|
||||
// /// </summary>
|
||||
// private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
||||
// {
|
||||
// if (await scheduler.CheckExists(jobKey))
|
||||
// {
|
||||
// Log.Information("【任务服务】移除已存在的任务: {JobKey}", jobKey);
|
||||
// await scheduler.DeleteJob(jobKey);
|
||||
// }
|
||||
// }
|
||||
|
||||
//}
|
||||
//}
|
||||
@@ -4,9 +4,11 @@ using dy.net.model.entity;
|
||||
using dy.net.repository;
|
||||
using dy.net.utils;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using SqlSugar;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.service
|
||||
@@ -81,6 +83,7 @@ namespace dy.net.service
|
||||
{
|
||||
existingVideo.VideoSavePath = updateData.VideoSavePath;
|
||||
existingVideo.VideoCoverSavePath = updateData.VideoCoverSavePath;
|
||||
existingVideo.ViedoType = updateData.ViedoType;
|
||||
}
|
||||
}
|
||||
// 批量更新数据库
|
||||
@@ -94,6 +97,10 @@ namespace dy.net.service
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateOne(DouyinVideo video)
|
||||
{
|
||||
return await _dyCollectVideoRepository.UpdateAsync(video);
|
||||
}
|
||||
|
||||
public async Task<VideoStaticsDto> GetStatics()
|
||||
{
|
||||
@@ -110,14 +117,17 @@ namespace dy.net.service
|
||||
VideoCount = list.Count,
|
||||
Categories = Categories,
|
||||
FavoriteCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_favorite),
|
||||
CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_collects),
|
||||
CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_collects || x.ViedoType == VideoTypeEnum.dy_custom_collect),
|
||||
FollowCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_follows),
|
||||
GraphicVideoCount = list.Count(x => x.IsMergeVideo == 1),
|
||||
|
||||
MixCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_mix),
|
||||
SeriesCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_series),
|
||||
VideoSizeTotal = DouyinFileUtils.ConvertBytesToGb(list.Sum(x => x.FileSize)),
|
||||
VideoFavoriteSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_favorite).Sum(x => x.FileSize)),
|
||||
VideoCollectSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_collects).Sum(x => x.FileSize)),
|
||||
VideoCollectSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_collects || x.ViedoType == VideoTypeEnum.dy_custom_collect).Sum(x => x.FileSize)),
|
||||
VideoFollowSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_follows).Sum(x => x.FileSize)),
|
||||
VideoMixSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_mix).Sum(x => x.FileSize)),
|
||||
VideoSeriesSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_series).Sum(x => x.FileSize)),
|
||||
GraphicVideoSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.IsMergeVideo == 1).Sum(x => x.FileSize)),
|
||||
|
||||
//TotalDiskSize= ByteToGbConverter.GetHostTotalDiskSpaceGB(),
|
||||
@@ -129,6 +139,28 @@ namespace dy.net.service
|
||||
data.GraphicVideoSize = "<0.01";//避免显示0.00误导用户
|
||||
}
|
||||
}
|
||||
if (data.VideoFavoriteSize == "0.00")
|
||||
{
|
||||
data.VideoFavoriteSize = "<0.01";//避免显示0.00误导用户
|
||||
}
|
||||
|
||||
if (data.VideoCollectSize == "0.00")
|
||||
{
|
||||
data.VideoCollectSize = "<0.01";//避免显示0.00误导用户
|
||||
}
|
||||
if (data.VideoFollowSize == "0.00")
|
||||
{
|
||||
data.VideoFollowSize = "<0.01";//避免显示0.00误导用户
|
||||
}
|
||||
if (data.VideoMixSize == "0.00")
|
||||
{
|
||||
data.VideoMixSize = "<0.01";//避免显示0.00误导用户
|
||||
}
|
||||
|
||||
if (data.VideoSeriesSize == "0.00")
|
||||
{
|
||||
data.VideoSeriesSize = "<0.01";//避免显示0.00误导用户
|
||||
}
|
||||
data.Authors = list.GroupBy(x => x.Author).Select(x => new VideoStaticsItemDto
|
||||
{
|
||||
Name = x.Key,
|
||||
@@ -170,10 +202,10 @@ namespace dy.net.service
|
||||
/// <param name="AuthorId"></param>
|
||||
/// <param name="ViedoNameSimplify"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
||||
public (string, string) GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
||||
{
|
||||
|
||||
return await _dyCollectVideoRepository.GetUperLastViedoFileName(AuthorId, ViedoNameSimplify);
|
||||
return _dyCollectVideoRepository.GetUperLastViedoFileName(AuthorId, ViedoNameSimplify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -346,7 +378,27 @@ namespace dy.net.service
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetTopsOrderBySyncTime(top);
|
||||
}
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<VideoChartItemDto>> GetChartData()
|
||||
{
|
||||
var list = await _dyCollectVideoRepository.GetListAsync(x => x.SyncTime > DateTime.Now.AddDays(-7));
|
||||
|
||||
var resultData = list.GroupBy(x => x.SyncTime.ToString("yyyyMMdd")).Select(g => new VideoChartItemDto
|
||||
{
|
||||
Date = g.Key,
|
||||
Collect = g.Count(x => x.ViedoType == VideoTypeEnum.dy_collects || x.ViedoType == VideoTypeEnum.dy_custom_collect),
|
||||
Favorite = g.Count(x => x.ViedoType == VideoTypeEnum.dy_favorite),
|
||||
Follow = g.Count(x => x.ViedoType != VideoTypeEnum.dy_follows),
|
||||
Graphic = g.Count(x => string.IsNullOrEmpty(x.FileHash)),
|
||||
Mix = g.Count(x => x.ViedoType != VideoTypeEnum.dy_mix),
|
||||
Series = g.Count(x => x.ViedoType != VideoTypeEnum.dy_series),
|
||||
})
|
||||
.ToList();
|
||||
return resultData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除无效记录(记录存在,用户手动把目录下的视频删了的情况,视频记录依然存在)
|
||||
@@ -456,5 +508,148 @@ namespace dy.net.service
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> HandOldFolderVideos()
|
||||
{
|
||||
// 1. 查询目标数据
|
||||
var list = await _dyCollectVideoRepository.GetListAsync(x=>x.ViedoType == VideoTypeEnum.dy_favorite || x.ViedoType == VideoTypeEnum.dy_collects );
|
||||
// 缓存已处理的「Tag1+下一级文件夹」组合(避免重复移动)
|
||||
var processedFolderPairs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
// 跳过空值(Tag1/Author为空)
|
||||
if (string.IsNullOrEmpty(item.Tag1) || string.IsNullOrEmpty(item.Author))
|
||||
{
|
||||
Log.Debug($"跳过:Tag1/Author为空,ItemId={item.Id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 标准化路径 + 拆分路径层级(核心:精准定位Tag1和其下一级文件夹)
|
||||
string oldVideoPath = item.VideoSavePath;
|
||||
// 统一分隔符为/,方便拆分层级
|
||||
string standardPath = oldVideoPath.Replace('\\', '/').Trim('/');
|
||||
string[] pathSegments = standardPath.Split('/'); // 拆分结果:["app","collect","校园教育","期末复习xxx","文件.mp4"]
|
||||
|
||||
// 2.1 找到Tag1在路径中的索引(比如"校园教育"的索引是2)
|
||||
int tag1Index = Array.IndexOf(pathSegments, item.Tag1);
|
||||
if (tag1Index == -1 || tag1Index + 1 >= pathSegments.Length - 1)
|
||||
{
|
||||
Log.Debug($"跳过:无Tag1下一级文件夹,Path={oldVideoPath},Tag1={item.Tag1}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2.2 解析核心路径(关键!)
|
||||
string tag1FolderRelative = string.Join("/", pathSegments.Take(tag1Index + 1)); // Tag1根目录(相对):app/collect/校园教育
|
||||
// Tag1根目录完整路径(如 D:/app/collect/校园教育 或 /app/collect/校园教育)
|
||||
string tag1RootFolderFull = Path.GetFullPath(
|
||||
Path.Combine(Path.GetPathRoot(oldVideoPath) ?? "",
|
||||
tag1FolderRelative.Replace('/', Path.DirectorySeparatorChar))
|
||||
);
|
||||
string tag1NextLevelFolderName = pathSegments[tag1Index + 1]; // Tag1下一级文件夹名:期末复习xxx
|
||||
// Tag1下一级文件夹完整路径:app/collect/校园教育/期末复习xxx
|
||||
string tag1NextLevelFolderFull = Path.GetFullPath(
|
||||
Path.Combine(tag1RootFolderFull, tag1NextLevelFolderName)
|
||||
);
|
||||
|
||||
// 2.3 拼接新路径(替换Tag1为Author,保留下一级文件夹名)
|
||||
// 新Author根目录完整路径:app/collect/张三
|
||||
string authorRootFolderFull = tag1RootFolderFull.Replace(item.Tag1, item.Author);
|
||||
// 新的下一级文件夹完整路径:app/collect/张三/期末复习xxx
|
||||
string authorNextLevelFolderFull = Path.GetFullPath(
|
||||
Path.Combine(authorRootFolderFull, tag1NextLevelFolderName)
|
||||
);
|
||||
|
||||
// 2.4 防重复处理(Tag1下一级文件夹已处理则跳过)
|
||||
string folderPairKey = $"{tag1NextLevelFolderFull}|{authorNextLevelFolderFull}";
|
||||
if (processedFolderPairs.Contains(folderPairKey))
|
||||
{
|
||||
Log.Debug($"跳过:下一级文件夹已处理,Key={folderPairKey}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 核心判断:检查Tag1的下一级文件夹是否有文件(而非Tag1根目录)
|
||||
if (!Directory.Exists(tag1NextLevelFolderFull))
|
||||
{
|
||||
Log.Warning($"跳过:Tag1下一级文件夹不存在,Path={tag1NextLevelFolderFull}");
|
||||
continue;
|
||||
}
|
||||
string[] nextLevelFiles = Directory.GetFiles(tag1NextLevelFolderFull); // 非递归,只查该文件夹下的文件
|
||||
if (nextLevelFiles.Length == 0)
|
||||
{
|
||||
Log.Debug($"跳过:Tag1下一级文件夹无文件,Path={tag1NextLevelFolderFull}");
|
||||
processedFolderPairs.Add(folderPairKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. 移动Tag1的下一级文件夹(保留文件夹名,整体移动到Author目录下)
|
||||
// 4.1 确保新Author目录存在
|
||||
Directory.CreateDirectory(authorRootFolderFull);
|
||||
|
||||
// 4.2 目标文件夹已存在则跳过(如需覆盖,可删除此行+添加Directory.Delete(authorNextLevelFolderFull, true))
|
||||
if (Directory.Exists(authorNextLevelFolderFull))
|
||||
{
|
||||
Log.Debug($"跳过:目标下一级文件夹已存在,Path={authorNextLevelFolderFull}");
|
||||
processedFolderPairs.Add(folderPairKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4.3 移动整个下一级文件夹(保留名称和内部所有文件)
|
||||
Directory.Move(tag1NextLevelFolderFull, authorNextLevelFolderFull);
|
||||
Log.Debug($"移动Tag1下一级文件夹成功:{tag1NextLevelFolderFull} → {authorNextLevelFolderFull}");
|
||||
|
||||
// 5. 关键新增:检查Tag1根目录是否为空,为空则删除
|
||||
if (Directory.Exists(tag1RootFolderFull))
|
||||
{
|
||||
// 检查Tag1根目录下是否还有任何文件/文件夹
|
||||
bool isTag1RootEmpty = !Directory.EnumerateFileSystemEntries(tag1RootFolderFull).Any();
|
||||
if (isTag1RootEmpty)
|
||||
{
|
||||
Directory.Delete(tag1RootFolderFull, false); // false=仅删除空目录,避免误删
|
||||
Log.Debug($"删除空Tag1根目录:{tag1RootFolderFull}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug($"Tag1根目录非空,不删除:{tag1RootFolderFull}");
|
||||
}
|
||||
}
|
||||
|
||||
// 标记已处理
|
||||
processedFolderPairs.Add(folderPairKey);
|
||||
|
||||
// 6. 更新当前Item的视频路径(替换Tag1为Author,保留后续层级)
|
||||
string newVideoPath = oldVideoPath.Replace(item.Tag1, item.Author);
|
||||
item.VideoSavePath = newVideoPath;
|
||||
Log.Debug($"更新Item路径:{oldVideoPath} → {newVideoPath}");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Log.Error(ex, $"移动失败(IO异常),ItemId={item.Id},Path={item.VideoSavePath}");
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
Log.Error(ex, $"删除/移动失败(权限不足),ItemId={item.Id},Path={item.VideoSavePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"处理失败(未知错误),ItemId={item.Id},Path={item.VideoSavePath}");
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新数据库
|
||||
if (list.Any())
|
||||
{
|
||||
await BatchInsertOrUpdate(list);
|
||||
Log.Debug($"批量更新数据库完成,共处理{list.Count}条数据");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using dy.net.model.dto;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
@@ -8,6 +9,46 @@ namespace dy.net.utils
|
||||
/// </summary>
|
||||
public static class DouyinRequestParamManager
|
||||
{
|
||||
|
||||
|
||||
#region 基础参数(直接返回模板引用,存在交叉修改风险)
|
||||
/// <summary>
|
||||
/// 私有静态只读基础参数模板(仅创建1次)
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> _baseParamTemplate = new Dictionary<string, string>
|
||||
{
|
||||
{"device_platform", "webapp"},
|
||||
{"aid", "6383"},
|
||||
{"channel", "channel_pc_web"},
|
||||
{"pc_client_type", "1"},
|
||||
{"pc_libra_divert", "Windows"},
|
||||
{"cookie_enabled", "true"},
|
||||
{"browser_language", "zh-CN"},
|
||||
{"browser_platform", "Win32"},
|
||||
{"browser_name", "Chrome"},
|
||||
{"browser_online", "true"},
|
||||
{"engine_name", "Blink"},
|
||||
{"os_name", "Windows"},
|
||||
{"os_version", "10"},
|
||||
{"device_memory", "8"},
|
||||
{"platform", "PC"},
|
||||
{"downlink", "10"},
|
||||
{"effective_type", "4g"},
|
||||
{"round_trip_time", "0"},
|
||||
{"update_version_code", "170400"},
|
||||
{"whale_cut_token", ""}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static Dictionary<string, string> InitBaseParams()
|
||||
{
|
||||
return new Dictionary<string, string>(_baseParamTemplate);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 全局常量(接口调用公共配置)
|
||||
/// <summary>
|
||||
/// 抖音全局域名
|
||||
@@ -399,42 +440,24 @@ namespace dy.net.utils
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 基础参数(直接返回模板引用,存在交叉修改风险)
|
||||
/// <summary>
|
||||
/// 私有静态只读基础参数模板(仅创建1次)
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> _baseParamTemplate = new Dictionary<string, string>
|
||||
{
|
||||
{"device_platform", "webapp"},
|
||||
{"aid", "6383"},
|
||||
{"channel", "channel_pc_web"},
|
||||
{"pc_client_type", "1"},
|
||||
{"pc_libra_divert", "Windows"},
|
||||
{"cookie_enabled", "true"},
|
||||
{"browser_language", "zh-CN"},
|
||||
{"browser_platform", "Win32"},
|
||||
{"browser_name", "Chrome"},
|
||||
{"browser_online", "true"},
|
||||
{"engine_name", "Blink"},
|
||||
{"os_name", "Windows"},
|
||||
{"os_version", "10"},
|
||||
{"device_memory", "8"},
|
||||
{"platform", "PC"},
|
||||
{"downlink", "10"},
|
||||
{"effective_type", "4g"},
|
||||
{"round_trip_time", "0"},
|
||||
{"update_version_code", "170400"},
|
||||
{"whale_cut_token", ""}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// 获取视频类型名称
|
||||
/// </summary>
|
||||
/// <param name="videoType"></param>
|
||||
/// <returns></returns>
|
||||
private static Dictionary<string, string> InitBaseParams()
|
||||
public static string GetDesc(this VideoTypeEnum videoType)
|
||||
{
|
||||
return new Dictionary<string, string>(_baseParamTemplate);
|
||||
return videoType switch
|
||||
{
|
||||
VideoTypeEnum.dy_favorite => "喜欢",
|
||||
VideoTypeEnum.dy_collects => "默认收藏夹",
|
||||
VideoTypeEnum.dy_follows => "关注",
|
||||
VideoTypeEnum.ImageVideo => "图文视频",
|
||||
VideoTypeEnum.dy_custom_collect => "自定义收藏夹",
|
||||
VideoTypeEnum.dy_mix => "合集",
|
||||
VideoTypeEnum.dy_series => "短剧",
|
||||
_ => string.Empty // 匹配所有未定义的枚举值,返回空字符串(替代原 default)
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -45,17 +45,17 @@ namespace dy.net.utils
|
||||
}
|
||||
var nfoActorFullPath = Path.Combine(actorsDir, $"{video.Author}{fileExt}");
|
||||
|
||||
if (File.Exists(nfoActorFullPath))
|
||||
if (!File.Exists(nfoActorFullPath))
|
||||
{
|
||||
File.Delete(nfoActorFullPath);
|
||||
File.Copy(video.AuthorAvatar, nfoActorFullPath, overwrite: true);
|
||||
//File.Delete(video.AuthorAvatar);
|
||||
}
|
||||
// 执行复制(CopyTo支持覆盖,但先删除更可控)
|
||||
File.Copy(video.AuthorAvatar, nfoActorFullPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GenerateNfoFile(new DouyinVideoNfo
|
||||
|
||||
var nfoInfo= new DouyinVideoNfo
|
||||
{
|
||||
Actors = new List<Actor>
|
||||
{
|
||||
@@ -70,7 +70,19 @@ namespace dy.net.utils
|
||||
Thumbnail = "poster.jpg",// 使用poster作为缩略图
|
||||
ReleaseDate = video.CreateTime,
|
||||
Genres = new List<string> { video.Tag1, video.Tag2, video.Tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoFullPath);
|
||||
};
|
||||
if (video.ViedoType == VideoTypeEnum.dy_mix || video.ViedoType == VideoTypeEnum.dy_series)
|
||||
{
|
||||
string directory = Path.GetDirectoryName(nfoFullPath);
|
||||
nfoFullPath = Path.Combine(directory, "tvshow.nfo");
|
||||
if (!File.Exists(nfoFullPath))
|
||||
GenerateNfoFile(nfoInfo, nfoFullPath, "tvshow");
|
||||
}
|
||||
else
|
||||
{
|
||||
GenerateNfoFile(nfoInfo, nfoFullPath);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -78,7 +90,7 @@ namespace dy.net.utils
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateNfoFile(DouyinVideoNfo videoInfo, string filePath)
|
||||
private static void GenerateNfoFile(DouyinVideoNfo videoInfo, string filePath,string xmlRoot= "movie")
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -89,7 +101,7 @@ namespace dy.net.utils
|
||||
throw new ArgumentException("文件路径不能为空", nameof(filePath));
|
||||
|
||||
// 创建根元素
|
||||
XElement root = new XElement("movie");
|
||||
XElement root = new XElement(xmlRoot);
|
||||
//root.Add(new XElement("outline"));
|
||||
root.Add(new XElement("lockdata", true));
|
||||
//root.Add(new XElement("director", videoInfo.Author));
|
||||
|
||||
Reference in New Issue
Block a user