static void Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; var opencc = new OpenChineseConverter(); var twText = "這個小丑醜歸醜但很管用,這個滑鼠,長髮散發出香氣,列印中文為何出亂碼,阿拉伯聯合大公國,義大利,尚比亞"; var cnText = "这个小丑丑归丑但很管用,这个鼠标,长发散发出香气,打印中文为何出乱码,阿拉伯联合酋长国,意大利,赞比亚"; // 轉台灣正體 var openccTWText = opencc.ToTaiwanFromSimplifiedWithPhrases(cnText); // 轉簡體 var openccCNText = opencc.ToSimplifiedFromTaiwanWithPhrases(twText); // 顯示結果 Console.WriteLine($"===== 原文 =====\n{cnText}\n===== 轉繁體 =====\n{openccTWText}"); Console.WriteLine($"\n===== 原文 =====\n{twText}\n===== 轉簡體 =====\n{openccCNText}"); Console.ReadLine(); }
public static int MuxAV(string videoPath, string audioPath, string outPath, string desc = "", string title = "", string episodeId = "", string pic = "", string lang = "", List <Subtitle> subs = null, bool audioOnly = false, bool videoOnly = false, string aid = "", string cid = "") { desc = EscapeString(desc); title = EscapeString(title); if (outPath.Contains("/") && !Directory.Exists(Path.GetDirectoryName(outPath))) { Directory.CreateDirectory(Path.GetDirectoryName(outPath)); } //----分析并生成-i参数 StringBuilder inputArg = new StringBuilder(); StringBuilder metaArg = new StringBuilder(); if (!string.IsNullOrEmpty(videoPath)) { inputArg.Append($" -i \"{videoPath}\" "); } if (!string.IsNullOrEmpty(audioPath)) { inputArg.Append($" -i \"{audioPath}\" "); } if (!string.IsNullOrEmpty(pic)) { inputArg.Append($" -i \"{pic}\" "); } string[] files = System.IO.Directory.GetFiles(Directory.GetCurrentDirectory(), $"temp/{aid}/{aid}.{cid}.*.srt"); if (subs != null) { for (int i = 0; i < subs.Count; i++) { if (File.Exists(subs[i].path) && File.ReadAllText(subs[i].path) != "") { inputArg.Append($" -i \"{subs[i].path}\" "); metaArg.Append($" -metadata:s:s:{i} handler_name=\"{SubDescDic[subs[i].lan]}\" -metadata:s:s:{i} language={SubLangDic[subs[i].lan]} -metadata:s:s:{i} title=\"{SubTitleDic[subs[i].lan]}\""); } } if (files.Length > 0 && subs.Count < 1) { Log("正在合併現有字幕..."); for (int e = 0; e < files.Length; e++) { inputArg.Append($" -i \"{files[e]}\" "); } } } if (!string.IsNullOrEmpty(pic)) { metaArg.Append(" -disposition:v:1 attached_pic "); } var inputCount = Regex.Matches(inputArg.ToString(), "-i \"").Count; for (int i = 0; i < inputCount; i++) { inputArg.Append($" -map {i} "); } var converter = new OpenChineseConverter(); string titletcov = converter.ToTaiwanFromSimplifiedWithPhrases(title); string desccov = converter.ToTaiwanFromSimplifiedWithPhrases(desc); //----分析完毕 var arguments = $"-loglevel warning -y " + inputArg.ToString() + metaArg.ToString() + $" -metadata title=\"" + titletcov + "\" " + (lang == "" ? "" : $"-metadata:s:a:0 language={lang} ") + $"-metadata description=\"{desccov}\" " + (episodeId == "" ? "" : $"-metadata album=\"{titletcov}\" ") + (audioOnly ? " -vn " : "") + (videoOnly ? " -an " : "") + $"-c copy " + (subs != null ? " -c:s mov_text " : "") + $"\"{outPath}\""; LogDebug("ffmpeg命令:{0}", arguments); return(ffmpeg(arguments)); }
private static async Task DoWorkAsync(MyOption myOption) { Console.BackgroundColor = ConsoleColor.DarkBlue; Console.ForegroundColor = ConsoleColor.White; var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; Console.Write($"BBDown version {ver.Major}.{ver.Minor}.{ver.Build}, Bilibili Downloader.\r\n"); Console.ResetColor(); Console.Write("BBDown Server Edition"); Console.WriteLine(); //检测更新 new Thread(async() => { await CheckUpdateAsync(); }).Start(); try { //Read cookie from cookie.txt string cookiePath = Directory.GetCurrentDirectory(); if (!File.Exists($"cookie.txt")) { File.Create($"{cookiePath}/cookie.txt"); } string cookieString = File.ReadAllText($"{cookiePath}/cookie.txt"); if (cookieString != null) { cookieString = ""; } LogDebug(cookieString); bool interactMode = myOption.Interactive; bool infoMode = myOption.OnlyShowInfo; bool tvApi = myOption.UseTvApi; bool intlApi = myOption.UseIntlApi; bool hevc = myOption.OnlyHevc; bool hideStreams = myOption.HideStreams; bool multiThread = myOption.MultiThread; bool audioOnly = myOption.AudioOnly; bool videoOnly = myOption.VideoOnly; bool skipMux = myOption.SkipMux; bool showAll = myOption.ShowAll; bool useAria2c = myOption.UseAria2c; DEBUG_LOG = myOption.Debug; string input = myOption.Url; string lang = myOption.Language; string selectPage = myOption.SelectPage.ToUpper(); string aidOri = ""; //原始aid COOKIE = cookieString; TOKEN = myOption.AccessToken.Replace("access_token=", ""); //audioOnly和videoOnly同时开启则全部忽视 if (audioOnly && videoOnly) { audioOnly = false; videoOnly = false; } List <string> selectedPages = null; if (!string.IsNullOrEmpty(GetQueryString("p", input))) { selectedPages = new List <string>(); selectedPages.Add(GetQueryString("p", input)); } LogDebug("運行參數:{0}", myOption); if (string.IsNullOrEmpty(COOKIE) && File.Exists(Path.Combine(AppContext.BaseDirectory, "BBDown.data")) && !tvApi) { Log("加載本地cookie..."); LogDebug("文件路徑:{0}", Path.Combine(AppContext.BaseDirectory, "BBDown.data")); COOKIE = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "BBDown.data")); } if (string.IsNullOrEmpty(TOKEN) && File.Exists(Path.Combine(AppContext.BaseDirectory, "BBDownTV.data")) && tvApi) { Log("加載本地token..."); LogDebug("文件路徑:{0}", Path.Combine(AppContext.BaseDirectory, "BBDownTV.data")); TOKEN = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "BBDownTV.data")); TOKEN = TOKEN.Replace("access_token=", ""); } Log("獲取aid..."); aidOri = await GetAvIdAsync(input); Log("獲取aid結束: " + aidOri); //-p的优先级大于URL中的自带p参数,所以先清空selectedPages if (!string.IsNullOrEmpty(selectPage) && selectPage != "ALL" && selectPage != "LATEST") { selectedPages = new List <string>(); try { string tmp = selectPage; tmp = tmp.Trim().Trim(','); if (tmp.Contains("-")) { int start = int.Parse(tmp.Split('-')[0]); int end = int.Parse(tmp.Split('-')[1]); for (int i = start; i <= end; i++) { selectedPages.Add(i.ToString()); } } else { foreach (var s in tmp.Split(',')) { selectedPages.Add(s); } } } catch { LogError("解析分P參數時失敗了~"); selectedPages = null; }; } if (selectPage == "ALL") { selectedPages = null; } if (string.IsNullOrEmpty(aidOri)) { throw new Exception("輸入有誤"); } Log("獲取視頻信息..."); IFetcher fetcher = new BBDownNormalInfoFetcher(); if (aidOri.StartsWith("cheese")) { fetcher = new BBDownCheeseInfoFetcher(); } else if (aidOri.StartsWith("ep")) { fetcher = new BBDownBangumiInfoFetcher(); } var vInfo = fetcher.Fetch(aidOri); string title = vInfo.Title; string desc = vInfo.Desc; string pic = vInfo.Pic; string pubTime = vInfo.PubTime; LogColor("視頻標題: " + title); LogDebug("發佈時間: " + pubTime); List <Page> pagesInfo = vInfo.PagesInfo; List <Subtitle> subtitleInfo = new List <Subtitle>(); bool more = false; bool bangumi = vInfo.IsBangumi; bool cheese = vInfo.IsCheese; //打印分P信息 foreach (Page p in pagesInfo) { if (!showAll && more && p.index != pagesInfo.Count) { continue; } if (!showAll && !more && p.index > 5) { Log("......"); more = true; } else { LogDebug($"P{p.index}: [{p.cid}] [{p.title}] [{FormatTime(p.dur)}]"); } } if (selectPage == "LATEST") { selectedPages = new List <string>(); selectedPages.Add(pagesInfo.Count.ToString()); LogDebug(pagesInfo.Count.ToString()); } //如果用户没有选择分P,根据epid来确定某一集 if (selectedPages == null && selectPage != "ALL" && !string.IsNullOrEmpty(vInfo.Index) && selectPage != "LATEST") { selectedPages = new List <string> { vInfo.Index }; Log("程序已自動選擇你輸入的集數,如果要下載其他集數請自行指定分P(可使用參數-p ALL代表全部 -p LATEST代表最新一集)"); } Log($"共計 {pagesInfo.Count} 個分P, 已選擇:" + (selectedPages == null ? "ALL" : string.Join(",", selectedPages))); //过滤不需要的分P if (selectedPages != null) { pagesInfo = pagesInfo.Where(p => selectedPages.Contains(p.index.ToString())).ToList(); } foreach (Page p in pagesInfo) { Log($"開始解析P{p.index}..."); if (!infoMode) { if (!Directory.Exists($"temp/{p.aid}")) { Directory.CreateDirectory($"temp/{p.aid}"); } if (!File.Exists($"temp/{p.aid}/{p.aid}.jpg")) { Log("下載封面..."); LogDebug("下載:{0}", pic); new WebClient().DownloadFile(pic, $"temp/{p.aid}/{p.aid}.jpg"); } string[] files = System.IO.Directory.GetFiles(Directory.GetCurrentDirectory(), $"temp/{p.aid}/{p.aid}.{p.cid}.*.srt"); if (files.Length > 0) { Log("字幕已經獲取..."); for (int i = 0; i < files.Length; i++) { LogDebug(files[i]); } } else { LogDebug("獲取字幕..."); subtitleInfo = BBDownSubUtil.GetSubtitles(p.aid, p.cid, p.epid, intlApi); foreach (Subtitle s in subtitleInfo) { Log($"下載字幕 {s.lan} => {BBDownSubUtil.SubDescDic[s.lan]}..."); LogDebug("下載:{0}", s.url); BBDownSubUtil.SaveSubtitle(s.url, s.path); } } } string webJsonStr = ""; List <Video> videoTracks = new List <Video>(); List <Audio> audioTracks = new List <Audio>(); List <string> clips = new List <string>(); List <string> dfns = new List <string>(); string indexStr = p.index.ToString("0".PadRight(pagesInfo.OrderByDescending(_p => _p.index).First().index.ToString().Length, '0')); string videoPath = $"temp/{p.aid}/{p.aid}.P{indexStr}.{p.cid}.mp4"; string audioPath = $"temp/{p.aid}/{p.aid}.P{indexStr}.{p.cid}.m4a"; //处理文件夹以.结尾导致的异常情况 if (title.EndsWith(".")) { title += "_fix"; } var converter = new OpenChineseConverter(); title = converter.ToTaiwanFromSimplified(title); Log(title); string ep = p.index.ToString("D2"); //讀取JSON存放 string jsonpath = Directory.GetCurrentDirectory(); string jsonfile = File.ReadAllText($"{jsonpath}/config.json"); dynamic json = JValue.Parse(jsonfile); string dirname = json.dir; title = Regex.Replace(title, @"[<>:""/\\|?*]", "-"); //调用解析 (webJsonStr, videoTracks, audioTracks, clips, dfns) = ExtractTracks(hevc, aidOri, p.aid, p.cid, p.epid, tvApi, intlApi); //File.WriteAllText($"debug.json", JObject.Parse(webJsonStr).ToString()); JObject respJson = JObject.Parse(webJsonStr); string outPath = dirname + (pagesInfo.Count > 1 ? $"/{json.prefix}{title}[{ep}][0000P]{json.suffix}" + $".mp4" : $"/{json.prefix}{title}[{ep}][0000P]{json.suffix}.mp4"); //此处代码简直灾难,后续优化吧 if ((videoTracks.Count != 0 || audioTracks.Count != 0) && clips.Count == 0) //dash { if (webJsonStr.Contains("\"video\":[") && videoTracks.Count == 0) { LogError("沒有找到符合要求的視頻流"); if (!audioOnly) { continue; } } if (webJsonStr.Contains("\"audio\":[") && audioTracks.Count == 0) { LogError("沒有找到符合要求的音頻流"); if (!videoOnly) { continue; } } //降序 videoTracks.Sort(Compare); audioTracks.Sort(Compare); if (audioOnly) { videoTracks.Clear(); } if (videoOnly) { audioTracks.Clear(); } int vIndex = 0; int aIndex = 0; if (!hideStreams) { //展示所有的音视频流信息 if (videoTracks.Count > 0) { Log($"共計{videoTracks.Count}條視頻流."); int index = 0; foreach (var v in videoTracks) { int pDur = p.dur == 0 ? v.dur : p.dur; LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [{v.bandwith} kbps] [~{FormatFileSize(pDur * v.bandwith * 1024 / 8)}]".Replace("[] ", ""), false); if (infoMode) { Console.WriteLine(v.baseUrl); } } } if (audioTracks.Count > 0) { Log($"共計{audioTracks.Count}條音頻流."); int index = 0; foreach (var a in audioTracks) { int pDur = p.dur == 0 ? a.dur : p.dur; LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false); if (infoMode) { Console.WriteLine(a.baseUrl); } } } } if (infoMode) { continue; } if (interactMode && !hideStreams) { if (videoTracks.Count > 0) { Log("請選擇一條視頻流(輸入序號): ", false); Console.ForegroundColor = ConsoleColor.Cyan; vIndex = Convert.ToInt32(Console.ReadLine()); if (vIndex > videoTracks.Count || vIndex < 0) { vIndex = 0; } Console.ResetColor(); } if (audioTracks.Count > 0) { Log("請選擇一條音頻流(輸入序號): ", false); Console.ForegroundColor = ConsoleColor.Cyan; aIndex = Convert.ToInt32(Console.ReadLine()); if (aIndex > audioTracks.Count || aIndex < 0) { aIndex = 0; } Console.ResetColor(); } } Log($"已選擇的流:"); if (videoTracks.Count > 0) { LogColor($"[視頻] [{videoTracks[vIndex].dfn}] [{videoTracks[vIndex].res}] [{videoTracks[vIndex].codecs}] [{videoTracks[vIndex].fps}] [{videoTracks[vIndex].bandwith} kbps] [~{FormatFileSize(videoTracks[vIndex].dur * videoTracks[vIndex].bandwith * 1024 / 8)}]".Replace("[] ", ""), false); } if (audioTracks.Count > 0) { LogColor($"[音頻] [{audioTracks[aIndex].codecs}] [{audioTracks[aIndex].bandwith} kbps] [~{FormatFileSize(audioTracks[aIndex].dur * audioTracks[aIndex].bandwith * 1024 / 8)}]", false); } outPath = dirname + (pagesInfo.Count > 1 ? $"/{json.prefix}{title}[{ep}][{videoTracks[vIndex].dfn}]{json.suffix}" + $".mp4" : $"/{json.prefix}{title}[{ep}][{videoTracks[vIndex].dfn}]{json.suffix}.mp4"); if (File.Exists(outPath) && new FileInfo(outPath).Length != 0) { Log($"{outPath}已存在, 跳過下載..."); continue; } if (videoTracks.Count > 0) { if (multiThread && !videoTracks[vIndex].baseUrl.Contains("-cmcc-")) { Log($"開始多線程下載P{p.index}視頻..."); await MultiThreadDownloadFileAsync(videoTracks[vIndex].baseUrl, videoPath, useAria2c); Log("合併視頻分片..."); CombineMultipleFilesIntoSingleFile(GetFiles(Path.GetDirectoryName(videoPath), ".vclip"), videoPath); Log("清理分片..."); foreach (var file in new DirectoryInfo(Path.GetDirectoryName(videoPath)).EnumerateFiles("*.?clip")) { file.Delete(); } } else { if (multiThread && videoTracks[vIndex].baseUrl.Contains("-cmcc-")) { LogError("檢測到cmcc域名cdn, 已經禁用多線程"); } Log($"開始下載P{p.index}視頻..."); await DownloadFile(videoTracks[vIndex].baseUrl, videoPath, useAria2c); } } if (audioTracks.Count > 0) { if (multiThread && !audioTracks[aIndex].baseUrl.Contains("-cmcc-")) { Log($"開始多線程下載P{p.index}音頻..."); await MultiThreadDownloadFileAsync(audioTracks[aIndex].baseUrl, audioPath, useAria2c); Log("合併音頻分片..."); CombineMultipleFilesIntoSingleFile(GetFiles(Path.GetDirectoryName(audioPath), ".aclip"), audioPath); Log("清理分片..."); foreach (var file in new DirectoryInfo(Path.GetDirectoryName(videoPath)).EnumerateFiles("*.?clip")) { file.Delete(); } } else { if (multiThread && audioTracks[aIndex].baseUrl.Contains("-cmcc-")) { LogError("檢測到cmcc域名cdn, 已經禁用多線程"); } Log($"開始下載P{p.index}音頻..."); await DownloadFile(audioTracks[aIndex].baseUrl, audioPath, useAria2c); } } Log($"下載P{p.index}完畢"); if (videoTracks.Count == 0) { videoPath = ""; } if (audioTracks.Count == 0) { audioPath = ""; } if (skipMux) { continue; } Log("開始合併音視頻" + (subtitleInfo.Count > 0 ? "和字幕" : "") + "..."); int code = MuxAV(videoPath, audioPath, outPath, desc, title, vInfo.PagesInfo.Count > 1 ? ($"P{indexStr}.{p.title}") : "", File.Exists($"temp/{p.aid}/{p.aid}.jpg") ? $"temp/{p.aid}/{p.aid}.jpg" : "", lang, subtitleInfo, audioOnly, videoOnly, p.aid, p.cid); if (code != 0 || !File.Exists(outPath) || new FileInfo(outPath).Length == 0) { LogError("合併失敗"); continue; } Log("清理臨時文件..."); if (videoTracks.Count > 0) { File.Delete(videoPath); } if (audioTracks.Count > 0) { File.Delete(audioPath); } } else if (clips.Count > 0 && dfns.Count > 0) //flv { bool flag = false; reParse: //降序 videoTracks.Sort(Compare); if (interactMode && !flag) { int i = 0; dfns.ForEach(key => LogColor($"{i++}.{qualitys[key]}")); Log("請選擇最想要的清晰度(輸入序號): ", false); Console.ForegroundColor = ConsoleColor.Cyan; var vIndex = Convert.ToInt32(Console.ReadLine()); if (vIndex > dfns.Count || vIndex < 0) { vIndex = 0; } Console.ResetColor(); //重新解析 (webJsonStr, videoTracks, audioTracks, clips, dfns) = ExtractTracks(hevc, aidOri, p.aid, p.cid, p.epid, tvApi, intlApi, dfns[vIndex]); flag = true; videoTracks.Clear(); goto reParse; } Log($"共計{videoTracks.Count}條流(共有{clips.Count}個分段)."); int index = 0; foreach (var v in videoTracks) { LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [~{(v.size / 1024 / v.dur * 8).ToString("00")} kbps] [{FormatFileSize(v.size)}]".Replace("[] ", ""), false); if (infoMode) { clips.ForEach(delegate(string c) { Console.WriteLine(c); }); } } if (infoMode) { continue; } if (File.Exists(outPath) && new FileInfo(outPath).Length != 0) { Log($"{outPath}已存在, 跳過下載..."); continue; } var pad = string.Empty.PadRight(clips.Count.ToString().Length, '0'); for (int i = 0; i < clips.Count; i++) { var link = clips[i]; videoPath = $"temp/{p.aid}/{p.aid}.P{indexStr}.{p.cid}.{i.ToString(pad)}.mp4"; if (multiThread && !link.Contains("-cmcc-")) { if (videoTracks.Count != 0) { Log($"開始多線程下載P{p.index}視頻, 片段({(i + 1).ToString(pad)}/{clips.Count})..."); await MultiThreadDownloadFileAsync(link, videoPath, useAria2c); Log("合併視頻分片..."); CombineMultipleFilesIntoSingleFile(GetFiles(Path.GetDirectoryName(videoPath), ".vclip"), videoPath); } Log("清理分片..."); foreach (var file in new DirectoryInfo(Path.GetDirectoryName(videoPath)).EnumerateFiles("*.?clip")) { file.Delete(); } } else { if (multiThread && link.Contains("-cmcc-")) { LogError("檢測到cmcc域名cdn, 已經禁用多線程"); } if (videoTracks.Count != 0) { Log($"開始下載P{p.index}視頻, 片段({(i + 1).ToString(pad)}/{clips.Count})..."); await DownloadFile(link, videoPath, useAria2c); } } } Log($"下載P{p.index}完畢"); Log("開始合併分段..."); var files = GetFiles(Path.GetDirectoryName(videoPath), ".mp4"); videoPath = $"temp/{p.aid}/{p.aid}.P{indexStr}.{p.cid}.mp4"; MergeFLV(files, videoPath); Log("開始混流視頻" + (subtitleInfo.Count > 0 ? "和字幕" : "") + "..."); int code = MuxAV(videoPath, "", outPath, desc, title, vInfo.PagesInfo.Count > 1 ? ($"P{indexStr}.{p.title}") : "", File.Exists($"temp/{p.aid}/{p.aid}.jpg") ? $"{p.aid}/{p.aid}.jpg" : "", lang, subtitleInfo, audioOnly, videoOnly, p.aid, p.cid); if (code != 0 || !File.Exists(outPath) || new FileInfo(outPath).Length == 0) { LogError("合併失敗"); continue; } Log("清理臨時文件..."); if (videoTracks.Count != 0) { File.Delete(videoPath); } } else { if (webJsonStr.Contains("平台不可观看")) { throw new Exception("當前(WEB)平台不可觀看,請嘗試使用TV API解析。"); } else if (webJsonStr.Contains("购买后才能观看")) { throw new Exception("購買後才能觀看"); } else if (webJsonStr.Contains("大会员专享限制")) { throw new Exception("大會員專享限制"); } else if (webJsonStr.Contains("地区不可观看") || webJsonStr.Contains("地區不可觀看")) { throw new Exception("當前地區不可觀看,請嘗試使用代理解析。"); } LogError("解析此分P失敗(使用--debug查看詳細信息)"); LogDebug("{0}", webJsonStr); continue; } } Log("任務完成"); } catch (Exception e) { Console.BackgroundColor = ConsoleColor.Red; Console.ForegroundColor = ConsoleColor.White; Console.Write(e.Message); Console.ResetColor(); Console.WriteLine(); Thread.Sleep(1); } }