From 25e2a22dd2498e655757fd444b39cdc6354d267a Mon Sep 17 00:00:00 2001 From: jianzhichu Date: Wed, 3 Dec 2025 23:43:10 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=88=A0=E9=99=A4=E8=A7=86=E9=A2=91?= =?UTF-8?q?=EF=BC=8C=E6=B0=B8=E4=B8=8D=E4=B8=8B=E8=BD=BD=202=E3=80=81?= =?UTF-8?q?=E5=8E=BB=E9=87=8D-=E4=BC=98=E5=85=88=E7=BA=A7=203=E3=80=81?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=90=88=E6=88=90=E4=BC=98=E5=8C=96=204?= =?UTF-8?q?=E3=80=81=E5=85=B6=E4=BB=96=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/VideoController.cs | 46 +- Program.cs | 12 +- .../PublishProfiles/FolderProfile.pubxml.user | 2 +- README.md | 14 +- app/src/pages/set/AppSet.vue | 51 +- app/src/pages/workplace/RecordTable.vue | 88 ++- app/src/store/coreapi.ts | 11 +- appsettings.json | 2 +- docs/appconf.png | Bin 0 -> 132308 bytes dto/JobConfig.cs | 65 ++- dto/VideoTypeEnum.cs | 28 +- job/DependencyJobListener.cs | 109 ++++ job/DouYinCollectSyncJob.cs | 20 +- job/DouYinFavoritSyncJob.cs | 21 +- job/DouyinBasicSyncJob.cs | 195 ++++--- job/DouyinFollowedUsersSyncJob.cs | 4 +- job/DouyinFollowedViedoSyncJob.cs | 23 +- model/{ViedoReDown.cs => DouyinReDownload.cs} | 2 +- model/DouyinVideo.cs | 5 + model/DouyinVideoDelete.cs | 24 + repository/DouyinFollowRepository.cs | 16 +- repository/DouyinVideoRepository.cs | 18 +- service/DouyinCommonService.cs | 83 ++- service/DouyinFollowService.cs | 6 +- service/DouyinHttpClientService.cs | 108 ++++ service/DouyinMergeVideoService.cs | 2 +- service/DouyinQuartzJobService.cs | 520 ++++++++++-------- service/DouyinVideoService.cs | 85 ++- utils/DouyinFileNameHelper.cs | 165 +----- 29 files changed, 1023 insertions(+), 702 deletions(-) create mode 100644 docs/appconf.png create mode 100644 job/DependencyJobListener.cs rename model/{ViedoReDown.cs => DouyinReDownload.cs} (96%) create mode 100644 model/DouyinVideoDelete.cs diff --git a/Controllers/VideoController.cs b/Controllers/VideoController.cs index 7204362..a2d67ac 100644 --- a/Controllers/VideoController.cs +++ b/Controllers/VideoController.cs @@ -4,6 +4,7 @@ using dy.net.service; using dy.net.utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; using Newtonsoft.Json; using System.Drawing.Printing; using System.Threading.Tasks; @@ -15,12 +16,12 @@ namespace dy.net.Controllers public class VideoController : ControllerBase { 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.douyinQuartzJobService = douyinQuartzJobService; + this.douyinCommonService = douyinCommonService; } /// /// 分页查询收藏视频 @@ -175,6 +176,7 @@ namespace dy.net.Controllers }; } + /// /// 重新下载 /// @@ -193,7 +195,6 @@ namespace dy.net.Controllers var result = await dyCollectVideoService.ReDownloadViedoAsync(dto); if (result) { - //douyinQuartzJobService.StartReDownJobOnceAsync();...无法实现。。逆向失败 return Ok(new { code = 0, data = true }); } else @@ -202,5 +203,42 @@ namespace dy.net.Controllers } } } + /// + /// 删除视频-不再下载 + /// + /// + /// + [HttpGet("vdelete/{vid}")] + public async Task 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 { 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 }); + } + + } + + } + } } } diff --git a/Program.cs b/Program.cs index 967cf78..d036a9d 100644 --- a/Program.cs +++ b/Program.cs @@ -1,15 +1,7 @@ -using ClockSnowFlake; -using dy.net.dto; using dy.net.extension; -using dy.net.model; using dy.net.service; using dy.net.utils; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.FileProviders; -using Microsoft.IdentityModel.Tokens; using Serilog; -using SqlSugar; -using System.Reflection; using System.Text; namespace dy.net @@ -204,8 +196,8 @@ ____/ _|_)_____/ _| _| \_|\____| commonService.UpdateCollectViedoType(); // òƷͬ״̬Ϊδͬ commonService.UpdateAllCookieSyncedToZero(); - - if(!isDevelopment) + + if (!isDevelopment) { // ʱ var quartzJobService = services.GetRequiredService(); diff --git a/Properties/PublishProfiles/FolderProfile.pubxml.user b/Properties/PublishProfiles/FolderProfile.pubxml.user index 0dba74a..387043c 100644 --- a/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\ - 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||; + 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||; diff --git a/README.md b/README.md index f3b2b65..319aa00 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取, | 镜像标签 | 架构 | | ----------------- | -------------- | -| `beta_1.7.1` | x86_64 (amd64) | -| `arm_1.7.1` | ARM64 | +| `beta_1.7.3` | x86_64 (amd64) | +| `arm_1.7.3` | ARM64 | ### 最新镜像查看 [镜像列表](http://nas.synology2023.online:10108/api/docker/dysync/1) @@ -112,7 +112,7 @@ docker run -d --restart=always \ -v /opt/dysync/uper:/app/uper \ -p 10103:10101 \ --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 指定 ``` @@ -126,7 +126,7 @@ version: '3.8' services: 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 # 容器名称 restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止 ports: @@ -167,6 +167,8 @@ services: ![输入图片说明](docs/set.png) +![输入图片说明](docs/appconf.png) + ![输入图片说明](docs/player.png) ![输入图片说明](docs/follows.png) @@ -189,7 +191,7 @@ services: 8. ✅ Cookie 过期提醒,在D音授权页可查看 -9. ✅ 重复视频去重(一个视频同时属于收藏视频、喜欢的视频或指定的博主作品) +9. ✅ 自动根据去重规则进行去重(可设置去重优先级,同一个视频出现再多个分类时适用) 10. ✅ 在同步记录页面中直接播放视频 @@ -199,4 +201,6 @@ services: 12. ✅ 关注列表支持新增,非关注的博主 +13. ✅ 增加永久删除功能,删除后,以后不会再同步该视频 + diff --git a/app/src/pages/set/AppSet.vue b/app/src/pages/set/AppSet.vue index 96d0102..e44a5bc 100644 --- a/app/src/pages/set/AppSet.vue +++ b/app/src/pages/set/AppSet.vue @@ -13,11 +13,11 @@ - - + +
- 每次同步获取的条数,范围10-30条,建议使用默认值 + 每次同步获取的条数,范围10-30条(建议使用默认值18,抖音默认的分页大小)
@@ -121,24 +121,7 @@

其他配置

- - -
- - 系统运行日志的保留天数,范围1-90天,过期自动清理 -
-
- - -
- - - 启用后,同一个视频,只会下载一次。(但是暂时不能决定保留哪个文件夹的) - -
-
- - +
@@ -150,8 +133,30 @@
+
+ + 同一个视频同时存在在多个类型的视频分类时,按照优先级保优先级最高的一个。 + 鼠标放到≡,点击鼠标左键即可拖拽调整优先级, +
+ + +
+ + 系统运行日志的保留天数,范围1-90天,过期自动清理 +
+
+ + @@ -197,7 +202,7 @@ const template_options = [ // 状态定义 const componentDisabled = ref(true); -const downImgVideo = ref(false); +const downImgVideo = ref(true); // 表单数据结构(新增 FullFollowedTitleTemplate 字段) interface FormState { @@ -315,7 +320,7 @@ const getConfig = () => { PriorityLevel: res.data.priorityLevel, }); - downImgVideo.value = res.data.downImageVideoFromEnv; + // downImgVideo.value = res.data.downImageVideoFromEnv; tagData.value = JSON.parse(res.data.priorityLevel); } else { message.error(res.erro || '获取配置失败', 8); diff --git a/app/src/pages/workplace/RecordTable.vue b/app/src/pages/workplace/RecordTable.vue index 0bb5d60..3391bbd 100644 --- a/app/src/pages/workplace/RecordTable.vue +++ b/app/src/pages/workplace/RecordTable.vue @@ -125,6 +125,10 @@ 分享 + + + 删除 + @@ -225,7 +229,7 @@ const columns = ref([ title: '同步类型', dataIndex: 'viedoTypeStr', align: 'center', - width: 150, + width: 120, }, { title: '博主', @@ -236,7 +240,7 @@ const columns = ref([ { title: '视频类型', dataIndex: 'viedoCate', - width: 300, + width: 200, align: 'center', }, { @@ -276,7 +280,7 @@ watch(isBatchMode, (isOpen) => { // 基础状态(优化:删除冗余的 datas 响应式数组) const loading = ref(false); -const showImageViedo = ref(false); +const showImageViedo = ref(true); const dataSource = ref([]); // 直接用 ref 数组存储表格数据,减少响应式嵌套 // 查询参数 @@ -306,7 +310,16 @@ const pagination = ref({ current: 1, defaultPageSize: 10, total: 0, + showSizeChanger: true, // 强制显示「每页显示数量」下拉框(关键修复) 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 = () => { - useApiStore() - .apiGetConfig() - .then((res) => { - if (res.code === 0) { - showImageViedo.value = res.data.downImageVideoFromEnv; - } else { - message.warning(`获取配置失败: ${res.message}`); - } - GetRecords(); - }) - .catch((error) => { - console.error('获取配置失败:', error); - message.error('获取配置失败,请稍后重试'); - }); -}; +// const getConfig = () => { +// useApiStore() +// .apiGetConfig() +// .then((res) => { +// if (res.code === 0) { +// showImageViedo.value = res.data.downImageVideoFromEnv; +// } else { +// message.warning(`获取配置失败: ${res.message}`); +// } +// GetRecords(); +// }) +// .catch((error) => { +// console.error('获取配置失败:', error); +// message.error('获取配置失败,请稍后重试'); +// }); +// }; /** 视频类型切换事件 */ const onViedoTypeChanged = () => { @@ -613,9 +626,9 @@ const handleBatchDelete = () => { } Modal.confirm({ - title: '确认删除', - content: `您确定要删除选中的 ${selectedRowKeys.value.length} 条视频数据吗?此操作不可撤销!`, - okText: '确认删除', + title: '确认重新下载吗', + content: `您确定要重新下载选中的 ${selectedRowKeys.value.length} 条视频数据吗?`, + okText: '确认重新下载', cancelText: '取消', okType: 'danger', 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) => { if (!path) { @@ -764,7 +805,8 @@ const copyVideoPath = (path?: string) => { // -------------------------- 页面初始化 -------------------------- onMounted(() => { - getConfig(); + // getConfig(); + GetRecords(); }); diff --git a/app/src/store/coreapi.ts b/app/src/store/coreapi.ts index 138d154..1bf1da3 100644 --- a/app/src/store/coreapi.ts +++ b/app/src/store/coreapi.ts @@ -210,6 +210,14 @@ export const useApiStore = defineStore('coreapi', () => { }); } + //删除 + async function DeleteVideo(param: string) { + return http.request>('/api/video/vdelete/' + param, 'get').then(r => { + return r.data; + }).finally(() => { + + }); + } //检查版本 @@ -260,6 +268,7 @@ export const useApiStore = defineStore('coreapi', () => { SyncFollow, OpenOrCloseSync, OpenOrCloseFullSync, - ReDownViedos + ReDownViedos, + DeleteVideo }; }); diff --git a/appsettings.json b/appsettings.json index 22e9313..5a78556 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,5 +1,5 @@ { "dbconn": "", "dbtype": "Sqlite", - "tagName": "dev_1.0" + "tagName": "beta_1.7.3" } \ No newline at end of file diff --git a/docs/appconf.png b/docs/appconf.png new file mode 100644 index 0000000000000000000000000000000000000000..af264733931402deb9075466652bf2dd22fa4f28 GIT binary patch literal 132308 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

ObaJDF_Pi{BS~ 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 /// 任务配置实体 ///

+ //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)); + // } + + // /// + // /// 任务类型 + // /// + // public Type JobType { get; } + + // /// + // /// 任务Key + // /// + // public string JobKey { get; } + + // /// + // /// 触发器Key + // /// + // public string TriggerKey { get; } + + // /// + // /// 任务描述 + // /// + // public string Description { get; } + //} + + 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) { - 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)); + JobType = jobType ?? throw new ArgumentNullException(nameof(jobType), "任务类型不能为空"); + JobKey = jobKey ?? throw new ArgumentNullException(nameof(jobKey), "任务Key不能为空"); + TriggerKey = triggerKey ?? throw new ArgumentNullException(nameof(triggerKey), "触发器Key不能为空"); + Description = description ?? throw new ArgumentNullException(nameof(description), "任务描述不能为空"); } - - /// - /// 任务类型 - /// - public Type JobType { get; } - - /// - /// 任务Key - /// - public string JobKey { get; } - - /// - /// 触发器Key - /// - public string TriggerKey { get; } - - /// - /// 任务描述 - /// - public string Description { get; } } } diff --git a/dto/VideoTypeEnum.cs b/dto/VideoTypeEnum.cs index 52a0878..393c255 100644 --- a/dto/VideoTypeEnum.cs +++ b/dto/VideoTypeEnum.cs @@ -4,24 +4,26 @@ namespace dy.net.dto { public enum VideoTypeEnum { + /// + ///喜欢的 + /// [Description("喜欢的")] - Favorite = 1, + dy_favorite = 1, + /// + ///收藏的 + /// [Description("收藏的")] - Collect = 2, + dy_collects = 2, + /// + /// 关注的 + /// [Description("关注的")] - UperPost = 3, + dy_follows = 3, + /// + /// 图文视频,无用了,但是不能删... + /// [Description("图片视频")] ImageVideo = 4 } - - public enum QuartzJobTypeEnum - { - [Description("[Favorite]")] - Favorite = 1, - [Description("[Collect]")] - Collect = 2, - [Description("[Followed]")] - Followed = 3 - } } diff --git a/job/DependencyJobListener.cs b/job/DependencyJobListener.cs new file mode 100644 index 0000000..2d063b5 --- /dev/null +++ b/job/DependencyJobListener.cs @@ -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 +{ /// + /// 抖音任务依赖监听器(独立公共类) + /// 作用:监听任务执行完成事件,触发下一个依赖任务,实现顺序执行 + /// + public class DouyinJobDependencyListener : IJobListener + { + // 监听器名称(唯一标识,不可重复) + public string Name => "DouyinJobDependencyListener"; + + /// + /// 任务配置字典(从外部注入) + /// + private readonly Dictionary _jobConfigs; + + /// + /// 任务依赖关系(从外部注入,定义执行顺序) + /// + private readonly Dictionary _jobDependency; + + /// + /// 任务服务(用于触发下一个任务,从外部注入) + /// + private readonly DouyinQuartzJobService _jobService; + + /// + /// 构造函数(依赖注入) + /// + /// 任务配置 + /// 任务依赖关系 + /// 任务服务 + public DouyinJobDependencyListener( + Dictionary jobConfigs, + Dictionary jobDependency, + DouyinQuartzJobService jobService) + { + _jobConfigs = jobConfigs ?? throw new ArgumentNullException(nameof(jobConfigs), "任务配置不能为空"); + _jobDependency = jobDependency ?? throw new ArgumentNullException(nameof(jobDependency), "任务依赖关系不能为空"); + _jobService = jobService ?? throw new ArgumentNullException(nameof(jobService), "任务服务不能为空"); + } + + /// + /// 任务执行前触发(无需处理) + /// + public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + /// + /// 任务被否决执行时触发(无需处理) + /// + public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + /// + /// 任务执行完成后触发(核心逻辑:触发下一个依赖任务) + /// + 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); + } + } + } +} diff --git a/job/DouYinCollectSyncJob.cs b/job/DouYinCollectSyncJob.cs index 2f028e0..3a54028 100644 --- a/job/DouYinCollectSyncJob.cs +++ b/job/DouYinCollectSyncJob.cs @@ -15,9 +15,8 @@ namespace dy.net.job DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService douyinMergeVideoService) : base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService,douyinFollowService, douyinMergeVideoService) { } - protected override string JobType => SystemStaticUtil.DY_COLLECTS; - - protected override VideoTypeEnum VideoType => VideoTypeEnum.Collect; + + protected override VideoTypeEnum VideoType => VideoTypeEnum.dy_collects; protected override async Task BeforeProcessCookies() { @@ -61,34 +60,31 @@ namespace dy.net.job 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) { - Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频"); + Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频"); cookie.CollHasSyncd = 1; await douyinCookieService.UpdateAsync(cookie); } 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.Collect, - }; + return new VideoEntityDifferences(); } protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed) { var (tag1, _, _) = GetVideoTags(item); - var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizePath(tag1); - var folder = Path.Combine(cookie.SavePath, safeTag1, $"{DouyinFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}"); + var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizeLinuxFileName(tag1); + var folder = Path.Combine(cookie.SavePath, safeTag1, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}@{item.AwemeId}"); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); return folder; } diff --git a/job/DouYinFavoritSyncJob.cs b/job/DouYinFavoritSyncJob.cs index ab7e71b..59b1ec4 100644 --- a/job/DouYinFavoritSyncJob.cs +++ b/job/DouYinFavoritSyncJob.cs @@ -17,9 +17,8 @@ namespace dy.net.job DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService 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> GetValidCookies() { @@ -54,33 +53,25 @@ namespace dy.net.job 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) { - Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频"); + Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频"); cookie.FavHasSyncd = 1; await douyinCookieService.UpdateAsync(cookie); } 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) { var (tag1, _, _) = GetVideoTags(item); - var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizePath(tag1); - var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{DouyinFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}"); + var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizeLinuxFileName(tag1); + var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}@{item.AwemeId}"); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); return folder; } diff --git a/job/DouyinBasicSyncJob.cs b/job/DouyinBasicSyncJob.cs index d267c78..f061354 100644 --- a/job/DouyinBasicSyncJob.cs +++ b/job/DouyinBasicSyncJob.cs @@ -82,7 +82,7 @@ namespace dy.net.job /// 任务类型名称,用于日志记录和区分不同的同步任务 /// 子类必须实现此属性并返回具体的任务类型 /// - protected abstract string JobType { get; } + //protected abstract string VideoType { get; } protected abstract VideoTypeEnum VideoType { get; } @@ -131,14 +131,13 @@ namespace dy.net.job var config = douyinCommonService.GetConfig(); if (config == null) { - Log.Debug($"{JobType}-未获取到系统配置,任务终止!!!"); + Log.Debug($"{VideoType}-未获取到系统配置,任务终止!!!"); return; } - - // 2. 从配置中获取每页请求数量 - if (config.BatchCount > 0) - count = config.BatchCount.ToString(); + // 2. 从配置中获取每页请求数量--固定18 + //if (config.BatchCount > 0) + // count = config.BatchCount.ToString(); // 3. 在处理Cookie之前执行的预处理操作 await BeforeProcessCookies(); @@ -147,11 +146,11 @@ namespace dy.net.job var cookies = await GetValidCookies(); if (cookies == null || !cookies.Any()) { - Log.Debug($"{JobType}-无有效Cookie,任务终止!!!"); + Log.Debug($"{VideoType}-无有效Cookie,任务终止!!!"); return; } - Log.Debug($"{JobType}-共发现{cookies.Count}个Cookie,同步任务即将开始..."); + Log.Debug($"{VideoType}-共发现{cookies.Count}个Cookie,同步任务即将开始..."); // 6. 遍历每个有效的Cookie,执行同步操作 foreach (var cookie in cookies) @@ -262,8 +261,9 @@ namespace dy.net.job /// /// 用户Cookie /// 本次同步成功的视频数量 + /// /// 一个表示异步操作的任务 - protected abstract Task HandleSyncCompletion(DouyinCookie cookie, int syncCount); + protected abstract Task HandleSyncCompletion(DouyinCookie cookie, int syncCount,DouyinFollowed followed=null); /// /// 获取视频实体的差异信息 @@ -272,7 +272,10 @@ namespace dy.net.job /// 用户Cookie /// 视频信息 /// 视频实体的差异信息,包含视频类型和简化标题 - protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item); + protected virtual VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item) + { + return new VideoEntityDifferences(); + } /// /// 获取NFO文件中的图片(如海报)文件名 @@ -302,13 +305,13 @@ namespace dy.net.job // 检查Cookie是否有效 if (!IsCookieValid(cookie)) { - Log.Debug($"{JobType}-Cookie[{cookie.UserName}]无效,任务终止!!!"); + Log.Debug($"{VideoType}-Cookie[{cookie.UserName}]无效,任务终止!!!"); return; } - Log.Debug($"{JobType}- Cookie-[{cookie.UserName}]开始同步..."); + Log.Debug($"{VideoType}- Cookie-[{cookie.UserName}]开始同步..."); //up主上传视频特殊处理 - if (JobType == SystemStaticUtil.DY_FOLLOWEDS) + if (VideoType ==VideoTypeEnum.dy_follows) { int syncCount = 0; // 本次同步成功的视频数量 string cursor = "0"; // 初始游标 @@ -324,18 +327,18 @@ namespace dy.net.job } if (string.IsNullOrWhiteSpace(firstUp.SecUid)) { - Log.Debug($"{JobType}-Cookie[{firstUp.UperName}]无效,没有sec_userid,任务终止!!!"); + Log.Debug($"{VideoType}-Cookie[{firstUp.UperName}]无效,没有sec_userid,任务终止!!!"); return; } - foreach (var item in follows) + foreach (var followed in follows) { cursor = "0"; // 初始游标 - await GetViedos(cookie, config, syncCount, cursor, hasMore, item); + await GetViedos(cookie, config, syncCount, cursor, hasMore, followed); hasMore = true; // 处理同步完成后的操作 - await HandleSyncCompletion(cookie, syncCount); + await HandleSyncCompletion(cookie, syncCount,followed); } } else @@ -353,7 +356,7 @@ namespace dy.net.job } 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); if (data == null) { - Log.Debug($"{JobType}-Cookie[{cookie.UserName}]读取数据失败!!!"); + Log.Debug($"{VideoType}-Cookie[{cookie.UserName}]读取数据失败!!!"); break; } @@ -387,7 +390,7 @@ namespace dy.net.job //当syncCount达到上限时,跳出循环 if (config.BatchCount > 0 && syncCount >= config.BatchCount) { - Log.Debug($"{JobType}-Cookie[{cookie.UserName}]本次同步达到上限{config.BatchCount},停止同步!!!"); + Log.Debug($"{VideoType}-Cookie[{cookie.UserName}]本次同步达到上限{config.BatchCount},停止同步!!!"); break; } @@ -412,12 +415,28 @@ namespace dy.net.job var videos = new List(); 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) { continue; } + + if (exitVideo!=null) { + await douyinVideoService.DeleteById(exitVideo.Id); + } var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId); if (uper != null) { @@ -442,13 +461,11 @@ namespace dy.net.job return videos; } - private async Task AutoDistinct(AppConfig config, Aweme item) + private async Task AutoDistinct(AppConfig config, Aweme item,DouyinCookie cookie,DouyinVideo exitVideo) { // 去重,检查视频是否已存在(按优先级下载) if (config.AutoDistinct) { - // 1. 查询数据库中是否已存在该视频(通过 AwemeId 唯一标识) - var exitVideo = await douyinVideoService.GetByAwemeId(item.AwemeId); if (exitVideo != null) { // 2. 已存在视频:先判断本地文件是否存在 @@ -465,7 +482,7 @@ namespace dy.net.job } catch (Exception ex) { - Log.Error($"解析优先级配置失败:{ex.Message}", ex); + Log.Error($"{VideoType}-解析优先级配置失败:{ex.Message}", ex); priLevs = new List(); // 解析失败则用默认优先级 } } @@ -497,23 +514,24 @@ namespace dy.net.job if (exitVideoType == currentVideoType) { // 已存在同优先级视频 → 跳过下载(避免重复) - Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(同最高优先级),跳过"); + //Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(同最高优先级),跳过"); return false; } else { // 已存在「低优先级」视频 → 替换(删除旧文件,继续下载新的最高优先级视频) - Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(低优先级:{exitVideoType}),替换为最高优先级:{currentVideoType}"); + //Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在(低优先级:{exitVideoType}),替换为最高优先级:{currentVideoType}"); // 删除旧的低优先级文件(可选:也可保留备份,根据需求调整) try { - File.Delete(exitVideo.VideoSavePath); - Log.Debug($"已删除旧文件:{exitVideo.VideoSavePath}"); + //File.Delete(exitVideo.VideoSavePath); + DeleteOldViedo(config, exitVideo); + //Log.Debug($"已删除旧文件:{exitVideo.VideoSavePath}"); } 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) { // 已存在「最高优先级」视频 → 跳过(不替换最高优先级) - Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在最高优先级视频({maxPriorityType}),当前类型({currentVideoType})优先级低,跳过"); + //Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在最高优先级视频({maxPriorityType}),当前类型({currentVideoType})优先级低,跳过"); return false; } else @@ -539,39 +557,70 @@ namespace dy.net.job 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)) - { - File.Delete(exitVideo.VideoSavePath); - } - + DeleteOldViedo(config, exitVideo); // 继续下载 } else { // 当前类型优先级更低或相等 → 跳过 - Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在更高/同等优先级视频({exitVideoType}),当前类型({currentVideoType})跳过"); + //Log.Debug($"{VideoType}-视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]已存在更高/同等优先级视频({exitVideoType}),当前类型({currentVideoType})跳过"); 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)) { - // 数据库存在记录,但本地文件丢失 → 直接重新下载(无论优先级) - Log.Debug($"视频-{exitVideo.AwemeId}-[{exitVideo.VideoTitle}]数据库存在但本地文件丢失,重新下载"); + File.Delete(nfoPath); + //删除同名的封面文件 + //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 { - // 数据库和本地均无该视频 → 直接下载 - //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; } /// @@ -609,22 +658,22 @@ namespace dy.net.job // 如果文件已存在,跳过 if (File.Exists(savePath)) { - //Log.Debug($"{JobType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]已存在,跳过下载."); + //Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]已存在,跳过下载."); return null; } - Log.Debug($"{JobType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]开始下载..."); + Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}]开始下载..."); // 随机延迟,模拟人类操作 await Task.Delay(_random.Next(1, 4) * 1000); // 下载视频 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; } 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)) { - Log.Error($"{JobType}-图文视频同步-没有配置图片存储路径,任务终止!!!"); + Log.Error($"{VideoType}-图文视频同步-没有配置图片存储路径,任务终止!!!"); 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 { @@ -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); if (!mergeResult) { - Log.Error($"{JobType}-图文视频-[{DouyinFileNameHelper.SanitizePath(item.Desc)}]合成失败!!!"); + Log.Error($"{VideoType}-图文视频-[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc)}]合成失败!!!"); return null; } @@ -722,13 +771,13 @@ namespace dy.net.job // 检查合成后的视频文件是否有效 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)) { File.Delete(savePath); Directory.Delete(fileNamefolder, true); - Log.Error($"{JobType}-图文视频-删除合成失败的视频文件和目录..."); + Log.Error($"{VideoType}-图文视频-删除合成失败的视频文件和目录..."); } return null; } @@ -765,13 +814,14 @@ namespace dy.net.job // 特殊处理合成视频的字段 videoEntity.FileHash = string.Empty; // 合成视频没有原始文件哈希 videoEntity.VideoUrl = "/"; // 合成视频没有原始URL - videoEntity.ViedoType = VideoTypeEnum.ImageVideo; // 标记为图片合成视频 + videoEntity.ViedoType = VideoType; + videoEntity.IsMergeVideo = 1;// 标记为图片合成视频 return videoEntity; } catch (Exception ex) { - Log.Error(ex, $"{JobType}-图片视频同步-处理图片集并合成视频时出错"); + Log.Error(ex, $"{VideoType}-图片视频同步-处理图片集并合成视频时出错"); return null; } } @@ -788,30 +838,11 @@ namespace dy.net.job try { 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; } catch (Exception ex) { - Log.Error(ex, $"{JobType}-批量保存视频到数据库失败"); + Log.Error(ex, $"{VideoType}-批量保存视频到数据库失败"); // 清理保存失败的视频文件 await CleanupFailedVideos(videos); return 0; @@ -938,7 +969,7 @@ namespace dy.net.job /// 一个表示异步操作的任务 private async Task CleanupFailedVideos(List videos) { - Log.Debug($"{JobType}-数据库保存失败,开始清理本次下载的视频目录..."); + Log.Debug($"{VideoType}-数据库保存失败,开始清理本次下载的视频目录..."); foreach (var video in videos) { @@ -956,12 +987,12 @@ namespace dy.net.job { Directory.Delete(directory); } - Log.Debug($"{JobType}-清理失败视频文件成功: {video.VideoSavePath}!!!"); + Log.Debug($"{VideoType}-清理失败视频文件成功: {video.VideoSavePath}!!!"); } 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 { - ViedoType = diffs.VideoType, + ViedoType = VideoType, AwemeId = item.AwemeId, Author = item.Author?.Nickname, AuthorId = item.Author?.Uid, @@ -1053,7 +1084,7 @@ namespace dy.net.job /// /// 视频类型 /// - public VideoTypeEnum VideoType { get; set; } + //public VideoTypeEnum VideoType { get; set; } /// /// 简化的视频标题 diff --git a/job/DouyinFollowedUsersSyncJob.cs b/job/DouyinFollowedUsersSyncJob.cs index ccc8fe5..9dab692 100644 --- a/job/DouyinFollowedUsersSyncJob.cs +++ b/job/DouyinFollowedUsersSyncJob.cs @@ -84,10 +84,10 @@ namespace dy.net.job 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}人。"); } } } diff --git a/job/DouyinFollowedViedoSyncJob.cs b/job/DouyinFollowedViedoSyncJob.cs index d06641c..169a226 100644 --- a/job/DouyinFollowedViedoSyncJob.cs +++ b/job/DouyinFollowedViedoSyncJob.cs @@ -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> GetValidCookies() { @@ -56,7 +55,7 @@ namespace dy.net.job protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed) { #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); if (!string.IsNullOrWhiteSpace(followed.SavePath)) { @@ -71,7 +70,7 @@ namespace dy.net.job } 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 fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName; return Path.Combine(folder, fileNameFolder); @@ -115,7 +114,7 @@ namespace dy.net.job string fileName; 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; fileName = string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{Format}" : $"{existingName}.{Format}"; } @@ -130,11 +129,11 @@ namespace dy.net.job Id = item.AwemeId, ReleaseTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime), Resolution = $"{Width}×{Height}", - VideoTitle = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId), + VideoTitle = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId), Author = item.Author.Nickname }); - fileName= $"{DouyinFileNameHelper.SanitizePath(fullName)}.{Format}"; + fileName= $"{DouyinFileNameHelper.SanitizeLinuxFileName(fullName)}.{Format}"; } else { @@ -172,17 +171,17 @@ namespace dy.net.job 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) { - Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频"); + Serilog.Log.Debug($"{VideoType}-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频"); cookie.UperSyncd = 1; await douyinCookieService.UpdateAsync(cookie); } 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) { - simplifiedTitle = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId); + simplifiedTitle = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId); } - return new VideoEntityDifferences { - VideoType = VideoTypeEnum.UperPost, VideoTitleSimplify = simplifiedTitle }; } diff --git a/model/ViedoReDown.cs b/model/DouyinReDownload.cs similarity index 96% rename from model/ViedoReDown.cs rename to model/DouyinReDownload.cs index 0b3658d..8d41fa5 100644 --- a/model/ViedoReDown.cs +++ b/model/DouyinReDownload.cs @@ -6,7 +6,7 @@ namespace dy.net.model /// 重新下载的列表 /// [SugarTable(TableName = "dy_rd_video")] - public class ViedoReDown + public class DouyinReDownload { /// /// diff --git a/model/DouyinVideo.cs b/model/DouyinVideo.cs index cf6bb89..ff22f79 100644 --- a/model/DouyinVideo.cs +++ b/model/DouyinVideo.cs @@ -139,6 +139,11 @@ namespace dy.net.model [SugarColumn(Length =200,IsNullable =true)] public VideoTypeEnum ViedoType { get; set; } + /// + /// 是否合成视频 + /// + public int IsMergeVideo { get; set; } + [SugarColumn(IsIgnore = true)] public string ViedoTypeStr => ViedoType.GetDescription(); diff --git a/model/DouyinVideoDelete.cs b/model/DouyinVideoDelete.cs new file mode 100644 index 0000000..c8acff2 --- /dev/null +++ b/model/DouyinVideoDelete.cs @@ -0,0 +1,24 @@ +using SqlSugar; + +namespace dy.net.model +{ + /// + /// 永久删除的视频记录-不再下载 + /// + [SugarTable(TableName = "dy_delete_video")] + public class DouyinVideoDelete + { + /// + /// + /// + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } + /// + /// 原视频Id + /// + [SugarColumn(IsNullable =true,Length =50)] + public string ViedoId { get; set; } + + public DateTime DeleteTime { get; set; } + } +} diff --git a/repository/DouyinFollowRepository.cs b/repository/DouyinFollowRepository.cs index 537e375..359dde5 100644 --- a/repository/DouyinFollowRepository.cs +++ b/repository/DouyinFollowRepository.cs @@ -93,11 +93,11 @@ namespace dy.net.repository /// /// /// - public async Task Sync(List followInfos, string myselfUserId) + public async Task Sync(List followInfos, DouyinCookie ck) { // 基础参数校验 if (followInfos == null) followInfos = new List(); - if (string.IsNullOrWhiteSpace(myselfUserId)) + if (ck==null || string.IsNullOrWhiteSpace(ck.MyUserId)) { Serilog.Log.Error("同步关注列表失败:当前用户ID为空"); return false; @@ -107,7 +107,7 @@ namespace dy.net.repository { // 1. 查询现有关注列表 List existFollows = await Db.Queryable() - .Where(x => x.mySelfId == myselfUserId) + .Where(x => x.mySelfId == ck.MyUserId) .Where(x=>!x.IsNoFollowed) //排除手动添加但未关注的用户 .ToListAsync() ?? new List(); @@ -161,7 +161,7 @@ namespace dy.net.repository Id = IdGener.GetLong().ToString(), Enterprise = follow.EnterpriseVerifyReason, LastSyncTime = DateTime.UtcNow, - mySelfId = myselfUserId, + mySelfId = ck.MyUserId, SecUid = follow.SecUid, OpenSync = false, UperAvatar = follow.Avatar?.UrlList?.FirstOrDefault() ?? "", @@ -201,8 +201,6 @@ namespace dy.net.repository Serilog.Log.Error("同步关注列表失败:关注信息更新异常"); return false; } - - Serilog.Log.Debug($"同步关注列表:成功更新{toUpdateFollows.Count}条关注信息(用户ID:{myselfUserId})"); } // 6. 分批处理删除(单批200条) @@ -212,7 +210,7 @@ namespace dy.net.repository async batch => { 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; }); @@ -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; } catch (Exception ex) { - Serilog.Log.Error(ex, $"同步关注列表失败(用户ID:{myselfUserId}):{ex.Message}"); + Serilog.Log.Error(ex, $"同步关注列表失败({ck.UserName}):{ex.Message}"); return false; } } diff --git a/repository/DouyinVideoRepository.cs b/repository/DouyinVideoRepository.cs index 2005f6a..f640801 100644 --- a/repository/DouyinVideoRepository.cs +++ b/repository/DouyinVideoRepository.cs @@ -33,10 +33,11 @@ namespace dy.net.repository VideoTypeEnum? enumviedoType = null; - if (!string.IsNullOrEmpty(dto.ViedoType) && dto.ViedoType != "*") + if (!string.IsNullOrEmpty(dto.ViedoType) && dto.ViedoType != "*" && dto.ViedoType != "4") { enumviedoType = dto.ViedoType.ToVideoTypeEnum(); } + var where = this.Db.Queryable() //.WhereIF(!string.IsNullOrWhiteSpace(title), x => x.VideoTitle.Contains(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(start2.HasValue, x => x.CreateTime >= start2.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(); @@ -90,7 +92,7 @@ namespace dy.net.repository public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify) { - var video = await this.Db.Queryable().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.UperPost) + var video = await this.Db.Queryable().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.dy_follows) .Where(x => x.VideoTitleSimplify == ViedoNameSimplify) .OrderByDescending(x => x.CreateTime).FirstAsync(); @@ -140,7 +142,7 @@ namespace dy.net.repository /// /// /// - public bool InsertReDowns(List downs) + public bool InsertReDowns(List downs) { if (downs != null) { @@ -169,9 +171,9 @@ namespace dy.net.repository } // 构建更新条件(统一Where条件,避免重复代码) - var updateable = Db.Updateable() + var updateable = Db.Updateable() .Where(it => it.Id == videoId) - .SetColumns(it => new ViedoReDown + .SetColumns(it => new DouyinReDownload { Status = status, UpdateTime = DateTime.Now @@ -197,9 +199,9 @@ namespace dy.net.repository /// /// /// - public async Task> GetViedoReDowns() + public async Task> GetViedoReDowns() { - return await this.Db.Queryable() + return await this.Db.Queryable() .Where(x => x.Status == 0 || x.Status == 2) .ToListAsync(); } diff --git a/service/DouyinCommonService.cs b/service/DouyinCommonService.cs index 47feaf5..c6596a6 100644 --- a/service/DouyinCommonService.cs +++ b/service/DouyinCommonService.cs @@ -49,7 +49,7 @@ namespace dy.net.service LogKeepDay = 10, UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹 UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名 - DownImageVideo = false,//默认不下载图文视频 + DownImageVideo = true,//默认下载图文视频 DownMp3 = false, DownImage = false, ImageViedoSaveAlone = true, @@ -99,18 +99,18 @@ namespace dy.net.service } /// - /// 兼容旧版将之前同步了的我收藏的数据进行更新 + /// 初始化一些数据,兼容旧版数据结构。 /// public void UpdateCollectViedoType() { - + //更新视频类型字段-兼容老版本 string sql = @"UPDATE dy_collect_video SET ViedoType = CASE WHEN ViedoType = '0' THEN 0 WHEN ViedoType = '1' THEN 1 WHEN ViedoType = '2' THEN 2 WHEN ViedoType = '3' THEN 3 - WHEN ViedoType = '4' THEN 4 + WHEN ViedoType = '4' THEN 4 ELSE NULL END;"; @@ -119,16 +119,53 @@ namespace dy.net.service //更新关注表的IsNoFollowed字段为空的数据为0--兼容老版本-新加的字段 string followUpdateSql = @"Update dy_follow SET IsNoFollowed=0 WHERE IsNoFollowed is NULL"; sqlSugarClient.Ado.ExecuteCommand(followUpdateSql); - //var collectViedos = sqlSugarClient.Queryable().ToList(); - //if (collectViedos.Any()) - //{ - // collectViedos.ForEach(x => - // { - // x.ViedoType = x.; - // }); - // sqlSugarClient.Updateable(collectViedos).ExecuteCommand(); - //} + //更新图片视频的合并状态 + string updateIsMergeVideoSql = @"UPDATE dy_collect_video SET IsMergeVideo = 1 WHERE IsMergeVideo IS NULL and ViedoType='4';"; + sqlSugarClient.Ado.ExecuteCommand(updateIsMergeVideoSql); + + //更新非图片视频的合并状态 + string updateNoIsMergeVideoSql = @"UPDATE dy_collect_video SET IsMergeVideo = 0 WHERE IsMergeVideo IS NULL and ViedoType<>'4';"; + sqlSugarClient.Ado.ExecuteCommand(updateNoIsMergeVideoSql); + + //强制开启去重 + sqlSugarClient.Updateable().SetColumns(x => new AppConfig { AutoDistinct = true }).Where(x => !string.IsNullOrWhiteSpace(x.Id)).ExecuteCommand(); + //重新根据保存路径更新图片视频的类型为喜欢,收藏或关注 + var collectViedos = sqlSugarClient.Queryable().Where(x=>x.ViedoType==VideoTypeEnum.ImageVideo).ToList(); + if (collectViedos != null && collectViedos.Any()) + { + var cookies= sqlSugarClient.Queryable().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(); + } } /// /// 重置所有Cookie的同步状态为0 @@ -148,18 +185,28 @@ namespace dy.net.service } /// - /// 查询需要下载的所有重下载视频记录 + /// 查询是否已删除 /// + /// /// - public async Task> GetAllRedown() + public async Task ExistDeleteVideo(string videoId) { - return await sqlSugarClient.Queryable().Where(x => x.Status == 0 || x.Status == 2).ToListAsync(); + var count= await sqlSugarClient.Queryable().Where(x=>x.ViedoId==videoId).CountAsync(); + return count > 0; } - public async Task UpdateRedownStatus(List list) + /// + /// 新增要删除的视频 + /// + /// + /// + public async Task 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 测试创建数据库 ///// diff --git a/service/DouyinFollowService.cs b/service/DouyinFollowService.cs index 091291f..64935e0 100644 --- a/service/DouyinFollowService.cs +++ b/service/DouyinFollowService.cs @@ -55,11 +55,11 @@ namespace dy.net.service /// /// /// - /// + /// /// - public async Task Sync(List followInfos, string myselfUserId) + public async Task Sync(List followInfos, DouyinCookie ck) { - return await _followRepository.Sync(followInfos, myselfUserId); + return await _followRepository.Sync(followInfos, ck); } public async Task GetByUperId(string uperId,string myUid) diff --git a/service/DouyinHttpClientService.cs b/service/DouyinHttpClientService.cs index 1bbcd00..8fd0376 100644 --- a/service/DouyinHttpClientService.cs +++ b/service/DouyinHttpClientService.cs @@ -15,11 +15,14 @@ namespace dy.net.service public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme"; // 随机数生成器(避免重复实例化,保证随机性) private readonly IHttpClientFactory _clientFactory; + // 下载信号量锁:初始计数1,最大并发1(同时只能一个下载任务) + private readonly SemaphoreSlim _downloadSemaphore = new SemaphoreSlim(1, 1); public DouyinHttpClientService(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } + /// /// 查询用户收藏的视频 /// @@ -371,6 +374,111 @@ namespace dy.net.service } } + /// + /// 下载文件并保存到本地(支持重试机制+单线程限制,同时只能一个下载) + /// + /// 文件地址 + /// 保存路径 + /// 请求Cookie + /// HttpClient名称(默认"dy_down1") + /// 取消令牌(用于终止任务) + /// 流读取超时时间(默认60秒) + /// 最大重试次数(默认3次) + /// 初始重试延迟(默认1秒,指数退避) + /// 是否下载成功 + //public async Task 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; + // } + // } + //} + + /// /// 单次下载尝试(核心下载逻辑) /// diff --git a/service/DouyinMergeVideoService.cs b/service/DouyinMergeVideoService.cs index 2d49c98..4f23213 100644 --- a/service/DouyinMergeVideoService.cs +++ b/service/DouyinMergeVideoService.cs @@ -370,7 +370,7 @@ namespace dy.net.service // 延迟清理:给 FFmpeg 进程足够时间释放文件句柄(1秒) await Task.Delay(1000); Directory.Delete(tempDir, recursive: true); - Log.Debug($"临时目录已清理:{tempDir}"); + //Log.Debug($"临时目录已清理:{tempDir}"); } catch (Exception ex) { diff --git a/service/DouyinQuartzJobService.cs b/service/DouyinQuartzJobService.cs index ad19246..5f6b98d 100644 --- a/service/DouyinQuartzJobService.cs +++ b/service/DouyinQuartzJobService.cs @@ -1,6 +1,7 @@ using dy.net.dto; using dy.net.job; using Quartz; +using Quartz.Impl.Matchers; using Serilog; using System; using System.Threading.Tasks; @@ -13,242 +14,21 @@ namespace dy.net.service public class DouyinQuartzJobService { private readonly ISchedulerFactory _schedulerFactory; - private const string DefaultJobGroup = "group1"; + private const string DefaultJobGroup = "dysync.net"; private const int DefaultIntervalMinutes = 30; private const int DefaultCronStartDelaySeconds = 30; private const int DefaultSimpleStartDelaySeconds = 3; - - - public DouyinQuartzJobService(ISchedulerFactory schedulerFactory) + // 任务顺序依赖配置(核心:定义执行顺序,供 Listener 使用) + public Dictionary JobDependency { get; } = new() { - _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory)); - } + {"collect", "favorite"}, // collect 执行完 → 触发 favorite + {"favorite", "followed"}, // favorite 执行完 → 触发 followed + {"followed", null}, // followed 执行完 → 一轮任务结束 + }; - /// - /// 启动所有抖音相关定时任务 - /// - /// Cron表达式或间隔分钟数 - /// 任务之间的启动延迟(毫秒) - /// 是否启动成功 - public async Task InitOrReStartAllJobs(string expression, int delayBetweenJobs = 5000) - { - if (string.IsNullOrWhiteSpace(expression)) - { - Log.Warning("定时任务表达式为空,使用默认配置"); - expression = DefaultIntervalMinutes.ToString(); - } - - // 按顺序启动任务,避免并发 - var jobTasks = new List> - { - //关注列表 - //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); - } - - /// - /// 启动关注同步任务(单次执行) - /// - /// 是否启动成功 - public async Task StartFollowJobOnceAsync() - { - return await StartOneTimeJobAsync("follow_user_once"); - } - - - /// - /// 延迟后启动任务 - /// - private async Task DelayAndStartJobAsync(string jobKey, string expression, int delayMs) - { - if (delayMs > 0) - { - await Task.Delay(delayMs); - } - - // 如果是数字表达式,自动递增避免并发 - var adjustedExpression = AdjustExpressionForConcurrency(jobKey, expression); - return await StartJobAsync(jobKey, adjustedExpression); - } - - /// - /// 调整任务表达式以避免并发 - /// - 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(); - } - - /// - /// 启动指定定时任务 - /// - private async Task 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; - } - } - - /// - /// 启动单次执行任务 - /// - private async Task 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; - } - } - - /// - /// 创建触发器(支持Cron表达式和简单间隔) - /// - 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(); - } - - /// - /// 移除已存在的任务 - /// - private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey) - { - if (await scheduler.CheckExists(jobKey)) - { - Log.Information("移除已存在的任务: {JobKey}", jobKey); - await scheduler.DeleteJob(jobKey); - } - } - /// - /// 任务配置信息 - /// - private readonly Dictionary _jobConfigs = new() + // 任务配置信息(保持原有配置不变,改为 public 供 Listener 访问) + public Dictionary JobConfigs { get; } = new() { { "collect", @@ -267,11 +47,11 @@ namespace dy.net.service "抖音点赞同步任务") }, { - "uper", + "followed", new JobConfig( typeof(DouyinFollowedViedoSyncJob), - "dy.job.key.uper", - "dy.trigger.key.uper", + "dy.job.key.followed", + "dy.trigger.key.followed", "抖音UP主作品同步任务") }, { @@ -290,16 +70,270 @@ namespace dy.net.service "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)); + } + + /// + /// 启动所有抖音相关定时任务(顺序执行模式) + /// + /// Cron表达式或间隔分钟数(控制整个链条的执行频率) + /// 是否启动成功 + public async Task 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; + } + } + + /// + /// 启动关注同步任务(单次执行) + /// + public async Task StartFollowJobOnceAsync() + { + return await StartOneTimeJobAsync("follow_user_once"); + } + + /// + /// 注册独立的 JobListener(核心步骤) + /// + private async Task RegisterJobListener(IScheduler scheduler) + { + // 创建独立的 Listener 实例,注入依赖(任务配置、依赖关系、当前服务) + var dependencyListener = new DouyinJobDependencyListener( + JobConfigs, // 任务配置 + JobDependency, // 依赖顺序 + this // 任务服务(用于触发下一个任务) + ); + + // 注册 Listener:仅监听 DefaultJobGroup 分组的任务(精准匹配,避免影响其他任务) + scheduler.ListenerManager.AddJobListener( + dependencyListener, + GroupMatcher.GroupEquals(DefaultJobGroup) + ); + + Log.Information("【任务服务】JobListener 注册成功:{ListenerName}", dependencyListener.Name); + } + + /// + /// 移除所有已存在的任务(避免重复调度) + /// + 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); + } + } + } + + /// + /// 启动指定定时任务(public 修饰,供 Listener 调用) + /// + /// 任务配置Key(如:collect、favorite) + /// 定时表达式(依赖触发时传空) + /// 是否为依赖触发(true=立即执行,false=定时执行) + /// 是否启动成功 + public async Task 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; + } + } + + /// + /// 启动单次执行任务(保持原有逻辑不变) + /// + private async Task 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; + } + } + + /// + /// 创建「定时触发器」(按表达式执行,仅第一个任务使用) + /// + 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(); + } + + /// + /// 创建「依赖触发器」(立即执行,仅执行一次) + /// + private ITrigger CreateDependencyTrigger(TriggerKey triggerKey, string jobDescription) + { + return TriggerBuilder.Create() + .WithIdentity(triggerKey) + .WithDescription($"{jobDescription} - 依赖触发(立即执行)") + .StartNow() // 立即触发 + .Build(); + } + + /// + /// 移除已存在的任务(保持原有逻辑不变) + /// + private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey) + { + if (await scheduler.CheckExists(jobKey)) + { + Log.Information("【任务服务】移除已存在的任务: {JobKey}", jobKey); + await scheduler.DeleteJob(jobKey); + } + } + +} } \ No newline at end of file diff --git a/service/DouyinVideoService.cs b/service/DouyinVideoService.cs index 18a1720..eae2fa6 100644 --- a/service/DouyinVideoService.cs +++ b/service/DouyinVideoService.cs @@ -22,6 +22,11 @@ namespace dy.net.service } + public async Task DeleteById(string Id) + { + return await _dyCollectVideoRepository.DeleteByIdAsync(Id); + } + public async Task BatchInsertOrUpdate(List videos) { // 边界处理:传入列表为空直接返回成功 @@ -99,22 +104,22 @@ namespace dy.net.service CategoryCount = list.Select(x => x.Tag1).Distinct().Count(), VideoCount = list.Count, Categories = Categories, - FavoriteCount = list.Count(x => x.ViedoType == VideoTypeEnum.Favorite), - CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.Collect), - FollowCount = list.Count(x => x.ViedoType == VideoTypeEnum.UperPost), - GraphicVideoCount = list.Count(x => x.ViedoType == VideoTypeEnum.ImageVideo), + FavoriteCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_favorite), + CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_collects), + FollowCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_follows), + GraphicVideoCount = list.Count(x => x.IsMergeVideo == 1), VideoSizeTotal = ByteToGbConverter.ConvertBytesToGb(list.Sum(x => x.FileSize)), - VideoFavoriteSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.Favorite).Sum(x => x.FileSize)), - VideoCollectSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.Collect).Sum(x => x.FileSize)), - VideoFollowSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.UperPost).Sum(x => x.FileSize)), - GraphicVideoSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.ImageVideo).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.dy_collects).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.IsMergeVideo == 1).Sum(x => x.FileSize)), //TotalDiskSize= ByteToGbConverter.GetHostTotalDiskSpaceGB(), }; 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误导用户 } @@ -166,6 +171,8 @@ namespace dy.net.service return await _dyCollectVideoRepository.GetByIdAsync(id); } + + /// /// 重新下载选中的视频 /// @@ -194,7 +201,7 @@ namespace dy.net.service } // 3. 构建重新下载记录(提前准备数据,避免事务内耗时操作) - var reDownList = new List(); + var reDownList = new List(); var filePathsToDelete = new List(); // 收集待删除文件路径,统一处理 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(), CreateTime = DateTime.UtcNow, // 统一使用UTC时间,避免时区问题 @@ -233,7 +240,7 @@ namespace dy.net.service var transactionResult = await _dyCollectVideoRepository.UseTranAsync(async () => { // 4.1 批量插入重新下载记录(SqlSugar批量插入效率更高) - _dyCollectVideoRepository.InsertReDowns(reDownList); + _dyCollectVideoRepository.InsertReDowns(reDownList); // 4.2 批量删除原视频记录(使用视频实际存在的ID,避免无效删除) var actualDeleteIds = videos.Select(v => v.Id).ToList(); var deleteCount = await _dyCollectVideoRepository.DeleteByIdsAsync(actualDeleteIds); // 建议仓储层提供异步删除方法 @@ -242,75 +249,49 @@ namespace dy.net.service Serilog.Log.Error(e, "数据库事务执行失败:Ids={0}", string.Join(",", videoIds)); }); - // 5. 文件夹删除(非事务操作,失败不回滚数据库,可根据业务调整) - // 关键:先通过文件路径获取父文件夹,再删除整个文件夹(含子内容) - foreach (var filePath in filePathsToDelete) + // 5. 文件删除(非事务操作,失败不回滚数据库,可根据业务调整) + // 采用异步文件操作,避免同步IO阻塞线程(需.NET 5+支持) + foreach (var path in filePathsToDelete) { try { - // 1. 验证文件路径有效性 - if (string.IsNullOrWhiteSpace(filePath)) + if (File.Exists(path)) { - Serilog.Log.Error("文件路径为空,跳过文件夹删除"); - continue; - } - - // 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); + File.Delete(path); // 异步删除,提升并发性能 + Serilog.Log.Debug("视频文件删除成功:Path={0}", path); } else { - Serilog.Log.Error("父文件夹不存在,跳过删除:ParentDir={0},关联文件路径={1}", parentDirPath, filePath); + Serilog.Log.Error("视频文件不存在,跳过删除:Path={0}", path); } } catch (IOException ex) { - Serilog.Log.Error(ex, "文件夹删除失败(IO异常):Filepath={0},父文件夹路径={1}", filePath, Path.GetDirectoryName(filePath)); - } - 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)); + Serilog.Log.Error(ex, "视频文件删除失败:Path={0}", path); } } var CookieIds = reDownList.Select(x => x.CookieId).Distinct(); foreach (var ck in CookieIds) { - var cookie= douyinCookieRepository.GetById(ck); + var cookie = douyinCookieRepository.GetById(ck); if (cookie == null) continue; 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) { switch (item) { - case VideoTypeEnum.Favorite: + case VideoTypeEnum.dy_favorite: cookie.FavHasSyncd = 0; break; - case VideoTypeEnum.Collect: + case VideoTypeEnum.dy_collects: cookie.CollHasSyncd = 0; break; - case VideoTypeEnum.UperPost: + case VideoTypeEnum.dy_follows: cookie.UperSyncd = 0; break; case VideoTypeEnum.ImageVideo: @@ -338,7 +319,7 @@ namespace dy.net.service /// 获取待重新下载的视频列表 /// /// - public async Task> GetViedoReDowns() + public async Task> GetViedoReDowns() { return await _dyCollectVideoRepository.GetViedoReDowns(); } diff --git a/utils/DouyinFileNameHelper.cs b/utils/DouyinFileNameHelper.cs index f96cc09..cf1e1ac 100644 --- a/utils/DouyinFileNameHelper.cs +++ b/utils/DouyinFileNameHelper.cs @@ -10,156 +10,45 @@ namespace dy.net.utils /// public static class DouyinFileNameHelper { - #region 配置参数 /// + /// 处理文件名/文件夹名,确保符合 Linux 限制(最大 255 字节 UTF-8 编码) /// - private const int MaxFileNameBytes = 60; - - - - /// - /// 非法字符替换后的占位符(也可设为空字符串) - /// - private const string IllegalCharReplacement = ""; - - - #endregion - - #region 处理抖音视频文件名 - /// - /// 生成抖音视频文件名 - /// - /// 抖音原始标题 - /// 排序号--如果有重复标题,根据时间排序取最后一个的序号+1 从001开始 - /// 安全可用的文件名 - public static string GenerateFileName(string originalTitle,string videoId) + /// 原始名称(支持英文、中文、混合字符) + /// 截取后为空时的默认名称(默认 "default") + /// 符合 Linux 规则的合法名称 + public static string SanitizeLinuxFileName(string originalName, string defaultName = "default") { - // 1. 基础容错(处理空值) - originalTitle = string.IsNullOrWhiteSpace(originalTitle) ? videoId : originalTitle; + // 1. 空值处理:直接返回默认名 + if (string.IsNullOrWhiteSpace(originalName)) + return defaultName.Replace(" ",""); - // 2. 净化标题:移除非法内容 - string purifiedTitle = PurifyTitle(originalTitle, videoId); - - // 3. 长度控制:按UTF-8字节数截断(避免超系统限制) - string truncatedTitle = TruncateByByteLength(purifiedTitle, MaxFileNameBytes); - - return truncatedTitle.Trim(); - } - #endregion - - #region 内部辅助方法:标题净化 - /// - /// 净化标题(移除非法字符、表情、话题标签) - /// - private static string PurifyTitle(string title,string id) - { - if (string.IsNullOrEmpty(title)) + // 2. 过滤 Linux 非法字符: + // - 禁止:/(路径分隔符)、\0(空字符) + // - 替换:其他特殊字符(如 :*?"<>|\\ )为下划线 _,避免创建失败 + var invalidChars = new[] { '/', '\0', ':', '*', '?', '"', '<', '>', '|', '\\' }; + string sanitizedName = originalName; + foreach (var c in invalidChars) { - 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]", "-"); - // 步骤1:移除话题标签(#xxx 或 #xxx#yyy) - //title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled); + // 4. 超过 255 字节,截取前 255 字节(避免破坏 UTF-8 字符) + byte[] truncatedBytes = new byte[252]; + Array.Copy(utf8Bytes, truncatedBytes, 252); - // 步骤2:移除表情符号(匹配常见表情Unicode区块) - //string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]"; - //title = Regex.Replace(title, emojiPattern, "", RegexOptions.Compiled); + // 5. 字节数组转回字符串(自动忽略不完整的尾部字节,避免乱码) + string truncatedName = Encoding.UTF8.GetString(truncatedBytes).TrimEnd('\0').Replace(" ",""); // 移除可能的空字符 - // 步骤3:过滤系统非法字符(Windows/macOS/Linux通用禁止) - char[] illegalChars = new[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0', '\t', '\n', '\r' }; - 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 内部辅助方法:按字节数截断 - /// - /// 按UTF-8字节数截断字符串(避免截断半个中文) - /// - /// 要截断的字符串 - /// 最大字节数 - /// 截断后的字符串 - 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; - } + // 6. 极端情况:截取后为空(如全是非法字符替换后无有效内容),返回默认名 + return string.IsNullOrWhiteSpace(truncatedName) ? defaultName : truncatedName; } - /// /// 检查字符串是否仅包含字母、数字、简体中文(无特殊字符) /// /// 待检查的字符串 @@ -184,4 +73,4 @@ namespace dy.net.utils return Regex.IsMatch(input, pattern, RegexOptions.None); } } -} +} \ No newline at end of file