mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-23 00:17:16 +08:00
468 lines
21 KiB
C#
468 lines
21 KiB
C#
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Text.Json;
|
|
using static BBDown.Core.Logger;
|
|
using static BBDown.Core.Util.HTTPUtil;
|
|
using static BBDown.Core.Entity.Entity;
|
|
using System.Security.Cryptography;
|
|
using BBDown.Core.Entity;
|
|
|
|
namespace BBDown.Core;
|
|
|
|
public static partial class Parser
|
|
{
|
|
public static string WbiSign(string api)
|
|
{
|
|
return $"{api}&w_rid=" + string.Concat(MD5.HashData(Encoding.UTF8.GetBytes(api + Config.WBI)).Select(i => i.ToString("x2")).ToArray());
|
|
}
|
|
|
|
private static async Task<string> GetPlayJsonAsync(string encoding, string aidOri, string aid, string cid, string epId, bool tvApi, bool intl, bool appApi, string qn = "0")
|
|
{
|
|
LogDebug("aid={0},cid={1},epId={2},tvApi={3},IntlApi={4},appApi={5},qn={6}", aid, cid, epId, tvApi, intl, appApi, qn);
|
|
|
|
if (intl) return await GetPlayJsonAsync(aid, cid, epId, qn);
|
|
|
|
|
|
bool cheese = aidOri.StartsWith("cheese:");
|
|
bool bangumi = cheese || aidOri.StartsWith("ep:");
|
|
LogDebug("bangumi={0},cheese={1}", bangumi, cheese);
|
|
|
|
if (appApi) return await AppHelper.DoReqAsync(aid, cid, epId, qn, bangumi, encoding, Config.TOKEN);
|
|
|
|
string prefix = tvApi ? bangumi ? $"{Config.TVHOST}/pgc/player/api/playurltv" : $"{Config.TVHOST}/x/tv/playurl"
|
|
: bangumi ? $"{Config.HOST}/pgc/player/web/v2/playurl" : "api.bilibili.com/x/player/wbi/playurl";
|
|
prefix = $"https://{prefix}?";
|
|
|
|
string api;
|
|
if (tvApi)
|
|
{
|
|
StringBuilder apiBuilder = new();
|
|
if (Config.TOKEN != "") apiBuilder.Append($"access_key={Config.TOKEN}&");
|
|
apiBuilder.Append($"appkey=4409e2ce8ffd12b8&build=106500&cid={cid}&device=android");
|
|
if (bangumi) apiBuilder.Append($"&ep_id={epId}&expire=0");
|
|
apiBuilder.Append($"&fnval=4048&fnver=0&fourk=1&mid=0&mobi_app=android_tv_yst");
|
|
apiBuilder.Append($"&object_id={aid}&platform=android&playurl_type=1&qn={qn}&ts={GetTimeStamp(true)}");
|
|
api = $"{prefix}{apiBuilder}&sign={GetSign(apiBuilder.ToString(), false)}";
|
|
}
|
|
else
|
|
{
|
|
// 尝试提高可读性
|
|
StringBuilder apiBuilder = new();
|
|
apiBuilder.Append($"support_multi_audio=true&from_client=BROWSER&avid={aid}&cid={cid}&fnval=4048&fnver=0&fourk=1");
|
|
if (Config.AREA != "") apiBuilder.Append($"&access_key={Config.TOKEN}&area={Config.AREA}");
|
|
apiBuilder.Append($"&otype=json&qn={qn}");
|
|
if (bangumi) apiBuilder.Append($"&module=bangumi&ep_id={epId}&session=");
|
|
if (Config.COOKIE == "") apiBuilder.Append("&try_look=1");
|
|
apiBuilder.Append($"&wts={GetTimeStamp(true)}");
|
|
api = prefix + (bangumi ? apiBuilder.ToString() : WbiSign(apiBuilder.ToString()));
|
|
}
|
|
|
|
//课程接口
|
|
if (cheese) api = api.Replace("/pgc/", "/pugv/");
|
|
|
|
//Console.WriteLine(api);
|
|
string webJson = await GetWebSourceAsync(api);
|
|
//以下情况从网页源代码尝试解析
|
|
if (webJson.Contains("\"大会员专享限制\""))
|
|
{
|
|
Log("此视频需要大会员,您大概率需要登录一个有大会员的账号才可以下载,尝试从网页源码解析");
|
|
string webUrl = "https://www.bilibili.com/bangumi/play/ep" + epId;
|
|
string webSource = await GetWebSourceAsync(webUrl);
|
|
webJson = PlayerJsonRegex().Match(webSource).Groups[1].Value;
|
|
}
|
|
return webJson;
|
|
}
|
|
|
|
private static async Task<string> GetPlayJsonAsync(string aid, string cid, string epId, string qn, string code = "0")
|
|
{
|
|
bool isBiliPlus = Config.HOST != "api.bilibili.com";
|
|
string api = $"https://{(isBiliPlus ? Config.HOST : "api.biliintl.com")}/intl/gateway/v2/ogv/playurl?";
|
|
|
|
StringBuilder paramBuilder = new();
|
|
if (Config.TOKEN != "") paramBuilder.Append($"access_key={Config.TOKEN}&");
|
|
paramBuilder.Append($"aid={aid}");
|
|
if (isBiliPlus) paramBuilder.Append($"&appkey=7d089525d3611b1c&area={(Config.AREA == "" ? "th" : Config.AREA)}");
|
|
paramBuilder.Append($"&cid={cid}&ep_id={epId}&platform=android&prefer_code_type={code}&qn={qn}");
|
|
if (isBiliPlus) paramBuilder.Append($"&ts={GetTimeStamp(true)}");
|
|
|
|
paramBuilder.Append("&s_locale=zh_SG");
|
|
string param = paramBuilder.ToString();
|
|
api += (isBiliPlus ? $"{param}&sign={GetSign(param, true)}" : param);
|
|
|
|
string webJson = await GetWebSourceAsync(api);
|
|
return webJson;
|
|
}
|
|
|
|
public static async Task<ParsedResult> ExtractTracksAsync(string aidOri, string aid, string cid, string epId, bool tvApi, bool intlApi, bool appApi, string encoding, string qn = "0")
|
|
{
|
|
var intlCode = "0";
|
|
ParsedResult parsedResult = new();
|
|
|
|
//调用解析
|
|
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, qn);
|
|
|
|
LogDebug(parsedResult.WebJsonString);
|
|
|
|
startParsing:
|
|
var respJson = JsonDocument.Parse(parsedResult.WebJsonString);
|
|
var data = respJson.RootElement;
|
|
|
|
//intl接口
|
|
if (parsedResult.WebJsonString.Contains("\"stream_list\""))
|
|
{
|
|
int pDur = data.GetProperty("data").GetProperty("video_info").GetProperty("timelength").GetInt32() / 1000;
|
|
var audio = data.GetProperty("data").GetProperty("video_info").GetProperty("dash_audio").EnumerateArray().ToList();
|
|
foreach (var stream in data.GetProperty("data").GetProperty("video_info").GetProperty("stream_list").EnumerateArray())
|
|
{
|
|
if (stream.TryGetProperty("dash_video", out JsonElement dashVideo))
|
|
{
|
|
if (dashVideo.GetProperty("base_url").ToString() != "")
|
|
{
|
|
var videoId = stream.GetProperty("stream_info").GetProperty("quality").ToString();
|
|
var urlList = new List<string>() { dashVideo.GetProperty("base_url").ToString() };
|
|
urlList.AddRange(dashVideo.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
|
|
Video v = new()
|
|
{
|
|
dur = pDur,
|
|
id = videoId,
|
|
dfn = Config.qualitys[videoId],
|
|
bandwith = Convert.ToInt64(dashVideo.GetProperty("bandwidth").ToString()) / 1000,
|
|
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
|
|
codecs = GetVideoCodec(dashVideo.GetProperty("codecid").ToString()),
|
|
size = dashVideo.TryGetProperty("size", out var sizeNode) ? Convert.ToDouble(sizeNode.ToString()) : 0
|
|
};
|
|
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var node in audio)
|
|
{
|
|
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
|
|
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
|
|
Audio a = new()
|
|
{
|
|
id = node.GetProperty("id").ToString(),
|
|
dfn = node.GetProperty("id").ToString(),
|
|
dur = pDur,
|
|
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
|
|
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
|
|
codecs = "M4A"
|
|
};
|
|
if (!parsedResult.AudioTracks.Contains(a)) parsedResult.AudioTracks.Add(a);
|
|
}
|
|
|
|
if (intlCode == "0")
|
|
{
|
|
intlCode = "1";
|
|
parsedResult.WebJsonString = await GetPlayJsonAsync(aid, cid, epId, qn, intlCode);
|
|
goto startParsing;
|
|
}
|
|
|
|
return parsedResult;
|
|
}
|
|
// data节点一次性判断完
|
|
string? nodeName = null;
|
|
if (parsedResult.WebJsonString.Contains("\"result\":{"))
|
|
{
|
|
nodeName = "result";
|
|
|
|
// v2
|
|
if (parsedResult.WebJsonString.Contains("\"video_info\":{"))
|
|
{
|
|
nodeName = "video_info";
|
|
}
|
|
}
|
|
else if (parsedResult.WebJsonString.Contains("\"data\":{")) nodeName = "data";
|
|
var root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
|
|
|
|
bool bangumi = aidOri.StartsWith("ep:");
|
|
|
|
if (parsedResult.WebJsonString.Contains("\"dash\":{")) //dash
|
|
{
|
|
List<JsonElement>? audio = null;
|
|
List<JsonElement>? video = null;
|
|
List<JsonElement>? backgroundAudio = null;
|
|
List<JsonElement>? roleAudio = null;
|
|
int pDur = 0;
|
|
|
|
try { pDur = root.GetProperty("dash").GetProperty("duration").GetInt32(); } catch { }
|
|
try { pDur = root.GetProperty("timelength").GetInt32() / 1000; } catch { }
|
|
|
|
bool reParse = false;
|
|
reParse:
|
|
if (reParse)
|
|
{
|
|
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, GetMaxQn());
|
|
respJson = JsonDocument.Parse(parsedResult.WebJsonString);
|
|
data = respJson.RootElement;
|
|
root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
|
|
}
|
|
try { video = root.GetProperty("dash").GetProperty("video").EnumerateArray().ToList(); } catch { }
|
|
try { audio = root.GetProperty("dash").GetProperty("audio").EnumerateArray().ToList(); } catch { }
|
|
|
|
if (appApi && bangumi)
|
|
{
|
|
try { backgroundAudio = data.GetProperty("dubbing_info").GetProperty("background_audio").EnumerateArray().ToList(); } catch { }
|
|
try { roleAudio = data.GetProperty("dubbing_info").GetProperty("role_audio_list").EnumerateArray().ToList(); } catch { }
|
|
}
|
|
//处理杜比音频
|
|
try
|
|
{
|
|
if (audio != null)
|
|
{
|
|
if (!tvApi && root.GetProperty("dash").TryGetProperty("dolby", out JsonElement dolby))
|
|
{
|
|
if (dolby.TryGetProperty("audio", out JsonElement db))
|
|
{
|
|
audio.AddRange(db.EnumerateArray());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception) {; }
|
|
|
|
//处理Hi-Res无损
|
|
try
|
|
{
|
|
if (audio != null)
|
|
{
|
|
if (!tvApi && root.GetProperty("dash").TryGetProperty("flac", out JsonElement hiRes))
|
|
{
|
|
if (hiRes.TryGetProperty("audio", out JsonElement db))
|
|
{
|
|
if (db.ValueKind != JsonValueKind.Null)
|
|
audio.Add(db);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception) {; }
|
|
|
|
if (video != null)
|
|
{
|
|
foreach (var node in video)
|
|
{
|
|
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
|
|
if (node.TryGetProperty("backup_url", out JsonElement element) && element.ValueKind != JsonValueKind.Null)
|
|
{
|
|
urlList.AddRange(element.EnumerateArray().Select(i => i.ToString()));
|
|
}
|
|
var videoId = node.GetProperty("id").ToString();
|
|
Video v = new()
|
|
{
|
|
dur = pDur,
|
|
id = videoId,
|
|
dfn = Config.qualitys[videoId],
|
|
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
|
|
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
|
|
codecs = GetVideoCodec(node.GetProperty("codecid").ToString()),
|
|
size = node.TryGetProperty("size", out var sizeNode) ? Convert.ToDouble(sizeNode.ToString()) : 0
|
|
};
|
|
if (!tvApi && !appApi)
|
|
{
|
|
v.res = node.GetProperty("width").ToString() + "x" + node.GetProperty("height").ToString();
|
|
v.fps = node.GetProperty("frame_rate").ToString();
|
|
}
|
|
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
|
|
}
|
|
}
|
|
|
|
//此处处理免二压视频,需要单独再请求一次
|
|
if (!reParse && !appApi)
|
|
{
|
|
reParse = true;
|
|
goto reParse;
|
|
}
|
|
|
|
if (audio != null)
|
|
{
|
|
foreach (var node in audio)
|
|
{
|
|
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
|
|
if (node.TryGetProperty("backup_url", out JsonElement element) && element.ValueKind != JsonValueKind.Null)
|
|
{
|
|
urlList.AddRange(element.EnumerateArray().Select(i => i.ToString()));
|
|
}
|
|
var audioId = node.GetProperty("id").ToString();
|
|
var codecs = node.GetProperty("codecs").ToString();
|
|
codecs = codecs switch
|
|
{
|
|
"mp4a.40.2" => "M4A",
|
|
"mp4a.40.5" => "M4A",
|
|
"ec-3" => "E-AC-3",
|
|
"fLaC" => "FLAC",
|
|
_ => codecs
|
|
};
|
|
|
|
parsedResult.AudioTracks.Add(new Audio()
|
|
{
|
|
id = audioId,
|
|
dfn = audioId,
|
|
dur = pDur,
|
|
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
|
|
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
|
|
codecs = codecs
|
|
});
|
|
}
|
|
}
|
|
|
|
if (backgroundAudio != null && roleAudio != null)
|
|
{
|
|
foreach (var node in backgroundAudio)
|
|
{
|
|
var audioId = node.GetProperty("id").ToString();
|
|
var urlList = new List<string> { node.GetProperty("base_url").ToString() };
|
|
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
|
|
parsedResult.BackgroundAudioTracks.Add(new Audio()
|
|
{
|
|
id = audioId,
|
|
dfn = audioId,
|
|
dur = pDur,
|
|
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
|
|
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
|
|
codecs = node.GetProperty("codecs").ToString()
|
|
});
|
|
}
|
|
|
|
foreach (var role in roleAudio)
|
|
{
|
|
var roleAudioTracks = new List<Audio>();
|
|
foreach (var node in role.GetProperty("audio").EnumerateArray())
|
|
{
|
|
var audioId = node.GetProperty("id").ToString();
|
|
var urlList = new List<string> { node.GetProperty("base_url").ToString() };
|
|
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
|
|
roleAudioTracks.Add(new Audio()
|
|
{
|
|
id = audioId,
|
|
dfn = audioId,
|
|
dur = pDur,
|
|
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
|
|
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
|
|
codecs = node.GetProperty("codecs").ToString()
|
|
});
|
|
}
|
|
parsedResult.RoleAudioList.Add(new AudioMaterialInfo()
|
|
{
|
|
title = role.GetProperty("title").ToString(),
|
|
personName = role.GetProperty("person_name").ToString(),
|
|
path = $"{aid}/{aid}.{cid}.{role.GetProperty("audio_id").ToString()}.m4a",
|
|
audio = roleAudioTracks
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else if (parsedResult.WebJsonString.Contains("\"durl\":[")) //flv
|
|
{
|
|
//默认以最高清晰度解析
|
|
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, GetMaxQn());
|
|
data = JsonDocument.Parse(parsedResult.WebJsonString).RootElement;
|
|
root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
|
|
string quality = "";
|
|
string videoCodecid = "";
|
|
string url = "";
|
|
double size = 0;
|
|
double length = 0;
|
|
|
|
quality = root.GetProperty("quality").ToString();
|
|
videoCodecid = root.GetProperty("video_codecid").ToString();
|
|
//获取所有分段
|
|
foreach (var node in root.GetProperty("durl").EnumerateArray())
|
|
{
|
|
parsedResult.Clips.Add(node.GetProperty("url").ToString());
|
|
size += node.GetProperty("size").GetDouble();
|
|
length += node.GetProperty("length").GetDouble();
|
|
}
|
|
//TV模式可用清晰度
|
|
if (root.TryGetProperty("qn_extras", out JsonElement qnExtras))
|
|
{
|
|
parsedResult.Dfns.AddRange(qnExtras.EnumerateArray().Select(node => node.GetProperty("qn").ToString()));
|
|
}
|
|
else if (root.TryGetProperty("accept_quality", out JsonElement acceptQuality)) //非tv模式可用清晰度
|
|
{
|
|
parsedResult.Dfns.AddRange(acceptQuality.EnumerateArray()
|
|
.Select(node => node.ToString())
|
|
.Where(_qn => !string.IsNullOrEmpty(_qn)));
|
|
}
|
|
|
|
Video v = new()
|
|
{
|
|
id = quality,
|
|
dfn = Config.qualitys[quality],
|
|
baseUrl = url,
|
|
codecs = GetVideoCodec(videoCodecid),
|
|
dur = (int)length / 1000,
|
|
size = size
|
|
};
|
|
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
|
|
}
|
|
|
|
// 番剧片头片尾转分段信息, 预计效果: 正片? -> 片头 -> 正片 -> 片尾
|
|
if (bangumi)
|
|
{
|
|
if (root.TryGetProperty("clip_info_list", out JsonElement clipList))
|
|
{
|
|
parsedResult.ExtraPoints.AddRange(clipList.EnumerateArray().Select(clip => new ViewPoint()
|
|
{
|
|
title = clip.GetProperty("toastText").ToString().Replace("即将跳过", ""),
|
|
start = clip.GetProperty("start").GetInt32(),
|
|
end = clip.GetProperty("end").GetInt32()
|
|
})
|
|
);
|
|
parsedResult.ExtraPoints.Sort((p1, p2) => p1.start.CompareTo(p2.start));
|
|
var newPoints = new List<ViewPoint>();
|
|
int lastEnd = 0;
|
|
foreach (var point in parsedResult.ExtraPoints)
|
|
{
|
|
if (lastEnd < point.start)
|
|
newPoints.Add(new ViewPoint() { title = "正片", start = lastEnd, end = point.start });
|
|
newPoints.Add(point);
|
|
lastEnd = point.end;
|
|
}
|
|
parsedResult.ExtraPoints = newPoints;
|
|
}
|
|
|
|
}
|
|
|
|
return parsedResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 编码转换
|
|
/// </summary>
|
|
/// <param name="code"></param>
|
|
/// <returns></returns>
|
|
private static string GetVideoCodec(string code)
|
|
{
|
|
return code switch
|
|
{
|
|
"13" => "AV1",
|
|
"12" => "HEVC",
|
|
"7" => "AVC",
|
|
_ => "UNKNOWN"
|
|
};
|
|
}
|
|
|
|
private static string GetMaxQn()
|
|
{
|
|
return Config.qualitys.Keys.First();
|
|
}
|
|
|
|
private static string GetTimeStamp(bool bflag)
|
|
{
|
|
DateTimeOffset ts = DateTimeOffset.Now;
|
|
return bflag ? ts.ToUnixTimeSeconds().ToString() : ts.ToUnixTimeMilliseconds().ToString();
|
|
}
|
|
|
|
private static string GetSign(string parms, bool isBiliPlus)
|
|
{
|
|
string toEncode = parms + (isBiliPlus ? "acd495b248ec528c2eed1e862d393126" : "59b43e04ad6965f34319062b478f83dd");
|
|
return string.Concat(MD5.HashData(Encoding.UTF8.GetBytes(toEncode)).Select(i => i.ToString("x2")).ToArray());
|
|
}
|
|
|
|
[GeneratedRegex("window.__playinfo__=([\\s\\S]*?)<\\/script>")]
|
|
private static partial Regex PlayerJsonRegex();
|
|
[GeneratedRegex("http.*:\\d+")]
|
|
private static partial Regex BaseUrlRegex();
|
|
} |