diff --git a/images/app/index.png b/images/app/index.png index 6a71637..a950b9a 100644 Binary files a/images/app/index.png and b/images/app/index.png differ 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(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(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, "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() - { - appSettings = GetSettings(); - if (appSettings.Video.IsCreateFolderForMedia == 0) - { - // 第一次获取,先设置默认值 - IsCreateFolderForMedia(isCreateFolderForMedia); - return isCreateFolderForMedia; - } - return appSettings.Video.IsCreateFolderForMedia; - } - - /// - /// 设置是否为不同视频分别创建文件夹 - /// - /// - /// - public bool IsCreateFolderForMedia(AllowStatus isCreateFolderForMedia) - { - 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() + public List GetFileNameParts() { appSettings = GetSettings(); - if (appSettings.Video.IsDownloadCover == 0) + if (appSettings.Video.FileNameParts == null || appSettings.Video.FileNameParts.Count == 0) { // 第一次获取,先设置默认值 - IsDownloadCover(isDownloadCover); - return isDownloadCover; + SetFileNameParts(fileNameParts); + return fileNameParts; } - return appSettings.Video.IsDownloadCover; + return appSettings.Video.FileNameParts; } /// - /// 设置是否在下载视频的同时下载封面 + /// 设置文件命名格式 /// - /// + /// /// - public bool IsDownloadCover(AllowStatus isDownloadCover) + public bool SetFileNameParts(List fileNameParts) { - 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"; - public ViewDownloadFinishedViewModel(IEventAggregator eventAggregator) : base(eventAggregator) + #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,51 +251,63 @@ 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; - - bool isSucceed = SettingsManager.GetInstance().IsDownloadDanmaku(isDownloadDanmaku); - PublishTip(isSucceed); - } + if (SelectedOptionalField == -1) + { + return; + } - // 是否在下载视频的同时下载封面事件 - private DelegateCommand isDownloadCoverCommand; - public DelegateCommand IsDownloadCoverCommand => isDownloadCoverCommand ?? (isDownloadCoverCommand = new DelegateCommand(ExecuteIsDownloadCoverCommand)); + SelectedFileName.Add((DisplayFileNamePart)parameter); - /// - /// 是否在下载视频的同时下载封面事件 - /// - private void ExecuteIsDownloadCoverCommand() - { - AllowStatus isDownloadCover = IsDownloadCover ? AllowStatus.YES : AllowStatus.NO; + List fileName = new List(); + foreach (DisplayFileNamePart item in SelectedFileName) + { + fileName.Add(item.Id); + } - bool isSucceed = SettingsManager.GetInstance().IsDownloadCover(isDownloadCover); + bool isSucceed = SettingsManager.GetInstance().SetFileNameParts(fileName); PublishTip(isSucceed); + + SelectedOptionalField = -1; } + #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"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - 正在下载 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +