-
+
- 编辑弹幕
+
+
+ !val && (uiStore.uiConfig.medalInfoPanelIsSortMode.light = false)"
+ />
+ 编辑名单
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ !val && (uiStore.uiConfig.medalInfoPanelIsSortMode.like = false)"
+ />
+ 编辑名单
+
+
+
+
+
+
+
+
+
+
+
+ 编辑弹幕
+
+
+
+
+
+
+
+
+
+
+
+
+
+ !val && (uiStore.uiConfig.medalInfoPanelIsSortMode.danmu = false)"
+ />
+ 编辑名单
+
+
+
+
+
+
+
+
-
-
-
- 分钟 / 直播间
@@ -202,21 +300,23 @@ function handleRowClick(row: MedalInfoRow) {
!val && (uiStore.uiConfig.medalInfoPanelSortMode = false)"
+ @change="(val) => !val && (uiStore.uiConfig.medalInfoPanelIsSortMode.watch = false)"
/>
- 编辑名单
+
+
- 直播任务相关信息可在
+ 粉丝团任务相关信息可在
查看。
-
+
+
@@ -252,11 +353,18 @@ function handleRowClick(row: MedalInfoRow) {
新增弹幕
-
+
+
+
-
+
@@ -320,11 +428,11 @@ function handleRowClick(row: MedalInfoRow) {
!val && nextTick(() => initSelection(medalInfoTableData))"
+ @change="(val) => !val && nextTick(() => refreshSelection())"
/>
diff --git a/src/components/RemoveElement.vue b/src/components/RemoveElement.vue
index 2c2d2092..1779849a 100644
--- a/src/components/RemoveElement.vue
+++ b/src/components/RemoveElement.vue
@@ -53,12 +53,6 @@ const config = moduleStore.moduleConfig.RemoveElement
-
-
-
-
-
-
@@ -71,15 +65,6 @@ const config = moduleStore.moduleConfig.RemoveElement
-
-
-
-
-
-
diff --git a/src/components/icons/InfoIcon.vue b/src/components/icons/InfoIcon.vue
index 4fd01846..ac1150bd 100644
--- a/src/components/icons/InfoIcon.vue
+++ b/src/components/icons/InfoIcon.vue
@@ -18,6 +18,7 @@ const open = () => {
lockScroll: false,
autofocus: true,
confirmButtonText: 'OK',
+ customStyle: { '--el-messagebox-width': '28vw' },
}).catch(() => {})
}
diff --git a/src/library/bili-api/api.d.ts b/src/library/bili-api/api.d.ts
index efce3eb8..9d9e0dd8 100644
--- a/src/library/bili-api/api.d.ts
+++ b/src/library/bili-api/api.d.ts
@@ -12,6 +12,10 @@ interface Requests {
interface BapiMethods {
live: {
fansMedalPanel: (page: number, page_size?: number) => Promise
+ getActivatedMedalInfo: (
+ target_id: number,
+ web_location?: string,
+ ) => Promise
sendMsg: (
msg: string,
room_id: number,
diff --git a/src/library/bili-api/data.d.ts b/src/library/bili-api/data.d.ts
index e09edb6d..ca42266f 100644
--- a/src/library/bili-api/data.d.ts
+++ b/src/library/bili-api/data.d.ts
@@ -102,6 +102,48 @@ declare namespace LiveData {
}
}
+ namespace GetActivatedMedalInfo {
+ interface Data {
+ face: string
+ name: string
+ medal_name: string
+ fans_medal_count: number
+ level: number
+ is_lighted: boolean
+ intimacy: number
+ next_intimacy: number
+ task_light_days: number
+ task_info: TaskInfo[]
+ fans_club_gift_info: FansClubGiftInfo
+ medal_color_border: string
+ medal_color: string
+ medal_color_text: string
+ medal_color_level: string
+ guard_level: number
+ light_source: number
+ free_intimacy: number
+ reach_free_intimacy_limit: boolean
+ }
+
+ interface TaskInfo {
+ icon: string
+ title: string
+ sub_title: string
+ add_text: string
+ jump_type: string
+ is_done: boolean
+ icon_guard: string
+ icon_admiral: string
+ icon_captain: string
+ }
+
+ interface FansClubGiftInfo {
+ gift_id: number
+ price: number
+ gift_discount_info: any
+ }
+ }
+
namespace SendMsg {
interface Data {
mode_info: ModeInfo
diff --git a/src/library/bili-api/index.ts b/src/library/bili-api/index.ts
index d2471f67..93f16630 100644
--- a/src/library/bili-api/index.ts
+++ b/src/library/bili-api/index.ts
@@ -22,6 +22,14 @@ const BAPI: BapiMethods = {
page_size,
})
},
+ getActivatedMedalInfo: (target_id, web_location = '444.260') => {
+ const bili_jct = useBiliStore().cookies!.bili_jct
+ return request.live.get('/xlive/app-ucenter/v1/fansMedal/GetActivatedMedalInfo', {
+ csrf: bili_jct,
+ target_id, // 主播 uid
+ web_location,
+ })
+ },
sendMsg: (
msg,
roomid,
diff --git a/src/library/bili-api/response.d.ts b/src/library/bili-api/response.d.ts
index 0bf8b434..24a5d349 100644
--- a/src/library/bili-api/response.d.ts
+++ b/src/library/bili-api/response.d.ts
@@ -8,6 +8,13 @@ declare namespace Live {
data: LiveData.FansMedalPanel.Data
}
+ interface GetActivatedMedalInfo {
+ code: number
+ message: string
+ ttl: number
+ data: LiveData.GetActivatedMedalInfo.Data
+ }
+
interface SendMsg {
code: number
data: LiveData.SendMsg.Data
diff --git a/src/library/cookie/index.ts b/src/library/cookie/index.ts
index 3fd5f6cc..a623f330 100644
--- a/src/library/cookie/index.ts
+++ b/src/library/cookie/index.ts
@@ -2,15 +2,14 @@ class Cookie {
/**
* 获取所有 cookies
*/
- public static getAll(): Record {
- if (document.cookie === '') return {}
-
- const cookies = document.cookie.split('; ')
+ public static async getAll(): Promise> {
+ const cookies = await cookieStore.getAll()
const result: Record = {}
for (const cookie of cookies) {
- const [name, value] = cookie.split('=', 2)
- result[decodeURIComponent(name)] = decodeURIComponent(value ?? '')
+ if (cookie.name) {
+ result[cookie.name] = cookie.value ?? ''
+ }
}
return result
@@ -21,77 +20,96 @@ class Cookie {
* @param names cookie 名称数组
* @param defaultValue 当 cookie 不存在时使用的默认值,默认 undefined
*/
- public static get(names: string[], defaultValue?: string): Record
+ public static async get(
+ names: string[],
+ defaultValue?: string,
+ ): Promise>
/**
* 获取指定名称的一个 cookie
* @param name cookie 名称
* @param defaultValue 当 cookie 不存在时使用的默认值,默认 undefined
*/
- public static get(name: string, defaultValue?: string): string | undefined
+ public static async get(name: string, defaultValue?: string): Promise
/**
* 获取指定名称的一个或多个 cookies
* @param names cookie 名称或 cookie 名称数组
* @param defaultValue 当 cookie 不存在时使用的默认值,默认 undefined
*/
- public static get(names: string[] | string, defaultValue?: string) {
- const cookies = this.getAll()
-
+ public static async get(names: string[] | string, defaultValue?: string) {
if (Array.isArray(names)) {
const result: Record = {}
+ const cookies = await Promise.all(names.map((name) => cookieStore.get(name)))
- for (const name of names) {
- result[name] = cookies[name] ? cookies[name] : defaultValue
+ for (let i = 0; i < names.length; i++) {
+ result[names[i]] = cookies[i]?.value ?? defaultValue
}
return result
} else {
- return cookies[names] ? cookies[names] : defaultValue
+ const cookie = await cookieStore.get(names)
+ return cookie?.value ?? defaultValue
}
}
/**
- * 获取一组 cookies,如果有 cookie 未获取到,会反复获取直到超时为止
+ * 获取一组 cookies
*
- * TODO: 等 cookieStore 普及后使用监听取代轮询
+ * 如果有 cookie 未获取到,会通过监听 cookieStore 的 change 事件等待,直到全部获取或超时
*
* @param names 要获取的 cookie 名称数组
- * @param interval 获取间隔,默认 300 毫秒
* @param timeout 超时时间,若留空则永不超时
*/
public static getAsync(
names: T[],
- interval: number = 300,
timeout?: number,
): Promise> {
return new Promise((resolve, reject) => {
- let remainCookieNames = [...names]
- const cookies: Record = this.get(remainCookieNames)
+ const cookies = {} as Record
+ const nameSet = new Set(names)
+ let timeoutTimer: number | undefined
+
+ const cleanup = () => {
+ if (timeoutTimer !== undefined) clearTimeout(timeoutTimer)
+ cookieStore.removeEventListener('change', onChange)
+ }
- remainCookieNames = remainCookieNames.filter((r) => !cookies[r])
- if (remainCookieNames.length === 0) {
- resolve(>cookies)
- return
+ const checkDone = (): boolean => {
+ if (names.every((n) => n in cookies)) {
+ cleanup()
+ resolve(cookies)
+ return true
+ }
+ return false
}
- let timeoutTimer: number
+ const onChange = (event: CookieChangeEvent) => {
+ for (const c of event.changed) {
+ if (c.name && c.value && nameSet.has(c.name) && !(c.name in cookies)) {
+ cookies[c.name as T] = c.value
+ }
+ }
+ checkDone()
+ }
- const timer = setInterval(() => {
- Object.assign(cookies, this.get(remainCookieNames))
- remainCookieNames = remainCookieNames.filter((r) => !cookies[r])
+ cookieStore.addEventListener('change', onChange)
- if (remainCookieNames.length === 0) {
- if (timeout) clearTimeout(timeoutTimer)
- clearInterval(timer)
- resolve(>cookies)
+ Promise.all(names.map((name) => cookieStore.get(name))).then((initialCookies) => {
+ for (let i = 0; i < names.length; i++) {
+ const c = initialCookies[i]
+ if (c && c.value && !(names[i] in cookies)) {
+ cookies[names[i]] = c.value
+ }
}
- }, interval)
+ if (checkDone()) return
- if (timeout) {
- timeoutTimer = setTimeout(() => {
- clearInterval(timer)
- reject(new Error(`获取以下 cookie 超时:${remainCookieNames}`))
- }, timeout)
- }
+ if (timeout) {
+ timeoutTimer = setTimeout(() => {
+ const remain = names.filter((n) => !(n in cookies))
+ cleanup()
+ reject(new Error(`获取以下 cookie 超时:${remain}`))
+ }, timeout)
+ }
+ })
})
}
}
diff --git a/src/library/help-info/index.ts b/src/library/help-info/index.ts
index 3f70e0b5..e90877b0 100644
--- a/src/library/help-info/index.ts
+++ b/src/library/help-info/index.ts
@@ -35,10 +35,7 @@ const helpInfo: HelpInfo = {
list: {
title: '黑白名单 / 排序模式',
message: h('p', [
- h(
- 'div',
- '为了更精细地控制为哪些粉丝勋章执行点亮熄灭勋章和观看直播任务,你可以使用黑名单或白名单模式。',
- ),
+ h('div', '每个直播任务都有自己独立的黑名单或白名单。'),
h('ul', [
h('li', [
h('span', '黑名单:仅为'),
@@ -56,7 +53,7 @@ const helpInfo: HelpInfo = {
'div',
'使用白名单模式时,点击【编辑粉丝勋章名单】窗口右下角的开关即可在【常规模式】和【排序模式】之间切换。',
),
- h('div', '在排序模式下,你可以调整脚本执行观看任务的粉丝勋章顺序。'),
+ h('div', '在排序模式下,你可以调整脚本执行任务的粉丝勋章顺序。'),
h('ul', [
h('li', '使用鼠标拖拽表格中的行来调整顺序。'),
h('li', '拖拽行至表格的顶部和底部可以触发滚动。'),
@@ -73,18 +70,19 @@ const helpInfo: HelpInfo = {
'li',
h('p', [
h('strong', '点赞'),
- h('div', '对于正在直播的房间,给直播间点赞约30次。'),
- h('ul', [
- h('li', [h('span', '点赞次数为略微超过任务要求的随机值。')]),
- h('li', [h('span', '部分直播间无法完成该任务,原因未知。')]),
- ]),
+ h('div', '对于正在直播的房间,给直播间点赞。'),
+ h('div', '脚本会根据 B 站返回的任务要求自动决定点赞次数。'),
]),
),
h(
'li',
h('p', [
h('strong', '发送弹幕'),
- h('div', '对于未开播的房间,发送十条弹幕。'),
+ h('div', '对于未开播的房间,发送数条弹幕。'),
+ h(
+ 'div',
+ '脚本会根据 B 站返回的任务要求自动决定发送条数。发送失败时会自动小幅补发。',
+ ),
h('div', [
h(
'div',
@@ -113,17 +111,60 @@ const helpInfo: HelpInfo = {
]),
),
]),
+ h('div', [
+ h('strong', '注意:'),
+ h('ul', [
+ h('li', '点赞、发弹幕和观看直播任务都会等点亮熄灭勋章任务完成后再执行。'),
+ h(
+ 'li',
+ '熄灭的勋章被点亮后,通过完成任务获得的亲密度会被储蓄。需要投喂一个粉丝灯牌才能领取。',
+ ),
+ ]),
+ ]),
+ ]),
+ },
+ like: {
+ title: '点赞',
+ message: h('p', [
+ h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的点赞任务。'),
+ h('ul', [
+ h('li', '只有主播开播时点赞才会计入任务进度,因此只会给开播中的直播间点赞。'),
+ h(
+ 'li',
+ '脚本会根据 B 站返回的任务要求自动决定点赞次数。高等级的粉丝勋章拥有更高的任务上限,因此能获得更多的亲密度。',
+ ),
+ ]),
+ ]),
+ },
+ danmu: {
+ title: '发弹幕',
+ message: h('p', [
+ h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的发弹幕任务。'),
+ h('ul', [
+ h(
+ 'li',
+ '脚本会根据 B 站返回的任务要求自动决定发送条数。高等级的粉丝勋章拥有更高的任务上限,因此能获得更多的亲密度。',
+ ),
+ h(
+ 'li',
+ '点击【编辑弹幕】按钮编辑要发送的弹幕,脚本会从中按顺序循环抽取弹幕发送。建议修改默认的弹幕。',
+ ),
+ h(
+ 'li',
+ '若担心直播中刷屏被禁言,可开启【仅在未开播的直播间发弹幕】,此时会跳过当前正在直播的直播间。',
+ ),
+ h('li', '弹幕发送失败(比如被屏蔽词拦截)会自动小幅补发。'),
+ ]),
]),
},
watch: {
title: '观看直播',
message: h('p', [
- h('div', '完成粉丝勋章的观看直播任务。'),
+ h('div', '为已点亮的粉丝勋章完成完成粉丝团升级任务中的观看直播任务。'),
h('ul', [
h('li', '部分直播间因为没有设置直播分区导致任务无法完成。'),
h('li', '主播当前是否开播不会影响该任务的完成。'),
- h('li', '根据目前规则,观看15分钟可点亮粉丝勋章,再观看25分钟即可获得全部30亲密度。'),
- h('li', '由于计时存在误差,可以在目标基础上适当增加一些观看时长。'),
+ h('li', '脚本会根据 B 站返回的任务要求自动计算观看时长。'),
]),
h('div', [
h('strong', '注意:'),
@@ -141,14 +182,14 @@ const helpInfo: HelpInfo = {
title: '银瓜子换硬币',
message: h('p', [
h('div', '把银瓜子兑换为硬币。'),
- h('div', '具体兑换规则请点击直播间页面的“立即充值→银瓜子商店”查看。'),
+ h('div', '具体兑换规则请点击直播间页面右下角的“电池图标→银瓜子商店”查看。'),
]),
},
coinToSilver: {
title: '硬币换银瓜子',
message: h('p', [
h('div', '把硬币兑换为银瓜子。'),
- h('div', '具体兑换规则请点击直播间页面的“立即充值→银瓜子商店”查看。'),
+ h('div', '具体兑换规则请点击直播间页面右下角的“电池图标→银瓜子商店”查看。'),
]),
},
getYearVipPrivilege: {
@@ -259,10 +300,6 @@ const helpInfo: HelpInfo = {
title: '移除直播间相同弹幕连续提示',
message: '移除直播间相同弹幕连续提示。',
},
- removeRank: {
- title: '移除排行榜',
- message: '移除直播画面上方的人气榜/航海榜,赠送人气票的入口也在这里。',
- },
removeHeaderStuff: {
title: '移除直播画面上方杂项',
message: '移除直播画面上方各种杂七杂八的东西,比如排行榜、活动轮播图等。',
@@ -271,10 +308,6 @@ const helpInfo: HelpInfo = {
title: '移除礼物栏下方广告',
message: '移除礼物栏下方广告。',
},
- removeRecommendRoom: {
- title: '移除礼物栏下方推荐直播间',
- message: '移除礼物栏下方推荐直播间。',
- },
removeLiveMosaic: {
title: '移除直播间马赛克',
message: '移除部分直播间特有的马赛克。',
diff --git a/src/library/luxon/index.ts b/src/library/luxon/index.ts
index 20d62cbe..e41894cd 100644
--- a/src/library/luxon/index.ts
+++ b/src/library/luxon/index.ts
@@ -75,30 +75,27 @@ function delayToNextMoment(hour: number = 0, minute: number = 5): Duration {
}
/**
- * 判断现在是不是处在目标时间范围内
+ * 判断现在是不是在目标时间之前
*
- * 如果开始时间晚于结束时间,则将结束时间视为第二天的时间
- *
- * @param startHour 开始小时
- * @param startMinute 开始分钟
- * @param endHour 结束小时
- * @param endMinute 结束分钟
+ * @param hour 小时(0-23)
+ * @param minute 分钟(0-59)
*/
-function isNowIn(
- startHour: number,
- startMinute: number,
- endHour: number,
- endMinute: number,
-): boolean {
+function isNowBefore(hour: number, minute: number): boolean {
const now = DateTime.now()
- const start = DateTime.local(now.year, now.month, now.day, startHour, startMinute)
- let end = DateTime.local(now.year, now.month, now.day, endHour, endMinute)
-
- if (start > end) {
- end = end.plus({ days: 1 })
- }
+ const target = DateTime.local(now.year, now.month, now.day, hour, minute, 0, 0)
+ return now < target
+}
- return now >= start && now < end
+/**
+ * 判断现在是不是在目标时间之后
+ *
+ * @param hour 小时(0-23)
+ * @param minute 分钟(0-59)
+ */
+function isNowAfter(hour: number, minute: number): boolean {
+ const now = DateTime.now()
+ const target = DateTime.local(now.year, now.month, now.day, hour, minute, 0, 0)
+ return now > target
}
/**
@@ -115,4 +112,4 @@ function tsm(): number {
return DateTime.now().toMillis()
}
-export { isTimestampToday, delayToNextMoment, isNowIn, ts, tsm }
+export { isTimestampToday, delayToNextMoment, isNowBefore, isNowAfter, ts, tsm }
diff --git a/src/library/storage/defaultValues.ts b/src/library/storage/defaultValues.ts
index cc09fa3c..8b265e4f 100644
--- a/src/library/storage/defaultValues.ts
+++ b/src/library/storage/defaultValues.ts
@@ -12,7 +12,12 @@ const defaultValues: DefaultValues = {
isShowPanel: true,
activeMenuIndex: 'MainSiteTasks',
panelWidthPercent: 45,
- medalInfoPanelSortMode: false,
+ medalInfoPanelIsSortMode: {
+ light: false,
+ like: false,
+ danmu: false,
+ watch: false,
+ },
},
modules: {
DailyTasks: {
@@ -38,35 +43,67 @@ const defaultValues: DefaultValues = {
LiveTasks: {
medalTasks: {
light: {
+ enabled: false,
+ danmuList: [
+ '[dog]',
+ '[花]',
+ '[妙]',
+ '[哇]',
+ '[爱]',
+ '[比心]',
+ '[笑哭]',
+ '[捂脸]',
+ '[喝彩]',
+ '[大笑]',
+ '[惊喜]',
+ '[OK]',
+ '[汤圆]',
+ '[墨镜]',
+ `[牛]`,
+ ],
+ isWhiteList: false,
+ roomidList: [],
+ _lastEffectiveCompleteTime: 0,
+ _lastCompleteTime: 0,
+ },
+ like: {
+ enabled: false,
+ isWhiteList: false,
+ roomidList: [],
+ _lastCompleteTime: 0,
+ },
+ danmu: {
enabled: false,
danmuList: [
'(⌒▽⌒)',
'( ̄▽ ̄)',
- '(=・ω・=)',
+ '(=・v・=)',
'(`・ω・´)',
- '(〜 ̄△ ̄)〜',
+ '(〜 ̄△ ̄)~',
'(・∀・)',
'(°∀°)ノ',
'╮( ̄▽ ̄)╭',
'_(:3」∠)_',
'(^・ω・^ )',
'(● ̄(エ) ̄●)',
- 'ε=ε=(ノ≧∇≦)ノ',
+ '(ノ≧∇≦)ノ',
'⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄',
'←◡←',
`(●'◡'●)ノ♥`,
],
+ onlyWhenNotLiving: false,
+ isWhiteList: false,
+ roomidList: [],
_lastCompleteTime: 0,
},
watch: {
enabled: false,
- time: 25,
+ isWhiteList: false,
+ roomidList: [],
_watchingProgress: {},
_lastWatchTime: 0,
_lastCompleteTime: 0,
},
- isWhiteList: false,
- roomidList: [],
},
},
OtherTasks: {
@@ -125,18 +162,12 @@ const defaultValues: DefaultValues = {
removeComboCard: {
enabled: false,
},
- removeRank: {
- enabled: false,
- },
removeHeaderStuff: {
enabled: false,
},
removeFlipView: {
enabled: false,
},
- removeRecommendRoom: {
- enabled: false,
- },
removeLiveMosaic: {
enabled: false,
},
diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts
index d61fb645..0adf075b 100644
--- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts
+++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts
@@ -1,29 +1,63 @@
import BaseModule from '@/modules/BaseModule'
-import { storeToRefs } from 'pinia'
import { useBiliStore, useModuleStore } from '@/stores'
import { watch } from 'vue'
-import type { PublicMedalFilters } from './types'
-import { arrayToMap } from '@/library/utils'
+import type { MedalTaskSharedConfig, SharedMedalFilters } from './types'
+import { arrayToMap, sleep } from '@/library/utils'
import type { LiveData } from '@/library/bili-api/data'
+import BAPI from '@/library/bili-api'
+import { isTimestampToday } from '@/library/luxon'
+import _ from 'lodash'
+
+type JumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift'
class MedalModule extends BaseModule {
+ /** 最大弹幕补发次数 */
+ protected static readonly DANMU_RETRY_LIMIT = 3
+
+ /**
+ * 任务信息共享缓存
+ *
+ * target_id -> task_info Promise
+ */
+ private static taskInfoCache = new Map<
+ number,
+ Promise
+ >()
+
+ /**
+ * 简单限流队列
+ *
+ * 串行执行获取任务信息请求
+ */
+ private static taskInfoRequestQueue: Promise = Promise.resolve()
+
medalTasksConfig = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks
- protected PUBLIC_MEDAL_FILTERS: PublicMedalFilters = {
+ /** 所有粉丝勋章任务的公共配置 */
+ declare protected config: MedalTaskSharedConfig
+
+ protected SHARED_MEDAL_FILTERS: SharedMedalFilters = {
// 包含在白名单中或不包含在黑名单中返回true,否则返回false
- whiteBlackList: (m) =>
- this.medalTasksConfig.isWhiteList
- ? this.medalTasksConfig.roomidList.includes(m.room_info.room_id)
- : !this.medalTasksConfig.roomidList.includes(m.room_info.room_id),
+ meetWhiteOrBlackList: (m) =>
+ this.config.isWhiteList
+ ? this.config.roomidList.includes(m.room_info.room_id)
+ : !this.config.roomidList.includes(m.room_info.room_id),
// 等级小于120返回true,否则返回false
levelLt120: (medal) => medal.medal.level < 120,
+ // 点亮返回true,否则返回false
+ isLighted: (medal) => medal.medal.is_lighted === 1,
+ // 直播中返回true,否则返回false
+ isLiving: (medal) => medal.room_info.living_status === 1,
}
- protected sortMedals(medals: LiveData.FansMedalPanel.List[]): LiveData.FansMedalPanel.List[] {
- const orderMap = arrayToMap(this.medalTasksConfig.roomidList)
- return medals.sort(
- (a, b) => orderMap.get(a.room_info.room_id)! - orderMap.get(b.room_info.room_id)!,
- )
+ /**
+ * 对粉丝勋章列表进行排序
+ *
+ * 注意:该方法会修改传入的 medals 数组
+ */
+ protected sortMedals(medals: LiveData.FansMedalPanel.List[]): void {
+ const orderMap = arrayToMap(this.config.roomidList)
+ medals.sort((a, b) => orderMap.get(a.room_info.room_id)! - orderMap.get(b.room_info.room_id)!)
}
/**
@@ -33,20 +67,159 @@ class MedalModule extends BaseModule {
*/
protected waitForFansMedals(): Promise {
return new Promise((resolve) => {
- const { fansMedalsStatus } = storeToRefs(useBiliStore())
- if (fansMedalsStatus.value === 'loaded') {
+ const biliStore = useBiliStore()
+ if (biliStore.fansMedalsStatus === 'loaded') {
resolve(true)
} else {
- const unwatch = watch(fansMedalsStatus, (newValue) => {
- if (newValue === 'loaded') {
- unwatch()
- resolve(true)
- } else if (newValue === 'error') {
+ const unwatch = watch(
+ () => biliStore.fansMedalsStatus,
+ (newValue) => {
+ if (newValue === 'loaded') {
+ unwatch()
+ resolve(true)
+ } else if (newValue === 'error') {
+ unwatch()
+ resolve(false)
+ }
+ },
+ )
+ }
+ })
+ }
+
+ /**
+ * 解析每日任务进度文案 "每日上限 X/Y"
+ *
+ * @param sub_title API 返回的 sub_title
+ * @returns 解析成功返回 `{ current, limit }`,否则返回 `null`
+ */
+ protected static parseDailyLimit(
+ sub_title: string | undefined,
+ ): { current: number; limit: number } | null {
+ if (!sub_title) return null
+ const match = sub_title.match(/(\d+)\s*\/\s*(\d+)/)
+ if (!match) return null
+ return { current: Number(match[1]), limit: Number(match[2]) }
+ }
+
+ /**
+ * 按 jump_type 在 task_info 中查找任务项
+ */
+ protected static findTaskInfo(
+ task_info: LiveData.GetActivatedMedalInfo.TaskInfo[] | null,
+ jump_type: JumpType,
+ ): LiveData.GetActivatedMedalInfo.TaskInfo | undefined {
+ return task_info?.find((t) => t.jump_type === jump_type)
+ }
+
+ /**
+ * 从任务 title 中解析数字(如 "点赞30次" → 30,"发弹幕10次" → 10)
+ *
+ * @returns 解析成功返回数字,否则返回 null
+ */
+ protected static parseTitleCount(title: string | undefined): number | null {
+ if (!title) return null
+ const match = title.match(/\d+/)
+ if (!match) return null
+ return Number(match[0])
+ }
+
+ /**
+ * 清空任务信息共享缓存
+ */
+ protected static clearTaskInfoCache(): void {
+ MedalModule.taskInfoCache.clear()
+ }
+
+ /**
+ * 将获取任务信息请求加入全局串行队列
+ */
+ private static enqueueTaskInfoRequest(requester: () => Promise): Promise {
+ const task = MedalModule.taskInfoRequestQueue
+ .catch(() => undefined)
+ .then(async () => {
+ await sleep(_.random(300, 500))
+ return requester()
+ })
+
+ MedalModule.taskInfoRequestQueue = task.then(
+ () => undefined,
+ () => undefined,
+ )
+
+ return task
+ }
+
+ /**
+ * 获取指定粉丝勋章的任务信息
+ *
+ * 同一 target_id 的并发请求会共享同一个 Promise;
+ * 不同 target_id 的请求会进入全局串行队列,避免短时间内并发触发过多请求
+ *
+ * @param target_id 主播 uid
+ * @returns 成功返回任务信息数组,失败返回 null
+ */
+ protected fetchTaskInfo(
+ target_id: number,
+ ): Promise {
+ const cached = MedalModule.taskInfoCache.get(target_id)
+ if (cached) return cached
+
+ const promise = MedalModule.enqueueTaskInfoRequest(async () => {
+ try {
+ const response = await BAPI.live.getActivatedMedalInfo(target_id)
+ this.logger.log(`BAPI.live.getActivatedMedalInfo(${target_id}) response`, response)
+ if (response.code === 0) {
+ return response.data.task_info
+ } else {
+ this.logger.error(`BAPI.live.getActivatedMedalInfo(${target_id}) 失败`, response.message)
+ MedalModule.taskInfoCache.delete(target_id)
+ return null
+ }
+ } catch (error) {
+ this.logger.error(`BAPI.live.getActivatedMedalInfo(${target_id}) 出错`, error)
+ MedalModule.taskInfoCache.delete(target_id)
+ return null
+ }
+ })
+
+ MedalModule.taskInfoCache.set(target_id, promise)
+ return promise
+ }
+
+ /**
+ * 若点亮熄灭勋章任务已启用且今天未完成,等待其结束后再继续
+ *
+ * 用于让 点赞/发弹幕/观看直播 在 点亮熄灭勋章 任务完成后再执行
+ */
+ protected async waitForLightTask(): Promise {
+ const lightConfig = this.medalTasksConfig.light
+ if (!lightConfig.enabled) return
+ if (isTimestampToday(lightConfig._lastCompleteTime)) return
+
+ const moduleStore = useModuleStore()
+ const lightStatus = moduleStore.moduleStatus.DailyTasks.LiveTasks.medalTasks.light
+ if (lightStatus === 'done' || lightStatus === 'error') return
+
+ this.logger.log('等待点亮熄灭勋章任务完成后再执行')
+
+ return new Promise((resolve) => {
+ const unwatch = watch(
+ () => moduleStore.moduleStatus.DailyTasks.LiveTasks.medalTasks.light,
+ (newStatus) => {
+ if (newStatus === 'done' || newStatus === 'error') {
unwatch()
- resolve(false)
+
+ if (lightConfig._lastEffectiveCompleteTime === lightConfig._lastCompleteTime) {
+ // 如果点亮熄灭勋章模块确实进行了点亮操作
+ // 重新获取粉丝勋章(主要是为了获取最新的点亮状态和直播状态)
+ // FansMedals 模块内部做了防重入,因此无需担心会重复获取
+ moduleStore.rerunModule('Default_FansMedals')
+ }
+ resolve()
}
- })
- }
+ },
+ )
})
}
}
diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts
new file mode 100644
index 00000000..b83dc0dc
--- /dev/null
+++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts
@@ -0,0 +1,174 @@
+import { delayToNextMoment, isNowAfter, isNowBefore, isTimestampToday, tsm } from '@/library/luxon'
+import BAPI from '@/library/bili-api'
+import { useBiliStore, useModuleStore } from '@/stores'
+import { sleep } from '@/library/utils'
+import type { ModuleStatusTypes } from '@/types'
+import _ from 'lodash'
+import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule'
+import type { LiveData } from '@/library/bili-api/data'
+
+class DanmuTask extends MedalModule {
+ config = this.medalTasksConfig.danmu
+
+ set status(s: ModuleStatusTypes) {
+ useModuleStore().moduleStatus.DailyTasks.LiveTasks.medalTasks.danmu = s
+ }
+
+ /**
+ * 获取已点亮的粉丝勋章;开启 `onlyWhenNotLiving` 时排除开播中的
+ */
+ private getMedals(): LiveData.FansMedalPanel.List[] {
+ const fansMedals = useBiliStore().filteredFansMedals
+ const result = fansMedals.filter(
+ (medal) =>
+ this.SHARED_MEDAL_FILTERS.meetWhiteOrBlackList(medal) &&
+ this.SHARED_MEDAL_FILTERS.levelLt120(medal) &&
+ this.SHARED_MEDAL_FILTERS.isLighted(medal) &&
+ (!this.config.onlyWhenNotLiving || !this.SHARED_MEDAL_FILTERS.isLiving(medal)),
+ )
+
+ if (this.config.isWhiteList) {
+ this.sortMedals(result)
+ }
+
+ return result
+ }
+
+ /**
+ * 发弹幕
+ * @returns 是否发送成功
+ */
+ private async sendDanmu(medal: LiveData.FansMedalPanel.List, danmu: string): Promise {
+ const room_id = medal.room_info.room_id
+ const target_id = medal.medal.target_id
+ const nick_name = medal.anchor_info.nick_name
+ const medal_name = medal.medal.medal_name
+ const logMessage = `粉丝勋章【${medal_name}】 在主播【${nick_name}】(UID:${target_id})的直播间(${room_id})发送弹幕 ${danmu}`
+
+ try {
+ const response = await BAPI.live.sendMsg(danmu, room_id)
+ this.logger.log(`BAPI.live.sendMsg(${danmu}, ${room_id})`, response)
+ if (response.code === 0) {
+ if (response.msg === '') {
+ this.logger.log(`发弹幕 ${logMessage} 成功`)
+ return true
+ } else if (response.msg === 'k') {
+ this.logger.warn(`发弹幕 ${logMessage} 异常,弹幕可能包含屏蔽词`)
+ } else if (response.msg === 'f') {
+ this.logger.warn(`发弹幕 ${logMessage} 异常,弹幕被过滤`)
+ } else {
+ this.logger.warn(`发弹幕 ${logMessage} 异常,未知错误:${response.msg}`)
+ }
+ } else {
+ this.logger.error(`发弹幕 ${logMessage} 失败`, response.message)
+ }
+ } catch (error) {
+ this.logger.error(`发弹幕 ${logMessage} 出错`, error)
+ }
+
+ return false
+ }
+
+ public async run(): Promise {
+ this.logger.log('发弹幕模块开始运行')
+
+ if (!isTimestampToday(this.config._lastCompleteTime)) {
+ await this.waitForLightTask()
+
+ if (!(await this.waitForFansMedals())) {
+ this.logger.error('粉丝勋章数据不存在,不执行发弹幕任务')
+ this.status = 'error'
+ return
+ }
+
+ this.status = 'running'
+ MedalModule.clearTaskInfoCache()
+ const fansMedals = this.getMedals()
+ let allCompleted = true
+ let danmuIndex = 0
+
+ outer: for (let i = 0; i < fansMedals.length; i++) {
+ if (isNowAfter(23, 55) || isNowBefore(0, 5)) {
+ this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务')
+ allCompleted = false
+ break
+ }
+
+ const medal = fansMedals[i]
+ const taskInfo = await this.fetchTaskInfo(medal.medal.target_id)
+ if (!taskInfo) {
+ this.logger.error(
+ `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过发弹幕任务`,
+ )
+ continue
+ }
+
+ const item = MedalModule.findTaskInfo(taskInfo, 'sendDanmu')
+ if (!item) {
+ this.logger.error(
+ `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`,
+ )
+ continue
+ }
+
+ if (item.is_done) continue
+
+ const parsed = MedalModule.parseDailyLimit(item.sub_title)
+ if (!parsed) {
+ this.logger.error(
+ `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`,
+ )
+ continue
+ }
+
+ if (parsed.current >= parsed.limit) continue
+
+ let target = parsed.limit - parsed.current
+ let failedCount = 0
+
+ for (let j = 0; j < target; j++) {
+ if (isNowAfter(23, 55) || isNowBefore(0, 5)) {
+ this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务')
+ allCompleted = false
+ break outer
+ }
+
+ const danmuText = this.config.danmuList[danmuIndex++ % this.config.danmuList.length]
+ if (!(await this.sendDanmu(medal, danmuText))) {
+ if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) {
+ this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`)
+ await sleep(_.random(6000, 8000))
+ break
+ }
+ target++
+ }
+
+ if (j < target - 1 || i < fansMedals.length - 1) {
+ await sleep(_.random(6000, 8000))
+ }
+ }
+ }
+
+ if (allCompleted) {
+ this.config._lastCompleteTime = tsm()
+ this.status = 'done'
+ this.logger.log('发弹幕任务已完成')
+ } else {
+ this.status = ''
+ }
+ } else {
+ if (isNowBefore(0, 5)) {
+ this.logger.log('昨天的发弹幕任务已经完成过了,等到今天的00:05再执行')
+ } else {
+ this.logger.log('今天已经完成过发弹幕任务了')
+ this.status = 'done'
+ }
+ }
+
+ const diff = delayToNextMoment()
+ this.nextRunTimer = setTimeout(() => this.run(), diff.ms)
+ this.logger.log('距离发弹幕模块下次运行时间:', diff.str)
+ }
+}
+
+export default DanmuTask
diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/index.ts b/src/modules/dailyTasks/liveTasks/medalTasks/index.ts
index cf5cbb87..c19d3d30 100644
--- a/src/modules/dailyTasks/liveTasks/medalTasks/index.ts
+++ b/src/modules/dailyTasks/liveTasks/medalTasks/index.ts
@@ -1,2 +1,4 @@
export { default as DailyTask_LiveTask_LightTask } from './lightTask'
+export { default as DailyTask_LiveTask_LikeTask } from './likeTask'
+export { default as DailyTask_LiveTask_DanmuTask } from './danmuTask'
export { default as DailyTask_LiveTask_WatchTask } from './watchTask'
diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts
index 04213093..a00b5e12 100644
--- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts
+++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts
@@ -1,13 +1,14 @@
-import { delayToNextMoment, isNowIn, isTimestampToday, tsm } from '@/library/luxon'
+import { delayToNextMoment, isNowBefore, isTimestampToday, tsm } from '@/library/luxon'
import BAPI from '@/library/bili-api'
import { useBiliStore, useModuleStore } from '@/stores'
import { sleep } from '@/library/utils'
import type { ModuleStatusTypes } from '@/types'
import _ from 'lodash'
import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule'
-import type { LightTaskMedalFilters, MedalsByLivingStatus } from './types'
import type { LiveData } from '@/library/bili-api/data'
+type MedalsByLiving = [LiveData.FansMedalPanel.List[], LiveData.FansMedalPanel.List[]]
+
class LightTask extends MedalModule {
config = this.medalTasksConfig.light
@@ -15,39 +16,33 @@ class LightTask extends MedalModule {
useModuleStore().moduleStatus.DailyTasks.LiveTasks.medalTasks.light = s
}
- private MEDAL_FILTERS: LightTaskMedalFilters = {
- // 点亮返回true,否则返回false
- isLighted: (medal) => medal.medal.is_lighted === 1,
- // 直播中返回on,否则返回off
- livingStatus: (medal) => (medal.room_info.living_status === 1 ? 'on' : 'off'),
- }
-
/**
- * 获取粉丝勋章
- * @returns 根据直播状态划分、经过排序和过滤的粉丝勋章
+ * 获取未点亮的粉丝勋章
+ *
+ * @returns [[主播未开播的粉丝勋章], [主播开播中的粉丝勋章]]
*/
- private getMedals(): MedalsByLivingStatus {
+ private getMedals(): MedalsByLiving {
const fansMedals = useBiliStore().filteredFansMedals
-
- const result: MedalsByLivingStatus = {
- on: [],
- off: [],
- }
+ const result: MedalsByLiving = [[], []]
fansMedals.forEach((medal) => {
- if (!this.PUBLIC_MEDAL_FILTERS.whiteBlackList(medal) || this.MEDAL_FILTERS.isLighted(medal)) {
+ if (
+ !this.SHARED_MEDAL_FILTERS.meetWhiteOrBlackList(medal) ||
+ this.SHARED_MEDAL_FILTERS.isLighted(medal)
+ ) {
// 跳过被黑白名单过滤的和已经点亮的粉丝勋章
return
}
- const livingStatus = this.MEDAL_FILTERS.livingStatus(medal)
- result[livingStatus].push(medal)
+ // 根据直播状态划分
+ const index = this.SHARED_MEDAL_FILTERS.isLiving(medal) ? 1 : 0
+ result[index].push(medal)
})
- if (this.medalTasksConfig.isWhiteList) {
+ if (this.config.isWhiteList) {
// 白名单排序
- this.sortMedals(result.on)
- this.sortMedals(result.off)
+ this.sortMedals(result[0])
+ this.sortMedals(result[1])
}
return result
@@ -94,11 +89,15 @@ class LightTask extends MedalModule {
const response = await BAPI.live.sendMsg(danmu, room_id)
this.logger.log(`BAPI.live.sendMsg(${danmu}, ${room_id})`, response)
if (response.code === 0) {
- if (response.msg === 'k') {
- this.logger.warn(`点亮熄灭勋章-发送弹幕 ${logMessage} 异常,弹幕可能包含屏蔽词`)
- } else {
- this.logger.log(`点亮熄灭勋章-发送弹幕 ${logMessage} 成功`)
+ if (response.msg === '') {
+ this.logger.log(`点亮熄灭勋章-发弹幕 ${logMessage} 成功`)
return true
+ } else if (response.msg === 'k') {
+ this.logger.warn(`点亮熄灭勋章-发弹幕 ${logMessage} 异常,弹幕可能包含屏蔽词`)
+ } else if (response.msg === 'f') {
+ this.logger.warn(`点亮熄灭勋章-发弹幕 ${logMessage} 异常,弹幕被过滤`)
+ } else {
+ this.logger.warn(`点亮熄灭勋章-发弹幕 ${logMessage} 异常,未知错误:${response.msg}`)
}
} else {
this.logger.error(`点亮熄灭勋章-发送弹幕 ${logMessage} 失败`, response.message)
@@ -115,13 +114,17 @@ class LightTask extends MedalModule {
* @param medals
* @private
*/
- private async likeTask(medals: LiveData.FansMedalPanel.List[]) {
+ private async likeTask(medals: LiveData.FansMedalPanel.List[]): Promise {
for (let i = 0; i < medals.length; i++) {
const medal = medals[i]
- await this.like(medal, _.random(30, 35))
+ const taskInfo = await this.fetchTaskInfo(medal.medal.target_id)
+ const item = MedalModule.findTaskInfo(taskInfo, 'like')
+ // 从 title 解析点亮所需点赞次数(如 "点赞30次" → 30),失败时 fallback 到 30
+ const baseCount = MedalModule.parseTitleCount(item?.title) ?? 30
+ await this.like(medal, _.random(baseCount, baseCount + 5))
if (i < medals.length - 1) {
- await sleep(_.random(30000, 35000))
+ await sleep(_.random(15000, 20000))
}
}
}
@@ -131,19 +134,27 @@ class LightTask extends MedalModule {
* @param medals
* @private
*/
- private async sendDanmuTask(medals: LiveData.FansMedalPanel.List[]) {
+ private async sendDanmuTask(medals: LiveData.FansMedalPanel.List[]): Promise {
let danmuIndex = 0
for (let i = 0; i < medals.length; i++) {
const medal = medals[i]
- let target = 10
+ const taskInfo = await this.fetchTaskInfo(medal.medal.target_id)
+ const item = MedalModule.findTaskInfo(taskInfo, 'sendDanmu')
+ // 从 title 解析点亮所需弹幕条数(如 "发弹幕10次" → 10),失败时 fallback 到 10
+ let target = MedalModule.parseTitleCount(item?.title) ?? 10
+ let failedCount = 0
for (let j = 0; j < target; j++) {
const danmuText = this.config.danmuList[danmuIndex++ % this.config.danmuList.length]
if (!(await this.sendDanmu(medal, danmuText))) {
- // 弹幕发送失败,多尝试一次,每个直播间最多发13条
- target = Math.min(target + 1, 13)
+ if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) {
+ this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`)
+ await sleep(_.random(6000, 8000))
+ break
+ }
+ target++
}
if (i < medals.length - 1 || j < target - 1) {
@@ -164,15 +175,22 @@ class LightTask extends MedalModule {
}
this.status = 'running'
+ MedalModule.clearTaskInfoCache()
const fansMedals = this.getMedals()
-
- await Promise.allSettled([this.likeTask(fansMedals.on), this.sendDanmuTask(fansMedals.off)])
+ // 是否有需要点亮的粉丝勋章(是不是一次有效运行?)
+ const isEffectiveRun = fansMedals.some((medals) => medals.length > 0)
+ if (isEffectiveRun) {
+ await Promise.allSettled([this.sendDanmuTask(fansMedals[0]), this.likeTask(fansMedals[1])])
+ }
this.config._lastCompleteTime = tsm()
+ if (isEffectiveRun) {
+ this.config._lastEffectiveCompleteTime = this.config._lastCompleteTime
+ }
this.status = 'done'
this.logger.log('点亮熄灭勋章任务已完成')
} else {
- if (isNowIn(0, 0, 0, 5)) {
+ if (isNowBefore(0, 5)) {
this.logger.log('昨天的给点亮熄灭勋章任务已经完成过了,等到今天的00:05再执行')
} else {
this.logger.log('今天已经完成过点亮熄灭勋章任务了')
diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts
new file mode 100644
index 00000000..60df241b
--- /dev/null
+++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts
@@ -0,0 +1,154 @@
+import { delayToNextMoment, isNowAfter, isNowBefore, isTimestampToday, tsm } from '@/library/luxon'
+import BAPI from '@/library/bili-api'
+import { useBiliStore, useModuleStore } from '@/stores'
+import { sleep } from '@/library/utils'
+import type { ModuleStatusTypes } from '@/types'
+import _ from 'lodash'
+import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule'
+import type { LiveData } from '@/library/bili-api/data'
+
+class LikeTask extends MedalModule {
+ config = this.medalTasksConfig.like
+
+ set status(s: ModuleStatusTypes) {
+ useModuleStore().moduleStatus.DailyTasks.LiveTasks.medalTasks.like = s
+ }
+
+ /**
+ * 获取已点亮且主播开播中的粉丝勋章
+ */
+ private getMedals(): LiveData.FansMedalPanel.List[] {
+ const fansMedals = useBiliStore().filteredFansMedals
+ const result = fansMedals.filter(
+ (medal) =>
+ this.SHARED_MEDAL_FILTERS.meetWhiteOrBlackList(medal) &&
+ this.SHARED_MEDAL_FILTERS.levelLt120(medal) &&
+ this.SHARED_MEDAL_FILTERS.isLighted(medal) &&
+ this.SHARED_MEDAL_FILTERS.isLiving(medal),
+ )
+
+ if (this.config.isWhiteList) {
+ this.sortMedals(result)
+ }
+
+ return result
+ }
+
+ /**
+ * 点赞
+ */
+ private async like(medal: LiveData.FansMedalPanel.List, click_time: number): Promise {
+ const room_id = medal.room_info.room_id
+ const target_id = medal.medal.target_id
+ const nick_name = medal.anchor_info.nick_name
+ const medal_name = medal.medal.medal_name
+ const logMessage = `粉丝勋章【${medal_name}】 给主播【${nick_name}】(UID:${target_id})的直播间(${room_id})点赞 ${click_time} 次`
+
+ try {
+ const response = await BAPI.live.likeReport(room_id, target_id, click_time)
+ this.logger.log(`BAPI.live.likeReport(${room_id}, ${target_id}, ${click_time})`, response)
+ if (response.code === 0) {
+ this.logger.log(`点赞 ${logMessage} 成功`)
+ } else {
+ this.logger.error(`点赞 ${logMessage} 失败`, response.message)
+ }
+ } catch (error) {
+ this.logger.error(`点赞 ${logMessage} 出错`, error)
+ }
+ }
+
+ public async run(): Promise {
+ this.logger.log('点赞模块开始运行')
+
+ if (!isTimestampToday(this.config._lastCompleteTime)) {
+ await this.waitForLightTask()
+
+ if (!(await this.waitForFansMedals())) {
+ this.logger.error('粉丝勋章数据不存在,不执行点赞任务')
+ this.status = 'error'
+ return
+ }
+
+ this.status = 'running'
+ MedalModule.clearTaskInfoCache()
+ const fansMedals = this.getMedals()
+ let allCompleted = true
+
+ outer: for (let i = 0; i < fansMedals.length; i++) {
+ if (isNowAfter(23, 55) || isNowBefore(0, 5)) {
+ this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务')
+ allCompleted = false
+ break
+ }
+
+ const medal = fansMedals[i]
+ const taskInfo = await this.fetchTaskInfo(medal.medal.target_id)
+ if (!taskInfo) {
+ this.logger.error(
+ `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过点赞任务`,
+ )
+ continue
+ }
+
+ const item = MedalModule.findTaskInfo(taskInfo, 'like')
+ if (!item) {
+ this.logger.error(
+ `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`,
+ )
+ continue
+ }
+
+ if (item.is_done) continue
+
+ const parsed = MedalModule.parseDailyLimit(item.sub_title)
+ if (!parsed) {
+ this.logger.error(
+ `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的点赞任务的每日上限信息,跳过点赞任务`,
+ )
+ continue
+ }
+
+ if (parsed.current >= parsed.limit) continue
+
+ // 每轮点赞次数
+ const times = MedalModule.parseTitleCount(item.title) ?? 30
+ // 剩余点赞轮数
+ const remaining = parsed.limit - parsed.current
+ for (let j = 0; j < remaining; j++) {
+ if (isNowAfter(23, 55) || isNowBefore(0, 5)) {
+ this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务')
+ allCompleted = false
+ break outer
+ }
+
+ await this.like(medal, _.random(times, times + 5))
+
+ if (j < remaining - 1 || i < fansMedals.length - 1) {
+ await sleep(_.random(15000, 20000))
+ }
+ }
+ }
+
+ if (allCompleted) {
+ this.config._lastCompleteTime = tsm()
+ this.status = 'done'
+ this.logger.log('点赞任务已完成')
+ } else {
+ this.status = ''
+ }
+ } else {
+ if (isNowBefore(0, 5)) {
+ this.logger.log('昨天的点赞任务已经完成过了,等到今天的00:05再执行')
+ } else {
+ this.logger.log('今天已经完成过点赞任务了')
+ this.status = 'done'
+ }
+ }
+
+ const diff = delayToNextMoment()
+ this.nextRunTimer = setTimeout(() => this.run(), diff.ms)
+ this.logger.log('距离点赞模块下次运行时间:', diff.str)
+ }
+}
+
+export default LikeTask
diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts
index d2906188..95b3d92f 100644
--- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts
+++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts
@@ -1,17 +1,19 @@
import type { LiveData } from '@/library/bili-api/data'
+import type { ModuleConfig } from '@/types'
-type LiveStatus = 'on' | 'off'
+type _MedalTasks = ModuleConfig['DailyTasks']['LiveTasks']['medalTasks']
-interface PublicMedalFilters {
- whiteBlackList: (medal: LiveData.FansMedalPanel.List) => boolean
- levelLt120: (medal: LiveData.FansMedalPanel.List) => boolean
-}
+type MedalTaskSharedConfig =
+ | _MedalTasks['light']
+ | _MedalTasks['like']
+ | _MedalTasks['danmu']
+ | _MedalTasks['watch']
-interface LightTaskMedalFilters {
+interface SharedMedalFilters {
+ meetWhiteOrBlackList: (medal: LiveData.FansMedalPanel.List) => boolean
+ levelLt120: (medal: LiveData.FansMedalPanel.List) => boolean
isLighted: (medal: LiveData.FansMedalPanel.List) => boolean
- livingStatus: (medal: LiveData.FansMedalPanel.List) => LiveStatus
+ isLiving: (medal: LiveData.FansMedalPanel.List) => boolean
}
-type MedalsByLivingStatus = Record
-
-export { PublicMedalFilters, LightTaskMedalFilters, MedalsByLivingStatus }
+export { MedalTaskSharedConfig, SharedMedalFilters }
diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts
index 35897a4c..dd5d648e 100644
--- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts
+++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts
@@ -1,10 +1,10 @@
-import { delayToNextMoment, isNowIn, isTimestampToday, tsm } from '@/library/luxon'
+import { delayToNextMoment, isNowAfter, isNowBefore, isTimestampToday, tsm } from '@/library/luxon'
import BAPI from '@/library/bili-api'
import { useBiliStore, useModuleStore, usePlayerStore } from '@/stores'
import Logger from '@/library/logger'
import CryptoJS from 'crypto-js'
import { sleep, uuid } from '@/library/utils'
-import type { ModuleConfig, ModuleStatusTypes, RunAtMoment } from '@/types'
+import type { ModuleStatusTypes, RunAtMoment } from '@/types'
import _ from 'lodash'
import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule'
import type { LiveData } from '@/library/bili-api/data'
@@ -26,27 +26,24 @@ class RoomHeart {
parentID: number,
ruid: number,
watchedSeconds: number,
+ targetSeconds: number,
) {
this.roomID = roomID
this.areaID = areaID
this.parentID = parentID
this.ruid = ruid
this.watchedSeconds = watchedSeconds
-
- this.config = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks.watch
+ this.targetSeconds = targetSeconds
}
private logger = new Logger('RoomHeart')
- private config: ModuleConfig['DailyTasks']['LiveTasks']['medalTasks']['watch']
-
- set status(s: ModuleStatusTypes) {
- useModuleStore().moduleStatus.DailyTasks.LiveTasks.medalTasks.watch = s
- }
-
/** 今日当前直播间已观看时间(秒) */
private watchedSeconds: number
+ /** 当前直播间观看目标秒数 */
+ private readonly targetSeconds: number
+
private readonly areaID: number
private readonly parentID: number
private readonly roomID: number
@@ -131,7 +128,7 @@ class RoomHeart {
* X心跳,E心跳过后都是X心跳
*/
private async X(): Promise {
- if (isNowIn(23, 59, 0, 5)) {
+ if (isNowAfter(23, 58) || isNowBefore(0, 5)) {
this.logger.log(`即将或刚刚发生跨天,停止直播间 ${this.roomID} 的X心跳`)
return
}
@@ -166,8 +163,8 @@ class RoomHeart {
if (response.code === 0) {
this.seq += 1
this.updateProgress()
- if (this.watchedSeconds >= this.config.time * 60) {
- // 达到设置的观看时间,结束
+ if (this.watchedSeconds >= this.targetSeconds) {
+ // 达到目标观看时间,结束
return
}
;({
@@ -256,19 +253,19 @@ class WatchTask extends MedalModule {
private playerStore = usePlayerStore()
/**
- * 获取粉丝勋章
- * @returns 根据直播状态划分、经过排序和过滤的粉丝勋章
+ * 获取已点亮的粉丝勋章
*/
private getMedals(): LiveData.FansMedalPanel.List[] {
const fansMedals = useBiliStore().filteredFansMedals
const result = fansMedals.filter(
(medal) =>
- this.PUBLIC_MEDAL_FILTERS.whiteBlackList(medal) &&
- this.PUBLIC_MEDAL_FILTERS.levelLt120(medal),
+ this.SHARED_MEDAL_FILTERS.meetWhiteOrBlackList(medal) &&
+ this.SHARED_MEDAL_FILTERS.levelLt120(medal) &&
+ this.SHARED_MEDAL_FILTERS.isLighted(medal),
)
- if (this.medalTasksConfig.isWhiteList) {
+ if (this.config.isWhiteList) {
this.sortMedals(result)
}
@@ -283,22 +280,8 @@ class WatchTask extends MedalModule {
* @param roomid 房间号
* @returns [area_id, parent_area_id]
*/
- private async getAreaInfo(url: string, roomid: number): Promise<[number, number]> {
+ private async getAreaInfo(roomid: number): Promise<[number, number]> {
try {
- // 先尝试从 url 中提取 area_id 和 parent_area_id
- const urlObj = new URL(url)
- const area_id = Number(urlObj.searchParams.get('area_id'))
- const parent_area_id = Number(urlObj.searchParams.get('parent_area_id'))
-
- if (area_id > 0 && parent_area_id > 0) {
- this.logger.log(
- `已从直播间链接中(roomid = ${roomid})获取到分区数据(area_id = ${area_id},parent_area_id = ${parent_area_id})`,
- urlObj,
- )
- return [area_id, parent_area_id]
- }
-
- // 如果无法从 url 中提取到分区数据,调用 API 获取
const response = await BAPI.live.getInfoByRoom(roomid)
this.logger.log(`BAPI.live.getInfoByRoom(${roomid}) response`, response)
@@ -327,6 +310,8 @@ class WatchTask extends MedalModule {
})
if (!isTimestampToday(this.config._lastCompleteTime)) {
+ await this.waitForLightTask()
+
if (!(await this.waitForFansMedals())) {
this.logger.error('粉丝勋章数据不存在,不执行观看直播任务')
this.status = 'error'
@@ -334,6 +319,7 @@ class WatchTask extends MedalModule {
}
this.status = 'running'
+ MedalModule.clearTaskInfoCache()
if (!isTimestampToday(this.config._lastWatchTime, 0, 0)) {
// 如果上次观看(不是完成任务)的时间戳不在今天,将今天已观看的秒数置为0
@@ -352,7 +338,7 @@ class WatchTask extends MedalModule {
let i: number
for (i = 0; i < fansMedals.length; i++) {
- if (isNowIn(23, 55, 0, 5)) {
+ if (isNowAfter(23, 55) || isNowBefore(0, 5)) {
this.logger.log('即将或刚刚发生跨天,提早结束本轮观看直播任务')
break
}
@@ -360,27 +346,60 @@ class WatchTask extends MedalModule {
const medal = fansMedals[i]
const roomid = medal.room_info.room_id
const uid = medal.medal.target_id
- const [area_id, parent_area_id] = await this.getAreaInfo(medal.room_info.url, roomid)
+
+ const taskInfo = await this.fetchTaskInfo(uid)
+ if (!taskInfo) {
+ this.logger.error(
+ `无法获取主播【${medal.anchor_info.nick_name}】(UID:${uid})的粉丝团升级任务信息,跳过观看直播任务`,
+ )
+ continue
+ }
+
+ const item = MedalModule.findTaskInfo(taskInfo, 'watchLive')
+ if (!item) {
+ this.logger.error(
+ `无法在主播【${medal.anchor_info.nick_name}】(UID:${uid})的粉丝团升级任务信息中找到观看直播任务,跳过观看直播任务`,
+ )
+ continue
+ }
+
+ if (item.is_done) continue
+
+ const parsed = MedalModule.parseDailyLimit(item.sub_title)
+ if (!parsed) {
+ this.logger.error(
+ `无法解析主播【${medal.anchor_info.nick_name}】(UID:${uid})的观看直播任务的每日上限信息,跳过观看直播任务`,
+ )
+ continue
+ }
+ if (parsed.current >= parsed.limit) continue
+
+ const minutes = MedalModule.parseTitleCount(item.title) ?? 15
+
+ // 目标秒数 = 每日上限 × minutes × 60s + 60s 缓冲(补偿心跳计时误差)
+ const targetSeconds = parsed.limit * minutes * 60 + 60
+
+ if ((this.config._watchingProgress[roomid] ?? 0) >= targetSeconds) {
+ // 今日观看时间已达到目标值,跳过
+ continue
+ }
+
+ const [area_id, parent_area_id] = await this.getAreaInfo(roomid)
if (area_id > 0 && parent_area_id > 0) {
// area_id 和 parent_area_id 都大于 0,说明直播间设置了分区,心跳有效
- if (
- !this.config._watchingProgress[roomid] ||
- this.config._watchingProgress[roomid] < this.config.time * 60
- ) {
- // 今日观看时间未达到设置值,开始心跳
- this.logger.log(
- `粉丝勋章【${medal.medal.medal_name}】 开始直播间 ${roomid}(主播【${medal.anchor_info.nick_name}】,UID:${uid})的观看直播任务`,
- )
-
- await new RoomHeart(
- roomid,
- area_id,
- parent_area_id,
- uid,
- this.config._watchingProgress[roomid] ?? 0,
- ).start()
- }
+ this.logger.log(
+ `粉丝勋章【${medal.medal.medal_name}】 开始直播间 ${roomid}(主播【${medal.anchor_info.nick_name}】,UID:${uid})的观看直播任务,目标时长 ${targetSeconds / 60} 分钟)`,
+ )
+
+ await new RoomHeart(
+ roomid,
+ area_id,
+ parent_area_id,
+ uid,
+ this.config._watchingProgress[roomid] ?? 0,
+ targetSeconds,
+ ).start()
}
}
@@ -397,7 +416,7 @@ class WatchTask extends MedalModule {
this.config._lastCompleteTime = tsm()
}
} else {
- if (isNowIn(0, 0, 0, 5)) {
+ if (isNowBefore(0, 5)) {
this.logger.log('昨天的观看直播任务已经完成过了,等到今天的00:05再执行')
} else {
this.logger.log('今天已经完成过观看直播任务了')
diff --git a/src/modules/dailyTasks/mainSiteTasks/coinTask.ts b/src/modules/dailyTasks/mainSiteTasks/coinTask.ts
index b1b08c8e..787302cb 100644
--- a/src/modules/dailyTasks/mainSiteTasks/coinTask.ts
+++ b/src/modules/dailyTasks/mainSiteTasks/coinTask.ts
@@ -1,5 +1,5 @@
import BaseModule from '@/modules/BaseModule'
-import { isTimestampToday, delayToNextMoment, tsm, isNowIn } from '@/library/luxon'
+import { isTimestampToday, delayToNextMoment, tsm, isNowBefore } from '@/library/luxon'
import { useBiliStore, useModuleStore } from '@/stores'
import BAPI from '@/library/bili-api'
import type { ModuleStatusTypes } from '@/types'
@@ -169,7 +169,7 @@ class CoinTask extends BaseModule {
} else {
// 为了更加准确的语言描述和任务状态图标显示,需要判断当前所处的时间段
// 下面文字中的今天、昨天是指真实的今天、昨天而非在 isTimestampToday 函数中重新定义的
- if (isNowIn(0, 0, 0, 5)) {
+ if (isNowBefore(0, 5)) {
// 在半夜00:00 ~ 00:05
this.logger.log('昨天的每日投币任务已经完成过了,等到今天的00:05再执行')
} else {
diff --git a/src/modules/dailyTasks/mainSiteTasks/loginTask.ts b/src/modules/dailyTasks/mainSiteTasks/loginTask.ts
index 1c62aa6f..45fcdfe7 100644
--- a/src/modules/dailyTasks/mainSiteTasks/loginTask.ts
+++ b/src/modules/dailyTasks/mainSiteTasks/loginTask.ts
@@ -1,5 +1,5 @@
import BaseModule from '@/modules/BaseModule'
-import { isTimestampToday, delayToNextMoment, tsm, isNowIn } from '@/library/luxon'
+import { isTimestampToday, delayToNextMoment, tsm, isNowBefore } from '@/library/luxon'
import { useBiliStore, useModuleStore } from '@/stores'
import type { ModuleStatusTypes } from '@/types'
@@ -46,7 +46,7 @@ class LoginTask extends BaseModule {
this.status = 'done'
}
} else {
- if (isNowIn(0, 0, 0, 5)) {
+ if (isNowBefore(0, 5)) {
this.logger.log('昨天的每日登录任务已经完成过了,等到今天的00:05再执行')
} else {
this.logger.log('今天已经完成过每日登录任务了')
diff --git a/src/modules/dailyTasks/mainSiteTasks/shareTask.ts b/src/modules/dailyTasks/mainSiteTasks/shareTask.ts
index c687b9b4..6de7b445 100644
--- a/src/modules/dailyTasks/mainSiteTasks/shareTask.ts
+++ b/src/modules/dailyTasks/mainSiteTasks/shareTask.ts
@@ -1,5 +1,5 @@
import BaseModule from '@/modules/BaseModule'
-import { isTimestampToday, delayToNextMoment, tsm, isNowIn } from '@/library/luxon'
+import { isTimestampToday, delayToNextMoment, tsm, isNowBefore } from '@/library/luxon'
import { useBiliStore, useModuleStore } from '@/stores'
import BAPI from '@/library/bili-api'
import type { ModuleStatusTypes } from '@/types'
@@ -73,7 +73,7 @@ class ShareTask extends BaseModule {
this.logger.log('每日分享视频任务已完成')
}
} else {
- if (isNowIn(0, 0, 0, 5)) {
+ if (isNowBefore(0, 5)) {
this.logger.log('昨天的每日分享任务已经完成过了,等到今天的00:05再执行')
} else {
this.logger.log('今天已经完成过每日分享任务了')
diff --git a/src/modules/dailyTasks/mainSiteTasks/watchTask.ts b/src/modules/dailyTasks/mainSiteTasks/watchTask.ts
index 8498174d..4674716d 100644
--- a/src/modules/dailyTasks/mainSiteTasks/watchTask.ts
+++ b/src/modules/dailyTasks/mainSiteTasks/watchTask.ts
@@ -1,5 +1,5 @@
import BaseModule from '@/modules/BaseModule'
-import { isTimestampToday, delayToNextMoment, tsm, isNowIn } from '@/library/luxon'
+import { isTimestampToday, delayToNextMoment, tsm, isNowBefore } from '@/library/luxon'
import { useBiliStore, useModuleStore } from '@/stores'
import BAPI from '@/library/bili-api'
import type { ModuleStatusTypes } from '@/types'
@@ -74,7 +74,7 @@ class WatchTask extends BaseModule {
this.logger.log('每日观看视频任务已完成')
}
} else {
- if (isNowIn(0, 0, 0, 5)) {
+ if (isNowBefore(0, 5)) {
this.logger.log('昨天的每日观看视频任务已经完成过了,等到今天的00:05再执行')
} else {
this.logger.log('今天已经完成过每日观看视频任务了')
diff --git a/src/modules/dailyTasks/otherTasks/coinToSilverTask.ts b/src/modules/dailyTasks/otherTasks/coinToSilverTask.ts
index c99e674a..f30f5157 100644
--- a/src/modules/dailyTasks/otherTasks/coinToSilverTask.ts
+++ b/src/modules/dailyTasks/otherTasks/coinToSilverTask.ts
@@ -1,5 +1,5 @@
import BaseModule from '@/modules/BaseModule'
-import { isTimestampToday, delayToNextMoment, tsm, isNowIn } from '@/library/luxon'
+import { isTimestampToday, delayToNextMoment, tsm, isNowAfter } from '@/library/luxon'
import BAPI from '@/library/bili-api'
import type { ModuleStatusTypes } from '@/types'
import { useModuleStore } from '@/stores'
@@ -14,7 +14,7 @@ class CoinToSilverTask extends BaseModule {
private async exchange() {
try {
const response = await BAPI.live.coin2silver(this.config.num)
- this.logger.log(`BAPI.live.coin2silver{${this.config.num}} response`, response)
+ this.logger.log(`BAPI.live.coin2silver(${this.config.num}) response`, response)
if (response.code === 0) {
this.logger.log('硬币换银瓜子已完成,获得银瓜子:', response.data.silver)
this.config._lastCompleteTime = tsm()
@@ -36,7 +36,7 @@ class CoinToSilverTask extends BaseModule {
this.status = 'running'
await this.exchange()
} else {
- if (!isNowIn(0, 0, 0, 5)) {
+ if (isNowAfter(0, 5)) {
this.logger.log('今天已经完成过硬币换银瓜子任务了')
this.status = 'done'
} else {
diff --git a/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts b/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts
index a692fc5a..f86df1b4 100644
--- a/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts
+++ b/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts
@@ -15,7 +15,7 @@ class GetYearVipPrivilegeTask extends BaseModule {
useModuleStore().moduleStatus.DailyTasks.OtherTasks.getYearVipPrivilege = s
}
- private type2Name: Record = {
+ private type2Name: Record = {
1: '年度专享B币赠送',
2: '年度专享会员购优惠券',
3: '年度专享漫画礼包 - 漫画福利券',
@@ -27,6 +27,7 @@ class GetYearVipPrivilegeTask extends BaseModule {
15: '年度专享会员购星光宝盒88折券',
16: '大会员专享会员购10魔晶',
17: '大会员专享游戏优惠券',
+ 200: '超级大会员专享观影券',
}
/**
@@ -39,9 +40,15 @@ class GetYearVipPrivilegeTask extends BaseModule {
* 20: 尝试领取后提示:饿了么领取活动已经过期~;
* 21: 尝试领取后提示:超大会员身份状态异常
* 24,25,26,27: 尝试领取后提示:请求错误
- * 200: 尝试领取后提示:当前不符合领取条件
*/
- private blackList: number[] = [8, 14, 18, 19, 20, 21, 24, 25, 26, 27, 200]
+ private blackList = new Set([8, 14, 18, 19, 20, 21, 24, 25, 26, 27])
+
+ /**
+ * 根据权益类型获取权益名称
+ */
+ private getPrivilegeName(type: number): string {
+ return this.type2Name[type] ?? '未知'
+ }
/**
* 获取会员权益
@@ -72,18 +79,16 @@ class GetYearVipPrivilegeTask extends BaseModule {
const response = await BAPI.main.vip.receivePrivilege(type)
this.logger.log(`BAPI.main.vip.receivePrivilege(${type}) response`, response)
if (response.code === 0) {
- this.logger.log(
- `领取年度大会员权益(type = ${type}, ${this.type2Name[type] ?? '未知'})成功`,
- )
+ this.logger.log(`领取年度大会员权益(type = ${type}, ${this.getPrivilegeName(type)})成功`)
} else {
this.logger.error(
- `领取年度大会员权益(type = ${type}, ${this.type2Name[type] ?? '未知'})失败`,
+ `领取年度大会员权益(type = ${type}, ${this.getPrivilegeName(type)})失败`,
response.message,
)
}
} catch (error) {
this.logger.error(
- `领取年度大会员权益(type = ${type}, ${this.type2Name[type] ?? '未知'})出错`,
+ `领取年度大会员权益(type = ${type}, ${this.getPrivilegeName(type)})出错`,
error,
)
}
@@ -113,78 +118,109 @@ class GetYearVipPrivilegeTask extends BaseModule {
* 判断当前账号是否是年度大会员
*/
private isYearVip(): boolean {
- const biliStore = useBiliStore()
- const userInfo = biliStore.userInfo
- if (userInfo!.vip.status === 1 && userInfo!.vip.type === 2) {
- return true
- } else {
- this.logger.log('当前账号不是年度大会员,不领取权益')
+ const vip = useBiliStore().userInfo!.vip
+ return vip.status === 1 && (vip.type === 2 || vip.type === 0)
+ }
+
+ /**
+ * 判断当前账号是否是超级大会员(年度大会员的升级版)
+ */
+ private isSuperVip(): boolean {
+ const vip = useBiliStore().userInfo!.vip
+ return vip.status === 1 && vip.super_vip.is_super_vip // Or vip.type === 0
+ }
+
+ /**
+ * 判断当前账号是否满足权益领取条件
+ */
+ private canReceivePrivilege(
+ privilege: MainData.Vip.MyPrivilege.List,
+ isSuperVip: boolean,
+ ): boolean {
+ if (this.blackList.has(privilege.type)) {
+ return false
+ }
+
+ if (privilege.vip_type === 0 && !isSuperVip) {
+ this.logger.log(
+ `该权益(type = ${privilege.type}, ${this.getPrivilegeName(privilege.type)})需要超级大会员,当前账号不满足领取条件,跳过领取`,
+ )
return false
}
+
+ return true
}
public async run() {
this.logger.log('领取年度大会员权益模块开始运行')
- if (this.isYearVip()) {
- if (ts() >= this.config._nextReceiveTime) {
- // 当前时间已经超过了上次记录的下次领取时间,领取权益
- this.status = 'running'
- const list = await this.myPrivilege()
- if (list) {
- for (const i of list) {
- if (this.blackList.includes(i.type)) {
- // 不领取黑名单中的权益
- continue
+ if (!this.isYearVip()) {
+ this.logger.log('当前账号不是年度大会员,不领取权益')
+ return
+ }
+
+ if (ts() >= this.config._nextReceiveTime) {
+ // 当前时间已经超过了上次记录的下次领取时间,领取权益
+ this.status = 'running'
+ const list = await this.myPrivilege()
+ if (list) {
+ const isSuperVip = this.isSuperVip()
+
+ for (const privilege of list) {
+ if (!this.canReceivePrivilege(privilege, isSuperVip)) {
+ continue
+ }
+
+ if (privilege.state === 0) {
+ if (privilege.type === 9) {
+ // 专属等级加速包
+ await this.addExperience()
+ } else {
+ await this.receivePrivilege(privilege.type)
}
- if (i.state === 0) {
- if (i.type === 9) {
- // 专属等级加速包
- await this.addExperience()
+ } else if (privilege.state === 1) {
+ this.logger.log(
+ `该权益(type = ${privilege.type}, ${this.getPrivilegeName(privilege.type)})已经领取过了`,
+ )
+ } else {
+ // 需要完成任务才能领取
+ if (privilege.type === 9) {
+ const watchTaskConfig = useModuleStore().moduleConfig.DailyTasks.MainSiteTasks.watch
+ if (watchTaskConfig.enabled) {
+ this.logger.log('等待观看视频任务完成后再领取专属等级加速包(10主站经验)...')
+ watch(
+ () => watchTaskConfig._lastCompleteTime,
+ () => sleep(3000).then(() => this.addExperience()),
+ { once: true },
+ )
} else {
- await this.receivePrivilege(i.type)
- }
- } else if (i.state === 1) {
- this.logger.log(
- `该权益(type = ${i.type}, ${this.type2Name[i.type] ?? '未知'})已经领取过了`,
- )
- } else {
- // 需要完成任务才能领取
- if (i.type === 9) {
- const watchTaskConfig = useModuleStore().moduleConfig.DailyTasks.MainSiteTasks.watch
- if (watchTaskConfig.enabled) {
- this.logger.log('等待观看视频任务完成后再领取专属等级加速包(10主站经验)...')
- watch(
- () => watchTaskConfig._lastCompleteTime,
- () => sleep(3000).then(() => this.addExperience()),
- { once: true },
- )
- } else {
- this.logger.warn(
- '领取专属等级加速包(10主站经验)前需要观看任意一个视频,请打开【主站任务】中的【每日观看视频】,或是在运行脚本前手动观看',
- )
- }
+ this.logger.warn(
+ '领取专属等级加速包(10主站经验)前需要观看任意一个视频,请打开【主站任务】中的【每日观看视频】,或是在运行脚本前手动观看',
+ )
}
}
- await sleep(500)
}
- this.status = 'done'
- this.config._nextReceiveTime = Math.min(
- ...list.map((i) => i.period_end_unix).filter((unix) => unix > ts()),
- )
+ await sleep(500)
+ }
+ this.status = 'done'
+ const nextReceiveTimes = list.map((i) => i.period_end_unix).filter((unix) => unix > ts())
+ if (nextReceiveTimes.length > 0) {
+ this.config._nextReceiveTime = Math.min(...nextReceiveTimes)
+ } else {
+ this.config._nextReceiveTime = ts() + 86400
}
}
- // 等待下次运行或什么都不做
- const diff = this.config._nextReceiveTime - ts() + 3e5 // 增加5分钟延时
- if (diff < 86400) {
- this.logger.log(
- '领取年度大会员权益模块下次运行时间:',
- DateTime.fromSeconds(this.config._nextReceiveTime).toJSDate(),
- )
- this.nextRunTimer = setTimeout(() => this.run(), diff * 1000)
- } else {
- this.logger.log('距离下次领取年度大会员权益的时间超过一天,不计划下次运行')
- }
+ }
+
+ const diff = this.config._nextReceiveTime - ts() + 3e5 // 增加5分钟延时
+ if (diff < 86400) {
+ this.logger.log(
+ '领取年度大会员权益模块下次运行时间:',
+ DateTime.fromSeconds(this.config._nextReceiveTime).toJSDate(),
+ )
+ this.nextRunTimer = setTimeout(() => this.run(), diff * 1000)
+ } else {
+ this.logger.log('距离下次领取年度大会员权益的时间超过一天,不计划下次运行')
}
}
}
diff --git a/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts b/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts
index f1f46bff..52d5ad4a 100644
--- a/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts
+++ b/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts
@@ -1,5 +1,5 @@
import BaseModule from '@/modules/BaseModule'
-import { isTimestampToday, delayToNextMoment, tsm, isNowIn } from '@/library/luxon'
+import { isTimestampToday, delayToNextMoment, tsm, isNowBefore } from '@/library/luxon'
import BAPI from '@/library/bili-api'
import type { ModuleStatusTypes } from '@/types'
import { useModuleStore } from '@/stores'
@@ -41,7 +41,7 @@ class SilverToCoinTask extends BaseModule {
this.status = 'running'
this.exchange()
} else {
- if (isNowIn(0, 0, 0, 5)) {
+ if (isNowBefore(0, 5)) {
this.logger.log('昨天的银瓜子换硬币任务已经完成过了,等到今天的00:05再执行')
} else {
this.logger.log('今天已经完成过银瓜子换硬币任务了')
diff --git a/src/modules/default/cookies.ts b/src/modules/default/cookies.ts
index 6226dce3..dd718c38 100644
--- a/src/modules/default/cookies.ts
+++ b/src/modules/default/cookies.ts
@@ -13,7 +13,7 @@ class Cookies extends BaseModule {
* buvid3: 作为参数 buvid 在请求中出现,目前仅在主站 API 中使用
*/
private getCookies(): Promise {
- return Cookie.getAsync(['bili_jct', 'LIVE_BUVID', 'buvid3'], 300, 12e3)
+ return Cookie.getAsync(['bili_jct', 'LIVE_BUVID', 'buvid3'], 12e3)
}
public async run(): Promise {
diff --git a/src/modules/default/fansMedals.ts b/src/modules/default/fansMedals.ts
index 43da4315..85fc4f4f 100644
--- a/src/modules/default/fansMedals.ts
+++ b/src/modules/default/fansMedals.ts
@@ -8,6 +8,11 @@ import ModuleError from '@/library/error/ModuleError'
import _ from 'lodash'
class FansMedals extends BaseModule {
+ /**
+ * 是否正在运行
+ */
+ private isRunning = false
+
/**
* 获取粉丝勋章
*
@@ -50,27 +55,34 @@ class FansMedals extends BaseModule {
}
public async run(force = false): Promise {
- const biliStore = useBiliStore()
+ if (this.isRunning) {
+ return
+ }
+ this.isRunning = true
- const medalTasks = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks
- const taskValues = [medalTasks.light, medalTasks.watch]
+ try {
+ const biliStore = useBiliStore()
- if (
- (force ||
- // 开启了点亮熄灭勋章或观看直播功能且今天没完成过
- taskValues.some((t) => t.enabled && !isTimestampToday(t._lastCompleteTime, 0, 4))) &&
- // 如果正在获取粉丝勋章,不重复获取
- biliStore.fansMedalsStatus !== 'loading'
- ) {
- biliStore.fansMedalsStatus = 'loading'
- biliStore.fansMedals = await this.getFansMedals()
- biliStore.fansMedalsStatus = 'loaded'
- }
+ const medalTasks = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks
+ const taskValues = [medalTasks.light, medalTasks.like, medalTasks.danmu, medalTasks.watch]
+
+ if (
+ force ||
+ // 开启了任意粉丝勋章相关功能且今天没完成过
+ taskValues.some((t) => t.enabled && !isTimestampToday(t._lastCompleteTime, 0, 4))
+ ) {
+ biliStore.fansMedalsStatus = 'loading'
+ biliStore.fansMedals = await this.getFansMedals()
+ biliStore.fansMedalsStatus = 'loaded'
+ }
- this.nextRunTimer = setTimeout(
- () => this.run().catch((reason) => this.logger.error(reason)),
- delayToNextMoment(0, 4).ms,
- )
+ this.nextRunTimer = setTimeout(
+ () => this.run().catch((reason) => this.logger.error(reason)),
+ delayToNextMoment(0, 4).ms,
+ )
+ } finally {
+ this.isRunning = false
+ }
}
}
diff --git a/src/modules/removeElement/index.ts b/src/modules/removeElement/index.ts
index 6ea1b6ab..71ea0db7 100644
--- a/src/modules/removeElement/index.ts
+++ b/src/modules/removeElement/index.ts
@@ -5,8 +5,6 @@ export { default as RemoveElement_RemoveGameParty } from './removeGameParty'
export { default as RemoveElement_RemoveGiftPopover } from './removeGiftPopover'
export { default as RemoveElement_RemoveMicPopover } from './removeMicPopover'
export { default as RemoveElement_RemoveComboCard } from './removeComboCard'
-export { default as RemoveElement_RemoveRank } from './removeRank'
export { default as RemoveElement_RemoveHeaderStuff } from './removeHeaderStuff'
export { default as RemoveElement_RemoveFlipView } from './removeFlipView'
-export { default as RemoveElement_RemoveRecommendRoom } from './removeRecommendRoom'
export { default as RemoveElement_RemoveLiveMosaic } from './removeLiveMosaic'
diff --git a/src/modules/removeElement/removeHeaderStuff.ts b/src/modules/removeElement/removeHeaderStuff.ts
index 093d0fbc..ecaf375e 100644
--- a/src/modules/removeElement/removeHeaderStuff.ts
+++ b/src/modules/removeElement/removeHeaderStuff.ts
@@ -10,7 +10,9 @@ class RemoveHeaderStuff extends BaseModule {
public async run() {
this.logger.log('移除直播画面上方杂项模块开始运行')
- GM_addStyle('.header-info-ctnr .rows-ctnr .lower-row .right-ctnr { display: none !important }')
+ GM_addStyle(
+ '.header-info-ctnr .right-section .right-dynamic-modules { display: none !important }',
+ )
}
}
diff --git a/src/modules/removeElement/removeRank.ts b/src/modules/removeElement/removeRank.ts
deleted file mode 100644
index 76c142fd..00000000
--- a/src/modules/removeElement/removeRank.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { GM_addStyle } from '$'
-import BaseModule from '@/modules/BaseModule'
-import { useModuleStore } from '@/stores'
-
-class RemoveRank extends BaseModule {
- static runOnMultiplePages = true
-
- config = useModuleStore().moduleConfig.RemoveElement.removeRank
-
- public async run() {
- this.logger.log('移除排行榜模块开始运行')
-
- GM_addStyle('.popular-and-hot-rank { display: none !important }')
- }
-}
-
-export default RemoveRank
diff --git a/src/modules/removeElement/removeRecommendRoom.ts b/src/modules/removeElement/removeRecommendRoom.ts
deleted file mode 100644
index ab9659d4..00000000
--- a/src/modules/removeElement/removeRecommendRoom.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { GM_addStyle } from '$'
-import BaseModule from '@/modules/BaseModule'
-import { useModuleStore } from '@/stores'
-
-class RemoveRecommendRoom extends BaseModule {
- static runOnMultiplePages = true
-
- config = useModuleStore().moduleConfig.RemoveElement.removeRecommendRoom
-
- public async run() {
- this.logger.log('移除礼物栏下方推荐直播间模块开始运行')
-
- GM_addStyle('.room-info-ctnr { display: none !important }')
- }
-}
-
-export default RemoveRecommendRoom
diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts
index aaeb3560..2370d3eb 100644
--- a/src/stores/useModuleStore.ts
+++ b/src/stores/useModuleStore.ts
@@ -26,6 +26,8 @@ const defaultModuleStatus: ModuleStatus = {
LiveTasks: {
medalTasks: {
light: '',
+ like: '',
+ danmu: '',
watch: '',
},
},
@@ -93,11 +95,26 @@ export const useModuleStore = defineStore('module', () => {
medalTasks: {
light: () => {
moduleStatus.value.DailyTasks.LiveTasks.medalTasks.light = ''
+ moduleConfig.value.DailyTasks.LiveTasks.medalTasks.light._lastEffectiveCompleteTime = 0
moduleConfig.value.DailyTasks.LiveTasks.medalTasks.light._lastCompleteTime = 0
rerunModule('Default_FansMedals', true)
rerunModule('DailyTask_LiveTask_LightTask')
},
+ like: () => {
+ moduleStatus.value.DailyTasks.LiveTasks.medalTasks.like = ''
+ moduleConfig.value.DailyTasks.LiveTasks.medalTasks.like._lastCompleteTime = 0
+
+ rerunModule('Default_FansMedals', true)
+ rerunModule('DailyTask_LiveTask_LikeTask')
+ },
+ danmu: () => {
+ moduleStatus.value.DailyTasks.LiveTasks.medalTasks.danmu = ''
+ moduleConfig.value.DailyTasks.LiveTasks.medalTasks.danmu._lastCompleteTime = 0
+
+ rerunModule('Default_FansMedals', true)
+ rerunModule('DailyTask_LiveTask_DanmuTask')
+ },
watch: () => {
moduleStatus.value.DailyTasks.LiveTasks.medalTasks.watch = ''
moduleConfig.value.DailyTasks.LiveTasks.medalTasks.watch._lastCompleteTime = 0
diff --git a/src/types/helpInfo.d.ts b/src/types/helpInfo.d.ts
index a7d8e520..b202f84b 100644
--- a/src/types/helpInfo.d.ts
+++ b/src/types/helpInfo.d.ts
@@ -17,6 +17,8 @@ interface HelpInfo {
medalTasks: {
list: HelpInfoItem
light: HelpInfoItem
+ like: HelpInfoItem
+ danmu: HelpInfoItem
watch: HelpInfoItem
}
}
@@ -41,10 +43,8 @@ interface HelpInfo {
removeGiftPopover: HelpInfoItem
removeMicPopover: HelpInfoItem
removeComboCard: HelpInfoItem
- removeRank: HelpInfoItem
removeHeaderStuff: HelpInfoItem
removeFlipView: HelpInfoItem
- removeRecommendRoom: HelpInfoItem
removeLiveMosaic: HelpInfoItem
}
}
diff --git a/src/types/module.d.ts b/src/types/module.d.ts
index a5595263..e722c2e2 100644
--- a/src/types/module.d.ts
+++ b/src/types/module.d.ts
@@ -20,6 +20,8 @@ interface ModulesNeedStatus {
LiveTasks: {
medalTasks: {
light: T
+ like: T
+ danmu: T
watch: T
}
}
diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts
index 9284af2d..25017d64 100644
--- a/src/types/storage.d.ts
+++ b/src/types/storage.d.ts
@@ -24,17 +24,33 @@ interface ModuleConfig {
light: {
enabled: boolean
danmuList: string[]
+ isWhiteList: boolean
+ roomidList: number[]
+ _lastEffectiveCompleteTime: number
+ _lastCompleteTime: number
+ }
+ like: {
+ enabled: boolean
+ isWhiteList: boolean
+ roomidList: number[]
+ _lastCompleteTime: number
+ }
+ danmu: {
+ enabled: boolean
+ danmuList: string[]
+ onlyWhenNotLiving: boolean
+ isWhiteList: boolean
+ roomidList: number[]
_lastCompleteTime: number
}
watch: {
enabled: boolean
- time: number
+ isWhiteList: boolean
+ roomidList: number[]
_watchingProgress: Record
_lastWatchTime: number
_lastCompleteTime: number
}
- isWhiteList: boolean
- roomidList: number[]
}
}
OtherTasks: {
@@ -93,18 +109,12 @@ interface ModuleConfig {
removeComboCard: {
enabled: boolean
}
- removeRank: {
- enabled: boolean
- }
removeHeaderStuff: {
enabled: boolean
}
removeFlipView: {
enabled: boolean
}
- removeRecommendRoom: {
- enabled: boolean
- }
removeLiveMosaic: {
enabled: boolean
}
@@ -124,7 +134,12 @@ interface UiConfig {
isShowPanel: boolean
activeMenuIndex: MenuIndex
panelWidthPercent: number
- medalInfoPanelSortMode: boolean
+ medalInfoPanelIsSortMode: {
+ light: boolean
+ like: boolean
+ danmu: boolean
+ watch: boolean
+ }
}
interface Cache {
diff --git a/tsconfig.node.json b/tsconfig.node.json
index dd00d614..c9b2badc 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -1,14 +1,13 @@
+// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
- "nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
-
"compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
// Bundler mode provides a smoother developer experience.