修复动态视频多个小动态图视频 合成失败bug
其他优化
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
||||||
<History>True|2026-01-12T14:04:14.5886955Z||;False|2026-01-12T22:04:08.2008101+08:00||;True|2026-01-12T21:53:46.9947176+08:00||;True|2026-01-12T21:11:15.8358024+08:00||;True|2026-01-12T21:09:51.8663228+08:00||;True|2026-01-11T21:25:07.5052845+08:00||;False|2026-01-11T21:24:27.5744557+08:00||;True|2026-01-11T19:59:15.4734611+08:00||;False|2026-01-11T19:59:04.9543339+08:00||;True|2026-01-11T18:51:30.6649269+08:00||;True|2026-01-08T21:07:57.5094466+08:00||;True|2026-01-08T14:51:18.2266231+08:00||;True|2026-01-08T14:31:55.1137120+08:00||;True|2026-01-08T00:16:24.4451490+08:00||;True|2026-01-08T00:14:20.7430793+08:00||;True|2026-01-08T00:07:46.7228194+08:00||;True|2026-01-07T23:52:13.3290082+08:00||;True|2026-01-07T23:49:28.4838650+08:00||;True|2026-01-07T23:47:45.1936189+08:00||;True|2026-01-07T23:35:09.7818611+08:00||;True|2026-01-07T23:20:43.7462863+08:00||;True|2026-01-07T23:04:39.5140429+08:00||;False|2026-01-07T23:04:36.5367611+08:00||;True|2026-01-07T22:53:14.6013449+08:00||;True|2026-01-07T22:49:59.9549006+08:00||;True|2026-01-07T22:36:40.0254856+08:00||;False|2026-01-07T22:36:28.8275903+08:00||;True|2026-01-07T21:59:51.4355171+08:00||;True|2026-01-03T21:38:51.4307034+08:00||;True|2026-01-02T19:09:16.9668906+08:00||;False|2026-01-02T19:09:11.7496369+08:00||;True|2026-01-02T15:42:08.2215697+08:00||;True|2026-01-02T09:46:56.1654861+08:00||;True|2026-01-02T09:35:28.0211225+08:00||;True|2026-01-01T23:07:01.9022045+08:00||;False|2026-01-01T23:06:56.0537216+08:00||;True|2026-01-01T22:16:10.2974067+08:00||;True|2026-01-01T22:16:06.2123787+08:00||;False|2026-01-01T22:15:33.3626979+08:00||;True|2026-01-01T22:01:30.7161900+08:00||;True|2026-01-01T21:10:19.3664263+08:00||;True|2026-01-01T21:09:38.6071080+08:00||;True|2025-12-30T11:45:54.5543034+08:00||;True|2025-12-30T09:19:08.3178124+08:00||;True|2025-12-29T18:57:29.7032246+08:00||;True|2025-12-27T14:52:24.4780776+08:00||;False|2025-12-27T14:52:19.5635794+08:00||;True|2025-12-27T14:48:01.6252748+08:00||;False|2025-12-27T14:47:55.7976192+08:00||;True|2025-12-27T14:38:23.9723838+08:00||;True|2025-12-27T13:00:29.8583858+08:00||;True|2025-12-26T22:18:42.4015637+08:00||;True|2025-12-26T22:10:58.5274572+08:00||;True|2025-12-26T22:06:26.3129600+08:00||;True|2025-12-26T22:03:55.6718618+08:00||;False|2025-12-26T22:03:48.3809954+08:00||;True|2025-12-26T22:02:33.1840390+08:00||;True|2025-12-26T22:01:16.1660100+08:00||;True|2025-12-25T00:50:26.1116465+08:00||;True|2025-12-25T00:48:27.3087708+08:00||;True|2025-12-25T00:47:38.4835720+08:00||;</History>
|
<History>True|2026-01-13T14:03:06.4683237Z||;False|2026-01-13T22:03:01.2036027+08:00||;True|2026-01-13T21:36:05.8776209+08:00||;True|2026-01-13T21:33:02.0766289+08:00||;False|2026-01-13T21:32:57.1976141+08:00||;True|2026-01-13T19:14:35.2519439+08:00||;True|2026-01-13T15:07:50.0836745+08:00||;False|2026-01-13T15:06:35.5929685+08:00||;True|2026-01-13T10:21:41.6012776+08:00||;True|2026-01-13T09:57:25.4788847+08:00||;True|2026-01-13T09:53:14.4900360+08:00||;True|2026-01-13T09:23:53.6465295+08:00||;True|2026-01-12T22:04:14.5886955+08:00||;False|2026-01-12T22:04:08.2008101+08:00||;True|2026-01-12T21:53:46.9947176+08:00||;True|2026-01-12T21:11:15.8358024+08:00||;True|2026-01-12T21:09:51.8663228+08:00||;True|2026-01-11T21:25:07.5052845+08:00||;False|2026-01-11T21:24:27.5744557+08:00||;True|2026-01-11T19:59:15.4734611+08:00||;False|2026-01-11T19:59:04.9543339+08:00||;True|2026-01-11T18:51:30.6649269+08:00||;True|2026-01-08T21:07:57.5094466+08:00||;True|2026-01-08T14:51:18.2266231+08:00||;True|2026-01-08T14:31:55.1137120+08:00||;True|2026-01-08T00:16:24.4451490+08:00||;True|2026-01-08T00:14:20.7430793+08:00||;True|2026-01-08T00:07:46.7228194+08:00||;True|2026-01-07T23:52:13.3290082+08:00||;True|2026-01-07T23:49:28.4838650+08:00||;True|2026-01-07T23:47:45.1936189+08:00||;True|2026-01-07T23:35:09.7818611+08:00||;True|2026-01-07T23:20:43.7462863+08:00||;True|2026-01-07T23:04:39.5140429+08:00||;False|2026-01-07T23:04:36.5367611+08:00||;True|2026-01-07T22:53:14.6013449+08:00||;True|2026-01-07T22:49:59.9549006+08:00||;True|2026-01-07T22:36:40.0254856+08:00||;False|2026-01-07T22:36:28.8275903+08:00||;True|2026-01-07T21:59:51.4355171+08:00||;True|2026-01-03T21:38:51.4307034+08:00||;True|2026-01-02T19:09:16.9668906+08:00||;False|2026-01-02T19:09:11.7496369+08:00||;True|2026-01-02T15:42:08.2215697+08:00||;True|2026-01-02T09:46:56.1654861+08:00||;True|2026-01-02T09:35:28.0211225+08:00||;True|2026-01-01T23:07:01.9022045+08:00||;False|2026-01-01T23:06:56.0537216+08:00||;True|2026-01-01T22:16:10.2974067+08:00||;True|2026-01-01T22:16:06.2123787+08:00||;False|2026-01-01T22:15:33.3626979+08:00||;True|2026-01-01T22:01:30.7161900+08:00||;True|2026-01-01T21:10:19.3664263+08:00||;True|2026-01-01T21:09:38.6071080+08:00||;True|2025-12-30T11:45:54.5543034+08:00||;True|2025-12-30T09:19:08.3178124+08:00||;True|2025-12-29T18:57:29.7032246+08:00||;True|2025-12-27T14:52:24.4780776+08:00||;False|2025-12-27T14:52:19.5635794+08:00||;True|2025-12-27T14:48:01.6252748+08:00||;False|2025-12-27T14:47:55.7976192+08:00||;True|2025-12-27T14:38:23.9723838+08:00||;True|2025-12-27T13:00:29.8583858+08:00||;True|2025-12-26T22:18:42.4015637+08:00||;True|2025-12-26T22:10:58.5274572+08:00||;True|2025-12-26T22:06:26.3129600+08:00||;True|2025-12-26T22:03:55.6718618+08:00||;False|2025-12-26T22:03:48.3809954+08:00||;True|2025-12-26T22:02:33.1840390+08:00||;True|2025-12-26T22:01:16.1660100+08:00||;True|2025-12-25T00:50:26.1116465+08:00||;True|2025-12-25T00:48:27.3087708+08:00||;True|2025-12-25T00:47:38.4835720+08:00||;</History>
|
||||||
<LastFailureDetails />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
// 原有代码不变,此处省略(仅保留模板相关修改,脚本逻辑无变化)
|
||||||
import { getBase64 } from '@/utils/file';
|
import { getBase64 } from '@/utils/file';
|
||||||
import { FormInstance } from 'ant-design-vue';
|
import { FormInstance } from 'ant-design-vue';
|
||||||
import { reactive, ref, onMounted, UnwrapRef, watch } from 'vue';
|
import { reactive, ref, onMounted, UnwrapRef, watch } from 'vue';
|
||||||
@@ -320,49 +321,50 @@ const downImgVideo = ref(true);
|
|||||||
|
|
||||||
<a-form-item label="我的secUserId" name="secUserId">
|
<a-form-item label="我的secUserId" name="secUserId">
|
||||||
<div style="display: flex; align-items: center; gap: 6px;">
|
<div style="display: flex; align-items: center; gap: 6px;">
|
||||||
<a-input v-model:value="form.secUserId" style="flex: 1;" />
|
<a-input v-model:value="form.secUserId" style="flex: 1;" placeholder="如果要同步“我喜欢”的视频和关注列表时,必填!!!" />
|
||||||
<a-tooltip title="同步“我喜欢的”视频时,必填!!!">
|
<a-tooltip title="如果要同步“我喜欢”的视频和关注列表时,必填!!!">
|
||||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 收藏的存储路径 -->
|
||||||
<a-form-item label="收藏的存储路径" name="savePath">
|
<a-form-item label="收藏的存储路径" name="savePath">
|
||||||
<div style="display: flex; align-items: center; gap: 6px;">
|
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||||
<a-input v-model:value="form.savePath" />
|
<a-input v-model:value="form.savePath" style="width: 200px;" />
|
||||||
|
<a-alert message="不想同步收藏的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<!-- 新增:是否全部用一个地址开关 -->
|
<!-- 新增:是否全部用一个地址开关 -->
|
||||||
<a-form-item label="统一存储路径" name="useSinglePath">
|
<a-form-item label="统一存储路径" name="useSinglePath">
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
<a-switch v-model:checked="form.useSinglePath" :checked-value="true" :un-checked-value="false" size="default" />
|
<a-switch v-model:checked="form.useSinglePath" :checked-value="true" :un-checked-value="false" size="default" />
|
||||||
<span>{{ form.useSinglePath ? '开启(共用收藏视频路径,如果是容器部署-docker-compose只需要映射一个路径就行了)' : '关闭(各路径独立配置)' }}</span>
|
<span>{{ form.useSinglePath ? '共用收藏视频路径,如果是容器部署-docker-compose配置,只需要映射一个路径就行了' : '各类视频路径分开独立配置' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 喜欢的存储路径 -->
|
||||||
<a-form-item label="喜欢的存储路径" name="favSavePath">
|
<a-form-item label="喜欢的存储路径" name="favSavePath">
|
||||||
<div style="display: flex; align-items: center; gap: 6px;">
|
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||||
<a-input v-model:value="form.favSavePath" :disabled="form.useSinglePath" placeholder="开启统一存储路径模式后将自动同步收藏路径的值" />
|
<a-input v-model:value="form.favSavePath" :disabled="form.useSinglePath" placeholder="" style="width: 200px;" />
|
||||||
<a-tooltip title="同步“我喜欢的”视频时,必填!!!">
|
<a-alert message="不想同步喜欢的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 关注的存储路径 -->
|
||||||
<a-form-item label="关注的存储路径" name="upSavePath">
|
<a-form-item label="关注的存储路径" name="upSavePath">
|
||||||
<div style="display: flex; align-items: center; gap: 6px;">
|
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||||
<a-input v-model:value="form.upSavePath" :disabled="form.useSinglePath" placeholder="开启统一存储路径模式后将自动同步收藏路径的值" style="flex: 1;" />
|
<a-input v-model:value="form.upSavePath" :disabled="form.useSinglePath" placeholder="开启统一存储路径模式后将自动同步收藏路径的值" style="width: 200px;" />
|
||||||
<a-tooltip title="同步指定博主视频时必填!!!">
|
<a-alert message="不想同步关注列表博主的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 图文的存储路径 -->
|
||||||
<a-form-item label="图文的存储路径" name="imgSavePath">
|
<a-form-item label="图文的存储路径" name="imgSavePath">
|
||||||
<div style="display: flex; align-items: center; gap: 6px;">
|
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||||
<a-input v-model:value="form.imgSavePath" :disabled="form.useSinglePath" placeholder="开启统一存储路径模式后将自动同步收藏路径的值" style="flex: 1;" />
|
<a-input v-model:value="form.imgSavePath" :disabled="form.useSinglePath" placeholder="开启统一存储路径模式后将自动同步收藏路径的值" style="width: 200px;" />
|
||||||
<a-tooltip title="同步图文视频必填!!!">
|
<a-alert message="如果系统配置页面开启了同步图文视频,且开启了单独存储,则必填!!!" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
|
||||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
@@ -522,4 +524,57 @@ const downImgVideo = ref(true);
|
|||||||
background-color: #f5f5f5 !important;
|
background-color: #f5f5f5 !important;
|
||||||
color: #666 !important;
|
color: #666 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 修复:移除对alert的自定义样式修改,还原官方默认样式 */
|
||||||
|
/* 删除了原有的ant-alert-small和ant-alert-message自定义样式,避免覆盖官方主题 */
|
||||||
|
/* 如需调整alert间距,仅修改外层容器,不修改alert内部样式 */
|
||||||
|
.alert-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 新增全局样式块,确保alert样式不受scoped影响(仅针对alert还原默认) -->
|
||||||
|
<style>
|
||||||
|
/* 还原Ant Design Vue alert官方默认样式,消除scoped样式的间接影响 */
|
||||||
|
.ant-alert {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
/* padding: 16px 16px; */
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714;
|
||||||
|
list-style: none;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.ant-alert-small {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
/* 还原alert不同类型的官方默认颜色 */
|
||||||
|
.ant-alert-info {
|
||||||
|
background-color: #e6f4ff;
|
||||||
|
border: 1px solid #91caff;
|
||||||
|
}
|
||||||
|
.ant-alert-info .ant-alert-message {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
.ant-alert-warning {
|
||||||
|
background-color: #fffbe6;
|
||||||
|
border: 1px solid #ffe58f;
|
||||||
|
}
|
||||||
|
.ant-alert-warning .ant-alert-message {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
/* 确保alert图标颜色同步官方样式 */
|
||||||
|
.ant-alert-info .ant-alert-icon {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
.ant-alert-warning .ant-alert-icon {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<template v-if="item.isEditing">
|
<template v-if="item.isEditing">
|
||||||
<div class="edit-input-group">
|
<div class="edit-input-group">
|
||||||
<!-- 输入框禁用:item.isSaving 为 true 时 -->
|
<!-- 输入框禁用:item.isSaving 为 true 时 -->
|
||||||
<a-input v-model:value="item.savePath" placeholder="请输入文件夹名称" @keypress.enter="() => handleSavePath(item)" maxlength="10" :disabled="item.isSaving" />
|
<a-input v-model:value="item.savePath" placeholder="请输入文件夹名称" @keypress.enter="() => handleSavePath(item)" maxlength="30" :disabled="item.isSaving" />
|
||||||
<!-- 保存按钮禁用:item.isSaving 为 true 时 -->
|
<!-- 保存按钮禁用:item.isSaving 为 true 时 -->
|
||||||
<a-button type="text" class="edit-btn" @click="() => handleSavePath(item)" :disabled="item.isSaving">
|
<a-button type="text" class="edit-btn" @click="() => handleSavePath(item)" :disabled="item.isSaving">
|
||||||
<SaveOutlined />
|
<SaveOutlined />
|
||||||
|
|||||||
@@ -5,23 +5,23 @@
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3 class="section-title">任务调度</h3>
|
<h3 class="section-title">任务调度</h3>
|
||||||
|
|
||||||
<a-form-item has-feedback label="同步周期(分钟)" name="Cron" :wrapper-col="{ span: 20 }" style="margin-left: 30px">
|
<a-form-item has-feedback label="同步周期" name="Cron" :wrapper-col="{ span: 20 }">
|
||||||
<a-input-number v-model:value="formState.Cron" placeholder="请输入数字" :min="15" />
|
<a-input-number v-model:value="formState.Cron" placeholder="请输入数字" :min="15" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>同步任务执行间隔,最小15分钟,建议使用默认值</span>
|
<span>同步任务执行间隔(分钟),最小15分钟,建议使用默认值</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item has-feedback label="每次同步上限" name="BatchCount" :wrapper-col="{ span: 20 }" style="margin-left: 30px">
|
<a-form-item has-feedback label="单次最多" name="BatchCount" :wrapper-col="{ span: 20 }">
|
||||||
<a-input-number v-model:value="formState.BatchCount" placeholder="请输入每次下载数量上限(最大30)" :min="10" :max="30" />
|
<a-input-number v-model:value="formState.BatchCount" placeholder="请输入每次下载数量上限(最大30)" :min="10" :max="30" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>每次最大下载数量:10-30</span>
|
<span>每次任务最多下载数量:10-30,拉取视频数据超出该值将会等待下次任务执行。</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item has-feedback label="仅同步最新视频" name="OnlySyncNew" :wrapper-col="{ span: 20 }">
|
<a-form-item has-feedback label="仅新视频" name="OnlySyncNew" :wrapper-col="{ span: 20 }">
|
||||||
<a-switch v-model:checked="formState.OnlySyncNew" />
|
<a-switch v-model:checked="formState.OnlySyncNew" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
@@ -34,24 +34,24 @@
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3 class="section-title">博主视频</h3>
|
<h3 class="section-title">博主视频</h3>
|
||||||
|
|
||||||
<a-form-item has-feedback label="标题当文件名" name="UperUseViedoTitle" :wrapper-col="{ span: 20 }">
|
<a-form-item has-feedback label="默认规则" name="UperUseViedoTitle" :wrapper-col="{ span: 20 }">
|
||||||
<a-switch v-model:checked="formState.UperUseViedoTitle" />
|
<a-switch v-model:checked="formState.UperUseViedoTitle" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>开启后,用原标题作为文件名,不开启但又没设置标题规则模板,则默认用视频id命名</span>
|
<span>开启后,将使用原标题作为文件名,未开启且未设置标题规则模板,则默认用视频id命名</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item has-feedback label="不创建子目录" name="UperSaveTogether" :wrapper-col="{ span: 20 }">
|
<a-form-item has-feedback label="统一存储" name="UperSaveTogether" :wrapper-col="{ span: 20 }">
|
||||||
<a-switch v-model:checked="formState.UperSaveTogether" />
|
<a-switch v-model:checked="formState.UperSaveTogether" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>默认按博主名建文件夹,开启后直接存映射目录根目录</span>
|
<span>开启后,所有博主视频直接都存一个根目录;关闭后,按博主名称创建文件夹单独存储。</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<!-- 模板相关配置:只有关闭"标题当文件名"时才显示 -->
|
<!-- 模板相关配置:只有关闭"标题当文件名"时才显示 -->
|
||||||
<a-form-item has-feedback label="定义标题模板" name="FollowedTitleTemplate" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
|
<a-form-item has-feedback label="标题模板" name="FollowedTitleTemplate" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
|
||||||
<a-select v-model:value="formState.FollowedTitleTemplate" :options="template_options" mode="multiple" size="middle" placeholder="请选择占位符组合"></a-select>
|
<a-select v-model:value="formState.FollowedTitleTemplate" :options="template_options" mode="multiple" size="middle" placeholder="请选择占位符组合"></a-select>
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<p></p>
|
<p></p>
|
||||||
@@ -62,27 +62,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item has-feedback label="模板分隔符" name="FollowedTitleSeparator" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
|
<a-form-item has-feedback label="分隔符" name="FollowedTitleSeparator" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
|
||||||
<a-input v-model:value="formState.FollowedTitleSeparator" placeholder="请输入分隔符(如 - _ 或空)" style="width: 200px" />
|
<a-input v-model:value="formState.FollowedTitleSeparator" placeholder="请输入分隔符(如 - _ 或空)" style="width: 200px" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>
|
<span>
|
||||||
占位符之间的连接符(如“-”“_”“.”),为空则直接拼接<br />
|
占位符之间的连接符(如“-”“_”“.”),为空则直接拼接,不要使用无法用来作为文件名得特殊字符
|
||||||
<strong class="text-gray-700">示例:</strong>选择{Author}和{VideoTitle},分隔符填“-”,将生成“博主名称-视频标题”格式文件名
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<!-- 完整模板预览:只有关闭"标题当文件名"时才显示 -->
|
<!-- 完整模板预览:只有关闭"标题当文件名"时才显示 -->
|
||||||
<a-form-item label="完整模板预览" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
|
<a-form-item label="标题预览" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
|
||||||
<a-input v-model:value="formState.FullFollowedTitleTemplate" placeholder="最终的模板" disabled style="background: #f5f5f5" />
|
<a-input v-model:value="formState.FullFollowedTitleTemplate" placeholder="自定义标题预览" disabled style="background: #f5f5f5" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
|
||||||
<span>
|
|
||||||
实时展示您选择的占位符和分隔符组合效果<br />
|
|
||||||
<strong class="text-gray-700">示例:</strong>选择{Author}、{ReleaseTime}、{Id},分隔符填“_”,预览结果为“博主名称_20250101_123456”
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -126,7 +118,7 @@
|
|||||||
<div class="audio-upload-player-wrapper" style="display: flex; align-items: center; gap: 16px;">
|
<div class="audio-upload-player-wrapper" style="display: flex; align-items: center; gap: 16px;">
|
||||||
<a-upload :before-upload="beforeUpload" :custom-request="customUpload" :show-upload-list="false" accept=".mp3,.wav">
|
<a-upload :before-upload="beforeUpload" :custom-request="customUpload" :show-upload-list="false" accept=".mp3,.wav">
|
||||||
<a-button type="default">
|
<a-button type="default">
|
||||||
<UploadOutlined /> 选择默认音频文件
|
<UploadOutlined /> 选择文件
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
|
|
||||||
@@ -156,22 +148,14 @@
|
|||||||
<a-switch v-model:checked="formState.DownDynamicVideo" />
|
<a-switch v-model:checked="formState.DownDynamicVideo" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>针对有些视频是多个视频生成的,实际是分为多个视频,开启后将会分别下载多个视频,名字带_001,002这样</span>
|
<span>开启后,将下载动态视频(多个短的小视频拼接-合成)</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="formState.DownDynamicVideo" has-feedback label="合并动态视频" name="MegDynamicVideo" :wrapper-col="{ span: 20 }">
|
<a-form-item v-if="formState.DownDynamicVideo" has-feedback label="保留原视频" name="KeepDynamicVideo" :wrapper-col="{ span: 20 }">
|
||||||
<a-switch v-model:checked="formState.MegDynamicVideo" />
|
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
|
||||||
<span>开启后将多个动态视频会合并为一个视频</span>
|
|
||||||
</div>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item v-if="formState.MegDynamicVideo" has-feedback label="保留原视频" name="KeepDynamicVideo" :wrapper-col="{ span: 20 }">
|
|
||||||
<a-switch v-model:checked="formState.KeepDynamicVideo" />
|
<a-switch v-model:checked="formState.KeepDynamicVideo" />
|
||||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||||
<span>开启后,将保留合成视频之前的每个短视频</span>
|
<span>开启后,将保留合成视频之前的每个短视频(动态视频会有多个独立的短视频文件,可以选择保留-但不建议,否则emby会出现很多没有封面的视频...)</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +164,7 @@
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3 class="section-title">视频去重</h3>
|
<h3 class="section-title">视频去重</h3>
|
||||||
|
|
||||||
<a-form-item v-show="formState.AutoDistinct" has-feedback label="去重优先等级" name="PriorityLevel" :wrapper-col="{ span: 20 }">
|
<a-form-item v-show="formState.AutoDistinct" has-feedback label="去重优先级" name="PriorityLevel" :wrapper-col="{ span: 20 }">
|
||||||
<!-- Tag 拖拽容器 -->
|
<!-- Tag 拖拽容器 -->
|
||||||
<div class="tag-drag-container">
|
<div class="tag-drag-container">
|
||||||
<!-- Tag 拖拽容器(绑定 ref 供 Sortable 初始化) -->
|
<!-- Tag 拖拽容器(绑定 ref 供 Sortable 初始化) -->
|
||||||
|
|||||||
+54
-48
@@ -140,7 +140,7 @@ namespace dy.net.job
|
|||||||
Log.Debug($"{VideoType}-未获取到系统配置,任务终止!!!");
|
Log.Debug($"{VideoType}-未获取到系统配置,任务终止!!!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 2. 从配置中获取每页请求数量--固定18
|
// 2. 从配置中获取每页请求数量--固定18
|
||||||
//if (config.BatchCount > 0)
|
//if (config.BatchCount > 0)
|
||||||
@@ -260,7 +260,7 @@ namespace dy.net.job
|
|||||||
/// <param name="syncCount">本次同步成功的视频数量</param>
|
/// <param name="syncCount">本次同步成功的视频数量</param>
|
||||||
/// <param name="followed"></param>
|
/// <param name="followed"></param>
|
||||||
/// <returns>一个表示异步操作的任务</returns>
|
/// <returns>一个表示异步操作的任务</returns>
|
||||||
protected abstract Task HandleSyncCompletion(DouyinCookie cookie, int syncCount,DouyinFollowed followed=null);
|
protected abstract Task HandleSyncCompletion(DouyinCookie cookie, int syncCount, DouyinFollowed followed = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取视频实体的差异信息
|
/// 获取视频实体的差异信息
|
||||||
@@ -308,7 +308,7 @@ namespace dy.net.job
|
|||||||
Log.Debug($"{VideoType}- Cookie-[{cookie.UserName}]开始同步...");
|
Log.Debug($"{VideoType}- Cookie-[{cookie.UserName}]开始同步...");
|
||||||
|
|
||||||
//up主上传视频特殊处理
|
//up主上传视频特殊处理
|
||||||
if (VideoType ==VideoTypeEnum.dy_follows)
|
if (VideoType == VideoTypeEnum.dy_follows)
|
||||||
{
|
{
|
||||||
int syncCount = 0; // 本次同步成功的视频数量
|
int syncCount = 0; // 本次同步成功的视频数量
|
||||||
string cursor = "0"; // 初始游标
|
string cursor = "0"; // 初始游标
|
||||||
@@ -335,7 +335,7 @@ namespace dy.net.job
|
|||||||
await GetViedos(cookie, config, syncCount, cursor, hasMore, followed);
|
await GetViedos(cookie, config, syncCount, cursor, hasMore, followed);
|
||||||
hasMore = true;
|
hasMore = true;
|
||||||
// 处理同步完成后的操作
|
// 处理同步完成后的操作
|
||||||
await HandleSyncCompletion(cookie, syncCount,followed);
|
await HandleSyncCompletion(cookie, syncCount, followed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -387,7 +387,7 @@ namespace dy.net.job
|
|||||||
// 5. 检查批次上限,若满足条件则终止循环(核心优化:抽离为方法)
|
// 5. 检查批次上限,若满足条件则终止循环(核心优化:抽离为方法)
|
||||||
if (IsSyncLimitReached(cookie, config, syncCount))
|
if (IsSyncLimitReached(cookie, config, syncCount))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
//随机等待
|
//随机等待
|
||||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
await Task.Delay(_random.Next(5, 10) * 1000);
|
||||||
@@ -414,7 +414,7 @@ namespace dy.net.job
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 获取当前视频类型对应的状态判断逻辑
|
// 获取当前视频类型对应的状态判断逻辑
|
||||||
if (!_syncStatusCheckMap.TryGetValue(VideoType, out var isSyncCompleted))
|
if (!_syncStatusCheckMap.TryGetValue(VideoType, out var isSyncCompleted))
|
||||||
{
|
{
|
||||||
@@ -445,7 +445,6 @@ namespace dy.net.job
|
|||||||
var videos = new List<DouyinVideo>();
|
var videos = new List<DouyinVideo>();
|
||||||
foreach (var item in data.AwemeList)
|
foreach (var item in data.AwemeList)
|
||||||
{
|
{
|
||||||
//if (!item.Desc.Contains("人生苦短"))
|
|
||||||
//if (item.AwemeId!= "7202236830337551616")
|
//if (item.AwemeId!= "7202236830337551616")
|
||||||
//{
|
//{
|
||||||
// continue;
|
// continue;
|
||||||
@@ -469,7 +468,8 @@ namespace dy.net.job
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//如果存在,但是因为去重规则,选择的最高优先级变更了,需要删除原来的,重新下载。
|
//如果存在,但是因为去重规则,选择的最高优先级变更了,需要删除原来的,重新下载。
|
||||||
if (exitVideo != null) {
|
if (exitVideo != null)
|
||||||
|
{
|
||||||
await douyinVideoService.DeleteById(exitVideo.Id);
|
await douyinVideoService.DeleteById(exitVideo.Id);
|
||||||
}
|
}
|
||||||
var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId);
|
var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId);
|
||||||
@@ -484,7 +484,8 @@ namespace dy.net.job
|
|||||||
var video = await ProcessSingleVideo(cookie, item, data, config, followed);
|
var video = await ProcessSingleVideo(cookie, item, data, config, followed);
|
||||||
if (video != null)
|
if (video != null)
|
||||||
videos.Add(video);
|
videos.Add(video);
|
||||||
else{
|
else
|
||||||
|
{
|
||||||
|
|
||||||
//处理多个视频-组合的图文视频--类似动图。
|
//处理多个视频-组合的图文视频--类似动图。
|
||||||
List<string> dynamicVideoUrls = new List<string>();
|
List<string> dynamicVideoUrls = new List<string>();
|
||||||
@@ -512,30 +513,35 @@ namespace dy.net.job
|
|||||||
var dynamicVideo = await ProcessDynamicVideo(dynamicVideoUrls, cookie, item, data, config);
|
var dynamicVideo = await ProcessDynamicVideo(dynamicVideoUrls, cookie, item, data, config);
|
||||||
if (dynamicVideo != null)
|
if (dynamicVideo != null)
|
||||||
{
|
{
|
||||||
if(!string.IsNullOrEmpty(dynamicVideo.DynamicVideos))
|
if (!string.IsNullOrEmpty(dynamicVideo.DynamicVideos))
|
||||||
{
|
{
|
||||||
var dynamicVideos = JsonConvert.DeserializeObject<List<string>>(dynamicVideo.DynamicVideos);
|
var dynamicVideos = JsonConvert.DeserializeObject<List<string>>(dynamicVideo.DynamicVideos);
|
||||||
Log.Debug($"{VideoType}-动态视频[{item.Desc}],下载成功 ,共{dynamicVideos?.Count}个视频...");
|
Log.Debug($"{VideoType}-动态视频[{item.Desc}],下载成功 ,共{dynamicVideos?.Count}个视频...");
|
||||||
if (config.MegDynamicVideo)
|
if (dynamicVideos != null && dynamicVideos.Count > 0)
|
||||||
{
|
{
|
||||||
if (dynamicVideos != null && dynamicVideos.Count > 0)
|
int width = 1080;
|
||||||
|
int height = 1920;
|
||||||
|
var bit = item?.Video?.BitRate?.FirstOrDefault();
|
||||||
|
if (bit != null)
|
||||||
|
{
|
||||||
|
width = bit.PlayAddr.Width;
|
||||||
|
height = bit.PlayAddr.Height;
|
||||||
|
}
|
||||||
|
var savePath = DouyinFileNameHelper.RemoveNumberSuffix(dynamicVideo.VideoSavePath);
|
||||||
|
|
||||||
|
//下载音频文件
|
||||||
|
var mp3Url= item?.Music?.PlayUrl?.UrlList?.FirstOrDefault();
|
||||||
|
|
||||||
|
|
||||||
|
var outPath = await douyinMergeVideoService.MergeMultipleVideosAsync(dynamicVideos,mp3Url, savePath,cookie.Cookies, width, height);
|
||||||
|
if(!string.IsNullOrWhiteSpace(outPath))
|
||||||
{
|
{
|
||||||
int width = 1080;
|
|
||||||
int height = 1920;
|
|
||||||
var bit = item?.Video?.BitRate?.FirstOrDefault();
|
|
||||||
if (bit != null)
|
|
||||||
{
|
|
||||||
width = bit.PlayAddr.Width;
|
|
||||||
height = bit.PlayAddr.Height;
|
|
||||||
}
|
|
||||||
var savePath = DouyinFileNameHelper.RemoveNumberSuffix(dynamicVideo.VideoSavePath);
|
|
||||||
var outPath= await douyinMergeVideoService.MergeMultipleVideosAsync(dynamicVideos, savePath, width, height);
|
|
||||||
if (File.Exists(outPath))
|
if (File.Exists(outPath))
|
||||||
{
|
{
|
||||||
dynamicVideo.VideoSavePath = outPath;
|
dynamicVideo.VideoSavePath = outPath;
|
||||||
|
|
||||||
if (!config.KeepDynamicVideo)
|
if (!config.KeepDynamicVideo)
|
||||||
{
|
{
|
||||||
|
//不保留原视频-删除
|
||||||
foreach (var opath in dynamicVideos)
|
foreach (var opath in dynamicVideos)
|
||||||
{
|
{
|
||||||
if (File.Exists(opath))
|
if (File.Exists(opath))
|
||||||
@@ -543,13 +549,10 @@ namespace dy.net.job
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
videos.Add(dynamicVideo);
|
videos.Add(dynamicVideo);
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -571,7 +574,7 @@ namespace dy.net.job
|
|||||||
return videos;
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> AutoDistinct(AppConfig config, Aweme item,DouyinCookie cookie,DouyinVideo exitVideo)
|
private async Task<bool> AutoDistinct(AppConfig config, Aweme item, DouyinCookie cookie, DouyinVideo exitVideo)
|
||||||
{
|
{
|
||||||
// 去重,检查视频是否已存在(按优先级下载)
|
// 去重,检查视频是否已存在(按优先级下载)
|
||||||
if (config.AutoDistinct)
|
if (config.AutoDistinct)
|
||||||
@@ -786,7 +789,7 @@ namespace dy.net.job
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc,item.AwemeId)}]开始下载...");
|
Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]开始下载...");
|
||||||
// 随机延迟
|
// 随机延迟
|
||||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||||
|
|
||||||
@@ -795,7 +798,7 @@ namespace dy.net.job
|
|||||||
if (!Success)
|
if (!Success)
|
||||||
{
|
{
|
||||||
Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]下载失败!!!");
|
Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]下载失败!!!");
|
||||||
|
|
||||||
(bool flowControl, DouyinVideo value) = await SwitchOtherUrlAddressDown(cookie, item, videoUrl, savePath);
|
(bool flowControl, DouyinVideo value) = await SwitchOtherUrlAddressDown(cookie, item, videoUrl, savePath);
|
||||||
if (!flowControl)
|
if (!flowControl)
|
||||||
{
|
{
|
||||||
@@ -810,25 +813,27 @@ namespace dy.net.job
|
|||||||
await DownVideoCover(item, saveFolder, cookie, config);
|
await DownVideoCover(item, saveFolder, cookie, config);
|
||||||
// 下载作者头像
|
// 下载作者头像
|
||||||
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
|
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
|
||||||
|
|
||||||
// 创建视频实体
|
// 创建视频实体
|
||||||
return await CreateVideoEntity(config,cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
|
return await CreateVideoEntity(config, cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected async Task<DouyinVideo> ProcessDynamicVideo(List<string> dynamicUrls,DouyinCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed = null)
|
protected async Task<DouyinVideo> ProcessDynamicVideo(List<string> dynamicUrls, DouyinCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
// 获取视频标签
|
// 获取视频标签
|
||||||
var (tag1, tag2, tag3) = GetVideoTags(item);
|
var (tag1, tag2, tag3) = GetVideoTags(item);
|
||||||
// 创建保存文件夹
|
// 创建保存文件夹
|
||||||
var saveFolder = CreateSaveFolder(cookie, item, config, followed);
|
var saveFolder = CreateSaveFolder(cookie, item, config, followed);
|
||||||
// 获取视频文件名
|
// 获取视频文件名
|
||||||
var fileName = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)+".mp4";
|
//var fileName = DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId) + ".mp4";
|
||||||
|
|
||||||
|
var fileName = $"{item.AwemeId}.mp4";
|
||||||
// 拼接视频保存路径
|
// 拼接视频保存路径
|
||||||
var savePath = Path.Combine(saveFolder, fileName);
|
var savePath = Path.Combine(saveFolder, fileName);
|
||||||
|
|
||||||
@@ -841,6 +846,7 @@ namespace dy.net.job
|
|||||||
{
|
{
|
||||||
// 下载动态视频
|
// 下载动态视频
|
||||||
var dynamicSavePath = savePath.Replace(".mp4", $"_00{i}.mp4");
|
var dynamicSavePath = savePath.Replace(".mp4", $"_00{i}.mp4");
|
||||||
|
i++;
|
||||||
|
|
||||||
// 如果文件已存在,跳过
|
// 如果文件已存在,跳过
|
||||||
if (File.Exists(dynamicSavePath))
|
if (File.Exists(dynamicSavePath))
|
||||||
@@ -852,13 +858,12 @@ namespace dy.net.job
|
|||||||
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(dynamicUrl, dynamicSavePath, cookie.Cookies);
|
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(dynamicUrl, dynamicSavePath, cookie.Cookies);
|
||||||
if (!Success)
|
if (!Success)
|
||||||
{
|
{
|
||||||
Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]-00{i},下载失败!!!");
|
Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{dynamicSavePath}]-00{i},下载失败!!!");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dynamicSavePaths.Add(dynamicSavePath);
|
dynamicSavePaths.Add(dynamicSavePath);
|
||||||
i++;
|
Log.Debug($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{dynamicSavePath}]-00{i},下载完成.");
|
||||||
Log.Debug($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]-00{i},下载完成.");
|
|
||||||
}
|
}
|
||||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||||
}
|
}
|
||||||
@@ -881,11 +886,11 @@ namespace dy.net.job
|
|||||||
DataSize = DouyinFileUtils.GetTotalFileSize(dynamicSavePaths) // 合成视频的文件大小
|
DataSize = DouyinFileUtils.GetTotalFileSize(dynamicSavePaths) // 合成视频的文件大小
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return await CreateVideoEntity(config,cookie, item, virtualBitRate, dynamicSavePaths.FirstOrDefault(), saveFolder, tag1, tag2, tag3,"", "", data,dynamicSavePaths);
|
return await CreateVideoEntity(config, cookie, item, virtualBitRate, dynamicSavePaths.FirstOrDefault(), saveFolder, tag1, tag2, tag3, "", "", data, dynamicSavePaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -929,7 +934,7 @@ namespace dy.net.job
|
|||||||
{
|
{
|
||||||
Log.Debug($"{VideoType}-额外找到{otherUrls.Count}个视频地址,即将再次开始下载...");
|
Log.Debug($"{VideoType}-额外找到{otherUrls.Count}个视频地址,即将再次开始下载...");
|
||||||
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(videoUrl, savePath, cookie.Cookies, otherUrls);
|
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(videoUrl, savePath, cookie.Cookies, otherUrls);
|
||||||
if(Success)
|
if (Success)
|
||||||
{
|
{
|
||||||
Log.Debug($"{VideoType}-尝试多个地址后,下载成功,{savePath}");
|
Log.Debug($"{VideoType}-尝试多个地址后,下载成功,{savePath}");
|
||||||
}
|
}
|
||||||
@@ -1028,7 +1033,7 @@ namespace dy.net.job
|
|||||||
var mergeResult = await douyinMergeVideoService.MergeToVideo(cookie.Cookies, AppContext.BaseDirectory, reqParams, savePath, fileNamefolder, config.DownImageVideo, config.DownImage, config.DownMp3);
|
var mergeResult = await douyinMergeVideoService.MergeToVideo(cookie.Cookies, AppContext.BaseDirectory, reqParams, savePath, fileNamefolder, config.DownImageVideo, config.DownImage, config.DownMp3);
|
||||||
if (!mergeResult)
|
if (!mergeResult)
|
||||||
{
|
{
|
||||||
Log.Error($"{VideoType}-图文视频-[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc,item.AwemeId)}]合成失败!!!");
|
Log.Error($"{VideoType}-图文视频-[{DouyinFileNameHelper.SanitizeLinuxFileName(item.Desc, item.AwemeId)}]合成失败!!!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1051,7 +1056,8 @@ namespace dy.net.job
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
savePath = "";
|
savePath = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,7 +1066,7 @@ namespace dy.net.job
|
|||||||
await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie, item, config);
|
await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie, item, config);
|
||||||
// 下载作者头像
|
// 下载作者头像
|
||||||
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
|
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
|
||||||
|
|
||||||
// 获取视频标签
|
// 获取视频标签
|
||||||
var (tag1, tag2, tag3) = GetVideoTags(item);
|
var (tag1, tag2, tag3) = GetVideoTags(item);
|
||||||
// 为合成的视频创建一个“虚拟”的BitRate对象,以便复用CreateVideoEntity方法
|
// 为合成的视频创建一个“虚拟”的BitRate对象,以便复用CreateVideoEntity方法
|
||||||
@@ -1075,14 +1081,14 @@ namespace dy.net.job
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 创建视频实体
|
// 创建视频实体
|
||||||
var videoEntity =await CreateVideoEntity(config,
|
var videoEntity = await CreateVideoEntity(config,
|
||||||
cookie, item, virtualBitRate, savePath, fileNamefolder,
|
cookie, item, virtualBitRate, savePath, fileNamefolder,
|
||||||
tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
|
tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
|
||||||
|
|
||||||
// 特殊处理合成视频的字段
|
// 特殊处理合成视频的字段
|
||||||
videoEntity.FileHash = string.Empty; // 合成视频没有原始文件哈希
|
videoEntity.FileHash = string.Empty; // 合成视频没有原始文件哈希
|
||||||
videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
|
videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
|
||||||
videoEntity.ViedoType = VideoType;
|
videoEntity.ViedoType = VideoType;
|
||||||
videoEntity.IsMergeVideo = 1;// 标记为图片合成视频
|
videoEntity.IsMergeVideo = 1;// 标记为图片合成视频
|
||||||
|
|
||||||
return videoEntity;
|
return videoEntity;
|
||||||
@@ -1117,7 +1123,7 @@ namespace dy.net.job
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 下载视频封面
|
/// 下载视频封面
|
||||||
@@ -1248,7 +1254,7 @@ namespace dy.net.job
|
|||||||
// 如果封面文件不存在,则下载
|
// 如果封面文件不存在,则下载
|
||||||
if (!File.Exists(coverSavePath))
|
if (!File.Exists(coverSavePath))
|
||||||
{
|
{
|
||||||
await douyinHttpClientService.DownloadAsync(coverUrl, coverSavePath, cookie.Cookies);
|
await douyinHttpClientService.DownloadAsync(coverUrl, coverSavePath, cookie.Cookies);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1272,7 +1278,7 @@ namespace dy.net.job
|
|||||||
/// <returns>创建的视频实体对象</returns>
|
/// <returns>创建的视频实体对象</returns>
|
||||||
private async Task<DouyinVideo> CreateVideoEntity(AppConfig config,
|
private async Task<DouyinVideo> CreateVideoEntity(AppConfig config,
|
||||||
DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder,
|
DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder,
|
||||||
string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data,List<string> dynamicVideos=null)
|
string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data, List<string> dynamicVideos = null)
|
||||||
{
|
{
|
||||||
var diffs = GetVideoEntityDifferences(cookie, item);
|
var diffs = GetVideoEntityDifferences(cookie, item);
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ namespace dy.net.service
|
|||||||
OnlySyncNew = true,
|
OnlySyncNew = true,
|
||||||
DownDynamicVideo = false,
|
DownDynamicVideo = false,
|
||||||
KeepDynamicVideo = false,
|
KeepDynamicVideo = false,
|
||||||
MegDynamicVideo = false
|
MegDynamicVideo = true
|
||||||
};
|
};
|
||||||
sqlSugarClient.Insertable(config).ExecuteCommand();
|
sqlSugarClient.Insertable(config).ExecuteCommand();
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@@ -30,17 +30,48 @@ namespace dy.net.service
|
|||||||
/// 多视频合成一个视频
|
/// 多视频合成一个视频
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="videoFilePaths"></param>
|
/// <param name="videoFilePaths"></param>
|
||||||
|
/// <param name="audioPath"></param>
|
||||||
/// <param name="savePath"></param>
|
/// <param name="savePath"></param>
|
||||||
|
/// <param name="ck"></param>
|
||||||
/// <param name="width"></param>
|
/// <param name="width"></param>
|
||||||
/// <param name="height"></param>
|
/// <param name="height"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<string> MergeMultipleVideosAsync(
|
public async Task<string> MergeMultipleVideosAsync(
|
||||||
List<string> videoFilePaths,
|
List<string> videoFilePaths,
|
||||||
|
string audioPath,
|
||||||
string savePath,
|
string savePath,
|
||||||
|
string ck,
|
||||||
int width = 1080,
|
int width = 1080,
|
||||||
int height = 1920)
|
int height = 1920)
|
||||||
{
|
{
|
||||||
return await new FFmpegHelper().MergeMultipleVideosAsync(videoFilePaths, savePath, width, height);
|
string mp3FilePath = GetDefaultAudio();
|
||||||
|
if (!string.IsNullOrWhiteSpace(audioPath))
|
||||||
|
{
|
||||||
|
var (SuccessPaths, _) = await DownloadMediaAsync(new List<string> { audioPath }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck);
|
||||||
|
if (SuccessPaths != null && SuccessPaths.Length > 0)
|
||||||
|
{
|
||||||
|
mp3FilePath = SuccessPaths[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ffmpeg = new FFmpegHelper();
|
||||||
|
return await ffmpeg.MergeMultipleVideosAsync(videoFilePaths, mp3FilePath, savePath, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static string GetDefaultAudio()
|
||||||
|
{
|
||||||
|
string mp3FilePath = Path.Combine(AppContext.BaseDirectory, "mp3", "silent_10.mp3");
|
||||||
|
var uploadMp3 = Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "mp3"))
|
||||||
|
.Where(filePath => Path.GetFileNameWithoutExtension(filePath) != "silent_10")
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (!string.IsNullOrWhiteSpace(uploadMp3) && File.Exists(uploadMp3))
|
||||||
|
{
|
||||||
|
mp3FilePath = uploadMp3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mp3FilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -140,15 +171,7 @@ namespace dy.net.service
|
|||||||
|
|
||||||
if (rawAudios.Length == 0)
|
if (rawAudios.Length == 0)
|
||||||
{
|
{
|
||||||
|
var mp3Path = GetDefaultAudio();
|
||||||
var mp3Path = Path.Combine(AppContext.BaseDirectory, "mp3", "silent_10.mp3");
|
|
||||||
var uploadMp3 = Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "mp3"))
|
|
||||||
.Where(filePath => Path.GetFileNameWithoutExtension(filePath) != "silent_10")
|
|
||||||
.FirstOrDefault();
|
|
||||||
if (!string.IsNullOrWhiteSpace(uploadMp3) && File.Exists(uploadMp3))
|
|
||||||
{
|
|
||||||
mp3Path = uploadMp3;
|
|
||||||
}
|
|
||||||
rawAudios = new string[] { mp3Path };
|
rawAudios = new string[] { mp3Path };
|
||||||
Log.Debug("版权原因无法下载音频,使用默认无声音频文件");
|
Log.Debug("版权原因无法下载音频,使用默认无声音频文件");
|
||||||
}
|
}
|
||||||
@@ -269,7 +292,7 @@ namespace dy.net.service
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 调整图片显示时长
|
/// 调整图片显示时长
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AdjustImageDuration(MediaMergeRequest request)
|
private static void AdjustImageDuration(MediaMergeRequest request)
|
||||||
{
|
{
|
||||||
if (request.ImageUrls.Count <= 3)
|
if (request.ImageUrls.Count <= 3)
|
||||||
request.ImageDurationPerSecond = 3;
|
request.ImageDurationPerSecond = 3;
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ namespace dy.net.utils
|
|||||||
// 2. 过滤 Linux 非法字符:
|
// 2. 过滤 Linux 非法字符:
|
||||||
// - 禁止:/(路径分隔符)、\0(空字符)
|
// - 禁止:/(路径分隔符)、\0(空字符)
|
||||||
// - 替换:其他特殊字符(如 :*?"<>|\\ )为下划线 _,避免创建失败
|
// - 替换:其他特殊字符(如 :*?"<>|\\ )为下划线 _,避免创建失败
|
||||||
var invalidChars = new[] { '/', '\0', ':', '*', '?', '"', '<', '>', '|', '\\' };
|
//var invalidChars = new[] { '/', '\0', ':', '*', '?', '"', '<', '>', '|', '\\' };
|
||||||
|
|
||||||
|
var invalidChars = new[] {
|
||||||
|
'/', '\0', // Linux 核心非法字符
|
||||||
|
'\n', '\r', '\v', '\f', // 所有换行/回车/制表类控制字符
|
||||||
|
':', '*', '?', '"', '<', '>', '|', '\\' // 其他常见非法字符
|
||||||
|
};
|
||||||
string sanitizedName = originalName;
|
string sanitizedName = originalName;
|
||||||
foreach (var c in invalidChars)
|
foreach (var c in invalidChars)
|
||||||
{
|
{
|
||||||
@@ -82,19 +88,20 @@ namespace dy.net.utils
|
|||||||
/// <returns>true:无特殊字符(仅允许字符);false:含有特殊字符</returns>
|
/// <returns>true:无特殊字符(仅允许字符);false:含有特殊字符</returns>
|
||||||
public static bool IsValidWithoutSpecialChars(string input)
|
public static bool IsValidWithoutSpecialChars(string input)
|
||||||
{
|
{
|
||||||
// 空字符串默认返回 true(若需禁止空字符串,可先判断 string.IsNullOrWhiteSpace(input) 并返回 false)
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
if (string.IsNullOrEmpty(input))
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// 正则表达式说明:
|
// 正则表达式说明:
|
||||||
// ^ :匹配字符串开头
|
// ^ :匹配字符串开头
|
||||||
// $ :匹配字符串结尾
|
// $ :匹配字符串结尾
|
||||||
// [a-zA-Z0-9\u4E00-\u9FA5] :允许的字符范围
|
// [a-zA-Z0-9\u4E00-\u9FA5 _] :允许的字符范围(新增空格和下划线)
|
||||||
// a-zA-Z:大小写字母
|
// a-zA-Z:大小写字母
|
||||||
// 0-9:数字
|
// 0-9:数字
|
||||||
// \u4E00-\u9FA5:简体中文 Unicode 核心范围(覆盖99%+简体中文常用字)
|
// \u4E00-\u9FA5:简体中文 Unicode 核心范围
|
||||||
|
// 空格:普通空格字符
|
||||||
|
// _:下划线
|
||||||
// * :匹配 0 个或多个允许的字符(若需至少1个字符,可改为 +)
|
// * :匹配 0 个或多个允许的字符(若需至少1个字符,可改为 +)
|
||||||
const string pattern = @"^[a-zA-Z0-9\u4E00-\u9FA5]*$";
|
const string pattern = @"^[a-zA-Z0-9\u4E00-\u9FA5 _]*$";
|
||||||
|
|
||||||
// 忽略文化差异,仅按字符编码匹配
|
// 忽略文化差异,仅按字符编码匹配
|
||||||
return Regex.IsMatch(input, pattern, RegexOptions.None);
|
return Regex.IsMatch(input, pattern, RegexOptions.None);
|
||||||
|
|||||||
+78
-27
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Serilog;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -13,7 +14,7 @@ namespace dy.net.utils
|
|||||||
|
|
||||||
|
|
||||||
private string _ffmpegExecutablePath;
|
private string _ffmpegExecutablePath;
|
||||||
private string _ffprobeExecutablePath ;
|
private string _ffprobeExecutablePath;
|
||||||
|
|
||||||
public FFmpegHelper()
|
public FFmpegHelper()
|
||||||
{
|
{
|
||||||
@@ -24,15 +25,15 @@ namespace dy.net.utils
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// Debug 环境,通常是 Windows
|
// Debug 环境,通常是 Windows
|
||||||
_ffmpegExecutablePath = "E:\\down\\ffmpeg\\bin\\ffmpeg.exe";
|
_ffmpegExecutablePath = "E:\\down\\ffmpeg\\bin\\ffmpeg.exe";
|
||||||
_ffprobeExecutablePath = "E:\\down\\ffmpeg\\bin\\ffprobe.exe";
|
_ffprobeExecutablePath = "E:\\down\\ffmpeg\\bin\\ffprobe.exe";
|
||||||
#else
|
#else
|
||||||
// Release 环境,通常是 Docker Linux
|
// Release 环境,通常是 Docker Linux
|
||||||
_ffmpegExecutablePath = "ffmpeg";
|
_ffmpegExecutablePath = "ffmpeg";
|
||||||
_ffprobeExecutablePath = "ffprobe";
|
_ffprobeExecutablePath = "ffprobe";
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,10 +230,12 @@ namespace dy.net.utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合并多个视频文件为一个MP4视频
|
/// 合并多个视频文件为一个MP4视频(嵌入指定音频文件)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="videoFilePaths">待合并的视频路径列表(按合并顺序排列)</param>
|
/// <param name="videoFilePaths">待合并的视频路径列表(按合并顺序排列)</param>
|
||||||
|
/// <param name="audioPath">音频文件(可为null/空,为空则保留视频原音频(若有))</param>
|
||||||
/// <param name="savePath">输出视频的保存路径</param>
|
/// <param name="savePath">输出视频的保存路径</param>
|
||||||
/// <param name="width">输出视频宽度(自动修正为偶数)</param>
|
/// <param name="width">输出视频宽度(自动修正为偶数)</param>
|
||||||
/// <param name="height">输出视频高度(自动修正为偶数)</param>
|
/// <param name="height">输出视频高度(自动修正为偶数)</param>
|
||||||
@@ -241,6 +244,7 @@ namespace dy.net.utils
|
|||||||
/// <returns>输出视频路径</returns>
|
/// <returns>输出视频路径</returns>
|
||||||
public async Task<string> MergeMultipleVideosAsync(
|
public async Task<string> MergeMultipleVideosAsync(
|
||||||
List<string> videoFilePaths,
|
List<string> videoFilePaths,
|
||||||
|
string audioPath,
|
||||||
string savePath,
|
string savePath,
|
||||||
int width = 1080,
|
int width = 1080,
|
||||||
int height = 1920,
|
int height = 1920,
|
||||||
@@ -260,6 +264,11 @@ namespace dy.net.utils
|
|||||||
if (string.IsNullOrEmpty(savePath))
|
if (string.IsNullOrEmpty(savePath))
|
||||||
throw new ArgumentNullException(nameof(savePath));
|
throw new ArgumentNullException(nameof(savePath));
|
||||||
|
|
||||||
|
// 音频文件验证(若传入非空路径,则校验文件是否存在)
|
||||||
|
bool hasCustomAudio = !string.IsNullOrEmpty(audioPath) && File.Exists(audioPath);
|
||||||
|
//if (!string.IsNullOrEmpty(audioPath) && !hasCustomAudio)
|
||||||
|
// throw new FileNotFoundException("指定的音频文件未找到。", audioPath);
|
||||||
|
|
||||||
// 自动修正分辨率为偶数(H264编码要求)
|
// 自动修正分辨率为偶数(H264编码要求)
|
||||||
if (width % 2 != 0) width++;
|
if (width % 2 != 0) width++;
|
||||||
if (height % 2 != 0) height++;
|
if (height % 2 != 0) height++;
|
||||||
@@ -282,49 +291,91 @@ namespace dy.net.utils
|
|||||||
string escapedPath = videoPath.Replace("\\", "/").Replace("'", "\\'");
|
string escapedPath = videoPath.Replace("\\", "/").Replace("'", "\\'");
|
||||||
fileListContent.AppendLine($"file '{escapedPath}'");
|
fileListContent.AppendLine($"file '{escapedPath}'");
|
||||||
}
|
}
|
||||||
File.WriteAllText(tempListFile, fileListContent.ToString(), Encoding.UTF8);
|
File.WriteAllText(tempListFile, fileListContent.ToString(), new UTF8Encoding(false));
|
||||||
|
|
||||||
// 步骤2:构建FFmpeg合并参数
|
// 步骤2:构建FFmpeg合并+音频嵌入参数
|
||||||
var arguments = new List<string>
|
var arguments = new List<string>();
|
||||||
{
|
|
||||||
"-y", // 覆盖输出文件
|
|
||||||
"-f", "concat", // 指定合并格式
|
|
||||||
"-safe", "0", // 允许访问绝对路径
|
|
||||||
"-i", tempListFile, // 输入文件列表
|
|
||||||
|
|
||||||
// 视频编码参数(复用现有类的编码配置,保证输出格式统一)
|
// 核心修改:支持双输入(视频列表 + 自定义音频)
|
||||||
|
if (hasCustomAudio)
|
||||||
|
{
|
||||||
|
// 输入1:视频文件列表(仅读取视频流,忽略原音频)
|
||||||
|
arguments.AddRange(new[]
|
||||||
|
{
|
||||||
|
"-y", // 覆盖输出文件
|
||||||
|
"-f", "concat",
|
||||||
|
"-safe", "0",
|
||||||
|
"-i", tempListFile,
|
||||||
|
// 输入2:自定义音频文件
|
||||||
|
"-i", audioPath,
|
||||||
|
// 映射流:输入0的视频流 → 输出视频流;输入1的音频流 → 输出音频流
|
||||||
|
"-map", "0:v",
|
||||||
|
"-map", "1:a",
|
||||||
|
// 音频同步:保证视频与音频时长匹配(若音频较短,循环音频;若较长,截断音频)
|
||||||
|
"-shortest", // 以较短的流时长为准,避免无意义的空帧/静音
|
||||||
|
"-filter:a", "apad=pad_len=0" // 音频补全,防止音频时长略短于视频导致的截断
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 无自定义音频:仅合并视频,保留原视频音频(若有)
|
||||||
|
arguments.AddRange(new[]
|
||||||
|
{
|
||||||
|
"-y", // 覆盖输出文件
|
||||||
|
"-f", "concat",
|
||||||
|
"-safe", "0",
|
||||||
|
"-i", tempListFile,
|
||||||
|
// 映射流:保留原视频的视频流和音频流
|
||||||
|
"-map", "0:v",
|
||||||
|
"-map", "0:a?", // "?" 表示若存在音频流则映射,不存在则忽略,避免报错
|
||||||
|
"-shortest"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视频编码参数(复用现有类的编码配置,保证输出格式统一)
|
||||||
|
arguments.AddRange(new[]
|
||||||
|
{
|
||||||
"-c:v", VideoCodec,
|
"-c:v", VideoCodec,
|
||||||
"-preset", VideoPreset,
|
"-preset", VideoPreset,
|
||||||
"-crf", $"{VideoCrf}",
|
"-crf", $"{VideoCrf}",
|
||||||
"-s", $"{width}x{height}", // 统一输出分辨率
|
"-s", $"{width}x{height}", // 统一输出分辨率
|
||||||
"-pix_fmt", "yuv420p", // 兼容所有播放器
|
"-pix_fmt", "yuv420p", // 兼容所有播放器
|
||||||
"-profile:v", "main",
|
"-profile:v", "main"
|
||||||
|
});
|
||||||
|
|
||||||
// 音频编码参数
|
// 音频编码参数(无论是否自定义音频,统一编码格式保证兼容性)
|
||||||
|
arguments.AddRange(new[]
|
||||||
|
{
|
||||||
"-c:a", AudioCodec,
|
"-c:a", AudioCodec,
|
||||||
"-b:a", AudioBitrate,
|
"-b:a", AudioBitrate,
|
||||||
"-ac", "2", // 立体声
|
"-ac", "2", // 立体声
|
||||||
"-ar", "44100", // 标准采样率
|
"-ar", "44100" // 标准采样率
|
||||||
|
});
|
||||||
|
|
||||||
// 封装优化
|
// 封装优化
|
||||||
|
arguments.AddRange(new[]
|
||||||
|
{
|
||||||
"-f", "mp4",
|
"-f", "mp4",
|
||||||
"-movflags", "+faststart", // 适合网络播放
|
"-movflags", "+faststart" // 适合网络播放(将moov原子移至文件头部)
|
||||||
|
});
|
||||||
|
|
||||||
// 输出路径
|
// 输出路径
|
||||||
savePath
|
arguments.Add(savePath);
|
||||||
};
|
|
||||||
|
|
||||||
// 执行FFmpeg合并命令
|
//Log.Error($"savePath={savePath}");
|
||||||
|
// 执行FFmpeg合并+音频嵌入命令
|
||||||
await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
|
await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
|
||||||
|
|
||||||
// 验证输出文件
|
// 验证输出文件
|
||||||
if (File.Exists(savePath))
|
if (File.Exists(savePath))
|
||||||
{
|
{
|
||||||
|
Log.Debug($"视频合成成功:{savePath}");
|
||||||
return savePath;
|
return savePath;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("视频合并失败,未生成输出文件。");
|
Log.Error("视频合成失败,未生成输出文件。");
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
+15
-12
@@ -27,7 +27,7 @@ namespace dy.net.utils
|
|||||||
string videoDirectory = Path.GetDirectoryName(video.VideoSavePath); // 视频所在目录
|
string videoDirectory = Path.GetDirectoryName(video.VideoSavePath); // 视频所在目录
|
||||||
string videoFileNameWithoutExt = Path.GetFileNameWithoutExtension(video.VideoSavePath); // 无扩展名的文件名
|
string videoFileNameWithoutExt = Path.GetFileNameWithoutExtension(video.VideoSavePath); // 无扩展名的文件名
|
||||||
string nfoFullPath = Path.Combine(videoDirectory, $"{videoFileNameWithoutExt}.nfo"); // NFO文件完整路径
|
string nfoFullPath = Path.Combine(videoDirectory, $"{videoFileNameWithoutExt}.nfo"); // NFO文件完整路径
|
||||||
string postFullPath = Path.Combine(videoDirectory, "poster.jpg"); // NFO文件完整路径
|
//string postFullPath = Path.Combine(videoDirectory, "poster.jpg"); // NFO文件完整路径
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(video.AuthorAvatar))
|
if (!string.IsNullOrWhiteSpace(video.AuthorAvatar))
|
||||||
{
|
{
|
||||||
@@ -38,8 +38,12 @@ namespace dy.net.utils
|
|||||||
if (File.Exists(video.AuthorAvatar))
|
if (File.Exists(video.AuthorAvatar))
|
||||||
{
|
{
|
||||||
var fileExt = Path.GetExtension(video.AuthorAvatar);
|
var fileExt = Path.GetExtension(video.AuthorAvatar);
|
||||||
|
var actorsDir = Path.Combine(videoDirectory, ".actors");
|
||||||
var nfoActorFullPath = Path.Combine(videoDirectory, $"{video.Author}{fileExt}");
|
if(!Directory.Exists(actorsDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(actorsDir);
|
||||||
|
}
|
||||||
|
var nfoActorFullPath = Path.Combine(actorsDir, $"{video.Author}{fileExt}");
|
||||||
|
|
||||||
if (File.Exists(nfoActorFullPath))
|
if (File.Exists(nfoActorFullPath))
|
||||||
{
|
{
|
||||||
@@ -61,9 +65,9 @@ namespace dy.net.utils
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Author = video.Author,
|
Author = video.Author,
|
||||||
Poster = postFullPath,
|
Poster = "poster.jpg",
|
||||||
Title = video.VideoTitle,
|
Title = video.VideoTitle,
|
||||||
Thumbnail = postFullPath,// 使用poster作为缩略图
|
Thumbnail = "poster.jpg",// 使用poster作为缩略图
|
||||||
ReleaseDate = video.CreateTime,
|
ReleaseDate = video.CreateTime,
|
||||||
Genres = new List<string> { video.Tag1, video.Tag2, video.Tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
Genres = new List<string> { video.Tag1, video.Tag2, video.Tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||||
}, nfoFullPath);
|
}, nfoFullPath);
|
||||||
@@ -86,19 +90,18 @@ namespace dy.net.utils
|
|||||||
|
|
||||||
// 创建根元素
|
// 创建根元素
|
||||||
XElement root = new XElement("movie");
|
XElement root = new XElement("movie");
|
||||||
root.Add(new XElement("outline"));
|
//root.Add(new XElement("outline"));
|
||||||
root.Add(new XElement("lockdata", true));
|
root.Add(new XElement("lockdata", true));
|
||||||
root.Add(new XElement("director", videoInfo.Author));
|
//root.Add(new XElement("director", videoInfo.Author));
|
||||||
root.Add(new XElement("plot", $"<![CDATA[{videoInfo.Title}]]>"));
|
//root.Add(new XElement("plot", $"<![CDATA[{videoInfo.Title}]]>"));
|
||||||
|
|
||||||
// 添加视频信息(先清理无效字符)
|
// 添加视频信息(先清理无效字符)
|
||||||
if (!string.IsNullOrWhiteSpace(videoInfo.Title))
|
if (!string.IsNullOrWhiteSpace(videoInfo.Title))
|
||||||
root.Add(new XElement("title", CleanInvalidXmlChars(videoInfo.Title)));
|
root.Add(new XElement("title", CleanInvalidXmlChars(videoInfo.Title)));
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(videoInfo.Author))
|
//if (!string.IsNullOrWhiteSpace(videoInfo.Author))
|
||||||
root.Add(new XElement("author", CleanInvalidXmlChars(videoInfo.Author)));
|
// root.Add(new XElement("author", CleanInvalidXmlChars(videoInfo.Author)));
|
||||||
|
|
||||||
// 发布时间(无需清理,因为是格式化的日期字符串)
|
|
||||||
if (videoInfo.ReleaseDate.HasValue)
|
if (videoInfo.ReleaseDate.HasValue)
|
||||||
{
|
{
|
||||||
root.Add(new XElement("releasedate", videoInfo.ReleaseDate.Value.ToString("yyyy-MM-dd")));
|
root.Add(new XElement("releasedate", videoInfo.ReleaseDate.Value.ToString("yyyy-MM-dd")));
|
||||||
@@ -128,7 +131,7 @@ namespace dy.net.utils
|
|||||||
if (!string.IsNullOrWhiteSpace(actor.Role))
|
if (!string.IsNullOrWhiteSpace(actor.Role))
|
||||||
actorElement.Add(new XElement("role", CleanInvalidXmlChars(actor.Role)));
|
actorElement.Add(new XElement("role", CleanInvalidXmlChars(actor.Role)));
|
||||||
|
|
||||||
actorElement.Add(new XElement("tmdbid", "3141592610000"));//写死一个反正不存在的ID,防止被媒体管理软件误认
|
actorElement.Add(new XElement("tmdbid", ""));
|
||||||
|
|
||||||
root.Add(actorElement);
|
root.Add(actorElement);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user