您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bilibili AD TAGging - tagging ad videos by analyzing danmaku stats
// ==UserScript== // @name BADTAG // @version 1.2.6 // @description Bilibili AD TAGging - tagging ad videos by analyzing danmaku stats // @author jamesliu96 // @license MIT // @namespace https://jamesliu.info/ // @homepage https://gist.github.com/jamesliu96/aeef912d74bdd184cbb6781f1f3939b1 // @match https://*.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @connect bilibili.com // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/protobuf.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant window.onurlchange // ==/UserScript== const RULES = [ /[硬软推]广/, /(?:ying|ruan|tui)guang/i, /广[告子]/, /guang(?:gao|zi)/i, /[恰收]烂?[饭钱费]/, /(?:qia|shou)(?:lan)?(?:fan|qian|fei)/, /猝不及防/, /cubujifang/i, /拼(.)\1+/, /pdd/i, /黄车/, /[百千万亿]+补贴/, /带私?货/, /合作/, /接单/, /商业?单/, /连环?招/, /丝滑/, /大额/, /优惠/, /代金/, /帧起手/, /没完没了/, /定位/, /空降/, /降落/, ]; const THRES = 0.001; // https://raw.githubusercontent.com/SocialSisterYi/bilibili-API-collect/076712a93152c69c7f5437392267925d897a6e63/grpc_api/bilibili/community/service/dm/v1/dm.proto const PROTO = `syntax = "proto3"; package bilibili.community.service.dm.v1; //弹幕 service DM { // 获取分段弹幕 rpc DmSegMobile (DmSegMobileReq) returns (DmSegMobileReply); // 客户端弹幕元数据 字幕、分段、防挡蒙版等 rpc DmView(DmViewReq) returns (DmViewReply); // 修改弹幕配置 rpc DmPlayerConfig (DmPlayerConfigReq) returns (Response); // ott弹幕列表 rpc DmSegOtt(DmSegOttReq) returns(DmSegOttReply); // SDK弹幕列表 rpc DmSegSDK(DmSegSDKReq) returns(DmSegSDKReply); // rpc DmExpoReport(DmExpoReportReq) returns (DmExpoReportRes); } // message Avatar { // string id = 1; // string url = 2; // AvatarType avatar_type = 3; } // enum AvatarType { AvatarTypeNone = 0; // AvatarTypeNFT = 1; // } // message Bubble { // string text = 1; // string url = 2; } // enum BubbleType { BubbleTypeNone = 0; // BubbleTypeClickButton = 1; // BubbleTypeDmSettingPanel = 2; // } // message BubbleV2 { // string text = 1; // string url = 2; // BubbleType bubble_type = 3; // bool exposure_once = 4; // ExposureType exposure_type = 5; } // message Button { // string text = 1; // int32 action = 2; } // message BuzzwordConfig { // repeated BuzzwordShowConfig keywords = 1; } // message BuzzwordShowConfig { // string name = 1; // string schema = 2; // int32 source = 3; // int64 id = 4; // int64 buzzword_id = 5; // int32 schema_type = 6; } // message CheckBox { // string text = 1; // CheckboxType type = 2; // bool default_value = 3; // bool show = 4; } // enum CheckboxType { CheckboxTypeNone = 0; // CheckboxTypeEncourage = 1; // CheckboxTypeColorDM = 2; // } // message CheckBoxV2 { // string text = 1; // int32 type = 2; // bool default_value = 3; } // message ClickButton { // repeated string portrait_text = 1; // repeated string landscape_text = 2; // repeated string portrait_text_focus = 3; // repeated string landscape_text_focus = 4; // RenderType render_type = 5; // bool show = 6; // Bubble bubble = 7; } // message ClickButtonV2 { // repeated string portrait_text = 1; // repeated string landscape_text = 2; // repeated string portrait_text_focus = 3; // repeated string landscape_text_focus = 4; // int32 render_type = 5; // bool text_input_post = 6; // bool exposure_once = 7; // int32 exposure_type = 8; } // 互动弹幕条目信息 message CommandDm { // 弹幕id int64 id = 1; // 对象视频cid int64 oid = 2; // 发送者mid string mid = 3; // 互动弹幕指令 string command = 4; // 互动弹幕正文 string content = 5; // 出现时间 int32 progress = 6; // 创建时间 string ctime = 7; // 发布时间 string mtime = 8; // 扩展json数据 string extra = 9; // 弹幕id str类型 string idStr = 10; } // 弹幕ai云屏蔽列表 message DanmakuAIFlag { // 弹幕ai云屏蔽条目 repeated DanmakuFlag dm_flags = 1; } // 弹幕条目 message DanmakuElem { // 弹幕dmid int64 id = 1; // 弹幕出现位置(单位ms) int32 progress = 2; // 弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2) int32 mode = 3; // 弹幕字号 int32 fontsize = 4; // 弹幕颜色 uint32 color = 5; // 发送者mid hash string midHash = 6; // 弹幕正文 string content = 7; // 发送时间 int64 ctime = 8; // 权重 用于屏蔽等级 区间:[1,10] int32 weight = 9; // 动作 string action = 10; // 弹幕池 0:普通池 1:字幕池 2:特殊池(代码/BAS弹幕) int32 pool = 11; // 弹幕dmid str string idStr = 12; // 弹幕属性位(bin求AND) // bit0:保护 bit1:直播 bit2:高赞 int32 attr = 13; // string animation = 22; // 大会员专属颜色 DmColorfulType colorful = 24; } // 弹幕ai云屏蔽条目 message DanmakuFlag { // 弹幕dmid int64 dmid = 1; // 评分 uint32 flag = 2; } // 云屏蔽配置信息 message DanmakuFlagConfig { // 云屏蔽等级 int32 rec_flag = 1; // 云屏蔽文案 string rec_text = 2; // 云屏蔽开关 int32 rec_switch = 3; } // 弹幕默认配置 message DanmuDefaultPlayerConfig { bool player_danmaku_use_default_config = 1; // 是否使用推荐弹幕设置 bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽 int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级 bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕 bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕 bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕 bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕 bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕 bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕 float player_danmaku_opacity = 12; // 弹幕不透明度 float player_danmaku_scalingfactor = 13; // 弹幕缩放比例 float player_danmaku_domain = 14; // 弹幕显示区域 int32 player_danmaku_speed = 15; // 弹幕速度 bool inline_player_danmaku_switch = 16; // 是否开启弹幕 int32 player_danmaku_senior_mode_switch = 17; // int32 player_danmaku_ai_recommended_level_v2 = 18; // map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 19; // } // 弹幕配置 message DanmuPlayerConfig { bool player_danmaku_switch = 1; // 是否开启弹幕 bool player_danmaku_switch_save = 2; // 是否记录弹幕开关设置 bool player_danmaku_use_default_config = 3; // 是否使用推荐弹幕设置 bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽 int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级 bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕 bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕 bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕 bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕 bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕 bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕 float player_danmaku_opacity = 12; // 弹幕不透明度 float player_danmaku_scalingfactor = 13; // 弹幕缩放比例 float player_danmaku_domain = 14; // 弹幕显示区域 int32 player_danmaku_speed = 15; // 弹幕速度 bool player_danmaku_enableblocklist = 16; // 是否开启屏蔽列表 bool inline_player_danmaku_switch = 17; // 是否开启弹幕 int32 inline_player_danmaku_config = 18; // int32 player_danmaku_ios_switch_save = 19; // int32 player_danmaku_senior_mode_switch = 20; // int32 player_danmaku_ai_recommended_level_v2 = 21; // map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 22; // } // message DanmuPlayerConfigPanel { // string selection_text = 1; } // 弹幕显示区域自动配置 message DanmuPlayerDynamicConfig { // 时间 int32 progress = 1; // 弹幕显示区域 float player_danmaku_domain = 14; } // 弹幕配置信息 message DanmuPlayerViewConfig { // 弹幕默认配置 DanmuDefaultPlayerConfig danmuku_default_player_config = 1; // 弹幕用户配置 DanmuPlayerConfig danmuku_player_config = 2; // 弹幕显示区域自动配置列表 repeated DanmuPlayerDynamicConfig danmuku_player_dynamic_config = 3; // DanmuPlayerConfigPanel danmuku_player_config_panel = 4; } // web端用户弹幕配置 message DanmuWebPlayerConfig { bool dm_switch = 1; // 是否开启弹幕 bool ai_switch = 2; // 是否开启智能云屏蔽 int32 ai_level = 3; // 智能云屏蔽等级 bool blocktop = 4; // 是否屏蔽顶端弹幕 bool blockscroll = 5; // 是否屏蔽滚动弹幕 bool blockbottom = 6; // 是否屏蔽底端弹幕 bool blockcolor = 7; // 是否屏蔽彩色弹幕 bool blockspecial = 8; // 是否屏蔽重复弹幕 bool preventshade = 9; // bool dmask = 10; // float opacity = 11; // int32 dmarea = 12; // float speedplus = 13; // float fontsize = 14; // 弹幕字号 bool screensync = 15; // bool speedsync = 16; // string fontfamily = 17; // bool bold = 18; // 是否使用加粗 int32 fontborder = 19; // string draw_type = 20; // 弹幕渲染类型 int32 senior_mode_switch = 21; // int32 ai_level_v2 = 22; // map<int32, int32> ai_level_v2_map = 23; // } // 弹幕属性位值 enum DMAttrBit { DMAttrBitProtect = 0; // 保护弹幕 DMAttrBitFromLive = 1; // 直播弹幕 DMAttrHighLike = 2; // 高赞弹幕 } message DmColorful { DmColorfulType type = 1; // 颜色类型 string src = 2; // } enum DmColorfulType { NoneType = 0; // 无 VipGradualColor = 60001; // 渐变色 } // message DmExpoReportReq { // string session_id = 1; // int64 oid = 2; // string spmid = 4; } // message DmExpoReportRes {} // 修改弹幕配置-请求 message DmPlayerConfigReq { int64 ts = 1; // PlayerDanmakuSwitch switch = 2; // 是否开启弹幕 PlayerDanmakuSwitchSave switch_save = 3; // 是否记录弹幕开关设置 PlayerDanmakuUseDefaultConfig use_default_config = 4; // 是否使用推荐弹幕设置 PlayerDanmakuAiRecommendedSwitch ai_recommended_switch = 5; // 是否开启智能云屏蔽 PlayerDanmakuAiRecommendedLevel ai_recommended_level = 6; // 智能云屏蔽等级 PlayerDanmakuBlocktop blocktop = 7; // 是否屏蔽顶端弹幕 PlayerDanmakuBlockscroll blockscroll = 8; // 是否屏蔽滚动弹幕 PlayerDanmakuBlockbottom blockbottom = 9; // 是否屏蔽底端弹幕 PlayerDanmakuBlockcolorful blockcolorful = 10; // 是否屏蔽彩色弹幕 PlayerDanmakuBlockrepeat blockrepeat = 11; // 是否屏蔽重复弹幕 PlayerDanmakuBlockspecial blockspecial = 12; // 是否屏蔽高级弹幕 PlayerDanmakuOpacity opacity = 13; // 弹幕不透明度 PlayerDanmakuScalingfactor scalingfactor = 14; // 弹幕缩放比例 PlayerDanmakuDomain domain = 15; // 弹幕显示区域 PlayerDanmakuSpeed speed = 16; // 弹幕速度 PlayerDanmakuEnableblocklist enableblocklist = 17; // 是否开启屏蔽列表 InlinePlayerDanmakuSwitch inlinePlayerDanmakuSwitch = 18; // 是否开启弹幕 PlayerDanmakuSeniorModeSwitch senior_mode_switch = 19; // PlayerDanmakuAiRecommendedLevelV2 ai_recommended_level_v2 = 20; // } // message DmSegConfig { // int64 page_size = 1; // int64 total = 2; } // 获取弹幕-响应 message DmSegMobileReply { // 弹幕列表 repeated DanmakuElem elems = 1; // 是否已关闭弹幕 // 0:未关闭 1:已关闭 int32 state = 2; // 弹幕云屏蔽ai评分值 DanmakuAIFlag ai_flag = 3; repeated DmColorful colorfulSrc = 5; } // 获取弹幕-请求 message DmSegMobileReq { // 稿件avid/漫画epid int64 pid = 1; // 视频cid/漫画cid int64 oid = 2; // 弹幕类型 // 1:视频 2:漫画 int32 type = 3; // 分段(6min) int64 segment_index = 4; // 是否青少年模式 int32 teenagers_mode = 5; // int64 ps = 6; // int64 pe = 7; // int32 pull_mode = 8; // int32 from_scene = 9; } // ott弹幕列表-响应 message DmSegOttReply { // 是否已关闭弹幕 // 0:未关闭 1:已关闭 bool closed = 1; // 弹幕列表 repeated DanmakuElem elems = 2; } // ott弹幕列表-请求 message DmSegOttReq { // 稿件avid/漫画epid int64 pid = 1; // 视频cid/漫画cid int64 oid = 2; // 弹幕类型 // 1:视频 2:漫画 int32 type = 3; // 分段(6min) int64 segment_index = 4; } // 弹幕SDK-响应 message DmSegSDKReply { // 是否已关闭弹幕 // 0:未关闭 1:已关闭 bool closed = 1; // 弹幕列表 repeated DanmakuElem elems = 2; } // 弹幕SDK-请求 message DmSegSDKReq { // 稿件avid/漫画epid int64 pid = 1; // 视频cid/漫画cid int64 oid = 2; // 弹幕类型 // 1:视频 2:漫画 int32 type = 3; // 分段(6min) int64 segment_index = 4; } // 客户端弹幕元数据-响应 message DmViewReply { // 是否已关闭弹幕 // 0:未关闭 1:已关闭 bool closed = 1; // 智能防挡弹幕蒙版信息 VideoMask mask = 2; // 视频字幕 VideoSubtitle subtitle = 3; // 高级弹幕专包url(bfs) repeated string special_dms = 4; // 云屏蔽配置信息 DanmakuFlagConfig ai_flag = 5; // 弹幕配置信息 DanmuPlayerViewConfig player_config = 6; // 弹幕发送框样式 int32 send_box_style = 7; // 是否允许 bool allow = 8; // check box 是否展示 string check_box = 9; // check box 展示文本 string check_box_show_msg = 10; // 展示文案 string text_placeholder = 11; // 弹幕输入框文案 string input_placeholder = 12; // 用户举报弹幕 cid维度屏蔽的正则规则 repeated string report_filter_content = 13; // ExpoReport expo_report = 14; // BuzzwordConfig buzzword_config = 15; // repeated Expressions expressions = 16; // repeated PostPanel post_panel = 17; // repeated string activity_meta = 18; // repeated PostPanelV2 post_panel2 = 19; } // 客户端弹幕元数据-请求 message DmViewReq { // 稿件avid/漫画epid int64 pid = 1; // 视频cid/漫画cid int64 oid = 2; // 弹幕类型 // 1:视频 2:漫画 int32 type = 3; // 页面spm string spmid = 4; // 是否冷启 int32 is_hard_boot = 5; } // web端弹幕元数据-响应 // https://api.bilibili.com/x/v2/dm/web/view message DmWebViewReply { // 是否已关闭弹幕 // 0:未关闭 1:已关闭 int32 state = 1; // string text = 2; // string text_side = 3; // 分段弹幕配置 DmSegConfig dm_sge = 4; // 云屏蔽配置信息 DanmakuFlagConfig flag = 5; // 高级弹幕专包url(bfs) repeated string special_dms = 6; // check box 是否展示 bool check_box = 7; // 弹幕数 int64 count = 8; // 互动弹幕 repeated CommandDm commandDms = 9; // 用户弹幕配置 DanmuWebPlayerConfig player_config = 10; // 用户举报弹幕 cid维度屏蔽 repeated string report_filter_content = 11; // repeated Expressions expressions = 12; // repeated PostPanel post_panel = 13; // repeated string activity_meta = 14; } // message ExpoReport { // bool should_report_at_end = 1; } // enum ExposureType { ExposureTypeNone = 0; // ExposureTypeDMSend = 1; // } // message Expression { // repeated string keyword = 1; // string url = 2; // repeated Period period = 3; } // message Expressions { // repeated Expression data = 1; } // 是否开启弹幕 message InlinePlayerDanmakuSwitch { // bool value = 1; } // message Label { // string title = 1; // repeated string content = 2; } // message LabelV2 { // string title = 1; // repeated string content = 2; // bool exposure_once = 3; // int32 exposure_type = 4; } // message Period { // int64 start = 1; // int64 end = 2; } message PlayerDanmakuAiRecommendedLevel {bool value = 1;} // 智能云屏蔽等级 message PlayerDanmakuAiRecommendedLevelV2 {int32 value = 1;} // message PlayerDanmakuAiRecommendedSwitch {bool value = 1;} // 是否开启智能云屏蔽 message PlayerDanmakuBlockbottom {bool value = 1;} // 是否屏蔽底端弹幕 message PlayerDanmakuBlockcolorful {bool value = 1;} // 是否屏蔽彩色弹幕 message PlayerDanmakuBlockrepeat {bool value = 1;} // 是否屏蔽重复弹幕 message PlayerDanmakuBlockscroll {bool value = 1;} // 是否屏蔽滚动弹幕 message PlayerDanmakuBlockspecial {bool value = 1;} // 是否屏蔽高级弹幕 message PlayerDanmakuBlocktop {bool value = 1;} // 是否屏蔽顶端弹幕 message PlayerDanmakuDomain {float value = 1;} // 弹幕显示区域 message PlayerDanmakuEnableblocklist {bool value = 1;} // 是否开启屏蔽列表 message PlayerDanmakuOpacity {float value = 1;} // 弹幕不透明度 message PlayerDanmakuScalingfactor {float value = 1;} // 弹幕缩放比例 message PlayerDanmakuSeniorModeSwitch {int32 value = 1;} // message PlayerDanmakuSpeed {int32 value = 1;} // 弹幕速度 message PlayerDanmakuSwitch {bool value = 1; bool can_ignore = 2;} // 是否开启弹幕 message PlayerDanmakuSwitchSave {bool value = 1;} // 是否记录弹幕开关设置 message PlayerDanmakuUseDefaultConfig {bool value = 1;} // 是否使用推荐弹幕设置 // message PostPanel { // int64 start = 1; // int64 end = 2; // int64 priority = 3; // int64 biz_id = 4; // PostPanelBizType biz_type = 5; // ClickButton click_button = 6; // TextInput text_input = 7; // CheckBox check_box = 8; // Toast toast = 9; } // enum PostPanelBizType { PostPanelBizTypeNone = 0; // PostPanelBizTypeEncourage = 1; // PostPanelBizTypeColorDM = 2; // PostPanelBizTypeNFTDM = 3; // PostPanelBizTypeFragClose = 4; // PostPanelBizTypeRecommend = 5; // } // message PostPanelV2 { // int64 start = 1; // int64 end = 2; // int32 biz_type = 3; // ClickButtonV2 click_button = 4; // TextInputV2 text_input = 5; // CheckBoxV2 check_box = 6; // ToastV2 toast = 7; // BubbleV2 bubble = 8; // LabelV2 label = 9; // int32 post_status = 10; } // enum PostStatus { PostStatusNormal = 0; // PostStatusClosed = 1; // } // enum RenderType { RenderTypeNone = 0; // RenderTypeSingle = 1; // RenderTypeRotation = 2; // } // 修改弹幕配置-响应 message Response { // int32 code = 1; // string message = 2; } // enum SubtitleAiStatus { None = 0; // Exposure = 1; // Assist = 2; // } // enum SubtitleAiType { Normal = 0; // Translate = 1; // } // 单个字幕信息 message SubtitleItem { // 字幕id int64 id = 1; // 字幕id str string id_str = 2; // 字幕语言代码 string lan = 3; // 字幕语言 string lan_doc = 4; // 字幕文件url string subtitle_url = 5; // 字幕作者信息 UserInfo author = 6; // 字幕类型 SubtitleType type = 7; // string lan_doc_brief = 8; // SubtitleAiType ai_type = 9; // SubtitleAiStatus ai_status = 10; } enum SubtitleType { CC = 0; // CC字幕 AI = 1; // AI生成字幕 } // message TextInput { // repeated string portrait_placeholder = 1; // repeated string landscape_placeholder = 2; // RenderType render_type = 3; // bool placeholder_post = 4; // bool show = 5; // repeated Avatar avatar = 6; // PostStatus post_status = 7; // Label label = 8; } // message TextInputV2 { // repeated string portrait_placeholder = 1; // repeated string landscape_placeholder = 2; // RenderType render_type = 3; // bool placeholder_post = 4; // repeated Avatar avatar = 5; // int32 text_input_limit = 6; } // message Toast { // string text = 1; // int32 duration = 2; // bool show = 3; // Button button = 4; } // message ToastButtonV2 { // string text = 1; // int32 action = 2; } // enum ToastFunctionType { ToastFunctionTypeNone = 0; // ToastFunctionTypePostPanel = 1; // } // message ToastV2 { // string text = 1; // int32 duration = 2; // ToastButtonV2 toast_button_v2 = 3; } // 字幕作者信息 message UserInfo { // 用户mid int64 mid = 1; // 用户昵称 string name = 2; // 用户性别 string sex = 3; // 用户头像url string face = 4; // 用户签名 string sign = 5; // 用户等级 int32 rank = 6; } // 智能防挡弹幕蒙版信息 message VideoMask { // 视频cid int64 cid = 1; // 平台 // 0:web端 1:客户端 int32 plat = 2; // 帧率 int32 fps = 3; // 间隔时间 int64 time = 4; // 蒙版url string mask_url = 5; } // 视频字幕信息 message VideoSubtitle { // 视频原语言代码 string lan = 1; // 视频原语言 string lanDoc = 2; // 视频字幕列表 repeated SubtitleItem subtitles = 3; }`; /** @type {{decode:(b:ArrayLike<number>)=>{elems:{content:string;progress:number}[]}}} */ // eslint-disable-next-line no-undef const DmSegMobileReply = protobuf.parse(PROTO).root.lookup('DmSegMobileReply'); /** @type {Map<string,ReturnType<DmSegMobileReply['decode']>['elems']>} */ const segsCacheMap = new Map(); /** @param {string|undefined} oid */ const getSegs = async (oid) => { if (oid) { const cached = segsCacheMap.get(oid); if (cached) return cached; try { const segs = []; for (let i = 1; ; i++) { const { elems } = DmSegMobileReply.decode( new Uint8Array( await ( await fetch( `https://api.bilibili.com/x/v2/dm/web/seg.so?${new URLSearchParams( { oid, type: 1, segment_index: i, } )}` ) ).arrayBuffer() ) ); if (elems.length) segs.push(...elems); else break; } segsCacheMap.set(oid, segs); return segs; } catch {} } return []; }; const urlBvidRegex = /\/video\/(BV[1-9A-HJ-NP-Za-km-z]{10})/; /** @type {Map<string,string>} */ const urlCidCacheMap = new Map(); /** @returns {Promise<string|undefined>} */ const getCidFromUrl = async (url = location.href) => { const cached = urlCidCacheMap.get(url); if (cached) return cached; try { const sp = new URLSearchParams(new URL(url).search); const bvid = url.match(urlBvidRegex)?.[1] ?? sp.get('bvid'); if (!bvid) return; const p = sp.get('p') ?? 1; const res = await ( await fetch( `https://api.bilibili.com/x/player/pagelist?${new URLSearchParams({ bvid, })}` ) ).json(); if (Array.isArray(res?.data) && res.data.length) { const cid = (res.data.find((d) => d?.page == p) ?? res.data[0])?.cid; if (cid) { urlCidCacheMap.set(url, cid); return cid; } } } catch {} }; /** @returns {Promise<string|undefined>} */ const getCurrentCid = async () => { try { // eslint-disable-next-line no-undef if (__INITIAL_STATE__?.videoData?.cid) { // eslint-disable-next-line no-undef return __INITIAL_STATE__.videoData.cid; } // eslint-disable-next-line no-undef if (__INITIAL_STATE__?.videoInfo?.cid) { // eslint-disable-next-line no-undef return __INITIAL_STATE__.videoInfo.cid; } return await getCidFromUrl(); } catch {} }; const stMap = { '-1': { disabled: true, cursor: 'not-allowed', text: '...', color: 'white', backgroundColor: '#fc8bab', }, 0: { disabled: false, cursor: 'pointer', text: 'BADTAG', color: 'white', backgroundColor: '#fb7299', }, 1: { disabled: false, cursor: 'pointer', text: 'SAFE', color: 'forestgreen', backgroundColor: 'greenyellow', }, 2: { disabled: false, cursor: 'pointer', text: 'UNSAFE', color: 'darkred', backgroundColor: 'orangered', }, }; /** @param {HTMLButtonElement} btn */ const setState = (btn, st = 0) => { switch (st) { case -1: case 1: case 2: { const { disabled, cursor, text, color, backgroundColor } = stMap[st]; btn.disabled = disabled; btn.style.cursor = cursor; btn.textContent = text; btn.style.color = color; btn.style.backgroundColor = backgroundColor; break; } default: { const { disabled, cursor, text, color, backgroundColor } = stMap[0]; btn.disabled = disabled; btn.style.cursor = cursor; btn.textContent = text; btn.style.color = color; btn.style.backgroundColor = backgroundColor; } } }; /** @param {number} d */ const ms2d = (d) => { const ms = `${Math.floor((d % 1000) / 100)}`.padStart(2, '0'); const s = `${Math.floor((d / 1000) % 60)}`.padStart(2, '0'); const m = `${Math.floor((d / (1000 * 60)) % 60)}`.padStart(2, '0'); const h = `${Math.floor((d / (1000 * 60 * 60)) % 24)}`.padStart(2, '0'); return `${h}:${m}:${s}.${ms}`; }; /** @param {number} n */ const n2p = (n) => `${(n * 100).toFixed(2)}%`; /** @param {string} s */ const log = (s, color = '#fb7299', ...rest) => { console.log(`%c[BADTAG] ${s}`, `color:${color}`, ...rest); }; /** @param {string} s */ const match = (s) => RULES.filter((re) => re.test( s .split('') .map((s) => s.trim()) .join('') ) ); const THRES_KEY = 'THRES'; /** @returns {number} */ const getThres = () => GM_getValue(THRES_KEY, THRES); /** @param {number} n */ const setThres = (n) => GM_setValue(THRES_KEY, Math.max(0, Math.min(n, 1))); const menus = new Set(); const setupMenu = (n = getThres()) => { for (const menu of menus) GM_unregisterMenuCommand(menu); menus.clear(); menus.add( GM_registerMenuCommand( `⏫ [+${n2p(0.001)}]`, () => { setThres(getThres() + 0.001); }, 'p' ) ); menus.add( GM_registerMenuCommand( `🔼 [+${n2p(0.0001)}]`, () => { setThres(getThres() + 0.0001); }, 'm' ) ); menus.add( GM_registerMenuCommand( `🍩 ${n2p(n)}${n === THRES ? '' : ` [🔄 ${n2p(THRES)}]`}`, () => { setThres(THRES); }, 'r' ) ); menus.add( GM_registerMenuCommand( `🔽 [-${n2p(0.0001)}]`, () => { setThres(getThres() - 0.0001); }, 'z' ) ); menus.add( GM_registerMenuCommand( `⏬ [-${n2p(0.001)}]`, () => { setThres(getThres() - 0.001); }, 'q' ) ); }; /** * @param {Awaited<ReturnType<typeof getSegs>>} segs * @param {string} cid */ const analyze = (segs, cid) => { const ms = []; for (const seg of segs) { const mr = match(seg.content); if (mr.length) { log(`*HIT* ${ms2d(seg.progress)} "${seg.content}"`, undefined, mr, cid); ms.push({ mr, seg }); } } const m = ms.map(({ mr }) => mr).flat().length; const s = segs.length; const p = s ? m / s : 0; const thres = getThres(); const q = p > thres; const ps = `#${m}/${s}=${n2p(p)}${q ? '>=' : '<'}${n2p(thres)}`; log(`STATS ${ps}`, undefined, cid); if (q) { log('*** UNSAFE ***', 'red', cid); return { ms, ps, st: 2 }; } log('--- SAFE ---', 'green', cid); return { ms, ps, st: 1 }; }; /** @param {Element} elem */ const getCoords = (elem) => { const { top, left, width, height } = elem.getBoundingClientRect(); return { top: top + scrollY, left: left + scrollX, width, height, }; }; /** @param {HTMLElement} elem */ const isVisible = (elem) => Boolean( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); /** @type {Set<Element>} */ const tags = new Set(); const clearTags = () => { for (const tag of tags) tag.remove(); tags.clear(); }; /** @type {Set<Element>} */ const beacons = new Set(); const clearBeacons = () => { for (const beacon of beacons) beacon.remove(); beacons.clear(); }; /** @type {Element} */ let x; /** * @param {HTMLButtonElement} btn * @param {Element} dbg */ const main = async (btn, dbg) => { setState(btn, -1); dbg.textContent = ''; clearTags(); clearBeacons(); const cid = await getCurrentCid(); if (cid) { const { ms, ps, st } = analyze(await getSegs(cid), cid); setState(btn, st); const pb = document.querySelector('.bpx-player-progress'); const spb = document.querySelector('.bpx-player-shadow-progress-area'); // eslint-disable-next-line no-undef const dr = player?.getDuration?.(); if (dr) { /** @param {Element|null} tgt */ const addBeacon = (tgt) => { if (!tgt) return; const { top, left, width, height } = getCoords(tgt); for (const { seg: { progress }, } of ms) { const beacon = document.createElement('div'); beacon.style.position = 'absolute'; beacon.style.width = '1px'; beacon.style.height = `${height}px`; beacon.style.pointerEvents = `none`; beacon.style.backgroundColor = 'red'; beacon.style.zIndex = '1'; beacon.style.top = `${top}px`; beacon.style.left = `${left + (progress / 1000 / dr) * width}px`; beacons.add(beacon); x.appendChild(beacon); } }; addBeacon(pb); addBeacon(spb); } const pc = document.createElement('span'); pc.style.color = 'white'; pc.style.backgroundColor = '#fb7299'; pc.textContent = `${cid}\n`; const pss = document.createElement('span'); const { color, backgroundColor } = stMap[st]; pss.style.color = color; pss.style.backgroundColor = backgroundColor; pss.textContent = `${ps}\n`; dbg.append( pc, pss, ...ms .sort((a, b) => a.seg.progress - b.seg.progress) .map(({ mr, seg }) => { const t = document.createElement('span'); t.style.color = 'blue'; t.style.cursor = 'pointer'; t.textContent = ms2d(seg.progress); t.addEventListener('click', () => { // eslint-disable-next-line no-undef player?.seek?.(seg.progress / 1000); }); t.addEventListener('mouseenter', () => { t.style.fontWeight = 'bold'; }); t.addEventListener('mouseleave', () => { t.style.fontWeight = ''; }); return [ t, seg.content, ...mr.map((r) => { const rr = document.createElement('span'); rr.style.color = 'brown'; rr.textContent = `${r}`; return rr; }), '\n', ]; }) .flat() ); } else setState(btn); for (const elem of [...document.querySelectorAll('a')].filter( (e) => urlBvidRegex.test(e.href) && isVisible(e) )) { (async (x) => { const cid = await getCidFromUrl(elem.href); if (cid) { const { ps, st } = analyze(await getSegs(cid), cid); const tag = document.createElement('div'); tag.style.position = 'absolute'; tag.style.fontSize = '12px'; tag.style.fontWeight = 'bold'; tag.style.userSelect = 'none'; tag.style.zIndex = '2'; const { top, left } = getCoords(elem); tag.style.top = `${top}px`; tag.style.left = `${left}px`; const { text, color, backgroundColor } = stMap[st]; tag.style.color = color; tag.style.backgroundColor = backgroundColor; tag.textContent = text; tag.title = `${cid} ${ps}`; tag.addEventListener('click', () => { tag.style.visibility = 'hidden'; }); tag.addEventListener('mouseenter', () => { tag.style.opacity = '0.2'; }); tag.addEventListener('mouseleave', () => { tag.style.opacity = ''; }); tag.__TARGET__ = elem; tags.add(tag); x.appendChild(tag); } })(x); } }; addEventListener('load', () => { x = document.createElement('div'); const btn = document.createElement('button'); btn.style.position = 'fixed'; btn.style.bottom = '0'; btn.style.right = '0'; btn.style.width = '90px'; btn.style.height = '34px'; btn.style.border = 'none'; btn.style.borderTopLeftRadius = '6px'; btn.style.fontSize = '14px'; btn.style.fontWeight = 'bold'; btn.style.outline = '0'; btn.style.zIndex = '10'; btn.title = `${n2p(getThres())}`; const dbg = document.createElement('div'); dbg.style.position = 'fixed'; dbg.style.bottom = '34px'; dbg.style.right = '0'; dbg.style.maxHeight = 'calc(100% - 98px)'; dbg.style.overflowY = 'scroll'; dbg.style.whiteSpace = 'pre'; dbg.style.fontSize = '12px'; dbg.style.color = 'black'; dbg.style.opacity = '0.2'; dbg.style.backgroundColor = 'transparent'; dbg.style.visibility = 'hidden'; dbg.style.zIndex = '10'; btn.addEventListener('click', () => main(btn, dbg)); btn.addEventListener('contextmenu', (e) => { e.preventDefault(); dbg.style.visibility = dbg.style.visibility === 'hidden' ? '' : 'hidden'; }); dbg.addEventListener('mouseenter', () => { dbg.style.opacity = '1'; dbg.style.backgroundColor = 'white'; }); dbg.addEventListener('mouseleave', () => { dbg.style.opacity = '0.2'; dbg.style.backgroundColor = 'transparent'; }); x.appendChild(btn); x.appendChild(dbg); document.body.appendChild(x); const reset = () => { setState(btn); dbg.textContent = ''; clearTags(); clearBeacons(); }; addEventListener('urlchange', reset); reset(); setupMenu(); GM_addValueChangeListener(THRES_KEY, (key, oldValue, newValue) => { log(`set ${key} from ${n2p(oldValue)} to ${n2p(newValue)}`); setupMenu(newValue); btn.title = `${n2p(newValue)}`; }); });