mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
Update On Sun Nov 10 19:32:11 CET 2024
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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 流畅" }
|
||||
};
|
||||
}
|
@@ -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; //底部弹幕
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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; }
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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");
|
||||
|
@@ -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)}\"");
|
||||
}
|
||||
}
|
@@ -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("配置文件读取异常,忽略");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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:");
|
||||
}
|
||||
}
|
@@ -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); }
|
||||
}
|
||||
}
|
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
[](https://github.com/nilaoda/BBDown) [](https://github.com/nilaoda/BBDown) [](https://github.com/nilaoda/BBDown/releases) [](https://github.com/nilaoda/BBDown) [](https://github.com/nilaoda/BBDown/actions/workflows/build_latest.yml)
|
||||
|
||||
# BBDown
|
||||
一款命令行式哔哩哔哩下载器. Bilibili Downloader.
|
||||
一个命令行式哔哩哔哩下载器. Bilibili Downloader.
|
||||
|
||||
# 注意
|
||||
本软件混流时需要外部程序:
|
||||
|
Reference in New Issue
Block a user