diff --git a/job/DouYinCollectSyncJob.cs b/job/DouYinCollectSyncJob.cs
index 2336151..a90568c 100644
--- a/job/DouYinCollectSyncJob.cs
+++ b/job/DouYinCollectSyncJob.cs
@@ -53,7 +53,7 @@ namespace dy.net.job
return data?.Cursor ?? "0";
}
- protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
+ protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2, AppConfig config)
{
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
var folder = Path.Combine(cookie.SavePath, safeTag1, $"{TikTokFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
@@ -61,8 +61,9 @@ namespace dy.net.job
return folder;
}
- protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
+ protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item)
{
+ var bitRate=item.Video.BitRate.FirstOrDefault();
return $"{item.AwemeId}.{bitRate.Format}";
}
diff --git a/job/DouYinFavoritSyncJob.cs b/job/DouYinFavoritSyncJob.cs
index d0469c2..cb10a46 100644
--- a/job/DouYinFavoritSyncJob.cs
+++ b/job/DouYinFavoritSyncJob.cs
@@ -46,7 +46,7 @@ namespace dy.net.job
return data?.MaxCursor ?? "0";
}
- protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
+ protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2, AppConfig config)
{
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{TikTokFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
@@ -54,8 +54,9 @@ namespace dy.net.job
return folder;
}
- protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
+ protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item)
{
+ var bitRate = item.Video.BitRate.FirstOrDefault();
return $"{item.AwemeId}.{bitRate.Format}";
}
diff --git a/job/DouYinUperPostSyncJob.cs b/job/DouYinUperPostSyncJob.cs
index 7426821..bb46798 100644
--- a/job/DouYinUperPostSyncJob.cs
+++ b/job/DouYinUperPostSyncJob.cs
@@ -51,25 +51,63 @@ namespace dy.net.job
return data?.MaxCursor ?? "0";
}
- protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
+ protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2,AppConfig config)
{
// UP主视频通常按作者名创建文件夹
var authorName = string.IsNullOrWhiteSpace(item.Author?.Nickname) ? "UnknownAuthor" : TikTokFileNameHelper.SanitizePath(item.Author.Nickname);
var folder = Path.Combine(cookie.UpSavePath, authorName);
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
- return folder;
+ if(config.UperSaveTogether)
+ {
+ return folder;
+ }
+ else
+ {
+ var sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
+ var (existingName, _) = _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result;
+ var fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName;
+ return Path.Combine(folder, fileNameFolder);
+ }
}
- protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
+ protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item)
{
+ var bitRate = item.Video.BitRate.FirstOrDefault();
var config = _commonService.GetConfig();
+ var fileName = string.Empty;
if (config?.UperUseViedoTitle ?? false)
{
var sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
var (existingName, _) = _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result;
- return string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{bitRate.Format}" : $"{existingName}.{bitRate.Format}";
+ fileName= string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{bitRate.Format}" : $"{existingName}.{bitRate.Format}";
+ }
+ else
+ {
+ fileName = $"{item.AwemeId}.{bitRate.Format}";
+ }
+ return fileName;
+ }
+
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected override string GetNfoFileName(DouyinUserCookie cookie, Aweme item, AppConfig config, string imageType)
+ {
+ if (config.UperSaveTogether)
+ {
+ var videoFileName = GetVideoFileName(cookie, item);
+ return $"{Path.GetFileNameWithoutExtension(videoFileName)}{imageType}";
+ }
+ else
+ {
+ return base.GetNfoFileName(cookie,item,config,imageType);
}
- return $"{item.AwemeId}.{bitRate.Format}";
}
protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie)
diff --git a/job/DouyinBaseSyncJob.cs b/job/DouyinBaseSyncJob.cs
index bd350ef..7543153 100644
--- a/job/DouyinBaseSyncJob.cs
+++ b/job/DouyinBaseSyncJob.cs
@@ -6,88 +6,683 @@ using dy.net.service;
using dy.net.utils;
using Quartz;
using Quartz.Util;
+using Serilog;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
namespace dy.net.job
{
- [DisallowConcurrentExecution]
+ ///
+ /// 抖音数据同步任务基类
+ /// 所有具体的抖音同步任务(如收藏、关注、作品等)都应继承此类
+ /// 提供了通用的同步逻辑,如Cookie处理、视频下载、数据存储等
+ ///
+ [DisallowConcurrentExecution] // 禁止并发执行,确保同一时间只有一个实例在运行
public abstract class DouyinBaseSyncJob : IJob
{
- protected readonly DouyinCookieService _dyCookieService;
- protected readonly DouyinHttpClientService _douyinService;
- protected readonly DouyinVideoService _douyinVideoService;
- protected readonly DouyinCommonService _commonService;
- protected readonly Random _random = new Random();
- protected readonly IServiceProvider _serviceProvider;
- protected readonly IWebHostEnvironment _environment;
- protected string count = "18"; // 每页请求的视频数量
- protected abstract string JobType { get; }
-
-
- private bool _downImageVideo;
+ #region 受保护字段
///
- /// 清除保存失败的数据
+ /// 抖音Cookie服务,用于获取和管理用户Cookie
///
- ///
- ///
- ///
- ///
- ///
- ///
+ protected readonly DouyinCookieService _dyCookieService;
+
+ ///
+ /// 抖音HTTP客户端服务,用于发送HTTP请求
+ ///
+ protected readonly DouyinHttpClientService _douyinService;
+
+ ///
+ /// 抖音视频服务,用于视频信息的数据库操作
+ ///
+ protected readonly DouyinVideoService _douyinVideoService;
+
+ ///
+ /// 抖音通用服务,用于获取应用配置等
+ ///
+ protected readonly DouyinCommonService _commonService;
+
+ ///
+ /// 随机数生成器,用于生成随机延迟,模拟人类操作
+ ///
+ protected readonly Random _random = new Random();
+
+ ///
+ /// 服务提供器,用于获取其他服务实例
+ ///
+ protected readonly IServiceProvider _serviceProvider;
+
+ ///
+ /// Web主机环境,用于获取应用程序路径等信息
+ ///
+ protected readonly IWebHostEnvironment _environment;
+
+ ///
+ /// 每页请求的视频数量,可通过配置文件修改
+ ///
+ protected string count = "18";
+
+ #endregion
+
+ #region 私有字段
+
+ ///
+ /// 是否下载图片并合成视频
+ ///
+ private bool _downImageVideo;
+
+ #endregion
+
+ #region 抽象属性
+
+ ///
+ /// 任务类型名称,用于日志记录和区分不同的同步任务
+ /// 子类必须实现此属性并返回具体的任务类型
+ ///
+ protected abstract string JobType { get; }
+
+ #endregion
+
+ #region 构造函数
+
+ ///
+ /// 初始化 类的新实例
+ ///
+ /// 抖音Cookie服务
+ /// 抖音HTTP客户端服务
+ /// 抖音视频服务
+ /// 抖音通用服务
+ /// 服务提供器
+ /// Web主机环境
protected DouyinBaseSyncJob(
DouyinCookieService dyCookieService,
DouyinHttpClientService dyHttpClientService,
DouyinVideoService dyCollectVideoService,
- DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment)
+ DouyinCommonService commonService,
+ IServiceProvider serviceProvider,
+ IWebHostEnvironment webHostEnvironment)
{
- _dyCookieService = dyCookieService;
- _douyinService = dyHttpClientService;
- _douyinVideoService = dyCollectVideoService;
- _commonService = commonService;
- _serviceProvider = serviceProvider;
- _environment = webHostEnvironment;
-
+ _dyCookieService = dyCookieService ?? throw new ArgumentNullException(nameof(dyCookieService));
+ _douyinService = dyHttpClientService ?? throw new ArgumentNullException(nameof(dyHttpClientService));
+ _douyinVideoService = dyCollectVideoService ?? throw new ArgumentNullException(nameof(dyCollectVideoService));
+ _commonService = commonService ?? throw new ArgumentNullException(nameof(commonService));
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ _environment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
}
+ #endregion
+
+ #region 公共方法
+
///
- ///
+ /// 执行任务的主入口点
+ /// 由Quartz调度器调用,负责协调整个同步流程
///
- ///
- ///
+ /// 作业执行上下文
+ /// 一个表示异步操作的任务
public async Task Execute(IJobExecutionContext context)
{
+ // 1. 获取应用配置
var config = _commonService.GetConfig();
if (config == null)
{
- Serilog.Log.Debug($"{JobType}-请先在设置中初始化配置,再执行同步任务");
+ Log.Debug($"{JobType}-请先在设置中初始化配置,再执行同步任务");
return;
}
+
+ // 2. 从配置中获取每页请求数量
if (config.BatchCount > 0)
count = config.BatchCount.ToString();
- // 读取环境变量覆盖配置中是否下载图片视频配置
+ // 3. 初始化是否下载图片视频的设置
InitializeDownImageVideoSetting(config);
+ // 4. 在处理Cookie之前执行的预处理操作(子类可重写)
await BeforeProcessCookies();
+ // 5. 获取所有有效的Cookie
var cookies = await GetValidCookies();
if (cookies == null || !cookies.Any())
{
- Serilog.Log.Debug($"{JobType}-无可用Cookie,任务终止");
+ Log.Debug($"{JobType}-无可用Cookie,任务终止");
return;
}
- Serilog.Log.Debug($"{JobType}-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
+ Log.Debug($"{JobType}-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
+ // 6. 遍历每个有效的Cookie,执行同步操作
foreach (var cookie in cookies)
{
- await ProcessSyncUserCookie(cookie);
+ await ProcessSyncUserCookie(cookie, config);
}
}
+
+ #endregion
+
+ #region 受保护方法
+
///
- /// 检查并初始化下载图片视频的设置
+ /// 在处理Cookie之前执行的预处理操作
+ /// 子类可以重写此方法来实现特定的预处理逻辑
///
+ /// 一个表示异步操作的任务
+ protected virtual Task BeforeProcessCookies() => Task.CompletedTask;
+
+ ///
+ /// 获取所有有效的Cookie
+ /// 子类必须实现此方法,根据具体任务类型筛选有效的Cookie
+ ///
+ /// 有效的Cookie列表
+ protected abstract Task> GetValidCookies();
+
+ ///
+ /// 检查指定的Cookie是否有效
+ /// 子类必须实现此方法,提供具体的Cookie有效性检查逻辑
+ ///
+ /// 要检查的Cookie
+ /// 如果Cookie有效,则为true;否则为false
+ protected abstract bool IsCookieValid(DouyinUserCookie cookie);
+
+ ///
+ /// 根据Cookie和游标获取视频数据
+ /// 子类必须实现此方法,调用具体的API接口获取视频列表
+ ///
+ /// 用户Cookie
+ /// 分页游标,用于获取下一页数据
+ /// 视频信息对象,包含视频列表和分页信息
+ protected abstract Task FetchVideoData(DouyinUserCookie cookie, string cursor);
+
+ ///
+ /// 判断是否应该继续同步下一页数据
+ /// 子类必须实现此方法,根据API返回的分页信息判断是否还有更多数据
+ ///
+ /// 用户Cookie
+ /// 当前获取到的视频数据
+ /// 如果应该继续同步,则为true;否则为false
+ protected abstract bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data);
+
+ ///
+ /// 获取下一页数据的游标
+ /// 子类必须实现此方法,从当前视频数据中提取下一页的游标
+ ///
+ /// 当前获取到的视频数据
+ /// 下一页数据的游标
+ protected abstract string GetNextCursor(DouyinVideoInfo data);
+
+ ///
+ /// 创建视频保存文件夹
+ /// 子类必须实现此方法,根据具体的存储策略创建文件夹
+ ///
+ /// 用户Cookie
+ /// 视频信息
+ /// 视频标签1
+ /// 视频标签2
+ /// 应用配置
+ /// 创建的视频保存文件夹路径
+ protected abstract string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2, AppConfig config);
+
+ ///
+ /// 获取视频文件名
+ /// 子类必须实现此方法,根据具体的命名规则生成文件名
+ ///
+ /// 用户Cookie
+ /// 视频信息
+ /// 生成的视频文件名
+ protected abstract string GetVideoFileName(DouyinUserCookie cookie, Aweme item);
+
+ ///
+ /// 获取作者头像保存的基础路径
+ /// 子类必须实现此方法,指定头像的存储位置
+ ///
+ /// 用户Cookie
+ /// 作者头像保存的基础路径
+ protected abstract string GetAuthorAvatarBasePath(DouyinUserCookie cookie);
+
+ ///
+ /// 处理同步完成后的操作
+ /// 子类必须实现此方法,可用于更新同步状态、发送通知等
+ ///
+ /// 用户Cookie
+ /// 本次同步成功的视频数量
+ /// 一个表示异步操作的任务
+ protected abstract Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount);
+
+ ///
+ /// 获取视频实体的差异信息
+ /// 子类必须实现此方法,用于区分不同类型的视频(如普通视频、图片合成视频等)
+ ///
+ /// 用户Cookie
+ /// 视频信息
+ /// 视频实体的差异信息,包含视频类型和简化标题
+ protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item);
+
+ ///
+ /// 获取NFO文件中的图片(如海报)文件名
+ /// 子类可以重写此方法来自定义NFO图片的命名规则
+ ///
+ /// 当前操作的Cookie
+ /// 视频信息
+ /// 应用配置
+ /// 原文件名称(如poster.jpg)
+ /// 封面图片的文件名
+ protected virtual string GetNfoFileName(DouyinUserCookie cookie, Aweme item, AppConfig config, string fileName)
+ {
+ return fileName;
+ }
+
+ ///
+ /// 处理单个用户Cookie的同步逻辑
+ /// 负责循环获取视频数据、处理视频、保存视频信息等
+ ///
+ /// 用户Cookie
+ /// 应用配置
+ /// 一个表示异步操作的任务
+ protected async Task ProcessSyncUserCookie(DouyinUserCookie cookie, AppConfig config)
+ {
+ try
+ {
+ // 检查Cookie是否有效
+ if (!IsCookieValid(cookie))
+ {
+ Log.Debug($"{JobType}-Cookie[{cookie.UserName}]无效,跳过");
+ return;
+ }
+
+ Log.Debug($"{JobType}-开始同步 Cookie-[{cookie.UserName}]");
+
+ int syncCount = 0; // 本次同步成功的视频数量
+ string cursor = "0"; // 初始游标
+ bool hasMore = true; // 是否还有更多数据
+
+ // 循环获取视频数据
+ while (hasMore)
+ {
+ // 获取视频数据
+ var data = await FetchVideoData(cookie, cursor);
+ if (data == null)
+ {
+ Log.Debug($"{JobType}-Cookie[{cookie.UserName}]获取数据失败");
+ break;
+ }
+
+ // 判断是否还有更多数据
+ hasMore = ShouldContinueSync(cookie, data);
+ // 获取下一页游标
+ cursor = GetNextCursor(data);
+
+ // 如果没有视频数据,退出循环
+ if (data.AwemeList == null || !data.AwemeList.Any())
+ break;
+
+ // 处理视频列表
+ var videos = await ProcessVideoList(cookie, data, config);
+ // 保存视频信息到数据库
+ syncCount += await SaveVideos(videos);
+
+ // 随机延迟,模拟人类操作,避免请求过快
+ await Task.Delay(_random.Next(5, 10) * 1000);
+ }
+
+ // 处理同步完成后的操作
+ await HandleSyncCompletion(cookie, syncCount);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"{JobType}-处理Cookie[{cookie.Id}]时出错");
+ }
+ }
+
+ ///
+ /// 处理视频列表
+ /// 遍历视频列表,分别处理每个视频和图片集
+ ///
+ /// 用户Cookie
+ /// 视频信息对象
+ /// 应用配置
+ /// 处理后的视频实体列表
+ protected async Task> ProcessVideoList(DouyinUserCookie cookie, DouyinVideoInfo data, AppConfig config)
+ {
+ var videos = new List();
+ foreach (var item in data.AwemeList)
+ {
+ // 处理单个视频
+ var video = await ProcessSingleVideo(cookie, item, data, config);
+ if (video != null)
+ videos.Add(video);
+
+ // 如果配置了下载图片视频,则处理图片集并合成视频
+ if (_downImageVideo)
+ {
+ var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config);
+ if (mergevideo != null)
+ videos.Add(mergevideo);
+ }
+ }
+ return videos;
+ }
+
+ ///
+ /// 处理单个视频
+ /// 负责下载视频、封面、头像,生成NFO文件,创建视频实体等
+ ///
+ /// 用户Cookie
+ /// 视频信息
+ /// 视频信息对象
+ /// 应用配置
+ /// 处理后的视频实体,如果处理失败则为null
+ protected async Task ProcessSingleVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config)
+ {
+ // 检查视频数据是否有效
+ if (!IsAwemeValid(item)) return null;
+
+ // 获取视频的码率信息
+ var v = item.Video.BitRate.Where(x => !string.IsNullOrEmpty(x.Format)).FirstOrDefault();
+ if (v == null) return null;
+
+ // 获取视频播放地址
+ var videoUrl = v.PlayAddr.UrlList.Where(x => !string.IsNullOrEmpty(x))?.FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(videoUrl)) return null;
+
+ // 获取视频标签
+ var (tag1, tag2, tag3) = GetVideoTags(item);
+ // 创建保存文件夹
+ var saveFolder = CreateSaveFolder(cookie, item, tag1, tag2, config);
+ // 获取视频文件名
+ var fileName = GetVideoFileName(cookie, item);
+ // 拼接视频保存路径
+ var savePath = Path.Combine(saveFolder, fileName);
+
+ // 如果文件已存在,跳过
+ if (File.Exists(savePath)) return null;
+
+ Log.Debug($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
+ // 随机延迟,模拟人类操作
+ await Task.Delay(_random.Next(1, 4) * 1000);
+ // 下载视频
+ if (!await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies))
+ {
+ Log.Error($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载失败");
+ return null;
+ }
+
+ // 下载视频封面
+ await DownVideoCover(item, saveFolder, cookie, config);
+ // 下载作者头像
+ var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
+ // 生成NFO文件
+ await GenerateNfoFile(saveFolder, item, avatarSavePath, avatarUrl, cookie, config);
+ // 创建视频实体
+ return CreateVideoEntity(cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
+ }
+
+ ///
+ /// 处理图片集并合成为视频
+ /// 负责下载图片、合成视频、处理封面和头像等
+ ///
+ /// 用户Cookie
+ /// 视频信息(包含图片集)
+ /// 视频信息对象
+ /// 应用配置
+ /// 合成后的视频实体,如果处理失败则为null
+ protected async Task ProcessImageSetAndMergeToVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config)
+ {
+ try
+ {
+ // 提取图片URL列表
+ List imageUrls = item.Images?
+ .Where(img => img.UrlList != null && img.UrlList.Any())
+ .Select(img => img.UrlList.FirstOrDefault())
+ .Where(url => !string.IsNullOrWhiteSpace(url))
+ .ToList();
+
+ // 如果没有图片,返回null
+ if (imageUrls == null || !imageUrls.Any())
+ {
+ return null;
+ }
+
+ // 检查图片保存路径是否配置
+ if (string.IsNullOrWhiteSpace(cookie.ImgSavePath))
+ {
+ Log.Error($"{JobType}-图片视频同步-没有配置图片存储路径");
+ return null;
+ }
+
+ // 获取图片合成视频服务
+ var imageService = _serviceProvider.GetService();
+ if (imageService == null)
+ {
+ Log.Error($"{JobType} -图片视频同步-无法创建 ImageMergeToVideoService。");
+ return null;
+ }
+
+ // 创建图片保存文件夹
+ var fileNamefolder = Path.Combine(cookie.ImgSavePath, TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId));
+ if (!Directory.Exists(fileNamefolder)) Directory.CreateDirectory(fileNamefolder);
+ // 合成视频的保存路径
+ var savePath = Path.Combine(fileNamefolder, $"{item.AwemeId}.mp4");
+
+ // 如果文件已存在,返回null
+ if (File.Exists(savePath)) return null;
+
+ // 获取音乐URL
+ var mp3Url = item.Music?.PlayUrl?.UrlList?.FirstOrDefault();
+
+ // 准备合成视频的请求参数
+ var reqParams = new MediaMergeRequest
+ {
+ ImageDurationPerSecond = 3, // 每张图片显示的时长(秒)
+ OutputFormat = "mp4", // 输出视频格式
+ VideoFps = 30, // 视频帧率
+ AudioUrls = string.IsNullOrWhiteSpace(mp3Url) ? new List() : new List { mp3Url }, // 音频URL列表
+ ImageUrls = imageUrls, // 图片URL列表
+ VideoWidth = 1080, // 视频宽度
+ VideoHeight = 1920, // 视频高度
+ };
+
+ // 执行图片合成视频操作
+ var mergeResult = await imageService.MergeToVideo(AppContext.BaseDirectory, reqParams, savePath, fileNamefolder);
+ if (!mergeResult)
+ {
+ Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
+ return null;
+ }
+
+ // 检查合成后的视频文件是否有效
+ if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0)
+ {
+ Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
+ // 清理无效的文件和文件夹
+ if (Directory.Exists(fileNamefolder))
+ {
+ File.Delete(savePath);
+ Directory.Delete(fileNamefolder, true);
+ Log.Error($"{JobType}-图片视频同步-已删除合成失败的视频文件和目录");
+ }
+ return null;
+ }
+
+ // 下载视频封面(使用第一张图片作为封面)
+ await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie, item, config);
+ // 下载作者头像
+ var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
+ // 生成NFO文件
+ await GenerateNfoFile(fileNamefolder, item, avatarSavePath, avatarUrl, cookie, config);
+
+ // 获取视频标签
+ var (tag1, tag2, tag3) = GetVideoTags(item);
+ // 为合成的视频创建一个“虚拟”的BitRate对象,以便复用CreateVideoEntity方法
+ var virtualBitRate = new VideoBitRate
+ {
+ PlayAddr = new PlayAddr
+ {
+ Width = reqParams.VideoWidth,
+ Height = reqParams.VideoHeight,
+ DataSize = new FileInfo(savePath).Length // 合成视频的文件大小
+ }
+ };
+
+ // 创建视频实体
+ var videoEntity = CreateVideoEntity(
+ cookie, item, virtualBitRate, savePath, fileNamefolder,
+ tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
+
+ // 特殊处理合成视频的字段
+ videoEntity.FileHash = string.Empty; // 合成视频没有原始文件哈希
+ videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
+ videoEntity.ViedoType = VideoTypeEnum.ImageVideo.ToString(); // 标记为图片合成视频
+
+ return videoEntity;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"{JobType}-图片视频同步-处理图片集并合成视频时出错");
+ return null;
+ }
+ }
+
+ ///
+ /// 保存视频信息到数据库
+ /// 批量插入视频实体列表到数据库中
+ ///
+ /// 要保存的视频实体列表
+ /// 保存成功的视频数量
+ protected async Task SaveVideos(List videos)
+ {
+ if (!videos.Any()) return 0;
+ try
+ {
+ await _douyinVideoService.batchInsert(videos);
+ return videos.Count;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"{JobType}-批量保存视频到数据库失败");
+ // 清理保存失败的视频文件
+ await CleanupFailedVideos(videos);
+ return 0;
+ }
+ }
+
+ ///
+ /// 生成NFO文件
+ /// NFO文件包含视频的元数据信息,如标题、作者、封面等
+ ///
+ /// NFO文件的保存文件夹
+ /// 视频信息
+ /// 作者头像保存路径
+ /// 作者头像URL
+ ///
///
+ /// 一个表示异步操作的任务
+ protected async Task GenerateNfoFile(string saveFolder, Aweme item,
+ string avatarSavePath, string avatarUrl, DouyinUserCookie cookie, AppConfig config)
+ {
+ // 异步生成NFO文件,避免阻塞主线程
+ await Task.Run(() =>
+ {
+ (string tag1, string tag2, string tag3) = GetVideoTags(item);
+ var nfoFileName = GetNfoFileName(cookie, item, config, ".nfo");
+ var nfoPath = Path.Combine(saveFolder, nfoFileName);
+ NfoFileGenerator.GenerateNfoFile(new DouyinVideoNfo
+ {
+ Actors = new List
+ {
+ new() {
+ Name = item.Author?.Nickname,
+ Role = "主演",
+ Thumb = avatarSavePath
+ }
+ },
+ Author = item.Author?.Nickname,
+ Poster = GetNfoFileName(cookie, item, config, "poster.jpg"),
+ Title = item.Desc,
+ Thumbnail = GetNfoFileName(cookie, item, config, "fanart.jpg"),
+ ReleaseDate = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
+ Genres = new List { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
+ }, nfoPath);
+ });
+ }
+
+ ///
+ /// 下载视频封面
+ /// 从视频信息中提取封面URL并下载到指定文件夹
+ ///
+ /// 视频信息
+ /// 封面保存文件夹
+ /// 用户Cookie
+ /// 应用配置
+ /// 一个表示异步操作的任务
+ protected async Task DownVideoCover(Aweme item, string saveFolder, DouyinUserCookie cookie, AppConfig config)
+ {
+ var coverUrl = item.Video.Cover.UrlList?.FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(coverUrl)) return;
+ await DownVideoCover(coverUrl, saveFolder, cookie, item, config);
+ }
+
+ ///
+ /// 下载作者头像
+ /// 从视频信息中提取作者头像URL并下载到指定文件夹
+ ///
+ /// 用户Cookie
+ /// 视频信息
+ /// 一个元组,包含头像保存路径和头像URL
+ protected async Task<(string savePath, string url)> DownAuthorAvatar(DouyinUserCookie cookie, Aweme item)
+ {
+ if (item.Author == null) return (null, null);
+ // 优先获取高清头像
+ var avatarUrl = item.Author.AvatarLarger?.UrlList?.FirstOrDefault() ?? item.Author.AvatarThumb?.UrlList?.FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(avatarUrl)) return (null, null);
+
+ // 拼接头像保存路径
+ var avatarSavePath = Path.Combine(GetAuthorAvatarBasePath(cookie), $"{item.Author.Uid}.jpg");
+ var avatarDir = Path.GetDirectoryName(avatarSavePath);
+ // 创建头像保存文件夹
+ if (!Directory.Exists(avatarDir)) Directory.CreateDirectory(avatarDir);
+ // 如果头像文件不存在,则下载
+ if (!File.Exists(avatarSavePath))
+ {
+ await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie.Cookies);
+ }
+ return (avatarSavePath, avatarUrl);
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 检查视频数据是否有效
+ /// 验证视频信息是否包含必要的字段
+ ///
+ /// 视频信息
+ /// 如果视频数据有效,则为true;否则为false
+ private bool IsAwemeValid(Aweme item) => item != null && item.Video != null && item.Video.BitRate != null;
+
+ ///
+ /// 获取视频标签
+ /// 从视频信息中提取三个级别的标签
+ ///
+ /// 视频信息
+ /// 一个元组,包含三个级别的视频标签
+ private (string tag1, string tag2, string tag3) GetVideoTags(Aweme item)
+ {
+ var tags = item.VideoTags;
+ return (
+ tags?.FirstOrDefault(x => x.Level == 1)?.TagName,
+ tags?.FirstOrDefault(x => x.Level == 2)?.TagName,
+ tags?.FirstOrDefault(x => x.Level == 3)?.TagName
+ );
+ }
+
+ ///
+ /// 初始化是否下载图片视频的设置
+ /// 从环境变量和配置文件中读取设置,环境变量优先级更高
+ ///
+ /// 应用配置
private void InitializeDownImageVideoSetting(AppConfig config)
{
var downImageVideoConfig = Appsettings.Get("DOWN_IMGVIDEO");
@@ -99,135 +694,87 @@ namespace dy.net.job
}
///
- /// 检查并处理Cookie
+ /// 清理保存失败的视频文件
+ /// 当数据库保存失败时,删除已下载的视频文件和文件夹
///
- ///
- ///
- protected async Task ProcessSyncUserCookie(DouyinUserCookie cookie)
+ /// 保存失败的视频实体列表
+ /// 一个表示异步操作的任务
+ private async Task CleanupFailedVideos(List videos)
{
- try
+ foreach (var video in videos)
{
- if (!IsCookieValid(cookie))
+ // 异步删除文件和文件夹,避免阻塞主线程
+ await Task.Run(() =>
{
- Serilog.Log.Debug($"{JobType}-Cookie[{cookie.UserName}]无效,跳过");
- return;
- }
-
- Serilog.Log.Debug($"{JobType}-开始同步 Cookie-[{cookie.UserName}]");
-
- int syncCount = 0;
- string cursor = "0";
- bool hasMore = true;
-
- while (hasMore)
- {
- var data = await FetchVideoData(cookie, cursor);
- if (data == null)
+ try
{
- Serilog.Log.Debug($"{JobType}-Cookie[{cookie.UserName}]获取数据失败");
- break;
+ if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && File.Exists(video.VideoSavePath))
+ {
+ File.Delete(video.VideoSavePath);
+ }
+ string directory = Path.GetDirectoryName(video.VideoSavePath);
+ if (!string.IsNullOrWhiteSpace(directory) && Directory.Exists(directory) && Directory.GetFileSystemEntries(directory).Length == 0)
+ {
+ Directory.Delete(directory);
+ }
}
-
- hasMore = ShouldContinueSync(cookie, data);
- cursor = GetNextCursor(data);
-
- if (data.AwemeList == null || !data.AwemeList.Any())
- break;
-
- var videos = await ProcessVideoList(cookie, data);
- syncCount += await SaveVideos(videos);
-
- await Task.Delay(_random.Next(5, 10) * 1000);
- }
-
- await HandleSyncCompletion(cookie, syncCount);
- }
- catch (Exception ex)
- {
- Serilog.Log.Error(ex, $"{JobType}-处理Cookie[{cookie.Id}]时出错");
+ catch (Exception ex)
+ {
+ Log.Warning(ex, $"{JobType}-清理失败视频文件时出错: {video.VideoSavePath}");
+ }
+ });
}
+ Log.Debug($"{JobType}-因数据库保存失败,已清理本次下载的视频目录");
}
///
- /// 处理接口返回的视频数据
+ /// 下载视频封面(重载)
+ /// 根据指定的封面URL下载封面图片,并复制为fanart.jpg
///
- ///
- ///
- ///
- protected async Task> ProcessVideoList(DouyinUserCookie cookie, DouyinVideoInfo data)
+ /// 封面图片URL
+ /// 封面保存文件夹
+ /// 用户Cookie
+ /// 视频信息
+ /// 应用配置
+ /// 一个表示异步操作的任务
+ private async Task DownVideoCover(string coverUrl, string saveFolder, DouyinUserCookie cookie, Aweme item, AppConfig config)
{
- var videos = new List();
- foreach (var item in data.AwemeList)
- {
- var video = await ProcessSingleVideo(cookie, item, data);
- if (video != null)
- videos.Add(video);
+ if (string.IsNullOrWhiteSpace(coverUrl)) return;
+ // 获取封面图片文件名
+ var coverImgName = GetNfoFileName(cookie, item, config, "poster.jpg");
+ var coverSavePath = Path.Combine(saveFolder, coverImgName);
- //如果配置了下载图片视频,则处理图片集并合成视频
- if (_downImageVideo)
+ // 如果封面文件不存在,则下载
+ if (!File.Exists(coverSavePath))
+ {
+ var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie.Cookies);
+ if (downRes)
{
- var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data);
- if (mergevideo != null)
- videos.Add(mergevideo);
+ // 复制封面图片为fanart.jpg
+ var fanartImgName = GetNfoFileName(cookie, item, config, "fanart.jpg");
+ var copyPath = Path.Combine(saveFolder, fanartImgName);
+ File.Copy(coverSavePath, copyPath, true);
}
}
- return videos;
- }
-
- ///
- /// 处理单个视频数据
- ///
- ///
- ///
- ///
- ///
- protected async Task ProcessSingleVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data)
- {
- if (!IsAwemeValid(item)) return null;
-
- var v = item.Video.BitRate.FirstOrDefault();
- if (v == null) return null;
-
- var videoUrl = v.PlayAddr.UrlList?.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(videoUrl)) return null;
-
- var (tag1, tag2, tag3) = GetVideoTags(item);
- var saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
- var fileName = GetVideoFileName(cookie, item, v);
- var savePath = Path.Combine(saveFolder, fileName);
-
- if (File.Exists(savePath)) return null;
-
- Serilog.Log.Debug($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
- await Task.Delay(_random.Next(1, 4) * 1000);
- if (!await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies))
- {
- Serilog.Log.Error($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载失败");
- return null;
- }
-
- await DownVideoCover(item, saveFolder, cookie.Cookies);
- var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
- await GenerateNfoFile(saveFolder, item, tag1, tag2, tag3, avatarSavePath, avatarUrl);
- return CreateVideoEntity(cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
}
///
/// 创建视频实体
+ /// 根据视频信息、下载路径等创建DouyinVideo实体对象
///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- protected DouyinVideo CreateVideoEntity(
+ /// 用户Cookie
+ /// 视频信息
+ /// 视频码率信息
+ /// 视频保存路径
+ /// 视频保存文件夹
+ /// 视频标签1
+ /// 视频标签2
+ /// 视频标签3
+ /// 作者头像保存路径
+ /// 作者头像URL
+ /// 视频信息对象
+ /// 创建的视频实体对象
+ private DouyinVideo CreateVideoEntity(
DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder,
string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data)
{
@@ -261,286 +808,24 @@ namespace dy.net.job
};
}
- ///
- /// 保存视频信息到数据库
- ///
- ///
- ///
- protected async Task SaveVideos(List videos)
- {
- if (!videos.Any()) return 0;
- try
- {
- await _douyinVideoService.batchInsert(videos);
- return videos.Count;
- }
- catch (Exception ex)
- {
- Serilog.Log.Error(ex, $"{JobType}-批量保存视频到数据库失败");
- await CleanupFailedVideos(videos);
- return 0;
- }
- }
- ///
- /// 清理保存失败的视频文件
- ///
- ///
- ///
- protected async Task CleanupFailedVideos(List videos)
- {
- foreach (var video in videos)
- {
- if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(Path.GetDirectoryName(video.VideoSavePath)))
- {
- Directory.Delete(Path.GetDirectoryName(video.VideoSavePath), recursive: true);
- }
- }
- Serilog.Log.Debug($"{JobType}-因数据库保存失败,已清理本次下载的视频目录");
- }
-
- ///
- /// 下载视频封面
- ///
- ///
- ///
- ///
- ///
- protected async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
- {
- var coverUrl = item.Video.Cover.UrlList?.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(coverUrl)) return;
-
- var coverSavePath = Path.Combine(saveFolder, "poster.jpg");
- if (File.Exists(coverSavePath)) return;
-
- if (await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie))
- {
- File.Copy(coverSavePath, Path.Combine(saveFolder, "fanart.jpg"), true);
- }
- }
-
- ///
- /// 下载图片合成的视频封面
- ///
- ///
- ///
- ///
- ///
- private async Task DownVideoCover(string coverUrl, string saveFolder, string cookie)
- {
- if (string.IsNullOrWhiteSpace(coverUrl)) return;
- var coverImgName = "poster.jpg";
- var coverSavePath = Path.Combine(saveFolder, coverImgName);
-
- if (!File.Exists(coverSavePath))
- {
- // 封面下载前随机延迟
- var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
- if (downRes)
- {
- var copyPath = Path.Combine(saveFolder, "fanart.jpg");
- File.Copy(coverSavePath, copyPath, true);
- }
- }
- }
- ///
- /// 下载作者头像
- ///
- ///
- ///
- ///
- protected async Task<(string savePath, string url)> DownAuthorAvatar(DouyinUserCookie cookie, Aweme item)
- {
- if (item.Author == null) return (null, null);
- var avatarUrl = item.Author.AvatarLarger?.UrlList?.FirstOrDefault() ?? item.Author.AvatarThumb?.UrlList?.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(avatarUrl)) return (null, null);
-
- var avatarSavePath = Path.Combine(GetAuthorAvatarBasePath(cookie), $"{item.Author.Uid}.jpg");
- var avatarDir = Path.GetDirectoryName(avatarSavePath);
- if (!Directory.Exists(avatarDir)) Directory.CreateDirectory(avatarDir);
- if (!File.Exists(avatarSavePath))
- {
- await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie.Cookies);
- }
- return (avatarSavePath, avatarUrl);
- }
-
-
- ///
- /// 处理图片集并合成为视频
- ///
- ///
- ///
- ///
- ///
- protected async Task ProcessImageSetAndMergeToVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data)
- {
- try
- {
- List imageUrls = item.Images?
- .Where(img => img.UrlList != null && img.UrlList.Any())
- .Select(img => img.UrlList.FirstOrDefault())
- .Where(url => !string.IsNullOrWhiteSpace(url))
- .ToList();
-
- if (imageUrls == null || !imageUrls.Any())
- {
- return null;
- }
-
- if (string.IsNullOrWhiteSpace(cookie.ImgSavePath))
- {
- Serilog.Log.Error($"{JobType}-图片视频同步-没有配置图片存储路径");
- return null;
- }
- var imageService = _serviceProvider.GetService();
- if (imageService == null)
- {
- Serilog.Log.Error($"{JobType} -图片视频同步-无法创建 ImageMergeToVideoService。");
- return null;
- }
-
-
- var fileNamefolder = Path.Combine(cookie.ImgSavePath, TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId));
- if (!Directory.Exists(fileNamefolder)) Directory.CreateDirectory(fileNamefolder);
- var savePath = Path.Combine(fileNamefolder, $"{item.AwemeId}.mp4");
-
- if (File.Exists(savePath)) return null;
- var mp3Url = item.Music?.PlayUrl?.UrlList?.FirstOrDefault();
-
- var reqParams = new MediaMergeRequest
- {
- ImageDurationPerSecond = 3,
- OutputFormat = "mp4",
- VideoFps = 30,
- AudioUrls = string.IsNullOrWhiteSpace(mp3Url) ? new List() : new List { mp3Url },
- ImageUrls = imageUrls,
- VideoWidth = 1080,
- VideoHeight = 1920,
- };
-
- var mergeResult = await imageService.MergeToVideo(AppContext.BaseDirectory, reqParams, savePath,fileNamefolder);
- if (!mergeResult)
- {
- Serilog.Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
- return null;
- }
- if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0)
- {
- Serilog.Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
- if (Directory.Exists(fileNamefolder))
- {
- File.Delete(savePath);
- Directory.Delete(fileNamefolder, true);
- Serilog.Log.Error($"{JobType}-图片视频同步-已删除合成失败的视频文件和目录");
- }
- return null;
- }
-
- // 复用基类的方法
- await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie.Cookies);
- var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
-
- // 为合成的视频创建一个“虚拟”的BitRate对象,以便复用CreateVideoEntity
- var virtualBitRate = new VideoBitRate
- {
- PlayAddr = new PlayAddr
- {
- Width = reqParams.VideoWidth,
- Height = reqParams.VideoHeight,
- DataSize = new FileInfo(savePath).Length
- }
- };
-
- var (tag1, tag2, tag3) = GetVideoTags(item);
- await GenerateNfoFile(fileNamefolder, item, tag1, tag2, tag3, avatarSavePath, avatarUrl);
-
- var videoEntity = CreateVideoEntity(
- cookie, item, virtualBitRate, savePath, fileNamefolder,
- tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
-
- // 特殊处理合成视频的字段
- videoEntity.FileHash = string.Empty;
- videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
- videoEntity.ViedoType= VideoTypeEnum.ImageVideo.ToString();
- return videoEntity;
- }
- catch (Exception ex)
- {
- Serilog.Log.Error(ex, $"{JobType}-图片视频同步-处理图片集并合成视频时出错");
- return null;
- }
-
- }
-
- ///
- /// 生成NFO文件
- ///
- protected async Task GenerateNfoFile(string saveFolder, Aweme item, string tag1, string tag2, string tag3,
- string avatarSavePath, string avatarUrl)
- {
- var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
- NfoFileGenerator.GenerateNfoFile(new DouyinVideoNfo
- {
- Actors = new List
- {
- new() {
- Name = item.Author?.Nickname,
- Role = "主演",
- Thumb = avatarSavePath
- }
- },
- Author = item.Author?.Nickname,
- Poster = Path.Combine(saveFolder, "poster.jpg"),
- Title = item.Desc,
- Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
- ReleaseDate = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
- Genres = new List { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
- }, nfoPath);
- }
-
- ///
- /// 检查视频数据是否有效
- ///
- ///
- ///
- protected bool IsAwemeValid(Aweme item) => item != null && item.Video != null && item.Video.BitRate != null;
- ///
- /// 获取视频标签信息
- ///
- ///
- ///
- protected (string tag1, string tag2, string tag3) GetVideoTags(Aweme item)
- {
- var tags = item.VideoTags;
- return (
- tags?.FirstOrDefault(x => x.Level == 1)?.TagName,
- tags?.FirstOrDefault(x => x.Level == 2)?.TagName,
- tags?.FirstOrDefault(x => x.Level == 3)?.TagName
- );
- }
- ///
- /// AOP
- ///
- ///
- protected virtual Task BeforeProcessCookies() => Task.CompletedTask;
-
- protected abstract Task> GetValidCookies();
- protected abstract bool IsCookieValid(DouyinUserCookie cookie);
- protected abstract Task FetchVideoData(DouyinUserCookie cookie, string cursor);
- protected abstract bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data);
- protected abstract string GetNextCursor(DouyinVideoInfo data);
- protected abstract string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2);
- protected abstract string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate);
- protected abstract string GetAuthorAvatarBasePath(DouyinUserCookie cookie);
- protected abstract Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount);
- protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item);
-
+ #endregion
}
+ ///
+ /// 视频实体差异信息类
+ /// 用于存储不同类型视频之间的差异信息
+ ///
public class VideoEntityDifferences
{
+ ///
+ /// 视频类型
+ ///
public VideoTypeEnum VideoType { get; set; }
+
+ ///
+ /// 简化的视频标题
+ ///
public string VideoTitleSimplify { get; set; } = string.Empty;
}
+
}
\ No newline at end of file
diff --git a/model/AppConfig.cs b/model/AppConfig.cs
index 296ddfa..6b02b8f 100644
--- a/model/AppConfig.cs
+++ b/model/AppConfig.cs
@@ -22,10 +22,13 @@ namespace dy.net.model
public int BatchCount { get; set; } = 10;
///
- /// 博主视频是否 直接用标题做文件名,不另外加一层文件夹
+ /// 博主视频是否 直接用标题做文件名
///
public bool UperUseViedoTitle { get; set; }
-
+ ///
+ /// 是否博主视频直接放一个根目录,不另外按名字建文件夹
+ ///
+ public bool UperSaveTogether { get; set; }
///
/// 是否下载图片视频
///
diff --git a/service/DouyinQuartzJobService.cs b/service/DouyinQuartzJobService.cs
index ea10edd..6ca1407 100644
--- a/service/DouyinQuartzJobService.cs
+++ b/service/DouyinQuartzJobService.cs
@@ -29,7 +29,7 @@ namespace dy.net.service
cron++;
expression = cron.ToString();
}
- //await StartFavoriteJob(expression);
+ await StartFavoriteJob(expression);
await Task.Delay(delay);
if (int.TryParse(expression, out int cron2))
@@ -37,7 +37,7 @@ namespace dy.net.service
cron2++;
expression = cron2.ToString();
}
- await StartUperPostJob(expression);
+ //await StartUperPostJob(expression);
}
private async Task StartCollectJob(string expression)
diff --git a/utils/TikTokFileNameHelper.cs b/utils/TikTokFileNameHelper.cs
index 1f0c687..0e7fa0e 100644
--- a/utils/TikTokFileNameHelper.cs
+++ b/utils/TikTokFileNameHelper.cs
@@ -71,8 +71,8 @@ namespace dy.net.utils
//title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
- string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
- title = Regex.Replace(title, emojiPattern, "", RegexOptions.Compiled);
+ //string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
+ //title = Regex.Replace(title, emojiPattern, "", RegexOptions.Compiled);
// 步骤3:过滤系统非法字符(Windows/macOS/Linux通用禁止)
char[] illegalChars = new[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0', '\t', '\n', '\r' };