From ddb9f1e7c51034070456f668b5416998140143f9 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Oct 2023 00:56:23 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0api=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BBDown.sln | 62 +- BBDown/BBDown.csproj | 2 +- BBDown/BBDownApiServer.cs | 183 +++++ BBDown/BBDownDownloadUtil.cs | 5 +- BBDown/Program.cs | 919 ++++++++++++++------------ BBDown/ProgressBar.cs | 26 +- BBDown/Properties/launchSettings.json | 21 + 7 files changed, 745 insertions(+), 473 deletions(-) create mode 100644 BBDown/BBDownApiServer.cs create mode 100644 BBDown/Properties/launchSettings.json diff --git a/BBDown.sln b/BBDown.sln index 47ce2b7f7..a94635ad5 100644 --- a/BBDown.sln +++ b/BBDown.sln @@ -1,31 +1,31 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32210.308 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown", "BBDown\BBDown.csproj", "{0D491417-BFEA-481B-AE1A-3BE69172D818}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown.Core", "BBDown.Core\BBDown.Core.csproj", "{932F1279-4AD3-4226-97C6-B0997B1568AC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.Build.0 = Release|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {17E08D9B-5969-4301-9674-ADB3EA8E5DF1} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32210.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown", "BBDown\BBDown.csproj", "{0D491417-BFEA-481B-AE1A-3BE69172D818}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown.Core", "BBDown.Core\BBDown.Core.csproj", "{932F1279-4AD3-4226-97C6-B0997B1568AC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.Build.0 = Release|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {17E08D9B-5969-4301-9674-ADB3EA8E5DF1} + EndGlobalSection +EndGlobal diff --git a/BBDown/BBDown.csproj b/BBDown/BBDown.csproj index b43e13427..5dfbb6bdc 100644 --- a/BBDown/BBDown.csproj +++ b/BBDown/BBDown.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/BBDown/BBDownApiServer.cs b/BBDown/BBDownApiServer.cs new file mode 100644 index 000000000..9e35b8523 --- /dev/null +++ b/BBDown/BBDownApiServer.cs @@ -0,0 +1,183 @@ +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; +using System.Threading.Tasks; +using BBDown.Core; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +namespace BBDown; + +public class BBDownApiServer +{ + private WebApplication? app; + private List runningTasks = []; + private List finishedTasks = []; + + public void SetUpServer() + { + if (app is not null) return; + var builder = WebApplication.CreateSlimBuilder(); + builder.Services.ConfigureHttpJsonOptions((options) => + { + options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, AppJsonSerializerContext.Default); + }); + app = builder.Build(); + var taskStatusApi = app.MapGroup("/get-tasks"); + taskStatusApi.MapGet("/", handler: () => Results.Json(new DownloadTaskCollection(runningTasks, finishedTasks), AppJsonSerializerContext.Default.DownloadTaskCollection)); + taskStatusApi.MapGet("/running", handler: () => Results.Json(runningTasks, AppJsonSerializerContext.Default.ListDownloadTask)); + taskStatusApi.MapGet("/finished", handler: () => Results.Json(finishedTasks, AppJsonSerializerContext.Default.ListDownloadTask)); + taskStatusApi.MapGet("/{id}", (string id) => + { + var task = finishedTasks.FirstOrDefault(a => a.Aid == id); + var rtask = runningTasks.FirstOrDefault(a => a.Aid == id); + if (rtask is not null) task = rtask; + if (task is null) + { + return Results.NotFound(); + } + else + { + return Results.Json(task, AppJsonSerializerContext.Default.DownloadTask); + } + }); + app.MapPost("/add-task", (MyOptionBindingResult bindingResult) => + { + if (!bindingResult.IsValid) + { + //var exception = bindingResult.Exception; + return Results.Problem("输入有误"); + } + var req = bindingResult.Result; + AddDownloadTask(req); + return Results.Ok(); + }); + var finishedRemovalApi = app.MapGroup("remove-finished"); + finishedRemovalApi.MapGet("/", () => finishedTasks.RemoveAll(t => true)); + finishedRemovalApi.MapGet("/failed", () => finishedTasks.RemoveAll(t => !t.IsSuccessful)); + finishedRemovalApi.MapGet("/{id}", (string id) => finishedTasks.RemoveAll(t => t.Aid == id)); + } + + public void Run(string url) + { + if (app is null) return; + bool result = Uri.TryCreate(url, UriKind.Absolute, out Uri? uriResult) + && uriResult.Scheme == Uri.UriSchemeHttp; + if (!result) + { + Console.BackgroundColor = ConsoleColor.Red; + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine($"{url}不是合法的http URL,url示例:http://0.0.0.0:5000"); + Console.WriteLine("如果您需要https,请额外配置反向代理"); + Console.ResetColor(); + Console.WriteLine(); + Thread.Sleep(1); + Environment.Exit(1); + } + app.Run(url); + } + + private async Task AddDownloadTask(MyOption option) + { + var aid = await BBDownUtil.GetAvIdAsync(option.Url); + if (runningTasks.Any(task => task.Aid == aid)) return; + var task = new DownloadTask(aid, option.Url, DateTimeOffset.Now.ToUnixTimeSeconds()); + runningTasks.Add(task); + try + { + var (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, input, savePathFormat, lang, aidOri, delay) = Program.SetUpWork(option); + var (fetchedAid, vInfo, apiType) = await Program.GetVideoInfo(option, aidOri, input); + task.Title = vInfo.Title; + task.Pic = vInfo.Pic; + task.VideoPubTime = vInfo.PubTime; + await Program.DownloadPage(option, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, + input, savePathFormat, lang, fetchedAid, delay, apiType, task); + task.IsSuccessful = true; + } + catch (Exception e) + { + Console.BackgroundColor = ConsoleColor.Red; + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine($"{aid}下载失败"); + var msg = Config.DEBUG_LOG ? e.ToString() : e.Message; + Console.Write($"{msg}{Environment.NewLine}请尝试升级到最新版本后重试!"); + Console.ResetColor(); + Console.WriteLine(); + } + task.TaskFinishTime = DateTimeOffset.Now.ToUnixTimeSeconds(); + if (task.IsSuccessful) + { + task.Progress = 1f; + task.DownloadSpeed = (double)(task.TotalDownloadedBytes / (task.TaskFinishTime - task.TaskCreateTime)); + } + runningTasks.Remove(task); + finishedTasks.Add(task); + } +} + +public record DownloadTask(string Aid, string Url, long TaskCreateTime) +{ + [JsonInclude] + public string? Title = null; + [JsonInclude] + public string? Pic = null; + [JsonInclude] + public long? VideoPubTime = null; + [JsonInclude] + public long? TaskFinishTime = null; + [JsonInclude] + public double Progress = 0f; + [JsonInclude] + public double DownloadSpeed = 0f; + [JsonInclude] + public double TotalDownloadedBytes = 0f; + [JsonInclude] + public bool IsSuccessful = false; +}; +public record DownloadTaskCollection(List Running, List Finished); + +record struct MyOptionBindingResult(T? Result, Exception? Exception) +{ + public bool IsValid => Exception is null; + + public static async ValueTask> BindAsync(HttpContext httpContext) + { + try + { + var item = await httpContext.Request.ReadFromJsonAsync(SourceGenerationContext.Default.MyOption); + + if (item is null) return new(default, new NoNullAllowedException()); + + return new(item, null); + } + catch (Exception ex) + { + return new(default, ex); + } + } +} + +[JsonSerializable(typeof(ProblemDetails))] +[JsonSerializable(typeof(ValidationProblemDetails))] +[JsonSerializable(typeof(HttpValidationProblemDetails))] +[JsonSerializable(typeof(DownloadTask))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(DownloadTaskCollection))] +public partial class AppJsonSerializerContext : JsonSerializerContext +{ + +} + +[JsonSerializable(typeof(MyOption))] +internal partial class SourceGenerationContext : JsonSerializerContext +{ + +} diff --git a/BBDown/BBDownDownloadUtil.cs b/BBDown/BBDownDownloadUtil.cs index 29aaf599b..d58d1f5f2 100644 --- a/BBDown/BBDownDownloadUtil.cs +++ b/BBDown/BBDownDownloadUtil.cs @@ -21,6 +21,7 @@ public class DownloadConfig 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 onProgress, bool failOnRangeNotSupported = false) @@ -88,7 +89,7 @@ public static async Task DownloadFile(string url, string path, DownloadConfig co reDown: try { - using var progress = new ProgressBar(); + 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); } @@ -125,7 +126,7 @@ public static async Task MultiThreadDownloadFileAsync(string url, string path, D ConcurrentDictionary clipProgress = new(); foreach (var i in allClips) clipProgress[i.index] = 0; - using var progress = new ProgressBar(); + using var progress = new ProgressBar(config.RelatedTask); progress.Report(0); await Parallel.ForEachAsync(allClips, async (clip, _) => { diff --git a/BBDown/Program.cs b/BBDown/Program.cs index 90a803099..01b4e62e3 100644 --- a/BBDown/Program.cs +++ b/BBDown/Program.cs @@ -66,7 +66,7 @@ public static async Task Main(params string[] args) Console.CancelKeyPress += Console_CancelKeyPress; ServicePointManager.DefaultConnectionLimit = 2048; - var rootCommand = CommandLineInvoker.GetRootCommand(DoWorkAsync); + var rootCommand = CommandLineInvoker.GetRootCommand(RunApp); Command loginCommand = new( "login", "通过APP扫描二维码以登录您的WEB账号"); @@ -75,6 +75,15 @@ public static async Task Main(params string[] args) "logintv", "通过APP扫描二维码以登录您的TV账号"); rootCommand.AddCommand(loginTVCommand); + var serverUrlOpt = new Option( + new[] { "--listen", "-l" }, + description: "服务器监听url"); + Command runAsServerCommand = new( + "server", + "以服务器模式运行") + { serverUrlOpt }; + runAsServerCommand.SetHandler(StartServer, serverUrlOpt); + rootCommand.AddCommand(runAsServerCommand); rootCommand.Description = "BBDown是一个免费且便捷高效的哔哩哔哩下载/解析软件."; rootCommand.TreatUnmatchedTokensAsErrors = true; @@ -111,6 +120,11 @@ public static async Task Main(params string[] args) if (commandLineResult.CommandResult.Command.Name.ToLower() != Path.GetFileNameWithoutExtension(Environment.ProcessPath)!.ToLower()) { + // 服务器模式需要完整的arg列表 + if (commandLineResult.CommandResult.Command.Name.ToLower() == "server") + { + return await parser.InvokeAsync(args.ToArray()); + } newArgsList.Add(commandLineResult.CommandResult.Command.Name); return await parser.InvokeAsync(newArgsList.ToArray()); } @@ -147,529 +161,564 @@ public static async Task Main(params string[] args) return await parser.InvokeAsync(newArgsList.ToArray()); } - private static async Task DoWorkAsync(MyOption myOption) + private static Task RunApp(MyOption myOption) { //检测更新 CheckUpdateAsync(); - - try + return DoWorkAsync(myOption); + } + + private static void StartServer(string listenUrl) + { + //检测更新 + CheckUpdateAsync(); + var server = new BBDownApiServer(); + server.SetUpServer(); + server.Run(listenUrl); + } + + public static (Dictionary? encodingPriority, Dictionary? dfnPriority, string? firstEncoding, + bool downloadDanmaku, string input, string savePathFormat, string lang, string aidOri, int delay) + SetUpWork(MyOption myOption) + { + //处理废弃选项 + HandleDeprecatedOptions(myOption); + + //处理冲突选项 + HandleConflictingOptions(myOption); + + //寻找并设置所需的二进制文件路径 + FindBinaries(myOption); + + //切换工作目录 + ChangeWorkingDir(myOption); + + //解析优先级 + var encodingPriority = ParseEncodingPriority(myOption, out var firstEncoding); + var dfnPriority = ParseDfnPriority(myOption); + + //优先使用用户设置的UA + HTTPUtil.UserAgent = string.IsNullOrEmpty(myOption.UserAgent) ? HTTPUtil.UserAgent : myOption.UserAgent; + + bool downloadDanmaku = myOption.DownloadDanmaku || myOption.DanmakuOnly; + string input = myOption.Url; + string savePathFormat = myOption.FilePattern; + string lang = myOption.Language; + string aidOri = ""; //原始aid + int delay = Convert.ToInt32(myOption.DelayPerPage); + Config.DEBUG_LOG = myOption.Debug; + Config.HOST = myOption.Host; + Config.EPHOST = myOption.EpHost; + Config.AREA = myOption.Area; + Config.COOKIE = myOption.Cookie; + Config.TOKEN = myOption.AccessToken.Replace("access_token=", ""); + + LogDebug("AppDirectory: {0}", APP_DIR); + LogDebug("运行参数:{0}", JsonSerializer.Serialize(myOption, MyOptionJsonContext.Default.MyOption)); + return (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, input, savePathFormat, lang, aidOri, delay); + } + + public static async Task<(string fetchedAid, VInfo vInfo, string apiType)> GetVideoInfo(MyOption myOption, string aidOri, string input) + { + //加载认证信息 + LoadCredentials(myOption); + + // 检测是否登录了账号 + bool is_login = await CheckLogin(Config.COOKIE); + if (!myOption.UseIntlApi && !myOption.UseTvApi && Config.AREA == "") { - //处理废弃选项 - HandleDeprecatedOptions(myOption); - - //处理冲突选项 - HandleConflictingOptions(myOption); - - //寻找并设置所需的二进制文件路径 - FindBinaries(myOption); - - //切换工作目录 - ChangeWorkingDir(myOption); - - //解析优先级 - var encodingPriority = ParseEncodingPriority(myOption, out var firstEncoding); - var dfnPriority = ParseDfnPriority(myOption); - - //优先使用用户设置的UA - HTTPUtil.UserAgent = string.IsNullOrEmpty(myOption.UserAgent) ? HTTPUtil.UserAgent : myOption.UserAgent; - - bool downloadDanmaku = myOption.DownloadDanmaku || myOption.DanmakuOnly; - string input = myOption.Url; - string savePathFormat = myOption.FilePattern; - string lang = myOption.Language; - string aidOri = ""; //原始aid - int delay = Convert.ToInt32(myOption.DelayPerPage); - Config.DEBUG_LOG = myOption.Debug; - Config.HOST = myOption.Host; - Config.EPHOST = myOption.EpHost; - Config.AREA = myOption.Area; - Config.COOKIE = myOption.Cookie; - Config.TOKEN = myOption.AccessToken.Replace("access_token=", ""); - - LogDebug("AppDirectory: {0}", APP_DIR); - LogDebug("运行参数:{0}", JsonSerializer.Serialize(myOption, MyOptionJsonContext.Default.MyOption)); - - //加载认证信息 - LoadCredentials(myOption); - - // 检测是否登录了账号 - bool is_login = await CheckLogin(Config.COOKIE); - if (!myOption.UseIntlApi && !myOption.UseTvApi && Config.AREA == "") + Log("检测账号登录..."); + if (!is_login) { - Log("检测账号登录..."); - if (!is_login) - { - LogWarn("你尚未登录B站账号, 解析可能受到限制"); - } + LogWarn("你尚未登录B站账号, 解析可能受到限制"); } + } - Log("获取aid..."); - aidOri = await GetAvIdAsync(input); - Log("获取aid结束: " + aidOri); + Log("获取aid..."); + aidOri = await GetAvIdAsync(input); + Log("获取aid结束: " + aidOri); - if (string.IsNullOrEmpty(aidOri)) - { - throw new Exception("输入有误"); - } + if (string.IsNullOrEmpty(aidOri)) + { + throw new Exception("输入有误"); + } - Log("获取视频信息..."); - IFetcher fetcher = FetcherFactory.CreateFetcher(aidOri, myOption.UseIntlApi); - var vInfo = await fetcher.FetchAsync(aidOri); - - string title = vInfo.Title; - string pic = vInfo.Pic; - long pubTime = vInfo.PubTime; - LogColor("视频标题: " + title); - Log("发布时间: " + FormatTimeStamp(pubTime, "yyyy-MM-dd HH:mm:ss zzz")); - List pagesInfo = vInfo.PagesInfo; - List subtitleInfo = new(); - bool more = false; - bool bangumi = vInfo.IsBangumi; - bool cheese = vInfo.IsCheese; - string apiType = myOption.UseTvApi ? "TV" : (myOption.UseAppApi ? "APP" : (myOption.UseIntlApi ? "INTL" : "WEB")); - - //打印分P信息 - foreach (Page p in pagesInfo) + Log("获取视频信息..."); + IFetcher fetcher = FetcherFactory.CreateFetcher(aidOri, myOption.UseIntlApi); + var vInfo = await fetcher.FetchAsync(aidOri); + string title = vInfo.Title; + long pubTime = vInfo.PubTime; + LogColor("视频标题: " + title); + Log("发布时间: " + FormatTimeStamp(pubTime, "yyyy-MM-dd HH:mm:ss zzz")); + string apiType = myOption.UseTvApi ? "TV" : (myOption.UseAppApi ? "APP" : (myOption.UseIntlApi ? "INTL" : "WEB")); + + //打印分P信息 + List pagesInfo = vInfo.PagesInfo; + bool more = false; + foreach (Page p in pagesInfo) + { + if (!myOption.ShowAll) { - if (!myOption.ShowAll) + if (more && p.index != pagesInfo.Count) continue; + if (!more && p.index > 5) { - if (more && p.index != pagesInfo.Count) continue; - if (!more && p.index > 5) - { - Log("......"); - more = true; - continue; - } + Log("......"); + more = true; + continue; } - - Log($"P{p.index}: [{p.cid}] [{p.title}] [{FormatTime(p.dur)}]"); } - //获取已选择的分P列表 - List? selectedPages = GetSelectedPages(myOption, vInfo, input); - - Log($"共计 {pagesInfo.Count} 个分P, 已选择:" + (selectedPages == null ? "ALL" : string.Join(",", selectedPages))); - var pagesCount = pagesInfo.Count; + Log($"P{p.index}: [{p.cid}] [{p.title}] [{FormatTime(p.dur)}]"); + } + return (aidOri, vInfo, apiType); + } - //过滤不需要的分P - if (selectedPages != null) - { - pagesInfo = pagesInfo.Where(p => selectedPages.Contains(p.index.ToString())).ToList(); - } + public static async Task DownloadPage(MyOption myOption, VInfo vInfo, Dictionary? encodingPriority, Dictionary? dfnPriority, + string? firstEncoding, bool downloadDanmaku, string input, string savePathFormat, string lang, string aidOri, int delay, string apiType, DownloadTask? relatedTask = null) + { + string title = vInfo.Title; + string pic = vInfo.Pic; + long pubTime = vInfo.PubTime; + List pagesInfo = vInfo.PagesInfo; + List subtitleInfo = new(); + bool bangumi = vInfo.IsBangumi; + bool cheese = vInfo.IsCheese; + //获取已选择的分P列表 + List? selectedPages = GetSelectedPages(myOption, vInfo, input); + + Log($"共计 {pagesInfo.Count} 个分P, 已选择:" + (selectedPages == null ? "ALL" : string.Join(",", selectedPages))); + var pagesCount = pagesInfo.Count; + + //过滤不需要的分P + if (selectedPages != null) + { + pagesInfo = pagesInfo.Where(p => selectedPages.Contains(p.index.ToString())).ToList(); + } - // 根据p数选择存储路径 - savePathFormat = string.IsNullOrEmpty(myOption.FilePattern) ? SinglePageDefaultSavePath : myOption.FilePattern; - // 1. 多P; 2. 只有1P, 但是是番剧, 尚未完结时 按照多P处理 - if (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) - { - savePathFormat = string.IsNullOrEmpty(myOption.MultiFilePattern) ? MultiPageDefaultSavePath : myOption.MultiFilePattern; - } + // 根据p数选择存储路径 + savePathFormat = string.IsNullOrEmpty(myOption.FilePattern) ? SinglePageDefaultSavePath : myOption.FilePattern; + // 1. 多P; 2. 只有1P, 但是是番剧, 尚未完结时 按照多P处理 + if (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) + { + savePathFormat = string.IsNullOrEmpty(myOption.MultiFilePattern) ? MultiPageDefaultSavePath : myOption.MultiFilePattern; + } - foreach (Page p in pagesInfo) + foreach (Page p in pagesInfo) + { + bool selected = false; //用户是否已经手动选择过了轨道 + int retryCount = 0; + downloadPage: + try { - bool selected = false; //用户是否已经手动选择过了轨道 - int retryCount = 0; - downloadPage: - try + string desc = string.IsNullOrEmpty(p.desc) ? vInfo.Desc : p.desc; + if (pagesInfo.Count > 1 && delay > 0) { - string desc = string.IsNullOrEmpty(p.desc) ? vInfo.Desc : p.desc; - if (pagesInfo.Count > 1 && delay > 0) - { - Log($"停顿{delay}秒..."); - await Task.Delay(delay * 1000); - } + Log($"停顿{delay}秒..."); + await Task.Delay(delay * 1000); + } - Log($"开始解析P{p.index}... ({pagesInfo.IndexOf(p) + 1} of {pagesInfo.Count})"); + Log($"开始解析P{p.index}... ({pagesInfo.IndexOf(p) + 1} of {pagesInfo.Count})"); - LogDebug("尝试获取章节信息..."); - p.points = await FetchPointsAsync(p.cid, p.aid); + LogDebug("尝试获取章节信息..."); + p.points = await FetchPointsAsync(p.cid, p.aid); - string videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4"; - string audioPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.m4a"; - var coverPath = $"{p.aid}/{p.aid}.jpg"; + string videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4"; + string audioPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.m4a"; + var coverPath = $"{p.aid}/{p.aid}.jpg"; - //处理文件夹以.结尾导致的异常情况 - if (title.EndsWith(".")) title += "_fix"; - //处理文件夹以.开头导致的异常情况 - if (title.StartsWith(".")) title = "_" + title; + //处理文件夹以.结尾导致的异常情况 + if (title.EndsWith(".")) title += "_fix"; + //处理文件夹以.开头导致的异常情况 + if (title.StartsWith(".")) title = "_" + title; - //处理封面&&字幕 - if (!myOption.OnlyShowInfo) + //处理封面&&字幕 + if (!myOption.OnlyShowInfo) + { + if (!Directory.Exists(p.aid)) { - if (!Directory.Exists(p.aid)) - { - Directory.CreateDirectory(p.aid); - } - if (!myOption.SkipCover && !myOption.SubOnly && !File.Exists(coverPath) && !myOption.DanmakuOnly && !myOption.CoverOnly) + Directory.CreateDirectory(p.aid); + } + if (!myOption.SkipCover && !myOption.SubOnly && !File.Exists(coverPath) && !myOption.DanmakuOnly && !myOption.CoverOnly) + { + await DownloadFile((pic == "" ? p.cover! : pic), coverPath, new DownloadConfig()); + } + + if (!myOption.SkipSubtitle && !myOption.DanmakuOnly && !myOption.CoverOnly) + { + LogDebug("获取字幕..."); + subtitleInfo = await SubUtil.GetSubtitlesAsync(p.aid, p.cid, p.epid, p.index, myOption.UseIntlApi); + if (myOption.SkipAi && subtitleInfo.Any()) { - await DownloadFile((pic == "" ? p.cover! : pic), coverPath, new DownloadConfig()); + Log($"跳过下载AI字幕"); + subtitleInfo = subtitleInfo.Where(s => !s.lan.StartsWith("ai-")).ToList(); } - - if (!myOption.SkipSubtitle && !myOption.DanmakuOnly && !myOption.CoverOnly) + foreach (Subtitle s in subtitleInfo) { - LogDebug("获取字幕..."); - subtitleInfo = await SubUtil.GetSubtitlesAsync(p.aid, p.cid, p.epid, p.index, myOption.UseIntlApi); - if (myOption.SkipAi && subtitleInfo.Any()) - { - Log($"跳过下载AI字幕"); - subtitleInfo = subtitleInfo.Where(s => !s.lan.StartsWith("ai-")).ToList(); - } - foreach (Subtitle s in subtitleInfo) + Log($"下载字幕 {s.lan} => {SubUtil.GetSubtitleCode(s.lan).Item2}..."); + LogDebug("下载:{0}", s.url); + await SubUtil.SaveSubtitleAsync(s.url, s.path); + if (myOption.SubOnly && File.Exists(s.path) && File.ReadAllText(s.path) != "") { - Log($"下载字幕 {s.lan} => {SubUtil.GetSubtitleCode(s.lan).Item2}..."); - LogDebug("下载:{0}", s.url); - await SubUtil.SaveSubtitleAsync(s.url, s.path); - if (myOption.SubOnly && File.Exists(s.path) && File.ReadAllText(s.path) != "") + var _outSubPath = FormatSavePath(savePathFormat, title, null, null, p, pagesCount, apiType, pubTime); + if (_outSubPath.Contains('/')) { - var _outSubPath = FormatSavePath(savePathFormat, title, null, null, p, pagesCount, apiType, pubTime); - if (_outSubPath.Contains('/')) - { - if (!Directory.Exists(_outSubPath.Split('/').First())) - Directory.CreateDirectory(_outSubPath.Split('/').First()); - } - _outSubPath = _outSubPath[.._outSubPath.LastIndexOf('.')] + $".{s.lan}.srt"; - File.Move(s.path, _outSubPath, true); + if (!Directory.Exists(_outSubPath.Split('/').First())) + Directory.CreateDirectory(_outSubPath.Split('/').First()); } + _outSubPath = _outSubPath[.._outSubPath.LastIndexOf('.')] + $".{s.lan}.srt"; + File.Move(s.path, _outSubPath, true); } } - - if (myOption.SubOnly) - { - if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); - continue; - } } - //调用解析 - ParsedResult parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding); - List audioMaterial = new(); - if (!p.points.Any()) + if (myOption.SubOnly) { - p.points = parsedResult.ExtraPoints; + if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); + continue; } + } - if (Config.DEBUG_LOG) - { - File.WriteAllText($"debug_{DateTime.Now:yyyyMMddHHmmssfff}.json", parsedResult.WebJsonString); - } + //调用解析 + ParsedResult parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding); + List audioMaterial = new(); + if (!p.points.Any()) + { + p.points = parsedResult.ExtraPoints; + } + + if (Config.DEBUG_LOG) + { + File.WriteAllText($"debug_{DateTime.Now:yyyyMMddHHmmssfff}.json", parsedResult.WebJsonString); + } - var savePath = ""; + var savePath = ""; - var downloadConfig = new DownloadConfig() + var downloadConfig = new DownloadConfig() + { + UseAria2c = myOption.UseAria2c, + Aria2cArgs = myOption.Aria2cArgs, + ForceHttp = myOption.ForceHttp, + MultiThread = myOption.MultiThread, + RelatedTask = relatedTask, + }; + + //此处代码简直灾难, 后续优化吧 + if ((parsedResult.VideoTracks.Any() || parsedResult.AudioTracks.Any()) && !parsedResult.Clips.Any()) //dash + { + if (parsedResult.VideoTracks.Count == 0) { - UseAria2c = myOption.UseAria2c, - Aria2cArgs = myOption.Aria2cArgs, - ForceHttp = myOption.ForceHttp, - MultiThread = myOption.MultiThread, - }; - - //此处代码简直灾难, 后续优化吧 - if ((parsedResult.VideoTracks.Any() || parsedResult.AudioTracks.Any()) && !parsedResult.Clips.Any()) //dash + LogError("没有找到符合要求的视频流"); + if (!myOption.AudioOnly) continue; + } + if (parsedResult.AudioTracks.Count == 0) { - if (parsedResult.VideoTracks.Count == 0) - { - LogError("没有找到符合要求的视频流"); - if (!myOption.AudioOnly) continue; - } - if (parsedResult.AudioTracks.Count == 0) - { - LogError("没有找到符合要求的音频流"); - if (!myOption.VideoOnly) continue; - } + LogError("没有找到符合要求的音频流"); + if (!myOption.VideoOnly) continue; + } - if (myOption.AudioOnly) - { - parsedResult.VideoTracks.Clear(); - } - if (myOption.VideoOnly) - { - parsedResult.AudioTracks.Clear(); - parsedResult.BackgroundAudioTracks.Clear(); - parsedResult.RoleAudioList.Clear(); - } + if (myOption.AudioOnly) + { + parsedResult.VideoTracks.Clear(); + } + if (myOption.VideoOnly) + { + parsedResult.AudioTracks.Clear(); + parsedResult.BackgroundAudioTracks.Clear(); + parsedResult.RoleAudioList.Clear(); + } - //排序 - parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending); - parsedResult.AudioTracks.Sort(Compare); - parsedResult.BackgroundAudioTracks.Sort(Compare); + //排序 + parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending); + parsedResult.AudioTracks.Sort(Compare); + parsedResult.BackgroundAudioTracks.Sort(Compare); + foreach (var role in parsedResult.RoleAudioList) + { + role.audio.Sort(Compare); + } + if (myOption.AudioAscending) + { + parsedResult.AudioTracks.Reverse(); + parsedResult.BackgroundAudioTracks.Reverse(); foreach (var role in parsedResult.RoleAudioList) { - role.audio.Sort(Compare); - } - if (myOption.AudioAscending) - { - parsedResult.AudioTracks.Reverse(); - parsedResult.BackgroundAudioTracks.Reverse(); - foreach (var role in parsedResult.RoleAudioList) - { - role.audio.Reverse(); - } - } - - //打印轨道信息 - if (!myOption.HideStreams) - { - PrintAllTracksInfo(parsedResult, p.dur, myOption.OnlyShowInfo); + role.audio.Reverse(); } + } - //仅展示 跳过下载 - if (myOption.OnlyShowInfo) - { - continue; - } + //打印轨道信息 + if (!myOption.HideStreams) + { + PrintAllTracksInfo(parsedResult, p.dur, myOption.OnlyShowInfo); + } - int vIndex = 0; //用户手动选择的视频序号 - int aIndex = 0; //用户手动选择的音频序号 + //仅展示 跳过下载 + if (myOption.OnlyShowInfo) + { + continue; + } - //选择轨道 - if (myOption.Interactive && !selected) - { - SelectTrackManually(parsedResult, ref vIndex, ref aIndex); - selected = true; - } + int vIndex = 0; //用户手动选择的视频序号 + int aIndex = 0; //用户手动选择的音频序号 - Video? selectedVideo = parsedResult.VideoTracks.ElementAtOrDefault(vIndex); - Audio? selectedAudio = parsedResult.AudioTracks.ElementAtOrDefault(aIndex); - Audio? selectedBackgroundAudio = parsedResult.BackgroundAudioTracks.ElementAtOrDefault(aIndex); + //选择轨道 + if (myOption.Interactive && !selected) + { + SelectTrackManually(parsedResult, ref vIndex, ref aIndex); + selected = true; + } - LogDebug("Format Before: " + savePathFormat); - savePath = FormatSavePath(savePathFormat, title, selectedVideo, selectedAudio, p, pagesCount, apiType, pubTime); - LogDebug("Format After: " + savePath); + Video? selectedVideo = parsedResult.VideoTracks.ElementAtOrDefault(vIndex); + Audio? selectedAudio = parsedResult.AudioTracks.ElementAtOrDefault(aIndex); + Audio? selectedBackgroundAudio = parsedResult.BackgroundAudioTracks.ElementAtOrDefault(aIndex); - if (downloadDanmaku) - { - var danmakuXmlPath = savePath[..savePath.LastIndexOf('.')] + ".xml"; - var danmakuAssPath = savePath[..savePath.LastIndexOf('.')] + ".ass"; - Log("正在下载弹幕Xml文件"); - string danmakuUrl = $"https://comment.bilibili.com/{p.cid}.xml"; - await DownloadFile(danmakuUrl, danmakuXmlPath, downloadConfig); - var danmakus = DanmakuUtil.ParseXml(danmakuXmlPath); - if (danmakus != null) - { - Log("正在保存弹幕Ass文件..."); - await DanmakuUtil.SaveAsAssAsync(danmakus, danmakuAssPath); - } - else - { - Log("弹幕Xml解析失败, 删除Xml..."); - File.Delete(danmakuXmlPath); - } - if (myOption.DanmakuOnly) - { - if (Directory.Exists(p.aid)) - { - Directory.Delete(p.aid); - } - continue; - } - } + LogDebug("Format Before: " + savePathFormat); + savePath = FormatSavePath(savePathFormat, title, selectedVideo, selectedAudio, p, pagesCount, apiType, pubTime); + LogDebug("Format After: " + savePath); - if (myOption.CoverOnly) + if (downloadDanmaku) + { + var danmakuXmlPath = savePath[..savePath.LastIndexOf('.')] + ".xml"; + var danmakuAssPath = savePath[..savePath.LastIndexOf('.')] + ".ass"; + Log("正在下载弹幕Xml文件"); + string danmakuUrl = $"https://comment.bilibili.com/{p.cid}.xml"; + await DownloadFile(danmakuUrl, danmakuXmlPath, downloadConfig); + var danmakus = DanmakuUtil.ParseXml(danmakuXmlPath); + if (danmakus != null) { - var newCoverPath = savePath[..savePath.LastIndexOf('.')] + Path.GetExtension(pic); - await DownloadFile((pic == "" ? p.cover! : pic), newCoverPath, downloadConfig); - if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); - continue; + Log("正在保存弹幕Ass文件..."); + await DanmakuUtil.SaveAsAssAsync(danmakus, danmakuAssPath); } - - Log($"已选择的流:"); - PrintSelectedTrackInfo(selectedVideo, selectedAudio, p.dur); - - //用户开启了强制替换 - if (myOption.ForceReplaceHost) + else { - myOption.UposHost = BACKUP_HOST; + Log("弹幕Xml解析失败, 删除Xml..."); + File.Delete(danmakuXmlPath); } - - //处理PCDN - HandlePcdn(myOption, selectedVideo, selectedAudio); - - if (!myOption.OnlyShowInfo && File.Exists(savePath) && new FileInfo(savePath).Length != 0) + if (myOption.DanmakuOnly) { - Log($"{savePath}已存在, 跳过下载..."); - File.Delete(coverPath); - if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) + if (Directory.Exists(p.aid)) { - Directory.Delete(p.aid, true); + Directory.Delete(p.aid); } continue; } + } - if (selectedVideo != null) - { - //杜比视界, 若ffmpeg版本小于5.0, 使用mp4box封装 - if (selectedVideo.dfn == Config.qualitys["126"] && !myOption.UseMP4box && !CheckFFmpegDOVI()) - { - LogWarn($"检测到杜比视界清晰度且您的ffmpeg版本小于5.0,将使用mp4box混流..."); - myOption.UseMP4box = true; - } - Log($"开始下载P{p.index}视频..."); - await DownloadTrackAsync(selectedVideo.baseUrl, videoPath, downloadConfig, video: true); - } + if (myOption.CoverOnly) + { + var newCoverPath = savePath[..savePath.LastIndexOf('.')] + Path.GetExtension(pic); + await DownloadFile((pic == "" ? p.cover! : pic), newCoverPath, downloadConfig); + if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); + continue; + } - if (selectedAudio != null) - { - Log($"开始下载P{p.index}音频..."); - await DownloadTrackAsync(selectedAudio.baseUrl, audioPath, downloadConfig, video: false); - } + Log($"已选择的流:"); + PrintSelectedTrackInfo(selectedVideo, selectedAudio, p.dur); - if (selectedBackgroundAudio != null) - { - var backgroundPath = $"{p.aid}/{p.aid}.{p.cid}.P{p.index}.back_ground.m4a"; - Log($"开始下载P{p.index}背景配音..."); - await DownloadTrackAsync(selectedBackgroundAudio.baseUrl, backgroundPath, downloadConfig, video: false); - audioMaterial.Add(new AudioMaterial("背景音频", "", backgroundPath)); - } + //用户开启了强制替换 + if (myOption.ForceReplaceHost) + { + myOption.UposHost = BACKUP_HOST; + } - if (parsedResult.RoleAudioList.Any()) - { - foreach (var role in parsedResult.RoleAudioList) - { - Log($"开始下载P{p.index}配音[{role.title}]..."); - await DownloadTrackAsync(role.audio[aIndex].baseUrl, role.path, downloadConfig, video: false); - audioMaterial.Add(new AudioMaterial(role)); - } - } + //处理PCDN + HandlePcdn(myOption, selectedVideo, selectedAudio); - Log($"下载P{p.index}完毕"); - if (!parsedResult.VideoTracks.Any()) videoPath = ""; - if (!parsedResult.AudioTracks.Any()) audioPath = ""; - if (myOption.SkipMux) continue; - Log($"开始合并音视频{(subtitleInfo.Any() ? "和字幕" : "")}..."); - if (myOption.AudioOnly) - savePath = savePath[..^4] + ".m4a"; - int code = BBDownMuxer.MuxAV(myOption.UseMP4box, videoPath, audioPath, audioMaterial, savePath, - desc, - title, - p.ownerName ?? "", - (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "", - File.Exists(coverPath) ? coverPath : "", - lang, - subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux); - if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0) + if (!myOption.OnlyShowInfo && File.Exists(savePath) && new FileInfo(savePath).Length != 0) + { + Log($"{savePath}已存在, 跳过下载..."); + File.Delete(coverPath); + if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) { - LogError("合并失败"); continue; + Directory.Delete(p.aid, true); } - Log("清理临时文件..."); - Thread.Sleep(200); - if (parsedResult.VideoTracks.Any()) File.Delete(videoPath); - if (parsedResult.AudioTracks.Any()) File.Delete(audioPath); - if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters")); - foreach (var s in subtitleInfo) File.Delete(s.path); - foreach (var a in audioMaterial) File.Delete(a.path); - if (pagesInfo.Count == 1 || p.index == pagesInfo.Last().index || p.aid != pagesInfo.Last().aid) - File.Delete(coverPath); - if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); + continue; } - else if (parsedResult.Clips.Any() && parsedResult.Dfns.Any()) //flv + + if (selectedVideo != null) { - bool flag = false; - var clips = parsedResult.Clips; - var dfns = parsedResult.Dfns; - reParse: - //排序 - parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending); - - int vIndex = 0; - if (myOption.Interactive && !flag && !selected) + //杜比视界, 若ffmpeg版本小于5.0, 使用mp4box封装 + if (selectedVideo.dfn == Config.qualitys["126"] && !myOption.UseMP4box && !CheckFFmpegDOVI()) { - int i = 0; - dfns.ForEach(key => LogColor($"{i++}.{Config.qualitys[key]}")); - Log("请选择最想要的清晰度(输入序号): ", false); - Console.ForegroundColor = ConsoleColor.Cyan; - vIndex = Convert.ToInt32(Console.ReadLine()); - if (vIndex > dfns.Count || vIndex < 0) vIndex = 0; - Console.ResetColor(); - //重新解析 - parsedResult.VideoTracks.Clear(); - parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding, dfns[vIndex]); - if (!p.points.Any()) p.points = parsedResult.ExtraPoints; - flag = true; - selected = true; - goto reParse; + LogWarn($"检测到杜比视界清晰度且您的ffmpeg版本小于5.0,将使用mp4box混流..."); + myOption.UseMP4box = true; } + Log($"开始下载P{p.index}视频..."); + await DownloadTrackAsync(selectedVideo.baseUrl, videoPath, downloadConfig, video: true); + } - Log($"共计{parsedResult.VideoTracks.Count}条流(共有{clips.Count}个分段)."); - int index = 0; - foreach (var v in parsedResult.VideoTracks) - { - LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [~{v.size / 1024 / v.dur * 8:00} kbps] [{FormatFileSize(v.size)}]".Replace("[] ", ""), false); - if (myOption.OnlyShowInfo) - { - clips.ForEach(Console.WriteLine); - } - } - if (myOption.OnlyShowInfo) continue; - savePath = FormatSavePath(savePathFormat, title, parsedResult.VideoTracks.ElementAtOrDefault(vIndex), null, p, pagesCount, apiType, pubTime); - if (File.Exists(savePath) && new FileInfo(savePath).Length != 0) - { - Log($"{savePath}已存在, 跳过下载..."); - if (pagesInfo.Count == 1 && Directory.Exists(p.aid)) - { - Directory.Delete(p.aid, true); - } - continue; - } - var pad = string.Empty.PadRight(clips.Count.ToString().Length, '0'); - for (int i = 0; i < clips.Count; i++) + if (selectedAudio != null) + { + Log($"开始下载P{p.index}音频..."); + await DownloadTrackAsync(selectedAudio.baseUrl, audioPath, downloadConfig, video: false); + } + + if (selectedBackgroundAudio != null) + { + var backgroundPath = $"{p.aid}/{p.aid}.{p.cid}.P{p.index}.back_ground.m4a"; + Log($"开始下载P{p.index}背景配音..."); + await DownloadTrackAsync(selectedBackgroundAudio.baseUrl, backgroundPath, downloadConfig, video: false); + audioMaterial.Add(new AudioMaterial("背景音频", "", backgroundPath)); + } + + if (parsedResult.RoleAudioList.Any()) + { + foreach (var role in parsedResult.RoleAudioList) { - var link = clips[i]; - videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.{i.ToString(pad)}.mp4"; - Log($"开始下载P{p.index}视频, 片段({(i + 1).ToString(pad)}/{clips.Count})..."); - await DownloadTrackAsync(link, videoPath, downloadConfig, video: true); + Log($"开始下载P{p.index}配音[{role.title}]..."); + await DownloadTrackAsync(role.audio[aIndex].baseUrl, role.path, downloadConfig, video: false); + audioMaterial.Add(new AudioMaterial(role)); } - Log($"下载P{p.index}完毕"); - Log("开始合并分段..."); - var files = GetFiles(Path.GetDirectoryName(videoPath)!, ".mp4"); - videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4"; - BBDownMuxer.MergeFLV(files, videoPath); - if (myOption.SkipMux) continue; - Log($"开始混流视频{(subtitleInfo.Any() ? "和字幕" : "")}..."); - if (myOption.AudioOnly) - savePath = savePath[..^4] + ".m4a"; - int code = BBDownMuxer.MuxAV(false, videoPath, "", audioMaterial, savePath, - desc, - title, - p.ownerName ?? "", - (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "", - File.Exists(coverPath) ? coverPath : "", - lang, - subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux); - if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0) + } + + Log($"下载P{p.index}完毕"); + if (!parsedResult.VideoTracks.Any()) videoPath = ""; + if (!parsedResult.AudioTracks.Any()) audioPath = ""; + if (myOption.SkipMux) continue; + Log($"开始合并音视频{(subtitleInfo.Any() ? "和字幕" : "")}..."); + if (myOption.AudioOnly) + savePath = savePath[..^4] + ".m4a"; + int code = BBDownMuxer.MuxAV(myOption.UseMP4box, videoPath, audioPath, audioMaterial, savePath, + desc, + title, + p.ownerName ?? "", + (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "", + File.Exists(coverPath) ? coverPath : "", + lang, + subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux); + if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0) + { + LogError("合并失败"); continue; + } + Log("清理临时文件..."); + Thread.Sleep(200); + if (parsedResult.VideoTracks.Any()) File.Delete(videoPath); + if (parsedResult.AudioTracks.Any()) File.Delete(audioPath); + if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters")); + foreach (var s in subtitleInfo) File.Delete(s.path); + foreach (var a in audioMaterial) File.Delete(a.path); + if (pagesInfo.Count == 1 || p.index == pagesInfo.Last().index || p.aid != pagesInfo.Last().aid) + File.Delete(coverPath); + if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); + } + else if (parsedResult.Clips.Any() && parsedResult.Dfns.Any()) //flv + { + bool flag = false; + var clips = parsedResult.Clips; + var dfns = parsedResult.Dfns; + reParse: + //排序 + parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending); + + int vIndex = 0; + if (myOption.Interactive && !flag && !selected) + { + int i = 0; + dfns.ForEach(key => LogColor($"{i++}.{Config.qualitys[key]}")); + Log("请选择最想要的清晰度(输入序号): ", false); + Console.ForegroundColor = ConsoleColor.Cyan; + vIndex = Convert.ToInt32(Console.ReadLine()); + if (vIndex > dfns.Count || vIndex < 0) vIndex = 0; + Console.ResetColor(); + //重新解析 + parsedResult.VideoTracks.Clear(); + parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding, dfns[vIndex]); + if (!p.points.Any()) p.points = parsedResult.ExtraPoints; + flag = true; + selected = true; + goto reParse; + } + + Log($"共计{parsedResult.VideoTracks.Count}条流(共有{clips.Count}个分段)."); + int index = 0; + foreach (var v in parsedResult.VideoTracks) + { + LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [~{v.size / 1024 / v.dur * 8:00} kbps] [{FormatFileSize(v.size)}]".Replace("[] ", ""), false); + if (myOption.OnlyShowInfo) { - LogError("合并失败"); continue; + clips.ForEach(Console.WriteLine); } - Log("清理临时文件..."); - Thread.Sleep(200); - if (parsedResult.VideoTracks.Count != 0) File.Delete(videoPath); - foreach (var s in subtitleInfo) File.Delete(s.path); - foreach (var a in audioMaterial) File.Delete(a.path); - if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters")); - if (pagesInfo.Count == 1 || p.index == pagesInfo.Last().index || p.aid != pagesInfo.Last().aid) - File.Delete(coverPath); - if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); } - else + if (myOption.OnlyShowInfo) continue; + savePath = FormatSavePath(savePathFormat, title, parsedResult.VideoTracks.ElementAtOrDefault(vIndex), null, p, pagesCount, apiType, pubTime); + if (File.Exists(savePath) && new FileInfo(savePath).Length != 0) { - LogError("解析此分P失败(建议--debug查看详细信息)"); - if (parsedResult.WebJsonString.Length < 100) + Log($"{savePath}已存在, 跳过下载..."); + if (pagesInfo.Count == 1 && Directory.Exists(p.aid)) { - LogError(parsedResult.WebJsonString); + Directory.Delete(p.aid, true); } - LogDebug("{0}", parsedResult.WebJsonString); continue; } + var pad = string.Empty.PadRight(clips.Count.ToString().Length, '0'); + for (int i = 0; i < clips.Count; i++) + { + var link = clips[i]; + videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.{i.ToString(pad)}.mp4"; + Log($"开始下载P{p.index}视频, 片段({(i + 1).ToString(pad)}/{clips.Count})..."); + await DownloadTrackAsync(link, videoPath, downloadConfig, video: true); + } + Log($"下载P{p.index}完毕"); + Log("开始合并分段..."); + var files = GetFiles(Path.GetDirectoryName(videoPath)!, ".mp4"); + videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4"; + BBDownMuxer.MergeFLV(files, videoPath); + if (myOption.SkipMux) continue; + Log($"开始混流视频{(subtitleInfo.Any() ? "和字幕" : "")}..."); + if (myOption.AudioOnly) + savePath = savePath[..^4] + ".m4a"; + int code = BBDownMuxer.MuxAV(false, videoPath, "", audioMaterial, savePath, + desc, + title, + p.ownerName ?? "", + (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "", + File.Exists(coverPath) ? coverPath : "", + lang, + subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux); + if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0) + { + LogError("合并失败"); continue; + } + Log("清理临时文件..."); + Thread.Sleep(200); + if (parsedResult.VideoTracks.Count != 0) File.Delete(videoPath); + foreach (var s in subtitleInfo) File.Delete(s.path); + foreach (var a in audioMaterial) File.Delete(a.path); + if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters")); + if (pagesInfo.Count == 1 || p.index == pagesInfo.Last().index || p.aid != pagesInfo.Last().aid) + File.Delete(coverPath); + if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); } - catch (Exception ex) + else { - if (++retryCount > 2) throw; - LogError(ex.Message); - LogWarn("下载出现异常, 3秒后将进行自动重试..."); - await Task.Delay(3000); - goto downloadPage; + LogError("解析此分P失败(建议--debug查看详细信息)"); + if (parsedResult.WebJsonString.Length < 100) + { + LogError(parsedResult.WebJsonString); + } + LogDebug("{0}", parsedResult.WebJsonString); + continue; } } - Log("任务完成"); + catch (Exception ex) + { + if (++retryCount > 2) throw; + LogError(ex.Message); + LogWarn("下载出现异常, 3秒后将进行自动重试..."); + await Task.Delay(3000); + goto downloadPage; + } + } + Log("任务完成"); + } + private static async Task DoWorkAsync(MyOption myOption) + { + try + { + var (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, + input, savePathFormat, lang, aidOri, delay) = SetUpWork(myOption); + var (fetchedAid, vInfo, apiType) = await GetVideoInfo(myOption, aidOri, input); + await DownloadPage(myOption, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, + input, savePathFormat, lang, fetchedAid, delay, apiType); } catch (Exception e) { diff --git a/BBDown/ProgressBar.cs b/BBDown/ProgressBar.cs index 76df0f887..ea466b83c 100644 --- a/BBDown/ProgressBar.cs +++ b/BBDown/ProgressBar.cs @@ -27,15 +27,21 @@ class ProgressBar : IDisposable, IProgress private string speedString = ""; private readonly Timer speedTimer; - public ProgressBar() + //服务器模式使用,更新下载任务的进度 + 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. - if (!Console.IsOutputRedirected) + // 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(); @@ -66,8 +72,14 @@ private void SpeedTimerHandler(object? state) if (downloadedBytes > 0 && downloadedBytes - lastDownloadedBytes > 0) { - speedString = " - " + BBDownUtil.FormatFileSize(downloadedBytes - lastDownloadedBytes) + "/s"; + var delta = downloadedBytes - lastDownloadedBytes; + speedString = " - " + BBDownUtil.FormatFileSize(delta) + "/s"; lastDownloadedBytes = downloadedBytes; + if (RelatedTask is not null) + { + RelatedTask.DownloadSpeed = delta; + RelatedTask.TotalDownloadedBytes += delta; + } } ResetSpeedTimer(); @@ -88,6 +100,10 @@ private void TimerHandler(object? state) animation[animationIndex++ % animation.Length], speedString); UpdateText(text); + if (RelatedTask is not null) + { + RelatedTask.Progress = currentProgress; + } ResetTimer(); } @@ -95,6 +111,8 @@ private void TimerHandler(object? state) 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); diff --git a/BBDown/Properties/launchSettings.json b/BBDown/Properties/launchSettings.json new file mode 100644 index 000000000..679f957b4 --- /dev/null +++ b/BBDown/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "profiles": { + "BBDown": { + "commandName": "Project", + "commandLineArgs": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:58682" + }, + "BBDown.Server": { + "commandName": "Project", + "commandLineArgs": "server -l http://0.0.0.0:58682", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:58682" + } + } +} \ No newline at end of file From e88f92b2eceed5a26a549d7cf233edcc8d29d72c Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Oct 2023 12:18:23 +0800 Subject: [PATCH 02/11] fix: sln linebreak --- BBDown.sln | 62 +++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/BBDown.sln b/BBDown.sln index a94635ad5..47ce2b7f7 100644 --- a/BBDown.sln +++ b/BBDown.sln @@ -1,31 +1,31 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32210.308 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown", "BBDown\BBDown.csproj", "{0D491417-BFEA-481B-AE1A-3BE69172D818}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown.Core", "BBDown.Core\BBDown.Core.csproj", "{932F1279-4AD3-4226-97C6-B0997B1568AC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.Build.0 = Release|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {17E08D9B-5969-4301-9674-ADB3EA8E5DF1} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32210.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown", "BBDown\BBDown.csproj", "{0D491417-BFEA-481B-AE1A-3BE69172D818}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBDown.Core", "BBDown.Core\BBDown.Core.csproj", "{932F1279-4AD3-4226-97C6-B0997B1568AC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D491417-BFEA-481B-AE1A-3BE69172D818}.Release|Any CPU.Build.0 = Release|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {932F1279-4AD3-4226-97C6-B0997B1568AC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {17E08D9B-5969-4301-9674-ADB3EA8E5DF1} + EndGlobalSection +EndGlobal From 3ee3de140126bacc5ddba8ab1966fb7ff3dff439 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Oct 2023 13:58:10 +0800 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20=E8=A7=84=E8=8C=83=E5=8C=96?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BBDown/BBDownApiServer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BBDown/BBDownApiServer.cs b/BBDown/BBDownApiServer.cs index 9e35b8523..abc02e5ae 100644 --- a/BBDown/BBDownApiServer.cs +++ b/BBDown/BBDownApiServer.cs @@ -54,16 +54,16 @@ public void SetUpServer() if (!bindingResult.IsValid) { //var exception = bindingResult.Exception; - return Results.Problem("输入有误"); + return Results.BadRequest("输入有误"); } var req = bindingResult.Result; AddDownloadTask(req); return Results.Ok(); }); var finishedRemovalApi = app.MapGroup("remove-finished"); - finishedRemovalApi.MapGet("/", () => finishedTasks.RemoveAll(t => true)); - finishedRemovalApi.MapGet("/failed", () => finishedTasks.RemoveAll(t => !t.IsSuccessful)); - finishedRemovalApi.MapGet("/{id}", (string id) => finishedTasks.RemoveAll(t => t.Aid == id)); + finishedRemovalApi.MapGet("/", () => { finishedTasks.RemoveAll(t => true); return Results.Ok(); }); + finishedRemovalApi.MapGet("/failed", () => { finishedTasks.RemoveAll(t => !t.IsSuccessful); return Results.Ok(); }); + finishedRemovalApi.MapGet("/{id}", (string id) => { finishedTasks.RemoveAll(t => t.Aid == id); return Results.Ok(); }); } public void Run(string url) From b9a9baf654591acd596457a2d5a15f8c443f2020 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Oct 2023 14:08:29 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0API=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- json-api-doc.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 json-api-doc.md diff --git a/json-api-doc.md b/json-api-doc.md new file mode 100644 index 000000000..460204485 --- /dev/null +++ b/json-api-doc.md @@ -0,0 +1,144 @@ +# JSON API文档 + +## API + +如果以服务器模式启动BBDown,BBDown会在本地启动一个http server,该服务器有以下API: + +### 获取任务列表 +**Endpoint:** `/get-tasks/` + +**Method:** GET + +**Description:** 获取所有任务的列表,包括正在运行的任务和已完成的任务。 + +**Response:** JSON格式的`DownloadTaskCollection`。 + +### 获取正在运行的任务列表 +**Endpoint:** `/get-tasks/running` + +**Method:** GET + +**Description:** 获取所有正在运行的任务的列表。 + +**Response:** JSON格式的`List`, 正在运行的任务列表。 + +### 获取已完成的任务列表 +**Endpoint:** `/get-tasks/finished` + +**Method:** GET + +**Description:** 获取所有已完成的任务的列表。 + +**Response:** JSON格式的`List`, 已完成的任务列表。 + +### 获取特定任务 +**Endpoint:** `/get-tasks/{id}` + +**Method:** GET + +**Description:** 获取特定任务的详细信息,根据视频的AID。 + +**Parameters:** +- `{id}` (路径参数): 视频的AID + +**Response:** 如果找到匹配的任务,将返回JSON格式的`DownloadTask`。如果未找到匹配的任务,将返回404 Not Found。 + +### 添加任务 +**Endpoint:** `/add-task` + +**Method:** POST + +**Description:** 向任务列表中添加新任务。 + +**Request Body:** JSON格式的任务信息,需要符合`MyOption`数据结构。并不要求带有MyOption中的每一个字段,只需要有`Url`字段就够了。 + +**Response:** +- 如果请求有效并成功添加任务,将返回200 OK。 +- 如果请求无效,将返回400 Bad Request,并附带错误消息`"输入有误"`。 + +### 移除已完成的任务 +**Endpoint:** `/remove-finished` + +**Method:** GET + +**Description:** 移除所有已完成的任务 + +**Response:** +- 返回200 OK。 + +### 移除已完成的任务 +**Endpoint:** `/remove-finished/failed` + +**Method:** GET + +**Description:** 移除所有已完成但是失败(`IsSuccessful == false`)的任务 + +**Response:** +- 返回200 OK。 + +### 移除特定已完成的任务 +**Endpoint:** `/remove-finished/{id}` + +**Method:** GET + +**Description:** 移除特定已完成的任务,根据视频的AID。 + +**Parameters:** +- `{id}` (路径参数): 视频的AID + +**Response:** +- 无论是否能找到对应ID的任务,均返回200 OK。 + +## 数据结构 + +### `DownloadTask` 数据结构 +`DownloadTask` 数据结构表示一个下载任务的信息。 + +**属性:** +- `Aid` ``: 视频解析出的Aid,用作正在下载中的任务的唯一标识符,已完成任务中允许重复存在 +- `Url` ``: 下载任务请求时的URL,不一定需要完整的URL,命令行支持的`av|bv|BV|ep|ss`都可以在这里使用。 +- `TaskCreateTime` ``: 任务创建时间,Unix时间戳,精确到秒,本机时区。 +- `Title` ``: 视频的标题。 +- `Pic` ``: 视频的封面图片链接。 +- `VideoPubTime` ``: 视频发布时间,Unix时间戳,精确到秒。 +- `TaskFinishTime` ``: 任务完成时间,Unix时间戳,精确到秒,本机时区。 +- `Progress` ``: 任务的下载进度,为0-1区间范围的小数。 +- `DownloadSpeed` ``: 下载速度, 单位为Byte/s。下载中时为最后一次更新的实时速度,下载完成后为平均速度。 +- `TotalDownloadedBytes` ``: 总下载字节(Byte)数,完成后的数字比实际文件偏小。 +- `IsSuccessful` ``: 标识任务是否成功完成。 + +### `DownloadTaskCollection` 数据结构 +`DownloadTaskCollection` 数据结构包含两个列表,分别表示正在运行的任务和已完成的任务。 + +**属性:** +- `Running` `>`: 包含正在运行的任务的列表,每个元素都是 `DownloadTask` 数据结构。 +- `Finished` `>`: 包含已完成的任务的列表,每个元素都是 `DownloadTask` 数据结构。 + +### `MyOption` 数据结构 + +参考`BBDown/MyOption.cs`。属性和命令行参数几乎是一一对应的,相应的值填写使用命令行会使用的值即可。这个结构会随着版本变化,请参考对应版本时候的文件。 + +### 注意事项 +- 由于BBDown的下载进度回报频率所限,`TotalDownloadedBytes`会比实际下载的文件略低,大概会少等效于1秒下载速度的文件体积,如果文件本身就非常小那这个数字偏差会较大。 +- BBDown目前内部机制没有太好的方法取消单个下载任务,因此目前任务提交以后只能等任务失败或者完成。 +- 目前服务器没有对同时执行的下载任务数量做任何限制,如果短时间频繁添加任务就会同时执行相当数量的下载任务,需要小心注意不要耗尽资源。未来考虑添加下载队列。 + +### 使用例 + +#### 用BV号添加任务 + +```shell +curl -X POST -H 'Content-Type: application/json' -d '{ "Url": "BV1qt4y1X7TW" }' http://localhost:58682/add-task +``` + +#### 下载到指定目录 + +Windows: +```shell +curl -X POST -H 'Content-Type: application/json' -d { "Url": "BV1qt4y1X7TW", "FilePattern": "C:\\Downloads\\[]" }' http://localhost:58682/add-task +``` + +Unix-Like: +```shell +curl -X POST -H 'Content-Type: application/json' -d { "Url": "BV1qt4y1X7TW", "FilePattern": "/Downloads/[]" }' http://localhost:58682/add-task +``` From 3d0e66c33b02fb2e465835504c58807cf5ca87e6 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Oct 2023 14:09:38 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20README=E6=96=B0=E5=A2=9EAPI?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index e4204328c..5544fa34e 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,22 @@ BBDown -p ALL "https://www.bilibili.com/bangumi/play/ss33073" +
+API服务器 +启动服务器(默认设置) +```shell +BBDown server +``` + +启动服务器(自定义监听地址和端口): +```shell +BBDown server http://0.0.0.0:12450 +``` +API服务器不支持HTTPS配置,如果有需要请自行使用nginx等反向代理进行配置 + +API详细请参考`json-api-doc.md` +
+ # 演示 ![1](https://user-images.githubusercontent.com/20772925/88686407-a2001480-d129-11ea-8aac-97a0c71af115.gif) From cb0a8452c3a5a5740a446b1ac9316f4ffe51a6db Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Oct 2023 14:11:18 +0800 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20README=E6=A0=BC=E5=BC=8F=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5544fa34e..23664d428 100644 --- a/README.md +++ b/README.md @@ -292,12 +292,15 @@ BBDown -p ALL "https://www.bilibili.com/bangumi/play/ss33073"
API服务器 -启动服务器(默认设置) + +启动服务器(默认设置): + ```shell BBDown server ``` 启动服务器(自定义监听地址和端口): + ```shell BBDown server http://0.0.0.0:12450 ``` From a54ae1e6da905fd2213be08f2c6d91306d3e667c Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Oct 2023 14:29:37 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20README=E7=9A=84server=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A1=8C=E6=8F=8F=E8=BF=B0=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 23664d428..5b26b9eaf 100644 --- a/README.md +++ b/README.md @@ -293,17 +293,12 @@ BBDown -p ALL "https://www.bilibili.com/bangumi/play/ss33073"
API服务器 -启动服务器(默认设置): - -```shell -BBDown server -``` - 启动服务器(自定义监听地址和端口): ```shell -BBDown server http://0.0.0.0:12450 +BBDown server -l http://0.0.0.0:12450 ``` + API服务器不支持HTTPS配置,如果有需要请自行使用nginx等反向代理进行配置 API详细请参考`json-api-doc.md` From f9648836e3899cc1d26cde10e9387dceba95c1b5 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 25 Oct 2023 07:28:44 +0800 Subject: [PATCH 08/11] =?UTF-8?q?chore:=20=E7=BB=99=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B6=85=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- json-api-doc.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b26b9eaf..0d3f8b22a 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ BBDown server -l http://0.0.0.0:12450 API服务器不支持HTTPS配置,如果有需要请自行使用nginx等反向代理进行配置 -API详细请参考`json-api-doc.md` +API详细请参考[json-api-doc.md](./json-api-doc.md)
# 演示 diff --git a/json-api-doc.md b/json-api-doc.md index 460204485..f82fe995a 100644 --- a/json-api-doc.md +++ b/json-api-doc.md @@ -116,7 +116,7 @@ ### `MyOption` 数据结构 -参考`BBDown/MyOption.cs`。属性和命令行参数几乎是一一对应的,相应的值填写使用命令行会使用的值即可。这个结构会随着版本变化,请参考对应版本时候的文件。 +参考[BBDown/MyOption.cs](./BBDown/MyOption.cs)。属性和命令行参数几乎是一一对应的,相应的值填写使用命令行会使用的值即可。这个结构会随着版本变化,请参考对应版本时候的文件。 ### 注意事项 - 由于BBDown的下载进度回报频率所限,`TotalDownloadedBytes`会比实际下载的文件略低,大概会少等效于1秒下载速度的文件体积,如果文件本身就非常小那这个数字偏差会较大。 From b8d1925d9135232edec492bee999889abc1804f6 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 25 Oct 2023 08:14:00 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8command=EF=BC=8C=E8=AE=BE=E7=BD=AE=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E7=AB=AF=E5=8F=A3=E4=B8=BA23333?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BBDown/Program.cs | 9 +++++---- BBDown/Properties/launchSettings.json | 4 +--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/BBDown/Program.cs b/BBDown/Program.cs index 01b4e62e3..23fd37c64 100644 --- a/BBDown/Program.cs +++ b/BBDown/Program.cs @@ -79,7 +79,7 @@ public static async Task Main(params string[] args) new[] { "--listen", "-l" }, description: "服务器监听url"); Command runAsServerCommand = new( - "server", + "serve", "以服务器模式运行") { serverUrlOpt }; runAsServerCommand.SetHandler(StartServer, serverUrlOpt); @@ -121,7 +121,7 @@ public static async Task Main(params string[] args) if (commandLineResult.CommandResult.Command.Name.ToLower() != Path.GetFileNameWithoutExtension(Environment.ProcessPath)!.ToLower()) { // 服务器模式需要完整的arg列表 - if (commandLineResult.CommandResult.Command.Name.ToLower() == "server") + if (commandLineResult.CommandResult.Command.Name.ToLower() == "serve") { return await parser.InvokeAsync(args.ToArray()); } @@ -168,13 +168,14 @@ private static Task RunApp(MyOption myOption) return DoWorkAsync(myOption); } - private static void StartServer(string listenUrl) + private static void StartServer(string? listenUrl) { + var defaultListenUrl = "http://0.0.0.0:23333"; //检测更新 CheckUpdateAsync(); var server = new BBDownApiServer(); server.SetUpServer(); - server.Run(listenUrl); + server.Run(string.IsNullOrEmpty(listenUrl) ? defaultListenUrl : listenUrl); } public static (Dictionary? encodingPriority, Dictionary? dfnPriority, string? firstEncoding, diff --git a/BBDown/Properties/launchSettings.json b/BBDown/Properties/launchSettings.json index 679f957b4..ecd8446e9 100644 --- a/BBDown/Properties/launchSettings.json +++ b/BBDown/Properties/launchSettings.json @@ -2,7 +2,6 @@ "profiles": { "BBDown": { "commandName": "Project", - "commandLineArgs": "", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -10,8 +9,7 @@ }, "BBDown.Server": { "commandName": "Project", - "commandLineArgs": "server -l http://0.0.0.0:58682", - "launchBrowser": false, + "commandLineArgs": "serve", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, From e78cd2a99a84e4cea8fb36b4c94feae75ccec718 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 25 Oct 2023 08:28:19 +0800 Subject: [PATCH 10/11] =?UTF-8?q?chore:=20async=E6=96=B9=E6=B3=95=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E7=BB=9F=E4=B8=80=E4=BB=A5Async=E7=BB=93=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BBDown/BBDownApiServer.cs | 8 ++++---- BBDown/Program.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/BBDown/BBDownApiServer.cs b/BBDown/BBDownApiServer.cs index abc02e5ae..80cfeb7f1 100644 --- a/BBDown/BBDownApiServer.cs +++ b/BBDown/BBDownApiServer.cs @@ -57,7 +57,7 @@ public void SetUpServer() return Results.BadRequest("输入有误"); } var req = bindingResult.Result; - AddDownloadTask(req); + AddDownloadTaskAsync(req); return Results.Ok(); }); var finishedRemovalApi = app.MapGroup("remove-finished"); @@ -85,7 +85,7 @@ public void Run(string url) app.Run(url); } - private async Task AddDownloadTask(MyOption option) + private async Task AddDownloadTaskAsync(MyOption option) { var aid = await BBDownUtil.GetAvIdAsync(option.Url); if (runningTasks.Any(task => task.Aid == aid)) return; @@ -94,11 +94,11 @@ private async Task AddDownloadTask(MyOption option) try { var (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, input, savePathFormat, lang, aidOri, delay) = Program.SetUpWork(option); - var (fetchedAid, vInfo, apiType) = await Program.GetVideoInfo(option, aidOri, input); + var (fetchedAid, vInfo, apiType) = await Program.GetVideoInfoAsync(option, aidOri, input); task.Title = vInfo.Title; task.Pic = vInfo.Pic; task.VideoPubTime = vInfo.PubTime; - await Program.DownloadPage(option, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, + await Program.DownloadPageAsync(option, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, input, savePathFormat, lang, fetchedAid, delay, apiType, task); task.IsSuccessful = true; } diff --git a/BBDown/Program.cs b/BBDown/Program.cs index 23fd37c64..f7a5c1b4b 100644 --- a/BBDown/Program.cs +++ b/BBDown/Program.cs @@ -219,7 +219,7 @@ public static (Dictionary? encodingPriority, Dictionary GetVideoInfo(MyOption myOption, string aidOri, string input) + public static async Task<(string fetchedAid, VInfo vInfo, string apiType)> GetVideoInfoAsync(MyOption myOption, string aidOri, string input) { //加载认证信息 LoadCredentials(myOption); @@ -274,7 +274,7 @@ public static (Dictionary? encodingPriority, Dictionary? encodingPriority, Dictionary? dfnPriority, + public static async Task DownloadPageAsync(MyOption myOption, VInfo vInfo, Dictionary? encodingPriority, Dictionary? dfnPriority, string? firstEncoding, bool downloadDanmaku, string input, string savePathFormat, string lang, string aidOri, int delay, string apiType, DownloadTask? relatedTask = null) { string title = vInfo.Title; @@ -717,8 +717,8 @@ private static async Task DoWorkAsync(MyOption myOption) { var (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, input, savePathFormat, lang, aidOri, delay) = SetUpWork(myOption); - var (fetchedAid, vInfo, apiType) = await GetVideoInfo(myOption, aidOri, input); - await DownloadPage(myOption, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, + var (fetchedAid, vInfo, apiType) = await GetVideoInfoAsync(myOption, aidOri, input); + await DownloadPageAsync(myOption, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, input, savePathFormat, lang, fetchedAid, delay, apiType); } catch (Exception e) From 5d6623cdb8f10468fca65c48510b7ea413469b33 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 25 Oct 2023 12:14:55 +0800 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0CORS=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E5=85=81=E8=AE=B8=E6=B5=8F=E8=A7=88=E5=99=A8=E8=B7=A8?= =?UTF-8?q?=E5=9F=9F=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BBDown/BBDownApiServer.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/BBDown/BBDownApiServer.cs b/BBDown/BBDownApiServer.cs index 80cfeb7f1..b5e786048 100644 --- a/BBDown/BBDownApiServer.cs +++ b/BBDown/BBDownApiServer.cs @@ -30,7 +30,18 @@ public void SetUpServer() { options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, AppJsonSerializerContext.Default); }); + builder.Services.AddCors((options) => + { + options.AddPolicy("AllowAnyOrigin", + policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); app = builder.Build(); + app.UseCors("AllowAnyOrigin"); var taskStatusApi = app.MapGroup("/get-tasks"); taskStatusApi.MapGet("/", handler: () => Results.Json(new DownloadTaskCollection(runningTasks, finishedTasks), AppJsonSerializerContext.Default.DownloadTaskCollection)); taskStatusApi.MapGet("/running", handler: () => Results.Json(runningTasks, AppJsonSerializerContext.Default.ListDownloadTask));