\ 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