diff --git a/Controllers/ConfigController.cs b/Controllers/ConfigController.cs index 27f085e..3b21aba 100644 --- a/Controllers/ConfigController.cs +++ b/Controllers/ConfigController.cs @@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; +using Quartz.Util; +using System.Xml.Linq; using static Dm.net.buffer.ByteArrayBuffer; namespace dy.net.Controllers @@ -32,7 +34,7 @@ namespace dy.net.Controllers /// /// 分页查询 /// - /// 分页结果(视频列表和总数) + /// 分页结果 [HttpPost("paged")] public async Task GetPagedAsync( PageRequestDto dto) @@ -51,16 +53,26 @@ namespace dy.net.Controllers } ); } + + + /// + /// 查询所有用户Cookie + /// + /// + [HttpGet("list")] + public async Task GetAllList() + { + var list = await dyCookieService.GetAllOpendAsync(); + var result = list.Select(x => new { key= x.MyUserId, name= x.UserName }); + return Ok(new { code = 0, data = result }); + } /// /// 新增用户Cookie /// [HttpPost("add")] - public async Task AddAsync([FromBody] DouyinUserCookie dyUserCookies) + public async Task AddAsync([FromBody] DouyinCookie dyUserCookies) { - if(dyUserCookies.UpSecUserIdsJson!=null) - { - dyUserCookies.UpSecUserIds = JsonConvert.SerializeObject( dyUserCookies.UpSecUserIdsJson); - } + var result = await dyCookieService.Add(dyUserCookies); if (result) { @@ -74,14 +86,9 @@ namespace dy.net.Controllers /// 更新用户Cookie /// [HttpPost("update")] - public async Task UpdateAsync([FromBody] DouyinUserCookie dyUserCookies) + public async Task UpdateAsync([FromBody] DouyinCookie dyUserCookies) { - if (dyUserCookies.UpSecUserIdsJson != null ) - { - dyUserCookies.UpSecUserIds = JsonConvert.SerializeObject(dyUserCookies.UpSecUserIdsJson); - } - if (dyUserCookies.Id == "0") { dyUserCookies.Id=IdGener.GetLong().ToString(); @@ -144,26 +151,16 @@ namespace dy.net.Controllers public async Task ExecuteJobNow() { var config = commonService.GetConfig(); - await quartzJobService.StartJob(config.Cron); + if (config!=null) + await quartzJobService.InitOrReStartAllJobs(config.Cron.ToString()); return Ok(new { code = 0 , error = "" }); } private async Task ReStartJob() { var config= commonService.GetConfig(); - - //var cookies = await dyCookieService.GetAllCookies(); - //重置同步状态 - //foreach (var cookie in cookies) - //{ - // cookie.CollHasSyncd = 0; - // cookie.FavHasSyncd = 0; - // cookie.UperSyncd = 0; - // await dyCookieService.UpdateAsync(cookie); - //} - if (config!=null) - quartzJobService.StartJob(config.Cron); + quartzJobService.InitOrReStartAllJobs(config.Cron.ToString()); //避免前端等待 } } diff --git a/Controllers/FollowController.cs b/Controllers/FollowController.cs new file mode 100644 index 0000000..4adf63a --- /dev/null +++ b/Controllers/FollowController.cs @@ -0,0 +1,149 @@ +using dy.net.dto; +using dy.net.service; +using dy.net.utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace dy.net.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class FollowController : ControllerBase + { + private readonly DouyinFollowService _douyinFollowService; + private readonly DouyinQuartzJobService _douyinQuartzJobService; + + public FollowController(DouyinFollowService douyinFollowService, DouyinQuartzJobService douyinQuartzJobService) + { + this._douyinFollowService = douyinFollowService; + _douyinQuartzJobService = douyinQuartzJobService; + } + + + + /// + /// 分页查询 + /// + /// 分页结果 + [HttpPost("paged")] + public async Task GetPagedAsync( + FollowRequestDto dto) + { + if (string.IsNullOrWhiteSpace(dto.MySelfId)) + { + return Ok(new + { + code = 0, + data = new { } + }); + } + var (list, totalCount) = await _douyinFollowService.GetPagedAsync(dto); + return Ok(new + { + code = 0, + data = new + { + data = list, + total = totalCount, + pageIndex = dto.PageIndex, + pageSize = dto.PageSize + } + } + ); + } + /// + /// 重新同步-单次 + /// + /// + [HttpGet("sync")] + public async Task SyncFollowList() + { + //后台异步 + _douyinQuartzJobService.StartFollowJobOnceAsync(); + await Task.Delay(1000); + return Ok(new { code = 0 }); + } + + /// + /// 修改关注同步状态 + /// + /// + /// + [HttpPost("openOrCloseSync")] + public async Task OpenOrCloseSync(FollowUpdateDto dto) + { + + if (dto.OpenSync) + { + if (!string.IsNullOrWhiteSpace(dto.SavePath)) + { + if (!DouyinFileNameHelper.IsValidWithoutSpecialChars(dto.SavePath)) + { + return Ok(new + { + code = -1, + msg = "请输入有效文件夹名称(字母数字中文简体)" + }); + } + + if (dto.SavePath.Length > 10) + { + return Ok(new + { + code = -1, + msg = "请输入有效文件夹名称(最长10)" + }); + } + } + } + + var result= await _douyinFollowService.OpenOrCloseSync(dto); + return Ok(new + { + code = result ? 0 : -1, + data = result + }); + } + + /// + /// 修改关注全量同步状态 + /// + /// + /// + [HttpPost("openOrCloseFullSync")] + public async Task OpenOrCloseFullSync(FollowUpdateDto dto) + { + + if (dto.FullSync) + { + if (!string.IsNullOrWhiteSpace(dto.SavePath)) + { + if (!DouyinFileNameHelper.IsValidWithoutSpecialChars(dto.SavePath)) + { + return Ok(new + { + code = -1, + msg = "请输入有效文件夹名称(字母数字中文简体)" + }); + } + + if (dto.SavePath.Length > 10) + { + return Ok(new + { + code = -1, + msg = "请输入有效文件夹名称(最长10)" + }); + } + } + } + + var result = await _douyinFollowService.OpenOrCloseFullSync(dto); + return Ok(new + { + code = result ? 0 : -1, + data = result + }); + } + } +} diff --git a/Controllers/LogsController.cs b/Controllers/LogsController.cs index 3bd2145..ef274a0 100644 --- a/Controllers/LogsController.cs +++ b/Controllers/LogsController.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Http; +using dy.net.dto; +using dy.net.service; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Net.Http; @@ -9,10 +11,12 @@ namespace dy.net.Controllers public class LogsController : ControllerBase { private readonly IWebHostEnvironment webHostEnvironment; + private readonly DouyinHttpClientService douyinHttpClientService; - public LogsController(IWebHostEnvironment webHostEnvironment) + public LogsController(IWebHostEnvironment webHostEnvironment,DouyinHttpClientService douyinHttpClientService) { this.webHostEnvironment = webHostEnvironment; + this.douyinHttpClientService = douyinHttpClientService; } [HttpGet] @@ -33,6 +37,41 @@ namespace dy.net.Controllers //var fileContent = encoding.GetString(fileBytes); //return Content (fileContent, "text/plain", encoding); } + ///// + ///// 测试 + ///// + ///// + //[HttpGet] + //public async Task TestHttpClient() + //{ + // string count = "20"; + // string offset = "0"; + // string cookie = ""; + // bool hasmore = true; + + // { + // cookie = "gfkadpd=6383,41079; passport_csrf_token=a5da631bccfeddbc877bd4e1f72f142f; passport_csrf_token_default=a5da631bccfeddbc877bd4e1f72f142f; enter_pc_once=1; UIFID_TEMP=0e81ba593d64ebaca259bdbe302de8d7e55ac2e982f7412f10fbc5c77c64bb8b8830ffed112ea8b3bc54f3b6e2af3856daafa4efaafef18953612820da493aea7fe297beed13f9b141a7a3100c4d9aa7d1428f23d05cf5979bf209937ba9ad00da1efa59922c98942d4ce449e736ee2b; x-web-secsdk-uid=849eef8b-171b-4bd7-9cce-5752f14e53fd; douyin.com; device_web_cpu_core=32; device_web_memory_size=8; architecture=amd64; hevc_supported=true; dy_swidth=1707; dy_sheight=1067; s_v_web_id=verify_migsr5je_BJ1YiVbY_uR2U_4VXu_8Uje_GgwhVf7Y6hb8; fpk1=U2FsdGVkX19wNXQ61AQVcAXPKhYMylJU4AcD8JGkFeNZmB4mYB1XNB+bc+hU1lhq6ZruxA+I9Nwv4EeNoW414w==; fpk2=df46e1d3e7507fa3d6888a71a1894105; strategyABtestKey=%221764209447.927%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.5%7D; xgplayer_user_id=90669686736; bd_ticket_guard_client_web_domain=2; is_dash_user=1; n_mh=Bdbeto8B8DXIgXtXZG-7Vw3HWofM6NyG-v3Wgk1pL6s; is_staff_user=false; __security_server_data_status=1; publish_badge_show_info=%220%2C0%2C0%2C1764209517482%22; UIFID=0e81ba593d64ebaca259bdbe302de8d7e55ac2e982f7412f10fbc5c77c64bb8b8830ffed112ea8b3bc54f3b6e2af3856daafa4efaafef18953612820da493aeaebdeac0537b99b8e68f343c2476cf6347f7751a87d92952128116470f147215cf46b949f43f31aedcb660fd3c5c7eb2145183cc93d1b4202205b4af7d7c69ab55e860f96e315899a2ee74a262273694ec973ba682bb6fdc351e0e66250b21cf9aef3ccaa3e0c28b085fd095e947d92a5336b9e65706d649a7b79541feab3487f; SelfTabRedDotControl=%5B%5D; xg_device_score=7.708466147936333; my_rd=2; stream_player_status_params=%22%7B%5C%22is_auto_play%5C%22%3A0%2C%5C%22is_full_screen%5C%22%3A0%2C%5C%22is_full_webscreen%5C%22%3A0%2C%5C%22is_mute%5C%22%3A0%2C%5C%22is_speed%5C%22%3A1%2C%5C%22is_visible%5C%22%3A0%7D%22; ttwid=1%7CffZO3FHfC8TRR6jdc7mfx3iFnrCqB_UuViDs3Lvj484%7C1764213923%7C12e8625d232e8a4ef61fc36807cbbf75d47e741e1e3ee82818ce82fc95612188; passport_mfa_token=Cjdg9xehkrlYj4Y1CWlAlHwJkHR2vpgE2ofiFeM3C8Zinm%2FK4vD%2BS%2Fk4k0uLAYrRsB3jJ8xoZlY6GkoKPAAAAAAAAAAAAABPwiYdUJ8TJRCoJEg7%2BU1U9xJn8%2FiVx37dL0fj0JsjXpjYp%2BFLThU1gLHtftqV3kgk4RDl0IIOGPax0WwgAiIBA3w4KGo%3D; d_ticket=b5badc9fd18022a5dd711f9f79bd33bf33b59; passport_assist_user=CkGOoW38C6tHKDGFYJ-Az5SHV8tHsa8HLiQy3uxuO_Qxf_gBNX1gwhyZd9i2pSAqGqUDpE5z3XSLYripy9Qf2KRRlRpKCjwAAAAAAAAAAAAAT8M736Jo4z6WK0kchkJMfk7WgAGu6ZEzrFSguCH7TfcT0niPHFmH_gIzWAEnmo8pqD4QmNKCDhiJr9ZUIAEiAQO-WDqX; sid_guard=a8f80e93207ab87a7128acf6da58e379%7C1764213945%7C5183999%7CMon%2C+26-Jan-2026+03%3A25%3A44+GMT; uid_tt=339c6ee64effdce094392ef399cb7ecc; uid_tt_ss=339c6ee64effdce094392ef399cb7ecc; sid_tt=a8f80e93207ab87a7128acf6da58e379; sessionid=a8f80e93207ab87a7128acf6da58e379; sessionid_ss=a8f80e93207ab87a7128acf6da58e379; session_tlb_tag=sttt%7C9%7CqPgOkyB6uHpxKKz22ljjef_________B-B1oMrODP-ItQszjCwbW8eVitD4IIGm5p5M8HZ9mUmY%3D; session_tlb_tag_bk=sttt%7C9%7CqPgOkyB6uHpxKKz22ljjef_________B-B1oMrODP-ItQszjCwbW8eVitD4IIGm5p5M8HZ9mUmY%3D; sid_ucp_v1=1.0.0-KDA1Mzc3NGFlYjkxMWVhYmYwMjRkYjFlNzlmOTExMTJjNDU4YmMwYWEKIQinwqCz24yVAhC5iZ_JBhjvMSAMMLHN7ZIGOAVA-wdIBBoCbHEiIGE4ZjgwZTkzMjA3YWI4N2E3MTI4YWNmNmRhNThlMzc5; ssid_ucp_v1=1.0.0-KDA1Mzc3NGFlYjkxMWVhYmYwMjRkYjFlNzlmOTExMTJjNDU4YmMwYWEKIQinwqCz24yVAhC5iZ_JBhjvMSAMMLHN7ZIGOAVA-wdIBBoCbHEiIGE4ZjgwZTkzMjA3YWI4N2E3MTI4YWNmNmRhNThlMzc5; login_time=1764213943926; _bd_ticket_crypt_cookie=8557ac65ef240b5ae1ef4f42fc295a33; download_guide=%223%2F20251127%2F0%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1707%2C%5C%22screen_height%5C%22%3A1067%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A32%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A0%7D%22; __ac_signature=_02B4Z6wo00f01W7nUdgAAIDCMXJaFv5vWAFux1VAADKh6e; home_can_add_dy_2_desktop=%221%22; bd_ticket_guard_client_data=eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWl0ZXJhdGlvbi12ZXJzaW9uIjoxLCJiZC10aWNrZXQtZ3VhcmQtcmVlLXB1YmxpYy1rZXkiOiJCRmRYMTNGQjNZRC9jckpRMjRnbElqdVg3SFBINm5pSW5KSHVwczFkOWZYcTN6RG12RnZiNGxxVDhMOXkwV3dZRzZaVTJIZVZ5ZkFoMVBhRUJ6cGtGamc9IiwiYmQtdGlja2V0LWd1YXJkLXdlYi12ZXJzaW9uIjoyfQ%3D%3D; FOLLOW_NUMBER_YELLOW_POINT_INFO=%22MS4wLjABAAAAfHeXHAcMODTRd6RzfhNvGcNo9jzHveOylmtvMnmHQ6aeaM9kHHj9Oz5hffXp4TGT%2F1764259200000%2F0%2F1764220136874%2F0%22; odin_tt=edd4b1d4aae751ee0896cc364174559a12d84ce1f8b6a1da99790965bfb2bf9683312194f434f1f0b7226ff385ad0a5c3af1db88c3c746cb0e9a303dc8613b74; biz_trace_id=831e7c1c; WallpaperGuide=%7B%22showTime%22%3A1764223033231%2C%22closeTime%22%3A0%2C%22showCount%22%3A1%2C%22cursor1%22%3A8%2C%22cursor2%22%3A2%7D; FOLLOW_LIVE_POINT_INFO=%22MS4wLjABAAAAfHeXHAcMODTRd6RzfhNvGcNo9jzHveOylmtvMnmHQ6aeaM9kHHj9Oz5hffXp4TGT%2F1764259200000%2F1764217238572%2F1764223036809%2F0%22; sdk_source_info=7e276470716a68645a606960273f276364697660272927676c715a6d6069756077273f276364697660272927666d776a68605a607d71606b766c6a6b5a7666776c7571273f275e58272927666a6b766a69605a696c6061273f27636469766027292762696a6764695a7364776c6467696076273f275e582729277672715a646971273f2763646976602729277f6b5a666475273f2763646976602729276d6a6e5a6b6a716c273f2763646976602729276c6b6f5a7f6367273f27636469766027292771273f273c31333c303d35363731333234272927676c715a75776a716a666a69273f2763646976602778; bit_env=nl1wMQMlK740nH9CwNwtw9nW-axyI9dJrxf5kgYmELm5pzFHljUvSp4oQndC0NkuxnTy87qC0dvnETYah7B-A9apPsdLbcfSiM_Uut6c8ANQfqKhlvzSEI3nFSVsgT96_RHqMUfrRNUa1i6TZa917AZaz3WHrPF_5nlLzsF95CAlBpCxiI-W9wdKa5lMissqiUhAY4CRTEoGuXu2CuDqgrgI9WJ6cFngoB27EjQYAYt1k-K45Me1qCHbeJkPwuy7mA8DT9hJVrK7z3iOaw-ObHU1HL-al8AxFVAf6sfyIh_arjwxjG4ebl1wU6bkqyIYM7dgfo_Gdp-XzX8DbI5fUBGKsI7NSw9DO2rqpd4bdex4j-ZAqj4uK_KsCs4Uc1jRuMKRCps8d9AW_rc4AXPQHzI7b8-cE5GNomFe1lt4aFcnaNIAsmW77_Fax1PW3c8YeIbrjMKJ_RkJNkjs36nWTDf-W-Qt2gutul5cQXb18Isw5OBNyJM9cviGL7xJNsGLXl7gnfvgOswxAR3CvYActmevtbp3bBHQJnmFTlw_mSg%3D; gulu_source_res=eyJwX2luIjoiZjI1NzFkMzg0MDZkYWFhM2I1MGFkY2E0MjgxMDI4N2VmMDEwMDcxYjQzNTA2ZWJkY2RlOGYxZDZmMjYyZWQ0NCJ9; passport_auth_mix_state=yf0zr26dxo77w4o42cdt2f2w1ix5qz8q; __security_mc_1_s_sdk_crypt_sdk=3add21b7-4ccc-b199; __security_mc_1_s_sdk_cert_key=9e23dfb2-40a6-8498; __security_mc_1_s_sdk_sign_data_key_web_protect=a7f31503-424a-871a; bd_ticket_guard_client_data_v2=eyJyZWVfcHVibGljX2tleSI6IkJGZFgxM0ZCM1lEL2NySlEyNGdsSWp1WDdIUEg2bmlJbkpIdXBzMWQ5ZlhxM3pEbXZGdmI0bHFUOEw5eTBXd1lHNlpVMkhlVnlmQWgxUGFFQnpwa0ZqZz0iLCJ0c19zaWduIjoidHMuMi45NWViZjU5NzE3NzBkMTk3NTgxOWNhMzYyNTAzNGZhMzk3NWYyMTY2M2MwNmY0Yjk2MjZiNjhhNzgwMzEwMTJiYzRmYmU4N2QyMzE5Y2YwNTMxODYyNGNlZGExNDkxMWNhNDA2ZGVkYmViZWRkYjJlMzBmY2U4ZDRmYTAyNTc1ZCIsInJlcV9jb250ZW50Ijoic2VjX3RzIiwicmVxX3NpZ24iOiJEeElacmxmS3ROL2lzSytGRFZ3czNDaHQvTEt0L2dDazRQa0ZaMnJEVmpRPSIsInNlY190cyI6IiNjM3BLcFhDREpvczUxak9JUGw2RlMrNEtuWjllOVRGTy9KT2Qrdk1LZ3VtZk5WYm5Ob2s3QlRVNmprc2MifQ%3D%3D; IsDouyinActive=tru"; + // } + + // List followings = new List(); + // while (hasmore) + // { + // var response = await douyinHttpClientService.SyncMyFollows(count, offset, "MS4wLjABAAAAfHeXHAcMODTRd6RzfhNvGcNo9jzHveOylmtvMnmHQ6aeaM9kHHj9Oz5hffXp4TGT", cookie); + + // hasmore = response.HasMore; + + // if(response.Followings != null) + // { + // offset = response.Offset.ToString(); + // foreach (var item in response.Followings) + // { + // followings.Add(item); + // } + // } + // } + + // return Ok(followings); + //} } diff --git a/Controllers/VideoController.cs b/Controllers/VideoController.cs index 852bfd5..7204362 100644 --- a/Controllers/VideoController.cs +++ b/Controllers/VideoController.cs @@ -1,4 +1,5 @@ using dy.net.dto; +using dy.net.model; using dy.net.service; using dy.net.utils; using Microsoft.AspNetCore.Authorization; @@ -14,10 +15,12 @@ namespace dy.net.Controllers public class VideoController : ControllerBase { private readonly DouyinVideoService dyCollectVideoService; + private readonly DouyinQuartzJobService douyinQuartzJobService; - public VideoController(DouyinVideoService dyCollectVideoService) + public VideoController(DouyinVideoService dyCollectVideoService,DouyinQuartzJobService douyinQuartzJobService) { this.dyCollectVideoService = dyCollectVideoService; + this.douyinQuartzJobService = douyinQuartzJobService; } /// /// 分页查询收藏视频 @@ -26,7 +29,7 @@ namespace dy.net.Controllers [HttpPost("paged")] public async Task GetPagedAsync(DouyinVideoPageRequestDto dto) { - var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Author,dto.ViedoType,dto.Dates); + var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto); return Ok(new { code = 0, @@ -67,48 +70,87 @@ namespace dy.net.Controllers { var viedo = await dyCollectVideoService.GetById(vid); - if(viedo == null) + if (viedo == null) + { + return NotFound($"视频不存在:{vid}"); + } + return PlayViedo(viedo); + } + catch (Exception ex) + { + return StatusCode(500, $"视频加载失败:{ex.Message}"); + } + } + + private IActionResult PlayViedo(DouyinVideo viedo) + { + + // 1. 拼接完整物理路径(配置路径 + 文件名) + string videoFullPath = viedo.VideoSavePath; + + // 2. 验证文件是否存在 + if (!System.IO.File.Exists(videoFullPath)) + { + return NotFound($"视频文件不存在:{videoFullPath}"); + } + + // 3. 获取文件信息(大小、类型) + var fileInfo = new FileInfo(videoFullPath); + long fileSize = fileInfo.Length; + string contentType = GetContentType(videoFullPath); // 自动识别视频 MIME 类型 + + // 4. 处理分片请求(前端视频标签自动发起,支持断点续传) + if (Request.Headers.ContainsKey("Range") && long.TryParse(Request.Headers.Range.ToString().Split('=')[1].Split('-')[0], out long start)) + { + // 分片起始位置(前端请求的起始字节) + long end = Math.Min(start + 1024 * 1024 * 2, fileSize - 1); // 每片 2MB(可调整) + long chunkSize = end - start + 1; + + // 5. 设置分片响应头 + Response.StatusCode = StatusCodes.Status206PartialContent; + Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileSize}"); + Response.Headers.Add("Accept-Ranges", "bytes"); + Response.Headers.Add("Content-Length", chunkSize.ToString()); + + // 6. 读取分片并返回流 + var stream = new FileStream(videoFullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true); + stream.Seek(start, SeekOrigin.Begin); + return new FileStreamResult(stream, contentType); + } + else + { + // 完整文件请求(兼容旧浏览器) + return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true); + } + } + + + /// + /// 播放视频 + /// + /// + /// + /// + [HttpGet("/share/{vid}/{k}")] + [AllowAnonymous] + public async Task Share([FromRoute] string vid, [FromRoute] string k) + { + try + { + var viedo = await dyCollectVideoService.GetById(vid); + + if (viedo == null) { return NotFound($"视频不存在:{vid}"); } - // 1. 拼接完整物理路径(配置路径 + 文件名) - string videoFullPath = viedo.VideoSavePath; - - // 2. 验证文件是否存在 - if (!System.IO.File.Exists(videoFullPath)) + var expectedKey = (viedo.FileHash + viedo.AuthorId).Md5(); + if (expectedKey != k) { - return NotFound($"视频文件不存在:{videoFullPath}"); + return NotFound($"视频地址无效"); } - // 3. 获取文件信息(大小、类型) - var fileInfo = new FileInfo(videoFullPath); - long fileSize = fileInfo.Length; - string contentType = GetContentType(videoFullPath); // 自动识别视频 MIME 类型 - - // 4. 处理分片请求(前端视频标签自动发起,支持断点续传) - if (Request.Headers.ContainsKey("Range") && long.TryParse(Request.Headers.Range.ToString().Split('=')[1].Split('-')[0], out long start)) - { - // 分片起始位置(前端请求的起始字节) - long end = Math.Min(start + 1024 * 1024 * 2, fileSize - 1); // 每片 2MB(可调整) - long chunkSize = end - start + 1; - - // 5. 设置分片响应头 - Response.StatusCode = StatusCodes.Status206PartialContent; - Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileSize}"); - Response.Headers.Add("Accept-Ranges", "bytes"); - Response.Headers.Add("Content-Length", chunkSize.ToString()); - - // 6. 读取分片并返回流 - var stream = new FileStream(videoFullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true); - stream.Seek(start, SeekOrigin.Begin); - return new FileStreamResult(stream, contentType); - } - else - { - // 完整文件请求(兼容旧浏览器) - return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true); - } + return PlayViedo(viedo); } catch (Exception ex) { @@ -133,6 +175,32 @@ namespace dy.net.Controllers }; } + /// + /// 重新下载 + /// + /// + /// + [HttpPost("redown")] + public async Task ReDownload(ReDownViedoDto dto) + { + if (dto == null) + { + return Ok(new { code = -1, data = false }); + } + else + { + var result = await dyCollectVideoService.ReDownloadViedoAsync(dto); + if (result) + { + //douyinQuartzJobService.StartReDownJobOnceAsync();...无法实现。。逆向失败 + return Ok(new { code = 0, data = true }); + } + else + { + return Ok(new { code = -1, data = false }); + } + } + } } } diff --git a/Dockerfile-img b/Dockerfile similarity index 100% rename from Dockerfile-img rename to Dockerfile diff --git a/Program.cs b/Program.cs index 64b5bd6..25d02c2 100644 --- a/Program.cs +++ b/Program.cs @@ -28,7 +28,7 @@ namespace dy.net private static bool downImageVideo = false; public static void Main(string[] args) { - Console.ForegroundColor = ConsoleColor.Yellow; + //Console.ForegroundColor = ConsoleColor.Yellow; // ʼṩ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -53,18 +53,16 @@ namespace dy.net // Ӧ var app = builder.Build(); - - Log.Debug("ffmpeg=" + downImgConfig); + if (downImageVideo) + Log.Debug("ffmpeg is on"); // м ConfigureMiddleware(app, builder.Environment); // ʼӦ÷ - InitApplicationServices(app); + InitApplicationServices(app, isDevelopment); Serilog.Log.Debug("dy.sync service is starting..."); - - Log.Debug("Ĭ-Ʒ-ͬȫ-ΪرգɵȨ濪ȫͬ"); Log.Debug("dy.sync service is started successfully"); Console.WriteLine("------------------------------------------------------------------------"); Console.WriteLine(@" __ \\ \ / ___|\ \ / \ | ___| @@ -73,7 +71,7 @@ namespace dy.net ____/ _|_)_____/ _| _| \_|\____| "); - Console.ResetColor(); + //Console.ResetColor(); app.Run(); } @@ -125,13 +123,15 @@ ____/ _|_)_____/ _| _| \_|\____| // ִͷע 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); - } + + services.AddSingleton(); + ////ͼƬϳƵ-Ҫ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); @@ -189,7 +189,7 @@ ____/ _|_)_____/ _| _| \_|\____| /// /// ʼӦ÷ /// - private static void InitApplicationServices(WebApplication app) + private static void InitApplicationServices(WebApplication app,bool isDevelopment) { using var scope = app.Services.CreateScope(); var services = scope.ServiceProvider; @@ -212,9 +212,13 @@ ____/ _|_)_____/ _| _| \_|\____| commonService.UpdateCollectViedoType(); // òƷͬ״̬Ϊδͬ commonService.UpdateAllCookieSyncedToZero(); - // ʱ - var quartzJobService = services.GetRequiredService(); - quartzJobService.StartJob(config?.Cron ?? "30"); + + if(!isDevelopment) + { + // ʱ + var quartzJobService = services.GetRequiredService(); + quartzJobService.InitOrReStartAllJobs(config?.Cron <= 0 ? "30" : config.Cron.ToString()); + } } catch (Exception ex) diff --git a/Properties/PublishProfiles/FolderProfile.pubxml.user b/Properties/PublishProfiles/FolderProfile.pubxml.user index 13be5d0..7cf8601 100644 --- a/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -4,8 +4,37 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - <_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\ - True|2025-11-26T15:48:02.3957186Z||;True|2025-11-26T23:43:06.8154188+08:00||;False|2025-11-26T23:42:05.9191485+08:00||;True|2025-11-26T23:30:11.1295861+08:00||;True|2025-11-26T00:00:42.1507258+08:00||;True|2025-11-26T00:00:17.0107229+08:00||;True|2025-11-25T23:42:07.4349629+08:00||;False|2025-11-25T23:41:56.9328658+08:00||;True|2025-11-25T23:19:32.5262917+08:00||;True|2025-11-25T14:08:53.3850967+08:00||;True|2025-11-24T23:53:54.4283003+08:00||;True|2025-11-24T23:41:15.8248332+08:00||;True|2025-11-24T23:33:57.1844427+08:00||;True|2025-11-24T23:31:54.9228836+08:00||;True|2025-11-24T23:24:15.8681502+08:00||;True|2025-11-24T23:22:57.9046229+08:00||;True|2025-11-24T23:18:04.1131647+08:00||;True|2025-11-24T22:47:54.1336448+08:00||;True|2025-11-24T22:47:18.3613833+08:00||;True|2025-11-24T08:17:50.3994607+08:00||;True|2025-11-24T08:07:00.9951205+08:00||;True|2025-11-23T23:34:50.0030826+08:00||;True|2025-11-23T23:32:36.7452616+08:00||;True|2025-11-22T23:18:56.3202345+08:00||;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||; + <_PublishTargetUrl>bin\Release\net6.0\publish\ + True|2025-11-27T05:37:08.6884390Z||;False|2025-11-27T13:36:54.2595622+08:00||;True|2025-11-27T11:03:33.8401644+08:00||;False|2025-11-27T11:02:48.8942827+08:00||;True|2025-11-26T23:48:02.3957186+08:00||;True|2025-11-26T23:43:06.8154188+08:00||;False|2025-11-26T23:42:05.9191485+08:00||;True|2025-11-26T23:30:11.1295861+08:00||;True|2025-11-26T00:00:42.1507258+08:00||;True|2025-11-26T00:00:17.0107229+08:00||;True|2025-11-25T23:42:07.4349629+08:00||;False|2025-11-25T23:41:56.9328658+08:00||;True|2025-11-25T23:19:32.5262917+08:00||;True|2025-11-25T14:08:53.3850967+08:00||;True|2025-11-24T23:53:54.4283003+08:00||;True|2025-11-24T23:41:15.8248332+08:00||;True|2025-11-24T23:33:57.1844427+08:00||;True|2025-11-24T23:31:54.9228836+08:00||;True|2025-11-24T23:24:15.8681502+08:00||;True|2025-11-24T23:22:57.9046229+08:00||;True|2025-11-24T23:18:04.1131647+08:00||;True|2025-11-24T22:47:54.1336448+08:00||;True|2025-11-24T22:47:18.3613833+08:00||;True|2025-11-24T08:17:50.3994607+08:00||;True|2025-11-24T08:07:00.9951205+08:00||;True|2025-11-23T23:34:50.0030826+08:00||;True|2025-11-23T23:32:36.7452616+08:00||;True|2025-11-22T23:18:56.3202345+08:00||;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||; + + + 11/12/2025 21:56:08 + + + 11/22/2025 17:11:30 + + + 11/22/2025 17:11:30 + + + 11/22/2025 17:00:49 + + + 11/22/2025 17:11:19 + + + 11/22/2025 17:11:30 + + + 11/12/2025 21:56:08 + + + 11/23/2025 22:21:42 + + + 11/23/2025 19:09:25 + + \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index d6a40e5..9befb46 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "dy.net": { "commandName": "Project", - "launchBrowser": true, + //"launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/README.md b/README.md index 16088f5..82d42d1 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取, | `full_latest` | x86_64 (amd64) | **完整版**- 包含全部核心功能- **内置 FFmpeg**- 支持图文视频下载与合成 | | `arm_latest` | ARM64 | **ARM 标准版**- 核心功能(与 `latest` 一致) | | `full_arm_latest` | ARM64 | **ARM 完整版**- 完整功能(与 `full_latest` 一致) | -| `beta_1.0` | x86_64 (amd64) | **测试版 v1.0**- 测试版 | +| `beta_1.2` | x86_64 (amd64) | **测试版 v1.0**- 测试版 | ### 构建命令示例 @@ -186,7 +186,7 @@ services: 7. ✅ 将网站名称改成 "抖小云" 灵感来源于哪吒电影 驮门的那个小云云 -8. ⭕️ Cookie 过期提醒(或者看看能不能实现扫码登录自动获取 Cookie) +8. ⭕️ Cookie 过期提醒 9. ⭕️ 重复视频去重(一个视频同时属于收藏视频、喜欢的视频或指定的博主作品) diff --git a/app/coll2/author/2253324655537024.jpg b/app/coll2/author/2253324655537024.jpg new file mode 100644 index 0000000..b40c5b5 Binary files /dev/null and b/app/coll2/author/2253324655537024.jpg differ diff --git a/app/coll2/author/3432007521806423.jpg b/app/coll2/author/3432007521806423.jpg new file mode 100644 index 0000000..27a21cd Binary files /dev/null and b/app/coll2/author/3432007521806423.jpg differ diff --git a/app/coll2/author/3833318009221843.jpg b/app/coll2/author/3833318009221843.jpg new file mode 100644 index 0000000..a97c192 Binary files /dev/null and b/app/coll2/author/3833318009221843.jpg differ diff --git a/app/coll2/author/93288642596.jpg b/app/coll2/author/93288642596.jpg new file mode 100644 index 0000000..b3a8680 Binary files /dev/null and b/app/coll2/author/93288642596.jpg differ diff --git a/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/.nfo b/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/.nfo new file mode 100644 index 0000000..1f98977 --- /dev/null +++ b/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/.nfo @@ -0,0 +1,20 @@ + + + 打工人必备3个邪修办公工具,早用早下班!!#办公技巧 #干货分享 #文员 #找工作 #vlog日常 + 丸子不是圆子 + 2025-11-14 + 个人管理 + 职场技能 + 办公效率 + + + 丸子不是圆子 + 主演 + app/coll2\author\3833318009221843.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/7572551496727769209.mp4 b/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/7572551496727769209.mp4 new file mode 100644 index 0000000..d0a38ab Binary files /dev/null and b/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/7572551496727769209.mp4 differ diff --git a/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/poster.jpg b/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/poster.jpg new file mode 100644 index 0000000..a0b2a65 Binary files /dev/null and b/app/coll2/个人管理/打工人必备3个邪修办公工具,早用早下班!!#办公技巧#干货分享#文员#找工作#vlog日常@7572551496727769209/poster.jpg differ diff --git a/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/.nfo b/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/.nfo new file mode 100644 index 0000000..55b9cc6 --- /dev/null +++ b/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/.nfo @@ -0,0 +1,20 @@ + + + 冬天穿羽绒服就扎这三款丸子头 #简单盘发教程 #扎发教程 #简单好看发型 #日常编发 #盘发 + 爱编发的小童 + 2025-11-19 + 时尚 + 彩妆 + 发型 + + + 爱编发的小童 + 主演 + app/coll2\author\2253324655537024.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/7574407682967921235.mp4 b/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/7574407682967921235.mp4 new file mode 100644 index 0000000..a18da7a Binary files /dev/null and b/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/7574407682967921235.mp4 differ diff --git a/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/poster.jpg b/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/poster.jpg new file mode 100644 index 0000000..0e82461 Binary files /dev/null and b/app/coll2/时尚/冬天穿羽绒服就扎这三款丸子头#简单盘发教程#扎发教程#简单好看发型#日常编发#盘发@7574407682967921235/poster.jpg differ diff --git a/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/.nfo b/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/.nfo new file mode 100644 index 0000000..9785844 --- /dev/null +++ b/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/.nfo @@ -0,0 +1,20 @@ + + + “最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步…… + 新华社 + 2025-11-07 + 时政社会 + 社会新闻 + 民生 + + + 新华社 + 主演 + app/coll2\author\93288642596.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/7569780541189262618.mp4 b/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/7569780541189262618.mp4 new file mode 100644 index 0000000..f1e560c Binary files /dev/null and b/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/7569780541189262618.mp4 differ diff --git a/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/poster.jpg b/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/poster.jpg new file mode 100644 index 0000000..b27cf19 Binary files /dev/null and b/app/coll2/时政社会/“最好的医生”其实就在你身边,医生眼里的“最好”:白开水、阳光、徒步……@7569780541189262618/poster.jpg differ diff --git a/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/.nfo b/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/.nfo new file mode 100644 index 0000000..ab5aa9d --- /dev/null +++ b/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/.nfo @@ -0,0 +1,20 @@ + + + 披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。 + 新华社 + 2025-11-22 + 时政社会 + 社会新闻 + 民生 + + + 新华社 + 主演 + app/coll2\author\93288642596.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/7575464272650718479.mp4 b/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/7575464272650718479.mp4 new file mode 100644 index 0000000..667e164 Binary files /dev/null and b/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/7575464272650718479.mp4 differ diff --git a/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/poster.jpg b/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/poster.jpg new file mode 100644 index 0000000..02bfebc Binary files /dev/null and b/app/coll2/时政社会/披星戴月的日子值吗?男孩分享爸爸送给自己的一段话,自律的苦轻如鸿毛,后悔的痛重过千斤。@7575464272650718479/poster.jpg differ diff --git a/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/.nfo b/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/.nfo new file mode 100644 index 0000000..c7e372d --- /dev/null +++ b/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/.nfo @@ -0,0 +1,20 @@ + + + 每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧 #小学数学 #小学奥数 #小学数学解题技巧 #小学数学思维 #小学数学题讲解视频 + 懒熊数学思维(小学奥数) + 2025-11-25 + 校园教育 + K12教育 + 学科教育 + + + 懒熊数学思维(小学奥数) + 主演 + app/coll2\author\3432007521806423.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/7576556688581332265.mp4 b/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/7576556688581332265.mp4 new file mode 100644 index 0000000..4e683fe Binary files /dev/null and b/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/7576556688581332265.mp4 differ diff --git a/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/poster.jpg b/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/poster.jpg new file mode 100644 index 0000000..f463776 Binary files /dev/null and b/app/coll2/校园教育/每天练一题,让孩子爱上数学!小学数学奥数竖式谜口诀解题技巧#小学数学#小学奥数#小学数学解题技@7576556688581332265/poster.jpg differ diff --git a/app/coll2/随拍/炒瓜子晃两下就冒烟,这操作我直接看呆#路边摊美味#美食#搞笑#生活#热门@7561466856432799022/7561466856432799022.mp4 b/app/coll2/随拍/炒瓜子晃两下就冒烟,这操作我直接看呆#路边摊美味#美食#搞笑#生活#热门@7561466856432799022/7561466856432799022.mp4 new file mode 100644 index 0000000..fcd4d52 Binary files /dev/null and b/app/coll2/随拍/炒瓜子晃两下就冒烟,这操作我直接看呆#路边摊美味#美食#搞笑#生活#热门@7561466856432799022/7561466856432799022.mp4 differ diff --git a/app/fav2/author/2664814926116075.jpg b/app/fav2/author/2664814926116075.jpg new file mode 100644 index 0000000..111ea42 Binary files /dev/null and b/app/fav2/author/2664814926116075.jpg differ diff --git a/app/fav2/author/66899755406.jpg b/app/fav2/author/66899755406.jpg new file mode 100644 index 0000000..e187791 Binary files /dev/null and b/app/fav2/author/66899755406.jpg differ diff --git a/app/fav2/author/75856936333.jpg b/app/fav2/author/75856936333.jpg new file mode 100644 index 0000000..d61c430 Binary files /dev/null and b/app/fav2/author/75856936333.jpg differ diff --git a/app/fav2/亲子/他就爱没事找事,我懒得解释!#母子日常#一身反骨的人类幼崽@7370252700547566857/7370252700547566857.mp4 b/app/fav2/亲子/他就爱没事找事,我懒得解释!#母子日常#一身反骨的人类幼崽@7370252700547566857/7370252700547566857.mp4 new file mode 100644 index 0000000..8f7b17a Binary files /dev/null and b/app/fav2/亲子/他就爱没事找事,我懒得解释!#母子日常#一身反骨的人类幼崽@7370252700547566857/7370252700547566857.mp4 differ diff --git a/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/.nfo b/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/.nfo new file mode 100644 index 0000000..c59f87a --- /dev/null +++ b/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/.nfo @@ -0,0 +1,19 @@ + + + 万万没想到,文件传输助手竟如此全能 #文件传输助手 #冷知识 + 主持人阿喆 + 2025-08-01 + 科普 + 冷知识 + + + 主持人阿喆 + 主演 + app/fav2\author\75856936333.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/7533611997993225499.mp4 b/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/7533611997993225499.mp4 new file mode 100644 index 0000000..24c7453 Binary files /dev/null and b/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/7533611997993225499.mp4 differ diff --git a/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/poster.jpg b/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/poster.jpg new file mode 100644 index 0000000..e232b56 Binary files /dev/null and b/app/fav2/科普/万万没想到,文件传输助手竟如此全能#文件传输助手#冷知识@7533611997993225499/poster.jpg differ diff --git a/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/.nfo b/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/.nfo new file mode 100644 index 0000000..ce04559 --- /dev/null +++ b/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/.nfo @@ -0,0 +1,20 @@ + + + 幸福的愿望 想念变成空气在叹息 + 小思宇 + 2025-06-19 + 随拍 + 人物随拍 + 人物图片轮播 + + + 小思宇 + 主演 + app/fav2\author\2664814926116075.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/7517656545376390459.mp4 b/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/7517656545376390459.mp4 new file mode 100644 index 0000000..0516875 Binary files /dev/null and b/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/7517656545376390459.mp4 differ diff --git a/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/poster.jpg b/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/poster.jpg new file mode 100644 index 0000000..ded0c1b Binary files /dev/null and b/app/fav2/随拍/幸福的愿望想念变成空气在叹息@7517656545376390459/poster.jpg differ diff --git a/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/.nfo b/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/.nfo new file mode 100644 index 0000000..7e834b6 --- /dev/null +++ b/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/.nfo @@ -0,0 +1,20 @@ + + + 放假领她去帮我哥打包,体验生活的不容易。#生产车间现场 #拼命努力的挣钱 #工作实拍 + 郭小胖 + 2024-07-20 + 随拍 + 生活记录 + 日常vlog + + + 郭小胖 + 主演 + app/fav2\author\66899755406.jpg + + + poster.jpg + + poster.jpg + + \ No newline at end of file diff --git a/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/7393659258765724966.mp4 b/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/7393659258765724966.mp4 new file mode 100644 index 0000000..c818673 Binary files /dev/null and b/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/7393659258765724966.mp4 differ diff --git a/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/poster.jpg b/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/poster.jpg new file mode 100644 index 0000000..ff718c9 Binary files /dev/null and b/app/fav2/随拍/放假领她去帮我哥打包,体验生活的不容易。#生产车间现场#拼命努力的挣钱#工作实拍@7393659258765724966/poster.jpg differ diff --git a/app/package.json b/app/package.json index e7db7b2..a6dd6ac 100644 --- a/app/package.json +++ b/app/package.json @@ -51,7 +51,7 @@ "babel-preset-es2015": "^6.24.1", "babel-register": "^6.26.0", "body-parser": "^1.20.1", - "crypto-js": "^4.1.1", + "crypto-js": "^4.2.0", "gh-pages": "^3.1.0", "gulp": "^4.0.2", "gulp-clean": "^0.4.0", diff --git a/app/src/pages/cok/CookieTable.vue b/app/src/pages/cok/CookieTable.vue index 55a99cb..8e55fd3 100644 --- a/app/src/pages/cok/CookieTable.vue +++ b/app/src/pages/cok/CookieTable.vue @@ -1,12 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/pages/set/AppSet.vue b/app/src/pages/set/AppSet.vue index 8d217ea..89a87af 100644 --- a/app/src/pages/set/AppSet.vue +++ b/app/src/pages/set/AppSet.vue @@ -6,26 +6,63 @@

任务调度

- - + +
+ + 同步任务的执行间隔,最小15分钟,建议使用默认值 +
+ +
+ + 每次同步获取的条数,范围10-30条,建议使用默认值 +
-

博主视频

+

博主视频(仅关注有效)

- + +
+ + 启用后,关注的视频文件名将直接使用原视频标题,否则使用模板生成,如果既没有模板也没有开启,则系统默认使用视频Id作为文件名。 +
- + +
+ + 默认是按博主名字创建文件夹存储,启用后,关注的视频直接存放在根目录。 +
+ + + +
+ + 选择生成文件名的占位符,顺序即为文件名顺序(需配合分隔符使用) +
+
+ + + +
+ + 占位符之间的连接符(如“-”“_”),为空则直接拼接 +
+
+ + + + +
@@ -33,24 +70,62 @@ +
+ + 启用后,图文视频将合成为视频文件下载 +
- + +
+ + 启用后,将额外下载音频文件 +
- + +
+ + 启用后,将额外下载所有图片文件 +
+
+ + + +
+ + + 启用后,所有图文视频统一存储到 Cookie(抖音授权) 设置的目录。
否则,按类型存储到对应文件夹(比如视频属于收藏的视频,则存储到收藏视频的目录) +
+
+

其他配置

+
+ + 系统运行日志的保留天数,范围1-90天,过期自动清理 +
+ + +
+ + + 启用后,同一个视频,只会下载一次。 + +
+
+
+ @@ -64,22 +139,34 @@ @@ -229,6 +374,9 @@ const onCancel = () => { &:last-child { border-bottom: none; } + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #f0f0f0; } .section-title { @@ -240,44 +388,6 @@ const onCancel = () => { border-left: 3px solid @primary-color; } -// // 关键改动:强制标签靠左并添加100px左侧间距 -// .ant-form-item-label { -// text-align: left !important; /* 强制靠左对齐,覆盖默认右对齐 */ -// padding-right: 0 !important; /* 清除默认右侧内边距 */ -// margin-left: 100px !important; /* 标签左侧间距100px */ -// } - -// 保持配置项内容区域与标签对齐(可选,根据实际布局调整) -// .ant-form-item-control { -// margin-left: 0 !important; /* 清除之前可能添加的左侧间距,避免双重缩进 */ -// } - -.help-text { - margin-top: 8px; - font-size: 12px; - color: #666; - line-height: 1.5; - - p { - margin: 4px 0; - } -} - -.cron-link { - color: @primary-color; - text-decoration: underline; - transition: all 0.3s; - - &:hover { - color: #096dd9; - } -} - -.form-actions { - margin-top: 30px; - text-align: center; -} - .ant-form-item { margin-bottom: 18px; } @@ -286,7 +396,19 @@ const onCancel = () => { color: rgb(164 158 158) !important; background-color: #e6e6e6 !important; } + .ant-form-item-label { text-align: left !important; } + +// 统一提醒文字样式 +:deep(.flex.items-start.mt-1.text-sm.text-gray-500) { + line-height: 1.6; + white-space: normal; +} + +// 禁用输入框样式优化 +:deep(.ant-input-disabled) { + color: #666 !important; +} \ No newline at end of file diff --git a/app/src/pages/workplace/RecordTable.vue b/app/src/pages/workplace/RecordTable.vue index 51a1a30..ea6440d 100644 --- a/app/src/pages/workplace/RecordTable.vue +++ b/app/src/pages/workplace/RecordTable.vue @@ -1,63 +1,93 @@ \ No newline at end of file diff --git a/app/src/router/routes.ts b/app/src/router/routes.ts index 81ca4a4..9e7cb80 100644 --- a/app/src/router/routes.ts +++ b/app/src/router/routes.ts @@ -107,6 +107,18 @@ const routes: RouteRecordRaw[] = [ }, component: () => import('@/pages/workplace/Workplace.vue'), }, + { + path: '/follow', + name: '关注列表', + meta: { + icon: 'HeartOutlined', + view: 'self', + target: '_self', + renderMenu: true, + cacheable: false, + }, + component: () => import('@/pages/followd/index.vue'), + }, { path: '/cok', name: '抖音授权', @@ -119,6 +131,7 @@ const routes: RouteRecordRaw[] = [ }, component: () => import('@/pages/cok/Table.vue'), }, + { path: '/set', name: '系统配置', diff --git a/app/src/store/coreapi.ts b/app/src/store/coreapi.ts index 7f963b1..b84fba1 100644 --- a/app/src/store/coreapi.ts +++ b/app/src/store/coreapi.ts @@ -146,6 +146,14 @@ export const useApiStore = defineStore('coreapi', () => { }); } + async function CookieList() { + return http.request>('/api/config/list', 'get').then(r => { + return r.data; + }).finally(() => { + + }); + } + async function UpdateConfig(param: object) { return http.request>('/api/config/update', 'post_json', param).then(r => { @@ -162,16 +170,47 @@ export const useApiStore = defineStore('coreapi', () => { }); } - // async function playViedo(id: string) { - // return http.request>('/api/video/play/' + id, 'get').then(r => { - // return r.data; - // }).finally(() => { + //follows + async function FollowList(param: object) { + return http.request>('/api/follow/paged', 'post_json', param).then(r => { + return r.data; + }).finally(() => { - // }); - // } + }); + } + //同步关注列表 + async function SyncFollow() { + return http.request>('/api/follow/sync', 'get').then(r => { + return r.data; + }).finally(() => { + }); + } + //更新同步关注者状态 + async function OpenOrCloseSync(param: object) { + return http.request>('/api/follow/openOrCloseSync', 'post_json', param).then(r => { + return r.data; + }).finally(() => { + + }); + } + //更新同步关注者状态 + async function OpenOrCloseFullSync(param: object) { + return http.request>('/api/follow/openOrCloseFullSync', 'post_json', param).then(r => { + return r.data; + }).finally(() => { + + }); + } + //重新下载 + async function ReDownViedos(param: object) { + return http.request>('/api/video/redown', 'post_json', param).then(r => { + return r.data; + }).finally(() => { + + }); + } return { - // playViedo, deleteCookie, UpdateConfig, apiCheckInitStatus, @@ -184,6 +223,12 @@ export const useApiStore = defineStore('coreapi', () => { StartJobNow, VideoStatics, VideoPageList, - CookiePageList + CookiePageList, + CookieList, + FollowList, + SyncFollow, + OpenOrCloseSync, + OpenOrCloseFullSync, + ReDownViedos }; }); diff --git a/appsettings.json b/appsettings.json index fad79da..47fd9a6 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,5 +1,5 @@ { "dbconn": "", "dbtype": "Sqlite", - "DOWN_IMGVIDEO": "" + "DOWN_IMGVIDEO": "1" } \ No newline at end of file diff --git a/dto/DouyinFollowInfo.cs b/dto/DouyinFollowInfo.cs new file mode 100644 index 0000000..93db2fc --- /dev/null +++ b/dto/DouyinFollowInfo.cs @@ -0,0 +1,1793 @@ +using Newtonsoft.Json; + +namespace dy.net.dto +{ + //public class Extra + //{ + // /// + // /// + // /// + // public List fatal_item_ids { get; set; } + // /// + // /// + // /// + // public string logid { get; set; } + // /// + // /// + // /// + // public int now { get; set; } + //} + + //public class Avatar_168x168 + //{ + // /// + // /// + // /// + // public int height { get; set; } + // /// + // /// + // /// + // public string uri { get; set; } + // /// + // /// + // /// + // public List url_list { get; set; } + // /// + // /// + // /// + // public int width { get; set; } + //} + + //public class Avatar_300x300 + //{ + // /// + // /// + // /// + // public int height { get; set; } + // /// + // /// + // /// + // public string uri { get; set; } + // /// + // /// + // /// + // public List url_list { get; set; } + // /// + // /// + // /// + // public int width { get; set; } + //} + + + + //public class Avatar_thumb + //{ + // /// + // /// + // /// + // public int height { get; set; } + // /// + // /// + // /// + // public string uri { get; set; } + // /// + // /// + // /// + // public List url_list { get; set; } + // /// + // /// + // /// + // public int width { get; set; } + //} + + //public class Aweme_control + //{ + // /// + // /// + // /// + // public string can_comment { get; set; } + // /// + // /// + // /// + // public string can_forward { get; set; } + // /// + // /// + // /// + // public string can_share { get; set; } + // /// + // /// + // /// + // public string can_show_comment { get; set; } + //} + + //public class Cover_urlItem + //{ + // /// + // /// + // /// + // public int height { get; set; } + // /// + // /// + // /// + // public string uri { get; set; } + // /// + // /// + // /// + // public List url_list { get; set; } + // /// + // /// + // /// + // public int width { get; set; } + //} + + //public class Following_list_secondary_information_struct + //{ + // /// + // /// + // /// + // public int secondary_information_priority { get; set; } + // /// + // /// 6个作品未看 + // /// + // public string secondary_information_text { get; set; } + // /// + // /// + // /// + // public int secondary_information_text_type { get; set; } + //} + + //public class Original_musician + //{ + // /// + // /// + // /// + // public int digg_count { get; set; } + // /// + // /// + // /// + // public int music_count { get; set; } + // /// + // /// + // /// + // public int music_used_count { get; set; } + //} + + //public class Search_impr + //{ + // /// + // /// + // /// + // public string entity_id { get; set; } + //} + + //public class Share_qrcode_url + //{ + // /// + // /// + // /// + // public int height { get; set; } + // /// + // /// + // /// + // public string uri { get; set; } + // /// + // /// + // /// + // public List url_list { get; set; } + // /// + // /// + // /// + // public int width { get; set; } + //} + + //public class Share_info + //{ + // /// + // /// + // /// + // public string share_desc { get; set; } + // /// + // /// + // /// + // public string share_desc_info { get; set; } + // /// + // /// + // /// + // public Share_qrcode_url share_qrcode_url { get; set; } + // /// + // /// + // /// + // public string share_title { get; set; } + // /// + // /// + // /// + // public string share_title_myself { get; set; } + // /// + // /// + // /// + // public string share_title_other { get; set; } + // /// + // /// + // /// + // public string share_url { get; set; } + // /// + // /// + // /// + // public string share_weibo_desc { get; set; } + //} + + //public class Urge_detail + //{ + // /// + // /// + // /// + // public int user_urged { get; set; } + //} + + //public class Video_icon + //{ + // /// + // /// + // /// + // public int height { get; set; } + // /// + // /// + // /// + // public string uri { get; set; } + // /// + // /// + // /// + // public List url_list { get; set; } + // /// + // /// + // /// + // public int width { get; set; } + //} + + + + + + //public class FollowingsItemxxx + //{ + // ///// + // ///// + // ///// + // //public string accept_private_policy { get; set; } + // ///// + // ///// {"label_style":5,"label_text":"伊朗驻华大使馆官方账号","is_biz_account":1} + // ///// + // //public string account_cert_info { get; set; } + // ///// + // ///// + // ///// + // //public string account_region { get; set; } + // ///// + // ///// + // ///// + // //public string account_type { get; set; } + // ///// + // ///// + // ///// + // //public string activity { get; set; } + // ///// + // ///// + // ///// + // //public string activity_label { get; set; } + // ///// + // ///// + // ///// + // //public string ad_cover_title { get; set; } + // ///// + // ///// + // ///// + // //public string ad_cover_url { get; set; } + // ///// + // ///// + // ///// + // //public string ad_order_id { get; set; } + // ///// + // ///// + // ///// + // //public string age_gate_action { get; set; } + // ///// + // ///// + // ///// + // //public string age_gate_post_action { get; set; } + // ///// + // ///// + // ///// + // //public string age_gate_time { get; set; } + // ///// + // ///// + // ///// + // //public string allow_status { get; set; } + // ///// + // ///// + // ///// + // //public string anchor_info { get; set; } + // ///// + // ///// + // ///// + // //public string anchor_schedule_guide_txt { get; set; } + // ///// + // ///// + // ///// + // //public int apple_account { get; set; } + // ///// + // ///// + // ///// + // //public int authority_status { get; set; } + // ///// + // ///// + // ///// + // //public Avatar_168x168 avatar_168x168 { get; set; } + // ///// + // ///// + // ///// + // //public Avatar_300x300 avatar_300x300 { get; set; } + // ///// + // ///// + // ///// + // //public string avatar_decoration { get; set; } + // ///// + // ///// + // ///// + // //public string avatar_decoration_id { get; set; } + + // ///// + // ///// + // ///// + // //public Avatar_medium avatar_medium { get; set; } + // ///// + // ///// + // ///// + // //public string avatar_pendant_larger { get; set; } + // ///// + // ///// + // ///// + // //public string avatar_pendant_medium { get; set; } + // ///// + // ///// + // ///// + // //public string avatar_pendant_thumb { get; set; } + // ///// + // ///// + // ///// + // //public Avatar_thumb avatar_thumb { get; set; } + // ///// + // ///// + // ///// + // //public string avatar_update_reminder { get; set; } + // ///// + // ///// + // ///// + // //public string avatar_uri { get; set; } + // ///// + // ///// + // ///// + // //public Aweme_control aweme_control { get; set; } + // ///// + // ///// + // ///// + // //public int aweme_count { get; set; } + // ///// + // ///// + // ///// + // //public string aweme_cover { get; set; } + // ///// + // ///// + // ///// + // //public int aweme_hotsoon_auth { get; set; } + // ///// + // ///// + // ///// + // //public string aweme_hotsoon_auth_relation { get; set; } + // ///// + // ///// + // ///// + // //public List ban_user_functions { get; set; } + // ///// + // ///// + // ///// + // //public string bio_email { get; set; } + // ///// + // ///// + // ///// + // //public string bio_location { get; set; } + // ///// + // ///// + // ///// + // //public string bio_permission { get; set; } + // ///// + // ///// + // ///// + // //public string bio_phone { get; set; } + // ///// + // ///// + // ///// + // //public string bio_secure_url { get; set; } + // ///// + // ///// + // ///// + // //public string bio_url { get; set; } + // ///// + // ///// + // ///// + // //public string birthday_hide_level { get; set; } + // ///// + // ///// + // ///// + // //public string biz_account_info { get; set; } + // ///// + // ///// + // ///// + // //public string brand_info { get; set; } + // ///// + // ///// + // ///// + // //public string can_modify_hometown_info { get; set; } + // ///// + // ///// + // ///// + // //public string can_modify_school_info { get; set; } + // ///// + // ///// + // ///// + // //public string can_set_geofencing { get; set; } + // ///// + // ///// + // ///// + // //public string can_show_group_card { get; set; } + // ///// + // ///// + // ///// + // //public string cancel_type { get; set; } + // ///// + // ///// + // ///// + // //public string card_entries { get; set; } + // ///// + // ///// + // ///// + // //public string card_entries_info { get; set; } + // ///// + // ///// + // ///// + // //public string card_entries_not_display { get; set; } + // ///// + // ///// + // ///// + // //public string card_sort_priority { get; set; } + // ///// + // ///// + // ///// + // //public string category { get; set; } + // ///// + // ///// + // ///// + // //public string cha_list { get; set; } + // ///// + // ///// + // ///// + // //public string clean_following_reason { get; set; } + // ///// + // ///// + // ///// + // //public string collect_count { get; set; } + // ///// + // ///// + // ///// + // //public int comment_filter_status { get; set; } + // ///// + // ///// + // ///// + // //public int comment_setting { get; set; } + // ///// + // ///// + // ///// + // //public string commerce_bubble { get; set; } + // ///// + // ///// + // ///// + // //public string commerce_info { get; set; } + // ///// + // ///// + // ///// + // //public string commerce_permissions { get; set; } + // ///// + // ///// + // ///// + // //public string commerce_user_info { get; set; } + // ///// + // ///// + // ///// + // //public int commerce_user_level { get; set; } + // ///// + // ///// + // ///// + // //public int constellation { get; set; } + // ///// + // ///// + // ///// + // //public string contact_name { get; set; } + // ///// + // ///// + // ///// + // //public string content_language_already_popup { get; set; } + // ///// + // ///// + // ///// + // //public string count_status { get; set; } + // ///// + // ///// + // ///// + // //public string cover_colour { get; set; } + // ///// + // ///// + // ///// + // //public string cover_jump_url { get; set; } + // ///// + // ///// + // ///// + // //public List cover_url { get; set; } + // ///// + // ///// + // ///// + // //public int create_time { get; set; } + // ///// + // ///// + // ///// + // //public string creator_level { get; set; } + // ///// + // ///// + // ///// + // //public string custom_verify { get; set; } + // ///// + // ///// + // ///// + // //public string cv_level { get; set; } + // ///// + // ///// + // ///// + // //public string display_info { get; set; } + // ///// + // ///// + // ///// + // //public string display_wvalantine_activity_entry { get; set; } + // ///// + // ///// + // ///// + // //public string dog_card_info { get; set; } + // ///// + // ///// + // ///// + // //public string dongtai_count { get; set; } + // ///// + // ///// + // ///// + // //public string dormer_group { get; set; } + // ///// + // ///// + // ///// + // //public string dou_plus_share_location { get; set; } + // ///// + // ///// + // ///// + // //public string douplus_old_user { get; set; } + // ///// + // ///// + // ///// + // //public string douplus_toast { get; set; } + // ///// + // ///// + // ///// + // //public int download_prompt_ts { get; set; } + // ///// + // ///// + // ///// + // //public int download_setting { get; set; } + // ///// + // ///// + // ///// + // //public string dp_level { get; set; } + // ///// + // ///// + // ///// + // //public int duet_setting { get; set; } + // ///// + // ///// + // ///// + // //public string effect_detail { get; set; } + // ///// + // ///// + // ///// + // //public string enable_nearby_visible { get; set; } + // ///// + // ///// + // ///// + // //public string enable_wish { get; set; } + // ///// + // ///// + // ///// + // //public string enterprise_user_info { get; set; } + + // ///// + // ///// + // ///// + // //public string ever_over_1k_follower { get; set; } + // ///// + // ///// + // ///// + // //public string fast_comment_texts { get; set; } + // ///// + // ///// + // ///// + // //public int favoriting_count { get; set; } + // ///// + // ///// + // ///// + // //public int fb_expire_time { get; set; } + // ///// + // ///// + // ///// + // //public string follow_as_subscription { get; set; } + // ///// + // ///// + // ///// + // //public string follow_guide { get; set; } + // ///// + // ///// + // ///// + // //public int follow_status { get; set; } + // ///// + // ///// + // ///// + // //public string follow_verify_status { get; set; } + // ///// + // ///// + // ///// + // //public int follower_count { get; set; } + // ///// + // ///// + // ///// + // //public int follower_request_status { get; set; } + // ///// + // ///// + // ///// + // //public int follower_status { get; set; } + // ///// + // ///// + // ///// + // //public int following_count { get; set; } + // ///// + // ///// + // ///// + // //public Following_list_secondary_information_struct following_list_secondary_information_struct { get; set; } + // ///// + // ///// + // ///// + // //public string force_private_account { get; set; } + // ///// + // ///// + // ///// + // //public string forward_count { get; set; } + // ///// + // ///// + // ///// + // //public string friend_count { get; set; } + // ///// + // ///// + // ///// + // //public string general_permission { get; set; } + // ///// + // ///// + // ///// + // //public List geofencing { get; set; } + // ///// + // ///// + // ///// + // //public string google_account { get; set; } + // ///// + // ///// + // ///// + // //public string has_activity_medal { get; set; } + // ///// + // ///// + // ///// + // //public string has_card_edit_page_entrance { get; set; } + // ///// + // ///// + // ///// + // //public string has_email { get; set; } + // ///// + // ///// + // ///// + // //public string has_facebook_token { get; set; } + // ///// + // ///// + // ///// + // //public string has_help_desk_entrance { get; set; } + // ///// + // ///// + // ///// + // //public string has_insights { get; set; } + // ///// + // ///// + // ///// + // //public string has_orders { get; set; } + // ///// + // ///// + // ///// + // //public string has_story { get; set; } + // ///// + // ///// + // ///// + // //public string has_subscription { get; set; } + // ///// + // ///// + // ///// + // //public string has_twitter_token { get; set; } + // ///// + // ///// + // ///// + // //public string has_unread_story { get; set; } + // ///// + // ///// + // ///// + // //public string has_youtube_token { get; set; } + // ///// + // ///// + // ///// + // //public string hide_following_follower_list { get; set; } + // ///// + // ///// + // ///// + // //public string hide_location { get; set; } + // ///// + // ///// + // ///// + // //public string hide_search { get; set; } + // ///// + // ///// + // ///// + // //public string hide_shoot_button { get; set; } + // ///// + // ///// + // ///// + // //public string homepage_bottom_toast { get; set; } + // ///// + // ///// + // ///// + // //public string hometown { get; set; } + // ///// + // ///// + // ///// + // //public string hometown_fellowship { get; set; } + // ///// + // ///// + // ///// + // //public string hometown_visible { get; set; } + // ///// + // ///// + // ///// + // //public string honor_info { get; set; } + // ///// + // ///// + // ///// + // //public string hot_list { get; set; } + // ///// + // ///// + // ///// + // //public string im_age_stage { get; set; } + // ///// + // ///// + // ///// + // //public string im_examination_info { get; set; } + // ///// + // ///// + // ///// + // //public string im_subscription_publisher { get; set; } + // ///// + // ///// + // ///// + // //public string infringement_report_remind_info { get; set; } + // ///// + // ///// + // ///// + // //public string ins_id { get; set; } + // ///// + // ///// + // ///// + // //public string interest_tags { get; set; } + // ///// + // ///// + // ///// + // //public string is_activity_user { get; set; } + // ///// + // ///// + // ///// + // //public string is_ad_fake { get; set; } + // ///// + // ///// + // ///// + // //public string is_binded_weibo { get; set; } + // ///// + // ///// + // ///// + // //public string is_block { get; set; } + // ///// + // ///// + // ///// + // //public string is_blocked { get; set; } + // ///// + // ///// + // ///// + // //public string is_discipline_member { get; set; } + // ///// + // ///// + // ///// + // //public string is_dou_manager { get; set; } + // ///// + // ///// + // ///// + // //public string is_effect_artist { get; set; } + // ///// + // ///// + // ///// + // //public string is_email_verified { get; set; } + // ///// + // ///// + // ///// + // //public string is_equal_query { get; set; } + // ///// + // ///// + // ///// + // //public string is_flowcard_member { get; set; } + // ///// + // ///// + // ///// + // //public string is_gov_media_vip { get; set; } + // ///// + // ///// + // ///// + // //public string is_life_style { get; set; } + // ///// + // ///// + // ///// + // //public string is_minor { get; set; } + // ///// + // ///// + // ///// + // //public string is_mirror { get; set; } + // ///// + // ///// + // ///// + // //public string is_mix_user { get; set; } + // ///// + // ///// + // ///// + // //public string is_not_show { get; set; } + // ///// + // ///// + // ///// + // //public string is_phone_binded { get; set; } + // ///// + // ///// + // ///// + // //public string is_pro_account { get; set; } + // ///// + // ///// + // ///// + // //public string is_series_user { get; set; } + // ///// + // ///// + // ///// + // //public string is_star { get; set; } + // ///// + // ///// + // ///// + // //public string is_top { get; set; } + // ///// + // ///// + // ///// + // //public string is_verified { get; set; } + // ///// + // ///// + // ///// + // //public string iso_country_code { get; set; } + // ///// + // ///// + // ///// + // //public string item_list { get; set; } + // ///// + // ///// + // ///// + // //public int ky_only_predict { get; set; } + // ///// + // ///// + // ///// + // //public string language { get; set; } + // ///// + // ///// + // ///// + // //public string latest_order_time { get; set; } + // ///// + // ///// + // ///// + // //public string life_story_block { get; set; } + // ///// + // ///// + // ///// + // //public int live_agreement { get; set; } + // ///// + // ///// + // ///// + // //public int live_agreement_time { get; set; } + // ///// + // ///// + // ///// + // //public string live_commerce { get; set; } + // ///// + // ///// + // ///// + // //public int live_status { get; set; } + // ///// + // ///// + // ///// + // //public int live_verify { get; set; } + // ///// + // ///// + // ///// + // //public string login_platform { get; set; } + // ///// + // ///// + // ///// + // //public string message_chat_entry { get; set; } + // ///// + // ///// + // ///// + // //public string minor_mode { get; set; } + // ///// + // ///// + // ///// + // //public string mplatform_followers_count { get; set; } + // ///// + // ///// + // ///// + // //public string music_compliance_account { get; set; } + // ///// + // ///// + // ///// + // //public string name_field { get; set; } + // ///// + // ///// + // ///// + // //public string need_addr_card { get; set; } + // ///// + // ///// + // ///// + // //public string need_points { get; set; } + // ///// + // ///// + // ///// + // //public int need_recommend { get; set; } + // ///// + // ///// + // ///// + // //public int neiguang_shield { get; set; } + // ///// + // ///// + // ///// + // //public int new_friend_type { get; set; } + // ///// + // ///// + // ///// + // //public string new_story_cover { get; set; } + // ///// + // ///// + // ///// + // //public string new_visitor_count { get; set; } + + // ///// + // ///// + // ///// + // //public string nickname_update_reminder { get; set; } + // ///// + // ///// + // ///// + // //public string normal_top_comment_permission { get; set; } + // ///// + // ///// + // ///// + // //public List not_seen_item_id_list_v2 { get; set; } + // ///// + // ///// + // ///// + // //public string notify_private_account { get; set; } + // ///// + // ///// + // ///// + // //public string open_insight_time { get; set; } + // ///// + // ///// + // ///// + // //public Original_musician original_musician { get; set; } + // ///// + // ///// + // ///// + // //public string personalized_tag { get; set; } + // ///// + // ///// + // ///// + // //public string platform_sync_info { get; set; } + // ///// + // ///// + // ///// + // //public string play_count { get; set; } + // ///// + // ///// + // ///// + // //public string post_default_download_setting { get; set; } + // ///// + // ///// + // ///// + // //public string pr_exempt { get; set; } + // ///// + // ///// + // ///// + // //public string prevent_download { get; set; } + // ///// + // ///// + // ///// + // //public string private_account_review_reminder { get; set; } + // ///// + // ///// + // ///// + // //public string private_aweme_count { get; set; } + // ///// + // ///// + // ///// + // //public string pro_account_tcm_red_dot { get; set; } + // ///// + // ///// + // ///// + // //public string profile_completion { get; set; } + // ///// + // ///// + // ///// + // //public string profile_pv { get; set; } + // ///// + // ///// + // ///// + // //public string profile_story { get; set; } + // ///// + // ///// + // ///// + // //public string profile_tab_type { get; set; } + // ///// + // ///// + // ///// + // //public string publish_landing_tab { get; set; } + // ///// + // ///// + // ///// + // //public string punish_remind_info { get; set; } + // ///// + // ///// + // ///// + // //public string quick_shop_info { get; set; } + // ///// + // ///// + // ///// + // //public string r_fans_group_info { get; set; } + // ///// + // ///// + // ///// + // //public int react_setting { get; set; } + // ///// + // ///// + // ///// + // //public string realname_verify_status { get; set; } + // ///// + // ///// + // ///// + // //public string rec_age_stage { get; set; } + // ///// + // ///// + // ///// + // //public string recommend_reason { get; set; } + // ///// + // ///// + // ///// + // //public string recommend_reason_relation { get; set; } + // ///// + // ///// + // ///// + // //public string recommend_score { get; set; } + // ///// + // ///// + // ///// + // //public string recommend_template { get; set; } + // ///// + // ///// + // ///// + // //public string recommend_user_reason_source { get; set; } + // ///// + // ///// + // ///// + // //public int reflow_page_gid { get; set; } + // ///// + // ///// + // ///// + // //public int reflow_page_uid { get; set; } + // ///// + // ///// + // ///// + // //public string register_from { get; set; } + // ///// + // ///// + // ///// + // //public string register_time { get; set; } + // ///// + // ///// + // ///// + // //public string relation_label { get; set; } + // ///// + // ///// + // ///// + // //public string relation_ship { get; set; } + // ///// + // ///// + // ///// + // //public string relative_users { get; set; } + // ///// + // ///// + // ///// + // //public string remark_name { get; set; } + // ///// + // ///// + // ///// + // //public string room_cover { get; set; } + // ///// + // ///// + // ///// + // //public string room_data { get; set; } + // ///// + // ///// + // ///// + // //public int room_id { get; set; } + // ///// + // ///// + // ///// + // //public string room_id_str { get; set; } + // ///// + // ///// + // ///// + // //public string room_type_tag { get; set; } + // ///// + // ///// + // ///// + // //public string school_auth { get; set; } + // ///// + // ///// + // ///// + // //public int school_category { get; set; } + // ///// + // ///// + // ///// + // //public string school_id { get; set; } + // ///// + // ///// + // ///// + // //public string school_visible { get; set; } + // ///// + // ///// + // ///// + // //public Search_impr search_impr { get; set; } + + // ///// + // ///// + // ///// + // //public int secret { get; set; } + // ///// + // ///// + // ///// + // //public Share_info share_info { get; set; } + // ///// + // ///// + // ///// + // //public string share_qrcode_uri { get; set; } + // ///// + // ///// + // ///// + // //public int shield_comment_notice { get; set; } + // ///// + // ///// + // ///// + // //public int shield_digg_notice { get; set; } + // ///// + // ///// + // ///// + // //public int shield_follow_notice { get; set; } + // ///// + // ///// + // ///// + // //public string shop_micro_app { get; set; } + // ///// + // ///// + // ///// + // //public string short_id { get; set; } + // ///// + // ///// + // ///// + // //public string show_artist_playlist { get; set; } + // ///// + // ///// + // ///// + // //public string show_avatar_decoration_entrance { get; set; } + // ///// + // ///// + // ///// + // //public string show_effect_list { get; set; } + // ///// + // ///// + // ///// + // //public string show_favorite_list { get; set; } + // ///// + // ///// + // ///// + // //public string show_favorite_list_on_item { get; set; } + // ///// + // ///// + // ///// + // //public string show_first_avatar_decoration { get; set; } + // ///// + // ///// + // ///// + // //public string show_following_follower_banner { get; set; } + // ///// + // ///// + // ///// + // //public int show_gender_strategy { get; set; } + // ///// + // ///// + // ///// + // //public string show_image_bubble { get; set; } + // ///// + // ///// + // ///// + // //public string show_located_banner { get; set; } + // ///// + // ///// + // ///// + // //public string show_musician_card { get; set; } + // ///// + // ///// + // ///// + // //public string show_nearby_active { get; set; } + // ///// + // ///// + // ///// + // //public string show_privacy_banner { get; set; } + // ///// + // ///// + // ///// + // //public string show_private_tab { get; set; } + // ///// + // ///// + // ///// + // //public string show_relation_banner { get; set; } + // ///// + // ///// + // ///// + // //public string show_secret_banner { get; set; } + // ///// + // ///// + // ///// + // //public string show_subscription { get; set; } + // ///// + // ///// + // ///// + // //public string show_tel_book_banner { get; set; } + // ///// + // ///// + // ///// + // //public string show_user_ban_dialog { get; set; } + // ///// + // ///// + // ///// + // //public string signature { get; set; } + // ///// + // ///// + // ///// + // //public int signature_display_lines { get; set; } + // ///// + // ///// + // ///// + // //public string signature_language { get; set; } + // ///// + // ///// + // ///// + // //public int special_lock { get; set; } + // ///// + // ///// + // ///// + // //public string special_state_info { get; set; } + // ///// + // ///// + // ///// + // //public string sprint_support_user_info { get; set; } + // ///// + // ///// + // ///// + // //public string star_activity_entrance { get; set; } + // ///// + // ///// + // ///// + // //public string star_billboard_info { get; set; } + // ///// + // ///// + // ///// + // //public string star_billboard_rank { get; set; } + // ///// + // ///// + // ///// + // //public string star_use_new_download { get; set; } + // ///// + // ///// + // ///// + // //public int status { get; set; } + // ///// + // ///// + // ///// + // //public int stitch_setting { get; set; } + // ///// + // ///// + // ///// + // //public int story_count { get; set; } + // ///// + // ///// + // ///// + // //public string story_expired_guide { get; set; } + // ///// + // ///// + // ///// + // //public string story_open { get; set; } + // ///// + // ///// + // ///// + // //public string @string { get; set; } + // ///// + // ///// + // ///// + // //public int sync_to_toutiao { get; set; } + // ///// + // ///// + // ///// + // //public string tab_settings { get; set; } + // ///// + // ///// + // ///// + // //public string third_name { get; set; } + // ///// + // ///// + // ///// + // //public int total_favorited { get; set; } + // ///// + // ///// + // ///// + // //public int tw_expire_time { get; set; } + // ///// + // ///// + // ///// + // //public string twitter_id { get; set; } + // ///// + // ///// + // ///// + // //public string twitter_name { get; set; } + // ///// + // ///// + // ///// + // //public string type_label { get; set; } + // ///// + // ///// + // ///// + // //public string uid { get; set; } + // ///// + // ///// + // ///// + // //public string unique_id { get; set; } + // ///// + // ///// + // ///// + // //public int unique_id_modify_time { get; set; } + // ///// + // ///// + // ///// + // //public string unique_id_update_reminder { get; set; } + // ///// + // ///// + // ///// + // //public Urge_detail urge_detail { get; set; } + // ///// + // ///// + // ///// + // //public string user_canceled { get; set; } + // ///// + // ///// + // ///// + // //public string user_deleted { get; set; } + // ///// + // ///// + // ///// + // //public int user_mode { get; set; } + // ///// + // ///// + // ///// + // //public int user_not_see { get; set; } + // ///// + // ///// + // ///// + // //public int user_not_show { get; set; } + // ///// + // ///// + // ///// + // //public int user_period { get; set; } + // ///// + // ///// + // ///// + // //public int user_rate { get; set; } + // ///// + // ///// + // ///// + // //public string user_rate_remind_info { get; set; } + // ///// + // ///// + // ///// + // //public string user_rip_entry { get; set; } + // ///// + // ///// + // ///// + // //public string user_story_count { get; set; } + // ///// + // ///// + // ///// + // //public string user_tags { get; set; } + // ///// + // ///// + // ///// + // //public string vcd_auth_block { get; set; } + // ///// + // ///// + // ///// + // //public string verification_badge_type { get; set; } + // ///// + // ///// + // ///// + // //public int verification_type { get; set; } + // ///// + // ///// + // ///// + // //public string verify_info { get; set; } + // ///// + // ///// + // ///// + // //public string versatile_display { get; set; } + // ///// + // ///// + // ///// + // //public string video_cover { get; set; } + // ///// + // ///// + // ///// + // //public Video_icon video_icon { get; set; } + // ///// + // ///// + // ///// + // //public string video_icon_virtual_URI { get; set; } + // ///// + // ///// + // ///// + // //public string video_unread_info { get; set; } + // ///// + // ///// + // ///// + // //public string vs_personal { get; set; } + // ///// + // ///// + // ///// + // //public string vxe_tag { get; set; } + // ///// + // ///// + // ///// + // //public string watch_status { get; set; } + // ///// + // ///// + // ///// + // //public string weibo_name { get; set; } + // ///// + // ///// + // ///// + // //public string weibo_schema { get; set; } + // ///// + // ///// + // ///// + // //public string weibo_url { get; set; } + // ///// + // ///// + // ///// + // //public string weibo_verify { get; set; } + // ///// + // ///// + // ///// + // //public string white_cover_url { get; set; } + // ///// + // ///// + // ///// + // //public string with_commerce_enterprise_tab_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_commerce_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_commerce_newbie_task { get; set; } + // ///// + // ///// + // ///// + // //public string with_dou_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_douplus_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_ecp_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_fusion_shop_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_item_commerce_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_luban_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_new_goods { get; set; } + // ///// + // ///// + // ///// + // //public string with_shop_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_star_atlas_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_stick_entry { get; set; } + // ///// + // ///// + // ///// + // //public string with_visitor_shop_entry { get; set; } + // ///// + // ///// + // ///// + // //public string wx_info { get; set; } + // ///// + // ///// + // ///// + // //public string wx_tag { get; set; } + // ///// + // ///// + // ///// + // //public string youtube_channel_id { get; set; } + // ///// + // ///// + // ///// + // //public string youtube_channel_title { get; set; } + // ///// + // ///// + // ///// + // //public int youtube_expire_time { get; set; } + // ///// + // ///// + // ///// + // //public string youtube_last_refresh_time { get; set; } + // ///// + // ///// + // ///// + // //public string youtube_refresh_token { get; set; } + // ///// + // ///// + // ///// + // //public string yt_raw_token { get; set; } + // ///// + // ///// + // ///// + // //public string zero_post_user_task { get; set; } + //} + + //public class Log_pb + //{ + // /// + // /// + // /// + // public string impr_id { get; set; } + //} + + + + + + //public class Avatar_medium + //{ + // /// + // /// + // /// + // //public int height { get; set; } + // /// + // /// + // /// + // //public string uri { get; set; } + // /// + // /// + // /// + // //public List url_list { get; set; } + // /// + // /// + // /// + // //public int width { get; set; } + //} + + public class DouyinFollowInfo + { + /// + /// + /// + //public Extra extra { get; set; } + /// + /// + /// + /// + [JsonProperty("followings")] + public List Followings { get; set; } + /// + /// + /// + [JsonProperty("has_more")] + public bool HasMore { get; set; } + ///// + ///// + ///// + //[JsonProperty("hotsoon_has_more")] + //public int hotsoon_has_more { get; set; } + ///// + ///// + ///// + //public string hotsoon_text { get; set; } + ///// + /// + /// + //public Log_pb log_pb { get; set; } + /// + /// + /// + //public int max_time { get; set; } + /// + /// + /// + //public int min_time { get; set; } + /// + /// + /// + //public int mix_count { get; set; } + /// + /// 当前登陆人的用户ID + /// + [JsonProperty("myself_user_id")] + public string MySelfUserId { get; set; } + /// + /// + /// + [JsonProperty("offset")] + public int Offset { get; set; } + ///// + ///// + ///// + //public string rec_has_more { get; set; } + /// + /// + /// + public int status_code { get; set; } + /// + /// + /// + //public string store_page { get; set; } + /// + /// 总关注人数 + /// + [JsonProperty("total")] + public int Total { get; set; } + /// + /// + /// + //public int vcd_count { get; set; } + } + + + + public class FollowingsItem + { + /// + /// 头像 + /// + [JsonProperty("avatar_larger")] + public AvatarLarger Avatar { get; set; } + + /// + /// 官方账号(企业认证) + /// + [JsonProperty("enterprise_verify_reason")] + public string EnterpriseVerifyReason { get; set; } + + /// + /// 伊朗驻华大使馆 + /// + [JsonProperty("nickname")] + public string NickName { get; set; } + + /// + /// 关键--唯一标识符 + /// + [JsonProperty("sec_uid")] + public string SecUid { get; set; } + + ///// + ///// + ///// + //[JsonProperty("short_id")] + //public string ShortId { get; set; } + + /// + /// 签名 + /// + [JsonProperty("signature")] + public string Signature { get; set; } + + [JsonProperty("uid")] + public string UperId { get; set; } + } + + /// + /// 头像 + /// + public class AvatarLarger + { + /// + /// + /// + //public int height { get; set; } + ///// + ///// + ///// + //public string uri { get; set; } + /// + /// + /// + [JsonProperty("url_list")] + public List UrlList { get; set; } + /// + /// + /// + //public int width { get; set; } + } +} diff --git a/dto/DouyinVideoInfo.cs b/dto/DouyinVideoInfo.cs index d6d582c..e5cc25f 100644 --- a/dto/DouyinVideoInfo.cs +++ b/dto/DouyinVideoInfo.cs @@ -1259,7 +1259,8 @@ namespace dy.net.dto /// /// /// - //public int height { get; set; } + [JsonProperty("height")] + public int Height { get; set; } ///// ///// ///// @@ -1272,7 +1273,8 @@ namespace dy.net.dto /// /// /// - //public int width { get; set; } + [JsonProperty("width")] + public int Width { get; set; } } diff --git a/dto/DouyinVideoPageRequestDto.cs b/dto/DouyinVideoPageRequestDto.cs index a16f76e..e55f2f4 100644 --- a/dto/DouyinVideoPageRequestDto.cs +++ b/dto/DouyinVideoPageRequestDto.cs @@ -4,15 +4,19 @@ namespace dy.net.dto { public class DouyinVideoPageRequestDto : PageRequestDto { - // 3. 引用类型(string)如果允许为null,显式声明为 string? - public string? Tag { get; set; } + /// + /// 标题查询。 + /// + public string? Title { get; set; } public string? Author { get; set; } + //public string? Name { get; set; } public string? ViedoType { get; set; } - // 4. 泛型集合(List)允许为null,声明为 List? public List? Dates { get; set; } + public List? Dates2 { get; set; } + } @@ -23,4 +27,25 @@ namespace dy.net.dto public int PageSize { get; set; } = 10; } + + + public class FollowRequestDto: PageRequestDto + { + public string FollowUserName { get; set; } + + public string MySelfId { get; set; } + } + + public class FollowUpdateDto + { + + public string Id { get; set; } + + public bool OpenSync { get; set; } + + public bool FullSync { get; set; } + + public string SavePath { get; set; } + + } } diff --git a/dto/JobConfig.cs b/dto/JobConfig.cs new file mode 100644 index 0000000..f4ccd6e --- /dev/null +++ b/dto/JobConfig.cs @@ -0,0 +1,36 @@ +namespace dy.net.dto +{ + /// + /// 任务配置实体 + /// + public class JobConfig + { + public JobConfig(Type jobType, string jobKey, string triggerKey, string description) + { + JobType = jobType ?? throw new ArgumentNullException(nameof(jobType)); + JobKey = jobKey ?? throw new ArgumentNullException(nameof(jobKey)); + TriggerKey = triggerKey ?? throw new ArgumentNullException(nameof(triggerKey)); + Description = description ?? throw new ArgumentNullException(nameof(description)); + } + + /// + /// 任务类型 + /// + public Type JobType { get; } + + /// + /// 任务Key + /// + public string JobKey { get; } + + /// + /// 触发器Key + /// + public string TriggerKey { get; } + + /// + /// 任务描述 + /// + public string Description { get; } + } +} diff --git a/dto/MediaMergeRequest.cs b/dto/MediaMergeRequest.cs new file mode 100644 index 0000000..f66f2cd --- /dev/null +++ b/dto/MediaMergeRequest.cs @@ -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.net.dto +{ + public class MediaMergeRequest + { + /// 网络图片地址数组(必填) + public List ImageUrls { get; set; } + + /// 网络MP3地址数组(必填) + public List AudioUrls { get; set; } + + /// 每张图片显示时长(秒,默认3秒) + public int ImageDurationPerSecond { get; set; } = 3; + + /// 视频分辨率(格式:1920x1080,默认1920x1080) + public int VideoWidth { get; set; } = 1080; + public int VideoHeight { get; set; } = 1920; + + /// 输出视频格式(默认mp4) + public string OutputFormat { get; set; } = "mp4"; + + /// 视频帧率(默认25) + public int VideoFps { get; set; } = 25; + } +} diff --git a/dto/ReDownViedoDto.cs b/dto/ReDownViedoDto.cs new file mode 100644 index 0000000..fedf042 --- /dev/null +++ b/dto/ReDownViedoDto.cs @@ -0,0 +1,7 @@ +namespace dy.net.dto +{ + public class ReDownViedoDto + { + public List Ids { get; set; } + } +} diff --git a/dto/VideoTitleDataTemplate.cs b/dto/VideoTitleDataTemplate.cs new file mode 100644 index 0000000..e99f350 --- /dev/null +++ b/dto/VideoTitleDataTemplate.cs @@ -0,0 +1,26 @@ +namespace dy.net.dto +{ + public class VideoTitleDataTemplate + { + /// 对应 {id} + public string Id { get; set; } + + /// 对应 {VideoTitle} + public string VideoTitle { get; set; } = string.Empty; + + /// 对应 {SyncTime}(同步时间) + //public DateTime? SyncTime { get; set; } + + /// 对应 {ReleaseTime}(发布时间) + public DateTime? ReleaseTime { get; set; } + + /// 对应 {FileHash}(文件哈希值) + public string FileHash { get; set; } = string.Empty; + + /// 对应 {Resolution}(分辨率,如 1920x1080) + public string Resolution { get; set; } = string.Empty; + + /// 对应 {FileSize}(文件大小,单位:字节) + //public long FileSize { get; set; } = 0; + } +} diff --git a/dto/VideoTypeEnum.cs b/dto/VideoTypeEnum.cs index d5f32d4..52a0878 100644 --- a/dto/VideoTypeEnum.cs +++ b/dto/VideoTypeEnum.cs @@ -13,4 +13,15 @@ namespace dy.net.dto [Description("图片视频")] ImageVideo = 4 } + + + public enum QuartzJobTypeEnum + { + [Description("[Favorite]")] + Favorite = 1, + [Description("[Collect]")] + Collect = 2, + [Description("[Followed]")] + Followed = 3 + } } diff --git a/dy.net.csproj b/dy.net.csproj index 207e7fa..6d39504 100644 --- a/dy.net.csproj +++ b/dy.net.csproj @@ -73,10 +73,6 @@ - - - - Always @@ -93,7 +89,7 @@ Always - + Always diff --git a/dy.net.sln b/dy.net.sln index 06f87a6..62b2bde 100644 --- a/dy.net.sln +++ b/dy.net.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dy.net", "dy.net.csproj", " 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 @@ -19,17 +17,10 @@ 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 diff --git a/expand/dy.image/DownloadHelper.cs b/expand/dy.image/DownloadHelper.cs index 3ff96d4..1ba30a3 100644 --- a/expand/dy.image/DownloadHelper.cs +++ b/expand/dy.image/DownloadHelper.cs @@ -14,29 +14,12 @@ namespace dy.image public DownloadHelper(HttpClient httpClient) { _httpClient = httpClient; - _httpClient.Timeout = TimeSpan.FromSeconds(60); // 下载超时30秒 + _httpClient.Timeout = TimeSpan.FromSeconds(60); // 下载超时60秒 _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"); } - /// 下载网络文件到指定路径 - 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); - } + } } diff --git a/expand/dy.image/ImageMergeToVideoService.cs b/expand/dy.image/ImageMergeToVideoService.cs index 1ce7055..4301ca4 100644 --- a/expand/dy.image/ImageMergeToVideoService.cs +++ b/expand/dy.image/ImageMergeToVideoService.cs @@ -44,7 +44,7 @@ { if(downImage) { - for (int i = 0; i < rawImages.Count(); i++) + for (int i = 0; i < rawImages.Length; i++) { string sourcePath = rawImages[i]; // 重命名为有规律的文件名,如 temp_001.jpg, temp_002.png @@ -66,7 +66,7 @@ { if(downMp3) { - for (int i = 0; i < rawAudios.Count(); i++) + for (int i = 0; i < rawAudios.Length; i++) { string sourcePath = rawAudios[i]; // 重命名为有规律的文件名,如 temp_001.mp3, temp_002.mp3 diff --git a/extension/ServiceExtension.cs b/extension/ServiceExtension.cs index 403d7e5..899c398 100644 --- a/extension/ServiceExtension.cs +++ b/extension/ServiceExtension.cs @@ -180,6 +180,10 @@ namespace dy.net.extension client.DefaultRequestHeaders.Add("Referer", "https://www.douyin.com"); }); + services.AddHttpClient("dy_follow", client => + { + client.DefaultRequestHeaders.Referrer = new Uri("https://www.douyin.com/user/self?showTab=like"); + }); services.AddHttpClient("dy_favorite", client => { @@ -201,6 +205,10 @@ namespace dy.net.extension // 连接超时(建立连接的超时时间) ConnectTimeout = TimeSpan.FromSeconds(60) }) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true + }) // 配置客户端默认请求头 .ConfigureHttpClient(client => { @@ -208,26 +216,6 @@ namespace dy.net.extension client.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"); client.DefaultRequestHeaders.Add("Referer", "https://www.douyin.com"); }); - //services.AddHttpClient("dy_down_fav") - // .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler - // { - // // 控制并发连接数(根据服务器承受能力调整,建议5-20) - // MaxConnectionsPerServer = 5, - // // 禁用代理自动检测(减少不必要的延迟) - // UseProxy = false, - // // 连接超时(建立连接的超时时间) - // ConnectTimeout = TimeSpan.FromSeconds(60) - // }); - //services.AddHttpClient("dy_down_uper") - // .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler - // { - // // 控制并发连接数(根据服务器承受能力调整,建议5-20) - // MaxConnectionsPerServer = 5, - // // 禁用代理自动检测(减少不必要的延迟) - // UseProxy = false, - // // 连接超时(建立连接的超时时间) - // ConnectTimeout = TimeSpan.FromSeconds(60) - // }); } /// diff --git a/job/DouYinCollectSyncJob.cs b/job/DouYinCollectSyncJob.cs index 903e000..685f1af 100644 --- a/job/DouYinCollectSyncJob.cs +++ b/job/DouYinCollectSyncJob.cs @@ -6,47 +6,45 @@ using System; namespace dy.net.job { - public class DouyinCollectSyncJob : DouyinBaseSyncJob + public class DouyinCollectSyncJob : DouyinBasicSyncJob { public DouyinCollectSyncJob( - DouyinCookieService dyCookieService, - DouyinHttpClientService dyHttpClientService, - DouyinVideoService dyCollectVideoService, - DouyinCommonService commonService,IServiceProvider serviceProvider,IWebHostEnvironment webHostEnvironment) - : base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider,webHostEnvironment) { } + DouyinCookieService douyinCookieService, + DouyinHttpClientService douyinHttpClientService, + DouyinVideoService douyinVideoService, + DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService douyinMergeVideoService) + : base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService,douyinFollowService, douyinMergeVideoService) { } - protected override string JobType => "collect"; + protected override string JobType => SystemStaticUtil.DY_COLLECTS; protected override async Task BeforeProcessCookies() { var now = DateTime.Now; if (now.Hour == 1 && now.Minute < 30) { - _commonService.UpdateAllCookieSyncedToZero(); + douyinCommonService.UpdateAllCookieSyncedToZero(); //顺手清理下日志文件 LogFileCleaner.CleanOldLogFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"), 1); await Task.Delay(200); } } - protected override async Task> GetValidCookies() + protected override async Task> GetValidCookies() { - var cookies = await _dyCookieService.GetAllCookies(); - return cookies.Where(c => !string.IsNullOrWhiteSpace(c.SavePath)).ToList(); + return await douyinCookieService.GetAllOpendAsync(x => !string.IsNullOrWhiteSpace(x.SavePath)); } - protected override bool IsCookieValid(DouyinUserCookie cookie) + protected override bool IsCookieValid(DouyinCookie cookie) { - return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 && - !string.IsNullOrWhiteSpace(cookie.SavePath); + return !string.IsNullOrWhiteSpace(cookie.Cookies)&& !string.IsNullOrWhiteSpace(cookie.SavePath); } - protected override async Task FetchVideoData(DouyinUserCookie cookie, string cursor) + protected override async Task FetchVideoData(DouyinCookie cookie, string cursor,string uperUid) { - return await _douyinService.SyncCollectVideos(cursor, count, cookie.Cookies); + return await douyinHttpClientService.SyncCollectVideos(cursor, count, cookie.Cookies); } - protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data) + protected override bool ShouldContinueSync(DouyinCookie cookie, DouyinVideoInfo data, DouyinFollowed followed = null) { return data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0; } @@ -56,32 +54,18 @@ namespace dy.net.job return data?.Cursor ?? "0"; } - protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2, AppConfig config) - { - 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) - { - var bitRate=item.Video.BitRate.FirstOrDefault(); - return $"{item.AwemeId}.{bitRate.Format}"; - } - - protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie) + protected override string GetAuthorAvatarBasePath(DouyinCookie cookie) { return Path.Combine(cookie.SavePath, "author"); } - protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount) + protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount) { if (syncCount > 0) { Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频"); cookie.CollHasSyncd = 1; - await _dyCookieService.UpdateAsync(cookie); + await douyinCookieService.UpdateAsync(cookie); } else { @@ -90,12 +74,21 @@ namespace dy.net.job } - protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item) + protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item) { return new VideoEntityDifferences { VideoType = VideoTypeEnum.Collect, }; } + + protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed) + { + var (tag1, _, _) = GetVideoTags(item); + var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizePath(tag1); + var folder = Path.Combine(cookie.SavePath, safeTag1, $"{DouyinFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}"); + if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); + return folder; + } } } \ No newline at end of file diff --git a/job/DouYinFavoritSyncJob.cs b/job/DouYinFavoritSyncJob.cs index cb10a46..4ce2822 100644 --- a/job/DouYinFavoritSyncJob.cs +++ b/job/DouYinFavoritSyncJob.cs @@ -8,35 +8,34 @@ using System.Threading.Tasks; namespace dy.net.job { - public class DouyinFavoritSyncJob : DouyinBaseSyncJob + public class DouyinFavoritSyncJob : DouyinBasicSyncJob { public DouyinFavoritSyncJob( - DouyinCookieService dyCookieService, - DouyinHttpClientService dyHttpClientService, - DouyinVideoService dyCollectVideoService, - DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment) - : base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider, webHostEnvironment) { } + DouyinCookieService douyinCookieService, + DouyinHttpClientService douyinHttpClientService, + DouyinVideoService douyinVideoService, + DouyinCommonService douyinCommonService,DouyinFollowService douyinFollowService,DouyinMergeVideoService douyinMergeVideoService) + : base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService, douyinFollowService, douyinMergeVideoService) { } - protected override string JobType => "favorite"; - protected override async Task> GetValidCookies() + protected override string JobType => SystemStaticUtil.DY_FAVORITES; + protected override async Task> GetValidCookies() { - var cookies = await _dyCookieService.GetAllCookies(); - return cookies.Where(c => !string.IsNullOrWhiteSpace(c.FavSavePath)).ToList(); + return await douyinCookieService.GetAllOpendAsync(x=> !string.IsNullOrWhiteSpace(x.FavSavePath)); } - protected override bool IsCookieValid(DouyinUserCookie cookie) + protected override bool IsCookieValid(DouyinCookie cookie) { return !string.IsNullOrWhiteSpace(cookie.Cookies) && cookie.Cookies.Length >= 1000 && !string.IsNullOrWhiteSpace(cookie.FavSavePath) && !string.IsNullOrWhiteSpace(cookie.SecUserId) && cookie.SecUserId.Length >= 10; } - protected override async Task FetchVideoData(DouyinUserCookie cookie, string cursor) + protected override async Task FetchVideoData(DouyinCookie cookie, string cursor,string uperUid) { - return await _douyinService.SyncFavoriteVideos(count, cursor, cookie.SecUserId, cookie.Cookies); + return await douyinHttpClientService.SyncFavoriteVideos(count, cursor, cookie.SecUserId, cookie.Cookies); } - protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data) + protected override bool ShouldContinueSync(DouyinCookie cookie, DouyinVideoInfo data, DouyinFollowed followed=null) { return data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0; } @@ -46,32 +45,19 @@ namespace dy.net.job return data?.MaxCursor ?? "0"; } - protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2, AppConfig config) - { - var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : TikTokFileNameHelper.SanitizePath(tag1); - var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{TikTokFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}"); - if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); - return folder; - } - protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item) - { - var bitRate = item.Video.BitRate.FirstOrDefault(); - return $"{item.AwemeId}.{bitRate.Format}"; - } - - protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie) + protected override string GetAuthorAvatarBasePath(DouyinCookie cookie) { return Path.Combine(cookie.FavSavePath, "author"); } - protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount) + protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount) { if (syncCount > 0) { Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频"); cookie.FavHasSyncd = 1; - await _dyCookieService.UpdateAsync(cookie); + await douyinCookieService.UpdateAsync(cookie); } else { @@ -79,12 +65,21 @@ namespace dy.net.job } } - protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item) + protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item) { return new VideoEntityDifferences { VideoType = VideoTypeEnum.Favorite }; } + + protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed) + { + var (tag1, _, _) = GetVideoTags(item); + var safeTag1 = string.IsNullOrWhiteSpace(tag1) ? "other" : DouyinFileNameHelper.SanitizePath(tag1); + var folder = Path.Combine(cookie.FavSavePath, safeTag1, $"{DouyinFileNameHelper.SanitizePath(item.Desc)}@{item.AwemeId}"); + if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); + return folder; + } } } \ No newline at end of file diff --git a/job/DouYinUperPostSyncJob.cs b/job/DouYinUperPostSyncJob.cs deleted file mode 100644 index bb46798..0000000 --- a/job/DouYinUperPostSyncJob.cs +++ /dev/null @@ -1,149 +0,0 @@ -using dy.net.dto; -using dy.net.model; -using dy.net.service; -using dy.net.utils; -using Newtonsoft.Json; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace dy.net.job -{ - public class DouyinUperPostSyncJob : DouyinBaseSyncJob - { - public DouyinUperPostSyncJob( - DouyinCookieService dyCookieService, - DouyinHttpClientService dyHttpClientService, - DouyinVideoService dyCollectVideoService, - DouyinCommonService commonService, IServiceProvider serviceProvider, IWebHostEnvironment webHostEnvironment) - : base(dyCookieService, dyHttpClientService, dyCollectVideoService, commonService, serviceProvider, webHostEnvironment) { } - - protected override string JobType => "dyuploder"; - - protected override async Task> GetValidCookies() - { - var cookies = await _dyCookieService.GetAllCookies(); - return cookies.Where(x => !string.IsNullOrWhiteSpace(x.UpSecUserIds) && !string.IsNullOrWhiteSpace(x.UpSavePath)).ToList(); - } - - protected override bool IsCookieValid(DouyinUserCookie cookie) - { - return !string.IsNullOrWhiteSpace(cookie.Cookies) && !string.IsNullOrWhiteSpace(cookie.UpSavePath) && !string.IsNullOrWhiteSpace(cookie.UpSecUserIds); - } - - protected override async Task FetchVideoData(DouyinUserCookie cookie, string cursor) - { - // 简化处理:假设只同步第一个UP主 - var ups = JsonConvert.DeserializeObject>(cookie.UpSecUserIds); - var firstUpId = ups?.FirstOrDefault()?.uid; - if (string.IsNullOrEmpty(firstUpId)) return null; - - return await _douyinService.SyncUpderPostVideos(count, cursor, firstUpId, cookie.Cookies); - } - - protected override bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data) - { - return data != null && data.HasMore == 1 && cookie.UperSyncd == 0; - } - - protected override string GetNextCursor(DouyinVideoInfo data) - { - return data?.MaxCursor ?? "0"; - } - - protected override string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2,AppConfig config) - { - // 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); - if(config.UperSaveTogether) - { - return folder; - } - else - { - var sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId); - var (existingName, _) = _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result; - var fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName; - return Path.Combine(folder, fileNameFolder); - } - } - - protected override string GetVideoFileName(DouyinUserCookie cookie, Aweme item) - { - var bitRate = item.Video.BitRate.FirstOrDefault(); - var config = _commonService.GetConfig(); - var fileName = string.Empty; - if (config?.UperUseViedoTitle ?? false) - { - var sampleName = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId); - var (existingName, _) = _douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result; - fileName= string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{bitRate.Format}" : $"{existingName}.{bitRate.Format}"; - } - else - { - fileName = $"{item.AwemeId}.{bitRate.Format}"; - } - return fileName; - } - - - /// - /// - /// - /// - /// - /// - /// - /// - protected override string GetNfoFileName(DouyinUserCookie cookie, Aweme item, AppConfig config, string imageType) - { - if (config.UperSaveTogether) - { - var videoFileName = GetVideoFileName(cookie, item); - return $"{Path.GetFileNameWithoutExtension(videoFileName)}{imageType}"; - } - else - { - return base.GetNfoFileName(cookie,item,config,imageType); - } - } - - protected override string GetAuthorAvatarBasePath(DouyinUserCookie cookie) - { - return Path.Combine(cookie.UpSavePath, "author"); - } - - protected override async Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount) - { - if (syncCount > 0) - { - Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频"); - cookie.UperSyncd = 1; - await _dyCookieService.UpdateAsync(cookie); - } - else - { - Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频"); - } - } - - protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item) - { - var config = _commonService.GetConfig(); - string simplifiedTitle = string.Empty; - - if (config?.UperUseViedoTitle ?? false) - { - simplifiedTitle = TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId); - } - - return new VideoEntityDifferences - { - VideoType = VideoTypeEnum.UperPost, - VideoTitleSimplify = simplifiedTitle - }; - } - } -} \ No newline at end of file diff --git a/job/DouyinBaseSyncJob.cs b/job/DouyinBasicSyncJob.cs similarity index 69% rename from job/DouyinBaseSyncJob.cs rename to job/DouyinBasicSyncJob.cs index 9f9fce5..9bda377 100644 --- a/job/DouyinBaseSyncJob.cs +++ b/job/DouyinBasicSyncJob.cs @@ -1,5 +1,4 @@ using ClockSnowFlake; -using dy.image; using dy.net.dto; using dy.net.model; using dy.net.service; @@ -22,44 +21,44 @@ namespace dy.net.job /// 提供了通用的同步逻辑,如Cookie处理、视频下载、数据存储等 /// [DisallowConcurrentExecution] // 禁止并发执行,确保同一时间只有一个实例在运行 - public abstract class DouyinBaseSyncJob : IJob + public abstract class DouyinBasicSyncJob : IJob { #region 受保护字段 /// /// 抖音Cookie服务,用于获取和管理用户Cookie /// - protected readonly DouyinCookieService _dyCookieService; + protected readonly DouyinCookieService douyinCookieService; /// /// 抖音HTTP客户端服务,用于发送HTTP请求 /// - protected readonly DouyinHttpClientService _douyinService; + protected readonly DouyinHttpClientService douyinHttpClientService; /// /// 抖音视频服务,用于视频信息的数据库操作 /// - protected readonly DouyinVideoService _douyinVideoService; + protected readonly DouyinVideoService douyinVideoService; /// /// 抖音通用服务,用于获取应用配置等 /// - protected readonly DouyinCommonService _commonService; + protected readonly DouyinCommonService douyinCommonService; + /// + /// 抖音关注列表 + /// + private readonly DouyinFollowService douyinFollowService; + /// + /// 图文合成视频 + /// + private readonly DouyinMergeVideoService douyinMergeVideoService; /// /// 随机数生成器,用于生成随机延迟,模拟人类操作 /// protected readonly Random _random = new Random(); - /// - /// 服务提供器,用于获取其他服务实例 - /// - protected readonly IServiceProvider _serviceProvider; - /// - /// Web主机环境,用于获取应用程序路径等信息 - /// - protected readonly IWebHostEnvironment _environment; /// /// 每页请求的视频数量,可通过配置文件修改 @@ -90,28 +89,28 @@ namespace dy.net.job #region 构造函数 /// - /// 初始化 类的新实例 + /// 初始化 类的新实例 /// - /// 抖音Cookie服务 - /// 抖音HTTP客户端服务 - /// 抖音视频服务 - /// 抖音通用服务 - /// 服务提供器 - /// Web主机环境 - protected DouyinBaseSyncJob( - DouyinCookieService dyCookieService, - DouyinHttpClientService dyHttpClientService, - DouyinVideoService dyCollectVideoService, - DouyinCommonService commonService, - IServiceProvider serviceProvider, - IWebHostEnvironment webHostEnvironment) + /// 抖音Cookie服务 + /// 抖音HTTP客户端服务 + /// 抖音视频服务 + /// 抖音通用服务 + /// 抖音关注的 + /// 视频合成 + protected DouyinBasicSyncJob( + DouyinCookieService douyinCookieService, + DouyinHttpClientService douyinHttpClientService, + DouyinVideoService douyinVideoService, + DouyinCommonService douyinCommonService, + DouyinFollowService douyinFollowService, + DouyinMergeVideoService douyinMergeVideoService) { - _dyCookieService = dyCookieService ?? throw new ArgumentNullException(nameof(dyCookieService)); - _douyinService = dyHttpClientService ?? throw new ArgumentNullException(nameof(dyHttpClientService)); - _douyinVideoService = dyCollectVideoService ?? throw new ArgumentNullException(nameof(dyCollectVideoService)); - _commonService = commonService ?? throw new ArgumentNullException(nameof(commonService)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _environment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); + this.douyinCookieService = douyinCookieService ?? throw new ArgumentNullException(nameof(douyinCookieService)); + this.douyinHttpClientService = douyinHttpClientService ?? throw new ArgumentNullException(nameof(douyinHttpClientService)); + this.douyinVideoService = douyinVideoService ?? throw new ArgumentNullException(nameof(douyinVideoService)); + this.douyinCommonService = douyinCommonService ?? throw new ArgumentNullException(nameof(douyinCommonService)); + this.douyinFollowService = douyinFollowService; + this.douyinMergeVideoService = douyinMergeVideoService; } #endregion @@ -127,15 +126,13 @@ namespace dy.net.job public async Task Execute(IJobExecutionContext context) { // 1. 获取应用配置 - var config = _commonService.GetConfig(); + var config = douyinCommonService.GetConfig(); if (config == null) { Log.Debug($"{JobType}-未获取到系统配置,任务终止!!!"); return; } - //将配置项打印日志 - //config.PrintAsTable(); // 2. 从配置中获取每页请求数量 if (config.BatchCount > 0) @@ -169,8 +166,7 @@ namespace dy.net.job #region 受保护方法 /// - /// 在处理Cookie之前执行的预处理操作 - /// 子类可以重写此方法来实现特定的预处理逻辑 + /// 在处理Cookie之前执行的预处理操作-AOP /// /// 一个表示异步操作的任务 protected virtual Task BeforeProcessCookies() => Task.CompletedTask; @@ -180,15 +176,26 @@ namespace dy.net.job /// 子类必须实现此方法,根据具体任务类型筛选有效的Cookie /// /// 有效的Cookie列表 - protected abstract Task> GetValidCookies(); + protected abstract Task> GetValidCookies(); + + /// + /// 获取关注列表 + /// + /// + protected virtual Task> GetFollows() + { + return Task.FromResult(new List()); + } /// /// 检查指定的Cookie是否有效 - /// 子类必须实现此方法,提供具体的Cookie有效性检查逻辑 /// /// 要检查的Cookie /// 如果Cookie有效,则为true;否则为false - protected abstract bool IsCookieValid(DouyinUserCookie cookie); + protected virtual bool IsCookieValid(DouyinCookie cookie) + { + return !string.IsNullOrWhiteSpace(cookie.Cookies); + } /// /// 根据Cookie和游标获取视频数据 @@ -196,8 +203,9 @@ namespace dy.net.job /// /// 用户Cookie /// 分页游标,用于获取下一页数据 + /// 关注的人 /// 视频信息对象,包含视频列表和分页信息 - protected abstract Task FetchVideoData(DouyinUserCookie cookie, string cursor); + protected abstract Task FetchVideoData(DouyinCookie cookie, string cursor, string uperUid = ""); /// /// 判断是否应该继续同步下一页数据 @@ -205,8 +213,9 @@ namespace dy.net.job /// /// 用户Cookie /// 当前获取到的视频数据 + /// 关注博主 /// 如果应该继续同步,则为true;否则为false - protected abstract bool ShouldContinueSync(DouyinUserCookie cookie, DouyinVideoInfo data); + protected abstract bool ShouldContinueSync(DouyinCookie cookie, DouyinVideoInfo data, DouyinFollowed followed); /// /// 获取下一页数据的游标 @@ -222,28 +231,31 @@ namespace dy.net.job /// /// 用户Cookie /// 视频信息 - /// 视频标签1 - /// 视频标签2 + /// 关注用户 /// 应用配置 /// 创建的视频保存文件夹路径 - protected abstract string CreateSaveFolder(DouyinUserCookie cookie, Aweme item, string tag1, string tag2, AppConfig config); + protected abstract string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed); /// - /// 获取视频文件名 - /// 子类必须实现此方法,根据具体的命名规则生成文件名 + /// 获取视频文件名,默认就用id作文件名 /// /// 用户Cookie /// 视频信息 + /// 配置信息 /// 生成的视频文件名 - protected abstract string GetVideoFileName(DouyinUserCookie cookie, Aweme item); - + protected virtual string GetVideoFileName(DouyinCookie cookie, Aweme item, AppConfig config) + { + if (item.Video != null && item.Video.BitRate != null) + return $"{item.AwemeId}.{item.Video.BitRate.FirstOrDefault().Format}"; + return $"{item.AwemeId}.mp4"; + } /// /// 获取作者头像保存的基础路径 /// 子类必须实现此方法,指定头像的存储位置 /// /// 用户Cookie /// 作者头像保存的基础路径 - protected abstract string GetAuthorAvatarBasePath(DouyinUserCookie cookie); + protected abstract string GetAuthorAvatarBasePath(DouyinCookie cookie); /// /// 处理同步完成后的操作 @@ -252,7 +264,7 @@ namespace dy.net.job /// 用户Cookie /// 本次同步成功的视频数量 /// 一个表示异步操作的任务 - protected abstract Task HandleSyncCompletion(DouyinUserCookie cookie, int syncCount); + protected abstract Task HandleSyncCompletion(DouyinCookie cookie, int syncCount); /// /// 获取视频实体的差异信息 @@ -261,7 +273,7 @@ namespace dy.net.job /// 用户Cookie /// 视频信息 /// 视频实体的差异信息,包含视频类型和简化标题 - protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinUserCookie cookie, Aweme item); + protected abstract VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item); /// /// 获取NFO文件中的图片(如海报)文件名 @@ -272,7 +284,7 @@ namespace dy.net.job /// 应用配置 /// 原文件名称(如poster.jpg) /// 封面图片的文件名 - protected virtual string GetNfoFileName(DouyinUserCookie cookie, Aweme item, AppConfig config, string fileName) + protected virtual string GetNfoFileName(DouyinCookie cookie, Aweme item, AppConfig config, string fileName) { return fileName; } @@ -284,7 +296,7 @@ namespace dy.net.job /// 用户Cookie /// 应用配置 /// 一个表示异步操作的任务 - protected async Task ProcessSyncUserCookie(DouyinUserCookie cookie, AppConfig config) + protected async Task ProcessSyncUserCookie(DouyinCookie cookie, AppConfig config) { try { @@ -294,44 +306,51 @@ namespace dy.net.job Log.Debug($"{JobType}-Cookie[{cookie.UserName}]无效,任务终止!!!"); return; } - Log.Debug($"{JobType}- Cookie-[{cookie.UserName}]开始同步..."); - int syncCount = 0; // 本次同步成功的视频数量 - string cursor = "0"; // 初始游标 - bool hasMore = true; // 是否还有更多数据 - - // 循环获取视频数据 - while (hasMore) + //up主上传视频特殊处理 + if (JobType == SystemStaticUtil.DY_FOLLOWEDS) { - // 获取视频数据 - var data = await FetchVideoData(cookie, cursor); - if (data == null) + int syncCount = 0; // 本次同步成功的视频数量 + string cursor = "0"; // 初始游标 + bool hasMore = true; // 是否还有更多数据 + var follows = await douyinFollowService.GetSyncFollows(cookie.MyUserId); + + + //var ups = JsonConvert.DeserializeObject>(cookie.UpSecUserIds); + var firstUp = follows?.Where(x => !string.IsNullOrWhiteSpace(x.SecUid)).FirstOrDefault(); + if (firstUp == null) { - Log.Debug($"{JobType}-Cookie[{cookie.UserName}]读取数据失败!!!"); - break; + return; + } + if (string.IsNullOrWhiteSpace(firstUp.SecUid)) + { + Log.Debug($"{JobType}-Cookie[{firstUp.UperName}]无效,没有sec_userid,任务终止!!!"); + return; } - // 判断是否还有更多数据 - hasMore = ShouldContinueSync(cookie, data); - // 获取下一页游标 - cursor = GetNextCursor(data); - // 如果没有视频数据,退出循环 - if (data.AwemeList == null || !data.AwemeList.Any()) - break; + foreach (var item in follows) + { + cursor = "0"; // 初始游标 + await GetViedos(cookie, config, syncCount, cursor, hasMore, item); + hasMore = true; + // 处理同步完成后的操作 + await HandleSyncCompletion(cookie, syncCount); + } + } + else + { + int syncCount = 0; // 本次同步成功的视频数量 + string cursor = "0"; // 初始游标 + bool hasMore = true; // 是否还有更多数据 + (syncCount, cursor, hasMore) = await GetViedos(cookie, config, syncCount, cursor, hasMore); - // 处理视频列表 - var videos = await ProcessVideoList(cookie, data, config); - // 保存视频信息到数据库 - syncCount += await SaveVideos(videos); - - // 随机延迟,模拟人类操作,避免请求过快 - await Task.Delay(_random.Next(5, 10) * 1000); + // 处理同步完成后的操作 + await HandleSyncCompletion(cookie, syncCount); } - // 处理同步完成后的操作 - await HandleSyncCompletion(cookie, syncCount); + } catch (Exception ex) { @@ -339,6 +358,40 @@ namespace dy.net.job } } + private async Task<(int syncCount, string cursor, bool hasMore)> GetViedos(DouyinCookie cookie, AppConfig config, int syncCount, string cursor, bool hasMore, DouyinFollowed followed = null) + { + // 循环获取视频数据 + while (hasMore) + { + // 获取视频数据 + var data = await FetchVideoData(cookie, cursor, followed == null ? "" : followed?.SecUid); + if (data == null) + { + Log.Debug($"{JobType}-Cookie[{cookie.UserName}]读取数据失败!!!"); + break; + } + + // 判断是否还有更多数据 + hasMore = ShouldContinueSync(cookie, data, followed); + // 获取下一页游标 + cursor = GetNextCursor(data); + + // 如果没有视频数据,退出循环 + if (data.AwemeList == null || !data.AwemeList.Any()) + break; + + // 处理视频列表 + var videos = await ProcessVideoList(cookie, data, config, followed); + // 保存视频信息到数据库 + syncCount += await SaveVideos(videos); + + // 随机延迟,模拟人类操作,避免请求过快 + await Task.Delay(_random.Next(5, 10) * 1000); + } + + return (syncCount, cursor, hasMore); + } + /// /// 处理视频列表 /// 遍历视频列表,分别处理每个视频和图片集 @@ -346,23 +399,45 @@ namespace dy.net.job /// 用户Cookie /// 视频信息对象 /// 应用配置 + /// 关注的 /// 处理后的视频实体列表 - protected async Task> ProcessVideoList(DouyinUserCookie cookie, DouyinVideoInfo data, AppConfig config) + protected async Task> ProcessVideoList(DouyinCookie cookie, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed = null) { var videos = new List(); foreach (var item in data.AwemeList) { + //去重,检查视频是否已存在 + if (config.AutoDistinct) + { + var exitVideo = await douyinVideoService.GetByAwemeId(item.AwemeId); + if (exitVideo != null) + { + if (File.Exists(exitVideo.VideoSavePath)) + { + continue;// 已存在则跳过 + } + } + } + + var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId); + if (uper != null) + { + if (uper.FullSync) + { + followed ??= uper; + } + } // 处理单个视频 - var video = await ProcessSingleVideo(cookie, item, data, config); + var video = await ProcessSingleVideo(cookie, item, data, config, followed); if (video != null) videos.Add(video); // 如果配置了下载图片视频,则处理图片集并合成视频 if (_downImageVideo) { - if(config.DownImageVideo||config.DownMp3||config.DownImage) + if (config.DownImageVideo || config.DownMp3 || config.DownImage) { - var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config); + var mergevideo = await ProcessImageSetAndMergeToVideo(cookie, item, data, config, followed); if (mergevideo != null) videos.Add(mergevideo); } @@ -379,8 +454,9 @@ namespace dy.net.job /// 视频信息 /// 视频信息对象 /// 应用配置 + /// 关注 /// 处理后的视频实体,如果处理失败则为null - protected async Task ProcessSingleVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config) + protected async Task ProcessSingleVideo(DouyinCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed = null) { // 检查视频数据是否有效 if (!IsAwemeValid(item)) return null; @@ -396,27 +472,27 @@ namespace dy.net.job // 获取视频标签 var (tag1, tag2, tag3) = GetVideoTags(item); // 创建保存文件夹 - var saveFolder = CreateSaveFolder(cookie, item, tag1, tag2, config); + var saveFolder = CreateSaveFolder(cookie, item, config, followed); // 获取视频文件名 - var fileName = GetVideoFileName(cookie, item); + var fileName = GetVideoFileName(cookie, item, config); // 拼接视频保存路径 var savePath = Path.Combine(saveFolder, fileName); // 如果文件已存在,跳过 if (File.Exists(savePath)) return null; - Log.Debug($"{JobType}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]开始下载..."); + Log.Debug($"{JobType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]开始下载..."); // 随机延迟,模拟人类操作 await Task.Delay(_random.Next(1, 4) * 1000); // 下载视频 - if (!await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies)) + if (!await douyinHttpClientService.DownloadAsync(videoUrl, savePath, cookie.Cookies)) { - Log.Error($"{JobType}-{item?.Author?.Nickname??""}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载失败!!!"); + Log.Error($"{JobType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]下载失败!!!"); return null; } else { - Log.Debug($"{JobType}-{item?.Author?.Nickname ?? ""}-视频[{TikTokFileNameHelper.SanitizePath(item.Desc)}]下载完成."); + Log.Debug($"{JobType}-{item?.Author?.Nickname ?? ""}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]下载完成."); } // 下载视频封面 @@ -424,7 +500,7 @@ namespace dy.net.job // 下载作者头像 var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item); // 生成NFO文件 - await GenerateNfoFile(saveFolder, item, avatarSavePath, avatarUrl, cookie, config); + await GenerateNfoFile(saveFolder, item, avatarUrl, cookie, config); // 创建视频实体 return CreateVideoEntity(cookie, item, v, savePath, saveFolder, tag1, tag2, tag3, avatarSavePath, avatarUrl, data); } @@ -437,8 +513,9 @@ namespace dy.net.job /// 视频信息(包含图片集) /// 视频信息对象 /// 应用配置 + /// 应用配置 /// 合成后的视频实体,如果处理失败则为null - protected async Task ProcessImageSetAndMergeToVideo(DouyinUserCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config) + protected async Task ProcessImageSetAndMergeToVideo(DouyinCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed) { try { @@ -455,26 +532,28 @@ namespace dy.net.job return null; } - // 检查图片保存路径是否配置 - if (string.IsNullOrWhiteSpace(cookie.ImgSavePath)) - { - Log.Error($"{JobType}-图文视频同步-没有配置图片存储路径,任务终止!!!"); - return null; - } - - // 获取图片合成视频服务 - var imageService = _serviceProvider.GetService(); - if (imageService == null) - { - Log.Error($"{JobType} -图文视频同步异常,请联系作者!!!"); - return null; - } - // 创建图片保存文件夹 - var fileNamefolder = Path.Combine(cookie.ImgSavePath, TikTokFileNameHelper.GenerateFileName(item.Desc, item.AwemeId)); + var fileNamefolder = string.Empty; + + if (config.ImageViedoSaveAlone) + { + // 检查图片保存路径是否配置 + if (string.IsNullOrWhiteSpace(cookie.ImgSavePath)) + { + Log.Error($"{JobType}-图文视频同步-没有配置图片存储路径,任务终止!!!"); + return null; + } + fileNamefolder = Path.Combine(cookie.ImgSavePath, DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId)); + } + else + { + fileNamefolder = CreateSaveFolder(cookie, item, config, followed); + } if (!Directory.Exists(fileNamefolder)) Directory.CreateDirectory(fileNamefolder); + + var fileName = GetVideoFileName(cookie, item, config); // 合成视频的保存路径 - var savePath = Path.Combine(fileNamefolder, $"{item.AwemeId}.mp4"); + var savePath = Path.Combine(fileNamefolder, fileName); // 如果文件已存在,返回null if (File.Exists(savePath)) return null; @@ -482,6 +561,10 @@ namespace dy.net.job // 获取音乐URL var mp3Url = item.Music?.PlayUrl?.UrlList?.FirstOrDefault(); + var firstImage = item.Images.FirstOrDefault(); + int height = firstImage.Height; + int width = firstImage.Width; + // 准备合成视频的请求参数 var reqParams = new MediaMergeRequest { @@ -490,15 +573,15 @@ namespace dy.net.job VideoFps = 30, // 视频帧率 AudioUrls = string.IsNullOrWhiteSpace(mp3Url) ? new List() : new List { mp3Url }, // 音频URL列表 ImageUrls = imageUrls, // 图片URL列表 - VideoWidth = 1080, // 视频宽度 - VideoHeight = 1920, // 视频高度 + VideoWidth = width > 0 ? width : 1080, // 视频宽度 + VideoHeight = height > 0 ? height : 1920, // 视频高度 }; // 执行图片合成视频操作 - var mergeResult = await imageService.MergeToVideo(AppContext.BaseDirectory, reqParams, savePath, fileNamefolder,config.DownImageVideo,config.DownImage,config.DownMp3); + var mergeResult = await douyinMergeVideoService.MergeToVideo(cookie.Cookies, AppContext.BaseDirectory, reqParams, savePath, fileNamefolder, config.DownImageVideo, config.DownImage, config.DownMp3); if (!mergeResult) { - Log.Error($"{JobType}-图文视频-[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败!!!"); + Log.Error($"{JobType}-图文视频-[{DouyinFileNameHelper.SanitizePath(item.Desc)}]合成失败!!!"); return null; } @@ -507,7 +590,7 @@ namespace dy.net.job // 检查合成后的视频文件是否有效 if (!File.Exists(savePath) || new FileInfo(savePath).Length <= 0) { - Log.Error($"{JobType}-图文视频-[{TikTokFileNameHelper.SanitizePath(item.Desc)}]合成失败!!!"); + Log.Error($"{JobType}-图文视频-[{DouyinFileNameHelper.SanitizePath(item.Desc)}]合成失败!!!"); // 清理无效的文件和文件夹 if (Directory.Exists(fileNamefolder)) { @@ -522,12 +605,12 @@ namespace dy.net.job { } - // 下载视频封面(使用第一张图片作为封面) - await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie, item, config); + // 下载视频封面(使用第一张图片作为封面) + await DownVideoCover(imageUrls.FirstOrDefault(), fileNamefolder, cookie, item, config); // 下载作者头像 var (avatarSavePath, avatarUrl) = await DownAuthorAvatar(cookie, item); // 生成NFO文件 - await GenerateNfoFile(fileNamefolder, item, avatarSavePath, avatarUrl, cookie, config); + await GenerateNfoFile(fileNamefolder, item, avatarUrl, cookie, config); // 获取视频标签 var (tag1, tag2, tag3) = GetVideoTags(item); @@ -572,7 +655,26 @@ namespace dy.net.job if (!videos.Any()) return 0; try { - await _douyinVideoService.batchInsert(videos); + await douyinVideoService.batchInsert(videos); + + var redowns = await douyinCommonService.GetAllRedown(); + if (redowns != null && redowns.Any()) + { + //找出viedos和redowns重复的 + var duplicateVideos = videos.Where(v => redowns.Any(r => r.ViedoId == v.AwemeId)).ToList(); + if (duplicateVideos != null && duplicateVideos.Any()) + { + var duplicateVideosIds = duplicateVideos.Select(x => x.AwemeId).ToList(); + var downeds = redowns.Where(x => duplicateVideosIds.Contains(x.ViedoId))?.ToList(); + foreach (var item in downeds) + { + item.Status = 1; + var v = videos.FirstOrDefault(x => x.AwemeId == item.ViedoId); + Log.Debug($"{JobType}-重新下载成功:-{v.VideoTitle}"); + } + await douyinCommonService.UpdateRedownStatus(downeds); + } + } return videos.Count; } catch (Exception ex) @@ -591,12 +693,11 @@ namespace dy.net.job /// NFO文件的保存文件夹 /// 视频信息 /// 作者头像保存路径 - /// 作者头像URL /// /// /// 一个表示异步操作的任务 protected async Task GenerateNfoFile(string saveFolder, Aweme item, - string avatarSavePath, string avatarUrl, DouyinUserCookie cookie, AppConfig config) + string avatarSavePath, DouyinCookie cookie, AppConfig config) { // 异步生成NFO文件,避免阻塞主线程 await Task.Run(() => @@ -634,7 +735,7 @@ namespace dy.net.job /// 用户Cookie /// 应用配置 /// 一个表示异步操作的任务 - protected async Task DownVideoCover(Aweme item, string saveFolder, DouyinUserCookie cookie, AppConfig config) + protected async Task DownVideoCover(Aweme item, string saveFolder, DouyinCookie cookie, AppConfig config) { var coverUrl = item.Video.Cover.UrlList?.FirstOrDefault(); if (string.IsNullOrWhiteSpace(coverUrl)) return; @@ -648,7 +749,7 @@ namespace dy.net.job /// 用户Cookie /// 视频信息 /// 一个元组,包含头像保存路径和头像URL - protected async Task<(string savePath, string url)> DownAuthorAvatar(DouyinUserCookie cookie, Aweme item) + protected async Task<(string savePath, string url)> DownAuthorAvatar(DouyinCookie cookie, Aweme item) { if (item.Author == null) return (null, null); // 优先获取高清头像 @@ -663,7 +764,7 @@ namespace dy.net.job // 如果头像文件不存在,则下载 if (!File.Exists(avatarSavePath)) { - await _douyinService.DownloadAsync(avatarUrl, avatarSavePath, cookie.Cookies); + await douyinHttpClientService.DownloadAsync(avatarUrl, avatarSavePath, cookie.Cookies); } return (avatarSavePath, avatarUrl); } @@ -686,7 +787,7 @@ namespace dy.net.job /// /// 视频信息 /// 一个元组,包含三个级别的视频标签 - private (string tag1, string tag2, string tag3) GetVideoTags(Aweme item) + protected (string tag1, string tag2, string tag3) GetVideoTags(Aweme item) { var tags = item.VideoTags; return ( @@ -758,7 +859,7 @@ namespace dy.net.job /// 视频信息 /// 应用配置 /// 一个表示异步操作的任务 - private async Task DownVideoCover(string coverUrl, string saveFolder, DouyinUserCookie cookie, Aweme item, AppConfig config) + private async Task DownVideoCover(string coverUrl, string saveFolder, DouyinCookie cookie, Aweme item, AppConfig config) { if (string.IsNullOrWhiteSpace(coverUrl)) return; // 获取封面图片文件名 @@ -768,14 +869,7 @@ namespace dy.net.job // 如果封面文件不存在,则下载 if (!File.Exists(coverSavePath)) { - var downRes = await _douyinService.DownloadAsync(coverUrl, coverSavePath, cookie.Cookies); - //if (downRes) - //{ - // // 复制封面图片为fanart.jpg - // var fanartImgName = GetNfoFileName(cookie, item, config, "fanart.jpg"); - // var copyPath = Path.Combine(saveFolder, fanartImgName); - // File.Copy(coverSavePath, copyPath, true); - //} + var downRes = await douyinHttpClientService.DownloadAsync(coverUrl, coverSavePath, cookie.Cookies); } } @@ -796,7 +890,7 @@ namespace dy.net.job /// 视频信息对象 /// 创建的视频实体对象 private DouyinVideo CreateVideoEntity( - DouyinUserCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder, + DouyinCookie 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); diff --git a/job/DouyinFollowedUsersSyncJob.cs b/job/DouyinFollowedUsersSyncJob.cs new file mode 100644 index 0000000..528d15a --- /dev/null +++ b/job/DouyinFollowedUsersSyncJob.cs @@ -0,0 +1,81 @@ +using dy.net.dto; +using dy.net.model; +using dy.net.service; +using Quartz; +using System.Collections.Generic; + +namespace dy.net.job +{ + [DisallowConcurrentExecution] // 禁止并发执行,确保同一时间只有一个实例在运行 + public class DouyinFollowedUsersSyncJob : IJob + { + + + /// + /// 抖音Cookie服务,用于获取和管理用户Cookie + /// + protected readonly DouyinCookieService _dyCookieService; + + /// + /// 抖音HTTP客户端服务,用于发送HTTP请求 + /// + protected readonly DouyinHttpClientService _douyinService; + + /// + /// 抖音关注服务,用于管理关注数据 + /// + protected readonly DouyinFollowService _followService; + + public DouyinFollowedUsersSyncJob(DouyinCookieService dyCookieService, DouyinHttpClientService douyinService, DouyinFollowService followService) + { + _dyCookieService = dyCookieService; + _douyinService = douyinService; + _followService = followService; + } + + public async Task Execute(IJobExecutionContext context) + { + var cookies = await _dyCookieService.GetAllOpendAsync(); + if (cookies != null && cookies.Any()) + { + + foreach (var ck in cookies) + { + string count = "20"; + string offset = "0"; + bool hasmore = true; + int total= 0; + List follows = new List(); + while (hasmore) + { + var data = await _douyinService.SyncMyFollows(count, offset, ck.SecUserId, ck.Cookies); + + if (data != null) + { + // 绑定我的用户ID--之前没有这个字段 + if (string.IsNullOrWhiteSpace(ck.MyUserId)) + { + ck.MyUserId = data.MySelfUserId; + await _dyCookieService.UpdateAsync(ck); + } + total = data.Total; + hasmore = data.HasMore; + offset = data.Offset.ToString(); + if (data.Followings != null && data.Followings.Count > 0) + { + follows.AddRange(data.Followings); + } + } + } + + if (follows.Count > 0) + { + await _followService.Sync(follows, ck.MyUserId); + } + + Serilog.Log.Debug($"当前Cookie-[{ck.UserName}],本次同步关注列表完成,共同步关注{total}人。"); + } + } + } + } +} diff --git a/job/DouyinFollowedViedoSyncJob.cs b/job/DouyinFollowedViedoSyncJob.cs new file mode 100644 index 0000000..112d64d --- /dev/null +++ b/job/DouyinFollowedViedoSyncJob.cs @@ -0,0 +1,203 @@ +using ClockSnowFlake; +using dy.net.dto; +using dy.net.model; +using dy.net.service; +using dy.net.utils; +using Newtonsoft.Json; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace dy.net.job +{ + public class DouyinFollowedViedoSyncJob : DouyinBasicSyncJob + { + public DouyinFollowedViedoSyncJob(DouyinCookieService douyinCookieService, DouyinHttpClientService douyinHttpClientService, DouyinVideoService douyinVideoService, DouyinCommonService douyinCommonService, DouyinFollowService douyinFollowService, DouyinMergeVideoService douyinMergeVideoService) : base(douyinCookieService, douyinHttpClientService, douyinVideoService, douyinCommonService, douyinFollowService, douyinMergeVideoService) + { + } + + protected override string JobType => SystemStaticUtil.DY_FOLLOWEDS; + + protected override async Task> GetValidCookies() + { + return await douyinCookieService.GetAllOpendAsync(x => !string.IsNullOrWhiteSpace(x.UpSavePath)); + } + + protected override bool IsCookieValid(DouyinCookie cookie) + { + return !string.IsNullOrWhiteSpace(cookie.Cookies)&&!string.IsNullOrWhiteSpace(cookie.UpSavePath); + } + + protected override async Task FetchVideoData(DouyinCookie cookie, string cursor, string uperUid = "") + { + return await douyinHttpClientService.SyncUpderPostVideos(count, cursor, uperUid, cookie.Cookies); + } + + protected override bool ShouldContinueSync(DouyinCookie cookie, DouyinVideoInfo data, DouyinFollowed followed) + { + return data != null && data.HasMore == 1 && cookie.UperSyncd == 0 && followed.FullSync; + } + + protected override string GetNextCursor(DouyinVideoInfo data) + { + return data?.MaxCursor ?? "0"; + } + + /// + /// 关注用户特殊处理文件夹存储路径,用户可自定义保存路径 + /// + /// + /// + /// + /// + /// + protected override string CreateSaveFolder(DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed) + { + #region 默认使用UP主名称作为文件夹名称,若关注列表中有自定义保存路径则使用自定义路径 + var authorName = string.IsNullOrWhiteSpace(item.Author?.Nickname) ? "UnknownAuthor" : DouyinFileNameHelper.SanitizePath(item.Author.Nickname); + var folder = Path.Combine(cookie.UpSavePath, authorName); + if (!string.IsNullOrWhiteSpace(followed.SavePath)) + { + folder = Path.Combine(cookie.UpSavePath, followed.SavePath); + } + if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); + #endregion + + if (config.UperSaveTogether) + { + return folder; + } + else + { + var sampleName = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId); + var (existingName, _) = douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result; + var fileNameFolder = string.IsNullOrWhiteSpace(existingName) ? sampleName : existingName; + return Path.Combine(folder, fileNameFolder); + } + } + /// + /// 关注的视频,生成文件名称 + /// + /// + /// + /// + /// + protected override string GetVideoFileName(DouyinCookie cookie, Aweme item,AppConfig config) + { + + string Format = "mp4"; + string FileHash = ""; + string Height = ""; + string Width = ""; + + if (item.Video != null && item.Video.BitRate != null) + { + var bitrate = item.Video.BitRate.FirstOrDefault(); + Format = bitrate.Format; + FileHash = bitrate.PlayAddr.FileHash; + Height = bitrate.PlayAddr.Height.ToString(); + Width = bitrate.PlayAddr.Width.ToString(); + } + else + { + //图片合成视频,参数要自己写。 + var image = item.Images?.FirstOrDefault(); + if(image != null){ + FileHash = IdGener.GetGuid().ToLower().Replace("-","");//使用随机值,避免重复 + Height = image.Height.ToString(); + Width = image.Width.ToString(); + } + } + + + string fileName; + if (config?.UperUseViedoTitle ?? false)//优先 + { + var sampleName = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId); + var (existingName, _) = douyinVideoService.GetUperLastViedoFileName(item.Author.Uid, sampleName).Result; + fileName = string.IsNullOrWhiteSpace(existingName) ? $"{sampleName}.{Format}" : $"{existingName}.{Format}"; + } + else + { + + if (!string.IsNullOrWhiteSpace(config.FullFollowedTitleTemplate)) + { + var fullName = VideoTitleGenerator.Generate(config.FullFollowedTitleTemplate, new VideoTitleDataTemplate + { + FileHash = FileHash, + Id = item.AwemeId, + ReleaseTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime), + Resolution = $"{Width}×{Height}", + VideoTitle = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId) + }); + + fileName= $"{DouyinFileNameHelper.SanitizePath(fullName)}.{Format}"; + } + else + { + fileName = $"{item.AwemeId}.{Format}"; + } + } + return fileName; + + } + + + /// + /// + /// + /// + /// + /// + /// + /// + protected override string GetNfoFileName(DouyinCookie cookie, Aweme item, AppConfig config, string imageType) + { + if (config.UperSaveTogether) + { + var videoFileName = GetVideoFileName(cookie, item,config); + return $"{Path.GetFileNameWithoutExtension(videoFileName)}{imageType}"; + } + else + { + return base.GetNfoFileName(cookie, item, config, imageType); + } + } + + protected override string GetAuthorAvatarBasePath(DouyinCookie cookie) + { + return Path.Combine(cookie.UpSavePath, "author"); + } + + protected override async Task HandleSyncCompletion(DouyinCookie cookie, int syncCount) + { + if (syncCount > 0) + { + Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次同步成功{syncCount}条视频"); + cookie.UperSyncd = 1; + await douyinCookieService.UpdateAsync(cookie); + } + else + { + Serilog.Log.Debug($"{JobType}-Cookie-[{cookie.UserName}],本次没有查询到新的视频"); + } + } + + protected override VideoEntityDifferences GetVideoEntityDifferences(DouyinCookie cookie, Aweme item) + { + var config = douyinCommonService.GetConfig(); + string simplifiedTitle = string.Empty; + + if (config?.UperUseViedoTitle ?? false) + { + simplifiedTitle = DouyinFileNameHelper.GenerateFileName(item.Desc, item.AwemeId); + } + + return new VideoEntityDifferences + { + VideoType = VideoTypeEnum.UperPost, + VideoTitleSimplify = simplifiedTitle + }; + } + } +} \ No newline at end of file diff --git a/model/AppConfig.cs b/model/AppConfig.cs index 77d8c17..80cc0b4 100644 --- a/model/AppConfig.cs +++ b/model/AppConfig.cs @@ -1,4 +1,5 @@ using SqlSugar; +using System.Text.RegularExpressions; namespace dy.net.model { @@ -14,7 +15,7 @@ namespace dy.net.model public string Id { get; set; } [SugarColumn(Length =200,IsNullable =true)] - public string Cron { get; set; } + public int Cron { get; set; } /// /// 每次查询数量 @@ -46,10 +47,32 @@ namespace dy.net.model /// 日志保留天数,防止容器日志太多,默认10天 /// public int LogKeepDay { get; set; } = 10; + /// + /// 关注的视频标题命名模板{id}{VideoTitle}{SyncTime}{ReleaseTime}{FileHash}{Resolution}{FileSize} + /// + public string FollowedTitleTemplate { get; set; } + /// + /// 分隔符 + /// + public string FollowedTitleSeparator { get; set; } + /// + /// 完整的标题模板,包含分隔符 + /// + public string FullFollowedTitleTemplate { get; set; } + + + /// + /// 图文视频是否单独存放,否的话则按原类型存储位置存放,比如收藏夹、喜欢等 + /// + public bool ImageViedoSaveAlone { get; set; } [SugarColumn(IsIgnore=true)] public bool DownImageVideoFromEnv { get; set; } - + + /// + /// 自动去重-逻辑是遇到相同ID的视频直接跳过 + /// + public bool AutoDistinct { get; set; } } } diff --git a/model/DouyinCookie.cs b/model/DouyinCookie.cs new file mode 100644 index 0000000..f995f84 --- /dev/null +++ b/model/DouyinCookie.cs @@ -0,0 +1,142 @@ +using dy.net.dto; +using Newtonsoft.Json; +using SqlSugar; + +namespace dy.net.model +{ + [SugarTable(TableName = "dy_cookie")] + public class DouyinCookie + { + + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } + + /// + /// 用户抖音ID,对应我关注信息里面的myself_user_id + /// + public string MyUserId { get; set; } + /// + /// 用户描述 + /// + public string UserName { get; set; } + /// + /// 用户抖音Cookie + /// + [SugarColumn(Length =-1,IsNullable =true)] + public string Cookies { get; set; } + /// + /// 存储路径 + /// + [SugarColumn(Length =255,IsNullable =true)] + public string SavePath { get; set; } + + public int Status { get; set; } + + /// + /// 同步喜欢的视频需要sec_user_id + /// + [SugarColumn(Length =500,IsNullable =true)] + public string SecUserId { get; set; } + + /// + /// 喜欢的视频存储路径 + /// + [SugarColumn(Length =500,IsNullable =true)] + public string FavSavePath { get; set; } + + ///// + ///// 最新收藏夹的分页页码 + ///// + //[SugarColumn(Length =500,IsNullable =true)] + //public string CollectMaxCursor { get; set; } + ///// + ///// 最新我喜欢的分页页码 + ///// + //[SugarColumn(Length = 500, IsNullable = true)] + //public string FavoriteMaxCursor { get; set; } + + /// + /// 是否已经同步过了(就是是否是第一次同步) 0-未同步 1-已同步 + /// 如果是0,则同步时会获取全部数据;如果是1,则同步时只获取最新的数据(也就是只查一次) + /// 接口可以将该值改为0,下次开始同步就会重新获取全部数据 + /// + public int CollHasSyncd { get; set; } + public int FavHasSyncd { get; set; } + public int UperSyncd { get; set; } + /// + /// 关注的用户sec_user_id列表 json字符串存储 + /// + //[SugarColumn(Length =-1,IsNullable =true)] + //public string UpSecUserIds { get; set; } + + + /// + /// Up主发布的视频存储路径 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string UpSavePath { get; set; } + + + /// + /// 图片视频存储路径 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string ImgSavePath { get; set; } + + + //[SugarColumn(IsIgnore = true)] + //public List UpSecUserIdsJson + //{ + // get + // { + // // 反序列化逻辑(保持不变) + // if (string.IsNullOrWhiteSpace(UpSecUserIds)) + // { + // return new List(); + // } + + // try + // { + // return JsonConvert.DeserializeObject>(UpSecUserIds); + // } + // catch (JsonSerializationException ex) + // { + // // 日志记录(按需添加) + // // Logger.Error($"反序列化失败:{ex.Message},原始值:{UpSecUserIds}"); + // return new List(); + // } + // catch (Exception ex) + // { + // // 日志记录(按需添加) + // // Logger.Error($"处理异常:{ex.Message}"); + // return new List(); + // } + // } + // set + // { + // // 序列化逻辑:将列表转为JSON字符串,赋值给UpSecUserIds + // try + // { + // // 若传入的列表为null,直接设为空字符串(避免序列化后出现"null"字符串) + // UpSecUserIds = value == null + // ? string.Empty + // : JsonConvert.SerializeObject(value); + // } + // catch (JsonSerializationException ex) + // { + // // 捕获序列化异常(如对象循环引用、不支持的类型等) + // // Logger.Error($"序列化失败:{ex.Message},列表值:{value}"); + // // 异常时默认设为空字符串,避免存储错误的JSON + // UpSecUserIds = string.Empty; + // } + // catch (Exception ex) + // { + // // 捕获其他未知异常 + // // Logger.Error($"设置值异常:{ex.Message}"); + // UpSecUserIds = string.Empty; + // } + // } + //} + + } +} diff --git a/model/DouyinFollowed.cs b/model/DouyinFollowed.cs new file mode 100644 index 0000000..6d1db4f --- /dev/null +++ b/model/DouyinFollowed.cs @@ -0,0 +1,71 @@ +using Newtonsoft.Json; +using SqlSugar; + +namespace dy.net.model +{ + [SugarTable(TableName = "dy_follow")] + public class DouyinFollowed + { + + /// + /// + /// + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } + //[SugarColumn(IsNullable = false,Length =200)] + //public string UperId { get; set; } + + /// + /// 博主sec_uid + /// + [SugarColumn(IsNullable = false,Length =500)] + public string SecUid { get; set; } + + [SugarColumn(IsNullable = false, Length =200)] + public string UperName { get; set; } + + [SugarColumn(IsNullable = true, Length =1000)] + public string UperAvatar { get; set; } + /// + /// 官方账号(企业认证) + /// + [SugarColumn(IsNullable = true, Length =200)] + public string Enterprise { get; set; } + /// + /// 是否开启同步 + /// + public bool OpenSync { get; set; } + /// + /// 是否全量同步 + /// + public bool FullSync { get; set; } + /// + /// 最后更新时间 + /// + public DateTime LastSyncTime { get; set; } + + /// + /// 我的userId,关注者的抖音userid + /// + [SugarColumn(IsNullable = false,Length =200)] + public string mySelfId { get; set; } + + /// + /// 签名 + /// + [SugarColumn(IsNullable = true,Length =500)] + public string Signature { get; set; } + + /// + /// 同步文件保存路径 + /// + [SugarColumn(IsNullable = true,Length =500)] + public string SavePath { get; set; } + + /// + /// 博主Id + /// + [SugarColumn(IsNullable = true,Length =100)] + public string UperId { get; set; } + } +} diff --git a/model/DouyinUserCookie.cs b/model/DouyinUserCookie.cs deleted file mode 100644 index 163dd20..0000000 --- a/model/DouyinUserCookie.cs +++ /dev/null @@ -1,142 +0,0 @@ -using dy.net.dto; -using Newtonsoft.Json; -using SqlSugar; - -namespace dy.net.model -{ - [SugarTable(TableName = "dy_cookie")] - public class DouyinUserCookie - { - - [SugarColumn(IsPrimaryKey = true)] - public string Id { get; set; } - /// - /// 用户账号-唯一就行,手机号啥的 - /// - - //public string UserId { get; set; } - /// - /// 用户描述 - /// - public string UserName { get; set; } - /// - /// 用户抖音Cookie - /// - [SugarColumn(Length =-1,IsNullable =true)] - public string Cookies { get; set; } - /// - /// 存储路径 - /// - [SugarColumn(Length =255,IsNullable =true)] - public string SavePath { get; set; } - - public int Status { get; set; } - - /// - /// 同步喜欢的视频需要sec_user_id - /// - [SugarColumn(Length =500,IsNullable =true)] - public string SecUserId { get; set; } - - /// - /// 喜欢的视频存储路径 - /// - [SugarColumn(Length =500,IsNullable =true)] - public string FavSavePath { get; set; } - - ///// - ///// 最新收藏夹的分页页码 - ///// - //[SugarColumn(Length =500,IsNullable =true)] - //public string CollectMaxCursor { get; set; } - ///// - ///// 最新我喜欢的分页页码 - ///// - //[SugarColumn(Length = 500, IsNullable = true)] - //public string FavoriteMaxCursor { get; set; } - - /// - /// 是否已经同步过了(就是是否是第一次同步) 0-未同步 1-已同步 - /// 如果是0,则同步时会获取全部数据;如果是1,则同步时只获取最新的数据(也就是只查一次) - /// 接口可以将该值改为0,下次开始同步就会重新获取全部数据 - /// - public int CollHasSyncd { get; set; } - public int FavHasSyncd { get; set; } - public int UperSyncd { get; set; } - /// - /// 关注的用户sec_user_id列表 json字符串存储 - /// - [SugarColumn(Length =-1,IsNullable =true)] - public string UpSecUserIds { get; set; } - - - /// - /// Up主发布的视频存储路径 - /// - [SugarColumn(Length = 500, IsNullable = true)] - public string UpSavePath { get; set; } - - - /// - /// 图片视频存储路径 - /// - [SugarColumn(Length = 500, IsNullable = true)] - public string ImgSavePath { get; set; } - - - [SugarColumn(IsIgnore = true)] - public List UpSecUserIdsJson - { - get - { - // 反序列化逻辑(保持不变) - if (string.IsNullOrWhiteSpace(UpSecUserIds)) - { - return new List(); - } - - try - { - return JsonConvert.DeserializeObject>(UpSecUserIds); - } - catch (JsonSerializationException ex) - { - // 日志记录(按需添加) - // Logger.Error($"反序列化失败:{ex.Message},原始值:{UpSecUserIds}"); - return new List(); - } - catch (Exception ex) - { - // 日志记录(按需添加) - // Logger.Error($"处理异常:{ex.Message}"); - return new List(); - } - } - set - { - // 序列化逻辑:将列表转为JSON字符串,赋值给UpSecUserIds - try - { - // 若传入的列表为null,直接设为空字符串(避免序列化后出现"null"字符串) - UpSecUserIds = value == null - ? string.Empty - : JsonConvert.SerializeObject(value); - } - catch (JsonSerializationException ex) - { - // 捕获序列化异常(如对象循环引用、不支持的类型等) - // Logger.Error($"序列化失败:{ex.Message},列表值:{value}"); - // 异常时默认设为空字符串,避免存储错误的JSON - UpSecUserIds = string.Empty; - } - catch (Exception ex) - { - // 捕获其他未知异常 - // Logger.Error($"设置值异常:{ex.Message}"); - UpSecUserIds = string.Empty; - } - } - } - - } -} diff --git a/model/ViedoReDown.cs b/model/ViedoReDown.cs new file mode 100644 index 0000000..0b3658d --- /dev/null +++ b/model/ViedoReDown.cs @@ -0,0 +1,37 @@ +using SqlSugar; + +namespace dy.net.model +{ + /// + /// 重新下载的列表 + /// + [SugarTable(TableName = "dy_rd_video")] + public class ViedoReDown + { + /// + /// + /// + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } + /// + /// 原视频Id + /// + [SugarColumn(IsNullable =true,Length =50)] + public string ViedoId { get; set; } + /// + /// 原保存目录 + /// + [SugarColumn(IsNullable =true,Length =1000)] + public string SavePath { get; set; } + + public string CookieId { get; set; } + /// + /// 0-未下载 1-下载成功 2-下载失败 + /// + public int Status { get; set; } + + public DateTime CreateTime { get; set; } + public DateTime DownTime { get; set; } + public DateTime UpdateTime { get; set; } + } +} diff --git a/repository/BaseRepository.cs b/repository/BaseRepository.cs index 4cb6362..827d59a 100644 --- a/repository/BaseRepository.cs +++ b/repository/BaseRepository.cs @@ -98,6 +98,21 @@ namespace dy.net.repository { return Db.Deleteable().In(id).ExecuteCommand() > 0; } + /// + /// 事务执行 + /// + /// + /// + /// + public async Task UseTranAsync(Func action, Action errorCallBack) + { + var res = Db.Ado.UseTranAsync(async () => + { + await action(); + }, errorCallBack: errorCallBack); + + return res.IsCompletedSuccessfully; + } /// /// 根据主键删除(异步) diff --git a/repository/DouyinCookieRepository.cs b/repository/DouyinCookieRepository.cs new file mode 100644 index 0000000..8cc8547 --- /dev/null +++ b/repository/DouyinCookieRepository.cs @@ -0,0 +1,40 @@ +using dy.net.model; +using SqlSugar; +using System.Linq.Expressions; + +namespace dy.net.repository +{ + public class DouyinCookieRepository : BaseRepository + { + // 注入SQLSugar客户端 + public DouyinCookieRepository(ISqlSugarClient db) : base(db) + { + } + + public async Task> GetAllCookies(Expression> whereExpression = null) + { + // 1. 初始化查询:先加固定条件 Status == 1 + var query = Db.Queryable() + .Where(x => x.Status == 1); // 固定条件(必选) + + // 2. 若传入自定义条件,叠加 Where(自动 AND 组合) + if (whereExpression != null) + { + query = query.Where(whereExpression); // 自定义条件(可选) + } + + // 3. 执行查询(SqlSugar 自动合并所有 Where 条件) + return await query.ToListAsync(); + } + + public async Task<(List list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize) + { + var where = this.Db.Queryable(); + + var totalCount = await where.CountAsync(); + var list = await where.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); + return (list, totalCount); + } + + } +} diff --git a/repository/DouyinFollowRepository.cs b/repository/DouyinFollowRepository.cs new file mode 100644 index 0000000..8ba7111 --- /dev/null +++ b/repository/DouyinFollowRepository.cs @@ -0,0 +1,252 @@ +using ClockSnowFlake; +using dy.net.dto; +using dy.net.extension; +using dy.net.model; +using SqlSugar; + +namespace dy.net.repository +{ + public class DouyinFollowRepository : BaseRepository + { + // 注入SQLSugar客户端 + public DouyinFollowRepository(ISqlSugarClient db) : base(db) + { + } + + + + /// + /// 分页查询收藏视频 + /// + /// + /// 分页结果(视频列表和总数) + public async Task<(List list, int totalCount)> GetPagedAsync(FollowRequestDto dto) + { + var where = this.Db.Queryable() + .Where(x=>x.mySelfId==dto.MySelfId) + .WhereIF(!string.IsNullOrWhiteSpace(dto.FollowUserName), x => x.UperName.Contains(dto.FollowUserName)); + var totalCount = await where.CountAsync(); + var list = await where.OrderByDescending(x=>x.OpenSync).OrderByDescending(x => x.LastSyncTime).Skip((dto.PageIndex - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync(); + return (list, totalCount); + } + + + public async Task BatchInsert(List followeds) + { + return await Db.Insertable(followeds).ExecuteCommandAsync() > 0; + } + + + + public async Task BatchUpdate(List followeds) + { + return await Db.Updateable(followeds).ExecuteCommandAsync() > 0; + } + + + public async Task GetBySecUId(string secUid) + { + return await this.GetFirstAsync(x => x.SecUid == secUid); + } + + + public async Task GetBySecUId(string uperId,string myId) + { + return await this.GetFirstAsync(x => x.UperId == uperId && x.mySelfId == myId); + } + public async Task Update(DouyinFollowed followed) + { + return await this.UpdateAsync(followed); + } + + public async Task Insert(DouyinFollowed followed) + { + return await this.InsertAsync(followed); + } + + public async Task> GetSyncFollows(string userId) + { + return await this.Db.Queryable() + .Where(x => x.OpenSync == true).Where(x=>x.mySelfId== userId) + .ToListAsync(); + } + + /// + /// 同步关注列表(新增名字和签名变更检测) + /// + /// + /// + /// + public async Task Sync(List followInfos, string myselfUserId) + { + // 基础参数校验 + if (followInfos == null) followInfos = new List(); + if (string.IsNullOrWhiteSpace(myselfUserId)) + { + Serilog.Log.Error("同步关注列表失败:当前用户ID为空"); + return false; + } + + try + { + // 1. 查询现有关注列表 + List existFollows = await Db.Queryable() + .Where(x => x.mySelfId == myselfUserId) + .ToListAsync() ?? new List(); + + // 2. 提取现有和当前的SecUid集合(去重优化) + HashSet existSecUids = existFollows.Select(x => x.SecUid).ToHashSet(); + HashSet currentSecUids = followInfos.Select(x => x.SecUid).ToHashSet(); + + // 3. 计算新增、待删除和需要更新的记录 + var toAddFollows = followInfos.Where(x => !existSecUids.Contains(x.SecUid)).ToList(); + var toRemoveFollows = existFollows.Where(x => !currentSecUids.Contains(x.SecUid)).ToList(); + + // 3.1 筛选需要更新的记录(SecUid存在但名字或签名有变更) + var toUpdateFollows = new List(); + foreach (var existFollow in existFollows) + { + var newFollow = followInfos.FirstOrDefault(x => x.SecUid == existFollow.SecUid); + if (newFollow == null) continue; + + // 检查名字或签名是否变更(精确匹配,区分大小写和空格) + bool nameChanged = !string.Equals(existFollow.UperName, newFollow.NickName, StringComparison.Ordinal); + bool signatureChanged = !string.Equals(existFollow.Signature, newFollow.Signature, StringComparison.Ordinal); + bool enterpriseChanged = !string.Equals(existFollow.Enterprise, newFollow.EnterpriseVerifyReason, StringComparison.Ordinal); + bool uperAvatarChanged = !string.Equals(existFollow.UperAvatar, newFollow.Avatar.UrlList?.FirstOrDefault()??"", StringComparison.Ordinal); + //bool uperIdChanged = !string.Equals(existFollow.UperId, newFollow.UperId, StringComparison.Ordinal); + + if (nameChanged || signatureChanged|| uperAvatarChanged|| enterpriseChanged) + { + // 构造更新实体(仅赋值变更字段和必要字段) + var updateEntity = new DouyinFollowed + { + Id = existFollow.Id, // 主键必须保留,用于匹配 + mySelfId = existFollow.mySelfId, + SecUid = existFollow.SecUid, + UperName = newFollow.NickName, // 新名字 + Signature = newFollow.Signature, // 新签名 + UperId = newFollow.UperId, // 新的UperId + UperAvatar = newFollow.Avatar?.UrlList?.FirstOrDefault() ?? "", // 新头像 + Enterprise = newFollow.EnterpriseVerifyReason, // 新企业认证 + LastSyncTime = DateTime.UtcNow // 更新同步时间 + // 其他字段(如Enterprise、UperAvatar、OpenSync)保留原有值,无需赋值 + }; + toUpdateFollows.Add(updateEntity); + } + } + + // 4. 分批处理新增(单批200条) + if (toAddFollows.Any()) + { + Func mapToDouyinFollowed = follow => new DouyinFollowed + { + Id = IdGener.GetLong().ToString(), + Enterprise = follow.EnterpriseVerifyReason, + LastSyncTime = DateTime.UtcNow, + mySelfId = myselfUserId, + SecUid = follow.SecUid, + OpenSync = false, + UperAvatar = follow.Avatar?.UrlList?.FirstOrDefault() ?? "", + UperName = follow.NickName, + Signature = follow.Signature, + UperId = follow.UperId + }; + + bool batchAddSuccess = await BatchProcessAsync(toAddFollows, 200, + async batch => await BatchInsert(batch.Select(mapToDouyinFollowed).ToList())); + + if (!batchAddSuccess) + { + Serilog.Log.Error("同步关注列表失败:新增关注分批插入异常"); + return false; + } + } + + // 5. 分批处理更新(适配 SQLSugar 语法:UpdateColumns + WhereColumns) + if (toUpdateFollows.Any()) + { + bool batchUpdateSuccess = await BatchProcessAsync(toUpdateFollows, 200, + async batch => + { + // SQLSugar 正确用法:实体集合更新 + UpdateColumns(指定更新字段) + WhereColumns(指定匹配字段/主键) + int affectedRows = await Db.Updateable(batch) // 传入更新实体集合 + .UpdateColumns(x => new { x.UperName, x.Signature, x.LastSyncTime,x.Enterprise,x.UperAvatar}) // 仅更新这3个字段 + .WhereColumns(x => x.Id) // 按主键Id匹配现有记录 + .ExecuteCommandAsync(); + + // 受影响行数 > 0 或 批次无数据(正常),返回true;否则返回false + return affectedRows >= 0; + }); + + if (!batchUpdateSuccess) + { + Serilog.Log.Error("同步关注列表失败:关注信息更新异常"); + return false; + } + + Serilog.Log.Debug($"同步关注列表:成功更新{toUpdateFollows.Count}条关注信息(用户ID:{myselfUserId})"); + } + + // 6. 分批处理删除(单批200条) + if (toRemoveFollows.Any()) + { + bool batchDeleteSuccess = await BatchProcessAsync(toRemoveFollows, 200, + async batch => + { + var secUids = batch.Select(x => x.SecUid).ToList(); + await DeleteAsync(x => x.mySelfId == myselfUserId && secUids.Contains(x.SecUid)); + return true; + }); + + if (!batchDeleteSuccess) + { + Serilog.Log.Error("同步关注列表失败:取消关注分批删除异常"); + return false; + } + } + + Serilog.Log.Debug($"同步关注列表完成(用户ID:{myselfUserId}):新增{toAddFollows.Count}条,更新{toUpdateFollows.Count}条,删除{toRemoveFollows.Count}条"); + return true; + } + catch (Exception ex) + { + Serilog.Log.Error(ex, $"同步关注列表失败(用户ID:{myselfUserId}):{ex.Message}"); + return false; + } + } + + /// + /// 通用分批处理工具方法 + /// + /// 数据类型 + /// 待处理数据 + /// 单批大小 + /// 单批处理逻辑(返回是否成功) + /// 整体处理结果 + private async Task BatchProcessAsync(List dataList, int batchSize, Func, Task> processAction) + { + if (dataList == null || !dataList.Any() || batchSize <= 0) + return true; + + int totalCount = dataList.Count; + int batchCount = (int)Math.Ceiling((double)totalCount / batchSize); + + for (int i = 0; i < batchCount; i++) + { + var batch = dataList.Skip(i * batchSize).Take(batchSize).ToList(); + if (!batch.Any()) continue; + + bool success = await processAction(batch); + if (!success) + { + Serilog.Log.Debug($"分批处理失败:第{i + 1}批(数据范围:{i * batchSize}-{Math.Min((i + 1) * batchSize - 1, totalCount - 1)})"); + return false; + } + } + + return true; + } + + } +} diff --git a/repository/DouyinUserCookieRepository.cs b/repository/DouyinUserCookieRepository.cs deleted file mode 100644 index 9c278b2..0000000 --- a/repository/DouyinUserCookieRepository.cs +++ /dev/null @@ -1,29 +0,0 @@ -using dy.net.model; -using SqlSugar; - -namespace dy.net.repository -{ - public class DouyinUserCookieRepository : BaseRepository - { - // 注入SQLSugar客户端 - public DouyinUserCookieRepository(ISqlSugarClient db) : base(db) - { - } - - public async Task> GetAllCookies() - { - return await this.GetListAsync(x=>x.Status==1); - } - - - public async Task<(List list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize) - { - var where = this.Db.Queryable(); - - var totalCount = await where.CountAsync(); - var list = await where.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); - return (list, totalCount); - } - - } -} diff --git a/repository/DouyinVideoRepository.cs b/repository/DouyinVideoRepository.cs index 770c34a..90dc9db 100644 --- a/repository/DouyinVideoRepository.cs +++ b/repository/DouyinVideoRepository.cs @@ -15,50 +15,48 @@ namespace dy.net.repository } + + /// - /// 分页查询收藏视频 + /// /// - /// 页码(从1开始) - /// 每页数量 - /// 可选标签过滤 - /// 可选作者过滤 - /// 分页结果(视频列表和总数) - public async Task<(List list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null,string viedoType=null, List dates = null) + /// + /// + public async Task<(List list, int totalCount)> GetPagedAsync(DouyinVideoPageRequestDto dto) { + DateTime? start, end; + GetDateBetween(dto.Dates, out start, out end); - DateTime? start = null; - DateTime? end =null; - if(dates!=null && dates.Count==2) + DateTime? start2, end2; + GetDateBetween(dto.Dates2, out start2, out end2); + + + VideoTypeEnum? enumviedoType = null; + if (!string.IsNullOrEmpty(dto.ViedoType) && dto.ViedoType != "*") { - start = Convert.ToDateTime(dates[0]); - end = Convert.ToDateTime(dates[1]); + enumviedoType = dto.ViedoType.ToVideoTypeEnum(); } - else if(dates!=null && dates.Count==1) - { - start = Convert.ToDateTime(dates[0]); - } - VideoTypeEnum? enumviedoType = null; - if (!string.IsNullOrEmpty(viedoType)&&viedoType!="*") - { - enumviedoType = viedoType.ToVideoTypeEnum(); - } var where = this.Db.Queryable() - .WhereIF(!string.IsNullOrWhiteSpace(tag), x => x.Tag1 == tag) - .WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author) + //.WhereIF(!string.IsNullOrWhiteSpace(title), x => x.VideoTitle.Contains(title)) + .WhereIF(!string.IsNullOrWhiteSpace(dto.Title), x => x.VideoTitle.Contains(dto.Title)) + .WhereIF(!string.IsNullOrWhiteSpace(dto.Author), x => x.Author == dto.Author) .WhereIF(start.HasValue, x => x.SyncTime >= start.Value) .WhereIF(end.HasValue, x => x.SyncTime <= end.Value) + .WhereIF(start2.HasValue, x => x.CreateTime >= start2.Value) + .WhereIF(end2.HasValue, x => x.CreateTime <= end2.Value) .WhereIF(enumviedoType.HasValue, x => x.ViedoType == enumviedoType); 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().ToListAsync(); + var list = await where.OrderByDescending(x => x.SyncTime).Skip((dto.PageIndex - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync(); + if (list.Any()) + { + var users = await this.Db.Queryable().ToListAsync(); foreach (var item in list) { - var user= users.FirstOrDefault(x=>x.Id== item.CookieId); - if(user!=null) + var user = users.FirstOrDefault(x => x.Id == item.CookieId); + if (user != null) { item.DyUser = user.UserName; } @@ -67,19 +65,34 @@ namespace dy.net.repository return (list, totalCount); } + private static void GetDateBetween(List dates, out DateTime? start, out DateTime? end) + { + start = null; + end = null; + if (dates != null && dates.Count == 2) + { + start = Convert.ToDateTime(dates[0]); + end = Convert.ToDateTime(dates[1]); + } + else if (dates != null && dates.Count == 1) + { + start = Convert.ToDateTime(dates[0]); + } + } + /// - /// + /// 对于重复标题进行处理 /// /// /// /// - public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId,string ViedoNameSimplify) + public async Task<(string, string)> GetUperLastViedoFileName(string AuthorId, string ViedoNameSimplify) { - var video= await this.Db.Queryable().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.UperPost) - .Where(x => x.VideoTitleSimplify == ViedoNameSimplify) - .OrderByDescending(x => x.CreateTime).FirstAsync(); + var video = await this.Db.Queryable().Where(x => x.AuthorId == AuthorId && x.ViedoType == VideoTypeEnum.UperPost) + .Where(x => x.VideoTitleSimplify == ViedoNameSimplify) + .OrderByDescending(x => x.CreateTime).FirstAsync(); if (video != null) { @@ -107,9 +120,88 @@ namespace dy.net.repository { return (ViedoNameSimplify, ""); } - - } + /// + /// 根据视频ID列表获取视频信息 + /// + /// + /// + public async Task> GetByIds(List ids) + { + // 使用 Queryable 方法构建查询 + return await this.Db.Queryable() + .Where(x => ids.Contains(x.Id)) + .ToListAsync(); + } + + /// + /// 记录要重新下载的视频 + /// + /// + /// + public bool InsertReDowns(List downs) + { + if (downs != null) + { + return Db.Insertable(downs).ExecuteCommand() > 0; + + } + else + { + return false; + } + } + + /// + /// 更新视频重新下载状态 + /// + /// 视频ID(对应ViedoReDown.Id) + /// 状态值(1时会同步更新下载时间) + /// 是否更新成功(影响行数>0) + public async Task UpdateReDownStatus(string videoId, int status) + { + // 校验必填参数(避免无效数据库操作) + if (string.IsNullOrWhiteSpace(videoId)) + { + Serilog.Log.Error("更新重新下载状态失败:videoId为空"); + return false; + } + + // 构建更新条件(统一Where条件,避免重复代码) + var updateable = Db.Updateable() + .Where(it => it.Id == videoId) + .SetColumns(it => new ViedoReDown + { + Status = status, + UpdateTime = DateTime.Now + }); + + if (status == 1) + { + updateable = updateable.SetColumns(it => it.DownTime == DateTime.Now); + + } + int affectedRows = await updateable.ExecuteCommandAsync(); + + // 可选:记录更新结果日志 + if (affectedRows <= 0) + { + Serilog.Log.Debug("更新重新下载状态无匹配数据:videoId={0}, status={1}", videoId, status); + } + + return affectedRows > 0; + } + + /// + /// + /// + /// + public async Task> GetViedoReDowns() + { + return await this.Db.Queryable() + .Where(x => x.Status == 0 || x.Status == 2) + .ToListAsync(); + } } } diff --git a/service/DouyinCommonService.cs b/service/DouyinCommonService.cs index 6868c6f..f56c902 100644 --- a/service/DouyinCommonService.cs +++ b/service/DouyinCommonService.cs @@ -48,14 +48,18 @@ namespace dy.net.service AppConfig config = new AppConfig { Id = IdGener.GetLong().ToString(), - Cron = "30", - BatchCount = 10, + Cron = 30, + BatchCount = 18, LogKeepDay = 10, UperSaveTogether = false,//博主视频:true-->每个视频单独一个文件夹 false-->所有视频放在同一个文件夹 UperUseViedoTitle = false,//博主视频:true-->使用视频标题作为文件名 false-->使用视频id作为文件名 DownImageVideo = downLoadImage, DownMp3 = false, - DownImage = false + DownImage = false, + ImageViedoSaveAlone = true, + FollowedTitleTemplate ="", + FullFollowedTitleTemplate="", + FollowedTitleSeparator="" }; sqlSugarClient.Insertable(config).ExecuteCommand(); return config; @@ -81,11 +85,16 @@ namespace dy.net.service cc.UperSaveTogether = config.UperSaveTogether; cc.UperUseViedoTitle = config.UperUseViedoTitle; cc.LogKeepDay = config.LogKeepDay; - cc.DownMp3= config.DownMp3; - cc.DownImage= config.DownImage; + cc.DownMp3 = config.DownMp3; + cc.DownImage = config.DownImage; + cc.FollowedTitleTemplate = config.FollowedTitleTemplate; + cc.FullFollowedTitleTemplate= config.FullFollowedTitleTemplate; + cc.ImageViedoSaveAlone = config.ImageViedoSaveAlone; + cc.FollowedTitleSeparator = config.FollowedTitleSeparator; + cc.AutoDistinct = config.AutoDistinct; } - var update = await sqlSugarClient.Updateable(cc).ExecuteCommandAsync(); + int update = await sqlSugarClient.Updateable(cc).ExecuteCommandAsync(); return update > 0; } @@ -96,7 +105,7 @@ namespace dy.net.service public void UpdateCollectViedoType() { - string sql= @"UPDATE dy_collect_video + string sql = @"UPDATE dy_collect_video SET ViedoType = CASE WHEN ViedoType = '0' THEN 0 WHEN ViedoType = '1' THEN 1 @@ -124,33 +133,30 @@ namespace dy.net.service /// public bool UpdateAllCookieSyncedToZero() { - //var sql = "update dy_cookie set CollHasSyncd=0,FavHasSyncd=0,UperSyncd=0"; - // sqlSugarClient.Ado.ExecuteCommand(sql) > 0; - var cookies = sqlSugarClient.Queryable().ToList(); + var cookies = sqlSugarClient.Queryable().ToList(); foreach (var cookie in cookies) { cookie.CollHasSyncd = 0; cookie.FavHasSyncd = 0; cookie.UperSyncd = 0; - var upers = cookie.UpSecUserIds; - if (!string.IsNullOrWhiteSpace(upers)) - { - var uperList = Newtonsoft.Json.JsonConvert.DeserializeObject>(upers); - if (uperList != null && uperList.Count > 0) - { - foreach (var uper in uperList) - { - uper.syncAll = false; - } - } - var newUpers = Newtonsoft.Json.JsonConvert.SerializeObject(uperList); - cookie.UpSecUserIds = newUpers; - } } return sqlSugarClient.Updateable(cookies).ExecuteCommand() > 0; } + /// + /// 查询需要下载的所有重下载视频记录 + /// + /// + public async Task> GetAllRedown() + { + return await sqlSugarClient.Queryable().Where(x => x.Status == 0 || x.Status == 2).ToListAsync(); + } + + public async Task UpdateRedownStatus(List list) + { + return await sqlSugarClient.Updateable(list).ExecuteCommandAsync() > 0; + } #region 测试创建数据库 ///// diff --git a/service/DouyinCookieService.cs b/service/DouyinCookieService.cs index 8638b08..bc0ac02 100644 --- a/service/DouyinCookieService.cs +++ b/service/DouyinCookieService.cs @@ -8,20 +8,20 @@ namespace dy.net.service public class DouyinCookieService { - private readonly DouyinUserCookieRepository _cookieRepository; + private readonly DouyinCookieRepository _cookieRepository; - public DouyinCookieService(DouyinUserCookieRepository cookieRepository) + public DouyinCookieService(DouyinCookieRepository cookieRepository) { _cookieRepository = cookieRepository; } - public Task> GetAllCookies() + public Task> GetAllOpendAsync(Expression> whereExpression = null) { - return _cookieRepository.GetAllCookies(); + return _cookieRepository.GetAllCookies(whereExpression); } - public async Task Add(DouyinUserCookie dyUserCookies) + public async Task Add(DouyinCookie dyUserCookies) { return await _cookieRepository.InsertAsync(dyUserCookies); } @@ -34,7 +34,7 @@ namespace dy.net.service { return false; } - var cookie = new DouyinUserCookie + var cookie = new DouyinCookie { UserName = "douyin", Cookies = "--", @@ -53,19 +53,19 @@ namespace dy.net.service } // 查询单个 - public async Task GetByIdAsync(string id) + public async Task GetByIdAsync(string id) { return await _cookieRepository.GetByIdAsync(id); } // 查询列表(可加条件) - public async Task<(List list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize) + public async Task<(List list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize) { return await _cookieRepository.GetPagedAsync(pageIndex, pageSize); } // 更新 - public async Task UpdateAsync(DouyinUserCookie dyUserCookies) + public async Task UpdateAsync(DouyinCookie dyUserCookies) { return await _cookieRepository.UpdateAsync(dyUserCookies); } diff --git a/service/DouyinFollowService.cs b/service/DouyinFollowService.cs new file mode 100644 index 0000000..39f09ef --- /dev/null +++ b/service/DouyinFollowService.cs @@ -0,0 +1,89 @@ +using dy.net.dto; +using dy.net.model; +using dy.net.repository; +using SqlSugar; +using System.Linq.Expressions; + +namespace dy.net.service +{ + public class DouyinFollowService + { + + private readonly DouyinFollowRepository _followRepository; + + public DouyinFollowService(DouyinFollowRepository followRepository) + { + _followRepository = followRepository; + } + + /// + /// + /// + /// + /// + public async Task<(List list, int totalCount)> GetPagedAsync(FollowRequestDto dto) + { + return await _followRepository.GetPagedAsync(dto); + } + /// + /// + /// + /// + /// + /// + public async Task Sync(List followInfos, string myselfUserId) + { + return await _followRepository.Sync(followInfos, myselfUserId); + } + + public async Task GetByUperId(string uperId,string myUid) + { + return await _followRepository.GetBySecUId(uperId, myUid); + } + + /// + /// 打开或关闭同步 + /// + /// + /// + public async Task OpenOrCloseSync(FollowUpdateDto dto) + { + var followed = await _followRepository.GetByIdAsync(dto.Id); + if (followed != null) + { + followed.OpenSync = dto.OpenSync; + followed.FullSync = dto.FullSync; + followed.SavePath = dto.SavePath; + return await _followRepository.Update(followed); + } + return false; + } + + + /// + /// 打开或关闭同步 + /// + /// + /// + public async Task OpenOrCloseFullSync(FollowUpdateDto dto) + { + var followed = await _followRepository.GetByIdAsync(dto.Id); + if (followed != null) + { + followed.FullSync = dto.FullSync; + return await _followRepository.Update(followed); + } + return false; + } + + /// + /// 获取需要同步的关注列表 + /// + /// + public async Task> GetSyncFollows(string userUserId) + { + return await _followRepository.GetSyncFollows(userUserId); + } + + } +} diff --git a/service/DouyinHttpClientService.cs b/service/DouyinHttpClientService.cs index 778a299..bf31e49 100644 --- a/service/DouyinHttpClientService.cs +++ b/service/DouyinHttpClientService.cs @@ -1,9 +1,12 @@ using dy.net.dto; using dy.net.utils; +using NetTaste; using Newtonsoft.Json; using System; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection.PortableExecutable; namespace dy.net.service { @@ -195,10 +198,10 @@ namespace dy.net.service // 构建请求URL var queryString = new FormUrlEncodedContent(parameters); string fullUrl = $"{DouYinApi}/post?{await queryString.ReadAsStringAsync()}"; - var ablog = new ABogus();//计算X-Bogus - var a_bogus = ablog.GetValue(parameters); + //var ablog = new ABogus();//计算X-Bogus + //var a_bogus = ablog.GetValue(parameters); - fullUrl += $"&X-Bogus={a_bogus}"; + //fullUrl += $"&X-Bogus={a_bogus}"; var respose = await httpClient.GetAsync(fullUrl); if (respose.IsSuccessStatusCode) { @@ -219,6 +222,61 @@ namespace dy.net.service } + /// + /// 查询我的关注用户列表 + /// + /// + /// + /// + /// + /// + public async Task SyncMyFollows(string count,string offset,string secUserId,string cookie) + { + + try + { + using var httpClient = _clientFactory.CreateClient("dy_follow"); + if (httpClient.DefaultRequestHeaders.Contains("Cookie")) + { + httpClient.DefaultRequestHeaders.Remove("Cookie"); + } + httpClient.DefaultRequestHeaders.Add("Cookie", cookie); + var dics = DouyinBaseParamDics.MyFollowParams; + { + // 添加动态参数 + dics["sec_user_id"] = secUserId; + dics["count"] = count; + dics["offset"] = offset; + } + // 构建请求URL + var queryString = new FormUrlEncodedContent(dics); + string fullUrl = $"https://www.douyin.com/aweme/v1/web/user/following/list/?{await queryString.ReadAsStringAsync()}"; + var respose = await httpClient.GetAsync(fullUrl); + if (respose.IsSuccessStatusCode) + { + var data = await respose.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(data); + } + else + { + Serilog.Log.Error($"SyncMyFollows fail: {respose.StatusCode}"); + return null; + } + } + catch (Exception ex) + { + Serilog.Log.Error($"SyncMyFollows error: {ex.Message}"); + return null; + } + } + + + public async Task SyncReDown(string awemeId,string cookie) + { + return false; + //有空再研究把.a_bogus算法有问题 + } + //AI优化2 /// /// 下载文件并保存到本地(支持重试机制,优化批量下载稳定性) @@ -370,6 +428,28 @@ namespace dy.net.service return true; } + + ///// 下载网络文件到指定路径 + //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); + //} + + + /// /// 判断异常是否可重试 /// @@ -385,7 +465,7 @@ namespace dy.net.service /// /// 清理不完整的文件 /// - private void CleanupIncompleteFile(string savePath) + private static void CleanupIncompleteFile(string savePath) { if (File.Exists(savePath)) { @@ -400,212 +480,5 @@ namespace dy.net.service } } - //--AI优化后的-1 - - ///// - ///// 下载文件并保存到本地(优化批量下载假死问题) - ///// - ///// 文件地址 - ///// 保存路径 - ///// 请求Cookie - ///// 取消令牌(用于终止卡住的任务) - ///// 流读取超时时间(默认30秒) - ///// 是否下载成功 - //public async Task DownloadAsync( - // string videoUrl, - // string savePath, - // string cookie, string httpclientName=null, - // CancellationToken cancellationToken = default, - // TimeSpan? streamTimeout = null) - //{ - // // 流读取超时默认60秒(避免长时间无数据导致假死) - // var streamTimeoutValue = streamTimeout ?? TimeSpan.FromSeconds(60); - // // 记录上次流活动时间(用于超时判断) - // DateTime lastStreamActivity = DateTime.UtcNow; - - // try - // { - // // 确保目录存在 - // var directory = Path.GetDirectoryName(savePath); - // if (!Directory.Exists(directory)) - // { - // Directory.CreateDirectory(directory); - // } - - // // 先删除已存在文件(处理文件占用问题) - // if (File.Exists(savePath)) - // { - // // 重试删除(防止文件刚被释放) - // for (int i = 0; i < 3; i++) - // { - // try - // { - // File.Delete(savePath); - // break; - // } - // catch (IOException) when (i < 2) - // { - // await Task.Delay(100, cancellationToken).ConfigureAwait(false); - // } - // } - // } - // httpclientName??="dy_down1"; - // // 使用客户端工厂创建HttpClient(复用连接池) - // using (var httpClient = _clientFactory.CreateClient(httpclientName)) - // { - // // 清理并添加Cookie - // httpClient.DefaultRequestHeaders.Remove("Cookie"); - // httpClient.DefaultRequestHeaders.Add("Cookie", cookie); - // // 总请求超时(包括连接和初始响应) - // httpClient.Timeout = TimeSpan.FromMinutes(5); - - // // 发起请求(响应头就绪后返回,不等待完整内容) - // using (var response = await httpClient.GetAsync( - // videoUrl, - // HttpCompletionOption.ResponseHeadersRead, - // cancellationToken).ConfigureAwait(false)) - // { - // response.EnsureSuccessStatusCode(); // 验证HTTP状态码 - - // // 获取文件总大小(用于进度计算) - // long? totalBytes = response.Content.Headers.ContentLength; - - // // 读取响应流并写入文件 - // using (var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken) - // .ConfigureAwait(false)) - // // 优化FileStream:异步模式+顺序扫描(提升大文件写入效率) - // using (var fileStream = new FileStream( - // savePath, - // FileMode.CreateNew, - // FileAccess.Write, - // FileShare.None, - // bufferSize: 8192, - // options: FileOptions.Asynchronous | FileOptions.SequentialScan)) - // { - // byte[] buffer = new byte[8192]; - // int bytesRead; - // long totalRead = 0; - - // // 循环读取流(带超时和取消检查) - // while ((bytesRead = await responseStream.ReadAsync( - // buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) - // { - // // 检查流读取超时(长时间无数据) - // if (DateTime.UtcNow - lastStreamActivity > streamTimeoutValue) - // { - // throw new TimeoutException($"流读取超时({streamTimeoutValue.TotalSeconds}秒无数据)"); - // } - - // // 写入文件 - // await fileStream.WriteAsync( - // buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); - - // // 更新进度和活动时间 - // totalRead += bytesRead; - // lastStreamActivity = DateTime.UtcNow; - - // // (可选)进度上报逻辑 - // if (totalBytes.HasValue) - // { - // double progress = (double)totalRead / totalBytes.Value * 100; - // // 可通过事件或委托上报进度:OnProgressChanged(progress); - // } - // } - - // // 确保数据刷入磁盘 - // await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false); - // } - // } - // } - - // return true; - // } - // catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - // { - // // 正常取消操作(非错误) - // Serilog.Log.Information($"下载被取消:{videoUrl}"); - // return false; - // } - // catch (Exception ex) - // { - // // 记录错误并清理可能的不完整文件 - // Serilog.Log.Error(ex, $"下载失败:{videoUrl}"); - // if (File.Exists(savePath)) - // { - // try { File.Delete(savePath); } catch { /* 忽略删除失败 */ } - // } - // return false; - // } - //} - - ///// - ///// 下载文件并保存到本地 - ///// - ///// 文件地址 - ///// 保存路径 - ///// - //public async Task DownloadAsync(string videoUrl, string savePath, string cookie) - //{ - // try - // { - // // 防止文件被占用,先删除已存在的文件 - // if (File.Exists(savePath)) - // { - // File.Delete(savePath); - // } - // // 创建HTTP客户端(设置超时时间和请求头) - // using (var httpClient = _clientFactory.CreateClient("dy_down")) - // { - // if (httpClient.DefaultRequestHeaders.Contains("Cookie")) - // { - // httpClient.DefaultRequestHeaders.Remove("Cookie"); - // } - // httpClient.DefaultRequestHeaders.Add("Cookie", cookie); - // httpClient.Timeout = TimeSpan.FromMinutes(5); // 设置5分钟超时 - - // // 获取视频流 - // using (var response = await httpClient.GetAsync(videoUrl, HttpCompletionOption.ResponseHeadersRead)) - // { - // response.EnsureSuccessStatusCode(); // 确保请求成功 - - // // 获取文件总大小(用于进度显示) - // long? totalBytes = response.Content.Headers.ContentLength; - - // // 读取流并写入文件 - // using (var stream = await response.Content.ReadAsStreamAsync()) - // using (var fileStream = new FileStream(savePath, FileMode.CreateNew)) - // { - // byte[] buffer = new byte[8192]; - // int bytesRead; - // long totalRead = 0; - - // // 循环读取并写入 - // while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) - // { - // await fileStream.WriteAsync(buffer, 0, bytesRead); - // totalRead += bytesRead; - - // // 显示下载进度 - // if (totalBytes.HasValue) - // { - // double progress = (double)totalRead / totalBytes.Value * 100; - // //Console.Write($"\r下载进度:{progress:F2}% ({totalRead}/{totalBytes.Value} bytes)"); - // } - // } - // //Console.WriteLine(); // 进度显示结束后换行 - // } - // } - // } - // return true; - // } - // catch (Exception ex) - // { - // Serilog.Log.Error($"DownloadVideoAsync fail: {ex.Message}"); - // Serilog.Log.Error($"DownloadVideoAsync fail: {ex.StackTrace}"); - // return false; - // } - //} - - } } diff --git a/service/DouyinMergeVideoService.cs b/service/DouyinMergeVideoService.cs new file mode 100644 index 0000000..b5bf1bc --- /dev/null +++ b/service/DouyinMergeVideoService.cs @@ -0,0 +1,421 @@ +using dy.net.dto; +using dy.net.utils; +using Serilog; + +namespace dy.net.service +{ + + /// + /// 合并多张图片+音频为视频 + /// + public class DouyinMergeVideoService + { + private readonly FFmpegHelper _fFmpegHelper; + private readonly DouyinHttpClientService douyinHttpClientService; + + public DouyinMergeVideoService(FFmpegHelper fFmpegHelper,DouyinHttpClientService douyinHttpClientService) + { + _fFmpegHelper = fFmpegHelper; + this.douyinHttpClientService = douyinHttpClientService; + } + + // 1. 并发控制:限制同时运行的 FFmpeg 进程数(建议设为 1,FFmpeg 单进程更稳定) + private readonly SemaphoreSlim _ffmpegSemaphore = new SemaphoreSlim(1, 1); + // 重试次数配置 + private const int RetryCount = 3; + // 重试间隔(毫秒) + private const int RetryDelay = 1000; + + /// + /// 视频合成 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + //public async Task MergeToVideo(string cookie,string rootPath, MediaMergeRequest request,string outputVideoPath,string fileNamefolder,bool mergeImg2Viedo,bool downImage=false,bool downMp3=false) + //{ + + // 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",cookie); + // if (!string.IsNullOrEmpty(error)) + // { + // Serilog.Log.Error($"{error}"); + // return false; + // } + // else + // { + // if(downImage) + // { + // for (int i = 0; i < rawImages.Length; 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); + // if (destPath.Contains("小可爱") || sourcePath.Contains("小可爱")) { + // Console.WriteLine("发现小可爱图片"); + // } + // if (!File.Exists(destPath)) + // File.Copy(sourcePath, destPath); + // } + // } + // } + // // 2. 下载音频 + // var (rawAudios, audioError) = await DownloadMediaAsync(request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie); + // if (!string.IsNullOrEmpty(audioError)) + // { + // Serilog.Log.Error($"{audioError}"); + // return false; + // } + // else + // { + // if(downMp3) + // { + // for (int i = 0; i < rawAudios.Length; 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); + // if (!File.Exists(destPath)) + // File.Copy(sourcePath, destPath); + // } + // } + // } + // if (!mergeImg2Viedo) + // { + // // 不合成视频,直接返回成功 + // Serilog.Log.Debug($"不合成视频,直接返回"); + // return true; + // } + + // // 4. 合成视频 + // //var outputVideoPath = Path.Combine(tempDir, "output", $"merged-video.{request.OutputFormat.ToLower()}"); + + // // 2. 创建帮助类实例 + // // 在Docker容器内,FFmpeg通常在PATH中,所以直接用 "ffmpeg" 即可 + + // // 根据图片数量调整每张图片显示时长 + // if (request.ImageUrls.Count <= 3) + // { + // request.ImageDurationPerSecond = 3; + // } + // 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(p => + // { + // Console.WriteLine($"进度: {p:F2}%"); + // }); + + // // 5. 执行合成任务 + // using (var cancellationTokenSource = new CancellationTokenSource()) + // { + // string resultPath = await _fFmpegHelper.CreateVideoFromImagesAndAudioAsync( + // rawImages, + // rawAudios[0], + // outputVideoPath, + // request.VideoWidth, + // request.VideoHeight, + // progress, + // cancellationTokenSource.Token); + + // //Console.WriteLine($"视频合成成功!文件已保存至: {resultPath}"); + // Serilog.Log.Debug($"视频合成成功!文件已保存至: {resultPath}"); + // } + // } + // finally + // { + // // 清理临时目录(无论成功失败) + // if (Directory.Exists(tempDir)) + // { + // Directory.Delete(tempDir, recursive: true); + // } + // } + // return true; + // } + // catch (Exception ex) + // { + // Serilog.Log.Error($"{ex.StackTrace}"); + // return false; + // } + //} + + + /// + /// 视频合成(优化后:解决 FFmpeg 进程冲突,支持容错重试) + /// + public async Task MergeToVideo(string cookie, string rootPath, MediaMergeRequest request, + string outputVideoPath, string fileNamefolder, bool mergeImg2Viedo, bool downImage = false, bool downMp3 = false) + { + // 输入参数校验(避免无效执行) + if (request == null || request.ImageUrls == null || request.ImageUrls.Count == 0) + { + Log.Error("图片URL列表为空,无法合成视频"); + return false; + } + if (mergeImg2Viedo && (request.AudioUrls == null || request.AudioUrls.Count == 0)) + { + Log.Error("合成视频时音频URL列表为空"); + return false; + } + + string tempDir = null; + try + { + // 创建唯一临时目录(避免并发冲突) + tempDir = Path.Combine(rootPath, "temp", Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); // 确保目录存在 + + // 1. 下载图片 + var (rawImages, imageError) = await DownloadMediaAsync( + request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp", cookie); + if (!string.IsNullOrEmpty(imageError) || rawImages == null || rawImages.Length == 0) + { + Log.Error($"图片下载失败:{imageError ?? "未下载到任何图片"}"); + return false; + } + // 保存下载的图片(如果需要) + if (downImage) + { + await SaveDownloadedFilesAsync(rawImages, fileNamefolder, "temp_", "jpg"); + } + + // 2. 下载音频 + string[] rawAudios = Array.Empty(); + if (mergeImg2Viedo) + { + var (audios, audioError) = await DownloadMediaAsync( + request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie); + if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Length == 0) + { + Log.Error($"音频下载失败:{audioError ?? "未下载到任何音频"}"); + return false; + } + rawAudios = audios; + // 保存下载的音频(如果需要) + if (downMp3) + { + await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, "temp_", "mp3"); + } + } + + // 不合成视频,直接返回成功 + if (!mergeImg2Viedo) + { + Log.Debug("不合成视频,直接返回成功"); + return true; + } + + // 3. 调整图片显示时长 + AdjustImageDuration(request); + + // 4. 容错重试:处理 FFmpeg 进程冲突等临时错误 + bool mergeSuccess = await RetryOnFfmpegConflictAsync(async () => + { + // 并发控制:等待前一个 FFmpeg 进程完成 + await _ffmpegSemaphore.WaitAsync(); + try + { + // 每次合成创建独立的 FFmpegHelper 实例(避免状态共享) + var ffmpegHelper = new FFmpegHelper(); + // 配置视频参数(独立实例,无共享冲突) + ffmpegHelper.VideoWidth = request.VideoWidth > 0 ? request.VideoWidth : 1080; + ffmpegHelper.VideoHeight = request.VideoHeight > 0 ? request.VideoHeight : 1920; + ffmpegHelper.ImageDisplayDurationSeconds = request.ImageDurationPerSecond; + ffmpegHelper.OutputFrameRate = 30; + + // 进度回调 + var progress = new Progress(p => + { + Log.Debug($"视频合成进度:{p:F2}%"); + }); + + // 超时控制:避免 FFmpeg 进程无限运行(300秒=5分钟) + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300)); + // 执行合成(确保每个图片都参与) + string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync( + rawImages, // 所有下载的图片(确保无遗漏) + rawAudios[0], // 取第一个音频(可根据需求调整) + outputVideoPath, + ffmpegHelper.VideoWidth, + ffmpegHelper.VideoHeight, + progress, + cts.Token); + + Log.Debug($"视频合成成功!文件路径:{resultPath}"); + return !string.IsNullOrEmpty(resultPath) && File.Exists(resultPath); + } + finally + { + // 释放信号量,允许下一个任务执行 + _ffmpegSemaphore.Release(); + } + }); + + return mergeSuccess; + } + catch (Exception ex) + { + Log.Error(ex, "视频合成过程中发生未处理异常"); + return false; + } + finally + { + // 安全清理临时目录(确保 FFmpeg 进程已释放文件句柄) + await SafeCleanTempDirAsync(tempDir); + } + } + + + /// + /// 保存下载的文件(图片/音频)到目标目录 + /// + private async Task SaveDownloadedFilesAsync(string[] sourcePaths, string targetFolder, string fileNamePrefix, string defaultExt) + { + if (sourcePaths == null || sourcePaths.Length == 0) return; + Directory.CreateDirectory(targetFolder); // 确保目标目录存在 + + // 异步保存(不阻塞主线程) + await Task.WhenAll(sourcePaths.Select(async (sourcePath, index) => + { + if (!File.Exists(sourcePath)) return; + string extension = Path.GetExtension(sourcePath) ?? $".{defaultExt}"; + string destFileName = $"{fileNamePrefix}{index + 1:D3}{extension}"; + string destPath = Path.Combine(targetFolder, destFileName); + + // 避免文件覆盖,同时确保文件名唯一 + if (File.Exists(destPath)) + { + destFileName = $"{fileNamePrefix}{index + 1:D3}_{Guid.NewGuid().ToString("N")}{extension}"; + destPath = Path.Combine(targetFolder, destFileName); + } + + // 复制文件(异步避免阻塞) + await Task.Run(() => File.Copy(sourcePath, destPath, overwrite: false)); + Log.Debug($"已保存文件:{destPath}"); + })); + } + + /// + /// FFmpeg 进程冲突时重试 + /// + private async Task RetryOnFfmpegConflictAsync(Func> action) + { + int retryCount = 0; + while (retryCount < RetryCount) + { + try + { + return await action(); + } + catch (Exception ex) + { + // 识别 FFmpeg 进程冲突相关异常(根据实际异常信息调整条件) + if (ex.Message.Contains("FFmpeg 进程正在运行") || + ex.Message.Contains("进程已在运行") || + ex.Message.Contains("文件被另一个进程占用")) + { + retryCount++; + Log.Warning($"FFmpeg 进程冲突,第 {retryCount}/{RetryCount} 次重试... 异常信息:{ex.Message}"); + await Task.Delay(RetryDelay * retryCount); // 重试间隔递增 + continue; + } + // 非进程冲突异常,直接抛出 + throw; + } + } + Log.Error($"FFmpeg 进程冲突,重试 {RetryCount} 次后仍失败"); + return false; + } + + + + /// + /// 安全清理临时目录(避免文件被占用) + /// + private async Task SafeCleanTempDirAsync(string tempDir) + { + if (string.IsNullOrEmpty(tempDir) || !Directory.Exists(tempDir)) + return; + + try + { + // 延迟清理:给 FFmpeg 进程足够时间释放文件句柄(1秒) + await Task.Delay(1000); + Directory.Delete(tempDir, recursive: true); + Log.Debug($"临时目录已清理:{tempDir}"); + } + catch (Exception ex) + { + // 清理失败时备份目录,避免占用磁盘空间 + string backupDir = $"{tempDir}_backup_{Guid.NewGuid().ToString("N")}"; + Directory.Move(tempDir, backupDir); + Log.Error(ex, $"临时目录清理失败,已备份至:{backupDir}"); + } + } + /// + /// 调整图片显示时长 + /// + private void AdjustImageDuration(MediaMergeRequest request) + { + if (request.ImageUrls.Count <= 3) + request.ImageDurationPerSecond = 3; + else if (request.ImageUrls.Count > 20) + request.ImageDurationPerSecond = 2; + // 中间数量保持原有配置 + } + + + /// 通用媒体下载方法 + private async Task<(string[] SuccessPaths, string ErrorMsg)> DownloadMediaAsync( + List urls, string saveDir, string prefix, string ext,string cookie) + { + var successPaths = new List(); + 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 douyinHttpClientService.DownloadAsync(url, savePath, cookie); + successPaths.Add(savePath); + //Console.WriteLine($"下载成功:{url} → {savePath}"); + } + catch (Exception ex) + { + var error = $"下载失败:{url},错误:{ex.Message}"; + Serilog.Log.Error(error); + return (Array.Empty(), error); + } + } + return (successPaths.ToArray(), null); + } + + } +} diff --git a/service/DouyinQuartzJobService.cs b/service/DouyinQuartzJobService.cs index cd8431b..3b9b1dd 100644 --- a/service/DouyinQuartzJobService.cs +++ b/service/DouyinQuartzJobService.cs @@ -1,201 +1,303 @@ -using Quartz; +using dy.net.dto; using dy.net.job; +using Quartz; +using Serilog; +using System; +using System.Threading.Tasks; namespace dy.net.service { + /// + /// 抖音相关定时任务服务 + /// public class DouyinQuartzJobService { private readonly ISchedulerFactory _schedulerFactory; + private const string DefaultJobGroup = "group1"; + private const int DefaultIntervalMinutes = 30; + private const int DefaultCronStartDelaySeconds = 30; + private const int DefaultSimpleStartDelaySeconds = 3; + + public DouyinQuartzJobService(ISchedulerFactory schedulerFactory) { - _schedulerFactory = schedulerFactory; + _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory)); } /// - /// + /// 启动所有抖音相关定时任务 /// - /// - /// - /// - public async Task StartJob(string expression,int delay=5000) + /// Cron表达式或间隔分钟数 + /// 任务之间的启动延迟(毫秒) + /// 是否启动成功 + public async Task InitOrReStartAllJobs(string expression, int delayBetweenJobs = 5000) { - await StartCollectJob(expression); - - await Task.Delay(delay); - //如果是数字则加1分钟,减少并发 - if (int.TryParse(expression, out int cron)) + if (string.IsNullOrWhiteSpace(expression)) { - cron++; - expression = cron.ToString(); + Log.Warning("定时任务表达式为空,使用默认配置"); + expression = DefaultIntervalMinutes.ToString(); } - await StartFavoriteJob(expression); - await Task.Delay(delay); - if (int.TryParse(expression, out int cron2)) + // 按顺序启动任务,避免并发 + var jobTasks = new List> { - cron2++; - expression = cron2.ToString(); - } - await StartUperPostJob(expression); + //我收藏的作品 + StartJobAsync("collect", expression), + //我喜欢的作品 + DelayAndStartJobAsync("favorite", expression, delayBetweenJobs), + //关注的用户的作品 + DelayAndStartJobAsync("uper", expression, delayBetweenJobs * 2), + //关注列表 + DelayAndStartJobAsync("follow_user", expression, delayBetweenJobs * 3) + }; + + var results = await Task.WhenAll(jobTasks); + return results.All(success => success); } - private async Task StartCollectJob(string expression) + /// + /// 启动关注同步任务(单次执行) + /// + /// 是否启动成功 + public async Task StartFollowJobOnceAsync() { + return await StartOneTimeJobAsync("follow_user_once"); + } + + + /// + /// 延迟后启动任务 + /// + private async Task DelayAndStartJobAsync(string jobKey, string expression, int delayMs) + { + if (delayMs > 0) + { + await Task.Delay(delayMs); + } + + // 如果是数字表达式,自动递增避免并发 + var adjustedExpression = AdjustExpressionForConcurrency(jobKey, expression); + return await StartJobAsync(jobKey, adjustedExpression); + } + + /// + /// 调整任务表达式以避免并发 + /// + private string AdjustExpressionForConcurrency(string jobKey, string expression) + { + if (!int.TryParse(expression, out int interval)) + return expression; + + // 根据任务类型递增间隔,避免所有任务同时执行 + var jobIndex = _jobConfigs.Keys.ToList().IndexOf(jobKey); + return (interval + jobIndex).ToString(); + } + + /// + /// 启动指定定时任务 + /// + private async Task StartJobAsync(string configKey, string expression) + { + if (!_jobConfigs.TryGetValue(configKey, out var jobConfig)) + { + Log.Error("找不到任务配置: {ConfigKey}", configKey); + return false; + } + try { - var __scheduler1 = await _schedulerFactory.GetScheduler(); - var jobKey = new JobKey("dy.job.key.collect", "group1"); - var triggerKey = new TriggerKey("dy.trigger.key.collect", "group1"); + var scheduler = await _schedulerFactory.GetScheduler(); + var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup); + var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup); - var jobDetail = await __scheduler1.GetJobDetail(jobKey); - if (jobDetail != null) + // 删除已存在的任务 + await RemoveExistingJobAsync(scheduler, jobKey); + + // 创建任务详情 + var jobDetail = JobBuilder.Create(jobConfig.JobType) + .WithIdentity(jobKey) + .WithDescription(jobConfig.Description) + .Build(); + + // 创建触发器 + var trigger = CreateTrigger(triggerKey, expression, jobConfig.Description); + if (trigger == null) { - //await __scheduler1.Shutdown(); - await __scheduler1.DeleteJob(jobKey);//删掉原来的 + Log.Error("创建触发器失败: {JobDescription}", jobConfig.Description); + return false; } - IJobDetail job = JobBuilder.Create() - .WithIdentity(jobKey) - .Build(); - ITrigger trigger; - //var expression = Appsettings.Get("Interval"); - if (CronExpression.IsValidExpression(expression)) - { - trigger = TriggerBuilder.Create() - .WithIdentity(triggerKey) - .WithCronSchedule(expression) - .StartAt(DateTime.Now.AddSeconds(30)) - .StartNow() - .Build(); - } - else - { - int Interval = int.TryParse(expression, out int _Interval) ? _Interval : 30; - trigger = TriggerBuilder.Create() - .WithIdentity(triggerKey) - //.StartNow() - .StartAt(DateTime.Now.AddSeconds(3)) - .WithSimpleSchedule(x => x - .WithIntervalInMinutes(Interval) - .RepeatForever()) - .Build(); - } - // Tell Quartz to schedule the job using our trigger - await __scheduler1.ScheduleJob(job, trigger); + // 调度任务 + await scheduler.ScheduleJob(jobDetail, trigger); + Log.Information("启动定时任务成功 - {JobDescription}, 表达式: {Expression}", + jobConfig.Description, expression); + + return true; } catch (Exception ex) { - Serilog.Log.Error("start dy.collect job error", ex); + Log.Error(ex, "启动定时任务失败 - {JobDescription}", jobConfig.Description); return false; } - return true; } - - private async Task StartFavoriteJob(string expression) + /// + /// 启动单次执行任务 + /// + private async Task StartOneTimeJobAsync(string configKey) { + if (!_jobConfigs.TryGetValue(configKey, out var jobConfig)) + { + Log.Error("找不到任务配置: {ConfigKey}", configKey); + return false; + } + try { - var __scheduler1 = await _schedulerFactory.GetScheduler(); - var jobKey = new JobKey("dy.job.key.favorite", "group1"); - var triggerKey = new TriggerKey("dy.trigger.key.favorite", "group1"); + var scheduler = await _schedulerFactory.GetScheduler(); + var jobKey = new JobKey(jobConfig.JobKey, DefaultJobGroup); + var triggerKey = new TriggerKey(jobConfig.TriggerKey, DefaultJobGroup); - var jobDetail = await __scheduler1.GetJobDetail(jobKey); - if (jobDetail != null) - { - //await __scheduler1.Shutdown(); - await __scheduler1.DeleteJob(jobKey);//删掉原来的 - } + // 删除已存在的任务 + await RemoveExistingJobAsync(scheduler, jobKey); - IJobDetail job = JobBuilder.Create() - .WithIdentity(jobKey) - .Build(); - ITrigger trigger; - //var expression = Appsettings.Get("Interval"); - if (CronExpression.IsValidExpression(expression)) - { - trigger = TriggerBuilder.Create() - .WithIdentity(triggerKey) - .WithCronSchedule(expression) - .StartAt(DateTime.Now.AddSeconds(30)) - .StartNow() - .Build(); - } - else - { - int Interval = int.TryParse(expression, out int _Interval) ? _Interval : 30; - trigger = TriggerBuilder.Create() - .WithIdentity(triggerKey) - //.StartNow() - .StartAt(DateTime.Now.AddSeconds(3)) - .WithSimpleSchedule(x => x - .WithIntervalInMinutes(Interval) - .RepeatForever()) - .Build(); - } - // Tell Quartz to schedule the job using our trigger - await __scheduler1.ScheduleJob(job, trigger); + // 创建任务详情 + var jobDetail = JobBuilder.Create(jobConfig.JobType) + .WithIdentity(jobKey) + .WithDescription(jobConfig.Description) + .Build(); + + // 创建立即执行的触发器(只执行一次) + var trigger = TriggerBuilder.Create() + .WithIdentity(triggerKey) + .WithDescription($"{jobConfig.Description} - 单次执行") + .StartNow() + .Build(); + + // 调度任务 + await scheduler.ScheduleJob(jobDetail, trigger); + Log.Information("启动单次任务成功 - {JobDescription}", jobConfig.Description); + + return true; } catch (Exception ex) { - Serilog.Log.Error("start dy.favorite job error", ex); + Log.Error(ex, "启动单次任务失败 - {JobDescription}", jobConfig.Description); return false; } - return true; } - private async Task StartUperPostJob(string expression) + /// + /// 创建触发器(支持Cron表达式和简单间隔) + /// + private ITrigger? CreateTrigger(TriggerKey triggerKey, string expression, string jobDescription) { - try + // Cron表达式格式 + if (CronExpression.IsValidExpression(expression)) { - var __scheduler1 = await _schedulerFactory.GetScheduler(); - var jobKey = new JobKey("dy.job.key.uper", "group1"); - var triggerKey = new TriggerKey("dy.trigger.key.uper", "group1"); + return TriggerBuilder.Create() + .WithIdentity(triggerKey) + .WithDescription($"{jobDescription} - Cron调度") + .WithCronSchedule(expression) + .StartAt(DateTime.Now.AddSeconds(DefaultCronStartDelaySeconds)) + .Build(); + } - var jobDetail = await __scheduler1.GetJobDetail(jobKey); - if (jobDetail != null) - { - //await __scheduler1.Shutdown(); - await __scheduler1.DeleteJob(jobKey);//删掉原来的 - } + // 数字间隔格式(分钟) + if (int.TryParse(expression, out int intervalMinutes)) + { + intervalMinutes = Math.Max(1, intervalMinutes); // 最小间隔1分钟 + return TriggerBuilder.Create() + .WithIdentity(triggerKey) + .WithDescription($"{jobDescription} - 间隔{intervalMinutes}分钟") + .StartAt(DateTime.Now.AddSeconds(DefaultSimpleStartDelaySeconds)) + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(intervalMinutes) + .RepeatForever()) + .Build(); + } - IJobDetail job = JobBuilder.Create() - .WithIdentity(jobKey) - .Build(); - ITrigger trigger; - //var expression = Appsettings.Get("Interval"); - if (CronExpression.IsValidExpression(expression)) - { - trigger = TriggerBuilder.Create() - .WithIdentity(triggerKey) - .WithCronSchedule(expression) - .StartAt(DateTime.Now.AddSeconds(30)) - .StartNow() - .Build(); - } - else - { - int Interval = int.TryParse(expression, out int _Interval) ? _Interval : 30; - trigger = TriggerBuilder.Create() - .WithIdentity(triggerKey) - //.StartNow() - .StartAt(DateTime.Now.AddSeconds(3)) - .WithSimpleSchedule(x => x - .WithIntervalInMinutes(Interval) + // 无效表达式,使用默认配置 + Log.Warning("无效的任务表达式: {Expression},使用默认间隔{DefaultMinutes}分钟", + expression, DefaultIntervalMinutes); + + return TriggerBuilder.Create() + .WithIdentity(triggerKey) + .WithDescription($"{jobDescription} - 默认间隔调度") + .StartAt(DateTime.Now.AddSeconds(DefaultSimpleStartDelaySeconds)) + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(DefaultIntervalMinutes) .RepeatForever()) - .Build(); - } - // Tell Quartz to schedule the job using our trigger - await __scheduler1.ScheduleJob(job, trigger); - } - catch (Exception ex) - { - Serilog.Log.Error("start dy.uper job error", ex); - return false; - } - return true; + .Build(); } + /// + /// 移除已存在的任务 + /// + private async Task RemoveExistingJobAsync(IScheduler scheduler, JobKey jobKey) + { + if (await scheduler.CheckExists(jobKey)) + { + Log.Information("移除已存在的任务: {JobKey}", jobKey); + await scheduler.DeleteJob(jobKey); + } + } + /// + /// 任务配置信息 + /// + private readonly Dictionary _jobConfigs = new() + { + { + "collect", + new JobConfig( + typeof(DouyinCollectSyncJob), + "dy.job.key.collect", + "dy.trigger.key.collect", + "抖音收藏同步任务") + }, + { + "favorite", + new JobConfig( + typeof(DouyinFavoritSyncJob), + "dy.job.key.favorite", + "dy.trigger.key.favorite", + "抖音点赞同步任务") + }, + { + "uper", + new JobConfig( + typeof(DouyinFollowedViedoSyncJob), + "dy.job.key.uper", + "dy.trigger.key.uper", + "抖音UP主作品同步任务") + }, + { + "follow_user", + new JobConfig( + typeof(DouyinFollowedUsersSyncJob), + "dy.job.key.follow_user", + "dy.trigger.key.follow_user", + "抖音关注同步任务") + }, + { + "follow_user_once", + new JobConfig( + typeof(DouyinFollowedUsersSyncJob), + "dy.job.key.follow_user_once", + "dy.trigger.key.follow_user_once", + "抖音关注同步任务(单次执行)") + } + , + //{ + // "redown_once", + // new JobConfig( + // typeof(DouyinReDownSyncJob), + // "dy.job.key.redown_once", + // "dy.trigger.key.redown_once", + // "抖音重新下载任务(单次执行)") + //} + }; + } -} +} \ No newline at end of file diff --git a/service/DouyinVideoService.cs b/service/DouyinVideoService.cs index 611963b..68f3050 100644 --- a/service/DouyinVideoService.cs +++ b/service/DouyinVideoService.cs @@ -1,4 +1,5 @@ -using dy.net.dto; +using ClockSnowFlake; +using dy.net.dto; using dy.net.model; using dy.net.repository; using dy.net.utils; @@ -11,10 +12,12 @@ namespace dy.net.service { private readonly DouyinVideoRepository _dyCollectVideoRepository; + private readonly DouyinCookieRepository douyinCookieRepository; - public DouyinVideoService(DouyinVideoRepository dyCollectVideoRepository) + public DouyinVideoService(DouyinVideoRepository dyCollectVideoRepository, DouyinCookieRepository douyinCookieRepository) { _dyCollectVideoRepository = dyCollectVideoRepository; + this.douyinCookieRepository = douyinCookieRepository; } @@ -82,7 +85,7 @@ namespace dy.net.service }; if (data.GraphicVideoSize == "0.00") { - if(list.Where(x => x.ViedoType == VideoTypeEnum.ImageVideo).Sum(x => x.FileSize) > 0) + if (list.Where(x => x.ViedoType == VideoTypeEnum.ImageVideo).Sum(x => x.FileSize) > 0) { data.GraphicVideoSize = "<0.01";//避免显示0.00误导用户 } @@ -91,12 +94,24 @@ namespace dy.net.service return data; } - - //分页查询 - - public async Task<(List list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null, string viedoType = null, List? dates = null) + /// + /// + /// + /// + /// + public async Task GetByAwemeId(string awemeId) { - return await _dyCollectVideoRepository.GetPagedAsync(pageIndex, pageSize, tag, author, viedoType, dates); + return await _dyCollectVideoRepository.GetFirstAsync(x => x.AwemeId == awemeId); + } + + /// + /// + /// + /// + /// + public async Task<(List list, int totalCount)> GetPagedAsync(DouyinVideoPageRequestDto dto) + { + return await _dyCollectVideoRepository.GetPagedAsync(dto); } @@ -122,8 +137,155 @@ namespace dy.net.service return await _dyCollectVideoRepository.GetByIdAsync(id); } + /// + /// 重新下载选中的视频 + /// + /// 重新下载请求DTO(包含待处理视频ID列表) + /// 是否执行成功(true=流程执行完成,false=无有效数据或执行失败) + /// DTO或ID列表为空时抛出 + /// 文件操作失败时抛出(可根据业务调整处理方式) + public async Task ReDownloadViedoAsync(ReDownViedoDto dto) + { + // 1. 严格参数校验(避免无效流程) + if (dto == null) + throw new ArgumentNullException(nameof(dto), "重新下载请求DTO不能为空"); + if (dto.Ids == null || !dto.Ids.Any()) + { + Serilog.Log.Error("重新下载视频失败:待处理视频ID列表为空"); + return false; + } + + // 2. 查询有效视频记录(去重+非空校验,避免无效处理) + var videoIds = dto.Ids.Distinct().ToList(); // 去重,减少数据库查询和操作 + var videos = await _dyCollectVideoRepository.GetByIds(videoIds); + if (videos == null || !videos.Any()) + { + Serilog.Log.Debug("未查询到有效视频记录:Ids={0}", string.Join(",", videoIds)); + return false; + } + + // 3. 构建重新下载记录(提前准备数据,避免事务内耗时操作) + var reDownList = new List(); + var filePathsToDelete = new List(); // 收集待删除文件路径,统一处理 + + foreach (var video in videos) + { + // 跳过无保存路径的视频(避免无效文件操作) + if (string.IsNullOrWhiteSpace(video.VideoSavePath)) + { + Serilog.Log.Debug("视频无保存路径,跳过文件删除:VideoId={0}", video.Id); + continue; + } + + // 构建重新下载记录 + reDownList.Add(new ViedoReDown + { + Id = IdGener.GetLong().ToString(), + CreateTime = DateTime.UtcNow, // 统一使用UTC时间,避免时区问题 + Status = 0, // 0=待下载(建议用枚举替代魔法值) + SavePath = video.VideoSavePath, + ViedoId = video.AwemeId, + CookieId = video.CookieId + }); + + filePathsToDelete.Add(video.VideoSavePath); + } + + // 无有效重新下载记录时直接返回 + if (!reDownList.Any()) + { + Serilog.Log.Debug("无有效重新下载记录需要创建:VideoIds={0}", string.Join(",", videoIds)); + return false; + } + + try + { + // 4. 数据库操作(事务保证一致性:创建重新下载记录 + 删除原视频记录必须同时成功/失败) + var transactionResult = await _dyCollectVideoRepository.UseTranAsync(async () => + { + // 4.1 批量插入重新下载记录(SqlSugar批量插入效率更高) + _dyCollectVideoRepository.InsertReDowns(reDownList); + // 4.2 批量删除原视频记录(使用视频实际存在的ID,避免无效删除) + var actualDeleteIds = videos.Select(v => v.Id).ToList(); + var deleteCount = await _dyCollectVideoRepository.DeleteByIdsAsync(actualDeleteIds); // 建议仓储层提供异步删除方法 + }, e => + { + Serilog.Log.Error(e, "数据库事务执行失败:Ids={0}", string.Join(",", videoIds)); + }); + + // 5. 文件删除(非事务操作,失败不回滚数据库,可根据业务调整) + // 采用异步文件操作,避免同步IO阻塞线程(需.NET 5+支持) + foreach (var path in filePathsToDelete) + { + try + { + if (File.Exists(path)) + { + File.Delete(path); // 异步删除,提升并发性能 + Serilog.Log.Debug("视频文件删除成功:Path={0}", path); + } + else + { + Serilog.Log.Error("视频文件不存在,跳过删除:Path={0}", path); + } + } + catch (IOException ex) + { + Serilog.Log.Error(ex, "视频文件删除失败:Path={0}", path); + } + } + + var CookieIds = reDownList.Select(x => x.CookieId).Distinct(); + foreach (var ck in CookieIds) + { + var cookie= douyinCookieRepository.GetById(ck); + if (cookie == null) + continue; + var viedoTypes = videos.Where(x => x.CookieId == ck).Select(x => x.ViedoType).Distinct(); + + if(viedoTypes!=null&& viedoTypes.Any()) + { + foreach (VideoTypeEnum item in viedoTypes) + { + switch (item) + { + case VideoTypeEnum.Favorite: + cookie.FavHasSyncd = 0; + break; + case VideoTypeEnum.Collect: + cookie.CollHasSyncd = 0; + break; + case VideoTypeEnum.UperPost: + cookie.UperSyncd = 0; + break; + case VideoTypeEnum.ImageVideo: + break; + default: + break; + } + } + } + await douyinCookieRepository.UpdateAsync(cookie); + + } + Serilog.Log.Debug("重新下载视频流程执行完成:成功创建{0}条重新下载记录,删除{1}个文件,等待重新下载...", reDownList.Count, filePathsToDelete.Count); + return true; + } + catch (Exception ex) + { + Serilog.Log.Error(ex, "重新下载视频执行失败:Ids={0}", string.Join(",", videoIds)); + return false; + } + } + + + /// + /// 获取待重新下载的视频列表 + /// + /// + public async Task> GetViedoReDowns() + { + return await _dyCollectVideoRepository.GetViedoReDowns(); + } } - - - } diff --git a/utils/ABogus.cs b/utils/ABogus.cs index ef7d495..8ea66e9 100644 --- a/utils/ABogus.cs +++ b/utils/ABogus.cs @@ -205,7 +205,7 @@ namespace dy.net.utils return Sm3ToArray(Sm3ToArray(method + _endString)); } - // ... 其他成员和初始化代码 ... + public int[] Sm3ToArray(string data) { diff --git a/utils/ABogusNew.cs b/utils/ABogusNew.cs new file mode 100644 index 0000000..d68dd01 --- /dev/null +++ b/utils/ABogusNew.cs @@ -0,0 +1,535 @@ +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; +using System.Text; + +namespace dy.net.utils +{ + public static class StringProcessor + { + /// + /// 将字符串转换为字符数组 (ASCII) + /// + public static int[] ToOrdArray(string s) + { + return s.Select(c => (int)c).ToArray(); + } + + /// + /// 将整数数组转回字符串 + /// + public static string ToCharStr(int[] arr) + { + return new string(arr.Select(i => (char)i).ToArray()); + } + + /// + /// JavaScript 无符号右移操作 (>>>) + /// + public static int JsShiftRight(int value, int n) + { + uint uValue = (uint)value; + return (int)(uValue >> n); + } + + /// + /// 生成伪随机混淆字节字符串 (长度为 length * 4) + /// + private static Random _random = new Random(); + public static string GenerateRandomBytes(int length = 3) + { + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < length; i++) + { + int rd = _random.Next(10000); + + result.Append((char)(((rd & 255) & 170) | 1)); + result.Append((char)(((rd & 255) & 85) | 2)); + result.Append((char)((JsShiftRight(rd, 8) & 170) | 5)); + result.Append((char)((JsShiftRight(rd, 8) & 85) | 40)); + } + + return result.ToString(); + } + } + + public class CryptoUtility + { + public string Salt { get; set; } + public List Base64Alphabet { get; set; } + + private readonly int[] _bigArray = { + 121, 243, 55, 234, 103, 36, 47, 228, 30, 231, 106, 6, 115, 95, 78, 101, + 250, 207, 198, 50, 139, 227, 220, 105, 97, 143, 34, 28, 194, 215, 18, 100, + 159, 160, 43, 8, 169, 217, 180, 120, 247, 45, 90, 11, 27, 197, 46, 3, + 84, 72, 5, 68, 62, 56, 221, 75, 144, 79, 73, 161, 178, 81, 64, 187, + 134, 117, 186, 118, 16, 241, 130, 71, 89, 147, 122, 129, 65, 40, 88, 150, + 110, 219, 199, 255, 181, 254, 48, 4, 195, 248, 208, 32, 116, 167, 69, 201, + 17, 124, 125, 104, 96, 83, 80, 127, 236, 108, 154, 126, 204, 15, 20, 135, + 112, 158, 13, 1, 188, 164, 210, 237, 222, 98, 212, 77, 253, 42, 170, 202, + 26, 22, 29, 182, 251, 10, 173, 152, 58, 138, 54, 141, 185, 33, 157, 31, + 252, 132, 233, 235, 102, 196, 191, 223, 240, 148, 39, 123, 92, 82, 128, 109, + 57, 24, 38, 113, 209, 245, 2, 119, 153, 229, 189, 214, 230, 174, 232, 63, + 52, 205, 86, 140, 66, 175, 111, 171, 246, 133, 238, 193, 99, 60, 74, 91, + 225, 51, 76, 37, 145, 211, 166, 151, 213, 206, 0, 200, 244, 176, 218, 44, + 184, 172, 49, 216, 93, 168, 53, 21, 183, 41, 67, 85, 224, 155, 226, 242, + 87, 177, 146, 70, 190, 12, 162, 19, 137, 114, 25, 165, 163, 192, 23, 59, + 9, 94, 179, 107, 35, 7, 142, 131, 239, 203, 149, 136, 61, 249, 14, 156 + }; + + public CryptoUtility(string salt, List base64Alphabet) + { + Salt = salt; + Base64Alphabet = base64Alphabet; + } + + /// + /// 计算 SM3 哈希并返回 byte 数组(即整数列表) + /// + public static byte[] Sm3Hash(byte[] input) + { + var digest = new SM3Digest(); + digest.BlockUpdate(input, 0, input.Length); + byte[] output = new byte[digest.GetDigestSize()]; + digest.DoFinal(output, 0); + return output; + } + + /// + /// 对输入数据计算 SM3 哈希,并返回整数数组 + /// + public int[] Sm3ToArray(object input) + { + byte[] bytes; + + if (input is string str) + { + bytes = Encoding.UTF8.GetBytes(str); + } + else if (input is int[] arr) + { + bytes = arr.Select(b => (byte)b).ToArray(); + } + else + { + throw new ArgumentException("Input must be string or int[]"); + } + + byte[] hash = Sm3Hash(bytes); + return hash.Select(b => (int)b).ToArray(); + } + + /// + /// 添加盐值 + /// + public string AddSalt(string param) + { + return param + Salt; + } + + /// + /// 处理参数(可选加盐) + /// + public object ProcessParam(object param, bool addSalt) + { + if (param is string s && addSalt) + { + return AddSalt(s); + } + return param; + } + + /// + /// 获取参数哈希数组(双重哈希) + /// + public int[] ParamsToArray(object param, bool addSalt = true) + { + var processed = ProcessParam(param, addSalt); + var firstHash = Sm3ToArray(processed); + return Sm3ToArray(firstHash); + } + + /// + /// RC4 加密 + /// + public static byte[] Rc4Encrypt(byte[] key, string plaintext) + { + byte[] data = Encoding.UTF8.GetBytes(plaintext); + var rc4 = new RC4Engine(); + rc4.Init(true, new KeyParameter(key)); + + byte[] output = new byte[data.Length]; + rc4.ProcessBytes(data, 0, data.Length, output, 0); + return output; + } + + /// + /// 自定义 Base64 编码 + /// + public string Base64Encode(string input, int selectedAlphabet = 0) + { + string alphabet = Base64Alphabet[selectedAlphabet]; + var binary = new StringBuilder(); + + foreach (char c in input) + { + binary.Append(Convert.ToString(c, 2).PadLeft(8, '0')); + } + + while (binary.Length % 6 != 0) + { + binary.Append('0'); + } + + var chunks = new List(); + for (int i = 0; i < binary.Length; i += 6) + { + string chunk = binary.ToString(i, Math.Min(6, binary.Length - i)); + chunks.Add(Convert.ToInt32(chunk, 2)); + } + + var output = new StringBuilder(); + foreach (int index in chunks) + { + output.Append(alphabet[index]); + } + + // Padding + int padding = (6 - (binary.Length % 6)) % 6; + output.Append('=', padding / 2); + + return output.ToString(); + } + + /// + /// ABogus 自定义编码逻辑(类似Base64但不同分组) + /// + public string AbogusEncode(string input, int selectedAlphabet) + { + var abogus = new List(); + string alphabet = Base64Alphabet[selectedAlphabet]; + + for (int i = 0; i < input.Length; i += 3) + { + int n = 0; + if (i + 2 < input.Length) + { + n = (input[i] << 16) | (input[i + 1] << 8) | input[i + 2]; + } + else if (i + 1 < input.Length) + { + n = (input[i] << 16) | (input[i + 1] << 8); + } + else + { + n = input[i] << 16; + } + + int[] masks = { 0xFC0000, 0x03F000, 0x0FC0, 0x3F }; + int[] shifts = { 18, 12, 6, 0 }; + + for (int j = 0; j < 4; j++) + { + if ((j == 2 && i + 1 >= input.Length) || (j == 3 && i + 2 >= input.Length)) + break; + int val = (n & masks[j]) >> shifts[j]; + abogus.Add(alphabet[val]); + } + } + + while (abogus.Count % 4 != 0) + { + abogus.Add('='); + } + + return new string(abogus.ToArray()); + } + + /// + /// 字节数组变换加密(RC4-like 流密码) + /// + public string TransformBytes(int[] bytesList) + { + string bytesStr = StringProcessor.ToCharStr(bytesList); + var result = new List(); + + int indexB = _bigArray[1]; + int initialValue = 0; + int valueE = 0; + + for (int i = 0; i < bytesStr.Length; i++) + { + char ch = bytesStr[i]; + int charValue = ch; + + if (i == 0) + { + initialValue = _bigArray[indexB]; + int sumInitial = indexB + initialValue; + + _bigArray[1] = initialValue; + _bigArray[indexB] = indexB; + } + else + { + int sumInitial = initialValue + valueE; + sumInitial %= _bigArray.Length; + valueE = _bigArray[(i + 2) % _bigArray.Length]; + sumInitial = (indexB + valueE) % _bigArray.Length; + initialValue = _bigArray[sumInitial]; + } + + int sumInitialFinal = (indexB + (i == 0 ? initialValue : valueE)) % _bigArray.Length; + int valueF = _bigArray[sumInitialFinal]; + int encryptedChar = charValue ^ valueF; + result.Add((char)encryptedChar); + + // 更新状态 + valueE = _bigArray[(i + 2) % _bigArray.Length]; + sumInitialFinal = (indexB + valueE) % _bigArray.Length; + int temp = _bigArray[sumInitialFinal]; + _bigArray[sumInitialFinal] = _bigArray[(i + 2) % _bigArray.Length]; + _bigArray[(i + 2) % _bigArray.Length] = temp; + indexB = sumInitialFinal; + } + + return new string(result.ToArray()); + } + } + + public class BrowserFingerprintGenerator + { + private static Random _random = new Random(); + + public static string GenerateFingerprint(string browserType = "Edge") + { + return browserType switch + { + "Chrome" => _GenerateFingerprint("Win32"), + "Firefox" => _GenerateFingerprint("Win32"), + "Safari" => _GenerateFingerprint("MacIntel"), + "Edge" => _GenerateFingerprint("Win32"), + _ => _GenerateFingerprint("Win32") + }; + } + + private static string _GenerateFingerprint(string platform) + { + int innerWidth = _random.Next(1024, 1921); + int innerHeight = _random.Next(768, 1081); + int outerWidth = innerWidth + _random.Next(24, 33); + int outerHeight = innerHeight + _random.Next(75, 91); + int screenX = 0; + int screenY = _random.Next(2) == 0 ? 0 : 30; + int sizeWidth = _random.Next(1024, 1921); + int sizeHeight = _random.Next(768, 1081); + int availWidth = _random.Next(1280, 1921); + int availHeight = _random.Next(800, 1081); + + return $"{innerWidth}|{innerHeight}|{outerWidth}|{outerHeight}|" + + $"{screenX}|{screenY}|0|0|{sizeWidth}|{sizeHeight}|" + + $"{availWidth}|{availHeight}|{innerWidth}|{innerHeight}|24|24|{platform}"; + } + } + + public class ABogus2 + { + private int aid = 6383; + private int pageId = 0; + private string salt = "cus"; + private bool boe = false; + private double ddrt = 8.5; + private double ic = 8.5; + private List paths = new() { + "^/webcast/", "^/aweme/v1/", "^/aweme/v2/", "/v1/message/send", "^/live/", "^/captcha/", "^/ecom/" + }; + + private byte[] uaKey = { 0x00, 0x01, 0x0E }; + + private string character = "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"; + private string character2 = "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe"; + + private List characterList; + private CryptoUtility cryptoUtility; + + private string userAgent; + private string browserFp; + + private int[] sortIndex = { + 18, 20, 52, 26, 30, 34, 58, 38, 40, 53, 42, 21, 27, 54, 55, 31, 35, 57, 39, 41, 43, 22, 28, + 32, 60, 36, 23, 29, 33, 37, 44, 45, 59, 46, 47, 48, 49, 50, 24, 25, 65, 66, 70, 71 + }; + + private int[] sortIndex2 = { + 18, 20, 26, 30, 34, 38, 40, 42, 21, 27, 31, 35, 39, 41, 43, 22, 28, 32, 36, 23, 29, 33, 37, + 44, 45, 46, 47, 48, 49, 50, 24, 25, 52, 53, 54, 55, 57, 58, 59, 60, 65, 66, 70, 71 + }; + + public List Options { get; set; } = new() { 0, 1, 14 }; // POST 默认 + + public ABogus2(string fp = "", string userAgent = "", List options = null) + { + if (options != null) Options = options; + + this.userAgent = !string.IsNullOrEmpty(userAgent) + ? userAgent + : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"; + + this.browserFp = !string.IsNullOrEmpty(fp) + ? fp + : BrowserFingerprintGenerator.GenerateFingerprint("Edge"); + + characterList = new List { character, character2 }; + cryptoUtility = new CryptoUtility(salt, characterList); + } + + public string EncodeData(string data, int alphabetIndex = 0) + { + return cryptoUtility.AbogusEncode(data, alphabetIndex); + } + + public (string paramsWithAbogus, string abogus, string userAgent, string body) GenerateAbogus(string paramsStr, string body = "") + { + var abDir = new Dictionary + { + { 8, 3 }, + { 15, new { + aid = this.aid, + pageId = this.pageId, + boe = this.boe, + ddrt = this.ddrt, + paths = this.paths, + track = new { mode = 0, delay = 300, paths = new List() }, + dump = true, + rpU = "" + }}, + { 18, 44 }, + { 19, new[] { 1, 0, 1, 0, 1 } }, + { 66, 0 }, + { 69, 0 }, + { 70, 0 }, + { 71, 0 } + }; + + long startEncryption = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + int[] array1 = cryptoUtility.ParamsToArray(paramsStr); // 双重哈希 + int[] array2 = body != "" ? cryptoUtility.ParamsToArray(body) : new int[0]; + string encodedUa = cryptoUtility.Base64Encode( + StringProcessor.ToCharStr( + Array.ConvertAll(CryptoUtility.Rc4Encrypt(uaKey, userAgent), b => (int)b) + ), + 1 + ); + + int[] array3 = cryptoUtility.Sm3ToArray(encodedUa); // 不加盐 + + long endEncryption = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + // 插入时间戳高位 + abDir[20] = (byte)((startEncryption >> 24) & 0xFF); + abDir[21] = (byte)((startEncryption >> 16) & 0xFF); + abDir[22] = (byte)((startEncryption >> 8) & 0xFF); + abDir[23] = (byte)(startEncryption & 0xFF); + abDir[24] = (int)((startEncryption >> 32) & 0xFF); + abDir[25] = (int)((startEncryption >> 40) & 0xFF); + + // 请求选项 + abDir[26] = (byte)((Options[0] >> 24) & 0xFF); + abDir[27] = (byte)((Options[0] >> 16) & 0xFF); + abDir[28] = (byte)((Options[0] >> 8) & 0xFF); + abDir[29] = (byte)(Options[0] & 0xFF); + + abDir[30] = (byte)((Options[1] >> 8) & 0xFF); + abDir[31] = (byte)(Options[1] & 0xFF); + abDir[32] = (byte)((Options[1] >> 24) & 0xFF); + abDir[33] = (byte)((Options[1] >> 16) & 0xFF); + + abDir[34] = (byte)((Options[2] >> 24) & 0xFF); + abDir[35] = (byte)((Options[2] >> 16) & 0xFF); + abDir[36] = (byte)((Options[2] >> 8) & 0xFF); + abDir[37] = (byte)(Options[2] & 0xFF); + + abDir[38] = array1[21]; + abDir[39] = array1[22]; + abDir[40] = array2.Length > 21 ? array2[21] : 0; + abDir[41] = array2.Length > 22 ? array2[22] : 0; + abDir[42] = array3.Length > 23 ? array3[23] : 0; + abDir[43] = array3.Length > 24 ? array3[24] : 0; + + abDir[44] = (byte)((endEncryption >> 24) & 0xFF); + abDir[45] = (byte)((endEncryption >> 16) & 0xFF); + abDir[46] = (byte)((endEncryption >> 8) & 0xFF); + abDir[47] = (byte)(endEncryption & 0xFF); + abDir[48] = abDir[8]; + abDir[49] = (int)((endEncryption >> 32) & 0xFF); + abDir[50] = (int)((endEncryption >> 40) & 0xFF); + + abDir[51] = (byte)((pageId >> 24) & 0xFF); + abDir[52] = (byte)((pageId >> 16) & 0xFF); + abDir[53] = (byte)((pageId >> 8) & 0xFF); + abDir[54] = (byte)(pageId & 0xFF); + abDir[55] = pageId; + abDir[56] = aid; + abDir[57] = (byte)(aid & 0xFF); + abDir[58] = (byte)((aid >> 8) & 0xFF); + abDir[59] = (byte)((aid >> 16) & 0xFF); + abDir[60] = (byte)((aid >> 24) & 0xFF); + + abDir[64] = browserFp.Length; + abDir[65] = browserFp.Length; + + // 排序取值 + var sortedValues = sortIndex + .Select(k => Convert.ToInt32(abDir.GetValueOrDefault(k, 0))) + .ToList(); + + var fpArray = StringProcessor.ToOrdArray(browserFp).ToList(); + + int abXor = 0; + abXor = sortIndex2 + .Select(k => Convert.ToInt32(abDir.GetValueOrDefault(k, 0))) + .Aggregate(0, (x, y) => x ^ y); + + + sortedValues.AddRange(fpArray); + sortedValues.Add(abXor); + + string randomBytes = StringProcessor.GenerateRandomBytes(); + string transformed = cryptoUtility.TransformBytes(sortedValues.ToArray()); + + string abogusBytesStr = randomBytes + transformed; + string abogus = cryptoUtility.AbogusEncode(abogusBytesStr, 0); + + string finalParams = $"{paramsStr}&a_bogus={abogus}"; + + return (finalParams, abogus, userAgent, body); + } + } + + // 测试代码 + //public class ABogusTest + //{ + // //public static void Main() + // //{ + // // string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"; + // // string edgeFp = BrowserFingerprintGenerator.GenerateFingerprint("Edge"); + // // var abogus = new ABogus2(fp: edgeFp, userAgent: userAgent); + + // // // GET请求测试 + // // string getParams = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAArDVBosPJF3eIWVEFp0szuJ-e1V_-rK0ieJeWwpE77E8&max_cursor=0&locate_query=false&show_live_replay_strategy=1&need_time_list=1&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&from_user_page=1&update_version_code=170400&pc_client_type=1&pc_libra_divert=Windows&support_h265=1&support_dash=0&version_code=290100&version_name=29.1.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=131.0.0.0&browser_online=true&engine_name=Blink&engine_version=131.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50"; + // // var getResult = abogus.GenerateABogus(getParams); + // // Console.WriteLine($"GET 完整URL: https://www.douyin.com/aweme/v1/web/aweme/detail/?{getResult.Params}"); + // // Console.WriteLine($"GET ABogus: {getResult.ABogus}"); + + // // // POST请求测试 + // // string postParams = "device_platform=webapp&aid=6383&channel=channel_pc_web&pc_client_type=1&pc_libra_divert=Windows&update_version_code=170400&support_h265=1&support_dash=0&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=131.0.0.0&browser_online=true&engine_name=Blink&engine_version=131.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50"; + // // string postBody = "aweme_type=0&item_id=7467485482314763572&play_delta=1&source=0"; + // // var postResult = abogus.GenerateABogus(postParams, postBody); + // // Console.WriteLine($"POST 完整URL: https://www.douyin.com/aweme/v2/web/aweme/stats/?{postResult.Params}"); + // // Console.WriteLine($"POST ABogus: {postResult.ABogus}"); + // // Console.WriteLine($"POST Body: {postResult.Body}"); + // //} + //} +} + diff --git a/utils/DouyinBaseParamDics.cs b/utils/DouyinBaseParamDics.cs index c60834f..49bcf92 100644 --- a/utils/DouyinBaseParamDics.cs +++ b/utils/DouyinBaseParamDics.cs @@ -1,18 +1,95 @@ -using System.Threading.Channels; +using System.Collections.Generic; namespace dy.net.utils { - public class DouyinBaseParamDics + /// + /// 抖音网页端请求参数字典管理类 + /// 提供各类接口的标准化参数字典,对外暴露的属性名称和返回类型保持不变 + /// + public static class DouyinBaseParamDics { - public static Dictionary CollectParams { get; } = InitializeUserCollecParams(); + /// + /// 收藏列表参数(用户收藏的内容) + /// + public static Dictionary CollectParams { get; } = InitializeUserCollectParams(); + + /// + /// 用户收藏参数(与 CollectParams 功能区分,保留原始定义) + /// public static Dictionary FavoriteParams { get; } = InitializeUserFavoriteParams(); + + /// + /// 博主发布作品参数 + /// public static Dictionary UpderPostParams { get; } = InitializeUpderPostParams(); - private static Dictionary InitializeUserCollecParams() - { - var parameters = GetDyBaseParameters(); + /// + /// 我关注的博主列表参数 + /// + public static Dictionary MyFollowParams { get; } = InitializeMyFollowParams(); - // 覆盖与基础字段不同的值 + /// + /// 视频详情 + /// + public static Dictionary ViedoDetailParam { get; } = InitializeViedoDetailParam(); + + /// + /// 抖音作品列表请求参数(适配接口:https://www.douyin.com/aweme/v1/web/aweme/post/) + /// + public static Dictionary InitializeDouyinPostParams() + { + // 基于基础参数扩展,减少冗余 + var parameters = GetBaseParameters(); + + // 覆盖基础参数中不同的值 + parameters["version_code"] = "290100"; + parameters["version_name"] = "29.1.0"; + parameters["screen_width"] = "1707"; + parameters["screen_height"] = "1067"; + parameters["browser_name"] = "Chrome"; + parameters["browser_version"] = "142.0.0.0"; + parameters["engine_version"] = "142.0.0.0"; + parameters["cpu_core_num"] = "32"; + parameters["support_h265"] = "1"; + parameters["support_dash"] = "1"; + + // 添加特有参数 + parameters.AddRange(new Dictionary + { + {"sec_user_id", ""}, + {"max_cursor", "0"}, + {"locate_item_id", "7576282367263807451"}, + {"locate_query", "false"}, + {"count", "18"}, + {"show_live_replay_strategy", "1"}, + {"need_time_list", "1"}, + {"time_list_query", "0"}, + {"publish_video_strategy_type", "2"}, + {"from_user_page", "1"}, + {"webid", "7574080345697584675"}, + {"uifid", ""}, + {"msToken", ""}, + {"a_bogus", ""}, + {"verifyFp", ""}, + {"fp", ""}, + {"x-secsdk-web-expire", ""}, + {"x-secsdk-web-signature", ""}, + {"cut_version", "1"} + }); + + return parameters; + } + + #region 私有初始化方法 + + /// + /// 初始化收藏列表参数 + /// + private static Dictionary InitializeUserCollectParams() + { + var parameters = GetBaseParameters(); + + // 覆盖基础参数 parameters["version_code"] = "290100"; parameters["version_name"] = "29.1.0"; parameters["screen_width"] = "1920"; @@ -21,22 +98,27 @@ namespace dy.net.utils parameters["engine_version"] = "130.0.0.0"; parameters["cpu_core_num"] = "12"; - // 添加当前字段特有的键值对 - parameters["from_user_page"] = "1"; - parameters["locate_query"] = "false"; - parameters["need_time_list"] = "1"; - parameters["show_live_replay_strategy"] = "1"; - parameters["time_list_query"] = "0"; + // 添加特有参数 + parameters.AddRange(new Dictionary + { + {"from_user_page", "1"}, + {"locate_query", "false"}, + {"need_time_list", "1"}, + {"show_live_replay_strategy", "1"}, + {"time_list_query", "0"} + }); return parameters; } - // 初始化用户收藏参数 + /// + /// 初始化用户收藏参数 + /// private static Dictionary InitializeUserFavoriteParams() { - var parameters = GetDyBaseParameters(); + var parameters = GetBaseParameters(); - // 覆盖与基础字段不同的值 + // 覆盖基础参数 parameters["version_code"] = "170400"; parameters["version_name"] = "17.4.0"; parameters["screen_width"] = "1536"; @@ -44,154 +126,121 @@ namespace dy.net.utils parameters["browser_version"] = "140.0.0.0"; parameters["engine_version"] = "140.0.0.0"; parameters["cpu_core_num"] = "20"; - - // 添加当前字段特有的键值对 - parameters["min_cursor"] = "0"; - parameters["cut_version"] = "1"; - parameters["count"] = "18"; parameters["support_h265"] = "1"; parameters["support_dash"] = "1"; + // 添加特有参数 + parameters.AddRange(new Dictionary + { + {"min_cursor", "0"}, + {"cut_version", "1"}, + {"count", "18"} + }); + return parameters; } - // 初始化抖音博主发布作品参数 - private static Dictionary InitializeUpderPostParams() - { - var parameters = new Dictionary - { - { "WebIdLastTime", "1714385892" }, - { "aid", "1988" }, - { "app_language", "zh-Hans" }, - { "app_name", "tiktok_web" }, - { "browser_language", "zh-CN" }, - { "browser_name", "Mozilla" }, - { "browser_online", "true" }, - { "browser_platform", "Win32" }, - { "browser_version", "5.0%20%28Windows%29" }, - { "cookie_enabled", "true" }, - { "count", "18" }, - { "coverFormat", "2" }, - { "cursor", "0" }, - { "data_collection_enabled", "true" }, - { "device_id", "7380187414842836523" }, - { "device_platform", "webapp" }, - { "channel", "channel_pc_web" }, - { "focus_state", "true" }, - { "from_page", "user" }, - { "history_len", "3" }, - { "is_fullscreen", "false" }, - { "is_page_visible", "true" }, - { "language", "zh-Hans" }, - { "locate_item_id", "" }, - { "needPinnedItemIds", "true" }, - { "odinId", "7404669909585003563" }, - { "os", "windows" }, - { "post_item_list_request_type", "0" }, - { "priority_region", "US" }, - { "referer", "" }, - { "region", "US" }, - { "screen_height", "827" }, - { "screen_width", "1323" }, - { "secUid", "" }, - { "tz_name", "America%2FLos_Angeles" }, - { "user_is_login", "true" }, - { "webcast_language", "zh-Hans" }, - { "msToken", "" }, - { "_signature", "_02B4Z6wo000017oyWOQAAIDD9xNhTSnfaDu6MFxAAIlj23" }, - {"sec_user_id",""} - }; - return parameters; - } - - /// - /// 初始化抖音网页端用户作品列表请求参数(参数来源于目标URL) - /// 适配接口:https://www.douyin.com/aweme/v1/web/aweme/post/ + /// 初始化博主发布作品参数 /// - /// 抖音作品列表请求参数字典 - public static Dictionary InitializeDouyinPostParams() + private static Dictionary InitializeUpderPostParams() { - var parameters = new Dictionary + // 该接口参数差异较大,单独初始化(保留原始参数完整) + return new Dictionary + { + {"WebIdLastTime", "1714385892"}, + {"aid", "1988"}, + {"app_language", "zh-Hans"}, + {"app_name", "tiktok_web"}, + {"browser_language", "zh-CN"}, + {"browser_name", "Mozilla"}, + {"browser_online", "true"}, + {"browser_platform", "Win32"}, + {"browser_version", "5.0%20%28Windows%29"}, + {"cookie_enabled", "true"}, + {"count", "18"}, + {"coverFormat", "2"}, + {"cursor", "0"}, + {"data_collection_enabled", "true"}, + {"device_id", "7380187414842836523"}, + {"device_platform", "webapp"}, + {"channel", "channel_pc_web"}, + {"focus_state", "true"}, + {"from_page", "user"}, + {"history_len", "3"}, + {"is_fullscreen", "false"}, + {"is_page_visible", "true"}, + {"language", "zh-Hans"}, + {"locate_item_id", ""}, + {"needPinnedItemIds", "true"}, + {"odinId", "7404669909585003563"}, + {"os", "windows"}, + {"post_item_list_request_type", "0"}, + {"priority_region", "US"}, + {"referer", ""}, + {"region", "US"}, + {"screen_height", "827"}, + {"screen_width", "1323"}, + {"secUid", ""}, + {"sec_user_id", ""}, + {"tz_name", "America%2FLos_Angeles"}, + {"user_is_login", "true"}, + {"webcast_language", "zh-Hans"}, + {"msToken", ""} + }; + } + + /// + /// 初始化我关注的博主列表参数 + /// + private static Dictionary InitializeMyFollowParams() { - // 基础设备与渠道参数 - { "device_platform", "webapp" }, - { "aid", "6383" }, - { "channel", "channel_pc_web" }, - - // 用户标识参数 - { "sec_user_id", "" }, - - // 分页与内容定位参数 - { "max_cursor", "0" }, - { "locate_item_id", "7576282367263807451" }, - { "locate_query", "false" }, - { "count", "18" }, - - // 视频相关配置参数 - { "show_live_replay_strategy", "1" }, - { "need_time_list", "1" }, - { "time_list_query", "0" }, - { "publish_video_strategy_type", "2" }, - { "support_h265", "1" }, - { "support_dash", "1" }, - - // 版本与更新参数 - { "from_user_page", "1" }, - { "update_version_code", "170400" }, - { "version_code", "290100" }, - { "version_name", "29.1.0" }, - - // PC端特有参数 - { "pc_client_type", "1" }, - { "pc_libra_divert", "Windows" }, - { "cpu_core_num", "32" }, - - // 浏览器环境参数 - { "browser_language", "zh-CN" }, - { "browser_platform", "Win32" }, - { "browser_name", "Chrome" }, - { "browser_version", "142.0.0.0" }, - { "browser_online", "true" }, - { "engine_name", "Blink" }, - { "engine_version", "142.0.0.0" }, - - // 系统环境参数 - { "os_name", "Windows" }, - { "os_version", "10" }, - { "device_memory", "8" }, - { "platform", "PC" }, - { "cookie_enabled", "true" }, - - // 屏幕与网络参数 - { "screen_width", "1707" }, - { "screen_height", "1067" }, - { "downlink", "10" }, - { "effective_type", "4g" }, - { "round_trip_time", "0" }, - - // 用户唯一标识参数 - { "webid", "7574080345697584675" }, - { "uifid", "" }, - - // 安全验证与签名参数 - { "msToken", "" }, - { "a_bogus", "" }, - { "verifyFp", "" }, - { "fp", "" }, - { "x-secsdk-web-expire", "" }, - { "x-secsdk-web-signature", "" }, - - // 其他辅助参数 - { "whale_cut_token", "" }, - { "cut_version", "1" } - }; + var parameters = GetBaseParameters(); + + // 覆盖基础参数 + parameters["version_code"] = "170400"; + parameters["version_name"] = "17.4.0"; + parameters["screen_width"] = "1707"; + parameters["screen_height"] = "1067"; + parameters["browser_name"] = "Edge"; + parameters["browser_version"] = "141.0.0.0"; + parameters["engine_version"] = "141.0.0.0"; + parameters["cpu_core_num"] = "32"; + parameters["support_h265"] = "1"; + parameters["support_dash"] = "1"; + + // 添加特有参数 + parameters.AddRange(new Dictionary + { + {"user_id", "1218695735550247"}, + {"sec_user_id", ""}, + {"offset", "40"}, + {"min_time", "0"}, + {"max_time", "0"}, + {"count", "20"}, + {"source_type", "4"}, + {"gps_access", "0"}, + {"address_book_access", "0"}, + {"is_top", "1"}, + {"webid", "7577203855940994560"}, + {"uifid", ""}, + {"msToken", ""}, + {"a_bogus", ""}, + {"verifyFp", ""}, + {"fp", ""} + }); return parameters; } - // 静态基础参数(供内部初始化使用) - private static Dictionary GetDyBaseParameters() + #endregion + + #region 基础参数与扩展方法 + + /// + /// 获取抖音网页端基础参数字典(所有接口的公共参数) + /// + private static Dictionary GetBaseParameters() { return new Dictionary { @@ -199,6 +248,7 @@ namespace dy.net.utils {"aid", "6383"}, {"channel", "channel_pc_web"}, {"pc_client_type", "1"}, + {"pc_libra_divert", "Windows"}, {"cookie_enabled", "true"}, {"browser_language", "zh-CN"}, {"browser_platform", "Win32"}, @@ -211,13 +261,77 @@ namespace dy.net.utils {"platform", "PC"}, {"downlink", "10"}, {"effective_type", "4g"}, - {"pc_libra_divert", "Windows"}, - {"publish_video_strategy_type", "2"}, {"round_trip_time", "0"}, - {"whale_cut_token", ""}, - {"update_version_code", "170400"} + {"update_version_code", "170400"}, + {"whale_cut_token", ""} }; } + + private static Dictionary InitializeViedoDetailParam() + { + + Dictionary requestParams = new Dictionary +{ + { "device_platform", "webapp" }, + { "aid", "6383" }, + { "channel", "channel_pc_web" }, + { "aweme_id", "" }, + { "request_source", "600" }, + { "origin_type", "video_page" }, + { "update_version_code", "170400" }, + { "pc_client_type", "1" }, + { "pc_libra_divert", "Windows" }, + { "support_h265", "1" }, + { "support_dash", "1" }, + { "cpu_core_num", "32" }, + { "version_code", "190500" }, + { "version_name", "19.5.0" }, + { "cookie_enabled", "true" }, + { "screen_width", "1707" }, + { "screen_height", "1067" }, + { "browser_language", "zh-CN" }, + { "browser_platform", "Win32" }, + { "browser_name", "Edge" }, + { "browser_version", "141.0.0.0" }, + { "browser_online", "true" }, + { "engine_name", "Blink" }, + { "engine_version", "141.0.0.0" }, + { "os_name", "Windows" }, + { "os_version", "10" }, + { "device_memory", "8" }, + { "platform", "PC" }, + { "downlink", "10" }, + { "effective_type", "4g" }, + { "round_trip_time", "0" }, + { "webid", "7577203855940994560" }, + { "uifid", "0e81ba593d64ebaca259bdbe302de8d7e55ac2e982f7412f10fbc5c77c64bb8b8830ffed112ea8b3bc54f3b6e2af3856daafa4efaafef18953612820da493aeaebdeac0537b99b8e68f343c2476cf6347f7751a87d92952128116470f147215cf46b949f43f31aedcb660fd3c5c7eb2145183cc93d1b4202205b4af7d7c69ab55e860f96e315899a2ee74a262273694ec973ba682bb6fdc351e0e66250b21cf9aef3ccaa3e0c28b085fd095e947d92a5336b9e65706d649a7b79541feab3487f" }, + { "verifyFp", "verify_migsr5je_BJ1YiVbY_uR2U_4VXu_8Uje_GgwhVf7Y6hb8" }, + { "fp", "verify_migsr5je_BJ1YiVbY_uR2U_4VXu_8Uje_GgwhVf7Y6hb8" }, + //{ "msToken", "zVKqVOb-uBF-Opse1B5y9u0hw7SlRa49w8HB56cVSY0Uymjqs17JJ9qOUXv7IK0j3de1ErBjSVAm0JSd5SH1sYg7jeriVvGNJoDUM3dKwVCrqlapZgU9g2aYAeHs5oHddTGtZ3Pri9MIRltXVRdirZWMewfH8MlqmIVaCiTuGzRcZ6va9aWe0CyR" }, + //{ "a_bogus", "dy45kFWjOxRfOdFtmOnc9WxlY8L%2FNTuy6Pi2SYAP9PKGcwFcaWNpBNCfrxLuRUd%2FzuBzhe3HqdlMYDnc0zX0ZenkKmkkupv6Bt%2FC9L0LZZHvbBJZ7rgiemSxzk4O8KsOmAIbiM75AsBEIxo5VrCwAdlCu%2F-xBbmD%2Fp3vVATCE2ysUAujwn%2FVa-JDNw7qaf%3D%3D" }, + //{ "x-secsdk-web-expire", "1764378577130" }, + //{ "x-secsdk-web-signature", "3b8aa32e9457ebe3b65afdae11e2fe86" } +}; + return requestParams; + } + + + /// + /// 字典扩展方法:批量添加键值对(避免重复 Add 调用) + /// + private static void AddRange(this Dictionary target, Dictionary source) + { + foreach (var item in source) + { + // 避免键重复(如果有重复以源字典为准) + if (target.ContainsKey(item.Key)) + target[item.Key] = item.Value; + else + target.Add(item.Key, item.Value); + } + } + + #endregion } } \ No newline at end of file diff --git a/utils/TikTokFileNameHelper.cs b/utils/DouyinFileNameHelper.cs similarity index 81% rename from utils/TikTokFileNameHelper.cs rename to utils/DouyinFileNameHelper.cs index 0e7fa0e..28cd7dd 100644 --- a/utils/TikTokFileNameHelper.cs +++ b/utils/DouyinFileNameHelper.cs @@ -8,7 +8,7 @@ namespace dy.net.utils /// /// 抖音标题转文件名工具类(兼容Windows/macOS/Linux) /// - public static class TikTokFileNameHelper + public static class DouyinFileNameHelper { #region 配置参数 /// @@ -153,9 +153,9 @@ namespace dy.net.utils { path = path.Replace(c, '_'); } - if (path.Length > 50) + if (path.Length > 100) { - path = path.Substring(0, 50); + path = path.Substring(0, 100); } return path.Trim().Replace(" ", ""); } @@ -164,5 +164,31 @@ namespace dy.net.utils return path; } } + + + /// + /// 检查字符串是否仅包含字母、数字、简体中文(无特殊字符) + /// + /// 待检查的字符串 + /// true:无特殊字符(仅允许字符);false:含有特殊字符 + public static bool IsValidWithoutSpecialChars(string input) + { + // 空字符串默认返回 true(若需禁止空字符串,可先判断 string.IsNullOrWhiteSpace(input) 并返回 false) + if (string.IsNullOrEmpty(input)) + return true; + + // 正则表达式说明: + // ^ :匹配字符串开头 + // $ :匹配字符串结尾 + // [a-zA-Z0-9\u4E00-\u9FA5] :允许的字符范围 + // a-zA-Z:大小写字母 + // 0-9:数字 + // \u4E00-\u9FA5:简体中文 Unicode 核心范围(覆盖99%+简体中文常用字) + // * :匹配 0 个或多个允许的字符(若需至少1个字符,可改为 +) + const string pattern = @"^[a-zA-Z0-9\u4E00-\u9FA5]*$"; + + // 忽略文化差异,仅按字符编码匹配 + return Regex.IsMatch(input, pattern, RegexOptions.None); + } } } diff --git a/utils/FFmpegHelper.cs b/utils/FFmpegHelper.cs new file mode 100644 index 0000000..cdb499f --- /dev/null +++ b/utils/FFmpegHelper.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace dy.net.utils +{ + public class FFmpegHelper : IDisposable + { + #if DEBUG + // Debug 环境,通常是 Windows + private readonly string _ffmpegExecutablePath = "E:\\down\\ffmpeg\\bin\\ffmpeg.exe"; + private readonly string _ffprobeExecutablePath = "E:\\down\\ffmpeg\\bin\\ffprobe.exe"; + #else + // Release 环境,通常是 Docker Linux + private readonly string _ffmpegExecutablePath = "ffmpeg"; + private readonly string _ffprobeExecutablePath = "ffprobe"; + #endif + + + 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"; + + + /// + /// 将多张图片和一个音频文件合成为视频(最终终极版)。 + /// + public async Task CreateVideoFromImagesAndAudioAsync( + IEnumerable imageFilePaths, + string audioFilePath, + string outputVideoPath, + int VideoWidth = 1080, + int Height = 1920, + IProgress 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]; + string extension = Path.GetExtension(sourcePath); + string destFileName = $"temp_{i + 1:D3}{extension}"; + string destPath = Path.Combine(tempImageDir, destFileName); + File.Copy(sourcePath, destPath); + } + + string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList[0])); + double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2); + + // --- 修正点 1: 整合音频时长获取逻辑 --- + // 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环 + double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长 + try + { + // 使用 ffprobe 获取音频时长 + 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 + }; + + using (var process = new Process { StartInfo = startInfo }) + { + process.Start(); + string output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); + + if (double.TryParse(output, out double duration)) + { + audioDurationSeconds = duration; + //Console.WriteLine($"成功获取音频时长: {audioDurationSeconds:F2}s"); + } + } + } + catch (Exception ex) + { + Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}"); + } + + // 计算图片序列需要循环的次数 + int loopCount = 1; + double singleLoopDuration = imageList.Count * ImageDisplayDurationSeconds; + if (audioDurationSeconds > singleLoopDuration) + { + loopCount = (int)Math.Ceiling(audioDurationSeconds / singleLoopDuration); + //Console.WriteLine($"图片序列将循环 {loopCount} 次以匹配音频时长"); + } + + // --- 修正点 2: 重新构建 FFmpeg 参数列表 --- + //var arguments = new List + //{ + // "-y", // 覆盖输出文件 + + // // --- 所有输入文件放在最前面 --- + // // 图片序列输入 + // "-f", "image2", + // "-r", imageFps.ToString(CultureInfo.InvariantCulture), + // "-i", $"\"{imageSequencePattern}\"", + + // // 音频输入 + // "-i", $"\"{audioFilePath}\"", + + // // --- 修正点 3: 使用 filter_complex 对视频流进行循环 --- + // // [0:v] 表示第一个输入(图片序列)的视频流 + // // loop={loopCount-1} 表示循环 (次数-1) 次 + // // [v] 是处理后的视频流的别名 + // "-filter_complex", $"\"[0:v]loop={loopCount - 1}[v]\"", + + // // --- 修正点 4: 明确映射输出流 --- + // // 将处理后的视频流 [v] 映射到输出 + // "-map", "\"[v]\"", + // // 将第二个输入(音频文件)的音频流映射到输出 + // "-map", "\"1:a\"", + + // // --- 视频编码配置 --- + // "-c:v", VideoCodec, + // "-preset", VideoPreset, + // "-crf", $"{VideoCrf}", + // "-s", $"{VideoWidth}x{VideoHeight}", + // "-pix_fmt", "yuv420p", + + // // --- 音频编码配置 --- + // "-c:a", AudioCodec, + // "-b:a", $"{AudioBitrate}", + + // // --- 修正点 5: 使用 -shortest 参数 --- + // // 确保视频和音频同时结束,即使循环次数计算得不完全精确 + // "-shortest", + + // // --- 输出文件 --- + // $"\"{outputVideoPath}\"" + //}; + + //AI优化后。。20251129 + var arguments = new List + { + "-y", // 覆盖输出文件 + + // 图片序列输入:恢复你原有的 -r,移除可能冲突的参数 + "-f", "image2", + "-r", imageFps.ToString(CultureInfo.InvariantCulture), // 保留你原本的帧率参数 + "-start_number", "0", // 仅增加:避免序列编号问题(不影响原有逻辑) + "-i", imageSequencePattern, // 关键修复:去掉引号(原代码的引号是核心失败原因) + + // 音频输入:同样去掉引号 + "-i", audioFilePath, + + // 滤镜修复:恢复简单写法,避免复杂参数(适配旧版 FFmpeg) + "-filter_complex", $"[0:v]loop={loopCount - 1}[v]", // 去掉额外参数,和你原逻辑一致 + + // 流映射:去掉引号,恢复简单写法 + "-map", "[v]", + "-map", "1:a", + + // 视频编码:保留你的变量,只增加兼容性参数 + "-c:v", VideoCodec, // 保留你原本的编码变量(之前能跑通,说明该编码支持) + "-preset", VideoPreset, // 保留原变量 + "-crf", $"{VideoCrf}", // 保留原变量 + "-s", $"{VideoWidth}x{VideoHeight}", + "-pix_fmt", "yuv420p", // 仅增加:兼容性关键(不影响原有逻辑) + "-profile:v", "main", // 仅增加:适配多数播放器(不冲突) + + // 音频编码:保留你的变量,修复可能的无效值 + "-c:a", AudioCodec, // 保留原变量 + "-b:a", $"{AudioBitrate}", // 保留原变量 + "-ac", "2", // 仅增加:避免单声道兼容问题 + "-ar", "44100", // 仅增加:标准化采样率 + + // 封装优化:仅增加关键参数,不影响原有逻辑 + "-f", "mp4", // 明确格式(之前可能自动识别,现在显式声明更稳定) + "-movflags", "+faststart", // 仅增加:解决部分播放器无法播放 + + // 保留你原有的同步参数 + "-shortest", + + // 输出路径:去掉引号(核心修复点) + 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 + { + // 清理临时目录 + if (Directory.Exists(tempImageDir)) + { + Directory.Delete(tempImageDir, recursive: true); + } + } + } + + + + /// + /// 异步执行FFmpeg命令 + /// + private async Task ExecuteFFmpegAsync(string arguments, IProgress 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(); + } + } + +} diff --git a/utils/SystemStaticUtil.cs b/utils/SystemStaticUtil.cs index c7f0bb0..037e0bf 100644 --- a/utils/SystemStaticUtil.cs +++ b/utils/SystemStaticUtil.cs @@ -5,5 +5,12 @@ public static string DOWN_IMAGE_VIDEO_ENABLE="DOWN_IMGVIDEO"; public static string ASPNETCORE_URLS = "ASPNETCORE_URLS"; + + + public static string DY_FOLLOWEDS = "dy_followeds"; + public static string DY_COLLECTS = "dy_collects"; + public static string DY_FAVORITES = "dy_favorites"; + + } } diff --git a/utils/VideoTitleGenerator.cs b/utils/VideoTitleGenerator.cs new file mode 100644 index 0000000..fea1314 --- /dev/null +++ b/utils/VideoTitleGenerator.cs @@ -0,0 +1,81 @@ +using dy.net.dto; +using System.Text.RegularExpressions; + +namespace dy.net.utils +{ + /// + /// 视频标题模板生成器 + /// + public static class VideoTitleGenerator + { + /// + /// 根据用户模板和原始数据生成最终标题 + /// + /// 用户设置的模板(如 "视频_{id}_{VideoTitle}_{ReleaseTime}") + /// 标题所需的原始数据 + /// 时间字段格式化(默认:yyyy-MM-dd HH:mm:ss) + /// 占位符对应数据为空时的替换值(默认:空字符串) + /// 生成的最终标题 + /// 模板为空时抛出 + public static string Generate( + string template, + VideoTitleDataTemplate data, + string timeFormat = "yyyyMMddHHmmss", + string emptyPlaceholder = "") + { + // 校验入参 + if (string.IsNullOrWhiteSpace(template)) + throw new ArgumentNullException(nameof(template), "标题模板不能为空"); + data ??= new VideoTitleDataTemplate(); // 避免数据为空 + + // 1. 定义占位符与数据的映射关系(key:占位符名称,value:格式化后的值) + var placeholderMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // 普通字段 + ["Id"] = data.Id.ToString(), + ["VideoTitle"] = string.IsNullOrWhiteSpace(data.VideoTitle) ? emptyPlaceholder : data.VideoTitle, + ["FileHash"] = string.IsNullOrWhiteSpace(data.FileHash) ? emptyPlaceholder : data.FileHash, + ["Resolution"] = string.IsNullOrWhiteSpace(data.Resolution) ? emptyPlaceholder : data.Resolution, + + // 时间字段(支持空值处理) + //["SyncTime"] = data.SyncTime.HasValue ? data.SyncTime.Value.ToString(timeFormat) : emptyPlaceholder, + ["ReleaseTime"] = data.ReleaseTime.HasValue ? data.ReleaseTime.Value.ToString(timeFormat) : emptyPlaceholder, + + // 文件大小(自动格式化:字节→KB/MB/GB,保留1位小数) + //["FileSize"] = FormatFileSize(data.FileSize) ?? emptyPlaceholder + }; + + // 2. 正则匹配模板中的占位符({占位符名称}),并替换 + var regex = new Regex(@"\{(?[a-zA-Z0-9]+)\}", RegexOptions.Compiled); + var finalTitle = regex.Replace(template, match => + { + var placeholderKey = match.Groups["key"].Value; + // 存在对应映射则替换,否则保留原占位符(避免替换错误) + return placeholderMap.TryGetValue(placeholderKey, out var value) ? value : match.Value; + }); + + return finalTitle; + } + + /// + /// 格式化文件大小(字节→KB/MB/GB) + /// + private static string FormatFileSize(long fileSizeInBytes) + { + if (fileSizeInBytes < 0) return "无效大小"; + if (fileSizeInBytes == 0) return "0B"; + + const long kb = 1024; + const long mb = kb * 1024; + const long gb = mb * 1024; + + return fileSizeInBytes switch + { + < kb => $"{fileSizeInBytes}B", + < mb => $"{fileSizeInBytes / (double)kb:F1}KB", + < gb => $"{fileSizeInBytes / (double)mb:F1}MB", + _ => $"{fileSizeInBytes / (double)gb:F1}GB" + }; + } + } +} diff --git a/utils/XBogus.cs b/utils/XBogus.cs new file mode 100644 index 0000000..7ce40ce --- /dev/null +++ b/utils/XBogus.cs @@ -0,0 +1,290 @@ +using System.Security.Cryptography; +using System.Text; + +namespace dy.net.utils +{ + public class XBogus + { + private readonly int?[] _array; + private readonly string _character; + private readonly byte[] _uaKey = { 0x00, 0x01, 0x0c }; + private readonly string _userAgent; + + public string Params { get; private set; } + public string Xb { get; private set; } + + public XBogus(string userAgent = "") + { + // 初始化 Array 数组(对应 Python 的 self.Array) + _array = new int?[128]; + // 数字 0-9 对应 ASCII 48-57 + for (int i = 48; i <= 57; i++) + _array[i] = i - 48; + // 字母 A-F 对应 ASCII 65-70,映射为 10-15 + for (int i = 65; i <= 70; i++) + _array[i] = i - 55; + // 字母 a-f 对应 ASCII 97-102,映射为 10-15 + for (int i = 97; i <= 102; i++) + _array[i] = i - 87; + + // 字符映射表 + _character = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="; + + // 用户代理,默认值与 Python 一致 + _userAgent = string.IsNullOrEmpty(userAgent) + ? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0" + : userAgent; + } + + /// + /// 将字符串通过 MD5 哈希转换为整数数组 + /// + private int[] Md5StrToArray(string md5Str) + { + if (!string.IsNullOrEmpty(md5Str) && md5Str.Length > 32) + return md5Str.Select(c => (int)c).ToArray(); + + var result = new List(); + for (int i = 0; i < md5Str.Length; i += 2) + { + if (i + 1 >= md5Str.Length) + break; + + int? high = _array[md5Str[i]]; + int? low = _array[md5Str[i + 1]]; + if (high == null || low == null) + result.Add(0); + else + result.Add(((int)high << 4) | (int)low); + } + return result.ToArray(); + } + + /// + /// 多轮 MD5 哈希加密 URL 参数 + /// + private int[] Md5Encrypt(string urlParams) + { + string firstMd5 = Md5(urlParams); + int[] firstArray = Md5StrToArray(firstMd5); + string secondMd5 = Md5(firstArray); + return Md5StrToArray(secondMd5); + } + + /// + /// 计算 MD5 哈希值 + /// + private string Md5(object input) + { + int[] dataArray; + switch (input) + { + case string str: + dataArray = Md5StrToArray(str); + break; + case int[] arr: + dataArray = arr; + break; + default: + throw new ArgumentException("Invalid input type. Expected string or int array."); + } + + using (var md5 = MD5.Create()) + { + byte[] bytes = dataArray.Select(i => (byte)(i & 0xFF)).ToArray(); + byte[] hashBytes = md5.ComputeHash(bytes); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + + /// + /// 第一次编码转换 + /// + private string EncodingConversion( + int a, int b, int c, int e, int d, int t, int f, int r, int n, int o, + int i, int _, int x, int u, int s, int l, int v, int h, int p) + { + var bytes = new byte[] + { + (byte)a, (byte)i, (byte)b, (byte)_ , (byte)c, (byte)x, + (byte)e, (byte)u, (byte)d, (byte)s, (byte)t, (byte)l, + (byte)f, (byte)v, (byte)r, (byte)h, (byte)n, (byte)p, (byte)o + }; + return Encoding.GetEncoding("ISO-8859-1").GetString(bytes); + } + + /// + /// 第二次编码转换 + /// + private string EncodingConversion2(int a, int b, string c) + { + return ((char)a).ToString() + ((char)b).ToString() + c; + } + + /// + /// RC4 加密算法 + /// + private byte[] Rc4Encrypt(byte[] key, byte[] data) + { + int[] S = Enumerable.Range(0, 256).ToArray(); + int j = 0; + + // 初始化 S 盒 + for (int i = 0; i < 256; i++) + { + j = (j + S[i] + key[i % key.Length]) % 256; + (S[i], S[j]) = (S[j], S[i]); + } + + // 生成密文 + var encrypted = new byte[data.Length]; + int i2 = 0, j2 = 0; + for (int k = 0; k < data.Length; k++) + { + i2 = (i2 + 1) % 256; + j2 = (j2 + S[i2]) % 256; + (S[i2], S[j2]) = (S[j2], S[i2]); + int t = (S[i2] + S[j2]) % 256; + encrypted[k] = (byte)(data[k] ^ S[t]); + } + + return encrypted; + } + + /// + /// 位运算计算 + /// + private string Calculation(int a1, int a2, int a3) + { + int x1 = (a1 & 0xFF) << 16; + int x2 = (a2 & 0xFF) << 8; + int x3 = x1 | x2 | (a3 & 0xFF); + + char c1 = _character[(x3 & 0x0FC0000) >> 18]; // 16515072 = 0x0FC0000 + char c2 = _character[(x3 & 0x003F000) >> 12]; // 258048 = 0x003F000 + char c3 = _character[(x3 & 0x0000FC0) >> 6]; // 4032 = 0x0000FC0 + char c4 = _character[x3 & 0x3F]; + + return $"{c1}{c2}{c3}{c4}"; + } + + /// + /// 获取 X-Bogus 值 + /// + public (string Params, string Xb, string UserAgent) GetXBogus(string urlParams) + { + // 计算 array1 + byte[] uaBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(_userAgent); + byte[] rc4Ua = Rc4Encrypt(_uaKey, uaBytes); + string base64Ua = Convert.ToBase64String(rc4Ua); + string md5Ua = Md5(base64Ua); + int[] array1 = Md5StrToArray(md5Ua); + + // 计算 array2(固定 MD5:d41d8cd98f00b204e9800998ecf8427e 是空字符串的 MD5) + int[] array2 = Md5StrToArray(Md5(Md5StrToArray("d41d8cd98f00b204e9800998ecf8427e"))); + + // 计算 URL 参数的 MD5 数组 + int[] urlParamsArray = Md5Encrypt(urlParams); + + // 时间戳和固定值 + long timer = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + int ct = 536919696; + + // 构建 new_array + var newArray = new List + { + 64, 0.00390625, 1, 12, + urlParamsArray.Length > 14 ? urlParamsArray[14] : 0, + urlParamsArray.Length > 15 ? urlParamsArray[15] : 0, + array2.Length > 14 ? array2[14] : 0, + array2.Length > 15 ? array2[15] : 0, + array1.Length > 14 ? array1[14] : 0, + array1.Length > 15 ? array1[15] : 0, + (timer >> 24) & 0xFF, + (timer >> 16) & 0xFF, + (timer >> 8) & 0xFF, + timer & 0xFF, + (ct >> 24) & 0xFF, + (ct >> 16) & 0xFF, + (ct >> 8) & 0xFF, + ct & 0xFF + }; + + // 计算异或结果 + int xorResult = (int)newArray[0]; + for (int i = 1; i < newArray.Count; i++) + { + int b = (int)newArray[i]; + xorResult ^= b; + } + newArray.Add(xorResult); + + // 拆分 array3 和 array4 + var array3 = new List(); + var array4 = new List(); + for (int i = 0; i < newArray.Count; i++) + { + array3.Add((int)newArray[i]); + if (i + 1 < newArray.Count) + array4.Add((int)newArray[i + 1]); + i++; + } + + // 合并数组 + int[] mergeArray = array3.Concat(array4).ToArray(); + + // 生成乱码 + string encoding1 = EncodingConversion( + mergeArray[0], mergeArray[1], mergeArray[2], mergeArray[3], mergeArray[4], + mergeArray[5], mergeArray[6], mergeArray[7], mergeArray[8], mergeArray[9], + mergeArray[10], mergeArray[11], mergeArray[12], mergeArray[13], mergeArray[14], + mergeArray[15], mergeArray[16], mergeArray[17], mergeArray[18] + ); + + byte[] encoding1Bytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(encoding1); + byte[] rc4Key = Encoding.GetEncoding("ISO-8859-1").GetBytes("ÿ"); + byte[] rc4Encrypted = Rc4Encrypt(rc4Key, encoding1Bytes); + string rc4Str = Encoding.GetEncoding("ISO-8859-1").GetString(rc4Encrypted); + + string garbledCode = EncodingConversion2(2, 255, rc4Str); + + // 计算 X-Bogus + StringBuilder xbBuilder = new StringBuilder(); + for (int i = 0; i < garbledCode.Length; i += 3) + { + if (i + 2 >= garbledCode.Length) + break; + + int a = garbledCode[i]; + int b = garbledCode[i + 1]; + int c = garbledCode[i + 2]; + xbBuilder.Append(Calculation(a, b, c)); + } + + // 结果赋值 + Xb = xbBuilder.ToString(); + Params = $"{urlParams}&X-Bogus={Xb}"; + + return (Params, Xb, _userAgent); + } + } + + // 测试代码 + //public class XBogusTest + //{ + // public static void Main() + // { + // string ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"; + // var xb = new XBogus(ua); + + // string dyUrlParams = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAW9FWcqS7RdQAWPd2AA5fL_ilmqsIFUCQ_Iym6Yh9_cUa6ZRqVLjVQSUjlHrfXY1Y&max_cursor=0&locate_query=false&show_live_replay_strategy=1&need_time_list=1&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=122.0.0.0&browser_online=true&engine_name=Blink&engine_version=122.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7335414539335222835&msToken=p9Y7fUBuq9DKvAuN27Peml6JbaMqG2ZcXfFiyDv1jcHrCN00uidYqUgSuLsKl1onC-E_n82m-aKKYE0QGEmxIWZx9iueQ6WLbvzPfqnMk4GBAlQIHcDzxb38FLXXQxAm"; + // string tkUrlParams = "WebIdLastTime=1713796127&abTestVersion=%5Bobject%20Object%5D&aid=1988&appType=t&app_language=zh-Hans&app_name=tiktok_web&browser_name=Mozilla&browser_online=true&browser_platform=Win32&browser_version=5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F123.0.0.0%20Safari%2F537.36&channel=tiktok_web&device_id=7360698239018452498&odinId=7360698115047851026®ion=TW&tz_name=Asia%2FHong_Kong&uniqueId=rei_toy625"; + + // var dyResult = xb.GetXBogus(dyUrlParams); + // Console.WriteLine($"Douyin - URL: {dyResult.Params}, X-Bogus: {dyResult.Xb}, UA: {dyResult.UserAgent}"); + + // var tkResult = xb.GetXBogus(tkUrlParams); + // Console.WriteLine($"TikTok - URL: {tkResult.Params}, X-Bogus: {tkResult.Xb}, UA: {tkResult.UserAgent}"); + // } + //} +}