From 871f60df6b29e4fc1dd92c58658ba4bacf9cbd94 Mon Sep 17 00:00:00 2001 From: flyself <1432593898@qq.com> Date: Wed, 22 Dec 2021 10:12:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=9C=8D=E5=8A=A1=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/app/index.png | Bin 39101 -> 39743 bytes src/DownKyi.Core/Aria2cNet/AriaManager.cs | 20 +- src/DownKyi.Core/Aria2cNet/DownloadResult.cs | 12 + .../BiliApi/Bangumi/BangumiType.cs | 14 + .../BiliApi/BiliUtils/Constant.cs | 10 + .../BiliApi/Models/Json/SubRipText.cs | 14 + .../BiliApi/Models/Json/Subtitle.cs | 16 + .../BiliApi/Models/Json/SubtitleJson.cs | 69 ++ .../BiliApi/Video/Models/VideoPage.cs | 2 + .../BiliApi/VideoStream/Models/PlayerV2.cs | 34 + .../BiliApi/VideoStream/Models/Subtitle.cs | 25 + .../VideoStream/Models/SubtitleInfo.cs | 18 + .../BiliApi/VideoStream/VideoStream.cs | 79 +- src/DownKyi.Core/BiliApi/Zone/VideoZone.cs | 60 +- .../BiliApi/Zone/VideoZoneIcon.cs | 88 ++ src/DownKyi.Core/BiliApi/Zone/ZoneImage.xaml | 308 +++++++ src/DownKyi.Core/Danmaku2Ass/Studio.cs | 10 +- src/DownKyi.Core/Danmaku2Ass/Utils.cs | 1 + src/DownKyi.Core/DownKyi.Core.csproj | 19 +- src/DownKyi.Core/FFmpeg/FFmpegHelper.cs | 8 +- src/DownKyi.Core/FileName/FileName.cs | 130 +++ src/DownKyi.Core/FileName/FileNamePart.cs | 35 + src/DownKyi.Core/FileName/HyphenSeparated.cs | 31 + .../Settings/Models/VideoSettings.cs | 22 +- .../Settings/SettingsManager.Video.cs | 130 +-- src/DownKyi.Core/Utils/Format.cs | 44 +- src/DownKyi/App.xaml | 1 + src/DownKyi/App.xaml.cs | 29 +- src/DownKyi/DownKyi.csproj | 9 + src/DownKyi/Images/ButtonIcon.cs | 39 + src/DownKyi/Images/LogoIcon.cs | 2 +- src/DownKyi/Languages/Default.xaml | 40 +- src/DownKyi/Models/DisplayFileNamePart.cs | 17 + src/DownKyi/Models/DownloadBaseItem.cs | 122 +++ src/DownKyi/Models/DownloadStatus.cs | 13 + src/DownKyi/Models/DownloadedItem.cs | 9 + src/DownKyi/Models/DownloadingItem.cs | 165 ++++ src/DownKyi/Models/PlayStreamType.cs | 9 + src/DownKyi/Models/VideoInfoView.cs | 57 +- src/DownKyi/Models/VideoPage.cs | 34 +- src/DownKyi/Models/VideoQuality.cs | 17 +- src/DownKyi/Services/BangumiInfoService.cs | 43 +- src/DownKyi/Services/CheeseInfoService.cs | 5 + .../Services/Download/AriaDownloadService.cs | 763 ++++++++++++++++++ .../Services/Download/DownloadService.cs | 25 + .../Services/Download/IDownloadService.cs | 19 + src/DownKyi/Services/Utils.cs | 6 +- src/DownKyi/Services/VideoInfoService.cs | 7 +- src/DownKyi/Themes/ColorBrush.xaml | 2 + src/DownKyi/Themes/Colors/ColorDefault.xaml | 2 + src/DownKyi/Themes/Styles/StyleListBox.xaml | 119 ++- src/DownKyi/Utils/DictionaryResource.cs | 2 +- .../ViewDownloadFinishedViewModel.cs | 17 +- .../ViewDownloadingViewModel.cs | 120 ++- src/DownKyi/ViewModels/MainWindowViewModel.cs | 3 + .../ViewModels/Settings/ViewVideoViewModel.cs | 231 +++--- .../ViewModels/ViewVideoDetailViewModel.cs | 240 +++++- .../DownloadManager/ViewDownloading.xaml | 302 ++++++- src/DownKyi/Views/Settings/ViewVideo.xaml | 121 +-- src/DownKyi/Views/ViewVideoDetail.xaml | 53 +- 60 files changed, 3350 insertions(+), 492 deletions(-) create mode 100644 src/DownKyi.Core/Aria2cNet/DownloadResult.cs create mode 100644 src/DownKyi.Core/BiliApi/Models/Json/SubRipText.cs create mode 100644 src/DownKyi.Core/BiliApi/Models/Json/Subtitle.cs create mode 100644 src/DownKyi.Core/BiliApi/Models/Json/SubtitleJson.cs create mode 100644 src/DownKyi.Core/BiliApi/VideoStream/Models/PlayerV2.cs create mode 100644 src/DownKyi.Core/BiliApi/VideoStream/Models/Subtitle.cs create mode 100644 src/DownKyi.Core/BiliApi/VideoStream/Models/SubtitleInfo.cs create mode 100644 src/DownKyi.Core/BiliApi/Zone/VideoZoneIcon.cs create mode 100644 src/DownKyi.Core/BiliApi/Zone/ZoneImage.xaml create mode 100644 src/DownKyi.Core/FileName/FileName.cs create mode 100644 src/DownKyi.Core/FileName/FileNamePart.cs create mode 100644 src/DownKyi.Core/FileName/HyphenSeparated.cs create mode 100644 src/DownKyi/Models/DisplayFileNamePart.cs create mode 100644 src/DownKyi/Models/DownloadBaseItem.cs create mode 100644 src/DownKyi/Models/DownloadStatus.cs create mode 100644 src/DownKyi/Models/DownloadedItem.cs create mode 100644 src/DownKyi/Models/DownloadingItem.cs create mode 100644 src/DownKyi/Models/PlayStreamType.cs create mode 100644 src/DownKyi/Services/Download/AriaDownloadService.cs create mode 100644 src/DownKyi/Services/Download/DownloadService.cs create mode 100644 src/DownKyi/Services/Download/IDownloadService.cs diff --git a/images/app/index.png b/images/app/index.png index 6a71637ca7b1689d09f8ec1011d3638a896ef755..a950b9a04899e9ed8ba1955c2f6778a15b31c02f 100644 GIT binary patch literal 39743 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 z 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 literal 39101 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 diff --git a/src/DownKyi.Core/Aria2cNet/AriaManager.cs b/src/DownKyi.Core/Aria2cNet/AriaManager.cs index 59fd6e3..c40e2cc 100644 --- a/src/DownKyi.Core/Aria2cNet/AriaManager.cs +++ b/src/DownKyi.Core/Aria2cNet/AriaManager.cs @@ -1,5 +1,6 @@ using DownKyi.Core.Aria2cNet.Client; using DownKyi.Core.Logging; +using System; using System.Threading; namespace DownKyi.Core.Aria2cNet @@ -32,10 +33,14 @@ namespace DownKyi.Core.Aria2cNet /// /// 获取gid下载项的状态 + /// + /// TODO + /// 对于下载的不同状态的返回值的测试 /// /// + /// /// - public DownloadStatus GetDownloadStatus(string gid) + public DownloadResult GetDownloadStatus(string gid, Action action = null) { string filePath = ""; while (true) @@ -48,7 +53,7 @@ namespace DownKyi.Core.Aria2cNet if (status.Result.Error.Message.Contains("is not found")) { OnDownloadFinish(false, null, gid, status.Result.Error.Message); - return DownloadStatus.ABORT; + return DownloadResult.ABORT; } } @@ -60,9 +65,16 @@ namespace DownKyi.Core.Aria2cNet long totalLength = long.Parse(status.Result.Result.TotalLength); long completedLength = long.Parse(status.Result.Result.CompletedLength); long speed = long.Parse(status.Result.Result.DownloadSpeed); + // 回调 OnTellStatus(totalLength, completedLength, speed, gid); + // 在外部执行 + if (action != null) + { + action.Invoke(); + } + if (status.Result.Result.Status == "complete") { break; @@ -86,14 +98,14 @@ namespace DownKyi.Core.Aria2cNet // 返回回调信息,退出函数 OnDownloadFinish(false, null, gid, status.Result.Result.ErrorMessage); - return DownloadStatus.FAILED; + return DownloadResult.FAILED; } // 降低CPU占用 Thread.Sleep(100); } OnDownloadFinish(true, filePath, gid, null); - return DownloadStatus.SUCCESS; + return DownloadResult.SUCCESS; } /// diff --git a/src/DownKyi.Core/Aria2cNet/DownloadResult.cs b/src/DownKyi.Core/Aria2cNet/DownloadResult.cs new file mode 100644 index 0000000..17f6ab2 --- /dev/null +++ b/src/DownKyi.Core/Aria2cNet/DownloadResult.cs @@ -0,0 +1,12 @@ +namespace DownKyi.Core.Aria2cNet +{ + /// + /// 下载状态 + /// + public enum DownloadResult + { + SUCCESS = 1, + FAILED, + ABORT + } +} diff --git a/src/DownKyi.Core/BiliApi/Bangumi/BangumiType.cs b/src/DownKyi.Core/BiliApi/Bangumi/BangumiType.cs index fae5abc..16558bf 100644 --- a/src/DownKyi.Core/BiliApi/Bangumi/BangumiType.cs +++ b/src/DownKyi.Core/BiliApi/Bangumi/BangumiType.cs @@ -18,5 +18,19 @@ namespace DownKyi.Core.BiliApi.Bangumi { 10, "Unknown" } }; + public static Dictionary TypeId = new Dictionary() + { + { 1, 13 }, + { 2, 23 }, + { 3, 177 }, + { 4, 167 }, + { 5, 11 }, + { 6, -1 }, + { 7, -1 }, + { 8, -1 }, + { 9, -1 }, + { 10, -1 } + }; + } } diff --git a/src/DownKyi.Core/BiliApi/BiliUtils/Constant.cs b/src/DownKyi.Core/BiliApi/BiliUtils/Constant.cs index 7b0ce8a..157c706 100644 --- a/src/DownKyi.Core/BiliApi/BiliUtils/Constant.cs +++ b/src/DownKyi.Core/BiliApi/BiliUtils/Constant.cs @@ -14,5 +14,15 @@ namespace DownKyi.Core.BiliApi.BiliUtils { 30280, "192K" } }; + /// + /// 音质id及含义 + /// + public static Dictionary AudioQualityId { get; } = new Dictionary() + { + { "64K", 30216 }, + { "132K", 30232 }, + { "192K", 30280 } + }; + } } diff --git a/src/DownKyi.Core/BiliApi/Models/Json/SubRipText.cs b/src/DownKyi.Core/BiliApi/Models/Json/SubRipText.cs new file mode 100644 index 0000000..bfa9928 --- /dev/null +++ b/src/DownKyi.Core/BiliApi/Models/Json/SubRipText.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace DownKyi.Core.BiliApi.Models.Json +{ + public class SubRipText : BaseModel + { + [JsonProperty("lan")] + public string Lan { get; set; } + [JsonProperty("lan_doc")] + public string LanDoc { get; set; } + [JsonProperty("srtString")] + public string SrtString { get; set; } + } +} diff --git a/src/DownKyi.Core/BiliApi/Models/Json/Subtitle.cs b/src/DownKyi.Core/BiliApi/Models/Json/Subtitle.cs new file mode 100644 index 0000000..80f9c94 --- /dev/null +++ b/src/DownKyi.Core/BiliApi/Models/Json/Subtitle.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace DownKyi.Core.BiliApi.Models.Json +{ + public class Subtitle : BaseModel + { + [JsonProperty("from")] + public float From { get; set; } + [JsonProperty("to")] + public float To { get; set; } + [JsonProperty("location")] + public int Location { get; set; } + [JsonProperty("content")] + public string Content { get; set; } + } +} diff --git a/src/DownKyi.Core/BiliApi/Models/Json/SubtitleJson.cs b/src/DownKyi.Core/BiliApi/Models/Json/SubtitleJson.cs new file mode 100644 index 0000000..6437610 --- /dev/null +++ b/src/DownKyi.Core/BiliApi/Models/Json/SubtitleJson.cs @@ -0,0 +1,69 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace DownKyi.Core.BiliApi.Models.Json +{ + public class SubtitleJson : BaseModel + { + [JsonProperty("font_size")] + public float FontSize { get; set; } + [JsonProperty("font_color")] + public string FontColor { get; set; } + [JsonProperty("background_alpha")] + public float BackgroundAlpha { get; set; } + [JsonProperty("background_color")] + public string BackgroundColor { get; set; } + [JsonProperty("Stroke")] + public string Stroke { get; set; } + [JsonProperty("body")] + public List Body { get; set; } + + /// + /// srt格式字幕 + /// + /// + public string ToSubRip() + { + string subRip = string.Empty; + for (int i = 0; i < Body.Count; i++) + { + subRip += $"{i + 1}\n"; + subRip += $"{Second2hms(Body[i].From)} --> {Second2hms(Body[i].To)}\n"; + subRip += $"{Body[i].Content}\n"; + subRip += "\n"; + } + + return subRip; + } + + /// + /// 秒数转 时:分:秒 格式 + /// + /// + /// + private static string Second2hms(float seconds) + { + if (seconds < 0) + { + return "00:00:00,000"; + } + + int i = (int)Math.Floor(seconds / 1.0); + int dec = (int)(Math.Round(seconds % 1.0f, 2) * 100); + if (dec >= 100) + { + dec = 99; + } + + int min = (int)Math.Floor(i / 60.0); + int second = (int)(i % 60.0f); + + int hour = (int)Math.Floor(min / 60.0); + min = (int)Math.Floor(min % 60.0f); + + return $"{hour:D2}:{min:D2}:{second:D2},{dec:D3}"; + } + + } +} diff --git a/src/DownKyi.Core/BiliApi/Video/Models/VideoPage.cs b/src/DownKyi.Core/BiliApi/Video/Models/VideoPage.cs index bcde7ba..c640552 100644 --- a/src/DownKyi.Core/BiliApi/Video/Models/VideoPage.cs +++ b/src/DownKyi.Core/BiliApi/Video/Models/VideoPage.cs @@ -21,5 +21,7 @@ namespace DownKyi.Core.BiliApi.Video.Models public string Weblink { get; set; } [JsonProperty("dimension")] public Dimension Dimension { get; set; } + [JsonProperty("first_frame")] + public string FirstFrame { get; set; } } } diff --git a/src/DownKyi.Core/BiliApi/VideoStream/Models/PlayerV2.cs b/src/DownKyi.Core/BiliApi/VideoStream/Models/PlayerV2.cs new file mode 100644 index 0000000..9ed962d --- /dev/null +++ b/src/DownKyi.Core/BiliApi/VideoStream/Models/PlayerV2.cs @@ -0,0 +1,34 @@ +using DownKyi.Core.BiliApi.Models; +using Newtonsoft.Json; + +namespace DownKyi.Core.BiliApi.VideoStream.Models +{ + // https://api.bilibili.com/x/player/v2?cid={cid}&aid={avid}&bvid={bvid} + public class PlayerV2Origin : BaseModel + { + //[JsonProperty("code")] + //public int Code { get; set; } + //[JsonProperty("message")] + //public string Message { get; set; } + //[JsonProperty("ttl")] + //public int Ttl { get; set; } + [JsonProperty("data")] + public PlayerV2 Data { get; set; } + } + + public class PlayerV2 : BaseModel + { + [JsonProperty("aid")] + public long Aid { get; set; } + [JsonProperty("bvid")] + public string Bvid { get; set; } + // allow_bp + // no_share + [JsonProperty("cid")] + public long Cid { get; set; } + // ... + [JsonProperty("subtitle")] + public SubtitleInfo Subtitle { get; set; } + } + +} diff --git a/src/DownKyi.Core/BiliApi/VideoStream/Models/Subtitle.cs b/src/DownKyi.Core/BiliApi/VideoStream/Models/Subtitle.cs new file mode 100644 index 0000000..a0861b1 --- /dev/null +++ b/src/DownKyi.Core/BiliApi/VideoStream/Models/Subtitle.cs @@ -0,0 +1,25 @@ +using DownKyi.Core.BiliApi.Models; +using Newtonsoft.Json; + +namespace DownKyi.Core.BiliApi.VideoStream.Models +{ + public class Subtitle : BaseModel + { + [JsonProperty("id")] + public long Id { get; set; } + [JsonProperty("lan")] + public string Lan { get; set; } + [JsonProperty("lan_doc")] + public string LanDoc { get; set; } + [JsonProperty("is_lock")] + public bool IsLock { get; set; } + [JsonProperty("author_mid")] + public long AuthorMid { get; set; } + [JsonProperty("subtitle_url")] + public string SubtitleUrl { get; set; } + [JsonProperty("type")] + public int Type { get; set; } + [JsonProperty("id_str")] + public string IdStr { get; set; } + } +} diff --git a/src/DownKyi.Core/BiliApi/VideoStream/Models/SubtitleInfo.cs b/src/DownKyi.Core/BiliApi/VideoStream/Models/SubtitleInfo.cs new file mode 100644 index 0000000..27b3078 --- /dev/null +++ b/src/DownKyi.Core/BiliApi/VideoStream/Models/SubtitleInfo.cs @@ -0,0 +1,18 @@ +using DownKyi.Core.BiliApi.Models; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace DownKyi.Core.BiliApi.VideoStream.Models +{ + public class SubtitleInfo : BaseModel + { + [JsonProperty("allow_submit")] + public bool AllowSubmit { get; set; } + [JsonProperty("lan")] + public string Lan { get; set; } + [JsonProperty("lan_doc")] + public string LanDoc { get; set; } + [JsonProperty("subtitles")] + public List Subtitles { get; set; } + } +} diff --git a/src/DownKyi.Core/BiliApi/VideoStream/VideoStream.cs b/src/DownKyi.Core/BiliApi/VideoStream/VideoStream.cs index b41e84d..8490aea 100644 --- a/src/DownKyi.Core/BiliApi/VideoStream/VideoStream.cs +++ b/src/DownKyi.Core/BiliApi/VideoStream/VideoStream.cs @@ -1,13 +1,88 @@ -using DownKyi.Core.BiliApi.VideoStream.Models; +using DownKyi.Core.BiliApi.Models.Json; +using DownKyi.Core.BiliApi.VideoStream.Models; using DownKyi.Core.Logging; using Newtonsoft.Json; using System; +using System.Collections.Generic; namespace DownKyi.Core.BiliApi.VideoStream { public static class VideoStream { + /// + /// 获取播放器信息(web端) + /// + /// + /// + /// + /// + public static PlayerV2 PlayerV2(long avid, string bvid, long cid) + { + string url = $"https://api.bilibili.com/x/player/v2?cid={cid}&aid={avid}&bvid={bvid}"; + string referer = "https://www.bilibili.com"; + string response = WebClient.RequestWeb(url, referer); + + try + { + var playUrl = JsonConvert.DeserializeObject(response); + return playUrl?.Data; + } + catch (Exception e) + { + Utils.Debugging.Console.PrintLine("PlayerV2()发生异常: {0}", e); + LogManager.Error("PlayerV2()", e); + return null; + } + } + + /// + /// 获取所有字幕
+ /// 若视频没有字幕,返回null + ///
+ /// + /// + /// + /// + public static List GetSubtitle(long avid, string bvid, long cid) + { + List subRipTexts = new List(); + + // 获取播放器信息 + PlayerV2 player = PlayerV2(avid, bvid, cid); + if (player == null) { return subRipTexts; } + if (player.Subtitle != null && player.Subtitle.Subtitles != null && player.Subtitle.Subtitles.Count == 0) + { + return null; + } + + foreach (var subtitle in player.Subtitle.Subtitles) + { + string referer = "https://www.bilibili.com"; + string response = WebClient.RequestWeb($"https:{subtitle.SubtitleUrl}", referer); + + try + { + var subtitleJson = JsonConvert.DeserializeObject(response); + if (subtitleJson == null) { continue; } + + subRipTexts.Add(new SubRipText + { + Lan = subtitle.Lan, + LanDoc = subtitle.LanDoc, + SrtString = subtitleJson.ToSubRip() + }); + } + catch (Exception e) + { + Utils.Debugging.Console.PrintLine("GetSubtitle()发生异常: {0}", e); + LogManager.Error("GetSubtitle()", e); + } + } + + return subRipTexts; + } + /// /// 获取普通视频的视频流 /// @@ -92,7 +167,7 @@ namespace DownKyi.Core.BiliApi.VideoStream catch (Exception e) { Utils.Debugging.Console.PrintLine("GetPlayUrl()发生异常: {0}", e); - LogManager.Error("GetPlayUrl", e); + LogManager.Error("GetPlayUrl()", e); return null; } } diff --git a/src/DownKyi.Core/BiliApi/Zone/VideoZone.cs b/src/DownKyi.Core/BiliApi/Zone/VideoZone.cs index 741a918..038a120 100644 --- a/src/DownKyi.Core/BiliApi/Zone/VideoZone.cs +++ b/src/DownKyi.Core/BiliApi/Zone/VideoZone.cs @@ -2,7 +2,6 @@ namespace DownKyi.Core.BiliApi.Zone { - public class VideoZone { private static VideoZone that; @@ -21,7 +20,7 @@ namespace DownKyi.Core.BiliApi.Zone return that; } - public List GetZone() + public List GetZones() { return zones; } @@ -59,7 +58,7 @@ namespace DownKyi.Core.BiliApi.Zone zones.Add(new ZoneAttr(30, "vocaloid", "VOCALOID·UTAU", 3)); //以雅马哈Vocaloid和UTAU引擎为基础,包含其他调教引擎,运用各类音源进行的歌曲创作内容 zones.Add(new ZoneAttr(194, "electronic", "电音", 3)); //以电子合成器、音乐软体等产生的电子声响制作的音乐 zones.Add(new ZoneAttr(59, "perform", "演奏", 3)); //传统或非传统乐器及器材的演奏作品 - zones.Add(new ZoneAttr(193, "mv", "MV", 3)); //音乐录影带,为搭配音乐而拍摄的短片 + zones.Add(new ZoneAttr(193, "mv", "MV", 3)); //音乐录影带,为搭配音乐而拍摄或制作的视频 zones.Add(new ZoneAttr(29, "live", "音乐现场", 3)); //音乐实况表演视频 zones.Add(new ZoneAttr(130, "other", "音乐综合", 3)); //收录无法定义到其他音乐子分区的音乐视频 @@ -84,37 +83,48 @@ namespace DownKyi.Core.BiliApi.Zone zones.Add(new ZoneAttr(19, "mugen", "Mugen", 4)); //以Mugen引擎为平台制作、或与Mugen相关的游戏视频 //知识 - zones.Add(new ZoneAttr(36, "technology", "知识")); // 主分区 + zones.Add(new ZoneAttr(36, "knowledge", "知识")); // 主分区 zones.Add(new ZoneAttr(201, "science", "科学科普", 36)); //回答你的十万个为什么 - zones.Add(new ZoneAttr(124, "fun", "社科人文", 36)); //聊聊互联网社会法律,看看历史趣闻艺术,品品文化心理人物 - zones.Add(new ZoneAttr(207, "finance", "财经", 36)); //宏观经济分析,证券市场动态,商业帝国故事,知识与财富齐飞~ - zones.Add(new ZoneAttr(208, "campus", "校园学习", 36)); //老师很有趣,同学多人才,我们都爱搞学习 - zones.Add(new ZoneAttr(209, "career", "职业职场", 36)); //职场加油站,成为最有料的职场人 - zones.Add(new ZoneAttr(122, "wild", "野生技术协会", 36)); //炫酷技能大集合,是时候展现真正的技术了 + zones.Add(new ZoneAttr(124, "social_science", "社科·法律·心理", 36)); //基于社会科学、法学、心理学展开或个人观点输出的知识视频 + zones.Add(new ZoneAttr(228, "humanity_history", "人文历史", 36)); //看看古今人物,聊聊历史过往,品品文学典籍 + zones.Add(new ZoneAttr(207, "business", "财经商业", 36)); //说金融市场,谈宏观经济,一起畅聊商业故事 + zones.Add(new ZoneAttr(208, "campus", "校园学习", 36)); //老师很有趣,学生也有才,我们一起搞学习 + zones.Add(new ZoneAttr(209, "career", "职业职场", 36)); //职业分享、升级指南,一起成为最有料的职场人 + zones.Add(new ZoneAttr(229, "design", "设计·创意", 36)); //天马行空,创意设计,都在这里 + zones.Add(new ZoneAttr(122, "skill", "野生技能协会", 36)); //技能党集合,是时候展示真正的技术了 - //数码 - zones.Add(new ZoneAttr(188, "digital", "数码")); // 主分区 - zones.Add(new ZoneAttr(95, "mobile", "手机平板", 188)); //手机平板、app 和产品教程等相关视频 - zones.Add(new ZoneAttr(189, "pc", "电脑装机", 188)); //电脑、笔记本、装机配件、外设和软件教程等相关视频 - zones.Add(new ZoneAttr(190, "photography", "摄影摄像", 188)); //摄影摄像器材、拍摄剪辑技巧、拍摄作品分享等相关视频 - zones.Add(new ZoneAttr(191, "intelligence_av", "影音智能", 188)); //影音设备、智能硬件、生活家电等相关视频 + //科技 + zones.Add(new ZoneAttr(188, "tech", "科技")); // 主分区 + zones.Add(new ZoneAttr(95, "digital", "数码", 188)); //科技数码产品大全,一起来做发烧友 + zones.Add(new ZoneAttr(230, "application", "软件应用", 188)); //超全软件应用指南 + zones.Add(new ZoneAttr(231, "computer_tech", "计算机技术", 188)); //研究分析、教学演示、经验分享......有关计算机技术的都在这里 + zones.Add(new ZoneAttr(232, "industry", "工业·工程·机械", 188)); //前方高能,机甲重工即将出没 + zones.Add(new ZoneAttr(233, "diy", "极客DIY", 188)); //炫酷技能,极客文化,硬核技巧,准备好你的惊讶 + + //运动 + zones.Add(new ZoneAttr(234, "sports", "运动")); // 主分区 + zones.Add(new ZoneAttr(235, "basketballfootball", "篮球·足球", 234)); //与篮球、足球相关的视频,包括但不限于篮足球赛事、教学、评述、剪辑、剧情等相关内容 + zones.Add(new ZoneAttr(164, "aerobics", "健身", 234)); //与健身相关的视频,包括但不限于瑜伽、CrossFit、健美、力量举、普拉提、街健等相关内容 + zones.Add(new ZoneAttr(236, "athletic", "竞技体育", 234)); //与竞技体育相关的视频,包括但不限于乒乓、羽毛球、排球、赛车等竞技项目的赛事、评述、剪辑、剧情等相关内容 + zones.Add(new ZoneAttr(237, "culture", "运动文化", 234)); //与运动文化相关的视频,包络但不限于球鞋、球衣、球星卡等运动衍生品的分享、解读,体育产业的分析、科普等相关内容 + zones.Add(new ZoneAttr(238, "comprehensive", "运动综合", 234)); //与运动综合相关的视频,包括但不限于钓鱼、骑行、滑板等日常运动分享、教学、Vlog等相关内容 //汽车 zones.Add(new ZoneAttr(223, "car", "汽车")); // 主分区 zones.Add(new ZoneAttr(176, "life", "汽车生活", 223)); //分享汽车及出行相关的生活体验类视频 zones.Add(new ZoneAttr(224, "culture", "汽车文化", 223)); //车迷的精神圣地,包括汽车赛事、品牌历史、汽车改装、经典车型和汽车模型等 zones.Add(new ZoneAttr(225, "geek", "汽车极客", 223)); //汽车硬核达人聚集地,包括DIY造车、专业评测和技术知识分享 + zones.Add(new ZoneAttr(240, "motorcycle", "摩托车", 223)); //骑士们集合啦 zones.Add(new ZoneAttr(226, "smart", "智能出行", 223)); //探索新能源汽车和未来智能出行的前沿阵地 zones.Add(new ZoneAttr(227, "strategy", "购车攻略", 223)); //丰富详实的购车建议和新车体验 //生活 zones.Add(new ZoneAttr(160, "life", "生活")); // 主分区 zones.Add(new ZoneAttr(138, "funny", "搞笑", 160)); //各种沙雕有趣的搞笑剪辑,挑战,表演,配音等视频 - zones.Add(new ZoneAttr(21, "daily", "日常", 160)); //记录日常生活,分享生活故事 + zones.Add(new ZoneAttr(239, "home", "家居房产", 160)); //与买房、装修、居家生活相关的分享 zones.Add(new ZoneAttr(161, "handmake", "手工", 160)); //手工制品的制作过程或成品展示、教程、测评类视频 zones.Add(new ZoneAttr(162, "painting", "绘画", 160)); //绘画过程或绘画教程,以及绘画相关的所有视频 - zones.Add(new ZoneAttr(163, "sports", "运动", 160)); //运动相关的记录、教程、装备评测和精彩瞬间剪辑视频 - zones.Add(new ZoneAttr(174, "other", "其他", 160)); //对分区归属不明的视频进行归纳整合的特定分区 + zones.Add(new ZoneAttr(21, "daily", "日常", 160)); //记录日常生活,分享生活故事 //美食 zones.Add(new ZoneAttr(211, "food", "美食")); // 主分区 @@ -143,11 +153,9 @@ namespace DownKyi.Core.BiliApi.Zone //时尚 zones.Add(new ZoneAttr(155, "fashion", "时尚")); // 主分区 - zones.Add(new ZoneAttr(157, "makeup", "美妆", 155)); //涵盖妆容、发型、美甲等教程,彩妆、护肤相关产品测评、分享等 - zones.Add(new ZoneAttr(158, "clothing", "服饰", 155)); //服饰风格、搭配技巧相关的展示和教程视频 - zones.Add(new ZoneAttr(164, "aerobics", "健身", 155)); //器械、有氧、拉伸运动等,以达到强身健体、减肥瘦身、形体塑造目的 - zones.Add(new ZoneAttr(159, "catwalk", "T台", 155)); //发布会走秀现场及模特相关时尚片、采访、后台花絮 - zones.Add(new ZoneAttr(192, "trends", "风尚标", 155)); //时尚明星专访、街拍、时尚购物相关知识科普 + zones.Add(new ZoneAttr(157, "makeup", "美妆护肤", 155)); //彩妆护肤、美甲美发、仿妆、医美相关内容分享或产品测评 + zones.Add(new ZoneAttr(158, "clothing", "穿搭", 155)); //穿搭风格、穿搭技巧的展示分享,涵盖衣服、鞋靴、箱包配件、配饰(帽子、钟表、珠宝首饰)等 + zones.Add(new ZoneAttr(159, "trend", "时尚潮流", 155)); //时尚街拍、时装周、时尚大片,时尚品牌、潮流等行业相关记录及知识科普 //资讯 zones.Add(new ZoneAttr(202, "information", "资讯")); // 主分区 @@ -158,8 +166,10 @@ namespace DownKyi.Core.BiliApi.Zone //娱乐 zones.Add(new ZoneAttr(5, "ent", "娱乐")); // 主分区 - zones.Add(new ZoneAttr(71, "variety", "综艺", 5)); //国内外有趣的综艺和综艺相关精彩剪辑 - zones.Add(new ZoneAttr(137, "star", "明星", 5)); //娱乐圈动态、明星资讯相关 + zones.Add(new ZoneAttr(71, "variety", "综艺", 5)); //所有综艺相关,全部一手掌握! + zones.Add(new ZoneAttr(241, "talker", "娱乐杂谈", 5)); //娱乐人物解读、娱乐热点点评、娱乐行业分析 + zones.Add(new ZoneAttr(242, "fans", "粉丝创作", 5)); //粉丝向创作视频 + zones.Add(new ZoneAttr(137, "celebrity", "明星综合", 5)); //娱乐圈动态、明星资讯相关 //影视 zones.Add(new ZoneAttr(181, "cinephile", "影视")); // 主分区 diff --git a/src/DownKyi.Core/BiliApi/Zone/VideoZoneIcon.cs b/src/DownKyi.Core/BiliApi/Zone/VideoZoneIcon.cs new file mode 100644 index 0000000..d55ab22 --- /dev/null +++ b/src/DownKyi.Core/BiliApi/Zone/VideoZoneIcon.cs @@ -0,0 +1,88 @@ +namespace DownKyi.Core.BiliApi.Zone +{ + /// + /// 视频分区图标 + /// + public class VideoZoneIcon + { + private static VideoZoneIcon instance; + + /// + /// 获取VideoZoneIcon实例 + /// + /// + public static VideoZoneIcon Instance() + { + if (instance == null) + { + instance = new VideoZoneIcon(); + } + return instance; + } + + /// + /// 隐藏VideoZoneIcon()方法,必须使用单例模式 + /// + private VideoZoneIcon() { } + + /// + /// 根据tid,获取视频分区图标 + /// + /// + /// + public string GetZoneImageKey(int tid) + { + switch (tid) + { + // 课堂 + case -10: + return "Zone.cheeseDrawingImage"; + case 1: + return "Zone.dougaDrawingImage"; + case 13: + return "Zone.animeDrawingImage"; + case 167: + return "Zone.guochuangDrawingImage"; + case 3: + return "Zone.musicDrawingImage"; + case 129: + return "Zone.danceDrawingImage"; + case 4: + return "Zone.gameDrawingImage"; + case 36: + return "Zone.techDrawingImage"; + case 188: + return "Zone.digitalDrawingImage"; + case 234: + return "Zone.sportsDrawingImage"; + case 223: + return "Zone.carDrawingImage"; + case 160: + return "Zone.lifeDrawingImage"; + case 211: + return "Zone.foodDrawingImage"; + case 217: + return "Zone.animalDrawingImage"; + case 119: + return "Zone.kichikuDrawingImage"; + case 155: + return "Zone.fashionDrawingImage"; + case 202: + return "Zone.informationDrawingImage"; + case 5: + return "Zone.entDrawingImage"; + case 181: + return "Zone.cinephileDrawingImage"; + case 177: + return "Zone.documentaryDrawingImage"; + case 23: + return "Zone.movieDrawingImage"; + case 11: + return "Zone.teleplayDrawingImage"; + default: + return "videoUpDrawingImage"; + } + } + + } +} diff --git a/src/DownKyi.Core/BiliApi/Zone/ZoneImage.xaml b/src/DownKyi.Core/BiliApi/Zone/ZoneImage.xaml new file mode 100644 index 0000000..28a925d --- /dev/null +++ b/src/DownKyi.Core/BiliApi/Zone/ZoneImage.xaml @@ -0,0 +1,308 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/DownKyi.Core/Danmaku2Ass/Studio.cs b/src/DownKyi.Core/Danmaku2Ass/Studio.cs index 97acc48..630e323 100644 --- a/src/DownKyi.Core/Danmaku2Ass/Studio.cs +++ b/src/DownKyi.Core/Danmaku2Ass/Studio.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; @@ -67,7 +68,12 @@ namespace DownKyi.Core.Danmaku2Ass public void CreateFile(string fileName, string text) { - File.WriteAllText(fileName, text); + try + { + File.WriteAllText(fileName, text); + } + catch (Exception) + { } } public Dictionary Report() diff --git a/src/DownKyi.Core/Danmaku2Ass/Utils.cs b/src/DownKyi.Core/Danmaku2Ass/Utils.cs index 2d874cc..d54e48d 100644 --- a/src/DownKyi.Core/Danmaku2Ass/Utils.cs +++ b/src/DownKyi.Core/Danmaku2Ass/Utils.cs @@ -62,6 +62,7 @@ namespace DownKyi.Core.Danmaku2Ass int second = (int)(i % 60.0f); int hour = (int)Math.Floor(min / 60.0); + min = (int)Math.Floor(min % 60.0f); return $"{hour:D}:{min:D2}:{second:D2}.{dec:D2}"; } diff --git a/src/DownKyi.Core/DownKyi.Core.csproj b/src/DownKyi.Core/DownKyi.Core.csproj index 9ca3132..131523f 100644 --- a/src/DownKyi.Core/DownKyi.Core.csproj +++ b/src/DownKyi.Core/DownKyi.Core.csproj @@ -111,7 +111,7 @@ - + @@ -160,13 +160,19 @@ + + + + + + @@ -188,6 +194,7 @@ + @@ -203,6 +210,9 @@ + + + @@ -255,7 +265,12 @@ - + + + MSBuild:Compile + Designer + + diff --git a/src/DownKyi.Core/FFmpeg/FFmpegHelper.cs b/src/DownKyi.Core/FFmpeg/FFmpegHelper.cs index b77bf99..e94b204 100644 --- a/src/DownKyi.Core/FFmpeg/FFmpegHelper.cs +++ b/src/DownKyi.Core/FFmpeg/FFmpegHelper.cs @@ -7,13 +7,13 @@ namespace DownKyi.Core.FFmpeg { public static class FFmpegHelper { - private const string Tag = "PageToolboxDelogo"; + private const string Tag = "FFmpegHelper"; /// /// 合并音频和视频 /// - /// - /// + /// 音频 + /// 视频 /// public static bool MergeVideo(string video1, string video2, string destVideo) { @@ -24,7 +24,7 @@ namespace DownKyi.Core.FFmpeg } if (video2 == null || !File.Exists(video2)) { - param = $"-i \"{video1}\" -acodec copy -vcodec copy -f mp4 \"{destVideo}\""; + param = $"-i \"{video1}\" -acodec copy -f aac \"{destVideo}\""; } if (!File.Exists(video1) && !File.Exists(video2)) { return false; } diff --git a/src/DownKyi.Core/FileName/FileName.cs b/src/DownKyi.Core/FileName/FileName.cs new file mode 100644 index 0000000..23d9c1d --- /dev/null +++ b/src/DownKyi.Core/FileName/FileName.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace DownKyi.Core.FileName +{ + public class FileName + { + private readonly List nameParts; + private int order = -1; + private string section = "SECTION"; + private string mainTitle = "MAIN_TITLE"; + private string pageTitle = "PAGE_TITLE"; + private string videoZone = "VIDEO_ZONE"; + private string audioQuality = "AUDIO_QUALITY"; + private string videoQuality = "VIDEO_QUALITY"; + private string videoCodec = "VIDEO_CODEC"; + + private FileName(List nameParts) + { + this.nameParts = nameParts; + } + + public static FileName Builder(List nameParts) + { + return new FileName(nameParts); + } + + public FileName SetOrder(int order) + { + this.order = order; + return this; + } + + public FileName SetSection(string section) + { + this.section = section; + return this; + } + + public FileName SetMainTitle(string mainTitle) + { + this.mainTitle = mainTitle; + return this; + } + + public FileName SetPageTitle(string pageTitle) + { + this.pageTitle = pageTitle; + return this; + } + + public FileName SetVideoZone(string videoZone) + { + this.videoZone = videoZone; + return this; + } + + public FileName SetAudioQuality(string audioQuality) + { + this.audioQuality = audioQuality; + return this; + } + + public FileName SetVideoQuality(string videoQuality) + { + this.videoQuality = videoQuality; + return this; + } + + public FileName SetVideoCodec(string videoCodec) + { + this.videoCodec = videoCodec; + return this; + } + + public string RelativePath() + { + string path = string.Empty; + + foreach (FileNamePart part in nameParts) + { + switch (part) + { + case FileNamePart.ORDER: + if (order != -1) + { + path += order; + } + else + { + path += "ORDER"; + } + break; + case FileNamePart.SECTION: + path += section; + break; + case FileNamePart.MAIN_TITLE: + path += mainTitle; + break; + case FileNamePart.PAGE_TITLE: + path += pageTitle; + break; + case FileNamePart.VIDEO_ZONE: + path += videoZone; + break; + case FileNamePart.AUDIO_QUALITY: + path += audioQuality; + break; + case FileNamePart.VIDEO_QUALITY: + path += videoQuality; + break; + case FileNamePart.VIDEO_CODEC: + path += videoCodec; + break; + } + + if (((int)part) >= 100) + { + path += HyphenSeparated.Hyphen[(int)part]; + } + } + + // 避免连续多个斜杠 + path = Regex.Replace(path, @"//+", "/"); + // 避免以斜杠开头和结尾的情况 + return path.TrimEnd('/').TrimStart('/'); + } + + } +} diff --git a/src/DownKyi.Core/FileName/FileNamePart.cs b/src/DownKyi.Core/FileName/FileNamePart.cs new file mode 100644 index 0000000..551829c --- /dev/null +++ b/src/DownKyi.Core/FileName/FileNamePart.cs @@ -0,0 +1,35 @@ +namespace DownKyi.Core.FileName +{ + public enum FileNamePart + { + // Video + ORDER = 1, + SECTION, + MAIN_TITLE, + PAGE_TITLE, + VIDEO_ZONE, + AUDIO_QUALITY, + VIDEO_QUALITY, + VIDEO_CODEC, + + // 斜杠 + SLASH = 100, + + // HyphenSeparated + UNDERSCORE = 101, // 下划线 + HYPHEN, // 连字符 + PLUS, // 加号 + COMMA, // 逗号 + PERIOD, // 句号 + AND, // and + NUMBER, // # + OPEN_PAREN, // 左圆括号 + CLOSE_PAREN, // 右圆括号 + OPEN_BRACKET, // 左方括号 + CLOSE_BRACKET, // 右方括号 + OPEN_BRACE, // 左花括号 + CLOSE_brace, // 右花括号 + BLANK, // 空白符 + + } +} diff --git a/src/DownKyi.Core/FileName/HyphenSeparated.cs b/src/DownKyi.Core/FileName/HyphenSeparated.cs new file mode 100644 index 0000000..0bb3083 --- /dev/null +++ b/src/DownKyi.Core/FileName/HyphenSeparated.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace DownKyi.Core.FileName +{ + /// + /// 文件名字段 + /// + public static class HyphenSeparated + { + // 文件名的分隔符 + public static Dictionary Hyphen = new Dictionary() + { + { 100, "/" }, + { 101, "_" }, + { 102, "-" }, + { 103, "+" }, + { 104, "," }, + { 105, "." }, + { 106, "&" }, + { 107, "#" }, + { 108, "(" }, + { 109, ")" }, + { 110, "[" }, + { 111, "]" }, + { 112, "{" }, + { 113, "}" }, + { 114, " " }, + }; + + } +} diff --git a/src/DownKyi.Core/Settings/Models/VideoSettings.cs b/src/DownKyi.Core/Settings/Models/VideoSettings.cs index 0863423..d5fe834 100644 --- a/src/DownKyi.Core/Settings/Models/VideoSettings.cs +++ b/src/DownKyi.Core/Settings/Models/VideoSettings.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using DownKyi.Core.FileName; +using System.Collections.Generic; namespace DownKyi.Core.Settings.Models { @@ -7,16 +8,13 @@ namespace DownKyi.Core.Settings.Models ///
public class VideoSettings { - public VideoCodecs VideoCodecs { get; set; } - public int Quality { get; set; } - public int AudioQuality { get; set; } - public AllowStatus IsAddOrder { get; set; } - public AllowStatus IsTranscodingFlvToMp4 { get; set; } - public string SaveVideoRootPath { get; set; } - public List HistoryVideoRootPaths { get; set; } - public AllowStatus IsUseSaveVideoRootPath { get; set; } - public AllowStatus IsCreateFolderForMedia { get; set; } - public AllowStatus IsDownloadDanmaku { get; set; } - public AllowStatus IsDownloadCover { get; set; } + public VideoCodecs VideoCodecs { get; set; } // AVC or HEVC + public int Quality { get; set; } // 画质 + public int AudioQuality { get; set; } // 音质 + public AllowStatus IsTranscodingFlvToMp4 { get; set; } // 是否将flv转为mp4 + public string SaveVideoRootPath { get; set; } // 视频保存路径 + public List HistoryVideoRootPaths { get; set; } // 历史视频保存路径 + public AllowStatus IsUseSaveVideoRootPath { get; set; } // 是否使用默认视频保存路径 + public List FileNameParts { get; set; } // 文件命名格式 } } diff --git a/src/DownKyi.Core/Settings/SettingsManager.Video.cs b/src/DownKyi.Core/Settings/SettingsManager.Video.cs index 1b6700c..aa50f59 100644 --- a/src/DownKyi.Core/Settings/SettingsManager.Video.cs +++ b/src/DownKyi.Core/Settings/SettingsManager.Video.cs @@ -1,4 +1,5 @@ -using System; +using DownKyi.Core.FileName; +using System; using System.Collections.Generic; using System.IO; @@ -15,9 +16,6 @@ namespace DownKyi.Core.Settings // 设置优先下载音质 private readonly int audioQuality = 30280; - // 是否在下载的视频前增加序号 - private readonly AllowStatus isAddOrder = AllowStatus.NO; - // 是否下载flv视频后转码为mp4 private readonly AllowStatus isTranscodingFlvToMp4 = AllowStatus.YES; @@ -30,14 +28,21 @@ namespace DownKyi.Core.Settings // 是否使用默认下载目录,如果是,则每次点击下载选中项时不再询问下载目录 private readonly AllowStatus isUseSaveVideoRootPath = AllowStatus.NO; - // 是否为不同视频分别创建文件夹 - private readonly AllowStatus isCreateFolderForMedia = AllowStatus.YES; - - // 是否在下载视频的同时下载弹幕 - private readonly AllowStatus isDownloadDanmaku = AllowStatus.YES; - - // 是否在下载视频的同时下载封面 - private readonly AllowStatus isDownloadCover = AllowStatus.YES; + // 文件命名格式 + private readonly List fileNameParts = new List + { + FileNamePart.MAIN_TITLE, + FileNamePart.SLASH, + FileNamePart.SECTION, + FileNamePart.SLASH, + FileNamePart.ORDER, + FileNamePart.HYPHEN, + FileNamePart.PAGE_TITLE, + FileNamePart.HYPHEN, + FileNamePart.VIDEO_QUALITY, + FileNamePart.HYPHEN, + FileNamePart.VIDEO_CODEC, + }; /// /// 获取优先下载的视频编码 @@ -120,33 +125,6 @@ namespace DownKyi.Core.Settings return SetSettings(); } - /// - /// 获取是否给视频增加序号 - /// - /// - public AllowStatus IsAddOrder() - { - appSettings = GetSettings(); - if (appSettings.Video.IsAddOrder == 0) - { - // 第一次获取,先设置默认值 - IsAddOrder(isAddOrder); - return isAddOrder; - } - return appSettings.Video.IsAddOrder; - } - - /// - /// 设置是否给视频增加序号 - /// - /// - /// - public bool IsAddOrder(AllowStatus isAddOrder) - { - appSettings.Video.IsAddOrder = isAddOrder; - return SetSettings(); - } - /// /// 获取是否下载flv视频后转码为mp4 /// @@ -256,83 +234,29 @@ namespace DownKyi.Core.Settings } /// - /// 获取是否为不同视频分别创建文件夹 + /// 获取文件命名格式 /// /// - public AllowStatus IsCreateFolderForMedia() + public List GetFileNameParts() { appSettings = GetSettings(); - if (appSettings.Video.IsCreateFolderForMedia == 0) + if (appSettings.Video.FileNameParts == null || appSettings.Video.FileNameParts.Count == 0) { // 第一次获取,先设置默认值 - IsCreateFolderForMedia(isCreateFolderForMedia); - return isCreateFolderForMedia; + SetFileNameParts(fileNameParts); + return fileNameParts; } - return appSettings.Video.IsCreateFolderForMedia; + return appSettings.Video.FileNameParts; } /// - /// 设置是否为不同视频分别创建文件夹 + /// 设置文件命名格式 /// - /// + /// /// - public bool IsCreateFolderForMedia(AllowStatus isCreateFolderForMedia) + public bool SetFileNameParts(List fileNameParts) { - appSettings.Video.IsCreateFolderForMedia = isCreateFolderForMedia; - return SetSettings(); - } - - /// - /// 获取是否在下载视频的同时下载弹幕 - /// - /// - public AllowStatus IsDownloadDanmaku() - { - appSettings = GetSettings(); - if (appSettings.Video.IsDownloadDanmaku == 0) - { - // 第一次获取,先设置默认值 - IsDownloadDanmaku(isDownloadDanmaku); - return isDownloadDanmaku; - } - return appSettings.Video.IsDownloadDanmaku; - } - - /// - /// 设置是否在下载视频的同时下载弹幕 - /// - /// - /// - public bool IsDownloadDanmaku(AllowStatus isDownloadDanmaku) - { - appSettings.Video.IsDownloadDanmaku = isDownloadDanmaku; - return SetSettings(); - } - - /// - /// 获取是否在下载视频的同时下载封面 - /// - /// - public AllowStatus IsDownloadCover() - { - appSettings = GetSettings(); - if (appSettings.Video.IsDownloadCover == 0) - { - // 第一次获取,先设置默认值 - IsDownloadCover(isDownloadCover); - return isDownloadCover; - } - return appSettings.Video.IsDownloadCover; - } - - /// - /// 设置是否在下载视频的同时下载封面 - /// - /// - /// - public bool IsDownloadCover(AllowStatus isDownloadCover) - { - appSettings.Video.IsDownloadCover = isDownloadCover; + appSettings.Video.FileNameParts = fileNameParts; return SetSettings(); } diff --git a/src/DownKyi.Core/Utils/Format.cs b/src/DownKyi.Core/Utils/Format.cs index d8f08c8..c3a611c 100644 --- a/src/DownKyi.Core/Utils/Format.cs +++ b/src/DownKyi.Core/Utils/Format.cs @@ -1,4 +1,6 @@ -namespace DownKyi.Core.Utils +using System.Text.RegularExpressions; + +namespace DownKyi.Core.Utils { public static class Format { @@ -120,15 +122,15 @@ } else if (speed < 1024) { - formatSpeed = speed.ToString("#.##") + "B/s"; + formatSpeed = string.Format("{0:F2}", speed) + "B/s"; } else if (speed < 1024 * 1024) { - formatSpeed = (speed / 1024).ToString("#.##") + "KB/s"; + formatSpeed = string.Format("{0:F2}", speed / 1024) + "KB/s"; } else { - formatSpeed = (speed / 1024 / 1024).ToString("#.##") + "MB/s"; + formatSpeed = string.Format("{0:F2}", speed / 1024 / 1024) + "MB/s"; } return formatSpeed; } @@ -164,5 +166,39 @@ return formatFileSize; } + /// + /// 去除非法字符 + /// + /// + /// + public static string FormatFileName(string originName) + { + string destName = originName; + // Windows中不能作为文件名的字符 + destName = destName.Replace("\\", " "); + destName = destName.Replace("/", " "); + destName = destName.Replace(":", " "); + destName = destName.Replace("*", " "); + destName = destName.Replace("?", " "); + destName = destName.Replace("\"", " "); + destName = destName.Replace("<", " "); + destName = destName.Replace(">", " "); + destName = destName.Replace("|", " "); + + // 转义字符 + destName = destName.Replace("\a", ""); + destName = destName.Replace("\b", ""); + destName = destName.Replace("\f", ""); + destName = destName.Replace("\n", ""); + destName = destName.Replace("\r", ""); + destName = destName.Replace("\t", ""); + destName = destName.Replace("\v", ""); + + // 控制字符 + destName = Regex.Replace(destName, @"\p{C}+", string.Empty); + + return destName.Trim(); + } + } } diff --git a/src/DownKyi/App.xaml b/src/DownKyi/App.xaml index 34ef81c..7835032 100644 --- a/src/DownKyi/App.xaml +++ b/src/DownKyi/App.xaml @@ -9,6 +9,7 @@ + diff --git a/src/DownKyi/App.xaml.cs b/src/DownKyi/App.xaml.cs index bfad80b..d9b8f33 100644 --- a/src/DownKyi/App.xaml.cs +++ b/src/DownKyi/App.xaml.cs @@ -1,4 +1,6 @@ -using DownKyi.Utils; +using DownKyi.Models; +using DownKyi.Services.Download; +using DownKyi.Utils; using DownKyi.ViewModels; using DownKyi.ViewModels.Dialogs; using DownKyi.ViewModels.DownloadManager; @@ -11,6 +13,7 @@ using DownKyi.Views.Settings; using DownKyi.Views.Toolbox; using Prism.Ioc; using System; +using System.Collections.ObjectModel; using System.Windows; namespace DownKyi @@ -20,6 +23,12 @@ namespace DownKyi /// public partial class App { + public static ObservableCollection DownloadingList { get; set; } + public static ObservableCollection DownloadedList { get; set; } + + // 下载服务 + private IDownloadService downloadService; + protected override Window CreateShell() { // 设置主题 @@ -30,9 +39,27 @@ namespace DownKyi DictionaryResource.LoadLanguage("Default"); //DictionaryResource.LoadLanguage("en_US"); + // 初始化数据 + DownloadingList = new ObservableCollection(); + DownloadedList = new ObservableCollection(); + + // TODO 从数据库读取 + + // 启动下载服务 + downloadService = new AriaDownloadService(DownloadingList, DownloadedList); + downloadService.Start(); + return Container.Resolve(); } + protected override void OnExit(ExitEventArgs e) + { + // 关闭下载服务 + downloadService.End(); + + base.OnExit(e); + } + protected override void RegisterTypes(IContainerRegistry containerRegistry) { // pages diff --git a/src/DownKyi/DownKyi.csproj b/src/DownKyi/DownKyi.csproj index d2b905c..e75cd65 100644 --- a/src/DownKyi/DownKyi.csproj +++ b/src/DownKyi/DownKyi.csproj @@ -87,9 +87,15 @@ + + + + + + @@ -98,7 +104,10 @@ + + + diff --git a/src/DownKyi/Images/ButtonIcon.cs b/src/DownKyi/Images/ButtonIcon.cs index 73caefb..d747936 100644 --- a/src/DownKyi/Images/ButtonIcon.cs +++ b/src/DownKyi/Images/ButtonIcon.cs @@ -68,6 +68,41 @@ V8.5z M5.5,10v15h21V10H5.5z", Fill = "#FF000000" }; + + Delete = new VectorImage + { + Height = 18, + Width = 18, + Data = @"M634.29 513.52 l364.34 -363.32 q25.37 -27.4 25.37 -60.89 q0 -33.49 -26.38 -59.88 q-26.38 -26.39 -59.88 -26.39 + q-33.49 0 -60.9 25.38 l-363.32 364.33 l-363.32 -372.45 q-28.42 -20.3 -65.46 -20.3 q-37.04 0 -64.44 20.3 + q-20.3 27.4 -20.3 64.44 q0 37.04 20.3 65.46 l372.45 363.32 l-364.33 363.32 q-25.38 27.41 -25.38 60.9 q0 33.49 26.39 59.88 + q26.39 26.38 59.88 26.38 q33.49 0 60.89 -25.37 l363.32 -364.34 l363.32 364.34 q27.41 25.37 60.9 25.37 q33.49 0 59.88 -26.38 + q26.38 -26.38 26.38 -59.88 q0 -33.49 -25.37 -60.9 l-364.34 -363.32 Z", + Fill = "#FF000000" + }; + + Start = new VectorImage + { + Height = 20, + Width = 17, + Data = @"M895.12 402.34 l-633.28 -383.81 q-30.16 -17.82 -64.43 -18.51 q-34.27 -0.69 -65.11 16.45 q-30.84 17.13 -47.97 47.29 + q-17.13 30.16 -17.13 64.42 l0 767.62 q0 34.27 17.13 63.74 q17.14 29.47 47.97 47.29 q30.84 17.82 65.11 17.13 + q34.27 -0.69 64.43 -18.5 l633.28 -383.81 q28.79 -17.82 45.24 -46.6 q16.45 -28.79 16.45 -63.06 q0 -34.27 -16.45 -63.05 + q-16.45 -28.79 -45.24 -46.61 Z", + Fill = "#FF000000" + }; + + Pause = new VectorImage + { + Height = 20, + Width = 15, + Data = @"M255.66 0 q-53.75 0 -90.97 37.21 q-37.21 37.21 -37.21 90.96 l0 769.04 q1.38 55.12 37.21 90.95 q35.84 35.84 90.28 35.84 + q54.44 0 90.96 -35.84 q36.52 -35.83 37.9 -90.95 l0 -769.04 q-1.38 -53.75 -38.59 -90.96 q-37.21 -37.21 -89.58 -37.21 + ZM768.34 0 q-52.37 0 -89.58 37.21 q-37.21 37.21 -38.59 90.96 l0 769.04 q1.38 55.12 37.9 90.95 q36.52 35.84 90.96 35.84 + q54.44 0 90.28 -35.84 q35.83 -35.83 37.21 -90.95 l0 -769.04 q0 -53.75 -37.21 -90.96 q-37.22 -37.21 -90.97 -37.21 Z", + Fill = "#FF000000" + }; + } public VectorImage GeneralSearch { get; private set; } @@ -75,5 +110,9 @@ public VectorImage DownloadManage { get; private set; } public VectorImage Toolbox { get; private set; } + public VectorImage Delete { get; private set; } + public VectorImage Start { get; private set; } + public VectorImage Pause { get; private set; } + } } diff --git a/src/DownKyi/Images/LogoIcon.cs b/src/DownKyi/Images/LogoIcon.cs index 853394f..6e074bc 100644 --- a/src/DownKyi/Images/LogoIcon.cs +++ b/src/DownKyi/Images/LogoIcon.cs @@ -1,6 +1,6 @@ namespace DownKyi.Images { - class LogoIcon + public class LogoIcon { private static LogoIcon instance; public static LogoIcon Instance() diff --git a/src/DownKyi/Languages/Default.xaml b/src/DownKyi/Languages/Default.xaml index 559fef5..e3f29bb 100644 --- a/src/DownKyi/Languages/Default.xaml +++ b/src/DownKyi/Languages/Default.xaml @@ -64,10 +64,33 @@ 下载选中项 下载全部 + 已经添加到下载列表~ + 已经下载完成~ + 没有选中项符合下载要求! + 成功添加了 + 项~ + 正在下载 已下载 + 音频 + 视频 + 弹幕 + 字幕 + 封面 + 正在解析…… + 下载中…… + 混流中…… + 暂停中…… + 等待中…… + + 正在下载 + 个视频! + 全部暂停 + 全部开始 + 全部删除 + 按回车键应用设置 @@ -96,16 +119,23 @@ 视频 优先下载的视频编码: 优先下载的视频画质: - 启用视频编号 - 勾选后,将为下载完成的视频的文件名添加上序号 下载FLV视频后转码为mp4 使用默认下载目录 默认下载目录: 默认将文件下载到该文件夹中 更改目录 - 为不同视频分别创建文件夹 - 在下载视频的同时下载弹幕 - 在下载视频的同时下载封面 + 文件命名格式 + 文件名: + 可选字段: + 序号 + 视频章节 + 视频标题 + 分P标题 + 视频分区 + 音质 + 画质 + 视频编码 + 空格 弹幕 按类型屏蔽 diff --git a/src/DownKyi/Models/DisplayFileNamePart.cs b/src/DownKyi/Models/DisplayFileNamePart.cs new file mode 100644 index 0000000..4d8ab20 --- /dev/null +++ b/src/DownKyi/Models/DisplayFileNamePart.cs @@ -0,0 +1,17 @@ +using DownKyi.Core.FileName; +using Prism.Mvvm; + +namespace DownKyi.Models +{ + public class DisplayFileNamePart : BindableBase + { + public FileNamePart Id { get; set; } + + private string title; + public string Title + { + get => title; + set => SetProperty(ref title, value); + } + } +} diff --git a/src/DownKyi/Models/DownloadBaseItem.cs b/src/DownKyi/Models/DownloadBaseItem.cs new file mode 100644 index 0000000..6c6c102 --- /dev/null +++ b/src/DownKyi/Models/DownloadBaseItem.cs @@ -0,0 +1,122 @@ +using Prism.Mvvm; +using System; +using System.Collections.Generic; +using System.Windows.Media; + +namespace DownKyi.Models +{ + public class DownloadBaseItem : BindableBase + { + public DownloadBaseItem() + { + // 唯一id + Uuid = Guid.NewGuid().ToString("N"); + + // 初始化需要下载的内容 + NeedDownloadContent = new Dictionary + { + { "downloadAudio", true }, + { "downloadVideo", true }, + { "downloadDanmaku", true }, + { "downloadSubtitle", true }, + { "downloadCover", true } + }; + } + + // 此条下载项的id + public string Uuid { get; } + + // 需要下载的内容 + public Dictionary NeedDownloadContent { get; private set; } + + // 视频的id + public string Bvid { get; set; } + public long Avid { get; set; } + public long Cid { get; set; } + public long EpisodeId { get; set; } + + // 视频封面的url + public string CoverUrl { get; set; } + + private DrawingImage zoneImage; + public DrawingImage ZoneImage + { + get => zoneImage; + set => SetProperty(ref zoneImage, value); + } + + // 视频序号 + private int order; + public int Order + { + get => order; + set => SetProperty(ref order, value); + } + + // 视频主标题 + private string mainTitle; + public string MainTitle + { + get => mainTitle; + set => SetProperty(ref mainTitle, value); + } + + // 视频标题 + private string name; + public string Name + { + get => name; + set => SetProperty(ref name, value); + } + + // 时长 + private string duration; + public string Duration + { + get => duration; + set => SetProperty(ref duration, value); + } + + // 音频编码 + public int AudioCodecId { get; set; } + private string audioCodecName; + public string AudioCodecName + { + get => audioCodecName; + set => SetProperty(ref audioCodecName, value); + } + + // 视频编码 + // "hev1.2.4.L156.90" + // "avc1.640034" + //public string VideoCodecId { get; set; } + + // 视频编码名称,AVC、HEVC + private string videoCodecName; + public string VideoCodecName + { + get => videoCodecName; + set => SetProperty(ref videoCodecName, value); + } + + // 视频画质 + private Resolution resolution; + public Resolution Resolution + { + get => resolution; + set => SetProperty(ref resolution, value); + } + + // 文件路径,不包含扩展名,所有内容均以此路径下载 + public string FilePath { get; set; } + + // 文件大小 + private string fileSize; + public string FileSize + { + get => fileSize; + set => SetProperty(ref fileSize, value); + } + + } +} diff --git a/src/DownKyi/Models/DownloadStatus.cs b/src/DownKyi/Models/DownloadStatus.cs new file mode 100644 index 0000000..8aa0bd7 --- /dev/null +++ b/src/DownKyi/Models/DownloadStatus.cs @@ -0,0 +1,13 @@ +namespace DownKyi.Models +{ + public enum DownloadStatus + { + NOT_STARTED, // 未开始,从未开始下载 + WAIT_FOR_DOWNLOAD, // 等待下载,下载过,但是启动本次下载周期未开始,如重启程序后未开始 + PAUSE_STARTED, // 暂停启动下载 + PAUSE, // 暂停 + DOWNLOADING, // 下载中 + DOWNLOAD_SUCCEED, // 下载成功 + DOWNLOAD_FAILED, // 下载失败 + } +} diff --git a/src/DownKyi/Models/DownloadedItem.cs b/src/DownKyi/Models/DownloadedItem.cs new file mode 100644 index 0000000..c985651 --- /dev/null +++ b/src/DownKyi/Models/DownloadedItem.cs @@ -0,0 +1,9 @@ +namespace DownKyi.Models +{ + public class DownloadedItem : DownloadBaseItem + { + public DownloadedItem() : base() + { + } + } +} diff --git a/src/DownKyi/Models/DownloadingItem.cs b/src/DownKyi/Models/DownloadingItem.cs new file mode 100644 index 0000000..acde187 --- /dev/null +++ b/src/DownKyi/Models/DownloadingItem.cs @@ -0,0 +1,165 @@ +using DownKyi.Core.BiliApi.VideoStream.Models; +using DownKyi.Images; +using DownKyi.Utils; +using Prism.Commands; +using System.Collections.Generic; + +namespace DownKyi.Models +{ + public class DownloadingItem : DownloadBaseItem + { + public DownloadingItem() : base() + { + // 初始化下载的文件列表 + DownloadFiles = new List(); + + // 暂停继续按钮 + StartOrPause = ButtonIcon.Instance().Pause; + StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + + // 删除按钮 + Delete = ButtonIcon.Instance().Delete; + Delete.Fill = DictionaryResource.GetColor("ColorPrimary"); + } + + public PlayUrl PlayUrl { get; set; } + + // Aria相关 + public string Gid { get; set; } + + // 下载的文件 + public List DownloadFiles { get; private set; } + + // 视频类别 + public PlayStreamType PlayStreamType { get; set; } + + + // 正在下载内容(音频、视频、弹幕、字幕、封面) + private string downloadContent; + public string DownloadContent + { + get => downloadContent; + set => SetProperty(ref downloadContent, value); + } + + // 下载状态 + public DownloadStatus DownloadStatus { get; set; } + + // 下载状态显示 + private string downloadStatusTitle; + public string DownloadStatusTitle + { + get => downloadStatusTitle; + set => SetProperty(ref downloadStatusTitle, value); + } + + // 下载进度 + private float progress; + public float Progress + { + get => progress; + set => SetProperty(ref progress, value); + } + + // 已下载大小/文件大小 + private string downloadingFileSize; + public string DownloadingFileSize + { + get => downloadingFileSize; + set => SetProperty(ref downloadingFileSize, value); + } + + // 下载的最高速度 + public long MaxSpeed { get; set; } + + // 下载速度 + private string speedDisplay; + public string SpeedDisplay + { + get => speedDisplay; + set => SetProperty(ref speedDisplay, value); + } + + + #region 控制按钮 + + private VectorImage startOrPause; + public VectorImage StartOrPause + { + get => startOrPause; + set => SetProperty(ref startOrPause, value); + } + + private VectorImage delete; + public VectorImage Delete + { + get => delete; + set => SetProperty(ref delete, value); + } + + #endregion + + #region 命令申明 + + // 下载列表暂停继续事件 + private DelegateCommand startOrPauseCommand; + public DelegateCommand StartOrPauseCommand => startOrPauseCommand ?? (startOrPauseCommand = new DelegateCommand(ExecuteStartOrPauseCommand)); + + /// + /// 下载列表暂停继续事件 + /// + private void ExecuteStartOrPauseCommand() + { + switch (DownloadStatus) + { + case DownloadStatus.NOT_STARTED: + case DownloadStatus.WAIT_FOR_DOWNLOAD: + DownloadStatus = DownloadStatus.PAUSE_STARTED; + StartOrPause = ButtonIcon.Instance().Start; + StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + break; + case DownloadStatus.PAUSE_STARTED: + DownloadStatus = DownloadStatus.WAIT_FOR_DOWNLOAD; + StartOrPause = ButtonIcon.Instance().Pause; + StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + break; + case DownloadStatus.PAUSE: + DownloadStatus = DownloadStatus.DOWNLOADING; + StartOrPause = ButtonIcon.Instance().Pause; + StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + break; + case DownloadStatus.DOWNLOADING: + DownloadStatus = DownloadStatus.PAUSE; + StartOrPause = ButtonIcon.Instance().Start; + StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + break; + case DownloadStatus.DOWNLOAD_SUCCEED: + // 下载成功后会从下载列表中删除 + // 不会出现此分支 + break; + case DownloadStatus.DOWNLOAD_FAILED: + DownloadStatus = DownloadStatus.WAIT_FOR_DOWNLOAD; + StartOrPause = ButtonIcon.Instance().Pause; + StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + break; + default: + break; + } + } + + // 下载列表删除事件 + private DelegateCommand deleteCommand; + public DelegateCommand DeleteCommand => deleteCommand ?? (deleteCommand = new DelegateCommand(ExecuteDeleteCommand)); + + /// + /// 下载列表删除事件 + /// + private void ExecuteDeleteCommand() + { + App.DownloadingList.Remove(this); + } + + #endregion + + } +} diff --git a/src/DownKyi/Models/PlayStreamType.cs b/src/DownKyi/Models/PlayStreamType.cs new file mode 100644 index 0000000..959d7b0 --- /dev/null +++ b/src/DownKyi/Models/PlayStreamType.cs @@ -0,0 +1,9 @@ +namespace DownKyi.Models +{ + public enum PlayStreamType + { + VIDEO = 1, // 普通视频 + BANGUMI, // 番剧、电影、电视剧等 + CHEESE, // 课程 + } +} diff --git a/src/DownKyi/Models/VideoInfoView.cs b/src/DownKyi/Models/VideoInfoView.cs index 5eb92b3..d0d15d7 100644 --- a/src/DownKyi/Models/VideoInfoView.cs +++ b/src/DownKyi/Models/VideoInfoView.cs @@ -9,103 +9,104 @@ namespace DownKyi.Models { public string CoverUrl { get; set; } public long UpperMid { get; set; } + public int TypeId { get; set; } private BitmapImage cover; public BitmapImage Cover { - get { return cover; } - set { SetProperty(ref cover, value); } + get => cover; + set => SetProperty(ref cover, value); } private string title; public string Title { - get { return title; } - set { SetProperty(ref title, value); } + get => title; + set => SetProperty(ref title, value); } private string videoZone; public string VideoZone { - get { return videoZone; } - set { SetProperty(ref videoZone, value); } + get => videoZone; + set => SetProperty(ref videoZone, value); } private string createTime; public string CreateTime { - get { return createTime; } - set { SetProperty(ref createTime, value); } + get => createTime; + set => SetProperty(ref createTime, value); } private string playNumber; public string PlayNumber { - get { return playNumber; } - set { SetProperty(ref playNumber, value); } + get => playNumber; + set => SetProperty(ref playNumber, value); } private string danmakuNumber; public string DanmakuNumber { - get { return danmakuNumber; } - set { SetProperty(ref danmakuNumber, value); } + get => danmakuNumber; + set => SetProperty(ref danmakuNumber, value); } private string likeNumber; public string LikeNumber { - get { return likeNumber; } - set { SetProperty(ref likeNumber, value); } + get => likeNumber; + set => SetProperty(ref likeNumber, value); } private string coinNumber; public string CoinNumber { - get { return coinNumber; } - set { SetProperty(ref coinNumber, value); } + get => coinNumber; + set => SetProperty(ref coinNumber, value); } private string favoriteNumber; public string FavoriteNumber { - get { return favoriteNumber; } - set { SetProperty(ref favoriteNumber, value); } + get => favoriteNumber; + set => SetProperty(ref favoriteNumber, value); } private string shareNumber; public string ShareNumber { - get { return shareNumber; } - set { SetProperty(ref shareNumber, value); } + get => shareNumber; + set => SetProperty(ref shareNumber, value); } private string replyNumber; public string ReplyNumber { - get { return replyNumber; } - set { SetProperty(ref replyNumber, value); } + get => replyNumber; + set => SetProperty(ref replyNumber, value); } private string description; public string Description { - get { return description; } - set { SetProperty(ref description, value); } + get => description; + set => SetProperty(ref description, value); } private string upName; public string UpName { - get { return upName; } - set { SetProperty(ref upName, value); } + get => upName; + set => SetProperty(ref upName, value); } private BitmapImage upHeader; public BitmapImage UpHeader { - get { return upHeader; } - set { SetProperty(ref upHeader, value); } + get => upHeader; + set => SetProperty(ref upHeader, value); } } diff --git a/src/DownKyi/Models/VideoPage.cs b/src/DownKyi/Models/VideoPage.cs index 11ea48d..7b93e92 100644 --- a/src/DownKyi/Models/VideoPage.cs +++ b/src/DownKyi/Models/VideoPage.cs @@ -13,60 +13,62 @@ namespace DownKyi.Models public long Cid { get; set; } public long EpisodeId { get; set; } + public string FirstFrame { get; set; } + private bool isSelected; public bool IsSelected { - get { return isSelected; } - set { SetProperty(ref isSelected, value); } + get => isSelected; + set => SetProperty(ref isSelected, value); } private int order; public int Order { - get { return order; } - set { SetProperty(ref order, value); } + get => order; + set => SetProperty(ref order, value); } private string name; public string Name { - get { return name; } - set { SetProperty(ref name, value); } + get => name; + set => SetProperty(ref name, value); } private string duration; public string Duration { - get { return duration; } - set { SetProperty(ref duration, value); } + get => duration; + set => SetProperty(ref duration, value); } private List audioQualityFormatList; public List AudioQualityFormatList { - get { return audioQualityFormatList; } - set { SetProperty(ref audioQualityFormatList, value); } + get => audioQualityFormatList; + set => SetProperty(ref audioQualityFormatList, value); } private string audioQualityFormat; public string AudioQualityFormat { - get { return audioQualityFormat; } - set { SetProperty(ref audioQualityFormat, value); } + get => audioQualityFormat; + set => SetProperty(ref audioQualityFormat, value); } private List videoQualityList; public List VideoQualityList { - get { return videoQualityList; } - set { SetProperty(ref videoQualityList, value); } + get => videoQualityList; + set => SetProperty(ref videoQualityList, value); } private VideoQuality videoQuality; public VideoQuality VideoQuality { - get { return videoQuality; } - set { SetProperty(ref videoQuality, value); } + get => videoQuality; + set => SetProperty(ref videoQuality, value); } } diff --git a/src/DownKyi/Models/VideoQuality.cs b/src/DownKyi/Models/VideoQuality.cs index 6df9996..41b966d 100644 --- a/src/DownKyi/Models/VideoQuality.cs +++ b/src/DownKyi/Models/VideoQuality.cs @@ -8,30 +8,29 @@ namespace DownKyi.Models private int quality; public int Quality { - get { return quality; } - set { SetProperty(ref quality, value); } + get => quality; + set => SetProperty(ref quality, value); } private string qualityFormat; public string QualityFormat { - get { return qualityFormat; } - set { SetProperty(ref qualityFormat, value); } + get => qualityFormat; + set => SetProperty(ref qualityFormat, value); } private List videoCodecList; public List VideoCodecList { - get { return videoCodecList; } - set { SetProperty(ref videoCodecList, value); } + get => videoCodecList; + set => SetProperty(ref videoCodecList, value); } private string selectedVideoCodec; public string SelectedVideoCodec { - get { return selectedVideoCodec; } - set { SetProperty(ref selectedVideoCodec, value); } + get => selectedVideoCodec; + set => SetProperty(ref selectedVideoCodec, value); } - } } diff --git a/src/DownKyi/Services/BangumiInfoService.cs b/src/DownKyi/Services/BangumiInfoService.cs index 17f0d87..cd89dbf 100644 --- a/src/DownKyi/Services/BangumiInfoService.cs +++ b/src/DownKyi/Services/BangumiInfoService.cs @@ -8,6 +8,7 @@ using DownKyi.Models; using DownKyi.Utils; using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using System.Windows.Media.Imaging; namespace DownKyi.Services @@ -63,21 +64,28 @@ namespace DownKyi.Services string name; // 判断title是否为数字,如果是,则将share_copy作为name,否则将title作为name - if (int.TryParse(episode.Title, out int result)) - { - name = episode.ShareCopy; - } - else - { - if (episode.LongTitle != null && episode.LongTitle != "") - { - name = $"{episode.Title} {episode.LongTitle}"; - } - else - { - name = episode.Title; - } - } + //if (int.TryParse(episode.Title, out int result)) + //{ + // name = Regex.Replace(episode.ShareCopy, @"《.*?》", ""); + // //name = episode.ShareCopy; + //} + //else + //{ + // if (episode.LongTitle != null && episode.LongTitle != "") + // { + // name = $"{episode.Title} {episode.LongTitle}"; + // } + // else + // { + // name = episode.Title; + // } + //} + + // 将share_copy作为name,删除《》中的标题 + name = Regex.Replace(episode.ShareCopy, @"^《.*?》", ""); + + // 删除前后空白符 + name = name.Trim(); VideoPage page = new VideoPage { @@ -85,6 +93,7 @@ namespace DownKyi.Services Bvid = episode.Bvid, Cid = episode.Cid, EpisodeId = -1, + FirstFrame = episode.Cover, Order = order, Name = name, Duration = "N/A" @@ -141,6 +150,7 @@ namespace DownKyi.Services Bvid = episode.Bvid, Cid = episode.Cid, EpisodeId = -1, + FirstFrame = episode.Cover, Order = order, Name = name, Duration = "N/A" @@ -210,6 +220,9 @@ namespace DownKyi.Services videoInfoView.Cover = cover == null ? null : new BitmapImage(new Uri(cover)); videoInfoView.Title = bangumiSeason.Title; + // 分区id + videoInfoView.TypeId = BangumiType.TypeId[bangumiSeason.Type]; + videoInfoView.VideoZone = DictionaryResource.GetString(BangumiType.Type[bangumiSeason.Type]); videoInfoView.PlayNumber = Format.FormatNumber(bangumiSeason.Stat.Views); diff --git a/src/DownKyi/Services/CheeseInfoService.cs b/src/DownKyi/Services/CheeseInfoService.cs index 1a25a12..81f0987 100644 --- a/src/DownKyi/Services/CheeseInfoService.cs +++ b/src/DownKyi/Services/CheeseInfoService.cs @@ -61,6 +61,7 @@ namespace DownKyi.Services Bvid = null, Cid = episode.Cid, EpisodeId = episode.Id, + FirstFrame = episode.Cover, Order = order, Name = name, Duration = "N/A" @@ -129,6 +130,10 @@ namespace DownKyi.Services videoInfoView.Cover = cover == null ? null : new BitmapImage(new Uri(cover)); videoInfoView.Title = cheeseView.Title; + // 分区id + // 课堂的type id B站没有定义,这里自定义为-10 + videoInfoView.TypeId = -10; + videoInfoView.VideoZone = DictionaryResource.GetString("Cheese"); videoInfoView.CreateTime = ""; diff --git a/src/DownKyi/Services/Download/AriaDownloadService.cs b/src/DownKyi/Services/Download/AriaDownloadService.cs new file mode 100644 index 0000000..abcf28a --- /dev/null +++ b/src/DownKyi/Services/Download/AriaDownloadService.cs @@ -0,0 +1,763 @@ +using DownKyi.Core.Aria2cNet; +using DownKyi.Core.Aria2cNet.Client; +using DownKyi.Core.Aria2cNet.Client.Entity; +using DownKyi.Core.Aria2cNet.Server; +using DownKyi.Core.BiliApi.Login; +using DownKyi.Core.BiliApi.VideoStream; +using DownKyi.Core.BiliApi.VideoStream.Models; +using DownKyi.Core.Danmaku2Ass; +using DownKyi.Core.FFmpeg; +using DownKyi.Core.Logging; +using DownKyi.Core.Settings; +using DownKyi.Core.Storage; +using DownKyi.Core.Utils; +using DownKyi.Models; +using DownKyi.Utils; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace DownKyi.Services.Download +{ + /// + /// 音视频采用Aria下载,其余采用WebClient下载 + /// + public class AriaDownloadService : DownloadService, IDownloadService + { + private CancellationTokenSource tokenSource; + + public AriaDownloadService(ObservableCollection downloadingList, ObservableCollection downloadedList) : base(downloadingList, downloadedList) + { + Tag = "AriaDownloadService"; + } + + #region 音视频 + + /// + /// 下载音频,返回下载文件路径 + /// + /// + /// + public string DownloadAudio(DownloadingItem downloading) + { + // 更新状态显示 + downloading.DownloadStatusTitle = DictionaryResource.GetString("WhileDownloading"); + downloading.DownloadContent = DictionaryResource.GetString("DownloadingAudio"); + + // 如果没有Dash,返回null + if (downloading.PlayUrl == null || downloading.PlayUrl.Dash == null) { return null; } + + // 如果audio列表没有内容,则返回null + if (downloading.PlayUrl.Dash.Audio == null) { return null; } + else if (downloading.PlayUrl.Dash.Audio.Count == 0) { return null; } + + // 根据音频id匹配 + PlayUrlDashVideo downloadAudio = null; + foreach (PlayUrlDashVideo audio in downloading.PlayUrl.Dash.Audio) + { + if (audio.Id == downloading.AudioCodecId) + { + downloadAudio = audio; + break; + } + } + + return DownloadVideo(downloading, downloadAudio); + } + + /// + /// 下载视频,返回下载文件路径 + /// + /// + /// + public string DownloadVideo(DownloadingItem downloading) + { + // 更新状态显示 + downloading.DownloadStatusTitle = DictionaryResource.GetString("WhileDownloading"); + downloading.DownloadContent = DictionaryResource.GetString("DownloadingVideo"); + + // 如果没有Dash,返回null + if (downloading.PlayUrl == null || downloading.PlayUrl.Dash == null) { 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 (PlayUrlDashVideo video in downloading.PlayUrl.Dash.Video) + { + if (video.Id == downloading.Resolution.Id && Utils.GetVideoCodecName(video.Codecs) == downloading.VideoCodecName) + { + downloadVideo = video; + break; + } + } + + return DownloadVideo(downloading, downloadVideo); + } + + /// + /// 将下载音频和视频的函数中相同代码抽象出来 + /// + /// + /// + /// + private string DownloadVideo(DownloadingItem downloading, PlayUrlDashVideo downloadVideo) + { + // 如果为空,说明没有匹配到可下载的音频视频 + if (downloadVideo == null) { return null; } + + // 下载链接 + List urls = new List(); + if (downloadVideo.BaseUrl != null) { urls.Add(downloadVideo.BaseUrl); } + if (downloadVideo.BackupUrl != null) { urls.AddRange(downloadVideo.BackupUrl); } + + // 路径 + string[] temp = downloading.FilePath.Split('/'); + string path = downloading.FilePath.Replace(temp[temp.Length - 1], ""); + + // 下载文件名 + string fileName = Guid.NewGuid().ToString("N"); + + // 记录本次下载的文件 + downloading.DownloadFiles.Add(fileName); + + // 开始下载 + DownloadResult downloadStatus = DownloadByAria(downloading, urls, path, fileName); + switch (downloadStatus) + { + case DownloadResult.SUCCESS: + return Path.Combine(path, fileName); + case DownloadResult.FAILED: + return null; + case DownloadResult.ABORT: + return null; + default: + return null; + } + } + + #endregion + + /// + /// 下载封面 + /// + /// + public string DownloadCover(DownloadingItem downloading) + { + // 更新状态显示 + downloading.DownloadStatusTitle = DictionaryResource.GetString("WhileDownloading"); + downloading.DownloadContent = DictionaryResource.GetString("DownloadingCover"); + // 下载大小 + downloading.DownloadingFileSize = string.Empty; + // 下载速度 + downloading.SpeedDisplay = string.Empty; + + // 查询、保存封面 + StorageCover storageCover = new StorageCover(); + string cover = storageCover.GetCover(downloading.Avid, downloading.Bvid, downloading.Cid, downloading.CoverUrl); + if (cover == null) + { + return null; + } + + // 图片的扩展名 + string[] temp = downloading.CoverUrl.Split('.'); + string fileExtension = temp[temp.Length - 1]; + + // 图片的地址 + string coverPath = $"{StorageManager.GetCover()}/{cover}"; + + // 复制图片到指定位置 + try + { + string fileName = $"{downloading.FilePath}.{fileExtension}"; + File.Copy(coverPath, fileName); + + // 记录本次下载的文件 + downloading.DownloadFiles.Add(fileName); + + return fileName; + } + catch (Exception e) + { + Core.Utils.Debugging.Console.PrintLine(e); + LogManager.Error(Tag, e); + } + + return null; + } + + /// + /// 下载弹幕 + /// + /// + public string DownloadDanmaku(DownloadingItem downloading) + { + // 更新状态显示 + downloading.DownloadStatusTitle = DictionaryResource.GetString("WhileDownloading"); + downloading.DownloadContent = DictionaryResource.GetString("DownloadingDanmaku"); + // 下载大小 + downloading.DownloadingFileSize = string.Empty; + // 下载速度 + downloading.SpeedDisplay = string.Empty; + + string title = $"{downloading.Name}"; + string assFile = $"{downloading.FilePath}.ass"; + + // 记录本次下载的文件 + downloading.DownloadFiles.Add(assFile); + + int screenWidth = SettingsManager.GetInstance().GetDanmakuScreenWidth(); + int screenHeight = SettingsManager.GetInstance().GetDanmakuScreenHeight(); + //if (SettingsManager.GetInstance().IsCustomDanmakuResolution() != AllowStatus.YES) + //{ + // if (downloadingEntity.Width > 0 && downloadingEntity.Height > 0) + // { + // screenWidth = downloadingEntity.Width; + // screenHeight = downloadingEntity.Height; + // } + //} + + // 字幕配置 + Config subtitleConfig = new Config + { + Title = title, + ScreenWidth = screenWidth, + ScreenHeight = screenHeight, + FontName = SettingsManager.GetInstance().GetDanmakuFontName(), + BaseFontSize = SettingsManager.GetInstance().GetDanmakuFontSize(), + LineCount = SettingsManager.GetInstance().GetDanmakuLineCount(), + LayoutAlgorithm = SettingsManager.GetInstance().GetDanmakuLayoutAlgorithm().ToString("G").ToLower(), // async/sync + TuneDuration = 0, + DropOffset = 0, + BottomMargin = 0, + CustomOffset = 0 + }; + + Core.Danmaku2Ass.Bilibili.GetInstance() + .SetTopFilter(SettingsManager.GetInstance().GetDanmakuTopFilter() == AllowStatus.YES) + .SetBottomFilter(SettingsManager.GetInstance().GetDanmakuBottomFilter() == AllowStatus.YES) + .SetScrollFilter(SettingsManager.GetInstance().GetDanmakuScrollFilter() == AllowStatus.YES) + .Create(downloading.Avid, downloading.Cid, subtitleConfig, assFile); + + return assFile; + } + + /// + /// 下载字幕 + /// + /// + public List DownloadSubtitle(DownloadingItem downloading) + { + // 更新状态显示 + downloading.DownloadStatusTitle = DictionaryResource.GetString("WhileDownloading"); + downloading.DownloadContent = DictionaryResource.GetString("DownloadingSubtitle"); + // 下载大小 + downloading.DownloadingFileSize = string.Empty; + // 下载速度 + downloading.SpeedDisplay = string.Empty; + + List srtFiles = new List(); + + var subRipTexts = VideoStream.GetSubtitle(downloading.Avid, downloading.Bvid, downloading.Cid); + if (subRipTexts == null) + { + return null; + } + + foreach (var subRip in subRipTexts) + { + string srtFile = $"{downloading.FilePath}_{subRip.LanDoc}.srt"; + try + { + File.WriteAllText(srtFile, subRip.SrtString); + + // 记录本次下载的文件 + downloading.DownloadFiles.Add(srtFile); + + srtFiles.Add(srtFile); + } + catch (Exception e) + { + Core.Utils.Debugging.Console.PrintLine("DownloadSubtitle()发生异常: {0}", e); + LogManager.Error("DownloadSubtitle()", e); + } + } + + return srtFiles; + } + + /// + /// 混流音频和视频 + /// + /// + /// + /// + /// + public string MixedFlow(DownloadingItem downloading, string audioUid, string videoUid) + { + // 更新状态显示 + downloading.DownloadStatusTitle = DictionaryResource.GetString("MixedFlow"); + downloading.DownloadContent = DictionaryResource.GetString("DownloadingVideo"); + // 下载大小 + downloading.DownloadingFileSize = string.Empty; + // 下载速度 + downloading.SpeedDisplay = string.Empty; + + string finalFile = $"{downloading.FilePath}.mp4"; + if (videoUid == null) + { + finalFile = $"{downloading.FilePath}.aac"; + } + + // 合并音视频 + FFmpegHelper.MergeVideo(audioUid, videoUid, finalFile); + + // 获取文件大小 + if (File.Exists(finalFile)) + { + FileInfo info = new FileInfo(finalFile); + downloading.FileSize = Format.FormatFileSize(info.Length); + } + else + { + downloading.FileSize = Format.FormatFileSize(0); + } + + return finalFile; + } + + /// + /// 解析视频流的下载链接 + /// + /// + public void Parse(DownloadingItem downloading) + { + // 更新状态显示 + downloading.DownloadStatusTitle = DictionaryResource.GetString("Parsing"); + downloading.DownloadContent = string.Empty; + // 下载大小 + downloading.DownloadingFileSize = string.Empty; + // 下载速度 + downloading.SpeedDisplay = string.Empty; + + if (downloading.PlayUrl != null && downloading.DownloadStatus == DownloadStatus.NOT_STARTED) + { + return; + } + + // 解析 + switch (downloading.PlayStreamType) + { + case PlayStreamType.VIDEO: + downloading.PlayUrl = VideoStream.GetVideoPlayUrl(downloading.Avid, downloading.Bvid, downloading.Cid); + break; + case PlayStreamType.BANGUMI: + downloading.PlayUrl = VideoStream.GetBangumiPlayUrl(downloading.Avid, downloading.Bvid, downloading.Cid); + break; + case PlayStreamType.CHEESE: + downloading.PlayUrl = VideoStream.GetCheesePlayUrl(downloading.Avid, downloading.Bvid, downloading.Cid, downloading.EpisodeId); + break; + default: + break; + } + } + + /// + /// 停止下载服务 + /// + public void End() + { + // TODO + // 保存数据 + + // 关闭Aria服务器 + CloseAriaServer(); + + // 结束任务 + tokenSource.Cancel(); + } + + /// + /// 启动下载服务 + /// + public async void Start() + { + // 启动Aria服务器 + StartAriaServer(); + + await Task.Run(DoWork, (tokenSource = new CancellationTokenSource()).Token); + } + + /// + /// 执行任务 + /// + private void DoWork() + { + CancellationToken cancellationToken = tokenSource.Token; + while (true) + { + int maxDownloading = SettingsManager.GetInstance().GetAriaMaxConcurrentDownloads(); + int downloadingCount = 0; + foreach (DownloadingItem downloading in downloadingList) + { + if (downloading.DownloadStatus == DownloadStatus.DOWNLOADING) + { + downloadingCount++; + } + } + + foreach (DownloadingItem downloading in downloadingList) + { + if (downloadingCount >= maxDownloading) + { + break; + } + + // 开始下载 + if (downloading.DownloadStatus == DownloadStatus.NOT_STARTED || downloading.DownloadStatus == DownloadStatus.WAIT_FOR_DOWNLOAD) + { + SingleDownload(downloading); + downloadingCount++; + } + } + + // 判断是否该结束线程,若为true,跳出while循环 + if (cancellationToken.IsCancellationRequested) + { + Core.Utils.Debugging.Console.PrintLine("AriaDownloadService: 下载服务结束,跳出while循环"); + LogManager.Debug(Tag, "下载服务结束"); + break; + } + + // 降低CPU占用 + Thread.Sleep(100); + } + } + + /// + /// 下载一个视频 + /// + /// + /// + private async void SingleDownload(DownloadingItem downloading) + { + await Task.Run(new Action(() => + { + downloading.DownloadStatus = DownloadStatus.DOWNLOADING; + + // 初始化 + downloading.DownloadStatusTitle = string.Empty; + downloading.DownloadContent = string.Empty; + downloading.DownloadFiles.Clear(); + + // 解析并依次下载音频、视频、弹幕、字幕、封面等内容 + Parse(downloading); + + // 暂停 + Pause(downloading); + + string audioUid = null; + // 如果需要下载音频 + if (downloading.NeedDownloadContent["downloadAudio"]) + { + audioUid = DownloadAudio(downloading); + } + + // 暂停 + Pause(downloading); + + string videoUid = null; + // 如果需要下载视频 + if (downloading.NeedDownloadContent["downloadVideo"]) + { + videoUid = DownloadVideo(downloading); + } + + // 暂停 + Pause(downloading); + + string outputDanmaku = null; + // 如果需要下载弹幕 + if (downloading.NeedDownloadContent["downloadDanmaku"]) + { + outputDanmaku = DownloadDanmaku(downloading); + } + + // 暂停 + Pause(downloading); + + List outputSubtitles = null; + // 如果需要下载字幕 + if (downloading.NeedDownloadContent["downloadSubtitle"]) + { + outputSubtitles = DownloadSubtitle(downloading); + } + + // 暂停 + Pause(downloading); + + string outputCover = null; + // 如果需要下载封面 + if (downloading.NeedDownloadContent["downloadCover"]) + { + outputCover = DownloadCover(downloading); + } + + // 暂停 + Pause(downloading); + + // 混流 + string outputMedia = MixedFlow(downloading, audioUid, videoUid); + + // 暂停 + Pause(downloading); + + // 检测音频、视频是否下载成功 + if (downloading.NeedDownloadContent["downloadAudio"] || downloading.NeedDownloadContent["downloadVideo"]) + { + // 只有下载音频不下载视频时才输出aac + // 只要下载视频就输出mp4 + if (File.Exists(outputMedia)) + { + // 成功 + } + } + + // 检测弹幕是否下载成功 + if (downloading.NeedDownloadContent["downloadDanmaku"] && File.Exists(outputDanmaku)) + { + // 成功 + } + + // 检测字幕是否下载成功 + if (downloading.NeedDownloadContent["downloadSubtitle"]) + { + if (outputSubtitles == null) + { + // 为null时表示不存在字幕 + } + else + { + foreach (string subtitle in outputSubtitles) + { + if (File.Exists(subtitle)) + { + // 成功 + } + } + } + } + + // 检测封面是否下载成功 + if (downloading.NeedDownloadContent["downloadCover"] && File.Exists(outputCover)) + { + // 成功 + } + + // TODO + // 将下载结果写入数据库 + // 包括下载请求的DownloadingItem对象, + // 下载结果是否成功等 + // 对是否成功的判断:只要outputMedia存在则成功,否则失败 + + })); + } + + /// + /// 强制暂停 + /// + /// + private void Pause(DownloadingItem downloading) + { + string oldStatus = downloading.DownloadStatusTitle; + downloading.DownloadStatusTitle = DictionaryResource.GetString("Pausing"); + while (downloading.DownloadStatus == DownloadStatus.PAUSE) + { + // 降低CPU占用 + Thread.Sleep(100); + } + downloading.DownloadStatusTitle = DictionaryResource.GetString("Waiting"); + + int maxDownloading = SettingsManager.GetInstance().GetAriaMaxConcurrentDownloads(); + int downloadingCount; + do + { + downloadingCount = 0; + foreach (DownloadingItem item in downloadingList) + { + if (item.DownloadStatus == DownloadStatus.DOWNLOADING) + { + downloadingCount++; + } + } + + // 降低CPU占用 + Thread.Sleep(100); + } while (downloadingCount > maxDownloading); + + downloading.DownloadStatusTitle = oldStatus; + } + + /// + /// 启动Aria服务器 + /// + private async void StartAriaServer() + { + List header = new List + { + $"Cookie: {LoginHelper.GetLoginInfoCookiesString()}", + $"Origin: https://www.bilibili.com", + $"Referer: https://www.bilibili.com", + $"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 91.0.4472.77 Safari / 537.36" + }; + + AriaConfig config = new AriaConfig() + { + ListenPort = SettingsManager.GetInstance().GetAriaListenPort(), + Token = "downkyi", + LogLevel = SettingsManager.GetInstance().GetAriaLogLevel(), + MaxConcurrentDownloads = SettingsManager.GetInstance().GetAriaMaxConcurrentDownloads(), + MaxConnectionPerServer = 16, // 最大取16 + Split = SettingsManager.GetInstance().GetAriaSplit(), + //MaxTries = 5, + MinSplitSize = 10, // 10MB + MaxOverallDownloadLimit = SettingsManager.GetInstance().GetAriaMaxOverallDownloadLimit() * 1024L, // 输入的单位是KB/s,所以需要乘以1024 + MaxDownloadLimit = SettingsManager.GetInstance().GetAriaMaxDownloadLimit() * 1024L, // 输入的单位是KB/s,所以需要乘以1024 + MaxOverallUploadLimit = 0, + MaxUploadLimit = 0, + ContinueDownload = true, + FileAllocation = SettingsManager.GetInstance().GetAriaFileAllocation(), + Headers = header + }; + var task = await AriaServer.StartServerAsync(config); + if (task) { Console.WriteLine("Start ServerAsync Completed"); } + Console.WriteLine("Start ServerAsync end"); + + // 恢复所有下载 + //var ariaPause = await AriaClient.UnpauseAllAsync(); + //if (ariaPause != null) + //{ + // Core.Utils.Debugging.Console.PrintLine(ariaPause.ToString()); + //} + } + + /// + /// 关闭Aria服务器 + /// + private void CloseAriaServer() + { + new Thread(() => + { + // 暂停所有下载 + var ariaPause = AriaClient.PauseAllAsync(); + Core.Utils.Debugging.Console.PrintLine(ariaPause.ToString()); + + // 关闭服务器 + bool close = AriaServer.CloseServer(); + Core.Utils.Debugging.Console.PrintLine(close); + }) + { IsBackground = false } + .Start(); + } + + /// + /// 采用Aria下载文件 + /// + /// + /// + private DownloadResult DownloadByAria(DownloadingItem downloading, List urls, string path, string localFileName) + { + // path已斜杠结尾,去掉斜杠 + path = path.TrimEnd('/').TrimEnd('\\'); + + AriaSendOption option = new AriaSendOption + { + //HttpProxy = $"http://{Settings.GetAriaHttpProxy()}:{Settings.GetAriaHttpProxyListenPort()}", + Dir = path, + Out = localFileName + //Header = $"cookie: {Login.GetLoginInfoCookiesString()}\nreferer: https://www.bilibili.com", + //UseHead = "true", + //UserAgent = Utils.GetUserAgent() + }; + + // 如果设置了代理,则增加HttpProxy + if (SettingsManager.GetInstance().IsAriaHttpProxy() == AllowStatus.YES) + { + option.HttpProxy = $"http://{SettingsManager.GetInstance().GetAriaHttpProxy()}:{SettingsManager.GetInstance().GetAriaHttpProxyListenPort()}"; + } + + // 添加一个下载 + Task ariaAddUri = AriaClient.AddUriAsync(urls, option); + if (ariaAddUri == null || ariaAddUri.Result == null || ariaAddUri.Result.Result == null) + { + return DownloadResult.FAILED; + } + + // 保存gid + string gid = ariaAddUri.Result.Result; + downloading.Gid = gid; + + // 管理下载 + AriaManager ariaManager = new AriaManager(); + ariaManager.TellStatus += AriaTellStatus; + ariaManager.DownloadFinish += AriaDownloadFinish; + return ariaManager.GetDownloadStatus(gid, new Action(() => + { + switch (downloading.DownloadStatus) + { + case DownloadStatus.PAUSE: + Task ariaPause = AriaClient.PauseAsync(downloading.Gid); + // 通知UI,并阻塞当前线程 + Pause(downloading); + break; + case DownloadStatus.DOWNLOADING: + Task ariaUnpause = AriaClient.UnpauseAsync(downloading.Gid); + break; + } + })); + } + + private void AriaTellStatus(long totalLength, long completedLength, long speed, string gid) + { + // 当前的下载视频 + DownloadingItem video = downloadingList.FirstOrDefault(it => it.Gid == gid); + if (video == null) { return; } + + float percent = 0; + if (totalLength != 0) + { + percent = (float)completedLength / totalLength * 100; + } + + // 根据进度判断本次是否需要更新UI + if (Math.Abs(percent - video.Progress) < 0.01) { return; } + + // 下载进度 + video.Progress = percent; + + // 下载大小 + video.DownloadingFileSize = Format.FormatFileSize(completedLength) + "/" + Format.FormatFileSize(totalLength); + + // 下载速度 + video.SpeedDisplay = Format.FormatSpeed(speed); + + // 最大下载速度 + if (video.MaxSpeed < speed) + { + video.MaxSpeed = speed; + } + } + + private void AriaDownloadFinish(bool isSuccess, string downloadPath, string gid, string msg) + { + //throw new NotImplementedException(); + } + } +} diff --git a/src/DownKyi/Services/Download/DownloadService.cs b/src/DownKyi/Services/Download/DownloadService.cs new file mode 100644 index 0000000..07288ee --- /dev/null +++ b/src/DownKyi/Services/Download/DownloadService.cs @@ -0,0 +1,25 @@ +using DownKyi.Models; +using System.Collections.ObjectModel; + +namespace DownKyi.Services.Download +{ + public class DownloadService + { + protected string Tag = "DownloadService"; + + protected ObservableCollection downloadingList; + protected ObservableCollection downloadedList; + + /// + /// 初始化 + /// + /// + /// + public DownloadService(ObservableCollection downloadingList, ObservableCollection downloadedList) + { + this.downloadingList = downloadingList; + this.downloadedList = downloadedList; + } + + } +} diff --git a/src/DownKyi/Services/Download/IDownloadService.cs b/src/DownKyi/Services/Download/IDownloadService.cs new file mode 100644 index 0000000..8facfb4 --- /dev/null +++ b/src/DownKyi/Services/Download/IDownloadService.cs @@ -0,0 +1,19 @@ +using DownKyi.Models; +using System.Collections.Generic; + +namespace DownKyi.Services.Download +{ + 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 MixedFlow(DownloadingItem downloading, string audioUid, string videoUid); + + void Start(); + void End(); + } +} diff --git a/src/DownKyi/Services/Utils.cs b/src/DownKyi/Services/Utils.cs index 3807f04..c16cf98 100644 --- a/src/DownKyi/Services/Utils.cs +++ b/src/DownKyi/Services/Utils.cs @@ -26,9 +26,9 @@ namespace DownKyi.Services page.PlayUrl = playUrl; // 获取设置 - var userInfo = SettingsManager.GetInstance().GetUserInfo(); + UserInfoSettings userInfo = SettingsManager.GetInstance().GetUserInfo(); int defaultQuality = SettingsManager.GetInstance().GetQuality(); - var videoCodecs = SettingsManager.GetInstance().GetVideoCodecs(); + VideoCodecs videoCodecs = SettingsManager.GetInstance().GetVideoCodecs(); int defaultAudioQuality = SettingsManager.GetInstance().GetAudioQuality(); // 未登录时,最高仅720P @@ -201,7 +201,7 @@ namespace DownKyi.Services /// /// /// - private static string GetVideoCodecName(string origin) + internal static string GetVideoCodecName(string origin) { return origin.Contains("avc") ? "H.264/AVC" : origin.Contains("hev") ? "H.265/HEVC" : ""; } diff --git a/src/DownKyi/Services/VideoInfoService.cs b/src/DownKyi/Services/VideoInfoService.cs index 49c6b60..a807aaa 100644 --- a/src/DownKyi/Services/VideoInfoService.cs +++ b/src/DownKyi/Services/VideoInfoService.cs @@ -78,6 +78,7 @@ namespace DownKyi.Services Bvid = videoView.Bvid, Cid = page.Cid, EpisodeId = -1, + FirstFrame = page.FirstFrame, Order = order, Name = name, Duration = "N/A" @@ -114,6 +115,7 @@ namespace DownKyi.Services Bvid = episode.Bvid, Cid = episode.Cid, EpisodeId = -1, + FirstFrame = episode.Page.FirstFrame, Order = order, Name = episode.Title, Duration = "N/A" @@ -160,7 +162,7 @@ namespace DownKyi.Services // 分区 string videoZone = string.Empty; - var zoneList = Core.BiliApi.Zone.VideoZone.Instance().GetZone(); + var zoneList = Core.BiliApi.Zone.VideoZone.Instance().GetZones(); var zone = zoneList.Find(it => it.Id == videoView.Tid); if (zone != null) { @@ -203,6 +205,9 @@ namespace DownKyi.Services videoInfoView.Cover = cover == null ? null : new BitmapImage(new Uri(cover)); videoInfoView.Title = videoView.Title; + // 分区id + videoInfoView.TypeId = videoView.Tid; + videoInfoView.VideoZone = videoZone; DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); // 当地时区 diff --git a/src/DownKyi/Themes/ColorBrush.xaml b/src/DownKyi/Themes/ColorBrush.xaml index bc81eda..181e106 100644 --- a/src/DownKyi/Themes/ColorBrush.xaml +++ b/src/DownKyi/Themes/ColorBrush.xaml @@ -23,6 +23,7 @@ + @@ -34,6 +35,7 @@ + diff --git a/src/DownKyi/Themes/Colors/ColorDefault.xaml b/src/DownKyi/Themes/Colors/ColorDefault.xaml index fd949c1..7ce177c 100644 --- a/src/DownKyi/Themes/Colors/ColorDefault.xaml +++ b/src/DownKyi/Themes/Colors/ColorDefault.xaml @@ -23,6 +23,7 @@ #FFFFFFFF #FF00A1D6 #FFBDBDBD + #FFE4E4E4 #FFBDBDBD #C8BDBDBD #7FBDBDBD @@ -34,6 +35,7 @@ #FFF4F4F4 #FF999999 + #7F999999 #7FD0D0D0 #7FD0D0D0 diff --git a/src/DownKyi/Themes/Styles/StyleListBox.xaml b/src/DownKyi/Themes/Styles/StyleListBox.xaml index 0cd9181..5edeebd 100644 --- a/src/DownKyi/Themes/Styles/StyleListBox.xaml +++ b/src/DownKyi/Themes/Styles/StyleListBox.xaml @@ -1,5 +1,6 @@  + + + + + + + + + + + \ No newline at end of file diff --git a/src/DownKyi/Utils/DictionaryResource.cs b/src/DownKyi/Utils/DictionaryResource.cs index 93290d8..71147da 100644 --- a/src/DownKyi/Utils/DictionaryResource.cs +++ b/src/DownKyi/Utils/DictionaryResource.cs @@ -26,7 +26,7 @@ namespace DownKyi.Utils /// public static string GetString(string resourceKey) { - return (string)Application.Current.Resources[resourceKey]; + return Application.Current == null ? "" : (string)Application.Current.Resources[resourceKey]; } /// diff --git a/src/DownKyi/ViewModels/DownloadManager/ViewDownloadFinishedViewModel.cs b/src/DownKyi/ViewModels/DownloadManager/ViewDownloadFinishedViewModel.cs index 16f97fe..a305a98 100644 --- a/src/DownKyi/ViewModels/DownloadManager/ViewDownloadFinishedViewModel.cs +++ b/src/DownKyi/ViewModels/DownloadManager/ViewDownloadFinishedViewModel.cs @@ -1,8 +1,10 @@ -using Prism.Commands; +using DownKyi.Models; +using Prism.Commands; using Prism.Events; using Prism.Mvvm; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; namespace DownKyi.ViewModels.DownloadManager @@ -11,9 +13,20 @@ namespace DownKyi.ViewModels.DownloadManager { public const string Tag = "PageDownloadManagerDownloadFinished"; + #region 页面属性申明 + + private ObservableCollection downloadedList; + public ObservableCollection DownloadedList + { + get { return downloadedList; } + set { SetProperty(ref downloadedList, value); } + } + + #endregion + public ViewDownloadFinishedViewModel(IEventAggregator eventAggregator) : base(eventAggregator) { - + DownloadedList = App.DownloadedList; } } } diff --git a/src/DownKyi/ViewModels/DownloadManager/ViewDownloadingViewModel.cs b/src/DownKyi/ViewModels/DownloadManager/ViewDownloadingViewModel.cs index ba87677..547438a 100644 --- a/src/DownKyi/ViewModels/DownloadManager/ViewDownloadingViewModel.cs +++ b/src/DownKyi/ViewModels/DownloadManager/ViewDownloadingViewModel.cs @@ -1,9 +1,9 @@ -using Prism.Commands; +using DownKyi.Images; +using DownKyi.Models; +using DownKyi.Utils; +using Prism.Commands; using Prism.Events; -using Prism.Mvvm; -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.ObjectModel; namespace DownKyi.ViewModels.DownloadManager { @@ -11,9 +11,117 @@ namespace DownKyi.ViewModels.DownloadManager { public const string Tag = "PageDownloadManagerDownloading"; + #region 页面属性申明 + + private ObservableCollection downloadingList; + public ObservableCollection DownloadingList + { + get => downloadingList; + set => SetProperty(ref downloadingList, value); + } + + #endregion + public ViewDownloadingViewModel(IEventAggregator eventAggregator) : base(eventAggregator) { - + // 初始化DownloadingList + DownloadingList = App.DownloadingList; } + + #region 命令申明 + + // 暂停所有下载事件 + private DelegateCommand pauseAllDownloadingCommand; + public DelegateCommand PauseAllDownloadingCommand => pauseAllDownloadingCommand ?? (pauseAllDownloadingCommand = new DelegateCommand(ExecutePauseAllDownloadingCommand)); + + /// + /// 暂停所有下载事件 + /// + private void ExecutePauseAllDownloadingCommand() + { + foreach (DownloadingItem downloading in downloadingList) + { + switch (downloading.DownloadStatus) + { + case DownloadStatus.NOT_STARTED: + case DownloadStatus.WAIT_FOR_DOWNLOAD: + downloading.DownloadStatus = DownloadStatus.PAUSE_STARTED; + break; + case DownloadStatus.PAUSE_STARTED: + break; + case DownloadStatus.PAUSE: + break; + case DownloadStatus.DOWNLOADING: + downloading.DownloadStatus = DownloadStatus.PAUSE; + break; + case DownloadStatus.DOWNLOAD_SUCCEED: + // 下载成功后会从下载列表中删除 + // 不会出现此分支 + break; + case DownloadStatus.DOWNLOAD_FAILED: + break; + default: + break; + } + + downloading.StartOrPause = ButtonIcon.Instance().Start; + downloading.StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + } + } + + // 继续所有下载事件 + private DelegateCommand continueAllDownloadingCommand; + public DelegateCommand ContinueAllDownloadingCommand => continueAllDownloadingCommand ?? (continueAllDownloadingCommand = new DelegateCommand(ExecuteContinueAllDownloadingCommand)); + + /// + /// 继续所有下载事件 + /// + private void ExecuteContinueAllDownloadingCommand() + { + foreach (DownloadingItem downloading in downloadingList) + { + switch (downloading.DownloadStatus) + { + case DownloadStatus.NOT_STARTED: + case DownloadStatus.WAIT_FOR_DOWNLOAD: + break; + case DownloadStatus.PAUSE_STARTED: + downloading.DownloadStatus = DownloadStatus.WAIT_FOR_DOWNLOAD; + break; + case DownloadStatus.PAUSE: + downloading.DownloadStatus = DownloadStatus.DOWNLOADING; + break; + case DownloadStatus.DOWNLOADING: + break; + case DownloadStatus.DOWNLOAD_SUCCEED: + // 下载成功后会从下载列表中删除 + // 不会出现此分支 + break; + case DownloadStatus.DOWNLOAD_FAILED: + downloading.DownloadStatus = DownloadStatus.WAIT_FOR_DOWNLOAD; + break; + default: + break; + } + + downloading.StartOrPause = ButtonIcon.Instance().Pause; + downloading.StartOrPause.Fill = DictionaryResource.GetColor("ColorPrimary"); + } + } + + // 删除所有下载事件 + private DelegateCommand deleteAllDownloadingCommand; + public DelegateCommand DeleteAllDownloadingCommand => deleteAllDownloadingCommand ?? (deleteAllDownloadingCommand = new DelegateCommand(ExecuteDeleteAllDownloadingCommand)); + + /// + /// 删除所有下载事件 + /// + private void ExecuteDeleteAllDownloadingCommand() + { + DownloadingList.Clear(); + } + + #endregion + } } diff --git a/src/DownKyi/ViewModels/MainWindowViewModel.cs b/src/DownKyi/ViewModels/MainWindowViewModel.cs index 2f9f1fa..8066cf0 100644 --- a/src/DownKyi/ViewModels/MainWindowViewModel.cs +++ b/src/DownKyi/ViewModels/MainWindowViewModel.cs @@ -304,6 +304,7 @@ namespace DownKyi.ViewModels /// private void OnClipboardUpdated(object sender, EventArgs e) { + #region 执行第二遍时跳过 times += 1; DispatcherTimer timer = new DispatcherTimer { @@ -319,6 +320,8 @@ namespace DownKyi.ViewModels return; } + #endregion + AllowStatus isListenClipboard = SettingsManager.GetInstance().IsListenClipboard(); if (isListenClipboard != AllowStatus.YES) { diff --git a/src/DownKyi/ViewModels/Settings/ViewVideoViewModel.cs b/src/DownKyi/ViewModels/Settings/ViewVideoViewModel.cs index 46f3e62..5f47ece 100644 --- a/src/DownKyi/ViewModels/Settings/ViewVideoViewModel.cs +++ b/src/DownKyi/ViewModels/Settings/ViewVideoViewModel.cs @@ -1,4 +1,5 @@ -using DownKyi.Core.Settings; +using DownKyi.Core.FileName; +using DownKyi.Core.Settings; using DownKyi.Events; using DownKyi.Models; using DownKyi.Services; @@ -6,7 +7,9 @@ using DownKyi.Utils; using Prism.Commands; using Prism.Events; using Prism.Regions; +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; namespace DownKyi.ViewModels.Settings @@ -22,82 +25,76 @@ namespace DownKyi.ViewModels.Settings private List videoCodecs; public List VideoCodecs { - get { return videoCodecs; } - set { SetProperty(ref videoCodecs, value); } + get => videoCodecs; + set => SetProperty(ref videoCodecs, value); } private string selectedVideoCodec; public string SelectedVideoCodec { - get { return selectedVideoCodec; } - set { SetProperty(ref selectedVideoCodec, value); } + get => selectedVideoCodec; + set => SetProperty(ref selectedVideoCodec, value); } private List videoQualityList; public List VideoQualityList { - get { return videoQualityList; } - set { SetProperty(ref videoQualityList, value); } + get => videoQualityList; + set => SetProperty(ref videoQualityList, value); } private Resolution selectedVideoQuality; public Resolution SelectedVideoQuality { - get { return selectedVideoQuality; } - set { SetProperty(ref selectedVideoQuality, value); } - } - - private bool isAddVideoOrder; - public bool IsAddVideoOrder - { - get { return isAddVideoOrder; } - set { SetProperty(ref isAddVideoOrder, value); } + get => selectedVideoQuality; + set => SetProperty(ref selectedVideoQuality, value); } private bool isTranscodingFlvToMp4; public bool IsTranscodingFlvToMp4 { - get { return isTranscodingFlvToMp4; } - set { SetProperty(ref isTranscodingFlvToMp4, value); } + get => isTranscodingFlvToMp4; + set => SetProperty(ref isTranscodingFlvToMp4, value); } private bool isUseDefaultDirectory; public bool IsUseDefaultDirectory { - get { return isUseDefaultDirectory; } - set { SetProperty(ref isUseDefaultDirectory, value); } + get => isUseDefaultDirectory; + set => SetProperty(ref isUseDefaultDirectory, value); } private string saveVideoDirectory; public string SaveVideoDirectory { - get { return saveVideoDirectory; } - set { SetProperty(ref saveVideoDirectory, value); } + get => saveVideoDirectory; + set => SetProperty(ref saveVideoDirectory, value); } - private bool isCreateFolderForMedia; - public bool IsCreateFolderForMedia + private ObservableCollection selectedFileName; + public ObservableCollection SelectedFileName { - get { return isCreateFolderForMedia; } - set { SetProperty(ref isCreateFolderForMedia, value); } + get => selectedFileName; + set => SetProperty(ref selectedFileName, value); } - private bool isDownloadDanmaku; - public bool IsDownloadDanmaku + private ObservableCollection optionalFields; + public ObservableCollection OptionalFields { - get { return isDownloadDanmaku; } - set { SetProperty(ref isDownloadDanmaku, value); } + get => optionalFields; + set => SetProperty(ref optionalFields, value); } - private bool isDownloadCover; - public bool IsDownloadCover + private int selectedOptionalField; + public int SelectedOptionalField { - get { return isDownloadCover; } - set { SetProperty(ref isDownloadCover, value); } + get => selectedOptionalField; + set => SetProperty(ref selectedOptionalField, value); } #endregion + public ViewVideoViewModel(IEventAggregator eventAggregator) : base(eventAggregator) { @@ -113,6 +110,17 @@ namespace DownKyi.ViewModels.Settings // 优先下载画质 VideoQualityList = new ResolutionService().GetResolution(); + // 文件命名格式 + SelectedFileName = new ObservableCollection(); + OptionalFields = new ObservableCollection(); + foreach (FileNamePart item in Enum.GetValues(typeof(FileNamePart))) + { + string display = DisplayFileNamePart(item); + OptionalFields.Add(new DisplayFileNamePart { Id = item, Title = display }); + } + + SelectedOptionalField = -1; + #endregion } @@ -135,10 +143,6 @@ namespace DownKyi.ViewModels.Settings int quality = SettingsManager.GetInstance().GetQuality(); SelectedVideoQuality = VideoQualityList.FirstOrDefault(t => { return t.Id == quality; }); - // 是否在下载的视频前增加序号 - AllowStatus isAddOrder = SettingsManager.GetInstance().IsAddOrder(); - IsAddVideoOrder = isAddOrder == AllowStatus.YES; - // 是否下载flv视频后转码为mp4 AllowStatus isTranscodingFlvToMp4 = SettingsManager.GetInstance().IsTranscodingFlvToMp4(); IsTranscodingFlvToMp4 = isTranscodingFlvToMp4 == AllowStatus.YES; @@ -150,17 +154,14 @@ namespace DownKyi.ViewModels.Settings // 默认下载目录 SaveVideoDirectory = SettingsManager.GetInstance().GetSaveVideoRootPath(); - // 是否为不同视频分别创建文件夹 - AllowStatus isCreateFolderForMedia = SettingsManager.GetInstance().IsCreateFolderForMedia(); - IsCreateFolderForMedia = isCreateFolderForMedia == AllowStatus.YES; - - // 是否在下载视频的同时下载弹幕 - AllowStatus isDownloadDanmaku = SettingsManager.GetInstance().IsDownloadDanmaku(); - IsDownloadDanmaku = isDownloadDanmaku == AllowStatus.YES; - - // 是否在下载视频的同时下载封面 - AllowStatus isDownloadCover = SettingsManager.GetInstance().IsDownloadCover(); - IsDownloadCover = isDownloadCover == AllowStatus.YES; + // 文件命名格式 + List fileNameParts = SettingsManager.GetInstance().GetFileNameParts(); + SelectedFileName.Clear(); + foreach (FileNamePart item in fileNameParts) + { + string display = DisplayFileNamePart(item); + SelectedFileName.Add(new DisplayFileNamePart { Id = item, Title = display }); + } isOnNavigatedTo = false; } @@ -199,21 +200,6 @@ namespace DownKyi.ViewModels.Settings PublishTip(isSucceed); } - // 是否在下载的视频前增加序号事件 - private DelegateCommand IisAddVideoOrderCommand; - public DelegateCommand IsAddVideoOrderCommand => IisAddVideoOrderCommand ?? (IisAddVideoOrderCommand = new DelegateCommand(ExecuteIsAddVideoOrderCommand)); - - /// - /// 是否在下载的视频前增加序号事件 - /// - private void ExecuteIsAddVideoOrderCommand() - { - AllowStatus isAddOrder = IsAddVideoOrder ? AllowStatus.YES : AllowStatus.NO; - - bool isSucceed = SettingsManager.GetInstance().IsAddOrder(isAddOrder); - PublishTip(isSucceed); - } - // 是否下载flv视频后转码为mp4事件 private DelegateCommand isTranscodingFlvToMp4Command; public DelegateCommand IsTranscodingFlvToMp4Command => isTranscodingFlvToMp4Command ?? (isTranscodingFlvToMp4Command = new DelegateCommand(ExecuteIsTranscodingFlvToMp4Command)); @@ -265,50 +251,62 @@ namespace DownKyi.ViewModels.Settings } } - // 是否为不同视频分别创建文件夹事件 - private DelegateCommand isCreateFolderForMediaCommand; - public DelegateCommand IsCreateFolderForMediaCommand => isCreateFolderForMediaCommand ?? (isCreateFolderForMediaCommand = new DelegateCommand(ExecuteIsCreateFolderForMediaCommand)); + // 选中文件名字段点击事件 + private DelegateCommand selectedFileNameCommand; + public DelegateCommand SelectedFileNameCommand => selectedFileNameCommand ?? (selectedFileNameCommand = new DelegateCommand(ExecuteSelectedFileNameCommand)); /// - /// 是否为不同视频分别创建文件夹事件 + /// 选中文件名字段点击事件 /// - private void ExecuteIsCreateFolderForMediaCommand() + /// + private void ExecuteSelectedFileNameCommand(object parameter) { - AllowStatus isCreateFolderForMedia = IsCreateFolderForMedia ? AllowStatus.YES : AllowStatus.NO; + bool isSucceed = SelectedFileName.Remove((DisplayFileNamePart)parameter); + if (!isSucceed) + { + PublishTip(isSucceed); + return; + } - bool isSucceed = SettingsManager.GetInstance().IsCreateFolderForMedia(isCreateFolderForMedia); + List fileName = new List(); + foreach (DisplayFileNamePart item in SelectedFileName) + { + fileName.Add(item.Id); + } + + isSucceed = SettingsManager.GetInstance().SetFileNameParts(fileName); PublishTip(isSucceed); } - // 是否在下载视频的同时下载弹幕事件 - private DelegateCommand isDownloadDanmakuCommand; - public DelegateCommand IsDownloadDanmakuCommand => isDownloadDanmakuCommand ?? (isDownloadDanmakuCommand = new DelegateCommand(ExecuteIsDownloadDanmakuCommand)); + // 可选文件名字段点击事件 + private DelegateCommand optionalFieldsCommand; + public DelegateCommand OptionalFieldsCommand => optionalFieldsCommand ?? (optionalFieldsCommand = new DelegateCommand(ExecuteOptionalFieldsCommand)); /// - /// 是否在下载视频的同时下载弹幕事件 + /// 可选文件名字段点击事件 /// - private void ExecuteIsDownloadDanmakuCommand() + /// + private void ExecuteOptionalFieldsCommand(object parameter) { - AllowStatus isDownloadDanmaku = IsDownloadDanmaku ? AllowStatus.YES : AllowStatus.NO; + if (SelectedOptionalField == -1) + { + return; + } - bool isSucceed = SettingsManager.GetInstance().IsDownloadDanmaku(isDownloadDanmaku); + SelectedFileName.Add((DisplayFileNamePart)parameter); + + List fileName = new List(); + foreach (DisplayFileNamePart item in SelectedFileName) + { + fileName.Add(item.Id); + } + + bool isSucceed = SettingsManager.GetInstance().SetFileNameParts(fileName); PublishTip(isSucceed); + + SelectedOptionalField = -1; } - // 是否在下载视频的同时下载封面事件 - private DelegateCommand isDownloadCoverCommand; - public DelegateCommand IsDownloadCoverCommand => isDownloadCoverCommand ?? (isDownloadCoverCommand = new DelegateCommand(ExecuteIsDownloadCoverCommand)); - - /// - /// 是否在下载视频的同时下载封面事件 - /// - private void ExecuteIsDownloadCoverCommand() - { - AllowStatus isDownloadCover = IsDownloadCover ? AllowStatus.YES : AllowStatus.NO; - - bool isSucceed = SettingsManager.GetInstance().IsDownloadCover(isDownloadCover); - PublishTip(isSucceed); - } #endregion @@ -379,5 +377,54 @@ namespace DownKyi.ViewModels.Settings } } + /// + /// 文件名字段显示 + /// + /// + /// + private string DisplayFileNamePart(FileNamePart item) + { + string display = string.Empty; + switch (item) + { + case FileNamePart.ORDER: + display = DictionaryResource.GetString("DisplayOrder"); + break; + case FileNamePart.SECTION: + display = DictionaryResource.GetString("DisplaySection"); + break; + case FileNamePart.MAIN_TITLE: + display = DictionaryResource.GetString("DisplayMainTitle"); + break; + case FileNamePart.PAGE_TITLE: + display = DictionaryResource.GetString("DisplayPageTitle"); + break; + case FileNamePart.VIDEO_ZONE: + display = DictionaryResource.GetString("DisplayVideoZone"); + break; + case FileNamePart.AUDIO_QUALITY: + display = DictionaryResource.GetString("DisplayAudioQuality"); + break; + case FileNamePart.VIDEO_QUALITY: + display = DictionaryResource.GetString("DisplayVideoQuality"); + break; + case FileNamePart.VIDEO_CODEC: + display = DictionaryResource.GetString("DisplayVideoCodec"); + break; + } + + if (((int)item) >= 100) + { + display = HyphenSeparated.Hyphen[(int)item]; + } + + if (display == " ") + { + display = DictionaryResource.GetString("DisplaySpace"); + } + + return display; + } + } } diff --git a/src/DownKyi/ViewModels/ViewVideoDetailViewModel.cs b/src/DownKyi/ViewModels/ViewVideoDetailViewModel.cs index 6084403..271b7ae 100644 --- a/src/DownKyi/ViewModels/ViewVideoDetailViewModel.cs +++ b/src/DownKyi/ViewModels/ViewVideoDetailViewModel.cs @@ -1,6 +1,9 @@ using DownKyi.Core.BiliApi.BiliUtils; +using DownKyi.Core.BiliApi.Zone; +using DownKyi.Core.FileName; using DownKyi.Core.Logging; using DownKyi.Core.Settings; +using DownKyi.Core.Utils; using DownKyi.CustomControl; using DownKyi.Events; using DownKyi.Images; @@ -13,11 +16,13 @@ using Prism.Events; using Prism.Regions; using Prism.Services.Dialogs; using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; +using System.Windows.Media; namespace DownKyi.ViewModels { @@ -32,76 +37,75 @@ namespace DownKyi.ViewModels private VectorImage arrowBack; public VectorImage ArrowBack { - get { return arrowBack; } - set { SetProperty(ref arrowBack, value); } + get => arrowBack; + set => SetProperty(ref arrowBack, value); } private string inputText; public string InputText { - get { return inputText; } - set { SetProperty(ref inputText, value); } + get => inputText; + set => SetProperty(ref inputText, value); } private GifImage loading; public GifImage Loading { - get { return loading; } - set { SetProperty(ref loading, value); } + get => loading; + set => SetProperty(ref loading, value); } private Visibility loadingVisibility; public Visibility LoadingVisibility { - get { return loadingVisibility; } - set { SetProperty(ref loadingVisibility, value); } + get => loadingVisibility; + set => SetProperty(ref loadingVisibility, value); } private VectorImage downloadManage; public VectorImage DownloadManage { - get { return downloadManage; } - set { SetProperty(ref downloadManage, value); } + get => downloadManage; + set => SetProperty(ref downloadManage, value); } private VideoInfoView videoInfoView; public VideoInfoView VideoInfoView { - get { return videoInfoView; } - set { SetProperty(ref videoInfoView, value); } + get => videoInfoView; + set => SetProperty(ref videoInfoView, value); } private ObservableCollection videoSections; public ObservableCollection VideoSections { - get { return videoSections; } - set { SetProperty(ref videoSections, value); } + get => videoSections; + set => SetProperty(ref videoSections, value); } private bool isSelectAll; public bool IsSelectAll { - get { return isSelectAll; } - set { SetProperty(ref isSelectAll, value); } + get => isSelectAll; + set => SetProperty(ref isSelectAll, value); } private Visibility contentVisibility; public Visibility ContentVisibility { - get { return contentVisibility; } - set { SetProperty(ref contentVisibility, value); } + get => contentVisibility; + set => SetProperty(ref contentVisibility, value); } private Visibility noDataVisibility; public Visibility NoDataVisibility { - get { return noDataVisibility; } - set { SetProperty(ref noDataVisibility, value); } + get => noDataVisibility; + set => SetProperty(ref noDataVisibility, value); } #endregion - public ViewVideoDetailViewModel(IEventAggregator eventAggregator, IDialogService dialogService) : base(eventAggregator) { this.dialogService = dialogService; @@ -265,7 +269,7 @@ namespace DownKyi.ViewModels if (!(parameter is VideoSection section)) { return; } bool isSelectAll = true; - foreach (var page in section.VideoPages) + foreach (VideoPage page in section.VideoPages) { if (!page.IsSelected) { @@ -304,7 +308,7 @@ namespace DownKyi.ViewModels private void ExecuteKeySelectAllCommand(object parameter) { if (!(parameter is VideoSection section)) { return; } - foreach (var page in section.VideoPages) + foreach (VideoPage page in section.VideoPages) { page.IsSelected = true; } @@ -323,14 +327,14 @@ namespace DownKyi.ViewModels if (!(parameter is VideoSection section)) { return; } if (IsSelectAll) { - foreach (var page in section.VideoPages) + foreach (VideoPage page in section.VideoPages) { page.IsSelected = true; } } else { - foreach (var page in section.VideoPages) + foreach (VideoPage page in section.VideoPages) { page.IsSelected = false; } @@ -424,11 +428,11 @@ namespace DownKyi.ViewModels case ParseScope.NONE: break; case ParseScope.SELECTED_ITEM: - foreach (var section in VideoSections) + foreach (VideoSection section in VideoSections) { - foreach (var page in section.VideoPages) + foreach (VideoPage page in section.VideoPages) { - var videoPage = section.VideoPages.FirstOrDefault(t => t == page); + VideoPage videoPage = section.VideoPages.FirstOrDefault(t => t == page); if (videoPage.IsSelected) { @@ -439,13 +443,13 @@ namespace DownKyi.ViewModels } break; case ParseScope.CURRENT_SECTION: - foreach (var section in VideoSections) + foreach (VideoSection section in VideoSections) { if (section.IsSelected) { - foreach (var page in section.VideoPages) + foreach (VideoPage page in section.VideoPages) { - var videoPage = section.VideoPages.FirstOrDefault(t => t == page); + VideoPage videoPage = section.VideoPages.FirstOrDefault(t => t == page); // 执行解析任务 UnityUpdateView(ParseVideo, null, videoPage); @@ -454,11 +458,11 @@ namespace DownKyi.ViewModels } break; case ParseScope.ALL: - foreach (var section in VideoSections) + foreach (VideoSection section in VideoSections) { - foreach (var page in section.VideoPages) + foreach (VideoPage page in section.VideoPages) { - var videoPage = section.VideoPages.FirstOrDefault(t => t == page); + VideoPage videoPage = section.VideoPages.FirstOrDefault(t => t == page); // 执行解析任务 UnityUpdateView(ParseVideo, null, videoPage); @@ -530,12 +534,6 @@ namespace DownKyi.ViewModels downloadDanmaku = result.Parameters.GetValue("downloadDanmaku"); downloadSubtitle = result.Parameters.GetValue("downloadSubtitle"); downloadCover = result.Parameters.GetValue("downloadCover"); - - // 文件夹不存在则创建 - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } } }); } @@ -545,8 +543,164 @@ namespace DownKyi.ViewModels // 这时直接退出 if (directory == string.Empty) { return; } + // 文件夹不存在则创建 + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // 添加视频计数 + int i = 0; + // 添加到下载 - eventAggregator.GetEvent().Publish(directory); + foreach (VideoSection section in VideoSections) + { + foreach (VideoPage page in section.VideoPages) + { + // 只下载选中项,跳过未选中项 + if (!page.IsSelected) { continue; } + + // 没有解析的也跳过 + if (page.PlayUrl == null) { continue; } + + // 判断是否同一个视频,需要cid、画质、音质、视频编码都相同 + + // 如果存在正在下载列表,则跳过,并提示 + foreach (DownloadingItem item in App.DownloadingList) + { + if (item.Cid == page.Cid && item.Resolution.Id == page.VideoQuality.Quality && item.AudioCodecName == page.AudioQualityFormat && item.VideoCodecName == page.VideoQuality.SelectedVideoCodec) + { + eventAggregator.GetEvent().Publish($"{page.Name}{DictionaryResource.GetString("TipAlreadyToAddDownloading")}"); + continue; + } + } + + // 如果存在下载完成列表,弹出选择框是否再次下载 + foreach (DownloadedItem item in App.DownloadedList) + { + if (item.Cid == page.Cid && item.Resolution.Id == page.VideoQuality.Quality && item.AudioCodecName == page.AudioQualityFormat && item.VideoCodecName == page.VideoQuality.SelectedVideoCodec) + { + eventAggregator.GetEvent().Publish($"{page.Name}{DictionaryResource.GetString("TipAlreadyToAddDownloaded")}"); + continue; + } + } + + // 视频分区 + int zoneId = -1; + List zoneList = VideoZone.Instance().GetZones(); + ZoneAttr zone = zoneList.Find(it => it.Id == VideoInfoView.TypeId); + if (zone != null) + { + ZoneAttr zoneParent = zoneList.Find(it => it.Id == zone.ParentId); + if (zoneParent != null) + { + zoneId = zoneParent.Id; + } + } + + // 如果只有一个视频章节,则不在命名中出现 + string sectionName = string.Empty; + if (VideoSections.Count > 1) + { + sectionName = section.Title; + } + + // 文件路径 + List fileNameParts = SettingsManager.GetInstance().GetFileNameParts(); + FileName fileName = FileName.Builder(fileNameParts) + .SetOrder(page.Order) + .SetSection(Format.FormatFileName(sectionName)) + .SetMainTitle(Format.FormatFileName(VideoInfoView.Title)) + .SetPageTitle(Format.FormatFileName(page.Name)) + .SetVideoZone(VideoInfoView.VideoZone.Split('>')[0]) + .SetAudioQuality(page.AudioQualityFormat) + .SetVideoQuality(page.VideoQuality.QualityFormat) + .SetVideoCodec(page.VideoQuality.SelectedVideoCodec.Contains("AVC") ? "AVC" : page.VideoQuality.SelectedVideoCodec.Contains("HEVC") ? "HEVC" : ""); + string filePath = Path.Combine(directory, fileName.RelativePath()); + + // 视频类别 + PlayStreamType playStreamType; + switch (VideoInfoView.TypeId) + { + case -10: + playStreamType = PlayStreamType.CHEESE; + break; + case 13: + case 23: + case 177: + case 167: + case 11: + playStreamType = PlayStreamType.BANGUMI; + break; + case 1: + case 3: + case 129: + case 4: + case 36: + case 188: + case 234: + case 223: + case 160: + case 211: + case 217: + case 119: + case 155: + case 202: + case 5: + case 181: + default: + playStreamType = PlayStreamType.VIDEO; + break; + } + + // 如果不存在,直接添加到下载列表 + DownloadingItem downloading = new DownloadingItem + { + PlayUrl = page.PlayUrl, + + Bvid = page.Bvid, + Avid = page.Avid, + Cid = page.Cid, + EpisodeId = page.EpisodeId, + + CoverUrl = page.FirstFrame, + ZoneImage = (DrawingImage)Application.Current.Resources[VideoZoneIcon.Instance().GetZoneImageKey(zoneId)], + + Order = page.Order, + MainTitle = VideoInfoView.Title, + Name = page.Name, + Duration = page.Duration, + AudioCodecId = Constant.AudioQualityId[page.AudioQualityFormat], + AudioCodecName = page.AudioQualityFormat, + VideoCodecName = page.VideoQuality.SelectedVideoCodec, + Resolution = new Resolution { Name = page.VideoQuality.QualityFormat, Id = page.VideoQuality.Quality }, + FilePath = filePath, + + PlayStreamType = playStreamType, + DownloadStatus = DownloadStatus.NOT_STARTED, + }; + // 需要下载的内容 + downloading.NeedDownloadContent["downloadAudio"] = downloadAudio; + downloading.NeedDownloadContent["downloadVideo"] = downloadVideo; + downloading.NeedDownloadContent["downloadDanmaku"] = downloadDanmaku; + downloading.NeedDownloadContent["downloadSubtitle"] = downloadSubtitle; + downloading.NeedDownloadContent["downloadCover"] = downloadCover; + + // 添加到下载列表 + App.DownloadingList.Add(downloading); + i++; + } + } + + // 通知用户添加到下载列表的结果 + if (i == 0) + { + eventAggregator.GetEvent().Publish(DictionaryResource.GetString("TipAddDownloadingZero")); + } + else + { + eventAggregator.GetEvent().Publish($"{DictionaryResource.GetString("TipAddDownloadingFinished1")}{i}{DictionaryResource.GetString("TipAddDownloadingFinished2")}"); + } } /// @@ -627,12 +781,12 @@ namespace DownKyi.ViewModels NoDataVisibility = Visibility.Collapsed; } - var videoSections = videoInfoService.GetVideoSections(); + List videoSections = videoInfoService.GetVideoSections(); if (videoSections == null) { LogManager.Debug(Tag, "videoSections is not exist."); - var pages = videoInfoService.GetVideoPages(); + List pages = videoInfoService.GetVideoPages(); PropertyChangeAsync(new Action(() => { diff --git a/src/DownKyi/Views/DownloadManager/ViewDownloading.xaml b/src/DownKyi/Views/DownloadManager/ViewDownloading.xaml index eeb1677..b766407 100644 --- a/src/DownKyi/Views/DownloadManager/ViewDownloading.xaml +++ b/src/DownKyi/Views/DownloadManager/ViewDownloading.xaml @@ -2,9 +2,309 @@ x:Class="DownKyi.Views.DownloadManager.ViewDownloading" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="True"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - 正在下载 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +