1、去重
2、分享 3、视频播放 4、批量删除,重新下载 5、自定义标题(仅博主视频) 6、授权页面去掉博主添加,新增关注列表 7、图文视频合成优化 8、其他优化
This commit is contained in:
@@ -48,14 +48,18 @@ namespace dy.net.service
|
||||
AppConfig config = new AppConfig
|
||||
{
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Cron = "30",
|
||||
BatchCount = 10,
|
||||
Cron = 30,
|
||||
BatchCount = 18,
|
||||
LogKeepDay = 10,
|
||||
UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
|
||||
UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名
|
||||
DownImageVideo = downLoadImage,
|
||||
DownMp3 = false,
|
||||
DownImage = false
|
||||
DownImage = false,
|
||||
ImageViedoSaveAlone = true,
|
||||
FollowedTitleTemplate ="",
|
||||
FullFollowedTitleTemplate="",
|
||||
FollowedTitleSeparator=""
|
||||
};
|
||||
sqlSugarClient.Insertable(config).ExecuteCommand();
|
||||
return config;
|
||||
@@ -81,11 +85,16 @@ namespace dy.net.service
|
||||
cc.UperSaveTogether = config.UperSaveTogether;
|
||||
cc.UperUseViedoTitle = config.UperUseViedoTitle;
|
||||
cc.LogKeepDay = config.LogKeepDay;
|
||||
cc.DownMp3= config.DownMp3;
|
||||
cc.DownImage= config.DownImage;
|
||||
cc.DownMp3 = config.DownMp3;
|
||||
cc.DownImage = config.DownImage;
|
||||
cc.FollowedTitleTemplate = config.FollowedTitleTemplate;
|
||||
cc.FullFollowedTitleTemplate= config.FullFollowedTitleTemplate;
|
||||
cc.ImageViedoSaveAlone = config.ImageViedoSaveAlone;
|
||||
cc.FollowedTitleSeparator = config.FollowedTitleSeparator;
|
||||
cc.AutoDistinct = config.AutoDistinct;
|
||||
}
|
||||
|
||||
var update = await sqlSugarClient.Updateable<AppConfig>(cc).ExecuteCommandAsync();
|
||||
int update = await sqlSugarClient.Updateable(cc).ExecuteCommandAsync();
|
||||
|
||||
return update > 0;
|
||||
}
|
||||
@@ -96,7 +105,7 @@ namespace dy.net.service
|
||||
public void UpdateCollectViedoType()
|
||||
{
|
||||
|
||||
string sql= @"UPDATE dy_collect_video
|
||||
string sql = @"UPDATE dy_collect_video
|
||||
SET ViedoType = CASE
|
||||
WHEN ViedoType = '0' THEN 0
|
||||
WHEN ViedoType = '1' THEN 1
|
||||
@@ -124,33 +133,30 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public bool UpdateAllCookieSyncedToZero()
|
||||
{
|
||||
//var sql = "update dy_cookie set CollHasSyncd=0,FavHasSyncd=0,UperSyncd=0";
|
||||
// sqlSugarClient.Ado.ExecuteCommand(sql) > 0;
|
||||
var cookies = sqlSugarClient.Queryable<DouyinUserCookie>().ToList();
|
||||
|
||||
var cookies = sqlSugarClient.Queryable<DouyinCookie>().ToList();
|
||||
foreach (var cookie in cookies)
|
||||
{
|
||||
cookie.CollHasSyncd = 0;
|
||||
cookie.FavHasSyncd = 0;
|
||||
cookie.UperSyncd = 0;
|
||||
var upers = cookie.UpSecUserIds;
|
||||
if (!string.IsNullOrWhiteSpace(upers))
|
||||
{
|
||||
var uperList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(upers);
|
||||
if (uperList != null && uperList.Count > 0)
|
||||
{
|
||||
foreach (var uper in uperList)
|
||||
{
|
||||
uper.syncAll = false;
|
||||
}
|
||||
}
|
||||
var newUpers = Newtonsoft.Json.JsonConvert.SerializeObject(uperList);
|
||||
cookie.UpSecUserIds = newUpers;
|
||||
}
|
||||
}
|
||||
return sqlSugarClient.Updateable(cookies).ExecuteCommand() > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询需要下载的所有重下载视频记录
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<ViedoReDown>> GetAllRedown()
|
||||
{
|
||||
return await sqlSugarClient.Queryable<ViedoReDown>().Where(x => x.Status == 0 || x.Status == 2).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateRedownStatus(List<ViedoReDown> list)
|
||||
{
|
||||
return await sqlSugarClient.Updateable(list).ExecuteCommandAsync() > 0;
|
||||
}
|
||||
#region 测试创建数据库
|
||||
|
||||
///// <summary>
|
||||
|
||||
@@ -8,20 +8,20 @@ namespace dy.net.service
|
||||
public class DouyinCookieService
|
||||
{
|
||||
|
||||
private readonly DouyinUserCookieRepository _cookieRepository;
|
||||
private readonly DouyinCookieRepository _cookieRepository;
|
||||
|
||||
public DouyinCookieService(DouyinUserCookieRepository cookieRepository)
|
||||
public DouyinCookieService(DouyinCookieRepository cookieRepository)
|
||||
{
|
||||
_cookieRepository = cookieRepository;
|
||||
}
|
||||
|
||||
public Task<List<DouyinUserCookie>> GetAllCookies()
|
||||
public Task<List<DouyinCookie>> GetAllOpendAsync(Expression<Func<DouyinCookie, bool>> whereExpression = null)
|
||||
{
|
||||
return _cookieRepository.GetAllCookies();
|
||||
return _cookieRepository.GetAllCookies(whereExpression);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Add(DouyinUserCookie dyUserCookies)
|
||||
public async Task<bool> Add(DouyinCookie dyUserCookies)
|
||||
{
|
||||
return await _cookieRepository.InsertAsync(dyUserCookies);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace dy.net.service
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var cookie = new DouyinUserCookie
|
||||
var cookie = new DouyinCookie
|
||||
{
|
||||
UserName = "douyin",
|
||||
Cookies = "--",
|
||||
@@ -53,19 +53,19 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
// 查询单个
|
||||
public async Task<DouyinUserCookie> GetByIdAsync(string id)
|
||||
public async Task<DouyinCookie> GetByIdAsync(string id)
|
||||
{
|
||||
return await _cookieRepository.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
// 查询列表(可加条件)
|
||||
public async Task<(List<DouyinUserCookie> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
|
||||
public async Task<(List<DouyinCookie> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
|
||||
{
|
||||
return await _cookieRepository.GetPagedAsync(pageIndex, pageSize);
|
||||
}
|
||||
|
||||
// 更新
|
||||
public async Task<bool> UpdateAsync(DouyinUserCookie dyUserCookies)
|
||||
public async Task<bool> UpdateAsync(DouyinCookie dyUserCookies)
|
||||
{
|
||||
return await _cookieRepository.UpdateAsync(dyUserCookies);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using dy.net.dto;
|
||||
using dy.net.model;
|
||||
using dy.net.repository;
|
||||
using SqlSugar;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
public class DouyinFollowService
|
||||
{
|
||||
|
||||
private readonly DouyinFollowRepository _followRepository;
|
||||
|
||||
public DouyinFollowService(DouyinFollowRepository followRepository)
|
||||
{
|
||||
_followRepository = followRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(List<DouyinFollowed> list, int totalCount)> GetPagedAsync(FollowRequestDto dto)
|
||||
{
|
||||
return await _followRepository.GetPagedAsync(dto);
|
||||
}
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="followInfos"></param>
|
||||
/// <param name="myselfUserId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> Sync(List<FollowingsItem> followInfos, string myselfUserId)
|
||||
{
|
||||
return await _followRepository.Sync(followInfos, myselfUserId);
|
||||
}
|
||||
|
||||
public async Task<DouyinFollowed> GetByUperId(string uperId,string myUid)
|
||||
{
|
||||
return await _followRepository.GetBySecUId(uperId, myUid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开或关闭同步
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> OpenOrCloseSync(FollowUpdateDto dto)
|
||||
{
|
||||
var followed = await _followRepository.GetByIdAsync(dto.Id);
|
||||
if (followed != null)
|
||||
{
|
||||
followed.OpenSync = dto.OpenSync;
|
||||
followed.FullSync = dto.FullSync;
|
||||
followed.SavePath = dto.SavePath;
|
||||
return await _followRepository.Update(followed);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 打开或关闭同步
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> OpenOrCloseFullSync(FollowUpdateDto dto)
|
||||
{
|
||||
var followed = await _followRepository.GetByIdAsync(dto.Id);
|
||||
if (followed != null)
|
||||
{
|
||||
followed.FullSync = dto.FullSync;
|
||||
return await _followRepository.Update(followed);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取需要同步的关注列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<DouyinFollowed>> GetSyncFollows(string userUserId)
|
||||
{
|
||||
return await _followRepository.GetSyncFollows(userUserId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using dy.net.dto;
|
||||
using dy.net.utils;
|
||||
using NetTaste;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection.PortableExecutable;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
@@ -195,10 +198,10 @@ namespace dy.net.service
|
||||
// 构建请求URL
|
||||
var queryString = new FormUrlEncodedContent(parameters);
|
||||
string fullUrl = $"{DouYinApi}/post?{await queryString.ReadAsStringAsync()}";
|
||||
var ablog = new ABogus();//计算X-Bogus
|
||||
var a_bogus = ablog.GetValue(parameters);
|
||||
//var ablog = new ABogus();//计算X-Bogus
|
||||
//var a_bogus = ablog.GetValue(parameters);
|
||||
|
||||
fullUrl += $"&X-Bogus={a_bogus}";
|
||||
//fullUrl += $"&X-Bogus={a_bogus}";
|
||||
var respose = await httpClient.GetAsync(fullUrl);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -219,6 +222,61 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 查询我的关注用户列表
|
||||
/// </summary>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="secUserId"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<DouyinFollowInfo> SyncMyFollows(string count,string offset,string secUserId,string cookie)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = _clientFactory.CreateClient("dy_follow");
|
||||
if (httpClient.DefaultRequestHeaders.Contains("Cookie"))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Remove("Cookie");
|
||||
}
|
||||
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
var dics = DouyinBaseParamDics.MyFollowParams;
|
||||
{
|
||||
// 添加动态参数
|
||||
dics["sec_user_id"] = secUserId;
|
||||
dics["count"] = count;
|
||||
dics["offset"] = offset;
|
||||
}
|
||||
// 构建请求URL
|
||||
var queryString = new FormUrlEncodedContent(dics);
|
||||
string fullUrl = $"https://www.douyin.com/aweme/v1/web/user/following/list/?{await queryString.ReadAsStringAsync()}";
|
||||
var respose = await httpClient.GetAsync(fullUrl);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<DouyinFollowInfo>(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"SyncMyFollows fail: {respose.StatusCode}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error($"SyncMyFollows error: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> SyncReDown(string awemeId,string cookie)
|
||||
{
|
||||
return false;
|
||||
//有空再研究把.a_bogus算法有问题
|
||||
}
|
||||
|
||||
//AI优化2
|
||||
/// <summary>
|
||||
/// 下载文件并保存到本地(支持重试机制,优化批量下载稳定性)
|
||||
@@ -370,6 +428,28 @@ namespace dy.net.service
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
///// <summary>下载网络文件到指定路径</summary>
|
||||
//public async Task DownloadFileAsync(string url, string savePath)
|
||||
//{
|
||||
// if (string.IsNullOrEmpty(url)) throw new ArgumentNullException(nameof(url));
|
||||
// if (string.IsNullOrEmpty(savePath)) throw new ArgumentNullException(nameof(savePath));
|
||||
|
||||
// // 创建目录(如果不存在)
|
||||
// var directory = Path.GetDirectoryName(savePath)!;
|
||||
// if (!Directory.Exists(directory)) Directory.CreateDirectory(directory);
|
||||
|
||||
// // 下载文件
|
||||
// using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||
// response.EnsureSuccessStatusCode(); // 非2xx状态码抛出异常
|
||||
|
||||
// using var stream = await response.Content.ReadAsStreamAsync();
|
||||
// using var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
||||
// await stream.CopyToAsync(fileStream);
|
||||
//}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 判断异常是否可重试
|
||||
/// </summary>
|
||||
@@ -385,7 +465,7 @@ namespace dy.net.service
|
||||
/// <summary>
|
||||
/// 清理不完整的文件
|
||||
/// </summary>
|
||||
private void CleanupIncompleteFile(string savePath)
|
||||
private static void CleanupIncompleteFile(string savePath)
|
||||
{
|
||||
if (File.Exists(savePath))
|
||||
{
|
||||
@@ -400,212 +480,5 @@ namespace dy.net.service
|
||||
}
|
||||
}
|
||||
|
||||
//--AI优化后的-1
|
||||
|
||||
///// <summary>
|
||||
///// 下载文件并保存到本地(优化批量下载假死问题)
|
||||
///// </summary>
|
||||
///// <param name="videoUrl">文件地址</param>
|
||||
///// <param name="savePath">保存路径</param>
|
||||
///// <param name="cookie">请求Cookie</param>
|
||||
///// <param name="cancellationToken">取消令牌(用于终止卡住的任务)</param>
|
||||
///// <param name="streamTimeout">流读取超时时间(默认30秒)</param>
|
||||
///// <returns>是否下载成功</returns>
|
||||
//public async Task<bool> DownloadAsync(
|
||||
// string videoUrl,
|
||||
// string savePath,
|
||||
// string cookie, string httpclientName=null,
|
||||
// CancellationToken cancellationToken = default,
|
||||
// TimeSpan? streamTimeout = null)
|
||||
//{
|
||||
// // 流读取超时默认60秒(避免长时间无数据导致假死)
|
||||
// var streamTimeoutValue = streamTimeout ?? TimeSpan.FromSeconds(60);
|
||||
// // 记录上次流活动时间(用于超时判断)
|
||||
// DateTime lastStreamActivity = DateTime.UtcNow;
|
||||
|
||||
// try
|
||||
// {
|
||||
// // 确保目录存在
|
||||
// var directory = Path.GetDirectoryName(savePath);
|
||||
// if (!Directory.Exists(directory))
|
||||
// {
|
||||
// Directory.CreateDirectory(directory);
|
||||
// }
|
||||
|
||||
// // 先删除已存在文件(处理文件占用问题)
|
||||
// if (File.Exists(savePath))
|
||||
// {
|
||||
// // 重试删除(防止文件刚被释放)
|
||||
// for (int i = 0; i < 3; i++)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// File.Delete(savePath);
|
||||
// break;
|
||||
// }
|
||||
// catch (IOException) when (i < 2)
|
||||
// {
|
||||
// await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// httpclientName??="dy_down1";
|
||||
// // 使用客户端工厂创建HttpClient(复用连接池)
|
||||
// using (var httpClient = _clientFactory.CreateClient(httpclientName))
|
||||
// {
|
||||
// // 清理并添加Cookie
|
||||
// httpClient.DefaultRequestHeaders.Remove("Cookie");
|
||||
// httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
// // 总请求超时(包括连接和初始响应)
|
||||
// httpClient.Timeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
// // 发起请求(响应头就绪后返回,不等待完整内容)
|
||||
// using (var response = await httpClient.GetAsync(
|
||||
// videoUrl,
|
||||
// HttpCompletionOption.ResponseHeadersRead,
|
||||
// cancellationToken).ConfigureAwait(false))
|
||||
// {
|
||||
// response.EnsureSuccessStatusCode(); // 验证HTTP状态码
|
||||
|
||||
// // 获取文件总大小(用于进度计算)
|
||||
// long? totalBytes = response.Content.Headers.ContentLength;
|
||||
|
||||
// // 读取响应流并写入文件
|
||||
// using (var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken)
|
||||
// .ConfigureAwait(false))
|
||||
// // 优化FileStream:异步模式+顺序扫描(提升大文件写入效率)
|
||||
// using (var fileStream = new FileStream(
|
||||
// savePath,
|
||||
// FileMode.CreateNew,
|
||||
// FileAccess.Write,
|
||||
// FileShare.None,
|
||||
// bufferSize: 8192,
|
||||
// options: FileOptions.Asynchronous | FileOptions.SequentialScan))
|
||||
// {
|
||||
// byte[] buffer = new byte[8192];
|
||||
// int bytesRead;
|
||||
// long totalRead = 0;
|
||||
|
||||
// // 循环读取流(带超时和取消检查)
|
||||
// while ((bytesRead = await responseStream.ReadAsync(
|
||||
// buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0)
|
||||
// {
|
||||
// // 检查流读取超时(长时间无数据)
|
||||
// if (DateTime.UtcNow - lastStreamActivity > streamTimeoutValue)
|
||||
// {
|
||||
// throw new TimeoutException($"流读取超时({streamTimeoutValue.TotalSeconds}秒无数据)");
|
||||
// }
|
||||
|
||||
// // 写入文件
|
||||
// await fileStream.WriteAsync(
|
||||
// buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// // 更新进度和活动时间
|
||||
// totalRead += bytesRead;
|
||||
// lastStreamActivity = DateTime.UtcNow;
|
||||
|
||||
// // (可选)进度上报逻辑
|
||||
// if (totalBytes.HasValue)
|
||||
// {
|
||||
// double progress = (double)totalRead / totalBytes.Value * 100;
|
||||
// // 可通过事件或委托上报进度:OnProgressChanged(progress);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 确保数据刷入磁盘
|
||||
// await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
// {
|
||||
// // 正常取消操作(非错误)
|
||||
// Serilog.Log.Information($"下载被取消:{videoUrl}");
|
||||
// return false;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// // 记录错误并清理可能的不完整文件
|
||||
// Serilog.Log.Error(ex, $"下载失败:{videoUrl}");
|
||||
// if (File.Exists(savePath))
|
||||
// {
|
||||
// try { File.Delete(savePath); } catch { /* 忽略删除失败 */ }
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// 下载文件并保存到本地
|
||||
///// </summary>
|
||||
///// <param name="videoUrl">文件地址</param>
|
||||
///// <param name="savePath">保存路径</param>
|
||||
///// <param name="cookie"></param>
|
||||
//public async Task<bool> DownloadAsync(string videoUrl, string savePath, string cookie)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// // 防止文件被占用,先删除已存在的文件
|
||||
// if (File.Exists(savePath))
|
||||
// {
|
||||
// File.Delete(savePath);
|
||||
// }
|
||||
// // 创建HTTP客户端(设置超时时间和请求头)
|
||||
// using (var httpClient = _clientFactory.CreateClient("dy_down"))
|
||||
// {
|
||||
// if (httpClient.DefaultRequestHeaders.Contains("Cookie"))
|
||||
// {
|
||||
// httpClient.DefaultRequestHeaders.Remove("Cookie");
|
||||
// }
|
||||
// httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
// httpClient.Timeout = TimeSpan.FromMinutes(5); // 设置5分钟超时
|
||||
|
||||
// // 获取视频流
|
||||
// using (var response = await httpClient.GetAsync(videoUrl, HttpCompletionOption.ResponseHeadersRead))
|
||||
// {
|
||||
// response.EnsureSuccessStatusCode(); // 确保请求成功
|
||||
|
||||
// // 获取文件总大小(用于进度显示)
|
||||
// long? totalBytes = response.Content.Headers.ContentLength;
|
||||
|
||||
// // 读取流并写入文件
|
||||
// using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
// using (var fileStream = new FileStream(savePath, FileMode.CreateNew))
|
||||
// {
|
||||
// byte[] buffer = new byte[8192];
|
||||
// int bytesRead;
|
||||
// long totalRead = 0;
|
||||
|
||||
// // 循环读取并写入
|
||||
// while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
// {
|
||||
// await fileStream.WriteAsync(buffer, 0, bytesRead);
|
||||
// totalRead += bytesRead;
|
||||
|
||||
// // 显示下载进度
|
||||
// if (totalBytes.HasValue)
|
||||
// {
|
||||
// double progress = (double)totalRead / totalBytes.Value * 100;
|
||||
// //Console.Write($"\r下载进度:{progress:F2}% ({totalRead}/{totalBytes.Value} bytes)");
|
||||
// }
|
||||
// }
|
||||
// //Console.WriteLine(); // 进度显示结束后换行
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Serilog.Log.Error($"DownloadVideoAsync fail: {ex.Message}");
|
||||
// Serilog.Log.Error($"DownloadVideoAsync fail: {ex.StackTrace}");
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
using dy.net.dto;
|
||||
using dy.net.utils;
|
||||
using Serilog;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 合并多张图片+音频为视频
|
||||
/// </summary>
|
||||
public class DouyinMergeVideoService
|
||||
{
|
||||
private readonly FFmpegHelper _fFmpegHelper;
|
||||
private readonly DouyinHttpClientService douyinHttpClientService;
|
||||
|
||||
public DouyinMergeVideoService(FFmpegHelper fFmpegHelper,DouyinHttpClientService douyinHttpClientService)
|
||||
{
|
||||
_fFmpegHelper = fFmpegHelper;
|
||||
this.douyinHttpClientService = douyinHttpClientService;
|
||||
}
|
||||
|
||||
// 1. 并发控制:限制同时运行的 FFmpeg 进程数(建议设为 1,FFmpeg 单进程更稳定)
|
||||
private readonly SemaphoreSlim _ffmpegSemaphore = new SemaphoreSlim(1, 1);
|
||||
// 重试次数配置
|
||||
private const int RetryCount = 3;
|
||||
// 重试间隔(毫秒)
|
||||
private const int RetryDelay = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 视频合成
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="rootPath"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="outputVideoPath"></param>
|
||||
/// <param name="fileNamefolder"></param>
|
||||
/// <param name="mergeImg2Viedo"></param>
|
||||
/// <param name="downImage"></param>
|
||||
/// <param name="downMp3"></param>
|
||||
/// <returns></returns>
|
||||
//public async Task<bool> MergeToVideo(string cookie,string rootPath, MediaMergeRequest request,string outputVideoPath,string fileNamefolder,bool mergeImg2Viedo,bool downImage=false,bool downMp3=false)
|
||||
//{
|
||||
|
||||
// try
|
||||
// {
|
||||
// // 创建唯一临时目录(避免并发冲突)
|
||||
// var tempDir = Path.Combine(rootPath, "temp", Guid.NewGuid().ToString());
|
||||
// try
|
||||
// {
|
||||
// // 1. 下载图片
|
||||
// var (rawImages, error) = await DownloadMediaAsync(request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp",cookie);
|
||||
// if (!string.IsNullOrEmpty(error))
|
||||
// {
|
||||
// Serilog.Log.Error($"{error}");
|
||||
// return false;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if(downImage)
|
||||
// {
|
||||
// for (int i = 0; i < rawImages.Length; i++)
|
||||
// {
|
||||
// string sourcePath = rawImages[i];
|
||||
// // 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png
|
||||
// string extension = Path.GetExtension(sourcePath);
|
||||
// string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0
|
||||
// string destPath = Path.Combine(fileNamefolder, destFileName);
|
||||
// if (destPath.Contains("小可爱") || sourcePath.Contains("小可爱")) {
|
||||
// Console.WriteLine("发现小可爱图片");
|
||||
// }
|
||||
// if (!File.Exists(destPath))
|
||||
// File.Copy(sourcePath, destPath);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// // 2. 下载音频
|
||||
// var (rawAudios, audioError) = await DownloadMediaAsync(request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
|
||||
// if (!string.IsNullOrEmpty(audioError))
|
||||
// {
|
||||
// Serilog.Log.Error($"{audioError}");
|
||||
// return false;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if(downMp3)
|
||||
// {
|
||||
// for (int i = 0; i < rawAudios.Length; i++)
|
||||
// {
|
||||
// string sourcePath = rawAudios[i];
|
||||
// // 重命名为有规律的文件名,如 temp_001.mp3, temp_002.mp3
|
||||
// string extension = Path.GetExtension(sourcePath);
|
||||
// string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0
|
||||
// string destPath = Path.Combine(fileNamefolder, destFileName);
|
||||
// if (!File.Exists(destPath))
|
||||
// File.Copy(sourcePath, destPath);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (!mergeImg2Viedo)
|
||||
// {
|
||||
// // 不合成视频,直接返回成功
|
||||
// Serilog.Log.Debug($"不合成视频,直接返回");
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// // 4. 合成视频
|
||||
// //var outputVideoPath = Path.Combine(tempDir, "output", $"merged-video.{request.OutputFormat.ToLower()}");
|
||||
|
||||
// // 2. 创建帮助类实例
|
||||
// // 在Docker容器内,FFmpeg通常在PATH中,所以直接用 "ffmpeg" 即可
|
||||
|
||||
// // 根据图片数量调整每张图片显示时长
|
||||
// if (request.ImageUrls.Count <= 3)
|
||||
// {
|
||||
// request.ImageDurationPerSecond = 3;
|
||||
// }
|
||||
// if (request.ImageUrls.Count > 20)
|
||||
// {
|
||||
// request.ImageDurationPerSecond = 2;
|
||||
// }
|
||||
// // 3. (可选)自定义视频参数
|
||||
// _fFmpegHelper.VideoWidth = 1080;
|
||||
// _fFmpegHelper.VideoHeight = 1920;
|
||||
// _fFmpegHelper.ImageDisplayDurationSeconds = request.ImageDurationPerSecond;
|
||||
// _fFmpegHelper.OutputFrameRate = 30;
|
||||
|
||||
// // 4. 创建进度
|
||||
// var progress = new Progress<double>(p =>
|
||||
// {
|
||||
// Console.WriteLine($"进度: {p:F2}%");
|
||||
// });
|
||||
|
||||
// // 5. 执行合成任务
|
||||
// using (var cancellationTokenSource = new CancellationTokenSource())
|
||||
// {
|
||||
// string resultPath = await _fFmpegHelper.CreateVideoFromImagesAndAudioAsync(
|
||||
// rawImages,
|
||||
// rawAudios[0],
|
||||
// outputVideoPath,
|
||||
// request.VideoWidth,
|
||||
// request.VideoHeight,
|
||||
// progress,
|
||||
// cancellationTokenSource.Token);
|
||||
|
||||
// //Console.WriteLine($"视频合成成功!文件已保存至: {resultPath}");
|
||||
// Serilog.Log.Debug($"视频合成成功!文件已保存至: {resultPath}");
|
||||
// }
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// // 清理临时目录(无论成功失败)
|
||||
// if (Directory.Exists(tempDir))
|
||||
// {
|
||||
// Directory.Delete(tempDir, recursive: true);
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Serilog.Log.Error($"{ex.StackTrace}");
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 视频合成(优化后:解决 FFmpeg 进程冲突,支持容错重试)
|
||||
/// </summary>
|
||||
public async Task<bool> MergeToVideo(string cookie, string rootPath, MediaMergeRequest request,
|
||||
string outputVideoPath, string fileNamefolder, bool mergeImg2Viedo, bool downImage = false, bool downMp3 = false)
|
||||
{
|
||||
// 输入参数校验(避免无效执行)
|
||||
if (request == null || request.ImageUrls == null || request.ImageUrls.Count == 0)
|
||||
{
|
||||
Log.Error("图片URL列表为空,无法合成视频");
|
||||
return false;
|
||||
}
|
||||
if (mergeImg2Viedo && (request.AudioUrls == null || request.AudioUrls.Count == 0))
|
||||
{
|
||||
Log.Error("合成视频时音频URL列表为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
string tempDir = null;
|
||||
try
|
||||
{
|
||||
// 创建唯一临时目录(避免并发冲突)
|
||||
tempDir = Path.Combine(rootPath, "temp", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir); // 确保目录存在
|
||||
|
||||
// 1. 下载图片
|
||||
var (rawImages, imageError) = await DownloadMediaAsync(
|
||||
request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp", cookie);
|
||||
if (!string.IsNullOrEmpty(imageError) || rawImages == null || rawImages.Length == 0)
|
||||
{
|
||||
Log.Error($"图片下载失败:{imageError ?? "未下载到任何图片"}");
|
||||
return false;
|
||||
}
|
||||
// 保存下载的图片(如果需要)
|
||||
if (downImage)
|
||||
{
|
||||
await SaveDownloadedFilesAsync(rawImages, fileNamefolder, "temp_", "jpg");
|
||||
}
|
||||
|
||||
// 2. 下载音频
|
||||
string[] rawAudios = Array.Empty<string>();
|
||||
if (mergeImg2Viedo)
|
||||
{
|
||||
var (audios, audioError) = await DownloadMediaAsync(
|
||||
request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
|
||||
if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Length == 0)
|
||||
{
|
||||
Log.Error($"音频下载失败:{audioError ?? "未下载到任何音频"}");
|
||||
return false;
|
||||
}
|
||||
rawAudios = audios;
|
||||
// 保存下载的音频(如果需要)
|
||||
if (downMp3)
|
||||
{
|
||||
await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, "temp_", "mp3");
|
||||
}
|
||||
}
|
||||
|
||||
// 不合成视频,直接返回成功
|
||||
if (!mergeImg2Viedo)
|
||||
{
|
||||
Log.Debug("不合成视频,直接返回成功");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 调整图片显示时长
|
||||
AdjustImageDuration(request);
|
||||
|
||||
// 4. 容错重试:处理 FFmpeg 进程冲突等临时错误
|
||||
bool mergeSuccess = await RetryOnFfmpegConflictAsync(async () =>
|
||||
{
|
||||
// 并发控制:等待前一个 FFmpeg 进程完成
|
||||
await _ffmpegSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
// 每次合成创建独立的 FFmpegHelper 实例(避免状态共享)
|
||||
var ffmpegHelper = new FFmpegHelper();
|
||||
// 配置视频参数(独立实例,无共享冲突)
|
||||
ffmpegHelper.VideoWidth = request.VideoWidth > 0 ? request.VideoWidth : 1080;
|
||||
ffmpegHelper.VideoHeight = request.VideoHeight > 0 ? request.VideoHeight : 1920;
|
||||
ffmpegHelper.ImageDisplayDurationSeconds = request.ImageDurationPerSecond;
|
||||
ffmpegHelper.OutputFrameRate = 30;
|
||||
|
||||
// 进度回调
|
||||
var progress = new Progress<double>(p =>
|
||||
{
|
||||
Log.Debug($"视频合成进度:{p:F2}%");
|
||||
});
|
||||
|
||||
// 超时控制:避免 FFmpeg 进程无限运行(300秒=5分钟)
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300));
|
||||
// 执行合成(确保每个图片都参与)
|
||||
string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync(
|
||||
rawImages, // 所有下载的图片(确保无遗漏)
|
||||
rawAudios[0], // 取第一个音频(可根据需求调整)
|
||||
outputVideoPath,
|
||||
ffmpegHelper.VideoWidth,
|
||||
ffmpegHelper.VideoHeight,
|
||||
progress,
|
||||
cts.Token);
|
||||
|
||||
Log.Debug($"视频合成成功!文件路径:{resultPath}");
|
||||
return !string.IsNullOrEmpty(resultPath) && File.Exists(resultPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放信号量,允许下一个任务执行
|
||||
_ffmpegSemaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
return mergeSuccess;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "视频合成过程中发生未处理异常");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 安全清理临时目录(确保 FFmpeg 进程已释放文件句柄)
|
||||
await SafeCleanTempDirAsync(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 保存下载的文件(图片/音频)到目标目录
|
||||
/// </summary>
|
||||
private async Task SaveDownloadedFilesAsync(string[] sourcePaths, string targetFolder, string fileNamePrefix, string defaultExt)
|
||||
{
|
||||
if (sourcePaths == null || sourcePaths.Length == 0) return;
|
||||
Directory.CreateDirectory(targetFolder); // 确保目标目录存在
|
||||
|
||||
// 异步保存(不阻塞主线程)
|
||||
await Task.WhenAll(sourcePaths.Select(async (sourcePath, index) =>
|
||||
{
|
||||
if (!File.Exists(sourcePath)) return;
|
||||
string extension = Path.GetExtension(sourcePath) ?? $".{defaultExt}";
|
||||
string destFileName = $"{fileNamePrefix}{index + 1:D3}{extension}";
|
||||
string destPath = Path.Combine(targetFolder, destFileName);
|
||||
|
||||
// 避免文件覆盖,同时确保文件名唯一
|
||||
if (File.Exists(destPath))
|
||||
{
|
||||
destFileName = $"{fileNamePrefix}{index + 1:D3}_{Guid.NewGuid().ToString("N")}{extension}";
|
||||
destPath = Path.Combine(targetFolder, destFileName);
|
||||
}
|
||||
|
||||
// 复制文件(异步避免阻塞)
|
||||
await Task.Run(() => File.Copy(sourcePath, destPath, overwrite: false));
|
||||
Log.Debug($"已保存文件:{destPath}");
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FFmpeg 进程冲突时重试
|
||||
/// </summary>
|
||||
private async Task<bool> RetryOnFfmpegConflictAsync(Func<Task<bool>> action)
|
||||
{
|
||||
int retryCount = 0;
|
||||
while (retryCount < RetryCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 识别 FFmpeg 进程冲突相关异常(根据实际异常信息调整条件)
|
||||
if (ex.Message.Contains("FFmpeg 进程正在运行") ||
|
||||
ex.Message.Contains("进程已在运行") ||
|
||||
ex.Message.Contains("文件被另一个进程占用"))
|
||||
{
|
||||
retryCount++;
|
||||
Log.Warning($"FFmpeg 进程冲突,第 {retryCount}/{RetryCount} 次重试... 异常信息:{ex.Message}");
|
||||
await Task.Delay(RetryDelay * retryCount); // 重试间隔递增
|
||||
continue;
|
||||
}
|
||||
// 非进程冲突异常,直接抛出
|
||||
throw;
|
||||
}
|
||||
}
|
||||
Log.Error($"FFmpeg 进程冲突,重试 {RetryCount} 次后仍失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 安全清理临时目录(避免文件被占用)
|
||||
/// </summary>
|
||||
private async Task SafeCleanTempDirAsync(string tempDir)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tempDir) || !Directory.Exists(tempDir))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// 延迟清理:给 FFmpeg 进程足够时间释放文件句柄(1秒)
|
||||
await Task.Delay(1000);
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
Log.Debug($"临时目录已清理:{tempDir}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 清理失败时备份目录,避免占用磁盘空间
|
||||
string backupDir = $"{tempDir}_backup_{Guid.NewGuid().ToString("N")}";
|
||||
Directory.Move(tempDir, backupDir);
|
||||
Log.Error(ex, $"临时目录清理失败,已备份至:{backupDir}");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 调整图片显示时长
|
||||
/// </summary>
|
||||
private void AdjustImageDuration(MediaMergeRequest request)
|
||||
{
|
||||
if (request.ImageUrls.Count <= 3)
|
||||
request.ImageDurationPerSecond = 3;
|
||||
else if (request.ImageUrls.Count > 20)
|
||||
request.ImageDurationPerSecond = 2;
|
||||
// 中间数量保持原有配置
|
||||
}
|
||||
|
||||
|
||||
/// <summary>通用媒体下载方法</summary>
|
||||
private async Task<(string[] SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
|
||||
List<string> urls, string saveDir, string prefix, string ext,string cookie)
|
||||
{
|
||||
var successPaths = new List<string>();
|
||||
for (var i = 0; i < urls.Count; i++)
|
||||
{
|
||||
var url = urls[i];
|
||||
var fileExt = ext ?? Path.GetExtension(url).TrimStart('.') ?? "png";
|
||||
var fileName = $"{prefix}{i + 1}.{fileExt}";
|
||||
var savePath = Path.Combine(saveDir, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await douyinHttpClientService.DownloadAsync(url, savePath, cookie);
|
||||
successPaths.Add(savePath);
|
||||
//Console.WriteLine($"下载成功:{url} → {savePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"下载失败:{url},错误:{ex.Message}";
|
||||
Serilog.Log.Error(error);
|
||||
return (Array.Empty<string>(), error);
|
||||
}
|
||||
}
|
||||
return (successPaths.ToArray(), null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+248
-146
@@ -1,201 +1,303 @@
|
||||
using Quartz;
|
||||
using dy.net.dto;
|
||||
using dy.net.job;
|
||||
using Quartz;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
/// <summary>
|
||||
/// 抖音相关定时任务服务
|
||||
/// </summary>
|
||||
public class DouyinQuartzJobService
|
||||
{
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
private const string DefaultJobGroup = "group1";
|
||||
private const int DefaultIntervalMinutes = 30;
|
||||
private const int DefaultCronStartDelaySeconds = 30;
|
||||
private const int DefaultSimpleStartDelaySeconds = 3;
|
||||
|
||||
|
||||
|
||||
public DouyinQuartzJobService(ISchedulerFactory schedulerFactory)
|
||||
{
|
||||
_schedulerFactory = schedulerFactory;
|
||||
_schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// 启动所有抖音相关定时任务
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <param name="delay"></param>
|
||||
/// <returns></returns>
|
||||
public async Task StartJob(string expression,int delay=5000)
|
||||
/// <param name="expression">Cron表达式或间隔分钟数</param>
|
||||
/// <param name="delayBetweenJobs">任务之间的启动延迟(毫秒)</param>
|
||||
/// <returns>是否启动成功</returns>
|
||||
public async Task<bool> InitOrReStartAllJobs(string expression, int delayBetweenJobs = 5000)
|
||||
{
|
||||
await StartCollectJob(expression);
|
||||
|
||||
await Task.Delay(delay);
|
||||
//如果是数字则加1分钟,减少并发
|
||||
if (int.TryParse(expression, out int cron))
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
{
|
||||
cron++;
|
||||
expression = cron.ToString();
|
||||
Log.Warning("定时任务表达式为空,使用默认配置");
|
||||
expression = DefaultIntervalMinutes.ToString();
|
||||
}
|
||||
await StartFavoriteJob(expression);
|
||||
|
||||
await Task.Delay(delay);
|
||||
if (int.TryParse(expression, out int cron2))
|
||||
// 按顺序启动任务,避免并发
|
||||
var jobTasks = new List<Task<bool>>
|
||||
{
|
||||
cron2++;
|
||||
expression = cron2.ToString();
|
||||
}
|
||||
await StartUperPostJob(expression);
|
||||
//我收藏的作品
|
||||
StartJobAsync("collect", expression),
|
||||
//我喜欢的作品
|
||||
DelayAndStartJobAsync("favorite", expression, delayBetweenJobs),
|
||||
//关注的用户的作品
|
||||
DelayAndStartJobAsync("uper", expression, delayBetweenJobs * 2),
|
||||
//关注列表
|
||||
DelayAndStartJobAsync("follow_user", expression, delayBetweenJobs * 3)
|
||||
};
|
||||
|
||||
var results = await Task.WhenAll(jobTasks);
|
||||
return results.All(success => success);
|
||||
}
|
||||
|
||||
private async Task<bool> StartCollectJob(string expression)
|
||||
/// <summary>
|
||||
/// 启动关注同步任务(单次执行)
|
||||
/// </summary>
|
||||
/// <returns>是否启动成功</returns>
|
||||
public async Task<bool> StartFollowJobOnceAsync()
|
||||
{
|
||||
return await StartOneTimeJobAsync("follow_user_once");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 延迟后启动任务
|
||||
/// </summary>
|
||||
private async Task<bool> DelayAndStartJobAsync(string jobKey, string expression, int delayMs)
|
||||
{
|
||||
if (delayMs > 0)
|
||||
{
|
||||
await Task.Delay(delayMs);
|
||||
}
|
||||
|
||||
// 如果是数字表达式,自动递增避免并发
|
||||
var adjustedExpression = AdjustExpressionForConcurrency(jobKey, expression);
|
||||
return await StartJobAsync(jobKey, adjustedExpression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整任务表达式以避免并发
|
||||
/// </summary>
|
||||
private string AdjustExpressionForConcurrency(string jobKey, string expression)
|
||||
{
|
||||
if (!int.TryParse(expression, out int interval))
|
||||
return expression;
|
||||
|
||||
// 根据任务类型递增间隔,避免所有任务同时执行
|
||||
var jobIndex = _jobConfigs.Keys.ToList().IndexOf(jobKey);
|
||||
return (interval + jobIndex).ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动指定定时任务
|
||||
/// </summary>
|
||||
private async Task<bool> StartJobAsync(string configKey, string expression)
|
||||
{
|
||||
if (!_jobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
{
|
||||
Log.Error("找不到任务配置: {ConfigKey}", configKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var __scheduler1 = await _schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey("dy.job.key.collect", "group1");
|
||||
var triggerKey = new TriggerKey("dy.trigger.key.collect", "group1");
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
|
||||
var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup);
|
||||
|
||||
var jobDetail = await __scheduler1.GetJobDetail(jobKey);
|
||||
if (jobDetail != null)
|
||||
// 删除已存在的任务
|
||||
await RemoveExistingJobAsync(scheduler, jobKey);
|
||||
|
||||
// 创建任务详情
|
||||
var jobDetail = JobBuilder.Create(jobConfig.JobType)
|
||||
.WithIdentity(jobKey)
|
||||
.WithDescription(jobConfig.Description)
|
||||
.Build();
|
||||
|
||||
// 创建触发器
|
||||
var trigger = CreateTrigger(triggerKey, expression, jobConfig.Description);
|
||||
if (trigger == null)
|
||||
{
|
||||
//await __scheduler1.Shutdown();
|
||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||
Log.Error("创建触发器失败: {JobDescription}", jobConfig.Description);
|
||||
return false;
|
||||
}
|
||||
|
||||
IJobDetail job = JobBuilder.Create<DouyinCollectSyncJob>()
|
||||
.WithIdentity(jobKey)
|
||||
.Build();
|
||||
ITrigger trigger;
|
||||
//var expression = Appsettings.Get("Interval");
|
||||
if (CronExpression.IsValidExpression(expression))
|
||||
{
|
||||
trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithCronSchedule(expression)
|
||||
.StartAt(DateTime.Now.AddSeconds(30))
|
||||
.StartNow()
|
||||
.Build();
|
||||
}
|
||||
else
|
||||
{
|
||||
int Interval = int.TryParse(expression, out int _Interval) ? _Interval : 30;
|
||||
trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
//.StartNow()
|
||||
.StartAt(DateTime.Now.AddSeconds(3))
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInMinutes(Interval)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
}
|
||||
// Tell Quartz to schedule the job using our trigger
|
||||
await __scheduler1.ScheduleJob(job, trigger);
|
||||
// 调度任务
|
||||
await scheduler.ScheduleJob(jobDetail, trigger);
|
||||
Log.Information("启动定时任务成功 - {JobDescription}, 表达式: {Expression}",
|
||||
jobConfig.Description, expression);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error("start dy.collect job error", ex);
|
||||
Log.Error(ex, "启动定时任务失败 - {JobDescription}", jobConfig.Description);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private async Task<bool> StartFavoriteJob(string expression)
|
||||
/// <summary>
|
||||
/// 启动单次执行任务
|
||||
/// </summary>
|
||||
private async Task<bool> StartOneTimeJobAsync(string configKey)
|
||||
{
|
||||
if (!_jobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
{
|
||||
Log.Error("找不到任务配置: {ConfigKey}", configKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var __scheduler1 = await _schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey("dy.job.key.favorite", "group1");
|
||||
var triggerKey = new TriggerKey("dy.trigger.key.favorite", "group1");
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
|
||||
var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup);
|
||||
|
||||
var jobDetail = await __scheduler1.GetJobDetail(jobKey);
|
||||
if (jobDetail != null)
|
||||
{
|
||||
//await __scheduler1.Shutdown();
|
||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||
}
|
||||
// 删除已存在的任务
|
||||
await RemoveExistingJobAsync(scheduler, jobKey);
|
||||
|
||||
IJobDetail job = JobBuilder.Create<DouyinFavoritSyncJob>()
|
||||
.WithIdentity(jobKey)
|
||||
.Build();
|
||||
ITrigger trigger;
|
||||
//var expression = Appsettings.Get("Interval");
|
||||
if (CronExpression.IsValidExpression(expression))
|
||||
{
|
||||
trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithCronSchedule(expression)
|
||||
.StartAt(DateTime.Now.AddSeconds(30))
|
||||
.StartNow()
|
||||
.Build();
|
||||
}
|
||||
else
|
||||
{
|
||||
int Interval = int.TryParse(expression, out int _Interval) ? _Interval : 30;
|
||||
trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
//.StartNow()
|
||||
.StartAt(DateTime.Now.AddSeconds(3))
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInMinutes(Interval)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
}
|
||||
// Tell Quartz to schedule the job using our trigger
|
||||
await __scheduler1.ScheduleJob(job, trigger);
|
||||
// 创建任务详情
|
||||
var jobDetail = JobBuilder.Create(jobConfig.JobType)
|
||||
.WithIdentity(jobKey)
|
||||
.WithDescription(jobConfig.Description)
|
||||
.Build();
|
||||
|
||||
// 创建立即执行的触发器(只执行一次)
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithDescription($"{jobConfig.Description} - 单次执行")
|
||||
.StartNow()
|
||||
.Build();
|
||||
|
||||
// 调度任务
|
||||
await scheduler.ScheduleJob(jobDetail, trigger);
|
||||
Log.Information("启动单次任务成功 - {JobDescription}", jobConfig.Description);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error("start dy.favorite job error", ex);
|
||||
Log.Error(ex, "启动单次任务失败 - {JobDescription}", jobConfig.Description);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> StartUperPostJob(string expression)
|
||||
/// <summary>
|
||||
/// 创建触发器(支持Cron表达式和简单间隔)
|
||||
/// </summary>
|
||||
private ITrigger? CreateTrigger(TriggerKey triggerKey, string expression, string jobDescription)
|
||||
{
|
||||
try
|
||||
// Cron表达式格式
|
||||
if (CronExpression.IsValidExpression(expression))
|
||||
{
|
||||
var __scheduler1 = await _schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey("dy.job.key.uper", "group1");
|
||||
var triggerKey = new TriggerKey("dy.trigger.key.uper", "group1");
|
||||
return TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithDescription($"{jobDescription} - Cron调度")
|
||||
.WithCronSchedule(expression)
|
||||
.StartAt(DateTime.Now.AddSeconds(DefaultCronStartDelaySeconds))
|
||||
.Build();
|
||||
}
|
||||
|
||||
var jobDetail = await __scheduler1.GetJobDetail(jobKey);
|
||||
if (jobDetail != null)
|
||||
{
|
||||
//await __scheduler1.Shutdown();
|
||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||
}
|
||||
// 数字间隔格式(分钟)
|
||||
if (int.TryParse(expression, out int intervalMinutes))
|
||||
{
|
||||
intervalMinutes = Math.Max(1, intervalMinutes); // 最小间隔1分钟
|
||||
return TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithDescription($"{jobDescription} - 间隔{intervalMinutes}分钟")
|
||||
.StartAt(DateTime.Now.AddSeconds(DefaultSimpleStartDelaySeconds))
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInMinutes(intervalMinutes)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
}
|
||||
|
||||
IJobDetail job = JobBuilder.Create<DouyinUperPostSyncJob>()
|
||||
.WithIdentity(jobKey)
|
||||
.Build();
|
||||
ITrigger trigger;
|
||||
//var expression = Appsettings.Get("Interval");
|
||||
if (CronExpression.IsValidExpression(expression))
|
||||
{
|
||||
trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithCronSchedule(expression)
|
||||
.StartAt(DateTime.Now.AddSeconds(30))
|
||||
.StartNow()
|
||||
.Build();
|
||||
}
|
||||
else
|
||||
{
|
||||
int Interval = int.TryParse(expression, out int _Interval) ? _Interval : 30;
|
||||
trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
//.StartNow()
|
||||
.StartAt(DateTime.Now.AddSeconds(3))
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInMinutes(Interval)
|
||||
// 无效表达式,使用默认配置
|
||||
Log.Warning("无效的任务表达式: {Expression},使用默认间隔{DefaultMinutes}分钟",
|
||||
expression, DefaultIntervalMinutes);
|
||||
|
||||
return TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithDescription($"{jobDescription} - 默认间隔调度")
|
||||
.StartAt(DateTime.Now.AddSeconds(DefaultSimpleStartDelaySeconds))
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInMinutes(DefaultIntervalMinutes)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
}
|
||||
// Tell Quartz to schedule the job using our trigger
|
||||
await __scheduler1.ScheduleJob(job, trigger);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error("start dy.uper job error", ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除已存在的任务
|
||||
/// </summary>
|
||||
private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
||||
{
|
||||
if (await scheduler.CheckExists(jobKey))
|
||||
{
|
||||
Log.Information("移除已存在的任务: {JobKey}", jobKey);
|
||||
await scheduler.DeleteJob(jobKey);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 任务配置信息
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, JobConfig> _jobConfigs = new()
|
||||
{
|
||||
{
|
||||
"collect",
|
||||
new JobConfig(
|
||||
typeof(DouyinCollectSyncJob),
|
||||
"dy.job.key.collect",
|
||||
"dy.trigger.key.collect",
|
||||
"抖音收藏同步任务")
|
||||
},
|
||||
{
|
||||
"favorite",
|
||||
new JobConfig(
|
||||
typeof(DouyinFavoritSyncJob),
|
||||
"dy.job.key.favorite",
|
||||
"dy.trigger.key.favorite",
|
||||
"抖音点赞同步任务")
|
||||
},
|
||||
{
|
||||
"uper",
|
||||
new JobConfig(
|
||||
typeof(DouyinFollowedViedoSyncJob),
|
||||
"dy.job.key.uper",
|
||||
"dy.trigger.key.uper",
|
||||
"抖音UP主作品同步任务")
|
||||
},
|
||||
{
|
||||
"follow_user",
|
||||
new JobConfig(
|
||||
typeof(DouyinFollowedUsersSyncJob),
|
||||
"dy.job.key.follow_user",
|
||||
"dy.trigger.key.follow_user",
|
||||
"抖音关注同步任务")
|
||||
},
|
||||
{
|
||||
"follow_user_once",
|
||||
new JobConfig(
|
||||
typeof(DouyinFollowedUsersSyncJob),
|
||||
"dy.job.key.follow_user_once",
|
||||
"dy.trigger.key.follow_user_once",
|
||||
"抖音关注同步任务(单次执行)")
|
||||
}
|
||||
,
|
||||
//{
|
||||
// "redown_once",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinReDownSyncJob),
|
||||
// "dy.job.key.redown_once",
|
||||
// "dy.trigger.key.redown_once",
|
||||
// "抖音重新下载任务(单次执行)")
|
||||
//}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+173
-11
@@ -1,4 +1,5 @@
|
||||
using dy.net.dto;
|
||||
using ClockSnowFlake;
|
||||
using dy.net.dto;
|
||||
using dy.net.model;
|
||||
using dy.net.repository;
|
||||
using dy.net.utils;
|
||||
@@ -11,10 +12,12 @@ namespace dy.net.service
|
||||
{
|
||||
|
||||
private readonly DouyinVideoRepository _dyCollectVideoRepository;
|
||||
private readonly DouyinCookieRepository douyinCookieRepository;
|
||||
|
||||
public DouyinVideoService(DouyinVideoRepository dyCollectVideoRepository)
|
||||
public DouyinVideoService(DouyinVideoRepository dyCollectVideoRepository, DouyinCookieRepository douyinCookieRepository)
|
||||
{
|
||||
_dyCollectVideoRepository = dyCollectVideoRepository;
|
||||
this.douyinCookieRepository = douyinCookieRepository;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +85,7 @@ namespace dy.net.service
|
||||
};
|
||||
if (data.GraphicVideoSize == "0.00")
|
||||
{
|
||||
if(list.Where(x => x.ViedoType == VideoTypeEnum.ImageVideo).Sum(x => x.FileSize) > 0)
|
||||
if (list.Where(x => x.ViedoType == VideoTypeEnum.ImageVideo).Sum(x => x.FileSize) > 0)
|
||||
{
|
||||
data.GraphicVideoSize = "<0.01";//避免显示0.00误导用户
|
||||
}
|
||||
@@ -91,12 +94,24 @@ namespace dy.net.service
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
//分页查询
|
||||
|
||||
public async Task<(List<DouyinVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null, string viedoType = null, List<string>? dates = null)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="awemeId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<DouyinVideo> GetByAwemeId(string awemeId)
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetPagedAsync(pageIndex, pageSize, tag, author, viedoType, dates);
|
||||
return await _dyCollectVideoRepository.GetFirstAsync(x => x.AwemeId == awemeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(List<DouyinVideo> list, int totalCount)> GetPagedAsync(DouyinVideoPageRequestDto dto)
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetPagedAsync(dto);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,8 +137,155 @@ namespace dy.net.service
|
||||
return await _dyCollectVideoRepository.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新下载选中的视频
|
||||
/// </summary>
|
||||
/// <param name="dto">重新下载请求DTO(包含待处理视频ID列表)</param>
|
||||
/// <returns>是否执行成功(true=流程执行完成,false=无有效数据或执行失败)</returns>
|
||||
/// <exception cref="ArgumentNullException">DTO或ID列表为空时抛出</exception>
|
||||
/// <exception cref="IOException">文件操作失败时抛出(可根据业务调整处理方式)</exception>
|
||||
public async Task<bool> ReDownloadViedoAsync(ReDownViedoDto dto)
|
||||
{
|
||||
// 1. 严格参数校验(避免无效流程)
|
||||
if (dto == null)
|
||||
throw new ArgumentNullException(nameof(dto), "重新下载请求DTO不能为空");
|
||||
if (dto.Ids == null || !dto.Ids.Any())
|
||||
{
|
||||
Serilog.Log.Error("重新下载视频失败:待处理视频ID列表为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 查询有效视频记录(去重+非空校验,避免无效处理)
|
||||
var videoIds = dto.Ids.Distinct().ToList(); // 去重,减少数据库查询和操作
|
||||
var videos = await _dyCollectVideoRepository.GetByIds(videoIds);
|
||||
if (videos == null || !videos.Any())
|
||||
{
|
||||
Serilog.Log.Debug("未查询到有效视频记录:Ids={0}", string.Join(",", videoIds));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 构建重新下载记录(提前准备数据,避免事务内耗时操作)
|
||||
var reDownList = new List<ViedoReDown>();
|
||||
var filePathsToDelete = new List<string>(); // 收集待删除文件路径,统一处理
|
||||
|
||||
foreach (var video in videos)
|
||||
{
|
||||
// 跳过无保存路径的视频(避免无效文件操作)
|
||||
if (string.IsNullOrWhiteSpace(video.VideoSavePath))
|
||||
{
|
||||
Serilog.Log.Debug("视频无保存路径,跳过文件删除:VideoId={0}", video.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构建重新下载记录
|
||||
reDownList.Add(new ViedoReDown
|
||||
{
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
CreateTime = DateTime.UtcNow, // 统一使用UTC时间,避免时区问题
|
||||
Status = 0, // 0=待下载(建议用枚举替代魔法值)
|
||||
SavePath = video.VideoSavePath,
|
||||
ViedoId = video.AwemeId,
|
||||
CookieId = video.CookieId
|
||||
});
|
||||
|
||||
filePathsToDelete.Add(video.VideoSavePath);
|
||||
}
|
||||
|
||||
// 无有效重新下载记录时直接返回
|
||||
if (!reDownList.Any())
|
||||
{
|
||||
Serilog.Log.Debug("无有效重新下载记录需要创建:VideoIds={0}", string.Join(",", videoIds));
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 4. 数据库操作(事务保证一致性:创建重新下载记录 + 删除原视频记录必须同时成功/失败)
|
||||
var transactionResult = await _dyCollectVideoRepository.UseTranAsync(async () =>
|
||||
{
|
||||
// 4.1 批量插入重新下载记录(SqlSugar批量插入效率更高)
|
||||
_dyCollectVideoRepository.InsertReDowns(reDownList);
|
||||
// 4.2 批量删除原视频记录(使用视频实际存在的ID,避免无效删除)
|
||||
var actualDeleteIds = videos.Select(v => v.Id).ToList();
|
||||
var deleteCount = await _dyCollectVideoRepository.DeleteByIdsAsync(actualDeleteIds); // 建议仓储层提供异步删除方法
|
||||
}, e =>
|
||||
{
|
||||
Serilog.Log.Error(e, "数据库事务执行失败:Ids={0}", string.Join(",", videoIds));
|
||||
});
|
||||
|
||||
// 5. 文件删除(非事务操作,失败不回滚数据库,可根据业务调整)
|
||||
// 采用异步文件操作,避免同步IO阻塞线程(需.NET 5+支持)
|
||||
foreach (var path in filePathsToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path); // 异步删除,提升并发性能
|
||||
Serilog.Log.Debug("视频文件删除成功:Path={0}", path);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error("视频文件不存在,跳过删除:Path={0}", path);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "视频文件删除失败:Path={0}", path);
|
||||
}
|
||||
}
|
||||
|
||||
var CookieIds = reDownList.Select(x => x.CookieId).Distinct();
|
||||
foreach (var ck in CookieIds)
|
||||
{
|
||||
var cookie= douyinCookieRepository.GetById(ck);
|
||||
if (cookie == null)
|
||||
continue;
|
||||
var viedoTypes = videos.Where(x => x.CookieId == ck).Select(x => x.ViedoType).Distinct();
|
||||
|
||||
if(viedoTypes!=null&& viedoTypes.Any())
|
||||
{
|
||||
foreach (VideoTypeEnum item in viedoTypes)
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case VideoTypeEnum.Favorite:
|
||||
cookie.FavHasSyncd = 0;
|
||||
break;
|
||||
case VideoTypeEnum.Collect:
|
||||
cookie.CollHasSyncd = 0;
|
||||
break;
|
||||
case VideoTypeEnum.UperPost:
|
||||
cookie.UperSyncd = 0;
|
||||
break;
|
||||
case VideoTypeEnum.ImageVideo:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
await douyinCookieRepository.UpdateAsync(cookie);
|
||||
|
||||
}
|
||||
Serilog.Log.Debug("重新下载视频流程执行完成:成功创建{0}条重新下载记录,删除{1}个文件,等待重新下载...", reDownList.Count, filePathsToDelete.Count);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "重新下载视频执行失败:Ids={0}", string.Join(",", videoIds));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取待重新下载的视频列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<ViedoReDown>> GetViedoReDowns()
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetViedoReDowns();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user