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(); // 输入验证