Files
Archive/bbdown/BBDown/Program.cs
2025-08-03 20:40:41 +02:00

898 lines
40 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using static BBDown.Core.Entity.Entity;
using static BBDown.BBDownUtil;
using static BBDown.BBDownDownloadUtil;
using static BBDown.Core.Parser;
using static BBDown.Core.Logger;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using BBDown.Core;
using BBDown.Core.Util;
using System.Text.Json.Serialization;
using System.CommandLine.Builder;
using BBDown.Core.Entity;
namespace BBDown;
partial class Program
{
private static readonly string BACKUP_HOST = "upos-sz-mirrorcoso1.bilivideo.com";
public static string SinglePageDefaultSavePath { get; set; } = "<videoTitle>";
public static string MultiPageDefaultSavePath { get; set; } = "<videoTitle>/[P<pageNumberWithZero>]<pageTitle>";
public static readonly string APP_DIR = Path.GetDirectoryName(Environment.ProcessPath)!;
private static string FormatTimeStamp(long ts, string format)
{
try
{
return ts == 0 ? "null" : DateTimeOffset.FromUnixTimeSeconds(ts).ToLocalTime().ToString(format);
}
catch (Exception ex)
{
LogError($"格式化日期出错: {ex.Message}");
return ts.ToString();
}
}
[JsonSerializable(typeof(MyOption))]
[JsonSerializable(typeof(ServeRequestOptions))]
partial class MyOptionJsonContext : JsonSerializerContext { }
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
LogWarn("Force Exit...");
try
{
Console.ResetColor();
Console.CursorVisible = true;
if (!OperatingSystem.IsWindows())
System.Diagnostics.Process.Start("stty", "echo");
}
catch { }
Environment.Exit(0);
}
public static async Task<int> Main(params string[] args)
{
Console.CancelKeyPress += Console_CancelKeyPress;
ServicePointManager.DefaultConnectionLimit = 2048;
var rootCommand = CommandLineInvoker.GetRootCommand(RunApp);
Command loginCommand = new(
"login",
"通过APP扫描二维码以登录您的WEB账号");
rootCommand.AddCommand(loginCommand);
Command loginTVCommand = new(
"logintv",
"通过APP扫描二维码以登录您的TV账号");
rootCommand.AddCommand(loginTVCommand);
var serverUrlOpt = new Option<string>(
["--listen", "-l"],
description: "服务器监听url");
Command runAsServerCommand = new(
"serve",
"以服务器模式运行")
{ serverUrlOpt };
runAsServerCommand.SetHandler(StartServer, serverUrlOpt);
rootCommand.AddCommand(runAsServerCommand);
rootCommand.Description = "BBDown是一个免费且便捷高效的哔哩哔哩下载/解析软件.";
rootCommand.TreatUnmatchedTokensAsErrors = true;
//WEB登录
loginCommand.SetHandler(BBDownLoginUtil.LoginWEB);
//TV登录
loginTVCommand.SetHandler(BBDownLoginUtil.LoginTV);
var parser = new CommandLineBuilder(rootCommand)
.UseDefaults()
.EnablePosixBundling(false)
.UseExceptionHandler((ex, context) =>
{
LogError(ex.Message);
try { Console.CursorVisible = true; } catch { }
Thread.Sleep(3000);
Environment.Exit(1);
}, 1)
.Build();
var newArgsList = new List<string>();
var commandLineResult = rootCommand.Parse(args);
//显式抛出异常
if (commandLineResult.Errors.Any())
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(commandLineResult.Errors.First().Message);
Console.ResetColor();
Console.Error.WriteLine($"请使用 BBDown --help 查看帮助");
return 1;
}
if (commandLineResult.CommandResult.Command.Name.ToLower() != Path.GetFileNameWithoutExtension(Environment.ProcessPath)!.ToLower() && Path.GetFileNameWithoutExtension(Environment.ProcessPath)!.ToLower() != "dotnet")
{
// 服务器模式需要完整的arg列表
if (commandLineResult.CommandResult.Command.Name.ToLower() == "serve")
{
return await parser.InvokeAsync(args.ToArray());
}
newArgsList.Add(commandLineResult.CommandResult.Command.Name);
return await parser.InvokeAsync(newArgsList.ToArray());
}
foreach (var item in commandLineResult.CommandResult.Children)
{
if (item is ArgumentResult a)
{
newArgsList.Add(a.Tokens[0].Value);
}
else if (item is OptionResult o)
{
newArgsList.Add("--" + o.Option.Name);
newArgsList.AddRange(o.Tokens.Select(t => t.Value));
}
}
if (newArgsList.Contains("--debug"))
{
Config.DEBUG_LOG = true;
}
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("遇到问题请首先到以下地址查阅有无相关信息:\r\nhttps://github.com/nilaoda/BBDown/issues\r\n");
Console.WriteLine();
//处理配置文件
BBDownConfigParser.HandleConfig(newArgsList, rootCommand);
return await parser.InvokeAsync(newArgsList.ToArray());
}
private static Task RunApp(MyOption myOption)
{
//检测更新
_ = CheckUpdateAsync();
return DoWorkAsync(myOption);
}
private static void StartServer(string? listenUrl)
{
var defaultListenUrl = "http://0.0.0.0:23333";
//检测更新
_ = CheckUpdateAsync();
var server = new BBDownApiServer();
server.SetUpServer();
server.Run(string.IsNullOrEmpty(listenUrl) ? defaultListenUrl : listenUrl);
}
public static (Dictionary<string, byte> encodingPriority, Dictionary<string, int> dfnPriority, string? firstEncoding,
bool downloadDanmaku, BBDownDanmakuFormat[] downloadDanmakuFormats, string input, string savePathFormat, string lang, string aidOri, int delay)
SetUpWork(MyOption myOption)
{
//处理废弃选项
HandleDeprecatedOptions(myOption);
//处理冲突选项
HandleConflictingOptions(myOption);
//寻找并设置所需的二进制文件路径
FindBinaries(myOption);
//切换工作目录
ChangeWorkingDir(myOption);
//解析优先级
var encodingPriority = ParseEncodingPriority(myOption, out var firstEncoding);
var dfnPriority = ParseDfnPriority(myOption);
//优先使用用户设置的UA
HTTPUtil.UserAgent = string.IsNullOrEmpty(myOption.UserAgent) ? HTTPUtil.UserAgent : myOption.UserAgent;
bool downloadDanmaku = myOption.DownloadDanmaku || myOption.DanmakuOnly;
BBDownDanmakuFormat[] downloadDanmakuFormats = ParseDownloadDanmakuFormats(myOption);
string input = myOption.Url;
string savePathFormat = myOption.FilePattern;
string lang = myOption.Language;
string aidOri = ""; //原始aid
int delay = Convert.ToInt32(myOption.DelayPerPage);
Config.DEBUG_LOG = myOption.Debug;
Config.HOST = myOption.Host;
Config.EPHOST = myOption.EpHost;
Config.TVHOST = myOption.TvHost;
Config.AREA = myOption.Area;
Config.COOKIE = myOption.Cookie;
Config.TOKEN = myOption.AccessToken.Replace("access_token=", "");
LogDebug("AppDirectory: {0}", APP_DIR);
LogDebug("运行参数:{0}", JsonSerializer.Serialize(myOption, MyOptionJsonContext.Default.MyOption));
return (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, aidOri, delay);
}
public static async Task<(string fetchedAid, VInfo vInfo, string apiType)> GetVideoInfoAsync(MyOption myOption, string aidOri, string input)
{
// 加载认证信息
LoadCredentials(myOption);
// 检测是否登录了账号
if (myOption is { UseIntlApi: false, UseTvApi: false } && Config.AREA == "")
{
Log("检测账号登录...");
if (!await CheckLogin(Config.COOKIE))
{
LogWarn("你尚未登录B站账号, 解析可能受到限制");
}
}
Log("获取aid...");
aidOri = await GetAvIdAsync(input);
Log($"获取aid结束: {aidOri}");
if (string.IsNullOrEmpty(aidOri))
{
throw new Exception("输入有误");
}
Log("获取视频信息...");
IFetcher fetcher = FetcherFactory.CreateFetcher(aidOri, myOption.UseIntlApi);
VInfo? vInfo = null;
// 只输入 EP/SS 时优先按番剧查找,如果找不到则尝试按课程查找
try
{
vInfo = await fetcher.FetchAsync(aidOri);
}
catch (KeyNotFoundException e)
{
if (e.Message != "Arg_KeyNotFound") throw; // 错误消息不符合预期,抛出异常
if (aidOri.StartsWith("cheese:")) throw; // 已经按课程查找过,不再重复尝试
LogWarn("未找到此 EP/SS 对应番剧信息, 正在尝试按课程查找。");
aidOri = aidOri.Replace("ep", "cheese");
Log("新的 aid: " + aidOri);
if (string.IsNullOrEmpty(aidOri))
{
throw new Exception("输入有误");
}
Log("获取视频信息...");
fetcher = FetcherFactory.CreateFetcher(aidOri, myOption.UseIntlApi);
vInfo = await fetcher.FetchAsync(aidOri);
}
string title = vInfo.Title;
long pubTime = vInfo.PubTime;
LogColor("视频标题: " + title);
if (pubTime != 0)
{
Log("发布时间: " + FormatTimeStamp(pubTime, "yyyy-MM-dd HH:mm:ss zzz"));
}
var bvid = vInfo.PagesInfo.FirstOrDefault()?.bvid;
if (!string.IsNullOrEmpty(bvid) && !myOption.UseIntlApi)
{
Log($"视频URL: https://www.bilibili.com/video/{bvid}/");
}
var mid = vInfo.PagesInfo.FirstOrDefault(p => !string.IsNullOrEmpty(p.ownerMid))?.ownerMid;
if (!string.IsNullOrEmpty(mid))
{
Log($"UP主页: https://space.bilibili.com/{mid}");
}
if (vInfo.IsSteinGate && myOption.UseTvApi)
{
Log("视频为互动视频暂时不支持tv下载修改为默认下载");
myOption.UseTvApi = false;
}
string apiType = myOption.UseTvApi ? "TV" : (myOption.UseAppApi ? "APP" : (myOption.UseIntlApi ? "INTL" : "WEB"));
//打印分P信息
List<Page> pagesInfo = vInfo.PagesInfo;
bool more = false;
foreach (Page p in pagesInfo)
{
if (!myOption.ShowAll)
{
if (more && p.index != pagesInfo.Count) continue;
if (!more && p.index > 5)
{
Log("......");
more = true;
continue;
}
}
Log($"P{p.index}: [{p.cid}] [{p.title}] [{FormatTime(p.dur)}]");
}
return (aidOri, vInfo, apiType);
}
public static async Task DownloadPagesAsync(MyOption myOption, VInfo vInfo, Dictionary<string, byte> encodingPriority, Dictionary<string, int> dfnPriority,
string? firstEncoding, bool downloadDanmaku, BBDownDanmakuFormat[] downloadDanmakuFormats, string input, string savePathFormat, string lang, string aidOri, int delay, string apiType, DownloadTask? relatedTask = null)
{
List<Page> pagesInfo = vInfo.PagesInfo;
bool bangumi = vInfo.IsBangumi;
bool cheese = vInfo.IsCheese;
//获取已选择的分P列表
List<string>? selectedPages = GetSelectedPages(myOption, vInfo, input);
Log($"共计 {pagesInfo.Count} 个分P, 已选择:" + (selectedPages == null ? "ALL" : string.Join(",", selectedPages)));
var pagesCount = pagesInfo.Count;
//过滤不需要的分P
if (selectedPages != null)
{
pagesInfo = pagesInfo.Where(p => selectedPages.Contains(p.index.ToString())).ToList();
}
// 根据p数选择存储路径
savePathFormat = string.IsNullOrEmpty(myOption.FilePattern) ? SinglePageDefaultSavePath : myOption.FilePattern;
// 1. 多P; 2. 只有1P, 但是是番剧, 尚未完结时 按照多P处理
if (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd))
{
savePathFormat = string.IsNullOrEmpty(myOption.MultiFilePattern) ? MultiPageDefaultSavePath : myOption.MultiFilePattern;
}
foreach (Page p in pagesInfo)
{
if (pagesInfo.Count > 1 && delay > 0)
{
Log($"停顿{delay}秒...");
await Task.Delay(delay * 1000);
}
Log($"开始解析P{p.index}: {p.aid}... ({pagesInfo.IndexOf(p) + 1} of {pagesInfo.Count})");
if (myOption.SaveArchivesToFile)
{
if (CheckAidFromFile(p.aid))
{
Log($"aid: {p.aid}已下载过, 跳过下载...");
continue;
}
}
await DownloadPageAsync(p, myOption, vInfo, pagesInfo, encodingPriority, dfnPriority, firstEncoding,
downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, aidOri, apiType, relatedTask);
if (myOption.SaveArchivesToFile)
{
SaveAidToFile(p.aid);
}
}
Log("任务完成");
}
private static async Task DownloadPageAsync(Page p, MyOption myOption, VInfo vInfo, List<Page> selectedPagesInfo, Dictionary<string, byte> encodingPriority, Dictionary<string, int> dfnPriority,
string? firstEncoding, bool downloadDanmaku, BBDownDanmakuFormat[] downloadDanmakuFormats, string input, string savePathFormat, string lang, string aidOri, string apiType, DownloadTask? relatedTask = null)
{
string desc = string.IsNullOrEmpty(p.desc) ? vInfo.Desc : p.desc;
bool bangumi = vInfo.IsBangumi;
var pagesCount = selectedPagesInfo.Count;
List<Subtitle> subtitleInfo = [];
string title = vInfo.Title;
string pic = vInfo.Pic;
long pubTime = vInfo.PubTime;
bool selected = false; //用户是否已经手动选择过了轨道
int retryCount = 0;
downloadPage:
try
{
LogDebug("尝试获取章节信息...");
p.points = await FetchPointsAsync(p.cid, p.aid);
string videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4";
string audioPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.m4a";
var coverPath = $"{p.aid}/{p.aid}.jpg";
//处理文件夹以.结尾导致的异常情况
if (title.EndsWith('.')) title += "_fix";
//处理文件夹以.开头导致的异常情况
if (title.StartsWith('.')) title = "_" + title;
//处理封面&&字幕
if (!myOption.OnlyShowInfo)
{
if (!Directory.Exists(p.aid))
{
Directory.CreateDirectory(p.aid);
}
if (!myOption.SkipCover && !myOption.SubOnly && !File.Exists(coverPath) && !myOption.DanmakuOnly && !myOption.CoverOnly)
{
await DownloadFileAsync(pic == "" ? p.cover! : pic, coverPath, new DownloadConfig());
}
if (!myOption.SkipSubtitle && !myOption.DanmakuOnly && !myOption.CoverOnly)
{
LogDebug("获取字幕...");
subtitleInfo = await SubUtil.GetSubtitlesAsync(p.aid, p.cid, p.epid, p.index, myOption.UseIntlApi);
if (myOption.SkipAi && subtitleInfo.Any())
{
Log($"跳过下载AI字幕");
subtitleInfo = subtitleInfo.Where(s => !s.lan.StartsWith("ai-")).ToList();
}
foreach (Subtitle s in subtitleInfo)
{
Log($"下载字幕 {s.lan} => {SubUtil.GetSubtitleCode(s.lan).Item2}...");
LogDebug("下载:{0}", s.url);
await SubUtil.SaveSubtitleAsync(s.url, s.path);
if (myOption.SubOnly && File.Exists(s.path) && File.ReadAllText(s.path) != "")
{
var _outSubPath = FormatSavePath(savePathFormat, title, null, null, p, pagesCount, apiType, pubTime);
if (_outSubPath.Contains('/'))
{
if (!Directory.Exists(_outSubPath.Split('/').First()))
Directory.CreateDirectory(_outSubPath.Split('/').First());
}
_outSubPath = Path.ChangeExtension(_outSubPath, $".{s.lan}.srt");
File.Move(s.path, _outSubPath, true);
}
}
}
if (myOption.SubOnly)
{
if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true);
return;
}
}
//调用解析
ParsedResult parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding);
List<AudioMaterial> audioMaterial = [];
if (!p.points.Any())
{
p.points = parsedResult.ExtraPoints;
}
if (Config.DEBUG_LOG)
{
File.WriteAllText($"debug_{DateTime.Now:yyyyMMddHHmmssfff}.json", parsedResult.WebJsonString);
}
var savePath = "";
var downloadConfig = new DownloadConfig()
{
UseAria2c = myOption.UseAria2c,
Aria2cArgs = myOption.Aria2cArgs,
ForceHttp = myOption.ForceHttp,
MultiThread = myOption.MultiThread,
RelatedTask = relatedTask,
};
//此处代码简直灾难, 后续优化吧
if ((parsedResult.VideoTracks.Any() || parsedResult.AudioTracks.Any()) && !parsedResult.Clips.Any()) //dash
{
if (parsedResult.VideoTracks.Count == 0)
{
LogWarn("没有找到符合要求的视频流");
if (myOption.VideoOnly) return;
}
if (parsedResult.AudioTracks.Count == 0)
{
LogWarn("没有找到符合要求的音频流");
if (myOption.AudioOnly) return;
}
if (myOption.AudioOnly)
{
parsedResult.VideoTracks.Clear();
}
if (myOption.VideoOnly)
{
parsedResult.AudioTracks.Clear();
parsedResult.BackgroundAudioTracks.Clear();
parsedResult.RoleAudioList.Clear();
}
//排序
parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending);
parsedResult.AudioTracks = SortTracks(parsedResult.AudioTracks, encodingPriority, myOption.AudioAscending);
parsedResult.BackgroundAudioTracks = SortTracks(parsedResult.BackgroundAudioTracks, encodingPriority, myOption.AudioAscending);
foreach (var role in parsedResult.RoleAudioList)
{
role.audio = SortTracks(role.audio, encodingPriority, myOption.AudioAscending);
}
//打印轨道信息
if (!myOption.HideStreams)
{
PrintAllTracksInfo(parsedResult, p.dur, myOption.OnlyShowInfo);
}
//仅展示 跳过下载
if (myOption.OnlyShowInfo)
{
return;
}
int vIndex = 0; //用户手动选择的视频序号
int aIndex = 0; //用户手动选择的音频序号
//选择轨道
if (myOption.Interactive && !selected)
{
SelectTrackManually(parsedResult, ref vIndex, ref aIndex);
selected = true;
}
Video? selectedVideo = parsedResult.VideoTracks.ElementAtOrDefault(vIndex);
Audio? selectedAudio = parsedResult.AudioTracks.ElementAtOrDefault(aIndex);
Audio? selectedBackgroundAudio = parsedResult.BackgroundAudioTracks.ElementAtOrDefault(aIndex);
LogDebug("Format Before: " + savePathFormat);
savePath = FormatSavePath(savePathFormat, title, selectedVideo, selectedAudio, p, pagesCount, apiType, pubTime);
LogDebug("Format After: " + savePath);
if (downloadDanmaku)
{
var danmakuXmlPath = Path.ChangeExtension(savePath, ".xml");
var danmakuAssPath = Path.ChangeExtension(savePath, ".ass");
Log("正在下载弹幕Xml文件");
var danmakuUrl = $"https://comment.bilibili.com/{p.cid}.xml";
await DownloadFileAsync(danmakuUrl, danmakuXmlPath, downloadConfig);
var danmakus = DanmakuUtil.ParseXml(danmakuXmlPath);
if (danmakus == null)
{
Log("弹幕Xml解析失败, 删除Xml...");
File.Delete(danmakuXmlPath);
}
else if (danmakus.Length == 0)
{
Log("当前视频没有弹幕, 删除Xml...");
File.Delete(danmakuXmlPath);
}
else if (downloadDanmakuFormats.Contains(BBDownDanmakuFormat.Ass))
{
Log("正在保存弹幕Ass文件...");
await DanmakuUtil.SaveAsAssAsync(danmakus, danmakuAssPath);
}
// delete xml if possible
if (!downloadDanmakuFormats.Contains(BBDownDanmakuFormat.Xml) && File.Exists(danmakuXmlPath))
{
File.Delete(danmakuXmlPath);
}
if (myOption.DanmakuOnly)
{
if (Directory.Exists(p.aid))
{
Directory.Delete(p.aid);
}
return;
}
}
if (myOption.CoverOnly)
{
var coverUrl = pic == "" ? p.cover! : pic;
var newCoverPath = Path.ChangeExtension(savePath, Path.GetExtension(coverUrl));
await DownloadFileAsync(coverUrl, newCoverPath, downloadConfig);
if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true);
relatedTask?.SavePaths.Add(newCoverPath);
}
Log($"已选择的流:");
PrintSelectedTrackInfo(selectedVideo, selectedAudio, p.dur);
//用户开启了强制替换
if (myOption.ForceReplaceHost && string.IsNullOrEmpty(myOption.UposHost))
{
myOption.UposHost = BACKUP_HOST;
}
//处理PCDN
HandlePcdn(myOption, selectedVideo, selectedAudio);
if (!myOption.OnlyShowInfo && File.Exists(savePath) && new FileInfo(savePath).Length != 0)
{
Log($"{savePath}已存在, 跳过下载...");
relatedTask?.SavePaths.Add(savePath);
File.Delete(coverPath);
if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0)
{
Directory.Delete(p.aid, true);
}
return;
}
if (selectedVideo != null)
{
//杜比视界, 若ffmpeg版本小于5.0, 使用mp4box封装
if (selectedVideo.dfn == Config.qualitys["126"] && !myOption.UseMP4box && !CheckFFmpegDOVI())
{
LogWarn($"检测到杜比视界清晰度且您的ffmpeg版本小于5.0,将使用mp4box混流...");
myOption.UseMP4box = true;
}
Log($"开始下载P{p.index}视频...");
await DownloadTrackAsync(selectedVideo.baseUrl, videoPath, downloadConfig, video: true);
}
if (selectedAudio != null)
{
Log($"开始下载P{p.index}音频...");
await DownloadTrackAsync(selectedAudio.baseUrl, audioPath, downloadConfig, video: false);
}
if (selectedBackgroundAudio != null)
{
var backgroundPath = $"{p.aid}/{p.aid}.{p.cid}.P{p.index}.back_ground.m4a";
Log($"开始下载P{p.index}背景配音...");
await DownloadTrackAsync(selectedBackgroundAudio.baseUrl, backgroundPath, downloadConfig, video: false);
audioMaterial.Add(new AudioMaterial("背景音频", "", backgroundPath));
}
if (parsedResult.RoleAudioList.Any())
{
foreach (var role in parsedResult.RoleAudioList)
{
Log($"开始下载P{p.index}配音[{role.title}]...");
await DownloadTrackAsync(role.audio[aIndex].baseUrl, role.path, downloadConfig, video: false);
audioMaterial.Add(new AudioMaterial(role));
}
}
Log($"下载P{p.index}完毕");
if (!parsedResult.VideoTracks.Any()) videoPath = "";
if (!parsedResult.AudioTracks.Any()) audioPath = "";
if (myOption.SkipMux) return;
Log($"开始合并音视频{(subtitleInfo.Any() ? "" : "")}...");
if (myOption.AudioOnly)
savePath = savePath[..^4] + ".m4a";
var isHevc = selectedVideo?.codecs == "HEVC";
int code = BBDownMuxer.MuxAV(myOption.UseMP4box, p.bvid, videoPath, audioPath, audioMaterial, savePath,
desc,
title,
p.ownerName ?? "",
(pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "",
File.Exists(coverPath) ? coverPath : "",
lang,
subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux, isHevc);
if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0)
{
LogError("合并失败"); return;
}
Log("清理临时文件...");
Thread.Sleep(200);
if (parsedResult.VideoTracks.Any()) File.Delete(videoPath);
if (parsedResult.AudioTracks.Any()) File.Delete(audioPath);
if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters"));
foreach (var s in subtitleInfo) File.Delete(s.path);
foreach (var a in audioMaterial) File.Delete(a.path);
if (selectedPagesInfo.Count == 1 || p.index == selectedPagesInfo.Last().index || p.aid != selectedPagesInfo.Last().aid)
File.Delete(coverPath);
if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true);
}
else if (parsedResult.Clips.Any() && parsedResult.Dfns.Any()) //flv
{
bool flag = false;
var clips = parsedResult.Clips;
var dfns = parsedResult.Dfns;
reParse:
//排序
parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending);
int vIndex = 0;
if (myOption.Interactive && !flag && !selected)
{
int i = 0;
dfns.ForEach(key => LogColor($"{i++}.{Config.qualitys[key]}"));
Log("请选择最想要的清晰度(输入序号): ", false);
Console.ForegroundColor = ConsoleColor.Cyan;
vIndex = Convert.ToInt32(Console.ReadLine());
if (vIndex > dfns.Count || vIndex < 0) vIndex = 0;
Console.ResetColor();
//重新解析
parsedResult.VideoTracks.Clear();
parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding, dfns[vIndex]);
if (!p.points.Any()) p.points = parsedResult.ExtraPoints;
flag = true;
selected = true;
goto reParse;
}
Log($"共计{parsedResult.VideoTracks.Count}条流(共有{clips.Count}个分段).");
int index = 0;
foreach (var v in parsedResult.VideoTracks)
{
LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [~{v.size / 1024 / v.dur * 8:00} kbps] [{FormatFileSize(v.size)}]".Replace("[] ", ""), false);
if (myOption.OnlyShowInfo)
{
clips.ForEach(Console.WriteLine);
}
}
if (myOption.OnlyShowInfo) return;
savePath = FormatSavePath(savePathFormat, title, parsedResult.VideoTracks.ElementAtOrDefault(vIndex), null, p, pagesCount, apiType, pubTime);
if (File.Exists(savePath) && new FileInfo(savePath).Length != 0)
{
Log($"{savePath}已存在, 跳过下载...");
relatedTask?.SavePaths.Add(savePath);
if (selectedPagesInfo.Count == 1 && Directory.Exists(p.aid))
{
Directory.Delete(p.aid, true);
}
return;
}
var pad = string.Empty.PadRight(clips.Count.ToString().Length, '0');
for (int i = 0; i < clips.Count; i++)
{
var link = clips[i];
videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.{i.ToString(pad)}.mp4";
Log($"开始下载P{p.index}视频, 片段({(i + 1).ToString(pad)}/{clips.Count})...");
await DownloadTrackAsync(link, videoPath, downloadConfig, video: true);
}
Log($"下载P{p.index}完毕");
Log("开始合并分段...");
var files = GetFiles(Path.GetDirectoryName(videoPath)!, ".mp4");
videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4";
BBDownMuxer.MergeFLV(files, videoPath);
if (myOption.SkipMux) return;
Log($"开始混流视频{(subtitleInfo.Any() ? "" : "")}...");
if (myOption.AudioOnly)
savePath = savePath[..^4] + ".m4a";
int code = BBDownMuxer.MuxAV(false, p.bvid, videoPath, "", audioMaterial, savePath,
desc,
title,
p.ownerName ?? "",
(pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "",
File.Exists(coverPath) ? coverPath : "",
lang,
subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux);
if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0)
{
LogError("合并失败"); return;
}
Log("清理临时文件...");
Thread.Sleep(200);
if (parsedResult.VideoTracks.Count != 0) File.Delete(videoPath);
foreach (var s in subtitleInfo) File.Delete(s.path);
foreach (var a in audioMaterial) File.Delete(a.path);
if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters"));
if (selectedPagesInfo.Count == 1 || p.index == selectedPagesInfo.Last().index || p.aid != selectedPagesInfo.Last().aid)
File.Delete(coverPath);
if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true);
}
else
{
LogError("解析此分P失败(建议--debug查看详细信息)");
if (parsedResult.WebJsonString.Length < 100)
{
LogError(parsedResult.WebJsonString);
}
LogDebug("{0}", parsedResult.WebJsonString);
}
if (!string.IsNullOrWhiteSpace(savePath)) {
relatedTask?.SavePaths.Add(savePath);
}
}
catch (Exception ex)
{
if (++retryCount > 2) throw;
LogError(ex.Message);
LogWarn("下载出现异常, 3秒后将进行自动重试...");
await Task.Delay(3000);
goto downloadPage;
}
}
private static async Task DoWorkAsync(MyOption myOption)
{
try
{
var (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats,
input, savePathFormat, lang, aidOri, delay) = SetUpWork(myOption);
var (fetchedAid, vInfo, apiType) = await GetVideoInfoAsync(myOption, aidOri, input);
await DownloadPagesAsync(myOption, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats,
input, savePathFormat, lang, fetchedAid, delay, apiType);
}
catch (Exception e)
{
Console.BackgroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.White;
var msg = Config.DEBUG_LOG ? e.ToString() : e.Message;
Console.Write($"{msg}{Environment.NewLine}请尝试升级到最新版本后重试!");
Console.ResetColor();
Console.WriteLine();
Thread.Sleep(1);
Environment.Exit(1);
}
}
private static List<Video> SortTracks(List<Video> videoTracks, Dictionary<string, int> dfnPriority, Dictionary<string, byte> encodingPriority, bool videoAscending)
{
//用户同时输入了自定义分辨率优先级和自定义编码优先级, 则根据输入顺序依次进行排序
return dfnPriority.Any() && encodingPriority.Any() && Environment.CommandLine.IndexOf("--encoding-priority", StringComparison.Ordinal) < Environment.CommandLine.IndexOf("--dfn-priority")
? videoTracks
.OrderBy(v => encodingPriority.GetValueOrDefault(v.codecs, (byte)100))
.ThenBy(v => dfnPriority.GetValueOrDefault(v.dfn, 100))
.ThenByDescending(v => Convert.ToInt32(v.id))
.ThenBy(v => videoAscending ? v.bandwith : -v.bandwith)
.ToList()
: videoTracks
.OrderBy(v => dfnPriority.GetValueOrDefault(v.dfn, 100))
.ThenBy(v => encodingPriority.GetValueOrDefault(v.codecs, (byte)100))
.ThenByDescending(v => Convert.ToInt32(v.id))
.ThenBy(v => videoAscending ? v.bandwith : -v.bandwith)
.ToList();
}
private static List<Audio> SortTracks(List<Audio> audioTracks, Dictionary<string, byte> encodingPriority, bool audioAscending)
{
return audioTracks
.OrderBy(a => encodingPriority.GetValueOrDefault(a.shortCodecs, (byte)100))
.ThenBy(a => audioAscending ? a.bandwith : -a.bandwith)
.ToList();
}
private static string FormatSavePath(string savePathFormat, string title, Video? videoTrack, Audio? audioTrack, Page p, int pagesCount, string apiType, long pubTime)
{
var result = savePathFormat.Replace('\\', '/');
var regex = InfoRegex();
foreach (Match m in regex.Matches(result).Cast<Match>())
{
var key = m.Groups[1].Value;
//解析自定义日期格式
var defaultDateFormat = "yyyy-MM-dd_HH-mm-ss";
string[] prefixes = ["publishDate:", "videoDate:"];
foreach (var prefix in prefixes)
{
if (key.StartsWith(prefix))
{
defaultDateFormat = key[(key.IndexOf(':') + 1)..];
key = prefix.Replace(":", "");
break;
}
}
var v = key switch
{
"videoTitle" => GetValidFileName(title, filterSlash: true).Trim().TrimEnd('.').Trim(),
"pageNumber" => p.index.ToString(),
"pageNumberWithZero" => p.index.ToString().PadLeft(pagesCount.ToString().Length, '0'),
"pageTitle" => GetValidFileName(p.title, filterSlash: true).Trim().TrimEnd('.').Trim(),
"bvid" => p.bvid,
"aid" => p.aid,
"cid" => p.cid,
"ownerName" => p.ownerName == null ? "" : GetValidFileName(p.ownerName, filterSlash: true).Trim().TrimEnd('.').Trim(),
"ownerMid" => p.ownerMid ?? "",
"dfn" => videoTrack == null ? "" : videoTrack.dfn,
"res" => videoTrack == null ? "" : videoTrack.res,
"fps" => videoTrack == null ? "" : videoTrack.fps,
"videoCodecs" => videoTrack == null ? "" : videoTrack.codecs,
"videoBandwidth" => videoTrack == null ? "" : videoTrack.bandwith.ToString(),
"audioCodecs" => audioTrack == null ? "" : audioTrack.codecs,
"audioBandwidth" => audioTrack == null ? "" : audioTrack.bandwith.ToString(),
"publishDate" => FormatTimeStamp(pubTime, defaultDateFormat),
"videoDate" => FormatTimeStamp(p.pubTime, defaultDateFormat),
"apiType" => apiType,
_ => $"<{key}>"
};
result = result.Replace(m.Value, v);
}
if (!result.EndsWith(".mp4")) { result += ".mp4"; }
return result;
}
[GeneratedRegex("<([\\w:\\-.]+?)>")]
private static partial Regex InfoRegex();
}