1、增加图片视频
2、优化代码结构 3、up主下载分类逻辑可配置
This commit is contained in:
@@ -17,8 +17,8 @@ namespace dy.net.Controllers
|
|||||||
{
|
{
|
||||||
private readonly IWebHostEnvironment webHostEnvironment;
|
private readonly IWebHostEnvironment webHostEnvironment;
|
||||||
|
|
||||||
private readonly UserService _userService;
|
private readonly AdminUserService _userService;
|
||||||
public AuthController(UserService userService, IWebHostEnvironment webHostEnvironment )
|
public AuthController(AdminUserService userService, IWebHostEnvironment webHostEnvironment )
|
||||||
{
|
{
|
||||||
_userService=userService;
|
_userService=userService;
|
||||||
this.webHostEnvironment = webHostEnvironment;
|
this.webHostEnvironment = webHostEnvironment;
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ namespace dy.net.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class ConfigController : ControllerBase
|
public class ConfigController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DyCookieService dyCookieService;
|
private readonly DouyinCookieService dyCookieService;
|
||||||
|
|
||||||
private readonly CommonService commonService;
|
private readonly DouyinCommonService commonService;
|
||||||
private readonly QuartzJobService quartzJobService;
|
private readonly DouyinQuartzJobService quartzJobService;
|
||||||
|
|
||||||
public ConfigController(DyCookieService dyCookieService, CommonService commonService,QuartzJobService quartzJobService)
|
public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService,DouyinQuartzJobService quartzJobService)
|
||||||
{
|
{
|
||||||
this.dyCookieService = dyCookieService;
|
this.dyCookieService = dyCookieService;
|
||||||
this.commonService = commonService;
|
this.commonService = commonService;
|
||||||
@@ -55,7 +55,7 @@ namespace dy.net.Controllers
|
|||||||
/// 新增用户Cookie
|
/// 新增用户Cookie
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("add")]
|
[HttpPost("add")]
|
||||||
public async Task<IActionResult> AddAsync([FromBody] DyUserCookies dyUserCookies)
|
public async Task<IActionResult> AddAsync([FromBody] DouyinUserCookie dyUserCookies)
|
||||||
{
|
{
|
||||||
if(dyUserCookies.UpSecUserIdsJson!=null)
|
if(dyUserCookies.UpSecUserIdsJson!=null)
|
||||||
{
|
{
|
||||||
@@ -74,7 +74,7 @@ namespace dy.net.Controllers
|
|||||||
/// 更新用户Cookie
|
/// 更新用户Cookie
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("update")]
|
[HttpPost("update")]
|
||||||
public async Task<IActionResult> UpdateAsync([FromBody] DyUserCookies dyUserCookies)
|
public async Task<IActionResult> UpdateAsync([FromBody] DouyinUserCookie dyUserCookies)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (dyUserCookies.UpSecUserIdsJson != null )
|
if (dyUserCookies.UpSecUserIdsJson != null )
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ namespace dy.net.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class VideoController : ControllerBase
|
public class VideoController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DyCollectVideoService dyCollectVideoService;
|
private readonly DouyinVideoService dyCollectVideoService;
|
||||||
|
|
||||||
public VideoController(DyCollectVideoService dyCollectVideoService)
|
public VideoController(DouyinVideoService dyCollectVideoService)
|
||||||
{
|
{
|
||||||
this.dyCollectVideoService = dyCollectVideoService;
|
this.dyCollectVideoService = dyCollectVideoService;
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ namespace dy.net.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
[HttpPost("paged")]
|
[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);
|
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Author,dto.ViedoType,dto.Dates);
|
||||||
return Ok(new
|
return Ok(new
|
||||||
|
|||||||
+15
-3
@@ -2,9 +2,21 @@
|
|||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||||
WORKDIR /app
|
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 . .
|
COPY . .
|
||||||
ENV ASPNETCORE_URLS http://*:10101
|
ENV ASPNETCORE_URLS=http://*:10102
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
ENV DOWN_IMGVIDEO=1
|
||||||
ENTRYPOINT ["dotnet", "dy.net.dll"]
|
ENTRYPOINT ["dotnet", "dy.net.dll"]
|
||||||
ENV TZ=Asia/Shanghai
|
|
||||||
@@ -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
|
public class Program
|
||||||
{
|
{
|
||||||
// 常量定义
|
// 常量定义
|
||||||
private const string DefaultListenUrl = "http://*:10101";
|
private static string DefaultListenUrl = "http://*:10102";
|
||||||
private const string SpaRootPath = "app/dist";
|
private const string SpaRootPath = "app/dist";
|
||||||
private const string SpaSourcePath = "app/";
|
private const string SpaSourcePath = "app/";
|
||||||
private const string SwaggerDocTitle = "dy.net WebApi Docs";
|
private const string SwaggerDocTitle = "dy.net WebApi Docs";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打包时注意,如果是false,前端不允许修改开启下载图片和视频的选项
|
||||||
|
/// </summary>
|
||||||
|
private static bool downImageVideo = false;
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
// 初始化编码提供器
|
// 初始化编码提供器
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
// 构建Web应用
|
// 构建Web应用
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
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();
|
var isDevelopment = builder.Environment.IsDevelopment();
|
||||||
|
|
||||||
// 配置主机
|
// 配置主机
|
||||||
ConfigureHost(builder, isDevelopment);
|
ConfigureHost(builder, isDevelopment);
|
||||||
|
|
||||||
// 配置服务
|
// 配置服务
|
||||||
var services = builder.Services;
|
ConfigureServices(builder.Services, builder.Configuration, builder.Environment);
|
||||||
ConfigureServices(services, builder.Configuration, isDevelopment);
|
|
||||||
|
|
||||||
// 构建应用
|
// 构建应用
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
Log.Debug("DOWN_IMGVIDEO=" + downImgConfig);
|
||||||
|
|
||||||
// 配置中间件
|
// 配置中间件
|
||||||
ConfigureMiddleware(app, isDevelopment);
|
ConfigureMiddleware(app, builder.Environment);
|
||||||
|
|
||||||
// 初始化应用服务
|
// 初始化应用服务
|
||||||
InitApplicationServices(app);
|
InitApplicationServices(app);
|
||||||
|
|
||||||
// 启动应用
|
// 启动应用
|
||||||
Serilog.Log.Debug("dy.net service started successfully");
|
Log.Debug("dysync.net service started successfully");
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,8 +92,10 @@ namespace dy.net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置依赖注入服务
|
/// 配置依赖注入服务
|
||||||
/// </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生成器
|
// 雪花ID生成器
|
||||||
services.AddSnowFlakeId(options => options.WorkId = new Random().Next(1, 127));
|
services.AddSnowFlakeId(options => options.WorkId = new Random().Next(1, 127));
|
||||||
|
|
||||||
@@ -93,17 +109,23 @@ namespace dy.net
|
|||||||
services.AddSqlsugar(config);
|
services.AddSqlsugar(config);
|
||||||
|
|
||||||
// 定时任务
|
// 定时任务
|
||||||
services.AddQuartzService(config);
|
services.AddQuartzService();
|
||||||
|
|
||||||
// 仓储和服务注册
|
// 仓储和服务注册
|
||||||
services.AddServicesFromNamespace("dy.net.repository")
|
services.AddServicesFromNamespace("dy.net.repository")
|
||||||
.AddServicesFromNamespace("dy.net.service");
|
.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静态文件支持
|
// SPA静态文件支持
|
||||||
services.AddSpaStaticFiles(options => options.RootPath = SpaRootPath);
|
services.AddSpaStaticFiles(options => options.RootPath = SpaRootPath);
|
||||||
|
|
||||||
// 开发环境启用Swagger
|
// 开发环境启用Swagger
|
||||||
if (isDevelopment)
|
if (environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
services.AddSwagger();
|
services.AddSwagger();
|
||||||
}
|
}
|
||||||
@@ -120,13 +142,13 @@ namespace dy.net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置中间件
|
/// 配置中间件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ConfigureMiddleware(WebApplication app, bool isDevelopment)
|
private static void ConfigureMiddleware(WebApplication app, IWebHostEnvironment environment)
|
||||||
{
|
{
|
||||||
// 响应压缩
|
// 响应压缩
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
// 开发环境启用SwaggerUI
|
// 开发环境启用SwaggerUI
|
||||||
if (isDevelopment)
|
if (environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseCustomSwaggerUI(options => options.Title = SwaggerDocTitle);
|
app.UseCustomSwaggerUI(options => options.Title = SwaggerDocTitle);
|
||||||
}
|
}
|
||||||
@@ -145,7 +167,7 @@ namespace dy.net
|
|||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
// 生产环境启用SPA
|
// 生产环境启用SPA
|
||||||
if (!isDevelopment)
|
if (!environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseSpaStaticFiles();
|
app.UseSpaStaticFiles();
|
||||||
app.UseSpa(spa => spa.Options.SourcePath = SpaSourcePath);
|
app.UseSpa(spa => spa.Options.SourcePath = SpaSourcePath);
|
||||||
@@ -163,23 +185,23 @@ namespace dy.net
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 初始化用户
|
// 初始化用户
|
||||||
var userService = services.GetRequiredService<UserService>();
|
var userService = services.GetRequiredService<AdminUserService>();
|
||||||
userService.InitUser();
|
userService.InitUser();
|
||||||
|
|
||||||
// 初始化Cookie
|
// 初始化Cookie
|
||||||
var cookieService = services.GetRequiredService<DyCookieService>();
|
var cookieService = services.GetRequiredService<DouyinCookieService>();
|
||||||
cookieService.Init();
|
cookieService.InitCookie();
|
||||||
|
|
||||||
// 初始化配置
|
// 初始化配置
|
||||||
var commonService = services.GetRequiredService<CommonService>();
|
var commonService = services.GetRequiredService<DouyinCommonService>();
|
||||||
var config = commonService.InitConfig();
|
var config = commonService.InitConfig(downImageVideo);
|
||||||
|
|
||||||
// 更新收藏视频类型--兼容老版本-原来的旧数据没有这个类型字段
|
// 更新收藏视频类型--兼容老版本-原来的旧数据没有这个类型字段
|
||||||
commonService.UpdateCollectViedoType();
|
commonService.UpdateCollectViedoType();
|
||||||
// 重置博主作品同步状态为未同步
|
// 重置博主作品同步状态为未同步
|
||||||
commonService.UpdateAllCookieSyncedToZero();
|
commonService.UpdateAllCookieSyncedToZero();
|
||||||
// 启动定时任务
|
// 启动定时任务
|
||||||
var quartzJobService = services.GetRequiredService<QuartzJobService>();
|
var quartzJobService = services.GetRequiredService<DouyinQuartzJobService>();
|
||||||
quartzJobService.StartJob(config?.Cron ?? "30");
|
quartzJobService.StartJob(config?.Cron ?? "30");
|
||||||
|
|
||||||
Serilog.Log.Debug("系统初始化完成,会默认将-博主作品同步功能-同步全部作品重置为关闭(若要开启,可以到抖音授权页面中修改)");
|
Serilog.Log.Debug("系统初始化完成,会默认将-博主作品同步功能-同步全部作品重置为关闭(若要开启,可以到抖音授权页面中修改)");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
<_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 />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
Binary file not shown.
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dbconn": "",
|
"dbconn": "",
|
||||||
"dbtype": "Sqlite"
|
"dbtype": "Sqlite",
|
||||||
|
"DOWN_IMGVIDEO": "1",
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace dy.net.dto
|
namespace dy.net.dto
|
||||||
{
|
{
|
||||||
public class DyUpSecUserIdDto
|
public class DouyinUpSecUserIdDto
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -4,7 +4,7 @@ namespace dy.net.dto
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
public class CollectVideoInfo
|
public class DouyinVideoInfo
|
||||||
{
|
{
|
||||||
[JsonProperty("aweme_list")]
|
[JsonProperty("aweme_list")]
|
||||||
public List<Aweme> AwemeList { get; set; }
|
public List<Aweme> AwemeList { get; set; }
|
||||||
@@ -1267,7 +1267,8 @@ namespace dy.net.dto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> url_list { get; set; }
|
[JsonProperty("url_list")]
|
||||||
|
public List<string> UrlList { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
namespace dy.net.dto
|
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 DateTime? ReleaseDate { get; set; } // 可空,避免无发布时间时的默认值
|
||||||
public IEnumerable<string> Genres { get; set; } // 分类标签集合(如 ["动作", "科幻"])
|
public IEnumerable<string> Genres { get; set; } // 分类标签集合(如 ["动作", "科幻"])
|
||||||
|
|
||||||
|
public List<Actor> Actors { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace dy.net.dto
|
namespace dy.net.dto
|
||||||
{
|
{
|
||||||
public class VideoPageRequestDTO: PageRequestDto
|
public class DouyinVideoPageRequestDto : PageRequestDto
|
||||||
{
|
{
|
||||||
// 3. 引用类型(string)如果允许为null,显式声明为 string?
|
// 3. 引用类型(string)如果允许为null,显式声明为 string?
|
||||||
public string? Tag { get; set; }
|
public string? Tag { get; set; }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace dy.net.dto
|
namespace dy.net.dto
|
||||||
{
|
{
|
||||||
public class CollectVideoRequestDto
|
public class DouyinVideoRequestDto
|
||||||
{
|
{
|
||||||
public int cursor { get; set; }
|
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>
|
<ItemGroup>
|
||||||
<Compile Remove="app\**" />
|
<Compile Remove="app\**" />
|
||||||
|
<Compile Remove="expand\**" />
|
||||||
<Compile Remove="logs\**" />
|
<Compile Remove="logs\**" />
|
||||||
<Content Remove="app\**" />
|
<Content Remove="app\**" />
|
||||||
|
<Content Remove="expand\**" />
|
||||||
<Content Remove="logs\**" />
|
<Content Remove="logs\**" />
|
||||||
<EmbeddedResource Remove="app\**" />
|
<EmbeddedResource Remove="app\**" />
|
||||||
|
<EmbeddedResource Remove="expand\**" />
|
||||||
<EmbeddedResource Remove="logs\**" />
|
<EmbeddedResource Remove="logs\**" />
|
||||||
<None Remove="app\**" />
|
<None Remove="app\**" />
|
||||||
|
<None Remove="expand\**" />
|
||||||
<None Remove="logs\**" />
|
<None Remove="logs\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -68,14 +72,21 @@
|
|||||||
<None Include="dy.net.sln" />
|
<None Include="dy.net.sln" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="expand\dy.image\dy.image.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="db\dy.sqlite">
|
<None Update="db\dy.sqlite">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Dockerfile">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="Dockerfile-arm">
|
<None Update="Dockerfile-arm">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<None Update="Dockerfile">
|
<None Update="Dockerfile-dev">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+12
-1
@@ -1,10 +1,14 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.0.11010.61 d18.0
|
VisualStudioVersion = 18.0.11010.61
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dy.net", "dy.net.csproj", "{680660EF-ACAE-43A9-AB6C-B75532E758AD}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dy.net", "dy.net.csproj", "{680660EF-ACAE-43A9-AB6C-B75532E758AD}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{680660EF-ACAE-43A9-AB6C-B75532E758AD}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{38404DA7-A852-4960-9E95-8F70459AEB60} = {1503FF41-A045-48DF-B92C-DB81BAAAFE5B}
|
||||||
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {8238199F-0C28-42F2-ACFD-E8CBCCB2AB80}
|
SolutionGuid = {8238199F-0C28-42F2-ACFD-E8CBCCB2AB80}
|
||||||
EndGlobalSection
|
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>
|
/// </summary>
|
||||||
/// <param name="services"></param>
|
/// <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<DouyinCollectSyncJob>();
|
||||||
services.AddTransient<DouYinFavoritSyncJob>();
|
//services.AddTransient<DouyinFavoritSyncJob>();
|
||||||
services.AddTransient<DouYinUperPostSyncJob>();
|
//services.AddTransient<DouyinUperPostSyncJob>();
|
||||||
|
|
||||||
// 注册Quartz服务
|
// 注册Quartz服务
|
||||||
services.AddQuartz();
|
services.AddQuartz();
|
||||||
|
|
||||||
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||||
|
|
||||||
services.AddTransient<QuartzJobService>();
|
services.AddTransient<DouyinQuartzJobService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -264,8 +264,12 @@ namespace dy.net.extension
|
|||||||
?? ServiceLifetime.Transient;
|
?? ServiceLifetime.Transient;
|
||||||
|
|
||||||
// 查找该类实现的接口(优先注册为接口服务)
|
// 查找该类实现的接口(优先注册为接口服务)
|
||||||
|
//var interfaces = type.GetInterfaces()
|
||||||
|
// .Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
|
||||||
|
// .ToList();
|
||||||
|
// 查找该类实现的接口(排除 IDisposable)
|
||||||
var interfaces = type.GetInterfaces()
|
var interfaces = type.GetInterfaces()
|
||||||
.Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
|
.Where(i => i != typeof(IDisposable)) // <-- 关键修正
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (interfaces.Any())
|
if (interfaces.Any())
|
||||||
@@ -366,20 +370,7 @@ namespace dy.net.extension
|
|||||||
{
|
{
|
||||||
Version = "v1",
|
Version = "v1",
|
||||||
Title = "dy.net API Swagger Document",
|
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.OrderActionsBy((apiDesc) => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.HttpMethod}");
|
||||||
o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
|
o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
|
||||||
|
|||||||
+72
-342
@@ -1,367 +1,97 @@
|
|||||||
using ClockSnowFlake;
|
using dy.net.dto;
|
||||||
using dy.net.dto;
|
|
||||||
using dy.net.model;
|
using dy.net.model;
|
||||||
using dy.net.service;
|
using dy.net.service;
|
||||||
using dy.net.utils;
|
using dy.net.utils;
|
||||||
using Quartz;
|
|
||||||
|
|
||||||
namespace dy.net.job
|
namespace dy.net.job
|
||||||
{
|
{
|
||||||
[DisallowConcurrentExecution]
|
public class DouyinCollectSyncJob : DouyinBaseSyncJob
|
||||||
public class DouYinCollectSyncJob : IJob
|
|
||||||
{
|
{
|
||||||
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;
|
protected override async Task BeforeProcessCookies()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_dyCookieService = dyCookieService;
|
var now = DateTime.Now;
|
||||||
_douyinService = dyHttpClientService;
|
if (now.Hour == 1 && now.Minute < 30)
|
||||||
_douyinVideoService = dyCollectVideoService;
|
{
|
||||||
this.commonService = commonService;
|
_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();
|
var cookies = await _dyCookieService.GetAllCookies();
|
||||||
cookies= cookies.Where(c => !string.IsNullOrWhiteSpace(c.SavePath)).ToList();
|
return cookies.Where(c => !string.IsNullOrWhiteSpace(c.SavePath)).ToList();
|
||||||
if (cookies == null || !cookies.Any())
|
}
|
||||||
|
|
||||||
|
protected override bool IsCookieValid(DouyinUserCookie cookie)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 &&
|
||||||
|
!string.IsNullOrWhiteSpace(cookie.SavePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
|
||||||
|
{
|
||||||
|
return await _douyinService.SyncCollectVideos(cursor, count, cookie.Cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
|
||||||
|
{
|
||||||
|
return data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetNextCursor(DouyinVideoInfo data)
|
||||||
|
{
|
||||||
|
return data?.Cursor ?? "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.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,任务终止");
|
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
||||||
return;
|
cookie.CollHasSyncd = 1;
|
||||||
|
await _dyCookieService.UpdateAsync(cookie);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//Serilog.Log.Debug($"collect-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
|
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||||
//return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//每天凌晨1点到1点半之间执行一次重置同步状态-全部cookie
|
|
||||||
UpdateCookieSyncedToZero();
|
|
||||||
|
|
||||||
foreach (var cookie in cookies)
|
|
||||||
{
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DyCollectVideo> videos = new List<DyCollectVideo>();
|
|
||||||
foreach (var item in data.AwemeList)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 构造视频数据
|
|
||||||
DyCollectVideo video = new()
|
|
||||||
{
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Debug("collect-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//Serilog.Log.Debug($"没有查询到新的视频");
|
|
||||||
}
|
|
||||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
|
||||||
}
|
|
||||||
if (syncCount > 0)
|
|
||||||
{
|
|
||||||
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
|
||||||
// 更新同步状态为已同步
|
|
||||||
cookie.CollHasSyncd = 1;
|
|
||||||
await _dyCookieService.UpdateAsync(cookie);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Error($"collect-处理Cookie[{cookie.Id}]时出错:{ex.Message}");
|
|
||||||
Serilog.Log.Error($"collect-处理Cookie[{cookie.Id}]时出错22:{ex.StackTrace}");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 下载视频封面
|
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
|
||||||
/// </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)
|
|
||||||
{
|
{
|
||||||
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
return new VideoEntityDifferences
|
||||||
? 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))
|
|
||||||
{
|
{
|
||||||
// 封面下载前随机延迟
|
VideoType = VideoTypeEnum.Collect,
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+65
-325
@@ -1,349 +1,89 @@
|
|||||||
using ClockSnowFlake;
|
using dy.net.dto;
|
||||||
using dy.net.dto;
|
|
||||||
using dy.net.model;
|
using dy.net.model;
|
||||||
using dy.net.service;
|
using dy.net.service;
|
||||||
using dy.net.utils;
|
using dy.net.utils;
|
||||||
using Quartz;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace dy.net.job
|
namespace dy.net.job
|
||||||
{
|
{
|
||||||
[DisallowConcurrentExecution]
|
public class DouyinFavoritSyncJob : DouyinBaseSyncJob
|
||||||
public class DouYinFavoritSyncJob : IJob
|
|
||||||
{
|
{
|
||||||
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;
|
protected override string JobType => "favorite";
|
||||||
|
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_dyCookieService = dyCookieService;
|
var cookies = await _dyCookieService.GetAllCookies();
|
||||||
_douyinService = dyHttpClientService;
|
return cookies.Where(c => !string.IsNullOrWhiteSpace(c.FavSavePath)).ToList();
|
||||||
_douyinVideoService = dyCollectVideoService;
|
|
||||||
this.commonService = commonService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext context)
|
protected override bool IsCookieValid(DouyinUserCookie cookie)
|
||||||
{
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 &&
|
||||||
|
!string.IsNullOrWhiteSpace(cookie.FavSavePath) &&
|
||||||
|
!string.IsNullOrWhiteSpace(cookie.SecUserId) && cookie.SecUserId.Length >= 10;
|
||||||
|
}
|
||||||
|
|
||||||
var config = commonService.GetConfig();
|
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
|
||||||
if (config == null)
|
{
|
||||||
|
return await _douyinService.SyncFavoriteVideos(count, cursor, cookie.SecUserId, cookie.Cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
|
||||||
|
{
|
||||||
|
return data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.FavSavePath, "author");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount)
|
||||||
|
{
|
||||||
|
if (syncCount > 0)
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug("favorite-请先在设置中初始化配置,再执行同步任务");
|
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
||||||
return;
|
cookie.FavHasSyncd = 1;
|
||||||
}
|
await _dyCookieService.UpdateAsync(cookie);
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug($"favorite-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
|
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||||
//return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var cookie in cookies)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DyCollectVideo> videos = new List<DyCollectVideo>();
|
|
||||||
foreach (var item in data.AwemeList)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 构造视频数据
|
|
||||||
DyCollectVideo video = new()
|
|
||||||
{
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Debug("favorite-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//Serilog.Log.Debug($"没有查询到新的视频");
|
|
||||||
}
|
|
||||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
|
||||||
}
|
|
||||||
if (syncCount > 0)
|
|
||||||
{
|
|
||||||
Serilog.Log.Debug($"Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
|
|
||||||
// 更新同步状态为已同步
|
|
||||||
cookie.FavHasSyncd = 1;
|
|
||||||
await _dyCookieService.UpdateAsync(cookie);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错:{ex.Message}");
|
|
||||||
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错22:{ex.StackTrace}");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
|
||||||
/// 下载视频封面
|
|
||||||
/// </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)
|
|
||||||
{
|
{
|
||||||
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
return new VideoEntityDifferences
|
||||||
? 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))
|
|
||||||
{
|
{
|
||||||
// 封面下载前随机延迟
|
VideoType = VideoTypeEnum.Favorite
|
||||||
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)) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+83
-353
@@ -1,381 +1,111 @@
|
|||||||
using ClockSnowFlake;
|
using dy.net.dto;
|
||||||
using dy.net.dto;
|
|
||||||
using dy.net.model;
|
using dy.net.model;
|
||||||
using dy.net.service;
|
using dy.net.service;
|
||||||
using dy.net.utils;
|
using dy.net.utils;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Quartz;
|
using System.IO;
|
||||||
using SqlSugar.Extensions;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace dy.net.job
|
namespace dy.net.job
|
||||||
{
|
{
|
||||||
[DisallowConcurrentExecution]
|
public class DouyinUperPostSyncJob : DouyinBaseSyncJob
|
||||||
public class DouYinUperPostSyncJob : IJob
|
|
||||||
{
|
{
|
||||||
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;
|
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_dyCookieService = dyCookieService;
|
var cookies = await _dyCookieService.GetAllCookies();
|
||||||
_douyinService = dyHttpClientService;
|
return cookies.Where(x => !string.IsNullOrWhiteSpace(x.UpSecUserIds) && !string.IsNullOrWhiteSpace(x.UpSavePath)).ToList();
|
||||||
_douyinVideoService = dyCollectVideoService;
|
|
||||||
this.commonService = commonService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext context)
|
protected override bool IsCookieValid(DouyinUserCookie cookie)
|
||||||
{
|
{
|
||||||
var config = commonService.GetConfig();
|
return !string.IsNullOrWhiteSpace(cookie.Cookies) && !string.IsNullOrWhiteSpace(cookie.UpSavePath) && !string.IsNullOrWhiteSpace(cookie.UpSecUserIds);
|
||||||
if (config == null)
|
}
|
||||||
{
|
|
||||||
Serilog.Log.Debug("dyuploder-请先在设置中初始化配置,再执行同步任务");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (config.BatchCount > 0)
|
|
||||||
{
|
|
||||||
count = config.BatchCount.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
var cookies = await _dyCookieService.GetAllCookies();
|
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
|
||||||
cookies = cookies.Where(x => !string.IsNullOrWhiteSpace(x.UpSecUserIds) && !string.IsNullOrWhiteSpace(x.UpSavePath))?.ToList();
|
{
|
||||||
if (!cookies.Any())
|
// 简化处理:假设只同步第一个UP主
|
||||||
|
var ups = JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(cookie.UpSecUserIds);
|
||||||
|
var firstUpId = ups?.FirstOrDefault()?.uid;
|
||||||
|
if (string.IsNullOrEmpty(firstUpId)) return null;
|
||||||
|
|
||||||
|
return await _douyinService.SyncUpderPostVideos(count, cursor, firstUpId, cookie.Cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
|
||||||
|
{
|
||||||
|
return data != null && data.HasMore == 1 && cookie.UperSyncd == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetNextCursor(DouyinVideoInfo data)
|
||||||
|
{
|
||||||
|
return data?.MaxCursor ?? "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
|
||||||
|
{
|
||||||
|
var config = _commonService.GetConfig();
|
||||||
|
if (config?.UperUseViedoTitle ?? false)
|
||||||
{
|
{
|
||||||
Serilog.Log.Debug("dyuploder-无可用cookie,任务终止");
|
var sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
||||||
return;
|
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($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
|
||||||
|
cookie.UperSyncd = 1;
|
||||||
|
await _dyCookieService.UpdateAsync(cookie);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//Serilog.Log.Debug($"dyuploder-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
|
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
|
||||||
//return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var cookie in cookies)
|
|
||||||
{
|
|
||||||
//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;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DyCollectVideo> videos = new List<DyCollectVideo>();
|
|
||||||
foreach (var item in data.AwemeList)
|
|
||||||
{
|
|
||||||
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, 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var savePath = Path.Combine(saveFolder, fileName);
|
|
||||||
|
|
||||||
if (!File.Exists(savePath))
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 构造视频数据
|
|
||||||
DyCollectVideo video = new()
|
|
||||||
{
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}]-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//Serilog.Log.Debug($"没有查询到新的视频");
|
|
||||||
}
|
|
||||||
await Task.Delay(_random.Next(5, 10) * 1000);
|
|
||||||
}
|
|
||||||
if (syncCount > 0)
|
|
||||||
{
|
|
||||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}],本次共同步成功{syncCount}条视频");
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Serilog.Log.Debug($"dyuploder-[{uper.uper}],本次没有查询到新的视频");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Error($"dyuploder-[{uper.uper}]同步时出错:{ex.Message}");
|
|
||||||
Serilog.Log.Error($"dyuploder-[{uper.uper}]同步时出错222:{ex.StackTrace}");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新为已同步过
|
|
||||||
cookie.UperSyncd = 1;
|
|
||||||
await _dyCookieService.UpdateAsync(cookie);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
|
||||||
/// 下载视频封面
|
|
||||||
/// </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)
|
|
||||||
{
|
{
|
||||||
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
var config = _commonService.GetConfig();
|
||||||
? item.Video.Cover.UrlList[0] : null;
|
string simplifiedTitle = string.Empty;
|
||||||
if (string.IsNullOrWhiteSpace(coverUrl)) return;
|
|
||||||
|
|
||||||
var coverImgName = "poster.jpg";
|
if (config?.UperUseViedoTitle ?? false)
|
||||||
var coverSavePath = Path.Combine(saveFolder, coverImgName);
|
|
||||||
|
|
||||||
if (!File.Exists(coverSavePath))
|
|
||||||
{
|
{
|
||||||
// 封面下载前随机延迟
|
simplifiedTitle = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
|
||||||
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
|
|
||||||
if (downRes)
|
|
||||||
{
|
|
||||||
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
|
|
||||||
File.Copy(coverSavePath, copyPath, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new VideoEntityDifferences
|
||||||
|
{
|
||||||
|
VideoType = VideoTypeEnum.UperPost,
|
||||||
|
VideoTitleSimplify = simplifiedTitle
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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="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
|
namespace dy.net.model
|
||||||
{
|
{
|
||||||
[SqlSugar.SugarTable(TableName = "login_user_info")]
|
[SqlSugar.SugarTable(TableName = "login_user_info")]
|
||||||
public class LoginUserInfo
|
public class AdminUserInfo
|
||||||
{
|
{
|
||||||
public string UserName { get; set;}
|
public string UserName { get; set;}
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
+10
-1
@@ -25,6 +25,15 @@ namespace dy.net.model
|
|||||||
/// 博主视频是否 直接用标题做文件名,不另外加一层文件夹
|
/// 博主视频是否 直接用标题做文件名,不另外加一层文件夹
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UperUseViedoTitle { get; set; }
|
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
|
namespace dy.net.model
|
||||||
{
|
{
|
||||||
[SugarTable(TableName = "dy_cookie")]
|
[SugarTable(TableName = "dy_cookie")]
|
||||||
public class DyUserCookies
|
public class DouyinUserCookie
|
||||||
{
|
{
|
||||||
|
|
||||||
[SugarColumn(IsPrimaryKey = true)]
|
[SugarColumn(IsPrimaryKey = true)]
|
||||||
@@ -77,32 +77,39 @@ namespace dy.net.model
|
|||||||
public string UpSavePath { get; set; }
|
public string UpSavePath { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图片视频存储路径
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 500, IsNullable = true)]
|
||||||
|
public string ImgSavePath { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[SugarColumn(IsIgnore = true)]
|
[SugarColumn(IsIgnore = true)]
|
||||||
public List<DyUpSecUserIdDto> UpSecUserIdsJson
|
public List<DouyinUpSecUserIdDto> UpSecUserIdsJson
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
// 反序列化逻辑(保持不变)
|
// 反序列化逻辑(保持不变)
|
||||||
if (string.IsNullOrWhiteSpace(UpSecUserIds))
|
if (string.IsNullOrWhiteSpace(UpSecUserIds))
|
||||||
{
|
{
|
||||||
return new List<DyUpSecUserIdDto>();
|
return new List<DouyinUpSecUserIdDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject<List<DyUpSecUserIdDto>>(UpSecUserIds);
|
return JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(UpSecUserIds);
|
||||||
}
|
}
|
||||||
catch (JsonSerializationException ex)
|
catch (JsonSerializationException ex)
|
||||||
{
|
{
|
||||||
// 日志记录(按需添加)
|
// 日志记录(按需添加)
|
||||||
// Logger.Error($"反序列化失败:{ex.Message},原始值:{UpSecUserIds}");
|
// Logger.Error($"反序列化失败:{ex.Message},原始值:{UpSecUserIds}");
|
||||||
return new List<DyUpSecUserIdDto>();
|
return new List<DouyinUpSecUserIdDto>();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 日志记录(按需添加)
|
// 日志记录(按需添加)
|
||||||
// Logger.Error($"处理异常:{ex.Message}");
|
// Logger.Error($"处理异常:{ex.Message}");
|
||||||
return new List<DyUpSecUserIdDto>();
|
return new List<DouyinUpSecUserIdDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
@@ -4,7 +4,7 @@ namespace dy.net.model
|
|||||||
{
|
{
|
||||||
|
|
||||||
[SugarTable(TableName = "dy_collect_video")]
|
[SugarTable(TableName = "dy_collect_video")]
|
||||||
public class DyCollectVideo
|
public class DouyinVideo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -6,9 +6,9 @@ using SqlSugar;
|
|||||||
|
|
||||||
namespace dy.net.repository
|
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))
|
if (string.IsNullOrWhiteSpace(userName))
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,7 @@ namespace dy.net.repository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userInfo"></param>
|
/// <param name="userInfo"></param>
|
||||||
/// <returns></returns>
|
/// <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));
|
var isInit = this.GetFirst(x=>!string.IsNullOrWhiteSpace(x.Id));
|
||||||
if (isInit!=null)
|
if (isInit!=null)
|
||||||
@@ -3,22 +3,22 @@ using SqlSugar;
|
|||||||
|
|
||||||
namespace dy.net.repository
|
namespace dy.net.repository
|
||||||
{
|
{
|
||||||
public class DyCookieRepository : BaseRepository<DyUserCookies>
|
public class DouyinUserCookieRepository : BaseRepository<DouyinUserCookie>
|
||||||
{
|
{
|
||||||
// 注入SQLSugar客户端
|
// 注入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);
|
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 totalCount = await where.CountAsync();
|
||||||
var list = await where.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
var list = await where.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||||
@@ -6,10 +6,10 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace dy.net.repository
|
namespace dy.net.repository
|
||||||
{
|
{
|
||||||
public class DyCollectVideoRepository : BaseRepository<DyCollectVideo>
|
public class DouyinVideoRepository : BaseRepository<DouyinVideo>
|
||||||
{
|
{
|
||||||
// 注入SQLSugar客户端
|
// 注入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="tag">可选标签过滤</param>
|
||||||
/// <param name="author">可选作者过滤</param>
|
/// <param name="author">可选作者过滤</param>
|
||||||
/// <returns>分页结果(视频列表和总数)</returns>
|
/// <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;
|
DateTime? start = null;
|
||||||
@@ -38,7 +38,7 @@ namespace dy.net.repository
|
|||||||
start = Convert.ToDateTime(dates[0]);
|
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(tag), x => x.Tag1 == tag)
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
|
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
|
||||||
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
|
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
|
||||||
@@ -49,7 +49,7 @@ namespace dy.net.repository
|
|||||||
var totalCount = await where.CountAsync();
|
var totalCount = await where.CountAsync();
|
||||||
var list = await where.OrderByDescending(x=>x.SyncTime).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
var list = await where.OrderByDescending(x=>x.SyncTime).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||||
if (list.Any()) {
|
if (list.Any()) {
|
||||||
var users= await this.Db.Queryable<DyUserCookies>().ToListAsync();
|
var users= await this.Db.Queryable<DouyinUserCookie>().ToListAsync();
|
||||||
foreach (var item in list)
|
foreach (var item in list)
|
||||||
{
|
{
|
||||||
var user= users.FirstOrDefault(x=>x.Id== item.CookieId);
|
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)
|
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)
|
.Where(x => x.VideoTitleSimplify == ViedoNameSimplify)
|
||||||
.OrderByDescending(x => x.CreateTime).FirstAsync();
|
.OrderByDescending(x => x.CreateTime).FirstAsync();
|
||||||
|
|
||||||
@@ -4,12 +4,12 @@ using dy.net.repository;
|
|||||||
|
|
||||||
namespace dy.net.service
|
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;
|
_userRepository = userRepository;
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ namespace dy.net.service
|
|||||||
return await _userRepository.UpdatePwd(loginUser);
|
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);
|
return await _userRepository.GetUser(userName);
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ namespace dy.net.service
|
|||||||
|
|
||||||
public (int code, string erro) InitUser()
|
public (int code, string erro) InitUser()
|
||||||
{
|
{
|
||||||
LoginUserInfo userInfo = new LoginUserInfo
|
AdminUserInfo userInfo = new AdminUserInfo
|
||||||
{
|
{
|
||||||
UserName = "douyin",
|
UserName = "douyin",
|
||||||
Password = "douyin2025",
|
Password = "douyin2025",
|
||||||
@@ -7,11 +7,11 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace dy.net.service
|
namespace dy.net.service
|
||||||
{
|
{
|
||||||
public class CommonService
|
public class DouyinCommonService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient sqlSugarClient;
|
private readonly ISqlSugarClient sqlSugarClient;
|
||||||
|
|
||||||
public CommonService(ISqlSugarClient sqlSugarClient)
|
public DouyinCommonService(ISqlSugarClient sqlSugarClient)
|
||||||
{
|
{
|
||||||
this.sqlSugarClient = sqlSugarClient;
|
this.sqlSugarClient = sqlSugarClient;
|
||||||
}
|
}
|
||||||
@@ -20,21 +20,31 @@ namespace dy.net.service
|
|||||||
{
|
{
|
||||||
return sqlSugarClient.Queryable<AppConfig>().First();
|
return sqlSugarClient.Queryable<AppConfig>().First();
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
public AppConfig InitConfig()
|
/// 初始化并返回配置
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="downLoadImage"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public AppConfig InitConfig(bool downLoadImage)
|
||||||
{
|
{
|
||||||
var conf = GetConfig();
|
var conf = GetConfig();
|
||||||
if (conf != null)
|
if (conf != null)
|
||||||
|
{
|
||||||
|
conf.DownImageVideo = downLoadImage;
|
||||||
|
sqlSugarClient.Updateable(conf).ExecuteCommand();
|
||||||
return conf;
|
return conf;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AppConfig config = new AppConfig
|
AppConfig config = new AppConfig
|
||||||
{
|
{
|
||||||
Id = IdGener.GetLong().ToString(),
|
Id = IdGener.GetLong().ToString(),
|
||||||
Cron = "30",
|
Cron = "30",
|
||||||
BatchCount = 10
|
BatchCount = 10,
|
||||||
|
DownImageVideo = downLoadImage,
|
||||||
|
KeepLogDay = 10
|
||||||
};
|
};
|
||||||
sqlSugarClient.Insertable<AppConfig>(config).ExecuteCommand();
|
sqlSugarClient.Insertable(config).ExecuteCommand();
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +76,7 @@ namespace dy.net.service
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateCollectViedoType()
|
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())
|
if (collectViedos.Any())
|
||||||
{
|
{
|
||||||
@@ -85,7 +95,7 @@ namespace dy.net.service
|
|||||||
{
|
{
|
||||||
//var sql = "update dy_cookie set CollHasSyncd=0,FavHasSyncd=0,UperSyncd=0";
|
//var sql = "update dy_cookie set CollHasSyncd=0,FavHasSyncd=0,UperSyncd=0";
|
||||||
// sqlSugarClient.Ado.ExecuteCommand(sql) > 0;
|
// sqlSugarClient.Ado.ExecuteCommand(sql) > 0;
|
||||||
var cookies = sqlSugarClient.Queryable<DyUserCookies>().ToList();
|
var cookies = sqlSugarClient.Queryable<DouyinUserCookie>().ToList();
|
||||||
|
|
||||||
foreach (var cookie in cookies)
|
foreach (var cookie in cookies)
|
||||||
{
|
{
|
||||||
@@ -95,7 +105,7 @@ namespace dy.net.service
|
|||||||
var upers = cookie.UpSecUserIds;
|
var upers = cookie.UpSecUserIds;
|
||||||
if (!string.IsNullOrWhiteSpace(upers))
|
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)
|
if (uperList != null && uperList.Count > 0)
|
||||||
{
|
{
|
||||||
foreach (var uper in uperList)
|
foreach (var uper in uperList)
|
||||||
@@ -5,28 +5,28 @@ using System.Linq.Expressions;
|
|||||||
|
|
||||||
namespace dy.net.service
|
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;
|
_cookieRepository = cookieRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<DyUserCookies>> GetAllCookies()
|
public Task<List<DouyinUserCookie>> GetAllCookies()
|
||||||
{
|
{
|
||||||
return _cookieRepository.GetAllCookies();
|
return _cookieRepository.GetAllCookies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<bool> Add(DyUserCookies dyUserCookies)
|
public async Task<bool> Add(DouyinUserCookie dyUserCookies)
|
||||||
{
|
{
|
||||||
return await _cookieRepository.InsertAsync(dyUserCookies);
|
return await _cookieRepository.InsertAsync(dyUserCookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Init()
|
public bool InitCookie()
|
||||||
{
|
{
|
||||||
var initId= "2026";
|
var initId= "2026";
|
||||||
var exist = _cookieRepository.GetFirst(x => x.Id == initId);
|
var exist = _cookieRepository.GetFirst(x => x.Id == initId);
|
||||||
@@ -34,7 +34,7 @@ namespace dy.net.service
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var cookie = new DyUserCookies
|
var cookie = new DouyinUserCookie
|
||||||
{
|
{
|
||||||
UserName = "douyin",
|
UserName = "douyin",
|
||||||
Cookies = "--",
|
Cookies = "--",
|
||||||
@@ -46,25 +46,26 @@ namespace dy.net.service
|
|||||||
UpSavePath = "/app/uper",
|
UpSavePath = "/app/uper",
|
||||||
CollHasSyncd = 0,
|
CollHasSyncd = 0,
|
||||||
FavHasSyncd = 0,
|
FavHasSyncd = 0,
|
||||||
UperSyncd = 0
|
UperSyncd = 0,
|
||||||
|
ImgSavePath="/app/images",
|
||||||
};
|
};
|
||||||
return _cookieRepository.Insert(cookie);
|
return _cookieRepository.Insert(cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询单个
|
// 查询单个
|
||||||
public async Task<DyUserCookies> GetByIdAsync(string id)
|
public async Task<DouyinUserCookie> GetByIdAsync(string id)
|
||||||
{
|
{
|
||||||
return await _cookieRepository.GetByIdAsync(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);
|
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);
|
return await _cookieRepository.UpdateAsync(dyUserCookies);
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,12 @@ using System.Net.Http;
|
|||||||
|
|
||||||
namespace dy.net.service
|
namespace dy.net.service
|
||||||
{
|
{
|
||||||
public class DyHttpClientService
|
public class DouyinHttpClientService
|
||||||
{
|
{
|
||||||
public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme";
|
public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme";
|
||||||
// 随机数生成器(避免重复实例化,保证随机性)
|
// 随机数生成器(避免重复实例化,保证随机性)
|
||||||
private readonly IHttpClientFactory _clientFactory;
|
private readonly IHttpClientFactory _clientFactory;
|
||||||
public DyHttpClientService(IHttpClientFactory clientFactory)
|
public DouyinHttpClientService(IHttpClientFactory clientFactory)
|
||||||
{
|
{
|
||||||
_clientFactory = clientFactory;
|
_clientFactory = clientFactory;
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ namespace dy.net.service
|
|||||||
/// <param name="count"></param>
|
/// <param name="count"></param>
|
||||||
/// <param name="cookie"></param>
|
/// <param name="cookie"></param>
|
||||||
/// <returns></returns>
|
/// <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))
|
if (string.IsNullOrWhiteSpace(cursor))
|
||||||
{
|
{
|
||||||
@@ -50,7 +50,7 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||||
|
|
||||||
var dics = DySyncBaseParamDics.CollectParams;
|
var dics = DouyinBaseParamDics.CollectParams;
|
||||||
dics["cursor"]=cursor;
|
dics["cursor"]=cursor;
|
||||||
dics["count"] = count;
|
dics["count"] = count;
|
||||||
try
|
try
|
||||||
@@ -68,7 +68,7 @@ namespace dy.net.service
|
|||||||
if (respose.IsSuccessStatusCode)
|
if (respose.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var data = await respose.Content.ReadAsStringAsync();
|
var data = await respose.Content.ReadAsStringAsync();
|
||||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -92,7 +92,7 @@ namespace dy.net.service
|
|||||||
/// <param name="cookie"></param>
|
/// <param name="cookie"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="ArgumentException"></exception>
|
/// <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))
|
if (string.IsNullOrWhiteSpace(cursor))
|
||||||
{
|
{
|
||||||
@@ -118,7 +118,7 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||||
|
|
||||||
var dics = DySyncBaseParamDics.FavoriteParams;
|
var dics = DouyinBaseParamDics.FavoriteParams;
|
||||||
{
|
{
|
||||||
// 添加动态参数
|
// 添加动态参数
|
||||||
dics["max_cursor"] = cursor;
|
dics["max_cursor"] = cursor;
|
||||||
@@ -133,7 +133,7 @@ namespace dy.net.service
|
|||||||
if (respose.IsSuccessStatusCode)
|
if (respose.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var data = await respose.Content.ReadAsStringAsync();
|
var data = await respose.Content.ReadAsStringAsync();
|
||||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -158,7 +158,7 @@ namespace dy.net.service
|
|||||||
/// <param name="cookie"></param>
|
/// <param name="cookie"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="ArgumentException"></exception>
|
/// <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))
|
if (string.IsNullOrWhiteSpace(cursor))
|
||||||
{
|
{
|
||||||
@@ -184,7 +184,7 @@ namespace dy.net.service
|
|||||||
}
|
}
|
||||||
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||||
|
|
||||||
var parameters = DySyncBaseParamDics.UpderPostParams;
|
var parameters = DouyinBaseParamDics.UpderPostParams;
|
||||||
{
|
{
|
||||||
// 添加动态参数
|
// 添加动态参数
|
||||||
parameters["max_cursor"] = cursor;
|
parameters["max_cursor"] = cursor;
|
||||||
@@ -204,7 +204,7 @@ namespace dy.net.service
|
|||||||
if (respose.IsSuccessStatusCode)
|
if (respose.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var data = await respose.Content.ReadAsStringAsync();
|
var data = await respose.Content.ReadAsStringAsync();
|
||||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -3,11 +3,11 @@ using dy.net.job;
|
|||||||
|
|
||||||
namespace dy.net.service
|
namespace dy.net.service
|
||||||
{
|
{
|
||||||
public class QuartzJobService
|
public class DouyinQuartzJobService
|
||||||
{
|
{
|
||||||
private readonly ISchedulerFactory _schedulerFactory;
|
private readonly ISchedulerFactory _schedulerFactory;
|
||||||
|
|
||||||
public QuartzJobService(ISchedulerFactory schedulerFactory)
|
public DouyinQuartzJobService(ISchedulerFactory schedulerFactory)
|
||||||
{
|
{
|
||||||
_schedulerFactory = schedulerFactory;
|
_schedulerFactory = schedulerFactory;
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ namespace dy.net.service
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task StartJob(string expression,int delay=5000)
|
public async Task StartJob(string expression,int delay=5000)
|
||||||
{
|
{
|
||||||
await StartCollectJob(expression);
|
//await StartCollectJob(expression);
|
||||||
|
|
||||||
await Task.Delay(delay);
|
await Task.Delay(delay);
|
||||||
//如果是数字则加1分钟,减少并发
|
//如果是数字则加1分钟,减少并发
|
||||||
@@ -29,7 +29,7 @@ namespace dy.net.service
|
|||||||
cron++;
|
cron++;
|
||||||
expression = cron.ToString();
|
expression = cron.ToString();
|
||||||
}
|
}
|
||||||
await StartFavoriteJob(expression);
|
//await StartFavoriteJob(expression);
|
||||||
|
|
||||||
await Task.Delay(delay);
|
await Task.Delay(delay);
|
||||||
if (int.TryParse(expression, out int cron2))
|
if (int.TryParse(expression, out int cron2))
|
||||||
@@ -55,7 +55,7 @@ namespace dy.net.service
|
|||||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||||
}
|
}
|
||||||
|
|
||||||
IJobDetail job = JobBuilder.Create<DouYinCollectSyncJob>()
|
IJobDetail job = JobBuilder.Create<DouyinCollectSyncJob>()
|
||||||
.WithIdentity(jobKey)
|
.WithIdentity(jobKey)
|
||||||
.Build();
|
.Build();
|
||||||
ITrigger trigger;
|
ITrigger trigger;
|
||||||
@@ -108,7 +108,7 @@ namespace dy.net.service
|
|||||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||||
}
|
}
|
||||||
|
|
||||||
IJobDetail job = JobBuilder.Create<DouYinFavoritSyncJob>()
|
IJobDetail job = JobBuilder.Create<DouyinFavoritSyncJob>()
|
||||||
.WithIdentity(jobKey)
|
.WithIdentity(jobKey)
|
||||||
.Build();
|
.Build();
|
||||||
ITrigger trigger;
|
ITrigger trigger;
|
||||||
@@ -160,7 +160,7 @@ namespace dy.net.service
|
|||||||
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
await __scheduler1.DeleteJob(jobKey);//删掉原来的
|
||||||
}
|
}
|
||||||
|
|
||||||
IJobDetail job = JobBuilder.Create<DouYinUperPostSyncJob>()
|
IJobDetail job = JobBuilder.Create<DouyinUperPostSyncJob>()
|
||||||
.WithIdentity(jobKey)
|
.WithIdentity(jobKey)
|
||||||
.Build();
|
.Build();
|
||||||
ITrigger trigger;
|
ITrigger trigger;
|
||||||
@@ -7,18 +7,18 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace dy.net.service
|
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;
|
_dyCollectVideoRepository = dyCollectVideoRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<bool> batchInsert(List<DyCollectVideo> videos)
|
public async Task<bool> batchInsert(List<DouyinVideo> videos)
|
||||||
{
|
{
|
||||||
|
|
||||||
// 边界处理:如果传入的列表为空,直接返回成功(或根据业务返回false)
|
// 边界处理:如果传入的列表为空,直接返回成功(或根据业务返回false)
|
||||||
@@ -56,7 +56,7 @@ namespace dy.net.service
|
|||||||
public async Task<VideoStaticsDto> GetStatics()
|
public async Task<VideoStaticsDto> GetStatics()
|
||||||
{
|
{
|
||||||
|
|
||||||
List<DyCollectVideo> list = await this._dyCollectVideoRepository.GetAllAsync();
|
List<DouyinVideo> list = await this._dyCollectVideoRepository.GetAllAsync();
|
||||||
if (!list.Any())
|
if (!list.Any())
|
||||||
return new VideoStaticsDto();
|
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();
|
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);
|
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
|
namespace dy.net.utils
|
||||||
{
|
{
|
||||||
public class DySyncBaseParamDics
|
public class DouyinBaseParamDics
|
||||||
{
|
{
|
||||||
public static Dictionary<string, string> CollectParams { get; } = InitializeUserCollecParams();
|
public static Dictionary<string, string> CollectParams { get; } = InitializeUserCollecParams();
|
||||||
public static Dictionary<string, string> FavoriteParams { get; } = InitializeUserFavoriteParams();
|
public static Dictionary<string, string> FavoriteParams { get; } = InitializeUserFavoriteParams();
|
||||||
+1
-1
@@ -7,7 +7,7 @@ namespace dy.net.utils
|
|||||||
public static class Md5Util
|
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)
|
public static string Md5(this string inputString)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace dy.net.utils
|
|||||||
public class NfoFileGenerator
|
public class NfoFileGenerator
|
||||||
{
|
{
|
||||||
|
|
||||||
public static void GenerateNfoFile(VideoNfo videoInfo, string filePath)
|
public static void GenerateNfoFile(DouyinVideoNfo videoInfo, string filePath)
|
||||||
{
|
{
|
||||||
try
|
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))
|
if (!string.IsNullOrWhiteSpace(videoInfo.Thumbnail))
|
||||||
root.Add(new XElement("thumb", new XAttribute("aspect", "poster"), CleanInvalidXmlChars(videoInfo.Thumbnail)));
|
root.Add(new XElement("thumb", new XAttribute("aspect", "poster"), CleanInvalidXmlChars(videoInfo.Thumbnail)));
|
||||||
|
|
||||||
@@ -58,11 +88,18 @@ namespace dy.net.utils
|
|||||||
root
|
root
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
string directory = Path.GetDirectoryName(filePath);
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
doc.Save(filePath);
|
doc.Save(filePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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>
|
/// </summary>
|
||||||
private const string IllegalCharReplacement = "";
|
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
|
#endregion
|
||||||
|
|
||||||
#region 处理抖音视频文件名
|
#region 处理抖音视频文件名
|
||||||
@@ -42,7 +53,7 @@ namespace dy.net.utils
|
|||||||
// 3. 长度控制:按UTF-8字节数截断(避免超系统限制)
|
// 3. 长度控制:按UTF-8字节数截断(避免超系统限制)
|
||||||
string truncatedTitle = TruncateByByteLength(purifiedTitle, MaxFileNameBytes);
|
string truncatedTitle = TruncateByByteLength(purifiedTitle, MaxFileNameBytes);
|
||||||
|
|
||||||
return truncatedTitle;
|
return truncatedTitle.Trim();
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -57,7 +68,7 @@ namespace dy.net.utils
|
|||||||
title = id;
|
title = id;
|
||||||
}
|
}
|
||||||
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
||||||
title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
|
//title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
|
||||||
|
|
||||||
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
|
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
|
||||||
string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
|
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);
|
title = Regex.Replace(title, $"{Separator}+", Separator.ToString(), RegexOptions.Compiled);
|
||||||
|
|
||||||
// 步骤6:移除首尾无效字符(分隔符、点号)
|
// 步骤6:移除首尾无效字符(分隔符、点号)
|
||||||
title = title.Trim(Separator, '.');
|
title = title.Replace(" ","").Trim(Separator, '.');
|
||||||
|
|
||||||
// 容错:如果净化后为空,返回默认值
|
// 容错:如果净化后为空,返回默认值
|
||||||
return string.IsNullOrWhiteSpace(title) ? id : title;
|
return string.IsNullOrWhiteSpace(title) ? id : title;
|
||||||
@@ -123,28 +134,35 @@ namespace dy.net.utils
|
|||||||
// 清理路径中的特殊字符(避免创建文件夹失败)
|
// 清理路径中的特殊字符(避免创建文件夹失败)
|
||||||
public static string SanitizePath(string path)
|
public static string SanitizePath(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
try
|
||||||
{
|
{
|
||||||
path = "其他";
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
path = "其他";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
||||||
|
//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);
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var c in Path.GetInvalidFileNameChars())
|
||||||
|
{
|
||||||
|
path = path.Replace(c, '_');
|
||||||
|
}
|
||||||
|
if (path.Length > 50)
|
||||||
|
{
|
||||||
|
path = path.Substring(0, 50);
|
||||||
|
}
|
||||||
|
return path.Trim().Replace(" ", "");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy)
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var c in Path.GetInvalidFileNameChars())
|
|
||||||
{
|
{
|
||||||
path = path.Replace(c, '_');
|
return path;
|
||||||
}
|
}
|
||||||
if (path.Length > 50)
|
|
||||||
{
|
|
||||||
path = path.Substring(0, 50);
|
|
||||||
}
|
|
||||||
return path.Trim();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user