diff --git a/Properties/PublishProfiles/docker.pubxml.user b/Properties/PublishProfiles/docker.pubxml.user
index bd478f2..dd47be9 100644
--- a/Properties/PublishProfiles/docker.pubxml.user
+++ b/Properties/PublishProfiles/docker.pubxml.user
@@ -3,7 +3,7 @@
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\
- True|2026-02-11T12:39:23.8084775Z||;True|2026-02-11T20:37:06.6507806+08:00||;True|2026-02-11T20:04:19.1839991+08:00||;True|2026-02-10T21:59:37.6685615+08:00||;True|2026-02-10T15:43:23.5038986+08:00||;True|2026-02-10T14:42:48.6214001+08:00||;True|2026-02-06T23:15:19.0037070+08:00||;True|2026-02-06T22:52:30.1006808+08:00||;True|2026-02-06T22:49:02.0056235+08:00||;True|2026-02-06T22:45:30.6355735+08:00||;False|2026-02-06T22:45:25.7210123+08:00||;True|2026-02-06T22:38:37.6994272+08:00||;True|2026-02-06T22:14:47.8146805+08:00||;True|2026-02-03T22:34:12.3588819+08:00||;True|2026-02-03T22:20:08.9945012+08:00||;True|2026-02-03T21:06:17.7655312+08:00||;True|2026-02-02T23:24:09.9134119+08:00||;True|2026-02-02T23:21:52.3235946+08:00||;False|2026-02-02T23:21:47.1386702+08:00||;True|2026-02-02T23:20:46.6277206+08:00||;False|2026-02-02T23:20:40.3158947+08:00||;True|2026-02-02T22:55:39.4268475+08:00||;True|2026-02-01T21:34:30.5989260+08:00||;True|2026-02-01T21:30:20.7556066+08:00||;True|2026-01-30T21:56:47.1840381+08:00||;True|2026-01-28T22:01:00.8412391+08:00||;False|2026-01-28T21:49:29.1242233+08:00||;True|2026-01-28T21:48:26.1707204+08:00||;True|2026-01-28T21:48:05.8291762+08:00||;False|2026-01-28T21:48:01.1596391+08:00||;True|2026-01-28T21:37:59.4831763+08:00||;False|2026-01-28T21:37:53.5855464+08:00||;True|2026-01-28T21:27:51.1872828+08:00||;True|2026-01-28T13:01:14.1677037+08:00||;True|2026-01-28T12:36:45.7819776+08:00||;True|2026-01-28T12:09:15.6540204+08:00||;True|2026-01-28T12:08:40.0490617+08:00||;True|2026-01-28T11:58:55.4078870+08:00||;True|2026-01-27T22:37:03.3304942+08:00||;True|2026-01-27T22:05:40.4316281+08:00||;True|2026-01-26T22:30:43.5083924+08:00||;False|2026-01-26T22:30:33.7355213+08:00||;True|2026-01-26T22:26:26.0218548+08:00||;True|2026-01-26T22:21:32.3171217+08:00||;True|2026-01-26T22:20:40.3916780+08:00||;True|2026-01-26T22:16:19.1017273+08:00||;True|2026-01-26T22:13:34.2028883+08:00||;True|2026-01-26T21:46:23.7117812+08:00||;True|2026-01-26T21:41:19.6963033+08:00||;True|2026-01-25T21:27:13.9893777+08:00||;True|2026-01-24T21:45:22.8493624+08:00||;True|2026-01-24T21:31:35.4635265+08:00||;True|2026-01-24T21:27:31.3098506+08:00||;True|2026-01-24T15:47:01.6934638+08:00||;True|2026-01-24T15:32:35.6316009+08:00||;True|2026-01-24T15:16:28.7216639+08:00||;True|2026-01-23T22:14:26.8066324+08:00||;True|2026-01-23T13:36:48.4086406+08:00||;True|2026-01-23T13:30:27.2924204+08:00||;True|2026-01-23T13:02:53.3219368+08:00||;True|2026-01-23T13:01:54.3466638+08:00||;True|2026-01-23T13:01:28.8560809+08:00||;True|2026-01-22T23:18:32.0400436+08:00||;True|2026-01-22T22:29:29.5746209+08:00||;True|2026-01-22T21:32:21.7284081+08:00||;True|2026-01-22T21:20:26.5985955+08:00||;False|2026-01-22T21:20:21.1372539+08:00||;True|2026-01-22T21:11:18.3771739+08:00||;True|2026-01-22T20:58:55.9862778+08:00||;True|2026-01-22T20:51:48.6699461+08:00||;False|2026-01-22T20:51:42.2079210+08:00||;True|2026-01-22T20:50:34.7280114+08:00||;True|2026-01-22T20:50:11.1588492+08:00||;True|2026-01-22T16:11:14.0626037+08:00||;True|2026-01-22T15:53:16.6377347+08:00||;True|2026-01-22T15:45:34.0935091+08:00||;True|2026-01-22T12:44:28.1577014+08:00||;True|2026-01-22T12:40:45.7449335+08:00||;True|2026-01-22T12:39:07.0683214+08:00||;True|2026-01-22T09:55:49.7086190+08:00||;True|2026-01-22T01:37:53.6972395+08:00||;True|2026-01-22T01:34:14.2142463+08:00||;True|2026-01-22T01:32:50.9228911+08:00||;True|2026-01-22T01:29:43.8871986+08:00||;True|2026-01-22T01:29:40.7838709+08:00||;True|2026-01-22T01:27:34.4763970+08:00||;True|2026-01-22T01:26:55.0117367+08:00||;True|2026-01-22T01:23:19.4789587+08:00||;True|2026-01-19T10:19:44.8997385+08:00||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;True|2026-01-15T22:07:14.8243754+08:00||;
+ True|2026-02-12T04:18:02.1864306Z||;True|2026-02-11T20:39:23.8084775+08:00||;True|2026-02-11T20:37:06.6507806+08:00||;True|2026-02-11T20:04:19.1839991+08:00||;True|2026-02-10T21:59:37.6685615+08:00||;True|2026-02-10T15:43:23.5038986+08:00||;True|2026-02-10T14:42:48.6214001+08:00||;True|2026-02-06T23:15:19.0037070+08:00||;True|2026-02-06T22:52:30.1006808+08:00||;True|2026-02-06T22:49:02.0056235+08:00||;True|2026-02-06T22:45:30.6355735+08:00||;False|2026-02-06T22:45:25.7210123+08:00||;True|2026-02-06T22:38:37.6994272+08:00||;True|2026-02-06T22:14:47.8146805+08:00||;True|2026-02-03T22:34:12.3588819+08:00||;True|2026-02-03T22:20:08.9945012+08:00||;True|2026-02-03T21:06:17.7655312+08:00||;True|2026-02-02T23:24:09.9134119+08:00||;True|2026-02-02T23:21:52.3235946+08:00||;False|2026-02-02T23:21:47.1386702+08:00||;True|2026-02-02T23:20:46.6277206+08:00||;False|2026-02-02T23:20:40.3158947+08:00||;True|2026-02-02T22:55:39.4268475+08:00||;True|2026-02-01T21:34:30.5989260+08:00||;True|2026-02-01T21:30:20.7556066+08:00||;True|2026-01-30T21:56:47.1840381+08:00||;True|2026-01-28T22:01:00.8412391+08:00||;False|2026-01-28T21:49:29.1242233+08:00||;True|2026-01-28T21:48:26.1707204+08:00||;True|2026-01-28T21:48:05.8291762+08:00||;False|2026-01-28T21:48:01.1596391+08:00||;True|2026-01-28T21:37:59.4831763+08:00||;False|2026-01-28T21:37:53.5855464+08:00||;True|2026-01-28T21:27:51.1872828+08:00||;True|2026-01-28T13:01:14.1677037+08:00||;True|2026-01-28T12:36:45.7819776+08:00||;True|2026-01-28T12:09:15.6540204+08:00||;True|2026-01-28T12:08:40.0490617+08:00||;True|2026-01-28T11:58:55.4078870+08:00||;True|2026-01-27T22:37:03.3304942+08:00||;True|2026-01-27T22:05:40.4316281+08:00||;True|2026-01-26T22:30:43.5083924+08:00||;False|2026-01-26T22:30:33.7355213+08:00||;True|2026-01-26T22:26:26.0218548+08:00||;True|2026-01-26T22:21:32.3171217+08:00||;True|2026-01-26T22:20:40.3916780+08:00||;True|2026-01-26T22:16:19.1017273+08:00||;True|2026-01-26T22:13:34.2028883+08:00||;True|2026-01-26T21:46:23.7117812+08:00||;True|2026-01-26T21:41:19.6963033+08:00||;True|2026-01-25T21:27:13.9893777+08:00||;True|2026-01-24T21:45:22.8493624+08:00||;True|2026-01-24T21:31:35.4635265+08:00||;True|2026-01-24T21:27:31.3098506+08:00||;True|2026-01-24T15:47:01.6934638+08:00||;True|2026-01-24T15:32:35.6316009+08:00||;True|2026-01-24T15:16:28.7216639+08:00||;True|2026-01-23T22:14:26.8066324+08:00||;True|2026-01-23T13:36:48.4086406+08:00||;True|2026-01-23T13:30:27.2924204+08:00||;True|2026-01-23T13:02:53.3219368+08:00||;True|2026-01-23T13:01:54.3466638+08:00||;True|2026-01-23T13:01:28.8560809+08:00||;True|2026-01-22T23:18:32.0400436+08:00||;True|2026-01-22T22:29:29.5746209+08:00||;True|2026-01-22T21:32:21.7284081+08:00||;True|2026-01-22T21:20:26.5985955+08:00||;False|2026-01-22T21:20:21.1372539+08:00||;True|2026-01-22T21:11:18.3771739+08:00||;True|2026-01-22T20:58:55.9862778+08:00||;True|2026-01-22T20:51:48.6699461+08:00||;False|2026-01-22T20:51:42.2079210+08:00||;True|2026-01-22T20:50:34.7280114+08:00||;True|2026-01-22T20:50:11.1588492+08:00||;True|2026-01-22T16:11:14.0626037+08:00||;True|2026-01-22T15:53:16.6377347+08:00||;True|2026-01-22T15:45:34.0935091+08:00||;True|2026-01-22T12:44:28.1577014+08:00||;True|2026-01-22T12:40:45.7449335+08:00||;True|2026-01-22T12:39:07.0683214+08:00||;True|2026-01-22T09:55:49.7086190+08:00||;True|2026-01-22T01:37:53.6972395+08:00||;True|2026-01-22T01:34:14.2142463+08:00||;True|2026-01-22T01:32:50.9228911+08:00||;True|2026-01-22T01:29:43.8871986+08:00||;True|2026-01-22T01:29:40.7838709+08:00||;True|2026-01-22T01:27:34.4763970+08:00||;True|2026-01-22T01:26:55.0117367+08:00||;True|2026-01-22T01:23:19.4789587+08:00||;True|2026-01-19T10:19:44.8997385+08:00||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;
\ No newline at end of file
diff --git a/job/DouyinBasicSyncJob.cs b/job/DouyinBasicSyncJob.cs
index bf73082..3e869ce 100644
--- a/job/DouyinBasicSyncJob.cs
+++ b/job/DouyinBasicSyncJob.cs
@@ -6,6 +6,7 @@ using dy.net.service;
using dy.net.utils;
using Newtonsoft.Json;
using Quartz;
+using Quartz.Util;
using Serilog;
using System.Net;
@@ -513,6 +514,7 @@ namespace dy.net.job
///
/// 遍历视频列表,分别处理每个视频和图片集
///
+ ///
/// 用户Cookie
/// 视频信息对象
/// 应用配置
@@ -525,7 +527,7 @@ namespace dy.net.job
var videos = new List();
foreach (var item in data.AwemeList)
{
- //if (item.AwemeId != "7565895895016295726")
+ //if (item.AwemeId != "7321309610927770930")
//{
// continue;
//}
@@ -602,7 +604,7 @@ namespace dy.net.job
else
{
//处理多个视频-组合的图文视频--类似动图。
- List dynamicVideoUrls = new List();
+ List dynamicVideoUrls = new List();
// 当需要下载动态视频时,获取其他URL
if (config.DownDynamicVideo && item.Images != null && item.Images.Count > 0)
{
@@ -617,7 +619,7 @@ namespace dy.net.job
{
var height = btv.PlayAddr?.Height ?? 1920;
var width = btv.PlayAddr?.Width ?? 1080;
- DouyinDynamicVideoDto info = new DouyinDynamicVideoDto
+ DouyinMergeVideoDto info = new DouyinMergeVideoDto
{
Path = targetUrl,
Height = height,
@@ -639,7 +641,7 @@ namespace dy.net.job
{
if (!string.IsNullOrEmpty(dynamicVideo.DynamicVideos))
{
- var dynamicVideos = JsonConvert.DeserializeObject>(dynamicVideo.DynamicVideos);
+ var dynamicVideos = JsonConvert.DeserializeObject>(dynamicVideo.DynamicVideos);
Log.Debug($"[{cookie.UserName}][{VideoType.GetDesc()}]-动态视频[{item.Desc}],下载成功 ,共{dynamicVideos?.Count}个视频...");
if (dynamicVideos != null && dynamicVideos.Count > 0)
{
@@ -953,7 +955,7 @@ namespace dy.net.job
///
///
///
- protected async Task ProcessDynamicVideo(List dynamicUrls, DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed = null, DouyinCollectCate cate = null)
+ protected async Task ProcessDynamicVideo(List dynamicUrls, DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed = null, DouyinCollectCate cate = null)
{
// 创建保存文件夹
var saveFolder = CreateSaveFolder(cookie, item, config, followed, cate);
@@ -968,12 +970,12 @@ namespace dy.net.job
// 随机延迟
int i = 1;
- List dynamicSavePaths = new List();
+ List dynamicSavePaths = new List();
foreach (var dynamicUrl in dynamicUrls)
{
// 下载动态视频
var dynamicSavePath = savePath.Replace(".mp4", $"_00{i}.mp4");
- DouyinDynamicVideoDto v = new DouyinDynamicVideoDto() { Height = dynamicUrl.Height, Width = dynamicUrl.Width, Path = dynamicSavePath };
+ DouyinMergeVideoDto v = new DouyinMergeVideoDto() { Height = dynamicUrl.Height, Width = dynamicUrl.Width, Path = dynamicSavePath };
i++;
// 如果文件已存在,跳过
@@ -1087,10 +1089,10 @@ namespace dy.net.job
try
{
// 提取图片URL列表
- List imageUrls = item.Images?
+ List imageUrls = item.Images?
.Where(img => img.UrlList != null && img.UrlList.Any())
- .Select(img => img.UrlList.FirstOrDefault())
- .Where(url => !string.IsNullOrWhiteSpace(url))
+ .Select(img => new DouyinMergeVideoDto { Path = img.UrlList.FirstOrDefault(),Height=img.Height,Width=img.Width })
+ .Where(img => !string.IsNullOrWhiteSpace(img.Path))
.ToList();
// 如果没有图片,返回null
@@ -1169,8 +1171,8 @@ namespace dy.net.job
}
var coverUrl = cate is not null && cate.CateType != VideoTypeEnum.dy_custom_collect
- ? (item.MixInfo?.CoverUrl?.UrlList?.FirstOrDefault() ?? imageUrls.FirstOrDefault() ?? item.Music?.CoverHd?.UrlList?.FirstOrDefault())
- : imageUrls.FirstOrDefault();
+ ? (item.MixInfo?.CoverUrl?.UrlList?.FirstOrDefault() ?? imageUrls.FirstOrDefault()?.Path ?? item.Music?.CoverHd?.UrlList?.FirstOrDefault())
+ : imageUrls.FirstOrDefault()?.Path;
// 下载作者头像
var avatarSavePath = await DownAuthorAvatar(cookie, item);
@@ -1408,7 +1410,7 @@ namespace dy.net.job
/// 短剧、合集、自定义收藏夹
/// 创建的视频实体对象
private async Task CreateVideoEntity(AppConfig config,
- DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string coverSavePath, string avatorPath, List dynamicVideos = null, DouyinCollectCate cate = null)
+ DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string coverSavePath, string avatorPath, List dynamicVideos = null, DouyinCollectCate cate = null)
{
// 获取视频标签
var (tag1, tag2, tag3) = GetVideoTags(item);
diff --git a/model/dto/DouyinDynamicVideoDto.cs b/model/dto/DouyinDynamicVideoDto.cs
index ae5b764..5d1dd44 100644
--- a/model/dto/DouyinDynamicVideoDto.cs
+++ b/model/dto/DouyinDynamicVideoDto.cs
@@ -1,6 +1,6 @@
namespace dy.net.model.dto
{
- public class DouyinDynamicVideoDto
+ public class DouyinMergeVideoDto
{
public string Path { get; set; }
diff --git a/model/dto/MediaMergeRequest.cs b/model/dto/MediaMergeRequest.cs
index 5367b37..e8a6527 100644
--- a/model/dto/MediaMergeRequest.cs
+++ b/model/dto/MediaMergeRequest.cs
@@ -3,7 +3,7 @@
public class MediaMergeRequest
{
/// 网络图片地址数组(必填)
- public List ImageUrls { get; set; }
+ public List ImageUrls { get; set; }
/// 网络MP3地址数组(必填)
public List AudioUrls { get; set; }
diff --git a/service/DouyinMergeVideoService.cs b/service/DouyinMergeVideoService.cs
index 42fc3e1..e465f49 100644
--- a/service/DouyinMergeVideoService.cs
+++ b/service/DouyinMergeVideoService.cs
@@ -36,7 +36,7 @@ namespace dy.net.service
///
///
public async Task<(string mp4Path, string mp3Path)> MergeMultipleVideosAsync(
- List videoFilePaths,
+ List videoFilePaths,
string audioPath,
string savePath,
string ck)
@@ -45,11 +45,11 @@ namespace dy.net.service
string mergMusicPath = GetRandomMergeMusic();
if (!string.IsNullOrWhiteSpace(audioPath))
{
- var (SuccessPaths, _) = await DownloadMediaAsync(new List { audioPath }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck);
- if (SuccessPaths != null && SuccessPaths.Length > 0)
+ var (SuccessPaths, _) = await DownloadMediaAsync(new List { new DouyinMergeVideoDto { Path= audioPath } }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck);
+ if (SuccessPaths != null && SuccessPaths.Count > 0)
{
- mergMusicPath = SuccessPaths[0];
- mp3Path = SuccessPaths[0];
+ mergMusicPath = SuccessPaths.FirstOrDefault()?.Path;
+ mp3Path = SuccessPaths.FirstOrDefault()?.Path;
}
}
var ffmpeg = new FFmpegHelper();
@@ -126,7 +126,7 @@ namespace dy.net.service
// 1. 下载图片
var (rawImages, imageError) = await DownloadMediaAsync(
request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp", cookie);
- if (!string.IsNullOrEmpty(imageError) || rawImages == null || rawImages.Length == 0)
+ if (!string.IsNullOrEmpty(imageError) || rawImages == null || rawImages.Count == 0)
{
Log.Error($"图片下载失败:{imageError ?? "未下载到任何图片"}");
return false;
@@ -138,12 +138,12 @@ namespace dy.net.service
}
// 2. 下载音频
- string[] rawAudios = Array.Empty();
+ List rawAudios =new List();
if (mergeImg2Viedo && request.AudioUrls != null && request.AudioUrls.Count > 0)
{
var (audios, audioError) = await DownloadMediaAsync(
- request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
- if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Length == 0)
+ request.AudioUrls.Select(x => new DouyinMergeVideoDto { Path = x })?.ToList(), Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
+ if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Count == 0)
{
Log.Error($"音频下载失败:{audioError ?? "未下载到任何音频"}");
return false;
@@ -152,7 +152,7 @@ namespace dy.net.service
// 保存下载的音频(如果需要)
if (downMp3)
{
- var ext = Path.GetExtension(rawAudios[0]);
+ var ext = Path.GetExtension(rawAudios.FirstOrDefault()?.Path);
await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, ext, Path.GetFileNameWithoutExtension(outputVideoPath));
}
}
@@ -192,16 +192,16 @@ namespace dy.net.service
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300));
// 执行合成(确保每个图片都参与)
- if (rawAudios.Length == 0)
+ if (rawAudios.Count == 0)
{
var musicPath = GetRandomMergeMusic();
- rawAudios = new string[] { musicPath };
+ rawAudios = new List { new DouyinMergeVideoDto { Path= musicPath } };
Log.Debug("版权原因无法下载音频,使用默认无声音频文件");
}
string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync(
rawImages, // 所有下载的图片(确保无遗漏)
- rawAudios[0], // 取第一个音频(可根据需求调整)
+ rawAudios.FirstOrDefault()?.Path, // 取第一个音频(可根据需求调整)
outputVideoPath,
ffmpegHelper.VideoWidth,
ffmpegHelper.VideoHeight,
@@ -235,23 +235,23 @@ namespace dy.net.service
///
/// 保存下载的文件(图片/音频)到目标目录
///
- private static async Task SaveDownloadedFilesAsync(string[] sourcePaths, string targetFolder, string defaultExt, string videoFileName)
+ private static async Task SaveDownloadedFilesAsync(List sourcePaths, string targetFolder, string defaultExt, string videoFileName)
{
- if (sourcePaths == null || sourcePaths.Length == 0) return;
+ if (sourcePaths == null || sourcePaths.Count == 0) return;
Directory.CreateDirectory(targetFolder); // 确保目标目录存在
// 异步保存(不阻塞主线程)
await Task.WhenAll(sourcePaths.Select(async (sourcePath, index) =>
{
- if (!File.Exists(sourcePath)) return;
- string extension = Path.GetExtension(sourcePath) ?? $".{defaultExt}";
+ if (!File.Exists(sourcePath.Path)) return;
+ string extension = Path.GetExtension(sourcePath.Path) ?? $".{defaultExt}";
string destFileName = $"{videoFileName}{index + 1:D3}{extension}";
string destPath = Path.Combine(targetFolder, destFileName);
// 文件不存在时才复制,避免覆盖
if (!File.Exists(destPath))
{
- await Task.Run(() => File.Copy(sourcePath, destPath, overwrite: false));
+ await Task.Run(() => File.Copy(sourcePath.Path, destPath, overwrite: false));
Log.Debug($"已保存文件:{destPath}");
}
}));
@@ -326,23 +326,29 @@ namespace dy.net.service
/// 通用媒体下载方法
- private async Task<(string[] SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
- List urls, string saveDir, string prefix, string ext, string cookie)
+ private async Task<(List SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
+ List urls, string saveDir, string prefix, string ext, string cookie)
{
- var successPaths = new List();
+ var successPaths = new List();
for (var i = 0; i < urls.Count; i++)
{
var url = urls[i];
- var fileExt = ext ?? Path.GetExtension(url).TrimStart('.') ?? "png";
+ var fileExt = ext ?? Path.GetExtension(url.Path).TrimStart('.') ?? "png";
var fileName = $"{prefix}{i + 1}.{fileExt}";
var savePath = Path.Combine(saveDir, fileName);
try
{
- var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url, savePath, cookie);
+ var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url.Path, savePath, cookie);
if (Success)
{
- successPaths.Add(ActualSavePath);
+ successPaths.Add(new DouyinMergeVideoDto
+ {
+ Path = ActualSavePath,
+ Height = url.Height,
+ Width =
+ url.Width
+ });
}
else
{
@@ -354,10 +360,10 @@ namespace dy.net.service
{
var error = $"下载失败:{url},错误:{ex.Message}";
Serilog.Log.Error(error);
- return (Array.Empty(), error);
+ return (new List(), error);
}
}
- return (successPaths.ToArray(), null);
+ return (successPaths, null);
}
}
diff --git a/utils/FFmpegHelper.cs b/utils/FFmpegHelper.cs
index 654db2d..20ee074 100644
--- a/utils/FFmpegHelper.cs
+++ b/utils/FFmpegHelper.cs
@@ -51,14 +51,186 @@ namespace dy.net.utils
///
/// 将多张图片和一个音频文件合成为视频(最终终极版)。
///
+ //public async Task CreateVideoFromImagesAndAudioAsync(
+ // IEnumerable imageFilePaths,
+ // string audioFilePath,
+ // string outputVideoPath,
+ // int VideoWidth = 1080,
+ // int Height = 1920,
+ // IProgress progress = null,
+ // CancellationToken cancellationToken = default)
+ //{
+ // // 输入验证
+ // if (imageFilePaths == null || !imageFilePaths.Any())
+ // throw new ArgumentException("图片路径列表不能为空。", nameof(imageFilePaths));
+
+ // if (string.IsNullOrEmpty(audioFilePath) || !File.Exists(audioFilePath))
+ // throw new FileNotFoundException("音频文件未找到。", audioFilePath);
+
+ // if (string.IsNullOrEmpty(outputVideoPath))
+ // throw new ArgumentNullException(nameof(outputVideoPath));
+
+ // foreach (var imagePath in imageFilePaths)
+ // {
+ // if (!File.Exists(imagePath.Path))
+ // throw new FileNotFoundException("图片文件未找到。", imagePath.Path);
+ // }
+
+ // var outputDirectory = Path.GetDirectoryName(outputVideoPath);
+ // if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
+ // {
+ // 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].Path;
+ // string extension = Path.GetExtension(sourcePath);
+ // string destFileName = $"temp_{i + 1:D3}{extension}";
+ // string destPath = Path.Combine(tempImageDir, destFileName);
+ // File.Copy(sourcePath, destPath);
+ // }
+
+ // string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList.FirstOrDefault().Path));
+ // double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2);
+
+ // // --- 修正点 1: 整合音频时长获取逻辑 ---
+ // // 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环
+ // double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长
+ // try
+ // {
+ // // 使用 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)
+ // {
+ // Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
+ // }
+
+ // // 计算图片序列需要循环的次数
+ // int loopCount = 1;
+ // int imageCount = imageList.Count; // 要循环的图片总数(对应loop的size参数)
+ // double singleLoopDuration = imageCount * ImageDisplayDurationSeconds;
+
+ // if (audioDurationSeconds > singleLoopDuration)
+ // {
+ // loopCount = (int)Math.Ceiling(audioDurationSeconds / singleLoopDuration);
+ // //Console.WriteLine($"图片序列将循环 {loopCount} 次以匹配音频时长");
+ // }
+ // // H264编码要求宽高必须为偶数 20260103
+ // if (VideoWidth % 2 != 0)
+ // {
+ // VideoWidth++;
+ // }
+ // if (VideoHeight % 2 != 0)
+ // {
+ // VideoHeight++;
+ // }
+
+ // var arguments = new List
+ // {
+ // "-y", // 覆盖输出文件
+
+ // // 图片序列输入:保留原有逻辑,修复帧率含义(-r 是输出帧率,输入用-framerate更准确)
+ // "-f", "image2",
+ // "-framerate", imageFps.ToString(CultureInfo.InvariantCulture), // 输入帧率(图片播放速度)
+ // "-start_number", "0", // 序列起始编号
+ // "-i", imageSequencePattern, // 图片序列模板(无需引号,后续用ProcessStartInfo传参更安全)
+
+ // // 音频输入
+ // "-i", audioFilePath,
+
+ // // 滤镜修复:补充size参数(核心!解决Number of frames to loop is not set报错)
+ // // loop=循环次数:size=要循环的帧数;loop=-1表示无限循环(配合-shortest更简洁)
+ // "-filter_complex", $"[0:v]loop=loop={loopCount - 1}:size={imageCount}[v]",
+
+ // // 流映射
+ // "-map", "[v]",
+ // "-map", "1:a",
+
+ // // 视频编码:保留你的变量,确保兼容性
+ // "-c:v", VideoCodec,
+ // "-preset", VideoPreset,
+ // "-crf", $"{VideoCrf}",
+ // "-s", $"{VideoWidth}x{VideoHeight}",
+ // "-pix_fmt", "yuv420p", // 兼容所有播放器
+ // "-profile:v", "main",
+
+ // // 音频编码:标准化参数,避免无效值
+ // "-c:a", AudioCodec,
+ // "-b:a", $"{AudioBitrate}",
+ // "-ac", "2", // 立体声
+ // "-ar", "44100", // 标准采样率
+
+ // // 封装优化
+ // "-f", "mp4",
+ // "-movflags", "+faststart",
+
+ // // 同步参数:确保视频时长匹配音频
+ // "-shortest",
+
+ // // 输出路径
+ // outputVideoPath
+ // };
+
+ // // 执行命令
+ // await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
+ // if (File.Exists(outputVideoPath))
+ // {
+ // return outputVideoPath;
+ // }
+ // else
+ // {
+ // throw new InvalidOperationException("视频合成失败,未生成输出文件。");
+ // }
+ // }
+ // finally
+ // {
+ // // 清理临时目录
+ // if (Directory.Exists(tempImageDir))
+ // {
+ // Directory.Delete(tempImageDir, recursive: true);
+ // }
+ // }
+ //}
+
+
public async Task CreateVideoFromImagesAndAudioAsync(
- IEnumerable imageFilePaths,
- string audioFilePath,
- string outputVideoPath,
- int VideoWidth = 1080,
- int Height = 1920,
- IProgress progress = null,
- CancellationToken cancellationToken = default)
+ IEnumerable imageFilePaths,
+ string audioFilePath,
+ string outputVideoPath,
+ int VideoWidth = 1080,
+ int VideoHeight = 1920,
+ IProgress progress = null,
+ CancellationToken cancellationToken = default)
{
// 输入验证
if (imageFilePaths == null || !imageFilePaths.Any())
@@ -72,8 +244,11 @@ namespace dy.net.utils
foreach (var imagePath in imageFilePaths)
{
- if (!File.Exists(imagePath))
- throw new FileNotFoundException("图片文件未找到。", imagePath);
+ if (!File.Exists(imagePath.Path))
+ throw new FileNotFoundException("图片文件未找到。", imagePath.Path);
+ // 验证图片宽高字段有效性
+ if (imagePath.Width <= 0 || imagePath.Height <= 0)
+ throw new ArgumentException($"图片 {imagePath.Path} 的宽高必须大于0", nameof(imageFilePaths));
}
var outputDirectory = Path.GetDirectoryName(outputVideoPath);
@@ -91,22 +266,20 @@ namespace dy.net.utils
{
for (int i = 0; i < imageList.Count; i++)
{
- string sourcePath = imageList[i];
+ string sourcePath = imageList[i].Path;
string extension = Path.GetExtension(sourcePath);
string destFileName = $"temp_{i + 1:D3}{extension}";
string destPath = Path.Combine(tempImageDir, destFileName);
- File.Copy(sourcePath, destPath);
+ File.Copy(sourcePath, destPath, true);
}
- string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList[0]));
+ string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList.FirstOrDefault().Path));
double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2);
- // --- 修正点 1: 整合音频时长获取逻辑 ---
- // 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环
- double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长
+ // 获取音频时长
+ double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds;
try
{
- // 使用 ffprobe 获取音频时长
var startInfo = new ProcessStartInfo
{
FileName = _ffprobeExecutablePath,
@@ -126,103 +299,112 @@ namespace dy.net.utils
if (double.TryParse(output, out double duration))
{
audioDurationSeconds = duration;
- //Console.WriteLine($"成功获取音频时长: {audioDurationSeconds:F2}s");
}
}
}
catch (Exception ex)
{
- Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
+ Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
}
- // 计算图片序列需要循环的次数
+ // 计算图片序列循环次数
int loopCount = 1;
- int imageCount = imageList.Count; // 要循环的图片总数(对应loop的size参数)
+ int imageCount = imageList.Count;
double singleLoopDuration = imageCount * ImageDisplayDurationSeconds;
if (audioDurationSeconds > singleLoopDuration)
{
loopCount = (int)Math.Ceiling(audioDurationSeconds / singleLoopDuration);
- //Console.WriteLine($"图片序列将循环 {loopCount} 次以匹配音频时长");
- }
- // H264编码要求宽高必须为偶数 20260103
- if (VideoWidth % 2 != 0)
- {
- VideoWidth++;
- }
- if (VideoHeight % 2 != 0)
- {
- VideoHeight++;
}
+ // H264编码要求宽高为偶数
+ if (VideoWidth % 2 != 0) VideoWidth++;
+ if (VideoHeight % 2 != 0) VideoHeight++;
+
+ // ========== 核心优化:图片适配滤镜 ==========
+ // scale=iw*min({VideoWidth}/iw\,{VideoHeight}/ih):ih*min({VideoWidth}/iw\,{VideoHeight}/ih) 等比例缩放
+ // pad={VideoWidth}:{VideoHeight}:(ow-iw)/2:(oh-ih)/2:black 居中填充黑色背景
+ string fitFilter = $"scale=iw*min({VideoWidth}/iw\\,{VideoHeight}/ih):ih*min({VideoWidth}/iw\\,{VideoHeight}/ih),pad={VideoWidth}:{VideoHeight}:(ow-iw)/2:(oh-ih)/2:black";
+
+ // 组合最终滤镜:先适配尺寸,再循环
+ string filterComplex = $"[0:v]{fitFilter},loop=loop={loopCount - 1}:size={imageCount}[v]";
+
var arguments = new List
- {
- "-y", // 覆盖输出文件
+ {
+ "-y", // 覆盖输出文件
- // 图片序列输入:保留原有逻辑,修复帧率含义(-r 是输出帧率,输入用-framerate更准确)
- "-f", "image2",
- "-framerate", imageFps.ToString(CultureInfo.InvariantCulture), // 输入帧率(图片播放速度)
- "-start_number", "0", // 序列起始编号
- "-i", imageSequencePattern, // 图片序列模板(无需引号,后续用ProcessStartInfo传参更安全)
+ // 图片序列输入
+ "-f", "image2",
+ "-framerate", imageFps.ToString(CultureInfo.InvariantCulture),
+ "-start_number", "1", // 修正:临时文件是 temp_001、temp_002... 所以起始编号为1
+ "-i", imageSequencePattern,
- // 音频输入
- "-i", audioFilePath,
+ // 音频输入
+ "-i", audioFilePath,
- // 滤镜修复:补充size参数(核心!解决Number of frames to loop is not set报错)
- // loop=循环次数:size=要循环的帧数;loop=-1表示无限循环(配合-shortest更简洁)
- "-filter_complex", $"[0:v]loop=loop={loopCount - 1}:size={imageCount}[v]",
+ // 滤镜:尺寸适配 + 循环
+ "-filter_complex", filterComplex,
- // 流映射
- "-map", "[v]",
- "-map", "1:a",
+ // 流映射
+ "-map", "[v]",
+ "-map", "1:a",
- // 视频编码:保留你的变量,确保兼容性
- "-c:v", VideoCodec,
- "-preset", VideoPreset,
- "-crf", $"{VideoCrf}",
- "-s", $"{VideoWidth}x{VideoHeight}",
- "-pix_fmt", "yuv420p", // 兼容所有播放器
- "-profile:v", "main",
+ // 视频编码参数
+ "-c:v", VideoCodec,
+ "-preset", VideoPreset,
+ "-crf", $"{VideoCrf}",
+ "-s", $"{VideoWidth}x{VideoHeight}",
+ "-pix_fmt", "yuv420p",
+ "-profile:v", "main",
- // 音频编码:标准化参数,避免无效值
- "-c:a", AudioCodec,
- "-b:a", $"{AudioBitrate}",
- "-ac", "2", // 立体声
- "-ar", "44100", // 标准采样率
+ // 音频编码参数
+ "-c:a", AudioCodec,
+ "-b:a", $"{AudioBitrate}",
+ "-ac", "2",
+ "-ar", "44100",
- // 封装优化
- "-f", "mp4",
- "-movflags", "+faststart",
+ // 封装优化
+ "-f", "mp4",
+ "-movflags", "+faststart",
- // 同步参数:确保视频时长匹配音频
- "-shortest",
+ // 同步参数
+ "-shortest",
- // 输出路径
- outputVideoPath
- };
+ // 输出路径
+ outputVideoPath
+ };
- // 执行命令
+ // 执行FFmpeg命令
await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
- if (File.Exists(outputVideoPath))
- {
- return outputVideoPath;
- }
- else
+
+ if (!File.Exists(outputVideoPath))
{
throw new InvalidOperationException("视频合成失败,未生成输出文件。");
}
+
+ return outputVideoPath;
}
finally
{
- // 清理临时目录
+ // 清理临时目录(增加异常捕获,避免删除失败导致程序崩溃)
if (Directory.Exists(tempImageDir))
{
- Directory.Delete(tempImageDir, recursive: true);
+ try
+ {
+ Directory.Delete(tempImageDir, recursive: true);
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"清理临时目录失败: {ex.Message}");
+ }
}
}
}
+
+
+
///
/// 将单个视频转封装为统一编码/分辨率的临时视频(解决concat合并兼容问题)
///
@@ -232,7 +414,7 @@ namespace dy.net.utils
/// 取消令牌
/// 统一参数后的临时视频路径
private async Task CreateUnifiedTempVideoAsync(
- DouyinDynamicVideoDto videoDto,
+ DouyinMergeVideoDto videoDto,
int targetWidth, // 外部传入的动态目标宽(已取所有视频的最大值,偶数)
int targetHeight, // 外部传入的动态目标高(已取所有视频的最大值,偶数)
CancellationToken cancellationToken)
@@ -325,13 +507,13 @@ namespace dy.net.utils
/// 输出视频路径
// 移除了 width = 1080 和 height = 1920 两个传入参数
public async Task MergeMultipleVideosAsync(
- List videoFilePaths,
+ List videoFilePaths,
string audioPath,
string savePath,
IProgress progress = null,
CancellationToken cancellationToken = default)
{
- List validVideoList = new List();
+ List validVideoList = new List();
List tempUnifiedVideoPaths = new List();
// 输入验证