1、删除视频,永不下载
2、去重-优先级 3、视频合成优化 4、其他优化
This commit is contained in:
@@ -4,6 +4,7 @@ using dy.net.service;
|
|||||||
using dy.net.utils;
|
using dy.net.utils;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Drawing.Printing;
|
using System.Drawing.Printing;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -15,12 +16,12 @@ namespace dy.net.Controllers
|
|||||||
public class VideoController : ControllerBase
|
public class VideoController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DouyinVideoService dyCollectVideoService;
|
private readonly DouyinVideoService dyCollectVideoService;
|
||||||
private readonly DouyinQuartzJobService douyinQuartzJobService;
|
private readonly DouyinCommonService douyinCommonService;
|
||||||
|
|
||||||
public VideoController(DouyinVideoService dyCollectVideoService,DouyinQuartzJobService douyinQuartzJobService)
|
public VideoController(DouyinVideoService dyCollectVideoService, DouyinCommonService douyinCommonService)
|
||||||
{
|
{
|
||||||
this.dyCollectVideoService = dyCollectVideoService;
|
this.dyCollectVideoService = dyCollectVideoService;
|
||||||
this.douyinQuartzJobService = douyinQuartzJobService;
|
this.douyinCommonService = douyinCommonService;
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询收藏视频
|
/// 分页查询收藏视频
|
||||||
@@ -175,6 +176,7 @@ namespace dy.net.Controllers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重新下载
|
/// 重新下载
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -193,7 +195,6 @@ namespace dy.net.Controllers
|
|||||||
var result = await dyCollectVideoService.ReDownloadViedoAsync(dto);
|
var result = await dyCollectVideoService.ReDownloadViedoAsync(dto);
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
//douyinQuartzJobService.StartReDownJobOnceAsync();...无法实现。。逆向失败
|
|
||||||
return Ok(new { code = 0, data = true });
|
return Ok(new { code = 0, data = true });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -202,5 +203,42 @@ namespace dy.net.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 删除视频-不再下载
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("vdelete/{vid}")]
|
||||||
|
public async Task<IActionResult> DeleteVideo([FromRoute]string vid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(vid))
|
||||||
|
{
|
||||||
|
return Ok(new { code = -1, data = false });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var video = await dyCollectVideoService.GetById(vid);
|
||||||
|
if (video == null)
|
||||||
|
{
|
||||||
|
return Ok(new { code = -1, data = false });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var result = await dyCollectVideoService.ReDownloadViedoAsync(new ReDownViedoDto { Ids = new List<string> { vid } });
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
//加入删除逻辑
|
||||||
|
await douyinCommonService.AddDeleteVideo(new DouyinVideoDelete { ViedoId = video.AwemeId });
|
||||||
|
return Ok(new { code = 0, data = true });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Ok(new { code = -1, data = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-10
@@ -1,15 +1,7 @@
|
|||||||
using ClockSnowFlake;
|
|
||||||
using dy.net.dto;
|
|
||||||
using dy.net.extension;
|
using dy.net.extension;
|
||||||
using dy.net.model;
|
|
||||||
using dy.net.service;
|
using dy.net.service;
|
||||||
using dy.net.utils;
|
using dy.net.utils;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using SqlSugar;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace dy.net
|
namespace dy.net
|
||||||
@@ -204,8 +196,8 @@ ____/ _|_)_____/ _| _| \_|\____|
|
|||||||
commonService.UpdateCollectViedoType();
|
commonService.UpdateCollectViedoType();
|
||||||
// 重置博主作品同步状态为未同步
|
// 重置博主作品同步状态为未同步
|
||||||
commonService.UpdateAllCookieSyncedToZero();
|
commonService.UpdateAllCookieSyncedToZero();
|
||||||
|
|
||||||
if(!isDevelopment)
|
if (!isDevelopment)
|
||||||
{
|
{
|
||||||
// 启动定时任务
|
// 启动定时任务
|
||||||
var quartzJobService = services.GetRequiredService<DouyinQuartzJobService>();
|
var quartzJobService = services.GetRequiredService<DouyinQuartzJobService>();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
||||||
<History>True|2025-12-02T14:11:37.4645042Z||;False|2025-12-02T22:10:47.1320259+08:00||;True|2025-12-02T14:39:58.8932130+08:00||;True|2025-12-02T14:36:37.1529072+08:00||;True|2025-12-02T12:58:22.0951548+08:00||;True|2025-12-02T09:30:33.7066474+08:00||;True|2025-12-02T09:30:14.5481844+08:00||;True|2025-12-02T09:30:00.3123364+08:00||;False|2025-12-02T09:29:54.4615520+08:00||;True|2025-12-02T09:13:01.7995414+08:00||;False|2025-12-02T09:12:55.8360281+08:00||;True|2025-12-02T09:12:31.0156791+08:00||;True|2025-12-02T08:55:11.3773395+08:00||;True|2025-12-02T08:53:21.3927713+08:00||;False|2025-12-02T08:48:55.8638037+08:00||;True|2025-12-02T07:29:25.2192447+08:00||;False|2025-12-02T07:29:10.2504665+08:00||;True|2025-12-02T07:28:29.0769286+08:00||;True|2025-12-01T21:53:07.6474834+08:00||;True|2025-12-01T21:44:28.1674315+08:00||;True|2025-12-01T21:18:47.2119609+08:00||;True|2025-12-01T20:51:19.9948301+08:00||;True|2025-12-01T20:50:36.9904697+08:00||;True|2025-12-01T20:44:10.1695142+08:00||;True|2025-12-01T19:27:58.0479456+08:00||;True|2025-12-01T19:16:02.2288214+08:00||;False|2025-12-01T19:15:56.0372115+08:00||;True|2025-12-01T19:15:16.4854821+08:00||;False|2025-12-01T19:15:06.7001019+08:00||;True|2025-12-01T17:10:38.9739466+08:00||;False|2025-12-01T17:10:18.8928139+08:00||;True|2025-12-01T17:03:59.3171589+08:00||;True|2025-12-01T17:03:12.5681656+08:00||;True|2025-12-01T16:42:51.4415025+08:00||;True|2025-12-01T01:29:59.6975567+08:00||;True|2025-12-01T01:29:45.1898652+08:00||;False|2025-12-01T01:29:33.1787721+08:00||;True|2025-12-01T01:27:33.1297605+08:00||;True|2025-12-01T01:27:27.3976862+08:00||;False|2025-12-01T01:27:20.5193052+08:00||;True|2025-12-01T01:11:11.0210582+08:00||;True|2025-12-01T01:04:04.3307782+08:00||;True|2025-12-01T00:55:49.8018864+08:00||;True|2025-12-01T00:27:25.1323561+08:00||;True|2025-11-30T23:58:20.7284116+08:00||;True|2025-11-30T23:57:58.3027622+08:00||;True|2025-11-30T23:57:43.0829614+08:00||;False|2025-11-30T23:57:33.5606716+08:00||;True|2025-11-30T23:54:29.6203368+08:00||;True|2025-11-30T23:54:27.5383120+08:00||;False|2025-11-30T23:54:17.0823982+08:00||;True|2025-11-30T22:41:11.4538820+08:00||;True|2025-11-30T22:38:19.7064566+08:00||;True|2025-11-30T22:37:35.2889186+08:00||;True|2025-11-30T21:42:31.1408314+08:00||;True|2025-11-30T21:42:19.5385333+08:00||;False|2025-11-30T21:42:13.3290237+08:00||;True|2025-11-30T21:33:28.0806806+08:00||;True|2025-11-30T21:16:56.7088027+08:00||;False|2025-11-30T21:16:46.1444944+08:00||;True|2025-11-30T20:21:24.4059664+08:00||;False|2025-11-30T20:21:17.6006098+08:00||;True|2025-11-30T20:20:08.2030141+08:00||;False|2025-11-30T20:16:12.3608968+08:00||;True|2025-11-30T20:15:35.6048313+08:00||;True|2025-11-30T19:45:18.7229552+08:00||;True|2025-11-30T19:45:08.6083368+08:00||;False|2025-11-30T19:44:54.7824905+08:00||;True|2025-11-30T19:44:17.0179491+08:00||;True|2025-11-30T19:44:06.7073484+08:00||;False|2025-11-30T19:43:58.2896556+08:00||;True|2025-11-30T19:42:08.2261927+08:00||;True|2025-11-30T19:28:15.7115077+08:00||;False|2025-11-30T19:28:10.2280725+08:00||;True|2025-11-30T19:23:31.7859158+08:00||;True|2025-11-30T19:23:20.7788277+08:00||;True|2025-11-30T19:17:11.8583296+08:00||;False|2025-11-30T19:10:41.8369574+08:00||;True|2025-11-30T19:02:08.8184758+08:00||;False|2025-11-30T19:01:59.4711045+08:00||;True|2025-11-30T18:16:01.8372242+08:00||;True|2025-11-30T14:50:31.9356543+08:00||;True|2025-11-30T14:48:27.1833064+08:00||;True|2025-11-30T02:06:06.6099669+08:00||;True|2025-11-30T01:49:46.4358206+08:00||;False|2025-11-30T01:49:34.4884036+08:00||;True|2025-11-30T01:43:41.1028753+08:00||;True|2025-11-30T01:38:18.2856609+08:00||;True|2025-11-30T01:23:02.4378259+08:00||;True|2025-11-30T01:08:29.4041159+08:00||;True|2025-11-30T00:56:33.4172007+08:00||;False|2025-11-30T00:56:21.2777389+08:00||;True|2025-11-27T13:37:08.6884390+08:00||;False|2025-11-27T13:36:54.2595622+08:00||;True|2025-11-27T11:03:33.8401644+08:00||;False|2025-11-27T11:02:48.8942827+08:00||;True|2025-11-26T23:48:02.3957186+08:00||;True|2025-11-26T23:43:06.8154188+08:00||;False|2025-11-26T23:42:05.9191485+08:00||;True|2025-11-26T23:30:11.1295861+08:00||;</History>
|
<History>True|2025-12-03T15:34:17.1648971Z||;False|2025-12-03T23:34:07.7649161+08:00||;True|2025-12-03T22:32:56.2435003+08:00||;True|2025-12-03T22:27:45.6059666+08:00||;True|2025-12-03T15:55:28.0186597+08:00||;False|2025-12-03T15:54:17.5032724+08:00||;True|2025-12-03T15:53:10.2273509+08:00||;True|2025-12-02T22:11:37.4645042+08:00||;False|2025-12-02T22:10:47.1320259+08:00||;True|2025-12-02T14:39:58.8932130+08:00||;True|2025-12-02T14:36:37.1529072+08:00||;True|2025-12-02T12:58:22.0951548+08:00||;True|2025-12-02T09:30:33.7066474+08:00||;True|2025-12-02T09:30:14.5481844+08:00||;True|2025-12-02T09:30:00.3123364+08:00||;False|2025-12-02T09:29:54.4615520+08:00||;True|2025-12-02T09:13:01.7995414+08:00||;False|2025-12-02T09:12:55.8360281+08:00||;True|2025-12-02T09:12:31.0156791+08:00||;True|2025-12-02T08:55:11.3773395+08:00||;True|2025-12-02T08:53:21.3927713+08:00||;False|2025-12-02T08:48:55.8638037+08:00||;True|2025-12-02T07:29:25.2192447+08:00||;False|2025-12-02T07:29:10.2504665+08:00||;True|2025-12-02T07:28:29.0769286+08:00||;True|2025-12-01T21:53:07.6474834+08:00||;True|2025-12-01T21:44:28.1674315+08:00||;True|2025-12-01T21:18:47.2119609+08:00||;True|2025-12-01T20:51:19.9948301+08:00||;True|2025-12-01T20:50:36.9904697+08:00||;True|2025-12-01T20:44:10.1695142+08:00||;True|2025-12-01T19:27:58.0479456+08:00||;True|2025-12-01T19:16:02.2288214+08:00||;False|2025-12-01T19:15:56.0372115+08:00||;True|2025-12-01T19:15:16.4854821+08:00||;False|2025-12-01T19:15:06.7001019+08:00||;True|2025-12-01T17:10:38.9739466+08:00||;False|2025-12-01T17:10:18.8928139+08:00||;True|2025-12-01T17:03:59.3171589+08:00||;True|2025-12-01T17:03:12.5681656+08:00||;True|2025-12-01T16:42:51.4415025+08:00||;True|2025-12-01T01:29:59.6975567+08:00||;True|2025-12-01T01:29:45.1898652+08:00||;False|2025-12-01T01:29:33.1787721+08:00||;True|2025-12-01T01:27:33.1297605+08:00||;True|2025-12-01T01:27:27.3976862+08:00||;False|2025-12-01T01:27:20.5193052+08:00||;True|2025-12-01T01:11:11.0210582+08:00||;True|2025-12-01T01:04:04.3307782+08:00||;True|2025-12-01T00:55:49.8018864+08:00||;True|2025-12-01T00:27:25.1323561+08:00||;True|2025-11-30T23:58:20.7284116+08:00||;True|2025-11-30T23:57:58.3027622+08:00||;True|2025-11-30T23:57:43.0829614+08:00||;False|2025-11-30T23:57:33.5606716+08:00||;True|2025-11-30T23:54:29.6203368+08:00||;True|2025-11-30T23:54:27.5383120+08:00||;False|2025-11-30T23:54:17.0823982+08:00||;True|2025-11-30T22:41:11.4538820+08:00||;True|2025-11-30T22:38:19.7064566+08:00||;True|2025-11-30T22:37:35.2889186+08:00||;True|2025-11-30T21:42:31.1408314+08:00||;True|2025-11-30T21:42:19.5385333+08:00||;False|2025-11-30T21:42:13.3290237+08:00||;True|2025-11-30T21:33:28.0806806+08:00||;True|2025-11-30T21:16:56.7088027+08:00||;False|2025-11-30T21:16:46.1444944+08:00||;True|2025-11-30T20:21:24.4059664+08:00||;False|2025-11-30T20:21:17.6006098+08:00||;True|2025-11-30T20:20:08.2030141+08:00||;False|2025-11-30T20:16:12.3608968+08:00||;True|2025-11-30T20:15:35.6048313+08:00||;True|2025-11-30T19:45:18.7229552+08:00||;True|2025-11-30T19:45:08.6083368+08:00||;False|2025-11-30T19:44:54.7824905+08:00||;True|2025-11-30T19:44:17.0179491+08:00||;True|2025-11-30T19:44:06.7073484+08:00||;False|2025-11-30T19:43:58.2896556+08:00||;True|2025-11-30T19:42:08.2261927+08:00||;True|2025-11-30T19:28:15.7115077+08:00||;False|2025-11-30T19:28:10.2280725+08:00||;True|2025-11-30T19:23:31.7859158+08:00||;True|2025-11-30T19:23:20.7788277+08:00||;True|2025-11-30T19:17:11.8583296+08:00||;False|2025-11-30T19:10:41.8369574+08:00||;True|2025-11-30T19:02:08.8184758+08:00||;False|2025-11-30T19:01:59.4711045+08:00||;True|2025-11-30T18:16:01.8372242+08:00||;True|2025-11-30T14:50:31.9356543+08:00||;True|2025-11-30T14:48:27.1833064+08:00||;True|2025-11-30T02:06:06.6099669+08:00||;True|2025-11-30T01:49:46.4358206+08:00||;False|2025-11-30T01:49:34.4884036+08:00||;True|2025-11-30T01:43:41.1028753+08:00||;True|2025-11-30T01:38:18.2856609+08:00||;True|2025-11-30T01:23:02.4378259+08:00||;True|2025-11-30T01:08:29.4041159+08:00||;True|2025-11-30T00:56:33.4172007+08:00||;False|2025-11-30T00:56:21.2777389+08:00||;True|2025-11-27T13:37:08.6884390+08:00||;</History>
|
||||||
<LastFailureDetails />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取,
|
|||||||
|
|
||||||
| 镜像标签 | 架构 |
|
| 镜像标签 | 架构 |
|
||||||
| ----------------- | -------------- |
|
| ----------------- | -------------- |
|
||||||
| `beta_1.7.1` | x86_64 (amd64) |
|
| `beta_1.7.3` | x86_64 (amd64) |
|
||||||
| `arm_1.7.1` | ARM64 |
|
| `arm_1.7.3` | ARM64 |
|
||||||
|
|
||||||
### 最新镜像查看
|
### 最新镜像查看
|
||||||
[镜像列表](http://nas.synology2023.online:10108/api/docker/dysync/1)
|
[镜像列表](http://nas.synology2023.online:10108/api/docker/dysync/1)
|
||||||
@@ -112,7 +112,7 @@ docker run -d --restart=always \
|
|||||||
-v /opt/dysync/uper:/app/uper \
|
-v /opt/dysync/uper:/app/uper \
|
||||||
-p 10103:10101 \
|
-p 10103:10101 \
|
||||||
--name dysync2025 \
|
--name dysync2025 \
|
||||||
ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.7.1
|
ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.7.3
|
||||||
# 注意:-p 后面的容器端口,可以用环境变量类似:ASPNETCORE_URLS = http://+:10108 指定
|
# 注意:-p 后面的容器端口,可以用环境变量类似:ASPNETCORE_URLS = http://+:10108 指定
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
dysync:
|
dysync:
|
||||||
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.7.1
|
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.7.3
|
||||||
container_name: dysync2025 # 容器名称
|
container_name: dysync2025 # 容器名称
|
||||||
restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止
|
restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止
|
||||||
ports:
|
ports:
|
||||||
@@ -167,6 +167,8 @@ services:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
@@ -189,7 +191,7 @@ services:
|
|||||||
|
|
||||||
8. ✅ Cookie 过期提醒,在D音授权页可查看
|
8. ✅ Cookie 过期提醒,在D音授权页可查看
|
||||||
|
|
||||||
9. ✅ 重复视频去重(一个视频同时属于收藏视频、喜欢的视频或指定的博主作品)
|
9. ✅ 自动根据去重规则进行去重(可设置去重优先级,同一个视频出现再多个分类时适用)
|
||||||
|
|
||||||
10. ✅ 在同步记录页面中直接播放视频
|
10. ✅ 在同步记录页面中直接播放视频
|
||||||
|
|
||||||
@@ -199,4 +201,6 @@ services:
|
|||||||
|
|
||||||
12. ✅ 关注列表支持新增,非关注的博主
|
12. ✅ 关注列表支持新增,非关注的博主
|
||||||
|
|
||||||
|
13. ✅ 增加永久删除功能,删除后,以后不会再同步该视频
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item has-feedback label="每次同步(条数)" name="BatchCount" :wrapper-col="{ span:6}" style="margin-left:30px">
|
<a-form-item has-feedback label="每次同步上限" name="BatchCount" :wrapper-col="{ span:10}" style="margin-left:30px">
|
||||||
<a-input-number v-model:value="formState.BatchCount" placeholder="请输入查询条数(最大30)" :min="10" :max="30" />
|
<a-input-number v-model:value="formState.BatchCount" placeholder="请输入每次下载数量上限(最大30)" :min="10" :max="30" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>每次同步获取的条数,范围10-30条,建议使用默认值</span>
|
<span>每次同步获取的条数,范围10-30条(建议使用默认值18,抖音默认的分页大小)</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,24 +121,7 @@
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3 class="section-title">其他配置</h3>
|
<h3 class="section-title">其他配置</h3>
|
||||||
|
|
||||||
<a-form-item has-feedback label="日志保留(天数)" name="LogKeepDay" :wrapper-col="{ span: 6 }" style="margin-left:30px">
|
<a-form-item v-show="formState.AutoDistinct" has-feedback label="去重优先等级" name="PriorityLevel" :wrapper-col="{ span: 8 }">
|
||||||
<a-input-number v-model:value="formState.LogKeepDay" placeholder="请输入保留天数" :min="1" :max="90" />
|
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
|
||||||
<span>系统运行日志的保留天数,范围1-90天,过期自动清理</span>
|
|
||||||
</div>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item has-feedback label="是否自动去重" name="AutoDistinct" :wrapper-col="{ span: 10 }">
|
|
||||||
<a-switch v-model:checked="formState.AutoDistinct" />
|
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
|
||||||
<span>
|
|
||||||
启用后,同一个视频,只会下载一次。(但是暂时不能决定保留哪个文件夹的)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item v-show="formState.AutoDistinct" has-feedback label="优先级" name="PriorityLevel" :wrapper-col="{ span: 10 }">
|
|
||||||
<!-- Tag 拖拽容器 -->
|
<!-- Tag 拖拽容器 -->
|
||||||
<div class="tag-drag-container">
|
<div class="tag-drag-container">
|
||||||
<!-- Tag 拖拽容器(绑定 ref 供 Sortable 初始化) -->
|
<!-- Tag 拖拽容器(绑定 ref 供 Sortable 初始化) -->
|
||||||
@@ -150,8 +133,30 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
|
<span>同一个视频同时存在在多个类型的视频分类时,按照优先级保优先级最高的一个。
|
||||||
|
鼠标放到≡,点击鼠标左键即可拖拽调整优先级,</span>
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item has-feedback label="日志保留(天数)" name="LogKeepDay" :wrapper-col="{ span: 6 }" style="margin-left:30px">
|
||||||
|
<a-input-number v-model:value="formState.LogKeepDay" placeholder="请输入保留天数" :min="1" :max="90" />
|
||||||
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
|
<span>系统运行日志的保留天数,范围1-90天,过期自动清理</span>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- <a-form-item has-feedback label="是否自动去重" name="AutoDistinct" :wrapper-col="{ span: 10 }">
|
||||||
|
<a-switch v-model:checked="formState.AutoDistinct" disabled />
|
||||||
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
|
<span>
|
||||||
|
启用后,同一个视频,只会下载一次。(但是暂时不能决定保留哪个文件夹的)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a-form-item> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
@@ -197,7 +202,7 @@ const template_options = [
|
|||||||
|
|
||||||
// 状态定义
|
// 状态定义
|
||||||
const componentDisabled = ref(true);
|
const componentDisabled = ref(true);
|
||||||
const downImgVideo = ref(false);
|
const downImgVideo = ref(true);
|
||||||
|
|
||||||
// 表单数据结构(新增 FullFollowedTitleTemplate 字段)
|
// 表单数据结构(新增 FullFollowedTitleTemplate 字段)
|
||||||
interface FormState {
|
interface FormState {
|
||||||
@@ -315,7 +320,7 @@ const getConfig = () => {
|
|||||||
PriorityLevel: res.data.priorityLevel,
|
PriorityLevel: res.data.priorityLevel,
|
||||||
});
|
});
|
||||||
|
|
||||||
downImgVideo.value = res.data.downImageVideoFromEnv;
|
// downImgVideo.value = res.data.downImageVideoFromEnv;
|
||||||
tagData.value = JSON.parse(res.data.priorityLevel);
|
tagData.value = JSON.parse(res.data.priorityLevel);
|
||||||
} else {
|
} else {
|
||||||
message.error(res.erro || '获取配置失败', 8);
|
message.error(res.erro || '获取配置失败', 8);
|
||||||
|
|||||||
@@ -125,6 +125,10 @@
|
|||||||
<ShareAltOutlined />
|
<ShareAltOutlined />
|
||||||
分享
|
分享
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button type="link" danger @click="handleDelete(record)" :disabled="!record.id">
|
||||||
|
<DeleteOutlined />
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -225,7 +229,7 @@ const columns = ref([
|
|||||||
title: '同步类型',
|
title: '同步类型',
|
||||||
dataIndex: 'viedoTypeStr',
|
dataIndex: 'viedoTypeStr',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 150,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '博主',
|
title: '博主',
|
||||||
@@ -236,7 +240,7 @@ const columns = ref([
|
|||||||
{
|
{
|
||||||
title: '视频类型',
|
title: '视频类型',
|
||||||
dataIndex: 'viedoCate',
|
dataIndex: 'viedoCate',
|
||||||
width: 300,
|
width: 200,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,7 +280,7 @@ watch(isBatchMode, (isOpen) => {
|
|||||||
|
|
||||||
// 基础状态(优化:删除冗余的 datas 响应式数组)
|
// 基础状态(优化:删除冗余的 datas 响应式数组)
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showImageViedo = ref(false);
|
const showImageViedo = ref(true);
|
||||||
const dataSource = ref<DataItem[]>([]); // 直接用 ref 数组存储表格数据,减少响应式嵌套
|
const dataSource = ref<DataItem[]>([]); // 直接用 ref 数组存储表格数据,减少响应式嵌套
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
@@ -306,7 +310,16 @@ const pagination = ref({
|
|||||||
current: 1,
|
current: 1,
|
||||||
defaultPageSize: 10,
|
defaultPageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
showSizeChanger: true, // 强制显示「每页显示数量」下拉框(关键修复)
|
||||||
showTotal: () => `共 ${0} 条`,
|
showTotal: () => `共 ${0} 条`,
|
||||||
|
// showQuickJumper: true, // 显示快速跳转输入框(可选,增强体验)
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'], // 自定义每页条数选项(可选)
|
||||||
|
showSizeChange: (current, pageSize) => {
|
||||||
|
// 可选:监听每页条数变化,重置当前页为第1页(避免最后一页数据不足的问题)
|
||||||
|
pagination.value.current = 1;
|
||||||
|
pagination.value.defaultPageSize = pageSize;
|
||||||
|
GetRecords();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 视频播放相关配置
|
// 视频播放相关配置
|
||||||
@@ -450,22 +463,22 @@ const handleTableChange = (paginationObj: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 获取系统配置 */
|
/** 获取系统配置 */
|
||||||
const getConfig = () => {
|
// const getConfig = () => {
|
||||||
useApiStore()
|
// useApiStore()
|
||||||
.apiGetConfig()
|
// .apiGetConfig()
|
||||||
.then((res) => {
|
// .then((res) => {
|
||||||
if (res.code === 0) {
|
// if (res.code === 0) {
|
||||||
showImageViedo.value = res.data.downImageVideoFromEnv;
|
// showImageViedo.value = res.data.downImageVideoFromEnv;
|
||||||
} else {
|
// } else {
|
||||||
message.warning(`获取配置失败: ${res.message}`);
|
// message.warning(`获取配置失败: ${res.message}`);
|
||||||
}
|
// }
|
||||||
GetRecords();
|
// GetRecords();
|
||||||
})
|
// })
|
||||||
.catch((error) => {
|
// .catch((error) => {
|
||||||
console.error('获取配置失败:', error);
|
// console.error('获取配置失败:', error);
|
||||||
message.error('获取配置失败,请稍后重试');
|
// message.error('获取配置失败,请稍后重试');
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
/** 视频类型切换事件 */
|
/** 视频类型切换事件 */
|
||||||
const onViedoTypeChanged = () => {
|
const onViedoTypeChanged = () => {
|
||||||
@@ -613,9 +626,9 @@ const handleBatchDelete = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认重新下载吗',
|
||||||
content: `您确定要删除选中的 ${selectedRowKeys.value.length} 条视频数据吗?此操作不可撤销!`,
|
content: `您确定要重新下载选中的 ${selectedRowKeys.value.length} 条视频数据吗?`,
|
||||||
okText: '确认删除',
|
okText: '确认重新下载',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
@@ -753,6 +766,34 @@ const handleShare = (record: DataItem) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//视频删除不再下载
|
||||||
|
const handleDelete = (record: DataItem) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `您确定要删除这条视频数据吗?此操作不可撤销,以后也不会再下载!!!`,
|
||||||
|
okText: '确认删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
useApiStore()
|
||||||
|
.DeleteVideo(record.id)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code == 0) {
|
||||||
|
message.success('删除成功,再也不会下载!!!');
|
||||||
|
} else {
|
||||||
|
message.error('删除失败');
|
||||||
|
}
|
||||||
|
GetRecords();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败', error);
|
||||||
|
message.error('视频删除失败,请稍后再试');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 新增:复制视频路径方法
|
// 新增:复制视频路径方法
|
||||||
const copyVideoPath = (path?: string) => {
|
const copyVideoPath = (path?: string) => {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
@@ -764,7 +805,8 @@ const copyVideoPath = (path?: string) => {
|
|||||||
|
|
||||||
// -------------------------- 页面初始化 --------------------------
|
// -------------------------- 页面初始化 --------------------------
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getConfig();
|
// getConfig();
|
||||||
|
GetRecords();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,14 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
//删除
|
||||||
|
async function DeleteVideo(param: string) {
|
||||||
|
return http.request<any, Response<any>>('/api/video/vdelete/' + param, 'get').then(r => {
|
||||||
|
return r.data;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//检查版本
|
//检查版本
|
||||||
@@ -260,6 +268,7 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
SyncFollow,
|
SyncFollow,
|
||||||
OpenOrCloseSync,
|
OpenOrCloseSync,
|
||||||
OpenOrCloseFullSync,
|
OpenOrCloseFullSync,
|
||||||
ReDownViedos
|
ReDownViedos,
|
||||||
|
DeleteVideo
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dbconn": "",
|
"dbconn": "",
|
||||||
"dbtype": "Sqlite",
|
"dbtype": "Sqlite",
|
||||||
"tagName": "dev_1.0"
|
"tagName": "beta_1.7.3"
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
+41
-24
@@ -3,34 +3,51 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务配置实体
|
/// 任务配置实体
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
//public class JobConfig
|
||||||
|
//{
|
||||||
|
// public JobConfig(Type jobType, string jobKey, string triggerKey, string description)
|
||||||
|
// {
|
||||||
|
// JobType = jobType ?? throw new ArgumentNullException(nameof(jobType));
|
||||||
|
// JobKey = jobKey ?? throw new ArgumentNullException(nameof(jobKey));
|
||||||
|
// TriggerKey = triggerKey ?? throw new ArgumentNullException(nameof(triggerKey));
|
||||||
|
// Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// 任务类型
|
||||||
|
// /// </summary>
|
||||||
|
// public Type JobType { get; }
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// 任务Key
|
||||||
|
// /// </summary>
|
||||||
|
// public string JobKey { get; }
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// 触发器Key
|
||||||
|
// /// </summary>
|
||||||
|
// public string TriggerKey { get; }
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// 任务描述
|
||||||
|
// /// </summary>
|
||||||
|
// public string Description { get; }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
public class JobConfig
|
public class JobConfig
|
||||||
{
|
{
|
||||||
|
public Type JobType { get; }
|
||||||
|
public string JobKey { get; }
|
||||||
|
public string TriggerKey { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
public JobConfig(Type jobType, string jobKey, string triggerKey, string description)
|
public JobConfig(Type jobType, string jobKey, string triggerKey, string description)
|
||||||
{
|
{
|
||||||
JobType = jobType ?? throw new ArgumentNullException(nameof(jobType));
|
JobType = jobType ?? throw new ArgumentNullException(nameof(jobType), "任务类型不能为空");
|
||||||
JobKey = jobKey ?? throw new ArgumentNullException(nameof(jobKey));
|
JobKey = jobKey ?? throw new ArgumentNullException(nameof(jobKey), "任务Key不能为空");
|
||||||
TriggerKey = triggerKey ?? throw new ArgumentNullException(nameof(triggerKey));
|
TriggerKey = triggerKey ?? throw new ArgumentNullException(nameof(triggerKey), "触发器Key不能为空");
|
||||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
Description = description ?? throw new ArgumentNullException(nameof(description), "任务描述不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 任务类型
|
|
||||||
/// </summary>
|
|
||||||
public Type JobType { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 任务Key
|
|
||||||
/// </summary>
|
|
||||||
public string JobKey { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 触发器Key
|
|
||||||
/// </summary>
|
|
||||||
public string TriggerKey { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 任务描述
|
|
||||||
/// </summary>
|
|
||||||
public string Description { get; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-13
@@ -4,24 +4,26 @@ namespace dy.net.dto
|
|||||||
{
|
{
|
||||||
public enum VideoTypeEnum
|
public enum VideoTypeEnum
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///喜欢的
|
||||||
|
/// </summary>
|
||||||
[Description("喜欢的")]
|
[Description("喜欢的")]
|
||||||
Favorite = 1,
|
dy_favorite = 1,
|
||||||
|
/// <summary>
|
||||||
|
///收藏的
|
||||||
|
/// </summary>
|
||||||
[Description("收藏的")]
|
[Description("收藏的")]
|
||||||
Collect = 2,
|
dy_collects = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// 关注的
|
||||||
|
/// </summary>
|
||||||
[Description("关注的")]
|
[Description("关注的")]
|
||||||
UperPost = 3,
|
dy_follows = 3,
|
||||||
|
/// <summary>
|
||||||
|
/// 图文视频,无用了,但是不能删...
|
||||||
|
/// </summary>
|
||||||
[Description("图片视频")]
|
[Description("图片视频")]
|
||||||
ImageVideo = 4
|
ImageVideo = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public enum QuartzJobTypeEnum
|
|
||||||
{
|
|
||||||
[Description("[Favorite]")]
|
|
||||||
Favorite = 1,
|
|
||||||
[Description("[Collect]")]
|
|
||||||
Collect = 2,
|
|
||||||
[Description("[Followed]")]
|
|
||||||
Followed = 3
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using dy.net.dto;
|
||||||
|
using dy.net.service;
|
||||||
|
using Quartz;
|
||||||
|
using Serilog;
|
||||||
|
using static Quartz.Logging.OperationName;
|
||||||
|
|
||||||
|
namespace dy.net.job
|
||||||
|
{ /// <summary>
|
||||||
|
/// 抖音任务依赖监听器(独立公共类)
|
||||||
|
/// 作用:监听任务执行完成事件,触发下一个依赖任务,实现顺序执行
|
||||||
|
/// </summary>
|
||||||
|
public class DouyinJobDependencyListener : IJobListener
|
||||||
|
{
|
||||||
|
// 监听器名称(唯一标识,不可重复)
|
||||||
|
public string Name => "DouyinJobDependencyListener";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务配置字典(从外部注入)
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, JobConfig> _jobConfigs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务依赖关系(从外部注入,定义执行顺序)
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, string> _jobDependency;
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 触发下一个任务(标记为「依赖触发」,立即执行)
|
||||||
|
Log.Information("【任务监听】准备触发下一个任务: {NextJobName}(依赖触发)", nextConfigKey);
|
||||||
|
var triggerSuccess = await _jobService.StartJobAsync(nextConfigKey, "", isDependencyTrigger: true);
|
||||||
|
|
||||||
|
if (triggerSuccess)
|
||||||
|
{
|
||||||
|
Log.Information("【任务监听】下一个任务 {NextJobName} 触发成功", nextConfigKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Error("【任务监听】下一个任务 {NextJobName} 触发失败,任务链条中断", nextConfigKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,8 @@ namespace dy.net.job
|
|||||||
DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService douyinMergeVideoService)
|
DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService douyinMergeVideoService)
|
||||||
: base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService,douyinFollowService, douyinMergeVideoService) { }
|
: base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService,douyinFollowService, douyinMergeVideoService) { }
|
||||||
|
|
||||||
protected override string JobType => SystemStaticUtil.DY_COLLECTS;
|
|
||||||
|
protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_collects;
|
||||||
protected override VideoTypeEnum VideoType => VideoTypeEnum.Collect;
|
|
||||||
|
|
||||||
protected override async Task BeforeProcessCookies()
|
protected override async Task BeforeProcessCookies()
|
||||||
{
|
{
|
||||||
@@ -61,34 +60,31 @@ namespace dy.net.job
|
|||||||
return Path.Combine(cookie.SavePath, "author");
|
return Path.Combine(cookie.SavePath, "author");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount)
|
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount,DouyinFollowed followed)
|
||||||
{
|
{
|
||||||
if (syncCount > 0)
|
if (syncCount > 0)
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||||
cookie.CollHasSyncd = 1;
|
cookie.CollHasSyncd = 1;
|
||||||
await douyinCookieService.UpdateAsync(cookie);
|
await douyinCookieService.UpdateAsync(cookie);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],没有可以同步的新视频");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item)
|
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item)
|
||||||
{
|
{
|
||||||
return new VideoEntityDifferences
|
return new VideoEntityDifferences();
|
||||||
{
|
|
||||||
VideoType = VideoTypeEnum.Collect,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed)
|
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed)
|
||||||
{
|
{
|
||||||
var (tag1, _, _) = GetVideoTags(item);
|
var (tag1, _, _) = GetVideoTags(item);
|
||||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizePath(tag1);
|
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizeLinuxFileName(tag1);
|
||||||
var folder = Path.Combine(cookie.SavePath, safeTag1, $"{DouyinFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
|
var folder = Path.Combine(cookie.SavePath, safeTag1, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}@{item.AwemeId}");
|
||||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ namespace dy.net.job
|
|||||||
DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService douyinMergeVideoService)
|
DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService douyinMergeVideoService)
|
||||||
: base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService, douyinFollowService, douyinMergeVideoService) { }
|
: base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService, douyinFollowService, douyinMergeVideoService) { }
|
||||||
|
|
||||||
protected override string JobType => SystemStaticUtil.DY_FAVORITES;
|
|
||||||
|
|
||||||
protected override VideoTypeEnum VideoType => VideoTypeEnum.Favorite;
|
protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_favorite;
|
||||||
|
|
||||||
protected override async Task<List<DouyinCookie>> GetValidCookies()
|
protected override async Task<List<DouyinCookie>> GetValidCookies()
|
||||||
{
|
{
|
||||||
@@ -54,33 +53,25 @@ namespace dy.net.job
|
|||||||
return Path.Combine(cookie.FavSavePath, "author");
|
return Path.Combine(cookie.FavSavePath, "author");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount)
|
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount, DouyinFollowed followed)
|
||||||
{
|
{
|
||||||
if (syncCount > 0)
|
if (syncCount > 0)
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||||
cookie.FavHasSyncd = 1;
|
cookie.FavHasSyncd = 1;
|
||||||
await douyinCookieService.UpdateAsync(cookie);
|
await douyinCookieService.UpdateAsync(cookie);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],没有可以同步的新视频");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item)
|
|
||||||
{
|
|
||||||
return new VideoEntityDifferences
|
|
||||||
{
|
|
||||||
VideoType = VideoTypeEnum.Favorite
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed)
|
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed)
|
||||||
{
|
{
|
||||||
var (tag1, _, _) = GetVideoTags(item);
|
var (tag1, _, _) = GetVideoTags(item);
|
||||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizePath(tag1);
|
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizeLinuxFileName(tag1);
|
||||||
var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{DouyinFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
|
var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}@{item.AwemeId}");
|
||||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-82
@@ -82,7 +82,7 @@ namespace dy.net.job
|
|||||||
/// 任务类型名称,用于日志记录和区分不同的同步任务
|
/// 任务类型名称,用于日志记录和区分不同的同步任务
|
||||||
/// 子类必须实现此属性并返回具体的任务类型
|
/// 子类必须实现此属性并返回具体的任务类型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract string JobType { get; }
|
//protected abstract string VideoType { get; }
|
||||||
|
|
||||||
protected abstract VideoTypeEnum VideoType { get; }
|
protected abstract VideoTypeEnum VideoType { get; }
|
||||||
|
|
||||||
@@ -131,14 +131,13 @@ namespace dy.net.job
|
|||||||
var config = douyinCommonService.GetConfig();
|
var config = douyinCommonService.GetConfig();
|
||||||
if (config == null)
|
if (config == null)
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-未获取到系统配置,任务终止!!!");
|
Log.Debug($"{VideoType}-未获取到系统配置,任务终止!!!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 从配置中获取每页请求数量--固定18
|
||||||
// 2. 从配置中获取每页请求数量
|
//if (config.BatchCount > 0)
|
||||||
if (config.BatchCount > 0)
|
// count = config.BatchCount.ToString();
|
||||||
count = config.BatchCount.ToString();
|
|
||||||
|
|
||||||
// 3. 在处理Cookie之前执行的预处理操作
|
// 3. 在处理Cookie之前执行的预处理操作
|
||||||
await BeforeProcessCookies();
|
await BeforeProcessCookies();
|
||||||
@@ -147,11 +146,11 @@ namespace dy.net.job
|
|||||||
var cookies = await GetValidCookies();
|
var cookies = await GetValidCookies();
|
||||||
if (cookies == null || !cookies.Any())
|
if (cookies == null || !cookies.Any())
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-无有效Cookie,任务终止!!!");
|
Log.Debug($"{VideoType}-无有效Cookie,任务终止!!!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"{JobType}-共发现{cookies.Count}个Cookie,同步任务即将开始...");
|
Log.Debug($"{VideoType}-共发现{cookies.Count}个Cookie,同步任务即将开始...");
|
||||||
|
|
||||||
// 6. 遍历每个有效的Cookie,执行同步操作
|
// 6. 遍历每个有效的Cookie,执行同步操作
|
||||||
foreach (var cookie in cookies)
|
foreach (var cookie in cookies)
|
||||||
@@ -262,8 +261,9 @@ namespace dy.net.job
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cookie">用户Cookie</param>
|
/// <param name="cookie">用户Cookie</param>
|
||||||
/// <param name="syncCount">本次同步成功的视频数量</param>
|
/// <param name="syncCount">本次同步成功的视频数量</param>
|
||||||
|
/// <param name="followed"></param>
|
||||||
/// <returns>一个表示异步操作的任务</returns>
|
/// <returns>一个表示异步操作的任务</returns>
|
||||||
protected abstract Task HandleSyncCompletion(DouyinCookie cookie, int syncCount);
|
protected abstract Task HandleSyncCompletion(DouyinCookie cookie, int syncCount,DouyinFollowed followed=null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取视频实体的差异信息
|
/// 获取视频实体的差异信息
|
||||||
@@ -272,7 +272,10 @@ namespace dy.net.job
|
|||||||
/// <param name="cookie">用户Cookie</param>
|
/// <param name="cookie">用户Cookie</param>
|
||||||
/// <param name="item">视频信息</param>
|
/// <param name="item">视频信息</param>
|
||||||
/// <returns>视频实体的差异信息,包含视频类型和简化标题</returns>
|
/// <returns>视频实体的差异信息,包含视频类型和简化标题</returns>
|
||||||
protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item);
|
protected virtual VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item)
|
||||||
|
{
|
||||||
|
return new VideoEntityDifferences();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取NFO文件中的图片(如海报)文件名
|
/// 获取NFO文件中的图片(如海报)文件名
|
||||||
@@ -302,13 +305,13 @@ namespace dy.net.job
|
|||||||
// 检查Cookie是否有效
|
// 检查Cookie是否有效
|
||||||
if (!IsCookieValid(cookie))
|
if (!IsCookieValid(cookie))
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-Cookie[{cookie.UserName}]无效,任务终止!!!");
|
Log.Debug($"{VideoType}-Cookie[{cookie.UserName}]无效,任务终止!!!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.Debug($"{JobType}- Cookie-[{cookie.UserName}]开始同步...");
|
Log.Debug($"{VideoType}- Cookie-[{cookie.UserName}]开始同步...");
|
||||||
|
|
||||||
//up主上传视频特殊处理
|
//up主上传视频特殊处理
|
||||||
if (JobType == SystemStaticUtil.DY_FOLLOWEDS)
|
if (VideoType ==VideoTypeEnum.dy_follows)
|
||||||
{
|
{
|
||||||
int syncCount = 0; // 本次同步成功的视频数量
|
int syncCount = 0; // 本次同步成功的视频数量
|
||||||
string cursor = "0"; // 初始游标
|
string cursor = "0"; // 初始游标
|
||||||
@@ -324,18 +327,18 @@ namespace dy.net.job
|
|||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(firstUp.SecUid))
|
if (string.IsNullOrWhiteSpace(firstUp.SecUid))
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-Cookie[{firstUp.UperName}]无效,没有sec_userid,任务终止!!!");
|
Log.Debug($"{VideoType}-Cookie[{firstUp.UperName}]无效,没有sec_userid,任务终止!!!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
foreach (var item in follows)
|
foreach (var followed in follows)
|
||||||
{
|
{
|
||||||
cursor = "0"; // 初始游标
|
cursor = "0"; // 初始游标
|
||||||
await GetViedos(cookie, config, syncCount, cursor, hasMore, item);
|
await GetViedos(cookie, config, syncCount, cursor, hasMore, followed);
|
||||||
hasMore = true;
|
hasMore = true;
|
||||||
// 处理同步完成后的操作
|
// 处理同步完成后的操作
|
||||||
await HandleSyncCompletion(cookie, syncCount);
|
await HandleSyncCompletion(cookie, syncCount,followed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -353,7 +356,7 @@ namespace dy.net.job
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, $"{JobType}-Cookie[{cookie.Id}]同步出错!!!");
|
Log.Error(ex, $"{VideoType}-Cookie[{cookie.Id}]同步出错!!!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +369,7 @@ namespace dy.net.job
|
|||||||
var data = await FetchVideoData(cookie, cursor, followed == null ? "" : followed?.SecUid);
|
var data = await FetchVideoData(cookie, cursor, followed == null ? "" : followed?.SecUid);
|
||||||
if (data == null)
|
if (data == null)
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-Cookie[{cookie.UserName}]读取数据失败!!!");
|
Log.Debug($"{VideoType}-Cookie[{cookie.UserName}]读取数据失败!!!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +390,7 @@ namespace dy.net.job
|
|||||||
//当syncCount达到上限时,跳出循环
|
//当syncCount达到上限时,跳出循环
|
||||||
if (config.BatchCount > 0 && syncCount >= config.BatchCount)
|
if (config.BatchCount > 0 && syncCount >= config.BatchCount)
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-Cookie[{cookie.UserName}]本次同步达到上限{config.BatchCount},停止同步!!!");
|
Log.Debug($"{VideoType}-Cookie[{cookie.UserName}]本次同步达到上限{config.BatchCount},停止同步!!!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,12 +415,28 @@ namespace dy.net.job
|
|||||||
var videos = new List<DouyinVideo>();
|
var videos = new List<DouyinVideo>();
|
||||||
foreach (var item in data.AwemeList)
|
foreach (var item in data.AwemeList)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
//判断视频是否是强制删除且不再下载的视频
|
||||||
|
var deleteVideo = await douyinCommonService.ExistDeleteVideo(item.AwemeId);
|
||||||
|
if (deleteVideo)
|
||||||
|
{
|
||||||
|
Log.Debug($"{VideoType}-视频-{item.AwemeId}-[{item.Desc}]已被标记为强制删除,跳过下载");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
//判断是否存在视频,是否根据去重规则进行去重处理。。
|
//判断是否存在视频,是否根据去重规则进行去重处理。。
|
||||||
bool Goon = await AutoDistinct(config, item);
|
// 1. 查询数据库中是否已存在该视频(通过 AwemeId 唯一标识)
|
||||||
|
var exitVideo = await douyinVideoService.GetByAwemeId(item.AwemeId);
|
||||||
|
|
||||||
|
bool Goon = await AutoDistinct(config, item, cookie, exitVideo);
|
||||||
if (!Goon)
|
if (!Goon)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exitVideo!=null) {
|
||||||
|
await douyinVideoService.DeleteById(exitVideo.Id);
|
||||||
|
}
|
||||||
var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId);
|
var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId);
|
||||||
if (uper != null)
|
if (uper != null)
|
||||||
{
|
{
|
||||||
@@ -442,13 +461,11 @@ namespace dy.net.job
|
|||||||
return videos;
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> AutoDistinct(AppConfig config, Aweme item)
|
private async Task<bool> AutoDistinct(AppConfig config, Aweme item,DouyinCookie cookie,DouyinVideo exitVideo)
|
||||||
{
|
{
|
||||||
// 去重,检查视频是否已存在(按优先级下载)
|
// 去重,检查视频是否已存在(按优先级下载)
|
||||||
if (config.AutoDistinct)
|
if (config.AutoDistinct)
|
||||||
{
|
{
|
||||||
// 1. 查询数据库中是否已存在该视频(通过 AwemeId 唯一标识)
|
|
||||||
var exitVideo = await douyinVideoService.GetByAwemeId(item.AwemeId);
|
|
||||||
if (exitVideo != null)
|
if (exitVideo != null)
|
||||||
{
|
{
|
||||||
// 2. 已存在视频:先判断本地文件是否存在
|
// 2. 已存在视频:先判断本地文件是否存在
|
||||||
@@ -465,7 +482,7 @@ namespace dy.net.job
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error($"解析优先级配置失败:{ex.Message}", ex);
|
Log.Error($"{VideoType}-解析优先级配置失败:{ex.Message}", ex);
|
||||||
priLevs = new List<PriorityLevelDto>(); // 解析失败则用默认优先级
|
priLevs = new List<PriorityLevelDto>(); // 解析失败则用默认优先级
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,23 +514,24 @@ namespace dy.net.job
|
|||||||
if (exitVideoType == currentVideoType)
|
if (exitVideoType == currentVideoType)
|
||||||
{
|
{
|
||||||
// 已存在同优先级视频 → 跳过下载(避免重复)
|
// 已存在同优先级视频 → 跳过下载(避免重复)
|
||||||
Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(同最高优先级),跳过");
|
//Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(同最高优先级),跳过");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 已存在「低优先级」视频 → 替换(删除旧文件,继续下载新的最高优先级视频)
|
// 已存在「低优先级」视频 → 替换(删除旧文件,继续下载新的最高优先级视频)
|
||||||
Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(低优先级:{exitVideoType}),替换为最高优先级:{currentVideoType}");
|
//Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(低优先级:{exitVideoType}),替换为最高优先级:{currentVideoType}");
|
||||||
|
|
||||||
// 删除旧的低优先级文件(可选:也可保留备份,根据需求调整)
|
// 删除旧的低优先级文件(可选:也可保留备份,根据需求调整)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.Delete(exitVideo.VideoSavePath);
|
//File.Delete(exitVideo.VideoSavePath);
|
||||||
Log.Debug($"已删除旧文件:{exitVideo.VideoSavePath}");
|
DeleteOldViedo(config, exitVideo);
|
||||||
|
//Log.Debug($"已删除旧文件:{exitVideo.VideoSavePath}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error($"删除旧文件失败:{ex.Message}", ex);
|
Log.Error($"{VideoType}-删除旧文件失败:{ex.Message}", ex);
|
||||||
// 即使删除失败,仍继续下载(新文件会覆盖旧文件,或按路径规则重命名)
|
// 即使删除失败,仍继续下载(新文件会覆盖旧文件,或按路径规则重命名)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +544,7 @@ namespace dy.net.job
|
|||||||
if (exitVideoType == maxPriorityType)
|
if (exitVideoType == maxPriorityType)
|
||||||
{
|
{
|
||||||
// 已存在「最高优先级」视频 → 跳过(不替换最高优先级)
|
// 已存在「最高优先级」视频 → 跳过(不替换最高优先级)
|
||||||
Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在最高优先级视频({maxPriorityType}),当前类型({currentVideoType})优先级低,跳过");
|
//Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在最高优先级视频({maxPriorityType}),当前类型({currentVideoType})优先级低,跳过");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -539,39 +557,70 @@ namespace dy.net.job
|
|||||||
if (currentSort < exitSort)
|
if (currentSort < exitSort)
|
||||||
{
|
{
|
||||||
// 当前类型优先级更高 → 替换旧视频
|
// 当前类型优先级更高 → 替换旧视频
|
||||||
Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在低优先级视频({exitVideoType}),替换为当前优先级:{currentVideoType}");
|
//Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在低优先级视频({exitVideoType}),替换为当前优先级:{currentVideoType}");
|
||||||
|
|
||||||
// 删除旧文件
|
// 删除旧文件
|
||||||
if (File.Exists(exitVideo.VideoSavePath))
|
DeleteOldViedo(config, exitVideo);
|
||||||
{
|
|
||||||
File.Delete(exitVideo.VideoSavePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 继续下载
|
// 继续下载
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 当前类型优先级更低或相等 → 跳过
|
// 当前类型优先级更低或相等 → 跳过
|
||||||
Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在更高/同等优先级视频({exitVideoType}),当前类型({currentVideoType})跳过");
|
//Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在更高/同等优先级视频({exitVideoType}),当前类型({currentVideoType})跳过");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteOldViedo(AppConfig config, DouyinVideo exitVideo)
|
||||||
|
{
|
||||||
|
if (File.Exists(exitVideo.VideoSavePath))
|
||||||
|
{
|
||||||
|
//如果是关注的视频,并且是保存到同一文件夹,则只删除文件
|
||||||
|
if (VideoType == VideoTypeEnum.dy_follows && config.UperSaveTogether)
|
||||||
|
{
|
||||||
|
File.Delete(exitVideo.VideoSavePath);
|
||||||
|
//Log.Debug($"{VideoType}-已删除旧文件:{exitVideo.VideoSavePath}");
|
||||||
|
//删除同名的.nfo 文件
|
||||||
|
var nfoPath = Path.Combine(Path.GetDirectoryName(exitVideo.VideoSavePath), ".nfo");
|
||||||
|
if (File.Exists(nfoPath))
|
||||||
{
|
{
|
||||||
// 数据库存在记录,但本地文件丢失 → 直接重新下载(无论优先级)
|
File.Delete(nfoPath);
|
||||||
Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]数据库存在但本地文件丢失,重新下载");
|
//删除同名的封面文件
|
||||||
|
//Log.Debug($"{VideoType}-已删除旧nfo文件:{nfoPath}");
|
||||||
|
}
|
||||||
|
//删除同名的封面文件
|
||||||
|
var posterPath = Path.Combine(Path.GetDirectoryName(exitVideo.VideoSavePath), "poster.jpg");
|
||||||
|
if (File.Exists(posterPath))
|
||||||
|
{
|
||||||
|
File.Delete(posterPath);
|
||||||
|
//Log.Debug($"{VideoType}-已删除旧封面文件:{posterPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 数据库和本地均无该视频 → 直接下载
|
//如果是关注的,且不是保存到同一文件夹 或者其他类型视频,直接删除文件夹
|
||||||
//Log.Debug($"视频-{item.AwemeId}-[{item.Desc}]未存在,开始下载");
|
//
|
||||||
|
var dirPath = Path.GetDirectoryName(exitVideo.VideoSavePath);
|
||||||
|
if (Directory.Exists(dirPath))
|
||||||
|
{
|
||||||
|
Directory.Delete(dirPath, true);
|
||||||
|
//Log.Debug($"{VideoType}-已删除旧文件夹:{dirPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//查看是否还有其他文件,如果没有则删除文件夹
|
||||||
|
var parentDir = Path.GetDirectoryName(exitVideo.VideoSavePath);
|
||||||
|
if (Directory.Exists(parentDir) && !Directory.EnumerateFileSystemEntries(parentDir).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(parentDir);
|
||||||
|
Log.Debug($"{VideoType}-已删除空文件夹:{parentDir}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -609,22 +658,22 @@ namespace dy.net.job
|
|||||||
// 如果文件已存在,跳过
|
// 如果文件已存在,跳过
|
||||||
if (File.Exists(savePath))
|
if (File.Exists(savePath))
|
||||||
{
|
{
|
||||||
//Log.Debug($"{JobType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]已存在,跳过下载.");
|
//Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]已存在,跳过下载.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"{JobType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]开始下载...");
|
Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}]开始下载...");
|
||||||
// 随机延迟,模拟人类操作
|
// 随机延迟,模拟人类操作
|
||||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||||
// 下载视频
|
// 下载视频
|
||||||
if (!await douyinHttpClientService.DownloadAsync(videoUrl, savePath, cookie.Cookies))
|
if (!await douyinHttpClientService.DownloadAsync(videoUrl, savePath, cookie.Cookies))
|
||||||
{
|
{
|
||||||
Log.Error($"{JobType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]下载失败!!!");
|
Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}]下载失败!!!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]下载完成.");
|
Log.Debug($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}]下载完成.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载视频封面
|
// 下载视频封面
|
||||||
@@ -672,10 +721,10 @@ namespace dy.net.job
|
|||||||
// 检查图片保存路径是否配置
|
// 检查图片保存路径是否配置
|
||||||
if (string.IsNullOrWhiteSpace(cookie.ImgSavePath))
|
if (string.IsNullOrWhiteSpace(cookie.ImgSavePath))
|
||||||
{
|
{
|
||||||
Log.Error($"{JobType}-图文视频同步-没有配置图片存储路径,任务终止!!!");
|
Log.Error($"{VideoType}-图文视频同步-没有配置图片存储路径,任务终止!!!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
fileNamefolder = Path.Combine(cookie.ImgSavePath, DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId));
|
fileNamefolder = Path.Combine(cookie.ImgSavePath, DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -713,7 +762,7 @@ namespace dy.net.job
|
|||||||
var mergeResult = await douyinMergeVideoService.MergeToVideo(cookie.Cookies, AppContext.BaseDirectory, reqParams, savePath, fileNamefolder, config.DownImageVideo, config.DownImage, config.DownMp3);
|
var mergeResult = await douyinMergeVideoService.MergeToVideo(cookie.Cookies, AppContext.BaseDirectory, reqParams, savePath, fileNamefolder, config.DownImageVideo, config.DownImage, config.DownMp3);
|
||||||
if (!mergeResult)
|
if (!mergeResult)
|
||||||
{
|
{
|
||||||
Log.Error($"{JobType}-图文视频-[{DouyinFileNameHelper.SanitizePath(item.Desc)}]合成失败!!!");
|
Log.Error($"{VideoType}-图文视频-[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}]合成失败!!!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,13 +771,13 @@ namespace dy.net.job
|
|||||||
// 检查合成后的视频文件是否有效
|
// 检查合成后的视频文件是否有效
|
||||||
if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0)
|
if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0)
|
||||||
{
|
{
|
||||||
Log.Error($"{JobType}-图文视频-[{DouyinFileNameHelper.SanitizePath(item.Desc)}]合成失败!!!");
|
Log.Error($"{VideoType}-图文视频-[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}]合成失败!!!");
|
||||||
// 清理无效的文件和文件夹
|
// 清理无效的文件和文件夹
|
||||||
if (Directory.Exists(fileNamefolder))
|
if (Directory.Exists(fileNamefolder))
|
||||||
{
|
{
|
||||||
File.Delete(savePath);
|
File.Delete(savePath);
|
||||||
Directory.Delete(fileNamefolder, true);
|
Directory.Delete(fileNamefolder, true);
|
||||||
Log.Error($"{JobType}-图文视频-删除合成失败的视频文件和目录...");
|
Log.Error($"{VideoType}-图文视频-删除合成失败的视频文件和目录...");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -765,13 +814,14 @@ namespace dy.net.job
|
|||||||
// 特殊处理合成视频的字段
|
// 特殊处理合成视频的字段
|
||||||
videoEntity.FileHash = string.Empty; // 合成视频没有原始文件哈希
|
videoEntity.FileHash = string.Empty; // 合成视频没有原始文件哈希
|
||||||
videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
|
videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
|
||||||
videoEntity.ViedoType = VideoTypeEnum.ImageVideo; // 标记为图片合成视频
|
videoEntity.ViedoType = VideoType;
|
||||||
|
videoEntity.IsMergeVideo = 1;// 标记为图片合成视频
|
||||||
|
|
||||||
return videoEntity;
|
return videoEntity;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, $"{JobType}-图片视频同步-处理图片集并合成视频时出错");
|
Log.Error(ex, $"{VideoType}-图片视频同步-处理图片集并合成视频时出错");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -788,30 +838,11 @@ namespace dy.net.job
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await douyinVideoService.BatchInsertOrUpdate(videos);
|
await douyinVideoService.BatchInsertOrUpdate(videos);
|
||||||
|
|
||||||
var redowns = await douyinCommonService.GetAllRedown();
|
|
||||||
if (redowns != null && redowns.Any())
|
|
||||||
{
|
|
||||||
//找出viedos和redowns重复的
|
|
||||||
var duplicateVideos = videos.Where(v => redowns.Any(r => r.ViedoId == v.AwemeId)).ToList();
|
|
||||||
if (duplicateVideos != null && duplicateVideos.Any())
|
|
||||||
{
|
|
||||||
var duplicateVideosIds = duplicateVideos.Select(x => x.AwemeId).ToList();
|
|
||||||
var downeds = redowns.Where(x => duplicateVideosIds.Contains(x.ViedoId))?.ToList();
|
|
||||||
foreach (var item in downeds)
|
|
||||||
{
|
|
||||||
item.Status = 1;
|
|
||||||
var v = videos.FirstOrDefault(x => x.AwemeId == item.ViedoId);
|
|
||||||
Log.Debug($"{JobType}-重新下载成功:-{v.VideoTitle}");
|
|
||||||
}
|
|
||||||
await douyinCommonService.UpdateRedownStatus(downeds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return videos.Count;
|
return videos.Count;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, $"{JobType}-批量保存视频到数据库失败");
|
Log.Error(ex, $"{VideoType}-批量保存视频到数据库失败");
|
||||||
// 清理保存失败的视频文件
|
// 清理保存失败的视频文件
|
||||||
await CleanupFailedVideos(videos);
|
await CleanupFailedVideos(videos);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -938,7 +969,7 @@ namespace dy.net.job
|
|||||||
/// <returns>一个表示异步操作的任务</returns>
|
/// <returns>一个表示异步操作的任务</returns>
|
||||||
private async Task CleanupFailedVideos(List<DouyinVideo> videos)
|
private async Task CleanupFailedVideos(List<DouyinVideo> videos)
|
||||||
{
|
{
|
||||||
Log.Debug($"{JobType}-数据库保存失败,开始清理本次下载的视频目录...");
|
Log.Debug($"{VideoType}-数据库保存失败,开始清理本次下载的视频目录...");
|
||||||
|
|
||||||
foreach (var video in videos)
|
foreach (var video in videos)
|
||||||
{
|
{
|
||||||
@@ -956,12 +987,12 @@ namespace dy.net.job
|
|||||||
{
|
{
|
||||||
Directory.Delete(directory);
|
Directory.Delete(directory);
|
||||||
}
|
}
|
||||||
Log.Debug($"{JobType}-清理失败视频文件成功: {video.VideoSavePath}!!!");
|
Log.Debug($"{VideoType}-清理失败视频文件成功: {video.VideoSavePath}!!!");
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Warning(ex, $"{JobType}-清理失败视频文件出错: {video.VideoSavePath}!!!");
|
Log.Warning(ex, $"{VideoType}-清理失败视频文件出错: {video.VideoSavePath}!!!");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1015,7 +1046,7 @@ namespace dy.net.job
|
|||||||
|
|
||||||
return new DouyinVideo
|
return new DouyinVideo
|
||||||
{
|
{
|
||||||
ViedoType = diffs.VideoType,
|
ViedoType = VideoType,
|
||||||
AwemeId = item.AwemeId,
|
AwemeId = item.AwemeId,
|
||||||
Author = item.Author?.Nickname,
|
Author = item.Author?.Nickname,
|
||||||
AuthorId = item.Author?.Uid,
|
AuthorId = item.Author?.Uid,
|
||||||
@@ -1053,7 +1084,7 @@ namespace dy.net.job
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 视频类型
|
/// 视频类型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VideoTypeEnum VideoType { get; set; }
|
//public VideoTypeEnum VideoType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 简化的视频标题
|
/// 简化的视频标题
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ namespace dy.net.job
|
|||||||
|
|
||||||
if (follows.Count > 0)
|
if (follows.Count > 0)
|
||||||
{
|
{
|
||||||
await _followService.Sync(follows, ck.MyUserId);
|
await _followService.Sync(follows, ck );
|
||||||
}
|
}
|
||||||
|
|
||||||
Serilog.Log.Debug($"当前Cookie-[{ck.UserName}],本次同步关注列表完成,共同步关注{total}人。");
|
//Serilog.Log.Debug($"当前Cookie-[{ck.UserName}],本次同步关注列表完成,当前共关注{total}人。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ namespace dy.net.job
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string JobType => SystemStaticUtil.DY_FOLLOWEDS;
|
|
||||||
|
|
||||||
protected override VideoTypeEnum VideoType => VideoTypeEnum.UperPost;
|
protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_follows;
|
||||||
|
|
||||||
protected override async Task<List<DouyinCookie>> GetValidCookies()
|
protected override async Task<List<DouyinCookie>> GetValidCookies()
|
||||||
{
|
{
|
||||||
@@ -56,7 +55,7 @@ namespace dy.net.job
|
|||||||
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed)
|
protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed)
|
||||||
{
|
{
|
||||||
#region 默认使用UP主名称作为文件夹名称,若关注列表中有自定义保存路径则使用自定义路径
|
#region 默认使用UP主名称作为文件夹名称,若关注列表中有自定义保存路径则使用自定义路径
|
||||||
var authorName = string.IsNullOrWhiteSpace(item.Author?.Nickname) ? "UnknownAuthor" : DouyinFileNameHelper.SanitizePath(item.Author.Nickname);
|
var authorName = string.IsNullOrWhiteSpace(item.Author?.Nickname) ? "UnknownAuthor" : DouyinFileNameHelper.SanitizeLinuxFileName(item.Author.Nickname);
|
||||||
var folder = Path.Combine(cookie.UpSavePath, authorName);
|
var folder = Path.Combine(cookie.UpSavePath, authorName);
|
||||||
if (!string.IsNullOrWhiteSpace(followed.SavePath))
|
if (!string.IsNullOrWhiteSpace(followed.SavePath))
|
||||||
{
|
{
|
||||||
@@ -71,7 +70,7 @@ namespace dy.net.job
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var sampleName = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
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).Result;
|
||||||
var fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName;
|
var fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName;
|
||||||
return Path.Combine(folder, fileNameFolder);
|
return Path.Combine(folder, fileNameFolder);
|
||||||
@@ -115,7 +114,7 @@ namespace dy.net.job
|
|||||||
string fileName;
|
string fileName;
|
||||||
if (config?.UperUseViedoTitle ?? false)//优先
|
if (config?.UperUseViedoTitle ?? false)//优先
|
||||||
{
|
{
|
||||||
var sampleName = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
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).Result;
|
||||||
fileName = string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{Format}" : $"{existingName}.{Format}";
|
fileName = string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{Format}" : $"{existingName}.{Format}";
|
||||||
}
|
}
|
||||||
@@ -130,11 +129,11 @@ namespace dy.net.job
|
|||||||
Id = item.AwemeId,
|
Id = item.AwemeId,
|
||||||
ReleaseTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
ReleaseTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||||
Resolution = $"{Width}×{Height}",
|
Resolution = $"{Width}×{Height}",
|
||||||
VideoTitle = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId),
|
VideoTitle = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId),
|
||||||
Author = item.Author.Nickname
|
Author = item.Author.Nickname
|
||||||
});
|
});
|
||||||
|
|
||||||
fileName= $"{DouyinFileNameHelper.SanitizePath(fullName)}.{Format}";
|
fileName= $"{DouyinFileNameHelper.SanitizeLinuxFileName(fullName)}.{Format}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -172,17 +171,17 @@ namespace dy.net.job
|
|||||||
return Path.Combine(cookie.UpSavePath, "author");
|
return Path.Combine(cookie.UpSavePath, "author");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount)
|
protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount, DouyinFollowed followed)
|
||||||
{
|
{
|
||||||
if (syncCount > 0)
|
if (syncCount > 0)
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||||
cookie.UperSyncd = 1;
|
cookie.UperSyncd = 1;
|
||||||
await douyinCookieService.UpdateAsync(cookie);
|
await douyinCookieService.UpdateAsync(cookie);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}]-{(followed == null ? "" : $"{followed.UperName}")},没有可以同步的新视频");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,12 +192,10 @@ namespace dy.net.job
|
|||||||
|
|
||||||
if (config?.UperUseViedoTitle ?? false)
|
if (config?.UperUseViedoTitle ?? false)
|
||||||
{
|
{
|
||||||
simplifiedTitle = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
simplifiedTitle = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new VideoEntityDifferences
|
return new VideoEntityDifferences
|
||||||
{
|
{
|
||||||
VideoType = VideoTypeEnum.UperPost,
|
|
||||||
VideoTitleSimplify = simplifiedTitle
|
VideoTitleSimplify = simplifiedTitle
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace dy.net.model
|
|||||||
/// 重新下载的列表
|
/// 重新下载的列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarTable(TableName = "dy_rd_video")]
|
[SugarTable(TableName = "dy_rd_video")]
|
||||||
public class ViedoReDown
|
public class DouyinReDownload
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -139,6 +139,11 @@ namespace dy.net.model
|
|||||||
[SugarColumn(Length =200,IsNullable =true)]
|
[SugarColumn(Length =200,IsNullable =true)]
|
||||||
public VideoTypeEnum ViedoType { get; set; }
|
public VideoTypeEnum ViedoType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否合成视频
|
||||||
|
/// </summary>
|
||||||
|
public int IsMergeVideo { get; set; }
|
||||||
|
|
||||||
[SugarColumn(IsIgnore = true)]
|
[SugarColumn(IsIgnore = true)]
|
||||||
public string ViedoTypeStr => ViedoType.GetDescription();
|
public string ViedoTypeStr => ViedoType.GetDescription();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace dy.net.model
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 永久删除的视频记录-不再下载
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable(TableName = "dy_delete_video")]
|
||||||
|
public class DouyinVideoDelete
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsPrimaryKey = true)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 原视频Id
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable =true,Length =50)]
|
||||||
|
public string ViedoId { get; set; }
|
||||||
|
|
||||||
|
public DateTime DeleteTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,11 +93,11 @@ namespace dy.net.repository
|
|||||||
/// <param name="followInfos"></param>
|
/// <param name="followInfos"></param>
|
||||||
/// <param name="myselfUserId"></param>
|
/// <param name="myselfUserId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<bool> Sync(List<FollowingsItem> followInfos, string myselfUserId)
|
public async Task<bool> Sync(List<FollowingsItem> followInfos, DouyinCookie ck)
|
||||||
{
|
{
|
||||||
// 基础参数校验
|
// 基础参数校验
|
||||||
if (followInfos == null) followInfos = new List<FollowingsItem>();
|
if (followInfos == null) followInfos = new List<FollowingsItem>();
|
||||||
if (string.IsNullOrWhiteSpace(myselfUserId))
|
if (ck==null || string.IsNullOrWhiteSpace(ck.MyUserId))
|
||||||
{
|
{
|
||||||
Serilog.Log.Error("同步关注列表失败:当前用户ID为空");
|
Serilog.Log.Error("同步关注列表失败:当前用户ID为空");
|
||||||
return false;
|
return false;
|
||||||
@@ -107,7 +107,7 @@ namespace dy.net.repository
|
|||||||
{
|
{
|
||||||
// 1. 查询现有关注列表
|
// 1. 查询现有关注列表
|
||||||
List<DouyinFollowed> existFollows = await Db.Queryable<DouyinFollowed>()
|
List<DouyinFollowed> existFollows = await Db.Queryable<DouyinFollowed>()
|
||||||
.Where(x => x.mySelfId == myselfUserId)
|
.Where(x => x.mySelfId == ck.MyUserId)
|
||||||
.Where(x=>!x.IsNoFollowed) //排除手动添加但未关注的用户
|
.Where(x=>!x.IsNoFollowed) //排除手动添加但未关注的用户
|
||||||
.ToListAsync() ?? new List<DouyinFollowed>();
|
.ToListAsync() ?? new List<DouyinFollowed>();
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ namespace dy.net.repository
|
|||||||
Id = IdGener.GetLong().ToString(),
|
Id = IdGener.GetLong().ToString(),
|
||||||
Enterprise = follow.EnterpriseVerifyReason,
|
Enterprise = follow.EnterpriseVerifyReason,
|
||||||
LastSyncTime = DateTime.UtcNow,
|
LastSyncTime = DateTime.UtcNow,
|
||||||
mySelfId = myselfUserId,
|
mySelfId = ck.MyUserId,
|
||||||
SecUid = follow.SecUid,
|
SecUid = follow.SecUid,
|
||||||
OpenSync = false,
|
OpenSync = false,
|
||||||
UperAvatar = follow.Avatar?.UrlList?.FirstOrDefault() ?? "",
|
UperAvatar = follow.Avatar?.UrlList?.FirstOrDefault() ?? "",
|
||||||
@@ -201,8 +201,6 @@ namespace dy.net.repository
|
|||||||
Serilog.Log.Error("同步关注列表失败:关注信息更新异常");
|
Serilog.Log.Error("同步关注列表失败:关注信息更新异常");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serilog.Log.Debug($"同步关注列表:成功更新{toUpdateFollows.Count}条关注信息(用户ID:{myselfUserId})");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 分批处理删除(单批200条)
|
// 6. 分批处理删除(单批200条)
|
||||||
@@ -212,7 +210,7 @@ namespace dy.net.repository
|
|||||||
async batch =>
|
async batch =>
|
||||||
{
|
{
|
||||||
var secUids = batch.Select(x => x.SecUid).ToList();
|
var secUids = batch.Select(x => x.SecUid).ToList();
|
||||||
await DeleteAsync(x => x.mySelfId == myselfUserId && secUids.Contains(x.SecUid));
|
await DeleteAsync(x => x.mySelfId == ck.MyUserId && secUids.Contains(x.SecUid));
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,12 +221,12 @@ namespace dy.net.repository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serilog.Log.Debug($"同步关注列表完成(用户ID:{myselfUserId}):新增{toAddFollows.Count}条,更新{toUpdateFollows.Count}条,删除{toRemoveFollows.Count}条");
|
Serilog.Log.Debug($"dy_followed_users({ck.UserName})关注列表同步完成:新增{toAddFollows.Count}条,更新{toUpdateFollows.Count}条,删除{toRemoveFollows.Count}条");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Error(ex, $"同步关注列表失败(用户ID:{myselfUserId}):{ex.Message}");
|
Serilog.Log.Error(ex, $"同步关注列表失败({ck.UserName}):{ex.Message}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ namespace dy.net.repository
|
|||||||
|
|
||||||
|
|
||||||
VideoTypeEnum? enumviedoType = null;
|
VideoTypeEnum? enumviedoType = null;
|
||||||
if (!string.IsNullOrEmpty(dto.ViedoType) && dto.ViedoType != "*")
|
if (!string.IsNullOrEmpty(dto.ViedoType) && dto.ViedoType != "*" && dto.ViedoType != "4")
|
||||||
{
|
{
|
||||||
enumviedoType = dto.ViedoType.ToVideoTypeEnum();
|
enumviedoType = dto.ViedoType.ToVideoTypeEnum();
|
||||||
}
|
}
|
||||||
|
|
||||||
var where = this.Db.Queryable<DouyinVideo>()
|
var where = this.Db.Queryable<DouyinVideo>()
|
||||||
//.WhereIF(!string.IsNullOrWhiteSpace(title), x => x.VideoTitle.Contains(title))
|
//.WhereIF(!string.IsNullOrWhiteSpace(title), x => x.VideoTitle.Contains(title))
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(dto.Title), x => x.VideoTitle.Contains(dto.Title))
|
.WhereIF(!string.IsNullOrWhiteSpace(dto.Title), x => x.VideoTitle.Contains(dto.Title))
|
||||||
@@ -45,7 +46,8 @@ namespace dy.net.repository
|
|||||||
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
|
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
|
||||||
.WhereIF(start2.HasValue, x => x.CreateTime >= start2.Value)
|
.WhereIF(start2.HasValue, x => x.CreateTime >= start2.Value)
|
||||||
.WhereIF(end2.HasValue, x => x.CreateTime <= end2.Value)
|
.WhereIF(end2.HasValue, x => x.CreateTime <= end2.Value)
|
||||||
.WhereIF(enumviedoType.HasValue, x => x.ViedoType == enumviedoType);
|
.WhereIF(enumviedoType.HasValue, x => x.ViedoType == enumviedoType)
|
||||||
|
.WhereIF(dto.ViedoType == "4", x => x.IsMergeVideo == 1);
|
||||||
|
|
||||||
|
|
||||||
var totalCount = await where.CountAsync();
|
var totalCount = await where.CountAsync();
|
||||||
@@ -90,7 +92,7 @@ namespace dy.net.repository
|
|||||||
public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
||||||
{
|
{
|
||||||
|
|
||||||
var video = await this.Db.Queryable<DouyinVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.UperPost)
|
var video = await this.Db.Queryable<DouyinVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.dy_follows)
|
||||||
.Where(x => x.VideoTitleSimplify == ViedoNameSimplify)
|
.Where(x => x.VideoTitleSimplify == ViedoNameSimplify)
|
||||||
.OrderByDescending(x => x.CreateTime).FirstAsync();
|
.OrderByDescending(x => x.CreateTime).FirstAsync();
|
||||||
|
|
||||||
@@ -140,7 +142,7 @@ namespace dy.net.repository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="downs"></param>
|
/// <param name="downs"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public bool InsertReDowns(List<ViedoReDown> downs)
|
public bool InsertReDowns(List<DouyinReDownload> downs)
|
||||||
{
|
{
|
||||||
if (downs != null)
|
if (downs != null)
|
||||||
{
|
{
|
||||||
@@ -169,9 +171,9 @@ namespace dy.net.repository
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建更新条件(统一Where条件,避免重复代码)
|
// 构建更新条件(统一Where条件,避免重复代码)
|
||||||
var updateable = Db.Updateable<ViedoReDown>()
|
var updateable = Db.Updateable<DouyinReDownload>()
|
||||||
.Where(it => it.Id == videoId)
|
.Where(it => it.Id == videoId)
|
||||||
.SetColumns(it => new ViedoReDown
|
.SetColumns(it => new DouyinReDownload
|
||||||
{
|
{
|
||||||
Status = status,
|
Status = status,
|
||||||
UpdateTime = DateTime.Now
|
UpdateTime = DateTime.Now
|
||||||
@@ -197,9 +199,9 @@ namespace dy.net.repository
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<List<ViedoReDown>> GetViedoReDowns()
|
public async Task<List<DouyinReDownload>> GetViedoReDowns()
|
||||||
{
|
{
|
||||||
return await this.Db.Queryable<ViedoReDown>()
|
return await this.Db.Queryable<DouyinReDownload>()
|
||||||
.Where(x => x.Status == 0 || x.Status == 2)
|
.Where(x => x.Status == 0 || x.Status == 2)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ namespace dy.net.service
|
|||||||
LogKeepDay = 10,
|
LogKeepDay = 10,
|
||||||
UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
|
UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
|
||||||
UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名
|
UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名
|
||||||
DownImageVideo = false,//默认不下载图文视频
|
DownImageVideo = true,//默认下载图文视频
|
||||||
DownMp3 = false,
|
DownMp3 = false,
|
||||||
DownImage = false,
|
DownImage = false,
|
||||||
ImageViedoSaveAlone = true,
|
ImageViedoSaveAlone = true,
|
||||||
@@ -99,18 +99,18 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 兼容旧版将之前同步了的我收藏的数据进行更新
|
/// 初始化一些数据,兼容旧版数据结构。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateCollectViedoType()
|
public void UpdateCollectViedoType()
|
||||||
{
|
{
|
||||||
|
//更新视频类型字段-兼容老版本
|
||||||
string sql = @"UPDATE dy_collect_video
|
string sql = @"UPDATE dy_collect_video
|
||||||
SET ViedoType = CASE
|
SET ViedoType = CASE
|
||||||
WHEN ViedoType = '0' THEN 0
|
WHEN ViedoType = '0' THEN 0
|
||||||
WHEN ViedoType = '1' THEN 1
|
WHEN ViedoType = '1' THEN 1
|
||||||
WHEN ViedoType = '2' THEN 2
|
WHEN ViedoType = '2' THEN 2
|
||||||
WHEN ViedoType = '3' THEN 3
|
WHEN ViedoType = '3' THEN 3
|
||||||
WHEN ViedoType = '4' THEN 4
|
WHEN ViedoType = '4' THEN 4
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END;";
|
END;";
|
||||||
|
|
||||||
@@ -119,16 +119,53 @@ namespace dy.net.service
|
|||||||
//更新关注表的IsNoFollowed字段为空的数据为0--兼容老版本-新加的字段
|
//更新关注表的IsNoFollowed字段为空的数据为0--兼容老版本-新加的字段
|
||||||
string followUpdateSql = @"Update dy_follow SET IsNoFollowed=0 WHERE IsNoFollowed is NULL";
|
string followUpdateSql = @"Update dy_follow SET IsNoFollowed=0 WHERE IsNoFollowed is NULL";
|
||||||
sqlSugarClient.Ado.ExecuteCommand(followUpdateSql);
|
sqlSugarClient.Ado.ExecuteCommand(followUpdateSql);
|
||||||
//var collectViedos = sqlSugarClient.Queryable<DouyinVideo>().ToList();
|
|
||||||
|
|
||||||
//if (collectViedos.Any())
|
//更新图片视频的合并状态
|
||||||
//{
|
string updateIsMergeVideoSql = @"UPDATE dy_collect_video SET IsMergeVideo = 1 WHERE IsMergeVideo IS NULL and ViedoType='4';";
|
||||||
// collectViedos.ForEach(x =>
|
sqlSugarClient.Ado.ExecuteCommand(updateIsMergeVideoSql);
|
||||||
// {
|
|
||||||
// x.ViedoType = x.;
|
//更新非图片视频的合并状态
|
||||||
// });
|
string updateNoIsMergeVideoSql = @"UPDATE dy_collect_video SET IsMergeVideo = 0 WHERE IsMergeVideo IS NULL and ViedoType<>'4';";
|
||||||
// sqlSugarClient.Updateable(collectViedos).ExecuteCommand();
|
sqlSugarClient.Ado.ExecuteCommand(updateNoIsMergeVideoSql);
|
||||||
//}
|
|
||||||
|
//强制开启去重
|
||||||
|
sqlSugarClient.Updateable<AppConfig>().SetColumns(x => new AppConfig { AutoDistinct = true }).Where(x => !string.IsNullOrWhiteSpace(x.Id)).ExecuteCommand();
|
||||||
|
//重新根据保存路径更新图片视频的类型为喜欢,收藏或关注
|
||||||
|
var collectViedos = sqlSugarClient.Queryable<DouyinVideo>().Where(x=>x.ViedoType==VideoTypeEnum.ImageVideo).ToList();
|
||||||
|
if (collectViedos != null && collectViedos.Any())
|
||||||
|
{
|
||||||
|
var cookies= sqlSugarClient.Queryable<DouyinCookie>().ToList();
|
||||||
|
collectViedos.ForEach(x =>
|
||||||
|
{
|
||||||
|
var ck= cookies.FirstOrDefault(c => c.Id == x.CookieId);
|
||||||
|
if (ck != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
var savePath = x.VideoSavePath;
|
||||||
|
if (savePath != null)
|
||||||
|
{
|
||||||
|
if (savePath.StartsWith(ck.SavePath))
|
||||||
|
{
|
||||||
|
x.ViedoType = VideoTypeEnum.dy_collects;
|
||||||
|
}
|
||||||
|
else if(savePath.StartsWith(ck.UpSavePath))
|
||||||
|
{
|
||||||
|
x.ViedoType = VideoTypeEnum.dy_follows;
|
||||||
|
}
|
||||||
|
else if(savePath.StartsWith(ck.FavSavePath))
|
||||||
|
{
|
||||||
|
x.ViedoType = VideoTypeEnum.dy_favorite;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
x.ViedoType= VideoTypeEnum.dy_collects;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//只更新图片视频的类型
|
||||||
|
sqlSugarClient.Updateable(collectViedos).UpdateColumns(x => new DouyinVideo { ViedoType = x.ViedoType }).IgnoreColumns(true).ExecuteCommand();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重置所有Cookie的同步状态为0
|
/// 重置所有Cookie的同步状态为0
|
||||||
@@ -148,18 +185,28 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询需要下载的所有重下载视频记录
|
/// 查询是否已删除
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="videoId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<List<ViedoReDown>> GetAllRedown()
|
public async Task<bool> ExistDeleteVideo(string videoId)
|
||||||
{
|
{
|
||||||
return await sqlSugarClient.Queryable<ViedoReDown>().Where(x => x.Status == 0 || x.Status == 2).ToListAsync();
|
var count= await sqlSugarClient.Queryable<DouyinVideoDelete>().Where(x=>x.ViedoId==videoId).CountAsync();
|
||||||
|
return count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdateRedownStatus(List<ViedoReDown> list)
|
/// <summary>
|
||||||
|
/// 新增要删除的视频
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> AddDeleteVideo(DouyinVideoDelete dto)
|
||||||
{
|
{
|
||||||
return await sqlSugarClient.Updateable(list).ExecuteCommandAsync() > 0;
|
dto.Id=IdGener.GetLong().ToString();
|
||||||
|
dto.DeleteTime = DateTime.Now;
|
||||||
|
return sqlSugarClient.Insertable(dto).ExecuteCommand() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 测试创建数据库
|
#region 测试创建数据库
|
||||||
|
|
||||||
///// <summary>
|
///// <summary>
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ namespace dy.net.service
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="followInfos"></param>
|
/// <param name="followInfos"></param>
|
||||||
/// <param name="myselfUserId"></param>
|
/// <param name="ck"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<bool> Sync(List<FollowingsItem> followInfos, string myselfUserId)
|
public async Task<bool> Sync(List<FollowingsItem> followInfos, DouyinCookie ck)
|
||||||
{
|
{
|
||||||
return await _followRepository.Sync(followInfos, myselfUserId);
|
return await _followRepository.Sync(followInfos, ck);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DouyinFollowed> GetByUperId(string uperId,string myUid)
|
public async Task<DouyinFollowed> GetByUperId(string uperId,string myUid)
|
||||||
|
|||||||
@@ -15,11 +15,14 @@ namespace dy.net.service
|
|||||||
public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme";
|
public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme";
|
||||||
// 随机数生成器(避免重复实例化,保证随机性)
|
// 随机数生成器(避免重复实例化,保证随机性)
|
||||||
private readonly IHttpClientFactory _clientFactory;
|
private readonly IHttpClientFactory _clientFactory;
|
||||||
|
// 下载信号量锁:初始计数1,最大并发1(同时只能一个下载任务)
|
||||||
|
private readonly SemaphoreSlim _downloadSemaphore = new SemaphoreSlim(1, 1);
|
||||||
public DouyinHttpClientService(IHttpClientFactory clientFactory)
|
public DouyinHttpClientService(IHttpClientFactory clientFactory)
|
||||||
{
|
{
|
||||||
_clientFactory = clientFactory;
|
_clientFactory = clientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询用户收藏的视频
|
/// 查询用户收藏的视频
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -371,6 +374,111 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载文件并保存到本地(支持重试机制+单线程限制,同时只能一个下载)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="videoUrl">文件地址</param>
|
||||||
|
/// <param name="savePath">保存路径</param>
|
||||||
|
/// <param name="cookie">请求Cookie</param>
|
||||||
|
/// <param name="httpclientName">HttpClient名称(默认"dy_down1")</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌(用于终止任务)</param>
|
||||||
|
/// <param name="streamTimeout">流读取超时时间(默认60秒)</param>
|
||||||
|
/// <param name="maxRetryCount">最大重试次数(默认3次)</param>
|
||||||
|
/// <param name="initialRetryDelay">初始重试延迟(默认1秒,指数退避)</param>
|
||||||
|
/// <returns>是否下载成功</returns>
|
||||||
|
//public async Task<bool> DownloadAsync(
|
||||||
|
// string videoUrl,
|
||||||
|
// string savePath,
|
||||||
|
// string cookie,
|
||||||
|
// CancellationToken cancellationToken = default,
|
||||||
|
// TimeSpan? streamTimeout = null,
|
||||||
|
// int maxRetryCount = 3,
|
||||||
|
// TimeSpan? initialRetryDelay = null)
|
||||||
|
//{
|
||||||
|
// bool lockAcquired = false;
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// // 申请锁:如果已有下载任务在执行,会阻塞等待直到锁释放
|
||||||
|
// // 传入cancellationToken支持取消等待
|
||||||
|
// await _downloadSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
// lockAcquired = true; // 标记锁已获取
|
||||||
|
|
||||||
|
// // 重试参数初始化
|
||||||
|
// int retryCount = 0;
|
||||||
|
// var retryDelay = initialRetryDelay ?? TimeSpan.FromSeconds(1);
|
||||||
|
// streamTimeout ??= TimeSpan.FromSeconds(60);
|
||||||
|
|
||||||
|
// while (true)
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// return await TryDownloadOnceAsync(
|
||||||
|
// videoUrl, savePath, cookie, cancellationToken, streamTimeout.Value);
|
||||||
|
// }
|
||||||
|
// catch (Exception ex) when (IsRetryableException(ex) && retryCount < maxRetryCount)
|
||||||
|
// {
|
||||||
|
// retryCount++;
|
||||||
|
// var delay = TimeSpan.FromMilliseconds(retryDelay.TotalMilliseconds * Math.Pow(2, retryCount - 1));
|
||||||
|
// Serilog.Log.Warning(ex, $"下载失败(第{retryCount}/{maxRetryCount}次重试):{videoUrl},将在{delay.TotalSeconds:F1}秒后重试");
|
||||||
|
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||||
|
// }
|
||||||
|
// catch (OperationCanceledException)
|
||||||
|
// {
|
||||||
|
// Serilog.Log.Information($"重试等待被取消:{videoUrl}");
|
||||||
|
// CleanupIncompleteFile(savePath);
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
// {
|
||||||
|
// Serilog.Log.Information($"下载被取消:{videoUrl}");
|
||||||
|
// CleanupIncompleteFile(savePath);
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// Serilog.Log.Error(ex, $"下载失败(不可重试):{videoUrl}");
|
||||||
|
// CleanupIncompleteFile(savePath);
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// finally
|
||||||
|
// {
|
||||||
|
// // 确保锁一定释放(无论是否发生异常)
|
||||||
|
// if (lockAcquired)
|
||||||
|
// {
|
||||||
|
// _downloadSemaphore.Release();
|
||||||
|
// Serilog.Log.Debug($"下载锁已释放,下一个任务可执行");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
// 辅助类:用于using语句自动释放SemaphoreSlim
|
||||||
|
//public sealed class SemaphoreReleaser : IDisposable
|
||||||
|
//{
|
||||||
|
// private readonly SemaphoreSlim _semaphore;
|
||||||
|
// private bool _disposed;
|
||||||
|
|
||||||
|
// public SemaphoreReleaser(SemaphoreSlim semaphore)
|
||||||
|
// {
|
||||||
|
// _semaphore = semaphore ?? throw new ArgumentNullException(nameof(semaphore));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Dispose()
|
||||||
|
// {
|
||||||
|
// if (!_disposed)
|
||||||
|
// {
|
||||||
|
// _semaphore.Release(); // 释放锁,允许下一个任务执行
|
||||||
|
// _disposed = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单次下载尝试(核心下载逻辑)
|
/// 单次下载尝试(核心下载逻辑)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ namespace dy.net.service
|
|||||||
// 延迟清理:给 FFmpeg 进程足够时间释放文件句柄(1秒)
|
// 延迟清理:给 FFmpeg 进程足够时间释放文件句柄(1秒)
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
Directory.Delete(tempDir, recursive: true);
|
Directory.Delete(tempDir, recursive: true);
|
||||||
Log.Debug($"临时目录已清理:{tempDir}");
|
//Log.Debug($"临时目录已清理:{tempDir}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
+277
-243
@@ -1,6 +1,7 @@
|
|||||||
using dy.net.dto;
|
using dy.net.dto;
|
||||||
using dy.net.job;
|
using dy.net.job;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
using Quartz.Impl.Matchers;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -13,242 +14,21 @@ namespace dy.net.service
|
|||||||
public class DouyinQuartzJobService
|
public class DouyinQuartzJobService
|
||||||
{
|
{
|
||||||
private readonly ISchedulerFactory _schedulerFactory;
|
private readonly ISchedulerFactory _schedulerFactory;
|
||||||
private const string DefaultJobGroup = "group1";
|
private const string DefaultJobGroup = "dysync.net";
|
||||||
private const int DefaultIntervalMinutes = 30;
|
private const int DefaultIntervalMinutes = 30;
|
||||||
private const int DefaultCronStartDelaySeconds = 30;
|
private const int DefaultCronStartDelaySeconds = 30;
|
||||||
private const int DefaultSimpleStartDelaySeconds = 3;
|
private const int DefaultSimpleStartDelaySeconds = 3;
|
||||||
|
|
||||||
|
// 任务顺序依赖配置(核心:定义执行顺序,供 Listener 使用)
|
||||||
|
public Dictionary<string, string> JobDependency { get; } = new()
|
||||||
public DouyinQuartzJobService(ISchedulerFactory schedulerFactory)
|
|
||||||
{
|
{
|
||||||
_schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
|
{"collect", "favorite"}, // collect 执行完 → 触发 favorite
|
||||||
}
|
{"favorite", "followed"}, // favorite 执行完 → 触发 followed
|
||||||
|
{"followed", null}, // followed 执行完 → 一轮任务结束
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
// 任务配置信息(保持原有配置不变,改为 public 供 Listener 访问)
|
||||||
/// 启动所有抖音相关定时任务
|
public Dictionary<string, JobConfig> JobConfigs { get; } = new()
|
||||||
/// </summary>
|
|
||||||
/// <param name="expression">Cron表达式或间隔分钟数</param>
|
|
||||||
/// <param name="delayBetweenJobs">任务之间的启动延迟(毫秒)</param>
|
|
||||||
/// <returns>是否启动成功</returns>
|
|
||||||
public async Task<bool> InitOrReStartAllJobs(string expression, int delayBetweenJobs = 5000)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(expression))
|
|
||||||
{
|
|
||||||
Log.Warning("定时任务表达式为空,使用默认配置");
|
|
||||||
expression = DefaultIntervalMinutes.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按顺序启动任务,避免并发
|
|
||||||
var jobTasks = new List<Task<bool>>
|
|
||||||
{
|
|
||||||
//关注列表
|
|
||||||
//StartJobAsync("follow_user", expression),
|
|
||||||
//我收藏的作品
|
|
||||||
StartJobAsync("collect", expression),
|
|
||||||
//我喜欢的作品
|
|
||||||
DelayAndStartJobAsync("favorite", expression, delayBetweenJobs),
|
|
||||||
//关注的用户的作品
|
|
||||||
DelayAndStartJobAsync("uper", expression, delayBetweenJobs * 2),
|
|
||||||
//关注列表
|
|
||||||
DelayAndStartJobAsync("follow_user", (Convert.ToInt32(expression)*2*24).ToString(), delayBetweenJobs * 3)
|
|
||||||
};
|
|
||||||
|
|
||||||
var results = await Task.WhenAll(jobTasks);
|
|
||||||
return results.All(success => success);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 启动关注同步任务(单次执行)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>是否启动成功</returns>
|
|
||||||
public async Task<bool> StartFollowJobOnceAsync()
|
|
||||||
{
|
|
||||||
return await StartOneTimeJobAsync("follow_user_once");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 延迟后启动任务
|
|
||||||
/// </summary>
|
|
||||||
private async Task<bool> DelayAndStartJobAsync(string jobKey, string expression, int delayMs)
|
|
||||||
{
|
|
||||||
if (delayMs > 0)
|
|
||||||
{
|
|
||||||
await Task.Delay(delayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是数字表达式,自动递增避免并发
|
|
||||||
var adjustedExpression = AdjustExpressionForConcurrency(jobKey, expression);
|
|
||||||
return await StartJobAsync(jobKey, adjustedExpression);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 调整任务表达式以避免并发
|
|
||||||
/// </summary>
|
|
||||||
private string AdjustExpressionForConcurrency(string jobKey, string expression)
|
|
||||||
{
|
|
||||||
if (!int.TryParse(expression, out int interval))
|
|
||||||
return expression;
|
|
||||||
|
|
||||||
// 根据任务类型递增间隔,避免所有任务同时执行
|
|
||||||
var jobIndex = _jobConfigs.Keys.ToList().IndexOf(jobKey);
|
|
||||||
return (interval + jobIndex).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 启动指定定时任务
|
|
||||||
/// </summary>
|
|
||||||
private async Task<bool> StartJobAsync(string configKey, string expression)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// 创建触发器
|
|
||||||
var trigger = CreateTrigger(triggerKey, expression, jobConfig.Description);
|
|
||||||
if (trigger == null)
|
|
||||||
{
|
|
||||||
Log.Error("创建触发器失败: {JobDescription}", jobConfig.Description);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调度任务
|
|
||||||
await scheduler.ScheduleJob(jobDetail, trigger);
|
|
||||||
Log.Information("启动定时任务成功 - {JobDescription}, 表达式: {Expression}",
|
|
||||||
jobConfig.Description, 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)
|
|
||||||
.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>
|
|
||||||
/// 创建触发器(支持Cron表达式和简单间隔)
|
|
||||||
/// </summary>
|
|
||||||
private ITrigger? CreateTrigger(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 async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
|
||||||
{
|
|
||||||
if (await scheduler.CheckExists(jobKey))
|
|
||||||
{
|
|
||||||
Log.Information("移除已存在的任务: {JobKey}", jobKey);
|
|
||||||
await scheduler.DeleteJob(jobKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 任务配置信息
|
|
||||||
/// </summary>
|
|
||||||
private readonly Dictionary<string, JobConfig> _jobConfigs = new()
|
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
"collect",
|
"collect",
|
||||||
@@ -267,11 +47,11 @@ namespace dy.net.service
|
|||||||
"抖音点赞同步任务")
|
"抖音点赞同步任务")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uper",
|
"followed",
|
||||||
new JobConfig(
|
new JobConfig(
|
||||||
typeof(DouyinFollowedViedoSyncJob),
|
typeof(DouyinFollowedViedoSyncJob),
|
||||||
"dy.job.key.uper",
|
"dy.job.key.followed",
|
||||||
"dy.trigger.key.uper",
|
"dy.trigger.key.followed",
|
||||||
"抖音UP主作品同步任务")
|
"抖音UP主作品同步任务")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290,16 +70,270 @@ namespace dy.net.service
|
|||||||
"dy.trigger.key.follow_user_once",
|
"dy.trigger.key.follow_user_once",
|
||||||
"抖音关注同步任务(单次执行)")
|
"抖音关注同步任务(单次执行)")
|
||||||
}
|
}
|
||||||
,
|
|
||||||
//{
|
|
||||||
// "redown_once",
|
|
||||||
// new JobConfig(
|
|
||||||
// typeof(DouyinReDownSyncJob),
|
|
||||||
// "dy.job.key.redown_once",
|
|
||||||
// "dy.trigger.key.redown_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 → follow_user", expression);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Error("【任务服务】任务链条启动失败(第一个任务 {FirstJob} 启动失败)", firstJobConfigKey);
|
||||||
|
}
|
||||||
|
//启动follow_user--这个与其他几个任务没有依赖关系,所以单独启动
|
||||||
|
await StartJobAsync("follow_user", expression);
|
||||||
|
return startSuccess;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,11 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool> DeleteById(string Id)
|
||||||
|
{
|
||||||
|
return await _dyCollectVideoRepository.DeleteByIdAsync(Id);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> BatchInsertOrUpdate(List<DouyinVideo> videos)
|
public async Task<bool> BatchInsertOrUpdate(List<DouyinVideo> videos)
|
||||||
{
|
{
|
||||||
// 边界处理:传入列表为空直接返回成功
|
// 边界处理:传入列表为空直接返回成功
|
||||||
@@ -99,22 +104,22 @@ namespace dy.net.service
|
|||||||
CategoryCount = list.Select(x => x.Tag1).Distinct().Count(),
|
CategoryCount = list.Select(x => x.Tag1).Distinct().Count(),
|
||||||
VideoCount = list.Count,
|
VideoCount = list.Count,
|
||||||
Categories = Categories,
|
Categories = Categories,
|
||||||
FavoriteCount = list.Count(x => x.ViedoType == VideoTypeEnum.Favorite),
|
FavoriteCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_favorite),
|
||||||
CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.Collect),
|
CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_collects),
|
||||||
FollowCount = list.Count(x => x.ViedoType == VideoTypeEnum.UperPost),
|
FollowCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_follows),
|
||||||
GraphicVideoCount = list.Count(x => x.ViedoType == VideoTypeEnum.ImageVideo),
|
GraphicVideoCount = list.Count(x => x.IsMergeVideo == 1),
|
||||||
|
|
||||||
VideoSizeTotal = ByteToGbConverter.ConvertBytesToGb(list.Sum(x => x.FileSize)),
|
VideoSizeTotal = ByteToGbConverter.ConvertBytesToGb(list.Sum(x => x.FileSize)),
|
||||||
VideoFavoriteSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.Favorite).Sum(x => x.FileSize)),
|
VideoFavoriteSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_favorite).Sum(x => x.FileSize)),
|
||||||
VideoCollectSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.Collect).Sum(x => x.FileSize)),
|
VideoCollectSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_collects).Sum(x => x.FileSize)),
|
||||||
VideoFollowSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.UperPost).Sum(x => x.FileSize)),
|
VideoFollowSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_follows).Sum(x => x.FileSize)),
|
||||||
GraphicVideoSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.ImageVideo).Sum(x => x.FileSize)),
|
GraphicVideoSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.IsMergeVideo == 1).Sum(x => x.FileSize)),
|
||||||
|
|
||||||
//TotalDiskSize= ByteToGbConverter.GetHostTotalDiskSpaceGB(),
|
//TotalDiskSize= ByteToGbConverter.GetHostTotalDiskSpaceGB(),
|
||||||
};
|
};
|
||||||
if (data.GraphicVideoSize == "0.00")
|
if (data.GraphicVideoSize == "0.00")
|
||||||
{
|
{
|
||||||
if (list.Where(x => x.ViedoType == VideoTypeEnum.ImageVideo).Sum(x => x.FileSize) > 0)
|
if (list.Where(x => x.IsMergeVideo == 1).Sum(x => x.FileSize) > 0)
|
||||||
{
|
{
|
||||||
data.GraphicVideoSize = "<0.01";//避免显示0.00误导用户
|
data.GraphicVideoSize = "<0.01";//避免显示0.00误导用户
|
||||||
}
|
}
|
||||||
@@ -166,6 +171,8 @@ namespace dy.net.service
|
|||||||
return await _dyCollectVideoRepository.GetByIdAsync(id);
|
return await _dyCollectVideoRepository.GetByIdAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重新下载选中的视频
|
/// 重新下载选中的视频
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -194,7 +201,7 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 构建重新下载记录(提前准备数据,避免事务内耗时操作)
|
// 3. 构建重新下载记录(提前准备数据,避免事务内耗时操作)
|
||||||
var reDownList = new List<ViedoReDown>();
|
var reDownList = new List<DouyinReDownload>();
|
||||||
var filePathsToDelete = new List<string>(); // 收集待删除文件路径,统一处理
|
var filePathsToDelete = new List<string>(); // 收集待删除文件路径,统一处理
|
||||||
|
|
||||||
foreach (var video in videos)
|
foreach (var video in videos)
|
||||||
@@ -207,7 +214,7 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建重新下载记录
|
// 构建重新下载记录
|
||||||
reDownList.Add(new ViedoReDown
|
reDownList.Add(new DouyinReDownload
|
||||||
{
|
{
|
||||||
Id = IdGener.GetLong().ToString(),
|
Id = IdGener.GetLong().ToString(),
|
||||||
CreateTime = DateTime.UtcNow, // 统一使用UTC时间,避免时区问题
|
CreateTime = DateTime.UtcNow, // 统一使用UTC时间,避免时区问题
|
||||||
@@ -233,7 +240,7 @@ namespace dy.net.service
|
|||||||
var transactionResult = await _dyCollectVideoRepository.UseTranAsync(async () =>
|
var transactionResult = await _dyCollectVideoRepository.UseTranAsync(async () =>
|
||||||
{
|
{
|
||||||
// 4.1 批量插入重新下载记录(SqlSugar批量插入效率更高)
|
// 4.1 批量插入重新下载记录(SqlSugar批量插入效率更高)
|
||||||
_dyCollectVideoRepository.InsertReDowns(reDownList);
|
_dyCollectVideoRepository.InsertReDowns(reDownList);
|
||||||
// 4.2 批量删除原视频记录(使用视频实际存在的ID,避免无效删除)
|
// 4.2 批量删除原视频记录(使用视频实际存在的ID,避免无效删除)
|
||||||
var actualDeleteIds = videos.Select(v => v.Id).ToList();
|
var actualDeleteIds = videos.Select(v => v.Id).ToList();
|
||||||
var deleteCount = await _dyCollectVideoRepository.DeleteByIdsAsync(actualDeleteIds); // 建议仓储层提供异步删除方法
|
var deleteCount = await _dyCollectVideoRepository.DeleteByIdsAsync(actualDeleteIds); // 建议仓储层提供异步删除方法
|
||||||
@@ -242,75 +249,49 @@ namespace dy.net.service
|
|||||||
Serilog.Log.Error(e, "数据库事务执行失败:Ids={0}", string.Join(",", videoIds));
|
Serilog.Log.Error(e, "数据库事务执行失败:Ids={0}", string.Join(",", videoIds));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 文件夹删除(非事务操作,失败不回滚数据库,可根据业务调整)
|
// 5. 文件删除(非事务操作,失败不回滚数据库,可根据业务调整)
|
||||||
// 关键:先通过文件路径获取父文件夹,再删除整个文件夹(含子内容)
|
// 采用异步文件操作,避免同步IO阻塞线程(需.NET 5+支持)
|
||||||
foreach (var filePath in filePathsToDelete)
|
foreach (var path in filePathsToDelete)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. 验证文件路径有效性
|
if (File.Exists(path))
|
||||||
if (string.IsNullOrWhiteSpace(filePath))
|
|
||||||
{
|
{
|
||||||
Serilog.Log.Error("文件路径为空,跳过文件夹删除");
|
File.Delete(path); // 异步删除,提升并发性能
|
||||||
continue;
|
Serilog.Log.Debug("视频文件删除成功:Path={0}", path);
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 获取文件对应的父文件夹路径(无论文件是否存在,只要路径合法就能拿到父目录)
|
|
||||||
string parentDirPath = Path.GetDirectoryName(filePath);
|
|
||||||
if (string.IsNullOrWhiteSpace(parentDirPath))
|
|
||||||
{
|
|
||||||
Serilog.Log.Error("无法获取父文件夹路径,文件路径无效:Filepath={0}", filePath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 验证父文件夹是否存在
|
|
||||||
if (Directory.Exists(parentDirPath))
|
|
||||||
{
|
|
||||||
Directory.Delete(parentDirPath, recursive: true);
|
|
||||||
Serilog.Log.Debug("文件夹删除成功(含子内容):ParentDir={0},关联文件路径={1}", parentDirPath, filePath);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Serilog.Log.Error("父文件夹不存在,跳过删除:ParentDir={0},关联文件路径={1}", parentDirPath, filePath);
|
Serilog.Log.Error("视频文件不存在,跳过删除:Path={0}", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (IOException ex)
|
catch (IOException ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Error(ex, "文件夹删除失败(IO异常):Filepath={0},父文件夹路径={1}", filePath, Path.GetDirectoryName(filePath));
|
Serilog.Log.Error(ex, "视频文件删除失败:Path={0}", path);
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
// 单独捕获权限异常,更精准的日志提示
|
|
||||||
Serilog.Log.Error(ex, "文件夹删除失败(权限不足):Filepath={0},父文件夹路径={1}", filePath, Path.GetDirectoryName(filePath));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// 捕获所有其他异常,避免循环中断
|
|
||||||
Serilog.Log.Error(ex, "文件夹删除失败(未知异常):Filepath={0},父文件夹路径={1}", filePath, Path.GetDirectoryName(filePath));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var CookieIds = reDownList.Select(x => x.CookieId).Distinct();
|
var CookieIds = reDownList.Select(x => x.CookieId).Distinct();
|
||||||
foreach (var ck in CookieIds)
|
foreach (var ck in CookieIds)
|
||||||
{
|
{
|
||||||
var cookie= douyinCookieRepository.GetById(ck);
|
var cookie = douyinCookieRepository.GetById(ck);
|
||||||
if (cookie == null)
|
if (cookie == null)
|
||||||
continue;
|
continue;
|
||||||
var viedoTypes = videos.Where(x => x.CookieId == ck).Select(x => x.ViedoType).Distinct();
|
var viedoTypes = videos.Where(x => x.CookieId == ck).Select(x => x.ViedoType).Distinct();
|
||||||
|
|
||||||
if(viedoTypes!=null&& viedoTypes.Any())
|
if (viedoTypes != null && viedoTypes.Any())
|
||||||
{
|
{
|
||||||
foreach (VideoTypeEnum item in viedoTypes)
|
foreach (VideoTypeEnum item in viedoTypes)
|
||||||
{
|
{
|
||||||
switch (item)
|
switch (item)
|
||||||
{
|
{
|
||||||
case VideoTypeEnum.Favorite:
|
case VideoTypeEnum.dy_favorite:
|
||||||
cookie.FavHasSyncd = 0;
|
cookie.FavHasSyncd = 0;
|
||||||
break;
|
break;
|
||||||
case VideoTypeEnum.Collect:
|
case VideoTypeEnum.dy_collects:
|
||||||
cookie.CollHasSyncd = 0;
|
cookie.CollHasSyncd = 0;
|
||||||
break;
|
break;
|
||||||
case VideoTypeEnum.UperPost:
|
case VideoTypeEnum.dy_follows:
|
||||||
cookie.UperSyncd = 0;
|
cookie.UperSyncd = 0;
|
||||||
break;
|
break;
|
||||||
case VideoTypeEnum.ImageVideo:
|
case VideoTypeEnum.ImageVideo:
|
||||||
@@ -338,7 +319,7 @@ namespace dy.net.service
|
|||||||
/// 获取待重新下载的视频列表
|
/// 获取待重新下载的视频列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<List<ViedoReDown>> GetViedoReDowns()
|
public async Task<List<DouyinReDownload>> GetViedoReDowns()
|
||||||
{
|
{
|
||||||
return await _dyCollectVideoRepository.GetViedoReDowns();
|
return await _dyCollectVideoRepository.GetViedoReDowns();
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-138
@@ -10,156 +10,45 @@ namespace dy.net.utils
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DouyinFileNameHelper
|
public static class DouyinFileNameHelper
|
||||||
{
|
{
|
||||||
#region 配置参数
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 处理文件名/文件夹名,确保符合 Linux 限制(最大 255 字节 UTF-8 编码)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int MaxFileNameBytes = 60;
|
/// <param name="originalName">原始名称(支持英文、中文、混合字符)</param>
|
||||||
|
/// <param name="defaultName">截取后为空时的默认名称(默认 "default")</param>
|
||||||
|
/// <returns>符合 Linux 规则的合法名称</returns>
|
||||||
|
public static string SanitizeLinuxFileName(string originalName, string defaultName = "default")
|
||||||
/// <summary>
|
|
||||||
/// 非法字符替换后的占位符(也可设为空字符串)
|
|
||||||
/// </summary>
|
|
||||||
private const string IllegalCharReplacement = "";
|
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 处理抖音视频文件名
|
|
||||||
/// <summary>
|
|
||||||
/// 生成抖音视频文件名
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="originalTitle">抖音原始标题</param>
|
|
||||||
/// <param name="videoId">排序号--如果有重复标题,根据时间排序取最后一个的序号+1 从001开始</param>
|
|
||||||
/// <returns>安全可用的文件名</returns>
|
|
||||||
public static string GenerateFileName(string originalTitle,string videoId)
|
|
||||||
{
|
{
|
||||||
// 1. 基础容错(处理空值)
|
// 1. 空值处理:直接返回默认名
|
||||||
originalTitle = string.IsNullOrWhiteSpace(originalTitle) ? videoId : originalTitle;
|
if (string.IsNullOrWhiteSpace(originalName))
|
||||||
|
return defaultName.Replace(" ","");
|
||||||
|
|
||||||
// 2. 净化标题:移除非法内容
|
// 2. 过滤 Linux 非法字符:
|
||||||
string purifiedTitle = PurifyTitle(originalTitle, videoId);
|
// - 禁止:/(路径分隔符)、\0(空字符)
|
||||||
|
// - 替换:其他特殊字符(如 :*?"<>|\\ )为下划线 _,避免创建失败
|
||||||
// 3. 长度控制:按UTF-8字节数截断(避免超系统限制)
|
var invalidChars = new[] { '/', '\0', ':', '*', '?', '"', '<', '>', '|', '\\' };
|
||||||
string truncatedTitle = TruncateByByteLength(purifiedTitle, MaxFileNameBytes);
|
string sanitizedName = originalName;
|
||||||
|
foreach (var c in invalidChars)
|
||||||
return truncatedTitle.Trim();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 内部辅助方法:标题净化
|
|
||||||
/// <summary>
|
|
||||||
/// 净化标题(移除非法字符、表情、话题标签)
|
|
||||||
/// </summary>
|
|
||||||
private static string PurifyTitle(string title,string id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(title))
|
|
||||||
{
|
{
|
||||||
title = id;
|
sanitizedName = sanitizedName.Replace(c, '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 计算 UTF-8 字节数,若未超 255 字节,直接返回
|
||||||
|
byte[] utf8Bytes = Encoding.UTF8.GetBytes(sanitizedName);
|
||||||
|
if (utf8Bytes.Length <= 252)
|
||||||
|
return sanitizedName.Replace(" ", ""); ;
|
||||||
|
|
||||||
title = Regex.Replace(title, @"[^a-zA-Z0-9\u4e00-\u9fa5]", "-");
|
// 4. 超过 255 字节,截取前 255 字节(避免破坏 UTF-8 字符)
|
||||||
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
byte[] truncatedBytes = new byte[252];
|
||||||
//title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
|
Array.Copy(utf8Bytes, truncatedBytes, 252);
|
||||||
|
|
||||||
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
|
// 5. 字节数组转回字符串(自动忽略不完整的尾部字节,避免乱码)
|
||||||
//string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
|
string truncatedName = Encoding.UTF8.GetString(truncatedBytes).TrimEnd('\0').Replace(" ",""); // 移除可能的空字符
|
||||||
//title = Regex.Replace(title, emojiPattern, "", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
// 步骤3:过滤系统非法字符(Windows/macOS/Linux通用禁止)
|
// 6. 极端情况:截取后为空(如全是非法字符替换后无有效内容),返回默认名
|
||||||
char[] illegalChars = new[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0', '\t', '\n', '\r' };
|
return string.IsNullOrWhiteSpace(truncatedName) ? defaultName : truncatedName;
|
||||||
foreach (char c in illegalChars)
|
|
||||||
{
|
|
||||||
title = title.Replace(c.ToString(), IllegalCharReplacement);
|
|
||||||
}
|
|
||||||
char Separator = '-';
|
|
||||||
|
|
||||||
foreach (var c in Path.GetInvalidFileNameChars())
|
|
||||||
{
|
|
||||||
title = title.Replace(c, '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 步骤5:合并连续分隔符(避免多个---)
|
|
||||||
title = Regex.Replace(title, $"{Separator}+", Separator.ToString(), RegexOptions.Compiled);
|
|
||||||
|
|
||||||
// 步骤6:移除首尾无效字符(分隔符、点号)
|
|
||||||
title = title.Replace(" ","").Trim(Separator, '.');
|
|
||||||
title = title.Replace("--", "-");//避免多个连续----
|
|
||||||
// 容错:如果净化后为空,返回默认值
|
|
||||||
return string.IsNullOrWhiteSpace(title) ? id : title;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 内部辅助方法:按字节数截断
|
|
||||||
/// <summary>
|
|
||||||
/// 按UTF-8字节数截断字符串(避免截断半个中文)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="str">要截断的字符串</param>
|
|
||||||
/// <param name="maxBytes">最大字节数</param>
|
|
||||||
/// <returns>截断后的字符串</returns>
|
|
||||||
private static string TruncateByByteLength(string str, int maxBytes)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(str)) return str;
|
|
||||||
|
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(str);
|
|
||||||
if (bytes.Length <= maxBytes) return str;
|
|
||||||
|
|
||||||
// 从后往前截断,直到字节数≤maxBytes(避免半个中文)
|
|
||||||
for (int i = str.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
string truncated = str.Substring(0, i + 1);
|
|
||||||
if (Encoding.UTF8.GetByteCount(truncated) <= maxBytes)
|
|
||||||
{
|
|
||||||
return truncated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 极端情况(单个字符就超字节数),返回空字符串
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 清理路径中的特殊字符(避免创建文件夹失败)
|
|
||||||
public static string SanitizePath(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
path = "其他";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
|
||||||
//path = Regex.Replace(path, @"#\S+", "", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
|
|
||||||
//string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
|
|
||||||
//path = Regex.Replace(path, emojiPattern, "", RegexOptions.Compiled);
|
|
||||||
path = Regex.Replace(path, @"[^a-zA-Z0-9\u4e00-\u9fa5]", "-");
|
|
||||||
path = path.Replace("--", "-");//避免重复---
|
|
||||||
foreach (var c in Path.GetInvalidFileNameChars())
|
|
||||||
{
|
|
||||||
path = path.Replace(c, '_');
|
|
||||||
}
|
|
||||||
if (path.Length > 60)
|
|
||||||
{
|
|
||||||
path = path.Substring(0, 60);
|
|
||||||
}
|
|
||||||
return path.Trim().Replace(" ", "");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查字符串是否仅包含字母、数字、简体中文(无特殊字符)
|
/// 检查字符串是否仅包含字母、数字、简体中文(无特殊字符)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">待检查的字符串</param>
|
/// <param name="input">待检查的字符串</param>
|
||||||
@@ -184,4 +73,4 @@ namespace dy.net.utils
|
|||||||
return Regex.IsMatch(input, pattern, RegexOptions.None);
|
return Regex.IsMatch(input, pattern, RegexOptions.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user