From cc50ef35be8e001032d5781bb6cd09057950704f Mon Sep 17 00:00:00 2001 From: juening2000 <156571145+juening2000@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:28:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=AE=8C=E5=96=84=E5=B9=B6=E6=8F=90?= =?UTF-8?q?=E5=8D=87B=E7=AB=99=E7=9B=B4=E6=92=AD=E7=94=BB=E8=B4=A8?= =?UTF-8?q?=EF=BC=8C=E4=B8=94=E6=94=AF=E6=8C=81=E7=95=AA=E5=89=A7=E6=92=AD?= =?UTF-8?q?=E6=94=BE(hdr=E7=9C=9F=E5=BD=A9)=EF=BC=8C=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E7=9B=B4=E6=92=ADAPI=E5=88=B0v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增番剧/剧集播放支持:通过获取番剧季数据提取bvid/cid/aid进行播放 - 直播流升级至v2 playurl API,默认avc编码 - 优化PGC视频处理:aid已知时跳过冗余view API调用,统一使用playurl接口 - 完善直播qn画质映射,增加accept_qn自动降级逻辑,兼容HDR高画质流 --- Media/PlayParse/MediaPlayParse - Bilibili.as | 374 ++++++++++++++----- 1 file changed, 287 insertions(+), 87 deletions(-) diff --git a/Media/PlayParse/MediaPlayParse - Bilibili.as b/Media/PlayParse/MediaPlayParse - Bilibili.as index ab0ac53..61650d8 100644 --- a/Media/PlayParse/MediaPlayParse - Bilibili.as +++ b/Media/PlayParse/MediaPlayParse - Bilibili.as @@ -381,7 +381,7 @@ string makeWebUrl(string path) { string Video(string bvid, const string &in path, dictionary &MetaData, array &QualityList) { log("--------------------------------------------------"); string res; - string aid; + string aid = parse(path, "aid"); string title; string url; JsonReader reader; @@ -392,8 +392,11 @@ string Video(string bvid, const string &in path, dictionary &MetaData, array subtitle; - res = apiPost("/x/web-interface/view?bvid=" + bvid); + + if (aid.empty()) { + res = apiPost("/x/web-interface/view?bvid=" + bvid); if (res.empty()) { + log("Video view API response EMPTY", bvid); return url; } if (reader.parse(res, root) && root.isObject()) { @@ -421,9 +424,13 @@ string Video(string bvid, const string &in path, dictionary &MetaData, array= 0) { webUrl = redirect_url.asString(); ispgc = true; + ep_id = HostRegExpParse(redirect_url.asString(), "bangumi/play/ep([0-9]+)"); + log("Video ispgc detected", webUrl); + log("Video ep_id", ep_id); } MetaData["webUrl"] = makeWebUrl(webUrl); if (ConfigData.danmakuEnable) { @@ -433,9 +440,15 @@ string Video(string bvid, const string &in path, dictionary &MetaData, array 0) { - array chapt; - for (int i = 0; i < clip_info_list.size(); i++) { - JsonValue chapter = clip_info_list[i]; - if (chapter.isObject()) { - if (chapter["clipType"].asString() == "CLIP_TYPE_OP") { - dictionary startItem; - startItem["title"] = "哔哩哔哩-片头"; - startItem["time"] = formatUInt((chapter["start"].asInt() == 0 ? chapter["start"].asInt() : chapter["start"].asInt() + 1) * 1000); - chapt.insertLast(startItem); - dictionary contentItem; - contentItem["title"] = "哔哩哔哩-正片"; - contentItem["time"] = formatUInt((chapter["end"].asInt() - 1) * 1000); - chapt.insertLast(contentItem); - } else if (chapter["clipType"].asString() == "CLIP_TYPE_ED") { - dictionary endItem; - endItem["title"] = "哔哩哔哩-片尾"; - endItem["time"] = formatUInt((chapter["start"].asInt() + 1) * 1000); - chapt.insertLast(endItem); - } - } - } - if (!chapt.empty() && (@QualityList !is null)) { - MetaData["chapter"] = chapt; - } - } - } else { - data = root["data"]; - } + JsonValue data = root["data"]; if (data["dash"].isObject()) { JsonValue videos = data["dash"]["video"]; @@ -579,7 +562,27 @@ string Video(string bvid, const string &in path, dictionary &MetaData, array 0) { + url = data["durl"][0]["url"].asString(); + } + if (data["dash"].isObject() && (!data["durl"].isArray() || data["durl"].size() == 0)) { + if (!ispgc) { + string flv_res = apiPost("/x/player/playurl?avid=" + aid + "&cid=" + cid + "&qn=" + qn + "&fnval=16&fourk=1"); + if (!flv_res.empty()) { + JsonValue flv_root; + if (reader.parse(flv_res, flv_root) && flv_root.isObject()) { + if (flv_root["code"].asInt() == 0) { + JsonValue flv_data = flv_root["data"]; + if (flv_data["durl"].isArray() && flv_data["durl"].size() > 0) { + url = flv_data["durl"][0]["url"].asString(); + } + } + } + } + } + } + if (!data["dash"].isObject() && data["durl"].isArray()) { url = data["durl"][0]["url"].asString(); qn = data["quality"].asInt(); dictionary qualityitem; @@ -595,22 +598,13 @@ string Video(string bvid, const string &in path, dictionary &MetaData, array Banggumi(string id, string type) { type = "season_id"; } string url = "/pgc/view/web/season?" + type + "=" + id; + log("Banggumi request", url); string res = apiPost(url); if (res.empty()) { + log("Banggumi response EMPTY"); return videos; } if (Reader.parse(res, Root) && Root.isObject()) { if (Root["code"].asInt() != 0) { + log("Banggumi code != 0", Root["code"].asInt()); + log("Banggumi message", Root["message"].asString()); return videos; } JsonValue episodes = Root["result"]["episodes"]; @@ -1599,6 +1599,17 @@ string getCodec(int codecid) { return "未知"; } +string getLiveQuality(int qn) { + if (qn == 20000) return "4K超高清"; + if (qn == 15000) return "2K(原画)"; + if (qn == 10000) return "1080p(原画)"; + if (qn == 400) return "1080p(蓝光)"; + if (qn == 250) return "720p(超清)"; + if (qn == 150) return "480p(高清)"; + if (qn == 80) return "360p(流畅)"; + return ""; +} + string Audio(const string &in path, dictionary &MetaData, array &QualityList) { string id = HostRegExpParse(path, "/audio/au([0-9]+)"); JsonReader Reader; @@ -1638,7 +1649,6 @@ string Audio(const string &in path, dictionary &MetaData, array &Qua string Live(string id, const string &in path, dictionary &MetaData, array &QualityList) { string url = ""; int room_id = 0; - int qn = 10000; string res = post("https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=" + id); JsonReader Reader; JsonValue Root; @@ -1661,57 +1671,152 @@ string Live(string id, const string &in path, dictionary &MetaData, array 1) { - qualityitem2["url"] = data[1]["url"].asString(); - } - } else { - string quality_res = post("https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl?cid=" + room_id + "&platform=web&qn=" + quality + "&https_url_req=1&ptype=16"); - JsonValue temp; - if (Reader.parse(quality_res, temp) && temp.isObject()) { - if (temp["code"].asInt() != 0) { - continue; + JsonValue playurl = Root["data"]["playurl_info"]["playurl"]; + JsonValue streams = playurl["stream"]; + JsonValue g_qn_desc = playurl["g_qn_desc"]; + + JsonValue accept_qn; + bool accept_collected = false; + string cached_url; + string cached_codec; + int cached_qn = 0; + + if (streams.isArray()) { + for (int s = 0; s < streams.size(); s++) { + JsonValue formats = streams[s]["format"]; + if (formats.isArray()) { + for (int f = 0; f < formats.size(); f++) { + JsonValue codecs = formats[f]["codec"]; + if (codecs.isArray()) { + for (int c = 0; c < codecs.size(); c++) { + JsonValue codec = codecs[c]; + int current_qn = codec["current_qn"].asInt(); + string codec_name = codec["codec_name"].asString(); + string base_url = codec["base_url"].asString(); + JsonValue url_infos = codec["url_info"]; + string protocol_name = streams[s]["protocol_name"].asString(); + string format_name = formats[f]["format_name"].asString(); + + if (!accept_collected) { + accept_qn = codec["accept_qn"]; + accept_collected = true; + } + + if (url_infos.isArray() && url_infos.size() > 0) { + int best_idx = 0; + for (int ui = 0; ui < url_infos.size(); ui++) { + if (url_infos[ui]["host"].asString().find("gotcha") >= 0) { + best_idx = ui; + break; + } + } + string stream_url = url_infos[best_idx]["host"].asString() + base_url + url_infos[best_idx]["extra"].asString(); + log("stream", protocol_name + " " + format_name + " " + codec_name + " qn=" + current_qn + " host=" + url_infos[best_idx]["host"].asString()); + if (url.empty() && protocol_name == "http_stream" && format_name == "flv" && codec_name == "avc") { + url = stream_url; + } + if (cached_url.empty()) { + cached_url = stream_url; + cached_codec = codec_name; + cached_qn = current_qn; + } else if (codec_name == "avc" && cached_codec != "avc" && current_qn == cached_qn) { + cached_url = stream_url; + cached_codec = codec_name; + } + } + } } - JsonValue qyality_data = temp["data"]["durl"]; - if (qyality_data.isArray()) { - qualityitem["url"] = qyality_data[0]["url"].asString(); - if (qyality_data.size() > 1) { - qualityitem2["url"] = qyality_data[1]["url"].asString(); + } + } + } + } + + if (@QualityList !is null && accept_qn.isArray()) { + for (int qi = 0; qi < accept_qn.size(); qi++) { + int qn = accept_qn[qi].asInt(); + string label = getLiveQuality(qn); + if (label.empty() && g_qn_desc.isArray()) { + for (int di = 0; di < g_qn_desc.size(); di++) { + if (g_qn_desc[di]["qn"].asInt() == qn) { + label = g_qn_desc[di]["desc"].asString(); + break; + } + } + } + if (label.empty()) continue; + + string qn_url; + string qn_codec; + if (qn == cached_qn) { + qn_url = cached_url; + qn_codec = cached_codec; + } else { + string qn_res = post("https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=" + room_id + + "&platform=web&protocol=0,1&format=0,1,2&codec=0,1&qn=" + qn + "&dolby=5&panorama=1"); + JsonValue qn_root; + if (Reader.parse(qn_res, qn_root) && qn_root.isObject() && qn_root["code"].asInt() == 0) { + JsonValue qn_streams = qn_root["data"]["playurl_info"]["playurl"]["stream"]; + if (qn_streams.isArray() && qn_streams.size() > 0) { + JsonValue qn_formats = qn_streams[0]["format"]; + if (qn_formats.isArray() && qn_formats.size() > 0) { + JsonValue qn_codecs = qn_formats[0]["codec"]; + if (qn_codecs.isArray() && qn_codecs.size() > 0) { + for (int ci = 0; ci < qn_codecs.size(); ci++) { + JsonValue qc = qn_codecs[ci]; + string cn = qc["codec_name"].asString(); + JsonValue ui = qc["url_info"]; + if (ui.isArray() && ui.size() > 0) { + int best_ui = 0; + for (int uj = 0; uj < ui.size(); uj++) { + if (ui[uj]["host"].asString().find("gotcha") >= 0) { + best_ui = uj; + break; + } + } + string new_url = ui[best_ui]["host"].asString() + qc["base_url"].asString() + ui[best_ui]["extra"].asString(); + if (qn_url.empty() || cn == "avc") { + qn_url = new_url; + qn_codec = cn; + } + } + } } } } } - int itag = getItag(quality); - qualityitem["quality"] = qualities[i]["desc"].asString(); - qualityitem["qualityDetail"] = qualityitem["quality"]; - qualityitem["itag"] = itag; - QualityList.insertLast(qualityitem); + } + + if (!qn_url.empty()) { + string codec_suffix; + if (qn_codec == "avc") { + codec_suffix = " AVC"; + } else if (qn_codec == "hevc") { + codec_suffix = " HEVC"; + } else if (qn_codec == "av1") { + codec_suffix = " AV1"; + } else { + codec_suffix = " " + qn_codec; + } - qualityitem2["quality"] = "- " + qualities[i]["desc"].asString() + " 备份"; - qualityitem2["qualityDetail"] = qualityitem2["quality"]; - qualityitem2["itag"] = itag + 20; - QualityList.insertLast(qualityitem2); + dictionary qualityitem; + qualityitem["quality"] = label + codec_suffix; + qualityitem["qualityDetail"] = label + codec_suffix; + qualityitem["url"] = qn_url; + qualityitem["itag"] = getItag(qn); + QualityList.insertLast(qualityitem); } } } } + log("url", url); + log("Quality items", QualityList.length()); return url; } @@ -1725,6 +1830,11 @@ bool PlayitemCheck(const string &in path) { return true; } + if (path.find("bangumi/play/ep") >= 0) { + log("PlayitemCheck bangumi/ep TRUE", path); + return true; + } + if (path.find("live.bilibili.com") >= 0) { return true; } @@ -1779,7 +1889,9 @@ bool PlaylistCheck(const string &in path) { return true; } if(path.find("live.bilibili.com") >= 0){ - return true; + if(path.find("areaId") >= 0 || path.find("lol") >= 0 || path.find("hpjy") >= 0){ + return true; + } } if (path.find("www.bilibili.com") >= 0 && HostRegExpParse(path, "www.bilibili.com/([a-zA-Z0-9]+)").empty()) { return true; @@ -1875,9 +1987,11 @@ array PlaylistParse(const string &in path) { return Banggumi(HostRegExpParse(path, "bangumi/media/md([0-9]+)"), "media_id"); } if (path.find("bangumi/play/ep") >= 0) { + log("PlaylistParse bangumi ep detected", path); return Banggumi(HostRegExpParse(path, "bangumi/play/ep([0-9]+)"), "ep_id"); } if (path.find("bangumi/play/ss") >= 0) { + log("PlaylistParse bangumi ss detected", path); return Banggumi(HostRegExpParse(path, "bangumi/play/ss([0-9]+)"), "season_id"); } if (path.find("www.bilibili.com/v/popular/rank") >= 0) { @@ -1906,6 +2020,92 @@ array PlaylistParse(const string &in path) { string PlayitemParse(const string &in path, dictionary &MetaData, array &QualityList) { log("Playitem path", path); status = 2; + + if (path.find("bangumi/play/ep") >= 0) { + log("Bangumi item detected", path); + string epid = HostRegExpParse(path, "bangumi/play/ep([0-9]+)"); + log("Bangumi epid", epid); + if (!epid.empty()) { + string apiUrl = "/pgc/view/web/season?ep_id=" + epid; + string res = apiPost(apiUrl); + log("Bangumi season API response length", res.length()); + if (res.empty()) { + log("Bangumi season API response EMPTY"); + return ""; + } + JsonReader reader; + JsonValue root; + if (!reader.parse(res, root)) { + log("Bangumi season API JSON parse FAILED"); + return ""; + } + if (!root.isObject()) { + log("Bangumi season API root is NOT object"); + return ""; + } + log("Bangumi season API code", root["code"].asInt()); + if (root["code"].asInt() != 0) { + log("Bangumi season API message", root["message"].asString()); + return ""; + } + JsonValue result = root["result"]; + if (!result.isObject()) { + log("Bangumi season API result is NOT object"); + return ""; + } + JsonValue episodes = result["episodes"]; + if (!episodes.isArray()) { + log("Bangumi season API episodes is NOT array"); + return ""; + } + log("Bangumi episode count", episodes.size()); + if (episodes.size() == 0) { + log("Bangumi episode list EMPTY"); + return ""; + } + string bvid; + string cid; + string aid; + string title; + string cover; + for (int i = 0; i < episodes.size(); i++) { + if (episodes[i]["id"].asInt() == parseInt(epid)) { + bvid = episodes[i]["bvid"].asString(); + cid = episodes[i]["cid"].asString(); + aid = episodes[i]["aid"].asString(); + title = episodes[i]["share_copy"].asString(); + cover = episodes[i]["cover"].asString(); + break; + } + } + if (bvid.empty()) { + log("Bangumi ep not found by id, using first episode"); + bvid = episodes[0]["bvid"].asString(); + cid = episodes[0]["cid"].asString(); + aid = episodes[0]["aid"].asString(); + title = episodes[0]["share_copy"].asString(); + cover = episodes[0]["cover"].asString(); + } + log("Bangumi bvid", bvid); + log("Bangumi cid", cid); + log("Bangumi aid", aid); + if (!title.empty()) { + MetaData["title"] = title; + MetaData["content"] = title; + } + if (!cover.empty()) { + MetaData["thumbnail"] = cover; + } + if (!bvid.empty()) { + string videoUrl = "https://www.bilibili.com/video/" + bvid + "?cid=" + cid + "&aid=" + aid; + log("Bangumi calling Video with URL", videoUrl); + return Video(bvid, videoUrl, MetaData, QualityList); + } + log("Bangumi bvid EMPTY after lookup"); + } + return ""; + } + if (path.find("/video/BV") >= 0) { string bvid = parseBVId(path); return Video(bvid, path, MetaData, QualityList);