1、增加图片视频
2、优化代码结构 3、up主下载分类逻辑可配置
This commit is contained in:
@@ -17,8 +17,8 @@ namespace dy.net.Controllers
|
||||
{
|
||||
private readonly IWebHostEnvironment webHostEnvironment;
|
||||
|
||||
private readonly UserService _userService;
|
||||
public AuthController(UserService userService, IWebHostEnvironment webHostEnvironment )
|
||||
private readonly AdminUserService _userService;
|
||||
public AuthController(AdminUserService userService, IWebHostEnvironment webHostEnvironment )
|
||||
{
|
||||
_userService=userService;
|
||||
this.webHostEnvironment = webHostEnvironment;
|
||||
|
||||
@@ -14,12 +14,12 @@ namespace dy.net.Controllers
|
||||
[ApiController]
|
||||
public class ConfigController : ControllerBase
|
||||
{
|
||||
private readonly DyCookieService dyCookieService;
|
||||
private readonly DouyinCookieService dyCookieService;
|
||||
|
||||
private readonly CommonService commonService;
|
||||
private readonly QuartzJobService quartzJobService;
|
||||
private readonly DouyinCommonService commonService;
|
||||
private readonly DouyinQuartzJobService quartzJobService;
|
||||
|
||||
public ConfigController(DyCookieService dyCookieService, CommonService commonService,QuartzJobService quartzJobService)
|
||||
public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService,DouyinQuartzJobService quartzJobService)
|
||||
{
|
||||
this.dyCookieService = dyCookieService;
|
||||
this.commonService = commonService;
|
||||
@@ -55,7 +55,7 @@ namespace dy.net.Controllers
|
||||
/// 新增用户Cookie
|
||||
/// </summary>
|
||||
[HttpPost("add")]
|
||||
public async Task<IActionResult> AddAsync([FromBody] DyUserCookies dyUserCookies)
|
||||
public async Task<IActionResult> AddAsync([FromBody] DouyinUserCookie dyUserCookies)
|
||||
{
|
||||
if(dyUserCookies.UpSecUserIdsJson!=null)
|
||||
{
|
||||
@@ -74,7 +74,7 @@ namespace dy.net.Controllers
|
||||
/// 更新用户Cookie
|
||||
/// </summary>
|
||||
[HttpPost("update")]
|
||||
public async Task<IActionResult> UpdateAsync([FromBody] DyUserCookies dyUserCookies)
|
||||
public async Task<IActionResult> UpdateAsync([FromBody] DouyinUserCookie dyUserCookies)
|
||||
{
|
||||
|
||||
if (dyUserCookies.UpSecUserIdsJson != null )
|
||||
|
||||
@@ -12,9 +12,9 @@ namespace dy.net.Controllers
|
||||
[ApiController]
|
||||
public class VideoController : ControllerBase
|
||||
{
|
||||
private readonly DyCollectVideoService dyCollectVideoService;
|
||||
private readonly DouyinVideoService dyCollectVideoService;
|
||||
|
||||
public VideoController(DyCollectVideoService dyCollectVideoService)
|
||||
public VideoController(DouyinVideoService dyCollectVideoService)
|
||||
{
|
||||
this.dyCollectVideoService = dyCollectVideoService;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace dy.net.Controllers
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
[HttpPost("paged")]
|
||||
public async Task<IActionResult> GetPagedAsync(VideoPageRequestDTO dto)
|
||||
public async Task<IActionResult> GetPagedAsync(DouyinVideoPageRequestDto dto)
|
||||
{
|
||||
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Author,dto.ViedoType,dto.Dates);
|
||||
return Ok(new
|
||||
|
||||
+15
-3
@@ -2,9 +2,21 @@
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 10101
|
||||
EXPOSE 10102
|
||||
|
||||
|
||||
RUN echo "deb http://mirrors.aliyun.com/debian/ bookworm main non-free contrib" > /etc/apt/sources.list && \
|
||||
echo "deb http://mirrors.aliyun.com/debian-security/ bookworm-security main" >> /etc/apt/sources.list && \
|
||||
echo "deb http://mirrors.aliyun.com/debian/ bookworm-updates main non-free contrib" >> /etc/apt/sources.list && \
|
||||
echo "deb http://mirrors.aliyun.com/debian/ bookworm-backports main non-free contrib" >> /etc/apt/sources.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends ffmpeg && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN ffmpeg -version
|
||||
|
||||
COPY . .
|
||||
ENV ASPNETCORE_URLS http://*:10101
|
||||
ENTRYPOINT ["dotnet", "dy.net.dll"]
|
||||
ENV ASPNETCORE_URLS=http://*:10102
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV DOWN_IMGVIDEO=1
|
||||
ENTRYPOINT ["dotnet", "dy.net.dll"]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 10101
|
||||
|
||||
COPY . .
|
||||
ENV ASPNETCORE_URLS http://*:10101
|
||||
ENTRYPOINT ["dotnet", "dy.net.dll"]
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV DOWN_IMGVIDEO=1
|
||||
+41
-19
@@ -17,38 +17,52 @@ namespace dy.net
|
||||
public class Program
|
||||
{
|
||||
// 常量定义
|
||||
private const string DefaultListenUrl = "http://*:10101";
|
||||
private static string DefaultListenUrl = "http://*:10102";
|
||||
private const string SpaRootPath = "app/dist";
|
||||
private const string SpaSourcePath = "app/";
|
||||
private const string SwaggerDocTitle = "dy.net WebApi Docs";
|
||||
|
||||
/// <summary>
|
||||
/// 打包时注意,如果是false,前端不允许修改开启下载图片和视频的选项
|
||||
/// </summary>
|
||||
private static bool downImageVideo = false;
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// 初始化编码提供器
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
// 构建Web应用
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
//from docker yaml file 环境变量 或者 dockerfile 或appsettings.json
|
||||
DefaultListenUrl = builder.Configuration.GetValue<string>("ASPNETCORE_URLS") ?? DefaultListenUrl;
|
||||
var downImgConfig = builder.Configuration.GetValue<string>("DOWN_IMGVIDEO");
|
||||
|
||||
Console.WriteLine("DOWN_IMGVIDEO=" + downImgConfig);
|
||||
if (!string.IsNullOrEmpty(downImgConfig))
|
||||
{
|
||||
downImgConfig = downImgConfig.ToLower();
|
||||
downImageVideo = downImgConfig == "1" || downImgConfig == "y"||downImgConfig=="t"||downImgConfig=="true";
|
||||
}
|
||||
var isDevelopment = builder.Environment.IsDevelopment();
|
||||
|
||||
// 配置主机
|
||||
ConfigureHost(builder, isDevelopment);
|
||||
|
||||
// 配置服务
|
||||
var services = builder.Services;
|
||||
ConfigureServices(services, builder.Configuration, isDevelopment);
|
||||
ConfigureServices(builder.Services, builder.Configuration, builder.Environment);
|
||||
|
||||
// 构建应用
|
||||
var app = builder.Build();
|
||||
|
||||
Log.Debug("DOWN_IMGVIDEO=" + downImgConfig);
|
||||
|
||||
// 配置中间件
|
||||
ConfigureMiddleware(app, isDevelopment);
|
||||
ConfigureMiddleware(app, builder.Environment);
|
||||
|
||||
// 初始化应用服务
|
||||
InitApplicationServices(app);
|
||||
|
||||
// 启动应用
|
||||
Serilog.Log.Debug("dy.net service started successfully");
|
||||
Log.Debug("dysync.net service started successfully");
|
||||
app.Run();
|
||||
}
|
||||
|
||||
@@ -78,8 +92,10 @@ namespace dy.net
|
||||
/// <summary>
|
||||
/// 配置依赖注入服务
|
||||
/// </summary>
|
||||
private static void ConfigureServices(IServiceCollection services, IConfiguration config, bool isDevelopment)
|
||||
private static void ConfigureServices(IServiceCollection services, IConfiguration config, IWebHostEnvironment environment)
|
||||
{
|
||||
|
||||
services.AddSingleton(new Appsettings (config));
|
||||
// 雪花ID生成器
|
||||
services.AddSnowFlakeId(options => options.WorkId = new Random().Next(1, 127));
|
||||
|
||||
@@ -93,17 +109,23 @@ namespace dy.net
|
||||
services.AddSqlsugar(config);
|
||||
|
||||
// 定时任务
|
||||
services.AddQuartzService(config);
|
||||
services.AddQuartzService();
|
||||
|
||||
// 仓储和服务注册
|
||||
services.AddServicesFromNamespace("dy.net.repository")
|
||||
.AddServicesFromNamespace("dy.net.service");
|
||||
|
||||
//下载图片合成视频-需要ffmpeg支持,镜像会很大。
|
||||
if (downImageVideo)
|
||||
{
|
||||
//根据配置动态加载dy.image程序集
|
||||
Assembly assembly = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "dy.image.dll"));
|
||||
services.AddServicesFromNamespace("dy.image", assembly);
|
||||
}
|
||||
// SPA静态文件支持
|
||||
services.AddSpaStaticFiles(options => options.RootPath = SpaRootPath);
|
||||
|
||||
// 开发环境启用Swagger
|
||||
if (isDevelopment)
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
services.AddSwagger();
|
||||
}
|
||||
@@ -120,13 +142,13 @@ namespace dy.net
|
||||
/// <summary>
|
||||
/// 配置中间件
|
||||
/// </summary>
|
||||
private static void ConfigureMiddleware(WebApplication app, bool isDevelopment)
|
||||
private static void ConfigureMiddleware(WebApplication app, IWebHostEnvironment environment)
|
||||
{
|
||||
// 响应压缩
|
||||
app.UseResponseCompression();
|
||||
|
||||
// 开发环境启用SwaggerUI
|
||||
if (isDevelopment)
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
app.UseCustomSwaggerUI(options => options.Title = SwaggerDocTitle);
|
||||
}
|
||||
@@ -145,7 +167,7 @@ namespace dy.net
|
||||
app.MapControllers();
|
||||
|
||||
// 生产环境启用SPA
|
||||
if (!isDevelopment)
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
app.UseSpaStaticFiles();
|
||||
app.UseSpa(spa => spa.Options.SourcePath = SpaSourcePath);
|
||||
@@ -163,23 +185,23 @@ namespace dy.net
|
||||
try
|
||||
{
|
||||
// 初始化用户
|
||||
var userService = services.GetRequiredService<UserService>();
|
||||
var userService = services.GetRequiredService<AdminUserService>();
|
||||
userService.InitUser();
|
||||
|
||||
// 初始化Cookie
|
||||
var cookieService = services.GetRequiredService<DyCookieService>();
|
||||
cookieService.Init();
|
||||
var cookieService = services.GetRequiredService<DouyinCookieService>();
|
||||
cookieService.InitCookie();
|
||||
|
||||
// 初始化配置
|
||||
var commonService = services.GetRequiredService<CommonService>();
|
||||
var config = commonService.InitConfig();
|
||||
var commonService = services.GetRequiredService<DouyinCommonService>();
|
||||
var config = commonService.InitConfig(downImageVideo);
|
||||
|
||||
// 更新收藏视频类型--兼容老版本-原来的旧数据没有这个类型字段
|
||||
commonService.UpdateCollectViedoType();
|
||||
// 重置博主作品同步状态为未同步
|
||||
commonService.UpdateAllCookieSyncedToZero();
|
||||
// 启动定时任务
|
||||
var quartzJobService = services.GetRequiredService<QuartzJobService>();
|
||||
var quartzJobService = services.GetRequiredService<DouyinQuartzJobService>();
|
||||
quartzJobService.StartJob(config?.Cron ?? "30");
|
||||
|
||||
Serilog.Log.Debug("系统初始化完成,会默认将-博主作品同步功能-同步全部作品重置为关闭(若要开启,可以到抖音授权页面中修改)");
|
||||
|
||||
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
||||
<History>True|2025-11-12T15:06:38.7834109Z||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;False|2024-01-02T22:24:42.6577609+08:00||;True|2024-01-02T22:04:33.4708889+08:00||;True|2024-01-02T21:49:57.9249898+08:00||;True|2023-12-28T22:42:58.5976180+08:00||;True|2023-12-28T22:24:09.2051205+08:00||;True|2023-12-28T22:05:48.4865497+08:00||;True|2023-12-28T21:50:57.4549336+08:00||;True|2023-12-28T21:45:30.4557939+08:00||;True|2023-12-28T21:19:59.3614594+08:00||;False|2023-12-28T21:17:04.3069097+08:00||;False|2023-12-28T21:15:56.7788810+08:00||;False|2023-12-28T21:14:57.3617402+08:00||;False|2023-12-28T21:10:45.4382469+08:00||;True|2023-12-28T19:45:33.1431914+08:00||;True|2023-12-26T11:30:36.6418716+08:00||;True|2023-12-26T11:17:40.6433651+08:00||;True|2023-12-26T09:13:56.0741559+08:00||;True|2023-12-26T09:10:55.5358737+08:00||;True|2023-12-25T23:15:34.6534359+08:00||;True|2023-12-25T23:09:47.1566176+08:00||;False|2023-12-25T23:07:32.1118803+08:00||;True|2023-12-25T22:59:50.4643459+08:00||;True|2023-12-25T22:50:14.3584798+08:00||;False|2023-12-25T22:49:20.0279513+08:00||;True|2023-12-25T22:44:07.4183973+08:00||;True|2023-12-25T22:35:27.3847085+08:00||;True|2023-12-25T22:23:21.3542658+08:00||;True|2023-12-25T22:12:00.5219522+08:00||;</History>
|
||||
<History>True|2025-11-22T15:18:56.3202345Z||;True|2025-11-22T22:52:51.7203302+08:00||;True|2025-11-22T22:52:42.9620946+08:00||;True|2025-11-22T22:52:09.8257640+08:00||;True|2025-11-22T22:39:31.5894141+08:00||;True|2025-11-22T22:31:11.0704815+08:00||;True|2025-11-22T22:20:36.4579131+08:00||;True|2025-11-22T22:19:10.2281364+08:00||;True|2025-11-22T19:34:45.9336901+08:00||;True|2025-11-12T23:06:38.7834109+08:00||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;False|2024-01-02T22:24:42.6577609+08:00||;True|2024-01-02T22:04:33.4708889+08:00||;True|2024-01-02T21:49:57.9249898+08:00||;True|2023-12-28T22:42:58.5976180+08:00||;True|2023-12-28T22:24:09.2051205+08:00||;True|2023-12-28T22:05:48.4865497+08:00||;True|2023-12-28T21:50:57.4549336+08:00||;True|2023-12-28T21:45:30.4557939+08:00||;True|2023-12-28T21:19:59.3614594+08:00||;False|2023-12-28T21:17:04.3069097+08:00||;False|2023-12-28T21:15:56.7788810+08:00||;False|2023-12-28T21:14:57.3617402+08:00||;False|2023-12-28T21:10:45.4382469+08:00||;True|2023-12-28T19:45:33.1431914+08:00||;True|2023-12-26T11:30:36.6418716+08:00||;True|2023-12-26T11:17:40.6433651+08:00||;True|2023-12-26T09:13:56.0741559+08:00||;True|2023-12-26T09:10:55.5358737+08:00||;True|2023-12-25T23:15:34.6534359+08:00||;</History>
|
||||
<LastFailureDetails />
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Binary file not shown.
+2
-1
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"dbconn": "",
|
||||
"dbtype": "Sqlite"
|
||||
"dbtype": "Sqlite",
|
||||
"DOWN_IMGVIDEO": "1",
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace dy.net.dto
|
||||
{
|
||||
public class DyUpSecUserIdDto
|
||||
public class DouyinUpSecUserIdDto
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
@@ -4,7 +4,7 @@ namespace dy.net.dto
|
||||
{
|
||||
|
||||
|
||||
public class CollectVideoInfo
|
||||
public class DouyinVideoInfo
|
||||
{
|
||||
[JsonProperty("aweme_list")]
|
||||
public List<Aweme> AwemeList { get; set; }
|
||||
@@ -1267,7 +1267,8 @@ namespace dy.net.dto
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public List<string> url_list { get; set; }
|
||||
[JsonProperty("url_list")]
|
||||
public List<string> UrlList { get; set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -1,6 +1,14 @@
|
||||
namespace dy.net.dto
|
||||
{
|
||||
public class VideoNfo
|
||||
|
||||
// Actor.cs
|
||||
public class Actor
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Role { get; set; } // 角色名称
|
||||
public string Thumb { get; set; } // 演员头像 URL 或路径
|
||||
}
|
||||
public class DouyinVideoNfo
|
||||
{
|
||||
|
||||
/// 视频名称
|
||||
@@ -25,5 +33,7 @@
|
||||
// 新增的属性
|
||||
public DateTime? ReleaseDate { get; set; } // 可空,避免无发布时间时的默认值
|
||||
public IEnumerable<string> Genres { get; set; } // 分类标签集合(如 ["动作", "科幻"])
|
||||
|
||||
public List<Actor> Actors { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace dy.net.dto
|
||||
{
|
||||
public class VideoPageRequestDTO: PageRequestDto
|
||||
public class DouyinVideoPageRequestDto : PageRequestDto
|
||||
{
|
||||
// 3. 引用类型(string)如果允许为null,显式声明为 string?
|
||||
public string? Tag { get; set; }
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace dy.net.dto
|
||||
{
|
||||
public class CollectVideoRequestDto
|
||||
public class DouyinVideoRequestDto
|
||||
{
|
||||
public int cursor { get; set; }
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
namespace dy.net.dto
|
||||
{
|
||||
public class VideoNFOInfo
|
||||
{
|
||||
|
||||
// 基本信息
|
||||
/// <summary>
|
||||
/// 视频标题(通常是本地化标题)
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始标题(通常是影片的原名,如外语片的原名)
|
||||
/// </summary>
|
||||
public string OriginalTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序标题,用于媒体库排序时使用
|
||||
/// </summary>
|
||||
public string SortTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发行年份
|
||||
/// </summary>
|
||||
public int Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详细剧情简介
|
||||
/// </summary>
|
||||
public string Plot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剧情大纲(比Plot更简短的描述)
|
||||
/// </summary>
|
||||
public string Outline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 宣传语、标语(影片的简短宣传句子)
|
||||
/// </summary>
|
||||
public string Tagline { get; set; }
|
||||
|
||||
// 人员信息
|
||||
/// <summary>
|
||||
/// 导演姓名
|
||||
/// </summary>
|
||||
public string Director { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 演员列表
|
||||
/// </summary>
|
||||
public List<string> Actors { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 编剧列表
|
||||
/// </summary>
|
||||
public List<string> Writers { get; set; } = new List<string>();
|
||||
|
||||
// 媒体信息
|
||||
/// <summary>
|
||||
/// 类型(如动作、喜剧、科幻等,多个类型用逗号分隔)
|
||||
/// </summary>
|
||||
public string Genre { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评分(通常是10分制)
|
||||
/// </summary>
|
||||
public double Rating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评分人数
|
||||
/// </summary>
|
||||
public int Votes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 制作公司
|
||||
/// </summary>
|
||||
public string Studio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 首映日期
|
||||
/// </summary>
|
||||
public DateTime? Premiered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 片长(通常以分钟为单位,如"120分钟")
|
||||
/// </summary>
|
||||
public string Runtime { get; set; }
|
||||
|
||||
// 文件信息
|
||||
/// <summary>
|
||||
/// 文件名及路径
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小(以字节为单位)
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
|
||||
// 可根据需要添加更多字段
|
||||
/// <summary>
|
||||
/// 国家/地区(制作国家或地区)
|
||||
/// </summary>
|
||||
public string Country { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言(影片使用的语言)
|
||||
/// </summary>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视频编码格式(如H.264, H.265等)
|
||||
/// </summary>
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 音频编码格式(如AC3, DTS等)
|
||||
/// </summary>
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率(如1920x1080, 3840x2160等)
|
||||
/// </summary>
|
||||
public string Resolution { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace dy.net.dto
|
||||
{
|
||||
public enum VideoTypeEnum
|
||||
{
|
||||
|
||||
Favorite = 1,
|
||||
Collect = 2,
|
||||
UperPost = 3,
|
||||
ImageVideo = 4
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -23,12 +23,16 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="app\**" />
|
||||
<Compile Remove="expand\**" />
|
||||
<Compile Remove="logs\**" />
|
||||
<Content Remove="app\**" />
|
||||
<Content Remove="expand\**" />
|
||||
<Content Remove="logs\**" />
|
||||
<EmbeddedResource Remove="app\**" />
|
||||
<EmbeddedResource Remove="expand\**" />
|
||||
<EmbeddedResource Remove="logs\**" />
|
||||
<None Remove="app\**" />
|
||||
<None Remove="expand\**" />
|
||||
<None Remove="logs\**" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -68,14 +72,21 @@
|
||||
<None Include="dy.net.sln" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="expand\dy.image\dy.image.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="db\dy.sqlite">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Dockerfile">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Dockerfile-arm">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Dockerfile">
|
||||
<None Update="Dockerfile-dev">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
+12
-1
@@ -1,10 +1,14 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.0.11010.61 d18.0
|
||||
VisualStudioVersion = 18.0.11010.61
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dy.net", "dy.net.csproj", "{680660EF-ACAE-43A9-AB6C-B75532E758AD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dy.expand", "dy.expand", "{1503FF41-A045-48DF-B92C-DB81BAAAFE5B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dy.image", "expand\dy.image\dy.image.csproj", "{38404DA7-A852-4960-9E95-8F70459AEB60}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -15,10 +19,17 @@ Global
|
||||
{680660EF-ACAE-43A9-AB6C-B75532E758AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{680660EF-ACAE-43A9-AB6C-B75532E758AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{680660EF-ACAE-43A9-AB6C-B75532E758AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{38404DA7-A852-4960-9E95-8F70459AEB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{38404DA7-A852-4960-9E95-8F70459AEB60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{38404DA7-A852-4960-9E95-8F70459AEB60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{38404DA7-A852-4960-9E95-8F70459AEB60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{38404DA7-A852-4960-9E95-8F70459AEB60} = {1503FF41-A045-48DF-B92C-DB81BAAAFE5B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8238199F-0C28-42F2-ACFD-E8CBCCB2AB80}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace dy.extension
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.image
|
||||
{
|
||||
|
||||
public class DownloadHelper
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public DownloadHelper(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(60); // 下载超时30秒
|
||||
_httpClient.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
|
||||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36");
|
||||
_httpClient.DefaultRequestHeaders.Add("Referer", "https://www.douyin.com");
|
||||
}
|
||||
|
||||
/// <summary>下载网络文件到指定路径</summary>
|
||||
public async Task DownloadFileAsync(string url, string savePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) throw new ArgumentNullException(nameof(url));
|
||||
if (string.IsNullOrEmpty(savePath)) throw new ArgumentNullException(nameof(savePath));
|
||||
|
||||
// 创建目录(如果不存在)
|
||||
var directory = Path.GetDirectoryName(savePath)!;
|
||||
if (!Directory.Exists(directory)) Directory.CreateDirectory(directory);
|
||||
|
||||
// 下载文件
|
||||
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode(); // 非2xx状态码抛出异常
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
using var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
||||
await stream.CopyToAsync(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.image
|
||||
{
|
||||
public class FFmpegHelper : IDisposable
|
||||
{
|
||||
/// 测试环境windows
|
||||
private readonly string _ffmpegExecutablePath = "E:\\down\\ffmpeg\\bin\\ffmpeg";
|
||||
private readonly string _ffprobeExecutablePath = "E:\\down\\ffmpeg\\bin\\ffprobe";
|
||||
// Docker环境linux
|
||||
//private readonly string _ffmpegExecutablePath = "ffmpeg";
|
||||
//private readonly string _ffprobeExecutablePath = "ffprobe";
|
||||
private Process _ffmpegProcess;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
// 视频参数
|
||||
public int VideoWidth { get; set; } = 1080;
|
||||
public int VideoHeight { get; set; } = 1920;
|
||||
public int OutputFrameRate { get; set; } = 30;
|
||||
public int ImageDisplayDurationSeconds { get; set; } = 2;
|
||||
|
||||
// 编码参数
|
||||
public string VideoCodec { get; set; } = "libx264";
|
||||
public string VideoPreset { get; set; } = "medium";
|
||||
public int VideoCrf { get; set; } = 23;
|
||||
public string AudioCodec { get; set; } = "aac";
|
||||
public string AudioBitrate { get; set; } = "192k";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将多张图片和一个音频文件合成为视频(最终终极版)。
|
||||
/// </summary>
|
||||
public async Task<string> CreateVideoFromImagesAndAudioAsync(
|
||||
IEnumerable<string> imageFilePaths,
|
||||
string audioFilePath,
|
||||
string outputVideoPath,
|
||||
IProgress<double> progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 输入验证
|
||||
if (imageFilePaths == null || !imageFilePaths.Any())
|
||||
throw new ArgumentException("图片路径列表不能为空。", nameof(imageFilePaths));
|
||||
|
||||
if (string.IsNullOrEmpty(audioFilePath) || !File.Exists(audioFilePath))
|
||||
throw new FileNotFoundException("音频文件未找到。", audioFilePath);
|
||||
|
||||
if (string.IsNullOrEmpty(outputVideoPath))
|
||||
throw new ArgumentNullException(nameof(outputVideoPath));
|
||||
|
||||
foreach (var imagePath in imageFilePaths)
|
||||
{
|
||||
if (!File.Exists(imagePath))
|
||||
throw new FileNotFoundException("图片文件未找到。", imagePath);
|
||||
}
|
||||
|
||||
var outputDirectory = Path.GetDirectoryName(outputVideoPath);
|
||||
if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
// 关键步骤 1: 创建临时目录并生成有序图片序列
|
||||
string tempImageDir = Path.Combine(AppContext.BaseDirectory, "temp", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempImageDir);
|
||||
|
||||
var imageList = imageFilePaths.ToList();
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < imageList.Count; i++)
|
||||
{
|
||||
string sourcePath = imageList[i];
|
||||
// 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png
|
||||
string extension = Path.GetExtension(sourcePath);
|
||||
string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0
|
||||
string destPath = Path.Combine(tempImageDir, destFileName);
|
||||
File.Copy(sourcePath, destPath);
|
||||
}
|
||||
|
||||
// 关键步骤 2: 构建符合你成功经验的 FFmpeg 命令
|
||||
string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList[0]));
|
||||
double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2); ; // 例如 1/3 = 0.333... fps
|
||||
|
||||
// 音频滤镜:如果音频比图片长则截断,比图片短则循环
|
||||
string audioFilter = await GetAudioFilterAsync(audioFilePath, imageList.Count * ImageDisplayDurationSeconds);
|
||||
|
||||
// 构建 FFmpeg 参数列表
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"-y", // 覆盖输出文件
|
||||
// --- 输入图片序列的配置 ---
|
||||
"-f", "image2", // 明确指定输入为图片序列
|
||||
"-vcodec", "webp", // 强制使用 WebP 解码器
|
||||
"-r", imageFps.ToString(CultureInfo.InvariantCulture), // 设置图片播放速度
|
||||
$"-i", $"\"{imageSequencePattern}\"", // 图片序列的路径模式
|
||||
// --- 输入音频的配置 ---
|
||||
audioFilter, // 应用音频滤镜(可能为空)
|
||||
$"-i", $"\"{audioFilePath}\"", // 音频文件路径
|
||||
// --- 视频编码配置 ---
|
||||
"-c:v", VideoCodec, // 视频编码器 (如 libx264)
|
||||
"-preset", VideoPreset, // 编码预设 (如 medium)
|
||||
$"-crf", $"{VideoCrf}", // 视频质量因子
|
||||
$"-s", $"{VideoWidth}x{VideoHeight}", // 输出视频分辨率
|
||||
"-pix_fmt", "yuv420p", // 像素格式,确保兼容性
|
||||
// --- 音频编码配置 ---
|
||||
"-c:a", AudioCodec, // 音频编码器 (如 aac)
|
||||
$"-b:a", $"{AudioBitrate}", // 音频比特率 (如 192k)
|
||||
// --- 输出文件 ---
|
||||
$"\"{outputVideoPath}\"" // 最终输出视频路径
|
||||
};
|
||||
|
||||
string args = string.Join(" ", arguments);
|
||||
Console.WriteLine($"执行FFmpeg命令: {_ffmpegExecutablePath} {args}");
|
||||
|
||||
// 执行命令
|
||||
await ExecuteFFmpegAsync(args, progress, cancellationToken);
|
||||
|
||||
if (File.Exists(outputVideoPath))
|
||||
{
|
||||
return outputVideoPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("视频合成失败,未生成输出文件。");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 关键步骤 3: 清理临时文件
|
||||
if (Directory.Exists(tempImageDir))
|
||||
{
|
||||
Directory.Delete(tempImageDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成音频滤镜,用于循环或截断音频
|
||||
/// </summary>
|
||||
private async Task<string> GetAudioFilterAsync(string audioFilePath, double imageTotalDurationSeconds)
|
||||
{
|
||||
// 使用 ffprobe 获取音频时长
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _ffprobeExecutablePath,
|
||||
Arguments = $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"{audioFilePath}\"",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = System.Text.Encoding.UTF8
|
||||
};
|
||||
|
||||
using (var process = new Process { StartInfo = startInfo })
|
||||
{
|
||||
process.Start();
|
||||
string output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (double.TryParse(output, out double audioDurationSeconds))
|
||||
{
|
||||
Console.WriteLine($"音频时长: {audioDurationSeconds:F2}s, 图片总时长: {imageTotalDurationSeconds:F2}s");
|
||||
if (audioDurationSeconds < imageTotalDurationSeconds)
|
||||
{
|
||||
// 音频较短,需要循环
|
||||
double loopCount = Math.Ceiling(imageTotalDurationSeconds / audioDurationSeconds);
|
||||
return $"-filter_complex \"[1:a]loop={loopCount - 1}:size={Math.Round(audioDurationSeconds * 44100)}:start=0[a]\" -map \"[a]\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"获取音频时长失败,将不使用音频滤镜: {ex.Message}");
|
||||
}
|
||||
|
||||
// 音频较长或获取时长失败,不使用滤镜(默认截断)
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行FFmpeg命令
|
||||
/// </summary>
|
||||
private async Task ExecuteFFmpegAsync(string arguments, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
|
||||
{
|
||||
throw new InvalidOperationException("已有一个FFmpeg进程正在运行。");
|
||||
}
|
||||
|
||||
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _ffmpegExecutablePath,
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = System.Text.Encoding.UTF8,
|
||||
StandardErrorEncoding = System.Text.Encoding.UTF8
|
||||
};
|
||||
|
||||
_ffmpegProcess = new Process { StartInfo = startInfo };
|
||||
|
||||
_ffmpegProcess.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Data)) return;
|
||||
Console.WriteLine($"FFmpeg: {e.Data}");
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_ffmpegProcess.Start();
|
||||
_ffmpegProcess.BeginErrorReadLine();
|
||||
|
||||
using (_cancellationTokenSource.Token.Register(() =>
|
||||
{
|
||||
if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
|
||||
{
|
||||
try { _ffmpegProcess.Kill(); } catch { }
|
||||
}
|
||||
}))
|
||||
{
|
||||
await _ffmpegProcess.WaitForExitAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
if (_cancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
throw new OperationCanceledException("FFmpeg进程被用户取消。", _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
if (_ffmpegProcess.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"FFmpeg执行失败,退出码: {_ffmpegProcess.ExitCode}。请查看控制台输出获取详细错误信息。");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ffmpegProcess?.Dispose();
|
||||
_ffmpegProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_ffmpegProcess?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
namespace dy.image
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 合并多张图片+音频为视频
|
||||
/// </summary>
|
||||
public class ImageMergeToVideoService
|
||||
{
|
||||
private readonly DownloadHelper _downloadHelper;
|
||||
private readonly FFmpegHelper _fFmpegHelper;
|
||||
public ImageMergeToVideoService(DownloadHelper downloadHelper, FFmpegHelper fFmpegHelper)
|
||||
{
|
||||
_downloadHelper = downloadHelper;
|
||||
_fFmpegHelper = fFmpegHelper;
|
||||
}
|
||||
public async Task<bool> MergeToVideo(string rootPath, MediaMergeRequest request,string outputVideoPath,string fileNamefolder)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
// 创建唯一临时目录(避免并发冲突)
|
||||
var tempDir = Path.Combine(rootPath, "temp", Guid.NewGuid().ToString());
|
||||
try
|
||||
{
|
||||
// 1. 下载图片
|
||||
var (rawImages, error) = await DownloadMediaAsync(request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp");
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Serilog.Log.Error($"{error}");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < rawImages.Count(); i++)
|
||||
{
|
||||
string sourcePath = rawImages[i];
|
||||
// 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png
|
||||
string extension = Path.GetExtension(sourcePath);
|
||||
string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0
|
||||
string destPath = Path.Combine(fileNamefolder, destFileName);
|
||||
File.Copy(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 下载音频
|
||||
var (rawAudios, audioError) = await DownloadMediaAsync(request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3");
|
||||
if (!string.IsNullOrEmpty(audioError))
|
||||
{
|
||||
Serilog.Log.Error($"{audioError}");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < rawAudios.Count(); i++)
|
||||
{
|
||||
string sourcePath = rawAudios[i];
|
||||
// 重命名为有规律的文件名,如 temp_001.mp3, temp_002.mp3
|
||||
string extension = Path.GetExtension(sourcePath);
|
||||
string destFileName = $"temp_{i + 1:D3}{extension}"; // D3 确保是3位数字,不足补0
|
||||
string destPath = Path.Combine(fileNamefolder, destFileName);
|
||||
File.Copy(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. 合成视频
|
||||
//var outputVideoPath = Path.Combine(tempDir, "output", $"merged-video.{request.OutputFormat.ToLower()}");
|
||||
|
||||
// 2. 创建帮助类实例
|
||||
// 在Docker容器内,FFmpeg通常在PATH中,所以直接用 "ffmpeg" 即可
|
||||
|
||||
// 根据图片数量调整每张图片显示时长
|
||||
if (request.ImageUrls.Count <= 3)
|
||||
{
|
||||
request.ImageDurationPerSecond = 5;
|
||||
}
|
||||
if (request.ImageUrls.Count > 20)
|
||||
{
|
||||
request.ImageDurationPerSecond = 2;
|
||||
}
|
||||
// 3. (可选)自定义视频参数
|
||||
_fFmpegHelper.VideoWidth = 1080;
|
||||
_fFmpegHelper.VideoHeight = 1920;
|
||||
_fFmpegHelper.ImageDisplayDurationSeconds = request.ImageDurationPerSecond;
|
||||
_fFmpegHelper.OutputFrameRate = 30;
|
||||
|
||||
// 4. 创建进度
|
||||
var progress = new Progress<double>(p =>
|
||||
{
|
||||
Console.WriteLine($"进度: {p:F2}%");
|
||||
});
|
||||
|
||||
// 5. 执行合成任务
|
||||
using (var cancellationTokenSource = new CancellationTokenSource())
|
||||
{
|
||||
string resultPath = await _fFmpegHelper.CreateVideoFromImagesAndAudioAsync(
|
||||
rawImages,
|
||||
rawAudios[0],
|
||||
outputVideoPath,
|
||||
progress,
|
||||
cancellationTokenSource.Token);
|
||||
|
||||
Console.WriteLine($"视频合成成功!文件已保存至: {resultPath}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理临时目录(无论成功失败)
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error($"{ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>通用媒体下载方法</summary>
|
||||
private async Task<(string[] SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
|
||||
List<string> urls, string saveDir, string prefix, string ext)
|
||||
{
|
||||
var successPaths = new List<string>();
|
||||
for (var i = 0; i < urls.Count; i++)
|
||||
{
|
||||
var url = urls[i];
|
||||
var fileExt = ext ?? Path.GetExtension(url).TrimStart('.') ?? "png";
|
||||
var fileName = $"{prefix}{i + 1}.{fileExt}";
|
||||
var savePath = Path.Combine(saveDir, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await _downloadHelper.DownloadFileAsync(url, savePath);
|
||||
successPaths.Add(savePath);
|
||||
Console.WriteLine($"下载成功:{url} → {savePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"下载失败:{url},错误:{ex.Message}";
|
||||
Console.WriteLine(error);
|
||||
return (Array.Empty<string>(), error);
|
||||
}
|
||||
}
|
||||
return (successPaths.ToArray(), null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.image
|
||||
{
|
||||
public class MediaMergeRequest
|
||||
{
|
||||
/// <summary>网络图片地址数组(必填)</summary>
|
||||
public List<string> ImageUrls { get; set; }
|
||||
|
||||
/// <summary>网络MP3地址数组(必填)</summary>
|
||||
public List<string> AudioUrls { get; set; }
|
||||
|
||||
/// <summary>每张图片显示时长(秒,默认3秒)</summary>
|
||||
public int ImageDurationPerSecond { get; set; } = 3;
|
||||
|
||||
/// <summary>视频分辨率(格式:1920x1080,默认1920x1080)</summary>
|
||||
public int VideoWidth { get; set; } = 1080;
|
||||
public int VideoHeight { get; set; } = 1920;
|
||||
|
||||
/// <summary>输出视频格式(默认mp4)</summary>
|
||||
public string OutputFormat { get; set; } = "mp4";
|
||||
|
||||
/// <summary>视频帧率(默认25)</summary>
|
||||
public int VideoFps { get; set; } = 25;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -156,18 +156,18 @@ namespace dy.net.extension
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
public static void AddQuartzService(this IServiceCollection services, IConfiguration configuration)
|
||||
public static void AddQuartzService(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<DouYinCollectSyncJob>();
|
||||
services.AddTransient<DouYinFavoritSyncJob>();
|
||||
services.AddTransient<DouYinUperPostSyncJob>();
|
||||
//services.AddTransient<DouyinCollectSyncJob>();
|
||||
//services.AddTransient<DouyinFavoritSyncJob>();
|
||||
//services.AddTransient<DouyinUperPostSyncJob>();
|
||||
|
||||
// 注册Quartz服务
|
||||
services.AddQuartz();
|
||||
|
||||
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||
|
||||
services.AddTransient<QuartzJobService>();
|
||||
services.AddTransient<DouyinQuartzJobService>();
|
||||
}
|
||||
|
||||
|
||||
@@ -264,8 +264,12 @@ namespace dy.net.extension
|
||||
?? ServiceLifetime.Transient;
|
||||
|
||||
// 查找该类实现的接口(优先注册为接口服务)
|
||||
//var interfaces = type.GetInterfaces()
|
||||
// .Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
|
||||
// .ToList();
|
||||
// 查找该类实现的接口(排除 IDisposable)
|
||||
var interfaces = type.GetInterfaces()
|
||||
.Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
|
||||
.Where(i => i != typeof(IDisposable)) // <-- 关键修正
|
||||
.ToList();
|
||||
|
||||
if (interfaces.Any())
|
||||
@@ -366,20 +370,7 @@ namespace dy.net.extension
|
||||
{
|
||||
Version = "v1",
|
||||
Title = "dy.net API Swagger Document",
|
||||
//Description = "WebApi Swagger Document",
|
||||
|
||||
//TermsOfService = new Uri("https://ddnsapi.online.com"),
|
||||
//Contact = new OpenApiContact
|
||||
//{
|
||||
// Name = "剑之初",
|
||||
// Email = "xxxx@qq.com",
|
||||
// //Url = new Uri("https://ddns.online"),
|
||||
//},
|
||||
//License = new OpenApiLicense
|
||||
//{
|
||||
// Name = "许可证",
|
||||
// Url = new Uri("https://ddns.online"),
|
||||
//}
|
||||
});
|
||||
o.OrderActionsBy((apiDesc) => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.HttpMethod}");
|
||||
o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
|
||||
|
||||
+55
-325
@@ -1,367 +1,97 @@
|
||||
using ClockSnowFlake;
|
||||
using dy.net.dto;
|
||||
using dy.net.dto;
|
||||
using dy.net.model;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Quartz;
|
||||
|
||||
namespace dy.net.job
|
||||
{
|
||||
[DisallowConcurrentExecution]
|
||||
public class DouYinCollectSyncJob : IJob
|
||||
public class DouyinCollectSyncJob : DouyinBaseSyncJob
|
||||
{
|
||||
private readonly DyCookieService _dyCookieService;
|
||||
public DouyinCollectSyncJob(
|
||||
DouyinCookieService dyCookieService,
|
||||
DouyinHttpClientService dyHttpClientService,
|
||||
DouyinVideoService dyCollectVideoService,
|
||||
DouyinCommonService commonService,IServiceProvider serviceProvider,IWebHostEnvironment webHostEnvironment)
|
||||
: base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider,webHostEnvironment) { }
|
||||
|
||||
private readonly DyHttpClientService _douyinService;
|
||||
protected override string JobType => "collect";
|
||||
|
||||
private readonly DyCollectVideoService _douyinVideoService;
|
||||
private readonly CommonService commonService;
|
||||
private readonly Random _random = new Random();
|
||||
private string count = "18"; // 每页请求的视频数量,默认18
|
||||
|
||||
public DouYinCollectSyncJob(DyCookieService dyCookieService, DyHttpClientService dyHttpClientService, DyCollectVideoService dyCollectVideoService, CommonService commonService)
|
||||
protected override async Task BeforeProcessCookies()
|
||||
{
|
||||
_dyCookieService = dyCookieService;
|
||||
_douyinService = dyHttpClientService;
|
||||
_douyinVideoService = dyCollectVideoService;
|
||||
this.commonService = commonService;
|
||||
var now = DateTime.Now;
|
||||
if (now.Hour == 1 && now.Minute < 30)
|
||||
{
|
||||
_commonService.UpdateAllCookieSyncedToZero();
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
|
||||
{
|
||||
var config = commonService.GetConfig();
|
||||
if (config == null)
|
||||
{
|
||||
Serilog.Log.Debug("collect-请先在设置中初始化配置,再执行同步任务");
|
||||
return;
|
||||
}
|
||||
if (config.BatchCount > 0)
|
||||
{
|
||||
count = config.BatchCount.ToString();
|
||||
}
|
||||
|
||||
|
||||
var cookies = await _dyCookieService.GetAllCookies();
|
||||
cookies= cookies.Where(c => !string.IsNullOrWhiteSpace(c.SavePath)).ToList();
|
||||
if (cookies == null || !cookies.Any())
|
||||
{
|
||||
Serilog.Log.Debug("collect-无可用Cookie,任务终止");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Serilog.Log.Debug($"collect-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
|
||||
//return;
|
||||
return cookies.Where(c => !string.IsNullOrWhiteSpace(c.SavePath)).ToList();
|
||||
}
|
||||
|
||||
//每天凌晨1点到1点半之间执行一次重置同步状态-全部cookie
|
||||
UpdateCookieSyncedToZero();
|
||||
|
||||
foreach (var cookie in cookies)
|
||||
protected override bool IsCookieValid(DouyinUserCookie cookie)
|
||||
{
|
||||
|
||||
Serilog.Log.Debug($"collect-开始同步 Cookie-[{cookie.UserName}]收藏视频");
|
||||
if (string.IsNullOrWhiteSpace(cookie.Cookies) || cookie.Cookies.Length < 1000)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}]无效,跳过");
|
||||
continue;
|
||||
}
|
||||
//if (string.IsNullOrWhiteSpace(cookie.SavePath))
|
||||
//{
|
||||
// Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}]未设置保存路径,跳过");
|
||||
// continue;
|
||||
//}
|
||||
try
|
||||
{
|
||||
int syncCount = 0;// 记录本次Cookie同步的视频数量
|
||||
|
||||
int index = 0;
|
||||
bool hasMore = true;
|
||||
|
||||
string cursor = "0";
|
||||
|
||||
while (hasMore)
|
||||
{
|
||||
var data = await _douyinService.SyncCollectVideos(cursor, count, cookie.Cookies);
|
||||
hasMore = data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
|
||||
break;
|
||||
if (cookie.CollHasSyncd == 1)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-Cookie[{cookie.UserName}]已完整同步过,后续只获取最新一页数据");
|
||||
}
|
||||
//Serilog.Log.Debug($"还有数据需要同步吗?{(hasMore ? "YES" : "NO")}");
|
||||
if (data == null)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-Cookie[{cookie.UserName}]获取收藏数据失败,请检查一下Cookie");
|
||||
break;
|
||||
}
|
||||
cursor = data != null && !string.IsNullOrWhiteSpace(data.Cursor) ? data.Cursor : "0";
|
||||
|
||||
|
||||
if (data.AwemeList == null || !data.AwemeList.Any())
|
||||
{
|
||||
break;
|
||||
return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 &&
|
||||
!string.IsNullOrWhiteSpace(cookie.SavePath);
|
||||
}
|
||||
|
||||
List<DyCollectVideo> videos = new List<DyCollectVideo>();
|
||||
foreach (var item in data.AwemeList)
|
||||
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
|
||||
{
|
||||
if (item == null)
|
||||
continue;
|
||||
if (item.Video == null)
|
||||
continue;
|
||||
if (item.Video.BitRate == null)
|
||||
continue;
|
||||
var v = item.Video.BitRate.FirstOrDefault();
|
||||
var tags = item.VideoTags;
|
||||
if (v == null) continue;
|
||||
|
||||
var videoUrl = v.PlayAddr.UrlList != null && v.PlayAddr.UrlList.Any() ? v.PlayAddr.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(videoUrl)) continue;
|
||||
|
||||
var tag1 = tags.FirstOrDefault(x => x.Level == 1)?.TagName;
|
||||
var tag2 = tags.FirstOrDefault(x => x.Level == 2)?.TagName;
|
||||
var tag3 = tags.FirstOrDefault(x => x.Level == 3)?.TagName;
|
||||
string saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
|
||||
|
||||
|
||||
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
|
||||
var savePath = Path.Combine(saveFolder, fileName);
|
||||
|
||||
if (!File.Exists(savePath))
|
||||
{
|
||||
Serilog.Log.Debug($"collect-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
|
||||
|
||||
if (downVideo)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"collect-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
|
||||
}
|
||||
if (downVideo)
|
||||
{
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
|
||||
// 用AuthorId做文件名,避免昵称特殊字符
|
||||
var avatarImgName = $"{item.Author.Uid}.jpg";
|
||||
var avatarSavePath = Path.Combine(cookie.SavePath, "author", avatarImgName);
|
||||
await DownAuthorAvatar(cookie.SavePath, item, avatarSavePath, cookie.Cookies);
|
||||
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
// 备用头像链接
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
avatarUrl = item.Author?.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
|
||||
? item.Author.AvatarThumb.UrlList[0] : null;
|
||||
return await _douyinService.SyncCollectVideos(cursor, count, cookie.Cookies);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 构造视频数据
|
||||
DyCollectVideo video = new()
|
||||
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
|
||||
{
|
||||
ViedoType = "2",
|
||||
AwemeId = item.AwemeId,
|
||||
Author = item.Author?.Nickname,
|
||||
AuthorId = item.Author?.Uid,
|
||||
AuthorAvatar = avatarSavePath,
|
||||
AuthorAvatarUrl = avatarUrl,
|
||||
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||
VideoTitle = item.Desc,
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
|
||||
FileSize = v.PlayAddr.DataSize,
|
||||
FileHash = v.PlayAddr.FileHash,
|
||||
Tag1 = tag1,
|
||||
Tag2 = tag2,
|
||||
Tag3 = tag3,
|
||||
VideoUrl = videoUrl,
|
||||
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null,
|
||||
VideoSavePath = savePath,
|
||||
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
|
||||
SyncTime = DateTime.Now,
|
||||
DyUserId = data.Uid,
|
||||
CookieId = cookie.Id
|
||||
};
|
||||
videos.Add(video);
|
||||
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new VideoNfo
|
||||
{
|
||||
Author = video.Author,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = video.VideoTitle,
|
||||
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = video.CreateTime,
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
|
||||
}
|
||||
}
|
||||
index++;
|
||||
// 批量保存到数据库(减少数据库操作频率)
|
||||
if (videos.Any())
|
||||
{
|
||||
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
|
||||
try
|
||||
{
|
||||
await _douyinVideoService.batchInsert(videos);
|
||||
syncCount += videos.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error($"collect-批量保存视频到数据库失败:{ex.Message}", ex);
|
||||
|
||||
// 收集所有需要删除的目录(去重)
|
||||
foreach (var video in videos)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(video.VideoSavePath))
|
||||
{
|
||||
var deletePath = Path.GetDirectoryName(video.VideoSavePath);
|
||||
Directory.Delete(deletePath, recursive: true);
|
||||
//Serilog.Log.Debug($"已删除失败视频目录:{deletePath}");
|
||||
}
|
||||
return data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
|
||||
}
|
||||
|
||||
Serilog.Log.Debug("collect-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
|
||||
}
|
||||
}
|
||||
else
|
||||
protected override string GetNextCursor(DouyinVideoInfo data)
|
||||
{
|
||||
//Serilog.Log.Debug($"没有查询到新的视频");
|
||||
return data?.Cursor ?? "0";
|
||||
}
|
||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
||||
|
||||
protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
|
||||
{
|
||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
|
||||
var folder = Path.Combine(cookie.SavePath, safeTag1, $"{TikTokFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
}
|
||||
|
||||
protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
|
||||
{
|
||||
return $"{item.AwemeId}.{bitRate.Format}";
|
||||
}
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.SavePath, "author");
|
||||
}
|
||||
|
||||
protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount)
|
||||
{
|
||||
if (syncCount > 0)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||
// 更新同步状态为已同步
|
||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
||||
cookie.CollHasSyncd = 1;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
|
||||
{
|
||||
Serilog.Log.Error($"collect-处理Cookie[{cookie.Id}]时出错:{ex.Message}");
|
||||
Serilog.Log.Error($"collect-处理Cookie[{cookie.Id}]时出错22:{ex.StackTrace}");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载视频封面
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="saveFolder"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
|
||||
return new VideoEntityDifferences
|
||||
{
|
||||
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(coverUrl)) return;
|
||||
|
||||
var coverImgName = "poster.jpg";
|
||||
var coverSavePath = Path.Combine(saveFolder, coverImgName);
|
||||
|
||||
if (!File.Exists(coverSavePath))
|
||||
{
|
||||
// 封面下载前随机延迟
|
||||
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
|
||||
if (downRes)
|
||||
{
|
||||
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
|
||||
File.Copy(coverSavePath, copyPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载作者头像
|
||||
/// </summary>
|
||||
/// <param name="mainPath"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="avatarSavePath"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownAuthorAvatar(string mainPath, Aweme item, string avatarSavePath, string cookie)
|
||||
{
|
||||
if (item.Author == null) return;
|
||||
|
||||
var avatarUrl = item.Author.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl)) return;
|
||||
var path = Path.Combine(mainPath, "author");
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
if (!File.Exists(avatarSavePath))
|
||||
{
|
||||
// 头像下载前随机延迟
|
||||
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建目录
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="tag1"></param>
|
||||
/// <param name="tag2"></param>
|
||||
/// <returns></returns>
|
||||
private static string CreateSaveFolder(DyUserCookies cookie, Aweme item, string? tag1, string? tag2)
|
||||
{
|
||||
// 路径中避免特殊字符,用ID替代描述
|
||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
|
||||
List<string> pathParts = new List<string> { cookie.SavePath, safeTag1 };
|
||||
var saveFolder = Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), TikTokFileNameHelper.SanitizePath(item.Desc) + "@" + item.AwemeId);
|
||||
|
||||
// 创建文件夹(提前创建,避免下载时才操作)
|
||||
if (!Directory.Exists(saveFolder))
|
||||
{
|
||||
Directory.CreateDirectory(saveFolder);
|
||||
}
|
||||
return saveFolder;
|
||||
//if (string.IsNullOrWhiteSpace(tag2))
|
||||
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
|
||||
//else
|
||||
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), tag2 + "@" + item.AwemeId);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 重置Cookie的同步状态为0(每天凌晨1点到1点半之间执行一次)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private void UpdateCookieSyncedToZero()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
//如果当时间为凌晨1点到1点半之间,则重置为0
|
||||
if (now.Hour == 1 && now.Minute < 30)
|
||||
{
|
||||
commonService.UpdateAllCookieSyncedToZero();
|
||||
}
|
||||
VideoType = VideoTypeEnum.Collect,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-310
@@ -1,349 +1,89 @@
|
||||
using ClockSnowFlake;
|
||||
using dy.net.dto;
|
||||
using dy.net.dto;
|
||||
using dy.net.model;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Quartz;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.job
|
||||
{
|
||||
[DisallowConcurrentExecution]
|
||||
public class DouYinFavoritSyncJob : IJob
|
||||
public class DouyinFavoritSyncJob : DouyinBaseSyncJob
|
||||
{
|
||||
private readonly DyCookieService _dyCookieService;
|
||||
public DouyinFavoritSyncJob(
|
||||
DouyinCookieService dyCookieService,
|
||||
DouyinHttpClientService dyHttpClientService,
|
||||
DouyinVideoService dyCollectVideoService,
|
||||
DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment)
|
||||
: base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider, webHostEnvironment) { }
|
||||
|
||||
private readonly DyHttpClientService _douyinService;
|
||||
|
||||
private readonly DyCollectVideoService _douyinVideoService;
|
||||
private readonly CommonService commonService;
|
||||
private readonly Random _random = new Random();
|
||||
private string count = "18"; // 每页请求的视频数量,默认18
|
||||
|
||||
public DouYinFavoritSyncJob(DyCookieService dyCookieService, DyHttpClientService dyHttpClientService, DyCollectVideoService dyCollectVideoService,CommonService commonService)
|
||||
protected override string JobType => "favorite";
|
||||
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
|
||||
{
|
||||
_dyCookieService = dyCookieService;
|
||||
_douyinService = dyHttpClientService;
|
||||
_douyinVideoService = dyCollectVideoService;
|
||||
this.commonService = commonService;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
|
||||
var config = commonService.GetConfig();
|
||||
if (config == null)
|
||||
{
|
||||
Serilog.Log.Debug("favorite-请先在设置中初始化配置,再执行同步任务");
|
||||
return;
|
||||
}
|
||||
if (config.BatchCount > 0)
|
||||
{
|
||||
count = config.BatchCount.ToString();
|
||||
}
|
||||
var cookies = await _dyCookieService.GetAllCookies();
|
||||
cookies=cookies.Where(c => !string.IsNullOrWhiteSpace(c.FavSavePath)).ToList();
|
||||
if (cookies == null || !cookies.Any())
|
||||
{
|
||||
Serilog.Log.Debug("favorite-无可用Cookie,任务终止");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
|
||||
//return;
|
||||
return cookies.Where(c => !string.IsNullOrWhiteSpace(c.FavSavePath)).ToList();
|
||||
}
|
||||
|
||||
foreach (var cookie in cookies)
|
||||
protected override bool IsCookieValid(DouyinUserCookie cookie)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-开始同步 Cookie-[{cookie.UserName}]喜欢的视频");
|
||||
if (string.IsNullOrWhiteSpace(cookie.Cookies)|| cookie.Cookies.Length<1000)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]无效,跳过");
|
||||
continue;
|
||||
}
|
||||
if(string.IsNullOrWhiteSpace(cookie.FavSavePath))
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]未设置保存路径,跳过");
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(cookie.SecUserId)|| cookie.SecUserId.Length<10) {
|
||||
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]未设置SecUserId,跳过");
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
int syncCount = 0;// 记录本次Cookie同步的视频数量
|
||||
|
||||
int index = 0;
|
||||
bool hasMore = true;
|
||||
string cursor = "0";
|
||||
while (hasMore)
|
||||
{
|
||||
var data = await _douyinService.SyncFavoriteVideos(count,cursor, cookie.SecUserId, cookie.Cookies);
|
||||
hasMore = data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
|
||||
if (cookie.FavHasSyncd == 1)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-Cookie[{cookie.UserName}]已完整同步过,后续只获取最新一页数据");
|
||||
}
|
||||
//Serilog.Log.Debug($"还有数据需要同步吗?{(hasMore ? "YES" : "NO")}");
|
||||
if (data == null)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-Cookie[{cookie.UserName}]获取喜欢的数据失败,请检查一下Cookie和sec_user_id");
|
||||
break;
|
||||
}
|
||||
cursor = data != null && !string.IsNullOrWhiteSpace(data.MaxCursor) ? data.MaxCursor : "0";
|
||||
|
||||
if (data.AwemeList == null || !data.AwemeList.Any())
|
||||
{
|
||||
break;
|
||||
return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 &&
|
||||
!string.IsNullOrWhiteSpace(cookie.FavSavePath) &&
|
||||
!string.IsNullOrWhiteSpace(cookie.SecUserId) && cookie.SecUserId.Length >= 10;
|
||||
}
|
||||
|
||||
List<DyCollectVideo> videos = new List<DyCollectVideo>();
|
||||
foreach (var item in data.AwemeList)
|
||||
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
|
||||
{
|
||||
if (item == null)
|
||||
continue;
|
||||
if (item.Video == null)
|
||||
continue;
|
||||
if (item.Video.BitRate == null)
|
||||
continue;
|
||||
var v = item.Video.BitRate.FirstOrDefault();
|
||||
var tags = item.VideoTags;
|
||||
if (v == null) continue;
|
||||
|
||||
var videoUrl = v.PlayAddr.UrlList != null && v.PlayAddr.UrlList.Any() ? v.PlayAddr.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(videoUrl)) continue;
|
||||
|
||||
var tag1 = tags.FirstOrDefault(x => x.Level == 1)?.TagName;
|
||||
var tag2 = tags.FirstOrDefault(x => x.Level == 2)?.TagName;
|
||||
var tag3 = tags.FirstOrDefault(x => x.Level == 3)?.TagName;
|
||||
string saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
|
||||
|
||||
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
|
||||
var savePath = Path.Combine(saveFolder, fileName);
|
||||
|
||||
if (!File.Exists(savePath))
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
|
||||
|
||||
if (downVideo)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"favorite-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
if (downVideo)
|
||||
{
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
|
||||
// 用AuthorId做文件名,避免昵称特殊字符
|
||||
var avatarImgName = $"{item.Author.Uid}.jpg";
|
||||
var avatarSavePath = Path.Combine(cookie.FavSavePath, "author", avatarImgName);
|
||||
await DownAuthorAvatar(cookie.FavSavePath, item, avatarSavePath, cookie.Cookies);
|
||||
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
// 尝试使用AvatarThumb作为备用头像URL
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
avatarUrl = item.Author?.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
|
||||
? item.Author.AvatarThumb.UrlList[0] : null;
|
||||
return await _douyinService.SyncFavoriteVideos(count, cursor, cookie.SecUserId, cookie.Cookies);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 构造视频数据
|
||||
DyCollectVideo video = new()
|
||||
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
|
||||
{
|
||||
ViedoType="1",
|
||||
AwemeId = item.AwemeId,
|
||||
Author = item.Author?.Nickname,
|
||||
AuthorId = item.Author?.Uid,
|
||||
AuthorAvatar = avatarSavePath,
|
||||
AuthorAvatarUrl = avatarUrl,
|
||||
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||
VideoTitle = item.Desc,
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
|
||||
FileSize = v.PlayAddr.DataSize,
|
||||
FileHash = v.PlayAddr.FileHash,
|
||||
Tag1 = tag1,
|
||||
Tag2 = tag2,
|
||||
Tag3 = tag3,
|
||||
VideoUrl = videoUrl,
|
||||
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null,
|
||||
VideoSavePath = savePath,
|
||||
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
|
||||
SyncTime = DateTime.Now,
|
||||
DyUserId = data.Uid,
|
||||
CookieId = cookie.Id
|
||||
};
|
||||
videos.Add(video);
|
||||
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new VideoNfo
|
||||
{
|
||||
Author = video.Author,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = video.VideoTitle,
|
||||
Thumbnail= Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = video.CreateTime,
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
|
||||
}
|
||||
}
|
||||
index++;
|
||||
// 批量保存到数据库(减少数据库操作频率)
|
||||
if (videos.Any())
|
||||
{
|
||||
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
|
||||
try
|
||||
{
|
||||
await _douyinVideoService.batchInsert(videos);
|
||||
syncCount += videos.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error("favorite-批量保存视频到数据库失败:{ex.Message}", ex);
|
||||
|
||||
// 收集所有需要删除的目录(去重)
|
||||
foreach (var video in videos)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(video.VideoSavePath))
|
||||
{
|
||||
var deletePath = Path.GetDirectoryName(video.VideoSavePath);
|
||||
Directory.Delete(deletePath, recursive: true);
|
||||
//Serilog.Log.Debug($"已删除失败视频目录:{deletePath}");
|
||||
}
|
||||
return data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
|
||||
}
|
||||
|
||||
Serilog.Log.Debug("favorite-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
|
||||
protected override string GetNextCursor(DouyinVideoInfo data)
|
||||
{
|
||||
return data?.MaxCursor ?? "0";
|
||||
}
|
||||
|
||||
protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
|
||||
{
|
||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
|
||||
var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{TikTokFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
}
|
||||
else {
|
||||
//Serilog.Log.Debug($"没有查询到新的视频");
|
||||
|
||||
protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
|
||||
{
|
||||
return $"{item.AwemeId}.{bitRate.Format}";
|
||||
}
|
||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.FavSavePath, "author");
|
||||
}
|
||||
|
||||
protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount)
|
||||
{
|
||||
if (syncCount > 0)
|
||||
{
|
||||
Serilog.Log.Debug($"Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
||||
// 更新同步状态为已同步
|
||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
||||
cookie.FavHasSyncd = 1;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
else {
|
||||
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错:{ex.Message}");
|
||||
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错22:{ex.StackTrace}");
|
||||
}
|
||||
|
||||
|
||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载视频封面
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="saveFolder"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
|
||||
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
|
||||
{
|
||||
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(coverUrl)) return;
|
||||
|
||||
var coverImgName = "poster.jpg";
|
||||
var coverSavePath = Path.Combine(saveFolder, coverImgName);
|
||||
|
||||
if (!File.Exists(coverSavePath))
|
||||
return new VideoEntityDifferences
|
||||
{
|
||||
// 封面下载前随机延迟
|
||||
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
|
||||
if (downRes)
|
||||
{
|
||||
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
|
||||
File.Copy(coverSavePath, copyPath, true);
|
||||
VideoType = VideoTypeEnum.Favorite
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载作者头像
|
||||
/// </summary>
|
||||
/// <param name="mainPath"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="avatarSavePath"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownAuthorAvatar(string mainPath, Aweme item, string avatarSavePath, string cookie)
|
||||
{
|
||||
if (item.Author == null) return;
|
||||
|
||||
var avatarUrl = item.Author.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl)) {
|
||||
avatarUrl = item.Author.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
|
||||
? item.Author.AvatarThumb.UrlList[0] : null;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl)) return;
|
||||
var path = Path.Combine(mainPath, "author");
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
if (!File.Exists(avatarSavePath))
|
||||
{
|
||||
// 头像下载前随机延迟
|
||||
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建目录
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="tag1"></param>
|
||||
/// <param name="tag2"></param>
|
||||
/// <returns></returns>
|
||||
private static string CreateSaveFolder(DyUserCookies cookie, Aweme item, string? tag1, string? tag2)
|
||||
{
|
||||
// 路径中避免特殊字符,用ID替代描述
|
||||
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
|
||||
List<string> pathParts = new List<string> { cookie.FavSavePath, safeTag1 };
|
||||
var saveFolder= Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), TikTokFileNameHelper.SanitizePath(item.Desc) + "@" + item.AwemeId);
|
||||
|
||||
// 创建文件夹(提前创建,避免下载时才操作)
|
||||
if (!Directory.Exists(saveFolder))
|
||||
{
|
||||
Directory.CreateDirectory(saveFolder);
|
||||
}
|
||||
return saveFolder;
|
||||
//if (string.IsNullOrWhiteSpace(tag2))
|
||||
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
|
||||
//else
|
||||
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), tag2 + "@" + item.AwemeId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+65
-335
@@ -1,381 +1,111 @@
|
||||
using ClockSnowFlake;
|
||||
using dy.net.dto;
|
||||
using dy.net.dto;
|
||||
using dy.net.model;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Newtonsoft.Json;
|
||||
using Quartz;
|
||||
using SqlSugar.Extensions;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.job
|
||||
{
|
||||
[DisallowConcurrentExecution]
|
||||
public class DouYinUperPostSyncJob : IJob
|
||||
public class DouyinUperPostSyncJob : DouyinBaseSyncJob
|
||||
{
|
||||
private readonly DyCookieService _dyCookieService;
|
||||
public DouyinUperPostSyncJob(
|
||||
DouyinCookieService dyCookieService,
|
||||
DouyinHttpClientService dyHttpClientService,
|
||||
DouyinVideoService dyCollectVideoService,
|
||||
DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment)
|
||||
: base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider, webHostEnvironment) { }
|
||||
|
||||
private readonly DyHttpClientService _douyinService;
|
||||
protected override string JobType => "dyuploder";
|
||||
|
||||
private readonly DyCollectVideoService _douyinVideoService;
|
||||
private readonly CommonService commonService;
|
||||
private readonly Random _random = new Random();
|
||||
private string count = "18"; // 每页请求的视频数量,默认18
|
||||
|
||||
private readonly string httpDownName = "dy_down_uper";
|
||||
|
||||
public DouYinUperPostSyncJob(DyCookieService dyCookieService, DyHttpClientService dyHttpClientService, DyCollectVideoService dyCollectVideoService, CommonService commonService)
|
||||
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
|
||||
{
|
||||
_dyCookieService = dyCookieService;
|
||||
_douyinService = dyHttpClientService;
|
||||
_douyinVideoService = dyCollectVideoService;
|
||||
this.commonService = commonService;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var config = commonService.GetConfig();
|
||||
if (config == null)
|
||||
{
|
||||
Serilog.Log.Debug("dyuploder-请先在设置中初始化配置,再执行同步任务");
|
||||
return;
|
||||
}
|
||||
if (config.BatchCount > 0)
|
||||
{
|
||||
count = config.BatchCount.ToString();
|
||||
}
|
||||
|
||||
var cookies = await _dyCookieService.GetAllCookies();
|
||||
cookies = cookies.Where(x => !string.IsNullOrWhiteSpace(x.UpSecUserIds) && !string.IsNullOrWhiteSpace(x.UpSavePath))?.ToList();
|
||||
if (!cookies.Any())
|
||||
{
|
||||
Serilog.Log.Debug("dyuploder-无可用cookie,任务终止");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Serilog.Log.Debug($"dyuploder-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
|
||||
//return;
|
||||
return cookies.Where(x => !string.IsNullOrWhiteSpace(x.UpSecUserIds) && !string.IsNullOrWhiteSpace(x.UpSavePath)).ToList();
|
||||
}
|
||||
|
||||
foreach (var cookie in cookies)
|
||||
protected override bool IsCookieValid(DouyinUserCookie cookie)
|
||||
{
|
||||
//if(string.IsNullOrWhiteSpace(cookie.UpSavePath))
|
||||
//{
|
||||
// Serilog.Log.Debug($"dyuploder-Cookie[{cookie.UserName}]的UP主保存路径无效,跳过");
|
||||
// continue;
|
||||
//}
|
||||
var ups = JsonConvert.DeserializeObject<List<DyUpSecUserIdDto>>(cookie.UpSecUserIds);
|
||||
|
||||
foreach (var uper in ups)
|
||||
{
|
||||
|
||||
Serilog.Log.Debug($"dyuploder-开始同步-[{uper.uper}]主页作品数据");
|
||||
if (string.IsNullOrWhiteSpace(uper.uid) || uper.uid.Length < 10)
|
||||
{
|
||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}]的SecUserId无效,跳过");
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
int syncCount = 0;// 记录本次Cookie同步的视频数量
|
||||
|
||||
int index = 0;
|
||||
bool hasMore = true;
|
||||
|
||||
string cursor = "0";
|
||||
|
||||
while (hasMore)
|
||||
{
|
||||
var data = await _douyinService.SyncUpderPostVideos(count, cursor, uper.uid, cookie.Cookies);
|
||||
hasMore = data != null && data.HasMore == 1 && cookie.UperSyncd == 0 && uper.syncAll;
|
||||
if (cookie.UperSyncd == 1)
|
||||
{
|
||||
Serilog.Log.Debug($"dyuploder-Cookie[{cookie.UserName}]已完整同步过,后续只获取最新一页数据");
|
||||
}
|
||||
//Serilog.Log.Debug($"还有数据需要同步吗?{(hasMore ? "YES" : "NO")}");
|
||||
if (data == null)
|
||||
{
|
||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}]获取数据失败,请检查Up主SecUserId");
|
||||
break;
|
||||
}
|
||||
cursor = data != null && !string.IsNullOrWhiteSpace(data.MaxCursor) ? data.MaxCursor : "0";
|
||||
|
||||
|
||||
if (data.AwemeList == null || !data.AwemeList.Any())
|
||||
{
|
||||
break;
|
||||
return !string.IsNullOrWhiteSpace(cookie.Cookies) && !string.IsNullOrWhiteSpace(cookie.UpSavePath) && !string.IsNullOrWhiteSpace(cookie.UpSecUserIds);
|
||||
}
|
||||
|
||||
List<DyCollectVideo> videos = new List<DyCollectVideo>();
|
||||
foreach (var item in data.AwemeList)
|
||||
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
|
||||
{
|
||||
if (item == null)
|
||||
continue;
|
||||
if (item.Video == null)
|
||||
continue;
|
||||
if (item.Video.BitRate == null)
|
||||
continue;
|
||||
var v = item.Video.BitRate.FirstOrDefault();
|
||||
var tags = item.VideoTags;
|
||||
if (v == null) continue;
|
||||
// 简化处理:假设只同步第一个UP主
|
||||
var ups = JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(cookie.UpSecUserIds);
|
||||
var firstUpId = ups?.FirstOrDefault()?.uid;
|
||||
if (string.IsNullOrEmpty(firstUpId)) return null;
|
||||
|
||||
var videoUrl = v.PlayAddr.UrlList != null && v.PlayAddr.UrlList.Any() ? v.PlayAddr.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(videoUrl)) continue;
|
||||
|
||||
var tag1 = tags.FirstOrDefault(x => x.Level == 1)?.TagName;
|
||||
var tag2 = tags.FirstOrDefault(x => x.Level == 2)?.TagName;
|
||||
var tag3 = tags.FirstOrDefault(x => x.Level == 3)?.TagName;
|
||||
string saveFolder = CreateSaveFolder(cookie, uper.uper);
|
||||
|
||||
|
||||
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
|
||||
|
||||
string samplePrefix = null;
|
||||
string sampleName = null;
|
||||
if (config.UperUseViedoTitle)// 使用视频标题作为文件名
|
||||
{
|
||||
sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
||||
|
||||
var sampleViedo = await _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName);
|
||||
if (string.IsNullOrWhiteSpace(sampleViedo.Item1))
|
||||
{
|
||||
fileName = $"{sampleName}.{v.Format}";
|
||||
}
|
||||
else
|
||||
{
|
||||
fileName = $"{sampleViedo.Item1}.{v.Format}";
|
||||
samplePrefix = sampleViedo.Item2;
|
||||
}
|
||||
return await _douyinService.SyncUpderPostVideos(count, cursor, firstUpId, cookie.Cookies);
|
||||
}
|
||||
|
||||
var savePath = Path.Combine(saveFolder, fileName);
|
||||
|
||||
if (!File.Exists(savePath))
|
||||
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
|
||||
{
|
||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}]-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
|
||||
if (downVideo)
|
||||
{
|
||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}]-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"dyuploder-[{uper.uper}]-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
if (downVideo)
|
||||
{
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
|
||||
// 用AuthorId做文件名,避免昵称特殊字符
|
||||
var avatarImgName = $"{item.Author.Uid}.jpg";
|
||||
var avatarSavePath = Path.Combine(cookie.UpSavePath, "author", avatarImgName);
|
||||
await DownAuthorAvatar(cookie.UpSavePath, item, avatarSavePath, cookie.Cookies);
|
||||
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
// 有可能下载大头像失败,尝试用缩略图地址
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
avatarUrl = item.Author?.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
|
||||
? item.Author.AvatarThumb.UrlList[0] : null;
|
||||
return data != null && data.HasMore == 1 && cookie.UperSyncd == 0;
|
||||
}
|
||||
|
||||
|
||||
// 构造视频数据
|
||||
DyCollectVideo video = new()
|
||||
protected override string GetNextCursor(DouyinVideoInfo data)
|
||||
{
|
||||
ViedoType = "3",// UP主视频
|
||||
AwemeId = item.AwemeId,
|
||||
Author = item.Author?.Nickname,
|
||||
AuthorId = item.Author?.Uid,
|
||||
AuthorAvatar = avatarSavePath,
|
||||
AuthorAvatarUrl = avatarUrl,
|
||||
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||
VideoTitle = item.Desc,
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
|
||||
FileSize = v.PlayAddr.DataSize,
|
||||
FileHash = v.PlayAddr.FileHash,
|
||||
Tag1 = tag1,
|
||||
Tag2 = tag2,
|
||||
Tag3 = tag3,
|
||||
VideoUrl = videoUrl,
|
||||
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null,
|
||||
VideoSavePath = savePath,
|
||||
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
|
||||
SyncTime = DateTime.Now,
|
||||
DyUserId = data.Uid,
|
||||
CookieId = cookie.Id,
|
||||
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(sampleName))
|
||||
{
|
||||
video.VideoTitleSimplify = sampleName;
|
||||
if (!string.IsNullOrEmpty(samplePrefix))
|
||||
video.VideoTitleSimplifyPrefix = samplePrefix;
|
||||
}
|
||||
videos.Add(video);
|
||||
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new VideoNfo
|
||||
{
|
||||
Author = video.Author,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = video.VideoTitle,
|
||||
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = video.CreateTime,
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
|
||||
}
|
||||
}
|
||||
index++;
|
||||
// 批量保存到数据库(减少数据库操作频率)
|
||||
if (videos.Any())
|
||||
{
|
||||
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
|
||||
try
|
||||
{
|
||||
await _douyinVideoService.batchInsert(videos);
|
||||
syncCount += videos.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error($"dyuploder-[{uper.uper}]-批量保存视频到数据库失败:{ex.Message}", ex);
|
||||
|
||||
// 收集所有需要删除的目录(去重)
|
||||
foreach (var video in videos)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(video.VideoSavePath))
|
||||
{
|
||||
var deletePath = Path.GetDirectoryName(video.VideoSavePath);
|
||||
Directory.Delete(deletePath, recursive: true);
|
||||
//Serilog.Log.Debug($"已删除失败视频目录:{deletePath}");
|
||||
}
|
||||
return data?.MaxCursor ?? "0";
|
||||
}
|
||||
|
||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}]-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
|
||||
}
|
||||
}
|
||||
else
|
||||
protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
|
||||
{
|
||||
//Serilog.Log.Debug($"没有查询到新的视频");
|
||||
// UP主视频通常按作者名创建文件夹
|
||||
var authorName = string.IsNullOrWhiteSpace(item.Author?.Nickname) ? "UnknownAuthor" : TikTokFileNameHelper.SanitizePath(item.Author.Nickname);
|
||||
var folder = Path.Combine(cookie.UpSavePath, authorName);
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
return folder;
|
||||
}
|
||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
||||
|
||||
protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
|
||||
{
|
||||
var config = _commonService.GetConfig();
|
||||
if (config?.UperUseViedoTitle ?? false)
|
||||
{
|
||||
var sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
||||
var (existingName, _) = _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result;
|
||||
return string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{bitRate.Format}" : $"{existingName}.{bitRate.Format}";
|
||||
}
|
||||
return $"{item.AwemeId}.{bitRate.Format}";
|
||||
}
|
||||
|
||||
protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie)
|
||||
{
|
||||
return Path.Combine(cookie.UpSavePath, "author");
|
||||
}
|
||||
|
||||
protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount)
|
||||
{
|
||||
if (syncCount > 0)
|
||||
{
|
||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}],本次共同步成功{syncCount}条视频");
|
||||
|
||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
||||
cookie.UperSyncd = 1;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}],本次没有查询到新的视频");
|
||||
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
|
||||
{
|
||||
Serilog.Log.Error($"dyuploder-[{uper.uper}]同步时出错:{ex.Message}");
|
||||
Serilog.Log.Error($"dyuploder-[{uper.uper}]同步时出错222:{ex.StackTrace}");
|
||||
}
|
||||
var config = _commonService.GetConfig();
|
||||
string simplifiedTitle = string.Empty;
|
||||
|
||||
}
|
||||
|
||||
// 更新为已同步过
|
||||
cookie.UperSyncd = 1;
|
||||
await _dyCookieService.UpdateAsync(cookie);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载视频封面
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="saveFolder"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
|
||||
if (config?.UperUseViedoTitle ?? false)
|
||||
{
|
||||
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(coverUrl)) return;
|
||||
|
||||
var coverImgName = "poster.jpg";
|
||||
var coverSavePath = Path.Combine(saveFolder, coverImgName);
|
||||
|
||||
if (!File.Exists(coverSavePath))
|
||||
{
|
||||
// 封面下载前随机延迟
|
||||
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
|
||||
if (downRes)
|
||||
{
|
||||
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
|
||||
File.Copy(coverSavePath, copyPath, true);
|
||||
}
|
||||
}
|
||||
simplifiedTitle = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载作者头像
|
||||
/// </summary>
|
||||
/// <param name="mainPath"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="avatarSavePath"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownAuthorAvatar(string mainPath, Aweme item, string avatarSavePath, string cookie)
|
||||
return new VideoEntityDifferences
|
||||
{
|
||||
if (item.Author == null) return;
|
||||
|
||||
var avatarUrl = item.Author.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl)) return;
|
||||
var path = Path.Combine(mainPath, "author");
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
if (!File.Exists(avatarSavePath))
|
||||
{
|
||||
// 头像下载前随机延迟
|
||||
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie);
|
||||
VideoType = VideoTypeEnum.UperPost,
|
||||
VideoTitleSimplify = simplifiedTitle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建目录
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="uperName"></param>
|
||||
/// <returns></returns>
|
||||
private static string CreateSaveFolder(DyUserCookies cookie, string uperName)
|
||||
{
|
||||
var saveFolder = Path.Combine(cookie.UpSavePath, uperName);
|
||||
// 创建文件夹(提前创建,避免下载时才操作)
|
||||
if (!Directory.Exists(saveFolder))
|
||||
{
|
||||
Directory.CreateDirectory(saveFolder);
|
||||
}
|
||||
return saveFolder;
|
||||
// 路径中避免特殊字符,用ID替代描述
|
||||
//var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : SanitizePath(tag1);
|
||||
//List<string> pathParts = new List<string> { cookie.UpSavePath, safeTag1 };
|
||||
//return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
|
||||
//if (string.IsNullOrWhiteSpace(tag2))
|
||||
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
|
||||
//else
|
||||
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), tag2 + "@" + item.AwemeId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
using ClockSnowFlake;
|
||||
using dy.image;
|
||||
using dy.net.dto;
|
||||
using dy.net.model;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Quartz;
|
||||
using Quartz.Util;
|
||||
|
||||
namespace dy.net.job
|
||||
{
|
||||
[DisallowConcurrentExecution]
|
||||
public abstract class DouyinBaseSyncJob : IJob
|
||||
{
|
||||
protected readonly DouyinCookieService _dyCookieService;
|
||||
protected readonly DouyinHttpClientService _douyinService;
|
||||
protected readonly DouyinVideoService _douyinVideoService;
|
||||
protected readonly DouyinCommonService _commonService;
|
||||
protected readonly Random _random = new Random();
|
||||
protected readonly IServiceProvider _serviceProvider;
|
||||
protected readonly IWebHostEnvironment _environment;
|
||||
protected string count = "18"; // 每页请求的视频数量
|
||||
protected abstract string JobType { get; }
|
||||
|
||||
|
||||
private bool _downImageVideo;
|
||||
|
||||
/// <summary>
|
||||
/// 清除保存失败的数据
|
||||
/// </summary>
|
||||
/// <param name="dyCookieService"></param>
|
||||
/// <param name="dyHttpClientService"></param>
|
||||
/// <param name="dyCollectVideoService"></param>
|
||||
/// <param name="commonService"></param>
|
||||
/// <param name="serviceProvider"></param>
|
||||
/// <param name="webHostEnvironment"></param>
|
||||
protected DouyinBaseSyncJob(
|
||||
DouyinCookieService dyCookieService,
|
||||
DouyinHttpClientService dyHttpClientService,
|
||||
DouyinVideoService dyCollectVideoService,
|
||||
DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment)
|
||||
{
|
||||
_dyCookieService = dyCookieService;
|
||||
_douyinService = dyHttpClientService;
|
||||
_douyinVideoService = dyCollectVideoService;
|
||||
_commonService = commonService;
|
||||
_serviceProvider = serviceProvider;
|
||||
_environment = webHostEnvironment;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var config = _commonService.GetConfig();
|
||||
if (config == null)
|
||||
{
|
||||
Serilog.Log.Debug($"{JobType}-请先在设置中初始化配置,再执行同步任务");
|
||||
return;
|
||||
}
|
||||
if (config.BatchCount > 0)
|
||||
count = config.BatchCount.ToString();
|
||||
|
||||
// 读取环境变量覆盖配置中是否下载图片视频配置
|
||||
InitializeDownImageVideoSetting(config);
|
||||
|
||||
await BeforeProcessCookies();
|
||||
|
||||
var cookies = await GetValidCookies();
|
||||
if (cookies == null || !cookies.Any())
|
||||
{
|
||||
Serilog.Log.Debug($"{JobType}-无可用Cookie,任务终止");
|
||||
return;
|
||||
}
|
||||
|
||||
Serilog.Log.Debug($"{JobType}-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
|
||||
|
||||
foreach (var cookie in cookies)
|
||||
{
|
||||
await ProcessSyncUserCookie(cookie);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 检查并初始化下载图片视频的设置
|
||||
/// </summary>
|
||||
/// <param name="config"></param>
|
||||
private void InitializeDownImageVideoSetting(AppConfig config)
|
||||
{
|
||||
var downImageVideoConfig = Appsettings.Get("DOWN_IMGVIDEO");
|
||||
if (!string.IsNullOrWhiteSpace(downImageVideoConfig))
|
||||
{
|
||||
downImageVideoConfig = downImageVideoConfig.ToLower();
|
||||
_downImageVideo = config.DownImageVideo && (downImageVideoConfig == "1" || downImageVideoConfig == "y" || downImageVideoConfig == "t" || downImageVideoConfig == "true");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并处理Cookie
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task ProcessSyncUserCookie(DouyinUserCookie cookie)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsCookieValid(cookie))
|
||||
{
|
||||
Serilog.Log.Debug($"{JobType}-Cookie[{cookie.UserName}]无效,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
Serilog.Log.Debug($"{JobType}-开始同步 Cookie-[{cookie.UserName}]");
|
||||
|
||||
int syncCount = 0;
|
||||
string cursor = "0";
|
||||
bool hasMore = true;
|
||||
|
||||
while (hasMore)
|
||||
{
|
||||
var data = await FetchVideoData(cookie, cursor);
|
||||
if (data == null)
|
||||
{
|
||||
Serilog.Log.Debug($"{JobType}-Cookie[{cookie.UserName}]获取数据失败");
|
||||
break;
|
||||
}
|
||||
|
||||
hasMore = ShouldContinueSync(cookie, data);
|
||||
cursor = GetNextCursor(data);
|
||||
|
||||
if (data.AwemeList == null || !data.AwemeList.Any())
|
||||
break;
|
||||
|
||||
var videos = await ProcessVideoList(cookie, data);
|
||||
syncCount += await SaveVideos(videos);
|
||||
|
||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
||||
}
|
||||
|
||||
await HandleSyncCompletion(cookie, syncCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"{JobType}-处理Cookie[{cookie.Id}]时出错");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理接口返回的视频数据
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task<List<DouyinVideo>> ProcessVideoList(DouyinUserCookie cookie, DouyinVideoInfo data)
|
||||
{
|
||||
var videos = new List<DouyinVideo>();
|
||||
foreach (var item in data.AwemeList)
|
||||
{
|
||||
var video = await ProcessSingleVideo(cookie, item, data);
|
||||
if (video != null)
|
||||
videos.Add(video);
|
||||
|
||||
//如果配置了下载图片视频,则处理图片集并合成视频
|
||||
if (_downImageVideo)
|
||||
{
|
||||
var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data);
|
||||
if (mergevideo != null)
|
||||
videos.Add(mergevideo);
|
||||
}
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理单个视频数据
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task<DouyinVideo> ProcessSingleVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data)
|
||||
{
|
||||
if (!IsAwemeValid(item)) return null;
|
||||
|
||||
var v = item.Video.BitRate.FirstOrDefault();
|
||||
if (v == null) return null;
|
||||
|
||||
var videoUrl = v.PlayAddr.UrlList?.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(videoUrl)) return null;
|
||||
|
||||
var (tag1, tag2, tag3) = GetVideoTags(item);
|
||||
var saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
|
||||
var fileName = GetVideoFileName(cookie, item, v);
|
||||
var savePath = Path.Combine(saveFolder, fileName);
|
||||
|
||||
if (File.Exists(savePath)) return null;
|
||||
|
||||
Serilog.Log.Debug($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
if (!await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies))
|
||||
{
|
||||
Serilog.Log.Error($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
|
||||
await GenerateNfoFile(saveFolder, item, tag1, tag2, tag3, avatarSavePath, avatarUrl);
|
||||
return CreateVideoEntity(cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建视频实体
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="bitRate"></param>
|
||||
/// <param name="savePath"></param>
|
||||
/// <param name="saveFolder"></param>
|
||||
/// <param name="tag1"></param>
|
||||
/// <param name="tag2"></param>
|
||||
/// <param name="tag3"></param>
|
||||
/// <param name="avatarSavePath"></param>
|
||||
/// <param name="avatarUrl"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected DouyinVideo CreateVideoEntity(
|
||||
DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder,
|
||||
string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data)
|
||||
{
|
||||
var diffs = GetVideoEntityDifferences(cookie, item);
|
||||
|
||||
return new DouyinVideo
|
||||
{
|
||||
ViedoType = diffs.VideoType.GetHashCode().ToString(),
|
||||
AwemeId = item.AwemeId,
|
||||
Author = item.Author?.Nickname,
|
||||
AuthorId = item.Author?.Uid,
|
||||
AuthorAvatar = avatarSavePath,
|
||||
AuthorAvatarUrl = avatarUrl,
|
||||
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||
VideoTitle = item.Desc,
|
||||
VideoTitleSimplify = diffs.VideoTitleSimplify,
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Resolution = $"{bitRate.PlayAddr.Width}×{bitRate.PlayAddr.Height}",
|
||||
FileSize = bitRate.PlayAddr.DataSize,
|
||||
FileHash = bitRate.PlayAddr.FileHash,
|
||||
Tag1 = tag1,
|
||||
Tag2 = tag2,
|
||||
Tag3 = tag3,
|
||||
VideoUrl = bitRate.PlayAddr.UrlList?.FirstOrDefault(),
|
||||
VideoCoverUrl = item.Video.Cover.UrlList?.FirstOrDefault(),
|
||||
VideoSavePath = savePath,
|
||||
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
|
||||
SyncTime = DateTime.Now,
|
||||
DyUserId = item.AuthorUserId == 0 ? item.Author?.Uid : item.AuthorUserId.ToString(),
|
||||
CookieId = cookie.Id
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存视频信息到数据库
|
||||
/// </summary>
|
||||
/// <param name="videos"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task<int> SaveVideos(List<DouyinVideo> videos)
|
||||
{
|
||||
if (!videos.Any()) return 0;
|
||||
try
|
||||
{
|
||||
await _douyinVideoService.batchInsert(videos);
|
||||
return videos.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"{JobType}-批量保存视频到数据库失败");
|
||||
await CleanupFailedVideos(videos);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 清理保存失败的视频文件
|
||||
/// </summary>
|
||||
/// <param name="videos"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task CleanupFailedVideos(List<DouyinVideo> videos)
|
||||
{
|
||||
foreach (var video in videos)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(Path.GetDirectoryName(video.VideoSavePath)))
|
||||
{
|
||||
Directory.Delete(Path.GetDirectoryName(video.VideoSavePath), recursive: true);
|
||||
}
|
||||
}
|
||||
Serilog.Log.Debug($"{JobType}-因数据库保存失败,已清理本次下载的视频目录");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载视频封面
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="saveFolder"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
|
||||
{
|
||||
var coverUrl = item.Video.Cover.UrlList?.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(coverUrl)) return;
|
||||
|
||||
var coverSavePath = Path.Combine(saveFolder, "poster.jpg");
|
||||
if (File.Exists(coverSavePath)) return;
|
||||
|
||||
if (await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie))
|
||||
{
|
||||
File.Copy(coverSavePath, Path.Combine(saveFolder, "fanart.jpg"), true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载图片合成的视频封面
|
||||
/// </summary>
|
||||
/// <param name="coverUrl"></param>
|
||||
/// <param name="saveFolder"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownVideoCover(string coverUrl, string saveFolder, string cookie)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(coverUrl)) return;
|
||||
var coverImgName = "poster.jpg";
|
||||
var coverSavePath = Path.Combine(saveFolder, coverImgName);
|
||||
|
||||
if (!File.Exists(coverSavePath))
|
||||
{
|
||||
// 封面下载前随机延迟
|
||||
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
|
||||
if (downRes)
|
||||
{
|
||||
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
|
||||
File.Copy(coverSavePath, copyPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 下载作者头像
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task<(string savePath, string url)> DownAuthorAvatar(DouyinUserCookie cookie, Aweme item)
|
||||
{
|
||||
if (item.Author == null) return (null, null);
|
||||
var avatarUrl = item.Author.AvatarLarger?.UrlList?.FirstOrDefault() ?? item.Author.AvatarThumb?.UrlList?.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl)) return (null, null);
|
||||
|
||||
var avatarSavePath = Path.Combine(GetAuthorAvatarBasePath(cookie), $"{item.Author.Uid}.jpg");
|
||||
var avatarDir = Path.GetDirectoryName(avatarSavePath);
|
||||
if (!Directory.Exists(avatarDir)) Directory.CreateDirectory(avatarDir);
|
||||
if (!File.Exists(avatarSavePath))
|
||||
{
|
||||
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie.Cookies);
|
||||
}
|
||||
return (avatarSavePath, avatarUrl);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 处理图片集并合成为视频
|
||||
/// </summary>
|
||||
/// <param name="cookie"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task<DouyinVideo> ProcessImageSetAndMergeToVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string> imageUrls = item.Images?
|
||||
.Where(img => img.UrlList != null && img.UrlList.Any())
|
||||
.Select(img => img.UrlList.FirstOrDefault())
|
||||
.Where(url => !string.IsNullOrWhiteSpace(url))
|
||||
.ToList();
|
||||
|
||||
if (imageUrls == null || !imageUrls.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cookie.ImgSavePath))
|
||||
{
|
||||
Serilog.Log.Error($"{JobType}-图片视频同步-没有配置图片存储路径");
|
||||
return null;
|
||||
}
|
||||
var imageService = _serviceProvider.GetService<ImageMergeToVideoService>();
|
||||
if (imageService == null)
|
||||
{
|
||||
Serilog.Log.Error($"{JobType} -图片视频同步-无法创建 ImageMergeToVideoService。");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var fileNamefolder = Path.Combine(cookie.ImgSavePath, TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId));
|
||||
if (!Directory.Exists(fileNamefolder)) Directory.CreateDirectory(fileNamefolder);
|
||||
var savePath = Path.Combine(fileNamefolder, $"{item.AwemeId}.mp4");
|
||||
|
||||
if (File.Exists(savePath)) return null;
|
||||
var mp3Url = item.Music?.PlayUrl?.UrlList?.FirstOrDefault();
|
||||
|
||||
var reqParams = new MediaMergeRequest
|
||||
{
|
||||
ImageDurationPerSecond = 3,
|
||||
OutputFormat = "mp4",
|
||||
VideoFps = 30,
|
||||
AudioUrls = string.IsNullOrWhiteSpace(mp3Url) ? new List<string>() : new List<string> { mp3Url },
|
||||
ImageUrls = imageUrls,
|
||||
VideoWidth = 1080,
|
||||
VideoHeight = 1920,
|
||||
};
|
||||
|
||||
var mergeResult = await imageService.MergeToVideo(AppContext.BaseDirectory, reqParams, savePath,fileNamefolder);
|
||||
if (!mergeResult)
|
||||
{
|
||||
Serilog.Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
|
||||
return null;
|
||||
}
|
||||
if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0)
|
||||
{
|
||||
Serilog.Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
|
||||
if (Directory.Exists(fileNamefolder))
|
||||
{
|
||||
File.Delete(savePath);
|
||||
Directory.Delete(fileNamefolder, true);
|
||||
Serilog.Log.Error($"{JobType}-图片视频同步-已删除合成失败的视频文件和目录");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 复用基类的方法
|
||||
await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie.Cookies);
|
||||
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
|
||||
|
||||
// 为合成的视频创建一个“虚拟”的BitRate对象,以便复用CreateVideoEntity
|
||||
var virtualBitRate = new VideoBitRate
|
||||
{
|
||||
PlayAddr = new PlayAddr
|
||||
{
|
||||
Width = reqParams.VideoWidth,
|
||||
Height = reqParams.VideoHeight,
|
||||
DataSize = new FileInfo(savePath).Length
|
||||
}
|
||||
};
|
||||
|
||||
var (tag1, tag2, tag3) = GetVideoTags(item);
|
||||
await GenerateNfoFile(fileNamefolder, item, tag1, tag2, tag3, avatarSavePath, avatarUrl);
|
||||
|
||||
var videoEntity = CreateVideoEntity(
|
||||
cookie, item, virtualBitRate, savePath, fileNamefolder,
|
||||
tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
|
||||
|
||||
// 特殊处理合成视频的字段
|
||||
videoEntity.FileHash = string.Empty;
|
||||
videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
|
||||
videoEntity.ViedoType= VideoTypeEnum.ImageVideo.ToString();
|
||||
return videoEntity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"{JobType}-图片视频同步-处理图片集并合成视频时出错");
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成NFO文件
|
||||
/// </summary>
|
||||
protected async Task GenerateNfoFile(string saveFolder, Aweme item, string tag1, string tag2, string tag3,
|
||||
string avatarSavePath, string avatarUrl)
|
||||
{
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new DouyinVideoNfo
|
||||
{
|
||||
Actors = new List<Actor>
|
||||
{
|
||||
new() {
|
||||
Name = item.Author?.Nickname,
|
||||
Role = "主演",
|
||||
Thumb = avatarSavePath
|
||||
}
|
||||
},
|
||||
Author = item.Author?.Nickname,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = item.Desc,
|
||||
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查视频数据是否有效
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <returns></returns>
|
||||
protected bool IsAwemeValid(Aweme item) => item != null && item.Video != null && item.Video.BitRate != null;
|
||||
/// <summary>
|
||||
/// 获取视频标签信息
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <returns></returns>
|
||||
protected (string tag1, string tag2, string tag3) GetVideoTags(Aweme item)
|
||||
{
|
||||
var tags = item.VideoTags;
|
||||
return (
|
||||
tags?.FirstOrDefault(x => x.Level == 1)?.TagName,
|
||||
tags?.FirstOrDefault(x => x.Level == 2)?.TagName,
|
||||
tags?.FirstOrDefault(x => x.Level == 3)?.TagName
|
||||
);
|
||||
}
|
||||
/// <summary>
|
||||
/// AOP
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Task BeforeProcessCookies() => Task.CompletedTask;
|
||||
|
||||
protected abstract Task<List<DouyinUserCookie>> GetValidCookies();
|
||||
protected abstract bool IsCookieValid(DouyinUserCookie cookie);
|
||||
protected abstract Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor);
|
||||
protected abstract bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data);
|
||||
protected abstract string GetNextCursor(DouyinVideoInfo data);
|
||||
protected abstract string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2);
|
||||
protected abstract string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate);
|
||||
protected abstract string GetAuthorAvatarBasePath(DouyinUserCookie cookie);
|
||||
protected abstract Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount);
|
||||
protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item);
|
||||
|
||||
}
|
||||
|
||||
public class VideoEntityDifferences
|
||||
{
|
||||
public VideoTypeEnum VideoType { get; set; }
|
||||
public string VideoTitleSimplify { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace dy.net.model
|
||||
{
|
||||
[SqlSugar.SugarTable(TableName = "login_user_info")]
|
||||
public class LoginUserInfo
|
||||
public class AdminUserInfo
|
||||
{
|
||||
public string UserName { get; set;}
|
||||
public string Password { get; set; }
|
||||
@@ -26,5 +26,14 @@ namespace dy.net.model
|
||||
/// </summary>
|
||||
public bool UperUseViedoTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否下载图片视频
|
||||
/// </summary>
|
||||
public bool DownImageVideo { get; set; }
|
||||
/// <summary>
|
||||
/// 日志保留天数,防止容器日志太多,默认10天
|
||||
/// </summary>
|
||||
public int KeepLogDay { get; set; } = 10;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using SqlSugar;
|
||||
namespace dy.net.model
|
||||
{
|
||||
[SugarTable(TableName = "dy_cookie")]
|
||||
public class DyUserCookies
|
||||
public class DouyinUserCookie
|
||||
{
|
||||
|
||||
[SugarColumn(IsPrimaryKey = true)]
|
||||
@@ -77,32 +77,39 @@ namespace dy.net.model
|
||||
public string UpSavePath { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 图片视频存储路径
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string ImgSavePath { get; set; }
|
||||
|
||||
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public List<DyUpSecUserIdDto> UpSecUserIdsJson
|
||||
public List<DouyinUpSecUserIdDto> UpSecUserIdsJson
|
||||
{
|
||||
get
|
||||
{
|
||||
// 反序列化逻辑(保持不变)
|
||||
if (string.IsNullOrWhiteSpace(UpSecUserIds))
|
||||
{
|
||||
return new List<DyUpSecUserIdDto>();
|
||||
return new List<DouyinUpSecUserIdDto>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<List<DyUpSecUserIdDto>>(UpSecUserIds);
|
||||
return JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(UpSecUserIds);
|
||||
}
|
||||
catch (JsonSerializationException ex)
|
||||
{
|
||||
// 日志记录(按需添加)
|
||||
// Logger.Error($"反序列化失败:{ex.Message},原始值:{UpSecUserIds}");
|
||||
return new List<DyUpSecUserIdDto>();
|
||||
return new List<DouyinUpSecUserIdDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 日志记录(按需添加)
|
||||
// Logger.Error($"处理异常:{ex.Message}");
|
||||
return new List<DyUpSecUserIdDto>();
|
||||
return new List<DouyinUpSecUserIdDto>();
|
||||
}
|
||||
}
|
||||
set
|
||||
@@ -4,7 +4,7 @@ namespace dy.net.model
|
||||
{
|
||||
|
||||
[SugarTable(TableName = "dy_collect_video")]
|
||||
public class DyCollectVideo
|
||||
public class DouyinVideo
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -6,9 +6,9 @@ using SqlSugar;
|
||||
|
||||
namespace dy.net.repository
|
||||
{
|
||||
public class UserRepository : BaseRepository<LoginUserInfo>
|
||||
public class AdminUserRepository : BaseRepository<AdminUserInfo>
|
||||
{
|
||||
public UserRepository(ISqlSugarClient db) : base(db)
|
||||
public AdminUserRepository(ISqlSugarClient db) : base(db)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace dy.net.repository
|
||||
}
|
||||
|
||||
|
||||
public async Task<LoginUserInfo> GetUser(string userName=null)
|
||||
public async Task<AdminUserInfo> GetUser(string userName=null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
@@ -89,7 +89,7 @@ namespace dy.net.repository
|
||||
/// </summary>
|
||||
/// <param name="userInfo"></param>
|
||||
/// <returns></returns>
|
||||
public (int code, string erro) InitUser(LoginUserInfo userInfo)
|
||||
public (int code, string erro) InitUser(AdminUserInfo userInfo)
|
||||
{
|
||||
var isInit = this.GetFirst(x=>!string.IsNullOrWhiteSpace(x.Id));
|
||||
if (isInit!=null)
|
||||
@@ -3,22 +3,22 @@ using SqlSugar;
|
||||
|
||||
namespace dy.net.repository
|
||||
{
|
||||
public class DyCookieRepository : BaseRepository<DyUserCookies>
|
||||
public class DouyinUserCookieRepository : BaseRepository<DouyinUserCookie>
|
||||
{
|
||||
// 注入SQLSugar客户端
|
||||
public DyCookieRepository(ISqlSugarClient db) : base(db)
|
||||
public DouyinUserCookieRepository(ISqlSugarClient db) : base(db)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<List<DyUserCookies>> GetAllCookies()
|
||||
public async Task<List<DouyinUserCookie>> GetAllCookies()
|
||||
{
|
||||
return await this.GetListAsync(x=>x.Status==1);
|
||||
}
|
||||
|
||||
|
||||
public async Task<(List<DyUserCookies> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
|
||||
public async Task<(List<DouyinUserCookie> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
|
||||
{
|
||||
var where = this.Db.Queryable<DyUserCookies>();
|
||||
var where = this.Db.Queryable<DouyinUserCookie>();
|
||||
|
||||
var totalCount = await where.CountAsync();
|
||||
var list = await where.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
@@ -6,10 +6,10 @@ using System.Linq;
|
||||
|
||||
namespace dy.net.repository
|
||||
{
|
||||
public class DyCollectVideoRepository : BaseRepository<DyCollectVideo>
|
||||
public class DouyinVideoRepository : BaseRepository<DouyinVideo>
|
||||
{
|
||||
// 注入SQLSugar客户端
|
||||
public DyCollectVideoRepository(ISqlSugarClient db) : base(db)
|
||||
public DouyinVideoRepository(ISqlSugarClient db) : base(db)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace dy.net.repository
|
||||
/// <param name="tag">可选标签过滤</param>
|
||||
/// <param name="author">可选作者过滤</param>
|
||||
/// <returns>分页结果(视频列表和总数)</returns>
|
||||
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null,string viedoType=null, List<string> dates = null)
|
||||
public async Task<(List<DouyinVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null,string viedoType=null, List<string> dates = null)
|
||||
{
|
||||
|
||||
DateTime? start = null;
|
||||
@@ -38,7 +38,7 @@ namespace dy.net.repository
|
||||
start = Convert.ToDateTime(dates[0]);
|
||||
}
|
||||
|
||||
var where = this.Db.Queryable<DyCollectVideo>()
|
||||
var where = this.Db.Queryable<DouyinVideo>()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(tag), x => x.Tag1 == tag)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
|
||||
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
|
||||
@@ -49,7 +49,7 @@ namespace dy.net.repository
|
||||
var totalCount = await where.CountAsync();
|
||||
var list = await where.OrderByDescending(x=>x.SyncTime).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
if (list.Any()) {
|
||||
var users= await this.Db.Queryable<DyUserCookies>().ToListAsync();
|
||||
var users= await this.Db.Queryable<DouyinUserCookie>().ToListAsync();
|
||||
foreach (var item in list)
|
||||
{
|
||||
var user= users.FirstOrDefault(x=>x.Id== item.CookieId);
|
||||
@@ -72,7 +72,7 @@ namespace dy.net.repository
|
||||
public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId,string ViedoNameSimplify)
|
||||
{
|
||||
|
||||
var video= await this.Db.Queryable<DyCollectVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == "3")
|
||||
var video= await this.Db.Queryable<DouyinVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == "3")
|
||||
.Where(x => x.VideoTitleSimplify == ViedoNameSimplify)
|
||||
.OrderByDescending(x => x.CreateTime).FirstAsync();
|
||||
|
||||
@@ -4,12 +4,12 @@ using dy.net.repository;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
public class UserService
|
||||
public class AdminUserService
|
||||
{
|
||||
|
||||
private readonly UserRepository _userRepository;
|
||||
private readonly AdminUserRepository _userRepository;
|
||||
|
||||
public UserService(UserRepository userRepository)
|
||||
public AdminUserService(AdminUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace dy.net.service
|
||||
return await _userRepository.UpdatePwd(loginUser);
|
||||
}
|
||||
|
||||
public async Task<LoginUserInfo> GetUser(string userName=null)
|
||||
public async Task<AdminUserInfo> GetUser(string userName=null)
|
||||
{
|
||||
return await _userRepository.GetUser(userName);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ namespace dy.net.service
|
||||
|
||||
public (int code, string erro) InitUser()
|
||||
{
|
||||
LoginUserInfo userInfo = new LoginUserInfo
|
||||
AdminUserInfo userInfo = new AdminUserInfo
|
||||
{
|
||||
UserName = "douyin",
|
||||
Password = "douyin2025",
|
||||
@@ -7,11 +7,11 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
public class CommonService
|
||||
public class DouyinCommonService
|
||||
{
|
||||
private readonly ISqlSugarClient sqlSugarClient;
|
||||
|
||||
public CommonService(ISqlSugarClient sqlSugarClient)
|
||||
public DouyinCommonService(ISqlSugarClient sqlSugarClient)
|
||||
{
|
||||
this.sqlSugarClient = sqlSugarClient;
|
||||
}
|
||||
@@ -20,21 +20,31 @@ namespace dy.net.service
|
||||
{
|
||||
return sqlSugarClient.Queryable<AppConfig>().First();
|
||||
}
|
||||
|
||||
public AppConfig InitConfig()
|
||||
/// <summary>
|
||||
/// 初始化并返回配置
|
||||
/// </summary>
|
||||
/// <param name="downLoadImage"></param>
|
||||
/// <returns></returns>
|
||||
public AppConfig InitConfig(bool downLoadImage)
|
||||
{
|
||||
var conf = GetConfig();
|
||||
if (conf != null)
|
||||
{
|
||||
conf.DownImageVideo = downLoadImage;
|
||||
sqlSugarClient.Updateable(conf).ExecuteCommand();
|
||||
return conf;
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConfig config = new AppConfig
|
||||
{
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Cron = "30",
|
||||
BatchCount = 10
|
||||
BatchCount = 10,
|
||||
DownImageVideo = downLoadImage,
|
||||
KeepLogDay = 10
|
||||
};
|
||||
sqlSugarClient.Insertable<AppConfig>(config).ExecuteCommand();
|
||||
sqlSugarClient.Insertable(config).ExecuteCommand();
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -66,7 +76,7 @@ namespace dy.net.service
|
||||
/// </summary>
|
||||
public void UpdateCollectViedoType()
|
||||
{
|
||||
var collectViedos = sqlSugarClient.Queryable<DyCollectVideo>().Where(x => string.IsNullOrWhiteSpace(x.ViedoType)).ToList();
|
||||
var collectViedos = sqlSugarClient.Queryable<DouyinVideo>().Where(x => string.IsNullOrWhiteSpace(x.ViedoType)).ToList();
|
||||
|
||||
if (collectViedos.Any())
|
||||
{
|
||||
@@ -85,7 +95,7 @@ namespace dy.net.service
|
||||
{
|
||||
//var sql = "update dy_cookie set CollHasSyncd=0,FavHasSyncd=0,UperSyncd=0";
|
||||
// sqlSugarClient.Ado.ExecuteCommand(sql) > 0;
|
||||
var cookies = sqlSugarClient.Queryable<DyUserCookies>().ToList();
|
||||
var cookies = sqlSugarClient.Queryable<DouyinUserCookie>().ToList();
|
||||
|
||||
foreach (var cookie in cookies)
|
||||
{
|
||||
@@ -95,7 +105,7 @@ namespace dy.net.service
|
||||
var upers = cookie.UpSecUserIds;
|
||||
if (!string.IsNullOrWhiteSpace(upers))
|
||||
{
|
||||
var uperList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DyUpSecUserIdDto>>(upers);
|
||||
var uperList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(upers);
|
||||
if (uperList != null && uperList.Count > 0)
|
||||
{
|
||||
foreach (var uper in uperList)
|
||||
@@ -5,28 +5,28 @@ using System.Linq.Expressions;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
public class DyCookieService
|
||||
public class DouyinCookieService
|
||||
{
|
||||
|
||||
private readonly DyCookieRepository _cookieRepository;
|
||||
private readonly DouyinUserCookieRepository _cookieRepository;
|
||||
|
||||
public DyCookieService(DyCookieRepository cookieRepository)
|
||||
public DouyinCookieService(DouyinUserCookieRepository cookieRepository)
|
||||
{
|
||||
_cookieRepository = cookieRepository;
|
||||
}
|
||||
|
||||
public Task<List<DyUserCookies>> GetAllCookies()
|
||||
public Task<List<DouyinUserCookie>> GetAllCookies()
|
||||
{
|
||||
return _cookieRepository.GetAllCookies();
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Add(DyUserCookies dyUserCookies)
|
||||
public async Task<bool> Add(DouyinUserCookie dyUserCookies)
|
||||
{
|
||||
return await _cookieRepository.InsertAsync(dyUserCookies);
|
||||
}
|
||||
|
||||
public bool Init()
|
||||
public bool InitCookie()
|
||||
{
|
||||
var initId= "2026";
|
||||
var exist = _cookieRepository.GetFirst(x => x.Id == initId);
|
||||
@@ -34,7 +34,7 @@ namespace dy.net.service
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var cookie = new DyUserCookies
|
||||
var cookie = new DouyinUserCookie
|
||||
{
|
||||
UserName = "douyin",
|
||||
Cookies = "--",
|
||||
@@ -46,25 +46,26 @@ namespace dy.net.service
|
||||
UpSavePath = "/app/uper",
|
||||
CollHasSyncd = 0,
|
||||
FavHasSyncd = 0,
|
||||
UperSyncd = 0
|
||||
UperSyncd = 0,
|
||||
ImgSavePath="/app/images",
|
||||
};
|
||||
return _cookieRepository.Insert(cookie);
|
||||
}
|
||||
|
||||
// 查询单个
|
||||
public async Task<DyUserCookies> GetByIdAsync(string id)
|
||||
public async Task<DouyinUserCookie> GetByIdAsync(string id)
|
||||
{
|
||||
return await _cookieRepository.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
// 查询列表(可加条件)
|
||||
public async Task<(List<DyUserCookies> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
|
||||
public async Task<(List<DouyinUserCookie> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
|
||||
{
|
||||
return await _cookieRepository.GetPagedAsync(pageIndex, pageSize);
|
||||
}
|
||||
|
||||
// 更新
|
||||
public async Task<bool> UpdateAsync(DyUserCookies dyUserCookies)
|
||||
public async Task<bool> UpdateAsync(DouyinUserCookie dyUserCookies)
|
||||
{
|
||||
return await _cookieRepository.UpdateAsync(dyUserCookies);
|
||||
}
|
||||
@@ -7,12 +7,12 @@ using System.Net.Http;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
public class DyHttpClientService
|
||||
public class DouyinHttpClientService
|
||||
{
|
||||
public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme";
|
||||
// 随机数生成器(避免重复实例化,保证随机性)
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
public DyHttpClientService(IHttpClientFactory clientFactory)
|
||||
public DouyinHttpClientService(IHttpClientFactory clientFactory)
|
||||
{
|
||||
_clientFactory = clientFactory;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace dy.net.service
|
||||
/// <param name="count"></param>
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<CollectVideoInfo> SyncCollectVideos(string cursor, string count, string cookie)
|
||||
public async Task<DouyinVideoInfo> SyncCollectVideos(string cursor, string count, string cookie)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cursor))
|
||||
{
|
||||
@@ -50,7 +50,7 @@ namespace dy.net.service
|
||||
}
|
||||
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
|
||||
var dics = DySyncBaseParamDics.CollectParams;
|
||||
var dics = DouyinBaseParamDics.CollectParams;
|
||||
dics["cursor"]=cursor;
|
||||
dics["count"] = count;
|
||||
try
|
||||
@@ -68,7 +68,7 @@ namespace dy.net.service
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
||||
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -92,7 +92,7 @@ namespace dy.net.service
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public async Task<CollectVideoInfo> SyncFavoriteVideos(string count,string cursor, string secUserId, string cookie)
|
||||
public async Task<DouyinVideoInfo> SyncFavoriteVideos(string count,string cursor, string secUserId, string cookie)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cursor))
|
||||
{
|
||||
@@ -118,7 +118,7 @@ namespace dy.net.service
|
||||
}
|
||||
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
|
||||
var dics = DySyncBaseParamDics.FavoriteParams;
|
||||
var dics = DouyinBaseParamDics.FavoriteParams;
|
||||
{
|
||||
// 添加动态参数
|
||||
dics["max_cursor"] = cursor;
|
||||
@@ -133,7 +133,7 @@ namespace dy.net.service
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
||||
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -158,7 +158,7 @@ namespace dy.net.service
|
||||
/// <param name="cookie"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public async Task<CollectVideoInfo> SyncUpderPostVideos(string count, string cursor, string secUserId, string cookie)
|
||||
public async Task<DouyinVideoInfo> SyncUpderPostVideos(string count, string cursor, string secUserId, string cookie)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cursor))
|
||||
{
|
||||
@@ -184,7 +184,7 @@ namespace dy.net.service
|
||||
}
|
||||
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
|
||||
var parameters = DySyncBaseParamDics.UpderPostParams;
|
||||
var parameters = DouyinBaseParamDics.UpderPostParams;
|
||||
{
|
||||
// 添加动态参数
|
||||
parameters["max_cursor"] = cursor;
|
||||
@@ -204,7 +204,7 @@ namespace dy.net.service
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
||||
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3,11 +3,11 @@ using dy.net.job;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
public class QuartzJobService
|
||||
public class DouyinQuartzJobService
|
||||
{
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
|
||||
public QuartzJobService(ISchedulerFactory schedulerFactory)
|
||||
public DouyinQuartzJobService(ISchedulerFactory schedulerFactory)
|
||||
{
|
||||
_schedulerFactory = schedulerFactory;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace dy.net.service
|
||||
/// <returns></returns>
|
||||
public async Task StartJob(string expression,int delay=5000)
|
||||
{
|
||||
await StartCollectJob(expression);
|
||||
//await StartCollectJob(expression);
|
||||
|
||||
await Task.Delay(delay);
|
||||
//如果是数字则加1分钟,减少并发
|
||||
@@ -29,7 +29,7 @@ namespace dy.net.service
|
||||
cron++;
|
||||
expression = cron.ToString();
|
||||
}
|
||||
await StartFavoriteJob(expression);
|
||||
//await StartFavoriteJob(expression);
|
||||
|
||||
await Task.Delay(delay);
|
||||
if (int.TryParse(expression, out int cron2))
|
||||
@@ -55,7 +55,7 @@ namespace dy.net.service
|
||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||
}
|
||||
|
||||
IJobDetail job = JobBuilder.Create<DouYinCollectSyncJob>()
|
||||
IJobDetail job = JobBuilder.Create<DouyinCollectSyncJob>()
|
||||
.WithIdentity(jobKey)
|
||||
.Build();
|
||||
ITrigger trigger;
|
||||
@@ -108,7 +108,7 @@ namespace dy.net.service
|
||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||
}
|
||||
|
||||
IJobDetail job = JobBuilder.Create<DouYinFavoritSyncJob>()
|
||||
IJobDetail job = JobBuilder.Create<DouyinFavoritSyncJob>()
|
||||
.WithIdentity(jobKey)
|
||||
.Build();
|
||||
ITrigger trigger;
|
||||
@@ -160,7 +160,7 @@ namespace dy.net.service
|
||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||
}
|
||||
|
||||
IJobDetail job = JobBuilder.Create<DouYinUperPostSyncJob>()
|
||||
IJobDetail job = JobBuilder.Create<DouyinUperPostSyncJob>()
|
||||
.WithIdentity(jobKey)
|
||||
.Build();
|
||||
ITrigger trigger;
|
||||
@@ -7,18 +7,18 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
public class DyCollectVideoService
|
||||
public class DouyinVideoService
|
||||
{
|
||||
|
||||
private readonly DyCollectVideoRepository _dyCollectVideoRepository;
|
||||
private readonly DouyinVideoRepository _dyCollectVideoRepository;
|
||||
|
||||
public DyCollectVideoService(DyCollectVideoRepository dyCollectVideoRepository)
|
||||
public DouyinVideoService(DouyinVideoRepository dyCollectVideoRepository)
|
||||
{
|
||||
_dyCollectVideoRepository = dyCollectVideoRepository;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> batchInsert(List<DyCollectVideo> videos)
|
||||
public async Task<bool> batchInsert(List<DouyinVideo> videos)
|
||||
{
|
||||
|
||||
// 边界处理:如果传入的列表为空,直接返回成功(或根据业务返回false)
|
||||
@@ -56,7 +56,7 @@ namespace dy.net.service
|
||||
public async Task<VideoStaticsDto> GetStatics()
|
||||
{
|
||||
|
||||
List<DyCollectVideo> list = await this._dyCollectVideoRepository.GetAllAsync();
|
||||
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();
|
||||
@@ -85,7 +85,7 @@ namespace dy.net.service
|
||||
|
||||
//分页查询
|
||||
|
||||
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null, string viedoType = null, List<string>? dates = null)
|
||||
public async Task<(List<DouyinVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null, string viedoType = null, List<string>? dates = null)
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetPagedAsync(pageIndex, pageSize, tag, author, viedoType, dates);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public static class CookieValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// 粗略验证Cookie字符串的格式是否合法
|
||||
/// 核心校验规则:
|
||||
/// 1. 不为空或纯空白字符串
|
||||
/// 2. 由分号分隔的键值对组成(键值对格式为 key=value,key不能为空)
|
||||
/// 3. 键名(key)不包含非法字符(分号、逗号、空格、等号)
|
||||
/// 注:此方法为粗略验证,不严格遵循RFC 6265标准(如未校验控制字符、长度限制等)
|
||||
/// </summary>
|
||||
/// <param name="cookieStr">待验证的Cookie字符串</param>
|
||||
/// <returns>格式是否合法(true=合法,false=非法)</returns>
|
||||
public static bool IsRoughlyValidCookieFormat(string cookieStr)
|
||||
{
|
||||
// 规则1:Cookie字符串不能为空或纯空白
|
||||
if (string.IsNullOrWhiteSpace(cookieStr))
|
||||
{
|
||||
Serilog.Log.Error("验证失败:Cookie字符串为空或纯空白");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 按分号分隔Cookie项(处理项前后的空格,过滤空项)
|
||||
var cookieItems = cookieStr.Split(';')
|
||||
.Select(item => item.Trim()) // 去除项前后空格(如 "a=b; c=d" → ["a=b", "c=d"])
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item)) // 过滤空项(如末尾分号导致的空项)
|
||||
.ToList();
|
||||
|
||||
// 若分割后无有效项(如全是分号或空格)
|
||||
if (!cookieItems.Any())
|
||||
{
|
||||
Serilog.Log.Error("验证失败:Cookie字符串仅包含分隔符或空格");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 定义键名(key)的非法字符(粗略验证,选取最常见的非法字符)
|
||||
char[] invalidKeyChars = { ';', ',', ' ', '=' };
|
||||
|
||||
// 遍历每个Cookie项,验证键值对格式
|
||||
foreach (var item in cookieItems)
|
||||
{
|
||||
// 规则2:每个项必须包含等号(=),且等号不能是第一个字符(保证key非空)
|
||||
int equalsIndex = item.IndexOf('=');
|
||||
if (equalsIndex <= 0) // equalsIndex=0 → 以等号开头(key为空);equalsIndex=-1 → 无等号
|
||||
{
|
||||
Serilog.Log.Error($"验证失败:Cookie项 [{item}] 缺少等号或键名为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取键名(key)并验证
|
||||
string key = item.Substring(0, equalsIndex).Trim(); // 再次Trim以防key前后有空格(如 " key =value")
|
||||
|
||||
// 规则3:键名不能包含非法字符
|
||||
if (key.Any(c => invalidKeyChars.Contains(c)))
|
||||
{
|
||||
Serilog.Log.Error($"验证失败:Cookie项 [{item}] 的键名 [{key}] 包含非法字符(; , 空格 =)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// (可选)粗略验证value:此处不做严格限制,允许value为空(如 "key=")或包含特殊字符
|
||||
}
|
||||
|
||||
// 所有项均通过验证
|
||||
//Serilog.Log.Error("Cookie格式粗略验证通过");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
|
||||
|
||||
public static class DiskInfoHelper
|
||||
{/// <summary>
|
||||
/// 在Docker容器中获取Linux宿主机的本地固定磁盘总空间(GB)
|
||||
/// 前提:宿主机需挂载 /proc 到容器内的 /host/proc(启动时加 -v /proc:/host/proc:ro)
|
||||
/// </summary>
|
||||
/// <returns>总空间字符串(如 "1408.35 GB"),失败时返回错误信息</returns>
|
||||
public static string GetDockerHostTotalDiskSpaceGB()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 检查是否为Linux宿主机(Docker主要运行在Linux上)
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return "仅支持Linux宿主机(Docker容器内)";
|
||||
}
|
||||
|
||||
// 2. 检查宿主机/proc是否已挂载到容器
|
||||
string hostProcMountsPath = "/app/db/mounts";
|
||||
if (!File.Exists(hostProcMountsPath))
|
||||
{
|
||||
return "未检测到宿主机/proc挂载,请使用 -v /proc:/host/proc:ro 启动容器";
|
||||
}
|
||||
|
||||
// 3. 从宿主机/proc/mounts筛选物理磁盘挂载点(排除虚拟文件系统)
|
||||
var physicalMounts = GetHostPhysicalMounts(hostProcMountsPath);
|
||||
if (!physicalMounts.Any())
|
||||
{
|
||||
return "未找到宿主机的物理磁盘挂载点";
|
||||
}
|
||||
|
||||
// 4. 计算所有物理磁盘的总空间(通过df命令获取挂载点的总空间)
|
||||
long totalBytes = 0;
|
||||
foreach (var mountPoint in physicalMounts)
|
||||
{
|
||||
// 执行df命令获取宿主机挂载点的总空间(需容器内有df工具,或通过/proc/diskstats计算)
|
||||
var (success, bytes) = GetMountPointTotalBytes(mountPoint);
|
||||
if (success)
|
||||
{
|
||||
totalBytes += bytes;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalBytes == 0)
|
||||
{
|
||||
return "无法读取宿主机磁盘空间(可能权限不足)";
|
||||
}
|
||||
|
||||
// 5. 转换为GB
|
||||
return ConvertBytesToGb(totalBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"获取宿主机磁盘空间失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从宿主机/proc/mounts筛选物理磁盘挂载点(排除虚拟文件系统)
|
||||
/// </summary>
|
||||
private static List<string> GetHostPhysicalMounts(string hostProcMountsPath)
|
||||
{
|
||||
var physicalMounts = new List<string>();
|
||||
// 虚拟文件系统类型(排除这些类型,剩下的视为物理磁盘相关)
|
||||
var virtualFsTypes = new HashSet<string>
|
||||
{
|
||||
"tmpfs", "sysfs", "proc", "devtmpfs", "devpts", "cgroup", "cgroup2",
|
||||
"securityfs", "pstore", "debugfs", "hugetlbfs", "mqueue", "configfs",
|
||||
"fusectl", "overlay", "squashfs", "overlay2" // overlay/overlay2是Docker自身的存储驱动,需排除
|
||||
};
|
||||
|
||||
foreach (var line in File.ReadAllLines(hostProcMountsPath))
|
||||
{
|
||||
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 3)
|
||||
{
|
||||
string device = parts[0]; // 设备名(如/dev/sda1)
|
||||
string mountPoint = parts[1]; // 挂载点(如/)
|
||||
string fsType = parts[2]; // 文件系统类型
|
||||
|
||||
// 筛选条件:非虚拟文件系统 + 设备名以/dev/开头(物理设备)
|
||||
if (!virtualFsTypes.Contains(fsType) && device.StartsWith("/dev/"))
|
||||
{
|
||||
physicalMounts.Add(mountPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return physicalMounts.Distinct().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过df命令获取宿主机挂载点的总空间(字节)
|
||||
/// (需容器内安装coreutils,或替换为解析/proc/diskstats的逻辑)
|
||||
/// </summary>
|
||||
private static (bool success, long totalBytes) GetMountPointTotalBytes(string hostMountPoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 在容器内执行df命令,指定宿主机的挂载点(需宿主机路径在容器内可见,或通过/proc计算)
|
||||
// 注意:df命令返回的是1K-blocks,需转换为字节(*1024)
|
||||
var process = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "df",
|
||||
Arguments = $"-P {hostMountPoint}", // -P 确保输出格式一致
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
return (false, 0);
|
||||
}
|
||||
|
||||
// 解析df输出(第二行为数据行)
|
||||
var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lines.Length < 2)
|
||||
{
|
||||
return (false, 0);
|
||||
}
|
||||
|
||||
var dataParts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (dataParts.Length >= 2 && long.TryParse(dataParts[1], out long blocks))
|
||||
{
|
||||
return (true, blocks * 1024); // 1K-blocks -> 字节
|
||||
}
|
||||
|
||||
return (false, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字节转GB(1GB = 1024^3字节)
|
||||
/// </summary>
|
||||
private static string ConvertBytesToGb(long bytes)
|
||||
{
|
||||
if (bytes <= 0) return "磁盘空间计算错误";
|
||||
double gb = (double)bytes / (1024 * 1024 * 1024);
|
||||
return $"{gb:F2} GB";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public class DySyncBaseParamDics
|
||||
public class DouyinBaseParamDics
|
||||
{
|
||||
public static Dictionary<string, string> CollectParams { get; } = InitializeUserCollecParams();
|
||||
public static Dictionary<string, string> FavoriteParams { get; } = InitializeUserFavoriteParams();
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace dy.net.utils
|
||||
public static class Md5Util
|
||||
{
|
||||
|
||||
public static string JWT_TOKEN_KEY = "dy.net-key-" + IdGener.GetGuid();
|
||||
public static string JWT_TOKEN_KEY = "dysync.net-key-" + IdGener.GetGuid();
|
||||
public static string Md5(this string inputString)
|
||||
{
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace dy.net.utils
|
||||
public class NfoFileGenerator
|
||||
{
|
||||
|
||||
public static void GenerateNfoFile(VideoNfo videoInfo, string filePath)
|
||||
public static void GenerateNfoFile(DouyinVideoNfo videoInfo, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -44,6 +44,36 @@ namespace dy.net.utils
|
||||
}
|
||||
}
|
||||
|
||||
// --- 新增:处理演员信息 ---
|
||||
if (videoInfo.Actors != null && videoInfo.Actors.Any())
|
||||
{
|
||||
var actorsElement = new XElement("actors");
|
||||
foreach (var actor in videoInfo.Actors)
|
||||
{
|
||||
// 至少需要演员姓名
|
||||
if (!string.IsNullOrWhiteSpace(actor.Name))
|
||||
{
|
||||
var actorElement = new XElement("actor");
|
||||
actorElement.Add(new XElement("name", CleanInvalidXmlChars(actor.Name)));
|
||||
|
||||
// 可选的角色和头像
|
||||
if (!string.IsNullOrWhiteSpace(actor.Role))
|
||||
actorElement.Add(new XElement("role", CleanInvalidXmlChars(actor.Role)));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(actor.Thumb))
|
||||
actorElement.Add(new XElement("thumb", CleanInvalidXmlChars(actor.Thumb)));
|
||||
|
||||
actorsElement.Add(actorElement);
|
||||
}
|
||||
}
|
||||
// 将整个 <actors> 节点添加到根节点
|
||||
if (actorsElement.HasElements)
|
||||
{
|
||||
root.Add(actorsElement);
|
||||
}
|
||||
}
|
||||
// --- 演员信息处理结束 ---
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Thumbnail))
|
||||
root.Add(new XElement("thumb", new XAttribute("aspect", "poster"), CleanInvalidXmlChars(videoInfo.Thumbnail)));
|
||||
|
||||
@@ -58,11 +88,18 @@ namespace dy.net.utils
|
||||
root
|
||||
);
|
||||
|
||||
// 确保目录存在
|
||||
string directory = Path.GetDirectoryName(filePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
doc.Save(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error($"生成 {videoInfo?.Title ?? "未知视频"}, NFO文件时出错: {ex.Message}");
|
||||
Serilog.Log.Error(ex, $"生成 {videoInfo?.Title ?? "未知视频"} 的 NFO 文件时出错。");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
using dy.net.dto;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
/// <summary>
|
||||
/// NFO文件生成器,负责将VideoInfo对象转换为XML格式的NFO文件
|
||||
/// </summary>
|
||||
public class NfoGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 生成视频NFO文件
|
||||
/// </summary>
|
||||
/// <param name="videoInfo">视频信息对象,包含所有需要写入NFO的元数据</param>
|
||||
/// <param name="outputPath">输出文件的完整路径,包括文件名</param>
|
||||
public void GenerateNfoFile(VideoNFOInfo videoInfo, string outputPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建根元素,电影用"movie",电视剧集用"episodedetails",电视节目用"tvshow"
|
||||
XElement root = new XElement("movie");
|
||||
|
||||
// 添加基本信息
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Title))
|
||||
root.Add(new XElement("title", videoInfo.Title));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.OriginalTitle))
|
||||
root.Add(new XElement("originaltitle", videoInfo.OriginalTitle));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.SortTitle))
|
||||
root.Add(new XElement("sorttitle", videoInfo.SortTitle));
|
||||
|
||||
if (videoInfo.Year > 0)
|
||||
root.Add(new XElement("year", videoInfo.Year));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Plot))
|
||||
root.Add(new XElement("plot", videoInfo.Plot));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Outline))
|
||||
root.Add(new XElement("outline", videoInfo.Outline));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Tagline))
|
||||
root.Add(new XElement("tagline", videoInfo.Tagline));
|
||||
|
||||
// 添加人员信息
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Director))
|
||||
root.Add(new XElement("director", videoInfo.Director));
|
||||
|
||||
// 添加演员
|
||||
foreach (var actor in videoInfo.Actors)
|
||||
{
|
||||
root.Add(new XElement("actor",
|
||||
new XElement("name", actor)
|
||||
));
|
||||
}
|
||||
|
||||
// 添加编剧
|
||||
foreach (var writer in videoInfo.Writers)
|
||||
{
|
||||
root.Add(new XElement("writer", writer));
|
||||
}
|
||||
|
||||
// 添加媒体信息
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Genre))
|
||||
root.Add(new XElement("genre", videoInfo.Genre));
|
||||
|
||||
if (videoInfo.Rating > 0)
|
||||
root.Add(new XElement("rating", videoInfo.Rating));
|
||||
|
||||
if (videoInfo.Votes > 0)
|
||||
root.Add(new XElement("votes", videoInfo.Votes));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Studio))
|
||||
root.Add(new XElement("studio", videoInfo.Studio));
|
||||
|
||||
if (videoInfo.Premiered.HasValue)
|
||||
root.Add(new XElement("premiered", videoInfo.Premiered.Value.ToString("yyyy-MM-dd")));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Runtime))
|
||||
root.Add(new XElement("runtime", videoInfo.Runtime));
|
||||
|
||||
// 添加文件信息
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.FileName))
|
||||
root.Add(new XElement("filenameandpath", videoInfo.FileName));
|
||||
|
||||
if (videoInfo.FileSize > 0)
|
||||
root.Add(new XElement("filesize", videoInfo.FileSize));
|
||||
|
||||
// 添加新增字段
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Country))
|
||||
root.Add(new XElement("country", videoInfo.Country));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Language))
|
||||
root.Add(new XElement("language", videoInfo.Language));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.VideoCodec))
|
||||
root.Add(new XElement("codec", videoInfo.VideoCodec));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.AudioCodec))
|
||||
root.Add(new XElement("audiocodec", videoInfo.AudioCodec));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Resolution))
|
||||
root.Add(new XElement("resolution", videoInfo.Resolution));
|
||||
|
||||
// 创建文档并保存
|
||||
XDocument doc = new XDocument(
|
||||
new XDeclaration("1.0", "UTF-8", "yes"),
|
||||
root
|
||||
);
|
||||
|
||||
// 确保目录存在
|
||||
var directory = Path.GetDirectoryName(outputPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
doc.Save(outputPath);
|
||||
Console.WriteLine($"NFO文件已生成: {outputPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"生成NFO文件时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,17 @@ namespace dy.net.utils
|
||||
/// 非法字符替换后的占位符(也可设为空字符串)
|
||||
/// </summary>
|
||||
private const string IllegalCharReplacement = "";
|
||||
|
||||
|
||||
// 修正:使用 \U 前缀来表示超过 \uFFFF 的Unicode码点
|
||||
private static readonly Regex _emojiRegex = new Regex(
|
||||
@"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\U0001E000-\U0001EFFF\u2600-\u2B55\u200D]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex _hashtagRegex = new Regex(@"\#\S+", RegexOptions.Compiled);
|
||||
private static readonly Regex _invalidCharsRegex;
|
||||
private static readonly Regex _multipleUnderscoresRegex = new Regex(@"_+", RegexOptions.Compiled);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 处理抖音视频文件名
|
||||
@@ -42,7 +53,7 @@ namespace dy.net.utils
|
||||
// 3. 长度控制:按UTF-8字节数截断(避免超系统限制)
|
||||
string truncatedTitle = TruncateByByteLength(purifiedTitle, MaxFileNameBytes);
|
||||
|
||||
return truncatedTitle;
|
||||
return truncatedTitle.Trim();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -57,7 +68,7 @@ namespace dy.net.utils
|
||||
title = id;
|
||||
}
|
||||
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
||||
title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
|
||||
//title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
|
||||
|
||||
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
|
||||
string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
|
||||
@@ -81,7 +92,7 @@ namespace dy.net.utils
|
||||
title = Regex.Replace(title, $"{Separator}+", Separator.ToString(), RegexOptions.Compiled);
|
||||
|
||||
// 步骤6:移除首尾无效字符(分隔符、点号)
|
||||
title = title.Trim(Separator, '.');
|
||||
title = title.Replace(" ","").Trim(Separator, '.');
|
||||
|
||||
// 容错:如果净化后为空,返回默认值
|
||||
return string.IsNullOrWhiteSpace(title) ? id : title;
|
||||
@@ -122,6 +133,8 @@ namespace dy.net.utils
|
||||
|
||||
// 清理路径中的特殊字符(避免创建文件夹失败)
|
||||
public static string SanitizePath(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
@@ -129,11 +142,11 @@ namespace dy.net.utils
|
||||
}
|
||||
|
||||
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
||||
path = Regex.Replace(path, @"#\S+", "", RegexOptions.Compiled);
|
||||
//path = Regex.Replace(path, @"#\S+", "", RegexOptions.Compiled);
|
||||
|
||||
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
|
||||
string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
|
||||
path = Regex.Replace(path, emojiPattern, "", RegexOptions.Compiled);
|
||||
//string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
|
||||
//path = Regex.Replace(path, emojiPattern, "", RegexOptions.Compiled);
|
||||
|
||||
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
@@ -144,7 +157,12 @@ namespace dy.net.utils
|
||||
{
|
||||
path = path.Substring(0, 50);
|
||||
}
|
||||
return path.Trim();
|
||||
return path.Trim().Replace(" ", "");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user