320 lines
14 KiB
C#
320 lines
14 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|