Files
douyin/job/DouYinFavoritSyncJob.cs
T
2025-10-19 11:43:14 +08:00

356 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ClockSnowFlake;
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 DouYinFavoritSyncJob : IJob
{
private readonly DyCookieService _dyCookieService;
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)
{
_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();
if (cookies == null || !cookies.Any())
{
Serilog.Log.Debug("favorite-无可用Cookie,任务终止");
return;
}
else
{
Serilog.Log.Debug($"favorite-当前有{cookies.Count}个cookie开启了同步,即将开始同步");
//return;
}
foreach (var cookie in cookies)
{
Serilog.Log.Debug($"favorite-开始同步 Cookie-[{cookie.UserName}]喜欢的视频");
if (string.IsNullOrWhiteSpace(cookie.Cookies)|| cookie.Cookies.Length<1000)
{
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]无效,跳过");
continue;
}
if(string.IsNullOrWhiteSpace(cookie.FavSavePath))
{
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]未设置保存路径,跳过");
continue;
}
if (string.IsNullOrWhiteSpace(cookie.SecUserId)|| cookie.SecUserId.Length<10) {
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}]未设置SecUserId,跳过");
continue;
}
try
{
int syncCount = 0;// 记录本次Cookie同步的视频数量
int index = 0;
bool hasMore = true;
string cursor = "0";
while (hasMore)
{
var data = await _douyinService.SyncFavoriteVideos(count,cursor, cookie.SecUserId, cookie.Cookies);
hasMore = data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
if (cookie.FavHasSyncd == 1)
{
Serilog.Log.Debug($"favorite-Cookie[{cookie.UserName}]已完整同步过,后续只获取最新一页数据");
}
//Serilog.Log.Debug($"还有数据需要同步吗?{(hasMore ? "YES" : "NO")}");
if (data == null)
{
Serilog.Log.Debug($"favorite-Cookie[{cookie.UserName}]获取喜欢的数据失败,请检查一下Cookie和sec_user_id");
break;
}
cursor = data != null && !string.IsNullOrWhiteSpace(data.MaxCursor) ? data.MaxCursor : "0";
if (data.AwemeList == null || !data.AwemeList.Any())
{
break;
}
List<DyCollectVideo> videos = new List<DyCollectVideo>();
foreach (var item in data.AwemeList)
{
if (item == null)
continue;
if (item.Video == null)
continue;
if (item.Video.BitRate == null)
continue;
var v = item.Video.BitRate.FirstOrDefault();
var tags = item.VideoTags;
if (v == null) continue;
var videoUrl = v.PlayAddr.UrlList != null && v.PlayAddr.UrlList.Any() ? v.PlayAddr.UrlList[0] : null;
if (string.IsNullOrWhiteSpace(videoUrl)) continue;
var tag1 = tags.FirstOrDefault(x => x.Level == 1)?.TagName;
var tag2 = tags.FirstOrDefault(x => x.Level == 2)?.TagName;
var tag3 = tags.FirstOrDefault(x => x.Level == 3)?.TagName;
string saveFolder = CreateSaveFolder(cookie, item, tag1, tag2);
// 创建文件夹(提前创建,避免下载时才操作)
if (!Directory.Exists(saveFolder))
{
Directory.CreateDirectory(saveFolder);
}
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
var savePath = Path.Combine(saveFolder, fileName);
if (!File.Exists(savePath))
{
Serilog.Log.Debug($"favorite-视频[{SanitizePath(item.Desc)}]开始下载");
await Task.Delay(_random.Next(1, 4) * 1000);
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
Serilog.Log.Debug($"favorite-视频[{SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
if (downVideo)
{
await DownVideoCover(item, saveFolder, cookie.Cookies);
// 用AuthorId做文件名,避免昵称特殊字符
var avatarImgName = $"{item.Author.Uid}.jpg";
var avatarSavePath = Path.Combine(cookie.FavSavePath, "author", avatarImgName);
await DownAuthorAvatar(cookie.FavSavePath, item, avatarSavePath, cookie.Cookies);
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
? item.Author.AvatarLarger.UrlList[0] : null;
// 尝试使用AvatarThumb作为备用头像URL
if (string.IsNullOrWhiteSpace(avatarUrl))
{
avatarUrl = item.Author?.AvatarThumb?.UrlList != null && item.Author.AvatarThumb.UrlList.Any()
? item.Author.AvatarThumb.UrlList[0] : null;
}
// 构造视频数据
DyCollectVideo video = new()
{
ViedoType="1",
AwemeId = item.AwemeId,
Author = item.Author?.Nickname,
AuthorId = item.Author?.Uid,
AuthorAvatar = avatarSavePath,
AuthorAvatarUrl = avatarUrl,
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
VideoTitle = item.Desc,
Id = IdGener.GetLong().ToString(),
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
FileSize = v.PlayAddr.DataSize,
FileHash = v.PlayAddr.FileHash,
Tag1 = tag1,
Tag2 = tag2,
Tag3 = tag3,
VideoUrl = videoUrl,
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? item.Video.Cover.UrlList[0] : null,
VideoSavePath = savePath,
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
SyncTime = DateTime.Now,
DyUserId = data.Uid,
CookieId = cookie.Id
};
videos.Add(video);
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
NfoFileGenerator.GenerateNfoFile(new VideoNfo
{
Author = video.Author,
Poster = Path.Combine(saveFolder, "poster.jpg"),
Title = video.VideoTitle,
Thumbnail= Path.Combine(saveFolder, "fanart.jpg"),
ReleaseDate = video.CreateTime,
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
}, nfoPath);
}
}
else
{
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
}
}
index++;
// 批量保存到数据库(减少数据库操作频率)
if (videos.Any())
{
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
try
{
await _douyinVideoService.batchInsert(videos);
syncCount += videos.Count;
}
catch (Exception ex)
{
Serilog.Log.Error("favorite-批量保存视频到数据库失败:{ex.Message}", ex);
// 收集所有需要删除的目录(去重)
foreach (var video in videos)
{
if (!string.IsNullOrWhiteSpace(video.VideoSavePath) && Directory.Exists(video.VideoSavePath))
{
var deletePath = Path.GetDirectoryName(video.VideoSavePath);
Directory.Delete(deletePath, recursive: true);
//Serilog.Log.Debug($"已删除失败视频目录:{deletePath}");
}
}
Serilog.Log.Debug("favorite-因为数据库没有保存成功,本次下载的视频目录已尝试删除,将继续处理下一页数据");
}
}
else {
//Serilog.Log.Debug($"没有查询到新的视频");
}
await Task.Delay(_random.Next(5, 10) * 1000);
}
if (syncCount > 0)
{
Serilog.Log.Debug($"Cookie-[{cookie.UserName}],本次共同步成功{syncCount}条视频");
// 更新同步状态为已同步
cookie.FavHasSyncd = 1;
await _dyCookieService.UpdateAsync(cookie);
}
else {
Serilog.Log.Debug($"favorite-Cookie-[{cookie.UserName}],本次没有查询到新的视频");
}
}
catch (Exception ex)
{
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错:{ex.Message}");
Serilog.Log.Error($"favorite-处理Cookie[{cookie.Id}]时出错22{ex.StackTrace}");
}
}
}
/// <summary>
/// 下载视频封面
/// </summary>
/// <param name="item"></param>
/// <param name="saveFolder"></param>
/// <param name="cookie"></param>
/// <returns></returns>
private async Task DownVideoCover(Aweme item, string saveFolder, string cookie)
{
var coverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
? 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)) {
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" : SanitizePath(tag1);
List<string> pathParts = new List<string> { cookie.FavSavePath, 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);
}
// 清理路径中的特殊字符(避免创建文件夹失败)
private static string SanitizePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
path = "其他";
}
foreach (var c in Path.GetInvalidFileNameChars())
{
path = path.Replace(c, '_');
}
if (path.Length > 50)
{
path = path.Substring(0, 50);
}
return path.Trim();
}
}
}