diff --git a/BBDown/BBDownParser.cs b/BBDown/BBDownParser.cs index ffc431b99..cc47cbeae 100644 --- a/BBDown/BBDownParser.cs +++ b/BBDown/BBDownParser.cs @@ -13,8 +13,21 @@ public static string GetPlayJson(string aid, string cid, string epId, bool tvApi 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"); string api = $"https://{prefix}?avid={aid}&cid={cid}&qn={qn}&type=&otype=json" + (tvApi ? "" : "&fourk=1") + - "&fnver=0&fnval=16" + (tvApi ? "&device=android&platform=android&mobi_app=android_tv_yst&npcybs=0&force_host=0&build=102801" : "") + + "&fnver=0&fnval=16" + (tvApi ? "&device=android&platform=android" + + "&mobi_app=android_tv_yst&npcybs=0&force_host=0&build=102801" + + (Program.TOKEN != "" ? $"&access_key={GetQueryString("access_token", Program.TOKEN)}" : "") : "") + (bangumi ? $"&module=bangumi&ep_id={epId}&fourk=1" + "&session=" : ""); + if (tvApi && bangumi) + { + api = (Program.TOKEN != "" ? $"access_key={GetQueryString("access_token", Program.TOKEN)}&" : "") + + $"aid={aid}&appkey=4409e2ce8ffd12b8&build=102801" + + $"&cid={cid}&device=android&ep_id={epId}&expire=0" + + $"&fnval=16&fnver=0&fourk=1" + + $"&mid=0&mobi_app=android_tv_yst" + + $"&module=bangumi&npcybs=0&otype=json&platform=android" + + $"&qn={qn}&ts={GetTimeStamp(true)}"; + api = $"https://{prefix}?" + api + (bangumi ? $"&sign={GetSign(api)}" : ""); + } //Console.WriteLine(api); string webJson = GetWebSource(api); //以下情况从网页源代码尝试解析 diff --git a/BBDown/BBDownUtil.cs b/BBDown/BBDownUtil.cs index 298e8206a..d24b368fd 100644 --- a/BBDown/BBDownUtil.cs +++ b/BBDown/BBDownUtil.cs @@ -6,13 +6,16 @@ using System.Collections.Specialized; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.Cache; using System.Net.Http; using System.Net.Http.Headers; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Web; using static BBDown.BBDownEntity; namespace BBDown @@ -182,7 +185,8 @@ public static async Task DownloadFile(string url, string path) { long totalLength = -1; WebClient client = new WebClient(); - client.Headers.Add("Referer", "https://www.bilibili.com"); + if (!url.Contains("platform=android_tv_yst")) + client.Headers.Add("Referer", "https://www.bilibili.com"); client.Headers.Add("User-Agent", "Mozilla/5.0"); client.Headers.Add("Cookie", Program.COOKIE); client.Credentials = CredentialCache.DefaultCredentials; @@ -228,7 +232,8 @@ public static void MultiThreadDownloadFile(string url, string path) request.AllowAutoRedirect = true; request.KeepAlive = false; request.Method = "GET"; - request.Referer = "https://www.bilibili.com"; + if (!url.Contains("platform=android_tv_yst")) + request.Referer = "https://www.bilibili.com"; request.UserAgent = "Mozilla/5.0"; request.Headers.Add("Cookie", Program.COOKIE); if (clip.to != -1) @@ -358,7 +363,8 @@ public static string[] GetFiles(string dir, string ext) private static long GetFileSize(string url) { WebClient webClient = new WebClient(); - webClient.Headers.Add("Referer", "https://www.bilibili.com"); + if (!url.Contains("platform=android_tv_yst")) + webClient.Headers.Add("Referer", "https://www.bilibili.com"); webClient.Headers.Add("User-Agent", "Mozilla/5.0"); webClient.OpenRead(url); long totalSizeBytes = Convert.ToInt64(webClient.ResponseHeaders["Content-Length"]); @@ -441,5 +447,79 @@ public static string GetSession(string buvid3) //这个参数可以没有 所以此处就不写具体实现了 throw new NotImplementedException(); } + + public static string GetSign(string parms) + { + string toEncode = parms + "59b43e04ad6965f34319062b478f83dd"; + MD5 md5 = MD5.Create(); + byte[] bs = Encoding.UTF8.GetBytes(toEncode); + byte[] hs = md5.ComputeHash(bs); + StringBuilder sb = new StringBuilder(); + foreach (byte b in hs) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString(); + } + + public static string GetTimeStamp(bool bflag) + { + TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); + string ret = string.Empty; + if (bflag) + ret = Convert.ToInt64(ts.TotalSeconds).ToString(); + else + ret = Convert.ToInt64(ts.TotalMilliseconds).ToString(); + + return ret; + } + + //https://stackoverflow.com/questions/1344221/how-can-i-generate-random-alphanumeric-strings + private static Random random = new Random(); + public static string GetRandomString(int length) + { + const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + + //https://stackoverflow.com/a/45088333 + public static string ToQueryString(NameValueCollection nameValueCollection) + { + NameValueCollection httpValueCollection = HttpUtility.ParseQueryString(String.Empty); + httpValueCollection.Add(nameValueCollection); + return httpValueCollection.ToString(); + } + + public static NameValueCollection GetTVLoginParms() + { + NameValueCollection sb = new NameValueCollection(); + DateTime now = DateTime.Now; + string deviceId = GetRandomString(20); + string buvid = GetRandomString(37); + string fingerprint = $"{now.ToString("yyyyMMddHHmmssfff")}{GetRandomString(45)}"; + sb.Add("appkey","4409e2ce8ffd12b8"); + sb.Add("auth_code", ""); + sb.Add("bili_local_id", deviceId); + sb.Add("build", "102801"); + sb.Add("buvid", buvid); + sb.Add("channel", "master"); + sb.Add("device", "OnePlus"); + sb.Add($"device_id", deviceId); + sb.Add("device_name", "OnePlus7TPro"); + sb.Add("device_platform", "Android10OnePlusHD1910"); + sb.Add($"fingerprint", fingerprint); + sb.Add($"guid", buvid); + sb.Add($"local_fingerprint", fingerprint); + sb.Add($"local_id", buvid); + sb.Add("mobi_app", "android_tv_yst"); + sb.Add("networkstate", "wifi"); + sb.Add("platform", "android"); + sb.Add("sys_ver", "29"); + sb.Add($"ts", GetTimeStamp(true)); + sb.Add($"sign", GetSign(ToQueryString(sb))); + + return sb; + } } } diff --git a/BBDown/Program.cs b/BBDown/Program.cs index de51d376e..aa70124ff 100644 --- a/BBDown/Program.cs +++ b/BBDown/Program.cs @@ -16,12 +16,14 @@ using static BBDown.BBDownParser; using static BBDown.BBDownLogger; using static BBDown.BBDownMuxer; +using System.Text; namespace BBDown { class Program { public static string COOKIE = ""; + public static string TOKEN = ""; static Dictionary<string, string> qualitys = new Dictionary<string, string>() { {"120","超清 4K" }, {"116","高清 1080P60" },{"112","高清 1080P+" }, {"80","高清 1080P" }, {"74","高清 720P60" },{"64","高清 720P" }, @@ -49,6 +51,7 @@ class MyOption public bool MultiThread { get; set; } public string SelectPage { get; set; } public string Cookie { get; set; } + public string AccessToken { get; set; } } public static int Main(params string[] args) @@ -83,16 +86,24 @@ public static int Main(params string[] args) "选择指定分p或分p范围"), new Option<string>( new string[]{ "--cookie" ,"-c"}, - "设置字符串cookie用以下载网页接口的会员内容") + "设置字符串cookie用以下载网页接口的会员内容"), + new Option<string>( + new string[]{ "--access-token" ,"-a"}, + "设置access_token用以下载TV接口的会员内容") }; Command loginCommand = new Command( "login", - "通过APP扫描二维码以登录您的账号"); + "通过APP扫描二维码以登录您的WEB账号"); rootCommand.AddCommand(loginCommand); + Command loginTVCommand = new Command( + "logintv", + "通过APP扫描二维码以登录您的TV账号"); + rootCommand.AddCommand(loginTVCommand); rootCommand.Description = "BBDown是一个免费且便捷高效的哔哩哔哩下载/解析软件."; rootCommand.TreatUnmatchedTokensAsErrors = true; + //WEB登录 loginCommand.Handler = CommandHandler.Create(delegate { try @@ -147,6 +158,60 @@ public static int Main(params string[] args) catch (Exception e) { LogError(e.Message); } }); + //TV登录 + loginTVCommand.Handler = CommandHandler.Create(delegate + { + try + { + string loginUrl = "https://passport.snm0516.aisee.tv/x/passport-tv-login/qrcode/auth_code"; + string pollUrl = "https://passport.bilibili.com/x/passport-tv-login/qrcode/poll"; + var parms = GetTVLoginParms(); + Log("获取登录地址..."); + WebClient webClient = new WebClient(); + byte[] responseArray = webClient.UploadValues(loginUrl, parms); + string web = Encoding.UTF8.GetString(responseArray); + string url = JObject.Parse(web)["data"]["url"].ToString(); + string authCode = JObject.Parse(web)["data"]["auth_code"].ToString(); + Log("生成二维码..."); + QRCodeGenerator qrGenerator = new QRCodeGenerator(); + QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); + QRCode qrCode = new QRCode(qrCodeData); + Bitmap qrCodeImage = qrCode.GetGraphic(7); + qrCodeImage.Save("qrcode.png", System.Drawing.Imaging.ImageFormat.Png); + Log("生成二维码成功:qrcode.png, 请打开并扫描"); + parms.Set("auth_code", authCode); + parms.Set("ts", GetTimeStamp(true)); + parms.Remove("sign"); + parms.Add("sign", GetSign(ToQueryString(parms))); + while (true) + { + Thread.Sleep(1000); + responseArray = webClient.UploadValues(pollUrl, parms); + web = Encoding.UTF8.GetString(responseArray); + string code = JObject.Parse(web)["code"].ToString(); + if (code == "86038") + { + LogColor("二维码已过期, 请重新执行登录指令."); + break; + } + else if (code == "86039") //等待扫码 + { + continue; + } + else + { + string cc = JObject.Parse(web)["data"]["access_token"].ToString(); + Log("登录成功: AccessToken=" + cc); + //导出cookie + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "BBDownTV.data"), "access_token=" + cc); + File.Delete("qrcode.png"); + break; + } + } + } + catch (Exception e) { LogError(e.Message); } + }); + rootCommand.Handler = CommandHandler.Create<MyOption>(async (myOption) => { //Console.WriteLine(myOption.ToString()); @@ -160,7 +225,7 @@ private static async Task DoWorkAsync(MyOption myOption) { Console.BackgroundColor = ConsoleColor.DarkBlue; Console.ForegroundColor = ConsoleColor.White; - Console.Write("BBDown version 20200810[RC2], Bilibili Downloader.\r\n"); + Console.Write("BBDown version 20200816[RC3], Bilibili Downloader.\r\n"); Console.ResetColor(); Console.Write("请注意:这是一个测试版本,任何BUG请前往以下网址反馈:\r\n" + "https://github.com/nilaoda/BBDown/issues\r\n"); @@ -177,18 +242,24 @@ private static async Task DoWorkAsync(MyOption myOption) string selectPage = myOption.SelectPage; string aid = ""; COOKIE = myOption.Cookie; + TOKEN = myOption.AccessToken.Replace("access_token=", ""); List<string> selectedPages = null; if (!string.IsNullOrEmpty(GetQueryString("p", input))) { selectedPages = new List<string>(); selectedPages.Add(GetQueryString("p", input)); } - - if (File.Exists(Path.Combine(AppContext.BaseDirectory, "BBDown.data"))) + + if (File.Exists(Path.Combine(AppContext.BaseDirectory, "BBDown.data")) && !tvApi) { Log("加载本地cookie..."); COOKIE = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "BBDown.data")); } + if (File.Exists(Path.Combine(AppContext.BaseDirectory, "BBDownTV.data")) && tvApi) + { + Log("加载本地token..."); + TOKEN = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "BBDownTV.data")); + } Log("获取aid..."); aid = await GetAvIdAsync(input); Log("获取aid结束: " + aid); @@ -312,7 +383,7 @@ private static async Task DoWorkAsync(MyOption myOption) JArray video = null; //此处代码简直灾难,后续优化吧 - if (webJson.Contains("\"dash\":")) //dash + if (webJson.Contains("\"dash\":[")) //dash { string nodeName = "data"; if (webJson.Contains("\"result\":{")) diff --git a/README.md b/README.md index 0f6aa2c0f..cd0ce6558 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,25 @@ Arguments: <url> 视频地址 或 av|bv|BV|ep|ss Options: - -tv, --use-tv-api 使用TV端解析模式(不支持版权内容) - -hevc, --only-hevc 下载hevc编码 - -info, --only-show-info 仅解析不下载 - -hs, --hide-streams 不要显示所有可用音视频流 - -ia, --interactive 交互式选择清晰度 - -mt, --multi-thread 使用多线程下载 - -p, --select-page <select-page> 选择指定分p或分p范围 - -c, --cookie <cookie> 设置字符串cookie用以下载网页接口的会员内容 - --version Show version information - -?, -h, --help Show help and usage information + -tv, --use-tv-api 使用TV端解析模式(不支持版权内容) + -hevc, --only-hevc 下载hevc编码 + -info, --only-show-info 仅解析不下载 + -hs, --hide-streams 不要显示所有可用音视频流 + -ia, --interactive 交互式选择清晰度 + -mt, --multi-thread 使用多线程下载 + -p, --select-page <select-page> 选择指定分p或分p范围 + -c, --cookie <cookie> 设置字符串cookie用以下载网页接口的会员内容 + -a, --access-token <access-token> 设置access_token用以下载TV接口的会员内容 + --version Show version information + -?, -h, --help Show help and usage information Commands: - login 通过APP扫描二维码以登录您的账号 + login 通过APP扫描二维码以登录您的WEB账号 + logintv 通过APP扫描二维码以登录您的TV账号 ``` # 功能 -- [x] 番剧下载(Web) +- [x] 番剧下载(Web|TV) - [x] 普通内容下载(Web|TV) `(TV接口可以下载部分UP主的无水印内容)` - [x] 多分P自动下载 - [x] 选择指定分P进行下载 @@ -48,6 +50,9 @@ Commands: - [ ] 其他的懒得写了 # 更新日志 +* 2020年8月16日 0:04 + 支持TV版番剧下载 + 支持TV二维码登录 * 2020年8月10日 20:19 修复严重BUG * 2020年8月9日 0:16