完成合集、短剧
This commit is contained in:
@@ -11,18 +11,18 @@ namespace dy.net.service
|
||||
public class DouyinCollectCateService
|
||||
{
|
||||
|
||||
private readonly DouyinCollectCateRepository douyinCollectCateRepository;
|
||||
private readonly DouyinCollectCateRepository _douyinCollectCateRepository;
|
||||
|
||||
public DouyinCollectCateService(DouyinCollectCateRepository douyinCollectCateRepository)
|
||||
{
|
||||
douyinCollectCateRepository = douyinCollectCateRepository;
|
||||
_douyinCollectCateRepository = douyinCollectCateRepository;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<bool> AddAsync(DouyinCollectCate cate)
|
||||
{
|
||||
return await douyinCollectCateRepository.InsertAsync(cate);
|
||||
return await _douyinCollectCateRepository.InsertAsync(cate);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task<(List<DouyinCollectCate> list, int totalCount)> GetPagedAsync(DouyinCollectCateRequestDto dto)
|
||||
{
|
||||
return await douyinCollectCateRepository.GetPagedAsync(dto);
|
||||
return await _douyinCollectCateRepository.GetPagedAsync(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,7 +47,7 @@ namespace dy.net.service
|
||||
/// <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);
|
||||
return await _douyinCollectCateRepository.Sync(cates, ckId, cateType);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task<bool> BatchSwitchSync(List<DouyinCollectCateSwitchDto> dto)
|
||||
{
|
||||
return await douyinCollectCateRepository.SwitchBatchAsync(dto);
|
||||
return await _douyinCollectCateRepository.SwitchBatchAsync(dto);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,17 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task<List<DouyinCollectCate>> GetSyncCates(string cookieId, VideoTypeEnum cateType)
|
||||
{
|
||||
return await douyinCollectCateRepository.GetListAsync(x => x.CookieId == cookieId && x.CateType == 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,12 +49,11 @@ namespace dy.net.service
|
||||
Cron = 30,
|
||||
BatchCount = 18,
|
||||
LogKeepDay = 7,
|
||||
UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
|
||||
//UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹
|
||||
UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名
|
||||
DownImageVideo = true,//默认下载图文视频
|
||||
DownMp3 = false,
|
||||
DownImage = false,
|
||||
ImageViedoSaveAlone = true,
|
||||
FollowedTitleTemplate = "",
|
||||
FullFollowedTitleTemplate = "",
|
||||
FollowedTitleSeparator = "",
|
||||
|
||||
@@ -53,14 +53,14 @@ namespace dy.net.service
|
||||
var cookie = new DouyinCookie
|
||||
{
|
||||
UserName = "douyin2026",
|
||||
Cookies = "-",
|
||||
SecUserId = "-",
|
||||
Cookies = "",
|
||||
SecUserId = "",
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Status = 0,
|
||||
SavePath = "/app/collect",
|
||||
FavSavePath = "/app/favorite",
|
||||
UpSavePath = "/app/uper",
|
||||
ImgSavePath="/app/images",
|
||||
//ImgSavePath="/app/images",
|
||||
CollHasSyncd = 0,
|
||||
FavHasSyncd = 0,
|
||||
UperSyncd = 0,
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace dy.net.service
|
||||
requestParameters["cursor"] = cursor; //页码
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
@@ -220,12 +220,13 @@ namespace dy.net.service
|
||||
|
||||
var requestParameters = DouyinRequestParamManager.DouyinFolderCollectParams;
|
||||
{
|
||||
count = "15";
|
||||
requestParameters["cursor"] = cursor;
|
||||
requestParameters["count"] = count;
|
||||
requestParameters["collects_id"] = collectsId;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -278,7 +279,7 @@ namespace dy.net.service
|
||||
requestParameters["cursor"] = cursor; //页码
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
@@ -342,11 +343,11 @@ namespace dy.net.service
|
||||
var requestParameters = DouyinRequestParamManager.DouyinMixVideoParams;
|
||||
{
|
||||
requestParameters["cursor"] = cursor;
|
||||
requestParameters["count"] = count;
|
||||
requestParameters["count"] = "15";
|
||||
requestParameters["mix_id"] = mixId;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -394,11 +395,11 @@ namespace dy.net.service
|
||||
var requestParameters = DouyinRequestParamManager.DouyinSeriesListParams;
|
||||
{
|
||||
// 添加动态参数
|
||||
requestParameters["count"] = "100";
|
||||
requestParameters["count"] = "15";//固定15,多了直接返回参数不合法
|
||||
requestParameters["cursor"] = cursor; //页码
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
@@ -466,7 +467,7 @@ namespace dy.net.service
|
||||
requestParameters["series_id"] = seriesId;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -538,7 +539,7 @@ namespace dy.net.service
|
||||
requestParameters["sec_user_id"] = secUserId;
|
||||
requestParameters["count"] = count;
|
||||
}
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -610,7 +611,7 @@ namespace dy.net.service
|
||||
requestParameters["sec_user_id"] = secUserId;
|
||||
requestParameters["count"] = count;
|
||||
}
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -683,7 +684,7 @@ namespace dy.net.service
|
||||
requestParameters["offset"] = offset;
|
||||
}
|
||||
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Post, requestUrl, requestParameters, refererValue, cookie);
|
||||
var respose = await GetHttpResponseMessage(HttpMethod.Get, requestUrl, requestParameters, refererValue, cookie);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using dy.net.job;
|
||||
using dy.net.model.dto;
|
||||
using dy.net.utils;
|
||||
using Quartz;
|
||||
using Quartz.Impl.Matchers;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.service
|
||||
@@ -19,19 +21,11 @@ namespace dy.net.service
|
||||
private const int DefaultCronStartDelaySeconds = 30;
|
||||
private const int DefaultSimpleStartDelaySeconds = 3;
|
||||
|
||||
// 任务顺序依赖配置(核心:定义执行顺序,供 Listener 使用)
|
||||
public Dictionary<string, string> JobDependency { get; } = new()
|
||||
{
|
||||
{"collect", "favorite"}, // collect 执行完 → 触发 favorite
|
||||
{"favorite", "followed"}, // favorite 执行完 → 触发 followed
|
||||
{"followed", null}, // followed 执行完 → 一轮任务结束
|
||||
};
|
||||
|
||||
// 任务配置信息(保持原有配置不变,改为 public 供 Listener 访问)
|
||||
public Dictionary<string, JobConfig> JobConfigs { get; } = new()
|
||||
// 任务配置信息(修复了series任务的Key重复问题,确保每个任务Key唯一)
|
||||
public static Dictionary<string, JobConfig> JobConfigs { get; } = new()
|
||||
{
|
||||
{
|
||||
"collect",
|
||||
VideoTypeEnum.dy_collects.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinCollectSyncJob),
|
||||
"dy.job.key.collect",
|
||||
@@ -39,7 +33,7 @@ namespace dy.net.service
|
||||
"抖音收藏同步任务")
|
||||
},
|
||||
{
|
||||
"favorite",
|
||||
VideoTypeEnum.dy_favorite.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinFavoritSyncJob),
|
||||
"dy.job.key.favorite",
|
||||
@@ -47,12 +41,12 @@ namespace dy.net.service
|
||||
"抖音点赞同步任务")
|
||||
},
|
||||
{
|
||||
"followed",
|
||||
VideoTypeEnum.dy_follows.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinFollowedViedoSyncJob),
|
||||
typeof(DouyinFollowedSyncJob),
|
||||
"dy.job.key.followed",
|
||||
"dy.trigger.key.followed",
|
||||
"抖音UP主作品同步任务")
|
||||
"抖音关注博主作品同步任务")
|
||||
},
|
||||
{
|
||||
"follow_user",
|
||||
@@ -60,7 +54,31 @@ namespace dy.net.service
|
||||
typeof(DouyinFollowsAndCollnectsSyncJob),
|
||||
"dy.job.key.follow_user",
|
||||
"dy.trigger.key.follow_user",
|
||||
"抖音关注同步任务")
|
||||
"抖音关注列表同步任务")
|
||||
},
|
||||
{
|
||||
VideoTypeEnum.dy_custom_collect.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinCollectCustomSyncJob),
|
||||
"dy.job.key.custom_collect",
|
||||
"dy.trigger.key.custom_collect",
|
||||
"抖音自定义收藏夹列表同步任务")
|
||||
},
|
||||
{
|
||||
VideoTypeEnum.dy_mix.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinMixSyncJob),
|
||||
"dy.job.key.mix",
|
||||
"dy.trigger.key.mix",
|
||||
"抖音收藏夹合集同步任务")
|
||||
},
|
||||
{
|
||||
VideoTypeEnum.dy_series.GetDesc(),
|
||||
new JobConfig(
|
||||
typeof(DouyinSeriesSyncJob),
|
||||
"dy.job.key.series",
|
||||
"dy.trigger.key.series",
|
||||
"抖音收藏夹短剧同步任务")
|
||||
},
|
||||
{
|
||||
"follow_user_once",
|
||||
@@ -78,9 +96,9 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动所有抖音相关定时任务(顺序执行模式)
|
||||
/// 启动所有抖音相关定时任务(所有任务独立执行)
|
||||
/// </summary>
|
||||
/// <param name="expression">Cron表达式或间隔分钟数(控制整个链条的执行频率)</param>
|
||||
/// <param name="expression">Cron表达式或间隔分钟数(所有任务使用相同的执行频率)</param>
|
||||
/// <returns>是否启动成功</returns>
|
||||
public async Task<bool> InitOrReStartAllJobs(string expression)
|
||||
{
|
||||
@@ -94,31 +112,37 @@ namespace dy.net.service
|
||||
{
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
|
||||
// 1. 注册独立的 JobListener(核心:注入配置和服务)
|
||||
await RegisterJobListener(scheduler);
|
||||
|
||||
// 2. 移除所有已存在的任务(避免重复调度)
|
||||
// 移除所有已存在的任务(避免重复调度)
|
||||
await RemoveAllExistingJobs(scheduler);
|
||||
|
||||
// 3. 只启动第一个任务(collect),后续任务由 Listener 自动触发
|
||||
var firstJobConfigKey = "collect";
|
||||
var startSuccess = await StartJobAsync(firstJobConfigKey, expression);
|
||||
// 遍历启动所有任务(独立执行,无顺序依赖)
|
||||
var allJobKeys = JobConfigs.Keys.Where(k => k != "follow_user_once").ToList(); // 排除单次执行的任务
|
||||
foreach (var jobKey in allJobKeys)
|
||||
{
|
||||
if (jobKey == "follow_user")
|
||||
{
|
||||
expression = "60";
|
||||
}
|
||||
var startSuccess = await StartJobAsync(jobKey, expression);
|
||||
if (startSuccess)
|
||||
{
|
||||
Log.Debug($"成功启动任务:{jobKey},执行频率:{expression}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"启动任务失败:{jobKey}");
|
||||
}
|
||||
}
|
||||
|
||||
if (startSuccess)
|
||||
{
|
||||
Log.Debug($"同步任务执行顺序:collect → favorite → followed-->默认每{expression}分钟执行一次...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"任务执行失败(任务 {firstJobConfigKey} 启动失败)");
|
||||
}
|
||||
//启动follow_user--这个与其他几个任务没有依赖关系,所以单独启动
|
||||
await StartJobAsync("follow_user", expression);
|
||||
return startSuccess;
|
||||
Log.Information($"共启动 {allJobKeys.Count} 个定时任务,执行频率:{expression}");
|
||||
|
||||
//await StartJobAsync(VideoTypeEnum.dy_custom_collect.GetDesc(), expression);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "【任务服务】初始化任务链条异常");
|
||||
Log.Error(ex, "【任务服务】初始化所有定时任务异常");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -131,27 +155,6 @@ namespace dy.net.service
|
||||
return await StartOneTimeJobAsync("follow_user_once");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册独立的 JobListener(核心步骤)
|
||||
/// </summary>
|
||||
private async Task RegisterJobListener(IScheduler scheduler)
|
||||
{
|
||||
// 创建独立的 Listener 实例,注入依赖(任务配置、依赖关系、当前服务)
|
||||
var dependencyListener = new DouyinJobDependencyListener(
|
||||
JobConfigs, // 任务配置
|
||||
JobDependency, // 依赖顺序
|
||||
this // 任务服务(用于触发下一个任务)
|
||||
);
|
||||
|
||||
// 注册 Listener:仅监听 DefaultJobGroup 分组的任务(精准匹配,避免影响其他任务)
|
||||
scheduler.ListenerManager.AddJobListener(
|
||||
dependencyListener,
|
||||
GroupMatcher<JobKey>.GroupEquals(DefaultJobGroup)
|
||||
);
|
||||
|
||||
Log.Information("【任务服务】JobListener 注册成功:{ListenerName}", dependencyListener.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除所有已存在的任务(避免重复调度)
|
||||
/// </summary>
|
||||
@@ -169,13 +172,12 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动指定定时任务(public 修饰,供 Listener 调用)
|
||||
/// 启动指定定时任务(独立执行,无依赖触发)
|
||||
/// </summary>
|
||||
/// <param name="configKey">任务配置Key(如:collect、favorite)</param>
|
||||
/// <param name="expression">定时表达式(依赖触发时传空)</param>
|
||||
/// <param name="isDependencyTrigger">是否为依赖触发(true=立即执行,false=定时执行)</param>
|
||||
/// <param name="expression">定时表达式(Cron或间隔分钟数)</param>
|
||||
/// <returns>是否启动成功</returns>
|
||||
public async Task<bool> StartJobAsync(string configKey, string expression, bool isDependencyTrigger = false)
|
||||
public async Task<bool> StartJobAsync(string configKey, string expression)
|
||||
{
|
||||
if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
{
|
||||
@@ -187,33 +189,26 @@ namespace dy.net.service
|
||||
{
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
|
||||
// 触发器Key:区分「定时触发」和「依赖触发」,避免冲突
|
||||
var triggerKey = new TriggerKey(
|
||||
$"{jobConfig.TriggerKey}_{(isDependencyTrigger ? "dependency" : "main")}",
|
||||
DefaultJobGroup
|
||||
);
|
||||
var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup);
|
||||
|
||||
// 移除已存在的任务(防止重复执行)
|
||||
await RemoveExistingJobAsync(scheduler, jobKey);
|
||||
|
||||
// 创建任务详情(添加禁止并发执行特性,避免顺序混乱)
|
||||
// 创建任务详情(保留禁止并发执行,避免同一任务重复运行)
|
||||
var jobDetail = JobBuilder.Create(jobConfig.JobType)
|
||||
.WithIdentity(jobKey)
|
||||
.WithDescription(jobConfig.Description)
|
||||
.DisallowConcurrentExecution() // 关键:禁止同一任务并发执行
|
||||
.DisallowConcurrentExecution() // 禁止同一任务并发执行
|
||||
.Build();
|
||||
|
||||
// 创建立触发器
|
||||
ITrigger trigger = isDependencyTrigger
|
||||
? CreateDependencyTrigger(triggerKey, jobConfig.Description) // 依赖触发:立即执行
|
||||
: CreateScheduledTrigger(triggerKey, expression, jobConfig.Description); // 定时触发:按表达式执行
|
||||
// 创建定时触发器(仅使用定时触发,移除依赖触发逻辑)
|
||||
ITrigger trigger = CreateScheduledTrigger(triggerKey, expression, jobConfig.Description);
|
||||
|
||||
// 调度任务
|
||||
await scheduler.ScheduleJob(jobDetail, trigger);
|
||||
Log.Information("【任务服务】启动任务成功 - 任务描述: {JobDescription}, 触发类型: {TriggerType}, 表达式: {Expression}",
|
||||
Log.Information("【任务服务】启动任务成功 - 任务描述: {JobDescription}, 执行频率: {Expression}",
|
||||
jobConfig.Description,
|
||||
isDependencyTrigger ? "依赖触发(立即执行)" : "定时触发",
|
||||
isDependencyTrigger ? "无" : expression);
|
||||
expression);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -225,7 +220,7 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动单次执行任务(保持原有逻辑不变)
|
||||
/// 启动单次执行任务
|
||||
/// </summary>
|
||||
private async Task<bool> StartOneTimeJobAsync(string configKey)
|
||||
{
|
||||
@@ -268,9 +263,9 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建「定时触发器」(按表达式执行,仅第一个任务使用)
|
||||
/// 创建定时触发器(支持Cron表达式或分钟间隔)
|
||||
/// </summary>
|
||||
private ITrigger CreateScheduledTrigger(TriggerKey triggerKey, string expression, string jobDescription)
|
||||
private static ITrigger CreateScheduledTrigger(TriggerKey triggerKey, string expression, string jobDescription)
|
||||
{
|
||||
// Cron表达式格式
|
||||
if (CronExpression.IsValidExpression(expression))
|
||||
@@ -312,21 +307,9 @@ namespace dy.net.service
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建「依赖触发器」(立即执行,仅执行一次)
|
||||
/// 移除已存在的任务
|
||||
/// </summary>
|
||||
private ITrigger CreateDependencyTrigger(TriggerKey triggerKey, string jobDescription)
|
||||
{
|
||||
return TriggerBuilder.Create()
|
||||
.WithIdentity(triggerKey)
|
||||
.WithDescription($"{jobDescription} - 依赖触发(立即执行)")
|
||||
.StartNow() // 立即触发
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除已存在的任务(保持原有逻辑不变)
|
||||
/// </summary>
|
||||
private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
||||
private static async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
||||
{
|
||||
if (await scheduler.CheckExists(jobKey))
|
||||
{
|
||||
@@ -334,6 +317,5 @@ namespace dy.net.service
|
||||
await scheduler.DeleteJob(jobKey);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
//using dy.net.job;
|
||||
//using dy.net.model.dto;
|
||||
//using Quartz;
|
||||
//using Quartz.Impl.Matchers;
|
||||
//using Serilog;
|
||||
//using System;
|
||||
//using System.Threading.Tasks;
|
||||
|
||||
//namespace dy.net.service
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// 抖音相关定时任务服务
|
||||
// /// </summary>
|
||||
// public class DouyinQuartzJobService
|
||||
// {
|
||||
// private readonly ISchedulerFactory _schedulerFactory;
|
||||
// private const string DefaultJobGroup = "dysync.net";
|
||||
// private const int DefaultIntervalMinutes = 30;
|
||||
// private const int DefaultCronStartDelaySeconds = 30;
|
||||
// private const int DefaultSimpleStartDelaySeconds = 3;
|
||||
|
||||
// // 任务顺序依赖配置(核心:定义执行顺序,供 Listener 使用)
|
||||
// public Dictionary<string, string> JobDependency { get; } = new()
|
||||
// {
|
||||
// {"collect", "favorite"}, // collect 执行完 → 触发 favorite
|
||||
// {"favorite", "followed"}, // favorite 执行完 → 触发 followed
|
||||
// {"followed", null}, // followed 执行完 → 一轮任务结束
|
||||
// };
|
||||
|
||||
// // 任务配置信息(保持原有配置不变,改为 public 供 Listener 访问)
|
||||
// public Dictionary<string, JobConfig> JobConfigs { get; } = new()
|
||||
// {
|
||||
// {
|
||||
// "collect",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinCollectSyncJob),
|
||||
// "dy.job.key.collect",
|
||||
// "dy.trigger.key.collect",
|
||||
// "抖音收藏同步任务")
|
||||
// },
|
||||
// {
|
||||
// "favorite",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFavoritSyncJob),
|
||||
// "dy.job.key.favorite",
|
||||
// "dy.trigger.key.favorite",
|
||||
// "抖音点赞同步任务")
|
||||
// },
|
||||
// {
|
||||
// "followed",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFollowedSyncJob),
|
||||
// "dy.job.key.followed",
|
||||
// "dy.trigger.key.followed",
|
||||
// "抖音关注博主作品同步任务")
|
||||
// },
|
||||
// {
|
||||
// "follow_user",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFollowsAndCollnectsSyncJob),
|
||||
// "dy.job.key.follow_user",
|
||||
// "dy.trigger.key.follow_user",
|
||||
// "抖音关注列表同步任务")
|
||||
// },
|
||||
|
||||
// {
|
||||
// "custom_collect",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinCollectCustomSyncJob),
|
||||
// "dy.job.key.custom_collect",
|
||||
// "dy.trigger.key.custom_collect",
|
||||
// "抖音自定义收藏夹列表同步任务")
|
||||
// },
|
||||
|
||||
// {
|
||||
// "mix",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinMixSyncJob),
|
||||
// "dy.job.key.mix",
|
||||
// "dy.trigger.key.mix",
|
||||
// "抖音收藏夹合集同步任务")
|
||||
// },
|
||||
// {
|
||||
// "series",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinSeriesSyncJob),
|
||||
// "dy.job.key.mix",
|
||||
// "dy.trigger.key.mix",
|
||||
// "抖音收藏夹短剧同步任务")
|
||||
// },
|
||||
// {
|
||||
// "follow_user_once",
|
||||
// new JobConfig(
|
||||
// typeof(DouyinFollowsAndCollnectsSyncJob),
|
||||
// "dy.job.key.follow_user_once",
|
||||
// "dy.trigger.key.follow_user_once",
|
||||
// "抖音关注同步任务(单次执行)")
|
||||
// }
|
||||
// };
|
||||
|
||||
// public DouyinQuartzJobService(ISchedulerFactory schedulerFactory)
|
||||
// {
|
||||
// _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动所有抖音相关定时任务(顺序执行模式)
|
||||
// /// </summary>
|
||||
// /// <param name="expression">Cron表达式或间隔分钟数(控制整个链条的执行频率)</param>
|
||||
// /// <returns>是否启动成功</returns>
|
||||
// public async Task<bool> InitOrReStartAllJobs(string expression)
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(expression))
|
||||
// {
|
||||
// Log.Debug("定时任务表达式为空,使用默认配置({DefaultMinutes}分钟)", DefaultIntervalMinutes);
|
||||
// expression = DefaultIntervalMinutes.ToString();
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// var scheduler = await _schedulerFactory.GetScheduler();
|
||||
|
||||
// // 1. 注册独立的 JobListener(核心:注入配置和服务)
|
||||
// await RegisterJobListener(scheduler);
|
||||
|
||||
// // 2. 移除所有已存在的任务(避免重复调度)
|
||||
// await RemoveAllExistingJobs(scheduler);
|
||||
|
||||
// // 3. 只启动第一个任务(collect),后续任务由 Listener 自动触发
|
||||
// var firstJobConfigKey = "collect";
|
||||
// //var startSuccess = await StartJobAsync(firstJobConfigKey, expression);
|
||||
|
||||
// //if (startSuccess)
|
||||
// //{
|
||||
// // Log.Debug($"同步任务执行顺序:collect → favorite → followed-->默认每{expression}分钟执行一次...");
|
||||
// //}
|
||||
// //else
|
||||
// //{
|
||||
// // Log.Error($"任务执行失败(任务 {firstJobConfigKey} 启动失败)");
|
||||
// //}
|
||||
// //启动follow_user--这个与其他几个任务没有依赖关系,所以单独启动
|
||||
// //await StartJobAsync("collect", expression);
|
||||
// //await StartJobAsync("follow_user", expression);
|
||||
// //await StartJobAsync("follow_user", expression);
|
||||
// //await StartJobAsync("follow_user", expression);
|
||||
// //await StartJobAsync("custom_collect", expression);
|
||||
// //await StartJobAsync("mix", expression);
|
||||
// //await StartJobAsync("series", expression);
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Error(ex, "【任务服务】初始化任务链条异常");
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动关注同步任务(单次执行)
|
||||
// /// </summary>
|
||||
// public async Task<bool> StartFollowJobOnceAsync()
|
||||
// {
|
||||
// return await StartOneTimeJobAsync("follow_user_once");
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 注册独立的 JobListener(核心步骤)
|
||||
// /// </summary>
|
||||
// private async Task RegisterJobListener(IScheduler scheduler)
|
||||
// {
|
||||
// // 创建独立的 Listener 实例,注入依赖(任务配置、依赖关系、当前服务)
|
||||
// var dependencyListener = new DouyinJobDependencyListener(
|
||||
// JobConfigs, // 任务配置
|
||||
// JobDependency, // 依赖顺序
|
||||
// this // 任务服务(用于触发下一个任务)
|
||||
// );
|
||||
|
||||
// // 注册 Listener:仅监听 DefaultJobGroup 分组的任务(精准匹配,避免影响其他任务)
|
||||
// scheduler.ListenerManager.AddJobListener(
|
||||
// dependencyListener,
|
||||
// GroupMatcher<JobKey>.GroupEquals(DefaultJobGroup)
|
||||
// );
|
||||
|
||||
// Log.Information("【任务服务】JobListener 注册成功:{ListenerName}", dependencyListener.Name);
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 移除所有已存在的任务(避免重复调度)
|
||||
// /// </summary>
|
||||
// private 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.Information("【任务服务】移除已存在的任务: {JobKey}", jobKey);
|
||||
// await scheduler.DeleteJob(jobKey);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动指定定时任务(public 修饰,供 Listener 调用)
|
||||
// /// </summary>
|
||||
// /// <param name="configKey">任务配置Key(如:collect、favorite)</param>
|
||||
// /// <param name="expression">定时表达式(依赖触发时传空)</param>
|
||||
// /// <param name="isDependencyTrigger">是否为依赖触发(true=立即执行,false=定时执行)</param>
|
||||
// /// <returns>是否启动成功</returns>
|
||||
// public async Task<bool> StartJobAsync(string configKey, string expression, bool isDependencyTrigger = false)
|
||||
// {
|
||||
// if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
// {
|
||||
// Log.Error("【任务服务】找不到任务配置: {ConfigKey}", configKey);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// var scheduler = await _schedulerFactory.GetScheduler();
|
||||
// var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup);
|
||||
// // 触发器Key:区分「定时触发」和「依赖触发」,避免冲突
|
||||
// var triggerKey = new TriggerKey(
|
||||
// $"{jobConfig.TriggerKey}_{(isDependencyTrigger ? "dependency" : "main")}",
|
||||
// DefaultJobGroup
|
||||
// );
|
||||
|
||||
// // 移除已存在的任务(防止重复执行)
|
||||
// await RemoveExistingJobAsync(scheduler, jobKey);
|
||||
|
||||
// // 创建任务详情(添加禁止并发执行特性,避免顺序混乱)
|
||||
// var jobDetail = JobBuilder.Create(jobConfig.JobType)
|
||||
// .WithIdentity(jobKey)
|
||||
// .WithDescription(jobConfig.Description)
|
||||
// .DisallowConcurrentExecution() // 关键:禁止同一任务并发执行
|
||||
// .Build();
|
||||
|
||||
// // 创建立触发器
|
||||
// ITrigger trigger = isDependencyTrigger
|
||||
// ? CreateDependencyTrigger(triggerKey, jobConfig.Description) // 依赖触发:立即执行
|
||||
// : CreateScheduledTrigger(triggerKey, expression, jobConfig.Description); // 定时触发:按表达式执行
|
||||
|
||||
// // 调度任务
|
||||
// await scheduler.ScheduleJob(jobDetail, trigger);
|
||||
// Log.Information("【任务服务】启动任务成功 - 任务描述: {JobDescription}, 触发类型: {TriggerType}, 表达式: {Expression}",
|
||||
// jobConfig.Description,
|
||||
// isDependencyTrigger ? "依赖触发(立即执行)" : "定时触发",
|
||||
// isDependencyTrigger ? "无" : expression);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Error(ex, "【任务服务】启动任务失败 - 任务描述: {JobDescription}", jobConfig.Description);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 启动单次执行任务(保持原有逻辑不变)
|
||||
// /// </summary>
|
||||
// private async Task<bool> StartOneTimeJobAsync(string configKey)
|
||||
// {
|
||||
// if (!JobConfigs.TryGetValue(configKey, out var jobConfig))
|
||||
// {
|
||||
// Log.Error("【任务服务】找不到任务配置: {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.Information("【任务服务】启动单次任务成功 - 任务描述: {JobDescription}", jobConfig.Description);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Error(ex, "【任务服务】启动单次任务失败 - 任务描述: {JobDescription}", jobConfig.Description);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 创建「定时触发器」(按表达式执行,仅第一个任务使用)
|
||||
// /// </summary>
|
||||
// private 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.Warning("【任务服务】无效的任务表达式: {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 ITrigger CreateDependencyTrigger(TriggerKey triggerKey, string jobDescription)
|
||||
// {
|
||||
// return TriggerBuilder.Create()
|
||||
// .WithIdentity(triggerKey)
|
||||
// .WithDescription($"{jobDescription} - 依赖触发(立即执行)")
|
||||
// .StartNow() // 立即触发
|
||||
// .Build();
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 移除已存在的任务(保持原有逻辑不变)
|
||||
// /// </summary>
|
||||
// private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey)
|
||||
// {
|
||||
// if (await scheduler.CheckExists(jobKey))
|
||||
// {
|
||||
// Log.Information("【任务服务】移除已存在的任务: {JobKey}", jobKey);
|
||||
// await scheduler.DeleteJob(jobKey);
|
||||
// }
|
||||
// }
|
||||
|
||||
//}
|
||||
//}
|
||||
@@ -4,9 +4,11 @@ using dy.net.model.entity;
|
||||
using dy.net.repository;
|
||||
using dy.net.utils;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using SqlSugar;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.service
|
||||
@@ -81,6 +83,7 @@ namespace dy.net.service
|
||||
{
|
||||
existingVideo.VideoSavePath = updateData.VideoSavePath;
|
||||
existingVideo.VideoCoverSavePath = updateData.VideoCoverSavePath;
|
||||
existingVideo.ViedoType = updateData.ViedoType;
|
||||
}
|
||||
}
|
||||
// 批量更新数据库
|
||||
@@ -94,6 +97,10 @@ namespace dy.net.service
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateOne(DouyinVideo video)
|
||||
{
|
||||
return await _dyCollectVideoRepository.UpdateAsync(video);
|
||||
}
|
||||
|
||||
public async Task<VideoStaticsDto> GetStatics()
|
||||
{
|
||||
@@ -110,14 +117,17 @@ namespace dy.net.service
|
||||
VideoCount = list.Count,
|
||||
Categories = Categories,
|
||||
FavoriteCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_favorite),
|
||||
CollectCount = list.Count(x => x.ViedoType == VideoTypeEnum.dy_collects),
|
||||
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).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(),
|
||||
@@ -129,6 +139,28 @@ namespace dy.net.service
|
||||
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,
|
||||
@@ -170,10 +202,10 @@ namespace dy.net.service
|
||||
/// <param name="AuthorId"></param>
|
||||
/// <param name="ViedoNameSimplify"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
||||
public (string, string) GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify)
|
||||
{
|
||||
|
||||
return await _dyCollectVideoRepository.GetUperLastViedoFileName(AuthorId, ViedoNameSimplify);
|
||||
return _dyCollectVideoRepository.GetUperLastViedoFileName(AuthorId, ViedoNameSimplify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -346,7 +378,27 @@ namespace dy.net.service
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetTopsOrderBySyncTime(top);
|
||||
}
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<VideoChartItemDto>> GetChartData()
|
||||
{
|
||||
var list = await _dyCollectVideoRepository.GetListAsync(x => x.SyncTime > DateTime.Now.AddDays(-7));
|
||||
|
||||
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>
|
||||
/// 删除无效记录(记录存在,用户手动把目录下的视频删了的情况,视频记录依然存在)
|
||||
@@ -456,5 +508,148 @@ namespace dy.net.service
|
||||
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user