Merge feature/merge-upstream: Modal 小屏 + 视频播放 + Docker Alpine(冲突以 feature 为准)
部署到群晖 / deploy (push) Failing after 3h9m39s
部署到群晖 / 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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user