Files

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();
}