Merge feature/merge-upstream: Modal 小屏 + 视频播放 + Docker Alpine(冲突以 feature 为准)
部署到群晖 / deploy (push) Failing after 3h9m39s

# Conflicts:
#	Dockerfile
#	Dockerfile-arm
#	dto/VideoPageRequestDTO.cs
#	model/AppConfig.cs
#	repository/DyCollectVideoRepository.cs
#	service/CommonService.cs
#	service/DyCollectVideoService.cs
#	service/DyHttpClientService.cs
This commit is contained in:
yuming
2026-07-03 20:03:46 +08:00
203 changed files with 28876 additions and 10250 deletions
+50
View File
@@ -0,0 +1,50 @@
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.repository;
using dy.net.utils;
namespace dy.net.service
{
public class AdminUserService
{
private readonly AdminUserRepository _userRepository;
public AdminUserService(AdminUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<(int code, string erro)> UpdatePwd(UpdatePwdRequest loginUser)
{
return await _userRepository.UpdatePwd(loginUser);
}
public async Task<AdminUserInfo> GetUser(string userName = null)
{
return await _userRepository.GetUser(userName);
}
public async Task<bool> UpdateAvatar(string avatar)
{
return await _userRepository.UpdateAvatar(avatar);
}
public (int code, string erro) InitUser(string UserName, string Password)
{
AdminUserInfo userInfo = new AdminUserInfo
{
UserName = UserName,
Password = Password,
CreateTime = DateTime.Now
};
return _userRepository.InitUser(userInfo);
}
public bool ResetPwd(string pwd)
{
return _userRepository.ResetPwd(pwd);
}
}
}
-183
View File
@@ -1,183 +0,0 @@
using ClockSnowFlake;
using dy.net.dto;
using dy.net.model;
using SqlSugar;
using System.Reflection;
using System.Threading.Tasks;
namespace dy.net.service
{
public class CommonService
{
private readonly ISqlSugarClient sqlSugarClient;
public CommonService(ISqlSugarClient sqlSugarClient)
{
this.sqlSugarClient = sqlSugarClient;
}
public AppConfig GetConfig()
{
return sqlSugarClient.Queryable<AppConfig>().First();
}
public AppConfig InitConfig()
{
var conf = GetConfig();
if (conf != null)
return conf;
else
{
AppConfig config = new AppConfig
{
Id = IdGener.GetLong().ToString(),
Cron = "30",
BatchCount = 10
};
sqlSugarClient.Insertable<AppConfig>(config).ExecuteCommand();
return config;
}
}
public async Task<bool> UpdateConfig(AppConfig config)
{
var cc = await sqlSugarClient.Queryable<AppConfig>().FirstAsync(x => x.Id == config.Id);
if (cc == null)
return false;
else
{
cc.Cron = config.Cron;
cc.BatchCount = config.BatchCount;
// 更新下载配置
cc.EnableAutoDownload = config.EnableAutoDownload;
cc.DownloadDaysRange = config.DownloadDaysRange;
cc.AuthorWhitelist = config.AuthorWhitelist;
cc.AuthorBlacklist = config.AuthorBlacklist;
cc.DownloadFavorite = config.DownloadFavorite;
cc.DownloadCollect = config.DownloadCollect;
}
var update = await sqlSugarClient.Updateable<AppConfig>(cc).ExecuteCommandAsync();
return update > 0;
}
/// <summary>
/// 兼容旧版将之前同步了的我收藏的数据进行更新
/// </summary>
public void UpdateCollectViedoType()
{
var collectViedos = sqlSugarClient.Queryable<DyCollectVideo>().Where(x => string.IsNullOrWhiteSpace(x.ViedoType)).ToList();
if (collectViedos.Any())
{
collectViedos.ForEach(x =>
{
x.ViedoType = "2";
});
sqlSugarClient.Updateable(collectViedos).ExecuteCommand();
}
}
/// <summary>
/// 重置所有Cookie的同步状态为0
/// </summary>
/// <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<DyUserCookies>().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<DyUpSecUserIdDto>>(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;
}
#region 测试创建数据库
///// <summary>
///// 创建SQLite数据库连接字符串
///// </summary>
//private string CreateSqliteDBConn()
//{
// var dbFolder = Path.Combine(Environment.CurrentDirectory, "db");
// Directory.CreateDirectory(dbFolder); // 不存在则创建,无需判断
// var dbPath = Path.Combine(dbFolder, "dy.sqlite");
// if (!File.Exists(dbPath))
// {
// using (File.Create(dbPath)) { } // 使用using确保文件流关闭
// }
// return $"DataSource={dbPath}";
//}
///// <summary>
///// 初始化数据库
///// </summary>
//public ISqlSugarClient InitDataBase(DbType dbType, string connString)
//{
// // 处理SQLite连接字符串
// if (dbType == DbType.Sqlite)
// {
// connString = CreateSqliteDBConn();
// }
// if (string.IsNullOrWhiteSpace(connString))
// {
// return null;
// }
// return new SqlSugarClient(new ConnectionConfig
// {
// ConnectionString = connString,
// InitKeyType = InitKeyType.Attribute,
// DbType = dbType,
// IsAutoCloseConnection = true
// }, db =>
// {
// // 日志配置
// db.Aop.OnLogExecuting = (sql, pars) => Serilog.Log.Debug(sql);
// db.Aop.OnError = e =>
// {
// Serilog.Log.Error(e.Message);
// Serilog.Log.Error(e.Sql);
// };
// // 创建数据库和表
// db.DbMaintenance.CreateDatabase();
// var modelTypes = Assembly.GetExecutingAssembly()
// .GetTypes()
// .Where(t => t.Namespace?.StartsWith("dy.net.model") ?? false)
// .ToArray();
// db.CodeFirst.InitTables(modelTypes);
// });
//}
#endregion
}
}
+83
View File
@@ -0,0 +1,83 @@
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.repository;
namespace dy.net.service
{
public class DouyinCollectCateService
{
private readonly DouyinCollectCateRepository _douyinCollectCateRepository;
public DouyinCollectCateService(DouyinCollectCateRepository douyinCollectCateRepository)
{
_douyinCollectCateRepository = douyinCollectCateRepository;
}
public async Task<bool> AddAsync(DouyinCollectCate cate)
{
return await _douyinCollectCateRepository.InsertAsync(cate);
}
/// <summary>
///
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<(List<DouyinCollectCate> list, int totalCount)> GetPagedAsync(DouyinCollectCateRequestDto dto)
{
return await _douyinCollectCateRepository.GetPagedAsync(dto);
}
/// <summary>
///
/// </summary>
/// <param name="cates"></param>
/// <param name="ckId"></param>
/// <param name="cateType"></param>
/// <returns></returns>
public async Task<(int add, int update, int delete, bool succ)> Sync(List<DouyinCollectCate> cates, string ckId, VideoTypeEnum cateType)
{
return await _douyinCollectCateRepository.Sync(cates, ckId, cateType);
}
/// <summary>
/// 打开或关闭同步
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<bool> BatchSwitchSync(List<DouyinCollectCateSwitchDto> dto)
{
return await _douyinCollectCateRepository.SwitchBatchAsync(dto);
}
/// <summary>
/// 获取需要同步的合集、短剧、收藏夹
/// </summary>
/// <returns></returns>
public async Task<List<DouyinCollectCate>> GetSyncCates(string cookieId, VideoTypeEnum cateType)
{
return await _douyinCollectCateRepository.GetListAsync(x => x.CookieId == cookieId && x.CateType == cateType && x.Sync && x.Total > 0);
}
/// <summary>
/// 完结
/// </summary>
/// <param name="cate"></param>
/// <returns></returns>
public async Task<bool> UpdateCate2EndStatus(DouyinCollectCate cate)
{
return await _douyinCollectCateRepository.UpdateCateEndStatus(cate);
}
}
}
+308
View File
@@ -0,0 +1,308 @@
using ClockSnowFlake;
using dy.net.model.entity;
using dy.net.utils;
using SqlSugar;
//using static Org.BouncyCastle.Math.EC.ECCurve;
namespace dy.net.service
{
public class DouyinCommonService
{
private readonly ISqlSugarClient sqlSugarClient;
public DouyinCommonService(ISqlSugarClient sqlSugarClient)
{
this.sqlSugarClient = sqlSugarClient;
}
public AppConfig GetConfig()
{
return sqlSugarClient.Queryable<AppConfig>().First();
}
/// <summary>
/// 初始化并返回配置
/// </summary>
/// <returns></returns>
public AppConfig InitConfig()
{
var conf = GetConfig();
if (conf != null)
{
if (string.IsNullOrWhiteSpace(conf.PriorityLevel))
{
conf.PriorityLevel = "[{\"id\":1,\"name\":\"喜欢的视频\",\"sort\":1},{\"id\":2,\"name\":\"收藏的视频\",\"sort\":2},{\"id\":3,\"name\":\"关注的视频\",\"sort\":3}]";
}
conf.IsFirstRunning = true;//标记为程序刚启动第一次运行
conf.AutoDistinct = true;
if (!conf.VideoEncoder.HasValue)
conf.VideoEncoder = 264;
sqlSugarClient.Updateable(conf).ExecuteCommand();
//兼容旧版本
return conf;
}
else
{
AppConfig config = new AppConfig
{
Id = IdGener.GetLong().ToString(),
Cron = 30,
BatchCount = 18,
LogKeepDay = 7,
//UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
//UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名
DownImageVideo = true,//默认下载图文视频
DownMp3 = false,
DownImage = false,
FollowedTitleTemplate = "",
FullFollowedTitleTemplate = "",
FollowedTitleSeparator = "",
AutoDistinct = true,//默认开启
PriorityLevel = "[{\"id\":1,\"name\":\"喜欢的视频\",\"sort\":1},{\"id\":2,\"name\":\"收藏的视频\",\"sort\":2},{\"id\":3,\"name\":\"关注的视频\",\"sort\":3}]",
IsFirstRunning = true,
OnlySyncNew = true,
DownDynamicVideo = false,
KeepDynamicVideo = false,
MegDynamicVideo = true,
VideoEncoder = 264
};
sqlSugarClient.Insertable(config).ExecuteCommand();
return config;
}
}
/// <summary>
/// 更新AppConfig的IsFirstRunning属性为fasle
/// </summary>
/// <returns></returns>
public async Task SetConfigNotFirstRunning()
{
//更新IsFirstRunning为false
await sqlSugarClient.Updateable<AppConfig>()
.SetColumns(x => x.IsFirstRunning == false)
.Where(x => x.IsFirstRunning == true)
.ExecuteCommandAsync();
}
/// <summary>
/// 更新配置
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
public async Task<bool> UpdateConfig(AppConfig config)
{
var cc = await sqlSugarClient.Queryable<AppConfig>().FirstAsync(x => x.Id == config.Id);
if (cc == null)
return false;
else
{
int update = await sqlSugarClient.Updateable(config).IgnoreColumns(x => new { x.IsFirstRunning }).ExecuteCommandAsync();
return update > 0;
}
}
/// <summary>
/// 重置所有Cookie的同步状态为0
/// </summary>
/// <returns></returns>
//public bool UpdateAllCookieSyncedToZero()
//{
// var cookies = sqlSugarClient.Queryable<DouyinCookie>().ToList();
// foreach (var cookie in cookies)
// {
// cookie.CollHasSyncd = 0;
// cookie.FavHasSyncd = 0;
// cookie.UperSyncd = 0;
// }
// return sqlSugarClient.Updateable(cookies).ExecuteCommand() > 0;
//}
/// <summary>
/// 查询是否已删除
/// </summary>
/// <param name="videoId"></param>
/// <returns></returns>
public async Task<bool> ExistDeleteVideo(string videoId)
{
var count = await sqlSugarClient.Queryable<DouyinVideoDelete>().Where(x => x.ViedoId == videoId).CountAsync();
return count > 0;
}
/// <summary>
/// 新增要删除的视频
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<bool> AddDeleteVideo(DouyinVideoDelete dto)
{
dto.Id = IdGener.GetLong().ToString();
dto.DeleteTime = DateTime.Now;
return sqlSugarClient.Insertable(dto).ExecuteCommand() > 0;
}
/// <summary>
/// 查询已删除视频列表
/// </summary>
/// <returns></returns>
public async Task<List<DouyinVideoDelete>> GetDouyinDeleteVideos()
{
return await sqlSugarClient.Queryable<DouyinVideoDelete>()
.ToListAsync();
}
/// <summary>
/// 获取已经下载过视频的作者的保存路径
/// </summary>
/// <param name="uperId"></param>
/// <returns></returns>
public string GetDouyinUpSavePath(string uperId)
{
return sqlSugarClient.Queryable<DouyinVideoUp>().Where(x => x.UperId == uperId).Select(x => x.SavePath).First();
}
/// <summary>
/// 保存下载过视频的作者的保存路径
/// </summary>
/// <param name="uperId"></param>
/// <param name="SavePath"></param>
/// <returns></returns>
public bool SaveDouyinUpSavePath(string uperId, string SavePath)
{
if (string.IsNullOrEmpty(uperId))
return false;
var upSavePath = GetDouyinUpSavePath(uperId);
if (string.IsNullOrWhiteSpace(upSavePath))
{
sqlSugarClient.Insertable(new DouyinVideoUp
{
Id = IdGener.GetLong().ToString(),
SavePath = SavePath,
UperId = uperId
}).ExecuteCommand();
}
return true;
}
/// <summary>
/// 将所有关注博主的保存路径统一刷成博主姓名,防止博主名字改变后出现多个文件夹
/// </summary>
/// <returns></returns>
public async Task UpdateFollowedSavePathAsync()
{
var existNoSavePath = await sqlSugarClient.Queryable<DouyinFollowed>().Where(x => string.IsNullOrWhiteSpace(x.SavePath)).AnyAsync();
if (existNoSavePath)
{
var list = await sqlSugarClient.Queryable<DouyinFollowed>().ToListAsync();
var noSavePathList = list.Where(x => string.IsNullOrWhiteSpace(x.SavePath))?.ToList();
foreach (var item in noSavePathList)
{
var path = DouyinFileNameHelper.SanitizeLinuxFileName(item.UperName, "抖音号" + item.UperId, true);
path = DouyinFileNameHelper.LimitUnifiedCount(path, 20);
var existFollow = list.Where(x => x.UperId != item.UperId && x.SavePath == path).Any();
if (existFollow)
{
item.SavePath = path + "_" + item.UperId;
}
else
{
item.SavePath = path;
}
}
if (noSavePathList != null)
{
await sqlSugarClient.Updateable(noSavePathList).UpdateColumns(x => new { x.SavePath }).ExecuteCommandAsync();
}
}
}
public async Task<bool> UpdateImageVideoSavePath()
{
var exist = await sqlSugarClient.Queryable<DouyinVideo>().Where(x => x.IsMergeVideo == 1 && x.VideoSavePath.EndsWith("-poster.jpg")).AnyAsync();
if (exist)
{
// 执行更新操作
var updateCount = sqlSugarClient.Updateable<DouyinVideo>()
.SetColumns(it => new DouyinVideo
{
VideoSavePath = SqlFunc.Replace(it.VideoSavePath, "-poster.jpg", ".mp4")
})
.Where(it => it.IsMergeVideo == 1 && it.VideoSavePath.EndsWith("-poster.jpg"))
.ExecuteCommand();
if (updateCount > 0)
Serilog.Log.Debug($"更新了{updateCount}条图文视频存储路径,修复图文视频无法在页面播放的问题");
}
return true;
}
#region 测试创建数据库
///// <summary>
///// 创建SQLite数据库连接字符串
///// </summary>
//private string CreateSqliteDBConn()
//{
// var dbFolder = Path.Combine(Environment.CurrentDirectory, "db");
// Directory.CreateDirectory(dbFolder); // 不存在则创建,无需判断
// var dbPath = Path.Combine(dbFolder, "dy.sqlite");
// if (!File.Exists(dbPath))
// {
// using (File.Create(dbPath)) { } // 使用using确保文件流关闭
// }
// return $"DataSource={dbPath}";
//}
///// <summary>
///// 初始化数据库
///// </summary>
//public ISqlSugarClient InitDataBase(DbType dbType, string connString)
//{
// // 处理SQLite连接字符串
// if (dbType == DbType.Sqlite)
// {
// connString = CreateSqliteDBConn();
// }
// if (string.IsNullOrWhiteSpace(connString))
// {
// return null;
// }
// return new SqlSugarClient(new ConnectionConfig
// {
// ConnectionString = connString,
// InitKeyType = InitKeyType.Attribute,
// DbType = dbType,
// IsAutoCloseConnection = true
// }, db =>
// {
// // 日志配置
// db.Aop.OnLogExecuting = (sql, pars) => Serilog.Log.Debug(sql);
// db.Aop.OnError = e =>
// {
// Serilog.Log.Error(e.Message);
// Serilog.Log.Error(e.Sql);
// };
// // 创建数据库和表
// db.DbMaintenance.CreateDatabase();
// var modelTypes = Assembly.GetExecutingAssembly()
// .GetTypes()
// .Where(t => t.Namespace?.StartsWith("dy.net.model") ?? false)
// .ToArray();
// db.CodeFirst.InitTables(modelTypes);
// });
//}
#endregion
}
}
+169
View File
@@ -0,0 +1,169 @@
using ClockSnowFlake;
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.repository;
using System.Linq.Expressions;
namespace dy.net.service
{
public class DouyinCookieService
{
private readonly DouyinCookieRepository _cookieRepository;
public DouyinCookieService(DouyinCookieRepository cookieRepository)
{
_cookieRepository = cookieRepository;
}
public Task<List<DouyinCookie>> GetOpendCookiesAsync(Expression<Func<DouyinCookie, bool>> whereExpression = null)
{
return _cookieRepository.GetAllCookiesAsync(whereExpression);
}
public async Task<List<DouyinCookie>> GetOpendCookies()
{
return await _cookieRepository.GetAllCookies();
}
public Task<List<DouyinCookie>> GetAllAsync()
{
return _cookieRepository.GetAllAsync();
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public async Task<bool> IsInit()
{
return await _cookieRepository.ExistsAsync(x => !string.IsNullOrEmpty(x.Id));
}
public async Task<bool> Add(DouyinCookie dyUserCookies)
{
return await _cookieRepository.InsertAsync(dyUserCookies);
}
public async Task<bool> FastResetCookie(string id,string cookie)
{
var d= await _cookieRepository.FastResetCookie(id, cookie);
if (d)
{
var cookies =await _cookieRepository.GetByIdAsync(id);
cookies.StatusCode = 0;
cookies.StatusMsg = "正常";
await _cookieRepository.UpdateAsync(cookies);
}
return d;
}
public async Task<bool> Switch(DouyinCookieSwitchDto dto)
{
return await _cookieRepository.SwitchAsync(dto);
}
public bool InitCookie()
{
var exist = _cookieRepository.GetDefault();
if (exist != null)
{
return true;
}
var cookie = new DouyinCookie
{
UserName = "douyin2026",
Cookies = "",
SecUserId = "",
Id = IdGener.GetLong().ToString(),
Status = 0,
SavePath = "/app/collect",
FavSavePath = "/app/favorite",
UpSavePath = "/app/uper",
//ImgSavePath="/app/images",
//CollHasSyncd = 0,
//FavHasSyncd = 0,
//UperSyncd = 0,
MyUserId = ""
};
return _cookieRepository.Insert(cookie);
}
/// <summary>
/// 新增字段,兼容旧版本
/// </summary>
/// <returns></returns>
public async Task<bool> UpdateCookieToSupportOldVersionAsync()
{
var cookies = _cookieRepository.GetAll();
foreach (var item in cookies)
{
item.DownCollect = !string.IsNullOrWhiteSpace(item.SavePath) && !item.UseCollectFolder;
item.DownFavorite = !string.IsNullOrWhiteSpace(item.FavSavePath);
item.DownFollowd = !string.IsNullOrWhiteSpace(item.UpSavePath);
}
await _cookieRepository.UpdateRangeAsync(cookies);
return true;
}
// 查询单个
public async Task<DouyinCookie> GetByIdAsync(string id)
{
return await _cookieRepository.GetByIdAsync(id);
}
// 查询列表(可加条件)
public async Task<(List<DouyinCookie> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
{
return await _cookieRepository.GetPagedAsync(pageIndex, pageSize);
}
// 更新
public async Task<bool> UpdateAsync(DouyinCookie dyUserCookies)
{
return await _cookieRepository.UpdateAsync(dyUserCookies);
}
// 更新
public async Task<bool> UpdateCookieAsync(DouyinCookie dyUserCookies)
{
return await _cookieRepository.UpdateCookie(dyUserCookies);
}
// 删除(根据主键)
public async Task<bool> DeleteByIdAsync(string id)
{
return await _cookieRepository.DeleteByIdAsync(id);
}
// 批量删除
public async Task<int> DeleteByIdsAsync(IEnumerable<string> ids)
{
return await _cookieRepository.DeleteByIdsAsync(ids.Cast<object>());
}
public async Task<bool> ImportCookies(List<DouyinCookie> cookies)
{
await _cookieRepository.DeleteAsync(x => !string.IsNullOrWhiteSpace(x.Id));
await _cookieRepository.InsertRangeAsync(cookies);
return true;
}
/// <summary>
/// 将所有同步类型的同步状态改为已同步,这样,之后就不会再扫描所有接口数据,只会读取最新一页的数据了
/// </summary>
/// <returns></returns>
//public async Task<bool> SetOnlySyncNew()
//{
// var cks= await _cookieRepository.GetAllAsync();
// foreach (var item in cks)
// {
// item.CollHasSyncd = 1;
// item.FavHasSyncd = 1;
// item.UperSyncd = 1;
// }
// var d= await _cookieRepository.UpdateRangeAsync(cks);
// return d>0;
//}
}
}
+175
View File
@@ -0,0 +1,175 @@
using ClockSnowFlake;
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.model.response;
using dy.net.repository;
using SqlSugar;
namespace dy.net.service
{
public class DouyinFollowService
{
private readonly DouyinFollowRepository _followRepository;
public DouyinFollowService(DouyinFollowRepository followRepository)
{
_followRepository = followRepository;
}
public async Task<List<DouyinFollowGroupDto>> GetGroupByCookieAsync()
{
return await _followRepository.GetDouyinFollowGroup();
}
/// <summary>
///
/// </summary>
/// <param name="followed"></param>
/// <returns></returns>
public async Task<bool> AddAsync(DouyinFollowed followed)
{
var foll = await _followRepository.GetFirstAsync(x => x.SecUid == followed.SecUid && x.mySelfId == followed.mySelfId);
if (foll != null)
{
return false;
}
followed.Id = IdGener.GetLong().ToString();
followed.LastSyncTime = DateTime.UtcNow;
followed.IsNoFollowed = true;
return await _followRepository.InsertAsync(followed);
}
/// <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>
/// <returns></returns>
public async Task<List<DouyinFollowed>> GetHandFollows()
{
return await _followRepository.GetListAsync(x => x.IsNoFollowed);
}
/// <summary>
/// 导入非关注
/// </summary>
/// <param name="douyinFolloweds"></param>
/// <returns></returns>
public async Task<bool> AddHandFollows(List<DouyinFollowed> douyinFolloweds)
{
if (douyinFolloweds == null || !douyinFolloweds.Any()) return false;
var ids = douyinFolloweds.Select(x => x.Id).Distinct().ToList();
var existingFolloweds = await _followRepository.GetListAsync(x => ids.Contains(x.Id));
var toAddList = douyinFolloweds
.Where(x => !existingFolloweds.Any(e => e.Id == x.Id))
.ToList();
// 批量更新:先整理需要更新的实体
var toUpdateEntities = new List<DouyinFollowed>();
foreach (var updateItem in douyinFolloweds)
{
var existingItem = existingFolloweds.FirstOrDefault(e => e.Id == updateItem.Id);
if (existingItem != null)
{
toUpdateEntities.Add(updateItem);
}
}
int operateCount = 0;
// 批量新增
if (toAddList.Any())
{
operateCount += await _followRepository.InsertRangeAsync(toAddList);
}
// 批量更新
if (toUpdateEntities.Any())
{
await _followRepository.UpdateRangeAsync(toUpdateEntities);
operateCount += toUpdateEntities.Count;
}
return operateCount > 0;
}
/// <summary>
///
/// </summary>
/// <param name="followInfos"></param>
/// <param name="ck"></param>
/// <returns></returns>
public async Task<(int add, int update, bool succ)> Sync(List<FollowingsItem> followInfos, DouyinCookie ck)
{
return await _followRepository.Sync(followInfos, ck);
}
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 followdOpendCount = await _followRepository.CountAsync(x => x.FullSync);
if (followdOpendCount > 20 && dto.FullSync)
{
//throw new Exception("抱歉,为了减少被风控的可能,关注列表最多同时开启20个全量同步");
Serilog.Log.Debug("为了减少被风控的可能,尽量不要同时开启这么多,虽然没限制!!!");
}
var followed = await _followRepository.GetByIdAsync(dto.Id);
if (followed != null)
{
followed.OpenSync = dto.OpenSync;
followed.FullSync = dto.OpenSync && dto.FullSync;
followed.SavePath = dto.SavePath;
return await _followRepository.Update(followed);
}
return false;
}
public async Task<bool> DeleteFollow(FollowUpdateDto dto)
{
return await _followRepository.DeleteByIdAsync(dto.Id);
}
/// <summary>
/// 打开或关闭同步
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<bool> OpenOrCloseFullSync(FollowUpdateDto dto)
{
return await OpenOrCloseSync(dto);
}
/// <summary>
/// 获取需要同步的关注列表
/// </summary>
/// <returns></returns>
public async Task<List<DouyinFollowed>> GetSyncFollows(string userUserId)
{
return await _followRepository.GetSyncFollows(userUserId);
}
}
}
+760
View File
@@ -0,0 +1,760 @@
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.model.response;
using dy.net.utils;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Serilog;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace dy.net.service
{
public class DouyinHttpClientService : IDisposable
{
private readonly IHttpClientFactory _clientFactory;
private readonly JsonSerializerSettings _jsonSettings=new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Ignore
};
private bool _disposedValue;
public DouyinHttpClientService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
/// <summary>
/// 异步获取HTTP响应消息(优化版)
/// </summary>
private async Task<HttpResponseMessage> GetHttpResponseMessage(
HttpMethod httpMethod,
string requestUrl,
Dictionary<string, string> requestParameters,
string refererValue,
string cookie)
{
string fullUrl = requestUrl;
if (requestParameters != null && requestParameters.Count > 0)
{
fullUrl = QueryHelpers.AddQueryString(
requestUrl,
requestParameters.ToDictionary(
kv => kv.Key,
kv => new StringValues(kv.Value)
)
);
}
using var requestMessage = new HttpRequestMessage(httpMethod, fullUrl);
if (!string.IsNullOrEmpty(refererValue) && Uri.IsWellFormedUriString(refererValue, UriKind.Absolute))
{
requestMessage.Headers.Referrer = new Uri(refererValue);
}
if (!string.IsNullOrEmpty(cookie))
{
requestMessage.Headers.TryAddWithoutValidation("Cookie", cookie);
}
// 优化:移除using,由IHttpClientFactory管理生命周期
var httpClient = _clientFactory.CreateClient(DouyinRequestParamManager.DY_HTTP_CLIENT);
return await httpClient.SendAsync(requestMessage);
}
#region 收藏夹相关方法(保留原有逻辑,仅优化资源释放)
public async Task<DouyinVideoInfoResponse> SyncCollectVideos(string cursor, string count, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
if (string.IsNullOrWhiteSpace(count))
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
if (string.IsNullOrWhiteSpace(cookie))
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
try
{
var requestUrl = "/aweme/v1/web/aweme/listcollection";
var refererValue = "https://www.douyin.com";
var requestParameters = DouyinRequestParamManager.DouyinCollectParams;
requestParameters["cursor"] = cursor;
requestParameters["count"] = count;
using var response = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinVideoInfoResponse>(jsonReader);
if (model == null)
Log.Error($"SyncCollectVideos fail, data is null");
return model;
}
else
{
Log.Error($"SyncCollectVideos fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error($"SyncCollectVideos error: {ex.Message}", ex);
return null;
}
}
public async Task<DouyinCollectListResponse> SyncCollectFolderList(string cookie, string cursor)
{
if (string.IsNullOrWhiteSpace(cookie))
{
Log.Error("cookie为空,无法获取收藏夹列表 ");
return null;
}
try
{
var requestUrl = "/aweme/v1/web/collects/list";
var refererValue = "https://www.douyin.com/user/self?from_tab_name=main&showSubTab=favorite_folder&showTab=favorite_collection";
var requestParameters = DouyinRequestParamManager.DouyinCollectListParams;
requestParameters["count"] = "10";
requestParameters["cursor"] = cursor;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinCollectListResponse>(jsonReader);
if (model == null)
Log.Error($"SyncCollectFolderList fail, data is null");
return model;
}
else
{
Log.Error("SyncCollectFolderList ,{StatusCode}", response.StatusCode);
return null;
}
}
catch (Exception ex)
{
Log.Error("SyncCollectFolderList ,{error}", ex);
return null;
}
}
public async Task<DouyinVideoInfoResponse> SyncCollectVideosByCollectId(string cursor, string count, string cookie, string collectsId)
{
if (string.IsNullOrWhiteSpace(collectsId))
throw new ArgumentException($"“{nameof(collectsId)}”不能为 null 或空。", nameof(collectsId));
if (string.IsNullOrWhiteSpace(cursor))
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
if (string.IsNullOrWhiteSpace(count))
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
if (string.IsNullOrWhiteSpace(cookie))
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
try
{
var requestUrl = "/aweme/v1/web/collects/video/list";
var refererValue = "https://www.douyin.com/user/self?from_tab_name=main&showSubTab=favorite_folder&showTab=favorite_collection";
var requestParameters = DouyinRequestParamManager.DouyinFolderCollectParams;
requestParameters["cursor"] = cursor;
requestParameters["count"] = "15";
requestParameters["collects_id"] = collectsId;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinVideoInfoResponse>(jsonReader);
if (model == null)
Log.Error($"SyncCollectVideosByCollectId fail: data is null");
return model;
}
else
{
Log.Error($"SyncCollectVideosByCollectId fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error($"SyncCollectVideosByCollectId error: {ex.Message}", ex);
return null;
}
}
#endregion
#region 合集相关方法
public async Task<DouyinMixListResponse> SyncMixList(string cookie, string cursor)
{
if (string.IsNullOrWhiteSpace(cookie))
{
Log.Error("cookie为空,无法获取收藏夹列表 ");
return null;
}
try
{
var requestUrl = "/aweme/v1/web/mix/listcollection";
var refererValue = "https://www.douyin.com/user/self?";
var requestParameters = DouyinRequestParamManager.DouyinMixListParams;
requestParameters["count"] = "10";
requestParameters["cursor"] = cursor;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinMixListResponse>(jsonReader);
if (model == null)
Log.Error($"SyncMixList fail, data is null");
return model;
}
else
{
Log.Error($"SyncMixList : {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error("SyncMixList ,{error}", ex);
return null;
}
}
public async Task<DouyinVideoInfoResponse> SyncMixViedosByMixId(string cursor, string count, string cookie, string mixId)
{
if (string.IsNullOrWhiteSpace(mixId))
throw new ArgumentException($"“{nameof(mixId)}”不能为 null 或空。", nameof(mixId));
if (string.IsNullOrWhiteSpace(cursor))
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
if (string.IsNullOrWhiteSpace(count))
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
if (string.IsNullOrWhiteSpace(cookie))
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
try
{
var requestUrl = "/aweme/v1/web/mix/aweme";
var refererValue = "https://www.douyin.com/user/self?";
var requestParameters = DouyinRequestParamManager.DouyinMixVideoParams;
requestParameters["cursor"] = cursor;
requestParameters["count"] = "15";
requestParameters["mix_id"] = mixId;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinVideoInfoResponse>(jsonReader);
if (model == null)
Log.Error($"SyncMixViedosByMixId fail:data is null");
return model;
}
else
{
Log.Error($"SyncMixViedosByMixId fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error($"SyncMixViedosByMixId error: {ex.Message}", ex);
return null;
}
}
#endregion
#region 短剧相关方法
public async Task<DouyinSeriesListResponse> SyncSeriesList(string cookie, string cursor)
{
if (string.IsNullOrWhiteSpace(cookie))
{
Log.Error("cookie为空,无法获取收藏夹列表 ");
return null;
}
try
{
var requestUrl = "/aweme/v1/web/series/collections";
var refererValue = "https://www.douyin.com/user/self?";
var requestParameters = DouyinRequestParamManager.DouyinSeriesListParams;
requestParameters["count"] = "15";
requestParameters["cursor"] = cursor;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinSeriesListResponse>(jsonReader);
if (model == null)
Log.Error($"SyncShortList fail, data is null");
return model;
}
else
{
Log.Error($"SyncShortList fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error("SyncShortList ,{error}", ex);
return null;
}
}
public async Task<DouyinVideoInfoResponse> SyncSeriesViedosByMSeriesId(string cursor, string count, string cookie, string seriesId)
{
if (string.IsNullOrWhiteSpace(seriesId))
throw new ArgumentException($"“{nameof(seriesId)}”不能为 null 或空。", nameof(seriesId));
if (string.IsNullOrWhiteSpace(cursor))
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
if (string.IsNullOrWhiteSpace(count))
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
if (string.IsNullOrWhiteSpace(cookie))
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
try
{
var requestUrl = "/aweme/v1/web/series/aweme";
var refererValue = "https://www.douyin.com/user/self?";
var requestParameters = DouyinRequestParamManager.DouyinSeriesVideosParams;
requestParameters["cursor"] = cursor;
requestParameters["count"] = count;
requestParameters["series_id"] = seriesId;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinVideoInfoResponse>(jsonReader);
if (model == null)
Log.Error($"SyncSeriesViedosByMSeriesId fail:data is null");
return model;
}
else
{
Log.Error($"SyncSeriesViedosByMSeriesId fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error($"SyncSeriesViedosByMSeriesId error: {ex.Message}", ex);
return null;
}
}
#endregion
#region 喜欢/博主/关注相关方法
public async Task<DouyinVideoInfoResponse> SyncFavoriteVideos(string count, string cursor, string secUserId, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
if (string.IsNullOrWhiteSpace(count))
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
if (string.IsNullOrWhiteSpace(secUserId))
throw new ArgumentException($"“{nameof(secUserId)}”不能为 null 或空。", nameof(secUserId));
if (string.IsNullOrWhiteSpace(cookie))
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
try
{
var requestUrl = "/aweme/v1/web/aweme/favorite";
var refererValue = "https://www.douyin.com/user/self?showTab=like";
var requestParameters = DouyinRequestParamManager.DouyinFavoriteParams;
requestParameters["max_cursor"] = cursor;
requestParameters["sec_user_id"] = secUserId;
requestParameters["count"] = count;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinVideoInfoResponse>(jsonReader);
if (model == null)
Log.Error($"SyncFavoriteVideos fail, data is null");
return model;
}
else
{
Log.Error($"SyncFavoriteVideos fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error($"SyncFavoriteVideos error: {ex.Message}", ex);
return null;
}
}
public async Task<DouyinVideoInfoResponse> SyncUpderPostVideos(string count, string cursor, string secUserId, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
if (string.IsNullOrWhiteSpace(count))
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
if (string.IsNullOrWhiteSpace(secUserId))
throw new ArgumentException($"“{nameof(secUserId)}”不能为 null 或空。", nameof(secUserId));
if (string.IsNullOrWhiteSpace(cookie))
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
try
{
var requestUrl = "/aweme/v1/web/aweme/post";
var refererValue = "https://www.douyin.com/user/";
var requestParameters = DouyinRequestParamManager.DouyinUpderPostParams;
requestParameters["max_cursor"] = cursor;
requestParameters["sec_user_id"] = secUserId;
requestParameters["count"] = count;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinVideoInfoResponse>(jsonReader);
if (model == null)
Log.Error($"SyncUpderPostVideos fail, data is null");
return model;
}
else
{
Log.Error($"SyncUpderPostVideos StatusCode fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error($"SyncUpderPostVideos error: {ex.Message}", ex);
return null;
}
}
public async Task<DouyinFollowInfoResponse> SyncMyFollows(string count, string offset, string secUserId, string cookie)
{
if (string.IsNullOrWhiteSpace(offset))
throw new ArgumentException($"“{nameof(offset)}”不能为 null 或空。", nameof(offset));
if (string.IsNullOrWhiteSpace(count))
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
if (string.IsNullOrWhiteSpace(secUserId))
throw new ArgumentException($"“{nameof(secUserId)}”不能为 null 或空。", nameof(secUserId));
if (string.IsNullOrWhiteSpace(cookie))
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
try
{
var requestUrl = "/aweme/v1/web/user/following/list";
var refererValue = "https://www.douyin.com/user/self?showTab=like";
var requestParameters = DouyinRequestParamManager.DouyinMyFollowParams;
requestParameters["sec_user_id"] = secUserId;
requestParameters["count"] = count;
requestParameters["offset"] = offset;
using var response = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
if (response.IsSuccessStatusCode)
{
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(reader);
var model = JsonSerializer.Create(_jsonSettings).Deserialize<DouyinFollowInfoResponse>(jsonReader);
return model;
}
else
{
Log.Error($"SyncMyFollows fail: {response.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Log.Error($"SyncMyFollows error: {ex.Message}", ex);
// 优化:抛出前确保资源释放,使用包装异常保留原始堆栈
throw new InvalidOperationException("获取关注列表失败", ex);
}
}
#endregion
#region Cookie检查
public async Task<bool> CheckCookie(DouyinCookie douyinCookie)
{
if (douyinCookie == null || string.IsNullOrWhiteSpace(douyinCookie.Cookies))
return false;
try
{
if (!string.IsNullOrWhiteSpace(douyinCookie.SecUserId))
{
var res = await SyncMyFollows("1", "10", douyinCookie.SecUserId, douyinCookie.Cookies);
return res != null && res.StatusCode == 0;
}
else
{
var res = await SyncCollectVideos("0", "10", douyinCookie.Cookies);
return res != null && res.StatusCode == 0;
}
}
catch (Exception ex)
{
Log.Error(ex, "检查Cookie有效性失败");
return false;
}
}
#endregion
#region 下载相关(优化内存管理)
public async Task<(bool Success, string ActualSavePath)> DownloadAsync(
string videoUrl,
string savePath,
string cookie,
List<string> otherUrls = null,
CancellationToken cancellationToken = default,
TimeSpan? streamTimeout = null,
int maxRetryCount = 3,
TimeSpan? initialRetryDelay = null)
{
// 优化1:隔离外部列表,避免引用泄漏
var retryUrls = new List<string> { videoUrl };
if (otherUrls != null && otherUrls.Any())
{
retryUrls.AddRange(otherUrls);
maxRetryCount = Math.Min(maxRetryCount, retryUrls.Count); // 防止溢出
}
int retryCount = 0;
var retryDelay = initialRetryDelay ?? TimeSpan.FromSeconds(1);
streamTimeout ??= TimeSpan.FromSeconds(60);
while (retryCount < maxRetryCount)
{
try
{
string currentUrl = retryUrls[retryCount];
return await TryDownloadOnceAsync(currentUrl, savePath, cookie, cancellationToken, streamTimeout.Value);
}
catch (Exception ex) when (IsRetryableException(ex) && retryCount < maxRetryCount - 1)
{
retryCount++;
var delay = TimeSpan.FromMilliseconds(retryDelay.TotalMilliseconds * Math.Pow(2, retryCount - 1));
Log.Warning(ex, $"下载失败(第{retryCount}/{maxRetryCount}次重试):{videoUrl},将在{delay.TotalSeconds:F1}秒后重试");
try
{
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Log.Information($"重试等待被取消:{videoUrl}");
return (false, savePath);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Log.Information($"下载被取消:{videoUrl}");
return (false, savePath);
}
catch (Exception ex)
{
Log.Error(ex, $"下载失败(不可重试):{videoUrl}");
CleanupIncompleteFile(savePath);
return (false, savePath);
}
}
Log.Error($"下载失败:已达最大重试次数 {maxRetryCount},URL={videoUrl}");
return (false, savePath);
}
private async Task<(bool Success, string ActualSavePath)> TryDownloadOnceAsync(
string videoUrl,
string savePath,
string cookie,
CancellationToken cancellationToken,
TimeSpan streamTimeout)
{
DateTime lastStreamActivity = DateTime.UtcNow;
string actualSavePath = savePath;
string detectedExtension = string.Empty;
// 确保目录存在
var directory = Path.GetDirectoryName(savePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 清理已存在的文件
CleanupIncompleteFile(savePath);
// 优化2:使用using包裹下载专用HttpClient(工厂创建的Client仍可using,释放内部handler)
using var httpClient = _clientFactory.CreateClient(DouyinRequestParamManager.DY_HTTP_CLIENT_DOWN);
try
{
// 配置请求头
httpClient.DefaultRequestHeaders.Remove("Cookie"); // 先移除再添加,避免重复
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
httpClient.Timeout = TimeSpan.FromMinutes(5);
// 优化3:使用HttpCompletionOption.ResponseHeadersRead,不缓存整个响应
using var response = await httpClient.GetAsync(
videoUrl,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
// 检测文件类型
var ext = Path.GetExtension(savePath);
if (ext == ".mp3")
{
var contentType = response.Content.Headers.ContentType?.MediaType;
if (!string.IsNullOrEmpty(contentType))
{
if (contentType.Contains("audio/mp4") || contentType.Contains("audio/m4a"))
detectedExtension = "m4a";
else if (contentType.Contains("audio/mpeg") || contentType.Contains("audio/mp3"))
detectedExtension = "mp3";
else if (contentType.Contains("video/mp4"))
detectedExtension = "mp4";
}
}
// 修正保存路径
if (!string.IsNullOrEmpty(detectedExtension))
{
actualSavePath = Path.ChangeExtension(actualSavePath, detectedExtension.Trim('.'));
CleanupIncompleteFile(actualSavePath);
}
// 优化4:流式写入文件,且使用ArrayPool复用缓冲区(减少大对象分配)
using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var fileStream = new FileStream(
actualSavePath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize: 8192,
FileOptions.Asynchronous | FileOptions.SequentialScan);
var buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(8192); // 复用缓冲区
try
{
int bytesRead;
long totalRead = 0;
while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0)
{
if (DateTime.UtcNow - lastStreamActivity > streamTimeout)
throw new TimeoutException($"流读取超时({streamTimeout.TotalSeconds}秒无数据)");
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalRead += bytesRead;
lastStreamActivity = DateTime.UtcNow;
}
await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(buffer); // 归还缓冲区
}
return (true, actualSavePath);
}
catch
{
CleanupIncompleteFile(actualSavePath);
throw;
}
finally
{
//httpClient.DefaultRequestHeaders.Clear(); // 清空请求头,帮助GC
}
}
private bool IsRetryableException(Exception ex)
{
return ex is HttpRequestException
|| ex is TimeoutException
|| ex is IOException
|| (ex is AggregateException aggEx && aggEx.InnerExceptions.Any(IsRetryableException));
}
private static void CleanupIncompleteFile(string savePath)
{
if (File.Exists(savePath))
{
try
{
File.Delete(savePath);
}
catch (IOException ex)
{
Log.Error(ex, $"清理无效文件失败:{savePath}(可能被占用)");
}
}
}
#endregion
#region IDisposable 实现(优化版)
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
// 释放托管资源:仅清理本类创建的托管资源(此处无)
}
// 释放非托管资源:本类无非托管资源
// 优化:移除强制GC调用,让GC自动管理
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}
+370
View File
@@ -0,0 +1,370 @@
using dy.net.extension;
using dy.net.model.dto;
using dy.net.utils;
using Serilog;
using System.Security.Cryptography;
namespace dy.net.service
{
/// <summary>
/// 合并多张图片+音频为视频
/// </summary>
public class DouyinMergeVideoService
{
private readonly DouyinHttpClientService douyinHttpClientService;
public DouyinMergeVideoService(DouyinHttpClientService douyinHttpClientService)
{
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="videoFilePaths"></param>
/// <param name="audioPath"></param>
/// <param name="savePath"></param>
/// <param name="ck"></param>
/// <returns></returns>
public async Task<(string mp4Path, string mp3Path)> MergeMultipleVideosAsync(
List<DouyinMergeVideoDto> videoFilePaths,
string audioPath,
string savePath,
string ck)
{
string mp3Path = string.Empty;
string mergMusicPath = GetRandomMergeMusic();
if (!string.IsNullOrWhiteSpace(audioPath))
{
var (SuccessPaths, _) = await DownloadMediaAsync(new List<DouyinMergeVideoDto> { new DouyinMergeVideoDto { Path= audioPath } }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck);
if (SuccessPaths != null && SuccessPaths.Count > 0)
{
mergMusicPath = SuccessPaths.FirstOrDefault()?.Path;
mp3Path = SuccessPaths.FirstOrDefault()?.Path;
}
}
var ffmpeg = new FFmpegHelper();
var mp4Path = await ffmpeg.MergeMultipleVideosAsync(videoFilePaths, mergMusicPath, savePath);
return (mp4Path, mp3Path);
}
private static string GetRandomMergeMusic()
{
var allowedExtensions = new HashSet<string> { ".mp3", ".wav" };
string mergMusic = Path.Combine(AppContext.BaseDirectory, "mp3_0", "silent_10.mp3");//默认音频
var mp3folder = Path.Combine(AppContext.BaseDirectory, "mp3");
if (!string.IsNullOrWhiteSpace(ServiceExtension.FnDataFolder))
{
mp3folder = ServiceExtension.FnDataFolder;//飞牛数据目录
}
var customMusics = Directory.GetFiles(mp3folder)
.Where(filePath =>
allowedExtensions.Contains(Path.GetExtension(filePath).ToLowerInvariant()) &&
Path.GetFileNameWithoutExtension(filePath) != "silent_10")
.ToList();
if (customMusics != null && customMusics.Any())
{
if (customMusics.Count == 1)
return customMusics.FirstOrDefault();
// 使用加密级别的随机数生成器(RNGCryptoServiceProvider)获取高随机性的索引
using (var rng = RandomNumberGenerator.Create())
{
byte[] randomNumber = new byte[4];
rng.GetBytes(randomNumber);
// 将随机字节转换为非负整数,并取模得到有效索引
int randomIndex = Math.Abs(BitConverter.ToInt32(randomNumber, 0)) % customMusics.Count();
return customMusics.ToList()[randomIndex];
}
}
return mergMusic;
}
/// <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.Count == 0)
{
Log.Error($"图片下载失败:{imageError ?? "未下载到任何图片"}");
return false;
}
// 保存下载的图片(如果需要)
if (downImage)
{
await SaveDownloadedFilesAsync(rawImages, fileNamefolder, "jpg", Path.GetFileNameWithoutExtension(outputVideoPath));
}
// 2. 下载音频
List<DouyinMergeVideoDto> rawAudios =new List<DouyinMergeVideoDto>();
if (mergeImg2Viedo && request.AudioUrls != null && request.AudioUrls.Count > 0)
{
var (audios, audioError) = await DownloadMediaAsync(
request.AudioUrls.Select(x => new DouyinMergeVideoDto { Path = x })?.ToList(), Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Count == 0)
{
Log.Error($"音频下载失败:{audioError ?? "未下载到任何音频"}");
return false;
}
rawAudios = audios;
// 保存下载的音频(如果需要)
if (downMp3)
{
var ext = Path.GetExtension(rawAudios.FirstOrDefault()?.Path);
await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, ext, Path.GetFileNameWithoutExtension(outputVideoPath));
}
}
// 不合成视频,直接返回成功
if (!mergeImg2Viedo)
{
Log.Debug($"根据系统配置设置不下载图文视频-[{outputVideoPath}],{(downImage ? "下载图片" : "不下载图片")},{(downMp3 ? "下载音频" : "不下载音频")}");
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));
// 执行合成(确保每个图片都参与)
if (rawAudios.Count == 0)
{
var musicPath = GetRandomMergeMusic();
rawAudios = new List<DouyinMergeVideoDto> { new DouyinMergeVideoDto { Path= musicPath } };
Log.Debug("版权原因无法下载音频,使用默认无声音频文件");
}
string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync(
rawImages, // 所有下载的图片(确保无遗漏)
rawAudios.FirstOrDefault()?.Path, // 取第一个音频(可根据需求调整)
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 static async Task SaveDownloadedFilesAsync(List<DouyinMergeVideoDto> sourcePaths, string targetFolder, string defaultExt, string videoFileName)
{
if (sourcePaths == null || sourcePaths.Count == 0) return;
Directory.CreateDirectory(targetFolder); // 确保目标目录存在
// 异步保存(不阻塞主线程)
await Task.WhenAll(sourcePaths.Select(async (sourcePath, index) =>
{
if (!File.Exists(sourcePath.Path)) return;
string extension = Path.GetExtension(sourcePath.Path) ?? $".{defaultExt}";
string destFileName = $"{videoFileName}{index + 1:D3}{extension}";
string destPath = Path.Combine(targetFolder, destFileName);
// 文件不存在时才复制,避免覆盖
if (!File.Exists(destPath))
{
await Task.Run(() => File.Copy(sourcePath.Path, 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 static 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<(List<DouyinMergeVideoDto> SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
List<DouyinMergeVideoDto> urls, string saveDir, string prefix, string ext, string cookie)
{
var successPaths = new List<DouyinMergeVideoDto>();
for (var i = 0; i < urls.Count; i++)
{
var url = urls[i];
var fileExt = ext ?? Path.GetExtension(url.Path).TrimStart('.') ?? "png";
var fileName = $"{prefix}{i + 1}.{fileExt}";
var savePath = Path.Combine(saveDir, fileName);
try
{
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url.Path, savePath, cookie);
if (Success)
{
successPaths.Add(new DouyinMergeVideoDto
{
Path = ActualSavePath,
Height = url.Height,
Width =
url.Width
});
}
else
{
Serilog.Log.Error($"下载失败:{url} → {savePath}");
}
//Console.WriteLine($"下载成功:{url} → {savePath}");
}
catch (Exception ex)
{
var error = $"下载失败:{url},错误:{ex.Message}";
Serilog.Log.Error(error);
return (new List<DouyinMergeVideoDto>(), error);
}
}
return (successPaths, null);
}
}
}
+426
View File
@@ -0,0 +1,426 @@
using dy.net.job;
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.utils;
using Quartz;
using Serilog;
namespace dy.net.service
{
/// <summary>
/// 抖音相关定时任务服务
/// </summary>
public class DouyinQuartzJobService
{
private readonly ISchedulerFactory _schedulerFactory;
private readonly DouyinCookieService douyinCookieService;
private const string DefaultJobGroup = "dysync.net";
private const int DefaultIntervalMinutes = 30;
private const int DefaultCronStartDelaySeconds = 30;
private const int DefaultSimpleStartDelaySeconds = 3;
// 任务配置信息(修复了series任务的Key重复问题,确保每个任务Key唯一)
public static Dictionary<VideoTypeEnum, JobConfig> JobConfigs { get; } = new()
{
{
VideoTypeEnum.dy_collects,
new JobConfig(
typeof(DouyinCollectSyncJob),
"dy.job.key.collect",
"dy.trigger.key.collect",
"抖音收藏同步任务")
},
{
VideoTypeEnum.dy_favorite,
new JobConfig(
typeof(DouyinFavoritSyncJob),
"dy.job.key.favorite",
"dy.trigger.key.favorite",
"抖音点赞同步任务")
},
{
VideoTypeEnum.dy_follows,
new JobConfig(
typeof(DouyinFollowedSyncJob),
"dy.job.key.followed",
"dy.trigger.key.followed",
"抖音关注博主作品同步任务")
},
{
VideoTypeEnum.dy_followuser,
new JobConfig(
typeof(DouyinFollowsAndCollnectsSyncJob),
"dy.job.key.follow_user",
"dy.trigger.key.follow_user",
"抖音关注列表同步任务")
},
{
VideoTypeEnum.dy_custom_collect,
new JobConfig(
typeof(DouyinCollectCustomSyncJob),
"dy.job.key.custom_collect",
"dy.trigger.key.custom_collect",
"抖音自定义收藏夹列表同步任务")
},
{
VideoTypeEnum.dy_mix,
new JobConfig(
typeof(DouyinMixSyncJob),
"dy.job.key.mix",
"dy.trigger.key.mix",
"抖音收藏夹合集同步任务")
},
{
VideoTypeEnum.dy_series,
new JobConfig(
typeof(DouyinSeriesSyncJob),
"dy.job.key.series",
"dy.trigger.key.series",
"抖音收藏夹短剧同步任务")
},
{
VideoTypeEnum.dy_followuser_once,
new JobConfig(
typeof(DouyinFollowsAndCollnectsSyncJob),
"dy.job.key.sync_follow_user_once",
"dy.trigger.key.sync_follow_user_once",
"抖音关注同步任务(单次执行)")
}
};
public DouyinQuartzJobService(ISchedulerFactory schedulerFactory,DouyinCookieService douyinCookieService)
{
_schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
this.douyinCookieService = douyinCookieService;
}
/// <summary>
/// 初始化或重启所有抖音定时任务
/// </summary>
/// <param name="cronExpression">定时任务表达式(分钟数)</param>
/// <returns>是否成功初始化</returns>
public async Task<bool> InitOrReStartAllJobs(string cronExpression)
{
try
{
// 1. 获取并验证Cookie
var validCookies = await douyinCookieService.GetOpendCookiesAsync();
if (validCookies == null || !validCookies.Any())
{
Serilog.Log.Debug("没有有效的抖音Cookie,无法启动定时任务");
return false;
}
// 2. 处理定时任务表达式
var taskIntervalExpression = ResolveTaskExpression(cronExpression);
// 3. 获取调度器并清理现有任务
var scheduler = await _schedulerFactory.GetScheduler();
if (scheduler == null)
{
Log.Error("获取任务调度器失败,无法初始化定时任务");
return false;
}
await RemoveAllExistingJobs(scheduler);
// 4. 检查各类型任务的启用条件
var taskEnableConditions = GetTaskEnableConditions(validCookies);
// 5. 启动符合条件的定时任务
int successfullyStartedJobs = 0;
foreach (var jobKey in JobConfigs.Keys)
{
// 跳过一次性关注用户任务
if (jobKey == VideoTypeEnum.dy_followuser_once)
continue;
// 处理关注用户任务(固定60分钟执行频率)
if (jobKey == VideoTypeEnum.dy_followuser)
{
bool startSuccess = await StartSingleJobAsync(jobKey, "60");
if (startSuccess) successfullyStartedJobs++;
continue;
}
// 根据不同任务类型和启用条件启动任务
bool isTaskEnabled = jobKey switch
{
VideoTypeEnum.dy_favorite => taskEnableConditions.IsFavoriteEnabled,
VideoTypeEnum.dy_collects => taskEnableConditions.IsCollectEnabled,
VideoTypeEnum.dy_follows => taskEnableConditions.IsFollowedEnabled,
VideoTypeEnum.dy_custom_collect => taskEnableConditions.IsCustomCollectEnabled,
VideoTypeEnum.dy_mix => taskEnableConditions.IsMixEnabled,
VideoTypeEnum.dy_series => taskEnableConditions.IsSeriesEnabled,
_ => false
};
if (isTaskEnabled)
{
bool startSuccess = await StartSingleJobAsync(jobKey, taskIntervalExpression);
if (startSuccess) successfullyStartedJobs++;
}
}
// 6. 输出任务启动统计日志
Log.Information($"定时任务初始化完成,共尝试启动 {JobConfigs.Count - 1} 个任务,成功启动 {successfullyStartedJobs} 个");
return true;
}
catch (Exception ex)
{
Log.Error(ex, "【quartz】初始化所有抖音定时任务时发生异常");
return false;
}
}
/// <summary>
/// 解析任务执行表达式,为空时使用默认值
/// </summary>
/// <param name="inputExpression">输入的表达式</param>
/// <returns>处理后的表达式</returns>
private static string ResolveTaskExpression(string inputExpression)
{
if (string.IsNullOrWhiteSpace(inputExpression))
{
Log.Debug("定时任务表达式为空,使用默认配置({DefaultMinutes}分钟)", DefaultIntervalMinutes);
return DefaultIntervalMinutes.ToString();
}
return inputExpression;
}
/// <summary>
/// 获取各类型任务的启用条件
/// </summary>
/// <param name="cookies">有效的抖音Cookie列表</param>
/// <returns>任务启用条件集合</returns>
private static TaskEnableConditions GetTaskEnableConditions(IEnumerable<DouyinCookie> cookies)
{
return new TaskEnableConditions
{
IsCollectEnabled = cookies.Any(x => x.DownCollect && !x.UseCollectFolder && !string.IsNullOrWhiteSpace(x.SavePath)),
IsFavoriteEnabled = cookies.Any(x => x.DownFavorite && !string.IsNullOrWhiteSpace(x.FavSavePath)),
IsFollowedEnabled = cookies.Any(x => x.DownFollowd && !string.IsNullOrWhiteSpace(x.UpSavePath)),
IsMixEnabled = cookies.Any(x => x.DownMix && !string.IsNullOrWhiteSpace(x.MixPath)),
IsSeriesEnabled = cookies.Any(x => x.DownSeries && !string.IsNullOrWhiteSpace(x.SeriesPath)),
IsCustomCollectEnabled = cookies.Any(x => x.UseCollectFolder && !string.IsNullOrWhiteSpace(x.SavePath))
};
}
/// <summary>
/// 启动单个定时任务(封装重复的启动逻辑)
/// </summary>
/// <param name="jobKey">任务类型</param>
/// <param name="expression">执行频率表达式</param>
/// <returns>是否启动成功</returns>
private async Task<bool> StartSingleJobAsync(VideoTypeEnum jobKey, string expression)
{
try
{
bool startSuccess = await StartJobAsync(jobKey, expression);
if (startSuccess)
{
Log.Debug($"【quartz】成功启动任务:{jobKey},执行频率:{expression}分钟");
}
else
{
Log.Error($"【quartz】启动任务失败:{jobKey}");
}
return startSuccess;
}
catch (Exception ex)
{
Log.Error(ex, $"【quartz】启动任务 {jobKey} 时发生异常");
return false;
}
}
/// <summary>
/// 启动关注同步任务(单次执行)
/// </summary>
public async Task<bool> StartFollowJobOnceAsync()
{
return await StartOneTimeJobAsync(VideoTypeEnum.dy_followuser_once);
}
/// <summary>
/// 移除所有已存在的任务(避免重复调度)
/// </summary>
private static async Task RemoveAllExistingJobs(IScheduler scheduler)
{
var jobKeys = JobConfigs.Values.Select(config => new JobKey(config.JobKey, DefaultJobGroup)).ToList();
foreach (var jobKey in jobKeys)
{
if (await scheduler.CheckExists(jobKey))
{
//Log.Debug("【quartz】移除已存在的任务: {JobKey}", jobKey);
await scheduler.DeleteJob(jobKey);
}
}
}
/// <summary>
/// 启动指定定时任务(独立执行,无依赖触发)
/// </summary>
/// <param name="configKey">任务配置Key(如:collect、favorite)</param>
/// <param name="expression">定时表达式(Cron或间隔分钟数)</param>
/// <returns>是否启动成功</returns>
public async Task<bool> StartJobAsync(VideoTypeEnum configKey, string expression)
{
if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
{
Log.Error("【quartz】找不到任务配置: {ConfigKey}", configKey);
return false;
}
try
{
var scheduler = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup);
// 移除已存在的任务(防止重复执行)
await RemoveExistingJobAsync(scheduler, jobKey);
// 创建任务详情(保留禁止并发执行,避免同一任务重复运行)
var jobDetail = JobBuilder.Create(jobConfig.JobType)
.WithIdentity(jobKey)
.WithDescription(jobConfig.Description)
.DisallowConcurrentExecution() // 禁止同一任务并发执行
.Build();
// 创建定时触发器(仅使用定时触发,移除依赖触发逻辑)
ITrigger trigger = CreateScheduledTrigger(triggerKey, expression, jobConfig.Description);
// 调度任务
await scheduler.ScheduleJob(jobDetail, trigger);
Log.Information("【quartz】启动任务成功 - 任务描述: {JobDescription}, 执行频率: {Expression}",
jobConfig.Description,
expression);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "【quartz】启动任务失败 - 任务描述: {JobDescription}", jobConfig.Description);
return false;
}
}
/// <summary>
/// 启动单次执行任务
/// </summary>
private async Task<bool> StartOneTimeJobAsync(VideoTypeEnum configKey)
{
if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
{
Log.Error("【quartz】找不到任务配置: {ConfigKey}", configKey);
return false;
}
try
{
var scheduler = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup);
await RemoveExistingJobAsync(scheduler, jobKey);
var jobDetail = JobBuilder.Create(jobConfig.JobType)
.WithIdentity(jobKey)
.WithDescription(jobConfig.Description)
.DisallowConcurrentExecution()
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.WithDescription($"{jobConfig.Description} - 单次执行")
.StartNow()
.Build();
await scheduler.ScheduleJob(jobDetail, trigger);
Log.Debug("【quartz】启动单次任务成功 - 任务描述: {JobDescription}", jobConfig.Description);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "【quartz】启动单次任务失败 - 任务描述: {JobDescription}", jobConfig.Description);
return false;
}
}
/// <summary>
/// 创建定时触发器(支持Cron表达式或分钟间隔)
/// </summary>
private static ITrigger CreateScheduledTrigger(TriggerKey triggerKey, string expression, string jobDescription)
{
// Cron表达式格式
if (CronExpression.IsValidExpression(expression))
{
return TriggerBuilder.Create()
.WithIdentity(triggerKey)
.WithDescription($"{jobDescription} - Cron调度")
.WithCronSchedule(expression)
.StartAt(DateTime.Now.AddSeconds(DefaultCronStartDelaySeconds))
.Build();
}
// 数字间隔格式(分钟)
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();
}
// 无效表达式,使用默认配置
//Log.Debug("【任务服务】无效的任务表达式: {Expression},使用默认间隔{DefaultMinutes}分钟",
// expression, DefaultIntervalMinutes);
return TriggerBuilder.Create()
.WithIdentity(triggerKey)
.WithDescription($"{jobDescription} - 默认间隔调度")
.StartAt(DateTime.Now.AddSeconds(DefaultSimpleStartDelaySeconds))
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(DefaultIntervalMinutes)
.RepeatForever())
.Build();
}
/// <summary>
/// 移除已存在的任务
/// </summary>
private static async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
{
if (await scheduler.CheckExists(jobKey))
{
//Log.Debug("【quartz】移除已存在的任务: {JobKey}", jobKey);
await scheduler.DeleteJob(jobKey);
}
}
/// <summary>
/// 任务启用条件模型
/// </summary>
private class TaskEnableConditions
{
public bool IsCollectEnabled { get; set; }
public bool IsFavoriteEnabled { get; set; }
public bool IsFollowedEnabled { get; set; }
public bool IsMixEnabled { get; set; }
public bool IsSeriesEnabled { get; set; }
public bool IsCustomCollectEnabled { get; set; }
}
}
}
+658
View File
@@ -0,0 +1,658 @@
using ClockSnowFlake;
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.repository;
using dy.net.utils;
using Serilog;
using SqlSugar;
namespace dy.net.service
{
public class DouyinVideoService
{
private readonly ISqlSugarClient sqlSugarClient;
private readonly DouyinVideoRepository _dyCollectVideoRepository;
private readonly DouyinCookieRepository douyinCookieRepository;
public DouyinVideoService(DouyinVideoRepository dyCollectVideoRepository, DouyinCookieRepository douyinCookieRepository, ISqlSugarClient sqlSugarClient)
{
_dyCollectVideoRepository = dyCollectVideoRepository;
this.douyinCookieRepository = douyinCookieRepository;
this.sqlSugarClient = sqlSugarClient;
}
public async Task<bool> DeleteById(string Id)
{
return await _dyCollectVideoRepository.DeleteByIdAsync(Id);
}
public async Task<bool> BatchInsertOrUpdate(List<DouyinVideo> videos)
{
// 边界处理:传入列表为空直接返回成功
if (videos == null || !videos.Any())
return true;
// 1. 提取所有AwemeId(无需去重,用户保证无重复)
var allAwemeIds = videos.Select(v => v.AwemeId).ToList();
// 2. 查询数据库中已存在的视频记录(用于后续更新)
var existingVideos = await _dyCollectVideoRepository
.Query(x => allAwemeIds.Contains(x.AwemeId))
.ToListAsync();
// 3. 分拆数据集:不存在的(插入)、已存在的(更新)
var existingAwemeIdSet = existingVideos.Select(v => v.AwemeId).ToHashSet();
var videosToInsert = videos
.Where(v => !existingAwemeIdSet.Contains(v.AwemeId))
.ToList();
var videosToUpdate = videos
.Where(v => existingAwemeIdSet.Contains(v.AwemeId))
.ToList();
// 4. 事务包裹:确保插入/更新原子性
var transaction = await _dyCollectVideoRepository.UseTranAsync(async () =>
{
int insertedCount = 0;
int updatedCount = 0;
// 5. 批量插入新记录
if (videosToInsert.Any())
{
insertedCount = await _dyCollectVideoRepository.InsertRangeAsync(videosToInsert);
}
// 6. 批量更新已存在记录(核心逻辑)
if (videosToUpdate.Any())
{
// 建立AwemeId与待更新数据的映射(O(1)匹配效率)
var updateMap = videosToUpdate.ToDictionary(v => v.AwemeId);
// 遍历已存在实体,赋值需要更新的字段
List<DouyinVideo> updates = new List<DouyinVideo>();
foreach (var existingVideo in existingVideos)
{
if (updateMap.TryGetValue(existingVideo.AwemeId, out var updateData))
{
existingVideo.VideoSavePath = updateData.VideoSavePath;
existingVideo.VideoCoverSavePath = updateData.VideoCoverSavePath;
existingVideo.ViedoType = updateData.ViedoType;
}
}
// 批量更新数据库
updatedCount = await _dyCollectVideoRepository.UpdateRangeAsync(existingVideos);
}
//foreach (var item in videos.Where(x=>x.ViedoType==VideoTypeEnum.dy_collects||x.ViedoType == VideoTypeEnum.dy_favorite).GroupBy(x => x.AuthorId))
//{
//}
}, ex =>
{
Serilog.Log.Error(ex, "批量插入/更新抖音视频失败,AwemeIds:{AwemeIds}", string.Join(",", allAwemeIds));
});
return transaction;
}
public async Task<bool> UpdateOne(DouyinVideo video)
{
return await _dyCollectVideoRepository.UpdateAsync(video);
}
public async Task<VideoStaticsDto> GetStatics()
{
List<DouyinVideo> list = await this._dyCollectVideoRepository.GetAllAsync();
if (!list.Any())
return new VideoStaticsDto();
var Categories = list.GroupBy(x => x.Tag1).Select(x => new VideoStaticsItemDto { Name = x.Key, Count = x.LongCount() }).OrderByDescending(p => p.Count).ToList();
Categories.Where(x => string.IsNullOrWhiteSpace(x.Name)).ToList().ForEach(x => x.Name = "其他");
var data = new VideoStaticsDto
{
AuthorCount = list.Select(x => x.AuthorId).Distinct().Count(),
CategoryCount = list.Select(x => x.Tag1).Distinct().Count(),
VideoCount = list.Count,
Categories = Categories,
FavoriteCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_favorite),
CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_collects || x.ViedoType == VideoTypeEnum.dy_custom_collect),
FollowCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_follows),
GraphicVideoCount = list.Count(x => x.IsMergeVideo == 1),
MixCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_mix),
SeriesCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_series),
VideoSizeTotal = DouyinFileUtils.ConvertBytesToGb(list.Sum(x => x.FileSize)),
VideoFavoriteSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_favorite).Sum(x => x.FileSize)),
VideoCollectSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_collects || x.ViedoType == VideoTypeEnum.dy_custom_collect).Sum(x => x.FileSize)),
VideoFollowSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_follows).Sum(x => x.FileSize)),
VideoMixSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_mix).Sum(x => x.FileSize)),
VideoSeriesSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.ViedoType == VideoTypeEnum.dy_series).Sum(x => x.FileSize)),
GraphicVideoSize = DouyinFileUtils.ConvertBytesToGb(list.Where(x => x.IsMergeVideo == 1).Sum(x => x.FileSize)),
//TotalDiskSize= ByteToGbConverter.GetHostTotalDiskSpaceGB(),
};
if (data.GraphicVideoSize == "0.00")
{
if (list.Where(x => x.IsMergeVideo == 1).Sum(x => x.FileSize) > 0)
{
data.GraphicVideoSize = "<0.01";//避免显示0.00误导用户
}
}
if (data.VideoFavoriteSize == "0.00")
{
data.VideoFavoriteSize = "<0.01";//避免显示0.00误导用户
}
if (data.VideoCollectSize == "0.00")
{
data.VideoCollectSize = "<0.01";//避免显示0.00误导用户
}
if (data.VideoFollowSize == "0.00")
{
data.VideoFollowSize = "<0.01";//避免显示0.00误导用户
}
if (data.VideoMixSize == "0.00")
{
data.VideoMixSize = "<0.01";//避免显示0.00误导用户
}
if (data.VideoSeriesSize == "0.00")
{
data.VideoSeriesSize = "<0.01";//避免显示0.00误导用户
}
data.Authors = list.GroupBy(x => x.Author).Select(x => new VideoStaticsItemDto
{
Name = x.Key,
Count = x.LongCount(),
Icon = x.LastOrDefault()?.AuthorAvatarUrl,
UperId = x.LastOrDefault()?.AuthorId ?? x.LastOrDefault()?.DyUserId
}).OrderByDescending(d => d.Count).ToList();
return data;
}
/// <summary>
///
/// </summary>
/// <param name="awemeId"></param>
/// <returns></returns>
public async Task<DouyinVideo> GetByAwemeId(string awemeId)
{
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);
}
public async Task<List<DouyinVideo>> GetAllAsync()
{
return await _dyCollectVideoRepository.GetAllAsync();
}
/// <summary>
/// 关注的博主的视频如果配置为视频标题作为文件名,生成文件名
/// </summary>
/// <param name="AuthorId"></param>
/// <param name="ViedoNameSimplify"></param>
/// <returns></returns>
public (string, string) GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
{
return _dyCollectVideoRepository.GetUperLastViedoFileName(AuthorId, ViedoNameSimplify);
}
/// <summary>
/// 根据ID获取视频信息
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<DouyinVideo> GetById(string id)
{
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, bool forever = false)
{
// 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<DouyinReDownload>();
var filePathsToDelete = new List<(string path, bool onlyImgOrMp3)>(); // 收集待删除文件路径,统一处理
foreach (var video in videos)
{
// 跳过无保存路径的视频(避免无效文件操作)
if (string.IsNullOrWhiteSpace(video.VideoSavePath))
{
Serilog.Log.Debug("视频无保存路径,跳过文件删除:VideoId={0}", video.Id);
continue;
}
// 构建重新下载记录
reDownList.Add(new DouyinReDownload
{
Id = IdGener.GetLong().ToString(),
CreateTime = DateTime.UtcNow, // 统一使用UTC时间,避免时区问题
Status = 0, // 0=待下载(建议用枚举替代魔法值)
SavePath = video.VideoSavePath,
ViedoId = video.AwemeId,
CookieId = video.CookieId
});
filePathsToDelete.Add((video.VideoSavePath, video.OnlyImgOrOnlyMp3));
}
// 无有效重新下载记录时直接返回
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 video in filePathsToDelete)
{
try
{
if (File.Exists(video.path))
{
File.Delete(video.path); // 异步删除,提升并发性能
Serilog.Log.Debug("视频文件删除成功:Path={0}", video.path);
if (!video.onlyImgOrMp3)//如果是纯图片或纯音频文件,则不删除所在文件夹
{
//检查这个路径所在文件夹是否还有其他视频文件,如果没有则删除这个文件夹
var dir = Path.GetDirectoryName(video.path);
bool hasMp4File = Directory.EnumerateFiles(dir, "*.mp4", SearchOption.TopDirectoryOnly).Any(); // 只要存在一个MP4文件就返回true;
if (!hasMp4File)
{
Directory.Delete(dir, true);
}
}
}
else
{
Serilog.Log.Error("视频文件不存在,跳过删除:Path={0}", video);
}
}
catch (IOException ex)
{
Serilog.Log.Error(ex, "视频文件删除失败:Path={0}", video);
}
}
//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.dy_favorite:
// // cookie.FavHasSyncd = 0;
// // break;
// // case VideoTypeEnum.dy_collects:
// // cookie.CollHasSyncd = 0;
// // break;
// // case VideoTypeEnum.dy_follows:
// // cookie.UperSyncd = 0;
// // break;
// // case VideoTypeEnum.ImageVideo:
// // break;
// // default:
// // break;
// // }
// // }
// //}
// await douyinCookieRepository.UpdateAsync(cookie);
//}
if (!forever)
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;
}
}
public async Task<List<DouyinVideoTopDto>> GetLastSyncTop(int top = 5)
{
return await _dyCollectVideoRepository.GetTopsOrderBySyncTime(top);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public async Task<List<VideoChartItemDto>> GetChartData(int day = 7)
{
var date = DateTime.Now.AddDays(-day);
var list = await _dyCollectVideoRepository.GetListAsync(x => x.SyncTime > date);
var resultData = list.GroupBy(x => x.SyncTime.ToString("yyyyMMdd")).Select(g => new VideoChartItemDto
{
Date = g.Key,
Collect = g.Count(x => x.ViedoType == VideoTypeEnum.dy_collects || x.ViedoType == VideoTypeEnum.dy_custom_collect),
Favorite = g.Count(x => x.ViedoType == VideoTypeEnum.dy_favorite),
Follow = g.Count(x => x.ViedoType == VideoTypeEnum.dy_follows),
Graphic = g.Count(x => string.IsNullOrEmpty(x.FileHash)),
Mix = g.Count(x => x.ViedoType == VideoTypeEnum.dy_mix),
Series = g.Count(x => x.ViedoType == VideoTypeEnum.dy_series),
})
.ToList();
return resultData;
}
/// <summary>
/// 删除无效记录(记录存在,用户手动把目录下的视频删了的情况,视频记录依然存在)
/// </summary>
/// <returns></returns>
public async Task<List<DeleteInvalidVideoDto>> DeleteInvalidVideo()
{
var videos = await _dyCollectVideoRepository.GetAllAsync();
List<DeleteInvalidVideoDto> vList = new List<DeleteInvalidVideoDto>();
List<string> douyinVideoIds = new List<string>();
foreach (var v in videos)
{
if (!File.Exists(v.VideoSavePath))
{
douyinVideoIds.Add(v.Id);
vList.Add(new DeleteInvalidVideoDto { AwId = v.AwemeId, Title = v.VideoTitle, Path = v.VideoSavePath });
}
}
if (douyinVideoIds.Any())
{
await _dyCollectVideoRepository.DeleteByIdsAsync(douyinVideoIds);
}
return vList;
}
/// <summary>
/// 根据博主ID获取视频列表
/// </summary>
/// <param name="uperUid"></param>
/// <returns></returns>
internal async Task<List<DouyinVideo>> GetByAuthorId(string uperUid)
{
return await _dyCollectVideoRepository.GetListAsync(x => x.DyUserId == uperUid);
}
internal async Task<int> AddDeleteVideo(List<DouyinVideo> videos)
{
var deletes = videos.Select(video => new DouyinVideoDelete
{
ViedoId = video.AwemeId,
VideoTitle = video.VideoTitle,
VideoSavePath = video.VideoSavePath,
Id = IdGener.GetLong().ToString(),
DeleteTime = DateTime.Now
})?.ToList();
return await sqlSugarClient.Insertable<DouyinVideoDelete>(deletes).ExecuteCommandAsync();
}
/// <summary>
/// 彻底删除视频
/// </summary>
/// <param name="Ids"></param>
/// <returns></returns>
public async Task<bool> RealDeleteVideos(List<string> Ids)
{
if (Ids == null || !Ids.Any())
return false;
var videos = await _dyCollectVideoRepository.GetListAsync(x => Ids.Contains(x.Id));
if (videos != null && videos.Count > 0)
{
if (videos.Count <= 30)
{
var result = await ReDownloadViedoAsync(new ReDownViedoDto { Ids = videos.Select(x => x.Id)?.ToList() }, true);
if (result)
{
//加入删除逻辑
var deletes = await AddDeleteVideo(videos);
Serilog.Log.Debug($"批量永久删除博主{videos.FirstOrDefault()?.Author},共{deletes}条记录");
return true;
}
else
{
Serilog.Log.Error($"批量删除{videos.FirstOrDefault()?.Author}视频失败");
return false;
}
}
else
{
Task.Run(async () =>
{
var result = await ReDownloadViedoAsync(new ReDownViedoDto { Ids = videos.Select(x => x.Id)?.ToList() }, true);
if (result)
{
//加入删除逻辑
var deletes = await AddDeleteVideo(videos);
Serilog.Log.Debug($"批量永久删除博主{videos.FirstOrDefault()?.Author},{deletes}条记录");
}
else
{
Serilog.Log.Error($"批量删除{videos.FirstOrDefault()?.Author}视频失败");
}
});
return true;
}
}
else
{
Serilog.Log.Error($"没有查询到可删除的视频");
return false;
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public async Task<bool> HandOldFolderVideos()
{
// 1. 查询目标数据
var list = await _dyCollectVideoRepository.GetListAsync(x => x.ViedoType == VideoTypeEnum.dy_favorite || x.ViedoType == VideoTypeEnum.dy_collects);
// 缓存已处理的「Tag1+下一级文件夹」组合(避免重复移动)
var processedFolderPairs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in list)
{
// 跳过空值(Tag1/Author为空)
if (string.IsNullOrEmpty(item.Tag1) || string.IsNullOrEmpty(item.Author))
{
Log.Debug($"跳过:Tag1/Author为空,ItemId={item.Id}");
continue;
}
try
{
// 2. 标准化路径 + 拆分路径层级(核心:精准定位Tag1和其下一级文件夹)
string oldVideoPath = item.VideoSavePath;
// 统一分隔符为/,方便拆分层级
string standardPath = oldVideoPath.Replace('\\', '/').Trim('/');
string[] pathSegments = standardPath.Split('/'); // 拆分结果:["app","collect","校园教育","期末复习xxx","文件.mp4"]
// 2.1 找到Tag1在路径中的索引(比如"校园教育"的索引是2)
int tag1Index = Array.IndexOf(pathSegments, item.Tag1);
if (tag1Index == -1 || tag1Index + 1 >= pathSegments.Length - 1)
{
Log.Debug($"跳过:无Tag1下一级文件夹,Path={oldVideoPath},Tag1={item.Tag1}");
continue;
}
// 2.2 解析核心路径(关键!)
string tag1FolderRelative = string.Join("/", pathSegments.Take(tag1Index + 1)); // Tag1根目录(相对):app/collect/校园教育
// Tag1根目录完整路径(如 D:/app/collect/校园教育 或 /app/collect/校园教育)
string tag1RootFolderFull = Path.GetFullPath(
Path.Combine(Path.GetPathRoot(oldVideoPath) ?? "",
tag1FolderRelative.Replace('/', Path.DirectorySeparatorChar))
);
string tag1NextLevelFolderName = pathSegments[tag1Index + 1]; // Tag1下一级文件夹名:期末复习xxx
// Tag1下一级文件夹完整路径:app/collect/校园教育/期末复习xxx
string tag1NextLevelFolderFull = Path.GetFullPath(
Path.Combine(tag1RootFolderFull, tag1NextLevelFolderName)
);
// 2.3 拼接新路径(替换Tag1为Author,保留下一级文件夹名)
// 新Author根目录完整路径:app/collect/张三
string authorRootFolderFull = tag1RootFolderFull.Replace(item.Tag1, item.Author);
// 新的下一级文件夹完整路径:app/collect/张三/期末复习xxx
string authorNextLevelFolderFull = Path.GetFullPath(
Path.Combine(authorRootFolderFull, tag1NextLevelFolderName)
);
// 2.4 防重复处理(Tag1下一级文件夹已处理则跳过)
string folderPairKey = $"{tag1NextLevelFolderFull}|{authorNextLevelFolderFull}";
if (processedFolderPairs.Contains(folderPairKey))
{
Log.Debug($"跳过:下一级文件夹已处理,Key={folderPairKey}");
continue;
}
// 3. 核心判断:检查Tag1的下一级文件夹是否有文件(而非Tag1根目录)
if (!Directory.Exists(tag1NextLevelFolderFull))
{
Log.Warning($"跳过:Tag1下一级文件夹不存在,Path={tag1NextLevelFolderFull}");
continue;
}
string[] nextLevelFiles = Directory.GetFiles(tag1NextLevelFolderFull); // 非递归,只查该文件夹下的文件
if (nextLevelFiles.Length == 0)
{
Log.Debug($"跳过:Tag1下一级文件夹无文件,Path={tag1NextLevelFolderFull}");
processedFolderPairs.Add(folderPairKey);
continue;
}
// 4. 移动Tag1的下一级文件夹(保留文件夹名,整体移动到Author目录下)
// 4.1 确保新Author目录存在
Directory.CreateDirectory(authorRootFolderFull);
// 4.2 目标文件夹已存在则跳过(如需覆盖,可删除此行+添加Directory.Delete(authorNextLevelFolderFull, true))
if (Directory.Exists(authorNextLevelFolderFull))
{
Log.Debug($"跳过:目标下一级文件夹已存在,Path={authorNextLevelFolderFull}");
processedFolderPairs.Add(folderPairKey);
continue;
}
// 4.3 移动整个下一级文件夹(保留名称和内部所有文件)
Directory.Move(tag1NextLevelFolderFull, authorNextLevelFolderFull);
Log.Debug($"移动Tag1下一级文件夹成功:{tag1NextLevelFolderFull} → {authorNextLevelFolderFull}");
// 5. 关键新增:检查Tag1根目录是否为空,为空则删除
if (Directory.Exists(tag1RootFolderFull))
{
// 检查Tag1根目录下是否还有任何文件/文件夹
bool isTag1RootEmpty = !Directory.EnumerateFileSystemEntries(tag1RootFolderFull).Any();
if (isTag1RootEmpty)
{
Directory.Delete(tag1RootFolderFull, false); // false=仅删除空目录,避免误删
Log.Debug($"删除空Tag1根目录:{tag1RootFolderFull}");
}
else
{
Log.Debug($"Tag1根目录非空,不删除:{tag1RootFolderFull}");
}
}
// 标记已处理
processedFolderPairs.Add(folderPairKey);
// 6. 更新当前Item的视频路径(替换Tag1为Author,保留后续层级)
string newVideoPath = oldVideoPath.Replace(item.Tag1, item.Author);
item.VideoSavePath = newVideoPath;
Log.Debug($"更新Item路径:{oldVideoPath} → {newVideoPath}");
}
catch (IOException ex)
{
Log.Error(ex, $"移动失败(IO异常),ItemId={item.Id},Path={item.VideoSavePath}");
}
catch (UnauthorizedAccessException ex)
{
Log.Error(ex, $"删除/移动失败(权限不足),ItemId={item.Id},Path={item.VideoSavePath}");
}
catch (Exception ex)
{
Log.Error(ex, $"处理失败(未知错误),ItemId={item.Id},Path={item.VideoSavePath}");
}
}
// 批量更新数据库
if (list.Any())
{
await BatchInsertOrUpdate(list);
Log.Debug($"批量更新数据库完成,共处理{list.Count}条数据");
}
return true;
}
}
}
+3 -6
View File
@@ -1,8 +1,5 @@
using dy.net.model;
using dy.net.model.entity;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
namespace dy.net.service
{
@@ -11,9 +8,9 @@ namespace dy.net.service
/// </summary>
public class DownloadConfigService
{
private readonly CommonService _commonService;
private readonly DouyinCommonService _commonService;
public DownloadConfigService(CommonService commonService)
public DownloadConfigService(DouyinCommonService commonService)
{
_commonService = commonService;
}
-279
View File
@@ -1,279 +0,0 @@
using dy.net.dto;
using dy.net.model;
using dy.net.repository;
using dy.net.utils;
using System.ComponentModel;
using System.Threading.Tasks;
namespace dy.net.service
{
public class DyCollectVideoService
{
private readonly DyCollectVideoRepository _dyCollectVideoRepository;
public DyCollectVideoService(DyCollectVideoRepository dyCollectVideoRepository)
{
_dyCollectVideoRepository = dyCollectVideoRepository;
}
public async Task<bool> batchInsert(List<DyCollectVideo> videos)
{
// 边界处理:如果传入的列表为空,直接返回成功(或根据业务返回false)
if (videos == null || !videos.Any())
return true;
// 1. 提取待插入的所有AwemeId(去重,减少数据库查询压力)
var newAwemeIds = videos.Select(x => x.AwemeId)
.Distinct()
.ToList();
// 2. 查询数据库中已存在的AwemeId(只查需要的字段,提高效率)
var existingAwemeIds = await _dyCollectVideoRepository
.Query(x => newAwemeIds.Contains(x.AwemeId)) // 使用Query方法构建查询
.Select(x => x.AwemeId) // 只获取AwemeId,减少数据传输
.ToListAsync();
// 3. 过滤出数据库中不存在的视频(只保留新记录)
var videosToInsert = videos
.Where(video => !existingAwemeIds.Contains(video.AwemeId))
.ToList();
Serilog.Log.Debug($"batchInsert: 待插入{videos.Count}条,已存在{existingAwemeIds.Count}条,需要插入{videosToInsert.Count}条");
// 4. 如果没有需要插入的新记录,直接返回成功
if (!videosToInsert.Any())
{
Serilog.Log.Debug($"batchInsert: 所有视频都已存在,无需插入");
return true;
}
// 5. 批量插入过滤后的新记录
var insertedCount = await _dyCollectVideoRepository.InsertRangeAsync(videosToInsert);
Serilog.Log.Debug($"batchInsert: 实际插入{insertedCount}条记录");
// 返回是否插入成功(至少插入一条)
return insertedCount > 0;
}
public async Task<VideoStaticsDto> GetStatics()
{
List<DyCollectVideo> allList = await this._dyCollectVideoRepository.GetAllAsync();
if (!allList.Any())
return new VideoStaticsDto();
// 只统计文件实际存在的视频
var list = allList.Where(x =>
!string.IsNullOrWhiteSpace(x.VideoSavePath) &&
System.IO.File.Exists(x.VideoSavePath)
).ToList();
Serilog.Log.Debug($"统计视频:数据库记录 {allList.Count} 条,文件存在 {list.Count} 条");
if (!list.Any())
return new VideoStaticsDto();
// 按 viedoCate(Tag1/Tag2 组合)分组统计分类
var Categories = list
.GroupBy(x =>
{
// 使用与模型 ViedoCate 属性相同的逻辑:Tag1 + "/" + Tag2
var tag1 = string.IsNullOrWhiteSpace(x.Tag1) ? "" : x.Tag1;
var tag2 = string.IsNullOrWhiteSpace(x.Tag2) ? "" : x.Tag2;
var viedoCate = tag1 + "/" + tag2;
// 如果 Tag1 和 Tag2 都为空,标记为"未分类"
if (string.IsNullOrWhiteSpace(tag1) && string.IsNullOrWhiteSpace(tag2))
{
return "未分类";
}
return viedoCate;
})
.Select(x => new VideoStaticsItemDto
{
Name = x.Key,
Count = x.LongCount()
})
.OrderByDescending(p => p.Count)
.ToList();
var data = new VideoStaticsDto
{
AuthorCount = list.Select(x => x.AuthorId).Distinct().Count(),
CategoryCount = Categories.Count, // 使用实际分类数量
VideoCount = list.Count,
Categories = Categories,
FavoriteCount = list.Count(x => x.ViedoType == "1"),
CollectCount = list.Count(x => x.ViedoType == "2"),
FollowCount = list.Count(x => x.ViedoType == "3"),
VideoSizeTotal = ByteToGbConverter.ConvertBytesToGb(list.Sum(x => x.FileSize)),
VideoFavoriteSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == "1").Sum(x => x.FileSize)),
VideoCollectSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == "2").Sum(x => x.FileSize)),
VideoFollowSize = ByteToGbConverter.ConvertBytesToGb(list.Where(x => x.ViedoType == "3").Sum(x => x.FileSize)),
//TotalDiskSize= ByteToGbConverter.GetHostTotalDiskSpaceGB(),
};
data.Authors = list.GroupBy(x => x.Author).Select(x => new VideoStaticsItemDto { Name = x.Key, Count = x.LongCount(), Icon = x.FirstOrDefault().AuthorAvatarUrl }).OrderByDescending(d => d.Count).ToList();
return data;
}
//分页查询
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, List<string> tags = null, string author = null, string viedoType = null, List<string>? dates = null, bool? isDeleted = null, string sortField = null, string sortOrder = null)
{
return await _dyCollectVideoRepository.GetPagedAsync(pageIndex, pageSize, tag, tags, author, viedoType, dates, isDeleted, sortField, sortOrder);
}
/// <summary>
/// 获取所有视频类型列表(用于下拉框)
/// </summary>
/// <returns></returns>
public async Task<List<string>> GetAllCategoriesAsync()
{
try
{
// 获取所有视频记录
var allVideos = await _dyCollectVideoRepository.GetAllAsync();
Serilog.Log.Debug($"获取视频类型列表:共 {allVideos?.Count ?? 0} 条视频记录");
// 提取所有非空的 viedoCate(Tag1/Tag2 组合),去重并排序
// 使用与模型 ViedoCate 属性相同的逻辑:Tag1 + "/" + Tag2
var categories = allVideos
.Select(x =>
{
// 使用与模型 ViedoCate 属性相同的逻辑
var tag1 = string.IsNullOrWhiteSpace(x.Tag1) ? "" : x.Tag1;
var tag2 = string.IsNullOrWhiteSpace(x.Tag2) ? "" : x.Tag2;
var viedoCate = tag1 + "/" + tag2;
return viedoCate;
})
.Where(x => !string.IsNullOrWhiteSpace(x) && x != "/") // 过滤空值和只有 "/" 的情况
.Distinct()
.OrderBy(x => x)
.ToList();
// 检查是否存在空类型的视频(Tag1 和 Tag2 都为空)
var hasEmptyCategory = allVideos.Any(x => string.IsNullOrWhiteSpace(x.Tag1) && string.IsNullOrWhiteSpace(x.Tag2));
if (hasEmptyCategory)
{
// 在列表开头添加"未分类"选项
categories.Insert(0, "未分类");
}
Serilog.Log.Debug($"提取到的视频类型数量:{categories.Count},类型列表:{string.Join(", ", categories)}");
return categories;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "获取视频类型列表失败");
return new List<string>();
}
}
/// <summary>
/// 根据ID获取视频信息
/// </summary>
/// <param name="id">视频ID</param>
/// <returns></returns>
public async Task<DyCollectVideo> GetByIdAsync(string id)
{
return await _dyCollectVideoRepository.GetByIdAsync(id);
}
/// <summary>
/// 清理数据库中文件不存在的视频记录
/// </summary>
/// <returns>清理的记录数量</returns>
public async Task<int> CleanupMissingFilesAsync()
{
try
{
var allVideos = await _dyCollectVideoRepository.GetAllAsync();
if (!allVideos.Any())
return 0;
// 找出需要清理的视频ID:
// 1. VideoSavePath 不为空但文件不存在的记录
// 2. VideoSavePath 为空或null的记录(这些记录没有视频文件,也应该被清理)
var missingFileIds = allVideos
.Where(x =>
// 情况1:有路径但文件不存在
(!string.IsNullOrWhiteSpace(x.VideoSavePath) && !System.IO.File.Exists(x.VideoSavePath)) ||
// 情况2:路径为空(这些记录没有视频文件,也应该清理)
string.IsNullOrWhiteSpace(x.VideoSavePath)
)
.Select(x => x.Id)
.ToList();
if (!missingFileIds.Any())
{
Serilog.Log.Debug("清理检查:所有视频文件都存在,无需清理");
return 0;
}
var pathEmptyCount = allVideos.Count(x => string.IsNullOrWhiteSpace(x.VideoSavePath));
var fileMissingCount = allVideos.Count(x => !string.IsNullOrWhiteSpace(x.VideoSavePath) && !System.IO.File.Exists(x.VideoSavePath));
Serilog.Log.Debug($"清理检查:发现 {missingFileIds.Count} 条需要清理的记录(路径为空:{pathEmptyCount} 条,文件不存在:{fileMissingCount} 条)");
// 批量删除
var deletedCount = await _dyCollectVideoRepository.DeleteByIdsAsync(missingFileIds);
Serilog.Log.Debug($"清理完成:删除了 {deletedCount} 条记录");
return deletedCount;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "清理文件不存在的记录失败");
throw;
}
}
/// <summary>
/// 删除指定作者的所有视频数据
/// </summary>
/// <param name="authorName">作者名称</param>
/// <returns>删除的记录数量</returns>
public async Task<int> DeleteAuthorVideosAsync(string authorName)
{
try
{
var allVideos = await _dyCollectVideoRepository.GetAllAsync();
if (!allVideos.Any())
return 0;
// 找出该作者的所有视频ID
var authorVideoIds = allVideos
.Where(x => !string.IsNullOrWhiteSpace(x.Author) && x.Author.Equals(authorName, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Id)
.ToList();
if (!authorVideoIds.Any())
{
Serilog.Log.Debug($"删除作者视频:未找到作者 {authorName} 的视频记录");
return 0;
}
Serilog.Log.Debug($"删除作者视频:发现作者 {authorName} 的 {authorVideoIds.Count} 条视频记录");
// 批量删除
var deletedCount = await _dyCollectVideoRepository.DeleteByIdsAsync(authorVideoIds);
Serilog.Log.Debug($"删除完成:删除了 {deletedCount} 条记录");
return deletedCount;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, $"删除作者 {authorName} 的视频失败");
throw;
}
}
}
}
-86
View File
@@ -1,86 +0,0 @@
using dy.net.model;
using dy.net.repository;
using SqlSugar;
using System.Linq.Expressions;
namespace dy.net.service
{
public class DyCookieService
{
private readonly DyCookieRepository _cookieRepository;
public DyCookieService(DyCookieRepository cookieRepository)
{
_cookieRepository = cookieRepository;
}
public Task<List<DyUserCookies>> GetAllCookies()
{
return _cookieRepository.GetAllCookies();
}
public async Task<bool> Add(DyUserCookies dyUserCookies)
{
return await _cookieRepository.InsertAsync(dyUserCookies);
}
public bool Init()
{
var initId= "2026";
var exist = _cookieRepository.GetFirst(x => x.Id == initId);
if (exist != null)
{
return false;
}
var cookie = new DyUserCookies
{
UserName = "douyin",
Cookies = "--",
Id = initId,
SavePath = "/app/collect",
Status = 0,
SecUserId = "--",
FavSavePath = "/app/favorite",
UpSavePath = "/app/uper",
CollHasSyncd = 0,
FavHasSyncd = 0,
UperSyncd = 0
};
return _cookieRepository.Insert(cookie);
}
// 查询单个
public async Task<DyUserCookies> GetByIdAsync(string id)
{
return await _cookieRepository.GetByIdAsync(id);
}
// 查询列表(可加条件)
public async Task<(List<DyUserCookies> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
{
return await _cookieRepository.GetPagedAsync(pageIndex, pageSize);
}
// 更新
public async Task<bool> UpdateAsync(DyUserCookies dyUserCookies)
{
return await _cookieRepository.UpdateAsync(dyUserCookies);
}
// 删除(根据主键)
public async Task<bool> DeleteByIdAsync(string id)
{
return await _cookieRepository.DeleteByIdAsync(id);
}
// 批量删除
public async Task<int> DeleteByIdsAsync(IEnumerable<string> ids)
{
return await _cookieRepository.DeleteByIdsAsync(ids.Cast<object>());
}
}
}
-639
View File
@@ -1,639 +0,0 @@
using dy.net.dto;
using dy.net.utils;
using Newtonsoft.Json;
using System;
using System.Net;
using System.Net.Http;
namespace dy.net.service
{
public class DyHttpClientService
{
public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme";
// 随机数生成器(避免重复实例化,保证随机性)
private readonly IHttpClientFactory _clientFactory;
public DyHttpClientService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
/// <summary>
/// 查询用户收藏的视频
/// </summary>
/// <param name="cursor"></param>
/// <param name="count"></param>
/// <param name="cookie"></param>
/// <returns></returns>
public async Task<CollectVideoInfo> SyncCollectVideos(string cursor, string count, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
{
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
}
if (string.IsNullOrWhiteSpace(count))
{
throw new ArgumentException($"“{nameof(count)}”不能为 null 或空。", nameof(count));
}
if (string.IsNullOrWhiteSpace(cookie))
{
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
}
try
{
using var httpClient = _clientFactory.CreateClient("dy_collect");
if (httpClient.DefaultRequestHeaders.Contains("Cookie"))
{
httpClient.DefaultRequestHeaders.Remove("Cookie");
}
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
var dics = DySyncBaseParamDics.CollectParams;
dics["cursor"]=cursor;
dics["count"] = count;
try
{
var token = await TokenManager.GenRealMsTokenAsync();
dics["msToken"] = token;
}
catch (Exception ex)
{
Serilog.Log.Error($"获取mstoken失败{ex.Message}");
}
string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
var endPoint = BogusManager.XbModel2Endpoint($"{DouYinApi}/listcollection", dics, UserAgent);
var respose = await httpClient.PostAsync(endPoint, null);
if (respose.IsSuccessStatusCode)
{
var data = await respose.Content.ReadAsStringAsync();
Serilog.Log.Debug($"SyncCollectVideos API响应长度: {data?.Length ?? 0}");
var result = JsonConvert.DeserializeObject<CollectVideoInfo>(data);
if (result != null)
{
Serilog.Log.Debug($"SyncCollectVideos 解析结果: HasMore={result.HasMore}, AwemeList数量={result.AwemeList?.Count ?? 0}");
}
else
{
var preview = data != null && data.Length > 0 ? data.Substring(0, Math.Min(500, data.Length)) : "空响应";
Serilog.Log.Warning($"SyncCollectVideos 解析失败,响应内容前500字符: {preview}");
}
return result;
}
else
{
var errorContent = await respose.Content.ReadAsStringAsync();
var errorPreview = errorContent != null && errorContent.Length > 0 ? errorContent.Substring(0, Math.Min(500, errorContent.Length)) : "空响应";
Serilog.Log.Error($"SyncCollectVideos fail: {respose.StatusCode}, 响应内容: {errorPreview}");
return null;
}
}
catch (Exception ex)
{
Serilog.Log.Error($"SyncCollectVideos error: {ex.Message}");
return null;
}
}
/// <summary>
/// 查询用户喜欢的视频
/// </summary>
/// <param name="count"></param>
/// <param name="cursor"></param>
/// <param name="secUserId"></param>
/// <param name="cookie"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public async Task<CollectVideoInfo> SyncFavoriteVideos(string count,string cursor, string secUserId, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
{
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
}
if (string.IsNullOrWhiteSpace(secUserId))
{
throw new ArgumentException($"“{nameof(secUserId)}”不能为 null 或空。", nameof(secUserId));
}
if (string.IsNullOrWhiteSpace(cookie))
{
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
}
try
{
using var httpClient = _clientFactory.CreateClient("dy_favorite");
if (httpClient.DefaultRequestHeaders.Contains("Cookie"))
{
httpClient.DefaultRequestHeaders.Remove("Cookie");
}
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
var dics = DySyncBaseParamDics.FavoriteParams;
{
// 添加动态参数
dics["max_cursor"] = cursor;
dics["sec_user_id"] = secUserId;
dics["count"] = count;
}
// 构建请求URL
var queryString = new FormUrlEncodedContent(dics);
string fullUrl = $"{DouYinApi}/favorite?{await queryString.ReadAsStringAsync()}";
Serilog.Log.Debug($"SyncFavoriteVideos 请求URL: {fullUrl.Substring(0, Math.Min(200, fullUrl.Length))}...");
var respose = await httpClient.GetAsync(fullUrl);
if (respose.IsSuccessStatusCode)
{
var data = await respose.Content.ReadAsStringAsync();
Serilog.Log.Debug($"SyncFavoriteVideos API响应长度: {data?.Length ?? 0}");
var result = JsonConvert.DeserializeObject<CollectVideoInfo>(data);
if (result != null)
{
Serilog.Log.Debug($"SyncFavoriteVideos 解析结果: HasMore={result.HasMore}, AwemeList数量={result.AwemeList?.Count ?? 0}");
}
else
{
var preview = data != null && data.Length > 0 ? data.Substring(0, Math.Min(500, data.Length)) : "空响应";
Serilog.Log.Warning($"SyncFavoriteVideos 解析失败,响应内容前500字符: {preview}");
}
return result;
}
else
{
var errorContent = await respose.Content.ReadAsStringAsync();
var errorPreview = errorContent != null && errorContent.Length > 0 ? errorContent.Substring(0, Math.Min(500, errorContent.Length)) : "空响应";
Serilog.Log.Error($"SyncFavoriteVideos fail: {respose.StatusCode}, 响应内容: {errorPreview}");
return null;
}
}
catch (Exception ex)
{
Serilog.Log.Error($"SyncFavoriteVideos error: {ex.Message}");
return null;
}
}
/// <summary>
/// 查询Up主作品
/// </summary>
/// <param name="count"></param>
/// <param name="cursor"></param>
/// <param name="secUserId"></param>
/// <param name="cookie"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public async Task<CollectVideoInfo> SyncUpderPostVideos(string count, string cursor, string secUserId, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
{
throw new ArgumentException($"“{nameof(cursor)}”不能为 null 或空。", nameof(cursor));
}
if (string.IsNullOrWhiteSpace(secUserId))
{
throw new ArgumentException($"“{nameof(secUserId)}”不能为 null 或空。", nameof(secUserId));
}
if (string.IsNullOrWhiteSpace(cookie))
{
throw new ArgumentException($"“{nameof(cookie)}”不能为 null 或空。", nameof(cookie));
}
try
{
using var httpClient = _clientFactory.CreateClient("dy_uper");
if (httpClient.DefaultRequestHeaders.Contains("Cookie"))
{
httpClient.DefaultRequestHeaders.Remove("Cookie");
}
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
var parameters = DySyncBaseParamDics.UpderPostParams;
{
// 添加动态参数
parameters["max_cursor"] = cursor;
parameters["sec_user_id"] = secUserId;
parameters["count"] = count;
}
// 构建请求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);
fullUrl += $"&X-Bogus={a_bogus}";
var respose = await httpClient.GetAsync(fullUrl);
if (respose.IsSuccessStatusCode)
{
var data = await respose.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
}
else
{
Serilog.Log.Error($"SyncUpderPostVideos fail: {respose.StatusCode}");
return null;
}
}
catch (Exception ex)
{
Serilog.Log.Error($"SyncUpderPostVideos error: {ex.Message}");
return null;
}
}
//AI优化2
/// <summary>
/// 下载文件并保存到本地(支持重试机制,优化批量下载稳定性)
/// </summary>
/// <param name="videoUrl">文件地址</param>
/// <param name="savePath">保存路径</param>
/// <param name="cookie">请求Cookie</param>
/// <param name="httpclientName">HttpClient名称(默认"dy_down1")</param>
/// <param name="cancellationToken">取消令牌(用于终止任务)</param>
/// <param name="streamTimeout">流读取超时时间(默认60秒)</param>
/// <param name="maxRetryCount">最大重试次数(默认3次)</param>
/// <param name="initialRetryDelay">初始重试延迟(默认1秒,指数退避)</param>
/// <returns>是否下载成功</returns>
public async Task<bool> DownloadAsync(
string videoUrl,
string savePath,
string cookie,
CancellationToken cancellationToken = default,
TimeSpan? streamTimeout = null,
int maxRetryCount = 3,
TimeSpan? initialRetryDelay = null)
{
// 重试参数初始化
int retryCount = 0;
var retryDelay = initialRetryDelay ?? TimeSpan.FromSeconds(1); // 初始延迟1秒
streamTimeout ??= TimeSpan.FromSeconds(60); // 流读取超时默认60秒
while (true)
{
try
{
// 执行单次下载逻辑(提取为内部方法,避免代码重复)
return await TryDownloadOnceAsync(
videoUrl, savePath, cookie, cancellationToken, streamTimeout.Value);
}
catch (Exception ex) when (IsRetryableException(ex) && retryCount < maxRetryCount)
{
// 可重试异常且未达最大次数,等待后重试
retryCount++;
var delay = TimeSpan.FromMilliseconds(retryDelay.TotalMilliseconds * Math.Pow(2, retryCount - 1)); // 指数退避(1s→2s→4s...)
Serilog.Log.Warning(ex, $"下载失败(第{retryCount}/{maxRetryCount}次重试):{videoUrl},将在{delay.TotalSeconds:F1}秒后重试");
try
{
// 等待重试延迟(响应取消令牌)
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// 等待期间被取消,直接返回失败
Serilog.Log.Information($"重试等待被取消:{videoUrl}");
return false;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// 主动取消,不重试
Serilog.Log.Information($"下载被取消:{videoUrl}");
return false;
}
catch (Exception ex)
{
// 不可重试的异常(如参数错误、权限不足等),直接失败
Serilog.Log.Error(ex, $"下载失败(不可重试):{videoUrl}");
CleanupIncompleteFile(savePath); // 清理不完整文件
return false;
}
}
}
/// <summary>
/// 单次下载尝试(核心下载逻辑)
/// </summary>
private async Task<bool> TryDownloadOnceAsync(
string videoUrl,
string savePath,
string cookie,
CancellationToken cancellationToken,
TimeSpan streamTimeout)
{
DateTime lastStreamActivity = DateTime.UtcNow; // 流活动时间戳
// 确保目录存在
var directory = Path.GetDirectoryName(savePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 清理已存在的文件(避免占用)
CleanupIncompleteFile(savePath);
using (var httpClient = _clientFactory.CreateClient("dy_download"))
{
// 配置请求头
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))
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 > streamTimeout)
{
throw new TimeoutException($"流读取超时({streamTimeout.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;
}
/// <summary>
/// 判断异常是否可重试
/// </summary>
private bool IsRetryableException(Exception ex)
{
// 可重试的异常类型:网络错误、超时、服务器临时错误等
return ex is HttpRequestException // HTTP请求失败(如5xx错误、连接中断)
|| ex is TimeoutException // 超时(包括流超时和请求超时)
|| ex is IOException // IO错误(如临时磁盘问题)
|| (ex is AggregateException aggEx && aggEx.InnerExceptions.Any(IsRetryableException)); // 聚合异常中包含可重试项
}
/// <summary>
/// 清理不完整的文件
/// </summary>
private void CleanupIncompleteFile(string savePath)
{
if (File.Exists(savePath))
{
try
{
File.Delete(savePath);
}
catch (IOException ex)
{
Serilog.Log.Warning(ex, $"清理不完整文件失败:{savePath}(可能被占用)");
}
}
}
//--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;
// }
//}
}
}
+83
View File
@@ -0,0 +1,83 @@
using dy.net.model.dto;
using System.Globalization;
namespace dy.net.service
{
public class LogInfoService
{
public List<LogFileInfoDto> GetLogFiles(string logDirectory)
{
var logFiles = new List<LogFileInfoDto>();
// 验证目录是否存在
if (!Directory.Exists(logDirectory))
{
Serilog.Log.Error("日志目录不存在:{Directory}", logDirectory);
return logFiles;
}
// 获取最近10天的日期范围
var endDate = DateTime.Today;
var startDate = endDate.AddDays(-9);
// 遍历日期范围,查找对应日志文件
for (var date = startDate; date <= endDate; date = date.AddDays(1))
{
var dateStr = date.ToString("yyyyMMdd");
// 查找debug日志
var debugFile = Path.Combine(logDirectory, $"log-debug-{dateStr}.txt");
if (File.Exists(debugFile))
{
logFiles.Add(new LogFileInfoDto
{
Type = "debug",
Date = dateStr,
FileName = Path.GetFileName(debugFile)
});
}
// 查找error日志
var errorFile = Path.Combine(logDirectory, $"log-error-{dateStr}.txt");
if (File.Exists(errorFile))
{
logFiles.Add(new LogFileInfoDto
{
Type = "error",
Date = dateStr,
FileName = Path.GetFileName(errorFile)
});
}
}
return logFiles.OrderByDescending(x => x.Date).ToList();
}
public Stream GetLogFileStream(string logDirectory, string type, string date)
{
// 验证日期格式
if (!DateTime.TryParseExact(date, "yyyyMMdd", CultureInfo.InvariantCulture,
DateTimeStyles.None, out _))
{
throw new ArgumentException("日期格式错误,应为yyyyMMdd");
}
// 验证类型
if (type != "debug" && type != "error")
{
throw new ArgumentException("日志类型只能是debug或error");
}
var filePath = Path.Combine(logDirectory, $"log-{type}-{date}.txt");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("日志文件不存在", filePath);
}
// 返回文件流(使用FileStream确保流可被前端读取)
return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
}
}
-201
View File
@@ -1,201 +0,0 @@
using Quartz;
using dy.net.job;
namespace dy.net.service
{
public class QuartzJobService
{
private readonly ISchedulerFactory _schedulerFactory;
public QuartzJobService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}
/// <summary>
///
/// </summary>
/// <param name="expression"></param>
/// <param name="delay"></param>
/// <returns></returns>
public async Task StartJob(string expression,int delay=5000)
{
await StartCollectJob(expression);
await Task.Delay(delay);
//如果是数字则加1分钟,减少并发
if (int.TryParse(expression, out int cron))
{
cron++;
expression = cron.ToString();
}
await StartFavoriteJob(expression);
await Task.Delay(delay);
if (int.TryParse(expression, out int cron2))
{
cron2++;
expression = cron2.ToString();
}
await StartUperPostJob(expression);
}
private async Task<bool> StartCollectJob(string expression)
{
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 jobDetail = await __scheduler1.GetJobDetail(jobKey);
if (jobDetail != null)
{
//await __scheduler1.Shutdown();
await __scheduler1.DeleteJob(jobKey);//删掉原来的
}
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);
}
catch (Exception ex)
{
Serilog.Log.Error("start dy.collect job error", ex);
return false;
}
return true;
}
private async Task<bool> StartFavoriteJob(string expression)
{
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 jobDetail = await __scheduler1.GetJobDetail(jobKey);
if (jobDetail != null)
{
//await __scheduler1.Shutdown();
await __scheduler1.DeleteJob(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);
}
catch (Exception ex)
{
Serilog.Log.Error("start dy.favorite job error", ex);
return false;
}
return true;
}
private async Task<bool> StartUperPostJob(string expression)
{
try
{
var __scheduler1 = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey("dy.job.key.uper", "group1");
var triggerKey = new TriggerKey("dy.trigger.key.uper", "group1");
var jobDetail = await __scheduler1.GetJobDetail(jobKey);
if (jobDetail != null)
{
//await __scheduler1.Shutdown();
await __scheduler1.DeleteJob(jobKey);//删掉原来的
}
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)
.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;
}
}
}
-44
View File
@@ -1,44 +0,0 @@
using dy.net.dto;
using dy.net.model;
using dy.net.repository;
namespace dy.net.service
{
public class UserService
{
private readonly UserRepository _userRepository;
public UserService(UserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<(int code, string erro)> UpdatePwd(UpdatePwdRequest loginUser)
{
return await _userRepository.UpdatePwd(loginUser);
}
public async Task<LoginUserInfo> GetUser(string userName=null)
{
return await _userRepository.GetUser(userName);
}
public async Task<bool> UpdateAvatar(string avatar)
{
return await _userRepository.UpdateAvatar(avatar);
}
public (int code, string erro) InitUser()
{
LoginUserInfo userInfo = new LoginUserInfo
{
UserName = "douyin",
Password = "douyin2025",
CreateTime = DateTime.Now
};
return _userRepository.InitUser(userInfo);
}
}
}