mirror of
https://github.com/nilaoda/BBDown.git
synced 2026-10-29 04:37:22 +08:00
484 lines
21 KiB
C#
484 lines
21 KiB
C#
using BBDown.Core.Protobuf;
|
|
using Google.Protobuf;
|
|
using System.Text;
|
|
using static BBDown.Core.Entity.Entity;
|
|
using static BBDown.Core.Util.HTTPUtil;
|
|
using System.Text.RegularExpressions;
|
|
using System.Text.Json;
|
|
|
|
namespace BBDown.Core.Util;
|
|
|
|
public static partial class SubUtil
|
|
{
|
|
//https://i0.hdslb.com/bfs/subtitle/subtitle_lan.json
|
|
public static (string, string) GetSubtitleCode(string key)
|
|
{
|
|
//zh-hans => zh-Hans
|
|
if (NonCapsRegex().Match(key) is { Success: true } result)
|
|
{
|
|
var v = result.Value;
|
|
key = key.Replace(v, v.ToUpper());
|
|
}
|
|
|
|
return key switch
|
|
{
|
|
"ai-Zh" => ("chi", "中文(简体, AI识别)"),
|
|
"ai-En" => ("eng", "English(generated by ai)"),
|
|
"zh-CN" => ("chi", "中文(简体)"),
|
|
"zh-HK" => ("chi", "中文(香港繁體)"),
|
|
"zh-Hans" => ("chi", "中文(简体)"),
|
|
"zh-TW" => ("chi", "中文(台灣繁體)"),
|
|
"zh-Hant" => ("chi", "中文(繁體)"),
|
|
"en-US" => ("eng", "English(USA)"),
|
|
"ja" => ("jpn", "日本語"),
|
|
"ko" => ("kor", "한국어"),
|
|
"zh-SG" => ("chi", "中文(新加坡)"),
|
|
"ab" => ("abk", "Аҳәынҭқарра"),
|
|
"aa" => ("aar", "Qafár af"),
|
|
"af" => ("afr", "Afrikaans"),
|
|
"sq" => ("alb", "Gjuha shqipe"),
|
|
"ase" => ("ase", "American Sign Language"),
|
|
"am" => ("amh", "አማርኛ"),
|
|
"arc" => ("arc", "ܐܪܡܝܐ"),
|
|
"hy" => ("arm", "հայերեն"),
|
|
"as" => ("asm", "অসমীয়া"),
|
|
"ay" => ("aym", "Aymar aru"),
|
|
"az" => ("aze", "Azərbaycan"),
|
|
"bn" => ("ben", "বাংলা ভাষার"),
|
|
"ba" => ("bak", "Башҡорттеле"),
|
|
"eu" => ("baq", "Euskara"),
|
|
"be" => ("bel", "беларуская мова biełaruskaja mova"),
|
|
"bh" => ("bih", "Bihar"),
|
|
"bi" => ("bis", "Bislama"),
|
|
"bs" => ("bos", "босански"),
|
|
"br" => ("bre", "Breton"),
|
|
"bg" => ("bul", "български"),
|
|
"yue" => ("chi", "粵語"),
|
|
"yue-HK" => ("chi", "粵語(中國香港)"),
|
|
"ca" => ("cat", "català"),
|
|
"chr" => ("chr", "ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ"),
|
|
"cho" => ("cho", "Chahta'"),
|
|
"co" => ("cos", "lingua corsa"),
|
|
"hr" => ("hrv", "Hrvatska"),
|
|
"cs" => ("cze", "čeština"),
|
|
"da" => ("dan", "Dansk"),
|
|
"nl" => ("dut", "Nederlands"),
|
|
"nl-BE" => ("dut", "Nederlands(Belgisch)"),
|
|
"nl-NL" => ("dut", "Nederlands(Nederlands)"),
|
|
"dz" => ("dzo", "རྫོང་ཁ།"),
|
|
"en" => ("eng", "English"),
|
|
"en-CA" => ("eng", "English(Canada)"),
|
|
"en-IE" => ("eng", "English(Ireland)"),
|
|
"en-GB" => ("eng", "English(UK)"),
|
|
"eo" => ("epo", "Esperanto"),
|
|
"et" => ("est", "Eestlane"),
|
|
"fo" => ("fao", "føroyskt"),
|
|
"fj" => ("fij", "Vakaviti"),
|
|
"fil" => ("phi", "Pilipino"),
|
|
"fi" => ("fin", "Suomi"),
|
|
"fr" => ("fre", "Français"),
|
|
"fr-BE" => ("fre", "Français(Belgique)"),
|
|
"fr-CA" => ("fre", "Français(Canada)"),
|
|
"fr-FR" => ("fre", "Français(La France)"),
|
|
"fr-CH" => ("fre", "Français(Suisse)"),
|
|
"ff" => ("ful", "Fulani"),
|
|
"gl" => ("glg", "galego"),
|
|
"ka" => ("geo", "ქართული ენა"),
|
|
"de" => ("ger", "Deutsch"),
|
|
"de-AT" => ("ger", "Deutsch(Österreich)"),
|
|
"de-DE" => ("ger", "Deutsch(Deutschland)"),
|
|
"de-CH" => ("ger", "Deutsch(Schweiz)"),
|
|
"el" => ("gre", "Ελληνικά"),
|
|
"kl" => ("kal", "Kalaallisut"),
|
|
"gn" => ("grn", "avañe'ẽ"),
|
|
"gu" => ("guj", "ગુજરાતી"),
|
|
"hak" => ("hak", "Hak-kâ-fa"),
|
|
"hak-TW" => ("hak", "Hak-kâ-fa"),
|
|
"ha" => ("hau", "هَوُسَ"),
|
|
"iw" => ("heb", "שפה עברית"),
|
|
"hi" => ("hin", "हिन्दी"),
|
|
"hi-Latn" => ("hin", "हिंदी(फोनेटिक)"),
|
|
"hu" => ("hun", "Magyar"),
|
|
"is" => ("ice", "icelandic"),
|
|
"ig" => ("ibo", "Asụsụ Igbo"),
|
|
"id" => ("ind", "Indonesia"),
|
|
"ia" => ("ina", "Interlingua"),
|
|
"iu" => ("iku", "ᐃᓄᒃᑎᑐᑦ"),
|
|
"ik" => ("ipk", "Inupiat"),
|
|
"ga" => ("gle", "Gaeilge na hÉireann"),
|
|
"it" => ("ita", "Italiano"),
|
|
"jv" => ("jav", "ꦧꦱꦗꦮ"),
|
|
"kn" => ("kan", "ಕನ್ನಡ"),
|
|
"ks" => ("kas", "कॉशुर"),
|
|
"kk" => ("kaz", "Қазақ тілі"),
|
|
"km" => ("khm", "ភាសាខ្មែរ"),
|
|
"rw" => ("kin", "Ikinyarwanda"),
|
|
"tlh" => ("tlh", "tlhIngan Hol"),
|
|
"ku" => ("kur", "Kurdî"),
|
|
"ky" => ("kir", "кыргыз тили"),
|
|
"lo" => ("lao", "ພາສາລາວ"),
|
|
"la" => ("lat", "latīna"),
|
|
"lv" => ("lav", "latviešu valoda"),
|
|
"ln" => ("lin", "Lingála"),
|
|
"lt" => ("lit", "lietuvių kalba"),
|
|
"lb" => ("ltz", "Lëtzebuergesch"),
|
|
"mk" => ("mac", "Македонски јазик"),
|
|
"mg" => ("mlg", "maa.laa.gaas"),
|
|
"ms" => ("may", "Melayu"),
|
|
"ml" => ("mal", "മലയാളം"),
|
|
"mt" => ("mlt", "Lingwa Maltija"),
|
|
"mi" => ("mao", "Māori"),
|
|
"mr" => ("mar", "मराठी Marāṭhī"),
|
|
"mas" => ("mas", "Maasai"),
|
|
"nan" => ("nan", "閩南語"),
|
|
"nan-TW" => ("nan", "閩南語(台灣)"),
|
|
"lus" => ("lus", "Mizo ṭawng"),
|
|
"mo" => ("mol", "Limba moldovenească"),
|
|
"mn" => ("mon", "монгол хэл"),
|
|
"my" => ("bur", "မြန်မာဘာသာ"),
|
|
"na" => ("nau", "Dorerin Naoero"),
|
|
"nv" => ("nav", "Diné bizaad"),
|
|
"ne" => ("nep", "नेपाली Nepālī"),
|
|
"no" => ("nor", "norsk språk"),
|
|
"fa" => ("per", "فارسی"),
|
|
"fa-AF" => ("per", "فارسی"),
|
|
"fa-IR" => ("per", "فارسی"),
|
|
"pl" => ("pol", "Polski"),
|
|
"pt" => ("por", "Português"),
|
|
"pt-BR" => ("por", "Português(brasil)"),
|
|
"pt-PT" => ("por", "Português(portugal)"),
|
|
"ro" => ("rum", "Română"),
|
|
"ru" => ("rus", "Русский"),
|
|
"ru-Latn" => ("rus", "Русский(фонетический)"),
|
|
"sr" => ("srp", "Српски"),
|
|
"sr-Cyrl" => ("srp", "Српски(ћирилица)"),
|
|
"sr-Latn" => ("srp", "Српски(латиница)"),
|
|
"sh" => ("scr", "srpskohrvatski"),
|
|
"sk" => ("slo", "slovenský"),
|
|
"es" => ("spa", "Español"),
|
|
"es-419" => ("spa", "Español(Latinoamérica)"),
|
|
"es-MX" => ("spa", "Español(México)"),
|
|
"es-ES" => ("spa", "Español(España)"),
|
|
"es-US" => ("spa", "Español(Estados Unidos)"),
|
|
"sv" => ("swe", "Svenska"),
|
|
"tl" => ("tgl", "Tagalog"),
|
|
"th" => ("tha", "ไทย"),
|
|
"tr" => ("tur", "Türkçe"),
|
|
"uk" => ("ukr", "Українська"),
|
|
"ur" => ("urd", "Urdu"),
|
|
"vi" => ("vie", "Tiếng Việt"),
|
|
//太多了,我蚌埠住了,后面懒得查
|
|
//"ie" => ("", ""),
|
|
//"oc" => ("", ""),
|
|
//"or" => ("", ""),
|
|
//"om" => ("", ""),
|
|
//"ps" => ("", ""),
|
|
//"pa" => ("", ""),
|
|
//"qu" => ("", ""),
|
|
//"rm" => ("", ""),
|
|
//"rn" => ("", ""),
|
|
//"sm" => ("", ""),
|
|
//"sg" => ("", ""),
|
|
//"sa" => ("", ""),
|
|
//"gd" => ("", ""),
|
|
//"sdp" => ("", ""),
|
|
//"sn" => ("", ""),
|
|
//"scn" => ("", ""),
|
|
//"sd" => ("", ""),
|
|
//"si" => ("", ""),
|
|
//"sl" => ("", ""),
|
|
//"so" => ("", ""),
|
|
//"st" => ("", ""),
|
|
//"su" => ("", ""),
|
|
//"sw" => ("", ""),
|
|
//"ss" => ("", ""),
|
|
//"tg" => ("", ""),
|
|
//"ta" => ("", ""),
|
|
//"tt" => ("", ""),
|
|
//"te" => ("", ""),
|
|
//"ti" => ("", ""),
|
|
//"to" => ("", ""),
|
|
//"ts" => ("", ""),
|
|
//"tn" => ("", ""),
|
|
//"tk" => ("", ""),
|
|
//"tw" => ("", ""),
|
|
//"uz" => ("", ""),
|
|
//"vo" => ("", ""),
|
|
//"cy" => ("", ""),
|
|
//"fy" => ("", ""),
|
|
//"wo" => ("", ""),
|
|
//"xh" => ("", ""),
|
|
//"yi" => ("", ""),
|
|
//"yo" => ("", ""),
|
|
//"zu" => ("", ""),
|
|
_ => ("und", "Undetermined")
|
|
};
|
|
}
|
|
|
|
#region 字幕接口
|
|
|
|
private static async Task<List<Subtitle>?> GetIntlSubtitlesFromApi1Async(string aid, string cid, string epId, int index)
|
|
{
|
|
try
|
|
{
|
|
List<Subtitle> subtitles = new();
|
|
string api = "https://" + (Config.EPHOST == "api.bilibili.com" ? "api.biliintl.com" : Config.EPHOST) + $"/intl/gateway/web/v2/subtitle?episode_id={epId}";
|
|
string json = await GetWebSourceAsync(api);
|
|
using var infoJson = JsonDocument.Parse(json);
|
|
var subs = infoJson.RootElement.GetProperty("data").GetProperty("subtitles").EnumerateArray();
|
|
foreach (var sub in subs)
|
|
{
|
|
var lan = sub.GetProperty("lang_key").ToString();
|
|
var url = sub.GetProperty("url").ToString();
|
|
Subtitle subtitle = new()
|
|
{
|
|
url = url,
|
|
lan = lan,
|
|
path = $"{aid}/{aid}.{cid}.{lan}{(url.Contains(".json") ? ".srt" : ".ass")}"
|
|
};
|
|
|
|
//有空的URL 不合法
|
|
if (subtitles.Any(s => string.IsNullOrEmpty(s.url)))
|
|
throw new Exception("Bad url");
|
|
|
|
subtitles.Add(subtitle);
|
|
}
|
|
return subtitles;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static async Task<List<Subtitle>?> GetIntlSubtitlesFromApi2Async(string aid, string cid, string epId, int index)
|
|
{
|
|
try
|
|
{
|
|
List<Subtitle> subtitles = new();
|
|
string api = "https://" + (Config.HOST == "api.bilibili.com" ? "api.bilibili.tv" : Config.HOST) +
|
|
$"/intl/gateway/v2/ogv/view/app/season?ep_id={epId}&platform=android&s_locale=zh_SG" + (Config.TOKEN != "" ? $"&access_key={Config.TOKEN}" : "");
|
|
string json = await GetWebSourceAsync(api);
|
|
using var infoJson = JsonDocument.Parse(json);
|
|
var subs = infoJson.RootElement.GetProperty("result").GetProperty("modules")[0].GetProperty("data")
|
|
.GetProperty("episodes")[index - 1].GetProperty("subtitles").EnumerateArray();
|
|
foreach (var sub in subs)
|
|
{
|
|
var lan = sub.GetProperty("key").ToString();
|
|
var url = sub.GetProperty("url").ToString().Replace("\\\\/", "/");
|
|
Subtitle subtitle = new()
|
|
{
|
|
url = url,
|
|
lan = lan,
|
|
path = $"{aid}/{aid}.{cid}.{lan}{(url.Contains(".json") ? ".srt" : ".ass")}"
|
|
};
|
|
|
|
//有空的URL 不合法
|
|
if (subtitles.Any(s => string.IsNullOrEmpty(s.url)))
|
|
throw new Exception("Bad url");
|
|
|
|
subtitles.Add(subtitle);
|
|
}
|
|
return subtitles;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static async Task<List<Subtitle>?> GetSubtitlesFromApi1Async(string aid, string cid, string epId, int index)
|
|
{
|
|
try
|
|
{
|
|
List<Subtitle> subtitles = new();
|
|
string api = $"https://api.bilibili.com/x/web-interface/view?aid={aid}&cid={cid}";
|
|
string json = await GetWebSourceAsync(api);
|
|
using var infoJson = JsonDocument.Parse(json);
|
|
var subs = infoJson.RootElement.GetProperty("data").GetProperty("subtitle").GetProperty("list").EnumerateArray();
|
|
foreach (var sub in subs)
|
|
{
|
|
var lan = sub.GetProperty("lan").ToString();
|
|
Subtitle subtitle = new()
|
|
{
|
|
url = sub.GetProperty("subtitle_url").ToString(),
|
|
lan = lan,
|
|
path = $"{aid}/{aid}.{cid}.{lan}.srt"
|
|
};
|
|
subtitles.Add(subtitle);
|
|
}
|
|
|
|
//有空的URL 不合法
|
|
if (subtitles.Any(s => string.IsNullOrEmpty(s.url)))
|
|
throw new Exception("Bad url");
|
|
|
|
//无字幕片源 但是字幕没上导致的空列表,尝试从国际接口获取
|
|
//if (subtitles.Count == 0 && !string.IsNullOrEmpty(epId))
|
|
//{
|
|
// return await GetSubtitlesAsync(aid, cid, epId, true);
|
|
//}
|
|
return subtitles;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static async Task<List<Subtitle>?> GetSubtitlesFromApi2Async(string aid, string cid, string epId, int index)
|
|
{
|
|
try
|
|
{
|
|
List<Subtitle> subtitles = new();
|
|
string api = $"https://api.bilibili.com/x/player/wbi/v2?cid={cid}&aid={aid}";
|
|
string json = await GetWebSourceAsync(api);
|
|
using var infoJson = JsonDocument.Parse(json);
|
|
var subs = infoJson.RootElement.GetProperty("data").GetProperty("subtitle").GetProperty("subtitles").EnumerateArray();
|
|
foreach (var sub in subs)
|
|
{
|
|
var lan = sub.GetProperty("lan").ToString();
|
|
Subtitle subtitle = new()
|
|
{
|
|
url = sub.GetProperty("subtitle_url").ToString(),
|
|
lan = lan,
|
|
path = $"{aid}/{aid}.{cid}.{lan}.srt"
|
|
};
|
|
subtitles.Add(subtitle);
|
|
}
|
|
|
|
//有空的URL 不合法
|
|
if (subtitles.Any(s => string.IsNullOrEmpty(s.url)))
|
|
throw new Exception("Bad url");
|
|
|
|
return subtitles;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static byte[] GetPayload(long aid, long cid)
|
|
{
|
|
var obj = new DmViewReq
|
|
{
|
|
Pid = aid,
|
|
Oid = cid,
|
|
Type = 1,
|
|
Spmid = "main.ugc-video-detail.0.0",
|
|
};
|
|
return AppHelper.PackMessage(obj.ToByteArray());
|
|
}
|
|
|
|
private static async Task<List<Subtitle>?> GetSubtitlesFromApi3Async(string aid, string cid, string epId, int index)
|
|
{
|
|
try
|
|
{
|
|
List<Subtitle> subtitles = new();
|
|
//grpc调用接口 protobuf
|
|
string api = "https://app.biliapi.net/bilibili.community.service.dm.v1.DM/DmView";
|
|
|
|
var data = GetPayload(Convert.ToInt64(aid), Convert.ToInt64(cid));
|
|
|
|
var t = AppHelper.ReadMessage(await GetPostResponseAsync(api, data));
|
|
var resp = new MessageParser<DmViewReply>(() => new DmViewReply()).ParseFrom(t);
|
|
|
|
if (resp.Subtitle != null && resp.Subtitle.Subtitles != null)
|
|
{
|
|
subtitles.AddRange(resp.Subtitle.Subtitles.Select(item => new Subtitle() {
|
|
url = item.SubtitleUrl,
|
|
lan = item.Lan,
|
|
path = $"{aid}/{aid}.{cid}.{item.Lan}.srt"
|
|
}));
|
|
}
|
|
//有空的URL 不合法
|
|
if (subtitles.Any(s => string.IsNullOrEmpty(s.url)))
|
|
throw new Exception("Bad url");
|
|
|
|
return subtitles;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
public static async Task<List<Subtitle>> GetSubtitlesAsync(string aid, string cid, string epId, int index, bool intl)
|
|
{
|
|
List<Subtitle>? subtitles = new();
|
|
if (intl)
|
|
{
|
|
subtitles = await GetIntlSubtitlesFromApi1Async(aid, cid, epId, index) ?? await GetIntlSubtitlesFromApi2Async(aid, cid, epId, index);
|
|
}
|
|
else
|
|
{
|
|
if (Config.COOKIE == "")
|
|
{
|
|
subtitles = await GetSubtitlesFromApi3Async(aid, cid, epId, index); // 未登录只有APP可以拿到字幕了
|
|
}
|
|
else
|
|
{
|
|
subtitles = await GetSubtitlesFromApi2Async(aid, cid, epId, index)
|
|
?? await GetSubtitlesFromApi1Async(aid, cid, epId, index)
|
|
?? await GetSubtitlesFromApi3Async(aid, cid, epId, index);
|
|
}
|
|
|
|
}
|
|
|
|
if (subtitles == null)
|
|
{
|
|
return new List<Subtitle>(); //返回空列表
|
|
}
|
|
|
|
//修正 url 协议
|
|
foreach (var item in subtitles)
|
|
{
|
|
if (item.url.StartsWith("//")) item.url = "https:" + item.url;
|
|
}
|
|
|
|
return subtitles;
|
|
}
|
|
|
|
public static async Task SaveSubtitleAsync(string url, string path)
|
|
{
|
|
if (path.EndsWith(".srt"))
|
|
await File.WriteAllTextAsync(path, ConvertSubFromJson(await GetWebSourceAsync(url)), Encoding.UTF8);
|
|
else
|
|
await File.WriteAllTextAsync(path, await GetWebSourceAsync(url), Encoding.UTF8);
|
|
}
|
|
|
|
private static string ConvertSubFromJson(string jsonString)
|
|
{
|
|
StringBuilder lines = new();
|
|
var json = JsonDocument.Parse(jsonString);
|
|
var sub = json.RootElement.GetProperty("body").EnumerateArray().ToList();
|
|
for(int i = 0; i < sub.Count; i++)
|
|
{
|
|
var line = sub[i];
|
|
lines.AppendLine((i + 1).ToString());
|
|
if (line.TryGetProperty("from", out JsonElement from))
|
|
{
|
|
lines.AppendLine($"{FormatTime(from.GetDouble())} --> {FormatTime(line.GetProperty("to").GetDouble())}");
|
|
}
|
|
else
|
|
{
|
|
lines.AppendLine($"{FormatTime(0.0)} --> {FormatTime(line.GetProperty("to").GetDouble())}");
|
|
}
|
|
//有的没有内容
|
|
if (line.TryGetProperty("content", out JsonElement content))
|
|
lines.AppendLine(content.ToString());
|
|
lines.AppendLine();
|
|
}
|
|
return lines.ToString();
|
|
}
|
|
|
|
private static string FormatTime(double sec) //64.13
|
|
{
|
|
return TimeSpan.FromSeconds(sec).ToString(@"hh\:mm\:ss\,fff");
|
|
}
|
|
|
|
[GeneratedRegex("-[a-z]")]
|
|
private static partial Regex NonCapsRegex();
|
|
} |