From 2537e3df890e1f0547ef7cd284e80d649f7d8032 Mon Sep 17 00:00:00 2001 From: nilaoda Date: Sat, 3 Oct 2020 23:28:43 +0800 Subject: [PATCH] v1.3.0 --- BBDown/BBDown.csproj | 2 +- BBDown/BBDownBangumiInfoFetcher.cs | 59 ++++++++++++++ BBDown/BBDownCheeseInfoFetcher.cs | 6 +- BBDown/BBDownMuxer.cs | 2 +- BBDown/BBDownNormalInfoFetcher.cs | 16 +--- BBDown/BBDownParser.cs | 4 +- BBDown/BBDownSubUtil.cs | 66 +++++++++++++++ BBDown/BBDownUtil.cs | 119 ++++++++++++++++++++++----- BBDown/BBDownVInfo.cs | 6 -- BBDown/Program.cs | 125 +++++++++++++++++++---------- README.md | 18 ++++- 11 files changed, 334 insertions(+), 89 deletions(-) create mode 100644 BBDown/BBDownBangumiInfoFetcher.cs diff --git a/BBDown/BBDown.csproj b/BBDown/BBDown.csproj index dd76470f1..2ad648e9b 100644 --- a/BBDown/BBDown.csproj +++ b/BBDown/BBDown.csproj @@ -3,7 +3,7 @@ Exe netcoreapp3.1 - 1.2.4 + 1.3.0 BBDown是一个免费且便捷高效的哔哩哔哩下载/解析软件. diff --git a/BBDown/BBDownBangumiInfoFetcher.cs b/BBDown/BBDownBangumiInfoFetcher.cs new file mode 100644 index 000000000..e80721807 --- /dev/null +++ b/BBDown/BBDownBangumiInfoFetcher.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; +using static BBDown.BBDownEntity; +using static BBDown.BBDownUtil; + +namespace BBDown +{ + class BBDownBangumiInfoFetcher : IFetcher + { + public BBDownVInfo Fetch(string id) + { + id = id.Substring(3); + string index = ""; + string api = $"https://api.bilibili.com/pgc/view/web/season?ep_id={id}"; + string json = GetWebSource(api); + JObject infoJson = JObject.Parse(json); + string cover = infoJson["result"]["cover"].ToString(); + string title = infoJson["result"]["title"].ToString(); + string desc = infoJson["result"]["evaluate"].ToString(); + string pubTime = infoJson["result"]["publish"]["pub_time"].ToString(); + JArray pages = JArray.Parse(infoJson["result"]["episodes"].ToString()); + List pagesInfo = new List(); + int i = 1; + foreach (JObject page in pages) + { + string res = ""; + try + { + res = page["dimension"]["width"].ToString() + "x" + page["dimension"]["height"].ToString(); + } + catch (Exception) { } + string _title = page["long_title"].ToString(); + if(string.IsNullOrEmpty(_title)) _title = page["title"].ToString(); + Page p = new Page(i++, + page["aid"].ToString(), + page["cid"].ToString(), + page["id"].ToString(), + GetValidFileName(_title), + 0, res); + if (p.epid == id) index = p.index.ToString(); + pagesInfo.Add(p); + } + + var info = new BBDownVInfo(); + info.Title = GetValidFileName(title).Trim(); + info.Desc = GetValidFileName(desc).Trim(); + info.Pic = cover; + info.PubTime = pubTime; + info.PagesInfo = pagesInfo; + info.IsBangumi = true; + info.IsCheese = true; + info.Index = index; + + return info; + } + } +} diff --git a/BBDown/BBDownCheeseInfoFetcher.cs b/BBDown/BBDownCheeseInfoFetcher.cs index f5bfbe50f..460791218 100644 --- a/BBDown/BBDownCheeseInfoFetcher.cs +++ b/BBDown/BBDownCheeseInfoFetcher.cs @@ -11,6 +11,7 @@ class BBDownCheeseInfoFetcher : IFetcher { public BBDownVInfo Fetch(string id) { + id = id.Substring(7); string index = ""; string api = $"https://api.bilibili.com/pugv/view/web/season?ep_id={id}"; string json = GetWebSource(api); @@ -35,11 +36,10 @@ public BBDownVInfo Fetch(string id) pubTime = pubTime != "" ? (new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(Convert.ToDouble(pubTime)).ToLocalTime().ToString()) : ""; var info = new BBDownVInfo(); - info.Title = GetValidFileName(title); - info.Desc = GetValidFileName(desc); + info.Title = GetValidFileName(title).Trim(); + info.Desc = GetValidFileName(desc).Trim(); info.Pic = cover; info.PubTime = pubTime; - info.Subtitles = new List(); //课程不考虑外挂字幕 info.PagesInfo = pagesInfo; info.IsBangumi = true; info.IsCheese = true; diff --git a/BBDown/BBDownMuxer.cs b/BBDown/BBDownMuxer.cs index cb510852d..b5f9389a5 100644 --- a/BBDown/BBDownMuxer.cs +++ b/BBDown/BBDownMuxer.cs @@ -37,7 +37,7 @@ public static int ffmpeg(string parms) public static int MuxAV(string videoPath, string audioPath, string outPath, string desc = "", string title = "", string episodeId = "", string pic = "", List subs = null, bool audioOnly = false, bool videoOnly = false) { - if (!Directory.Exists(Path.GetDirectoryName(outPath))) + if (outPath.Contains("/") && ! Directory.Exists(Path.GetDirectoryName(outPath))) Directory.CreateDirectory(Path.GetDirectoryName(outPath)); //----分析并生成-i参数 StringBuilder inputArg = new StringBuilder(); diff --git a/BBDown/BBDownNormalInfoFetcher.cs b/BBDown/BBDownNormalInfoFetcher.cs index de8b3a321..d9a567e79 100644 --- a/BBDown/BBDownNormalInfoFetcher.cs +++ b/BBDown/BBDownNormalInfoFetcher.cs @@ -22,17 +22,6 @@ public BBDownVInfo Fetch(string id) pubTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(Convert.ToDouble(pubTime)).ToLocalTime().ToString(); bool bangumi = false; - JArray subs = JArray.Parse(infoJson["data"]["subtitle"]["list"].ToString()); - List subtitleInfo = new List(); - foreach (JObject sub in subs) - { - Subtitle subtitle = new Subtitle(); - subtitle.url = sub["subtitle_url"].ToString(); - subtitle.lan = sub["lan"].ToString(); - subtitle.path = $"{id}/{id}.{subtitle.lan}.srt"; - subtitleInfo.Add(subtitle); - } - JArray pages = JArray.Parse(infoJson["data"]["pages"].ToString()); List pagesInfo = new List(); foreach (JObject page in pages) @@ -63,11 +52,10 @@ public BBDownVInfo Fetch(string id) catch { } var info = new BBDownVInfo(); - info.Title = GetValidFileName(title); - info.Desc = GetValidFileName(desc); + info.Title = GetValidFileName(title).Trim(); + info.Desc = GetValidFileName(desc).Trim(); info.Pic = pic; info.PubTime = pubTime; - info.Subtitles = subtitleInfo; info.PagesInfo = pagesInfo; info.IsBangumi = bangumi; diff --git a/BBDown/BBDownParser.cs b/BBDown/BBDownParser.cs index 1fbeba32f..238c8f645 100644 --- a/BBDown/BBDownParser.cs +++ b/BBDown/BBDownParser.cs @@ -9,8 +9,10 @@ namespace BBDown { class BBDownParser { - public static string GetPlayJson(string aid, string cid, string epId, bool tvApi, bool bangumi, bool cheese, string qn = "0") + public static string GetPlayJson(string aidOri, string aid, string cid, string epId, bool tvApi, string qn = "0") { + bool cheese = aidOri.StartsWith("cheese:"); + bool bangumi = cheese || aidOri.StartsWith("ep:"); LogDebug("aid={0},cid={1},epId={2},tvApi={3},bangumi={4},cheese={5},qn={6}", aid, cid, epId, tvApi, bangumi, cheese, qn); string prefix = tvApi ? (bangumi ? "api.snm0516.aisee.tv/pgc/player/api/playurltv" : "api.snm0516.aisee.tv/x/tv/ugc/playurl") : (bangumi ? "api.bilibili.com/pgc/player/web/playurl" : "api.bilibili.com/x/player/playurl"); diff --git a/BBDown/BBDownSubUtil.cs b/BBDown/BBDownSubUtil.cs index d66ae4ce6..f1a0b9e47 100644 --- a/BBDown/BBDownSubUtil.cs +++ b/BBDown/BBDownSubUtil.cs @@ -3,12 +3,78 @@ using System.Collections.Generic; using System.IO; using System.Text; +using static BBDown.BBDownEntity; using static BBDown.BBDownUtil; +using static BBDown.BBDownLogger; +using System.Text.RegularExpressions; namespace BBDown { class BBDownSubUtil { + public static List GetSubtitles(string aid, string cid) + { + List subtitles = new List(); + try + { + string api = $"https://api.bilibili.com/x/web-interface/view?aid={aid}&cid={cid}"; + string json = GetWebSource(api); + JObject infoJson = JObject.Parse(json); + JArray subs = JArray.Parse(infoJson["data"]["subtitle"]["list"].ToString()); + foreach (JObject sub in subs) + { + Subtitle subtitle = new Subtitle(); + subtitle.url = sub["subtitle_url"].ToString(); + subtitle.lan = sub["lan"].ToString(); + subtitle.path = $"{aid}/{aid}.{cid}.{subtitle.lan}.srt"; + subtitles.Add(subtitle); + } + return subtitles; + } + catch (Exception) + { + try + { + //grpc调用接口 protobuf + string api = "https://app.biliapi.net/bilibili.community.service.dm.v1.DM/DmView"; + int _aid = Convert.ToInt32(aid); + int _cid = Convert.ToInt32(cid); + int _type = 1; + byte[] data = new byte[18]; + data[0] = 0x0; data[1] = 0x0; data[2] = 0x0; data[3] = 0x0; data[4] = 0xD; //先固定死了 + int i = 5; + data[i++] = Convert.ToByte(1 << 3 | 0); // index=1 + while ((_aid & -128) != 0) + { + data[i++] = Convert.ToByte((_aid & 127) | 128); + _aid = _aid >> 7; + } + data[i++] = Convert.ToByte(_aid); + data[i++] = Convert.ToByte(2 << 3 | 0); // index=2 + while ((_cid & -128) != 0) + { + data[i++] = Convert.ToByte((_cid & 127) | 128); + _cid = _cid >> 7; + } + data[i++] = Convert.ToByte(_cid); + data[i++] = Convert.ToByte(3 << 3 | 0); // index=3 + data[i++] = Convert.ToByte(_type); + string t = GetPostResponse(api, data); + Regex reg = new Regex("(zh-Han[st]).*?(http.*?\\.json)"); + foreach(Match m in reg.Matches(t)) + { + Subtitle subtitle = new Subtitle(); + subtitle.url = m.Groups[2].Value; + subtitle.lan = m.Groups[1].Value; + subtitle.path = $"{aid}/{aid}.{cid}.{subtitle.lan}.srt"; + subtitles.Add(subtitle); + } + return subtitles; + } + catch (Exception) { return subtitles; } //返回空列表 + } + } + public static void SaveSubtitle(string url, string path) { File.WriteAllText(path, ConvertSubFromJson(GetWebSource(url)), new UTF8Encoding()); diff --git a/BBDown/BBDownUtil.cs b/BBDown/BBDownUtil.cs index c7ea89223..7ad2c96d0 100644 --- a/BBDown/BBDownUtil.cs +++ b/BBDown/BBDownUtil.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -14,7 +13,6 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using System.Web; using static BBDown.BBDownEntity; @@ -24,6 +22,25 @@ namespace BBDown { class BBDownUtil { + public static async Task CheckUpdateAsync() + { + try + { + var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; + string nowVer = $"{ver.Major}.{ver.Minor}.{ver.Build}"; + string redirctUrl = await Get302("https://github.com/nilaoda/BBDown/releases/latest"); + string latestVer = redirctUrl.Replace("https://github.com/nilaoda/BBDown/releases/tag/", ""); + if (nowVer != latestVer && !latestVer.StartsWith("https")) + { + Console.Title = $"发现新版本:{latestVer}"; + } + } + catch (Exception) + { + ; + } + } + public static async Task GetAvIdAsync(string input) { if (input.StartsWith("http")) @@ -44,21 +61,29 @@ public static async Task GetAvIdAsync(string input) } else if (input.Contains("/cheese/")) { - string epId = Regex.Match(input, "ep(\\d{1,})").Groups[1].Value; + string epId = ""; + if (input.Contains("/ep")) + { + epId = Regex.Match(input, "/ep(\\d{1,})").Groups[1].Value; + } + else if(input.Contains("/ss")) + { + epId = GetEpidBySSId(Regex.Match(input, "/ss(\\d{1,})").Groups[1].Value); + } return $"cheese:{epId}"; } + else if (input.Contains("/ep")) + { + string epId = Regex.Match(input, "/ep(\\d{1,})").Groups[1].Value; + return $"ep:{epId}"; + } else { string web = GetWebSource(input); Regex regex = new Regex("window.__INITIAL_STATE__=([\\s\\S].*?);\\(function\\(\\)"); string json = regex.Match(web).Groups[1].Value; - string aid = JObject.Parse(json)["epInfo"]["aid"].ToString(); - string cid = JObject.Parse(json)["epInfo"]["cid"].ToString(); - if (aid == "-1" || cid == "-1") //重新获取 - { - aid = JObject.Parse(json)["epList"][0]["aid"].ToString(); - } - return aid; + string epId = JObject.Parse(json)["epList"][0]["id"].ToString(); + return $"ep:{epId}"; } } else if (input.StartsWith("BV")) @@ -73,18 +98,18 @@ public static async Task GetAvIdAsync(string input) { return input.ToLower().Replace("av", ""); } - else if (input.StartsWith("ep") || input.StartsWith("ss")) + else if (input.StartsWith("ep")) + { + string epId = Regex.Match(input, "ep(\\d{1,})").Groups[1].Value; + return $"ep:{epId}"; + } + else if (input.StartsWith("ss")) { string web = GetWebSource("https://www.bilibili.com/bangumi/play/" + input); Regex regex = new Regex("window.__INITIAL_STATE__=([\\s\\S].*?);\\(function\\(\\)"); string json = regex.Match(web).Groups[1].Value; - string aid = JObject.Parse(json)["epInfo"]["aid"].ToString(); - string cid = JObject.Parse(json)["epInfo"]["cid"].ToString(); - if (aid == "-1" || cid == "-1") //重新获取 - { - aid = JObject.Parse(json)["epList"][0]["aid"].ToString(); - } - return aid; + string epId = JObject.Parse(json)["epList"][0]["id"].ToString(); + return $"ep:{epId}"; } else { @@ -178,6 +203,56 @@ public static string GetWebSource(String url) return htmlCode; } + public static string GetPostResponse(string Url, byte[] postData) + { + string htmlCode = string.Empty; + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url); + request.Method = "POST"; + request.ContentType = "application/grpc"; + request.ContentLength = postData.Length; + request.UserAgent = "Dalvik/2.1.0 (Linux; U; Android 6.0.1; oneplus a5010 Build/V417IR) 6.10.0 os/android model/oneplus a5010 mobi_app/android build/6100500 channel/bili innerVer/6100500 osVer/6.0.1 network/2"; + request.Headers.Add("Cookie", Program.COOKIE); + Stream myRequestStream = request.GetRequestStream(); + myRequestStream.Write(postData); + myRequestStream.Close(); + HttpWebResponse webResponse = (HttpWebResponse)request.GetResponse(); + if (webResponse.ContentEncoding != null + && webResponse.ContentEncoding.ToLower() == "gzip") //如果使用了GZip则先解压 + { + using (Stream streamReceive = webResponse.GetResponseStream()) + { + using (var zipStream = + new GZipInputStream(streamReceive)) + { + using (StreamReader sr = new StreamReader(zipStream, Encoding.UTF8)) + { + htmlCode = sr.ReadToEnd(); + } + } + } + } + else + { + using (Stream streamReceive = webResponse.GetResponseStream()) + { + using (StreamReader sr = new StreamReader(streamReceive, Encoding.UTF8)) + { + htmlCode = sr.ReadToEnd(); + } + } + } + + if (webResponse != null) + { + webResponse.Close(); + } + if (request != null) + { + request.Abort(); + } + return htmlCode; + } + public static string GetAidByBV(string bv) { string api = $"https://api.bilibili.com/x/web-interface/archive/stat?bvid={bv}"; @@ -186,6 +261,14 @@ public static string GetAidByBV(string bv) return aid; } + public static string GetEpidBySSId(string ssid) + { + string api = $"https://api.bilibili.com/pugv/view/web/season?season_id={ssid}"; + string json = GetWebSource(api); + string epId = JObject.Parse(json)["data"]["episodes"][0]["id"].ToString(); + return epId; + } + public static async Task DownloadFile(string url, string path) { string tmpName = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path) + ".tmp"); diff --git a/BBDown/BBDownVInfo.cs b/BBDown/BBDownVInfo.cs index 384219ddc..7a1c41c4a 100644 --- a/BBDown/BBDownVInfo.cs +++ b/BBDown/BBDownVInfo.cs @@ -35,11 +35,6 @@ class BBDownVInfo private bool isBangumi; private bool isCheese; - /// - /// 视频CC字幕 - /// - private List subtitles; - /// /// 视频分P信息 /// @@ -52,7 +47,6 @@ class BBDownVInfo public bool IsBangumi { get => isBangumi; set => isBangumi = value; } public bool IsCheese { get => isCheese; set => isCheese = value; } public string Index { get => index; set => index = value; } - internal List Subtitles { get => subtitles; set => subtitles = value; } internal List PagesInfo { get => pagesInfo; set => pagesInfo = value; } } } diff --git a/BBDown/Program.cs b/BBDown/Program.cs index 2d0bc4528..fc732b6c0 100644 --- a/BBDown/Program.cs +++ b/BBDown/Program.cs @@ -8,7 +8,6 @@ using System.Drawing; using System.IO; using System.Net; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using static BBDown.BBDownEntity; @@ -252,6 +251,11 @@ private static async Task DoWorkAsync(MyOption myOption) Console.Write("请注意:任何BUG请前往以下网址反馈:\r\n" + "https://github.com/nilaoda/BBDown/issues\r\n"); Console.WriteLine(); + //检测更新 + new Thread(async () => + { + await CheckUpdateAsync(); + }).Start(); try { bool interactMode = myOption.Interactive; @@ -265,7 +269,7 @@ private static async Task DoWorkAsync(MyOption myOption) DEBUG_LOG = myOption.Debug; string input = myOption.Url; string selectPage = myOption.SelectPage; - string aid = ""; + string aidOri = ""; //原始aid COOKIE = myOption.Cookie; TOKEN = myOption.AccessToken != null ? myOption.AccessToken.Replace("access_token=", "") : ""; @@ -297,10 +301,10 @@ private static async Task DoWorkAsync(MyOption myOption) TOKEN = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "BBDownTV.data")); } Log("获取aid..."); - aid = await GetAvIdAsync(input); - Log("获取aid结束: " + aid); + aidOri = await GetAvIdAsync(input); + Log("获取aid结束: " + aidOri); //-p的优先级大于URL中的自带p参数,所以先清空selectedPages - if (!string.IsNullOrEmpty(selectPage)) + if (!string.IsNullOrEmpty(selectPage) && selectPage != "ALL") { selectedPages = new List(); try @@ -328,21 +332,18 @@ private static async Task DoWorkAsync(MyOption myOption) catch { LogError("解析分P参数时失败了~"); selectedPages = null; }; } - if (string.IsNullOrEmpty(aid)) throw new Exception("输入有误"); + if (string.IsNullOrEmpty(aidOri)) throw new Exception("输入有误"); + Log("获取视频信息..."); IFetcher fetcher = new BBDownNormalInfoFetcher(); - string cheeseId = ""; - if (aid.StartsWith("cheese")) + if (aidOri.StartsWith("cheese")) { - cheeseId = aid.Substring(7); fetcher = new BBDownCheeseInfoFetcher(); } - var vInfo = fetcher.Fetch(cheeseId != "" ? cheeseId : aid); - //如果用户没有选择分P,根据epid来确定某一集 - if (selectedPages == null && !string.IsNullOrEmpty(vInfo.Index)) + else if (aidOri.StartsWith("ep")) { - selectedPages = new List { vInfo.Index }; + fetcher = new BBDownBangumiInfoFetcher(); } - Log("获取视频信息..."); + var vInfo = fetcher.Fetch(aidOri); string title = vInfo.Title; string desc = vInfo.Desc; string pic = vInfo.Pic; @@ -350,35 +351,23 @@ private static async Task DoWorkAsync(MyOption myOption) LogColor("视频标题: " + title); Log("发布时间: " + pubTime); List pagesInfo = vInfo.PagesInfo; - List subtitleInfo = vInfo.Subtitles; + List subtitleInfo = new List(); bool more = false; bool bangumi = vInfo.IsBangumi; bool cheese = vInfo.IsCheese; if (!infoMode) { - if (!Directory.Exists(aid)) - { - Directory.CreateDirectory(aid); - } - Log("下载封面..."); - LogDebug("下载:{0}", pic); - new WebClient().DownloadFile(pic, $"{aid}/{aid}.jpg"); - foreach (Subtitle s in subtitleInfo) - { - Log($"下载字幕 {s.lan}..."); - LogDebug("下载:{0}", s.url); - BBDownSubUtil.SaveSubtitle(s.url, s.path); - } + } + //打印分P信息 foreach (Page p in pagesInfo) { - if (more) continue; - if (p.index > 5) + if (more && p.index != pagesInfo.Count) continue; + if (!more && p.index > 5) { - Log("P..."); - Log("分P太多, 已经省略部分..."); + Log("......"); more = true; } else @@ -386,6 +375,14 @@ private static async Task DoWorkAsync(MyOption myOption) Log($"P{p.index}: [{p.cid}] [{p.title}] [{FormatTime(p.dur)}]"); } } + + //如果用户没有选择分P,根据epid来确定某一集 + if (selectedPages == null && selectPage != "ALL" && !string.IsNullOrEmpty(vInfo.Index)) + { + selectedPages = new List { vInfo.Index }; + Log("程序已自动选择你输入的集数,如果要下载其他集数请自行指定分P(可使用参数ALL代表全部)"); + } + Log($"共计 {pagesInfo.Count} 个分P, 已选择:" + (selectedPages == null ? "ALL" : string.Join(",", selectedPages))); //过滤不需要的分P @@ -395,13 +392,34 @@ private static async Task DoWorkAsync(MyOption myOption) foreach (Page p in pagesInfo) { Log($"开始解析P{p.index}..."); + if (!infoMode) + { + if (!Directory.Exists(p.aid)) + { + Directory.CreateDirectory(p.aid); + } + if (!File.Exists($"{p.aid}/{p.aid}.jpg")) + { + Log("下载封面..."); + LogDebug("下载:{0}", pic); + new WebClient().DownloadFile(pic, $"{p.aid}/{p.aid}.jpg"); + } + LogDebug("获取字幕..."); + subtitleInfo = BBDownSubUtil.GetSubtitles(p.aid, p.cid); + foreach (Subtitle s in subtitleInfo) + { + Log($"下载字幕 {s.lan} => {BBDownSubUtil.SubDescDic[s.lan]}..."); + LogDebug("下载:{0}", s.url); + BBDownSubUtil.SaveSubtitle(s.url, s.path); + } + } List