From 2db61f4958f71aad0be0544c70a666b370305981 Mon Sep 17 00:00:00 2001 From: jianzhichu Date: Mon, 24 Nov 2025 22:47:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dup=E4=B8=BB=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=EF=BC=8C=E4=B8=8D=E4=B8=8B=E8=BD=BD=E5=9B=BE?= =?UTF-8?q?=E6=96=87=E8=A7=86=E9=A2=91=E7=9A=84bug=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=9B=BE=E6=96=87=E8=A7=86=E9=A2=91=E4=B8=8B=E8=BD=BD=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=88=E6=98=AF=E5=90=A6=E5=90=88=E6=88=90=E8=A7=86?= =?UTF-8?q?=E9=A2=91=EF=BC=8C=E6=98=AF=E5=90=A6=E5=8D=95=E7=8B=AC=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=9B=BE=E7=89=87=EF=BC=8C=E5=8D=95=E7=8B=AC=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E9=9F=B3=E9=A2=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 2 +- .../PublishProfiles/FolderProfile.pubxml.user | 2 +- app/src/pages/set/AppSet.vue | 13 ++ expand/dy.image/FFmpegHelper.cs | 197 +++++++++++++++--- expand/dy.image/ImageMergeToVideoService.cs | 57 +++-- extension/ServiceExtension.cs | 2 +- job/DouyinBaseSyncJob.cs | 54 +++-- model/AppConfig.cs | 10 + service/DouyinCommonService.cs | 8 +- service/DouyinHttpClientService.cs | 3 +- service/DouyinQuartzJobService.cs | 30 +-- utils/DouyinBaseParamDics.cs | 94 ++++++++- 12 files changed, 380 insertions(+), 92 deletions(-) diff --git a/Program.cs b/Program.cs index f40c8da..fd20f5d 100644 --- a/Program.cs +++ b/Program.cs @@ -52,7 +52,7 @@ namespace dy.net // ¹¹½¨ӦÓà var app = builder.Build(); - Log.Debug("DOWN_IMGVIDEO=" + downImgConfig); + Log.Debug("ffmpeg=" + downImgConfig); // ÅäÖÃÖмä¼þ ConfigureMiddleware(app, builder.Environment); diff --git a/Properties/PublishProfiles/FolderProfile.pubxml.user b/Properties/PublishProfiles/FolderProfile.pubxml.user index 2d9d237..524b273 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-11-24T00:07:00.9951205Z||;True|2025-11-23T23:34:50.0030826+08:00||;True|2025-11-23T23:32:36.7452616+08:00||;True|2025-11-22T23:18:56.3202345+08:00||;True|2025-11-22T22:52:51.7203302+08:00||;True|2025-11-22T22:52:42.9620946+08:00||;True|2025-11-22T22:52:09.8257640+08:00||;True|2025-11-22T22:39:31.5894141+08:00||;True|2025-11-22T22:31:11.0704815+08:00||;True|2025-11-22T22:20:36.4579131+08:00||;True|2025-11-22T22:19:10.2281364+08:00||;True|2025-11-22T19:34:45.9336901+08:00||;True|2025-11-12T23:06:38.7834109+08:00||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;False|2024-01-02T22:24:42.6577609+08:00||;True|2024-01-02T22:04:33.4708889+08:00||;True|2024-01-02T21:49:57.9249898+08:00||;True|2023-12-28T22:42:58.5976180+08:00||;True|2023-12-28T22:24:09.2051205+08:00||;True|2023-12-28T22:05:48.4865497+08:00||;True|2023-12-28T21:50:57.4549336+08:00||;True|2023-12-28T21:45:30.4557939+08:00||;True|2023-12-28T21:19:59.3614594+08:00||;False|2023-12-28T21:17:04.3069097+08:00||;False|2023-12-28T21:15:56.7788810+08:00||;False|2023-12-28T21:14:57.3617402+08:00||;False|2023-12-28T21:10:45.4382469+08:00||;True|2023-12-28T19:45:33.1431914+08:00||;True|2023-12-26T11:30:36.6418716+08:00||;True|2023-12-26T11:17:40.6433651+08:00||; + True|2025-11-24T00:17:50.3994607Z||;True|2025-11-24T08:07:00.9951205+08:00||;True|2025-11-23T23:34:50.0030826+08:00||;True|2025-11-23T23:32:36.7452616+08:00||;True|2025-11-22T23:18:56.3202345+08:00||;True|2025-11-22T22:52:51.7203302+08:00||;True|2025-11-22T22:52:42.9620946+08:00||;True|2025-11-22T22:52:09.8257640+08:00||;True|2025-11-22T22:39:31.5894141+08:00||;True|2025-11-22T22:31:11.0704815+08:00||;True|2025-11-22T22:20:36.4579131+08:00||;True|2025-11-22T22:19:10.2281364+08:00||;True|2025-11-22T19:34:45.9336901+08:00||;True|2025-11-12T23:06:38.7834109+08:00||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;False|2024-01-02T22:24:42.6577609+08:00||;True|2024-01-02T22:04:33.4708889+08:00||;True|2024-01-02T21:49:57.9249898+08:00||;True|2023-12-28T22:42:58.5976180+08:00||;True|2023-12-28T22:24:09.2051205+08:00||;True|2023-12-28T22:05:48.4865497+08:00||;True|2023-12-28T21:50:57.4549336+08:00||;True|2023-12-28T21:45:30.4557939+08:00||;True|2023-12-28T21:19:59.3614594+08:00||;False|2023-12-28T21:17:04.3069097+08:00||;False|2023-12-28T21:15:56.7788810+08:00||;False|2023-12-28T21:14:57.3617402+08:00||;False|2023-12-28T21:10:45.4382469+08:00||;True|2023-12-28T19:45:33.1431914+08:00||;True|2023-12-26T11:30:36.6418716+08:00||; \ No newline at end of file diff --git a/app/src/pages/set/AppSet.vue b/app/src/pages/set/AppSet.vue index 6d2929c..ae3ec7e 100644 --- a/app/src/pages/set/AppSet.vue +++ b/app/src/pages/set/AppSet.vue @@ -45,6 +45,13 @@ + + + + + + + @@ -85,6 +92,8 @@ interface FormState { UperSaveTogether: boolean; UperUseViedoTitle: boolean; LogKeepDay: number; + DownImage: boolean; + DownMp3: boolean; } // 表单初始数据 @@ -97,6 +106,8 @@ const formState: UnwrapRef = reactive({ UperSaveTogether: false, DownImageVideo: false, DownImageVideoFromEnv: false, + DownMp3: false, + DownImage: false, }); // 表单校验规则 @@ -144,6 +155,8 @@ const getConfig = () => { UperUseViedoTitle: res.data.uperUseViedoTitle, UperSaveTogether: res.data.uperSaveTogether, LogKeepDay: res.data.logKeepDay, + DownImage: res.data.downImage, + DownMp3: res.data.downMp3, }); // 监听环境变量配置,控制是否显示图片视频下载选项 diff --git a/expand/dy.image/FFmpegHelper.cs b/expand/dy.image/FFmpegHelper.cs index 02f2798..4f4abb7 100644 --- a/expand/dy.image/FFmpegHelper.cs +++ b/expand/dy.image/FFmpegHelper.cs @@ -70,6 +70,81 @@ namespace dy.image Directory.CreateDirectory(outputDirectory); } + //// 关键步骤 1: 创建临时目录并生成有序图片序列 + //string tempImageDir = Path.Combine(AppContext.BaseDirectory, "temp", Guid.NewGuid().ToString()); + //Directory.CreateDirectory(tempImageDir); + + //var imageList = imageFilePaths.ToList(); + //try + //{ + // for (int i = 0; i < imageList.Count; i++) + // { + // string sourcePath = imageList[i]; + // // 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png + // string extension = Path.GetExtension(sourcePath); + // string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0 + // string destPath = Path.Combine(tempImageDir, destFileName); + // File.Copy(sourcePath, destPath); + // } + + // // 关键步骤 2: 构建符合你成功经验的 FFmpeg 命令 + // string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList[0])); + // double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2); ; // 例如 1/3 = 0.333... fps + + // // 音频滤镜:如果音频比图片长则截断,比图片短则循环 + // string audioFilter = await GetAudioFilterAsync(audioFilePath, imageList.Count * ImageDisplayDurationSeconds); + + + // // 构建 FFmpeg 参数列表 + // var arguments = new List + // { + // "-y", // 覆盖输出文件 + // // --- 输入图片序列的配置 --- + // "-f", "image2", // 明确指定输入为图片序列 + // "-vcodec", "webp", // 强制使用 WebP 解码器 + // "-r", imageFps.ToString(CultureInfo.InvariantCulture), // 设置图片播放速度 + // $"-i", $"\"{imageSequencePattern}\"", // 图片序列的路径模式 + // // --- 输入音频的配置 --- + // audioFilter, // 应用音频滤镜(可能为空) + // $"-i", $"\"{audioFilePath}\"", // 音频文件路径 + // // --- 视频编码配置 --- + // "-c:v", VideoCodec, // 视频编码器 (如 libx264) + // "-preset", VideoPreset, // 编码预设 (如 medium) + // $"-crf", $"{VideoCrf}", // 视频质量因子 + // $"-s", $"{VideoWidth}x{VideoHeight}", // 输出视频分辨率 + // "-pix_fmt", "yuv420p", // 像素格式,确保兼容性 + // // --- 音频编码配置 --- + // "-c:a", AudioCodec, // 音频编码器 (如 aac) + // $"-b:a", $"{AudioBitrate}", // 音频比特率 (如 192k) + // // --- 输出文件 --- + // $"\"{outputVideoPath}\"" // 最终输出视频路径 + // }; + + // string args = string.Join(" ", arguments); + // Console.WriteLine($"执行FFmpeg命令: {_ffmpegExecutablePath} {args}"); + + // // 执行命令 + // await ExecuteFFmpegAsync(args, progress, cancellationToken); + + // if (File.Exists(outputVideoPath)) + // { + // return outputVideoPath; + // } + // else + // { + // throw new InvalidOperationException("视频合成失败,未生成输出文件。"); + // } + //} + //finally + //{ + // // 关键步骤 3: 清理临时文件 + // if (Directory.Exists(tempImageDir)) + // { + // Directory.Delete(tempImageDir, recursive: true); + // } + //} + + // 关键步骤 1: 创建临时目录并生成有序图片序列 string tempImageDir = Path.Combine(AppContext.BaseDirectory, "temp", Guid.NewGuid().ToString()); Directory.CreateDirectory(tempImageDir); @@ -80,51 +155,108 @@ namespace dy.image for (int i = 0; i < imageList.Count; i++) { string sourcePath = imageList[i]; - // 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png string extension = Path.GetExtension(sourcePath); - string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0 + string destFileName = $"temp_{i + 1:D3}{extension}"; string destPath = Path.Combine(tempImageDir, destFileName); File.Copy(sourcePath, destPath); } - // 关键步骤 2: 构建符合你成功经验的 FFmpeg 命令 string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList[0])); - double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2); ; // 例如 1/3 = 0.333... fps + double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2); - // 音频滤镜:如果音频比图片长则截断,比图片短则循环 - string audioFilter = await GetAudioFilterAsync(audioFilePath, imageList.Count * ImageDisplayDurationSeconds); - - // 构建 FFmpeg 参数列表 - var arguments = new List + // --- 修正点 1: 整合音频时长获取逻辑 --- + // 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环 + double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长 + try { - "-y", // 覆盖输出文件 - // --- 输入图片序列的配置 --- - "-f", "image2", // 明确指定输入为图片序列 - "-vcodec", "webp", // 强制使用 WebP 解码器 - "-r", imageFps.ToString(CultureInfo.InvariantCulture), // 设置图片播放速度 - $"-i", $"\"{imageSequencePattern}\"", // 图片序列的路径模式 - // --- 输入音频的配置 --- - audioFilter, // 应用音频滤镜(可能为空) - $"-i", $"\"{audioFilePath}\"", // 音频文件路径 - // --- 视频编码配置 --- - "-c:v", VideoCodec, // 视频编码器 (如 libx264) - "-preset", VideoPreset, // 编码预设 (如 medium) - $"-crf", $"{VideoCrf}", // 视频质量因子 - $"-s", $"{VideoWidth}x{VideoHeight}", // 输出视频分辨率 - "-pix_fmt", "yuv420p", // 像素格式,确保兼容性 - // --- 音频编码配置 --- - "-c:a", AudioCodec, // 音频编码器 (如 aac) - $"-b:a", $"{AudioBitrate}", // 音频比特率 (如 192k) - // --- 输出文件 --- - $"\"{outputVideoPath}\"" // 最终输出视频路径 - }; + // 使用 ffprobe 获取音频时长 + var startInfo = new ProcessStartInfo + { + FileName = _ffprobeExecutablePath, + Arguments = $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"{audioFilePath}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = new Process { StartInfo = startInfo }) + { + process.Start(); + string output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); + + if (double.TryParse(output, out double duration)) + { + audioDurationSeconds = duration; + Console.WriteLine($"成功获取音频时长: {audioDurationSeconds:F2}s"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}"); + } + + // 计算图片序列需要循环的次数 + int loopCount = 1; + double singleLoopDuration = imageList.Count * ImageDisplayDurationSeconds; + if (audioDurationSeconds > singleLoopDuration) + { + loopCount = (int)Math.Ceiling(audioDurationSeconds / singleLoopDuration); + Console.WriteLine($"图片序列将循环 {loopCount} 次以匹配音频时长"); + } + + // --- 修正点 2: 重新构建 FFmpeg 参数列表 --- + var arguments = new List + { + "-y", // 覆盖输出文件 + + // --- 所有输入文件放在最前面 --- + // 图片序列输入 + "-f", "image2", + "-r", imageFps.ToString(CultureInfo.InvariantCulture), + "-i", $"\"{imageSequencePattern}\"", + + // 音频输入 + "-i", $"\"{audioFilePath}\"", + + // --- 修正点 3: 使用 filter_complex 对视频流进行循环 --- + // [0:v] 表示第一个输入(图片序列)的视频流 + // loop={loopCount-1} 表示循环 (次数-1) 次 + // [v] 是处理后的视频流的别名 + "-filter_complex", $"\"[0:v]loop={loopCount - 1}[v]\"", + + // --- 修正点 4: 明确映射输出流 --- + // 将处理后的视频流 [v] 映射到输出 + "-map", "\"[v]\"", + // 将第二个输入(音频文件)的音频流映射到输出 + "-map", "\"1:a\"", + + // --- 视频编码配置 --- + "-c:v", VideoCodec, + "-preset", VideoPreset, + "-crf", $"{VideoCrf}", + "-s", $"{VideoWidth}x{VideoHeight}", + "-pix_fmt", "yuv420p", + + // --- 音频编码配置 --- + "-c:a", AudioCodec, + "-b:a", $"{AudioBitrate}", + + // --- 修正点 5: 使用 -shortest 参数 --- + // 确保视频和音频同时结束,即使循环次数计算得不完全精确 + "-shortest", + + // --- 输出文件 --- + $"\"{outputVideoPath}\"" + }; string args = string.Join(" ", arguments); Console.WriteLine($"执行FFmpeg命令: {_ffmpegExecutablePath} {args}"); // 执行命令 await ExecuteFFmpegAsync(args, progress, cancellationToken); - if (File.Exists(outputVideoPath)) { return outputVideoPath; @@ -133,10 +265,11 @@ namespace dy.image { throw new InvalidOperationException("视频合成失败,未生成输出文件。"); } + } finally { - // 关键步骤 3: 清理临时文件 + // 清理临时目录 if (Directory.Exists(tempImageDir)) { Directory.Delete(tempImageDir, recursive: true); diff --git a/expand/dy.image/ImageMergeToVideoService.cs b/expand/dy.image/ImageMergeToVideoService.cs index c39c0c2..1ce7055 100644 --- a/expand/dy.image/ImageMergeToVideoService.cs +++ b/expand/dy.image/ImageMergeToVideoService.cs @@ -13,7 +13,18 @@ _downloadHelper = downloadHelper; _fFmpegHelper = fFmpegHelper; } - public async Task MergeToVideo(string rootPath, MediaMergeRequest request,string outputVideoPath,string fileNamefolder) + /// + /// 视频合成 + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task MergeToVideo(string rootPath, MediaMergeRequest request,string outputVideoPath,string fileNamefolder,bool mergeImg2Viedo,bool downImage=false,bool downMp3=false) { try @@ -31,17 +42,19 @@ } else { - for (int i = 0; i < rawImages.Count(); i++) + if(downImage) { - string sourcePath = rawImages[i]; - // 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png - string extension = Path.GetExtension(sourcePath); - string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0 - string destPath = Path.Combine(fileNamefolder, destFileName); - File.Copy(sourcePath, destPath); + for (int i = 0; i < rawImages.Count(); i++) + { + string sourcePath = rawImages[i]; + // 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png + string extension = Path.GetExtension(sourcePath); + string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0 + string destPath = Path.Combine(fileNamefolder, destFileName); + File.Copy(sourcePath, destPath); + } } } - // 2. 下载音频 var (rawAudios, audioError) = await DownloadMediaAsync(request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3"); if (!string.IsNullOrEmpty(audioError)) @@ -51,17 +64,25 @@ } else { - for (int i = 0; i < rawAudios.Count(); i++) + if(downMp3) { - string sourcePath = rawAudios[i]; - // 重命名为有规律的文件名,如 temp_001.mp3, temp_002.mp3 - string extension = Path.GetExtension(sourcePath); - string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0 - string destPath = Path.Combine(fileNamefolder, destFileName); - File.Copy(sourcePath, destPath); + for (int i = 0; i < rawAudios.Count(); i++) + { + string sourcePath = rawAudios[i]; + // 重命名为有规律的文件名,如 temp_001.mp3, temp_002.mp3 + string extension = Path.GetExtension(sourcePath); + string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0 + string destPath = Path.Combine(fileNamefolder, destFileName); + File.Copy(sourcePath, destPath); + } } } - + if (!mergeImg2Viedo) + { + // 不合成视频,直接返回成功 + Serilog.Log.Debug($"不合成视频,直接返回"); + return true; + } // 4. 合成视频 //var outputVideoPath = Path.Combine(tempDir, "output", $"merged-video.{request.OutputFormat.ToLower()}"); @@ -72,7 +93,7 @@ // 根据图片数量调整每张图片显示时长 if (request.ImageUrls.Count <= 3) { - request.ImageDurationPerSecond = 5; + request.ImageDurationPerSecond = 3; } if (request.ImageUrls.Count > 20) { diff --git a/extension/ServiceExtension.cs b/extension/ServiceExtension.cs index 79aaa6a..403d7e5 100644 --- a/extension/ServiceExtension.cs +++ b/extension/ServiceExtension.cs @@ -188,7 +188,7 @@ namespace dy.net.extension services.AddHttpClient("dy_uper", client => { - //client.DefaultRequestHeaders.Referrer = new Uri("https://www.douyin.com/user/self?showTab=like"); + client.DefaultRequestHeaders.Referrer = new Uri("https://www.douyin.com/user/MS4wLjABAAAA1zFIBhWG-3qS8MiggDMhyCpqDnvfGYf_mtsdgtBzV7A?from_tab_name=main&showTab=post&vid=7576282367263807451"); }); // 配置HttpClient(Startup.cs或Program.cs) services.AddHttpClient("dy_download") diff --git a/job/DouyinBaseSyncJob.cs b/job/DouyinBaseSyncJob.cs index 9bfacac..2e93fb8 100644 --- a/job/DouyinBaseSyncJob.cs +++ b/job/DouyinBaseSyncJob.cs @@ -4,6 +4,7 @@ using dy.net.dto; using dy.net.model; using dy.net.service; using dy.net.utils; +using Newtonsoft.Json; using Quartz; using Quartz.Util; using Serilog; @@ -133,6 +134,10 @@ namespace dy.net.job return; } + //将配置项打印日志 + Log.Debug($"最新:{JsonConvert.SerializeObject(config)}"); + + // 2. 从配置中获取每页请求数量 if (config.BatchCount > 0) count = config.BatchCount.ToString(); @@ -356,9 +361,12 @@ namespace dy.net.job // 如果配置了下载图片视频,则处理图片集并合成视频 if (_downImageVideo) { - var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config); - if (mergevideo != null) - videos.Add(mergevideo); + if(config.DownImage||config.DownMp3||config.DownImage) + { + var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config); + if (mergevideo != null) + videos.Add(mergevideo); + } } } return videos; @@ -404,9 +412,13 @@ namespace dy.net.job // 下载视频 if (!await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies)) { - Log.Error($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载失败"); + Log.Error($"{JobType}-{item?.Author?.Nickname??""}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载失败"); return null; } + else + { + Log.Debug($"{JobType}-{item?.Author?.Nickname ?? ""}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载完成"); + } // 下载视频封面 await DownVideoCover(item, saveFolder, cookie, config); @@ -455,7 +467,7 @@ namespace dy.net.job var imageService = _serviceProvider.GetService(); if (imageService == null) { - Log.Error($"{JobType} -图片视频同步-无法创建 ImageMergeToVideoService。"); + Log.Error($"{JobType} -图片视频同步-无法实例化 ImageMergeToVideoService。"); return null; } @@ -484,29 +496,35 @@ namespace dy.net.job }; // 执行图片合成视频操作 - var mergeResult = await imageService.MergeToVideo(AppContext.BaseDirectory, reqParams, savePath, fileNamefolder); + var mergeResult = await imageService.MergeToVideo(AppContext.BaseDirectory, reqParams, savePath, fileNamefolder,config.DownImageVideo,config.DownImage,config.DownMp3); if (!mergeResult) { Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败"); return null; } - // 检查合成后的视频文件是否有效 - if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0) + if (config.DownImageVideo) { - Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败"); - // 清理无效的文件和文件夹 - if (Directory.Exists(fileNamefolder)) + // 检查合成后的视频文件是否有效 + if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0) { - File.Delete(savePath); - Directory.Delete(fileNamefolder, true); - Log.Error($"{JobType}-图片视频同步-已删除合成失败的视频文件和目录"); + Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败"); + // 清理无效的文件和文件夹 + if (Directory.Exists(fileNamefolder)) + { + File.Delete(savePath); + Directory.Delete(fileNamefolder, true); + Log.Error($"{JobType}-图片视频同步-已删除合成失败的视频文件和目录"); + } + return null; } - return null; + } + else + { } - // 下载视频封面(使用第一张图片作为封面) - await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie, item, config); + // 下载视频封面(使用第一张图片作为封面) + await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie, item, config); // 下载作者头像 var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item); // 生成NFO文件 @@ -690,7 +708,7 @@ namespace dy.net.job if (!string.IsNullOrWhiteSpace(downImageVideoConfig)) { downImageVideoConfig = downImageVideoConfig.ToLower(); - _downImageVideo = config.DownImageVideo && (downImageVideoConfig == "1" || downImageVideoConfig == "y" || downImageVideoConfig == "t" || downImageVideoConfig == "true"); + _downImageVideo = config.DownImageVideo && downImageVideoConfig == "1"; } } diff --git a/model/AppConfig.cs b/model/AppConfig.cs index 425d017..77d8c17 100644 --- a/model/AppConfig.cs +++ b/model/AppConfig.cs @@ -33,6 +33,15 @@ namespace dy.net.model /// 是否下载图片视频 /// public bool DownImageVideo { get; set; } + /// + /// 图文视频 是否额外下载图片 + /// + public bool DownImage { get; set; } + /// + /// 图文视频 是否额外下载mp3 + /// + public bool DownMp3 { get; set; } + /// /// 日志保留天数,防止容器日志太多,默认10天 /// @@ -40,6 +49,7 @@ namespace dy.net.model [SugarColumn(IsIgnore=true)] public bool DownImageVideoFromEnv { get; set; } + } } diff --git a/service/DouyinCommonService.cs b/service/DouyinCommonService.cs index 2602008..6868c6f 100644 --- a/service/DouyinCommonService.cs +++ b/service/DouyinCommonService.cs @@ -50,8 +50,12 @@ namespace dy.net.service Id = IdGener.GetLong().ToString(), Cron = "30", BatchCount = 10, + LogKeepDay = 10, + UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹 + UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名 DownImageVideo = downLoadImage, - LogKeepDay = 10 + DownMp3 = false, + DownImage = false }; sqlSugarClient.Insertable(config).ExecuteCommand(); return config; @@ -77,6 +81,8 @@ namespace dy.net.service cc.UperSaveTogether = config.UperSaveTogether; cc.UperUseViedoTitle = config.UperUseViedoTitle; cc.LogKeepDay = config.LogKeepDay; + cc.DownMp3= config.DownMp3; + cc.DownImage= config.DownImage; } var update = await sqlSugarClient.Updateable(cc).ExecuteCommandAsync(); diff --git a/service/DouyinHttpClientService.cs b/service/DouyinHttpClientService.cs index d6daee2..778a299 100644 --- a/service/DouyinHttpClientService.cs +++ b/service/DouyinHttpClientService.cs @@ -184,7 +184,7 @@ namespace dy.net.service } httpClient.DefaultRequestHeaders.Add("Cookie", cookie); - var parameters = DouyinBaseParamDics.UpderPostParams; + var parameters = DouyinBaseParamDics.InitializeDouyinPostParams();//修复关注的不下载图文视频 { // 添加动态参数 parameters["max_cursor"] = cursor; @@ -195,7 +195,6 @@ namespace dy.net.service // 构建请求URL var queryString = new FormUrlEncodedContent(parameters); string fullUrl = $"{DouYinApi}/post?{await queryString.ReadAsStringAsync()}"; - var ablog = new ABogus();//计算X-Bogus var a_bogus = ablog.GetValue(parameters); diff --git a/service/DouyinQuartzJobService.cs b/service/DouyinQuartzJobService.cs index cd8431b..390caea 100644 --- a/service/DouyinQuartzJobService.cs +++ b/service/DouyinQuartzJobService.cs @@ -20,23 +20,23 @@ namespace dy.net.service /// public async Task StartJob(string expression,int delay=5000) { - await StartCollectJob(expression); + //await StartCollectJob(expression); - await Task.Delay(delay); - //如果是数字则加1分钟,减少并发 - if (int.TryParse(expression, out int cron)) - { - cron++; - expression = cron.ToString(); - } - await StartFavoriteJob(expression); + //await Task.Delay(delay); + ////如果是数字则加1分钟,减少并发 + //if (int.TryParse(expression, out int cron)) + //{ + // cron++; + // expression = cron.ToString(); + //} + //await StartFavoriteJob(expression); - await Task.Delay(delay); - if (int.TryParse(expression, out int cron2)) - { - cron2++; - expression = cron2.ToString(); - } + //await Task.Delay(delay); + //if (int.TryParse(expression, out int cron2)) + //{ + // cron2++; + // expression = cron2.ToString(); + //} await StartUperPostJob(expression); } diff --git a/utils/DouyinBaseParamDics.cs b/utils/DouyinBaseParamDics.cs index c0cf2d0..c60834f 100644 --- a/utils/DouyinBaseParamDics.cs +++ b/utils/DouyinBaseParamDics.cs @@ -1,4 +1,6 @@ -namespace dy.net.utils +using System.Threading.Channels; + +namespace dy.net.utils { public class DouyinBaseParamDics { @@ -67,14 +69,14 @@ { "browser_online", "true" }, { "browser_platform", "Win32" }, { "browser_version", "5.0%20%28Windows%29" }, - { "channel", "tiktok_web" }, { "cookie_enabled", "true" }, { "count", "18" }, { "coverFormat", "2" }, { "cursor", "0" }, { "data_collection_enabled", "true" }, { "device_id", "7380187414842836523" }, - { "device_platform", "web_pc" }, + { "device_platform", "webapp" }, + { "channel", "channel_pc_web" }, { "focus_state", "true" }, { "from_page", "user" }, { "history_len", "3" }, @@ -102,6 +104,92 @@ return parameters; } + + /// + /// 初始化抖音网页端用户作品列表请求参数(参数来源于目标URL) + /// 适配接口:https://www.douyin.com/aweme/v1/web/aweme/post/ + /// + /// 抖音作品列表请求参数字典 + public static Dictionary InitializeDouyinPostParams() + { + var parameters = new Dictionary + { + // 基础设备与渠道参数 + { "device_platform", "webapp" }, + { "aid", "6383" }, + { "channel", "channel_pc_web" }, + + // 用户标识参数 + { "sec_user_id", "" }, + + // 分页与内容定位参数 + { "max_cursor", "0" }, + { "locate_item_id", "7576282367263807451" }, + { "locate_query", "false" }, + { "count", "18" }, + + // 视频相关配置参数 + { "show_live_replay_strategy", "1" }, + { "need_time_list", "1" }, + { "time_list_query", "0" }, + { "publish_video_strategy_type", "2" }, + { "support_h265", "1" }, + { "support_dash", "1" }, + + // 版本与更新参数 + { "from_user_page", "1" }, + { "update_version_code", "170400" }, + { "version_code", "290100" }, + { "version_name", "29.1.0" }, + + // PC端特有参数 + { "pc_client_type", "1" }, + { "pc_libra_divert", "Windows" }, + { "cpu_core_num", "32" }, + + // 浏览器环境参数 + { "browser_language", "zh-CN" }, + { "browser_platform", "Win32" }, + { "browser_name", "Chrome" }, + { "browser_version", "142.0.0.0" }, + { "browser_online", "true" }, + { "engine_name", "Blink" }, + { "engine_version", "142.0.0.0" }, + + // 系统环境参数 + { "os_name", "Windows" }, + { "os_version", "10" }, + { "device_memory", "8" }, + { "platform", "PC" }, + { "cookie_enabled", "true" }, + + // 屏幕与网络参数 + { "screen_width", "1707" }, + { "screen_height", "1067" }, + { "downlink", "10" }, + { "effective_type", "4g" }, + { "round_trip_time", "0" }, + + // 用户唯一标识参数 + { "webid", "7574080345697584675" }, + { "uifid", "" }, + + // 安全验证与签名参数 + { "msToken", "" }, + { "a_bogus", "" }, + { "verifyFp", "" }, + { "fp", "" }, + { "x-secsdk-web-expire", "" }, + { "x-secsdk-web-signature", "" }, + + // 其他辅助参数 + { "whale_cut_token", "" }, + { "cut_version", "1" } + }; + + return parameters; + } + // 静态基础参数(供内部初始化使用) private static Dictionary GetDyBaseParameters() {