1、增加图片视频

2、优化代码结构
3、up主下载分类逻辑可配置
This commit is contained in:
jianzhichu
2025-11-23 00:46:41 +08:00
parent 7cacdaedd4
commit 8cef13c926
51 changed files with 1604 additions and 1667 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ namespace dy.net.Controllers
{
private readonly IWebHostEnvironment webHostEnvironment;
private readonly UserService _userService;
public AuthController(UserService userService, IWebHostEnvironment webHostEnvironment )
private readonly AdminUserService _userService;
public AuthController(AdminUserService userService, IWebHostEnvironment webHostEnvironment )
{
_userService=userService;
this.webHostEnvironment = webHostEnvironment;
+6 -6
View File
@@ -14,12 +14,12 @@ namespace dy.net.Controllers
[ApiController]
public class ConfigController : ControllerBase
{
private readonly DyCookieService dyCookieService;
private readonly DouyinCookieService dyCookieService;
private readonly CommonService commonService;
private readonly QuartzJobService quartzJobService;
private readonly DouyinCommonService commonService;
private readonly DouyinQuartzJobService quartzJobService;
public ConfigController(DyCookieService dyCookieService, CommonService commonService,QuartzJobService quartzJobService)
public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService,DouyinQuartzJobService quartzJobService)
{
this.dyCookieService = dyCookieService;
this.commonService = commonService;
@@ -55,7 +55,7 @@ namespace dy.net.Controllers
/// 新增用户Cookie
/// </summary>
[HttpPost("add")]
public async Task<IActionResult> AddAsync([FromBody] DyUserCookies dyUserCookies)
public async Task<IActionResult> AddAsync([FromBody] DouyinUserCookie dyUserCookies)
{
if(dyUserCookies.UpSecUserIdsJson!=null)
{
@@ -74,7 +74,7 @@ namespace dy.net.Controllers
/// 更新用户Cookie
/// </summary>
[HttpPost("update")]
public async Task<IActionResult> UpdateAsync([FromBody] DyUserCookies dyUserCookies)
public async Task<IActionResult> UpdateAsync([FromBody] DouyinUserCookie dyUserCookies)
{
if (dyUserCookies.UpSecUserIdsJson != null )
+3 -3
View File
@@ -12,9 +12,9 @@ namespace dy.net.Controllers
[ApiController]
public class VideoController : ControllerBase
{
private readonly DyCollectVideoService dyCollectVideoService;
private readonly DouyinVideoService dyCollectVideoService;
public VideoController(DyCollectVideoService dyCollectVideoService)
public VideoController(DouyinVideoService dyCollectVideoService)
{
this.dyCollectVideoService = dyCollectVideoService;
}
@@ -23,7 +23,7 @@ namespace dy.net.Controllers
/// </summary>
/// <param name="dto"></param>
[HttpPost("paged")]
public async Task<IActionResult> GetPagedAsync(VideoPageRequestDTO dto)
public async Task<IActionResult> GetPagedAsync(DouyinVideoPageRequestDto dto)
{
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Author,dto.ViedoType,dto.Dates);
return Ok(new
+15 -3
View File
@@ -2,9 +2,21 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 10101
EXPOSE 10102
RUN echo "deb http://mirrors.aliyun.com/debian/ bookworm main non-free contrib" > /etc/apt/sources.list && \
echo "deb http://mirrors.aliyun.com/debian-security/ bookworm-security main" >> /etc/apt/sources.list && \
echo "deb http://mirrors.aliyun.com/debian/ bookworm-updates main non-free contrib" >> /etc/apt/sources.list && \
echo "deb http://mirrors.aliyun.com/debian/ bookworm-backports main non-free contrib" >> /etc/apt/sources.list && \
apt-get update && \
apt-get install -y --no-install-recommends ffmpeg && \
rm -rf /var/lib/apt/lists/*
RUN ffmpeg -version
COPY . .
ENV ASPNETCORE_URLS http://*:10101
ENTRYPOINT ["dotnet", "dy.net.dll"]
ENV ASPNETCORE_URLS=http://*:10102
ENV TZ=Asia/Shanghai
ENV DOWN_IMGVIDEO=1
ENTRYPOINT ["dotnet", "dy.net.dll"]
+11
View File
@@ -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
View File
@@ -17,38 +17,52 @@ namespace dy.net
public class Program
{
// 常量定义
private const string DefaultListenUrl = "http://*:10101";
private static string DefaultListenUrl = "http://*:10102";
private const string SpaRootPath = "app/dist";
private const string SpaSourcePath = "app/";
private const string SwaggerDocTitle = "dy.net WebApi Docs";
/// <summary>
/// 打包时注意,如果是false,前端不允许修改开启下载图片和视频的选项
/// </summary>
private static bool downImageVideo = false;
public static void Main(string[] args)
{
// 初始化编码提供器
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// 构建Web应用
var builder = WebApplication.CreateBuilder(args);
//from docker yaml file 环境变量 或者 dockerfile 或appsettings.json
DefaultListenUrl = builder.Configuration.GetValue<string>("ASPNETCORE_URLS") ?? DefaultListenUrl;
var downImgConfig = builder.Configuration.GetValue<string>("DOWN_IMGVIDEO");
Console.WriteLine("DOWN_IMGVIDEO=" + downImgConfig);
if (!string.IsNullOrEmpty(downImgConfig))
{
downImgConfig = downImgConfig.ToLower();
downImageVideo = downImgConfig == "1" || downImgConfig == "y"||downImgConfig=="t"||downImgConfig=="true";
}
var isDevelopment = builder.Environment.IsDevelopment();
// 配置主机
ConfigureHost(builder, isDevelopment);
// 配置服务
var services = builder.Services;
ConfigureServices(services, builder.Configuration, isDevelopment);
ConfigureServices(builder.Services, builder.Configuration, builder.Environment);
// 构建应用
var app = builder.Build();
Log.Debug("DOWN_IMGVIDEO=" + downImgConfig);
// 配置中间件
ConfigureMiddleware(app, isDevelopment);
ConfigureMiddleware(app, builder.Environment);
// 初始化应用服务
InitApplicationServices(app);
// 启动应用
Serilog.Log.Debug("dy.net service started successfully");
Log.Debug("dysync.net service started successfully");
app.Run();
}
@@ -78,8 +92,10 @@ namespace dy.net
/// <summary>
/// 配置依赖注入服务
/// </summary>
private static void ConfigureServices(IServiceCollection services, IConfiguration config, bool isDevelopment)
private static void ConfigureServices(IServiceCollection services, IConfiguration config, IWebHostEnvironment environment)
{
services.AddSingleton(new Appsettings (config));
// 雪花ID生成器
services.AddSnowFlakeId(options => options.WorkId = new Random().Next(1, 127));
@@ -93,17 +109,23 @@ namespace dy.net
services.AddSqlsugar(config);
// 定时任务
services.AddQuartzService(config);
services.AddQuartzService();
// 仓储和服务注册
services.AddServicesFromNamespace("dy.net.repository")
.AddServicesFromNamespace("dy.net.service");
//下载图片合成视频-需要ffmpeg支持,镜像会很大。
if (downImageVideo)
{
//根据配置动态加载dy.image程序集
Assembly assembly = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "dy.image.dll"));
services.AddServicesFromNamespace("dy.image", assembly);
}
// SPA静态文件支持
services.AddSpaStaticFiles(options => options.RootPath = SpaRootPath);
// 开发环境启用Swagger
if (isDevelopment)
if (environment.IsDevelopment())
{
services.AddSwagger();
}
@@ -120,13 +142,13 @@ namespace dy.net
/// <summary>
/// 配置中间件
/// </summary>
private static void ConfigureMiddleware(WebApplication app, bool isDevelopment)
private static void ConfigureMiddleware(WebApplication app, IWebHostEnvironment environment)
{
// 响应压缩
app.UseResponseCompression();
// 开发环境启用SwaggerUI
if (isDevelopment)
if (environment.IsDevelopment())
{
app.UseCustomSwaggerUI(options => options.Title = SwaggerDocTitle);
}
@@ -145,7 +167,7 @@ namespace dy.net
app.MapControllers();
// 生产环境启用SPA
if (!isDevelopment)
if (!environment.IsDevelopment())
{
app.UseSpaStaticFiles();
app.UseSpa(spa => spa.Options.SourcePath = SpaSourcePath);
@@ -163,23 +185,23 @@ namespace dy.net
try
{
// 初始化用户
var userService = services.GetRequiredService<UserService>();
var userService = services.GetRequiredService<AdminUserService>();
userService.InitUser();
// 初始化Cookie
var cookieService = services.GetRequiredService<DyCookieService>();
cookieService.Init();
var cookieService = services.GetRequiredService<DouyinCookieService>();
cookieService.InitCookie();
// 初始化配置
var commonService = services.GetRequiredService<CommonService>();
var config = commonService.InitConfig();
var commonService = services.GetRequiredService<DouyinCommonService>();
var config = commonService.InitConfig(downImageVideo);
// 更新收藏视频类型--兼容老版本-原来的旧数据没有这个类型字段
commonService.UpdateCollectViedoType();
// 重置博主作品同步状态为未同步
commonService.UpdateAllCookieSyncedToZero();
// 启动定时任务
var quartzJobService = services.GetRequiredService<QuartzJobService>();
var quartzJobService = services.GetRequiredService<DouyinQuartzJobService>();
quartzJobService.StartJob(config?.Cron ?? "30");
Serilog.Log.Debug("系统初始化完成,会默认将-博主作品同步功能-同步全部作品重置为关闭(若要开启,可以到抖音授权页面中修改)");
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Project>
<PropertyGroup>
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
<History>True|2025-11-12T15:06:38.7834109Z||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;False|2024-01-02T22:24:42.6577609+08:00||;True|2024-01-02T22:04:33.4708889+08:00||;True|2024-01-02T21:49:57.9249898+08:00||;True|2023-12-28T22:42:58.5976180+08:00||;True|2023-12-28T22:24:09.2051205+08:00||;True|2023-12-28T22:05:48.4865497+08:00||;True|2023-12-28T21:50:57.4549336+08:00||;True|2023-12-28T21:45:30.4557939+08:00||;True|2023-12-28T21:19:59.3614594+08:00||;False|2023-12-28T21:17:04.3069097+08:00||;False|2023-12-28T21:15:56.7788810+08:00||;False|2023-12-28T21:14:57.3617402+08:00||;False|2023-12-28T21:10:45.4382469+08:00||;True|2023-12-28T19:45:33.1431914+08:00||;True|2023-12-26T11:30:36.6418716+08:00||;True|2023-12-26T11:17:40.6433651+08:00||;True|2023-12-26T09:13:56.0741559+08:00||;True|2023-12-26T09:10:55.5358737+08:00||;True|2023-12-25T23:15:34.6534359+08:00||;True|2023-12-25T23:09:47.1566176+08:00||;False|2023-12-25T23:07:32.1118803+08:00||;True|2023-12-25T22:59:50.4643459+08:00||;True|2023-12-25T22:50:14.3584798+08:00||;False|2023-12-25T22:49:20.0279513+08:00||;True|2023-12-25T22:44:07.4183973+08:00||;True|2023-12-25T22:35:27.3847085+08:00||;True|2023-12-25T22:23:21.3542658+08:00||;True|2023-12-25T22:12:00.5219522+08:00||;</History>
<History>True|2025-11-22T15:18:56.3202345Z||;True|2025-11-22T22:52:51.7203302+08:00||;True|2025-11-22T22:52:42.9620946+08:00||;True|2025-11-22T22:52:09.8257640+08:00||;True|2025-11-22T22:39:31.5894141+08:00||;True|2025-11-22T22:31:11.0704815+08:00||;True|2025-11-22T22:20:36.4579131+08:00||;True|2025-11-22T22:19:10.2281364+08:00||;True|2025-11-22T19:34:45.9336901+08:00||;True|2025-11-12T23:06:38.7834109+08:00||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;False|2024-01-02T22:24:42.6577609+08:00||;True|2024-01-02T22:04:33.4708889+08:00||;True|2024-01-02T21:49:57.9249898+08:00||;True|2023-12-28T22:42:58.5976180+08:00||;True|2023-12-28T22:24:09.2051205+08:00||;True|2023-12-28T22:05:48.4865497+08:00||;True|2023-12-28T21:50:57.4549336+08:00||;True|2023-12-28T21:45:30.4557939+08:00||;True|2023-12-28T21:19:59.3614594+08:00||;False|2023-12-28T21:17:04.3069097+08:00||;False|2023-12-28T21:15:56.7788810+08:00||;False|2023-12-28T21:14:57.3617402+08:00||;False|2023-12-28T21:10:45.4382469+08:00||;True|2023-12-28T19:45:33.1431914+08:00||;True|2023-12-26T11:30:36.6418716+08:00||;True|2023-12-26T11:17:40.6433651+08:00||;True|2023-12-26T09:13:56.0741559+08:00||;True|2023-12-26T09:10:55.5358737+08:00||;True|2023-12-25T23:15:34.6534359+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -1,4 +1,5 @@
{
"dbconn": "",
"dbtype": "Sqlite"
"dbtype": "Sqlite",
"DOWN_IMGVIDEO": "1",
}
@@ -2,7 +2,7 @@
namespace dy.net.dto
{
public class DyUpSecUserIdDto
public class DouyinUpSecUserIdDto
{
/// <summary>
@@ -4,7 +4,7 @@ namespace dy.net.dto
{
public class CollectVideoInfo
public class DouyinVideoInfo
{
[JsonProperty("aweme_list")]
public List<Aweme> AwemeList { get; set; }
@@ -1267,7 +1267,8 @@ namespace dy.net.dto
/// <summary>
///
/// </summary>
public List<string> url_list { get; set; }
[JsonProperty("url_list")]
public List<string> UrlList { get; set; }
/// <summary>
///
/// </summary>
+11 -1
View File
@@ -1,6 +1,14 @@
namespace dy.net.dto
{
public class VideoNfo
// Actor.cs
public class Actor
{
public string Name { get; set; }
public string Role { get; set; } // 角色名称
public string Thumb { get; set; } // 演员头像 URL 或路径
}
public class DouyinVideoNfo
{
/// 视频名称
@@ -25,5 +33,7 @@
// 新增的属性
public DateTime? ReleaseDate { get; set; } // 可空,避免无发布时间时的默认值
public IEnumerable<string> Genres { get; set; } // 分类标签集合(如 ["动作", "科幻"]
public List<Actor> Actors { get; set; }
}
}
@@ -2,7 +2,7 @@
namespace dy.net.dto
{
public class VideoPageRequestDTO: PageRequestDto
public class DouyinVideoPageRequestDto : PageRequestDto
{
// 3. 引用类型(string)如果允许为null,显式声明为 string?
public string? Tag { get; set; }
@@ -1,6 +1,6 @@
namespace dy.net.dto
{
public class CollectVideoRequestDto
public class DouyinVideoRequestDto
{
public int cursor { get; set; }
-126
View File
@@ -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; }
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace dy.net.dto
{
public enum VideoTypeEnum
{
Favorite = 1,
Collect = 2,
UperPost = 3,
ImageVideo = 4
}
}
+12 -1
View File
@@ -23,12 +23,16 @@
<ItemGroup>
<Compile Remove="app\**" />
<Compile Remove="expand\**" />
<Compile Remove="logs\**" />
<Content Remove="app\**" />
<Content Remove="expand\**" />
<Content Remove="logs\**" />
<EmbeddedResource Remove="app\**" />
<EmbeddedResource Remove="expand\**" />
<EmbeddedResource Remove="logs\**" />
<None Remove="app\**" />
<None Remove="expand\**" />
<None Remove="logs\**" />
</ItemGroup>
@@ -68,14 +72,21 @@
<None Include="dy.net.sln" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="expand\dy.image\dy.image.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="db\dy.sqlite">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Dockerfile">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Dockerfile-arm">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Dockerfile">
<None Update="Dockerfile-dev">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
+12 -1
View File
@@ -1,10 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11010.61 d18.0
VisualStudioVersion = 18.0.11010.61
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dy.net", "dy.net.csproj", "{680660EF-ACAE-43A9-AB6C-B75532E758AD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dy.expand", "dy.expand", "{1503FF41-A045-48DF-B92C-DB81BAAAFE5B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dy.image", "expand\dy.image\dy.image.csproj", "{38404DA7-A852-4960-9E95-8F70459AEB60}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,10 +19,17 @@ Global
{680660EF-ACAE-43A9-AB6C-B75532E758AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{680660EF-ACAE-43A9-AB6C-B75532E758AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{680660EF-ACAE-43A9-AB6C-B75532E758AD}.Release|Any CPU.Build.0 = Release|Any CPU
{38404DA7-A852-4960-9E95-8F70459AEB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{38404DA7-A852-4960-9E95-8F70459AEB60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38404DA7-A852-4960-9E95-8F70459AEB60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38404DA7-A852-4960-9E95-8F70459AEB60}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{38404DA7-A852-4960-9E95-8F70459AEB60} = {1503FF41-A045-48DF-B92C-DB81BAAAFE5B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8238199F-0C28-42F2-ACFD-E8CBCCB2AB80}
EndGlobalSection
+7
View File
@@ -0,0 +1,7 @@
namespace dy.extension
{
public class Class1
{
}
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+42
View File
@@ -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);
}
}
}
+261
View File
@@ -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();
}
}
}
+152
View File
@@ -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);
}
}
}
+31
View File
@@ -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;
}
}
+12
View File
@@ -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>
+10 -19
View File
@@ -156,18 +156,18 @@ namespace dy.net.extension
///
/// </summary>
/// <param name="services"></param>
public static void AddQuartzService(this IServiceCollection services, IConfiguration configuration)
public static void AddQuartzService(this IServiceCollection services)
{
services.AddTransient<DouYinCollectSyncJob>();
services.AddTransient<DouYinFavoritSyncJob>();
services.AddTransient<DouYinUperPostSyncJob>();
//services.AddTransient<DouyinCollectSyncJob>();
//services.AddTransient<DouyinFavoritSyncJob>();
//services.AddTransient<DouyinUperPostSyncJob>();
// 注册Quartz服务
services.AddQuartz();
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
services.AddTransient<QuartzJobService>();
services.AddTransient<DouyinQuartzJobService>();
}
@@ -264,8 +264,12 @@ namespace dy.net.extension
?? ServiceLifetime.Transient;
// 查找该类实现的接口(优先注册为接口服务)
//var interfaces = type.GetInterfaces()
// .Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
// .ToList();
// 查找该类实现的接口(排除 IDisposable
var interfaces = type.GetInterfaces()
.Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
.Where(i => i != typeof(IDisposable)) // <-- 关键修正
.ToList();
if (interfaces.Any())
@@ -366,20 +370,7 @@ namespace dy.net.extension
{
Version = "v1",
Title = "dy.net API Swagger Document",
//Description = "WebApi Swagger Document",
//TermsOfService = new Uri("https://ddnsapi.online.com"),
//Contact = new OpenApiContact
//{
// Name = "剑之初",
// Email = "xxxx@qq.com",
// //Url = new Uri("https://ddns.online"),
//},
//License = new OpenApiLicense
//{
// Name = "许可证",
// Url = new Uri("https://ddns.online"),
//}
});
o.OrderActionsBy((apiDesc) => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.HttpMethod}");
o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
+55 -325
View File
@@ -1,367 +1,97 @@
using ClockSnowFlake;
using dy.net.dto;
using dy.net.dto;
using dy.net.model;
using dy.net.service;
using dy.net.utils;
using Quartz;
namespace dy.net.job
{
[DisallowConcurrentExecution]
public class DouYinCollectSyncJob : IJob
public class DouyinCollectSyncJob : DouyinBaseSyncJob
{
private readonly DyCookieService _dyCookieService;
public DouyinCollectSyncJob(
DouyinCookieService dyCookieService,
DouyinHttpClientService dyHttpClientService,
DouyinVideoService dyCollectVideoService,
DouyinCommonService commonService,IServiceProvider serviceProvider,IWebHostEnvironment webHostEnvironment)
: base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider,webHostEnvironment) { }
private readonly DyHttpClientService _douyinService;
protected override string JobType => "collect";
private readonly DyCollectVideoService _douyinVideoService;
private readonly CommonService commonService;
private readonly Random _random = new Random();
private string count = "18"; // 每页请求的视频数量,默认18
public DouYinCollectSyncJob(DyCookieService dyCookieService, DyHttpClientService dyHttpClientService, DyCollectVideoService dyCollectVideoService, CommonService commonService)
protected override async Task BeforeProcessCookies()
{
_dyCookieService = dyCookieService;
_douyinService = dyHttpClientService;
_douyinVideoService = dyCollectVideoService;
this.commonService = commonService;
var now = DateTime.Now;
if (now.Hour == 1 && now.Minute < 30)
{
_commonService.UpdateAllCookieSyncedToZero();
await Task.Delay(200);
}
}
public async Task Execute(IJobExecutionContext context)
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
{
var config = commonService.GetConfig();
if (config == null)
{
Serilog.Log.Debug("collect-请先在设置中初始化配置,再执行同步任务");
return;
}
if (config.BatchCount > 0)
{
count = config.BatchCount.ToString();
}
var cookies = await _dyCookieService.GetAllCookies();
cookies= cookies.Where(c => !string.IsNullOrWhiteSpace(c.SavePath)).ToList();
if (cookies == null || !cookies.Any())
{
Serilog.Log.Debug("collect-无可用Cookie,任务终止");
return;
}
else
{
//Serilog.Log.Debug($"collect-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
//return;
return cookies.Where(c => !string.IsNullOrWhiteSpace(c.SavePath)).ToList();
}
//每天凌晨1点到1点半之间执行一次重置同步状态-全部cookie
UpdateCookieSyncedToZero();
foreach (var cookie in cookies)
protected override bool IsCookieValid(DouyinUserCookie cookie)
{
Serilog.Log.Debug($"collect-开始同步 Cookie-[{cookie.UserName}]收藏视频");
if (string.IsNullOrWhiteSpace(cookie.Cookies) || cookie.Cookies.Length < 1000)
{
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}]无效,跳过");
continue;
}
//if (string.IsNullOrWhiteSpace(cookie.SavePath))
//{
// Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}]未设置保存路径,跳过");
// continue;
//}
try
{
int syncCount = 0;// 记录本次Cookie同步的视频数量
int index = 0;
bool hasMore = true;
string cursor = "0";
while (hasMore)
{
var data = await _douyinService.SyncCollectVideos(cursor, count, cookie.Cookies);
hasMore = data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
break;
if (cookie.CollHasSyncd == 1)
{
Serilog.Log.Debug($"collect-Cookie[{cookie.UserName}]已完整同步过,后续只获取最新一页数据");
}
//Serilog.Log.Debug($"还有数据需要同步吗?{(hasMore ? "YES" : "NO")}");
if (data == null)
{
Serilog.Log.Debug($"collect-Cookie[{cookie.UserName}]获取收藏数据失败,请检查一下Cookie");
break;
}
cursor = data != null && !string.IsNullOrWhiteSpace(data.Cursor) ? data.Cursor : "0";
if (data.AwemeList == null || !data.AwemeList.Any())
{
break;
return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 &&
!string.IsNullOrWhiteSpace(cookie.SavePath);
}
List<DyCollectVideo> videos = new List<DyCollectVideo>();
foreach (var item in data.AwemeList)
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
{
if (item == null)
continue;
if (item.Video == null)
continue;
if (item.Video.BitRate == null)
continue;
var v = item.Video.BitRate.FirstOrDefault();
var tags = item.VideoTags;
if (v == null) continue;
var videoUrl = v.PlayAddr.UrlList != null && v.PlayAddr.UrlList.Any() ? v.PlayAddr.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(videoUrl)) continue;
var tag1 = tags.FirstOrDefault(x => x.Level == 1)?.TagName;
var tag2 = tags.FirstOrDefault(x => x.Level == 2)?.TagName;
var tag3 = tags.FirstOrDefault(x => x.Level == 3)?.TagName;
string saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
var savePath = Path.Combine(saveFolder, fileName);
if (!File.Exists(savePath))
{
Serilog.Log.Debug($"collect-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
await Task.Delay(_random.Next(1, 4) * 1000);
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
if (downVideo)
{
Serilog.Log.Debug($"collect-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "" : "")}");
}
else
{
Serilog.Log.Error($"collect-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "" : "")}");
}
if (downVideo)
{
await DownVideoCover(item, saveFolder, cookie.Cookies);
// 用AuthorId做文件名,避免昵称特殊字符
var avatarImgName = $"{item.Author.Uid}.jpg";
var avatarSavePath = Path.Combine(cookie.SavePath, "author", avatarImgName);
await DownAuthorAvatar(cookie.SavePath, item, avatarSavePath, cookie.Cookies);
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
? item.Author.AvatarLarger.UrlList[0] : null;
// 备用头像链接
if (string.IsNullOrWhiteSpace(avatarUrl))
{
avatarUrl = item.Author?.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
? item.Author.AvatarThumb.UrlList[0] : null;
return await _douyinService.SyncCollectVideos(cursor, count, cookie.Cookies);
}
// 构造视频数据
DyCollectVideo video = new()
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
{
ViedoType = "2",
AwemeId = item.AwemeId,
Author = item.Author?.Nickname,
AuthorId = item.Author?.Uid,
AuthorAvatar = avatarSavePath,
AuthorAvatarUrl = avatarUrl,
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
VideoTitle = item.Desc,
Id = IdGener.GetLong().ToString(),
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
FileSize = v.PlayAddr.DataSize,
FileHash = v.PlayAddr.FileHash,
Tag1 = tag1,
Tag2 = tag2,
Tag3 = tag3,
VideoUrl = videoUrl,
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? item.Video.Cover.UrlList[0] : null,
VideoSavePath = savePath,
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
SyncTime = DateTime.Now,
DyUserId = data.Uid,
CookieId = cookie.Id
};
videos.Add(video);
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
NfoFileGenerator.GenerateNfoFile(new VideoNfo
{
Author = video.Author,
Poster = Path.Combine(saveFolder, "poster.jpg"),
Title = video.VideoTitle,
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
ReleaseDate = video.CreateTime,
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
}, nfoPath);
}
}
else
{
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
}
}
index++;
// 批量保存到数据库(减少数据库操作频率)
if (videos.Any())
{
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
try
{
await _douyinVideoService.batchInsert(videos);
syncCount += videos.Count;
}
catch (Exception ex)
{
Serilog.Log.Error($"collect-批量保存视频到数据库失败:{ex.Message}", ex);
// 收集所有需要删除的目录(去重)
foreach (var video in videos)
{
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(video.VideoSavePath))
{
var deletePath = Path.GetDirectoryName(video.VideoSavePath);
Directory.Delete(deletePath, recursive: true);
//Serilog.Log.Debug($"已删除失败视频目录:{deletePath}");
}
return data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
}
Serilog.Log.Debug("collect-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
}
}
else
protected override string GetNextCursor(DouyinVideoInfo data)
{
//Serilog.Log.Debug($"没有查询到新的视频");
return data?.Cursor ?? "0";
}
await Task.Delay(_random.Next(5, 10) * 1000);
protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
{
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
var folder = Path.Combine(cookie.SavePath, safeTag1, $"{TikTokFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
return folder;
}
protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
{
return $"{item.AwemeId}.{bitRate.Format}";
}
protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie)
{
return Path.Combine(cookie.SavePath, "author");
}
protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount)
{
if (syncCount > 0)
{
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
// 更新同步状态为已同步
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
cookie.CollHasSyncd = 1;
await _dyCookieService.UpdateAsync(cookie);
}
else
{
Serilog.Log.Debug($"collect-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
}
}
}
catch (Exception ex)
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
{
Serilog.Log.Error($"collect-处理Cookie[{cookie.Id}]时出错:{ex.Message}");
Serilog.Log.Error($"collect-处理Cookie[{cookie.Id}]时出错22{ex.StackTrace}");
}
}
}
/// <summary>
/// 下载视频封面
/// </summary>
/// <param name="item"></param>
/// <param name="saveFolder"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
return new VideoEntityDifferences
{
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? item.Video.Cover.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(coverUrl)) return;
var coverImgName = "poster.jpg";
var coverSavePath = Path.Combine(saveFolder, coverImgName);
if (!File.Exists(coverSavePath))
{
// 封面下载前随机延迟
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
if (downRes)
{
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
File.Copy(coverSavePath, copyPath, true);
}
}
}
/// <summary>
/// 下载作者头像
/// </summary>
/// <param name="mainPath"></param>
/// <param name="item"></param>
/// <param name="avatarSavePath"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownAuthorAvatar(string mainPath, Aweme item, string avatarSavePath, string cookie)
{
if (item.Author == null) return;
var avatarUrl = item.Author.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
? item.Author.AvatarLarger.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(avatarUrl)) return;
var path = Path.Combine(mainPath, "author");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
if (!File.Exists(avatarSavePath))
{
// 头像下载前随机延迟
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie);
}
}
/// <summary>
/// 创建目录
/// </summary>
/// <param name="cookie"></param>
/// <param name="item"></param>
/// <param name="tag1"></param>
/// <param name="tag2"></param>
/// <returns></returns>
private static string CreateSaveFolder(DyUserCookies cookie, Aweme item, string? tag1, string? tag2)
{
// 路径中避免特殊字符,用ID替代描述
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
List<string> pathParts = new List<string> { cookie.SavePath, safeTag1 };
var saveFolder = Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), TikTokFileNameHelper.SanitizePath(item.Desc) + "@" + item.AwemeId);
// 创建文件夹(提前创建,避免下载时才操作)
if (!Directory.Exists(saveFolder))
{
Directory.CreateDirectory(saveFolder);
}
return saveFolder;
//if (string.IsNullOrWhiteSpace(tag2))
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
//else
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), tag2 + "@" + item.AwemeId);
}
/// <summary>
/// 重置Cookie的同步状态为0(每天凌晨1点到1点半之间执行一次)
/// </summary>
/// <returns></returns>
private void UpdateCookieSyncedToZero()
{
var now = DateTime.Now;
//如果当时间为凌晨1点到1点半之间,则重置为0
if (now.Hour == 1 && now.Minute < 30)
{
commonService.UpdateAllCookieSyncedToZero();
}
VideoType = VideoTypeEnum.Collect,
};
}
}
}
+50 -310
View File
@@ -1,349 +1,89 @@
using ClockSnowFlake;
using dy.net.dto;
using dy.net.dto;
using dy.net.model;
using dy.net.service;
using dy.net.utils;
using Quartz;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace dy.net.job
{
[DisallowConcurrentExecution]
public class DouYinFavoritSyncJob : IJob
public class DouyinFavoritSyncJob : DouyinBaseSyncJob
{
private readonly DyCookieService _dyCookieService;
public DouyinFavoritSyncJob(
DouyinCookieService dyCookieService,
DouyinHttpClientService dyHttpClientService,
DouyinVideoService dyCollectVideoService,
DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment)
: base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider, webHostEnvironment) { }
private readonly DyHttpClientService _douyinService;
private readonly DyCollectVideoService _douyinVideoService;
private readonly CommonService commonService;
private readonly Random _random = new Random();
private string count = "18"; // 每页请求的视频数量,默认18
public DouYinFavoritSyncJob(DyCookieService dyCookieService, DyHttpClientService dyHttpClientService, DyCollectVideoService dyCollectVideoService,CommonService commonService)
protected override string JobType => "favorite";
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
{
_dyCookieService = dyCookieService;
_douyinService = dyHttpClientService;
_douyinVideoService = dyCollectVideoService;
this.commonService = commonService;
}
public async Task Execute(IJobExecutionContext context)
{
var config = commonService.GetConfig();
if (config == null)
{
Serilog.Log.Debug("favorite-请先在设置中初始化配置,再执行同步任务");
return;
}
if (config.BatchCount > 0)
{
count = config.BatchCount.ToString();
}
var cookies = await _dyCookieService.GetAllCookies();
cookies=cookies.Where(c => !string.IsNullOrWhiteSpace(c.FavSavePath)).ToList();
if (cookies == null || !cookies.Any())
{
Serilog.Log.Debug("favorite-无可用Cookie,任务终止");
return;
}
else
{
Serilog.Log.Debug($"favorite-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
//return;
return cookies.Where(c => !string.IsNullOrWhiteSpace(c.FavSavePath)).ToList();
}
foreach (var cookie in cookies)
protected override bool IsCookieValid(DouyinUserCookie cookie)
{
Serilog.Log.Debug($"favorite-开始同步 Cookie-[{cookie.UserName}]喜欢的视频");
if (string.IsNullOrWhiteSpace(cookie.Cookies)|| cookie.Cookies.Length<1000)
{
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]无效,跳过");
continue;
}
if(string.IsNullOrWhiteSpace(cookie.FavSavePath))
{
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]未设置保存路径,跳过");
continue;
}
if (string.IsNullOrWhiteSpace(cookie.SecUserId)|| cookie.SecUserId.Length<10) {
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]未设置SecUserId,跳过");
continue;
}
try
{
int syncCount = 0;// 记录本次Cookie同步的视频数量
int index = 0;
bool hasMore = true;
string cursor = "0";
while (hasMore)
{
var data = await _douyinService.SyncFavoriteVideos(count,cursor, cookie.SecUserId, cookie.Cookies);
hasMore = data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
if (cookie.FavHasSyncd == 1)
{
Serilog.Log.Debug($"favorite-Cookie[{cookie.UserName}]已完整同步过,后续只获取最新一页数据");
}
//Serilog.Log.Debug($"还有数据需要同步吗?{(hasMore ? "YES" : "NO")}");
if (data == null)
{
Serilog.Log.Debug($"favorite-Cookie[{cookie.UserName}]获取喜欢的数据失败,请检查一下Cookie和sec_user_id");
break;
}
cursor = data != null && !string.IsNullOrWhiteSpace(data.MaxCursor) ? data.MaxCursor : "0";
if (data.AwemeList == null || !data.AwemeList.Any())
{
break;
return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 &&
!string.IsNullOrWhiteSpace(cookie.FavSavePath) &&
!string.IsNullOrWhiteSpace(cookie.SecUserId) && cookie.SecUserId.Length >= 10;
}
List<DyCollectVideo> videos = new List<DyCollectVideo>();
foreach (var item in data.AwemeList)
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
{
if (item == null)
continue;
if (item.Video == null)
continue;
if (item.Video.BitRate == null)
continue;
var v = item.Video.BitRate.FirstOrDefault();
var tags = item.VideoTags;
if (v == null) continue;
var videoUrl = v.PlayAddr.UrlList != null && v.PlayAddr.UrlList.Any() ? v.PlayAddr.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(videoUrl)) continue;
var tag1 = tags.FirstOrDefault(x => x.Level == 1)?.TagName;
var tag2 = tags.FirstOrDefault(x => x.Level == 2)?.TagName;
var tag3 = tags.FirstOrDefault(x => x.Level == 3)?.TagName;
string saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
var savePath = Path.Combine(saveFolder, fileName);
if (!File.Exists(savePath))
{
Serilog.Log.Debug($"favorite-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
await Task.Delay(_random.Next(1, 4) * 1000);
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
if (downVideo)
{
Serilog.Log.Debug($"favorite-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "" : "")}");
}
else
{
Serilog.Log.Error($"favorite-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "" : "")}");
}
if (downVideo)
{
await DownVideoCover(item, saveFolder, cookie.Cookies);
// 用AuthorId做文件名,避免昵称特殊字符
var avatarImgName = $"{item.Author.Uid}.jpg";
var avatarSavePath = Path.Combine(cookie.FavSavePath, "author", avatarImgName);
await DownAuthorAvatar(cookie.FavSavePath, item, avatarSavePath, cookie.Cookies);
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
? item.Author.AvatarLarger.UrlList[0] : null;
// 尝试使用AvatarThumb作为备用头像URL
if (string.IsNullOrWhiteSpace(avatarUrl))
{
avatarUrl = item.Author?.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
? item.Author.AvatarThumb.UrlList[0] : null;
return await _douyinService.SyncFavoriteVideos(count, cursor, cookie.SecUserId, cookie.Cookies);
}
// 构造视频数据
DyCollectVideo video = new()
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
{
ViedoType="1",
AwemeId = item.AwemeId,
Author = item.Author?.Nickname,
AuthorId = item.Author?.Uid,
AuthorAvatar = avatarSavePath,
AuthorAvatarUrl = avatarUrl,
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
VideoTitle = item.Desc,
Id = IdGener.GetLong().ToString(),
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
FileSize = v.PlayAddr.DataSize,
FileHash = v.PlayAddr.FileHash,
Tag1 = tag1,
Tag2 = tag2,
Tag3 = tag3,
VideoUrl = videoUrl,
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? item.Video.Cover.UrlList[0] : null,
VideoSavePath = savePath,
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
SyncTime = DateTime.Now,
DyUserId = data.Uid,
CookieId = cookie.Id
};
videos.Add(video);
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
NfoFileGenerator.GenerateNfoFile(new VideoNfo
{
Author = video.Author,
Poster = Path.Combine(saveFolder, "poster.jpg"),
Title = video.VideoTitle,
Thumbnail= Path.Combine(saveFolder, "fanart.jpg"),
ReleaseDate = video.CreateTime,
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
}, nfoPath);
}
}
else
{
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
}
}
index++;
// 批量保存到数据库(减少数据库操作频率)
if (videos.Any())
{
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
try
{
await _douyinVideoService.batchInsert(videos);
syncCount += videos.Count;
}
catch (Exception ex)
{
Serilog.Log.Error("favorite-批量保存视频到数据库失败:{ex.Message}", ex);
// 收集所有需要删除的目录(去重)
foreach (var video in videos)
{
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(video.VideoSavePath))
{
var deletePath = Path.GetDirectoryName(video.VideoSavePath);
Directory.Delete(deletePath, recursive: true);
//Serilog.Log.Debug($"已删除失败视频目录:{deletePath}");
}
return data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
}
Serilog.Log.Debug("favorite-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
protected override string GetNextCursor(DouyinVideoInfo data)
{
return data?.MaxCursor ?? "0";
}
protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
{
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{TikTokFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}");
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
return folder;
}
else {
//Serilog.Log.Debug($"没有查询到新的视频");
protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
{
return $"{item.AwemeId}.{bitRate.Format}";
}
await Task.Delay(_random.Next(5, 10) * 1000);
protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie)
{
return Path.Combine(cookie.FavSavePath, "author");
}
protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount)
{
if (syncCount > 0)
{
Serilog.Log.Debug($"Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
// 更新同步状态为已同步
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
cookie.FavHasSyncd = 1;
await _dyCookieService.UpdateAsync(cookie);
}
else {
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
}
}
catch (Exception ex)
else
{
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错:{ex.Message}");
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错22{ex.StackTrace}");
}
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
}
}
/// <summary>
/// 下载视频封面
/// </summary>
/// <param name="item"></param>
/// <param name="saveFolder"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
{
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? item.Video.Cover.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(coverUrl)) return;
var coverImgName = "poster.jpg";
var coverSavePath = Path.Combine(saveFolder, coverImgName);
if (!File.Exists(coverSavePath))
return new VideoEntityDifferences
{
// 封面下载前随机延迟
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
if (downRes)
{
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
File.Copy(coverSavePath, copyPath, true);
VideoType = VideoTypeEnum.Favorite
};
}
}
}
/// <summary>
/// 下载作者头像
/// </summary>
/// <param name="mainPath"></param>
/// <param name="item"></param>
/// <param name="avatarSavePath"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownAuthorAvatar(string mainPath, Aweme item, string avatarSavePath, string cookie)
{
if (item.Author == null) return;
var avatarUrl = item.Author.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
? item.Author.AvatarLarger.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(avatarUrl)) {
avatarUrl = item.Author.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
? item.Author.AvatarThumb.UrlList[0] : null;
}
if (string.IsNullOrWhiteSpace(avatarUrl)) return;
var path = Path.Combine(mainPath, "author");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
if (!File.Exists(avatarSavePath))
{
// 头像下载前随机延迟
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie);
}
}
/// <summary>
/// 创建目录
/// </summary>
/// <param name="cookie"></param>
/// <param name="item"></param>
/// <param name="tag1"></param>
/// <param name="tag2"></param>
/// <returns></returns>
private static string CreateSaveFolder(DyUserCookies cookie, Aweme item, string? tag1, string? tag2)
{
// 路径中避免特殊字符,用ID替代描述
var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1);
List<string> pathParts = new List<string> { cookie.FavSavePath, safeTag1 };
var saveFolder= Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), TikTokFileNameHelper.SanitizePath(item.Desc) + "@" + item.AwemeId);
// 创建文件夹(提前创建,避免下载时才操作)
if (!Directory.Exists(saveFolder))
{
Directory.CreateDirectory(saveFolder);
}
return saveFolder;
//if (string.IsNullOrWhiteSpace(tag2))
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
//else
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), tag2 + "@" + item.AwemeId);
}
}
}
+65 -335
View File
@@ -1,381 +1,111 @@
using ClockSnowFlake;
using dy.net.dto;
using dy.net.dto;
using dy.net.model;
using dy.net.service;
using dy.net.utils;
using Newtonsoft.Json;
using Quartz;
using SqlSugar.Extensions;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace dy.net.job
{
[DisallowConcurrentExecution]
public class DouYinUperPostSyncJob : IJob
public class DouyinUperPostSyncJob : DouyinBaseSyncJob
{
private readonly DyCookieService _dyCookieService;
public DouyinUperPostSyncJob(
DouyinCookieService dyCookieService,
DouyinHttpClientService dyHttpClientService,
DouyinVideoService dyCollectVideoService,
DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment)
: base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider, webHostEnvironment) { }
private readonly DyHttpClientService _douyinService;
protected override string JobType => "dyuploder";
private readonly DyCollectVideoService _douyinVideoService;
private readonly CommonService commonService;
private readonly Random _random = new Random();
private string count = "18"; // 每页请求的视频数量,默认18
private readonly string httpDownName = "dy_down_uper";
public DouYinUperPostSyncJob(DyCookieService dyCookieService, DyHttpClientService dyHttpClientService, DyCollectVideoService dyCollectVideoService, CommonService commonService)
protected override async Task<List<DouyinUserCookie>> GetValidCookies()
{
_dyCookieService = dyCookieService;
_douyinService = dyHttpClientService;
_douyinVideoService = dyCollectVideoService;
this.commonService = commonService;
}
public async Task Execute(IJobExecutionContext context)
{
var config = commonService.GetConfig();
if (config == null)
{
Serilog.Log.Debug("dyuploder-请先在设置中初始化配置,再执行同步任务");
return;
}
if (config.BatchCount > 0)
{
count = config.BatchCount.ToString();
}
var cookies = await _dyCookieService.GetAllCookies();
cookies = cookies.Where(x => !string.IsNullOrWhiteSpace(x.UpSecUserIds) && !string.IsNullOrWhiteSpace(x.UpSavePath))?.ToList();
if (!cookies.Any())
{
Serilog.Log.Debug("dyuploder-无可用cookie,任务终止");
return;
}
else
{
//Serilog.Log.Debug($"dyuploder-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
//return;
return cookies.Where(x => !string.IsNullOrWhiteSpace(x.UpSecUserIds) && !string.IsNullOrWhiteSpace(x.UpSavePath)).ToList();
}
foreach (var cookie in cookies)
protected override bool IsCookieValid(DouyinUserCookie cookie)
{
//if(string.IsNullOrWhiteSpace(cookie.UpSavePath))
//{
// Serilog.Log.Debug($"dyuploder-Cookie[{cookie.UserName}]的UP主保存路径无效,跳过");
// continue;
//}
var ups = JsonConvert.DeserializeObject<List<DyUpSecUserIdDto>>(cookie.UpSecUserIds);
foreach (var uper in ups)
{
Serilog.Log.Debug($"dyuploder-开始同步-[{uper.uper}]主页作品数据");
if (string.IsNullOrWhiteSpace(uper.uid) || uper.uid.Length < 10)
{
Serilog.Log.Debug($"dyuploder-[{uper.uper}]的SecUserId无效,跳过");
continue;
}
try
{
int syncCount = 0;// 记录本次Cookie同步的视频数量
int index = 0;
bool hasMore = true;
string cursor = "0";
while (hasMore)
{
var data = await _douyinService.SyncUpderPostVideos(count, cursor, uper.uid, cookie.Cookies);
hasMore = data != null && data.HasMore == 1 && cookie.UperSyncd == 0 && uper.syncAll;
if (cookie.UperSyncd == 1)
{
Serilog.Log.Debug($"dyuploder-Cookie[{cookie.UserName}]已完整同步过,后续只获取最新一页数据");
}
//Serilog.Log.Debug($"还有数据需要同步吗?{(hasMore ? "YES" : "NO")}");
if (data == null)
{
Serilog.Log.Debug($"dyuploder-[{uper.uper}]获取数据失败,请检查Up主SecUserId");
break;
}
cursor = data != null && !string.IsNullOrWhiteSpace(data.MaxCursor) ? data.MaxCursor : "0";
if (data.AwemeList == null || !data.AwemeList.Any())
{
break;
return !string.IsNullOrWhiteSpace(cookie.Cookies) && !string.IsNullOrWhiteSpace(cookie.UpSavePath) && !string.IsNullOrWhiteSpace(cookie.UpSecUserIds);
}
List<DyCollectVideo> videos = new List<DyCollectVideo>();
foreach (var item in data.AwemeList)
protected override async Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor)
{
if (item == null)
continue;
if (item.Video == null)
continue;
if (item.Video.BitRate == null)
continue;
var v = item.Video.BitRate.FirstOrDefault();
var tags = item.VideoTags;
if (v == null) continue;
// 简化处理:假设只同步第一个UP主
var ups = JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(cookie.UpSecUserIds);
var firstUpId = ups?.FirstOrDefault()?.uid;
if (string.IsNullOrEmpty(firstUpId)) return null;
var videoUrl = v.PlayAddr.UrlList != null && v.PlayAddr.UrlList.Any() ? v.PlayAddr.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(videoUrl)) continue;
var tag1 = tags.FirstOrDefault(x => x.Level == 1)?.TagName;
var tag2 = tags.FirstOrDefault(x => x.Level == 2)?.TagName;
var tag3 = tags.FirstOrDefault(x => x.Level == 3)?.TagName;
string saveFolder = CreateSaveFolder(cookie, uper.uper);
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
string samplePrefix = null;
string sampleName = null;
if (config.UperUseViedoTitle)// 使用视频标题作为文件名
{
sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
var sampleViedo = await _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName);
if (string.IsNullOrWhiteSpace(sampleViedo.Item1))
{
fileName = $"{sampleName}.{v.Format}";
}
else
{
fileName = $"{sampleViedo.Item1}.{v.Format}";
samplePrefix = sampleViedo.Item2;
}
return await _douyinService.SyncUpderPostVideos(count, cursor, firstUpId, cookie.Cookies);
}
var savePath = Path.Combine(saveFolder, fileName);
if (!File.Exists(savePath))
protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data)
{
Serilog.Log.Debug($"dyuploder-[{uper.uper}]-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
await Task.Delay(_random.Next(1, 4) * 1000);
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
if (downVideo)
{
Serilog.Log.Debug($"dyuploder-[{uper.uper}]-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "" : "")}");
}
else
{
Serilog.Log.Error($"dyuploder-[{uper.uper}]-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载{(downVideo ? "" : "")}");
}
if (downVideo)
{
await DownVideoCover(item, saveFolder, cookie.Cookies);
// 用AuthorId做文件名,避免昵称特殊字符
var avatarImgName = $"{item.Author.Uid}.jpg";
var avatarSavePath = Path.Combine(cookie.UpSavePath, "author", avatarImgName);
await DownAuthorAvatar(cookie.UpSavePath, item, avatarSavePath, cookie.Cookies);
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
? item.Author.AvatarLarger.UrlList[0] : null;
// 有可能下载大头像失败,尝试用缩略图地址
if (string.IsNullOrWhiteSpace(avatarUrl))
{
avatarUrl = item.Author?.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
? item.Author.AvatarThumb.UrlList[0] : null;
return data != null && data.HasMore == 1 && cookie.UperSyncd == 0;
}
// 构造视频数据
DyCollectVideo video = new()
protected override string GetNextCursor(DouyinVideoInfo data)
{
ViedoType = "3",// UP主视频
AwemeId = item.AwemeId,
Author = item.Author?.Nickname,
AuthorId = item.Author?.Uid,
AuthorAvatar = avatarSavePath,
AuthorAvatarUrl = avatarUrl,
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
VideoTitle = item.Desc,
Id = IdGener.GetLong().ToString(),
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
FileSize = v.PlayAddr.DataSize,
FileHash = v.PlayAddr.FileHash,
Tag1 = tag1,
Tag2 = tag2,
Tag3 = tag3,
VideoUrl = videoUrl,
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? item.Video.Cover.UrlList[0] : null,
VideoSavePath = savePath,
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
SyncTime = DateTime.Now,
DyUserId = data.Uid,
CookieId = cookie.Id,
};
if (!string.IsNullOrEmpty(sampleName))
{
video.VideoTitleSimplify = sampleName;
if (!string.IsNullOrEmpty(samplePrefix))
video.VideoTitleSimplifyPrefix = samplePrefix;
}
videos.Add(video);
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
NfoFileGenerator.GenerateNfoFile(new VideoNfo
{
Author = video.Author,
Poster = Path.Combine(saveFolder, "poster.jpg"),
Title = video.VideoTitle,
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
ReleaseDate = video.CreateTime,
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
}, nfoPath);
}
}
else
{
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
}
}
index++;
// 批量保存到数据库(减少数据库操作频率)
if (videos.Any())
{
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
try
{
await _douyinVideoService.batchInsert(videos);
syncCount += videos.Count;
}
catch (Exception ex)
{
Serilog.Log.Error($"dyuploder-[{uper.uper}]-批量保存视频到数据库失败:{ex.Message}", ex);
// 收集所有需要删除的目录(去重)
foreach (var video in videos)
{
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(video.VideoSavePath))
{
var deletePath = Path.GetDirectoryName(video.VideoSavePath);
Directory.Delete(deletePath, recursive: true);
//Serilog.Log.Debug($"已删除失败视频目录:{deletePath}");
}
return data?.MaxCursor ?? "0";
}
Serilog.Log.Debug($"dyuploder-[{uper.uper}]-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
}
}
else
protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2)
{
//Serilog.Log.Debug($"没有查询到新的视频");
// UP主视频通常按作者名创建文件夹
var authorName = string.IsNullOrWhiteSpace(item.Author?.Nickname) ? "UnknownAuthor" : TikTokFileNameHelper.SanitizePath(item.Author.Nickname);
var folder = Path.Combine(cookie.UpSavePath, authorName);
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
return folder;
}
await Task.Delay(_random.Next(5, 10) * 1000);
protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate)
{
var config = _commonService.GetConfig();
if (config?.UperUseViedoTitle ?? false)
{
var sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
var (existingName, _) = _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result;
return string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{bitRate.Format}" : $"{existingName}.{bitRate.Format}";
}
return $"{item.AwemeId}.{bitRate.Format}";
}
protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie)
{
return Path.Combine(cookie.UpSavePath, "author");
}
protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount)
{
if (syncCount > 0)
{
Serilog.Log.Debug($"dyuploder-[{uper.uper}],本次同步成功{syncCount}条视频");
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频");
cookie.UperSyncd = 1;
await _dyCookieService.UpdateAsync(cookie);
}
else
{
Serilog.Log.Debug($"dyuploder-[{uper.uper}],本次没有查询到新的视频");
Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
}
}
}
catch (Exception ex)
protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item)
{
Serilog.Log.Error($"dyuploder-[{uper.uper}]同步时出错:{ex.Message}");
Serilog.Log.Error($"dyuploder-[{uper.uper}]同步时出错222{ex.StackTrace}");
}
var config = _commonService.GetConfig();
string simplifiedTitle = string.Empty;
}
// 更新为已同步过
cookie.UperSyncd = 1;
await _dyCookieService.UpdateAsync(cookie);
}
}
/// <summary>
/// 下载视频封面
/// </summary>
/// <param name="item"></param>
/// <param name="saveFolder"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
if (config?.UperUseViedoTitle ?? false)
{
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? item.Video.Cover.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(coverUrl)) return;
var coverImgName = "poster.jpg";
var coverSavePath = Path.Combine(saveFolder, coverImgName);
if (!File.Exists(coverSavePath))
{
// 封面下载前随机延迟
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
if (downRes)
{
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
File.Copy(coverSavePath, copyPath, true);
}
}
simplifiedTitle = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId);
}
/// <summary>
/// 下载作者头像
/// </summary>
/// <param name="mainPath"></param>
/// <param name="item"></param>
/// <param name="avatarSavePath"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownAuthorAvatar(string mainPath, Aweme item, string avatarSavePath, string cookie)
return new VideoEntityDifferences
{
if (item.Author == null) return;
var avatarUrl = item.Author.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
? item.Author.AvatarLarger.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(avatarUrl)) return;
var path = Path.Combine(mainPath, "author");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
if (!File.Exists(avatarSavePath))
{
// 头像下载前随机延迟
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie);
VideoType = VideoTypeEnum.UperPost,
VideoTitleSimplify = simplifiedTitle
};
}
}
/// <summary>
/// 创建目录
/// </summary>
/// <param name="cookie"></param>
/// <param name="uperName"></param>
/// <returns></returns>
private static string CreateSaveFolder(DyUserCookies cookie, string uperName)
{
var saveFolder = Path.Combine(cookie.UpSavePath, uperName);
// 创建文件夹(提前创建,避免下载时才操作)
if (!Directory.Exists(saveFolder))
{
Directory.CreateDirectory(saveFolder);
}
return saveFolder;
// 路径中避免特殊字符,用ID替代描述
//var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : SanitizePath(tag1);
//List<string> pathParts = new List<string> { cookie.UpSavePath, safeTag1 };
//return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
//if (string.IsNullOrWhiteSpace(tag2))
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), SanitizePath(item.Desc) + "@" + item.AwemeId);
//else
// return Path.Combine(pathParts[0], string.Join("-", pathParts.Skip(1)), tag2 + "@" + item.AwemeId);
}
}
}
+546
View File
@@ -0,0 +1,546 @@
using ClockSnowFlake;
using dy.image;
using dy.net.dto;
using dy.net.model;
using dy.net.service;
using dy.net.utils;
using Quartz;
using Quartz.Util;
namespace dy.net.job
{
[DisallowConcurrentExecution]
public abstract class DouyinBaseSyncJob : IJob
{
protected readonly DouyinCookieService _dyCookieService;
protected readonly DouyinHttpClientService _douyinService;
protected readonly DouyinVideoService _douyinVideoService;
protected readonly DouyinCommonService _commonService;
protected readonly Random _random = new Random();
protected readonly IServiceProvider _serviceProvider;
protected readonly IWebHostEnvironment _environment;
protected string count = "18"; // 每页请求的视频数量
protected abstract string JobType { get; }
private bool _downImageVideo;
/// <summary>
/// 清除保存失败的数据
/// </summary>
/// <param name="dyCookieService"></param>
/// <param name="dyHttpClientService"></param>
/// <param name="dyCollectVideoService"></param>
/// <param name="commonService"></param>
/// <param name="serviceProvider"></param>
/// <param name="webHostEnvironment"></param>
protected DouyinBaseSyncJob(
DouyinCookieService dyCookieService,
DouyinHttpClientService dyHttpClientService,
DouyinVideoService dyCollectVideoService,
DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment)
{
_dyCookieService = dyCookieService;
_douyinService = dyHttpClientService;
_douyinVideoService = dyCollectVideoService;
_commonService = commonService;
_serviceProvider = serviceProvider;
_environment = webHostEnvironment;
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Execute(IJobExecutionContext context)
{
var config = _commonService.GetConfig();
if (config == null)
{
Serilog.Log.Debug($"{JobType}-请先在设置中初始化配置,再执行同步任务");
return;
}
if (config.BatchCount > 0)
count = config.BatchCount.ToString();
// 读取环境变量覆盖配置中是否下载图片视频配置
InitializeDownImageVideoSetting(config);
await BeforeProcessCookies();
var cookies = await GetValidCookies();
if (cookies == null || !cookies.Any())
{
Serilog.Log.Debug($"{JobType}-无可用Cookie,任务终止");
return;
}
Serilog.Log.Debug($"{JobType}-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
foreach (var cookie in cookies)
{
await ProcessSyncUserCookie(cookie);
}
}
/// <summary>
/// 检查并初始化下载图片视频的设置
/// </summary>
/// <param name="config"></param>
private void InitializeDownImageVideoSetting(AppConfig config)
{
var downImageVideoConfig = Appsettings.Get("DOWN_IMGVIDEO");
if (!string.IsNullOrWhiteSpace(downImageVideoConfig))
{
downImageVideoConfig = downImageVideoConfig.ToLower();
_downImageVideo = config.DownImageVideo && (downImageVideoConfig == "1" || downImageVideoConfig == "y" || downImageVideoConfig == "t" || downImageVideoConfig == "true");
}
}
/// <summary>
/// 检查并处理Cookie
/// </summary>
/// <param name="cookie"></param>
/// <returns></returns>
protected async Task ProcessSyncUserCookie(DouyinUserCookie cookie)
{
try
{
if (!IsCookieValid(cookie))
{
Serilog.Log.Debug($"{JobType}-Cookie[{cookie.UserName}]无效,跳过");
return;
}
Serilog.Log.Debug($"{JobType}-开始同步 Cookie-[{cookie.UserName}]");
int syncCount = 0;
string cursor = "0";
bool hasMore = true;
while (hasMore)
{
var data = await FetchVideoData(cookie, cursor);
if (data == null)
{
Serilog.Log.Debug($"{JobType}-Cookie[{cookie.UserName}]获取数据失败");
break;
}
hasMore = ShouldContinueSync(cookie, data);
cursor = GetNextCursor(data);
if (data.AwemeList == null || !data.AwemeList.Any())
break;
var videos = await ProcessVideoList(cookie, data);
syncCount += await SaveVideos(videos);
await Task.Delay(_random.Next(5, 10) * 1000);
}
await HandleSyncCompletion(cookie, syncCount);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, $"{JobType}-处理Cookie[{cookie.Id}]时出错");
}
}
/// <summary>
/// 处理接口返回的视频数据
/// </summary>
/// <param name="cookie"></param>
/// <param name="data"></param>
/// <returns></returns>
protected async Task<List<DouyinVideo>> ProcessVideoList(DouyinUserCookie cookie, DouyinVideoInfo data)
{
var videos = new List<DouyinVideo>();
foreach (var item in data.AwemeList)
{
var video = await ProcessSingleVideo(cookie, item, data);
if (video != null)
videos.Add(video);
//如果配置了下载图片视频,则处理图片集并合成视频
if (_downImageVideo)
{
var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data);
if (mergevideo != null)
videos.Add(mergevideo);
}
}
return videos;
}
/// <summary>
/// 处理单个视频数据
/// </summary>
/// <param name="cookie"></param>
/// <param name="item"></param>
/// <param name="data"></param>
/// <returns></returns>
protected async Task<DouyinVideo> ProcessSingleVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data)
{
if (!IsAwemeValid(item)) return null;
var v = item.Video.BitRate.FirstOrDefault();
if (v == null) return null;
var videoUrl = v.PlayAddr.UrlList?.FirstOrDefault();
if (string.IsNullOrWhiteSpace(videoUrl)) return null;
var (tag1, tag2, tag3) = GetVideoTags(item);
var saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
var fileName = GetVideoFileName(cookie, item, v);
var savePath = Path.Combine(saveFolder, fileName);
if (File.Exists(savePath)) return null;
Serilog.Log.Debug($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载");
await Task.Delay(_random.Next(1, 4) * 1000);
if (!await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies))
{
Serilog.Log.Error($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载失败");
return null;
}
await DownVideoCover(item, saveFolder, cookie.Cookies);
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
await GenerateNfoFile(saveFolder, item, tag1, tag2, tag3, avatarSavePath, avatarUrl);
return CreateVideoEntity(cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
}
/// <summary>
/// 创建视频实体
/// </summary>
/// <param name="cookie"></param>
/// <param name="item"></param>
/// <param name="bitRate"></param>
/// <param name="savePath"></param>
/// <param name="saveFolder"></param>
/// <param name="tag1"></param>
/// <param name="tag2"></param>
/// <param name="tag3"></param>
/// <param name="avatarSavePath"></param>
/// <param name="avatarUrl"></param>
/// <param name="data"></param>
/// <returns></returns>
protected DouyinVideo CreateVideoEntity(
DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder,
string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data)
{
var diffs = GetVideoEntityDifferences(cookie, item);
return new DouyinVideo
{
ViedoType = diffs.VideoType.GetHashCode().ToString(),
AwemeId = item.AwemeId,
Author = item.Author?.Nickname,
AuthorId = item.Author?.Uid,
AuthorAvatar = avatarSavePath,
AuthorAvatarUrl = avatarUrl,
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
VideoTitle = item.Desc,
VideoTitleSimplify = diffs.VideoTitleSimplify,
Id = IdGener.GetLong().ToString(),
Resolution = $"{bitRate.PlayAddr.Width}×{bitRate.PlayAddr.Height}",
FileSize = bitRate.PlayAddr.DataSize,
FileHash = bitRate.PlayAddr.FileHash,
Tag1 = tag1,
Tag2 = tag2,
Tag3 = tag3,
VideoUrl = bitRate.PlayAddr.UrlList?.FirstOrDefault(),
VideoCoverUrl = item.Video.Cover.UrlList?.FirstOrDefault(),
VideoSavePath = savePath,
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
SyncTime = DateTime.Now,
DyUserId = item.AuthorUserId == 0 ? item.Author?.Uid : item.AuthorUserId.ToString(),
CookieId = cookie.Id
};
}
/// <summary>
/// 保存视频信息到数据库
/// </summary>
/// <param name="videos"></param>
/// <returns></returns>
protected async Task<int> SaveVideos(List<DouyinVideo> videos)
{
if (!videos.Any()) return 0;
try
{
await _douyinVideoService.batchInsert(videos);
return videos.Count;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, $"{JobType}-批量保存视频到数据库失败");
await CleanupFailedVideos(videos);
return 0;
}
}
/// <summary>
/// 清理保存失败的视频文件
/// </summary>
/// <param name="videos"></param>
/// <returns></returns>
protected async Task CleanupFailedVideos(List<DouyinVideo> videos)
{
foreach (var video in videos)
{
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(Path.GetDirectoryName(video.VideoSavePath)))
{
Directory.Delete(Path.GetDirectoryName(video.VideoSavePath), recursive: true);
}
}
Serilog.Log.Debug($"{JobType}-因数据库保存失败,已清理本次下载的视频目录");
}
/// <summary>
/// 下载视频封面
/// </summary>
/// <param name="item"></param>
/// <param name="saveFolder"></param>
/// <param name="cookie"></param>
/// <returns></returns>
protected async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
{
var coverUrl = item.Video.Cover.UrlList?.FirstOrDefault();
if (string.IsNullOrWhiteSpace(coverUrl)) return;
var coverSavePath = Path.Combine(saveFolder, "poster.jpg");
if (File.Exists(coverSavePath)) return;
if (await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie))
{
File.Copy(coverSavePath, Path.Combine(saveFolder, "fanart.jpg"), true);
}
}
/// <summary>
/// 下载图片合成的视频封面
/// </summary>
/// <param name="coverUrl"></param>
/// <param name="saveFolder"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownVideoCover(string coverUrl, string saveFolder, string cookie)
{
if (string.IsNullOrWhiteSpace(coverUrl)) return;
var coverImgName = "poster.jpg";
var coverSavePath = Path.Combine(saveFolder, coverImgName);
if (!File.Exists(coverSavePath))
{
// 封面下载前随机延迟
var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie);
if (downRes)
{
var copyPath = Path.Combine(saveFolder, "fanart.jpg");
File.Copy(coverSavePath, copyPath, true);
}
}
}
/// <summary>
/// 下载作者头像
/// </summary>
/// <param name="cookie"></param>
/// <param name="item"></param>
/// <returns></returns>
protected async Task<(string savePath, string url)> DownAuthorAvatar(DouyinUserCookie cookie, Aweme item)
{
if (item.Author == null) return (null, null);
var avatarUrl = item.Author.AvatarLarger?.UrlList?.FirstOrDefault() ?? item.Author.AvatarThumb?.UrlList?.FirstOrDefault();
if (string.IsNullOrWhiteSpace(avatarUrl)) return (null, null);
var avatarSavePath = Path.Combine(GetAuthorAvatarBasePath(cookie), $"{item.Author.Uid}.jpg");
var avatarDir = Path.GetDirectoryName(avatarSavePath);
if (!Directory.Exists(avatarDir)) Directory.CreateDirectory(avatarDir);
if (!File.Exists(avatarSavePath))
{
await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie.Cookies);
}
return (avatarSavePath, avatarUrl);
}
/// <summary>
/// 处理图片集并合成为视频
/// </summary>
/// <param name="cookie"></param>
/// <param name="item"></param>
/// <param name="data"></param>
/// <returns></returns>
protected async Task<DouyinVideo> ProcessImageSetAndMergeToVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data)
{
try
{
List<string> imageUrls = item.Images?
.Where(img => img.UrlList != null && img.UrlList.Any())
.Select(img => img.UrlList.FirstOrDefault())
.Where(url => !string.IsNullOrWhiteSpace(url))
.ToList();
if (imageUrls == null || !imageUrls.Any())
{
return null;
}
if (string.IsNullOrWhiteSpace(cookie.ImgSavePath))
{
Serilog.Log.Error($"{JobType}-图片视频同步-没有配置图片存储路径");
return null;
}
var imageService = _serviceProvider.GetService<ImageMergeToVideoService>();
if (imageService == null)
{
Serilog.Log.Error($"{JobType} -图片视频同步-无法创建 ImageMergeToVideoService。");
return null;
}
var fileNamefolder = Path.Combine(cookie.ImgSavePath, TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId));
if (!Directory.Exists(fileNamefolder)) Directory.CreateDirectory(fileNamefolder);
var savePath = Path.Combine(fileNamefolder, $"{item.AwemeId}.mp4");
if (File.Exists(savePath)) return null;
var mp3Url = item.Music?.PlayUrl?.UrlList?.FirstOrDefault();
var reqParams = new MediaMergeRequest
{
ImageDurationPerSecond = 3,
OutputFormat = "mp4",
VideoFps = 30,
AudioUrls = string.IsNullOrWhiteSpace(mp3Url) ? new List<string>() : new List<string> { mp3Url },
ImageUrls = imageUrls,
VideoWidth = 1080,
VideoHeight = 1920,
};
var mergeResult = await imageService.MergeToVideo(AppContext.BaseDirectory, reqParams, savePath,fileNamefolder);
if (!mergeResult)
{
Serilog.Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
return null;
}
if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0)
{
Serilog.Log.Error($"{JobType}-图片视频同步-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败");
if (Directory.Exists(fileNamefolder))
{
File.Delete(savePath);
Directory.Delete(fileNamefolder, true);
Serilog.Log.Error($"{JobType}-图片视频同步-已删除合成失败的视频文件和目录");
}
return null;
}
// 复用基类的方法
await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie.Cookies);
var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item);
// 为合成的视频创建一个“虚拟”的BitRate对象,以便复用CreateVideoEntity
var virtualBitRate = new VideoBitRate
{
PlayAddr = new PlayAddr
{
Width = reqParams.VideoWidth,
Height = reqParams.VideoHeight,
DataSize = new FileInfo(savePath).Length
}
};
var (tag1, tag2, tag3) = GetVideoTags(item);
await GenerateNfoFile(fileNamefolder, item, tag1, tag2, tag3, avatarSavePath, avatarUrl);
var videoEntity = CreateVideoEntity(
cookie, item, virtualBitRate, savePath, fileNamefolder,
tag1, tag2, tag3, avatarSavePath, avatarUrl, data);
// 特殊处理合成视频的字段
videoEntity.FileHash = string.Empty;
videoEntity.VideoUrl = "/"; // 合成视频没有原始URL
videoEntity.ViedoType= VideoTypeEnum.ImageVideo.ToString();
return videoEntity;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, $"{JobType}-图片视频同步-处理图片集并合成视频时出错");
return null;
}
}
/// <summary>
/// 生成NFO文件
/// </summary>
protected async Task GenerateNfoFile(string saveFolder, Aweme item, string tag1, string tag2, string tag3,
string avatarSavePath, string avatarUrl)
{
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
NfoFileGenerator.GenerateNfoFile(new DouyinVideoNfo
{
Actors = new List<Actor>
{
new() {
Name = item.Author?.Nickname,
Role = "主演",
Thumb = avatarSavePath
}
},
Author = item.Author?.Nickname,
Poster = Path.Combine(saveFolder, "poster.jpg"),
Title = item.Desc,
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
ReleaseDate = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
}, nfoPath);
}
/// <summary>
/// 检查视频数据是否有效
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected bool IsAwemeValid(Aweme item) => item != null && item.Video != null && item.Video.BitRate != null;
/// <summary>
/// 获取视频标签信息
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected (string tag1, string tag2, string tag3) GetVideoTags(Aweme item)
{
var tags = item.VideoTags;
return (
tags?.FirstOrDefault(x => x.Level == 1)?.TagName,
tags?.FirstOrDefault(x => x.Level == 2)?.TagName,
tags?.FirstOrDefault(x => x.Level == 3)?.TagName
);
}
/// <summary>
/// AOP
/// </summary>
/// <returns></returns>
protected virtual Task BeforeProcessCookies() => Task.CompletedTask;
protected abstract Task<List<DouyinUserCookie>> GetValidCookies();
protected abstract bool IsCookieValid(DouyinUserCookie cookie);
protected abstract Task<DouyinVideoInfo> FetchVideoData(DouyinUserCookie cookie, string cursor);
protected abstract bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data);
protected abstract string GetNextCursor(DouyinVideoInfo data);
protected abstract string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2);
protected abstract string GetVideoFileName(DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate);
protected abstract string GetAuthorAvatarBasePath(DouyinUserCookie cookie);
protected abstract Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount);
protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item);
}
public class VideoEntityDifferences
{
public VideoTypeEnum VideoType { get; set; }
public string VideoTitleSimplify { get; set; } = string.Empty;
}
}
@@ -3,7 +3,7 @@
namespace dy.net.model
{
[SqlSugar.SugarTable(TableName = "login_user_info")]
public class LoginUserInfo
public class AdminUserInfo
{
public string UserName { get; set;}
public string Password { get; set; }
+9
View File
@@ -26,5 +26,14 @@ namespace dy.net.model
/// </summary>
public bool UperUseViedoTitle { get; set; }
/// <summary>
/// 是否下载图片视频
/// </summary>
public bool DownImageVideo { get; set; }
/// <summary>
/// 日志保留天数,防止容器日志太多,默认10天
/// </summary>
public int KeepLogDay { get; set; } = 10;
}
}
@@ -5,7 +5,7 @@ using SqlSugar;
namespace dy.net.model
{
[SugarTable(TableName = "dy_cookie")]
public class DyUserCookies
public class DouyinUserCookie
{
[SugarColumn(IsPrimaryKey = true)]
@@ -77,32 +77,39 @@ namespace dy.net.model
public string UpSavePath { get; set; }
/// <summary>
/// 图片视频存储路径
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string ImgSavePath { get; set; }
[SugarColumn(IsIgnore = true)]
public List<DyUpSecUserIdDto> UpSecUserIdsJson
public List<DouyinUpSecUserIdDto> UpSecUserIdsJson
{
get
{
// 反序列化逻辑(保持不变)
if (string.IsNullOrWhiteSpace(UpSecUserIds))
{
return new List<DyUpSecUserIdDto>();
return new List<DouyinUpSecUserIdDto>();
}
try
{
return JsonConvert.DeserializeObject<List<DyUpSecUserIdDto>>(UpSecUserIds);
return JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(UpSecUserIds);
}
catch (JsonSerializationException ex)
{
// 日志记录(按需添加)
// Logger.Error($"反序列化失败:{ex.Message},原始值:{UpSecUserIds}");
return new List<DyUpSecUserIdDto>();
return new List<DouyinUpSecUserIdDto>();
}
catch (Exception ex)
{
// 日志记录(按需添加)
// Logger.Error($"处理异常:{ex.Message}");
return new List<DyUpSecUserIdDto>();
return new List<DouyinUpSecUserIdDto>();
}
}
set
@@ -4,7 +4,7 @@ namespace dy.net.model
{
[SugarTable(TableName = "dy_collect_video")]
public class DyCollectVideo
public class DouyinVideo
{
/// <summary>
///
@@ -6,9 +6,9 @@ using SqlSugar;
namespace dy.net.repository
{
public class UserRepository : BaseRepository<LoginUserInfo>
public class AdminUserRepository : BaseRepository<AdminUserInfo>
{
public UserRepository(ISqlSugarClient db) : base(db)
public AdminUserRepository(ISqlSugarClient db) : base(db)
{
}
@@ -50,7 +50,7 @@ namespace dy.net.repository
}
public async Task<LoginUserInfo> GetUser(string userName=null)
public async Task<AdminUserInfo> GetUser(string userName=null)
{
if (string.IsNullOrWhiteSpace(userName))
{
@@ -89,7 +89,7 @@ namespace dy.net.repository
/// </summary>
/// <param name="userInfo"></param>
/// <returns></returns>
public (int code, string erro) InitUser(LoginUserInfo userInfo)
public (int code, string erro) InitUser(AdminUserInfo userInfo)
{
var isInit = this.GetFirst(x=>!string.IsNullOrWhiteSpace(x.Id));
if (isInit!=null)
@@ -3,22 +3,22 @@ using SqlSugar;
namespace dy.net.repository
{
public class DyCookieRepository : BaseRepository<DyUserCookies>
public class DouyinUserCookieRepository : BaseRepository<DouyinUserCookie>
{
// 注入SQLSugar客户端
public DyCookieRepository(ISqlSugarClient db) : base(db)
public DouyinUserCookieRepository(ISqlSugarClient db) : base(db)
{
}
public async Task<List<DyUserCookies>> GetAllCookies()
public async Task<List<DouyinUserCookie>> GetAllCookies()
{
return await this.GetListAsync(x=>x.Status==1);
}
public async Task<(List<DyUserCookies> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
public async Task<(List<DouyinUserCookie> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
{
var where = this.Db.Queryable<DyUserCookies>();
var where = this.Db.Queryable<DouyinUserCookie>();
var totalCount = await where.CountAsync();
var list = await where.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
@@ -6,10 +6,10 @@ using System.Linq;
namespace dy.net.repository
{
public class DyCollectVideoRepository : BaseRepository<DyCollectVideo>
public class DouyinVideoRepository : BaseRepository<DouyinVideo>
{
// 注入SQLSugar客户端
public DyCollectVideoRepository(ISqlSugarClient db) : base(db)
public DouyinVideoRepository(ISqlSugarClient db) : base(db)
{
}
@@ -23,7 +23,7 @@ namespace dy.net.repository
/// <param name="tag">可选标签过滤</param>
/// <param name="author">可选作者过滤</param>
/// <returns>分页结果(视频列表和总数)</returns>
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null,string viedoType=null, List<string> dates = null)
public async Task<(List<DouyinVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null,string viedoType=null, List<string> dates = null)
{
DateTime? start = null;
@@ -38,7 +38,7 @@ namespace dy.net.repository
start = Convert.ToDateTime(dates[0]);
}
var where = this.Db.Queryable<DyCollectVideo>()
var where = this.Db.Queryable<DouyinVideo>()
.WhereIF(!string.IsNullOrWhiteSpace(tag), x => x.Tag1 == tag)
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
@@ -49,7 +49,7 @@ namespace dy.net.repository
var totalCount = await where.CountAsync();
var list = await where.OrderByDescending(x=>x.SyncTime).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
if (list.Any()) {
var users= await this.Db.Queryable<DyUserCookies>().ToListAsync();
var users= await this.Db.Queryable<DouyinUserCookie>().ToListAsync();
foreach (var item in list)
{
var user= users.FirstOrDefault(x=>x.Id== item.CookieId);
@@ -72,7 +72,7 @@ namespace dy.net.repository
public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId,string ViedoNameSimplify)
{
var video= await this.Db.Queryable<DyCollectVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == "3")
var video= await this.Db.Queryable<DouyinVideo>().Where(x => x.AuthorId == AuthorId && x.ViedoType == "3")
.Where(x => x.VideoTitleSimplify == ViedoNameSimplify)
.OrderByDescending(x => x.CreateTime).FirstAsync();
@@ -4,12 +4,12 @@ using dy.net.repository;
namespace dy.net.service
{
public class UserService
public class AdminUserService
{
private readonly UserRepository _userRepository;
private readonly AdminUserRepository _userRepository;
public UserService(UserRepository userRepository)
public AdminUserService(AdminUserRepository userRepository)
{
_userRepository = userRepository;
}
@@ -20,7 +20,7 @@ namespace dy.net.service
return await _userRepository.UpdatePwd(loginUser);
}
public async Task<LoginUserInfo> GetUser(string userName=null)
public async Task<AdminUserInfo> GetUser(string userName=null)
{
return await _userRepository.GetUser(userName);
}
@@ -32,7 +32,7 @@ namespace dy.net.service
public (int code, string erro) InitUser()
{
LoginUserInfo userInfo = new LoginUserInfo
AdminUserInfo userInfo = new AdminUserInfo
{
UserName = "douyin",
Password = "douyin2025",
@@ -7,11 +7,11 @@ using System.Threading.Tasks;
namespace dy.net.service
{
public class CommonService
public class DouyinCommonService
{
private readonly ISqlSugarClient sqlSugarClient;
public CommonService(ISqlSugarClient sqlSugarClient)
public DouyinCommonService(ISqlSugarClient sqlSugarClient)
{
this.sqlSugarClient = sqlSugarClient;
}
@@ -20,21 +20,31 @@ namespace dy.net.service
{
return sqlSugarClient.Queryable<AppConfig>().First();
}
public AppConfig InitConfig()
/// <summary>
/// 初始化并返回配置
/// </summary>
/// <param name="downLoadImage"></param>
/// <returns></returns>
public AppConfig InitConfig(bool downLoadImage)
{
var conf = GetConfig();
if (conf != null)
{
conf.DownImageVideo = downLoadImage;
sqlSugarClient.Updateable(conf).ExecuteCommand();
return conf;
}
else
{
AppConfig config = new AppConfig
{
Id = IdGener.GetLong().ToString(),
Cron = "30",
BatchCount = 10
BatchCount = 10,
DownImageVideo = downLoadImage,
KeepLogDay = 10
};
sqlSugarClient.Insertable<AppConfig>(config).ExecuteCommand();
sqlSugarClient.Insertable(config).ExecuteCommand();
return config;
}
@@ -66,7 +76,7 @@ namespace dy.net.service
/// </summary>
public void UpdateCollectViedoType()
{
var collectViedos = sqlSugarClient.Queryable<DyCollectVideo>().Where(x => string.IsNullOrWhiteSpace(x.ViedoType)).ToList();
var collectViedos = sqlSugarClient.Queryable<DouyinVideo>().Where(x => string.IsNullOrWhiteSpace(x.ViedoType)).ToList();
if (collectViedos.Any())
{
@@ -85,7 +95,7 @@ namespace dy.net.service
{
//var sql = "update dy_cookie set CollHasSyncd=0,FavHasSyncd=0,UperSyncd=0";
// sqlSugarClient.Ado.ExecuteCommand(sql) > 0;
var cookies = sqlSugarClient.Queryable<DyUserCookies>().ToList();
var cookies = sqlSugarClient.Queryable<DouyinUserCookie>().ToList();
foreach (var cookie in cookies)
{
@@ -95,7 +105,7 @@ namespace dy.net.service
var upers = cookie.UpSecUserIds;
if (!string.IsNullOrWhiteSpace(upers))
{
var uperList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DyUpSecUserIdDto>>(upers);
var uperList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DouyinUpSecUserIdDto>>(upers);
if (uperList != null && uperList.Count > 0)
{
foreach (var uper in uperList)
@@ -5,28 +5,28 @@ using System.Linq.Expressions;
namespace dy.net.service
{
public class DyCookieService
public class DouyinCookieService
{
private readonly DyCookieRepository _cookieRepository;
private readonly DouyinUserCookieRepository _cookieRepository;
public DyCookieService(DyCookieRepository cookieRepository)
public DouyinCookieService(DouyinUserCookieRepository cookieRepository)
{
_cookieRepository = cookieRepository;
}
public Task<List<DyUserCookies>> GetAllCookies()
public Task<List<DouyinUserCookie>> GetAllCookies()
{
return _cookieRepository.GetAllCookies();
}
public async Task<bool> Add(DyUserCookies dyUserCookies)
public async Task<bool> Add(DouyinUserCookie dyUserCookies)
{
return await _cookieRepository.InsertAsync(dyUserCookies);
}
public bool Init()
public bool InitCookie()
{
var initId= "2026";
var exist = _cookieRepository.GetFirst(x => x.Id == initId);
@@ -34,7 +34,7 @@ namespace dy.net.service
{
return false;
}
var cookie = new DyUserCookies
var cookie = new DouyinUserCookie
{
UserName = "douyin",
Cookies = "--",
@@ -46,25 +46,26 @@ namespace dy.net.service
UpSavePath = "/app/uper",
CollHasSyncd = 0,
FavHasSyncd = 0,
UperSyncd = 0
UperSyncd = 0,
ImgSavePath="/app/images",
};
return _cookieRepository.Insert(cookie);
}
// 查询单个
public async Task<DyUserCookies> GetByIdAsync(string id)
public async Task<DouyinUserCookie> GetByIdAsync(string id)
{
return await _cookieRepository.GetByIdAsync(id);
}
// 查询列表(可加条件)
public async Task<(List<DyUserCookies> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
public async Task<(List<DouyinUserCookie> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize)
{
return await _cookieRepository.GetPagedAsync(pageIndex, pageSize);
}
// 更新
public async Task<bool> UpdateAsync(DyUserCookies dyUserCookies)
public async Task<bool> UpdateAsync(DouyinUserCookie dyUserCookies)
{
return await _cookieRepository.UpdateAsync(dyUserCookies);
}
@@ -7,12 +7,12 @@ using System.Net.Http;
namespace dy.net.service
{
public class DyHttpClientService
public class DouyinHttpClientService
{
public static readonly string DouYinApi = "https://www.douyin.com/aweme/v1/web/aweme";
// 随机数生成器(避免重复实例化,保证随机性)
private readonly IHttpClientFactory _clientFactory;
public DyHttpClientService(IHttpClientFactory clientFactory)
public DouyinHttpClientService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
@@ -24,7 +24,7 @@ namespace dy.net.service
/// <param name="count"></param>
/// <param name="cookie"></param>
/// <returns></returns>
public async Task<CollectVideoInfo> SyncCollectVideos(string cursor, string count, string cookie)
public async Task<DouyinVideoInfo> SyncCollectVideos(string cursor, string count, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
{
@@ -50,7 +50,7 @@ namespace dy.net.service
}
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
var dics = DySyncBaseParamDics.CollectParams;
var dics = DouyinBaseParamDics.CollectParams;
dics["cursor"]=cursor;
dics["count"] = count;
try
@@ -68,7 +68,7 @@ namespace dy.net.service
if (respose.IsSuccessStatusCode)
{
var data = await respose.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
}
else
{
@@ -92,7 +92,7 @@ namespace dy.net.service
/// <param name="cookie"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public async Task<CollectVideoInfo> SyncFavoriteVideos(string count,string cursor, string secUserId, string cookie)
public async Task<DouyinVideoInfo> SyncFavoriteVideos(string count,string cursor, string secUserId, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
{
@@ -118,7 +118,7 @@ namespace dy.net.service
}
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
var dics = DySyncBaseParamDics.FavoriteParams;
var dics = DouyinBaseParamDics.FavoriteParams;
{
// 添加动态参数
dics["max_cursor"] = cursor;
@@ -133,7 +133,7 @@ namespace dy.net.service
if (respose.IsSuccessStatusCode)
{
var data = await respose.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
}
else
{
@@ -158,7 +158,7 @@ namespace dy.net.service
/// <param name="cookie"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public async Task<CollectVideoInfo> SyncUpderPostVideos(string count, string cursor, string secUserId, string cookie)
public async Task<DouyinVideoInfo> SyncUpderPostVideos(string count, string cursor, string secUserId, string cookie)
{
if (string.IsNullOrWhiteSpace(cursor))
{
@@ -184,7 +184,7 @@ namespace dy.net.service
}
httpClient.DefaultRequestHeaders.Add("Cookie", cookie);
var parameters = DySyncBaseParamDics.UpderPostParams;
var parameters = DouyinBaseParamDics.UpderPostParams;
{
// 添加动态参数
parameters["max_cursor"] = cursor;
@@ -204,7 +204,7 @@ namespace dy.net.service
if (respose.IsSuccessStatusCode)
{
var data = await respose.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
return JsonConvert.DeserializeObject<DouyinVideoInfo>(data);
}
else
{
@@ -3,11 +3,11 @@ using dy.net.job;
namespace dy.net.service
{
public class QuartzJobService
public class DouyinQuartzJobService
{
private readonly ISchedulerFactory _schedulerFactory;
public QuartzJobService(ISchedulerFactory schedulerFactory)
public DouyinQuartzJobService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}
@@ -20,7 +20,7 @@ namespace dy.net.service
/// <returns></returns>
public async Task StartJob(string expression,int delay=5000)
{
await StartCollectJob(expression);
//await StartCollectJob(expression);
await Task.Delay(delay);
//如果是数字则加1分钟,减少并发
@@ -29,7 +29,7 @@ namespace dy.net.service
cron++;
expression = cron.ToString();
}
await StartFavoriteJob(expression);
//await StartFavoriteJob(expression);
await Task.Delay(delay);
if (int.TryParse(expression, out int cron2))
@@ -55,7 +55,7 @@ namespace dy.net.service
await __scheduler1.DeleteJob(jobKey);//删掉原来的
}
IJobDetail job = JobBuilder.Create<DouYinCollectSyncJob>()
IJobDetail job = JobBuilder.Create<DouyinCollectSyncJob>()
.WithIdentity(jobKey)
.Build();
ITrigger trigger;
@@ -108,7 +108,7 @@ namespace dy.net.service
await __scheduler1.DeleteJob(jobKey);//删掉原来的
}
IJobDetail job = JobBuilder.Create<DouYinFavoritSyncJob>()
IJobDetail job = JobBuilder.Create<DouyinFavoritSyncJob>()
.WithIdentity(jobKey)
.Build();
ITrigger trigger;
@@ -160,7 +160,7 @@ namespace dy.net.service
await __scheduler1.DeleteJob(jobKey);//删掉原来的
}
IJobDetail job = JobBuilder.Create<DouYinUperPostSyncJob>()
IJobDetail job = JobBuilder.Create<DouyinUperPostSyncJob>()
.WithIdentity(jobKey)
.Build();
ITrigger trigger;
@@ -7,18 +7,18 @@ using System.Threading.Tasks;
namespace dy.net.service
{
public class DyCollectVideoService
public class DouyinVideoService
{
private readonly DyCollectVideoRepository _dyCollectVideoRepository;
private readonly DouyinVideoRepository _dyCollectVideoRepository;
public DyCollectVideoService(DyCollectVideoRepository dyCollectVideoRepository)
public DouyinVideoService(DouyinVideoRepository dyCollectVideoRepository)
{
_dyCollectVideoRepository = dyCollectVideoRepository;
}
public async Task<bool> batchInsert(List<DyCollectVideo> videos)
public async Task<bool> batchInsert(List<DouyinVideo> videos)
{
// 边界处理:如果传入的列表为空,直接返回成功(或根据业务返回false)
@@ -56,7 +56,7 @@ namespace dy.net.service
public async Task<VideoStaticsDto> GetStatics()
{
List<DyCollectVideo> list = await this._dyCollectVideoRepository.GetAllAsync();
List<DouyinVideo> list = await this._dyCollectVideoRepository.GetAllAsync();
if (!list.Any())
return new VideoStaticsDto();
var Categories = list.GroupBy(x => x.Tag1).Select(x => new VideoStaticsItemDto { Name = x.Key, Count = x.LongCount() }).OrderByDescending(p => p.Count).ToList();
@@ -85,7 +85,7 @@ namespace dy.net.service
//分页查询
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null, string viedoType = null, List<string>? dates = null)
public async Task<(List<DouyinVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null, string viedoType = null, List<string>? dates = null)
{
return await _dyCollectVideoRepository.GetPagedAsync(pageIndex, pageSize, tag, author, viedoType, dates);
}
-69
View File
@@ -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;
}
}
}
-163
View File
@@ -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>
/// 字节转GB1GB = 1024^3字节)
/// </summary>
private static string ConvertBytesToGb(long bytes)
{
if (bytes <= 0) return "磁盘空间计算错误";
double gb = (double)bytes / (1024 * 1024 * 1024);
return $"{gb:F2} GB";
}
}
}
@@ -1,6 +1,6 @@
namespace dy.net.utils
{
public class DySyncBaseParamDics
public class DouyinBaseParamDics
{
public static Dictionary<string, string> CollectParams { get; } = InitializeUserCollecParams();
public static Dictionary<string, string> FavoriteParams { get; } = InitializeUserFavoriteParams();
+1 -1
View File
@@ -7,7 +7,7 @@ namespace dy.net.utils
public static class Md5Util
{
public static string JWT_TOKEN_KEY = "dy.net-key-" + IdGener.GetGuid();
public static string JWT_TOKEN_KEY = "dysync.net-key-" + IdGener.GetGuid();
public static string Md5(this string inputString)
{
+39 -2
View File
@@ -10,7 +10,7 @@ namespace dy.net.utils
public class NfoFileGenerator
{
public static void GenerateNfoFile(VideoNfo videoInfo, string filePath)
public static void GenerateNfoFile(DouyinVideoNfo videoInfo, string filePath)
{
try
{
@@ -44,6 +44,36 @@ namespace dy.net.utils
}
}
// --- 新增:处理演员信息 ---
if (videoInfo.Actors != null && videoInfo.Actors.Any())
{
var actorsElement = new XElement("actors");
foreach (var actor in videoInfo.Actors)
{
// 至少需要演员姓名
if (!string.IsNullOrWhiteSpace(actor.Name))
{
var actorElement = new XElement("actor");
actorElement.Add(new XElement("name", CleanInvalidXmlChars(actor.Name)));
// 可选的角色和头像
if (!string.IsNullOrWhiteSpace(actor.Role))
actorElement.Add(new XElement("role", CleanInvalidXmlChars(actor.Role)));
if (!string.IsNullOrWhiteSpace(actor.Thumb))
actorElement.Add(new XElement("thumb", CleanInvalidXmlChars(actor.Thumb)));
actorsElement.Add(actorElement);
}
}
// 将整个 <actors> 节点添加到根节点
if (actorsElement.HasElements)
{
root.Add(actorsElement);
}
}
// --- 演员信息处理结束 ---
if (!string.IsNullOrWhiteSpace(videoInfo.Thumbnail))
root.Add(new XElement("thumb", new XAttribute("aspect", "poster"), CleanInvalidXmlChars(videoInfo.Thumbnail)));
@@ -58,11 +88,18 @@ namespace dy.net.utils
root
);
// 确保目录存在
string directory = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
doc.Save(filePath);
}
catch (Exception ex)
{
Serilog.Log.Error($"生成 {videoInfo?.Title ?? ""}, NFO文件时出错: {ex.Message}");
Serilog.Log.Error(ex, $"生成 {videoInfo?.Title ?? ""} NFO 文件时出错");
}
}
-128
View File
@@ -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}");
}
}
}
}
+25 -7
View File
@@ -22,6 +22,17 @@ namespace dy.net.utils
/// 非法字符替换后的占位符(也可设为空字符串)
/// </summary>
private const string IllegalCharReplacement = "";
// 修正:使用 \U 前缀来表示超过 \uFFFF 的Unicode码点
private static readonly Regex _emojiRegex = new Regex(
@"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\U0001E000-\U0001EFFF\u2600-\u2B55\u200D]",
RegexOptions.Compiled);
private static readonly Regex _hashtagRegex = new Regex(@"\#\S+", RegexOptions.Compiled);
private static readonly Regex _invalidCharsRegex;
private static readonly Regex _multipleUnderscoresRegex = new Regex(@"_+", RegexOptions.Compiled);
#endregion
#region
@@ -42,7 +53,7 @@ namespace dy.net.utils
// 3. 长度控制:按UTF-8字节数截断(避免超系统限制)
string truncatedTitle = TruncateByByteLength(purifiedTitle, MaxFileNameBytes);
return truncatedTitle;
return truncatedTitle.Trim();
}
#endregion
@@ -57,7 +68,7 @@ namespace dy.net.utils
title = id;
}
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy
title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
//title = Regex.Replace(title, @"#\S+", "", RegexOptions.Compiled);
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
@@ -81,7 +92,7 @@ namespace dy.net.utils
title = Regex.Replace(title, $"{Separator}+", Separator.ToString(), RegexOptions.Compiled);
// 步骤6:移除首尾无效字符(分隔符、点号)
title = title.Trim(Separator, '.');
title = title.Replace(" ","").Trim(Separator, '.');
// 容错:如果净化后为空,返回默认值
return string.IsNullOrWhiteSpace(title) ? id : title;
@@ -122,6 +133,8 @@ namespace dy.net.utils
// 清理路径中的特殊字符(避免创建文件夹失败)
public static string SanitizePath(string path)
{
try
{
if (string.IsNullOrWhiteSpace(path))
{
@@ -129,11 +142,11 @@ namespace dy.net.utils
}
// 步骤1:移除话题标签(#xxx 或 #xxx#yyy
path = Regex.Replace(path, @"#\S+", "", RegexOptions.Compiled);
//path = Regex.Replace(path, @"#\S+", "", RegexOptions.Compiled);
// 步骤2:移除表情符号(匹配常见表情Unicode区块)
string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
path = Regex.Replace(path, emojiPattern, "", RegexOptions.Compiled);
//string emojiPattern = @"[\u1F600-\u1F64F\u1F300-\u1F5FF\u1F680-\u1F6FF\u1E000-\u1EFFF\u2600-\u2B55\u200D]";
//path = Regex.Replace(path, emojiPattern, "", RegexOptions.Compiled);
foreach (var c in Path.GetInvalidFileNameChars())
@@ -144,7 +157,12 @@ namespace dy.net.utils
{
path = path.Substring(0, 50);
}
return path.Trim();
return path.Trim().Replace(" ", "");
}
catch (Exception ex)
{
return path;
}
}
}
}