diff --git a/DownKyi.Core/BiliApi/VideoStream/Models/PlayUrl.cs b/DownKyi.Core/BiliApi/VideoStream/Models/PlayUrl.cs index bcbe573..ca1d677 100644 --- a/DownKyi.Core/BiliApi/VideoStream/Models/PlayUrl.cs +++ b/DownKyi.Core/BiliApi/VideoStream/Models/PlayUrl.cs @@ -34,6 +34,10 @@ public class PlayUrl : BaseModel [JsonProperty("durl")] public List Durl { get; set; } [JsonProperty("dash")] public PlayUrlDash Dash { get; set; } + [JsonProperty("quality")] public int Quality { get; set; } + + [JsonProperty("video_codecid")] public int VideoCodecid { get; set; } + [JsonProperty("support_formats")] public List SupportFormats { get; set; } // high_format } \ No newline at end of file diff --git a/DownKyi.Core/FFMpeg/FFMpeg.cs b/DownKyi.Core/FFMpeg/FFMpeg.cs index 6b1b974..c1a0f99 100644 --- a/DownKyi.Core/FFMpeg/FFMpeg.cs +++ b/DownKyi.Core/FFMpeg/FFMpeg.cs @@ -163,4 +163,61 @@ public class FFMpeg .NotifyOnError(action.Invoke) .ProcessSynchronously(false); } + + + /// + /// 合并多个FLV视频片段为一个完整视频 + /// + /// FLV片段路径列表(按顺序) + /// 输出视频路径 + /// 进度回调 + /// 是否成功 + public bool ConcatVideos(List inputFlvs, string outputVideo, Action action) + { + try + { + if (inputFlvs == null || inputFlvs.Count == 0) + { + return false; + } + + // 验证所有输入文件都存在 + foreach (var video in inputFlvs) + { + if (!File.Exists(video)) + { + action?.Invoke($"文件不存在: {video}"); + return false; + } + } + + LogManager.Debug(Tag, $"开始合并 {inputFlvs.Count} 个视频到 {outputVideo}"); + + + var listFile = Path.Combine(Path.GetTempPath(), $"flvlist_{DateTime.Now:yyyyMMddHHmmss}.txt"); + File.WriteAllLines(listFile, inputFlvs.Select(f => $"file '{f.Replace("'", "'\\''")}'")); + + FFMpegArguments + .FromFileInput(listFile, false, options => options + .WithCustomArgument("-f concat -safe 0")) + .OutputToFile(outputVideo, true, options => options + .WithVideoCodec("libx264") + .WithAudioCodec("aac") + .WithCustomArgument("-movflags +faststart") + .WithCustomArgument("-avoid_negative_ts make_zero") + ) + .NotifyOnOutput(action.Invoke) + .NotifyOnError(action.Invoke) + .ProcessSynchronously(false); + + try { File.Delete(listFile); } catch { } + LogManager.Debug(Tag, "视频合并完成"); + return true; + } + catch (Exception ex) + { + LogManager.Error(Tag, ex); + return false; + } + } } \ No newline at end of file diff --git a/DownKyi/Languanges/Default.axaml b/DownKyi/Languanges/Default.axaml index e487860..2f41377 100644 --- a/DownKyi/Languanges/Default.axaml +++ b/DownKyi/Languanges/Default.axaml @@ -149,6 +149,7 @@ 正在解析…… 下载中…… 混流中…… + 视频合并中…… 暂停中…… 等待中…… 下载失败 diff --git a/DownKyi/Models/VideoPlayUrlBasic.cs b/DownKyi/Models/VideoPlayUrlBasic.cs new file mode 100644 index 0000000..f86bbb8 --- /dev/null +++ b/DownKyi/Models/VideoPlayUrlBasic.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DownKyi.Models +{ + public class VideoPlayUrlBasic + { + public List BackupUrl { get; set; } + public string BaseUrl { get; set; } + + public int Id { get; set; } + + public string Codecs { get; set; } + } +} diff --git a/DownKyi/Services/Download/AddToDownloadService.cs b/DownKyi/Services/Download/AddToDownloadService.cs index 84855dd..9da6cac 100644 --- a/DownKyi/Services/Download/AddToDownloadService.cs +++ b/DownKyi/Services/Download/AddToDownloadService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -287,6 +288,8 @@ public class AddToDownloadService // 如果存在正在下载列表,则跳过,并提示 var isDownloading = false; + + foreach (var item in App.DownloadingList) { if (item.DownloadBase == null) @@ -294,9 +297,15 @@ public class AddToDownloadService continue; } - if (item.DownloadBase.Cid == page.Cid && item.Resolution.Id == page.VideoQuality.Quality && - item.AudioCodec.Name == page.AudioQualityFormat && - item.VideoCodecName == page.VideoQuality.SelectedVideoCodec) + bool f = item.DownloadBase.Cid == page.Cid && + item.Resolution.Id == page.VideoQuality.Quality && + item.VideoCodecName == page.VideoQuality.SelectedVideoCodec && + ( + (page.PlayUrl.Dash != null && item.AudioCodec.Name == page.AudioQualityFormat) || + (page.PlayUrl.Dash == null && page.PlayUrl.Durl != null) + ); + + if (f) { eventAggregator.GetEvent() .Publish($"{page.Name}{DictionaryResource.GetString("TipAlreadyToAddDownloading")}"); @@ -319,8 +328,15 @@ public class AddToDownloadService continue; } - if (item.DownloadBase.Cid == page.Cid && item.Resolution.Id == page.VideoQuality.Quality && item.AudioCodec.Name == page.AudioQualityFormat && - item.VideoCodecName == page.VideoQuality.SelectedVideoCodec) + bool f = item.DownloadBase.Cid == page.Cid && + item.Resolution.Id == page.VideoQuality.Quality && + item.VideoCodecName == page.VideoQuality.SelectedVideoCodec && + ( + (page.PlayUrl.Dash != null && item.AudioCodec.Name == page.AudioQualityFormat) || + (page.PlayUrl.Dash == null && page.PlayUrl.Durl != null) + ); + + if (f) { // eventAggregator.GetEvent().Publish($"{page.Name}{DictionaryResource.GetString("TipAlreadyToAddDownloaded")}"); // isDownloaded = true; @@ -328,30 +344,30 @@ public class AddToDownloadService switch (repeatDownloadStrategy) { case RepeatDownloadStrategy.Ask: - { - var result = ButtonResult.Cancel; - await Dispatcher.UIThread.Invoke(async () => { - var param = new DialogParameters + var result = ButtonResult.Cancel; + await Dispatcher.UIThread.Invoke(async () => + { + var param = new DialogParameters { { "message", $"{item.Name}已下载,是否重新下载" }, }; - await dialogService.ShowDialogAsync(ViewAlreadyDownloadedDialogViewModel.Tag, param, buttonResult => { result = buttonResult.Result; }); - }); + await dialogService.ShowDialogAsync(ViewAlreadyDownloadedDialogViewModel.Tag, param, buttonResult => { result = buttonResult.Result; }); + }); - if (result == ButtonResult.OK) - { - App.PropertyChangeAsync(() => { App.DownloadedList.Remove(item); }); - isDownloaded = false; - } - else - { - isDownloaded = true; - } + if (result == ButtonResult.OK) + { + App.PropertyChangeAsync(() => { App.DownloadedList.Remove(item); }); + isDownloaded = false; + } + else + { + isDownloaded = true; + } - break; - } + break; + } case RepeatDownloadStrategy.ReDownload: isDownloaded = false; break; diff --git a/DownKyi/Services/Download/AriaDownloadService.cs b/DownKyi/Services/Download/AriaDownloadService.cs index 9b1092d..85f70fe 100644 --- a/DownKyi/Services/Download/AriaDownloadService.cs +++ b/DownKyi/Services/Download/AriaDownloadService.cs @@ -64,6 +64,17 @@ public class AriaDownloadService : DownloadService, IDownloadService /// /// /// + private string DownloadVideo(DownloadingItem downloading, VideoPlayUrlBasic? downloadVideo) + { + return DownloadVideo(downloading,new PlayUrlDashVideo + { + Id = downloadVideo.Id, + Codecs = downloadVideo.Codecs, + BaseUrl = downloadVideo.BaseUrl, + BackupUrl = downloadVideo.BackupUrl + } ); + } + private string DownloadVideo(DownloadingItem downloading, PlayUrlDashVideo? downloadVideo) { // 如果为空,说明没有匹配到可下载的音频视频 diff --git a/DownKyi/Services/Download/BuiltinDownloadService.cs b/DownKyi/Services/Download/BuiltinDownloadService.cs index 57d2f56..86c130e 100644 --- a/DownKyi/Services/Download/BuiltinDownloadService.cs +++ b/DownKyi/Services/Download/BuiltinDownloadService.cs @@ -10,6 +10,7 @@ using DownKyi.Core.BiliApi.VideoStream.Models; using DownKyi.Core.Logging; using DownKyi.Core.Settings; using DownKyi.Core.Utils; +using DownKyi.Models; using DownKyi.PrismExtension.Dialog; using DownKyi.Utils; using DownKyi.ViewModels.DownloadManager; @@ -57,6 +58,17 @@ public class BuiltinDownloadService : DownloadService, IDownloadService return DownloadVideo(downloading, downloadVideo); } + private string DownloadVideo(DownloadingItem downloading, VideoPlayUrlBasic? downloadVideo) + { + return DownloadVideo(downloading, new PlayUrlDashVideo + { + Id = downloadVideo.Id, + Codecs = downloadVideo.Codecs, + BaseUrl = downloadVideo.BaseUrl, + BackupUrl = downloadVideo.BackupUrl + }); + } + /// /// 将下载音频和视频的函数中相同代码抽象出来 /// diff --git a/DownKyi/Services/Download/CustomAriaDownloadService.cs b/DownKyi/Services/Download/CustomAriaDownloadService.cs index 97a50f1..a77a172 100644 --- a/DownKyi/Services/Download/CustomAriaDownloadService.cs +++ b/DownKyi/Services/Download/CustomAriaDownloadService.cs @@ -58,6 +58,17 @@ public class CustomAriaDownloadService : DownloadService, IDownloadService return DownloadVideo(downloading, downloadVideo); } + private string DownloadVideo(DownloadingItem downloading, VideoPlayUrlBasic? downloadVideo) + { + return DownloadVideo(downloading, new PlayUrlDashVideo + { + Id = downloadVideo.Id, + Codecs = downloadVideo.Codecs, + BaseUrl = downloadVideo.BaseUrl, + BackupUrl = downloadVideo.BackupUrl + }); + } + /// /// 将下载音频和视频的函数中相同代码抽象出来 /// diff --git a/DownKyi/Services/Download/DownloadService.cs b/DownKyi/Services/Download/DownloadService.cs index cded8f8..a0ef47c 100644 --- a/DownKyi/Services/Download/DownloadService.cs +++ b/DownKyi/Services/Download/DownloadService.cs @@ -19,6 +19,7 @@ using DownKyi.Models; using DownKyi.PrismExtension.Dialog; using DownKyi.Utils; using DownKyi.ViewModels.DownloadManager; +using ImTools; using Console = DownKyi.Core.Utils.Debugging.Console; namespace DownKyi.Services.Download; @@ -114,7 +115,7 @@ public abstract class DownloadService return downloadAudio; } - protected PlayUrlDashVideo? BaseDownloadVideo(DownloadingItem downloading) + protected VideoPlayUrlBasic? BaseDownloadVideo(DownloadingItem downloading) { // 更新状态显示 downloading.DownloadStatusTitle = DictionaryResource.GetString("WhileDownloading"); @@ -125,35 +126,37 @@ public abstract class DownloadService // 下载速度 downloading.SpeedDisplay = string.Empty; - // 如果没有Dash,返回null - if (downloading.PlayUrl == null || downloading.PlayUrl.Dash == null) + if (downloading.PlayUrl?.Dash?.Video?.Count > 0) { - return null; - } - - // 如果Video列表没有内容,则返回null - if (downloading.PlayUrl.Dash.Video == null) - { - return null; - } - else if (downloading.PlayUrl.Dash.Video.Count == 0) - { - return null; - } - - // 根据视频编码匹配 - PlayUrlDashVideo? downloadVideo = null; - foreach (var video in downloading.PlayUrl.Dash.Video) - { - var codecs = Constant.GetCodecIds().FirstOrDefault(t => t.Id == video.CodecId); - if (video.Id == downloading.Resolution.Id && codecs?.Name == downloading.VideoCodecName) + foreach (var video in downloading.PlayUrl.Dash.Video) { - downloadVideo = video; - break; + var codecs = Constant.GetCodecIds().FirstOrDefault(t => t.Id == video.CodecId); + if (video.Id == downloading.Resolution.Id && codecs?.Name == downloading.VideoCodecName) + { + return new() + { + BackupUrl = video.BackupUrl, + Codecs = video.Codecs, + Id = video.Id, + BaseUrl = video.BaseUrl + }; + } } } - return downloadVideo; + if (downloading?.PlayUrl?.Durl?.Count > 0) + { + var durl = downloading.PlayUrl.Durl.First(); + return new() + { + BackupUrl = durl.BackupUrl, + BaseUrl = durl.Url, + Codecs = downloading.PlayUrl.VideoCodecid.GetHashCode().ToString(), + Id = downloading.DownloadBase.Bvid.GetHashCode() + }; + } + + return null; } protected string BaseDownloadCover(DownloadingItem downloading, string coverUrl, string fileName) @@ -240,7 +243,8 @@ public abstract class DownloadService return assFile; } - + + protected List BaseDownloadSubtitle(DownloadingItem downloading) { // 更新状态显示 @@ -330,6 +334,29 @@ public abstract class DownloadService return finalFile; } + + + private string ConcatVideos(DownloadingItem downloading,List videoUids) + { + downloading.DownloadStatusTitle = DictionaryResource.GetString("ConcatVideos"); + downloading.DownloadContent = DictionaryResource.GetString("DownloadingVideo"); + downloading.DownloadingFileSize = string.Empty; + downloading.SpeedDisplay = string.Empty; + + var finalFile = $"{downloading.DownloadBase.FilePath}.mp4"; + FFMpeg.Instance.ConcatVideos(videoUids,finalFile,(x)=>{}); + if (File.Exists(finalFile)) + { + var info = new FileInfo(finalFile); + downloading.FileSize = Format.FormatFileSize(info.Length); + } + else + { + downloading.FileSize = Format.FormatFileSize(0); + } + return finalFile; + } + protected void BaseParse(DownloadingItem downloading) { @@ -491,7 +518,7 @@ public abstract class DownloadService try { - await Task.Run(() => + await Task.Run(async () => { // 初始化 downloading.DownloadStatusTitle = string.Empty; @@ -504,54 +531,141 @@ public abstract class DownloadService // 暂停 Pause(downloading); - string? audioUid = null; - // 如果需要下载音频 - if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"]) + var isMediaSuccess = true; + + if (downloading.PlayUrl.Dash != null) { - //audioUid = DownloadAudio(downloading); - for (var i = 0; i < retry; i++) + string? audioUid = null; + + string? videoUid = null; + // 如果需要下载音频 + if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"]) { - audioUid = DownloadAudio(downloading); - if (audioUid != null && audioUid != nullMark) + for (var i = 0; i < retry; i++) { - break; + audioUid = DownloadAudio(downloading); + if (audioUid != null && audioUid != nullMark) + { + break; + } } } - } - if (audioUid == nullMark) - { - DownloadFailed(downloading); - return; - } - - // 暂停 - Pause(downloading); - - string? videoUid = null; - // 如果需要下载视频 - if (downloading.DownloadBase.NeedDownloadContent["downloadVideo"]) - { - //videoUid = DownloadVideo(downloading); - for (var i = 0; i < retry; i++) + if (audioUid == nullMark) { - videoUid = DownloadVideo(downloading); - if (videoUid != null && videoUid != nullMark) + DownloadFailed(downloading); + return; + } + + Pause(downloading); + + + // 如果需要下载视频 + if (downloading.DownloadBase.NeedDownloadContent["downloadVideo"]) + { + //videoUid = DownloadVideo(downloading); + for (var i = 0; i < retry; i++) { - break; + videoUid = DownloadVideo(downloading); + if (videoUid != null && videoUid != nullMark) + { + break; + } } } - } - if (videoUid == nullMark) + if (videoUid == nullMark) + { + DownloadFailed(downloading); + return; + } + + Pause(downloading); + + // 混流 + var outputMedia = string.Empty; + if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] || + downloading.DownloadBase.NeedDownloadContent["downloadVideo"]) + { + outputMedia = MixedFlow(downloading, audioUid, videoUid); + } + + // 检测音频、视频是否下载成功 + + if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] || + downloading.DownloadBase.NeedDownloadContent["downloadVideo"]) + { + // 只有下载音频不下载视频时才输出aac + // 只要下载视频就输出mp4 + // 成功 + isMediaSuccess = File.Exists(outputMedia); + } + } + else if(downloading.PlayUrl.Durl != null) { - DownloadFailed(downloading); - return; + if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] || + downloading.DownloadBase.NeedDownloadContent["downloadVideo"] ) + { + var durls = downloading.PlayUrl.Durl.ToList(); + var downloadStatus = durls + .Select((durl, index) => new { Durl = durl, Index = index }) + .ToDictionary(x => x.Index, x => new { Durl = x.Durl, Result = string.Empty }); + + for (int i = 0; i < durls.Count; i++) + { + downloading.PlayUrl.Durl = new List { durls[i] }; + var result = DownloadVideo(downloading); + downloadStatus[i] = new { Durl = durls[i], Result = result ?? nullMark }; + } + + int retryCount = 0; + while (retryCount < retry && downloadStatus.Values + .Any(x => x.Result == nullMark)) + { + var toRetry = downloadStatus + .Where(x => retryCount == 0 || x.Value.Result == nullMark) + .ToList(); + + foreach (var item in toRetry) + { + downloading.PlayUrl.Durl = new List { item.Value.Durl }; + var result = DownloadVideo(downloading); + downloadStatus[item.Key] = new { item.Value.Durl, Result = result }; + } + retryCount++; + await Task.Delay(1000); + } + + if(downloadStatus.Values.Any(x => x.Result == nullMark)) + { + DownloadFailed(downloading); + return; + } + + Pause(downloading); + + if (durls.Count > 1) + { + var output = ConcatVideos(downloading,downloadStatus.Values + .Select(x => x.Result).ToList()); + + isMediaSuccess = File.Exists(output); + } + else + { + var outputMedia = MixedFlow(downloading, null, downloadStatus.First().Value.Result); + isMediaSuccess = File.Exists(outputMedia); + } + } + + if(downloading.DownloadBase.NeedDownloadContent["downloadAudio"] && + !downloading.DownloadBase.NeedDownloadContent["downloadVideo"]) + { + //音频分离? + } + + Pause(downloading); } - - // 暂停 - Pause(downloading); - string? outputDanmaku = null; // 如果需要下载弹幕 if (downloading.DownloadBase.NeedDownloadContent["downloadDanmaku"]) @@ -591,13 +705,6 @@ public abstract class DownloadService // 暂停 Pause(downloading); - // 混流 - var outputMedia = string.Empty; - if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] || downloading.DownloadBase.NeedDownloadContent["downloadVideo"]) - { - outputMedia = MixedFlow(downloading, audioUid, videoUid); - } - // 这里本来只有IsExist,没有pause,不知道怎么处理 // 是否存在 //isExist = IsExist(downloading); @@ -606,16 +713,6 @@ public abstract class DownloadService // return; //} - // 检测音频、视频是否下载成功 - var isMediaSuccess = true; - if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] || downloading.DownloadBase.NeedDownloadContent["downloadVideo"]) - { - // 只有下载音频不下载视频时才输出aac - // 只要下载视频就输出mp4 - // 成功 - isMediaSuccess = File.Exists(outputMedia); - } - // 检测弹幕是否下载成功 var isDanmakuSuccess = true; if (downloading.DownloadBase.NeedDownloadContent["downloadDanmaku"]) @@ -827,7 +924,7 @@ public abstract class DownloadService workTask = Task.Run(DoWork); } - + #region 抽象接口函数 public abstract void Parse(DownloadingItem downloading); diff --git a/DownKyi/Services/Download/IDownloadService.cs b/DownKyi/Services/Download/IDownloadService.cs index 6455f1c..0bbb282 100644 --- a/DownKyi/Services/Download/IDownloadService.cs +++ b/DownKyi/Services/Download/IDownloadService.cs @@ -8,6 +8,8 @@ public interface IDownloadService void Parse(DownloadingItem downloading); string DownloadAudio(DownloadingItem downloading); string DownloadVideo(DownloadingItem downloading); + + string DownloadDanmaku(DownloadingItem downloading); List DownloadSubtitle(DownloadingItem downloading); string DownloadCover(DownloadingItem downloading, string coverUrl, string fileName); diff --git a/DownKyi/Services/Utils.cs b/DownKyi/Services/Utils.cs index 2799317..afd7c8c 100644 --- a/DownKyi/Services/Utils.cs +++ b/DownKyi/Services/Utils.cs @@ -51,19 +51,6 @@ internal static class Utils } } - if (playUrl.Durl != null) - { - // 音质 - - // 画质 - - // 视频编码 - - // 时长 - - return; - } - if (playUrl.Dash != null) { // 如果video列表或者audio列表没有内容,则返回false @@ -96,6 +83,26 @@ internal static class Utils return; } + + + if (playUrl.Durl?.Count > 0) + { + var codeIds = Constant.GetCodecIds(); + var qns = Constant.GetResolutions(); + var quality = new VideoQuality + { + Quality = playUrl.Quality, + QualityFormat = qns.First(x => x.Id == playUrl.Quality).Name, + VideoCodecList = new(codeIds.Where(x => x.Id == playUrl.VideoCodecid) + .Select(x => x.Name).ToList()), + SelectedVideoCodec = codeIds.First(x => x.Id == playUrl.VideoCodecid).Name + }; + + page.VideoQualityList = new List { quality }; + page.VideoQuality = page.VideoQualityList[0]; + page.Duration = Format.FormatDuration(playUrl.Durl.Select(x => x.Length).Sum() / 1000); + return; + } } ///