Learn.VideoAnalysis/VideoAnalysisCore/Common/DownloadFile.cs

320 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using Downloader;
using Microsoft.Extensions.DependencyInjection;
using SqlSugar;
using SqlSugar.IOC;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using VideoAnalysisCore.Job;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Enum;
using AlibabaCloud.SDK.Vod20170321;
using UserCenter.Model.Enum;
using System.Security.Policy;
using AlibabaCloud.SDK.Vod20170321.Models;
namespace VideoAnalysisCore.Common
{
public static class DownloadFileExpand
{
/// <summary>
/// 初始化下载器
/// </summary>
/// <param name="DownloadSpeed">下载速度mb/s </param>
public static void AddDownloadFileExpand(this IServiceCollection services)
{
DownloadFile.DownloadSpeed = AppCommon.Config.TaskSetting.DownloadSpeed;
services.AddTransient<DownloadFile>();
DownloadFile.Opt = new DownloadConfiguration()
{
// 通常主机支持的最大值为8000字节默认值是8000
BufferBlockSize = 10240,
// 要下载的文件部分数量默认值是1
ChunkCount = 8,
// 下载速度限制为2MB/秒,默认值为零(即无限制)
MaximumBytesPerSecond = 1024 * 1024 * DownloadFile.DownloadSpeed,
// 故障转移时的最大重试次数
MaxTryAgainOnFailover = 5,
// 每50MB后释放内存缓冲区
MaximumMemoryBufferBytes = 1024 * 1024 * 50,
// 是否并行下载文件的各个部分。默认值为false
ParallelDownload = true,
// 并行下载的数量。默认值与分块数量相同
ParallelCount = 4,
// 每个流块读取器的超时时间毫秒默认值是1000
Timeout = 1000,
// 如果只想下载大型文件的特定字节范围则设置为true
RangeDownload = false,
// 大型文件下载范围的起始偏移量
RangeLow = 0,
// 大型文件下载范围的结束偏移量
RangeHigh = 0,
// 下载失败完成时清除包块数据默认值为false
ClearPackageOnCompletionWithFailure = true,
// 将文件分多个部分下载时的最小分块大小默认值是512
MinimumSizeOfChunking = 1024,
// 在开始下载之前按照文件大小预留文件的存储空间默认值为false
ReserveStorageSpaceBeforeStartingDownload = true,
// 在下载进度改变事件中通过ReceivedBytes获取按需下载的数据
EnableLiveStreaming = false,
// 配置和自定义请求头
RequestConfiguration =
{
Accept = "*/*",
//CookieContainer = cookies,
Headers = new WebHeaderCollection(), // { 你的自定义头部信息 }
KeepAlive = true, // 默认值为false
ProtocolVersion = HttpVersion.Version11, // 默认值是HTTP 1.1
UseDefaultCredentials = false,
// 你的自定义用户代理或你的应用名称/应用版本。
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
//Proxy = new WebProxy()//使用代理
//{
// Address = new Uri("http://Learn.VideoAnalysis"),
// UseDefaultCredentials = false,
// Credentials = System.Net.CredentialCache.DefaultNetworkCredentials,
// BypassProxyOnLocal = true
//}
}
};
}
}
/// <summary>
///
/// </summary>
public class DownloadFile
{
public static DownloadConfiguration Opt { get; set; } = default!;
public static int DownloadSpeed { get; set; } = default!;
private readonly Repository<VideoTask> videoTaskDB;
private readonly Repository<NodePackageInfo> packageInfoTaskDB;
private readonly Client vodClient;
private readonly RedisManager redisManager;
private readonly IServiceProvider serviceProvider;
readonly string taskVideoName = "task.mp4";
readonly string taskPPTVideoName = "ppt.mp4";
// 注入工作流管理器,用于更新进度
private readonly VideoSliceWorkflowManager _videoSliceWorkflowManager;
private readonly TidySlideWorkflowManager _tidySlideWorkflowManager;
public DownloadFile(Repository<VideoTask> videoTaskDB, Client vodClient, Repository<NodePackageInfo> nackageInfoTaskDB, RedisManager redisManager, IServiceProvider serviceProvider, VideoSliceWorkflowManager videoSliceWorkflowManager, TidySlideWorkflowManager tidySlideWorkflowManager)
{
this.videoTaskDB = videoTaskDB;
this.vodClient = vodClient;
this.packageInfoTaskDB = nackageInfoTaskDB;
this.redisManager = redisManager;
this.serviceProvider = serviceProvider;
_videoSliceWorkflowManager = videoSliceWorkflowManager;
_tidySlideWorkflowManager = tidySlideWorkflowManager;
}
// 辅助方法:根据工作流名称获取对应的管理器实例
private dynamic GetWorkflowManager(string workflowName)
{
if (workflowName == "TidySlideWorkflow") return _tidySlideWorkflowManager;
return _videoSliceWorkflowManager;
}
// 根据 Content-Type 映射文件后缀
static string GetExtensionFromContentType(HttpResponseMessage res)
{
var contentType = res.Content.Headers.ContentType?.MediaType;
return contentType switch
{
"application/pdf" => ".pdf",
"image/jpeg" => ".jpg",
"image/png" => ".png",
"application/zip" => ".zip",
"text/html" => ".html",
"audio/wav" => ".wav",
"audio/mp4" => ".mp4",
"audio/mp3" => ".mp3",
// 根据需要添加其他 Content-Type 映射
_ => ".mp4", // 默认二进制文件
};
}
/// <summary>
/// 使用 HttpClient 下载任务的资源文件到本地
/// </summary>
/// <param name="task"></param>
/// <returns></returns>
public async Task RunTask(string task, string workflowName = "VideoSliceWorkflow")
{
var taskId = long.Parse(task);
var taskInfo = await GetTaskInfoAsync(taskId);
var fileUrl = await GetMediaUrlAsync(taskInfo);
// 准备本地目录
var localPath = task.LocalPath();
await PrepareDirectoryAndDbAsync(taskId, localPath);
// 处理 PPT 视频
await ProcessPPTVideoAsync(taskInfo, localPath, taskId, workflowName, task);
// 下载主视频
await DownloadWithCacheCheckAsync(taskId, fileUrl, localPath, taskVideoName, workflowName, task, "主视频");
}
private async Task<VideoTask> GetTaskInfoAsync(long taskId)
{
var taskInfo = await videoTaskDB.CopyNew().AsQueryable()
.Where(s => s.Id == taskId).FirstAsync();
if (taskInfo is null)
throw new Exception($"任务为null/是教研视频/没有视频课程名称");
return taskInfo;
}
private async Task<string> GetMediaUrlAsync(VideoTask taskInfo)
{
var fileUrl = taskInfo.MediaUrl;
if (string.IsNullOrEmpty(fileUrl))
{
var videoInfo = await vodClient.GetPlayInfoAsync(new GetPlayInfoRequest()
{
VideoId = taskInfo.TagId,
Formats = "mp4",
OutputType = "cdn",
AuthTimeout = 3600 * 24 * 12,
});
if (videoInfo is null || videoInfo.StatusCode != 200 && !videoInfo.Body.PlayInfoList.PlayInfo.Any())
throw new Exception($"{DateTime.Now} 视频订阅=>获取阿里云视频信息失败 VideoCode {taskInfo.TagId} StatusCode {videoInfo?.StatusCode}");
fileUrl = videoInfo.Body.PlayInfoList.PlayInfo.First().PlayURL;
}
if (string.IsNullOrEmpty(fileUrl))
throw new Exception($"任务id[{taskInfo.Id}] 资源地址无效 {fileUrl}");
// 尝试从 URL 中获取文件后缀 (保留原有校验逻辑)
string fileExtension = Path.GetExtension(new Uri(fileUrl).AbsolutePath);
if (string.IsNullOrEmpty(fileExtension))
throw new Exception($"未能从资源路径中获取文件后缀");
return fileUrl;
}
private async Task PrepareDirectoryAndDbAsync(long taskId, string localPath)
{
if (!Directory.Exists(AppCommon.TaskCachedFile)) Directory.CreateDirectory(AppCommon.TaskCachedFile);
if (!Directory.Exists(localPath)) Directory.CreateDirectory(localPath);
var outputPath = Path.Combine(localPath, taskVideoName);
await videoTaskDB.CopyNew()
.AsUpdateable()
.SetColumns(it => it.LocalMediaPath == outputPath)
.Where(it => it.Id == taskId)
.ExecuteCommandAsync();
}
private async Task ProcessPPTVideoAsync(VideoTask taskInfo, string localPath, long taskId, string workflowName, string taskStr)
{
// 更新 PPT 视频 URL
if (string.IsNullOrEmpty(taskInfo.PPTVideoUrl) && !string.IsNullOrEmpty(taskInfo.PPTVideoCode))
{
taskInfo.PPTVideoUrl = await packageInfoTaskDB.AsQueryable()
.Where(s => s.VideoCode == taskInfo.PPTVideoCode)
.Select(s => s.VideoUrl)
.FirstAsync();
if (!string.IsNullOrEmpty(taskInfo.PPTVideoUrl))
{
await videoTaskDB.CopyNew().AsUpdateable(taskInfo)
.UpdateColumns(it => new { it.PPTVideoUrl })
.ExecuteCommandAsync();
}
}
// 下载 PPT 视频
if (!string.IsNullOrEmpty(taskInfo.PPTVideoUrl))
{
await DownloadWithCacheCheckAsync(taskId, taskInfo.PPTVideoUrl, localPath, taskPPTVideoName, workflowName, taskStr, "PPT视频", true);
}
}
private async Task DownloadWithCacheCheckAsync(long taskId, string url, string localPath, string fileName, string workflowName, string taskStr, string logPrefix, bool isPPT = false)
{
var filePath = Path.Combine(localPath, fileName);
if (File.Exists(filePath) && new FileInfo(filePath).Length > 10 * 1024 * 1024)
{
await GetWorkflowManager(workflowName).AddTaskLog(taskId, $"{logPrefix}命中本地缓存");
}
else
{
await Download(url, localPath, fileName, (s, e) =>
{
var progressPrefix = isPPT ? "PPT->" : "";
GetWorkflowManager(workflowName).SetTaskProgress(taskStr, $"{progressPrefix}" + Math.Round(e.ProgressPercentage, 1));
});
}
}
/// <summary>
/// 下载文件
/// </summary>
/// <param name="fileUrl"></param>
/// <param name="localPath"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public async Task Download(string fileUrl, string localPath, string fileName,Action<object? , Downloader.DownloadProgressChangedEventArgs> change)
{
var res = new TaskCompletionSource();
using IDownload download = DownloadBuilder.New()
.WithUrl(fileUrl)
.WithDirectory(localPath)
.WithFileName(fileName)
.WithConfiguration(Opt)
.Build();
var pI = 0;
download.DownloadProgressChanged += (object? sender, Downloader.DownloadProgressChangedEventArgs e) =>
{
pI++;
if (pI % 20 == 0)
change(sender, e);
};
download.DownloadFileCompleted += async (object? sender, AsyncCompletedEventArgs e) =>
{
if (download.Status == DownloadStatus.Failed && e.Error != null)
{
// 检查磁盘空间不足异常
if (e.Error.Message.Contains("not enough space on the disk", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"{DateTime.Now} 下载失败:磁盘空间不足。尝试清理缓存...");
try
{
using var scope = serviceProvider.CreateScope();
var clearJob = scope.ServiceProvider.GetRequiredService<ClearAllCacheJob>();
await clearJob.Invoke();
}
catch (Exception ex)
{
Console.WriteLine($"清理缓存失败: {ex.Message}");
}
res.SetException(new Exception($"磁盘空间不足,下载失败。请手动清理盘符 {Path.GetPathRoot(localPath)}。详细错误:{e.Error.Message}"));
}
else
{
res.SetException(e.Error);
}
}
else if (download.Status == DownloadStatus.Completed)
{
res.SetResult();
}
};
await download.StartAsync();
// 等待回调函数完成
await res.Task;
}
}
}