feat: add fnOS packaging, storage workflows and release pipeline

This commit is contained in:
2026-08-11 18:05:49 +08:00
parent c5922f9b08
commit 95932f0199
181 changed files with 24024 additions and 1164 deletions
+47 -20
View File
@@ -102,12 +102,14 @@ namespace dy.net.service
/// 视频合成(优化后:解决 FFmpeg 进程冲突,支持容错重试)
/// </summary>
public async Task<bool> MergeToVideo(string cookie, string rootPath, MediaMergeRequest request,
string outputVideoPath, string fileNamefolder, bool mergeImg2Viedo, bool downImage = false, bool downMp3 = false)
string outputVideoPath, string fileNamefolder, bool mergeImg2Viedo, bool downImage = false,
bool downMp3 = false, Action<string> reportWarning = null, Action<string> reportError = null)
{
// 输入参数校验(避免无效执行)
if (request == null || request.ImageUrls == null || request.ImageUrls.Count == 0)
{
Log.Error("图片URL列表为空,无法合成视频");
reportError?.Invoke("图片 URL 列表为空");
return false;
}
//if (mergeImg2Viedo && (request.AudioUrls == null || request.AudioUrls.Count == 0))
@@ -128,7 +130,9 @@ namespace dy.net.service
request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp", cookie);
if (!string.IsNullOrEmpty(imageError) || rawImages == null || rawImages.Count == 0)
{
Log.Error($"图片下载失败:{imageError ?? ""}");
var reason = imageError ?? "未下载到任何图片";
Log.Error($"图片下载失败:{reason}");
reportError?.Invoke($"图片下载失败:{reason}");
return false;
}
// 保存下载的图片(如果需要)
@@ -138,19 +142,25 @@ namespace dy.net.service
}
// 2. 下载音频
List<DouyinMergeVideoDto> rawAudios =new List<DouyinMergeVideoDto>();
if (mergeImg2Viedo && request.AudioUrls != null && request.AudioUrls.Count > 0)
List<DouyinMergeVideoDto> rawAudios = new List<DouyinMergeVideoDto>();
var audioWarningReported = false;
if ((mergeImg2Viedo || downMp3) && request.AudioUrls != null && request.AudioUrls.Count > 0)
{
var (audios, audioError) = await DownloadMediaAsync(
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;
var warning = downMp3
? "作品音频不可用,已降级处理,未生成单独音频文件"
: "作品音频不可用,图文视频已降级为无声视频";
Log.Warning("音频下载失败:{Reason}{Fallback}",
audioError ?? "未下载到任何音频", warning);
reportWarning?.Invoke(warning);
audioWarningReported = true;
}
rawAudios = audios;
else rawAudios = audios;
// 保存下载的音频(如果需要)
if (downMp3)
if (downMp3 && rawAudios.Count > 0)
{
var ext = Path.GetExtension(rawAudios.FirstOrDefault()?.Path);
await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, ext, Path.GetFileNameWithoutExtension(outputVideoPath));
@@ -161,7 +171,11 @@ namespace dy.net.service
if (!mergeImg2Viedo)
{
Log.Debug($"根据系统配置设置不下载图文视频-[{outputVideoPath}],{(downImage ? "" : "")},{(downMp3 ? "" : "")}");
return true;
// When only an audio attachment was requested, an unavailable audio source
// leaves no persistent artifact and must remain a real item-level failure.
var hasArtifact = downImage || !downMp3 || rawAudios.Count > 0;
if (!hasArtifact) reportError?.Invoke("音频下载失败,且当前任务未配置保存图片");
return hasArtifact;
}
// 3. 调整图片显示时长
@@ -194,14 +208,20 @@ namespace dy.net.service
if (rawAudios.Count == 0)
{
var musicPath = GetRandomMergeMusic();
rawAudios = new List<DouyinMergeVideoDto> { new DouyinMergeVideoDto { Path= musicPath } };
Log.Debug("版权原因无法下载音频,使用默认无声音频文件");
if (!audioWarningReported)
{
var warning = downMp3
? "作品没有可用音频,已生成无声视频,未生成单独音频文件"
: "作品没有可用音频,已生成无声视频";
reportWarning?.Invoke(warning);
audioWarningReported = true;
}
Log.Warning("图文作品没有可用音频,将直接生成无声视频");
}
string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync(
rawImages, // 所有下载的图片(确保无遗漏)
rawAudios.FirstOrDefault()?.Path, // 取第一个音频(可根据需求调整)
rawAudios.FirstOrDefault()?.Path, // 音频缺失时 FFmpegHelper 会生成无声视频
outputVideoPath,
ffmpegHelper.VideoWidth,
ffmpegHelper.VideoHeight,
@@ -218,11 +238,13 @@ namespace dy.net.service
}
});
if (!mergeSuccess) reportError?.Invoke("FFmpeg 未生成有效的视频文件");
return mergeSuccess;
}
catch (Exception ex)
{
Log.Error(ex, "视频合成过程中发生未处理异常");
reportError?.Invoke($"{ex.GetType().Name}{ex.GetBaseException().Message}");
return false;
}
finally
@@ -330,6 +352,7 @@ namespace dy.net.service
List<DouyinMergeVideoDto> urls, string saveDir, string prefix, string ext, string cookie)
{
var successPaths = new List<DouyinMergeVideoDto>();
var failures = new List<string>();
for (var i = 0; i < urls.Count; i++)
{
var url = urls[i];
@@ -339,12 +362,12 @@ namespace dy.net.service
try
{
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url.Path, savePath, cookie);
if (Success)
var result = await douyinHttpClientService.DownloadAsync(url.Path, savePath, cookie);
if (result.Success)
{
successPaths.Add(new DouyinMergeVideoDto
{
Path = ActualSavePath,
Path = result.ActualSavePath,
Height = url.Height,
Width =
url.Width
@@ -352,18 +375,22 @@ namespace dy.net.service
}
else
{
Serilog.Log.Error($"下载失败:{url} → {savePath}");
failures.Add($"{result.SourceHost ?? ""}/HTTP {result.HttpStatusCode?.ToString() ?? "-"}/{result.FailureKind}{result.Message}");
Serilog.Log.Warning("合成素材下载失败:Host={Host}, Status={Status}, Kind={Kind}, Target={Target}",
result.SourceHost, result.HttpStatusCode, result.FailureKind, savePath);
}
//Console.WriteLine($"下载成功:{url} → {savePath}");
}
catch (Exception ex)
{
var error = $"下载失败:{url},错误:{ex.Message}";
Serilog.Log.Error(error);
var error = $"合成素材下载失败:{ex.GetType().Name}{ex.GetBaseException().Message}";
Serilog.Log.Error("合成素材下载异常:Type={ExceptionType}, Target={Target}", ex.GetType().Name, savePath);
return (new List<DouyinMergeVideoDto>(), error);
}
}
return (successPaths, null);
return successPaths.Count == 0 && failures.Count > 0
? (successPaths, string.Join("", failures.Take(3)))
: (successPaths, null);
}
}