Update On Sun Nov 10 19:32:11 CET 2024

This commit is contained in:
github-action[bot]
2024-11-10 19:32:12 +01:00
parent 5a387216ee
commit 2b3bd5e54a
166 changed files with 8101 additions and 7329 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,26 @@
namespace BBDown.Core
namespace BBDown.Core;
public static class Config
{
public class Config
{
//For WEB
public static string COOKIE { get; set; } = "";
//For APP/TV
public static string TOKEN { get; set; } = "";
//日志级别
public static bool DEBUG_LOG { get; set; } = false;
//BiliPlus Host
public static string HOST { get; set; } = "api.bilibili.com";
//BiliPlus EP Host
public static string EPHOST { get; set; } = "api.bilibili.com";
//BiliPlus Area
public static string AREA { get; set; } = "";
//For WEB
public static string COOKIE { get; set; } = "";
//For APP/TV
public static string TOKEN { get; set; } = "";
//日志级别
public static bool DEBUG_LOG { get; set; } = false;
//BiliPlus Host
public static string HOST { get; set; } = "api.bilibili.com";
//BiliPlus EP Host
public static string EPHOST { get; set; } = "api.bilibili.com";
//BiliPlus Area
public static string AREA { get; set; } = "";
public static string WBI { get; set; } = "";
public static string WBI { get; set; } = "";
public static readonly Dictionary<string, string> qualitys = new() {
{"127","8K 超高清" }, {"126","杜比视界" }, {"125","HDR 真彩" }, {"120","4K 超清" }, {"116","1080P 高帧率" },
{"112","1080P 高码率" }, {"100","智能修复" }, {"80","1080P 高清" }, {"74","720P 高帧率" },
{"64","720P 高清" }, {"48","720P 高清" }, {"32","480P 清晰" }, {"16","360P 流畅" },
{"5","144P 流畅" }, {"6","240P 流畅" }
};
}
}
public static readonly Dictionary<string, string> qualitys = new() {
{"127","8K 超高清" }, {"126","杜比视界" }, {"125","HDR 真彩" }, {"120","4K 超清" }, {"116","1080P 高帧率" },
{"112","1080P 高码率" }, {"100","智能修复" }, {"80","1080P 高清" }, {"74","720P 高帧率" },
{"64","720P 高清" }, {"48","720P 高清" }, {"32","480P 清晰" }, {"16","360P 流畅" },
{"5","144P 流畅" }, {"6","240P 流畅" }
};
}

View File

@@ -2,241 +2,240 @@
using System.Text;
using System.Xml;
namespace BBDown.Core
namespace BBDown.Core;
public static class DanmakuUtil
{
public class DanmakuUtil
private const int MONITOR_WIDTH = 1920; //渲染字幕时的渲染范围的高度
private const int MONITOR_HEIGHT = 1080; //渲染字幕时的渲染范围的高度
private const int FONT_SIZE = 40; //字体大小
private const double MOVE_SPEND_TIME = 8.00; //单条条滚动弹幕存在时间(控制速度)
private const double TOP_SPEND_TIME = 4.00; //单条顶部或底部弹幕存在时间
private const int PROTECT_LENGTH = 50; //滚动弹幕屏占百分比
public static readonly DanmakuComparer comparer = new();
/*public static async Task DownloadAsync(Page p, string xmlPath, bool aria2c, string aria2cProxy)
{
private const int MONITOR_WIDTH = 1920; //渲染字幕时的渲染范围的高度
private const int MONITOR_HEIGHT = 1080; //渲染字幕时的渲染范围的高度
private const int FONT_SIZE = 40; //字体大小
private const double MOVE_SPEND_TIME = 8.00; //单条条滚动弹幕存在时间(控制速度)
private const double TOP_SPEND_TIME = 4.00; //单条顶部或底部弹幕存在时间
private const int PROTECT_LENGTH = 50; //滚动弹幕屏占百分比
public static readonly DanmakuComparer comparer = new();
string danmakuUrl = "https://comment.bilibili.com/" + p.cid + ".xml";
await DownloadFile(danmakuUrl, xmlPath, aria2c, aria2cProxy);
}*/
/*public static async Task DownloadAsync(Page p, string xmlPath, bool aria2c, string aria2cProxy)
public static DanmakuItem[]? ParseXml(string xmlPath)
{
// 解析xml文件
XmlDocument xmlFile = new();
XmlReaderSettings settings = new()
{
string danmakuUrl = "https://comment.bilibili.com/" + p.cid + ".xml";
await DownloadFile(danmakuUrl, xmlPath, aria2c, aria2cProxy);
}*/
public static DanmakuItem[]? ParseXml(string xmlPath)
IgnoreComments = true//忽略文档里面的注释
};
var danmakus = new List<DanmakuItem>();
using (var reader = XmlReader.Create(xmlPath, settings))
{
// 解析xml文件
XmlDocument xmlFile = new();
XmlReaderSettings settings = new()
try
{
IgnoreComments = true//忽略文档里面的注释
};
var danmakus = new List<DanmakuItem>();
using (var reader = XmlReader.Create(xmlPath, settings))
{
try
{
xmlFile.Load(reader);
}
catch (Exception ex)
{
LogDebug("解析字幕xml时出现异常: {0}", ex.ToString());
return null;
}
xmlFile.Load(reader);
}
XmlNode? rootNode = xmlFile.SelectSingleNode("i");
if (rootNode != null)
catch (Exception ex)
{
XmlElement rootElement = (XmlElement)rootNode;
XmlNodeList? dNodeList = rootElement.SelectNodes("d");
if (dNodeList != null)
LogDebug("解析字幕xml时出现异常: {0}", ex.ToString());
return null;
}
}
XmlNode? rootNode = xmlFile.SelectSingleNode("i");
if (rootNode != null)
{
XmlElement rootElement = (XmlElement)rootNode;
XmlNodeList? dNodeList = rootElement.SelectNodes("d");
if (dNodeList != null)
{
foreach (XmlNode node in dNodeList)
{
foreach (XmlNode node in dNodeList)
XmlElement dElement = (XmlElement)node;
string attr = dElement.GetAttribute("p").ToString();
if (attr != null)
{
XmlElement dElement = (XmlElement)node;
string attr = dElement.GetAttribute("p").ToString();
if (attr != null)
string[] vs = attr.Split(',');
if (vs.Length >= 8)
{
string[] vs = attr.Split(',');
if (vs.Length >= 8)
{
DanmakuItem danmaku = new(vs, dElement.InnerText);
danmakus.Add(danmaku);
}
DanmakuItem danmaku = new(vs, dElement.InnerText);
danmakus.Add(danmaku);
}
}
}
}
return danmakus.ToArray();
}
/// <summary>
/// 保存为ASS字幕文件
/// </summary>
/// <param name="danmakus">弹幕</param>
/// <param name="outputPath">保存路径</param>
/// <returns></returns>
public static async Task SaveAsAssAsync(DanmakuItem[] danmakus, string outputPath)
{
var sb = new StringBuilder();
// ASS字幕文件头
sb.AppendLine("[Script Info]");
sb.AppendLine("Script Updated By: BBDown(https://github.com/nilaoda/BBDown)");
sb.AppendLine("ScriptType: v4.00+");
sb.AppendLine($"PlayResX: {MONITOR_WIDTH}");
sb.AppendLine($"PlayResY: {MONITOR_HEIGHT}");
sb.AppendLine($"Aspect Ratio: {MONITOR_WIDTH}:{MONITOR_HEIGHT}");
sb.AppendLine("Collisions: Normal");
sb.AppendLine("WrapStyle: 2");
sb.AppendLine("ScaledBorderAndShadow: yes");
sb.AppendLine("YCbCr Matrix: TV.601");
sb.AppendLine("[V4+ Styles]");
sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
sb.AppendLine($"Style: BBDOWN_Style, 黑体, {FONT_SIZE}, &H00FFFFFF, &H00FFFFFF, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 2, 0, 7, 0, 0, 0, 0");
sb.AppendLine("[Events]");
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
PositionController controller = new(); // 弹幕位置控制器
Array.Sort(danmakus, comparer);
foreach (DanmakuItem danmaku in danmakus)
{
int height = controller.UpdatePosition(danmaku.DanmakuMode, danmaku.Second, danmaku.Content.Length);
if (height == -1) continue;
string effect = "";
effect += danmaku.DanmakuMode switch
{
3 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {MONITOR_HEIGHT - FONT_SIZE - height})",
2 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {height})",
_ => $"\\move({MONITOR_WIDTH}, {height}, {-danmaku.Content.Length * FONT_SIZE}, {height})",
};
if (danmaku.Color != "FFFFFF")
{
effect += $"\\c&{danmaku.Color}&";
}
sb.AppendLine($"Dialogue: 2,{danmaku.StartTime},{danmaku.EndTime},BBDOWN_Style,,0000,0000,0000,,{{{effect}}}{danmaku.Content}");
}
await File.WriteAllTextAsync(outputPath, sb.ToString(), Encoding.UTF8);
}
protected class PositionController
{
readonly int maxLine = MONITOR_HEIGHT * PROTECT_LENGTH / FONT_SIZE / 100; //总行数
// 三个位置的弹幕队列,记录弹幕结束时间
readonly List<double> moveQueue = new();
readonly List<double> topQueue = new();
readonly List<double> bottomQueue = new();
public PositionController()
{
for (int i = 0; i < maxLine; i++)
{
moveQueue.Add(0.00);
topQueue.Add(0.00);
bottomQueue.Add(0.00);
}
}
public int UpdatePosition(int type, double time, int length)
{
// 获取可用位置
List<double> vs;
double displayTime = TOP_SPEND_TIME;
if (type == POS_BOTTOM)
{
vs = bottomQueue;
}
else if (type == POS_TOP)
{
vs = topQueue;
}
else
{
vs = moveQueue;
displayTime = MOVE_SPEND_TIME * (length + 5) * FONT_SIZE / (MONITOR_WIDTH + (length * MOVE_SPEND_TIME));
}
for (int i = 0; i < maxLine; i++)
{
if (time >= vs[i])
{ // 此条弹幕已结束,更新该位置信息
vs[i] = time + displayTime;
return i * FONT_SIZE;
}
}
return -1;
}
}
public class DanmakuItem
{
public DanmakuItem(string[] attrs, string content)
{
DanmakuMode = attrs[1] switch
{
"4" => POS_BOTTOM,
"5" => POS_TOP,
_ => POS_MOVE,
};
try
{
double second = double.Parse(attrs[0]);
Second = second;
StartTime = ComputeTime(second);
EndTime = ComputeTime(second + (DanmakuMode == 1 ? MOVE_SPEND_TIME : TOP_SPEND_TIME));
}
catch (Exception e)
{
Log(e.Message);
}
FontSize = attrs[2];
try
{
int colorD = int.Parse(attrs[3]);
Color = string.Format("{0:X6}", colorD);
}
catch (FormatException e)
{
Log(e.Message);
}
Timestamp = attrs[4];
Content = content;
}
private static string ComputeTime(double second)
{
int hour = (int)second / 3600;
int minute = (int)(second - (hour * 3600)) / 60;
second -= (hour * 3600) + (minute * 60);
return hour.ToString() + string.Format(":{0:D2}:", minute) + string.Format("{0:00.00}", second);
}
public string Content { get; set; } = "";
// 弹幕内容
public string StartTime { get; set; } = "";
// 出现时间
public double Second { get; set; } = 0.00;
// 出现时间(秒为单位)
public string EndTime { get; set; } = "";
// 消失时间
public int DanmakuMode { get; set; } = POS_MOVE;
// 弹幕类型
public string FontSize { get; set; } = "";
// 字号
public string Color { get; set; } = "";
// 颜色
public string Timestamp { get; set; } = "";
// 时间戳
}
public class DanmakuComparer : IComparer<DanmakuItem>
{
public int Compare(DanmakuItem? x, DanmakuItem? y)
{
if (x == null) return -1;
if (y == null) return 1;
return x.Second.CompareTo(y.Second);
}
}
private const int POS_MOVE = 1; //滚动弹幕
private const int POS_TOP = 2; //顶部弹幕
private const int POS_BOTTOM = 3; //底部弹幕
return danmakus.ToArray();
}
}
/// <summary>
/// 保存为ASS字幕文件
/// </summary>
/// <param name="danmakus">弹幕</param>
/// <param name="outputPath">保存路径</param>
/// <returns></returns>
public static async Task SaveAsAssAsync(DanmakuItem[] danmakus, string outputPath)
{
var sb = new StringBuilder();
// ASS字幕文件头
sb.AppendLine("[Script Info]");
sb.AppendLine("Script Updated By: BBDown(https://github.com/nilaoda/BBDown)");
sb.AppendLine("ScriptType: v4.00+");
sb.AppendLine($"PlayResX: {MONITOR_WIDTH}");
sb.AppendLine($"PlayResY: {MONITOR_HEIGHT}");
sb.AppendLine($"Aspect Ratio: {MONITOR_WIDTH}:{MONITOR_HEIGHT}");
sb.AppendLine("Collisions: Normal");
sb.AppendLine("WrapStyle: 2");
sb.AppendLine("ScaledBorderAndShadow: yes");
sb.AppendLine("YCbCr Matrix: TV.601");
sb.AppendLine("[V4+ Styles]");
sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
sb.AppendLine($"Style: BBDOWN_Style, 黑体, {FONT_SIZE}, &H00FFFFFF, &H00FFFFFF, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 2, 0, 7, 0, 0, 0, 0");
sb.AppendLine("[Events]");
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
PositionController controller = new(); // 弹幕位置控制器
Array.Sort(danmakus, comparer);
foreach (DanmakuItem danmaku in danmakus)
{
int height = controller.UpdatePosition(danmaku.DanmakuMode, danmaku.Second, danmaku.Content.Length);
if (height == -1) continue;
string effect = "";
effect += danmaku.DanmakuMode switch
{
3 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {MONITOR_HEIGHT - FONT_SIZE - height})",
2 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {height})",
_ => $"\\move({MONITOR_WIDTH}, {height}, {-danmaku.Content.Length * FONT_SIZE}, {height})",
};
if (danmaku.Color != "FFFFFF")
{
effect += $"\\c&{danmaku.Color}&";
}
sb.AppendLine($"Dialogue: 2,{danmaku.StartTime},{danmaku.EndTime},BBDOWN_Style,,0000,0000,0000,,{{{effect}}}{danmaku.Content}");
}
await File.WriteAllTextAsync(outputPath, sb.ToString(), Encoding.UTF8);
}
protected class PositionController
{
readonly int maxLine = MONITOR_HEIGHT * PROTECT_LENGTH / FONT_SIZE / 100; //总行数
// 三个位置的弹幕队列,记录弹幕结束时间
readonly List<double> moveQueue = new();
readonly List<double> topQueue = new();
readonly List<double> bottomQueue = new();
public PositionController()
{
for (int i = 0; i < maxLine; i++)
{
moveQueue.Add(0.00);
topQueue.Add(0.00);
bottomQueue.Add(0.00);
}
}
public int UpdatePosition(int type, double time, int length)
{
// 获取可用位置
List<double> vs;
double displayTime = TOP_SPEND_TIME;
if (type == POS_BOTTOM)
{
vs = bottomQueue;
}
else if (type == POS_TOP)
{
vs = topQueue;
}
else
{
vs = moveQueue;
displayTime = MOVE_SPEND_TIME * (length + 5) * FONT_SIZE / (MONITOR_WIDTH + (length * MOVE_SPEND_TIME));
}
for (int i = 0; i < maxLine; i++)
{
if (time >= vs[i])
{ // 此条弹幕已结束,更新该位置信息
vs[i] = time + displayTime;
return i * FONT_SIZE;
}
}
return -1;
}
}
public class DanmakuItem
{
public DanmakuItem(string[] attrs, string content)
{
DanmakuMode = attrs[1] switch
{
"4" => POS_BOTTOM,
"5" => POS_TOP,
_ => POS_MOVE,
};
try
{
double second = double.Parse(attrs[0]);
Second = second;
StartTime = ComputeTime(second);
EndTime = ComputeTime(second + (DanmakuMode == 1 ? MOVE_SPEND_TIME : TOP_SPEND_TIME));
}
catch (Exception e)
{
Log(e.Message);
}
FontSize = attrs[2];
try
{
int colorD = int.Parse(attrs[3]);
Color = string.Format("{0:X6}", colorD);
}
catch (FormatException e)
{
Log(e.Message);
}
Timestamp = attrs[4];
Content = content;
}
private static string ComputeTime(double second)
{
int hour = (int)second / 3600;
int minute = (int)(second - (hour * 3600)) / 60;
second -= (hour * 3600) + (minute * 60);
return hour.ToString() + string.Format(":{0:D2}:", minute) + string.Format("{0:00.00}", second);
}
public string Content { get; set; } = "";
// 弹幕内容
public string StartTime { get; set; } = "";
// 出现时间
public double Second { get; set; } = 0.00;
// 出现时间(秒为单位)
public string EndTime { get; set; } = "";
// 消失时间
public int DanmakuMode { get; set; } = POS_MOVE;
// 弹幕类型
public string FontSize { get; set; } = "";
// 字号
public string Color { get; set; } = "";
// 颜色
public string Timestamp { get; set; } = "";
// 时间戳
}
public class DanmakuComparer : IComparer<DanmakuItem>
{
public int Compare(DanmakuItem? x, DanmakuItem? y)
{
if (x == null) return -1;
if (y == null) return 1;
return x.Second.CompareTo(y.Second);
}
}
private const int POS_MOVE = 1; //滚动弹幕
private const int POS_TOP = 2; //顶部弹幕
private const int POS_BOTTOM = 3; //底部弹幕
}

View File

@@ -1,224 +1,223 @@
using BBDown.Core.Util;
using System.Diagnostics.CodeAnalysis;
namespace BBDown.Core.Entity
namespace BBDown.Core.Entity;
public static class Entity
{
public class Entity
public class Page
{
public class Page
public required int index;
public required string aid;
public required string cid;
public required string epid;
public required string title;
public required int dur;
public required string res;
public required long pubTime;
public string? cover;
public string? desc;
public string? ownerName;
public string? ownerMid;
public string bvid
{
public required int index;
public required string aid;
public required string cid;
public required string epid;
public required string title;
public required int dur;
public required string res;
public required long pubTime;
public string? cover;
public string? desc;
public string? ownerName;
public string? ownerMid;
public string bvid
{
get => BilibiliBvConverter.Encode(long.Parse(aid));
}
public List<ViewPoint> points = new();
get => BilibiliBvConverter.Encode(long.Parse(aid));
}
public List<ViewPoint> points = new();
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime)
{
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
}
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover)
{
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
this.cover = cover;
}
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover, string desc)
{
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
this.cover = cover;
this.desc = desc;
}
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover, string desc, string ownerName, string ownerMid)
{
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
this.cover = cover;
this.desc = desc;
this.ownerName = ownerName;
this.ownerMid = ownerMid;
}
[SetsRequiredMembers]
public Page(int index, Page page)
{
this.index = index;
this.aid = page.aid;
this.cid = page.cid;
this.epid = page.epid;
this.title = page.title;
this.dur = page.dur;
this.res = page.res;
this.pubTime = page.pubTime;
this.cover = page.cover;
this.ownerName = page.ownerName;
this.ownerMid = page.ownerMid;
}
public override bool Equals(object? obj)
{
return obj is Page page &&
aid == page.aid &&
cid == page.cid &&
epid == page.epid;
}
public override int GetHashCode()
{
return HashCode.Combine(aid, cid, epid);
}
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime)
{
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
}
public class ViewPoint
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover)
{
public required string title;
public required int start;
public required int end;
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
this.cover = cover;
}
public class Video
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover, string desc)
{
public required string id;
public required string dfn;
public required string baseUrl;
public string? res;
public string? fps;
public required string codecs;
public long bandwith;
public int dur;
public double size;
public override bool Equals(object? obj)
{
return obj is Video video &&
id == video.id &&
dfn == video.dfn &&
res == video.res &&
fps == video.fps &&
codecs == video.codecs &&
bandwith == video.bandwith &&
dur == video.dur;
}
public override int GetHashCode()
{
return HashCode.Combine(id, dfn, res, fps, codecs, bandwith, dur);
}
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
this.cover = cover;
this.desc = desc;
}
public class Audio
[SetsRequiredMembers]
public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover, string desc, string ownerName, string ownerMid)
{
public required string id;
public required string dfn;
public required string baseUrl;
public required string codecs;
public required long bandwith;
public required int dur;
public override bool Equals(object? obj)
{
return obj is Audio audio &&
id == audio.id &&
dfn == audio.dfn &&
codecs == audio.codecs &&
bandwith == audio.bandwith &&
dur == audio.dur;
}
public override int GetHashCode()
{
return HashCode.Combine(id, dfn, codecs, bandwith, dur);
}
this.aid = aid;
this.index = index;
this.cid = cid;
this.epid = epid;
this.title = title;
this.dur = dur;
this.res = res;
this.pubTime = pubTime;
this.cover = cover;
this.desc = desc;
this.ownerName = ownerName;
this.ownerMid = ownerMid;
}
public class Subtitle
[SetsRequiredMembers]
public Page(int index, Page page)
{
public required string lan;
public required string url;
public required string path;
this.index = index;
this.aid = page.aid;
this.cid = page.cid;
this.epid = page.epid;
this.title = page.title;
this.dur = page.dur;
this.res = page.res;
this.pubTime = page.pubTime;
this.cover = page.cover;
this.ownerName = page.ownerName;
this.ownerMid = page.ownerMid;
}
public class Clip
public override bool Equals(object? obj)
{
public required int index;
public required long from;
public required long to;
return obj is Page page &&
aid == page.aid &&
cid == page.cid &&
epid == page.epid;
}
public class AudioMaterial
public override int GetHashCode()
{
public required string title;
public required string personName;
public required string path;
[SetsRequiredMembers]
public AudioMaterial(string title, string personName, string path)
{
this.title = title;
this.personName = personName;
this.path = path;
}
[SetsRequiredMembers]
public AudioMaterial(AudioMaterialInfo audioMaterialInfo)
{
this.title = audioMaterialInfo.title;
this.personName = audioMaterialInfo.personName;
this.path = audioMaterialInfo.path;
}
}
public class AudioMaterialInfo
{
public required string title;
public required string personName;
public required string path;
public required List<Audio> audio;
return HashCode.Combine(aid, cid, epid);
}
}
}
public class ViewPoint
{
public required string title;
public required int start;
public required int end;
}
public class Video
{
public required string id;
public required string dfn;
public required string baseUrl;
public string? res;
public string? fps;
public required string codecs;
public long bandwith;
public int dur;
public double size;
public override bool Equals(object? obj)
{
return obj is Video video &&
id == video.id &&
dfn == video.dfn &&
res == video.res &&
fps == video.fps &&
codecs == video.codecs &&
bandwith == video.bandwith &&
dur == video.dur;
}
public override int GetHashCode()
{
return HashCode.Combine(id, dfn, res, fps, codecs, bandwith, dur);
}
}
public class Audio
{
public required string id;
public required string dfn;
public required string baseUrl;
public required string codecs;
public required long bandwith;
public required int dur;
public override bool Equals(object? obj)
{
return obj is Audio audio &&
id == audio.id &&
dfn == audio.dfn &&
codecs == audio.codecs &&
bandwith == audio.bandwith &&
dur == audio.dur;
}
public override int GetHashCode()
{
return HashCode.Combine(id, dfn, codecs, bandwith, dur);
}
}
public class Subtitle
{
public required string lan;
public required string url;
public required string path;
}
public class Clip
{
public required int index;
public required long from;
public required long to;
}
public class AudioMaterial
{
public required string title;
public required string personName;
public required string path;
[SetsRequiredMembers]
public AudioMaterial(string title, string personName, string path)
{
this.title = title;
this.personName = personName;
this.path = path;
}
[SetsRequiredMembers]
public AudioMaterial(AudioMaterialInfo audioMaterialInfo)
{
this.title = audioMaterialInfo.title;
this.personName = audioMaterialInfo.personName;
this.path = audioMaterialInfo.path;
}
}
public class AudioMaterialInfo
{
public required string title;
public required string personName;
public required string path;
public required List<Audio> audio;
}
}

View File

@@ -1,22 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Entity.Entity;
namespace BBDown.Core.Entity
namespace BBDown.Core.Entity;
public class ParsedResult
{
public class ParsedResult
{
public string WebJsonString { get; set; }
public List<Video> VideoTracks { get; set; } = new();
public List<Audio> AudioTracks { get; set; } = new();
public List<Audio> BackgroundAudioTracks { get; set; } = new();
public List<AudioMaterialInfo> RoleAudioList { get; set; } = new();
public List<ViewPoint> ExtraPoints { get; set; } = new();
// ⬇⬇⬇⬇⬇ FOR FLV ⬇⬇⬇⬇⬇
public List<string> Clips { get; set; } = new();
public List<string> Dfns { get; set; } = new();
}
}
public string WebJsonString { get; set; }
public List<Video> VideoTracks { get; set; } = new();
public List<Audio> AudioTracks { get; set; } = new();
public List<Audio> BackgroundAudioTracks { get; set; } = new();
public List<AudioMaterialInfo> RoleAudioList { get; set; } = new();
public List<ViewPoint> ExtraPoints { get; set; } = new();
// ⬇⬇⬇⬇⬇ FOR FLV ⬇⬇⬇⬇⬇
public List<string> Clips { get; set; } = new();
public List<string> Dfns { get; set; } = new();
}

View File

@@ -1,49 +1,48 @@
using static BBDown.Core.Entity.Entity;
namespace BBDown.Core.Entity
namespace BBDown.Core.Entity;
public class VInfo
{
public class VInfo
{
/// <summary>
/// 视频标题
/// </summary>
public required string Title { get; set; }
/// <summary>
/// 视频标题
/// </summary>
public required string Title { get; set; }
/// <summary>
/// 视频描述
/// </summary>
public required string Desc { get; set; }
/// <summary>
/// 视频描述
/// </summary>
public required string Desc { get; set; }
/// <summary>
/// 视频封面
/// </summary>
public required string Pic { get; set; }
/// <summary>
/// 视频封面
/// </summary>
public required string Pic { get; set; }
/// <summary>
/// 视频发布时间
/// </summary>
public required long PubTime { get; set; }
public bool IsBangumi { get; set; }
public bool IsCheese { get; set; }
/// <summary>
/// 视频发布时间
/// </summary>
public required long PubTime { get; set; }
public bool IsBangumi { get; set; }
public bool IsCheese { get; set; }
/// <summary>
/// 番剧是否完结
/// </summary>
public bool IsBangumiEnd { get; set; }
/// <summary>
/// 番剧是否完结
/// </summary>
public bool IsBangumiEnd { get; set; }
/// <summary>
/// 视频index 用于番剧或课程判断当前选择的是第几集
/// </summary>
public string? Index { get; set; }
/// <summary>
/// 视频index 用于番剧或课程判断当前选择的是第几集
/// </summary>
public string? Index { get; set; }
/// <summary>
/// 视频分P信息
/// </summary>
public required List<Page> PagesInfo { get; set; }
/// <summary>
/// 视频分P信息
/// </summary>
public required List<Page> PagesInfo { get; set; }
/// <summary>
/// 是否为互动视频
/// </summary>
public bool IsSteinGate { get; set; }
}
}
/// <summary>
/// 是否为互动视频
/// </summary>
public bool IsSteinGate { get; set; }
}

View File

@@ -3,81 +3,80 @@ using System.Text.Json;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Util.HTTPUtil;
namespace BBDown.Core.Fetcher
{
public class BangumiInfoFetcher : IFetcher
{
public async Task<VInfo> FetchAsync(string id)
{
id = id[3..];
string index = "";
string api = $"https://{Config.EPHOST}/pgc/view/web/season?ep_id={id}";
string json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var result = infoJson.RootElement.GetProperty("result");
string cover = result.GetProperty("cover").ToString();
string title = result.GetProperty("title").ToString();
string desc = result.GetProperty("evaluate").ToString();
string pubTimeStr = result.GetProperty("publish").GetProperty("pub_time").ToString();
long pubTime = string.IsNullOrEmpty(pubTimeStr) ? 0 : DateTimeOffset.ParseExact(pubTimeStr, "yyyy-MM-dd HH:mm:ss", null).ToUnixTimeSeconds();
var pages = result.GetProperty("episodes").EnumerateArray();
List<Page> pagesInfo = new();
int i = 1;
namespace BBDown.Core.Fetcher;
//episodes为空; 或者未包含对应epid番外/花絮什么的
if (!(pages.Any() && result.GetProperty("episodes").ToString().Contains($"/ep{id}")))
public class BangumiInfoFetcher : IFetcher
{
public async Task<VInfo> FetchAsync(string id)
{
id = id[3..];
string index = "";
string api = $"https://{Config.EPHOST}/pgc/view/web/season?ep_id={id}";
string json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var result = infoJson.RootElement.GetProperty("result");
string cover = result.GetProperty("cover").ToString();
string title = result.GetProperty("title").ToString();
string desc = result.GetProperty("evaluate").ToString();
string pubTimeStr = result.GetProperty("publish").GetProperty("pub_time").ToString();
long pubTime = string.IsNullOrEmpty(pubTimeStr) ? 0 : DateTimeOffset.ParseExact(pubTimeStr, "yyyy-MM-dd HH:mm:ss", null).ToUnixTimeSeconds();
var pages = result.GetProperty("episodes").EnumerateArray();
List<Page> pagesInfo = new();
int i = 1;
//episodes为空; 或者未包含对应epid番外/花絮什么的
if (!(pages.Any() && result.GetProperty("episodes").ToString().Contains($"/ep{id}")))
{
if (result.TryGetProperty("section", out JsonElement sections))
{
if (result.TryGetProperty("section", out JsonElement sections))
foreach (var section in sections.EnumerateArray())
{
foreach (var section in sections.EnumerateArray())
if (section.ToString().Contains($"/ep{id}"))
{
if (section.ToString().Contains($"/ep{id}"))
{
title += "[" + section.GetProperty("title").ToString() + "]";
pages = section.GetProperty("episodes").EnumerateArray();
break;
}
title += "[" + section.GetProperty("title").ToString() + "]";
pages = section.GetProperty("episodes").EnumerateArray();
break;
}
}
}
foreach (var page in pages)
{
//跳过预告
if (page.TryGetProperty("badge", out JsonElement badge) && badge.ToString() == "预告") continue;
string res = "";
try
{
res = page.GetProperty("dimension").GetProperty("width").ToString() + "x" + page.GetProperty("dimension").GetProperty("height").ToString();
}
catch (Exception) { }
string _title = page.GetProperty("title").ToString() + " " + page.GetProperty("long_title").ToString();
_title = _title.Trim();
Page p = new(i++,
page.GetProperty("aid").ToString(),
page.GetProperty("cid").ToString(),
page.GetProperty("id").ToString(),
_title,
0, res,
page.GetProperty("pub_time").GetInt64());
if (p.epid == id) index = p.index.ToString();
pagesInfo.Add(p);
}
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = cover,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = true,
IsCheese = true,
Index = index
};
return info;
}
foreach (var page in pages)
{
//跳过预告
if (page.TryGetProperty("badge", out JsonElement badge) && badge.ToString() == "预告") continue;
string res = "";
try
{
res = page.GetProperty("dimension").GetProperty("width").ToString() + "x" + page.GetProperty("dimension").GetProperty("height").ToString();
}
catch (Exception) { }
string _title = page.GetProperty("title").ToString() + " " + page.GetProperty("long_title").ToString();
_title = _title.Trim();
Page p = new(i++,
page.GetProperty("aid").ToString(),
page.GetProperty("cid").ToString(),
page.GetProperty("id").ToString(),
_title,
0, res,
page.GetProperty("pub_time").GetInt64());
if (p.epid == id) index = p.index.ToString();
pagesInfo.Add(p);
}
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = cover,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = true,
IsCheese = true,
Index = index
};
return info;
}
}
}

View File

@@ -3,57 +3,56 @@ using System.Text.Json;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Util.HTTPUtil;
namespace BBDown.Core.Fetcher
namespace BBDown.Core.Fetcher;
public class CheeseInfoFetcher : IFetcher
{
public class CheeseInfoFetcher : IFetcher
public async Task<VInfo> FetchAsync(string id)
{
public async Task<VInfo> FetchAsync(string id)
id = id[7..];
string index = "";
string api = $"https://api.bilibili.com/pugv/view/web/season?ep_id={id}";
string json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
string cover = data.GetProperty("cover").ToString();
string title = data.GetProperty("title").ToString();
string desc = data.GetProperty("subtitle").ToString();
string ownerName = data.GetProperty("up_info").GetProperty("uname").ToString();
string ownerMid = data.GetProperty("up_info").GetProperty("mid").ToString();
var pages = data.GetProperty("episodes").EnumerateArray();
List<Page> pagesInfo = new();
foreach (var page in pages)
{
id = id[7..];
string index = "";
string api = $"https://api.bilibili.com/pugv/view/web/season?ep_id={id}";
string json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
string cover = data.GetProperty("cover").ToString();
string title = data.GetProperty("title").ToString();
string desc = data.GetProperty("subtitle").ToString();
string ownerName = data.GetProperty("up_info").GetProperty("uname").ToString();
string ownerMid = data.GetProperty("up_info").GetProperty("mid").ToString();
var pages = data.GetProperty("episodes").EnumerateArray();
List<Page> pagesInfo = new();
foreach (var page in pages)
{
Page p = new(page.GetProperty("index").GetInt32(),
page.GetProperty("aid").ToString(),
page.GetProperty("cid").ToString(),
page.GetProperty("id").ToString(),
page.GetProperty("title").ToString().Trim(),
page.GetProperty("duration").GetInt32(),
"",
page.GetProperty("release_date").GetInt64(),
"",
"",
ownerName,
ownerMid);
if (p.epid == id) index = p.index.ToString();
pagesInfo.Add(p);
}
long pubTime = pagesInfo.Any() ? pagesInfo[0].pubTime : 0;
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = cover,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = true,
IsCheese = true,
Index = index
};
return info;
Page p = new(page.GetProperty("index").GetInt32(),
page.GetProperty("aid").ToString(),
page.GetProperty("cid").ToString(),
page.GetProperty("id").ToString(),
page.GetProperty("title").ToString().Trim(),
page.GetProperty("duration").GetInt32(),
"",
page.GetProperty("release_date").GetInt64(),
"",
"",
ownerName,
ownerMid);
if (p.epid == id) index = p.index.ToString();
pagesInfo.Add(p);
}
long pubTime = pagesInfo.Any() ? pagesInfo[0].pubTime : 0;
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = cover,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = true,
IsCheese = true,
Index = index
};
return info;
}
}
}

View File

@@ -4,103 +4,102 @@ using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Util.HTTPUtil;
namespace BBDown.Core.Fetcher
namespace BBDown.Core.Fetcher;
/// <summary>
/// 收藏夹解析
/// https://space.bilibili.com/3/favlist
///
/// </summary>
public class FavListFetcher : IFetcher
{
/// <summary>
/// 收藏夹解析
/// https://space.bilibili.com/3/favlist
///
/// </summary>
public class FavListFetcher : IFetcher
public async Task<VInfo> FetchAsync(string id)
{
public async Task<VInfo> FetchAsync(string id)
id = id[6..];
var favId = id.Split(':')[0];
var mid = id.Split(':')[1];
//查找默认收藏夹
if (favId == "")
{
id = id[6..];
var favId = id.Split(':')[0];
var mid = id.Split(':')[1];
//查找默认收藏夹
if (favId == "")
var favListApi = $"https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid={mid}";
favId = JsonDocument.Parse(await GetWebSourceAsync(favListApi)).RootElement.GetProperty("data").GetProperty("list").EnumerateArray().First().GetProperty("id").ToString();
}
int pageSize = 20;
int index = 1;
List<Page> pagesInfo = new();
var api = $"https://api.bilibili.com/x/v3/fav/resource/list?media_id={favId}&pn=1&ps={pageSize}&order=mtime&type=2&tid=0&platform=web";
var json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
int totalCount = data.GetProperty("info").GetProperty("media_count").GetInt32();
int totalPage = (int)Math.Ceiling((double)totalCount / pageSize);
var title = data.GetProperty("info").GetProperty("title").GetString()!;
var intro = data.GetProperty("info").GetProperty("intro").GetString()!;
long pubTime = data.GetProperty("info").GetProperty("ctime").GetInt64();
var userName = data.GetProperty("info").GetProperty("upper").GetProperty("name").ToString();
var medias = data.GetProperty("medias").EnumerateArray().ToList();
for (int page = 2; page <= totalPage; page++)
{
api = $"https://api.bilibili.com/x/v3/fav/resource/list?media_id={favId}&pn={page}&ps={pageSize}&order=mtime&type=2&tid=0&platform=web";
json = await GetWebSourceAsync(api);
var jsonDoc = JsonDocument.Parse(json);
data = jsonDoc.RootElement.GetProperty("data");
medias.AddRange(data.GetProperty("medias").EnumerateArray().ToList());
}
foreach (var m in medias)
{
//只处理视频类型(可以直接在query param上指定type=2)
// if (m.GetProperty("type").GetInt32() != 2) continue;
//只处理未失效视频
if (m.GetProperty("attr").GetInt32() != 0) continue;
var pageCount = m.GetProperty("page").GetInt32();
if (pageCount > 1)
{
var favListApi = $"https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid={mid}";
favId = JsonDocument.Parse(await GetWebSourceAsync(favListApi)).RootElement.GetProperty("data").GetProperty("list").EnumerateArray().First().GetProperty("id").ToString();
}
int pageSize = 20;
int index = 1;
List<Page> pagesInfo = new();
var api = $"https://api.bilibili.com/x/v3/fav/resource/list?media_id={favId}&pn=1&ps={pageSize}&order=mtime&type=2&tid=0&platform=web";
var json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
int totalCount = data.GetProperty("info").GetProperty("media_count").GetInt32();
int totalPage = (int)Math.Ceiling((double)totalCount / pageSize);
var title = data.GetProperty("info").GetProperty("title").GetString()!;
var intro = data.GetProperty("info").GetProperty("intro").GetString()!;
long pubTime = data.GetProperty("info").GetProperty("ctime").GetInt64();
var userName = data.GetProperty("info").GetProperty("upper").GetProperty("name").ToString();
var medias = data.GetProperty("medias").EnumerateArray().ToList();
for (int page = 2; page <= totalPage; page++)
{
api = $"https://api.bilibili.com/x/v3/fav/resource/list?media_id={favId}&pn={page}&ps={pageSize}&order=mtime&type=2&tid=0&platform=web";
json = await GetWebSourceAsync(api);
var jsonDoc = JsonDocument.Parse(json);
data = jsonDoc.RootElement.GetProperty("data");
medias.AddRange(data.GetProperty("medias").EnumerateArray().ToList());
}
foreach (var m in medias)
{
//只处理视频类型(可以直接在query param上指定type=2)
// if (m.GetProperty("type").GetInt32() != 2) continue;
//只处理未失效视频
if (m.GetProperty("attr").GetInt32() != 0) continue;
var pageCount = m.GetProperty("page").GetInt32();
if (pageCount > 1)
var tmpInfo = await new NormalInfoFetcher().FetchAsync(m.GetProperty("id").ToString());
foreach (var item in tmpInfo.PagesInfo)
{
var tmpInfo = await new NormalInfoFetcher().FetchAsync(m.GetProperty("id").ToString());
foreach (var item in tmpInfo.PagesInfo)
Page p = new(index++, item)
{
Page p = new(index++, item)
{
title = m.GetProperty("title").ToString() + $"_P{item.index}_{item.title}",
cover = tmpInfo.Pic,
desc = m.GetProperty("intro").ToString()
};
if (!pagesInfo.Contains(p)) pagesInfo.Add(p);
}
}
else
{
Page p = new(index++,
m.GetProperty("id").ToString(),
m.GetProperty("ugc").GetProperty("first_cid").ToString(),
"", //epid
m.GetProperty("title").ToString(),
m.GetProperty("duration").GetInt32(),
"",
m.GetProperty("pubtime").GetInt64(),
m.GetProperty("cover").ToString(),
m.GetProperty("intro").ToString(),
m.GetProperty("upper").GetProperty("name").ToString(),
m.GetProperty("upper").GetProperty("mid").ToString());
title = m.GetProperty("title").ToString() + $"_P{item.index}_{item.title}",
cover = tmpInfo.Pic,
desc = m.GetProperty("intro").ToString()
};
if (!pagesInfo.Contains(p)) pagesInfo.Add(p);
}
}
var info = new VInfo
else
{
Title = title.Trim(),
Desc = intro.Trim(),
Pic = "",
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = false
};
return info;
Page p = new(index++,
m.GetProperty("id").ToString(),
m.GetProperty("ugc").GetProperty("first_cid").ToString(),
"", //epid
m.GetProperty("title").ToString(),
m.GetProperty("duration").GetInt32(),
"",
m.GetProperty("pubtime").GetInt64(),
m.GetProperty("cover").ToString(),
m.GetProperty("intro").ToString(),
m.GetProperty("upper").GetProperty("name").ToString(),
m.GetProperty("upper").GetProperty("mid").ToString());
if (!pagesInfo.Contains(p)) pagesInfo.Add(p);
}
}
var info = new VInfo
{
Title = title.Trim(),
Desc = intro.Trim(),
Pic = "",
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = false
};
return info;
}
}
}

View File

@@ -4,124 +4,123 @@ using System.Text.RegularExpressions;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Util.HTTPUtil;
namespace BBDown.Core.Fetcher
namespace BBDown.Core.Fetcher;
public partial class IntlBangumiInfoFetcher : IFetcher
{
public partial class IntlBangumiInfoFetcher : IFetcher
public async Task<VInfo> FetchAsync(string id)
{
public async Task<VInfo> FetchAsync(string id)
id = id[3..];
string index = "";
//string api = $"https://api.global.bilibili.com/intl/gateway/ogv/m/view?ep_id={id}";
string api = "https://" + (Config.HOST == "api.bilibili.com" ? "api.bilibili.tv" : Config.HOST) +
$"/intl/gateway/v2/ogv/view/app/season?ep_id={id}&platform=android&s_locale=zh_SG&mobi_app=bstar_a" + (Config.TOKEN != "" ? $"&access_key={Config.TOKEN}" : "");
string json = (await GetWebSourceAsync(api)).Replace("\\/", "/");
using var infoJson = JsonDocument.Parse(json);
var result = infoJson.RootElement.GetProperty("result");
string seasonId = result.GetProperty("season_id").ToString();
string cover = result.GetProperty("cover").ToString();
string title = result.GetProperty("title").ToString();
string desc = result.GetProperty("evaluate").ToString();
if (cover == "")
{
id = id[3..];
string index = "";
//string api = $"https://api.global.bilibili.com/intl/gateway/ogv/m/view?ep_id={id}";
string api = "https://" + (Config.HOST == "api.bilibili.com" ? "api.bilibili.tv" : Config.HOST) +
$"/intl/gateway/v2/ogv/view/app/season?ep_id={id}&platform=android&s_locale=zh_SG&mobi_app=bstar_a" + (Config.TOKEN != "" ? $"&access_key={Config.TOKEN}" : "");
string json = (await GetWebSourceAsync(api)).Replace("\\/", "/");
using var infoJson = JsonDocument.Parse(json);
var result = infoJson.RootElement.GetProperty("result");
string seasonId = result.GetProperty("season_id").ToString();
string cover = result.GetProperty("cover").ToString();
string title = result.GetProperty("title").ToString();
string desc = result.GetProperty("evaluate").ToString();
if (cover == "")
string animeUrl = $"https://bangumi.bilibili.com/anime/{seasonId}";
var web = await GetWebSourceAsync(animeUrl);
if (web != "")
{
string animeUrl = $"https://bangumi.bilibili.com/anime/{seasonId}";
var web = await GetWebSourceAsync(animeUrl);
if (web != "")
Regex regex = StateRegex();
string _json = regex.Match(web).Groups[1].Value;
using var _tempJson = JsonDocument.Parse(_json);
cover = _tempJson.RootElement.GetProperty("mediaInfo").GetProperty("cover").ToString();
title = _tempJson.RootElement.GetProperty("mediaInfo").GetProperty("title").ToString();
desc = _tempJson.RootElement.GetProperty("mediaInfo").GetProperty("evaluate").ToString();
}
}
string pubTimeStr = result.GetProperty("publish").GetProperty("pub_time").ToString();
long pubTime = string.IsNullOrEmpty(pubTimeStr) ? 0 : DateTimeOffset.ParseExact(pubTimeStr, "yyyy-MM-dd HH:mm:ss", null).ToUnixTimeSeconds();
var pages = new List<JsonElement>();
if (result.TryGetProperty("episodes", out JsonElement episodes))
{
pages = episodes.EnumerateArray().ToList();
}
List<Page> pagesInfo = new();
int i = 1;
if (result.TryGetProperty("modules", out JsonElement modules))
{
foreach (var section in modules.EnumerateArray())
{
if (section.ToString().Contains($"/{id}"))
{
Regex regex = StateRegex();
string _json = regex.Match(web).Groups[1].Value;
using var _tempJson = JsonDocument.Parse(_json);
cover = _tempJson.RootElement.GetProperty("mediaInfo").GetProperty("cover").ToString();
title = _tempJson.RootElement.GetProperty("mediaInfo").GetProperty("title").ToString();
desc = _tempJson.RootElement.GetProperty("mediaInfo").GetProperty("evaluate").ToString();
pages = section.GetProperty("data").GetProperty("episodes").EnumerateArray().ToList();
break;
}
}
}
string pubTimeStr = result.GetProperty("publish").GetProperty("pub_time").ToString();
long pubTime = string.IsNullOrEmpty(pubTimeStr) ? 0 : DateTimeOffset.ParseExact(pubTimeStr, "yyyy-MM-dd HH:mm:ss", null).ToUnixTimeSeconds();
var pages = new List<JsonElement>();
if (result.TryGetProperty("episodes", out JsonElement episodes))
/*if (pages.Count == 0)
{
if (web != "")
{
pages = episodes.EnumerateArray().ToList();
string epApi = $"https://api.bilibili.com/pgc/web/season/section?season_id={seasonId}";
var _web = GetWebSource(epApi);
pages = JArray.Parse(JObject.Parse(_web)["result"]["main_section"]["episodes"].ToString());
}
List<Page> pagesInfo = new();
int i = 1;
if (result.TryGetProperty("modules", out JsonElement modules))
else if (infoJson["data"]["modules"] != null)
{
foreach (var section in modules.EnumerateArray())
foreach (JObject section in JArray.Parse(infoJson["data"]["modules"].ToString()))
{
if (section.ToString().Contains($"/{id}"))
if (section.ToString().Contains($"ep_id={id}"))
{
pages = section.GetProperty("data").GetProperty("episodes").EnumerateArray().ToList();
pages = JArray.Parse(section["data"]["episodes"].ToString());
break;
}
}
}
}*/
/*if (pages.Count == 0)
foreach (var page in pages)
{
//跳过预告
if (page.TryGetProperty("badge", out JsonElement badge) && badge.ToString() == "预告") continue;
string res = "";
try
{
if (web != "")
{
string epApi = $"https://api.bilibili.com/pgc/web/season/section?season_id={seasonId}";
var _web = GetWebSource(epApi);
pages = JArray.Parse(JObject.Parse(_web)["result"]["main_section"]["episodes"].ToString());
}
else if (infoJson["data"]["modules"] != null)
{
foreach (JObject section in JArray.Parse(infoJson["data"]["modules"].ToString()))
{
if (section.ToString().Contains($"ep_id={id}"))
{
pages = JArray.Parse(section["data"]["episodes"].ToString());
break;
}
}
}
}*/
foreach (var page in pages)
{
//跳过预告
if (page.TryGetProperty("badge", out JsonElement badge) && badge.ToString() == "预告") continue;
string res = "";
try
{
res = page.GetProperty("dimension").GetProperty("width").ToString() + "x" + page.GetProperty("dimension").GetProperty("height").ToString();
}
catch (Exception) { }
string _title = page.GetProperty("title").ToString() + " " + page.GetProperty("long_title").ToString();
_title = _title.Trim();
Page p = new(i++,
page.GetProperty("aid").ToString(),
page.GetProperty("cid").ToString(),
page.GetProperty("id").ToString(),
_title,
0, res,
page.TryGetProperty("pub_time", out JsonElement pub_time) ? pub_time.GetInt64() : 0);
if (p.epid == id) index = p.index.ToString();
pagesInfo.Add(p);
res = page.GetProperty("dimension").GetProperty("width").ToString() + "x" + page.GetProperty("dimension").GetProperty("height").ToString();
}
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = cover,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = true,
IsCheese = true,
Index = index
};
return info;
catch (Exception) { }
string _title = page.GetProperty("title").ToString() + " " + page.GetProperty("long_title").ToString();
_title = _title.Trim();
Page p = new(i++,
page.GetProperty("aid").ToString(),
page.GetProperty("cid").ToString(),
page.GetProperty("id").ToString(),
_title,
0, res,
page.TryGetProperty("pub_time", out JsonElement pub_time) ? pub_time.GetInt64() : 0);
if (p.epid == id) index = p.index.ToString();
pagesInfo.Add(p);
}
[GeneratedRegex("window.__INITIAL_STATE__=([\\s\\S].*?);\\(function\\(\\)")]
private static partial Regex StateRegex();
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = cover,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = true,
IsCheese = true,
Index = index
};
return info;
}
[GeneratedRegex("window.__INITIAL_STATE__=([\\s\\S].*?);\\(function\\(\\)")]
private static partial Regex StateRegex();
}

View File

@@ -3,46 +3,46 @@ using System.Text.Json;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Util.HTTPUtil;
namespace BBDown.Core.Fetcher
{
/// <summary>
/// 合集解析
/// https://space.bilibili.com/23630128/channel/collectiondetail?sid=2045
/// https://www.bilibili.com/medialist/play/23630128?business=space_collection&business_id=2045 (无法从该链接打开合集)
/// </summary>
public class MediaListFetcher : IFetcher
{
public async Task<VInfo> FetchAsync(string id)
{
id = id[10..];
var api = $"https://api.bilibili.com/x/v1/medialist/info?type=8&biz_id={id}&tid=0";
var json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
var listTitle = data.GetProperty("title").GetString()!;
var intro = data.GetProperty("intro").GetString()!;
long pubTime = data.GetProperty("ctime").GetInt64()!;
namespace BBDown.Core.Fetcher;
List<Page> pagesInfo = new();
bool hasMore = true;
var oid = "";
int index = 1;
while (hasMore)
/// <summary>
/// 合集解析
/// https://space.bilibili.com/23630128/channel/collectiondetail?sid=2045
/// https://www.bilibili.com/medialist/play/23630128?business=space_collection&business_id=2045 (无法从该链接打开合集)
/// </summary>
public class MediaListFetcher : IFetcher
{
public async Task<VInfo> FetchAsync(string id)
{
id = id[10..];
var api = $"https://api.bilibili.com/x/v1/medialist/info?type=8&biz_id={id}&tid=0";
var json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
var listTitle = data.GetProperty("title").GetString()!;
var intro = data.GetProperty("intro").GetString()!;
long pubTime = data.GetProperty("ctime").GetInt64()!;
List<Page> pagesInfo = new();
bool hasMore = true;
var oid = "";
int index = 1;
while (hasMore)
{
var listApi = $"https://api.bilibili.com/x/v2/medialist/resource/list?type=8&oid={oid}&otype=2&biz_id={id}&with_current=true&mobi_app=web&ps=20&direction=false&sort_field=1&tid=0&desc=false";
json = await GetWebSourceAsync(listApi);
using var listJson = JsonDocument.Parse(json);
data = listJson.RootElement.GetProperty("data");
hasMore = data.GetProperty("has_more").GetBoolean();
foreach (var m in data.GetProperty("media_list").EnumerateArray())
{
var listApi = $"https://api.bilibili.com/x/v2/medialist/resource/list?type=8&oid={oid}&otype=2&biz_id={id}&with_current=true&mobi_app=web&ps=20&direction=false&sort_field=1&tid=0&desc=false";
json = await GetWebSourceAsync(listApi);
using var listJson = JsonDocument.Parse(json);
data = listJson.RootElement.GetProperty("data");
hasMore = data.GetProperty("has_more").GetBoolean();
foreach (var m in data.GetProperty("media_list").EnumerateArray())
var pageCount = m.GetProperty("page").GetInt32();
var desc = m.GetProperty("intro").GetString()!;
var ownerName = m.GetProperty("upper").GetProperty("name").ToString();
var ownerMid = m.GetProperty("upper").GetProperty("mid").ToString();
foreach (var page in m.GetProperty("pages").EnumerateArray())
{
var pageCount = m.GetProperty("page").GetInt32();
var desc = m.GetProperty("intro").GetString()!;
var ownerName = m.GetProperty("upper").GetProperty("name").ToString();
var ownerMid = m.GetProperty("upper").GetProperty("mid").ToString();
foreach (var page in m.GetProperty("pages").EnumerateArray())
{
Page p = new(index++,
Page p = new(index++,
m.GetProperty("id").ToString(),
page.GetProperty("id").ToString(),
"", //epid
@@ -54,24 +54,23 @@ namespace BBDown.Core.Fetcher
desc,
ownerName,
ownerMid);
if (!pagesInfo.Contains(p)) pagesInfo.Add(p);
else index--;
}
oid = m.GetProperty("id").ToString();
if (!pagesInfo.Contains(p)) pagesInfo.Add(p);
else index--;
}
oid = m.GetProperty("id").ToString();
}
var info = new VInfo
{
Title = listTitle.Trim(),
Desc = intro.Trim(),
Pic = "",
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = false
};
return info;
}
var info = new VInfo
{
Title = listTitle.Trim(),
Desc = intro.Trim(),
Pic = "",
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = false
};
return info;
}
}
}

View File

@@ -5,128 +5,127 @@ using System.Xml;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Util.HTTPUtil;
namespace BBDown.Core.Fetcher
namespace BBDown.Core.Fetcher;
public partial class NormalInfoFetcher : IFetcher
{
public partial class NormalInfoFetcher : IFetcher
public async Task<VInfo> FetchAsync(string id)
{
public async Task<VInfo> FetchAsync(string id)
string api = $"https://api.bilibili.com/x/web-interface/view?aid={id}";
string json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
string title = data.GetProperty("title").ToString();
string desc = data.GetProperty("desc").ToString();
string pic = data.GetProperty("pic").ToString();
var owner = data.GetProperty("owner");
string ownerMid = owner.GetProperty("mid").ToString();
string ownerName = owner.GetProperty("name").ToString();
long pubTime = data.GetProperty("pubdate").GetInt64();
bool bangumi = false;
var bvid = data.GetProperty("bvid").ToString();
var cid = data.GetProperty("cid").GetInt64();
// 互动视频 1:是 0:否
var isSteinGate = data.GetProperty("rights").GetProperty("is_stein_gate").GetInt16();
// 分p信息
List<Page> pagesInfo = new();
var pages = data.GetProperty("pages").EnumerateArray().ToList();
foreach (var page in pages)
{
string api = $"https://api.bilibili.com/x/web-interface/view?aid={id}";
string json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
string title = data.GetProperty("title").ToString();
string desc = data.GetProperty("desc").ToString();
string pic = data.GetProperty("pic").ToString();
var owner = data.GetProperty("owner");
string ownerMid = owner.GetProperty("mid").ToString();
string ownerName = owner.GetProperty("name").ToString();
long pubTime = data.GetProperty("pubdate").GetInt64();
bool bangumi = false;
var bvid = data.GetProperty("bvid").ToString();
var cid = data.GetProperty("cid").GetInt64();
// 互动视频 1:是 0:否
var isSteinGate = data.GetProperty("rights").GetProperty("is_stein_gate").GetInt16();
// 分p信息
List<Page> pagesInfo = new();
var pages = data.GetProperty("pages").EnumerateArray().ToList();
foreach (var page in pages)
{
Page p = new(page.GetProperty("page").GetInt32(),
id,
page.GetProperty("cid").ToString(),
"", //epid
page.GetProperty("part").ToString().Trim(),
page.GetProperty("duration").GetInt32(),
page.GetProperty("dimension").GetProperty("width").ToString() + "x" + page.GetProperty("dimension").GetProperty("height").ToString(),
pubTime, //分p视频没有发布时间
"",
"",
ownerName,
ownerMid
);
pagesInfo.Add(p);
}
if (isSteinGate == 1) // 互动视频获取分P信息
{
var playerSoApi = $"https://api.bilibili.com/x/player.so?bvid={bvid}&id=cid:{cid}";
var playerSoText = await GetWebSourceAsync(playerSoApi);
var playerSoXml = new XmlDocument();
playerSoXml.LoadXml($"<root>{playerSoText}</root>");
var interactionNode = playerSoXml.SelectSingleNode("//interaction");
if (interactionNode is { InnerText.Length: > 0 })
{
var graphVersion = JsonDocument.Parse(interactionNode.InnerText).RootElement
.GetProperty("graph_version").GetInt64();
var edgeInfoApi = $"https://api.bilibili.com/x/stein/edgeinfo_v2?graph_version={graphVersion}&bvid={bvid}";
var edgeInfoJson = await GetWebSourceAsync(edgeInfoApi);
var edgeInfoData = JsonDocument.Parse(edgeInfoJson).RootElement.GetProperty("data");
var questions = edgeInfoData.GetProperty("edges").GetProperty("questions").EnumerateArray()
.ToList();
var index = 2; // 互动视频分P索引从2开始
foreach (var question in questions)
{
var choices = question.GetProperty("choices").EnumerateArray().ToList();
foreach (var page in choices)
{
Page p = new(index++,
id,
page.GetProperty("cid").ToString(),
"", //epid
page.GetProperty("option").ToString().Trim(),
0,
"",
pubTime, //分p视频没有发布时间
"",
"",
ownerName,
ownerMid
);
pagesInfo.Add(p);
}
}
}
else
{
throw new Exception("互动视频获取分P信息失败");
}
}
try
{
if (data.GetProperty("redirect_url").ToString().Contains("bangumi"))
{
bangumi = true;
string epId = EpIdRegex().Match(data.GetProperty("redirect_url").ToString()).Groups[1].Value;
//番剧内容通常不会有分P如果有分P则不需要epId参数
if (pages.Count == 1)
{
pagesInfo.ForEach(p => p.epid = epId);
}
}
}
catch { }
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = pic,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = bangumi,
IsSteinGate = isSteinGate == 1
};
return info;
Page p = new(page.GetProperty("page").GetInt32(),
id,
page.GetProperty("cid").ToString(),
"", //epid
page.GetProperty("part").ToString().Trim(),
page.GetProperty("duration").GetInt32(),
page.GetProperty("dimension").GetProperty("width").ToString() + "x" + page.GetProperty("dimension").GetProperty("height").ToString(),
pubTime, //分p视频没有发布时间
"",
"",
ownerName,
ownerMid
);
pagesInfo.Add(p);
}
[GeneratedRegex("ep(\\d+)")]
private static partial Regex EpIdRegex();
if (isSteinGate == 1) // 互动视频获取分P信息
{
var playerSoApi = $"https://api.bilibili.com/x/player.so?bvid={bvid}&id=cid:{cid}";
var playerSoText = await GetWebSourceAsync(playerSoApi);
var playerSoXml = new XmlDocument();
playerSoXml.LoadXml($"<root>{playerSoText}</root>");
var interactionNode = playerSoXml.SelectSingleNode("//interaction");
if (interactionNode is { InnerText.Length: > 0 })
{
var graphVersion = JsonDocument.Parse(interactionNode.InnerText).RootElement
.GetProperty("graph_version").GetInt64();
var edgeInfoApi = $"https://api.bilibili.com/x/stein/edgeinfo_v2?graph_version={graphVersion}&bvid={bvid}";
var edgeInfoJson = await GetWebSourceAsync(edgeInfoApi);
var edgeInfoData = JsonDocument.Parse(edgeInfoJson).RootElement.GetProperty("data");
var questions = edgeInfoData.GetProperty("edges").GetProperty("questions").EnumerateArray()
.ToList();
var index = 2; // 互动视频分P索引从2开始
foreach (var question in questions)
{
var choices = question.GetProperty("choices").EnumerateArray().ToList();
foreach (var page in choices)
{
Page p = new(index++,
id,
page.GetProperty("cid").ToString(),
"", //epid
page.GetProperty("option").ToString().Trim(),
0,
"",
pubTime, //分p视频没有发布时间
"",
"",
ownerName,
ownerMid
);
pagesInfo.Add(p);
}
}
}
else
{
throw new Exception("互动视频获取分P信息失败");
}
}
try
{
if (data.GetProperty("redirect_url").ToString().Contains("bangumi"))
{
bangumi = true;
string epId = EpIdRegex().Match(data.GetProperty("redirect_url").ToString()).Groups[1].Value;
//番剧内容通常不会有分P如果有分P则不需要epId参数
if (pages.Count == 1)
{
pagesInfo.ForEach(p => p.epid = epId);
}
}
}
catch { }
var info = new VInfo
{
Title = title.Trim(),
Desc = desc.Trim(),
Pic = pic,
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = bangumi,
IsSteinGate = isSteinGate == 1
};
return info;
}
}
[GeneratedRegex("ep(\\d+)")]
private static partial Regex EpIdRegex();
}

View File

@@ -3,47 +3,47 @@ using System.Text.Json;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Util.HTTPUtil;
namespace BBDown.Core.Fetcher
{
/// <summary>
/// 列表解析
/// https://space.bilibili.com/23630128/channel/seriesdetail?sid=340933
/// </summary>
public class SeriesListFetcher : IFetcher
{
public async Task<VInfo> FetchAsync(string id)
{
//套用BBDownMediaListFetcher.cs的代码
//只修改id = id.Substring(12);以及api地址的type=5
id = id[12..];
var api = $"https://api.bilibili.com/x/v1/medialist/info?type=5&biz_id={id}&tid=0";
var json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
var listTitle = data.GetProperty("title").GetString()!;
var intro = data.GetProperty("intro").GetString()!;
long pubTime = data.GetProperty("ctime").GetInt64();
namespace BBDown.Core.Fetcher;
List<Page> pagesInfo = new();
bool hasMore = true;
var oid = "";
int index = 1;
while (hasMore)
/// <summary>
/// 列表解析
/// https://space.bilibili.com/23630128/channel/seriesdetail?sid=340933
/// </summary>
public class SeriesListFetcher : IFetcher
{
public async Task<VInfo> FetchAsync(string id)
{
//套用BBDownMediaListFetcher.cs的代码
//只修改id = id.Substring(12);以及api地址的type=5
id = id[12..];
var api = $"https://api.bilibili.com/x/v1/medialist/info?type=5&biz_id={id}&tid=0";
var json = await GetWebSourceAsync(api);
using var infoJson = JsonDocument.Parse(json);
var data = infoJson.RootElement.GetProperty("data");
var listTitle = data.GetProperty("title").GetString()!;
var intro = data.GetProperty("intro").GetString()!;
long pubTime = data.GetProperty("ctime").GetInt64();
List<Page> pagesInfo = new();
bool hasMore = true;
var oid = "";
int index = 1;
while (hasMore)
{
var listApi = $"https://api.bilibili.com/x/v2/medialist/resource/list?type=5&oid={oid}&otype=2&biz_id={id}&bvid=&with_current=true&mobi_app=web&ps=20&direction=false&sort_field=1&tid=0&desc=true";
json = await GetWebSourceAsync(listApi);
using var listJson = JsonDocument.Parse(json);
data = listJson.RootElement.GetProperty("data");
hasMore = data.GetProperty("has_more").GetBoolean();
foreach (var m in data.GetProperty("media_list").EnumerateArray())
{
var listApi = $"https://api.bilibili.com/x/v2/medialist/resource/list?type=5&oid={oid}&otype=2&biz_id={id}&bvid=&with_current=true&mobi_app=web&ps=20&direction=false&sort_field=1&tid=0&desc=true";
json = await GetWebSourceAsync(listApi);
using var listJson = JsonDocument.Parse(json);
data = listJson.RootElement.GetProperty("data");
hasMore = data.GetProperty("has_more").GetBoolean();
foreach (var m in data.GetProperty("media_list").EnumerateArray())
var pageCount = m.GetProperty("page").GetInt32();
var desc = m.GetProperty("intro").GetString()!;
var ownerName = m.GetProperty("upper").GetProperty("name").ToString();
var ownerMid = m.GetProperty("upper").GetProperty("mid").ToString();
foreach (var page in m.GetProperty("pages").EnumerateArray())
{
var pageCount = m.GetProperty("page").GetInt32();
var desc = m.GetProperty("intro").GetString()!;
var ownerName = m.GetProperty("upper").GetProperty("name").ToString();
var ownerMid = m.GetProperty("upper").GetProperty("mid").ToString();
foreach (var page in m.GetProperty("pages").EnumerateArray())
{
Page p = new(index++,
Page p = new(index++,
m.GetProperty("id").ToString(),
page.GetProperty("id").ToString(),
"", //epid
@@ -55,24 +55,23 @@ namespace BBDown.Core.Fetcher
desc,
ownerName,
ownerMid);
if (!pagesInfo.Contains(p)) pagesInfo.Add(p);
else index--;
}
oid = m.GetProperty("id").ToString();
if (!pagesInfo.Contains(p)) pagesInfo.Add(p);
else index--;
}
oid = m.GetProperty("id").ToString();
}
var info = new VInfo
{
Title = listTitle.Trim(),
Desc = intro.Trim(),
Pic = "",
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = false
};
return info;
}
var info = new VInfo
{
Title = listTitle.Trim(),
Desc = intro.Trim(),
Pic = "",
PubTime = pubTime,
PagesInfo = pagesInfo,
IsBangumi = false
};
return info;
}
}
}

View File

@@ -1,77 +1,74 @@
using BBDown.Core.Entity;
using System;
using System.Text;
using System.Text.Json;
using static BBDown.Core.Util.HTTPUtil;
using static BBDown.Core.Logger;
namespace BBDown.Core.Fetcher
namespace BBDown.Core.Fetcher;
public class SpaceVideoFetcher : IFetcher
{
public class SpaceVideoFetcher : IFetcher
public async Task<VInfo> FetchAsync(string id)
{
public async Task<VInfo> FetchAsync(string id)
id = id[4..];
// using the live API can bypass w_rid
string userInfoApi = $"https://api.live.bilibili.com/live_user/v1/Master/info?uid={id}";
string userName = GetValidFileName(JsonDocument.Parse(await GetWebSourceAsync(userInfoApi)).RootElement.GetProperty("data").GetProperty("info").GetProperty("uname").ToString(), ".", true);
List<string> urls = new();
int pageSize = 50;
int pageNumber = 1;
var api = Parser.WbiSign($"mid={id}&order=pubdate&pn={pageNumber}&ps={pageSize}&tid=0&wts={DateTimeOffset.Now.ToUnixTimeSeconds().ToString()}");
api = $"https://api.bilibili.com/x/space/wbi/arc/search?{api}";
string json = await GetWebSourceAsync(api);
var infoJson = JsonDocument.Parse(json);
var pages = infoJson.RootElement.GetProperty("data").GetProperty("list").GetProperty("vlist").EnumerateArray();
foreach (var page in pages)
{
id = id[4..];
// using the live API can bypass w_rid
string userInfoApi = $"https://api.live.bilibili.com/live_user/v1/Master/info?uid={id}";
string userName = GetValidFileName(JsonDocument.Parse(await GetWebSourceAsync(userInfoApi)).RootElement.GetProperty("data").GetProperty("info").GetProperty("uname").ToString(), ".", true);
List<string> urls = new();
int pageSize = 50;
int pageNumber = 1;
var api = Parser.WbiSign($"mid={id}&order=pubdate&pn={pageNumber}&ps={pageSize}&tid=0&wts={DateTimeOffset.Now.ToUnixTimeSeconds().ToString()}");
api = $"https://api.bilibili.com/x/space/wbi/arc/search?{api}";
string json = await GetWebSourceAsync(api);
var infoJson = JsonDocument.Parse(json);
var pages = infoJson.RootElement.GetProperty("data").GetProperty("list").GetProperty("vlist").EnumerateArray();
foreach (var page in pages)
{
urls.Add($"https://www.bilibili.com/video/av{page.GetProperty("aid")}");
}
int totalCount = infoJson.RootElement.GetProperty("data").GetProperty("page").GetProperty("count").GetInt32();
int totalPage = (int)Math.Ceiling((double)totalCount / pageSize);
while (pageNumber < totalPage)
{
pageNumber++;
urls.AddRange(await GetVideosByPageAsync(pageNumber, pageSize, id));
}
File.WriteAllText($"{userName}的投稿视频.txt", string.Join('\n', urls));
Log("目前下载器不支持下载用户的全部投稿视频不过程序已经获取到了该用户的全部投稿视频地址你可以自行使用批处理脚本等手段调用本程序进行批量下载。如在Windows系统你可以使用如下代码");
Console.WriteLine();
Console.WriteLine(@"@echo Off
urls.Add($"https://www.bilibili.com/video/av{page.GetProperty("aid")}");
}
int totalCount = infoJson.RootElement.GetProperty("data").GetProperty("page").GetProperty("count").GetInt32();
int totalPage = (int)Math.Ceiling((double)totalCount / pageSize);
while (pageNumber < totalPage)
{
pageNumber++;
urls.AddRange(await GetVideosByPageAsync(pageNumber, pageSize, id));
}
await File.WriteAllTextAsync($"{userName}的投稿视频.txt", string.Join(Environment.NewLine, urls));
Log("目前下载器不支持下载用户的全部投稿视频不过程序已经获取到了该用户的全部投稿视频地址你可以自行使用批处理脚本等手段调用本程序进行批量下载。如在Windows系统你可以使用如下代码");
Console.WriteLine();
Console.WriteLine(@"@echo Off
For /F %%a in (urls.txt) Do (BBDown.exe ""%%a"")
pause");
Console.WriteLine();
throw new Exception("暂不支持该功能");
}
static async Task<List<string>> GetVideosByPageAsync(int pageNumber, int pageSize, string mid)
{
List<string> urls = new();
var api = Parser.WbiSign($"mid={mid}&order=pubdate&pn={pageNumber}&ps={pageSize}&tid=0&wts={DateTimeOffset.Now.ToUnixTimeSeconds().ToString()}");
api = $"https://api.bilibili.com/x/space/wbi/arc/search?{api}";
string json = await GetWebSourceAsync(api);
var infoJson = JsonDocument.Parse(json);
var pages = infoJson.RootElement.GetProperty("data").GetProperty("list").GetProperty("vlist").EnumerateArray();
foreach (var page in pages)
{
urls.Add($"https://www.bilibili.com/video/av{page.GetProperty("aid")}");
}
return urls;
}
private static string GetValidFileName(string input, string re = ".", bool filterSlash = false)
{
string title = input;
foreach (char invalidChar in Path.GetInvalidFileNameChars())
{
title = title.Replace(invalidChar.ToString(), re);
}
if (filterSlash)
{
title = title.Replace("/", re);
title = title.Replace("\\", re);
}
return title;
}
Console.WriteLine();
throw new Exception("暂不支持该功能");
}
}
static async Task<List<string>> GetVideosByPageAsync(int pageNumber, int pageSize, string mid)
{
List<string> urls = new();
var api = Parser.WbiSign($"mid={mid}&order=pubdate&pn={pageNumber}&ps={pageSize}&tid=0&wts={DateTimeOffset.Now.ToUnixTimeSeconds().ToString()}");
api = $"https://api.bilibili.com/x/space/wbi/arc/search?{api}";
string json = await GetWebSourceAsync(api);
var infoJson = JsonDocument.Parse(json);
var pages = infoJson.RootElement.GetProperty("data").GetProperty("list").GetProperty("vlist").EnumerateArray();
foreach (var page in pages)
{
urls.Add($"https://www.bilibili.com/video/av{page.GetProperty("aid")}");
}
return urls;
}
private static string GetValidFileName(string input, string re = ".", bool filterSlash = false)
{
string title = input;
foreach (char invalidChar in Path.GetInvalidFileNameChars())
{
title = title.Replace(invalidChar.ToString(), re);
}
if (filterSlash)
{
title = title.Replace("/", re);
title = title.Replace("\\", re);
}
return title;
}
}

View File

@@ -1,49 +1,41 @@
using BBDown.Core.Fetcher;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BBDown.Core
namespace BBDown.Core;
public static class FetcherFactory
{
public sealed class FetcherFactory
/// <summary>
/// 根据不同场景获取不同的Info解析器
/// </summary>
/// <param name="aidOri"></param>
/// <returns>IFetcher</returns>
public static IFetcher CreateFetcher(string aidOri, bool useIntlApi)
{
private FetcherFactory() { }
/// <summary>
/// 根据不同场景获取不同的Info解析器
/// </summary>
/// <param name="aidOri"></param>
/// <returns>IFetcher</returns>
public static IFetcher CreateFetcher(string aidOri, bool useIntlApi)
IFetcher fetcher = new NormalInfoFetcher();
if (aidOri.StartsWith("cheese"))
{
IFetcher fetcher = new NormalInfoFetcher();
if (aidOri.StartsWith("cheese"))
{
fetcher = new CheeseInfoFetcher();
}
else if (aidOri.StartsWith("ep"))
{
fetcher = useIntlApi ? new IntlBangumiInfoFetcher() : new BangumiInfoFetcher();
}
else if (aidOri.StartsWith("mid"))
{
fetcher = new SpaceVideoFetcher();
}
else if (aidOri.StartsWith("listBizId"))
{
fetcher = new MediaListFetcher();
}
else if (aidOri.StartsWith("seriesBizId"))
{
fetcher = new SeriesListFetcher();
}
else if (aidOri.StartsWith("favId"))
{
fetcher = new FavListFetcher();
}
return fetcher;
fetcher = new CheeseInfoFetcher();
}
else if (aidOri.StartsWith("ep"))
{
fetcher = useIntlApi ? new IntlBangumiInfoFetcher() : new BangumiInfoFetcher();
}
else if (aidOri.StartsWith("mid"))
{
fetcher = new SpaceVideoFetcher();
}
else if (aidOri.StartsWith("listBizId"))
{
fetcher = new MediaListFetcher();
}
else if (aidOri.StartsWith("seriesBizId"))
{
fetcher = new SeriesListFetcher();
}
else if (aidOri.StartsWith("favId"))
{
fetcher = new FavListFetcher();
}
return fetcher;
}
}
}

View File

@@ -1,7 +1,6 @@
namespace BBDown.Core
namespace BBDown.Core;
public interface IFetcher
{
public interface IFetcher
{
Task<Entity.VInfo> FetchAsync(string id);
}
}
Task<Entity.VInfo> FetchAsync(string id);
}

View File

@@ -1,61 +1,60 @@
namespace BBDown.Core
namespace BBDown.Core;
public static class Logger
{
public class Logger
public static void Log(object text, bool enter = true)
{
public static void Log(object text, bool enter = true)
{
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - " + text);
if (enter) Console.WriteLine();
}
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - " + text);
if (enter) Console.WriteLine();
}
public static void LogError(object text)
{
public static void LogError(object text)
{
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - ");
Console.ForegroundColor = ConsoleColor.Red;
Console.Write(text);
Console.ResetColor();
Console.WriteLine();
}
public static void LogColor(object text, bool time = true)
{
if (time)
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - ");
Console.ForegroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.Cyan;
if (time)
Console.Write(text);
Console.ResetColor();
Console.WriteLine();
}
else
Console.Write(" " + text);
Console.ResetColor();
Console.WriteLine();
}
public static void LogColor(object text, bool time = true)
public static void LogWarn(object text, bool time = true)
{
if (time)
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - ");
Console.ForegroundColor = ConsoleColor.DarkYellow;
if (time)
Console.Write(text);
else
Console.Write(" " + text);
Console.ResetColor();
Console.WriteLine();
}
public static void LogDebug(string toFormat, params object[] args)
{
if (Config.DEBUG_LOG)
{
if (time)
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - ");
Console.ForegroundColor = ConsoleColor.Cyan;
if (time)
Console.Write(text);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - ");
if (args.Length > 0)
Console.Write(string.Format(toFormat, args).Trim());
else
Console.Write(" " + text);
Console.Write(toFormat);
Console.ResetColor();
Console.WriteLine();
}
public static void LogWarn(object text, bool time = true)
{
if (time)
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - ");
Console.ForegroundColor = ConsoleColor.DarkYellow;
if (time)
Console.Write(text);
else
Console.Write(" " + text);
Console.ResetColor();
Console.WriteLine();
}
public static void LogDebug(string toFormat, params object[] args)
{
if (Config.DEBUG_LOG)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]") + " - ");
if (args.Length > 0)
Console.Write(string.Format(toFormat, args).Trim());
else
Console.Write(toFormat);
Console.ResetColor();
Console.WriteLine();
}
}
}
}
}

View File

@@ -7,313 +7,332 @@ using static BBDown.Core.Entity.Entity;
using System.Security.Cryptography;
using BBDown.Core.Entity;
namespace BBDown.Core
namespace BBDown.Core;
public static partial class Parser
{
public partial class Parser
public static string WbiSign(string api)
{
public static string WbiSign(string api)
return $"{api}&w_rid=" + string.Concat(MD5.HashData(Encoding.UTF8.GetBytes(api + Config.WBI)).Select(i => i.ToString("x2")).ToArray());
}
private static async Task<string> GetPlayJsonAsync(string encoding, string aidOri, string aid, string cid, string epId, bool tvApi, bool intl, bool appApi, string qn = "0")
{
LogDebug("aid={0},cid={1},epId={2},tvApi={3},IntlApi={4},appApi={5},qn={6}", aid, cid, epId, tvApi, intl, appApi, qn);
if (intl) return await GetPlayJsonAsync(aid, cid, epId, qn);
bool cheese = aidOri.StartsWith("cheese:");
bool bangumi = cheese || aidOri.StartsWith("ep:");
LogDebug("bangumi={0},cheese={1}", bangumi, cheese);
if (appApi) return await AppHelper.DoReqAsync(aid, cid, epId, qn, bangumi, encoding, Config.TOKEN);
string prefix = tvApi ? bangumi ? "api.snm0516.aisee.tv/pgc/player/api/playurltv" : "api.snm0516.aisee.tv/x/tv/playurl"
: bangumi ? $"{Config.HOST}/pgc/player/web/v2/playurl" : "api.bilibili.com/x/player/wbi/playurl";
prefix = $"https://{prefix}?";
string api;
if (tvApi)
{
return $"{api}&w_rid=" + string.Concat(MD5.HashData(Encoding.UTF8.GetBytes(api + Config.WBI)).Select(i => i.ToString("x2")).ToArray());
StringBuilder apiBuilder = new();
if (Config.TOKEN != "") apiBuilder.Append($"access_key={Config.TOKEN}&");
apiBuilder.Append($"appkey=4409e2ce8ffd12b8&build=106500&cid={cid}&device=android");
if (bangumi) apiBuilder.Append($"&ep_id={epId}&expire=0");
apiBuilder.Append($"&fnval=4048&fnver=0&fourk=1&mid=0&mobi_app=android_tv_yst");
apiBuilder.Append($"&object_id={aid}&platform=android&playurl_type=1&qn={qn}&ts={GetTimeStamp(true)}");
api = $"{prefix}{apiBuilder}&sign={GetSign(apiBuilder.ToString(), false)}";
}
else
{
// 尝试提高可读性
StringBuilder apiBuilder = new();
apiBuilder.Append($"support_multi_audio=true&from_client=BROWSER&avid={aid}&cid={cid}&fnval=4048&fnver=0&fourk=1");
if (Config.AREA != "") apiBuilder.Append($"&access_key={Config.TOKEN}&area={Config.AREA}");
apiBuilder.Append($"&otype=json&qn={qn}");
if (bangumi) apiBuilder.Append($"&module=bangumi&ep_id={epId}&session=");
if (Config.COOKIE == "") apiBuilder.Append("&try_look=1");
apiBuilder.Append($"&wts={GetTimeStamp(true)}");
api = prefix + (bangumi ? apiBuilder.ToString() : WbiSign(apiBuilder.ToString()));
}
private static async Task<string> GetPlayJsonAsync(string encoding, string aidOri, string aid, string cid, string epId, bool tvApi, bool intl, bool appApi, string qn = "0")
//课程接口
if (cheese) api = api.Replace("/pgc/", "/pugv/");
//Console.WriteLine(api);
string webJson = await GetWebSourceAsync(api);
//以下情况从网页源代码尝试解析
if (webJson.Contains("\"大会员专享限制\""))
{
LogDebug("aid={0},cid={1},epId={2},tvApi={3},IntlApi={4},appApi={5},qn={6}", aid, cid, epId, tvApi, intl, appApi, qn);
if (intl) return await GetPlayJsonAsync(aid, cid, epId, qn);
bool cheese = aidOri.StartsWith("cheese:");
bool bangumi = cheese || aidOri.StartsWith("ep:");
LogDebug("bangumi={0},cheese={1}", bangumi, cheese);
if (appApi) return await AppHelper.DoReqAsync(aid, cid, epId, qn, bangumi, encoding, Config.TOKEN);
string prefix = tvApi ? bangumi ? "api.snm0516.aisee.tv/pgc/player/api/playurltv" : "api.snm0516.aisee.tv/x/tv/playurl"
: bangumi ? $"{Config.HOST}/pgc/player/web/v2/playurl" : "api.bilibili.com/x/player/wbi/playurl";
prefix = $"https://{prefix}?";
string api;
if (tvApi)
{
StringBuilder apiBuilder = new();
if (Config.TOKEN != "") apiBuilder.Append($"access_key={Config.TOKEN}&");
apiBuilder.Append($"appkey=4409e2ce8ffd12b8&build=106500&cid={cid}&device=android");
if (bangumi) apiBuilder.Append($"&ep_id={epId}&expire=0");
apiBuilder.Append($"&fnval=4048&fnver=0&fourk=1&mid=0&mobi_app=android_tv_yst");
apiBuilder.Append($"&object_id={aid}&platform=android&playurl_type=1&qn={qn}&ts={GetTimeStamp(true)}");
api = $"{prefix}{apiBuilder}&sign={GetSign(apiBuilder.ToString(), false)}";
}
else
{
// 尝试提高可读性
StringBuilder apiBuilder = new();
apiBuilder.Append($"support_multi_audio=true&from_client=BROWSER&avid={aid}&cid={cid}&fnval=4048&fnver=0&fourk=1");
if (Config.AREA != "") apiBuilder.Append($"&access_key={Config.TOKEN}&area={Config.AREA}");
apiBuilder.Append($"&otype=json&qn={qn}");
if (bangumi) apiBuilder.Append($"&module=bangumi&ep_id={epId}&session=");
if (Config.COOKIE == "") apiBuilder.Append("&try_look=1");
apiBuilder.Append($"&wts={GetTimeStamp(true)}");
api = prefix + (bangumi ? apiBuilder.ToString() : WbiSign(apiBuilder.ToString()));
}
//课程接口
if (cheese) api = api.Replace("/pgc/", "/pugv/");
//Console.WriteLine(api);
string webJson = await GetWebSourceAsync(api);
//以下情况从网页源代码尝试解析
if (webJson.Contains("\"大会员专享限制\""))
{
Log("此视频需要大会员,您大概率需要登录一个有大会员的账号才可以下载,尝试从网页源码解析");
string webUrl = "https://www.bilibili.com/bangumi/play/ep" + epId;
string webSource = await GetWebSourceAsync(webUrl);
webJson = PlayerJsonRegex().Match(webSource).Groups[1].Value;
}
return webJson;
Log("此视频需要大会员,您大概率需要登录一个有大会员的账号才可以下载,尝试从网页源码解析");
string webUrl = "https://www.bilibili.com/bangumi/play/ep" + epId;
string webSource = await GetWebSourceAsync(webUrl);
webJson = PlayerJsonRegex().Match(webSource).Groups[1].Value;
}
return webJson;
}
private static async Task<string> GetPlayJsonAsync(string aid, string cid, string epId, string qn, string code = "0")
{
bool isBiliPlus = Config.HOST != "api.bilibili.com";
string api = $"https://{(isBiliPlus ? Config.HOST : "api.biliintl.com")}/intl/gateway/v2/ogv/playurl?";
private static async Task<string> GetPlayJsonAsync(string aid, string cid, string epId, string qn, string code = "0")
{
bool isBiliPlus = Config.HOST != "api.bilibili.com";
string api = $"https://{(isBiliPlus ? Config.HOST : "api.biliintl.com")}/intl/gateway/v2/ogv/playurl?";
StringBuilder paramBuilder = new();
if (Config.TOKEN != "") paramBuilder.Append($"access_key={Config.TOKEN}&");
paramBuilder.Append($"aid={aid}");
if (isBiliPlus) paramBuilder.Append($"&appkey=7d089525d3611b1c&area={(Config.AREA == "" ? "th" : Config.AREA)}");
paramBuilder.Append($"&cid={cid}&ep_id={epId}&platform=android&prefer_code_type={code}&qn={qn}");
if (isBiliPlus) paramBuilder.Append($"&ts={GetTimeStamp(true)}");
StringBuilder paramBuilder = new();
if (Config.TOKEN != "") paramBuilder.Append($"access_key={Config.TOKEN}&");
paramBuilder.Append($"aid={aid}");
if (isBiliPlus) paramBuilder.Append($"&appkey=7d089525d3611b1c&area={(Config.AREA == "" ? "th" : Config.AREA)}");
paramBuilder.Append($"&cid={cid}&ep_id={epId}&platform=android&prefer_code_type={code}&qn={qn}");
if (isBiliPlus) paramBuilder.Append($"&ts={GetTimeStamp(true)}");
string param = paramBuilder.ToString();
api += (isBiliPlus ? $"{param}&sign={GetSign(param, true)}" : param);
string param = paramBuilder.ToString();
api += (isBiliPlus ? $"{param}&sign={GetSign(param, true)}" : param);
string webJson = await GetWebSourceAsync(api);
return webJson;
}
string webJson = await GetWebSourceAsync(api);
return webJson;
}
public static async Task<ParsedResult> ExtractTracksAsync(string aidOri, string aid, string cid, string epId, bool tvApi, bool intlApi, bool appApi, string encoding, string qn = "0")
{
var intlCode = "0";
ParsedResult parsedResult = new();
public static async Task<ParsedResult> ExtractTracksAsync(string aidOri, string aid, string cid, string epId, bool tvApi, bool intlApi, bool appApi, string encoding, string qn = "0")
{
var intlCode = "0";
ParsedResult parsedResult = new();
//调用解析
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, qn);
//调用解析
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, qn);
LogDebug(parsedResult.WebJsonString);
LogDebug(parsedResult.WebJsonString);
startParsing:
var respJson = JsonDocument.Parse(parsedResult.WebJsonString);
var data = respJson.RootElement;
var respJson = JsonDocument.Parse(parsedResult.WebJsonString);
var data = respJson.RootElement;
//intl接口
if (parsedResult.WebJsonString.Contains("\"stream_list\""))
//intl接口
if (parsedResult.WebJsonString.Contains("\"stream_list\""))
{
int pDur = data.GetProperty("data").GetProperty("video_info").GetProperty("timelength").GetInt32() / 1000;
var audio = data.GetProperty("data").GetProperty("video_info").GetProperty("dash_audio").EnumerateArray().ToList();
foreach (var stream in data.GetProperty("data").GetProperty("video_info").GetProperty("stream_list").EnumerateArray())
{
int pDur = data.GetProperty("data").GetProperty("video_info").GetProperty("timelength").GetInt32() / 1000;
var audio = data.GetProperty("data").GetProperty("video_info").GetProperty("dash_audio").EnumerateArray().ToList();
foreach (var stream in data.GetProperty("data").GetProperty("video_info").GetProperty("stream_list").EnumerateArray())
if (stream.TryGetProperty("dash_video", out JsonElement dashVideo))
{
if (stream.TryGetProperty("dash_video", out JsonElement dashVideo))
if (dashVideo.GetProperty("base_url").ToString() != "")
{
if (dashVideo.GetProperty("base_url").ToString() != "")
{
var videoId = stream.GetProperty("stream_info").GetProperty("quality").ToString();
var urlList = new List<string>() { dashVideo.GetProperty("base_url").ToString() };
urlList.AddRange(dashVideo.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
Video v = new()
{
dur = pDur,
id = videoId,
dfn = Config.qualitys[videoId],
bandwith = Convert.ToInt64(dashVideo.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = GetVideoCodec(dashVideo.GetProperty("codecid").ToString()),
size = dashVideo.TryGetProperty("size", out var sizeNode) ? Convert.ToDouble(sizeNode.ToString()) : 0
};
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
}
}
}
foreach (var node in audio)
{
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
Audio a = new()
{
id = node.GetProperty("id").ToString(),
dfn = node.GetProperty("id").ToString(),
dur = pDur,
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = "M4A"
};
if (!parsedResult.AudioTracks.Contains(a)) parsedResult.AudioTracks.Add(a);
}
if (intlCode == "0")
{
intlCode = "1";
parsedResult.WebJsonString = await GetPlayJsonAsync(aid, cid, epId, qn, intlCode);
goto startParsing;
}
return parsedResult;
}
// data节点一次性判断完
string nodeName = null;
if (parsedResult.WebJsonString.Contains("\"result\":{"))
{
nodeName = "result";
// v2
if (parsedResult.WebJsonString.Contains("\"video_info\":{"))
{
nodeName = "video_info";
}
}
else if (parsedResult.WebJsonString.Contains("\"data\":{")) nodeName = "data";
var root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
bool bangumi = aidOri.StartsWith("ep:");
if (parsedResult.WebJsonString.Contains("\"dash\":{")) //dash
{
List<JsonElement>? audio = null;
List<JsonElement>? video = null;
List<JsonElement>? backgroundAudio = null;
List<JsonElement>? roleAudio = null;
int pDur = 0;
try { pDur = root.GetProperty("dash").GetProperty("duration").GetInt32(); } catch { }
try { pDur = root.GetProperty("timelength").GetInt32() / 1000; } catch { }
bool reParse = false;
reParse:
if (reParse)
{
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, GetMaxQn());
respJson = JsonDocument.Parse(parsedResult.WebJsonString);
data = respJson.RootElement;
root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
}
try { video = root.GetProperty("dash").GetProperty("video").EnumerateArray().ToList(); } catch { }
try { audio = root.GetProperty("dash").GetProperty("audio").EnumerateArray().ToList(); } catch { }
if (appApi && bangumi)
{
try { backgroundAudio = data.GetProperty("dubbing_info").GetProperty("background_audio").EnumerateArray().ToList(); } catch { }
try { roleAudio = data.GetProperty("dubbing_info").GetProperty("role_audio_list").EnumerateArray().ToList(); } catch { }
}
//处理杜比音频
try
{
if (audio != null)
{
if (!tvApi && root.GetProperty("dash").TryGetProperty("dolby", out JsonElement dolby))
{
if (dolby.TryGetProperty("audio", out JsonElement db))
{
audio.AddRange(db.EnumerateArray());
}
}
}
}
catch (Exception) {; }
//处理Hi-Res无损
try
{
if (audio != null)
{
if (!tvApi && root.GetProperty("dash").TryGetProperty("flac", out JsonElement hiRes))
{
if (hiRes.TryGetProperty("audio", out JsonElement db))
{
if (db.ValueKind != JsonValueKind.Null)
audio.Add(db);
}
}
}
}
catch (Exception) {; }
if (video != null)
{
foreach (var node in video)
{
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
if (node.TryGetProperty("backup_url", out JsonElement element) && element.ValueKind != JsonValueKind.Null)
{
urlList.AddRange(element.EnumerateArray().Select(i => i.ToString()));
}
var videoId = node.GetProperty("id").ToString();
var videoId = stream.GetProperty("stream_info").GetProperty("quality").ToString();
var urlList = new List<string>() { dashVideo.GetProperty("base_url").ToString() };
urlList.AddRange(dashVideo.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
Video v = new()
{
dur = pDur,
id = videoId,
dfn = Config.qualitys[videoId],
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
bandwith = Convert.ToInt64(dashVideo.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = GetVideoCodec(node.GetProperty("codecid").ToString()),
size = node.TryGetProperty("size", out var sizeNode) ? Convert.ToDouble(sizeNode.ToString()) : 0
codecs = GetVideoCodec(dashVideo.GetProperty("codecid").ToString()),
size = dashVideo.TryGetProperty("size", out var sizeNode) ? Convert.ToDouble(sizeNode.ToString()) : 0
};
if (!tvApi && !appApi)
{
v.res = node.GetProperty("width").ToString() + "x" + node.GetProperty("height").ToString();
v.fps = node.GetProperty("frame_rate").ToString();
}
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
}
}
}
//此处处理免二压视频,需要单独再请求一次
if (!reParse && !appApi)
foreach (var node in audio)
{
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
Audio a = new()
{
reParse = true;
goto reParse;
}
id = node.GetProperty("id").ToString(),
dfn = node.GetProperty("id").ToString(),
dur = pDur,
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = "M4A"
};
if (!parsedResult.AudioTracks.Contains(a)) parsedResult.AudioTracks.Add(a);
}
if (intlCode == "0")
{
intlCode = "1";
parsedResult.WebJsonString = await GetPlayJsonAsync(aid, cid, epId, qn, intlCode);
goto startParsing;
}
return parsedResult;
}
// data节点一次性判断完
string nodeName = null;
if (parsedResult.WebJsonString.Contains("\"result\":{"))
{
nodeName = "result";
// v2
if (parsedResult.WebJsonString.Contains("\"video_info\":{"))
{
nodeName = "video_info";
}
}
else if (parsedResult.WebJsonString.Contains("\"data\":{")) nodeName = "data";
var root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
bool bangumi = aidOri.StartsWith("ep:");
if (parsedResult.WebJsonString.Contains("\"dash\":{")) //dash
{
List<JsonElement>? audio = null;
List<JsonElement>? video = null;
List<JsonElement>? backgroundAudio = null;
List<JsonElement>? roleAudio = null;
int pDur = 0;
try { pDur = root.GetProperty("dash").GetProperty("duration").GetInt32(); } catch { }
try { pDur = root.GetProperty("timelength").GetInt32() / 1000; } catch { }
bool reParse = false;
reParse:
if (reParse)
{
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, GetMaxQn());
respJson = JsonDocument.Parse(parsedResult.WebJsonString);
data = respJson.RootElement;
root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
}
try { video = root.GetProperty("dash").GetProperty("video").EnumerateArray().ToList(); } catch { }
try { audio = root.GetProperty("dash").GetProperty("audio").EnumerateArray().ToList(); } catch { }
if (appApi && bangumi)
{
try { backgroundAudio = data.GetProperty("dubbing_info").GetProperty("background_audio").EnumerateArray().ToList(); } catch { }
try { roleAudio = data.GetProperty("dubbing_info").GetProperty("role_audio_list").EnumerateArray().ToList(); } catch { }
}
//处理杜比音频
try
{
if (audio != null)
{
foreach (var node in audio)
if (!tvApi && root.GetProperty("dash").TryGetProperty("dolby", out JsonElement dolby))
{
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
if (node.TryGetProperty("backup_url", out JsonElement element) && element.ValueKind != JsonValueKind.Null)
if (dolby.TryGetProperty("audio", out JsonElement db))
{
urlList.AddRange(element.EnumerateArray().Select(i => i.ToString()));
audio.AddRange(db.EnumerateArray());
}
var audioId = node.GetProperty("id").ToString();
var codecs = node.GetProperty("codecs").ToString();
codecs = codecs switch
{
"mp4a.40.2" => "M4A",
"mp4a.40.5" => "M4A",
"ec-3" => "E-AC-3",
"fLaC" => "FLAC",
_ => codecs
};
parsedResult.AudioTracks.Add(new Audio()
{
id = audioId,
dfn = audioId,
dur = pDur,
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = codecs
});
}
}
}
catch (Exception) {; }
if (backgroundAudio != null && roleAudio != null)
//处理Hi-Res无损
try
{
if (audio != null)
{
foreach (var node in backgroundAudio)
if (!tvApi && root.GetProperty("dash").TryGetProperty("flac", out JsonElement hiRes))
{
if (hiRes.TryGetProperty("audio", out JsonElement db))
{
if (db.ValueKind != JsonValueKind.Null)
audio.Add(db);
}
}
}
}
catch (Exception) {; }
if (video != null)
{
foreach (var node in video)
{
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
if (node.TryGetProperty("backup_url", out JsonElement element) && element.ValueKind != JsonValueKind.Null)
{
urlList.AddRange(element.EnumerateArray().Select(i => i.ToString()));
}
var videoId = node.GetProperty("id").ToString();
Video v = new()
{
dur = pDur,
id = videoId,
dfn = Config.qualitys[videoId],
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = GetVideoCodec(node.GetProperty("codecid").ToString()),
size = node.TryGetProperty("size", out var sizeNode) ? Convert.ToDouble(sizeNode.ToString()) : 0
};
if (!tvApi && !appApi)
{
v.res = node.GetProperty("width").ToString() + "x" + node.GetProperty("height").ToString();
v.fps = node.GetProperty("frame_rate").ToString();
}
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
}
}
//此处处理免二压视频,需要单独再请求一次
if (!reParse && !appApi)
{
reParse = true;
goto reParse;
}
if (audio != null)
{
foreach (var node in audio)
{
var urlList = new List<string>() { node.GetProperty("base_url").ToString() };
if (node.TryGetProperty("backup_url", out JsonElement element) && element.ValueKind != JsonValueKind.Null)
{
urlList.AddRange(element.EnumerateArray().Select(i => i.ToString()));
}
var audioId = node.GetProperty("id").ToString();
var codecs = node.GetProperty("codecs").ToString();
codecs = codecs switch
{
"mp4a.40.2" => "M4A",
"mp4a.40.5" => "M4A",
"ec-3" => "E-AC-3",
"fLaC" => "FLAC",
_ => codecs
};
parsedResult.AudioTracks.Add(new Audio()
{
id = audioId,
dfn = audioId,
dur = pDur,
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = codecs
});
}
}
if (backgroundAudio != null && roleAudio != null)
{
foreach (var node in backgroundAudio)
{
var audioId = node.GetProperty("id").ToString();
var urlList = new List<string> { node.GetProperty("base_url").ToString() };
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
parsedResult.BackgroundAudioTracks.Add(new Audio()
{
id = audioId,
dfn = audioId,
dur = pDur,
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = node.GetProperty("codecs").ToString()
});
}
foreach (var role in roleAudio)
{
var roleAudioTracks = new List<Audio>();
foreach (var node in role.GetProperty("audio").EnumerateArray())
{
var audioId = node.GetProperty("id").ToString();
var urlList = new List<string> { node.GetProperty("base_url").ToString() };
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
parsedResult.BackgroundAudioTracks.Add(new Audio()
roleAudioTracks.Add(new Audio()
{
id = audioId,
dfn = audioId,
@@ -323,146 +342,126 @@ namespace BBDown.Core
codecs = node.GetProperty("codecs").ToString()
});
}
foreach (var role in roleAudio)
parsedResult.RoleAudioList.Add(new AudioMaterialInfo()
{
var roleAudioTracks = new List<Audio>();
foreach (var node in role.GetProperty("audio").EnumerateArray())
{
var audioId = node.GetProperty("id").ToString();
var urlList = new List<string> { node.GetProperty("base_url").ToString() };
urlList.AddRange(node.GetProperty("backup_url").EnumerateArray().Select(i => i.ToString()));
roleAudioTracks.Add(new Audio()
{
id = audioId,
dfn = audioId,
dur = pDur,
bandwith = Convert.ToInt64(node.GetProperty("bandwidth").ToString()) / 1000,
baseUrl = urlList.FirstOrDefault(i => !BaseUrlRegex().IsMatch(i), urlList.First()),
codecs = node.GetProperty("codecs").ToString()
});
}
parsedResult.RoleAudioList.Add(new AudioMaterialInfo()
{
title = role.GetProperty("title").ToString(),
personName = role.GetProperty("person_name").ToString(),
path = $"{aid}/{aid}.{cid}.{role.GetProperty("audio_id").ToString()}.m4a",
audio = roleAudioTracks
});
}
title = role.GetProperty("title").ToString(),
personName = role.GetProperty("person_name").ToString(),
path = $"{aid}/{aid}.{cid}.{role.GetProperty("audio_id").ToString()}.m4a",
audio = roleAudioTracks
});
}
}
else if (parsedResult.WebJsonString.Contains("\"durl\":[")) //flv
}
else if (parsedResult.WebJsonString.Contains("\"durl\":[")) //flv
{
//默认以最高清晰度解析
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, GetMaxQn());
data = JsonDocument.Parse(parsedResult.WebJsonString).RootElement;
root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
string quality = "";
string videoCodecid = "";
string url = "";
double size = 0;
double length = 0;
quality = root.GetProperty("quality").ToString();
videoCodecid = root.GetProperty("video_codecid").ToString();
//获取所有分段
foreach (var node in root.GetProperty("durl").EnumerateArray())
{
//默认以最高清晰度解析
parsedResult.WebJsonString = await GetPlayJsonAsync(encoding, aidOri, aid, cid, epId, tvApi, intlApi, appApi, GetMaxQn());
data = JsonDocument.Parse(parsedResult.WebJsonString).RootElement;
root = nodeName == null ? data : nodeName == "video_info" ? data.GetProperty("result").GetProperty(nodeName) : data.GetProperty(nodeName);
string quality = "";
string videoCodecid = "";
string url = "";
double size = 0;
double length = 0;
quality = root.GetProperty("quality").ToString();
videoCodecid = root.GetProperty("video_codecid").ToString();
//获取所有分段
foreach (var node in root.GetProperty("durl").EnumerateArray())
{
parsedResult.Clips.Add(node.GetProperty("url").ToString());
size += node.GetProperty("size").GetDouble();
length += node.GetProperty("length").GetDouble();
}
//TV模式可用清晰度
if (root.TryGetProperty("qn_extras", out JsonElement qnExtras))
{
parsedResult.Dfns.AddRange(qnExtras.EnumerateArray().Select(node => node.GetProperty("qn").ToString()));
}
else if (root.TryGetProperty("accept_quality", out JsonElement acceptQuality)) //非tv模式可用清晰度
{
parsedResult.Dfns.AddRange(acceptQuality.EnumerateArray()
.Select(node => node.ToString())
.Where(_qn => !string.IsNullOrEmpty(_qn)));
}
Video v = new()
{
id = quality,
dfn = Config.qualitys[quality],
baseUrl = url,
codecs = GetVideoCodec(videoCodecid),
dur = (int)length / 1000,
size = size
};
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
parsedResult.Clips.Add(node.GetProperty("url").ToString());
size += node.GetProperty("size").GetDouble();
length += node.GetProperty("length").GetDouble();
}
//TV模式可用清晰度
if (root.TryGetProperty("qn_extras", out JsonElement qnExtras))
{
parsedResult.Dfns.AddRange(qnExtras.EnumerateArray().Select(node => node.GetProperty("qn").ToString()));
}
else if (root.TryGetProperty("accept_quality", out JsonElement acceptQuality)) //非tv模式可用清晰度
{
parsedResult.Dfns.AddRange(acceptQuality.EnumerateArray()
.Select(node => node.ToString())
.Where(_qn => !string.IsNullOrEmpty(_qn)));
}
// 番剧片头片尾转分段信息, 预计效果: 正片? -> 片头 -> 正片 -> 片尾
if (bangumi)
Video v = new()
{
if (root.TryGetProperty("clip_info_list", out JsonElement clipList))
{
parsedResult.ExtraPoints.AddRange(clipList.EnumerateArray().Select(clip => new ViewPoint()
id = quality,
dfn = Config.qualitys[quality],
baseUrl = url,
codecs = GetVideoCodec(videoCodecid),
dur = (int)length / 1000,
size = size
};
if (!parsedResult.VideoTracks.Contains(v)) parsedResult.VideoTracks.Add(v);
}
// 番剧片头片尾转分段信息, 预计效果: 正片? -> 片头 -> 正片 -> 片尾
if (bangumi)
{
if (root.TryGetProperty("clip_info_list", out JsonElement clipList))
{
parsedResult.ExtraPoints.AddRange(clipList.EnumerateArray().Select(clip => new ViewPoint()
{
title = clip.GetProperty("toastText").ToString().Replace("即将跳过", ""),
start = clip.GetProperty("start").GetInt32(),
end = clip.GetProperty("end").GetInt32()
})
);
parsedResult.ExtraPoints.Sort((p1, p2) => p1.start.CompareTo(p2.start));
var newPoints = new List<ViewPoint>();
int lastEnd = 0;
foreach (var point in parsedResult.ExtraPoints)
{
if (lastEnd < point.start)
newPoints.Add(new ViewPoint() { title = "正片", start = lastEnd, end = point.start });
newPoints.Add(point);
lastEnd = point.end;
}
parsedResult.ExtraPoints = newPoints;
);
parsedResult.ExtraPoints.Sort((p1, p2) => p1.start.CompareTo(p2.start));
var newPoints = new List<ViewPoint>();
int lastEnd = 0;
foreach (var point in parsedResult.ExtraPoints)
{
if (lastEnd < point.start)
newPoints.Add(new ViewPoint() { title = "正片", start = lastEnd, end = point.start });
newPoints.Add(point);
lastEnd = point.end;
}
parsedResult.ExtraPoints = newPoints;
}
return parsedResult;
}
/// <summary>
/// 编码转换
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
private static string GetVideoCodec(string code)
{
return code switch
{
"13" => "AV1",
"12" => "HEVC",
"7" => "AVC",
_ => "UNKNOWN"
};
}
private static string GetMaxQn()
{
return Config.qualitys.Keys.First();
}
private static string GetTimeStamp(bool bflag)
{
DateTimeOffset ts = DateTimeOffset.Now;
return bflag ? ts.ToUnixTimeSeconds().ToString() : ts.ToUnixTimeMilliseconds().ToString();
}
private static string GetSign(string parms, bool isBiliPlus)
{
string toEncode = parms + (isBiliPlus ? "acd495b248ec528c2eed1e862d393126" : "59b43e04ad6965f34319062b478f83dd");
return string.Concat(MD5.HashData(Encoding.UTF8.GetBytes(toEncode)).Select(i => i.ToString("x2")).ToArray());
}
[GeneratedRegex("window.__playinfo__=([\\s\\S]*?)<\\/script>")]
private static partial Regex PlayerJsonRegex();
[GeneratedRegex("http.*:\\d+")]
private static partial Regex BaseUrlRegex();
return parsedResult;
}
}
/// <summary>
/// 编码转换
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
private static string GetVideoCodec(string code)
{
return code switch
{
"13" => "AV1",
"12" => "HEVC",
"7" => "AVC",
_ => "UNKNOWN"
};
}
private static string GetMaxQn()
{
return Config.qualitys.Keys.First();
}
private static string GetTimeStamp(bool bflag)
{
DateTimeOffset ts = DateTimeOffset.Now;
return bflag ? ts.ToUnixTimeSeconds().ToString() : ts.ToUnixTimeMilliseconds().ToString();
}
private static string GetSign(string parms, bool isBiliPlus)
{
string toEncode = parms + (isBiliPlus ? "acd495b248ec528c2eed1e862d393126" : "59b43e04ad6965f34319062b478f83dd");
return string.Concat(MD5.HashData(Encoding.UTF8.GetBytes(toEncode)).Select(i => i.ToString("x2")).ToArray());
}
[GeneratedRegex("window.__playinfo__=([\\s\\S]*?)<\\/script>")]
private static partial Regex PlayerJsonRegex();
[GeneratedRegex("http.*:\\d+")]
private static partial Regex BaseUrlRegex();
}

View File

@@ -1,76 +1,74 @@
using System;
using System.Text;
using System.Text;
namespace BBDown.Core.Util
namespace BBDown.Core.Util;
//code from: https://github.com/Colerar/abv/blob/main/src/lib.rs
public static class BilibiliBvConverter
{
//code from: https://github.com/Colerar/abv/blob/main/src/lib.rs
public static class BilibiliBvConverter
private const long XOR_CODE = 23442827791579L;
private const long MASK_CODE = (1L << 51) - 1;
private const long MAX_AID = MASK_CODE + 1;
private const long MIN_AID = 1L;
private const long BASE = 58L;
private const byte BV_LEN = 9;
private static readonly byte[] ALPHABET = Encoding.ASCII.GetBytes("FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf");
private static readonly Dictionary<byte, long> REV_ALPHABETA = new Dictionary<byte, long>();
static BilibiliBvConverter()
{
private const long XOR_CODE = 23442827791579L;
private const long MASK_CODE = (1L << 51) - 1;
private const long MAX_AID = MASK_CODE + 1;
private const long MIN_AID = 1L;
private const long BASE = 58L;
private const byte BV_LEN = 9;
private static readonly byte[] ALPHABET = Encoding.ASCII.GetBytes("FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf");
private static readonly Dictionary<byte, long> REV_ALPHABETA = new Dictionary<byte, long>();
static BilibiliBvConverter()
for (byte i = 0; i < ALPHABET.Length; i++)
{
for (byte i = 0; i < ALPHABET.Length; i++)
{
REV_ALPHABETA[ALPHABET[i]] = i;
}
}
public static string Encode(long avid)
{
if (avid < MIN_AID)
{
throw new Exception($"Av {avid} is smaller than {MIN_AID}");
}
if (avid >= MAX_AID)
{
throw new Exception($"Av {avid} is bigger than {MAX_AID}");
}
var bvid = new byte[BV_LEN];
long tmp = (MAX_AID | avid) ^ XOR_CODE;
for (byte i = BV_LEN - 1; tmp != 0; i--)
{
bvid[i] = ALPHABET[tmp % BASE];
tmp /= BASE;
}
(bvid[0], bvid[6]) = (bvid[6], bvid[0]);
(bvid[1], bvid[4]) = (bvid[4], bvid[1]);
return "BV1" + Encoding.ASCII.GetString(bvid);
}
public static long Decode(string bvid_str)
{
if (bvid_str.Length != BV_LEN)
{
throw new Exception($"Bv BV1{bvid_str} must to be 12 char");
}
byte[] bvid = Encoding.ASCII.GetBytes(bvid_str);
(bvid[0], bvid[6]) = (bvid[6], bvid[0]);
(bvid[1], bvid[4]) = (bvid[4], bvid[1]);
long avid = 0;
foreach (byte b in bvid)
{
avid = avid * BASE + REV_ALPHABETA[b];
}
return (avid & MASK_CODE) ^ XOR_CODE;
REV_ALPHABETA[ALPHABET[i]] = i;
}
}
}
public static string Encode(long avid)
{
if (avid < MIN_AID)
{
throw new Exception($"Av {avid} is smaller than {MIN_AID}");
}
if (avid >= MAX_AID)
{
throw new Exception($"Av {avid} is bigger than {MAX_AID}");
}
var bvid = new byte[BV_LEN];
long tmp = (MAX_AID | avid) ^ XOR_CODE;
for (byte i = BV_LEN - 1; tmp != 0; i--)
{
bvid[i] = ALPHABET[tmp % BASE];
tmp /= BASE;
}
(bvid[0], bvid[6]) = (bvid[6], bvid[0]);
(bvid[1], bvid[4]) = (bvid[4], bvid[1]);
return "BV1" + Encoding.ASCII.GetString(bvid);
}
public static long Decode(string bvid_str)
{
if (bvid_str.Length != BV_LEN)
{
throw new Exception($"Bv BV1{bvid_str} must to be 12 char");
}
byte[] bvid = Encoding.ASCII.GetBytes(bvid_str);
(bvid[0], bvid[6]) = (bvid[6], bvid[0]);
(bvid[1], bvid[4]) = (bvid[4], bvid[1]);
long avid = 0;
foreach (byte b in bvid)
{
avid = avid * BASE + REV_ALPHABETA[b];
}
return (avid & MASK_CODE) ^ XOR_CODE;
}
}

View File

@@ -1,105 +1,103 @@
using System;
using System.Net;
using System.Net;
using System.Net.Http.Headers;
using static BBDown.Core.Logger;
namespace BBDown.Core.Util
{
public class HTTPUtil
{
namespace BBDown.Core.Util;
public static readonly HttpClient AppHttpClient = new(new HttpClientHandler
public static class HTTPUtil
{
public static readonly HttpClient AppHttpClient = new(new HttpClientHandler
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.All,
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
})
{
Timeout = TimeSpan.FromMinutes(2)
};
private static readonly Random random = new Random();
private static readonly string[] platforms = { "Windows NT 10.0; Win64", "Macintosh; Intel Mac OS X 10_15", "X11; Linux x86_64" };
private static string RandomVersion(int min, int max)
{
double version = random.NextDouble() * (max - min) + min;
return version.ToString("F3");
}
private static string GetRandomUserAgent()
{
string[] browsers = { $"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{RandomVersion(80, 110)} Safari/537.36", $"Gecko/20100101 Firefox/{RandomVersion(80, 110)}" };
return $"Mozilla/5.0 ({platforms[random.Next(platforms.Length)]}) {browsers[random.Next(browsers.Length)]}";
}
public static string UserAgent { get; set; } = GetRandomUserAgent();
public static async Task<string> GetWebSourceAsync(string url)
{
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);
webRequest.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
webRequest.Headers.TryAddWithoutValidation("Cookie", (url.Contains("/ep") || url.Contains("/ss")) ? Config.COOKIE + ";CURRENT_FNVAL=4048;" : Config.COOKIE);
if (url.Contains("api.bilibili.com"))
webRequest.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com/");
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
webRequest.Headers.Connection.Clear();
LogDebug("获取网页内容: Url: {0}, Headers: {1}", url, webRequest.Headers);
var webResponse = (await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
string htmlCode = await webResponse.Content.ReadAsStringAsync();
LogDebug("Response: {0}", htmlCode);
return htmlCode;
}
// 重写重定向处理, 自动跟随多次重定向
public static async Task<string> GetWebLocationAsync(string url)
{
using var webRequest = new HttpRequestMessage(HttpMethod.Head, url);
webRequest.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
webRequest.Headers.Connection.Clear();
LogDebug("获取网页重定向地址: Url: {0}, Headers: {1}", url, webRequest.Headers);
var webResponse = (await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
string location = webResponse.RequestMessage.RequestUri.AbsoluteUri;
LogDebug("Location: {0}", location);
return location;
}
public static async Task<byte[]> GetPostResponseAsync(string Url, byte[] postData, Dictionary<string, string> headers = null)
{
LogDebug("Post to: {0}, data: {1}", Url, Convert.ToBase64String(postData));
ByteArrayContent content = new(postData);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/grpc");
HttpRequestMessage request = new()
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.All,
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
})
{
Timeout = TimeSpan.FromMinutes(2)
RequestUri = new Uri(Url),
Method = HttpMethod.Post,
Content = content,
//Version = HttpVersion.Version20
};
private static readonly Random random = new Random();
private static readonly string[] platforms = { "Windows NT 10.0; Win64", "Macintosh; Intel Mac OS X 10_15", "X11; Linux x86_64" };
private static string RandomVersion(int min, int max)
if (headers != null)
{
double version = random.NextDouble() * (max - min) + min;
return version.ToString("F3");
foreach (KeyValuePair<string, string> header in headers)
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
else
{
request.Headers.TryAddWithoutValidation("User-Agent", "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.TryAddWithoutValidation("grpc-encoding", "gzip");
}
private static string GetRandomUserAgent()
{
string[] browsers = { $"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{RandomVersion(80, 110)} Safari/537.36", $"Gecko/20100101 Firefox/{RandomVersion(80, 110)}" };
return $"Mozilla/5.0 ({platforms[random.Next(platforms.Length)]}) {browsers[random.Next(browsers.Length)]}";
}
HttpResponseMessage response = await AppHttpClient.SendAsync(request);
byte[] bytes = await response.Content.ReadAsByteArrayAsync();
public static string UserAgent { get; set; } = GetRandomUserAgent();
public static async Task<string> GetWebSourceAsync(string url)
{
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);
webRequest.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
webRequest.Headers.TryAddWithoutValidation("Cookie", (url.Contains("/ep") || url.Contains("/ss")) ? Config.COOKIE + ";CURRENT_FNVAL=4048;" : Config.COOKIE);
if (url.Contains("api.bilibili.com"))
webRequest.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com/");
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
webRequest.Headers.Connection.Clear();
LogDebug("获取网页内容: Url: {0}, Headers: {1}", url, webRequest.Headers);
var webResponse = (await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
string htmlCode = await webResponse.Content.ReadAsStringAsync();
LogDebug("Response: {0}", htmlCode);
return htmlCode;
}
// 重写重定向处理, 自动跟随多次重定向
public static async Task<string> GetWebLocationAsync(string url)
{
using var webRequest = new HttpRequestMessage(HttpMethod.Head, url);
webRequest.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
webRequest.Headers.Connection.Clear();
LogDebug("获取网页重定向地址: Url: {0}, Headers: {1}", url, webRequest.Headers);
var webResponse = (await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
string location = webResponse.RequestMessage.RequestUri.AbsoluteUri;
LogDebug("Location: {0}", location);
return location;
}
public static async Task<byte[]> GetPostResponseAsync(string Url, byte[] postData, Dictionary<string, string> headers = null)
{
LogDebug("Post to: {0}, data: {1}", Url, Convert.ToBase64String(postData));
ByteArrayContent content = new(postData);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/grpc");
HttpRequestMessage request = new()
{
RequestUri = new Uri(Url),
Method = HttpMethod.Post,
Content = content,
//Version = HttpVersion.Version20
};
if (headers != null)
{
foreach (KeyValuePair<string, string> header in headers)
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
else
{
request.Headers.TryAddWithoutValidation("User-Agent", "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.TryAddWithoutValidation("grpc-encoding", "gzip");
}
HttpResponseMessage response = await AppHttpClient.SendAsync(request);
byte[] bytes = await response.Content.ReadAsByteArrayAsync();
return bytes;
}
return bytes;
}
}
}

View File

@@ -6,480 +6,479 @@ using static BBDown.Core.Util.HTTPUtil;
using System.Text.RegularExpressions;
using System.Text.Json;
namespace BBDown.Core.Util
namespace BBDown.Core.Util;
public static partial class SubUtil
{
public partial class SubUtil
//https://i0.hdslb.com/bfs/subtitle/subtitle_lan.json
public static (string, string) GetSubtitleCode(string key)
{
//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)
{
//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")
};
var v = result.Value;
key = key.Replace(v, v.ToUpper());
}
#region
private static async Task<List<Subtitle>?> GetIntlSubtitlesFromApi1Async(string aid, string cid, string epId, int index)
return key switch
{
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")}"
};
"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")
};
}
//有空的URL 不合法
if (subtitles.Any(s => string.IsNullOrEmpty(s.url)))
throw new Exception("Bad url");
#region
subtitles.Add(subtitle);
}
return subtitles;
}
catch (Exception)
{
return null;
}
}
private static async Task<List<Subtitle>?> GetIntlSubtitlesFromApi2Async(string aid, string cid, string epId, int index)
private static async Task<List<Subtitle>?> GetIntlSubtitlesFromApi1Async(string aid, string cid, string epId, int index)
{
try
{
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)
{
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("lang_key").ToString();
var url = sub.GetProperty("url").ToString();
Subtitle subtitle = new()
{
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 = 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");
//无字幕片源 但是字幕没上导致的空列表,尝试从国际接口获取
//if (subtitles.Count == 0 && !string.IsNullOrEmpty(epId))
//{
// return await GetSubtitlesAsync(aid, cid, epId, true);
//}
return subtitles;
}
catch (Exception)
{
return null;
subtitles.Add(subtitle);
}
return subtitles;
}
private static async Task<List<Subtitle>?> GetSubtitlesFromApi2Async(string aid, string cid, string epId, int index)
catch (Exception)
{
try
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)
{
List<Subtitle> subtitles = new();
string api = $"https://api.bilibili.com/x/player/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("key").ToString();
var url = sub.GetProperty("url").ToString().Replace("\\\\/", "/");
Subtitle subtitle = new()
{
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 = 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");
return subtitles;
}
catch (Exception)
{
return null;
subtitles.Add(subtitle);
}
return subtitles;
}
private static byte[] GetPayload(long aid, long cid)
catch (Exception)
{
var obj = new DmViewReq
{
Pid = aid,
Oid = cid,
Type = 1,
Spmid = "main.ugc-video-detail.0.0",
};
return AppHelper.PackMessage(obj.ToByteArray());
return null;
}
}
private static async Task<List<Subtitle>?> GetSubtitlesFromApi3Async(string aid, string cid, string epId, int index)
private static async Task<List<Subtitle>?> GetSubtitlesFromApi1Async(string aid, string cid, string epId, int index)
{
try
{
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)
{
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)
var lan = sub.GetProperty("lan").ToString();
Subtitle subtitle = new()
{
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");
url = sub.GetProperty("subtitle_url").ToString(),
lan = lan,
path = $"{aid}/{aid}.{cid}.{lan}.srt"
};
subtitles.Add(subtitle);
}
return subtitles;
}
catch (Exception)
{
return null;
}
//有空的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;
}
#endregion
public static async Task<List<Subtitle>> GetSubtitlesAsync(string aid, string cid, string epId, int index, bool intl)
catch (Exception)
{
List<Subtitle>? subtitles = new();
if (intl)
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/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)
{
subtitles = await GetIntlSubtitlesFromApi1Async(aid, cid, epId, index) ?? await GetIntlSubtitlesFromApi2Async(aid, cid, epId, index);
}
else
{
if (Config.COOKIE == "")
var lan = sub.GetProperty("lan").ToString();
Subtitle subtitle = new()
{
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);
}
url = sub.GetProperty("subtitle_url").ToString(),
lan = lan,
path = $"{aid}/{aid}.{cid}.{lan}.srt"
};
subtitles.Add(subtitle);
}
if (subtitles == null)
{
return new List<Subtitle>(); //返回空列表
}
//修正 url 协议
foreach (var item in subtitles)
{
if (item.url.StartsWith("//")) item.url = "https:" + item.url;
}
//有空的URL 不合法
if (subtitles.Any(s => string.IsNullOrEmpty(s.url)))
throw new Exception("Bad url");
return subtitles;
}
public static async Task SaveSubtitleAsync(string url, string path)
catch (Exception)
{
if (path.EndsWith(".srt"))
await File.WriteAllTextAsync(path, ConvertSubFromJson(await GetWebSourceAsync(url)), Encoding.UTF8);
else
await File.WriteAllTextAsync(path, await GetWebSourceAsync(url), Encoding.UTF8);
return null;
}
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();
}
}
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();
}

View File

@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
@@ -19,8 +16,8 @@ namespace BBDown;
public class BBDownApiServer
{
private WebApplication? app;
private List<DownloadTask> runningTasks = [];
private List<DownloadTask> finishedTasks = [];
private readonly List<DownloadTask> runningTasks = [];
private readonly List<DownloadTask> finishedTasks = [];
public void SetUpServer()
{
@@ -55,10 +52,7 @@ public class BBDownApiServer
{
return Results.NotFound();
}
else
{
return Results.Json(task, AppJsonSerializerContext.Default.DownloadTask);
}
return Results.Json(task, AppJsonSerializerContext.Default.DownloadTask);
});
app.MapPost("/add-task", (MyOptionBindingResult<MyOption> bindingResult) =>
{
@@ -68,7 +62,7 @@ public class BBDownApiServer
return Results.BadRequest("输入有误");
}
var req = bindingResult.Result;
AddDownloadTaskAsync(req);
_ = AddDownloadTaskAsync(req);
return Results.Ok();
});
var finishedRemovalApi = app.MapGroup("remove-finished");

View File

@@ -2,32 +2,31 @@
using System.IO;
using System.Threading.Tasks;
namespace BBDown
namespace BBDown;
static class BBDownAria2c
{
class BBDownAria2c
public static string ARIA2C = "aria2c";
public static async Task<int> RunCommandCodeAsync(string command, string args)
{
public static string ARIA2C = "aria2c";
public static async Task<int> RunCommandCodeAsync(string command, string args)
{
using Process p = new();
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = false;
p.StartInfo.FileName = command;
p.StartInfo.Arguments = args;
p.Start();
await p.WaitForExitAsync();
return p.ExitCode;
}
public static async Task DownloadFileByAria2cAsync(string url, string path, string extraArgs)
{
var headerArgs = "";
if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android"))
headerArgs += " --header=\"Referer: https://www.bilibili.com\"";
headerArgs += " --header=\"User-Agent: Mozilla/5.0\"";
headerArgs += $" --header=\"Cookie: {Core.Config.COOKIE}\"";
await RunCommandCodeAsync(ARIA2C, $" --auto-file-renaming=false --download-result=hide --allow-overwrite=true --console-log-level=warn -x16 -s16 -j16 -k5M {headerArgs} {extraArgs} \"{url}\" -d \"{Path.GetDirectoryName(path)}\" -o \"{Path.GetFileName(path)}\"");
}
using Process p = new();
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = false;
p.StartInfo.FileName = command;
p.StartInfo.Arguments = args;
p.Start();
await p.WaitForExitAsync();
return p.ExitCode;
}
}
public static async Task DownloadFileByAria2cAsync(string url, string path, string extraArgs)
{
var headerArgs = "";
if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android"))
headerArgs += " --header=\"Referer: https://www.bilibili.com\"";
headerArgs += " --header=\"User-Agent: Mozilla/5.0\"";
headerArgs += $" --header=\"Cookie: {Core.Config.COOKIE}\"";
await RunCommandCodeAsync(ARIA2C, $" --auto-file-renaming=false --download-result=hide --allow-overwrite=true --console-log-level=warn -x16 -s16 -j16 -k5M {headerArgs} {extraArgs} \"{url}\" -d \"{Path.GetDirectoryName(path)}\" -o \"{Path.GetFileName(path)}\"");
}
}

View File

@@ -5,62 +5,56 @@ using System.CommandLine;
using System.IO;
using System.Linq;
using static BBDown.Core.Logger;
using System.Text;
using System.Threading.Tasks;
namespace BBDown
namespace BBDown;
internal static class BBDownConfigParser
{
internal class BBDownConfigParser
public static void HandleConfig(List<string> newArgsList, RootCommand rootCommand)
{
public static void HandleConfig(List<string> newArgsList, RootCommand rootCommand)
try
{
try
var configPath = newArgsList.Contains("--config-file")
? newArgsList.ElementAt(newArgsList.IndexOf("--config-file") + 1)
: Path.Combine(Program.APP_DIR, "BBDown.config");
if (File.Exists(configPath))
{
var configPath = newArgsList.Contains("--config-file")
? newArgsList.ElementAt(newArgsList.IndexOf("--config-file") + 1)
: Path.Combine(Program.APP_DIR, "BBDown.config");
if (File.Exists(configPath))
{
Log($"加载配置文件: {configPath}");
var configArgs = File
.ReadAllLines(configPath)
.Where(s => !string.IsNullOrEmpty(s) && !s.StartsWith("#"))
.SelectMany(s =>
Log($"加载配置文件: {configPath}");
var configArgs = File
.ReadAllLines(configPath)
.Where(s => !string.IsNullOrEmpty(s) && !s.StartsWith('#'))
.SelectMany(s =>
{
var trimLine = s.Trim();
if (trimLine.IndexOf('-') == 0 && trimLine.IndexOf(' ') != -1)
if (trimLine.StartsWith('-') && trimLine.Contains(' '))
{
var spaceIndex = trimLine.IndexOf(' ');
var paramsGroup = new string[] { trimLine[..spaceIndex], trimLine[spaceIndex..] };
var paramsGroup = new[] { trimLine[..spaceIndex], trimLine[spaceIndex..] };
return paramsGroup.Where(s => !string.IsNullOrEmpty(s)).Select(s => s.Trim(' ').Trim('\"'));
}
else
{
return new string[] { trimLine.Trim('\"') };
}
return [trimLine.Trim('\"')];
}
);
var configArgsResult = rootCommand.Parse(configArgs.ToArray());
foreach (var item in configArgsResult.CommandResult.Children)
);
var configArgsResult = rootCommand.Parse(configArgs.ToArray());
foreach (var item in configArgsResult.CommandResult.Children)
{
if (item is OptionResult o)
{
if (item is OptionResult o)
if (!newArgsList.Contains("--" + o.Option.Name))
{
if (!newArgsList.Contains("--" + o.Option.Name))
{
newArgsList.Add("--" + o.Option.Name);
newArgsList.AddRange(o.Tokens.Select(t => t.Value));
}
newArgsList.Add("--" + o.Option.Name);
newArgsList.AddRange(o.Tokens.Select(t => t.Value));
}
}
//命令行的优先级>配置文件优先级
LogDebug("新的命令行参数: " + string.Join(" ", newArgsList));
}
}
catch (Exception)
{
LogError("配置文件读取异常,忽略");
//命令行的优先级>配置文件优先级
LogDebug("新的命令行参数: " + string.Join(" ", newArgsList));
}
}
catch (Exception)
{
LogError("配置文件读取异常,忽略");
}
}
}
}

View File

@@ -4,221 +4,217 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using static BBDown.Core.Entity.Entity;
using static BBDown.Core.Logger;
using static BBDown.Core.Util.HTTPUtil;
using System.Collections.Concurrent;
namespace BBDown
namespace BBDown;
internal static class BBDownDownloadUtil
{
internal class BBDownDownloadUtil
public class DownloadConfig
{
public class DownloadConfig
public bool UseAria2c { get; set; } = false;
public string Aria2cArgs { get; set; } = string.Empty;
public bool ForceHttp { get; set; } = false;
public bool MultiThread { get; set; } = false;
public DownloadTask? RelatedTask { get; set; } = null;
}
private static async Task RangeDownloadToTmpAsync(int id, string url, string tmpName, long fromPosition, long? toPosition, Action<int, long, long> onProgress, bool failOnRangeNotSupported = false)
{
DateTimeOffset? lastTime = File.Exists(tmpName) ? new FileInfo(tmpName).LastWriteTimeUtc : null;
using var fileStream = new FileStream(tmpName, FileMode.Create);
fileStream.Seek(0, SeekOrigin.End);
var downloadedBytes = fromPosition + fileStream.Position;
using var httpRequestMessage = new HttpRequestMessage();
if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android"))
httpRequestMessage.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com");
httpRequestMessage.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
httpRequestMessage.Headers.TryAddWithoutValidation("Cookie", Core.Config.COOKIE);
httpRequestMessage.Headers.Range = new(downloadedBytes, toPosition);
httpRequestMessage.Headers.IfRange = lastTime != null ? new(lastTime.Value) : null;
httpRequestMessage.RequestUri = new(url);
using var response = (await AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
if (response.StatusCode == HttpStatusCode.OK) // server doesn't response a partial content
{
public bool UseAria2c { get; set; } = false;
public string Aria2cArgs { get; set; } = string.Empty;
public bool ForceHttp { get; set; } = false;
public bool MultiThread { get; set; } = false;
public DownloadTask? RelatedTask { get; set; } = null;
if (failOnRangeNotSupported && (downloadedBytes > 0 || toPosition != null)) throw new NotSupportedException("Range request is not supported.");
downloadedBytes = 0;
fileStream.Seek(0, SeekOrigin.Begin);
}
private static async Task RangeDownloadToTmpAsync(int id, string url, string tmpName, long fromPosition, long? toPosition, Action<int, long, long> onProgress, bool failOnRangeNotSupported = false)
using var stream = await response.Content.ReadAsStreamAsync();
var totalBytes = downloadedBytes + (response.Content.Headers.ContentLength ?? long.MaxValue - downloadedBytes);
const int blockSize = 1048576 / 4;
var buffer = new byte[blockSize];
while (downloadedBytes < totalBytes)
{
DateTimeOffset? lastTime = File.Exists(tmpName) ? new FileInfo(tmpName).LastWriteTimeUtc : null;
using var fileStream = new FileStream(tmpName, FileMode.Create);
fileStream.Seek(0, SeekOrigin.End);
var downloadedBytes = fromPosition + fileStream.Position;
using var httpRequestMessage = new HttpRequestMessage();
if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android"))
httpRequestMessage.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com");
httpRequestMessage.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
httpRequestMessage.Headers.TryAddWithoutValidation("Cookie", Core.Config.COOKIE);
httpRequestMessage.Headers.Range = new(downloadedBytes, toPosition);
httpRequestMessage.Headers.IfRange = lastTime != null ? new(lastTime.Value) : null;
httpRequestMessage.RequestUri = new(url);
using var response = (await AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
if (response.StatusCode == HttpStatusCode.OK) // server doesn't response a partial content
{
if (failOnRangeNotSupported && (downloadedBytes > 0 || toPosition != null)) throw new NotSupportedException("Range request is not supported.");
downloadedBytes = 0;
fileStream.Seek(0, SeekOrigin.Begin);
}
using var stream = await response.Content.ReadAsStreamAsync();
var totalBytes = downloadedBytes + (response.Content.Headers.ContentLength ?? long.MaxValue - downloadedBytes);
const int blockSize = 1048576 / 4;
var buffer = new byte[blockSize];
while (downloadedBytes < totalBytes)
{
var recevied = await stream.ReadAsync(buffer);
if (recevied == 0) break;
await fileStream.WriteAsync(buffer.AsMemory(0, recevied));
await fileStream.FlushAsync();
downloadedBytes += recevied;
onProgress(id, downloadedBytes - fromPosition, totalBytes);
}
if (response.Content.Headers.ContentLength != null && (response.Content.Headers.ContentLength != new FileInfo(tmpName).Length))
throw new Exception("Retry...");
var recevied = await stream.ReadAsync(buffer);
if (recevied == 0) break;
await fileStream.WriteAsync(buffer.AsMemory(0, recevied));
await fileStream.FlushAsync();
downloadedBytes += recevied;
onProgress(id, downloadedBytes - fromPosition, totalBytes);
}
public static async Task DownloadFile(string url, string path, DownloadConfig config)
if (response.Content.Headers.ContentLength != null && (response.Content.Headers.ContentLength != new FileInfo(tmpName).Length))
throw new Exception("Retry...");
}
public static async Task DownloadFile(string url, string path, DownloadConfig config)
{
if (string.IsNullOrEmpty(url)) return;
if (config.ForceHttp) url = ReplaceUrl(url);
LogDebug("Start downloading: {0}", url);
string desDir = Path.GetDirectoryName(path)!;
if (!string.IsNullOrEmpty(desDir) && !Directory.Exists(desDir)) Directory.CreateDirectory(desDir);
if (config.UseAria2c)
{
if (string.IsNullOrEmpty(url)) return;
if (config.ForceHttp) url = ReplaceUrl(url);
LogDebug("Start downloading: {0}", url);
string desDir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(desDir) && !Directory.Exists(desDir)) Directory.CreateDirectory(desDir);
if (config.UseAria2c)
{
await BBDownAria2c.DownloadFileByAria2cAsync(url, path, config.Aria2cArgs);
if (File.Exists(path + ".aria2") || !File.Exists(path))
throw new Exception("aria2下载可能存在错误");
Console.WriteLine();
return;
}
int retry = 0;
string tmpName = Path.Combine(desDir, Path.GetFileNameWithoutExtension(path) + ".tmp");
await BBDownAria2c.DownloadFileByAria2cAsync(url, path, config.Aria2cArgs);
if (File.Exists(path + ".aria2") || !File.Exists(path))
throw new Exception("aria2下载可能存在错误");
Console.WriteLine();
return;
}
int retry = 0;
string tmpName = Path.Combine(desDir, Path.GetFileNameWithoutExtension(path) + ".tmp");
reDown:
try
{
using var progress = new ProgressBar(config.RelatedTask);
await RangeDownloadToTmpAsync(0, url, tmpName, 0, null, (_, downloaded, total) => progress.Report((double)downloaded / total, downloaded));
File.Move(tmpName, path, true);
}
catch (Exception)
{
if (++retry == 3) throw;
goto reDown;
}
}
public static async Task MultiThreadDownloadFileAsync(string url, string path, DownloadConfig config)
{
if (config.ForceHttp) url = ReplaceUrl(url);
LogDebug("Start downloading: {0}", url);
if (config.UseAria2c)
{
await BBDownAria2c.DownloadFileByAria2cAsync(url, path, config.Aria2cArgs);
if (File.Exists(path + ".aria2") || !File.Exists(path))
throw new Exception("aria2下载可能存在错误");
Console.WriteLine();
return;
}
long fileSize = await GetFileSizeAsync(url);
LogDebug("文件大小:{0} bytes", fileSize);
//已下载过, 跳过下载
if (File.Exists(path) && new FileInfo(path).Length == fileSize)
{
LogDebug("文件已下载过, 跳过下载");
return;
}
List<Clip> allClips = GetAllClips(url, fileSize);
int total = allClips.Count;
LogDebug("分段数量:{0}", total);
ConcurrentDictionary<int, long> clipProgress = new();
foreach (var i in allClips) clipProgress[i.index] = 0;
using var progress = new ProgressBar(config.RelatedTask);
progress.Report(0);
await Parallel.ForEachAsync(allClips, async (clip, _) =>
{
int retry = 0;
string tmp = Path.Combine(Path.GetDirectoryName(path)!, clip.index.ToString("00000") + "_" + Path.GetFileNameWithoutExtension(path) + (Path.GetExtension(path).EndsWith(".mp4") ? ".vclip" : ".aclip"));
reDown:
try
{
using var progress = new ProgressBar(config.RelatedTask);
await RangeDownloadToTmpAsync(0, url, tmpName, 0, null, (_, downloaded, total) => progress.Report((double)downloaded / total, downloaded));
File.Move(tmpName, path, true);
await RangeDownloadToTmpAsync(clip.index, url, tmp, clip.from, clip.to == -1 ? null : clip.to, (index, downloaded, _) =>
{
clipProgress[index] = downloaded;
progress.Report((double)clipProgress.Values.Sum() / fileSize, clipProgress.Values.Sum());
}, true);
}
catch (NotSupportedException)
{
if (++retry == 3) throw new Exception($"服务器可能并不支持多线程下载, 请使用 --multi-thread false 关闭多线程");
goto reDown;
}
catch (Exception)
{
if (++retry == 3) throw;
if (++retry == 3) throw new Exception($"Failed to download clip {clip.index}");
goto reDown;
}
}
});
}
public static async Task MultiThreadDownloadFileAsync(string url, string path, DownloadConfig config)
//此函数主要是切片下载逻辑
private static List<Clip> GetAllClips(string url, long fileSize)
{
List<Clip> clips = [];
int index = 0;
long counter = 0;
int perSize = 20 * 1024 * 1024;
while (fileSize > 0)
{
if (config.ForceHttp) url = ReplaceUrl(url);
LogDebug("Start downloading: {0}", url);
if (config.UseAria2c)
Clip c = new()
{
await BBDownAria2c.DownloadFileByAria2cAsync(url, path, config.Aria2cArgs);
if (File.Exists(path + ".aria2") || !File.Exists(path))
throw new Exception("aria2下载可能存在错误");
Console.WriteLine();
return;
}
long fileSize = await GetFileSizeAsync(url);
LogDebug("文件大小:{0} bytes", fileSize);
//已下载过, 跳过下载
if (File.Exists(path) && new FileInfo(path).Length == fileSize)
{
LogDebug("文件已下载过, 跳过下载");
return;
}
List<Clip> allClips = GetAllClips(url, fileSize);
int total = allClips.Count;
LogDebug("分段数量:{0}", total);
ConcurrentDictionary<int, long> clipProgress = new();
foreach (var i in allClips) clipProgress[i.index] = 0;
using var progress = new ProgressBar(config.RelatedTask);
progress.Report(0);
await Parallel.ForEachAsync(allClips, async (clip, _) =>
{
int retry = 0;
string tmp = Path.Combine(Path.GetDirectoryName(path)!, clip.index.ToString("00000") + "_" + Path.GetFileNameWithoutExtension(path) + (Path.GetExtension(path).EndsWith(".mp4") ? ".vclip" : ".aclip"));
reDown:
try
{
await RangeDownloadToTmpAsync(clip.index, url, tmp, clip.from, clip.to == -1 ? null : clip.to, (index, downloaded, _) =>
{
clipProgress[index] = downloaded;
progress.Report((double)clipProgress.Values.Sum() / fileSize, clipProgress.Values.Sum());
}, true);
}
catch (NotSupportedException)
{
if (++retry == 3) throw new Exception($"服务器可能并不支持多线程下载, 请使用 --multi-thread false 关闭多线程");
goto reDown;
}
catch (Exception)
{
if (++retry == 3) throw new Exception($"Failed to download clip {clip.index}");
goto reDown;
}
});
}
//此函数主要是切片下载逻辑
private static List<Clip> GetAllClips(string url, long fileSize)
{
List<Clip> clips = new();
int index = 0;
long counter = 0;
int perSize = 20 * 1024 * 1024;
while (fileSize > 0)
{
Clip c = new()
{
index = index,
from = counter,
to = counter + perSize
};
//没到最后
if (fileSize - perSize > 0)
{
fileSize -= perSize;
counter += perSize + 1;
index++;
clips.Add(c);
}
//已到最后
else
{
c.to = -1;
clips.Add(c);
break;
}
}
return clips;
}
private static async Task<long> GetFileSizeAsync(string url)
{
using var httpRequestMessage = new HttpRequestMessage();
if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android"))
httpRequestMessage.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com");
httpRequestMessage.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
httpRequestMessage.Headers.TryAddWithoutValidation("Cookie", Core.Config.COOKIE);
httpRequestMessage.RequestUri = new(url);
var response = (await AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
long totalSizeBytes = response.Content.Headers.ContentLength ?? 0;
return totalSizeBytes;
}
/// <summary>
/// 将下载地址强制转换为HTTP
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private static string ReplaceUrl(string url)
{
if (url.Contains(".mcdn.bilivideo.cn:"))
{
LogDebug("对[*.mcdn.bilivideo.cn:xxx]域名不做处理");
return url;
index = index,
from = counter,
to = counter + perSize
};
//没到最后
if (fileSize - perSize > 0)
{
fileSize -= perSize;
counter += perSize + 1;
index++;
clips.Add(c);
}
//已到最后
else
{
LogDebug("将https更改为http");
return url.Replace("https:", "http:");
c.to = -1;
clips.Add(c);
break;
}
}
return clips;
}
}
private static async Task<long> GetFileSizeAsync(string url)
{
using var httpRequestMessage = new HttpRequestMessage();
if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android"))
httpRequestMessage.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com");
httpRequestMessage.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
httpRequestMessage.Headers.TryAddWithoutValidation("Cookie", Core.Config.COOKIE);
httpRequestMessage.RequestUri = new(url);
var response = (await AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
long totalSizeBytes = response.Content.Headers.ContentLength ?? 0;
return totalSizeBytes;
}
/// <summary>
/// 将下载地址强制转换为HTTP
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private static string ReplaceUrl(string url)
{
if (url.Contains(".mcdn.bilivideo.cn:"))
{
LogDebug("对[*.mcdn.bilivideo.cn:xxx]域名不做处理");
return url;
}
LogDebug("将https更改为http");
return url.Replace("https:", "http:");
}
}

View File

@@ -9,123 +9,122 @@ using System.Text.Json;
using System.Net.Http;
using BBDown.Core.Util;
namespace BBDown
namespace BBDown;
internal static class BBDownLoginUtil
{
internal class BBDownLoginUtil
public static async Task<string> GetLoginStatusAsync(string qrcodeKey)
{
public static async Task<string> GetLoginStatusAsync(string qrcodeKey)
{
string queryUrl = $"https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qrcodeKey}&source=main-fe-header";
return await HTTPUtil.GetWebSourceAsync(queryUrl);
}
public static async Task LoginWEB()
{
try
{
Log("获取登录地址...");
string loginUrl = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header";
string url = JsonDocument.Parse(await HTTPUtil.GetWebSourceAsync(loginUrl)).RootElement.GetProperty("data").GetProperty("url").ToString();
string qrcodeKey = GetQueryString("qrcode_key", url);
//Log(oauthKey);
//Log(url);
bool flag = false;
Log("生成二维码...");
QRCodeGenerator qrGenerator = new();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
PngByteQRCode pngByteCode = new(qrCodeData);
File.WriteAllBytes("qrcode.png", pngByteCode.GetGraphic(7));
Log("生成二维码成功: qrcode.png, 请打开并扫描, 或扫描打印的二维码");
var consoleQRCode = new ConsoleQRCode(qrCodeData);
consoleQRCode.GetGraphic();
while (true)
{
await Task.Delay(1000);
string w = await GetLoginStatusAsync(qrcodeKey);
int code = JsonDocument.Parse(w).RootElement.GetProperty("data").GetProperty("code").GetInt32();
if (code == 86038)
{
LogColor("二维码已过期, 请重新执行登录指令.");
break;
}
else if (code == 86101) //等待扫码
{
continue;
}
else if (code == 86090) //等待确认
{
if (!flag)
{
Log("扫码成功, 请确认...");
flag = !flag;
}
}
else
{
string cc = JsonDocument.Parse(w).RootElement.GetProperty("data").GetProperty("url").ToString();
Log("登录成功: SESSDATA=" + GetQueryString("SESSDATA", cc));
//导出cookie, 转义英文逗号 否则部分场景会出问题
File.WriteAllText(Path.Combine(Program.APP_DIR, "BBDown.data"), cc[(cc.IndexOf('?') + 1)..].Replace("&", ";").Replace(",", "%2C"));
File.Delete("qrcode.png");
break;
}
}
}
catch (Exception e) { LogError(e.Message); }
}
public static async Task LoginTV()
{
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("获取登录地址...");
byte[] responseArray = await (await HTTPUtil.AppHttpClient.PostAsync(loginUrl, new FormUrlEncodedContent(parms.ToDictionary()))).Content.ReadAsByteArrayAsync();
string web = Encoding.UTF8.GetString(responseArray);
string url = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("url").ToString();
string authCode = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("auth_code").ToString();
Log("生成二维码...");
QRCodeGenerator qrGenerator = new();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
PngByteQRCode pngByteCode = new(qrCodeData);
File.WriteAllBytes("qrcode.png", pngByteCode.GetGraphic(7));
Log("生成二维码成功: qrcode.png, 请打开并扫描, 或扫描打印的二维码");
var consoleQRCode = new ConsoleQRCode(qrCodeData);
consoleQRCode.GetGraphic();
parms.Set("auth_code", authCode);
parms.Set("ts", GetTimeStamp(true));
parms.Remove("sign");
parms.Add("sign", GetSign(ToQueryString(parms)));
while (true)
{
await Task.Delay(1000);
responseArray = await (await HTTPUtil.AppHttpClient.PostAsync(pollUrl, new FormUrlEncodedContent(parms.ToDictionary()))).Content.ReadAsByteArrayAsync();
web = Encoding.UTF8.GetString(responseArray);
string code = JsonDocument.Parse(web).RootElement.GetProperty("code").ToString();
if (code == "86038")
{
LogColor("二维码已过期, 请重新执行登录指令.");
break;
}
else if (code == "86039") //等待扫码
{
continue;
}
else
{
string cc = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("access_token").ToString();
Log("登录成功: AccessToken=" + cc);
//导出cookie
File.WriteAllText(Path.Combine(Program.APP_DIR, "BBDownTV.data"), "access_token=" + cc);
File.Delete("qrcode.png");
break;
}
}
}
catch (Exception e) { LogError(e.Message); }
}
string queryUrl = $"https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qrcodeKey}&source=main-fe-header";
return await HTTPUtil.GetWebSourceAsync(queryUrl);
}
}
public static async Task LoginWEB()
{
try
{
Log("获取登录地址...");
string loginUrl = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header";
string url = JsonDocument.Parse(await HTTPUtil.GetWebSourceAsync(loginUrl)).RootElement.GetProperty("data").GetProperty("url").ToString();
string qrcodeKey = GetQueryString("qrcode_key", url);
//Log(oauthKey);
//Log(url);
bool flag = false;
Log("生成二维码...");
QRCodeGenerator qrGenerator = new();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
PngByteQRCode pngByteCode = new(qrCodeData);
await File.WriteAllBytesAsync("qrcode.png", pngByteCode.GetGraphic(7));
Log("生成二维码成功: qrcode.png, 请打开并扫描, 或扫描打印的二维码");
var consoleQRCode = new ConsoleQRCode(qrCodeData);
consoleQRCode.GetGraphic();
while (true)
{
await Task.Delay(1000);
string w = await GetLoginStatusAsync(qrcodeKey);
int code = JsonDocument.Parse(w).RootElement.GetProperty("data").GetProperty("code").GetInt32();
if (code == 86038)
{
LogColor("二维码已过期, 请重新执行登录指令.");
break;
}
else if (code == 86101) //等待扫码
{
continue;
}
else if (code == 86090) //等待确认
{
if (!flag)
{
Log("扫码成功, 请确认...");
flag = !flag;
}
}
else
{
string cc = JsonDocument.Parse(w).RootElement.GetProperty("data").GetProperty("url").ToString();
Log("登录成功: SESSDATA=" + GetQueryString("SESSDATA", cc));
//导出cookie, 转义英文逗号 否则部分场景会出问题
await File.WriteAllTextAsync(Path.Combine(Program.APP_DIR, "BBDown.data"), cc[(cc.IndexOf('?') + 1)..].Replace("&", ";").Replace(",", "%2C"));
File.Delete("qrcode.png");
break;
}
}
}
catch (Exception e) { LogError(e.Message); }
}
public static async Task LoginTV()
{
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("获取登录地址...");
byte[] responseArray = await (await HTTPUtil.AppHttpClient.PostAsync(loginUrl, new FormUrlEncodedContent(parms.ToDictionary()))).Content.ReadAsByteArrayAsync();
string web = Encoding.UTF8.GetString(responseArray);
string url = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("url").ToString();
string authCode = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("auth_code").ToString();
Log("生成二维码...");
QRCodeGenerator qrGenerator = new();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
PngByteQRCode pngByteCode = new(qrCodeData);
await File.WriteAllBytesAsync("qrcode.png", pngByteCode.GetGraphic(7));
Log("生成二维码成功: qrcode.png, 请打开并扫描, 或扫描打印的二维码");
var consoleQRCode = new ConsoleQRCode(qrCodeData);
consoleQRCode.GetGraphic();
parms.Set("auth_code", authCode);
parms.Set("ts", GetTimeStamp(true));
parms.Remove("sign");
parms.Add("sign", GetSign(ToQueryString(parms)));
while (true)
{
await Task.Delay(1000);
responseArray = await (await HTTPUtil.AppHttpClient.PostAsync(pollUrl, new FormUrlEncodedContent(parms.ToDictionary()))).Content.ReadAsByteArrayAsync();
web = Encoding.UTF8.GetString(responseArray);
string code = JsonDocument.Parse(web).RootElement.GetProperty("code").ToString();
if (code == "86038")
{
LogColor("二维码已过期, 请重新执行登录指令.");
break;
}
else if (code == "86039") //等待扫码
{
continue;
}
else
{
string cc = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("access_token").ToString();
Log("登录成功: AccessToken=" + cc);
//导出cookie
await File.WriteAllTextAsync(Path.Combine(Program.APP_DIR, "BBDownTV.data"), "access_token=" + cc);
File.Delete("qrcode.png");
break;
}
}
}
catch (Exception e) { LogError(e.Message); }
}
}

View File

@@ -10,212 +10,211 @@ using static BBDown.Core.Logger;
using System.IO;
using BBDown.Core;
namespace BBDown
namespace BBDown;
static partial class BBDownMuxer
{
partial class BBDownMuxer
public static string FFMPEG = "ffmpeg";
public static string MP4BOX = "mp4box";
private static int RunExe(string app, string parms, bool customBin = false)
{
public static string FFMPEG = "ffmpeg";
public static string MP4BOX = "mp4box";
int code = 0;
Process p = new();
p.StartInfo.FileName = app;
p.StartInfo.Arguments = parms;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.CreateNoWindow = false;
p.ErrorDataReceived += delegate (object sendProcess, DataReceivedEventArgs output) {
if (!string.IsNullOrWhiteSpace(output.Data))
Log(output.Data);
};
p.StartInfo.StandardErrorEncoding = Encoding.UTF8;
p.Start();
p.BeginErrorReadLine();
p.WaitForExit();
p.Close();
p.Dispose();
return code;
}
private static int RunExe(string app, string parms, bool customBin = false)
private static string EscapeString(string str)
{
return string.IsNullOrEmpty(str) ? str : str.Replace("\"", "'").Replace("\\", "\\\\");
}
private static int MuxByMp4box(string videoPath, string audioPath, string outPath, string desc, string title, string author, string episodeId, string pic, string lang, List<Subtitle>? subs, bool audioOnly, bool videoOnly, List<ViewPoint>? points)
{
StringBuilder inputArg = new();
StringBuilder metaArg = new();
int nowId = 0;
inputArg.Append(" -inter 500 -noprog ");
if (!string.IsNullOrEmpty(videoPath))
{
int code = 0;
Process p = new();
p.StartInfo.FileName = app;
p.StartInfo.Arguments = parms;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.CreateNoWindow = false;
p.ErrorDataReceived += delegate (object sendProcess, DataReceivedEventArgs output) {
if (!string.IsNullOrWhiteSpace(output.Data))
Log(output.Data);
};
p.StartInfo.StandardErrorEncoding = Encoding.UTF8;
p.Start();
p.BeginErrorReadLine();
p.WaitForExit();
p.Close();
p.Dispose();
return code;
inputArg.Append($" -add \"{videoPath}#trackID={(audioOnly && audioPath == "" ? "2" : "1")}:name=\" ");
nowId++;
}
private static string EscapeString(string str)
if (!string.IsNullOrEmpty(audioPath))
{
return string.IsNullOrEmpty(str) ? str : str.Replace("\"", "'").Replace("\\", "\\\\");
inputArg.Append($" -add \"{audioPath}:lang={(lang == "" ? "und" : lang)}\" ");
nowId++;
}
private static int MuxByMp4box(string videoPath, string audioPath, string outPath, string desc, string title, string author, string episodeId, string pic, string lang, List<Subtitle>? subs, bool audioOnly, bool videoOnly, List<ViewPoint>? points)
if (points != null && points.Any())
{
StringBuilder inputArg = new();
StringBuilder metaArg = new();
int nowId = 0;
inputArg.Append(" -inter 500 -noprog ");
if (!string.IsNullOrEmpty(videoPath))
{
inputArg.Append($" -add \"{videoPath}#trackID={(audioOnly && audioPath == "" ? "2" : "1")}:name=\" ");
nowId++;
}
if (!string.IsNullOrEmpty(audioPath))
{
inputArg.Append($" -add \"{audioPath}:lang={(lang == "" ? "und" : lang)}\" ");
nowId++;
}
if (points != null && points.Any())
{
var meta = GetMp4boxMetaString(points);
var metaFile = Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters");
File.WriteAllText(metaFile, meta);
inputArg.Append($" -chap \"{metaFile}\" ");
}
if (!string.IsNullOrEmpty(pic))
metaArg.Append($":cover=\"{pic}\"");
if (!string.IsNullOrEmpty(episodeId))
metaArg.Append($":album=\"{title}\":title=\"{episodeId}\"");
else
metaArg.Append($":title=\"{title}\"");
metaArg.Append($":comment=\"{desc}\"");
metaArg.Append($":artist=\"{author}\"");
var meta = GetMp4boxMetaString(points);
var metaFile = Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters");
File.WriteAllText(metaFile, meta);
inputArg.Append($" -chap \"{metaFile}\" ");
}
if (!string.IsNullOrEmpty(pic))
metaArg.Append($":cover=\"{pic}\"");
if (!string.IsNullOrEmpty(episodeId))
metaArg.Append($":album=\"{title}\":title=\"{episodeId}\"");
else
metaArg.Append($":title=\"{title}\"");
metaArg.Append($":comment=\"{desc}\"");
metaArg.Append($":artist=\"{author}\"");
if (subs != null)
if (subs != null)
{
for (int i = 0; i < subs.Count; i++)
{
for (int i = 0; i < subs.Count; i++)
if (File.Exists(subs[i].path) && File.ReadAllText(subs[i].path!) != "")
{
if (File.Exists(subs[i].path) && File.ReadAllText(subs[i].path!) != "")
{
nowId++;
inputArg.Append($" -add \"{subs[i].path}#trackID=1:name=:hdlr=sbtl:lang={GetSubtitleCode(subs[i].lan).Item1}\" ");
inputArg.Append($" -udta {nowId}:type=name:str=\"{GetSubtitleCode(subs[i].lan).Item2}\" ");
}
nowId++;
inputArg.Append($" -add \"{subs[i].path}#trackID=1:name=:hdlr=sbtl:lang={GetSubtitleCode(subs[i].lan).Item1}\" ");
inputArg.Append($" -udta {nowId}:type=name:str=\"{GetSubtitleCode(subs[i].lan).Item2}\" ");
}
}
//----分析完毕
var arguments = (Config.DEBUG_LOG ? " -verbose " : "") + inputArg.ToString() + (metaArg.ToString() == "" ? "" : " -itags tool=" + metaArg.ToString()) + $" -new -- \"{outPath}\"";
LogDebug("mp4box命令: {0}", arguments);
return RunExe(MP4BOX, arguments, MP4BOX != "mp4box");
}
public static int MuxAV(bool useMp4box, string videoPath, string audioPath, List<AudioMaterial> audioMaterial, string outPath, string desc = "", string title = "", string author = "", string episodeId = "", string pic = "", string lang = "", List<Subtitle>? subs = null, bool audioOnly = false, bool videoOnly = false, List<ViewPoint>? points = null, long pubTime = 0, bool simplyMux = false)
//----分析完毕
var arguments = (Config.DEBUG_LOG ? " -verbose " : "") + inputArg.ToString() + (metaArg.ToString() == "" ? "" : " -itags tool=" + metaArg.ToString()) + $" -new -- \"{outPath}\"";
LogDebug("mp4box命令: {0}", arguments);
return RunExe(MP4BOX, arguments, MP4BOX != "mp4box");
}
public static int MuxAV(bool useMp4box, string videoPath, string audioPath, List<AudioMaterial> audioMaterial, string outPath, string desc = "", string title = "", string author = "", string episodeId = "", string pic = "", string lang = "", List<Subtitle>? subs = null, bool audioOnly = false, bool videoOnly = false, List<ViewPoint>? points = null, long pubTime = 0, bool simplyMux = false)
{
if (audioOnly && audioPath != "")
videoPath = "";
if (videoOnly)
audioPath = "";
desc = EscapeString(desc);
title = EscapeString(title);
episodeId = EscapeString(episodeId);
if (useMp4box)
{
if (audioOnly && audioPath != "")
videoPath = "";
if (videoOnly)
audioPath = "";
desc = EscapeString(desc);
title = EscapeString(title);
episodeId = EscapeString(episodeId);
return MuxByMp4box(videoPath, audioPath, outPath, desc, title, author, episodeId, pic, lang, subs, audioOnly, videoOnly, points);
}
if (useMp4box)
{
return MuxByMp4box(videoPath, audioPath, outPath, desc, title, author, episodeId, pic, lang, subs, audioOnly, videoOnly, points);
}
if (outPath.Contains('/') && ! Directory.Exists(Path.GetDirectoryName(outPath)))
Directory.CreateDirectory(Path.GetDirectoryName(outPath)!);
//----分析并生成-i参数
StringBuilder inputArg = new();
StringBuilder metaArg = new();
byte inputCount = 0;
foreach (string path in new string[] { videoPath, audioPath })
{
if (!string.IsNullOrEmpty(path))
{
inputCount++;
inputArg.Append($"-i \"{path}\" ");
}
}
if (audioMaterial.Any())
{
byte audioCount = 0;
metaArg.Append("-metadata:s:a:0 title=\"原音频\" ");
foreach (var audio in audioMaterial)
{
inputCount++;
audioCount++;
inputArg.Append($"-i \"{audio.path}\" ");
if (!string.IsNullOrWhiteSpace(audio.title)) metaArg.Append($"-metadata:s:a:{audioCount} title=\"{audio.title}\" ");
if (!string.IsNullOrWhiteSpace(audio.personName)) metaArg.Append($"-metadata:s:a:{audioCount} artist=\"{audio.personName}\" ");
}
}
if (!string.IsNullOrEmpty(pic))
if (outPath.Contains('/') && ! Directory.Exists(Path.GetDirectoryName(outPath)))
Directory.CreateDirectory(Path.GetDirectoryName(outPath)!);
//----分析并生成-i参数
StringBuilder inputArg = new();
StringBuilder metaArg = new();
byte inputCount = 0;
foreach (string path in new[] { videoPath, audioPath })
{
if (!string.IsNullOrEmpty(path))
{
inputCount++;
inputArg.Append($"-i \"{pic}\" ");
inputArg.Append($"-i \"{path}\" ");
}
if (subs != null)
{
for (int i = 0; i < subs.Count; i++)
{
if(File.Exists(subs[i].path) && File.ReadAllText(subs[i].path!) != "")
{
inputCount++;
inputArg.Append($"-i \"{subs[i].path}\" ");
metaArg.Append($"-metadata:s:s:{i} title=\"{GetSubtitleCode(subs[i].lan).Item2}\" -metadata:s:s:{i} language={GetSubtitleCode(subs[i].lan).Item1} ");
}
}
}
if (!string.IsNullOrEmpty(pic))
metaArg.Append($"-disposition:v:{(audioOnly ? "0" : "1")} attached_pic ");
// var inputCount = InputRegex().Matches(inputArg.ToString()).Count;
if (points != null && points.Any())
{
var meta = GetFFmpegMetaString(points);
var metaFile = Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters");
File.WriteAllText(metaFile, meta);
inputArg.Append($"-i \"{metaFile}\" -map_chapters {inputCount} ");
}
inputArg.Append(string.Concat(Enumerable.Range(0, inputCount).Select(i => $"-map {i} ")));
//----分析完毕
StringBuilder argsBuilder = new StringBuilder();
argsBuilder.Append($"-loglevel {(Config.DEBUG_LOG ? "verbose" : "warning")} -y ");
argsBuilder.Append(inputArg);
argsBuilder.Append(metaArg);
if (!simplyMux) {
argsBuilder.Append($"-metadata title=\"{(episodeId == "" ? title : episodeId)}\" ");
if (lang != "") argsBuilder.Append($"-metadata:s:a:0 language={lang} ");
if (!string.IsNullOrWhiteSpace(desc)) argsBuilder.Append($"-metadata description=\"{desc}\" ");
if (!string.IsNullOrEmpty(author)) argsBuilder.Append($"-metadata artist=\"{author}\" ");
if (episodeId != "") argsBuilder.Append($"-metadata album=\"{title}\" ");
if (pubTime != 0) argsBuilder.Append($"-metadata creation_time=\"{(DateTimeOffset.FromUnixTimeSeconds(pubTime).ToString("yyyy-MM-ddTHH:mm:ss.ffffffZ"))}\" ");
}
argsBuilder.Append("-c copy ");
if (audioOnly && audioPath == "") argsBuilder.Append("-vn ");
if (subs != null) argsBuilder.Append("-c:s mov_text ");
argsBuilder.Append($"-movflags faststart -strict unofficial -strict -2 -f mp4 -- \"{outPath}\"");
string arguments = argsBuilder.ToString();
LogDebug("ffmpeg命令: {0}", arguments);
return RunExe(FFMPEG, arguments, FFMPEG != "ffmpeg");
}
public static void MergeFLV(string[] files, string outPath)
if (audioMaterial.Any())
{
if (files.Length == 1)
byte audioCount = 0;
metaArg.Append("-metadata:s:a:0 title=\"原音频\" ");
foreach (var audio in audioMaterial)
{
File.Move(files[0], outPath);
inputCount++;
audioCount++;
inputArg.Append($"-i \"{audio.path}\" ");
if (!string.IsNullOrWhiteSpace(audio.title)) metaArg.Append($"-metadata:s:a:{audioCount} title=\"{audio.title}\" ");
if (!string.IsNullOrWhiteSpace(audio.personName)) metaArg.Append($"-metadata:s:a:{audioCount} artist=\"{audio.personName}\" ");
}
else
}
if (!string.IsNullOrEmpty(pic))
{
inputCount++;
inputArg.Append($"-i \"{pic}\" ");
}
if (subs != null)
{
for (int i = 0; i < subs.Count; i++)
{
foreach (var file in files)
if(File.Exists(subs[i].path) && File.ReadAllText(subs[i].path!) != "")
{
var tmpFile = Path.Combine(Path.GetDirectoryName(file)!, Path.GetFileNameWithoutExtension(file) + ".ts");
var arguments = $"-loglevel warning -y -i \"{file}\" -map 0 -c copy -f mpegts -bsf:v h264_mp4toannexb \"{tmpFile}\"";
LogDebug("ffmpeg命令: {0}", arguments);
RunExe("ffmpeg", arguments);
File.Delete(file);
inputCount++;
inputArg.Append($"-i \"{subs[i].path}\" ");
metaArg.Append($"-metadata:s:s:{i} title=\"{GetSubtitleCode(subs[i].lan).Item2}\" -metadata:s:s:{i} language={GetSubtitleCode(subs[i].lan).Item1} ");
}
var f = GetFiles(Path.GetDirectoryName(files[0])!, ".ts");
CombineMultipleFilesIntoSingleFile(f, outPath);
foreach (var s in f) File.Delete(s);
}
}
if (!string.IsNullOrEmpty(pic))
metaArg.Append($"-disposition:v:{(audioOnly ? "0" : "1")} attached_pic ");
// var inputCount = InputRegex().Matches(inputArg.ToString()).Count;
if (points != null && points.Any())
{
var meta = GetFFmpegMetaString(points);
var metaFile = Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters");
File.WriteAllText(metaFile, meta);
inputArg.Append($"-i \"{metaFile}\" -map_chapters {inputCount} ");
}
inputArg.Append(string.Concat(Enumerable.Range(0, inputCount).Select(i => $"-map {i} ")));
//----分析完毕
StringBuilder argsBuilder = new StringBuilder();
argsBuilder.Append($"-loglevel {(Config.DEBUG_LOG ? "verbose" : "warning")} -y ");
argsBuilder.Append(inputArg);
argsBuilder.Append(metaArg);
if (!simplyMux) {
argsBuilder.Append($"-metadata title=\"{(episodeId == "" ? title : episodeId)}\" ");
if (lang != "") argsBuilder.Append($"-metadata:s:a:0 language={lang} ");
if (!string.IsNullOrWhiteSpace(desc)) argsBuilder.Append($"-metadata description=\"{desc}\" ");
if (!string.IsNullOrEmpty(author)) argsBuilder.Append($"-metadata artist=\"{author}\" ");
if (episodeId != "") argsBuilder.Append($"-metadata album=\"{title}\" ");
if (pubTime != 0) argsBuilder.Append($"-metadata creation_time=\"{(DateTimeOffset.FromUnixTimeSeconds(pubTime).ToString("yyyy-MM-ddTHH:mm:ss.ffffffZ"))}\" ");
}
argsBuilder.Append("-c copy ");
if (audioOnly && audioPath == "") argsBuilder.Append("-vn ");
if (subs != null) argsBuilder.Append("-c:s mov_text ");
argsBuilder.Append($"-movflags faststart -strict unofficial -strict -2 -f mp4 -- \"{outPath}\"");
string arguments = argsBuilder.ToString();
LogDebug("ffmpeg命令: {0}", arguments);
return RunExe(FFMPEG, arguments, FFMPEG != "ffmpeg");
}
public static void MergeFLV(string[] files, string outPath)
{
if (files.Length == 1)
{
File.Move(files[0], outPath);
}
else
{
foreach (var file in files)
{
var tmpFile = Path.Combine(Path.GetDirectoryName(file)!, Path.GetFileNameWithoutExtension(file) + ".ts");
var arguments = $"-loglevel warning -y -i \"{file}\" -map 0 -c copy -f mpegts -bsf:v h264_mp4toannexb \"{tmpFile}\"";
LogDebug("ffmpeg命令: {0}", arguments);
RunExe("ffmpeg", arguments);
File.Delete(file);
}
var f = GetFiles(Path.GetDirectoryName(files[0])!, ".ts");
CombineMultipleFilesIntoSingleFile(f, outPath);
foreach (var s in f) File.Delete(s);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,227 +1,223 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Binding;
using System.CommandLine.Parsing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BBDown
namespace BBDown;
internal static class CommandLineInvoker
{
internal class CommandLineInvoker
private static readonly Argument<string> Url = new("url", description: "视频地址 或 av|bv|BV|ep|ss");
private static readonly Option<bool> UseTvApi = new(["--use-tv-api", "-tv"], "使用TV端解析模式");
private static readonly Option<bool> UseAppApi = new(["--use-app-api", "-app"], "使用APP端解析模式");
private static readonly Option<bool> UseIntlApi = new(["--use-intl-api", "-intl"], "使用国际版(东南亚视频)解析模式");
private static readonly Option<bool> UseMP4box = new(["--use-mp4box"], "使用MP4Box来混流");
private static readonly Option<string> EncodingPriority = new(["--encoding-priority", "-e"], "视频编码的选择优先级, 用逗号分割 例: \"hevc,av1,avc\"");
private static readonly Option<string> DfnPriority = new(["--dfn-priority", "-q"], "画质优先级,用逗号分隔 例: \"8K 超高清, 1080P 高码率, HDR 真彩, 杜比视界\"");
private static readonly Option<bool> OnlyShowInfo = new(["--only-show-info", "-info"], "仅解析而不进行下载");
private static readonly Option<bool> HideStreams = new(["--hide-streams", "-hs"], "不要显示所有可用音视频流");
private static readonly Option<bool> Interactive = new(["--interactive", "-ia"], "交互式选择清晰度");
private static readonly Option<bool> ShowAll = new(["--show-all"], "展示所有分P标题");
private static readonly Option<bool> UseAria2c = new(["--use-aria2c", "-aria2"], "调用aria2c进行下载(你需要自行准备好二进制可执行文件)");
private static readonly Option<string> Aria2cArgs = new(["--aria2c-args"], "调用aria2c的附加参数(默认参数包含\"-x16 -s16 -j16 -k 5M\", 使用时注意字符串转义)");
private static readonly Option<bool> MultiThread = new(["--multi-thread", "-mt"], "使用多线程下载(默认开启)");
private static readonly Option<string> SelectPage = new(["--select-page", "-p"], "选择指定分p或分p范围: (-p 8 或 -p 1,2 或 -p 3-5 或 -p ALL 或 -p LAST 或 -p 3,5,LATEST)");
private static readonly Option<bool> SimplyMux = new(["--simply-mux"], "精简混流,不增加描述、作者等信息");
private static readonly Option<bool> AudioOnly = new(["--audio-only"], "仅下载音频");
private static readonly Option<bool> VideoOnly = new(["--video-only"], "仅下载视频");
private static readonly Option<bool> DanmakuOnly = new(["--danmaku-only"], "仅下载弹幕");
private static readonly Option<bool> CoverOnly = new(["--cover-only"], "仅下载封面");
private static readonly Option<bool> SubOnly = new(["--sub-only"], "仅下载字幕");
private static readonly Option<bool> Debug = new(["--debug"], "输出调试日志");
private static readonly Option<bool> SkipMux = new(["--skip-mux"], "跳过混流步骤");
private static readonly Option<bool> SkipSubtitle = new(["--skip-subtitle"], "跳过字幕下载");
private static readonly Option<bool> SkipCover = new(["--skip-cover"], "跳过封面下载");
private static readonly Option<bool> ForceHttp = new(["--force-http"], "下载音视频时强制使用HTTP协议替换HTTPS(默认开启)");
private static readonly Option<bool> DownloadDanmaku = new(["--download-danmaku", "-dd"], "下载弹幕");
private static readonly Option<bool> SkipAi = new(["--skip-ai"], description: "跳过AI字幕下载(默认开启)");
private static readonly Option<bool> VideoAscending = new(["--video-ascending"], "视频升序(最小体积优先)");
private static readonly Option<bool> AudioAscending = new(["--audio-ascending"], "音频升序(最小体积优先)");
private static readonly Option<bool> AllowPcdn = new(["--allow-pcdn"], "不替换PCDN域名, 仅在正常情况与--upos-host均无法下载时使用");
private static readonly Option<string> Language = new(["--language"], "设置混流的音频语言(代码), 如chi, jpn等");
private static readonly Option<string> UserAgent = new(["--user-agent", "-ua"], "指定user-agent, 否则使用随机user-agent");
private static readonly Option<string> Cookie = new(["--cookie", "-c"], "设置字符串cookie用以下载网页接口的会员内容");
private static readonly Option<string> AccessToken = new(["--access-token", "-token"], "设置access_token用以下载TV/APP接口的会员内容");
private static readonly Option<string> WorkDir = new(["--work-dir"], "设置程序的工作目录");
private static readonly Option<string> FFmpegPath = new(["--ffmpeg-path"], "设置ffmpeg的路径");
private static readonly Option<string> Mp4boxPath = new(["--mp4box-path"], "设置mp4box的路径");
private static readonly Option<string> Aria2cPath = new(["--aria2c-path"], "设置aria2c的路径");
private static readonly Option<string> UposHost = new(["--upos-host"], "自定义upos服务器");
private static readonly Option<bool> ForceReplaceHost = new(["--force-replace-host"], "强制替换下载服务器host(默认开启)");
private static readonly Option<bool> SaveArchivesToFile = new(["--save-archives-to-file"], "将下载过的视频记录到本地文件中, 用于后续跳过下载同个视频");
private static readonly Option<string> DelayPerPage = new(["--delay-per-page"], "设置下载合集分P之间的下载间隔时间(单位: 秒, 默认无间隔)");
private static readonly Option<string> FilePattern = new(["--file-pattern", "-F"],
$"使用内置变量自定义单P存储文件名:\r\n\r\n" +
$"<videoTitle>: 视频主标题\r\n" +
$"<pageNumber>: 视频分P序号\r\n" +
$"<pageNumberWithZero>: 视频分P序号(前缀补零)\r\n" +
$"<pageTitle>: 视频分P标题\r\n" +
$"<bvid>: 视频BV号\r\n" +
$"<aid>: 视频aid\r\n" +
$"<cid>: 视频cid\r\n" +
$"<dfn>: 视频清晰度\r\n" +
$"<res>: 视频分辨率\r\n" +
$"<fps>: 视频帧率\r\n" +
$"<videoCodecs>: 视频编码\r\n" +
$"<videoBandwidth>: 视频码率\r\n" +
$"<audioCodecs>: 音频编码\r\n" +
$"<audioBandwidth>: 音频码率\r\n" +
$"<ownerName>: 上传者名称\r\n" +
$"<ownerMid>: 上传者mid\r\n" +
$"<publishDate>: 收藏夹/番剧/合集发布时间\r\n" +
$"<videoDate>: 视频发布时间(分p视频发布时间与<publishDate>相同)\r\n" +
$"<apiType>: API类型(TV/APP/INTL/WEB)\r\n\r\n" +
$"默认为: {Program.SinglePageDefaultSavePath}\r\n");
private static readonly Option<string> MultiFilePattern = new(["--multi-file-pattern", "-M"], $"使用内置变量自定义多P存储文件名:\r\n\r\n默认为: {Program.MultiPageDefaultSavePath}\r\n");
private static readonly Option<string> Host = new(["--host"], "指定BiliPlus host(使用BiliPlus需要access_token, 不需要cookie, 解析服务器能够获取你账号的大部分权限!)");
private static readonly Option<string> EpHost = new(["--ep-host"], "指定BiliPlus EP host(用于代理api.bilibili.com/pgc/view/web/season, 大部分解析服务器不支持代理该接口)");
private static readonly Option<string> Area = new(["--area"], "(hk|tw|th) 使用BiliPlus时必选, 指定BiliPlus area");
private static readonly Option<string> ConfigFile = new(["--config-file"], "读取指定的BBDown本地配置文件(默认为: BBDown.config)");//以下仅为兼容旧版本命令行, 不建议使用
private static readonly Option<string> Aria2cProxy = new(["--aria2c-proxy"], "调用aria2c进行下载时的代理地址配置") { IsHidden = true };
private static readonly Option<bool> OnlyHevc = new(["--only-hevc", "-hevc"], "只下载hevc编码") { IsHidden = true };
private static readonly Option<bool> OnlyAvc = new(["--only-avc", "-avc"], "只下载avc编码") { IsHidden = true };
private static readonly Option<bool> OnlyAv1 = new(["--only-av1", "-av1"], "只下载av1编码") { IsHidden = true };
private static readonly Option<bool> AddDfnSubfix = new(["--add-dfn-subfix"], "为文件加入清晰度后缀, 如XXX[1080P 高码率]") { IsHidden = true };
private static readonly Option<bool> NoPaddingPageNum = new(["--no-padding-page-num"], "不给分P序号补零") { IsHidden = true };
private static readonly Option<bool> BandwithAscending = new(["--bandwith-ascending"], "比特率升序(最小体积优先)") { IsHidden = true };
class MyOptionBinder : BinderBase<MyOption>
{
private readonly static Argument<string> Url = new("url", description: "视频地址 或 av|bv|BV|ep|ss");
private readonly static Option<bool> UseTvApi = new(new string[] { "--use-tv-api", "-tv" }, "使用TV端解析模式");
private readonly static Option<bool> UseAppApi = new(new string[] { "--use-app-api", "-app" }, "使用APP端解析模式");
private readonly static Option<bool> UseIntlApi = new(new string[] { "--use-intl-api", "-intl" }, "使用国际版(东南亚视频)解析模式");
private readonly static Option<bool> UseMP4box = new(new string[] { "--use-mp4box" }, "使用MP4Box来混流");
private readonly static Option<string> EncodingPriority = new(new string[] { "--encoding-priority", "-e" }, "视频编码的选择优先级, 用逗号分割 例: \"hevc,av1,avc\"");
private readonly static Option<string> DfnPriority = new(new string[] { "--dfn-priority", "-q" }, "画质优先级,用逗号分隔 例: \"8K 超高清, 1080P 高码率, HDR 真彩, 杜比视界\"");
private readonly static Option<bool> OnlyShowInfo = new(new string[] { "--only-show-info", "-info" }, "仅解析而不进行下载");
private readonly static Option<bool> HideStreams = new(new string[] { "--hide-streams", "-hs" }, "不要显示所有可用音视频流");
private readonly static Option<bool> Interactive = new(new string[] { "--interactive", "-ia" }, "交互式选择清晰度");
private readonly static Option<bool> ShowAll = new(new string[] { "--show-all" }, "展示所有分P标题");
private readonly static Option<bool> UseAria2c = new(new string[] { "--use-aria2c", "-aria2" }, "调用aria2c进行下载(你需要自行准备好二进制可执行文件)");
private readonly static Option<string> Aria2cArgs = new(new string[] { "--aria2c-args" }, "调用aria2c的附加参数(默认参数包含\"-x16 -s16 -j16 -k 5M\", 使用时注意字符串转义)");
private readonly static Option<bool> MultiThread = new(new string[] { "--multi-thread", "-mt" }, "使用多线程下载(默认开启)");
private readonly static Option<string> SelectPage = new(new string[] { "--select-page", "-p" }, "选择指定分p或分p范围: (-p 8 或 -p 1,2 或 -p 3-5 或 -p ALL 或 -p LAST 或 -p 3,5,LATEST)");
private readonly static Option<bool> SimplyMux = new(new string[] { "--simply-mux" }, "精简混流,不增加描述、作者等信息");
private readonly static Option<bool> AudioOnly = new(new string[] { "--audio-only" }, "仅下载音频");
private readonly static Option<bool> VideoOnly = new(new string[] { "--video-only" }, "仅下载视频");
private readonly static Option<bool> DanmakuOnly = new(new string[] { "--danmaku-only" }, "仅下载弹幕");
private readonly static Option<bool> CoverOnly = new(new string[] { "--cover-only" }, "仅下载封面");
private readonly static Option<bool> SubOnly = new(new string[] { "--sub-only" }, "仅下载字幕");
private readonly static Option<bool> Debug = new(new string[] { "--debug" }, "输出调试日志");
private readonly static Option<bool> SkipMux = new(new string[] { "--skip-mux" }, "跳过混流步骤");
private readonly static Option<bool> SkipSubtitle = new(new string[] { "--skip-subtitle" }, "跳过字幕下载");
private readonly static Option<bool> SkipCover = new(new string[] { "--skip-cover" }, "跳过封面下载");
private readonly static Option<bool> ForceHttp = new(new string[] { "--force-http" }, "下载音视频时强制使用HTTP协议替换HTTPS(默认开启)");
private readonly static Option<bool> DownloadDanmaku = new(new string[] { "--download-danmaku", "-dd" }, "下载弹幕");
private readonly static Option<bool> SkipAi = new(new string[] { "--skip-ai" }, description: "跳过AI字幕下载(默认开启)");
private readonly static Option<bool> VideoAscending = new(new string[] { "--video-ascending" }, "视频升序(最小体积优先)");
private readonly static Option<bool> AudioAscending = new(new string[] { "--audio-ascending" }, "音频升序(最小体积优先)");
private readonly static Option<bool> AllowPcdn = new(new string[] { "--allow-pcdn" }, "不替换PCDN域名, 仅在正常情况与--upos-host均无法下载时使用");
private readonly static Option<string> Language = new(new string[] { "--language" }, "设置混流的音频语言(代码), 如chi, jpn等");
private readonly static Option<string> UserAgent = new(new string[] { "--user-agent", "-ua" }, "指定user-agent, 否则使用随机user-agent");
private readonly static Option<string> Cookie = new(new string[] { "--cookie", "-c" }, "设置字符串cookie用以下载网页接口的会员内容");
private readonly static Option<string> AccessToken = new(new string[] { "--access-token", "-token" }, "设置access_token用以下载TV/APP接口的会员内容");
private readonly static Option<string> WorkDir = new(new string[] { "--work-dir" }, "设置程序的工作目录");
private readonly static Option<string> FFmpegPath = new(new string[] { "--ffmpeg-path" }, "设置ffmpeg的路径");
private readonly static Option<string> Mp4boxPath = new(new string[] { "--mp4box-path" }, "设置mp4box的路径");
private readonly static Option<string> Aria2cPath = new(new string[] { "--aria2c-path" }, "设置aria2c的路径");
private readonly static Option<string> UposHost = new(new string[] { "--upos-host" }, "自定义upos服务器");
private readonly static Option<bool> ForceReplaceHost = new(new string[] { "--force-replace-host" }, "强制替换下载服务器host(默认开启)");
private readonly static Option<bool> SaveArchivesToFile = new(new string[] { "--save-archives-to-file" }, "将下载过的视频记录到本地文件中, 用于后续跳过下载同个视频");
private readonly static Option<string> DelayPerPage = new(new string[] { "--delay-per-page" }, "设置下载合集分P之间的下载间隔时间(单位: 秒, 默认无间隔)");
private readonly static Option<string> FilePattern = new(new string[] { "--file-pattern", "-F" },
$"使用内置变量自定义单P存储文件名:\r\n\r\n" +
$"<videoTitle>: 视频主标题\r\n" +
$"<pageNumber>: 视频分P序号\r\n" +
$"<pageNumberWithZero>: 视频分P序号(前缀补零)\r\n" +
$"<pageTitle>: 视频分P标题\r\n" +
$"<bvid>: 视频BV号\r\n" +
$"<aid>: 视频aid\r\n" +
$"<cid>: 视频cid\r\n" +
$"<dfn>: 视频清晰度\r\n" +
$"<res>: 视频分辨率\r\n" +
$"<fps>: 视频帧率\r\n" +
$"<videoCodecs>: 视频编码\r\n" +
$"<videoBandwidth>: 视频码率\r\n" +
$"<audioCodecs>: 音频编码\r\n" +
$"<audioBandwidth>: 音频码率\r\n" +
$"<ownerName>: 上传者名称\r\n" +
$"<ownerMid>: 上传者mid\r\n" +
$"<publishDate>: 收藏夹/番剧/合集发布时间\r\n" +
$"<videoDate>: 视频发布时间(分p视频发布时间与<publishDate>相同)\r\n" +
$"<apiType>: API类型(TV/APP/INTL/WEB)\r\n\r\n" +
$"默认为: {Program.SinglePageDefaultSavePath}\r\n");
private readonly static Option<string> MultiFilePattern = new(new string[] { "--multi-file-pattern", "-M" }, $"使用内置变量自定义多P存储文件名:\r\n\r\n默认为: {Program.MultiPageDefaultSavePath}\r\n");
private readonly static Option<string> Host = new(new string[] { "--host" }, "指定BiliPlus host(使用BiliPlus需要access_token, 不需要cookie, 解析服务器能够获取你账号的大部分权限!)");
private readonly static Option<string> EpHost = new(new string[] { "--ep-host" }, "指定BiliPlus EP host(用于代理api.bilibili.com/pgc/view/web/season, 大部分解析服务器不支持代理该接口)");
private readonly static Option<string> Area = new(new string[] { "--area" }, "(hk|tw|th) 使用BiliPlus时必选, 指定BiliPlus area");
private readonly static Option<string> ConfigFile = new(new string[] { "--config-file" }, "读取指定的BBDown本地配置文件(默认为: BBDown.config)");//以下仅为兼容旧版本命令行, 不建议使用
private readonly static Option<string> Aria2cProxy = new(new string[] { "--aria2c-proxy" }, "调用aria2c进行下载时的代理地址配置") { IsHidden = true };
private readonly static Option<bool> OnlyHevc = new(new string[] { "--only-hevc", "-hevc" }, "只下载hevc编码") { IsHidden = true };
private readonly static Option<bool> OnlyAvc = new(new string[] { "--only-avc", "-avc" }, "只下载avc编码") { IsHidden = true };
private readonly static Option<bool> OnlyAv1 = new(new string[] { "--only-av1", "-av1" }, "只下载av1编码") { IsHidden = true };
private readonly static Option<bool> AddDfnSubfix = new(new string[] { "--add-dfn-subfix" }, "为文件加入清晰度后缀, 如XXX[1080P 高码率]") { IsHidden = true };
private readonly static Option<bool> NoPaddingPageNum = new(new string[] { "--no-padding-page-num" }, "不给分P序号补零") { IsHidden = true };
private readonly static Option<bool> BandwithAscending = new(new string[] { "--bandwith-ascending" }, "比特率升序(最小体积优先)") { IsHidden = true };
class MyOptionBinder : BinderBase<MyOption>
protected override MyOption GetBoundValue(BindingContext bindingContext)
{
protected override MyOption GetBoundValue(BindingContext bindingContext)
var option = new MyOption
{
var option = new MyOption
{
Url = bindingContext.ParseResult.GetValueForArgument(Url)
};
if (bindingContext.ParseResult.HasOption(UseTvApi)) option.UseTvApi = bindingContext.ParseResult.GetValueForOption(UseTvApi)!;
if (bindingContext.ParseResult.HasOption(UseAppApi)) option.UseAppApi = bindingContext.ParseResult.GetValueForOption(UseAppApi)!;
if (bindingContext.ParseResult.HasOption(UseIntlApi)) option.UseIntlApi = bindingContext.ParseResult.GetValueForOption(UseIntlApi)!;
if (bindingContext.ParseResult.HasOption(UseMP4box)) option.UseMP4box = bindingContext.ParseResult.GetValueForOption(UseMP4box)!;
if (bindingContext.ParseResult.HasOption(EncodingPriority)) option.EncodingPriority = bindingContext.ParseResult.GetValueForOption(EncodingPriority)!;
if (bindingContext.ParseResult.HasOption(DfnPriority)) option.DfnPriority = bindingContext.ParseResult.GetValueForOption(DfnPriority)!;
if (bindingContext.ParseResult.HasOption(OnlyShowInfo)) option.OnlyShowInfo = bindingContext.ParseResult.GetValueForOption(OnlyShowInfo)!;
if (bindingContext.ParseResult.HasOption(ShowAll)) option.ShowAll = bindingContext.ParseResult.GetValueForOption(ShowAll)!;
if (bindingContext.ParseResult.HasOption(UseAria2c)) option.UseAria2c = bindingContext.ParseResult.GetValueForOption(UseAria2c)!;
if (bindingContext.ParseResult.HasOption(Interactive)) option.Interactive = bindingContext.ParseResult.GetValueForOption(Interactive)!;
if (bindingContext.ParseResult.HasOption(HideStreams)) option.HideStreams = bindingContext.ParseResult.GetValueForOption(HideStreams)!;
if (bindingContext.ParseResult.HasOption(MultiThread)) option.MultiThread = bindingContext.ParseResult.GetValueForOption(MultiThread)!;
if (bindingContext.ParseResult.HasOption(SimplyMux)) option.SimplyMux = bindingContext.ParseResult.GetValueForOption(SimplyMux)!;
if (bindingContext.ParseResult.HasOption(VideoOnly)) option.VideoOnly = bindingContext.ParseResult.GetValueForOption(VideoOnly)!;
if (bindingContext.ParseResult.HasOption(AudioOnly)) option.AudioOnly = bindingContext.ParseResult.GetValueForOption(AudioOnly)!;
if (bindingContext.ParseResult.HasOption(DanmakuOnly)) option.DanmakuOnly = bindingContext.ParseResult.GetValueForOption(DanmakuOnly)!;
if (bindingContext.ParseResult.HasOption(CoverOnly)) option.CoverOnly = bindingContext.ParseResult.GetValueForOption(CoverOnly)!;
if (bindingContext.ParseResult.HasOption(SubOnly)) option.SubOnly = bindingContext.ParseResult.GetValueForOption(SubOnly)!;
if (bindingContext.ParseResult.HasOption(Debug)) option.Debug = bindingContext.ParseResult.GetValueForOption(Debug)!;
if (bindingContext.ParseResult.HasOption(SkipMux)) option.SkipMux = bindingContext.ParseResult.GetValueForOption(SkipMux)!;
if (bindingContext.ParseResult.HasOption(SkipSubtitle)) option.SkipSubtitle = bindingContext.ParseResult.GetValueForOption(SkipSubtitle)!;
if (bindingContext.ParseResult.HasOption(SkipCover)) option.SkipCover = bindingContext.ParseResult.GetValueForOption(SkipCover)!;
if (bindingContext.ParseResult.HasOption(ForceHttp)) option.ForceHttp = bindingContext.ParseResult.GetValueForOption(ForceHttp)!;
if (bindingContext.ParseResult.HasOption(DownloadDanmaku)) option.DownloadDanmaku = bindingContext.ParseResult.GetValueForOption(DownloadDanmaku)!;
if (bindingContext.ParseResult.HasOption(SkipAi)) option.SkipAi = bindingContext.ParseResult.GetValueForOption(SkipAi)!;
if (bindingContext.ParseResult.HasOption(VideoAscending)) option.VideoAscending = bindingContext.ParseResult.GetValueForOption(VideoAscending)!;
if (bindingContext.ParseResult.HasOption(AudioAscending)) option.AudioAscending = bindingContext.ParseResult.GetValueForOption(AudioAscending)!;
if (bindingContext.ParseResult.HasOption(AllowPcdn)) option.AllowPcdn = bindingContext.ParseResult.GetValueForOption(AllowPcdn)!;
if (bindingContext.ParseResult.HasOption(FilePattern)) option.FilePattern = bindingContext.ParseResult.GetValueForOption(FilePattern)!;
if (bindingContext.ParseResult.HasOption(MultiFilePattern)) option.MultiFilePattern = bindingContext.ParseResult.GetValueForOption(MultiFilePattern)!;
if (bindingContext.ParseResult.HasOption(SelectPage)) option.SelectPage = bindingContext.ParseResult.GetValueForOption(SelectPage)!;
if (bindingContext.ParseResult.HasOption(Language)) option.Language = bindingContext.ParseResult.GetValueForOption(Language)!;
if (bindingContext.ParseResult.HasOption(UserAgent)) option.UserAgent = bindingContext.ParseResult.GetValueForOption(UserAgent)!;
if (bindingContext.ParseResult.HasOption(Cookie)) option.Cookie = bindingContext.ParseResult.GetValueForOption(Cookie)!;
if (bindingContext.ParseResult.HasOption(AccessToken)) option.AccessToken = bindingContext.ParseResult.GetValueForOption(AccessToken)!;
if (bindingContext.ParseResult.HasOption(Aria2cArgs)) option.Aria2cArgs = bindingContext.ParseResult.GetValueForOption(Aria2cArgs)!;
if (bindingContext.ParseResult.HasOption(WorkDir)) option.WorkDir = bindingContext.ParseResult.GetValueForOption(WorkDir)!;
if (bindingContext.ParseResult.HasOption(FFmpegPath)) option.FFmpegPath = bindingContext.ParseResult.GetValueForOption(FFmpegPath)!;
if (bindingContext.ParseResult.HasOption(Mp4boxPath)) option.Mp4boxPath = bindingContext.ParseResult.GetValueForOption(Mp4boxPath)!;
if (bindingContext.ParseResult.HasOption(Aria2cPath)) option.Aria2cPath = bindingContext.ParseResult.GetValueForOption(Aria2cPath)!;
if (bindingContext.ParseResult.HasOption(UposHost)) option.UposHost = bindingContext.ParseResult.GetValueForOption(UposHost)!;
if (bindingContext.ParseResult.HasOption(ForceReplaceHost)) option.ForceReplaceHost = bindingContext.ParseResult.GetValueForOption(ForceReplaceHost)!;
if (bindingContext.ParseResult.HasOption(SaveArchivesToFile)) option.SaveArchivesToFile = bindingContext.ParseResult.GetValueForOption(SaveArchivesToFile)!;
if (bindingContext.ParseResult.HasOption(DelayPerPage)) option.DelayPerPage = bindingContext.ParseResult.GetValueForOption(DelayPerPage)!;
if (bindingContext.ParseResult.HasOption(Host)) option.Host = bindingContext.ParseResult.GetValueForOption(Host)!;
if (bindingContext.ParseResult.HasOption(EpHost)) option.EpHost = bindingContext.ParseResult.GetValueForOption(EpHost)!;
if (bindingContext.ParseResult.HasOption(Area)) option.Area = bindingContext.ParseResult.GetValueForOption(Area)!;
if (bindingContext.ParseResult.HasOption(ConfigFile)) option.ConfigFile = bindingContext.ParseResult.GetValueForOption(ConfigFile)!;
if (bindingContext.ParseResult.HasOption(Aria2cProxy)) option.Aria2cProxy = bindingContext.ParseResult.GetValueForOption(Aria2cProxy)!;
if (bindingContext.ParseResult.HasOption(OnlyHevc)) option.OnlyHevc = bindingContext.ParseResult.GetValueForOption(OnlyHevc)!;
if (bindingContext.ParseResult.HasOption(OnlyAvc)) option.OnlyAvc = bindingContext.ParseResult.GetValueForOption(OnlyAvc)!;
if (bindingContext.ParseResult.HasOption(OnlyAv1)) option.OnlyAv1 = bindingContext.ParseResult.GetValueForOption(OnlyAv1)!;
if (bindingContext.ParseResult.HasOption(AddDfnSubfix)) option.AddDfnSubfix = bindingContext.ParseResult.GetValueForOption(AddDfnSubfix)!;
if (bindingContext.ParseResult.HasOption(NoPaddingPageNum)) option.NoPaddingPageNum = bindingContext.ParseResult.GetValueForOption(NoPaddingPageNum)!;
if (bindingContext.ParseResult.HasOption(BandwithAscending)) option.BandwithAscending = bindingContext.ParseResult.GetValueForOption(BandwithAscending)!;
return option;
}
}
public static RootCommand GetRootCommand(Func<MyOption, Task> action)
{
var rootCommand = new RootCommand
{
Url,
UseTvApi,
UseAppApi,
UseIntlApi,
UseMP4box,
EncodingPriority,
DfnPriority,
OnlyShowInfo,
ShowAll,
UseAria2c,
Interactive,
HideStreams,
MultiThread,
VideoOnly,
AudioOnly,
DanmakuOnly,
SubOnly,
CoverOnly,
Debug,
SkipMux,
SkipSubtitle,
SkipCover,
ForceHttp,
DownloadDanmaku,
SkipAi,
VideoAscending,
AudioAscending,
AllowPcdn,
FilePattern,
MultiFilePattern,
SelectPage,
Language,
UserAgent,
Cookie,
AccessToken,
Aria2cArgs,
WorkDir,
FFmpegPath,
Mp4boxPath,
Aria2cPath,
UposHost,
ForceReplaceHost,
SaveArchivesToFile,
DelayPerPage,
Host,
EpHost,
Area,
ConfigFile,
Aria2cProxy,
OnlyHevc,
OnlyAvc,
OnlyAv1,
AddDfnSubfix,
NoPaddingPageNum,
BandwithAscending
Url = bindingContext.ParseResult.GetValueForArgument(Url)
};
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder());
return rootCommand;
if (bindingContext.ParseResult.HasOption(UseTvApi)) option.UseTvApi = bindingContext.ParseResult.GetValueForOption(UseTvApi)!;
if (bindingContext.ParseResult.HasOption(UseAppApi)) option.UseAppApi = bindingContext.ParseResult.GetValueForOption(UseAppApi)!;
if (bindingContext.ParseResult.HasOption(UseIntlApi)) option.UseIntlApi = bindingContext.ParseResult.GetValueForOption(UseIntlApi)!;
if (bindingContext.ParseResult.HasOption(UseMP4box)) option.UseMP4box = bindingContext.ParseResult.GetValueForOption(UseMP4box)!;
if (bindingContext.ParseResult.HasOption(EncodingPriority)) option.EncodingPriority = bindingContext.ParseResult.GetValueForOption(EncodingPriority)!;
if (bindingContext.ParseResult.HasOption(DfnPriority)) option.DfnPriority = bindingContext.ParseResult.GetValueForOption(DfnPriority)!;
if (bindingContext.ParseResult.HasOption(OnlyShowInfo)) option.OnlyShowInfo = bindingContext.ParseResult.GetValueForOption(OnlyShowInfo)!;
if (bindingContext.ParseResult.HasOption(ShowAll)) option.ShowAll = bindingContext.ParseResult.GetValueForOption(ShowAll)!;
if (bindingContext.ParseResult.HasOption(UseAria2c)) option.UseAria2c = bindingContext.ParseResult.GetValueForOption(UseAria2c)!;
if (bindingContext.ParseResult.HasOption(Interactive)) option.Interactive = bindingContext.ParseResult.GetValueForOption(Interactive)!;
if (bindingContext.ParseResult.HasOption(HideStreams)) option.HideStreams = bindingContext.ParseResult.GetValueForOption(HideStreams)!;
if (bindingContext.ParseResult.HasOption(MultiThread)) option.MultiThread = bindingContext.ParseResult.GetValueForOption(MultiThread)!;
if (bindingContext.ParseResult.HasOption(SimplyMux)) option.SimplyMux = bindingContext.ParseResult.GetValueForOption(SimplyMux)!;
if (bindingContext.ParseResult.HasOption(VideoOnly)) option.VideoOnly = bindingContext.ParseResult.GetValueForOption(VideoOnly)!;
if (bindingContext.ParseResult.HasOption(AudioOnly)) option.AudioOnly = bindingContext.ParseResult.GetValueForOption(AudioOnly)!;
if (bindingContext.ParseResult.HasOption(DanmakuOnly)) option.DanmakuOnly = bindingContext.ParseResult.GetValueForOption(DanmakuOnly)!;
if (bindingContext.ParseResult.HasOption(CoverOnly)) option.CoverOnly = bindingContext.ParseResult.GetValueForOption(CoverOnly)!;
if (bindingContext.ParseResult.HasOption(SubOnly)) option.SubOnly = bindingContext.ParseResult.GetValueForOption(SubOnly)!;
if (bindingContext.ParseResult.HasOption(Debug)) option.Debug = bindingContext.ParseResult.GetValueForOption(Debug)!;
if (bindingContext.ParseResult.HasOption(SkipMux)) option.SkipMux = bindingContext.ParseResult.GetValueForOption(SkipMux)!;
if (bindingContext.ParseResult.HasOption(SkipSubtitle)) option.SkipSubtitle = bindingContext.ParseResult.GetValueForOption(SkipSubtitle)!;
if (bindingContext.ParseResult.HasOption(SkipCover)) option.SkipCover = bindingContext.ParseResult.GetValueForOption(SkipCover)!;
if (bindingContext.ParseResult.HasOption(ForceHttp)) option.ForceHttp = bindingContext.ParseResult.GetValueForOption(ForceHttp)!;
if (bindingContext.ParseResult.HasOption(DownloadDanmaku)) option.DownloadDanmaku = bindingContext.ParseResult.GetValueForOption(DownloadDanmaku)!;
if (bindingContext.ParseResult.HasOption(SkipAi)) option.SkipAi = bindingContext.ParseResult.GetValueForOption(SkipAi)!;
if (bindingContext.ParseResult.HasOption(VideoAscending)) option.VideoAscending = bindingContext.ParseResult.GetValueForOption(VideoAscending)!;
if (bindingContext.ParseResult.HasOption(AudioAscending)) option.AudioAscending = bindingContext.ParseResult.GetValueForOption(AudioAscending)!;
if (bindingContext.ParseResult.HasOption(AllowPcdn)) option.AllowPcdn = bindingContext.ParseResult.GetValueForOption(AllowPcdn)!;
if (bindingContext.ParseResult.HasOption(FilePattern)) option.FilePattern = bindingContext.ParseResult.GetValueForOption(FilePattern)!;
if (bindingContext.ParseResult.HasOption(MultiFilePattern)) option.MultiFilePattern = bindingContext.ParseResult.GetValueForOption(MultiFilePattern)!;
if (bindingContext.ParseResult.HasOption(SelectPage)) option.SelectPage = bindingContext.ParseResult.GetValueForOption(SelectPage)!;
if (bindingContext.ParseResult.HasOption(Language)) option.Language = bindingContext.ParseResult.GetValueForOption(Language)!;
if (bindingContext.ParseResult.HasOption(UserAgent)) option.UserAgent = bindingContext.ParseResult.GetValueForOption(UserAgent)!;
if (bindingContext.ParseResult.HasOption(Cookie)) option.Cookie = bindingContext.ParseResult.GetValueForOption(Cookie)!;
if (bindingContext.ParseResult.HasOption(AccessToken)) option.AccessToken = bindingContext.ParseResult.GetValueForOption(AccessToken)!;
if (bindingContext.ParseResult.HasOption(Aria2cArgs)) option.Aria2cArgs = bindingContext.ParseResult.GetValueForOption(Aria2cArgs)!;
if (bindingContext.ParseResult.HasOption(WorkDir)) option.WorkDir = bindingContext.ParseResult.GetValueForOption(WorkDir)!;
if (bindingContext.ParseResult.HasOption(FFmpegPath)) option.FFmpegPath = bindingContext.ParseResult.GetValueForOption(FFmpegPath)!;
if (bindingContext.ParseResult.HasOption(Mp4boxPath)) option.Mp4boxPath = bindingContext.ParseResult.GetValueForOption(Mp4boxPath)!;
if (bindingContext.ParseResult.HasOption(Aria2cPath)) option.Aria2cPath = bindingContext.ParseResult.GetValueForOption(Aria2cPath)!;
if (bindingContext.ParseResult.HasOption(UposHost)) option.UposHost = bindingContext.ParseResult.GetValueForOption(UposHost)!;
if (bindingContext.ParseResult.HasOption(ForceReplaceHost)) option.ForceReplaceHost = bindingContext.ParseResult.GetValueForOption(ForceReplaceHost)!;
if (bindingContext.ParseResult.HasOption(SaveArchivesToFile)) option.SaveArchivesToFile = bindingContext.ParseResult.GetValueForOption(SaveArchivesToFile)!;
if (bindingContext.ParseResult.HasOption(DelayPerPage)) option.DelayPerPage = bindingContext.ParseResult.GetValueForOption(DelayPerPage)!;
if (bindingContext.ParseResult.HasOption(Host)) option.Host = bindingContext.ParseResult.GetValueForOption(Host)!;
if (bindingContext.ParseResult.HasOption(EpHost)) option.EpHost = bindingContext.ParseResult.GetValueForOption(EpHost)!;
if (bindingContext.ParseResult.HasOption(Area)) option.Area = bindingContext.ParseResult.GetValueForOption(Area)!;
if (bindingContext.ParseResult.HasOption(ConfigFile)) option.ConfigFile = bindingContext.ParseResult.GetValueForOption(ConfigFile)!;
if (bindingContext.ParseResult.HasOption(Aria2cProxy)) option.Aria2cProxy = bindingContext.ParseResult.GetValueForOption(Aria2cProxy)!;
if (bindingContext.ParseResult.HasOption(OnlyHevc)) option.OnlyHevc = bindingContext.ParseResult.GetValueForOption(OnlyHevc)!;
if (bindingContext.ParseResult.HasOption(OnlyAvc)) option.OnlyAvc = bindingContext.ParseResult.GetValueForOption(OnlyAvc)!;
if (bindingContext.ParseResult.HasOption(OnlyAv1)) option.OnlyAv1 = bindingContext.ParseResult.GetValueForOption(OnlyAv1)!;
if (bindingContext.ParseResult.HasOption(AddDfnSubfix)) option.AddDfnSubfix = bindingContext.ParseResult.GetValueForOption(AddDfnSubfix)!;
if (bindingContext.ParseResult.HasOption(NoPaddingPageNum)) option.NoPaddingPageNum = bindingContext.ParseResult.GetValueForOption(NoPaddingPageNum)!;
if (bindingContext.ParseResult.HasOption(BandwithAscending)) option.BandwithAscending = bindingContext.ParseResult.GetValueForOption(BandwithAscending)!;
return option;
}
}
}
public static RootCommand GetRootCommand(Func<MyOption, Task> action)
{
var rootCommand = new RootCommand
{
Url,
UseTvApi,
UseAppApi,
UseIntlApi,
UseMP4box,
EncodingPriority,
DfnPriority,
OnlyShowInfo,
ShowAll,
UseAria2c,
Interactive,
HideStreams,
MultiThread,
VideoOnly,
AudioOnly,
DanmakuOnly,
SubOnly,
CoverOnly,
Debug,
SkipMux,
SkipSubtitle,
SkipCover,
ForceHttp,
DownloadDanmaku,
SkipAi,
VideoAscending,
AudioAscending,
AllowPcdn,
FilePattern,
MultiFilePattern,
SelectPage,
Language,
UserAgent,
Cookie,
AccessToken,
Aria2cArgs,
WorkDir,
FFmpegPath,
Mp4boxPath,
Aria2cPath,
UposHost,
ForceReplaceHost,
SaveArchivesToFile,
DelayPerPage,
Host,
EpHost,
Area,
ConfigFile,
Aria2cProxy,
OnlyHevc,
OnlyAvc,
OnlyAv1,
AddDfnSubfix,
NoPaddingPageNum,
BandwithAscending
};
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder());
return rootCommand;
}
}

View File

@@ -1,33 +1,32 @@
using QRCoder;
using System;
namespace BBDown
namespace BBDown;
public class ConsoleQRCode : AbstractQRCode
{
public class ConsoleQRCode : AbstractQRCode
{
public ConsoleQRCode() { }
public ConsoleQRCode() { }
public ConsoleQRCode(QRCodeData data) : base(data) { }
public ConsoleQRCode(QRCodeData data) : base(data) { }
public void GetGraphic() => GetGraphic(ConsoleColor.Black, ConsoleColor.White);
public void GetGraphic() => GetGraphic(ConsoleColor.Black, ConsoleColor.White);
public void GetGraphic(ConsoleColor darkColor, ConsoleColor lightColor)
public void GetGraphic(ConsoleColor darkColor, ConsoleColor lightColor)
{
var previousBackColor = Console.BackgroundColor;
var previousForeColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.White;
for (int y = 0; y < QrCodeData.ModuleMatrix.Count; y++)
{
var previousBackColor = Console.BackgroundColor;
var previousForeColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.White;
for (int y = 0; y < QrCodeData.ModuleMatrix.Count; y++)
for (int x = 0; x < QrCodeData.ModuleMatrix[y].Count; x++)
{
for (int x = 0; x < QrCodeData.ModuleMatrix[y].Count; x++)
{
Console.ForegroundColor = QrCodeData.ModuleMatrix[y][x] ? darkColor : lightColor;
Console.Write("██");
}
Console.BackgroundColor = darkColor;
Console.WriteLine("");
Console.ForegroundColor = QrCodeData.ModuleMatrix[y][x] ? darkColor : lightColor;
Console.Write("██");
}
Console.BackgroundColor = previousBackColor;
Console.ForegroundColor = previousForeColor;
Console.BackgroundColor = darkColor;
Console.WriteLine("");
}
Console.BackgroundColor = previousBackColor;
Console.ForegroundColor = previousForeColor;
}
}
}

View File

@@ -1,69 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BBDown;
namespace BBDown
internal class MyOption
{
internal class MyOption
{
public string Url { get; set; } = default!;
public bool UseTvApi { get; set; }
public bool UseAppApi { get; set; }
public bool UseIntlApi { get; set; }
public bool UseMP4box { get; set; }
public string? EncodingPriority { get; set; }
public string? DfnPriority { get; set; }
public bool OnlyShowInfo { get; set; }
public bool ShowAll { get; set; }
public bool UseAria2c { get; set; }
public bool Interactive { get; set; }
public bool HideStreams { get; set; }
public bool MultiThread { get; set; } = true;
public bool SimplyMux { get; set; } = false;
public bool VideoOnly { get; set; }
public bool AudioOnly { get; set; }
public bool DanmakuOnly { get; set; }
public bool CoverOnly { get; set; }
public bool SubOnly { get; set; }
public bool Debug { get; set; }
public bool SkipMux { get; set; }
public bool SkipSubtitle { get; set; }
public bool SkipCover { get; set; }
public bool ForceHttp { get; set; } = true;
public bool DownloadDanmaku { get; set; } = false;
public bool SkipAi { get; set; } = true;
public bool VideoAscending { get; set; } = false;
public bool AudioAscending { get; set; } = false;
public bool AllowPcdn { get; set; } = false;
public bool ForceReplaceHost { get; set; } = true;
public bool SaveArchivesToFile { get; set; } = false;
public string FilePattern { get; set; } = "";
public string MultiFilePattern { get; set; } = "";
public string SelectPage { get; set; } = "";
public string Language { get; set; } = "";
public string UserAgent { get; set; } = "";
public string Cookie { get; set; } = "";
public string AccessToken { get; set; } = "";
public string Aria2cArgs { get; set; } = "";
public string WorkDir { get; set; } = "";
public string FFmpegPath { get; set; } = "";
public string Mp4boxPath { get; set; } = "";
public string Aria2cPath { get; set; } = "";
public string UposHost { get; set; } = "";
public string DelayPerPage { get; set; } = "0";
public string Host { get; set; } = "api.bilibili.com";
public string EpHost { get; set; } = "api.bilibili.com";
public string Area { get; set; } = "";
public string? ConfigFile { get; set; }
//以下仅为兼容旧版本命令行,不建议使用
public string Aria2cProxy { get; set; } = "";
public bool OnlyHevc { get; set; }
public bool OnlyAvc { get; set; }
public bool OnlyAv1 { get; set; }
public bool AddDfnSubfix { get; set; }
public bool NoPaddingPageNum { get; set; }
public bool BandwithAscending { get; set; }
}
}
public string Url { get; set; } = default!;
public bool UseTvApi { get; set; }
public bool UseAppApi { get; set; }
public bool UseIntlApi { get; set; }
public bool UseMP4box { get; set; }
public string? EncodingPriority { get; set; }
public string? DfnPriority { get; set; }
public bool OnlyShowInfo { get; set; }
public bool ShowAll { get; set; }
public bool UseAria2c { get; set; }
public bool Interactive { get; set; }
public bool HideStreams { get; set; }
public bool MultiThread { get; set; } = true;
public bool SimplyMux { get; set; } = false;
public bool VideoOnly { get; set; }
public bool AudioOnly { get; set; }
public bool DanmakuOnly { get; set; }
public bool CoverOnly { get; set; }
public bool SubOnly { get; set; }
public bool Debug { get; set; }
public bool SkipMux { get; set; }
public bool SkipSubtitle { get; set; }
public bool SkipCover { get; set; }
public bool ForceHttp { get; set; } = true;
public bool DownloadDanmaku { get; set; } = false;
public bool SkipAi { get; set; } = true;
public bool VideoAscending { get; set; } = false;
public bool AudioAscending { get; set; } = false;
public bool AllowPcdn { get; set; } = false;
public bool ForceReplaceHost { get; set; } = true;
public bool SaveArchivesToFile { get; set; } = false;
public string FilePattern { get; set; } = "";
public string MultiFilePattern { get; set; } = "";
public string SelectPage { get; set; } = "";
public string Language { get; set; } = "";
public string UserAgent { get; set; } = "";
public string Cookie { get; set; } = "";
public string AccessToken { get; set; } = "";
public string Aria2cArgs { get; set; } = "";
public string WorkDir { get; set; } = "";
public string FFmpegPath { get; set; } = "";
public string Mp4boxPath { get; set; } = "";
public string Aria2cPath { get; set; } = "";
public string UposHost { get; set; } = "";
public string DelayPerPage { get; set; } = "0";
public string Host { get; set; } = "api.bilibili.com";
public string EpHost { get; set; } = "api.bilibili.com";
public string Area { get; set; } = "";
public string? ConfigFile { get; set; }
//以下仅为兼容旧版本命令行,不建议使用
public string Aria2cProxy { get; set; } = "";
public bool OnlyHevc { get; set; }
public bool OnlyAvc { get; set; }
public bool OnlyAv1 { get; set; }
public bool AddDfnSubfix { get; set; }
public bool NoPaddingPageNum { get; set; }
public bool BandwithAscending { get; set; }
}

View File

@@ -11,502 +11,501 @@ using BBDown.Core;
using BBDown.Core.Entity;
using static BBDown.BBDownDownloadUtil;
namespace BBDown
namespace BBDown;
internal partial class Program
{
internal partial class Program
/// <summary>
/// 兼容旧版本命令行参数并给出警告
/// </summary>
/// <param name="myOption"></param>
private static void HandleDeprecatedOptions(MyOption myOption)
{
/// <summary>
/// 兼容旧版本命令行参数并给出警告
/// </summary>
/// <param name="myOption"></param>
private static void HandleDeprecatedOptions(MyOption myOption)
if (myOption.AddDfnSubfix)
{
if (myOption.AddDfnSubfix)
LogWarn("--add-dfn-subfix 已被弃用, 建议使用 --file-pattern/-F 或 --multi-file-pattern/-M 来自定义输出文件名格式");
if (string.IsNullOrEmpty(myOption.FilePattern) && string.IsNullOrEmpty(myOption.MultiFilePattern))
{
LogWarn("--add-dfn-subfix 已被弃用, 建议使用 --file-pattern/-F 或 --multi-file-pattern/-M 来自定义输出文件名格式");
if (string.IsNullOrEmpty(myOption.FilePattern) && string.IsNullOrEmpty(myOption.MultiFilePattern))
{
SinglePageDefaultSavePath += "[<dfn>]";
MultiPageDefaultSavePath += "[<dfn>]";
LogWarn($"已切换至 -F \"{SinglePageDefaultSavePath}\" -M \"{MultiPageDefaultSavePath}\"");
}
}
if (myOption.Aria2cProxy != "")
{
LogWarn("--aria2c-proxy 已被弃用, 请使用 --aria2c-args 来设置aria2c代理, 本次执行已添加该代理");
myOption.Aria2cArgs += $" --all-proxy=\"{myOption.Aria2cProxy}\"";
}
if (myOption.OnlyHevc)
{
LogWarn("--only-hevc/-hevc 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将hevc设置为最高优先级");
myOption.EncodingPriority = "hevc";
}
if (myOption.OnlyAvc)
{
LogWarn("--only-avc/-avc 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将avc设置为最高优先级");
myOption.EncodingPriority = "avc";
}
if (myOption.OnlyAv1)
{
LogWarn("--only-av1/-av1 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将av1设置为最高优先级");
myOption.EncodingPriority = "av1";
}
if (myOption.NoPaddingPageNum)
{
LogWarn("--no-padding-page-num 已被弃用, 建议使用 --file-pattern/-F 或 --multi-file-pattern/-M 来自定义输出文件名格式");
if (string.IsNullOrEmpty(myOption.FilePattern) && string.IsNullOrEmpty(myOption.MultiFilePattern))
{
MultiPageDefaultSavePath = MultiPageDefaultSavePath.Replace("<pageNumberWithZero>", "<pageNumber>");
LogWarn($"已切换至 -M \"{MultiPageDefaultSavePath}\"");
}
}
if (myOption.BandwithAscending)
{
LogWarn("--bandwith-ascending 已被弃用, 建议使用 --video-ascending 与 --audio-ascending 来指定视频或音频是否升序, 本次执行已将视频与音频均设为升序");
myOption.VideoAscending = true;
myOption.AudioAscending = true;
SinglePageDefaultSavePath += "[<dfn>]";
MultiPageDefaultSavePath += "[<dfn>]";
LogWarn($"已切换至 -F \"{SinglePageDefaultSavePath}\" -M \"{MultiPageDefaultSavePath}\"");
}
}
/// <summary>
/// 解析用户指定的编码优先级
/// </summary>
/// <param name="myOption"></param>
/// <returns></returns>
private static Dictionary<string, byte> ParseEncodingPriority(MyOption myOption, out string firstEncoding)
if (myOption.Aria2cProxy != "")
{
var encodingPriority = new Dictionary<string, byte>();
firstEncoding = "";
if (myOption.EncodingPriority != null)
LogWarn("--aria2c-proxy 已被弃用, 请使用 --aria2c-args 来设置aria2c代理, 本次执行已添加该代理");
myOption.Aria2cArgs += $" --all-proxy=\"{myOption.Aria2cProxy}\"";
}
if (myOption.OnlyHevc)
{
LogWarn("--only-hevc/-hevc 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将hevc设置为最高优先级");
myOption.EncodingPriority = "hevc";
}
if (myOption.OnlyAvc)
{
LogWarn("--only-avc/-avc 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将avc设置为最高优先级");
myOption.EncodingPriority = "avc";
}
if (myOption.OnlyAv1)
{
LogWarn("--only-av1/-av1 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将av1设置为最高优先级");
myOption.EncodingPriority = "av1";
}
if (myOption.NoPaddingPageNum)
{
LogWarn("--no-padding-page-num 已被弃用, 建议使用 --file-pattern/-F 或 --multi-file-pattern/-M 来自定义输出文件名格式");
if (string.IsNullOrEmpty(myOption.FilePattern) && string.IsNullOrEmpty(myOption.MultiFilePattern))
{
var encodingPriorityTemp = myOption.EncodingPriority.Replace("", ",").Split(',').Select(s => s.ToUpper().Trim()).Where(s => !string.IsNullOrEmpty(s));
byte index = 0;
firstEncoding = encodingPriorityTemp.First();
foreach (string encoding in encodingPriorityTemp)
{
if (encodingPriority.ContainsKey(encoding)) { continue; }
encodingPriority[encoding] = index;
index++;
}
MultiPageDefaultSavePath = MultiPageDefaultSavePath.Replace("<pageNumberWithZero>", "<pageNumber>");
LogWarn($"已切换至 -M \"{MultiPageDefaultSavePath}\"");
}
return encodingPriority;
}
if (myOption.BandwithAscending)
{
LogWarn("--bandwith-ascending 已被弃用, 建议使用 --video-ascending 与 --audio-ascending 来指定视频或音频是否升序, 本次执行已将视频与音频均设为升序");
myOption.VideoAscending = true;
myOption.AudioAscending = true;
}
}
/// <summary>
/// 解析用户指定的编码优先级
/// </summary>
/// <param name="myOption"></param>
/// <returns></returns>
private static Dictionary<string, byte> ParseEncodingPriority(MyOption myOption, out string firstEncoding)
{
var encodingPriority = new Dictionary<string, byte>();
firstEncoding = "";
if (myOption.EncodingPriority != null)
{
var encodingPriorityTemp = myOption.EncodingPriority.Replace('', ',').Split(',').Select(s => s.ToUpper().Trim()).Where(s => !string.IsNullOrEmpty(s)).ToList();
byte index = 0;
firstEncoding = encodingPriorityTemp.First();
foreach (string encoding in encodingPriorityTemp)
{
if (encodingPriority.ContainsKey(encoding)) { continue; }
encodingPriority[encoding] = index;
index++;
}
}
return encodingPriority;
}
/// <summary>
/// 解析用户输入的清晰度规格优先级
/// </summary>
/// <param name="myOption"></param>
/// <returns></returns>
private static Dictionary<string, int> ParseDfnPriority(MyOption myOption)
{
var dfnPriority = new Dictionary<string, int>();
if (myOption.DfnPriority != null)
{
var dfnPriorityTemp = myOption.DfnPriority.Replace("", ",").Split(',').Select(s => s.ToUpper().Trim()).Where(s => !string.IsNullOrEmpty(s));
int index = 0;
foreach (string dfn in dfnPriorityTemp)
{
if (dfnPriority.ContainsKey(dfn)) { continue; }
dfnPriority[dfn] = index;
index++;
}
}
return dfnPriority;
}
/// <summary>
/// 寻找并设置所需的二进制文件
/// </summary>
/// <param name="myOption"></param>
/// <exception cref="Exception"></exception>
private static void FindBinaries(MyOption myOption)
{
if (!string.IsNullOrEmpty(myOption.FFmpegPath) && File.Exists(myOption.FFmpegPath))
{
BBDownMuxer.FFMPEG = myOption.FFmpegPath;
}
/// <summary>
/// 解析用户输入的清晰度规格优先级
/// </summary>
/// <param name="myOption"></param>
/// <returns></returns>
private static Dictionary<string, int> ParseDfnPriority(MyOption myOption)
if (!string.IsNullOrEmpty(myOption.Mp4boxPath) && File.Exists(myOption.Mp4boxPath))
{
var dfnPriority = new Dictionary<string, int>();
if (myOption.DfnPriority != null)
{
var dfnPriorityTemp = myOption.DfnPriority.Replace("", ",").Split(',').Select(s => s.ToUpper().Trim()).Where(s => !string.IsNullOrEmpty(s));
int index = 0;
foreach (string dfn in dfnPriorityTemp)
{
if (dfnPriority.ContainsKey(dfn)) { continue; }
dfnPriority[dfn] = index;
index++;
}
}
return dfnPriority;
BBDownMuxer.MP4BOX = myOption.Mp4boxPath;
}
/// <summary>
/// 寻找并设置所需的二进制文件
/// </summary>
/// <param name="myOption"></param>
/// <exception cref="Exception"></exception>
private static void FindBinaries(MyOption myOption)
if (!string.IsNullOrEmpty(myOption.Aria2cPath) && File.Exists(myOption.Aria2cPath))
{
if (!string.IsNullOrEmpty(myOption.FFmpegPath) && File.Exists(myOption.FFmpegPath))
BBDownAria2c.ARIA2C = myOption.Aria2cPath;
}
//寻找ffmpeg或mp4box
if (!myOption.SkipMux)
{
if (myOption.UseMP4box)
{
BBDownMuxer.FFMPEG = myOption.FFmpegPath;
}
if (!string.IsNullOrEmpty(myOption.Mp4boxPath) && File.Exists(myOption.Mp4boxPath))
{
BBDownMuxer.MP4BOX = myOption.Mp4boxPath;
}
if (!string.IsNullOrEmpty(myOption.Aria2cPath) && File.Exists(myOption.Aria2cPath))
{
BBDownAria2c.ARIA2C = myOption.Aria2cPath;
}
//寻找ffmpeg或mp4box
if (!myOption.SkipMux)
{
if (myOption.UseMP4box)
if (string.IsNullOrEmpty(BBDownMuxer.MP4BOX) || !File.Exists(BBDownMuxer.MP4BOX))
{
if (string.IsNullOrEmpty(BBDownMuxer.MP4BOX) || !File.Exists(BBDownMuxer.MP4BOX))
{
var binPath = FindExecutable("mp4box") ?? FindExecutable("MP4box");
if (string.IsNullOrEmpty(binPath))
throw new Exception("找不到可执行的mp4box文件");
BBDownMuxer.MP4BOX = binPath;
}
}
else if (string.IsNullOrEmpty(BBDownMuxer.FFMPEG) || !File.Exists(BBDownMuxer.FFMPEG))
{
var binPath = FindExecutable("ffmpeg");
var binPath = FindExecutable("mp4box") ?? FindExecutable("MP4box");
if (string.IsNullOrEmpty(binPath))
throw new Exception("找不到可执行的ffmpeg文件");
BBDownMuxer.FFMPEG = binPath;
throw new Exception("找不到可执行的mp4box文件");
BBDownMuxer.MP4BOX = binPath;
}
}
//寻找aria2c
if (myOption.UseAria2c)
else if (string.IsNullOrEmpty(BBDownMuxer.FFMPEG) || !File.Exists(BBDownMuxer.FFMPEG))
{
if (string.IsNullOrEmpty(BBDownAria2c.ARIA2C) || !File.Exists(BBDownAria2c.ARIA2C))
{
var binPath = FindExecutable("aria2c");
if (string.IsNullOrEmpty(binPath))
throw new Exception("找不到可执行的aria2c文件");
BBDownAria2c.ARIA2C = binPath;
}
var binPath = FindExecutable("ffmpeg");
if (string.IsNullOrEmpty(binPath))
throw new Exception("找不到可执行的ffmpeg文件");
BBDownMuxer.FFMPEG = binPath;
}
}
/// <summary>
/// 处理有冲突的选项
/// </summary>
/// <param name="myOption"></param>
private static void HandleConflictingOptions(MyOption myOption)
//寻找aria2c
if (myOption.UseAria2c)
{
//手动选择时不能隐藏流
if (myOption.Interactive)
if (string.IsNullOrEmpty(BBDownAria2c.ARIA2C) || !File.Exists(BBDownAria2c.ARIA2C))
{
myOption.HideStreams = false;
var binPath = FindExecutable("aria2c");
if (string.IsNullOrEmpty(binPath))
throw new Exception("找不到可执行的aria2c文件");
BBDownAria2c.ARIA2C = binPath;
}
//audioOnly和videoOnly同时开启则全部忽视
if (myOption.AudioOnly && myOption.VideoOnly)
}
}
/// <summary>
/// 处理有冲突的选项
/// </summary>
/// <param name="myOption"></param>
private static void HandleConflictingOptions(MyOption myOption)
{
//手动选择时不能隐藏流
if (myOption.Interactive)
{
myOption.HideStreams = false;
}
//audioOnly和videoOnly同时开启则全部忽视
if (myOption.AudioOnly && myOption.VideoOnly)
{
myOption.AudioOnly = false;
myOption.VideoOnly = false;
}
if (myOption.SkipSubtitle)
{
myOption.SubOnly = false;
}
}
/// <summary>
/// 设置用户输入的自定义工作目录
/// </summary>
/// <param name="myOption"></param>
private static void ChangeWorkingDir(MyOption myOption)
{
if (!string.IsNullOrEmpty(myOption.WorkDir))
{
//解释环境变量
myOption.WorkDir = Environment.ExpandEnvironmentVariables(myOption.WorkDir);
var dir = Path.GetFullPath(myOption.WorkDir);
if (!Directory.Exists(dir))
{
myOption.AudioOnly = false;
myOption.VideoOnly = false;
Directory.CreateDirectory(dir);
}
if (myOption.SkipSubtitle)
//设置工作目录
Environment.CurrentDirectory = dir;
LogDebug("切换工作目录至:{0}", dir);
}
}
/// <summary>
/// 加载用户的认证信息cookie或token
/// </summary>
/// <param name="myOption"></param>
private static void LoadCredentials(MyOption myOption)
{
if (string.IsNullOrEmpty(Config.COOKIE) && File.Exists(Path.Combine(APP_DIR, "BBDown.data")))
{
Log("加载本地cookie...");
LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDown.data"));
Config.COOKIE = File.ReadAllText(Path.Combine(APP_DIR, "BBDown.data"));
}
if (string.IsNullOrEmpty(Config.TOKEN) && File.Exists(Path.Combine(APP_DIR, "BBDownTV.data")) && myOption.UseTvApi)
{
Log("加载本地token...");
LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDownTV.data"));
Config.TOKEN = File.ReadAllText(Path.Combine(APP_DIR, "BBDownTV.data"));
Config.TOKEN = Config.TOKEN.Replace("access_token=", "");
}
if (string.IsNullOrEmpty(Config.TOKEN) && File.Exists(Path.Combine(APP_DIR, "BBDownApp.data")) && myOption.UseAppApi)
{
Log("加载本地token...");
LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDownApp.data"));
Config.TOKEN = File.ReadAllText(Path.Combine(APP_DIR, "BBDownApp.data"));
Config.TOKEN = Config.TOKEN.Replace("access_token=", "");
}
}
private static object fileLock = new object();
public static void SaveAidToFile(string aid)
{
lock (fileLock)
{
string filePath = Path.Combine(APP_DIR, "BBDown.archives");
LogDebug("文件路径:{0}", filePath);
File.AppendAllText(filePath, $"{aid}|");
}
}
public static bool CheckAidFromFile(string aid)
{
lock (fileLock)
{
string filePath = Path.Combine(APP_DIR, "BBDown.archives");
if (!File.Exists(filePath)) return false;
LogDebug("文件路径:{0}", filePath);
var text = File.ReadAllText(filePath);
return text.Split('|').Any(item => item == aid);
}
}
/// <summary>
/// 获取选中的分P列表
/// </summary>
/// <param name="myOption"></param>
/// <param name="vInfo"></param>
/// <param name="input"></param>
/// <returns></returns>
private static List<string>? GetSelectedPages(MyOption myOption, VInfo vInfo, string input)
{
List<string>? selectedPages = null;
List<Page> pagesInfo = vInfo.PagesInfo;
string selectPage = myOption.SelectPage.ToUpper().Trim().Trim(',');
if (string.IsNullOrEmpty(selectPage))
{
//如果用户没有选择分P, 根据epid或query param来确定某一集
if (!string.IsNullOrEmpty(vInfo.Index))
{
myOption.SubOnly = false;
selectedPages = [vInfo.Index];
Log("程序已自动选择你输入的集数, 如果要下载其他集数请自行指定分P(如可使用-p ALL代表全部)");
}
else if (!string.IsNullOrEmpty(GetQueryString("p", input)))
{
selectedPages = [GetQueryString("p", input)];
Log("程序已自动选择你输入的集数, 如果要下载其他集数请自行指定分P(如可使用-p ALL代表全部)");
}
}
/// <summary>
/// 设置用户输入的自定义工作目录
/// </summary>
/// <param name="myOption"></param>
private static void ChangeWorkingDir(MyOption myOption)
else if (selectPage != "ALL")
{
if (!string.IsNullOrEmpty(myOption.WorkDir))
selectedPages = new List<string>();
//选择最新分P
string lastPage = pagesInfo.Count.ToString();
foreach (string key in new[] { "LAST", "NEW", "LATEST" })
{
//解释环境变量
myOption.WorkDir = Environment.ExpandEnvironmentVariables(myOption.WorkDir);
var dir = Path.GetFullPath(myOption.WorkDir);
if (!Directory.Exists(dir))
selectPage = selectPage.Replace(key, lastPage);
}
try
{
if (selectPage.Contains('-'))
{
Directory.CreateDirectory(dir);
}
//设置工作目录
Environment.CurrentDirectory = dir;
LogDebug("切换工作目录至:{0}", dir);
}
}
/// <summary>
/// 加载用户的认证信息cookie或token
/// </summary>
/// <param name="myOption"></param>
private static void LoadCredentials(MyOption myOption)
{
if (string.IsNullOrEmpty(Config.COOKIE) && File.Exists(Path.Combine(APP_DIR, "BBDown.data")))
{
Log("加载本地cookie...");
LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDown.data"));
Config.COOKIE = File.ReadAllText(Path.Combine(APP_DIR, "BBDown.data"));
}
if (string.IsNullOrEmpty(Config.TOKEN) && File.Exists(Path.Combine(APP_DIR, "BBDownTV.data")) && myOption.UseTvApi)
{
Log("加载本地token...");
LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDownTV.data"));
Config.TOKEN = File.ReadAllText(Path.Combine(APP_DIR, "BBDownTV.data"));
Config.TOKEN = Config.TOKEN.Replace("access_token=", "");
}
if (string.IsNullOrEmpty(Config.TOKEN) && File.Exists(Path.Combine(APP_DIR, "BBDownApp.data")) && myOption.UseAppApi)
{
Log("加载本地token...");
LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDownApp.data"));
Config.TOKEN = File.ReadAllText(Path.Combine(APP_DIR, "BBDownApp.data"));
Config.TOKEN = Config.TOKEN.Replace("access_token=", "");
}
}
private static object fileLock = new object();
public static void SaveAidToFile(string aid)
{
lock (fileLock)
{
string filePath = Path.Combine(APP_DIR, "BBDown.archives");
LogDebug("文件路径:{0}", filePath);
File.AppendAllText(filePath, $"{aid}|");
}
}
public static bool CheckAidFromFile(string aid)
{
lock (fileLock)
{
string filePath = Path.Combine(APP_DIR, "BBDown.archives");
if (!File.Exists(filePath)) return false;
LogDebug("文件路径:{0}", filePath);
var text = File.ReadAllText(filePath);
return text.Split('|').Any(item => item == aid);
}
}
/// <summary>
/// 获取选中的分P列表
/// </summary>
/// <param name="myOption"></param>
/// <param name="vInfo"></param>
/// <param name="input"></param>
/// <returns></returns>
private static List<string>? GetSelectedPages(MyOption myOption, VInfo vInfo, string input)
{
List<string>? selectedPages = null;
List<Page> pagesInfo = vInfo.PagesInfo;
string selectPage = myOption.SelectPage.ToUpper().Trim().Trim(',');
if (string.IsNullOrEmpty(selectPage))
{
//如果用户没有选择分P, 根据epid或query param来确定某一集
if (!string.IsNullOrEmpty(vInfo.Index))
{
selectedPages = new List<string> { vInfo.Index };
Log("程序已自动选择你输入的集数, 如果要下载其他集数请自行指定分P(如可使用-p ALL代表全部)");
}
else if (!string.IsNullOrEmpty(GetQueryString("p", input)))
{
selectedPages = new List<string> { GetQueryString("p", input) };
Log("程序已自动选择你输入的集数, 如果要下载其他集数请自行指定分P(如可使用-p ALL代表全部)");
}
}
else if (selectPage != "ALL")
{
selectedPages = new List<string>();
//选择最新分P
string lastPage = pagesInfo.Count.ToString();
foreach (string key in new string[] { "LAST", "NEW", "LATEST" })
{
selectPage = selectPage.Replace(key, lastPage);
}
try
{
if (selectPage.Contains('-'))
string[] tmp = selectPage.Split('-');
int start = int.Parse(tmp[0]);
int end = int.Parse(tmp[1]);
for (int i = start; i <= end; i++)
{
string[] tmp = selectPage.Split('-');
int start = int.Parse(tmp[0]);
int end = int.Parse(tmp[1]);
for (int i = start; i <= end; i++)
{
selectedPages.Add(i.ToString());
}
}
else
{
foreach (var s in selectPage.Split(','))
{
selectedPages.Add(s);
}
selectedPages.Add(i.ToString());
}
}
catch { LogError("解析分P参数时失败了~"); selectedPages = null; };
}
return selectedPages;
}
/// <summary>
/// 处理CDN域名
/// </summary>
/// <param name="myOption"></param>
/// <param name="video"></param>
/// <param name="audio"></param>
private static void HandlePcdn(MyOption myOption, Video? selectedVideo, Audio? selectedAudio)
{
if (myOption.UposHost == "")
{
//处理PCDN
if (!myOption.AllowPcdn)
else
{
var pcdnReg = PcdnRegex();
if (selectedVideo != null && pcdnReg.IsMatch(selectedVideo.baseUrl))
foreach (var s in selectPage.Split(','))
{
LogWarn($"检测到视频流为PCDN, 尝试强制替换为{BACKUP_HOST}……");
selectedVideo.baseUrl = pcdnReg.Replace(selectedVideo.baseUrl, $"://{BACKUP_HOST}/");
}
if (selectedAudio != null && pcdnReg.IsMatch(selectedAudio.baseUrl))
{
LogWarn($"检测到音频流为PCDN, 尝试强制替换为{BACKUP_HOST}……");
selectedAudio.baseUrl = pcdnReg.Replace(selectedAudio.baseUrl, $"://{BACKUP_HOST}/");
selectedPages.Add(s);
}
}
var akamReg = AkamRegex();
if (selectedVideo != null && Config.AREA != "" && selectedVideo.baseUrl.Contains("akamaized.net"))
{
LogWarn($"检测到视频流为外国源, 尝试强制替换为{BACKUP_HOST}……");
selectedVideo.baseUrl = akamReg.Replace(selectedVideo.baseUrl, $"://{BACKUP_HOST}/");
}
if (selectedAudio != null && Config.AREA != "" && selectedAudio.baseUrl.Contains("akamaized.net"))
{
LogWarn($"检测到音频流为外国源, 尝试强制替换为{BACKUP_HOST}……");
selectedAudio.baseUrl = akamReg.Replace(selectedAudio.baseUrl, $"://{BACKUP_HOST}/");
}
}
else
{
if (selectedVideo != null)
{
LogWarn($"尝试将视频流强制替换为{myOption.UposHost}……");
selectedVideo.baseUrl = UposRegex().Replace(selectedVideo.baseUrl, $"://{myOption.UposHost}/");
}
if (selectedAudio != null)
{
LogWarn($"尝试将音频流强制替换为{myOption.UposHost}……");
selectedAudio.baseUrl = UposRegex().Replace(selectedAudio.baseUrl, $"://{myOption.UposHost}/");
}
}
catch { LogError("解析分P参数时失败了~"); selectedPages = null; };
}
/// <summary>
/// 打印解析到的各个轨道信息
/// </summary>
/// <param name="parsedResult"></param>
/// <param name="pageDur"></param>
private static void PrintAllTracksInfo(ParsedResult parsedResult, int pageDur, bool onlyShowInfo)
return selectedPages;
}
/// <summary>
/// 处理CDN域名
/// </summary>
/// <param name="myOption"></param>
/// <param name="video"></param>
/// <param name="audio"></param>
private static void HandlePcdn(MyOption myOption, Video? selectedVideo, Audio? selectedAudio)
{
if (myOption.UposHost == "")
{
if (parsedResult.BackgroundAudioTracks.Any() && parsedResult.RoleAudioList.Any())
//处理PCDN
if (!myOption.AllowPcdn)
{
Log($"共计{parsedResult.BackgroundAudioTracks.Count}条背景音频流.");
int index = 0;
foreach (var a in parsedResult.BackgroundAudioTracks)
var pcdnReg = PcdnRegex();
if (selectedVideo != null && pcdnReg.IsMatch(selectedVideo.baseUrl))
{
int pDur = pageDur == 0 ? a.dur : pageDur;
LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false);
LogWarn($"检测到视频流为PCDN, 尝试强制替换为{BACKUP_HOST}……");
selectedVideo.baseUrl = pcdnReg.Replace(selectedVideo.baseUrl, $"://{BACKUP_HOST}/");
}
Log($"共计{parsedResult.RoleAudioList.Count}条配音, 每条包含{parsedResult.RoleAudioList[0].audio.Count}条配音流.");
index = 0;
foreach (var a in parsedResult.RoleAudioList[0].audio)
if (selectedAudio != null && pcdnReg.IsMatch(selectedAudio.baseUrl))
{
int pDur = pageDur == 0 ? a.dur : pageDur;
LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false);
LogWarn($"检测到音频流为PCDN, 尝试强制替换为{BACKUP_HOST}……");
selectedAudio.baseUrl = pcdnReg.Replace(selectedAudio.baseUrl, $"://{BACKUP_HOST}/");
}
}
//展示所有的音视频流信息
if (parsedResult.VideoTracks.Any())
var akamReg = AkamRegex();
if (selectedVideo != null && Config.AREA != "" && selectedVideo.baseUrl.Contains("akamaized.net"))
{
Log($"共计{parsedResult.VideoTracks.Count}条视频流.");
int index = 0;
foreach (var v in parsedResult.VideoTracks)
{
int pDur = pageDur == 0 ? v.dur : pageDur;
var size = v.size > 0 ? v.size : pDur * v.bandwith * 1024 / 8;
LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [{v.bandwith} kbps] [~{FormatFileSize(size)}]".Replace("[] ", ""), false);
if (onlyShowInfo) Console.WriteLine(v.baseUrl);
}
LogWarn($"检测到视频流为外国源, 尝试强制替换为{BACKUP_HOST}……");
selectedVideo.baseUrl = akamReg.Replace(selectedVideo.baseUrl, $"://{BACKUP_HOST}/");
}
if (parsedResult.AudioTracks.Any())
if (selectedAudio != null && Config.AREA != "" && selectedAudio.baseUrl.Contains("akamaized.net"))
{
Log($"共计{parsedResult.AudioTracks.Count}条音频流.");
int index = 0;
foreach (var a in parsedResult.AudioTracks)
{
int pDur = pageDur == 0 ? a.dur : pageDur;
LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false);
if (onlyShowInfo) Console.WriteLine(a.baseUrl);
}
LogWarn($"检测到音频流为外国源, 尝试强制替换为{BACKUP_HOST}……");
selectedAudio.baseUrl = akamReg.Replace(selectedAudio.baseUrl, $"://{BACKUP_HOST}/");
}
}
private static void PrintSelectedTrackInfo(Video? selectedVideo, Audio? selectedAudio, int pageDur)
else
{
if (selectedVideo != null)
{
int pDur = pageDur == 0 ? selectedVideo.dur : pageDur;
var size = selectedVideo.size > 0 ? selectedVideo.size : pDur * selectedVideo.bandwith * 1024 / 8;
LogColor($"[视频] [{selectedVideo.dfn}] [{selectedVideo.res}] [{selectedVideo.codecs}] [{selectedVideo.fps}] [{selectedVideo.bandwith} kbps] [~{FormatFileSize(size)}]".Replace("[] ", ""), false);
LogWarn($"尝试将视频流强制替换为{myOption.UposHost}……");
selectedVideo.baseUrl = UposRegex().Replace(selectedVideo.baseUrl, $"://{myOption.UposHost}/");
}
if (selectedAudio != null)
{
int pDur = pageDur == 0 ? selectedAudio.dur : pageDur;
LogColor($"[音频] [{selectedAudio.codecs}] [{selectedAudio.bandwith} kbps] [~{FormatFileSize(pDur * selectedAudio.bandwith * 1024 / 8)}]", false);
LogWarn($"尝试将音频流强制替换为{myOption.UposHost}……");
selectedAudio.baseUrl = UposRegex().Replace(selectedAudio.baseUrl, $"://{myOption.UposHost}/");
}
}
/// <summary>
/// 引导用户进行手动选择轨道
/// </summary>
/// <param name="parsedResult"></param>
/// <param name="vIndex"></param>
/// <param name="aIndex"></param>
private static void SelectTrackManually(ParsedResult parsedResult, ref int vIndex, ref int aIndex)
{
if (parsedResult.VideoTracks.Any())
{
Log("请选择一条视频流(输入序号): ", false);
Console.ForegroundColor = ConsoleColor.Cyan;
vIndex = Convert.ToInt32(Console.ReadLine());
if (vIndex > parsedResult.VideoTracks.Count || vIndex < 0) vIndex = 0;
Console.ResetColor();
}
if (parsedResult.AudioTracks.Any())
{
Log("请选择一条音频流(输入序号): ", false);
Console.ForegroundColor = ConsoleColor.Cyan;
aIndex = Convert.ToInt32(Console.ReadLine());
if (aIndex > parsedResult.AudioTracks.Count || aIndex < 0) aIndex = 0;
Console.ResetColor();
}
}
/// <summary>
/// 下载轨道
/// </summary>
/// <returns></returns>
private static async Task DownloadTrackAsync(string url, string destPath, DownloadConfig downloadConfig, bool video)
{
if (downloadConfig.MultiThread && !url.Contains("-cmcc-"))
{
// 下载前先清理残片
foreach (var file in new DirectoryInfo(Path.GetDirectoryName(destPath)!).EnumerateFiles("*.?clip")) file.Delete();
await MultiThreadDownloadFileAsync(url, destPath, downloadConfig);
Log($"合并{(video ? "" : "")}分片...");
CombineMultipleFilesIntoSingleFile(GetFiles(Path.GetDirectoryName(destPath)!, $".{(video ? "v" : "a")}clip"), destPath);
Log("清理分片...");
foreach (var file in new DirectoryInfo(Path.GetDirectoryName(destPath)!).EnumerateFiles("*.?clip")) file.Delete();
}
else
{
if (downloadConfig.MultiThread && url.Contains("-cmcc-"))
{
LogWarn("检测到cmcc域名cdn, 已经禁用多线程");
downloadConfig.ForceHttp = false;
}
await DownloadFile(url, destPath, downloadConfig);
}
}
[GeneratedRegex("://.*:\\d+/")]
private static partial Regex PcdnRegex();
[GeneratedRegex("://.*akamaized\\.net/")]
private static partial Regex AkamRegex();
[GeneratedRegex("://[^/]+/")]
private static partial Regex UposRegex();
}
}
/// <summary>
/// 打印解析到的各个轨道信息
/// </summary>
/// <param name="parsedResult"></param>
/// <param name="pageDur"></param>
private static void PrintAllTracksInfo(ParsedResult parsedResult, int pageDur, bool onlyShowInfo)
{
if (parsedResult.BackgroundAudioTracks.Any() && parsedResult.RoleAudioList.Any())
{
Log($"共计{parsedResult.BackgroundAudioTracks.Count}条背景音频流.");
int index = 0;
foreach (var a in parsedResult.BackgroundAudioTracks)
{
int pDur = pageDur == 0 ? a.dur : pageDur;
LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false);
}
Log($"共计{parsedResult.RoleAudioList.Count}条配音, 每条包含{parsedResult.RoleAudioList[0].audio.Count}条配音流.");
index = 0;
foreach (var a in parsedResult.RoleAudioList[0].audio)
{
int pDur = pageDur == 0 ? a.dur : pageDur;
LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false);
}
}
//展示所有的音视频流信息
if (parsedResult.VideoTracks.Any())
{
Log($"共计{parsedResult.VideoTracks.Count}条视频流.");
int index = 0;
foreach (var v in parsedResult.VideoTracks)
{
int pDur = pageDur == 0 ? v.dur : pageDur;
var size = v.size > 0 ? v.size : pDur * v.bandwith * 1024 / 8;
LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [{v.bandwith} kbps] [~{FormatFileSize(size)}]".Replace("[] ", ""), false);
if (onlyShowInfo) Console.WriteLine(v.baseUrl);
}
}
if (parsedResult.AudioTracks.Any())
{
Log($"共计{parsedResult.AudioTracks.Count}条音频流.");
int index = 0;
foreach (var a in parsedResult.AudioTracks)
{
int pDur = pageDur == 0 ? a.dur : pageDur;
LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false);
if (onlyShowInfo) Console.WriteLine(a.baseUrl);
}
}
}
private static void PrintSelectedTrackInfo(Video? selectedVideo, Audio? selectedAudio, int pageDur)
{
if (selectedVideo != null)
{
int pDur = pageDur == 0 ? selectedVideo.dur : pageDur;
var size = selectedVideo.size > 0 ? selectedVideo.size : pDur * selectedVideo.bandwith * 1024 / 8;
LogColor($"[视频] [{selectedVideo.dfn}] [{selectedVideo.res}] [{selectedVideo.codecs}] [{selectedVideo.fps}] [{selectedVideo.bandwith} kbps] [~{FormatFileSize(size)}]".Replace("[] ", ""), false);
}
if (selectedAudio != null)
{
int pDur = pageDur == 0 ? selectedAudio.dur : pageDur;
LogColor($"[音频] [{selectedAudio.codecs}] [{selectedAudio.bandwith} kbps] [~{FormatFileSize(pDur * selectedAudio.bandwith * 1024 / 8)}]", false);
}
}
/// <summary>
/// 引导用户进行手动选择轨道
/// </summary>
/// <param name="parsedResult"></param>
/// <param name="vIndex"></param>
/// <param name="aIndex"></param>
private static void SelectTrackManually(ParsedResult parsedResult, ref int vIndex, ref int aIndex)
{
if (parsedResult.VideoTracks.Any())
{
Log("请选择一条视频流(输入序号): ", false);
Console.ForegroundColor = ConsoleColor.Cyan;
vIndex = Convert.ToInt32(Console.ReadLine());
if (vIndex > parsedResult.VideoTracks.Count || vIndex < 0) vIndex = 0;
Console.ResetColor();
}
if (parsedResult.AudioTracks.Any())
{
Log("请选择一条音频流(输入序号): ", false);
Console.ForegroundColor = ConsoleColor.Cyan;
aIndex = Convert.ToInt32(Console.ReadLine());
if (aIndex > parsedResult.AudioTracks.Count || aIndex < 0) aIndex = 0;
Console.ResetColor();
}
}
/// <summary>
/// 下载轨道
/// </summary>
/// <returns></returns>
private static async Task DownloadTrackAsync(string url, string destPath, DownloadConfig downloadConfig, bool video)
{
if (downloadConfig.MultiThread && !url.Contains("-cmcc-"))
{
// 下载前先清理残片
foreach (var file in new DirectoryInfo(Path.GetDirectoryName(destPath)!).EnumerateFiles("*.?clip")) file.Delete();
await MultiThreadDownloadFileAsync(url, destPath, downloadConfig);
Log($"合并{(video ? "" : "")}分片...");
CombineMultipleFilesIntoSingleFile(GetFiles(Path.GetDirectoryName(destPath)!, $".{(video ? "v" : "a")}clip"), destPath);
Log("清理分片...");
foreach (var file in new DirectoryInfo(Path.GetDirectoryName(destPath)!).EnumerateFiles("*.?clip")) file.Delete();
}
else
{
if (downloadConfig.MultiThread && url.Contains("-cmcc-"))
{
LogWarn("检测到cmcc域名cdn, 已经禁用多线程");
downloadConfig.ForceHttp = false;
}
await DownloadFile(url, destPath, downloadConfig);
}
}
[GeneratedRegex("://.*:\\d+/")]
private static partial Regex PcdnRegex();
[GeneratedRegex("://.*akamaized\\.net/")]
private static partial Regex AkamRegex();
[GeneratedRegex("://[^/]+/")]
private static partial Regex UposRegex();
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,158 +5,157 @@ using System.Threading;
/**
* From https://gist.github.com/DanielSWolf/0ab6a96899cc5377bf54
*/
namespace BBDown
namespace BBDown;
class ProgressBar : IDisposable, IProgress<double>
{
class ProgressBar : IDisposable, IProgress<double>
private const int blockCount = 40;
private readonly TimeSpan animationInterval = TimeSpan.FromSeconds(1.0 / 8);
private const string animation = @"|/-\";
private readonly Timer timer;
private double currentProgress = 0;
private string currentText = string.Empty;
private bool disposed = false;
private int animationIndex = 0;
//速度计算
private readonly TimeSpan speedCalcInterval = TimeSpan.FromSeconds(1);
private long lastDownloadedBytes = 0;
private long downloadedBytes = 0;
private string speedString = "";
private readonly Timer speedTimer;
//服务器模式使用,更新下载任务的进度
private DownloadTask? RelatedTask = null;
public ProgressBar(DownloadTask? task = null)
{
private const int blockCount = 40;
private readonly TimeSpan animationInterval = TimeSpan.FromSeconds(1.0 / 8);
private const string animation = @"|/-\";
private readonly Timer timer;
private double currentProgress = 0;
private string currentText = string.Empty;
private bool disposed = false;
private int animationIndex = 0;
//速度计算
private readonly TimeSpan speedCalcInterval = TimeSpan.FromSeconds(1);
private long lastDownloadedBytes = 0;
private long downloadedBytes = 0;
private string speedString = "";
private readonly Timer speedTimer;
//服务器模式使用,更新下载任务的进度
private DownloadTask? RelatedTask = null;
public ProgressBar(DownloadTask? task = null)
timer = new Timer(TimerHandler);
speedTimer = new Timer(SpeedTimerHandler);
if (task is not null) RelatedTask = task;
// A progress bar is only for temporary display in a console window.
// If the console output is redirected to a file, draw nothing.
// Otherwise, we'll end up with a lot of garbage in the target file.
// However, if this progressbar is for a server download task,
// we still need it to report progress no matter where stdout is redirected.
// The prevention of writing garbage should be controlled on the methods do the actual writing.
if (!Console.IsOutputRedirected || RelatedTask is not null)
{
timer = new Timer(TimerHandler);
speedTimer = new Timer(SpeedTimerHandler);
if (task is not null) RelatedTask = task;
// A progress bar is only for temporary display in a console window.
// If the console output is redirected to a file, draw nothing.
// Otherwise, we'll end up with a lot of garbage in the target file.
// However, if this progressbar is for a server download task,
// we still need it to report progress no matter where stdout is redirected.
// The prevention of writing garbage should be controlled on the methods do the actual writing.
if (!Console.IsOutputRedirected || RelatedTask is not null)
{
ResetTimer();
ResetSpeedTimer();
ResetTimer();
ResetSpeedTimer();
}
}
public void Report(double value)
{
// Make sure value is in [0..1] range
value = Math.Max(0, Math.Min(1, value));
Interlocked.Exchange(ref currentProgress, value);
}
public void Report(double value, long bytesCount)
{
// Make sure value is in [0..1] range
value = Math.Max(0, Math.Min(1, value));
Interlocked.Exchange(ref currentProgress, value);
Interlocked.Exchange(ref downloadedBytes, bytesCount);
}
private void SpeedTimerHandler(object? state)
{
lock (speedTimer)
{
if (disposed) return;
if (downloadedBytes > 0 && downloadedBytes - lastDownloadedBytes > 0)
{
var delta = downloadedBytes - lastDownloadedBytes;
speedString = " - " + BBDownUtil.FormatFileSize(delta) + "/s";
lastDownloadedBytes = downloadedBytes;
if (RelatedTask is not null)
{
RelatedTask.DownloadSpeed = delta;
RelatedTask.TotalDownloadedBytes += delta;
}
}
ResetSpeedTimer();
}
}
private void TimerHandler(object? state)
{
lock (timer)
{
if (disposed) return;
int progressBlockCount = (int)(currentProgress * blockCount);
int percent = (int)(currentProgress * 100);
string text = string.Format(" [{0}{1}] {2,3}% {3}{4}",
new string('#', progressBlockCount), new string('-', blockCount - progressBlockCount),
percent,
animation[animationIndex++ % animation.Length],
speedString);
UpdateText(text);
if (RelatedTask is not null)
{
RelatedTask.Progress = currentProgress;
}
ResetTimer();
}
}
private void UpdateText(string text)
{
// Write nothing when output is redirected
if (Console.IsOutputRedirected) return;
// Get length of common portion
int commonPrefixLength = 0;
int commonLength = Math.Min(currentText.Length, text.Length);
while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength])
{
commonPrefixLength++;
}
// Backtrack to the first differing character
StringBuilder outputBuilder = new();
outputBuilder.Append('\b', currentText.Length - commonPrefixLength);
// Output new suffix
outputBuilder.Append(text[commonPrefixLength..]);
// If the new text is shorter than the old one: delete overlapping characters
int overlapCount = currentText.Length - text.Length;
if (overlapCount > 0)
{
outputBuilder.Append(' ', overlapCount);
outputBuilder.Append('\b', overlapCount);
}
Console.Write(outputBuilder);
currentText = text;
}
private void ResetTimer()
{
timer.Change(animationInterval, TimeSpan.FromMilliseconds(-1));
}
private void ResetSpeedTimer()
{
speedTimer.Change(speedCalcInterval, TimeSpan.FromMilliseconds(-1));
}
public void Dispose()
{
lock (timer)
{
disposed = true;
UpdateText(string.Empty);
}
}
}
}
public void Report(double value)
{
// Make sure value is in [0..1] range
value = Math.Max(0, Math.Min(1, value));
Interlocked.Exchange(ref currentProgress, value);
}
public void Report(double value, long bytesCount)
{
// Make sure value is in [0..1] range
value = Math.Max(0, Math.Min(1, value));
Interlocked.Exchange(ref currentProgress, value);
Interlocked.Exchange(ref downloadedBytes, bytesCount);
}
private void SpeedTimerHandler(object? state)
{
lock (speedTimer)
{
if (disposed) return;
if (downloadedBytes > 0 && downloadedBytes - lastDownloadedBytes > 0)
{
var delta = downloadedBytes - lastDownloadedBytes;
speedString = " - " + BBDownUtil.FormatFileSize(delta) + "/s";
lastDownloadedBytes = downloadedBytes;
if (RelatedTask is not null)
{
RelatedTask.DownloadSpeed = delta;
RelatedTask.TotalDownloadedBytes += delta;
}
}
ResetSpeedTimer();
}
}
private void TimerHandler(object? state)
{
lock (timer)
{
if (disposed) return;
int progressBlockCount = (int)(currentProgress * blockCount);
int percent = (int)(currentProgress * 100);
string text = string.Format(" [{0}{1}] {2,3}% {3}{4}",
new string('#', progressBlockCount), new string('-', blockCount - progressBlockCount),
percent,
animation[animationIndex++ % animation.Length],
speedString);
UpdateText(text);
if (RelatedTask is not null)
{
RelatedTask.Progress = currentProgress;
}
ResetTimer();
}
}
private void UpdateText(string text)
{
// Write nothing when output is redirected
if (Console.IsOutputRedirected) return;
// Get length of common portion
int commonPrefixLength = 0;
int commonLength = Math.Min(currentText.Length, text.Length);
while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength])
{
commonPrefixLength++;
}
// Backtrack to the first differing character
StringBuilder outputBuilder = new();
outputBuilder.Append('\b', currentText.Length - commonPrefixLength);
// Output new suffix
outputBuilder.Append(text[commonPrefixLength..]);
// If the new text is shorter than the old one: delete overlapping characters
int overlapCount = currentText.Length - text.Length;
if (overlapCount > 0)
{
outputBuilder.Append(' ', overlapCount);
outputBuilder.Append('\b', overlapCount);
}
Console.Write(outputBuilder);
currentText = text;
}
private void ResetTimer()
{
timer.Change(animationInterval, TimeSpan.FromMilliseconds(-1));
}
private void ResetSpeedTimer()
{
speedTimer.Change(speedCalcInterval, TimeSpan.FromMilliseconds(-1));
}
public void Dispose()
{
lock (timer)
{
disposed = true;
UpdateText(string.Empty);
}
}
}

View File

@@ -1,7 +1,7 @@
[![img](https://img.shields.io/github/stars/nilaoda/BBDown?label=%E7%82%B9%E8%B5%9E)](https://github.com/nilaoda/BBDown) [![img](https://img.shields.io/github/last-commit/nilaoda/BBDown?label=%E6%9C%80%E8%BF%91%E6%8F%90%E4%BA%A4)](https://github.com/nilaoda/BBDown) [![img](https://img.shields.io/github/release/nilaoda/BBDown?label=%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)](https://github.com/nilaoda/BBDown/releases) [![img](https://img.shields.io/github/license/nilaoda/BBDown?label=%E8%AE%B8%E5%8F%AF%E8%AF%81)](https://github.com/nilaoda/BBDown) [![Build Latest](https://github.com/nilaoda/BBDown/actions/workflows/build_latest.yml/badge.svg)](https://github.com/nilaoda/BBDown/actions/workflows/build_latest.yml)
# BBDown
命令行式哔哩哔哩下载器. Bilibili Downloader.
命令行式哔哩哔哩下载器. Bilibili Downloader.
# 注意
本软件混流时需要外部程序: