1、视频下载失败-403,重新切换其他下载地址尝试下载

2、支持动态视频下载(抖音里面属于图文-但是又不是图文)--实际是多个视频组合的这种。(没有将视频做合并的计划)
This commit is contained in:
jianzhichu
2025-12-07 23:09:02 +08:00
parent 123124bb51
commit 41e2bf6ce0
11 changed files with 374 additions and 47 deletions
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Project>
<PropertyGroup>
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
<History>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||;</History>
<History>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||;</History>
<LastFailureDetails />
</PropertyGroup>
<ItemGroup>
+4 -4
View File
@@ -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:
+10 -1
View File
@@ -105,7 +105,13 @@
<span>启用后将单独下载所有图片文件</span>
</div>
</a-form-item>
<a-form-item has-feedback label="下载动态视频" name="DownImage" :wrapper-col="{ span: 6 }">
<a-switch v-model:checked="formState.DownDynamicVideo" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>针对有些视频是多个视频生成的实际是分为多个视频启用后将会分别下载多个视频名字带_001,002这样</span>
</div>
</a-form-item>
<a-form-item has-feedback label="是否统一存储" name="ImageViedoSaveAlone" :wrapper-col="{ span: 10 }">
<a-switch v-model:checked="formState.ImageViedoSaveAlone" />
<div class="flex items-start mt-1 text-sm text-gray-500">
@@ -222,6 +228,7 @@ interface FormState {
FullFollowedTitleTemplate: string; // 新增:完整模板字符串(自动生成)
AutoDistinct: boolean;
PriorityLevel: string;
DownDynamicVideo: boolean;
}
// 表单初始数据
@@ -242,6 +249,7 @@ const formState: UnwrapRef<FormState> = 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;
+3 -3
View File
@@ -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"
}
+4
View File
@@ -1270,6 +1270,10 @@ namespace dy.net.dto
/// </summary>
[JsonProperty("url_list")]
public List<string> UrlList { get; set; }
[JsonProperty("video")]
public Video DynamicVideo { get; set; }
/// <summary>
///
/// </summary>
+201 -21
View File
@@ -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<DouyinVideo>();
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,14 +445,64 @@ namespace dy.net.job
if (video != null)
videos.Add(video);
// 如果配置了下载图片视频,则处理图片集并合成视频
// 处理图文视频
if (config.DownImageVideo || config.DownMp3 || config.DownImage)
{
//处理多个视频-组合的图文视频--类似动图。
if (config.DownDynamicVideo)
{
List<string> dynamicVideoUrls = new List<string>();
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
}
/// <summary>
/// 处理单个视频
/// 处理单个视频--正常的收藏喜欢关注的 视频
/// 负责下载视频、封面、头像,生成NFO文件,创建视频实体等
/// </summary>
/// <param name="cookie">用户Cookie</param>
@@ -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<DouyinVideo> ProcessDynamicVideo(List<string> 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<string> dynamicSavePaths = new List<string>();
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);
}
/// <summary>
/// 寻找其他视频地址下载...
/// </summary>
/// <param name="cookie"></param>
/// <param name="item"></param>
/// <param name="videoUrl"></param>
/// <param name="savePath"></param>
/// <returns></returns>
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<string>();
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);
}
/// <summary>
/// 处理图片集并合成为视频
/// 负责下载图片、合成视频、处理封面和头像等
@@ -1042,14 +1216,15 @@ namespace dy.net.job
/// <param name="avatarSavePath">作者头像保存路径</param>
/// <param name="avatarUrl">作者头像URL</param>
/// <param name="data">视频信息对象</param>
/// <param name="dynamicVideos">动态视频</param>
/// <returns>创建的视频实体对象</returns>
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<string> 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
+6 -1
View File
@@ -82,7 +82,12 @@ namespace dy.net.model
/// 去重优先级配置,json字符串格式存储
/// </summary>
[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; }
/// <summary>
/// 是否下载动态图视频
/// </summary>
public bool DownDynamicVideo { get; set; }
}
}
+6 -1
View File
@@ -70,7 +70,7 @@ namespace dy.net.model
/// <summary>
/// 视频地址(解析会有多个取第一个)
/// </summary>
[SugarColumn(Length =0200,IsNullable =true)]
[SugarColumn(Length =2000,IsNullable =true)]
public string VideoUrl { get; set; }
/// <summary>
/// 视频下载后保存路径
@@ -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; }
}
}
+109 -4
View File
@@ -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
/// <param name="videoUrl">文件地址</param>
/// <param name="savePath">保存路径</param>
/// <param name="cookie">请求Cookie</param>
/// <param name="otherUrls">其他下载地址</param>
/// <param name="cancellationToken">取消令牌(用于终止任务)</param>
/// <param name="streamTimeout">流读取超时时间(默认60秒)</param>
/// <param name="maxRetryCount">最大重试次数(默认3次)</param>
@@ -314,6 +317,7 @@ namespace dy.net.service
string videoUrl,
string savePath,
string cookie,
List<string> otherUrls=null,
CancellationToken cancellationToken = default,
TimeSpan? streamTimeout = null,
int maxRetryCount = 3,
@@ -323,15 +327,25 @@ 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
{
// 执行单次下载逻辑(提取为内部方法,避免代码重复)
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
}
}
/// <summary>
/// 快速检测目标URL是否返回403 Forbidden状态码
/// </summary>
/// <param name="url">目标地址</param>
/// <returns>true=403状态,false=非403状态,null=请求异常</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// 串行逐个检测URL,找到第一个「非403/404且请求成功」的URL立即终止并返回
/// </summary>
/// <param name="urls">待检测URL列表</param>
/// <returns>第一个符合条件的URLnull=所有URL都是403/404,或全部请求失败)</returns>
public async Task<string> CheckPayUrlIsOKAsync(List<string> 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;
}
}
}
+5 -5
View File
@@ -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(),
};
@@ -1,6 +1,6 @@
namespace dy.net.utils
{
public class ByteToGbConverter
public class DouyinFileUtils
{
/// <summary>
/// 将字节数转换为GB(采用二进制换算:1GB = 1024^3 Byte
@@ -55,5 +55,24 @@
return $"获取宿主机总磁盘空间失败:{ex.Message}";
}
}
/// <summary>
/// 极简版:计算多个文件的总大小(字节)
/// </summary>
/// <param name="filePaths">文件路径列表</param>
/// <returns>总字节数</returns>
public static long GetTotalFileSize(List<string> filePaths)
{
long totalSize = 0;
foreach (var path in filePaths)
{
// 仅判断文件是否存在,存在则累加大小
if (File.Exists(path))
{
totalSize += new FileInfo(path).Length;
}
}
return totalSize;
}
}
}