ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

哔哩哔哩 Linux 客户端番剧动态修复:bvid 失效时如何通过 dynamic_id 链路还原视频信息

哔哩哔哩 Linux 客户端番剧动态修复:bvid 失效时如何通过 dynamic_id 链路还原视频信息 桌面应用音视频【免费下载链接】bilibili-linux基于哔哩哔哩官方客户端移植的Linux版本 支持漫游项目地址https://gitcode.com/gh_mirrors/bi/bilibili-linux点击查看免费下载本文基于开源项目 bilibili-linux哔哩哔哩官方客户端的 Linux 移植版支持漫游的 docs/DynamicAnime.md 展开完整还原了项目中番剧类视频信息修复的设计思路当以 bvid 请求视频详情接口失败时如何借助动态Dynamic数据流建立bvid ↔ dynamic_id映射进而通过动态详情接口取出ep_id并重定向到正确的番剧播放页。读完本文你将掌握这套备用取数链路的完整实现脉络包括动态流拦截、IndexedDB 映射存储、HTTP/gRPC 双通道取详情、以及番剧数据重组等关键技术点。背景为什么用 bvid 走 detail 会获取失败项目在漫游与番剧播放场景中官方客户端页面依赖https://api.bilibili.com/x/web-interface/view/detail这类接口获取视频详情。对于番剧PGC内容仅凭bvid请求该接口时经常拿不到数据——原因从源码可以推断番剧条目在 Web 端详情接口中并不总是以普通稿件UGC形式暴露视频信息实际托管在 PGC 体系中需要ep_id/season_id才能正确定位。原文档的第一条结论正是使用 bvid 通过 detail 是会获取失败的因此项目必须寻找另一条不依赖 bvid 的取数路径动态Dynamic数据流。UP 主发布番剧动态时动态卡片中同时携带了稿件bvid与动态自身 IDdynamic_id而动态详情get_dynamic_detail返回的card字段里又能解析出ep_id。于是一条完整的替补链路应运而生。整体方案三条数据通路串成一条修复链原文档用 4 个步骤概括了整条链路结合 src/extension/document/response-replace.ts 的实现可以归纳为三个环节收集映射在动态流接口响应上做拦截把bvid与dynamic_id关联起来存入 IndexedDBRecordbvid, dynamic_id的落地形态取动态详情当view/detail以 bvid 查询失败时用映射查回dynamic_id通过动态详情接口拿到完整的动态卡片提取 ep_id 并重定向从动态卡片中解析ep_id用动态数据重组成番剧详情响应并把播放页重定向到https://www.bilibili.com/bangumi/play/ep...。整个过程对应源码中的三处核心实现环节关键文件作用动态流拦截与映射落库response-replace.ts 中x/polymer/web-dynamic/v1/feed/all与x/polymer/web-dynamic/desktop/v1/feed/all两个 Hook遍历动态列表写入bvid → dynamic_id动态详情获取bilibili-api.ts 的getDynamicDetail()与 electron-tool.ts 的roaming/queryDynamicDetail分别走 HTTP 与 gRPC 两个通道详情重组与跳转utils.ts 的genVideoDetailByDynamicDetail()与view/detailHook构造视频详情数据结构、提取ep_id第一步拦截动态流建立 bvid ↔ dynamic_id 映射原文档第 2 步要求在取到动态信息时想办法存入 dynamic_id可以关联 bvid 和 dynamic_id。项目通过XMLHttpRequest/fetch响应替换机制对动态 feed 接口做 Hook。动态流接口一Web 端 feed在 response-replace.ts 中拦截https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/allhttps://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all: async (req: CustomXMLHttpRequest) { log.info(动态处理1...) const resp JSON.parse(req.responseText) if (resp.code 0) { try { const db new CustomIndexedDB() await db.open(); const { items } resp.data for (const item of items) { if (item.modules.module_author.mid 11783021) { await db.putBvid2DynamicId({ bvid: item.modules.module_dynamic.major.archive.bvid, dynamic_id: item.id_str }) } } } catch (e) { log.error(动态信息1:, e) } } },关键点过滤条件仅收集module_author.mid 11783021的动态源码中硬编码的 UP 主 mid从上下文看专门用于番剧类动态收集bvid 来源item.modules.module_dynamic.major.archive.bviddynamic_id 来源item.id_str字符串形式的动态 ID避免大整数精度丢失。动态流接口二桌面端 feed同一文件 response-replace.ts 还 Hook 了//api.bilibili.com/x/polymer/web-dynamic/desktop/v1/feed/all兼容桌面端动态结构//api.bilibili.com/x/polymer/web-dynamic/desktop/v1/feed/all: async (req: CustomXMLHttpRequest) { const resp JSON.parse(req.responseText) if (resp.code 0) { try { const db new CustomIndexedDB() await db.open(); const { items } resp.data for (const item of items) { if (item.modules[0].module_author.user.mid 11783021) { await db.putBvid2DynamicId({ bvid: item.modules[1].module_dynamic.dyn_archive.bvid, dynamic_id: item.id_str }) } } } catch (e) { ... } } },注意两个接口的数据结构差异Web 端作者信息在item.modules.module_author.mid稿件在item.modules.module_dynamic.major.archive.bvid桌面端作者信息在item.modules[0].module_author.user.mid稿件在item.modules[1].module_dynamic.dyn_archive.bvid。这正是原文档第 2 步想办法存入的落地细节——两套动态接口都要覆盖才能保证映射表足够完整。映射落库IndexedDB映射关系不是内存临时变量而是持久化在 IndexedDB 中。参见 src/extension/common/db.ts数据库名Bvid2DynamicId版本2对象仓库objectStore名b2dkeyPath: bvid即以 bvid 为主键putBvid2DynamicId(b2d)写入一条映射getBvid2DynamicId(bvid)按 bvid 反查dynamic_id。对应的数据类型定义在 src/extension/common/types.tsexport interface Bvid2DynamicIdType { bvid: string dynamic_id: string }keyPath: bvid天然保证同一条动态重复出现时只保留最新记录与文档中Recordbvid, dynamic_id的设计意图完全一致。第二步动态详情获取——HTTP 与 gRPC 双通道原文档第 3 步给出动态详情接口获取动态详情https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id通道一HTTP 接口封装该接口在 bilibili-api.ts 中被封装为getDynamicDetail()getDynamicDetail(dynamicId: string) { const url https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id${dynamicId} return GET(url).then(res { const resp JSON.parse(res.responseText) if (resp.code 0) { return Promise.resolve(resp) } return Promise.reject(resp) }) }入参为dynamic_id来自上一步的映射表resp.code 0视为成功返回体中的data即动态详情对象后续用于构造视频详情。通道二gRPC 动态详情手机端通道HTTP 接口之外项目还通过 Electron 主进程的 IPC 通道调用 gRPC 接口获取手机端动态详情见 src/inject/common/electron-tool.tsipcMain.handle( roaming/queryDynamicDetail, async (_, dynamicId, accessKey) { const transport new GrpcTransport({ host: grpc.biliapi.net, channelCredentials: ChannelCredentials.createSsl(), clientOptions: { grpc.primary_user_agent: Dalvik/2.1.0 ... } }); const client new DynamicClient(transport); const meta: RpcMetadata { x-bili-gaia-vtoken: , x-bili-aurora-eid: UlcBQFgHB1M, x-bili-trace-id: ..., x-bili-fawkes-req-bin: CglhbmRyb2lkNjQSBHByb2QaCDlhMjU2NWM2, }; // 组装 x-bili-metadata-bin、x-bili-device-bin 等固定二进制元数据 const reqData { dynamicId: ${dynamicId} }; const result await client.dynDetail(reqData, { meta }); return DynDetailReply.toJson(result.response, { enumAsInteger: false, useProtoFieldName: true }); } );该通道对应的 proto 定义在 res/protos/dynamic.proto// v2动态, rpc 按字母顺序排列 service Dynamic { // 动态详情页 rpc DynDetail(DynDetailReq) returns (DynDetailReply); } message DynDetailReply { // 动态详情 DynamicItem item 1; } message DynDetailReq { // 动态ID string dynamic_id 2; }请求携带dynamic_id响应返回DynamicItem item。生成的 TypeScript 客户端代码位于 src/inject/common/dynamic.ts其中dynamic_id为 protobuf string 字段field 2同时动态卡片内还定义了avidint64field 7与bvidstringfield 26字段。为什么需要双通道从 response-replace.ts 的调用顺序可以看到Web 动态详情与手机端动态详情的返回结构并不一致两者配合才能既拿到标准card数据HTTP 通道又拿到含dyn_archive.uri的完整模块数据gRPC 通道后者正是提取ep_id的关键。第三步从 card 中提取 ep_id重组番剧详情并重定向原文档第 4 步card 里面可以取到 ep_id完整逻辑位于view/detail的 Fetch Hook 中见 response-replace.tshttps://api.bilibili.com/x/web-interface/view/detail: async (data: FetchReplaceType) { const resp await data.res.clone().json() try { if (resp.code ! 0) { // 1. 用 bvid 反查 dynamic_id const db new CustomIndexedDB() await db.open() const b2d await db.getBvid2DynamicId(params.bvid) if (!b2d) return data.res // 2. 双通道获取动态详情 const bili new BiliBiliApi(); const detail await bili.getDynamicDetail(b2d.dynamic_id) const dynamicDetail await window.biliBridge.callNative(roaming/queryDynamicDetail, b2d.dynamic_id, UTILS.getAccessToken()) // 3. 从 module_dynamic 的 uri 中提取 ep_id const dynamic dynamicDetail.item.modules.find((e) e.module_type module_dynamic) const epid dynamic.module_dynamic.dyn_archive.uri.match(/ep\d/)[0] // 4. 用动态卡片数据重组成 view/detail 响应结构 const res await UTILS.genVideoDetailByDynamicDetail(detail.data) res.View.redirect_url https://www.bilibili.com/bangumi/play/${epid} // 5. 用修复后的数据覆盖原响应 (data.res as any).data { code: 0, message: , msg: , data: res } } } catch (e) { ... } return data.res }ep_id 的提取位置这里提取的并不是 HTTP 动态详情card里的字段而是手机端 gRPC 动态详情中module_dynamic.dyn_archive.uri里的番剧地址段uri.match(/ep\d/)会从类似bilibili://bangumi/play/ep123456的 URI 中摘出ep123456。之后redirect_url被设置为https://www.bilibili.com/bangumi/play/${epid}页面即可跳转到正确的番剧播放页。值得一提的关联点ep_id在漫游链路中还与season_id建立缓存映射window.epId2seasonId[ep_id]见 response-replace.ts番剧播放 URL 的获取会优先使用season_idep_id作为兜底入参传入/pgc/player/api/playurl等接口详见 bilibili-api.ts。响应数据结构重组genVideoDetailByDynamicDetail()位于 utils.ts它把动态详情的card转成view/detail所需的完整响应骨架async genVideoDetailByDynamicDetail(dynamicDetail: Recordstring, any) { const res { View: {}, Card: {}, Tags: [], Reply: {}, Related: [], Spec: null, hot_share: { show: false, list: [] }, elec: {}, recommend: null, view_addit: {}, guide: null, query_tags: null, is_old_user: false, } const card JSON.parse(dynamicDetail.card.card) if (card.rights) { card.rights.download 1 } res.View card const resp await new BiliBiliApi().getUserCard(card.owner.mid) res.Card resp.data return res }要点View直接取自动态卡片card.card其中含标题、封面、播放地址等番剧信息强制开启card.rights.download 1保证下载能力在修复链路中可用CardUP 主信息通过getUserCard(card.owner.mid)单独请求 src/extension/common/bilibili-api.ts 中的x/web-interface/card接口补齐返回结构与 Web 端view/detail的标准字段View/Card/Tags/Reply/Related等保持一致从而让上层页面无需改动即可消费这份假数据。完整调用链总览把上述三块拼起来bvid 失败场景下的完整修复链路为用户打开番剧页 → view/detail(bvid) 返回 code ! 0 → CustomIndexedDB.getBvid2DynamicId(bvid) [db.ts] → 未命中映射 → 原样返回无法修复 → 命中映射 → ├─ BiliBiliApi.getDynamicDetail(dynamic_id) [bilibili-api.ts] HTTP 通道 └─ biliBridge.callNative(roaming/queryDynamicDetail, ...) [electron-tool.ts] gRPC 通道 → 从 gRPC 动态的 module_dynamic.dyn_archive.uri 提取 ep_id → genVideoDetailByDynamicDetail(card) 重组响应 [utils.ts] → View.redirect_url bangumi/play/epXXX → 覆盖 view/detail 响应code 置 0页面恢复正常而映射表的数据来源又依赖前置步骤中两个动态 feed 接口的持续拦截写入putBvid2DynamicId。也就是说用户必须先浏览过包含目标番剧的动态流映射表中才会有对应的dynamic_id这是该方案生效的前提条件。局限性与设计启示从源码结构可以总结出这套方案的特点与限制依赖动态流数据映射表只在动态 feed 被拦截并写入后才可用如果从未加载过该番剧的动态getBvid2DynamicId返回空修复链路会直接放弃if (!b2d) return data.res。硬编码 mid 过滤目前只收集 mid 为11783021的 UP 主动态映射覆盖面与该账号的动态发布范围强相关属于针对性修复而非通用兜底。双通道冗余HTTP 与 gRPC 两套动态详情接口结构不同代码里同时保留是为了互补——HTTP 通道提供标准card数据gRPC 通道提供含dyn_archive.uri的模块化数据ep_id的提取依赖后者。proto 与生成代码对应gRPC 通道的请求/响应结构以 res/protos/dynamic.proto 为准运行时客户端由 src/inject/common/dynamic.ts 提供二者需保持同步。这套备用取数链路的通用价值在于当主接口按 bvid 取详情对特定内容类型失效时利用动态数据流中的冗余信息dynamic_id → card → ep_id建立旁路最终在不改动上层页面的前提下还原出完整可用的番剧详情响应是 Electron 客户端中响应替换 旁路数据修复模式的典型实现。赞分享桌面应用音视频【免费下载链接】bilibili-linux基于哔哩哔哩官方客户端移植的Linux版本 支持漫游项目地址https://gitcode.com/gh_mirrors/bi/bilibili-linux点击查看免费下载相关推荐Linux用户如何畅享B站哔哩哔哩Linux客户端完整使用指南Linux用户如何畅享B站哔哩哔哩Linux客户端完整使用指南 作为一名Linux爱好者你是否曾经遇到过这样的困扰想要在Linux系统上观看B站视频却发桌面应用音视频【亲测免费】 哔哩哔哩 Linux 客户端项目推荐哔哩哔哩 Linux 客户端项目推荐 项目基础介绍和主要编程语言 哔哩哔哩 Linux 客户端 是一个基于哔哩哔哩官方客户端移植的开源项目专门为 Linux桌面应用音视频如何在Linux系统上安装哔哩哔哩客户端完整指南 如何在Linux系统上安装哔哩哔哩客户端完整指南 哔哩哔哩Linux版是基于官方客户端移植的开源项目专为Linux用户打造支持漫游、弹幕共享和自定义桌面应用音视频上一篇如何快速上手 tf_efficientnet_b1.ns_jft_in1k5分钟完成图像分类部署下一篇跨浏览器兼容性挑战easytimer.js 如何优雅支持IE9到现代浏览器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表