diff --git a/Properties/PublishProfiles/FolderProfile.pubxml.user b/Properties/PublishProfiles/FolderProfile.pubxml.user index c815d2e..24f4535 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-06T05:07:29.7182102Z||;True|2025-12-06T13:05:46.2855366+08:00||;False|2025-12-06T13:05:40.3550904+08:00||;True|2025-12-06T13:03:47.8773880+08:00||;True|2025-12-06T13:03:28.9521030+08:00||;True|2025-12-05T23:11:51.5026151+08:00||;True|2025-12-05T12:46:26.1415079+08:00||;False|2025-12-05T12:46:19.1274332+08:00||;True|2025-12-05T09:04:36.7111717+08:00||;True|2025-12-05T08:22:51.7343317+08:00||;True|2025-12-05T08:20:32.0383739+08:00||;True|2025-12-04T21:58:29.4112766+08:00||;True|2025-12-04T21:58:10.0382218+08:00||;True|2025-12-04T21:57:55.3894059+08:00||;True|2025-12-04T21:10:38.8943797+08:00||;True|2025-12-04T21:09:31.5024569+08:00||;True|2025-12-04T08:22:47.9617427+08:00||;False|2025-12-04T08:22:41.3759733+08:00||;True|2025-12-04T08:14:02.2478893+08:00||;True|2025-12-04T07:57:00.2208139+08:00||;True|2025-12-03T23:58:13.3693303+08:00||;True|2025-12-03T23:57:40.4257893+08:00||;True|2025-12-03T23:56:38.8717769+08:00||;True|2025-12-03T23:56:24.0663568+08:00||;False|2025-12-03T23:55:36.1902974+08:00||;True|2025-12-03T23:51:30.6688504+08:00||;True|2025-12-03T23:34:17.1648971+08:00||;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||; + True|2025-12-07T15:02:10.0037815Z||;True|2025-12-07T22:48:25.7859648+08:00||;True|2025-12-07T20:49:13.6683281+08:00||;True|2025-12-07T08:47:30.1236366+08:00||;True|2025-12-06T13:14:32.4410202+08:00||;True|2025-12-06T13:07:29.7182102+08:00||;True|2025-12-06T13:05:46.2855366+08:00||;False|2025-12-06T13:05:40.3550904+08:00||;True|2025-12-06T13:03:47.8773880+08:00||;True|2025-12-06T13:03:28.9521030+08:00||;True|2025-12-05T23:11:51.5026151+08:00||;True|2025-12-05T12:46:26.1415079+08:00||;False|2025-12-05T12:46:19.1274332+08:00||;True|2025-12-05T09:04:36.7111717+08:00||;True|2025-12-05T08:22:51.7343317+08:00||;True|2025-12-05T08:20:32.0383739+08:00||;True|2025-12-04T21:58:29.4112766+08:00||;True|2025-12-04T21:58:10.0382218+08:00||;True|2025-12-04T21:57:55.3894059+08:00||;True|2025-12-04T21:10:38.8943797+08:00||;True|2025-12-04T21:09:31.5024569+08:00||;True|2025-12-04T08:22:47.9617427+08:00||;False|2025-12-04T08:22:41.3759733+08:00||;True|2025-12-04T08:14:02.2478893+08:00||;True|2025-12-04T07:57:00.2208139+08:00||;True|2025-12-03T23:58:13.3693303+08:00||;True|2025-12-03T23:57:40.4257893+08:00||;True|2025-12-03T23:56:38.8717769+08:00||;True|2025-12-03T23:56:24.0663568+08:00||;False|2025-12-03T23:55:36.1902974+08:00||;True|2025-12-03T23:51:30.6688504+08:00||;True|2025-12-03T23:34:17.1648971+08:00||;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||; diff --git a/README.md b/README.md index 2c2ba01..672472f 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取, | 镜像标签 | 架构 | | ----------------- | -------------- | -| `beta_1.8.0` | x86_64 (amd64) | -| `arm_1.8.0` | ARM64 | +| `beta_1.8.2` | x86_64 (amd64) | +| `arm_1.8.2` | ARM64 | ### 最新镜像查看 [镜像列表](http://nas.synology2023.online:10108/api/docker/dysync/1) @@ -116,7 +116,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.7 + ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.8.2 # 注意:-p 后面的容器端口,可以用环境变量类似:ASPNETCORE_URLS = http://+:10108 指定 ``` @@ -130,7 +130,7 @@ version: '3.8' services: dysync: - image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.7.7 + image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.8.2 container_name: dysync2025 # 容器名称 restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止 ports: diff --git a/app/src/pages/set/AppSet.vue b/app/src/pages/set/AppSet.vue index 9778359..1c3e720 100644 --- a/app/src/pages/set/AppSet.vue +++ b/app/src/pages/set/AppSet.vue @@ -105,7 +105,13 @@ 启用后,将单独下载所有图片文件 - + + +
+ + 针对有些视频是多个视频生成的,实际是分为多个视频,启用后将会分别下载多个视频,名字带_001,002这样 +
+
@@ -222,6 +228,7 @@ interface FormState { FullFollowedTitleTemplate: string; // 新增:完整模板字符串(自动生成) AutoDistinct: boolean; PriorityLevel: string; + DownDynamicVideo: boolean; } // 表单初始数据 @@ -242,6 +249,7 @@ const formState: UnwrapRef = reactive({ FullFollowedTitleTemplate: '', // 初始为空 AutoDistinct: false, PriorityLevel: '', + DownDynamicVideo: false, }); // 实时计算完整模板(可选:让用户实时预览,提交时无需重复计算) @@ -318,6 +326,7 @@ const getConfig = () => { ImageViedoSaveAlone: res.data.imageViedoSaveAlone, AutoDistinct: res.data.autoDistinct, PriorityLevel: res.data.priorityLevel, + DownDynamicVideo: res.data.downDynamicVideo, }); // downImgVideo.value = res.data.downImageVideoFromEnv; diff --git a/appsettings.json b/appsettings.json index f6a48b4..68b21e5 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,7 +1,7 @@ { "dbconn": "", - //"tagName": "arm_1.8.0", - //"tagName": "dev_1.2", - "tagName": "beta_1.8.0", + //"tagName": "arm_1.8.2", + //"tagName": "dev_1.2.2", + "tagName": "beta_1.8.2", "dbtype": "Sqlite" } \ No newline at end of file diff --git a/dto/DouyinVideoInfo.cs b/dto/DouyinVideoInfo.cs index e5cc25f..e956abf 100644 --- a/dto/DouyinVideoInfo.cs +++ b/dto/DouyinVideoInfo.cs @@ -1270,6 +1270,10 @@ namespace dy.net.dto /// [JsonProperty("url_list")] public List UrlList { get; set; } + + + [JsonProperty("video")] + public Video DynamicVideo { get; set; } /// /// /// diff --git a/job/DouyinBasicSyncJob.cs b/job/DouyinBasicSyncJob.cs index f72cf30..3126152 100644 --- a/job/DouyinBasicSyncJob.cs +++ b/job/DouyinBasicSyncJob.cs @@ -134,11 +134,7 @@ namespace dy.net.job Log.Debug($"{VideoType}-未获取到系统配置,任务终止!!!"); return; } - if(VideoType== VideoTypeEnum.dy_follows ) - { - Log.Debug($"{VideoType}-系统配置未启用关注同步,任务终止!!!"); - return; - } + // 2. 从配置中获取每页请求数量--固定18 //if (config.BatchCount > 0) @@ -151,10 +147,9 @@ namespace dy.net.job var cookies = await GetValidCookies(); if (cookies == null || !cookies.Any()) { - Log.Debug($"{VideoType}-无有效Cookie,任务终止!!!"); + Log.Debug($"{VideoType}-无有效的Cookie,任务终止!!!"); return; } - Log.Debug($"{VideoType}-共发现{cookies.Count}个Cookie,同步任务即将开始..."); // 6. 遍历每个有效的Cookie,执行同步操作 @@ -411,12 +406,11 @@ namespace dy.net.job var videos = new List(); foreach (var item in data.AwemeList) { - - if(!item.Desc.Contains("致敬先烈")) - { - continue; - } - + //if (!item.Desc.Contains("人生苦短")) + ////if (!item.Desc.Contains("如果你也喜欢")) + //{ + // continue; + //} //判断视频是否是强制删除且不再下载的视频 var deleteVideo = await douyinCommonService.ExistDeleteVideo(item.AwemeId); if (deleteVideo) @@ -434,7 +428,7 @@ namespace dy.net.job { continue; } - + //如果存在,但是因为去重规则,选择的最高优先级变更了,需要删除原来的,重新下载。 if (exitVideo!=null) { await douyinVideoService.DeleteById(exitVideo.Id); } @@ -451,12 +445,62 @@ namespace dy.net.job if (video != null) videos.Add(video); - // 如果配置了下载图片视频,则处理图片集并合成视频 + + // 处理图文视频 if (config.DownImageVideo || config.DownMp3 || config.DownImage) { - var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config, followed); - if (mergevideo != null) - videos.Add(mergevideo); + //处理多个视频-组合的图文视频--类似动图。 + + if (config.DownDynamicVideo) + { + List dynamicVideoUrls = new List(); + if (item.Images != null&& item.Images.Count > 0) + { + foreach (var img in item.Images) + { + if (img.DynamicVideo != null&& img.DynamicVideo.BitRate!=null&& img.DynamicVideo.BitRate.Count>0) + { + foreach (var btv in img.DynamicVideo.BitRate) + { + var payAddr = btv.PlayAddr; + if(payAddr != null&& payAddr.UrlList!=null&&payAddr.UrlList.Count>0) + { + dynamicVideoUrls.Add(payAddr.UrlList.FirstOrDefault(x => x.StartsWith("https://www.douyin.com/aweme/v1/play"))); + } + } + } + } + } + + //说明这个是图文视频里面的动态视频拼接的。。。 + if (dynamicVideoUrls.Count > 0) + { + var dynamicVideo = await ProcessDynamicVideo(dynamicVideoUrls, cookie, item, data, config); + if (dynamicVideo != null) + { + videos.Add(dynamicVideo); + Log.Debug($"{VideoType}-动态视频[{item.Desc}],下载成功 ,共{dynamicVideo.DynamicVideos.Count()}个视频..."); + } + else + { + Log.Debug($"{VideoType}-动态视频[{item.Desc}],下载失败..."); + } + } + else + { + var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config, followed); + if (mergevideo != null) + videos.Add(mergevideo); + } + } + else + { + var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config, followed); + if (mergevideo != null) + videos.Add(mergevideo); + } + + } } return videos; @@ -625,7 +669,7 @@ namespace dy.net.job } /// - /// 处理单个视频 + /// 处理单个视频--正常的收藏喜欢关注的 视频 /// 负责下载视频、封面、头像,生成NFO文件,创建视频实体等 /// /// 用户Cookie @@ -664,19 +708,24 @@ namespace dy.net.job } Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc,item.AwemeId)}]开始下载..."); - // 随机延迟,模拟人类操作 + // 随机延迟 await Task.Delay(_random.Next(1, 4) * 1000); + // 下载视频 if (!await douyinHttpClientService.DownloadAsync(videoUrl, savePath, cookie.Cookies)) { Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]下载失败!!!"); - return null; + + (bool flowControl, DouyinVideo value) = await SwitchOtherUrlAddressDown(cookie, item, videoUrl, savePath); + if (!flowControl) + { + return value; + } } else { Log.Debug($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]下载完成."); } - // 下载视频封面 await DownVideoCover(item, saveFolder, cookie, config); // 下载作者头像 @@ -687,6 +736,131 @@ namespace dy.net.job return CreateVideoEntity(cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data); } + + + + + protected async Task ProcessDynamicVideo(List dynamicUrls,DouyinCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed = null) + { + + + // 获取视频标签 + var (tag1, tag2, tag3) = GetVideoTags(item); + // 创建保存文件夹 + var saveFolder = CreateSaveFolder(cookie, item, config, followed); + // 获取视频文件名 + var fileName = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)+".mp4"; + // 拼接视频保存路径 + var savePath = Path.Combine(saveFolder, fileName); + + Log.Debug($"{VideoType}-动态视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]开始下载..."); + // 随机延迟 + int i = 1; + + List dynamicSavePaths = new List(); + foreach (var dynamicUrl in dynamicUrls) + { + // 下载动态视频 + var dynamicSavePath = savePath.Replace(".mp4", $"_00{i}.mp4"); + + // 如果文件已存在,跳过 + if (File.Exists(dynamicSavePath)) + { + //Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]已存在,跳过下载."); + dynamicSavePaths.Add(dynamicSavePath); + continue; + } + + if (!await douyinHttpClientService.DownloadAsync(dynamicUrl, dynamicSavePath, cookie.Cookies)) + { + Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]-00{i},下载失败!!!"); + } + else + { + dynamicSavePaths.Add(dynamicSavePath); + i++; + Log.Debug($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]-00{i},下载完成."); + } + await Task.Delay(_random.Next(1, 4) * 1000); + } + + + //// 下载视频封面 + //await DownVideoCover(item, saveFolder, cookie, config); + //// 下载作者头像 + //var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item); + //// 生成NFO文件 + //await GenerateNfoFile(saveFolder, item, avatarUrl, cookie, config); + // 创建视频实体 + // 为合成的视频创建一个“虚拟”的BitRate对象,以便复用CreateVideoEntity方法 + var virtualBitRate = new VideoBitRate + { + PlayAddr = new PlayAddr + { + Width = item?.Images?.FirstOrDefault()?.Width ?? 0, + Height = item?.Images?.FirstOrDefault()?.Height ?? 0, + DataSize = DouyinFileUtils.GetTotalFileSize(dynamicSavePaths) // 合成视频的文件大小 + } + }; + return CreateVideoEntity(cookie, item, virtualBitRate, dynamicSavePaths.FirstOrDefault(), saveFolder, tag1, tag2, tag3,"", "", data,dynamicSavePaths); + } + + + + + + + + + /// + /// 寻找其他视频地址下载... + /// + /// + /// + /// + /// + /// + private async Task<(bool flowControl, DouyinVideo value)> SwitchOtherUrlAddressDown(DouyinCookie cookie, Aweme item, string videoUrl, string savePath) + { + Log.Debug($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}],正在尝试切换其他地址再次重新下载..."); + var otherUrls = new List(); + + foreach (var bit in item.Video.BitRate) + { + if (bit == null) + { + continue; + } + var payUrls = bit.PlayAddr; + + if (payUrls == null || payUrls.UrlList == null || payUrls.UrlList.Count == 0) + { + continue; + } + + foreach (var payurl in payUrls.UrlList) + { + if (payurl == videoUrl)// 排除最开始下载失败的视频地址 + continue; + otherUrls.Add(payurl); + } + } + + if (otherUrls.Count > 0) + { + Log.Debug($"{VideoType}-额外找到{otherUrls.Count}个视频地址,即将再次开始下载..."); + var result2 = await douyinHttpClientService.DownloadAsync(videoUrl, savePath, cookie.Cookies, otherUrls); + Log.Debug($"{VideoType}-尝试多个地址后,下载成功,{savePath}"); + } + else + { + Log.Error($"{VideoType}-确实没有可以下载的视频链接2," + item.Desc); + return (flowControl: false, value: null); + } + + return (flowControl: true, value: null); + } + /// /// 处理图片集并合成为视频 /// 负责下载图片、合成视频、处理封面和头像等 @@ -1042,14 +1216,15 @@ namespace dy.net.job /// 作者头像保存路径 /// 作者头像URL /// 视频信息对象 + /// 动态视频 /// 创建的视频实体对象 private DouyinVideo CreateVideoEntity( DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder, - string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data) + string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data,List dynamicVideos=null) { var diffs = GetVideoEntityDifferences(cookie, item); - return new DouyinVideo + var video= new DouyinVideo { ViedoType = VideoType, AwemeId = item.AwemeId, @@ -1075,6 +1250,11 @@ namespace dy.net.job DyUserId = item.AuthorUserId == 0 ? item.Author?.Uid : item.AuthorUserId.ToString(), CookieId = cookie.Id }; + if (dynamicVideos != null && dynamicVideos.Count > 0) + { + video.DynamicVideos = JsonConvert.SerializeObject(dynamicVideos); + } + return video; } #endregion diff --git a/model/AppConfig.cs b/model/AppConfig.cs index 4e3b3d4..9752ba0 100644 --- a/model/AppConfig.cs +++ b/model/AppConfig.cs @@ -82,7 +82,12 @@ namespace dy.net.model /// 去重优先级配置,json字符串格式存储 /// [SugarColumn(Length = 500, IsNullable = true)] - public string PriorityLevel { get; set; }// = "[{\"id\":1,\"name\":\"喜欢的视频\",\"sort\":1},{\"id\":2,\"name\":\"收藏的视频\",\"sort\":2},{\"id\":3,\"name\":\"关注的视频\",\"sort\":3}]"; + public string PriorityLevel { get; set; } + + /// + /// 是否下载动态图视频 + /// + public bool DownDynamicVideo { get; set; } } } diff --git a/model/DouyinVideo.cs b/model/DouyinVideo.cs index ff22f79..800b8a5 100644 --- a/model/DouyinVideo.cs +++ b/model/DouyinVideo.cs @@ -70,7 +70,7 @@ namespace dy.net.model /// /// 视频地址(解析会有多个取第一个) /// - [SugarColumn(Length =0200,IsNullable =true)] + [SugarColumn(Length =2000,IsNullable =true)] public string VideoUrl { get; set; } /// /// 视频下载后保存路径 @@ -149,5 +149,10 @@ namespace dy.net.model [SugarColumn(IsIgnore = true)] public string ViedoCate =>(string.IsNullOrWhiteSpace(Tag1) ? "" : Tag1) + "/" + (string.IsNullOrWhiteSpace(Tag2) ? "" : Tag2); + + + // 普通字符串字段,显式标记为 SQLite TEXT 类型 + [SugarColumn(ColumnDataType = "TEXT", Length = -1)] + public string DynamicVideos { get; set; } } } diff --git a/service/DouyinHttpClientService.cs b/service/DouyinHttpClientService.cs index 3eb114f..7937a92 100644 --- a/service/DouyinHttpClientService.cs +++ b/service/DouyinHttpClientService.cs @@ -2,11 +2,14 @@ using dy.net.utils; using NetTaste; using Newtonsoft.Json; +using Serilog; using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection.PortableExecutable; +using System.Text.Json; +using System.Threading.Tasks.Dataflow; namespace dy.net.service { @@ -15,8 +18,6 @@ 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; @@ -271,6 +272,7 @@ namespace dy.net.service Serilog.Log.Error($"SyncMyFollows error: {err.StatusMsg}"); callBack(err); } + Serilog.Log.Error($"SyncMyFollows error: 关注列表为空 :{data}"); } else { @@ -305,6 +307,7 @@ namespace dy.net.service /// 文件地址 /// 保存路径 /// 请求Cookie + /// 其他下载地址 /// 取消令牌(用于终止任务) /// 流读取超时时间(默认60秒) /// 最大重试次数(默认3次) @@ -314,6 +317,7 @@ namespace dy.net.service string videoUrl, string savePath, string cookie, + List otherUrls=null, CancellationToken cancellationToken = default, TimeSpan? streamTimeout = null, int maxRetryCount = 3, @@ -323,14 +327,24 @@ namespace dy.net.service int retryCount = 0; var retryDelay = initialRetryDelay ?? TimeSpan.FromSeconds(1); // 初始延迟1秒 streamTimeout ??= TimeSpan.FromSeconds(60); // 流读取超时默认60秒 - + if (otherUrls != null) { + maxRetryCount = otherUrls.Count; + } while (true) { try { - // 执行单次下载逻辑(提取为内部方法,避免代码重复) - return await TryDownloadOnceAsync( - videoUrl, savePath, cookie, cancellationToken, streamTimeout.Value); + if (otherUrls != null&& retryCount>0) { + return await TryDownloadOnceAsync( + otherUrls[retryCount], savePath, cookie, cancellationToken, streamTimeout.Value); + } + else + { + return await TryDownloadOnceAsync( + videoUrl, savePath, cookie, cancellationToken, streamTimeout.Value); + } + + } catch (Exception ex) when (IsRetryableException(ex) && retryCount < maxRetryCount) { @@ -605,5 +619,96 @@ namespace dy.net.service } } + + + + + /// + /// 快速检测目标URL是否返回403 Forbidden状态码 + /// + /// 目标地址 + /// true=403状态,false=非403状态,null=请求异常 + public async Task PayUrlIsOKAsync(string url) + { + if (string.IsNullOrWhiteSpace(url) || !Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + Serilog.Log.Error("URL格式无效"); + return false; + } + + try + { + // 使用Head请求仅获取响应头,提升效率 + using var request = new HttpRequestMessage(HttpMethod.Head, url); + using var _httpClient = _clientFactory.CreateClient("dy_download"); + using var response = await _httpClient.SendAsync( + request + ); + + // 直接判断状态码是否为403 + bool isOk = response.StatusCode == HttpStatusCode.OK; + + Log.Debug($"URL: {url} | 状态码: {(int)response.StatusCode} ({response.StatusCode})"); + return isOk; + } + catch (HttpRequestException ex) + { + // 处理HTTP请求异常(如连接失败、DNS解析错误等) + Serilog.Log.Error($"请求异常: {ex.Message}"); + return false; + } + catch (TaskCanceledException ex) + { + // 处理超时异常 + Serilog.Log.Error($"请求超时: {ex.Message}"); + return false; + } + catch (Exception ex) + { + // 其他异常 + Serilog.Log.Error($"未知异常: {ex.Message}"); + return false; + } + } + + + + /// + /// 串行逐个检测URL,找到第一个「非403/404且请求成功」的URL立即终止并返回 + /// + /// 待检测URL列表 + /// 第一个符合条件的URL(null=所有URL都是403/404,或全部请求失败) + public async Task CheckPayUrlIsOKAsync(List urls) + { + // 逐个遍历URL,串行检测 + foreach (var url in urls) + { + try + { + // 跳过无效URL + if (string.IsNullOrWhiteSpace(url) || !Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + Log.Error($"URL格式无效:{url},跳过检测"); + continue; + } + // 检测当前URL是否为403/404 + bool isOk = await PayUrlIsOKAsync(url); + + // 核心判断:请求成功且不是403/404 → 立即返回该URL + if (isOk) + { + Log.Debug($"✅ 找到可以下载的地址:{url}..."); + return url; + } + } + catch (Exception ex) + { + Log.Error(ex, $"检查下载地址 {url} 时发生异常,继续检查下一个"); + } + } + return null; + } + + } } diff --git a/service/DouyinVideoService.cs b/service/DouyinVideoService.cs index b35610a..284c79e 100644 --- a/service/DouyinVideoService.cs +++ b/service/DouyinVideoService.cs @@ -109,11 +109,11 @@ namespace dy.net.service 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.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)), + VideoSizeTotal = DouyinFileUtils.ConvertBytesToGb(list.Sum(x => x.FileSize)), + VideoFavoriteSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_favorite).Sum(x => x.FileSize)), + VideoCollectSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_collects).Sum(x => x.FileSize)), + VideoFollowSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_follows).Sum(x => x.FileSize)), + GraphicVideoSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.IsMergeVideo == 1).Sum(x => x.FileSize)), //TotalDiskSize= ByteToGbConverter.GetHostTotalDiskSpaceGB(), }; diff --git a/utils/ByteToGbConverter.cs b/utils/DouyinFileUtils.cs similarity index 75% rename from utils/ByteToGbConverter.cs rename to utils/DouyinFileUtils.cs index a544a2c..2ff4fe7 100644 --- a/utils/ByteToGbConverter.cs +++ b/utils/DouyinFileUtils.cs @@ -1,6 +1,6 @@ namespace dy.net.utils { - public class ByteToGbConverter + public class DouyinFileUtils { /// /// 将字节数转换为GB(采用二进制换算:1GB = 1024^3 Byte) @@ -55,5 +55,24 @@ return $"获取宿主机总磁盘空间失败:{ex.Message}"; } } + + /// + /// 极简版:计算多个文件的总大小(字节) + /// + /// 文件路径列表 + /// 总字节数 + public static long GetTotalFileSize(List filePaths) + { + long totalSize = 0; + foreach (var path in filePaths) + { + // 仅判断文件是否存在,存在则累加大小 + if (File.Exists(path)) + { + totalSize += new FileInfo(path).Length; + } + } + return totalSize; + } } }