From b728db4ba53fd97c6b6952e348c5941f95ec2b6a Mon Sep 17 00:00:00 2001 From: flyself <1432593898@qq.com> Date: Sat, 12 Jun 2021 23:02:57 +0800 Subject: [PATCH] v1.4.0 --- CHANGELOG.md | 9 + README.md | 14 +- src/Core/Common.cs | 440 ++++++++++ src/Core/Downloader.cs | 534 ++++++++++++ src/Core/Encryptor.cs | 316 +++++++ src/Core/FFmpegHelper.cs | 254 ++++++ src/Core/FileDownloadUtil.cs | 198 +++++ src/Core/MachineCode.cs | 142 +++ src/Core/UserSpaceOld.cs | 148 ++++ src/Core/Utils.cs | 430 +++++++++ src/Core/VideoZone.cs | 209 +++++ src/Core/api/danmaku/BiliDanmaku.cs | 36 + src/Core/api/danmaku/DanmakuProtobuf.cs | 115 +++ src/Core/api/danmaku/proto/Danmaku.cs | 814 ++++++++++++++++++ src/Core/api/danmaku/proto/Danmaku.proto | 20 + .../api/fileDownload/FileDownloadConfig.cs | 42 + .../api/fileDownload/FileDownloadEvent.cs | 24 + .../api/fileDownload/FileDownloadHelper.cs | 317 +++++++ src/Core/api/fileDownload/FileDownloadInfo.cs | 77 ++ src/Core/api/fileDownload/FileInfo.cs | 43 + .../api/fileDownload/ThreadDownloadInfo.cs | 11 + src/Core/api/login/Login.cs | 107 +++ src/Core/api/login/LoginHelper.cs | 231 +++++ src/Core/api/users/UserSpace.cs | 44 +- src/Core/aria2cNet/Aria2c.cs | 433 ++++++++++ src/Core/aria2cNet/AriaManager.cs | 43 +- src/Core/aria2cNet/client/AriaClient.cs | 4 +- .../aria2cNet/client/AriaClientWebSocket.cs | 111 +++ src/Core/aria2cNet/server/AriaServer.cs | 3 +- src/Core/danmaku2ass/Bilibili.cs | 163 ++++ src/Core/danmaku2ass/Collision.cs | 61 ++ src/Core/danmaku2ass/Config.cs | 55 ++ src/Core/danmaku2ass/Creater.cs | 88 ++ src/Core/danmaku2ass/Danmaku.cs | 12 + src/Core/danmaku2ass/Dictionary.cs | 6 + src/Core/danmaku2ass/Display.cs | 406 +++++++++ src/Core/danmaku2ass/Filter.cs | 89 ++ src/Core/danmaku2ass/Producer.cs | 105 +++ src/Core/danmaku2ass/Studio.cs | 84 ++ src/Core/danmaku2ass/Subtitle.cs | 155 ++++ src/Core/danmaku2ass/Utils.cs | 228 +++++ src/Core/entity/BangumiMedia.cs | 35 + src/Core/entity/BangumiSeason.cs | 199 +++++ src/Core/entity/CheeseList.cs | 44 + src/Core/entity/CheeseSeason.cs | 52 ++ src/Core/entity/Danmu.cs | 25 + src/Core/entity/FavFolder.cs | 49 ++ src/Core/entity/FavResource.cs | 54 ++ src/Core/entity/LoginUrl.cs | 50 ++ src/Core/entity/MyInfo.cs | 96 +++ src/Core/entity/Nav.cs | 88 ++ src/Core/entity/PlayUrl.cs | 89 ++ src/Core/entity/Stat.cs | 21 + src/Core/entity/UserSettings.cs | 31 + src/Core/entity/VideoDetail.cs | 28 + src/Core/entity/VideoView.cs | 207 +++++ src/Core/entity2/login/LoginStatus.cs | 49 ++ src/Core/entity2/login/LoginUrl.cs | 26 + src/Core/entity2/users/SpacePublication.cs | 2 + src/Core/settings/ReadMe.md | 167 ++++ src/Core/settings/Settings.about.cs | 65 ++ src/Core/settings/Settings.base.cs | 123 +++ src/Core/settings/Settings.class.cs | 90 ++ src/Core/settings/Settings.cs | 132 +++ src/Core/settings/Settings.danmaku.cs | 298 +++++++ src/Core/settings/Settings.network.cs | 324 +++++++ src/Core/settings/Settings.video.cs | 273 ++++++ 67 files changed, 9208 insertions(+), 30 deletions(-) create mode 100644 src/Core/Common.cs create mode 100644 src/Core/Downloader.cs create mode 100644 src/Core/Encryptor.cs create mode 100644 src/Core/FFmpegHelper.cs create mode 100644 src/Core/FileDownloadUtil.cs create mode 100644 src/Core/MachineCode.cs create mode 100644 src/Core/UserSpaceOld.cs create mode 100644 src/Core/Utils.cs create mode 100644 src/Core/VideoZone.cs create mode 100644 src/Core/api/danmaku/BiliDanmaku.cs create mode 100644 src/Core/api/danmaku/DanmakuProtobuf.cs create mode 100644 src/Core/api/danmaku/proto/Danmaku.cs create mode 100644 src/Core/api/danmaku/proto/Danmaku.proto create mode 100644 src/Core/api/fileDownload/FileDownloadConfig.cs create mode 100644 src/Core/api/fileDownload/FileDownloadEvent.cs create mode 100644 src/Core/api/fileDownload/FileDownloadHelper.cs create mode 100644 src/Core/api/fileDownload/FileDownloadInfo.cs create mode 100644 src/Core/api/fileDownload/FileInfo.cs create mode 100644 src/Core/api/fileDownload/ThreadDownloadInfo.cs create mode 100644 src/Core/api/login/Login.cs create mode 100644 src/Core/api/login/LoginHelper.cs create mode 100644 src/Core/aria2cNet/Aria2c.cs create mode 100644 src/Core/aria2cNet/client/AriaClientWebSocket.cs create mode 100644 src/Core/danmaku2ass/Bilibili.cs create mode 100644 src/Core/danmaku2ass/Collision.cs create mode 100644 src/Core/danmaku2ass/Config.cs create mode 100644 src/Core/danmaku2ass/Creater.cs create mode 100644 src/Core/danmaku2ass/Danmaku.cs create mode 100644 src/Core/danmaku2ass/Dictionary.cs create mode 100644 src/Core/danmaku2ass/Display.cs create mode 100644 src/Core/danmaku2ass/Filter.cs create mode 100644 src/Core/danmaku2ass/Producer.cs create mode 100644 src/Core/danmaku2ass/Studio.cs create mode 100644 src/Core/danmaku2ass/Subtitle.cs create mode 100644 src/Core/danmaku2ass/Utils.cs create mode 100644 src/Core/entity/BangumiMedia.cs create mode 100644 src/Core/entity/BangumiSeason.cs create mode 100644 src/Core/entity/CheeseList.cs create mode 100644 src/Core/entity/CheeseSeason.cs create mode 100644 src/Core/entity/Danmu.cs create mode 100644 src/Core/entity/FavFolder.cs create mode 100644 src/Core/entity/FavResource.cs create mode 100644 src/Core/entity/LoginUrl.cs create mode 100644 src/Core/entity/MyInfo.cs create mode 100644 src/Core/entity/Nav.cs create mode 100644 src/Core/entity/PlayUrl.cs create mode 100644 src/Core/entity/Stat.cs create mode 100644 src/Core/entity/UserSettings.cs create mode 100644 src/Core/entity/VideoDetail.cs create mode 100644 src/Core/entity/VideoView.cs create mode 100644 src/Core/entity2/login/LoginStatus.cs create mode 100644 src/Core/entity2/login/LoginUrl.cs create mode 100644 src/Core/settings/ReadMe.md create mode 100644 src/Core/settings/Settings.about.cs create mode 100644 src/Core/settings/Settings.base.cs create mode 100644 src/Core/settings/Settings.class.cs create mode 100644 src/Core/settings/Settings.cs create mode 100644 src/Core/settings/Settings.danmaku.cs create mode 100644 src/Core/settings/Settings.network.cs create mode 100644 src/Core/settings/Settings.video.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c7b22..cde6449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 更新日志 +* `2021/06/12` v1.4.0 + 1. [修复] 视频无封面时崩溃的问题。 + 2. [优化] 视频分区代码与图标。 + 3. [优化] 屏蔽频道中的失效视频。 + 4. [优化] 二维码登录逻辑。 + 5. [新增] 弹幕下载(借鉴并重写于项目 [niconvert](https://github.com/muzuiget/niconvert))。 + 6. [新增] 弹幕样式设置。 + 7. [新增] 下载弹幕、下载封面的选项。 + * `2021/04/27` v1.3.8 1. [修复] FLV视频只有1个分段时下载失败的问题。 2. [修复] UP主视频页面闪退的问题。 diff --git a/README.md b/README.md index e412790..1938d19 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - [x] 支持视频、番剧、剧集、电影、课程下载 - [x] **支持用户收藏夹、订阅、稍后再看、历史记录下载** - [x] ~~**支持港澳台番剧下载,解除地区限制**~~ +- [x] 支持弹幕下载、样式设置 - [x] 支持封面下载 - [x] 支持断点续传 - [x] 支持Aria2c @@ -60,11 +61,14 @@ [全部更新日志](https://github.com/FlySelfLog/downkyi/blob/main/CHANGELOG.md) -* `2021/04/27` v1.3.8 - 1. [修复] FLV视频只有1个分段时下载失败的问题。 - 2. [修复] UP主视频页面闪退的问题。 - 3. [修复] 电视剧图标丢失的问题。 - 4. [新增] UP主视频页面下载全部视频的按钮。 +* `2021/06/12` v1.4.0 + 1. [修复] 视频无封面时崩溃的问题。 + 2. [优化] 视频分区代码与图标。 + 3. [优化] 屏蔽频道中的失效视频。 + 4. [优化] 二维码登录逻辑。 + 5. [新增] 弹幕下载(借鉴并重写于项目 [niconvert](https://github.com/muzuiget/niconvert))。 + 6. [新增] 弹幕样式设置。 + 7. [新增] 下载弹幕、下载封面的选项。 ## 下载 diff --git a/src/Core/Common.cs b/src/Core/Common.cs new file mode 100644 index 0000000..61ac6ab --- /dev/null +++ b/src/Core/Common.cs @@ -0,0 +1,440 @@ +using System; +using System.Text.RegularExpressions; + +namespace Core +{ + public static class Common + { + // 配置文件所在路径 + public static readonly string ConfigPath = "./Config/"; + + // 日志、历史等文件所在路径 + public static readonly string RecordPath = "./Config/"; + + /// + /// 判断字符串是否以http或https开头,且域名为bilibili.com + /// + /// + /// + public static bool IsUrl(string input) + { + if (input.StartsWith("http://") || input.StartsWith("https://")) + { + if (input.Contains("www.bilibili.com")) { return true; } + else { return false; } + } + else { return false; } + } + + + /// + /// 判断是否为avid + /// + /// + /// + public static bool IsAvid(string input) + { + if (input.StartsWith("av")) + { + bool isInt = Regex.IsMatch(input.Remove(0, 2), @"^\d+$"); + return isInt; + } + return false; + } + + /// + /// 判断是否为bvid + /// + /// + /// + public static bool IsBvid(string input) + { + if (input.StartsWith("BV") && input.Length == 12) + { + return true; + } + return false; + } + + /// + /// 判断是否是用户空间的url + /// + /// + /// + public static bool IsUserSpaceUrl(string input) + { + if (input.StartsWith("http://") || input.StartsWith("https://")) + { + if (input.Contains("space.bilibili.com")) + { + return true; + } + else { return false; } + } + else { return false; } + } + + /// + /// 判断是否是用户id + /// + /// + /// + public static bool IsUserId(string input) + { + string inputLower = input.ToLower(); + if (inputLower.StartsWith("uid:")) + { + string uid = inputLower.TrimStart(new char[] { 'u', 'i', 'd', ':' }); + return IsInt(uid); + } + else if (inputLower.StartsWith("uid")) + { + string uid = inputLower.TrimStart(new char[] { 'u', 'i', 'd' }); + return IsInt(uid); + } + else { return false; } + } + + /// + /// 获取用户id + /// + /// + /// + public static long GetUserId(string url) + { + string[] strList = url.Split('?'); + string baseUrl = strList[0]; + + var match = Regex.Match(baseUrl, @"\d+"); + if (match.Success) + { + return GetInt(match.Value); + } + else + { + return -1; + } + } + + /// + /// 从url中解析id,包括bvid和番剧的id + /// + /// + /// + public static string GetVideoId(string url) + { + string[] strList = url.Split('?'); + string baseUrl = strList[0]; + + string[] str2List = baseUrl.Split('/'); + string id = str2List[str2List.Length - 1]; + + if (id == "") + { + // 字符串末尾 + return str2List[str2List.Length - 2]; + } + + return id; + } + + /// + /// 判断是视频还是番剧 + /// + /// + /// + public static VideoType GetVideoType(string url) + { + if (url.ToLower().Contains("/video/bv")) + { + return VideoType.VIDEO; + } + + if (url.ToLower().Contains("/video/av")) + { + return VideoType.VIDEO_AV; + } + + if (url.ToLower().Contains("/bangumi/play/ss")) + { + return VideoType.BANGUMI_SEASON; + } + + if (url.ToLower().Contains("/bangumi/play/ep")) + { + return VideoType.BANGUMI_EPISODE; + } + + if (url.ToLower().Contains("/bangumi/media/md")) + { + return VideoType.BANGUMI_MEDIA; + } + + if (url.ToLower().Contains("/cheese/play/ss")) + { + return VideoType.CHEESE_SEASON; + } + + if (url.ToLower().Contains("/cheese/play/ep")) + { + return VideoType.CHEESE_EPISODE; + } + + return VideoType.NONE; + } + + /// + /// 格式化Duration时间 + /// + /// + /// + public static string FormatDuration(long duration) + { + string formatDuration; + if (duration / 60 > 0) + { + long dur = duration / 60; + if (dur / 60 > 0) + { + formatDuration = $"{dur / 60}h{dur % 60}m{duration % 60}s"; + } + else + { + formatDuration = $"{duration / 60}m{duration % 60}s"; + } + } + else + { + formatDuration = $"{duration}s"; + } + return formatDuration; + } + + /// + /// 格式化Duration时间,格式为00:00:00 + /// + /// + /// + public static string FormatDuration2(long duration) + { + string formatDuration; + if (duration / 60 > 0) + { + long dur = duration / 60; + if (dur / 60 > 0) + { + formatDuration = string.Format("{0:D2}", dur / 60) + ":" + string.Format("{0:D2}", dur % 60) + ":" + string.Format("{0:D2}", duration % 60); + } + else + { + formatDuration = "00:" + string.Format("{0:D2}", duration / 60) + ":" + string.Format("{0:D2}", duration % 60); + } + } + else + { + formatDuration = "00:00:" + string.Format("{0:D2}", duration); + } + return formatDuration; + } + + /// + /// 格式化Duration时间,格式为00:00 + /// + /// + /// + public static string FormatDuration3(long duration) + { + string formatDuration; + if (duration / 60 > 0) + { + long dur = duration / 60; + if (dur / 60 > 0) + { + formatDuration = string.Format("{0:D2}", dur / 60) + ":" + string.Format("{0:D2}", dur % 60) + ":" + string.Format("{0:D2}", duration % 60); + } + else + { + formatDuration = string.Format("{0:D2}", duration / 60) + ":" + string.Format("{0:D2}", duration % 60); + } + } + else + { + formatDuration = "00:" + string.Format("{0:D2}", duration); + } + return formatDuration; + } + + /// + /// 格式化数字,超过10000的数字将单位改为万,超过100000000的数字将单位改为亿,并保留1位小数 + /// + /// + /// + public static string FormatNumber(long number) + { + if (number > 99999999) + { + return (number / 100000000.0f).ToString("F1") + "亿"; + } + + if (number > 9999) + { + return (number / 10000.0f).ToString("F1") + "万"; + } + else + { + return number.ToString(); + } + } + + /// + /// 去除非法字符 + /// + /// + /// + 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(); + } + + /// + /// 格式化网速 + /// + /// + /// + public static string FormatSpeed(float speed) + { + string formatSpeed; + if (speed <= 0) + { + formatSpeed = "0B/s"; + } + else if (speed < 1024) + { + formatSpeed = speed.ToString("#.##") + "B/s"; + } + else if (speed < 1024 * 1024) + { + formatSpeed = (speed / 1024).ToString("#.##") + "KB/s"; + } + else + { + formatSpeed = (speed / 1024 / 1024).ToString("#.##") + "MB/s"; + } + return formatSpeed; + } + + /// + /// 格式化字节大小,可用于文件大小的显示 + /// + /// + /// + public static string FormatFileSize(long fileSize) + { + string formatFileSize; + if (fileSize <= 0) + { + formatFileSize = "0B"; + } + else if (fileSize < 1024) + { + formatFileSize = fileSize.ToString() + "B"; + } + else if (fileSize < 1024 * 1024) + { + formatFileSize = (fileSize / 1024.0).ToString("#.##") + "KB"; + } + else if (fileSize < 1024 * 1024 * 1024) + { + formatFileSize = (fileSize / 1024.0 / 1024.0).ToString("#.##") + "MB"; + } + else + { + formatFileSize = (fileSize / 1024.0 / 1024.0 / 1024.0).ToString("#.##") + "GB"; + } + return formatFileSize; + } + + private static long GetInt(string value) + { + if (IsInt(value)) + { + return long.Parse(value); + } + else + { + return -1; + } + } + + private static bool IsInt(string value) + { + return Regex.IsMatch(value, @"^\d+$"); + } + + } + + + /// + /// 支持的视频类型 + /// + public enum VideoType + { + NONE, + VIDEO, // 对应 /video/BV + VIDEO_AV, // 对应 /video/av + // BANGUMI, // BANGUMI细分为以下三个部分 + BANGUMI_SEASON, // 对应 /bangumi/play/ss + BANGUMI_EPISODE, // 对应 /bangumi/play/ep + BANGUMI_MEDIA, // 对应 /bangumi/media/md + CHEESE_SEASON, // 对应 /cheese/play/ss + CHEESE_EPISODE // 对应 /cheese/play/ep + } + + /// + /// 线程 + /// + public enum ThreadStatus + { + MAIN_UI, // 主线程 + START_PARSE, // 开始解析url + ADD_ALL_TO_DOWNLOAD, + START_DOWNLOAD + } + + /// + /// 视频的编码格式,flv也视为编码放这里 + /// + [Serializable] + public enum VideoCodec + { + NONE = 1, + AVC, + HEVC, + FLV + } + +} diff --git a/src/Core/Downloader.cs b/src/Core/Downloader.cs new file mode 100644 index 0000000..02e3459 --- /dev/null +++ b/src/Core/Downloader.cs @@ -0,0 +1,534 @@ +using Core.entity; +using Core.settings; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Text.RegularExpressions; + +namespace Core +{ + public static class Downloader + { + /// + /// 获得远程文件的大小 + /// + /// + /// + /// + public static long GetRemoteFileSize(string url, string referer) + { + try + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.Method = "GET"; + request.Timeout = 30 * 1000; + request.UserAgent = Utils.GetUserAgent(); + //request.ContentType = "text/html;charset=UTF-8"; + request.Headers["accept-language"] = "zh-CN,zh;q=0.9,en;q=0.8"; + request.Referer = referer; + + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + return response.ContentLength; + } + catch (Exception e) + { + Console.WriteLine("GetRemoteFileSize()发生异常: {0}", e); + return 0; + } + } + + /// + /// 获得视频详情及播放列表 + /// + /// + /// + public static VideoViewData GetVideoInfo(string bvid, long aid, string referer, bool isBackup = false) + { + string url; + if (bvid != null) + { + url = $"https://api.bilibili.com/x/web-interface/view?bvid={bvid}"; + } + else if (aid >= 0) + { + url = $"https://api.bilibili.com/x/web-interface/view?aid={aid}"; + } + else + { return null; } + + // 采用备用的api,只能获取cid + if (isBackup) + { + string backupUrl = $"https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp"; + url = backupUrl; + } + + string response = Utils.RequestWeb(url, referer); + + try + { + VideoView videoView; + if (isBackup) + { + Pagelist pagelist = JsonConvert.DeserializeObject(response); + + videoView = new VideoView + { + code = pagelist.code, + message = pagelist.message, + ttl = pagelist.ttl + }; + videoView.data.pages = pagelist.data; + } + else + { + videoView = JsonConvert.DeserializeObject(response); + } + + if (videoView != null) + { + if (videoView.data != null) + { + return videoView.data; + } + else + { + return null; + + // 进入备选的url中 + //return GetVideoInfo(bvid, referer, true); + } + } + else + { + return null; + } + } + catch (JsonReaderException e) + { + Console.WriteLine("GetVideoInfo()发生JsonReader异常: {0}", e); + return null; + } + catch (Exception e) + { + Console.WriteLine("GetVideoInfo()发生异常: {0}", e); + return null; + } + } + + /// + /// 通过seasonId获得番剧的剧集详情 + /// + /// + /// + public static BangumiSeasonResult GetBangumiSeason(long seasonId, string referer) + { + string url = $"https://api.bilibili.com/pgc/view/web/season?season_id={seasonId}"; + string response = Utils.RequestWeb(url, referer); + + try + { + BangumiSeason bangumiSeason = JsonConvert.DeserializeObject(response); + if (bangumiSeason != null) { return bangumiSeason.result; } + else { return null; } + } + catch (JsonReaderException e) + { + Console.WriteLine("GetBangumiSeason()发生JsonReader异常: {0}", e); + return null; + } + catch (Exception e) + { + Console.WriteLine("GetBangumiSeason()发生异常: {0}", e); + return null; + } + } + + public static long GetBangumiSeasonIdByMedia(long mediaId, string referer) + { + string url = $"https://api.bilibili.com/pgc/review/user?media_id={mediaId}"; + string response = Utils.RequestWeb(url, referer); + + try + { + BangumiMedia bangumiMedia = JsonConvert.DeserializeObject(response); + if (bangumiMedia.result.media != null) { return bangumiMedia.result.media.season_id; } + else { return 0; } + } + catch (JsonReaderException e) + { + Console.WriteLine("GetBangumiSeasonIdByMedia()发生JsonReader异常: {0}", e); + return 0; + } + catch (Exception e) + { + Console.WriteLine("GetBangumiSeasonIdByMedia()发生异常: {0}", e); + return 0; + } + } + + public static long GetBangumiSeasonIdByEpisode(long episode, string referer) + { + string url = $"https://www.bilibili.com/bangumi/play/ep{episode}"; + string response = Utils.RequestWeb(url, referer); + + // "ssId": 28324, + string pattern = "\"ssId\":\\s?\\d+,"; + Regex regex = new Regex(pattern); + Match match = regex.Match(response); + + // 删除多余的字符 + string ssId = match.Value.Replace("ssId", ""); + ssId = ssId.Replace("\"", ""); + ssId = ssId.Replace(":", ""); + ssId = ssId.Replace(" ", ""); + ssId = ssId.Replace(",", ""); + + long seasonId; + try + { + seasonId = long.Parse(ssId); + } + catch (FormatException e) + { + Console.WriteLine("GetBangumiSeasonIdByEpisode()发生异常: {0}", e); + return 0; + } + return seasonId; + } + + /// + /// 通过ep_id获得课程的信息 + /// + /// + /// + /// + public static CheeseSeasonData GetCheeseSeason(long seasonId, long episode, string referer) + { + string url = $"https://api.bilibili.com/pugv/view/web/season?"; + if (seasonId != 0) + { + url += $"season_id={seasonId}"; + } + else if (episode != 0) + { + url += $"ep_id={episode}"; + } + + string response = Utils.RequestWeb(url, referer); + + try + { + CheeseSeason cheeseSeason = JsonConvert.DeserializeObject(response); + if (cheeseSeason != null) { return cheeseSeason.data; } + else { return null; } + } + catch (JsonReaderException e) + { + Console.WriteLine("GetCheeseSeason()发生JsonReader异常: {0}", e); + return null; + } + catch (Exception e) + { + Console.WriteLine("GetCheeseSeason()发生异常: {0}", e); + return null; + } + } + + //public static long GetCheeseEpisodeIdBySeasonId(long seasonId, string referer) + //{ + // string url = $"https://api.bilibili.com/pugv/view/web/ep/list?season_id={seasonId}&pn=1"; + // string response = Utils.RequestWeb(url, referer); + + // try + // { + // CheeseList cheeseList = JsonConvert.DeserializeObject(response); + // if (cheeseList.data.items != null && cheeseList.data.items.Count > 0) + // { + // return cheeseList.data.items[0].id; + // } + // else { return 0; } + // } + // catch (JsonReaderException e) + // { + // Console.WriteLine("发生异常: {0}", e); + // return 0; + // } + //} + + /// + /// 获得音视频流链接 + /// + /// 视频的bvid + /// 视频的cid + public static PlayUrlData GetStreamInfo(string bvid, long avid, long cid, long episodeId, int quality, string referer, bool isProxy = false, int proxy = 0) + { + string baseUrlVideo = "https://api.bilibili.com/x/player/playurl"; + string baseUrlSeason = "https://api.bilibili.com/pgc/player/web/playurl"; + string baseUrlCheese = "https://api.bilibili.com/pugv/player/web/playurl"; + string baseUrl; + VideoType videoType = Common.GetVideoType(referer); + switch (videoType) + { + case VideoType.VIDEO: + baseUrl = baseUrlVideo; + break; + case VideoType.VIDEO_AV: + baseUrl = baseUrlVideo; + break; + case VideoType.BANGUMI_SEASON: + baseUrl = baseUrlSeason; + break; + case VideoType.BANGUMI_EPISODE: + baseUrl = baseUrlSeason; + break; + case VideoType.BANGUMI_MEDIA: + baseUrl = baseUrlSeason; + break; + case VideoType.CHEESE_SEASON: + baseUrl = baseUrlCheese; + break; + case VideoType.CHEESE_EPISODE: + baseUrl = baseUrlCheese; + break; + default: + baseUrl = baseUrlVideo; + break; + } + // TODO 没有的参数不加入url + //string url = $"{baseUrl}?cid={cid}&bvid={bvid}&avid={avid}&ep_id={episodeId}&qn={quality}&otype=json&fourk=1&fnver=0&fnval=16"; + string url = $"{baseUrl}?cid={cid}&qn={quality}&otype=json&fourk=1&fnver=0&fnval=16"; + if (bvid != null) + { + url += $"&bvid={bvid}"; + } + if (avid != 0) + { + url += $"&avid={avid}"; + } + if (episodeId != 0) + { + url += $"&ep_id={episodeId}"; + } + + // 代理网址 + //https://www.biliplus.com/BPplayurl.php?cid=180873425&qn=116&type=&otype=json&fourk=1&bvid=BV1pV411o7yD&ep_id=317925&fnver=0&fnval=16&module=pgc + if (isProxy && proxy == 1) + { + string proxyUrl1 = "https://www.biliplus.com/BPplayurl.php"; + url = $"{proxyUrl1}?cid={cid}&bvid={bvid}&qn={quality}&otype=json&fourk=1&fnver=0&fnval=16&module=pgc"; + } + else if (isProxy && proxy == 2) + { + string proxyUrl2 = "https://biliplus.ipcjs.top/BPplayurl.php"; + url = $"{proxyUrl2}?cid={cid}&bvid={bvid}&qn={quality}&otype=json&fourk=1&fnver=0&fnval=16&module=pgc"; + } + + string response = Utils.RequestWeb(url, referer); + //Console.WriteLine(response); + + try + { + PlayUrl playUrl; + if (isProxy) + { + PlayUrlData playUrlData = JsonConvert.DeserializeObject(response); + + playUrl = new PlayUrl + { + result = playUrlData + }; + } + else + { + playUrl = JsonConvert.DeserializeObject(response); + } + + if (playUrl != null) + { + if (playUrl.data != null) { return playUrl.data; } + if (playUrl.result != null) { return playUrl.result; } + + // 无法从B站获取数据,进入代理网站 + if (Settings.GetInstance().IsLiftingOfRegion() == ALLOW_STATUS.YES) + { + switch (proxy) + { + case 0: + return GetStreamInfo(bvid, avid, cid, episodeId, quality, referer, true, 1); + case 1: + return GetStreamInfo(bvid, avid, cid, episodeId, quality, referer, true, 2); + case 2: + return null; + } + } + + return null; + } + else { return null; } + } + catch (JsonReaderException e) + { + Console.WriteLine("GetStreamInfo()发生JsonReader异常: {0}", e); + return null; + } + catch (Exception e) + { + Console.WriteLine("GetStreamInfo()发生异常: {0}", e); + return null; + } + } + + //public static List GetAllDanmaku(long cid, long publishTime, string referer) + //{ + // List danmakus = new List(); + + // // 设置视频发布日期 + // DateTime publishDate = new DateTime(1970, 1, 1); + // publishDate = publishDate.AddSeconds(publishTime); + + // // 获得有弹幕的日期date + // List danmakuDateList = new List(); + // while (true) + // { + // string month = publishDate.ToString("yyyy-MM"); + // string url = $"https://api.bilibili.com/x/v2/dm/history/index?type=1&oid={cid}&month={month}"; + // string response = Utils.RequestWeb(url, referer); + + // DanmuDate danmakuDate; + // try + // { + // danmakuDate = JsonConvert.DeserializeObject(response); + // } + // catch (Exception e) + // { + // Console.WriteLine("GetAllDanmaku()发生异常: {0}", e); + // continue; + // } + // if (danmakuDate != null || danmakuDate.data != null) { danmakuDateList.AddRange(danmakuDate.data); } + + // if (publishDate.CompareTo(DateTime.Now) > 0) { break; } + // publishDate = publishDate.AddMonths(1); + // Thread.Sleep(100); + // } + + // // 获取弹幕 + // foreach (var date in danmakuDateList) + // { + // Console.WriteLine(date); + + // List danmakusOfOneDay = GetDanmaku(cid, date); + + // foreach (Danmaku danmaku in danmakusOfOneDay) + // { + // if (danmakus.Find(it => it.DanmuId == danmaku.DanmuId) == null) + // { + // danmakus.Add(danmaku); + // } + // } + // } + + // // 按弹幕发布时间排序 + // danmakus = danmakus.OrderBy(it => it.Timestamp).ToList(); + + // return danmakus; + //} + + //public static List GetDanmaku(long cid, string date, string referer) + //{ + // string url = $"https://api.bilibili.com/x/v2/dm/history?type=1&oid={cid}&date={date}"; + // string response = Utils.RequestWeb(url, referer); + + // // + // // {"code":-101,"message":"账号未登录","ttl":1} + // if (response.Contains("")) + // { + // List danmakus = new List(); + + // XmlDocument doc = new XmlDocument(); + // doc.LoadXml(response); + // // 取得节点名为d的XmlNode集合 + // XmlNodeList danmuList = doc.GetElementsByTagName("d"); + // foreach (XmlNode node in danmuList) + // { + // // 返回的是文字内容 + // string nodeText = node.InnerText; + // // 节点p属性值 + // string childValue = node.Attributes["p"].Value; + // // 拆分属性 + // string[] attrs = childValue.Split(','); + + // Danmaku danmaku = new Danmaku + // { + // Text = nodeText, + // Time = float.Parse(attrs[0]), + // Type = int.Parse(attrs[1]), + // Fontsize = int.Parse(attrs[2]), + // Color = long.Parse(attrs[3]), + // Timestamp = long.Parse(attrs[4]), + // Pool = int.Parse(attrs[5]), + // UserId = attrs[6], + // DanmuId = attrs[7] + // }; + // danmakus.Add(danmaku); + // } + + // return danmakus; + // } + // else + // { + // DanmuFromWeb danmu = JsonConvert.DeserializeObject(response); + // if (danmu != null) { Console.WriteLine(danmu.message); } + // return null; + // } + //} + + ///// + ///// 获取弹幕,不需要登录信息,只能获取3000条弹幕 + ///// + ///// + ///// + //public static List GetDanmaku(long cid, string referer) + //{ + // string url = $"https://api.bilibili.com/x/v1/dm/list.so?oid={cid}"; + // string response = Utils.RequestWeb(url, referer); + + // if (response.Contains("")) + // { + // List danmakus = new List(); + + // XmlDocument doc = new XmlDocument(); + // doc.LoadXml(response); + // // 取得节点名为d的XmlNode集合 + // XmlNodeList danmuList = doc.GetElementsByTagName("d"); + // foreach (XmlNode node in danmuList) + // { + // // 返回的是文字内容 + // string nodeText = node.InnerText; + // // 节点p属性值 + // string childValue = node.Attributes["p"].Value; + // // 拆分属性 + // string[] attrs = childValue.Split(','); + + // Danmaku danmaku = new Danmaku + // { + // Text = nodeText, + // Time = float.Parse(attrs[0]), + // Type = int.Parse(attrs[1]), + // Fontsize = int.Parse(attrs[2]), + // Color = long.Parse(attrs[3]), + // Timestamp = long.Parse(attrs[4]), + // Pool = int.Parse(attrs[5]), + // UserId = attrs[6], + // DanmuId = attrs[7] + // }; + // danmakus.Add(danmaku); + // } + + // return danmakus; + // } + // return null; + //} + + } +} diff --git a/src/Core/Encryptor.cs b/src/Core/Encryptor.cs new file mode 100644 index 0000000..7083606 --- /dev/null +++ b/src/Core/Encryptor.cs @@ -0,0 +1,316 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Core +{ + public static class Encryptor + { + private const ulong FC_TAG = 0xFC010203040506CF; + private const int BUFFER_SIZE = 128 * 1024; + + /// + /// 加密文件 + /// + /// 待加密文件 + /// 加密后输入文件 + /// 加密密码 + public static void EncryptFile(string inFile, string outFile, string password) + { + using (FileStream fin = File.OpenRead(inFile), fout = File.OpenWrite(outFile)) + { + long lSize = fin.Length; // 输入文件长度 + int size = (int)lSize; + byte[] bytes = new byte[BUFFER_SIZE]; // 缓存 + int read = -1; // 输入文件读取数量 + int value = 0; + + // 获取IV和salt + byte[] IV = GenerateRandomBytes(16); + byte[] salt = GenerateRandomBytes(16); + + // 创建加密对象 + SymmetricAlgorithm sma = CreateRijndael(password, salt); + sma.IV = IV; + + // 在输出文件开始部分写入IV和salt + fout.Write(IV, 0, IV.Length); + fout.Write(salt, 0, salt.Length); + + // 创建散列加密 + HashAlgorithm hasher = SHA256.Create(); + using (CryptoStream cout = new CryptoStream(fout, sma.CreateEncryptor(), CryptoStreamMode.Write), + chash = new CryptoStream(Stream.Null, hasher, CryptoStreamMode.Write)) + { + BinaryWriter bw = new BinaryWriter(cout); + bw.Write(lSize); + + bw.Write(FC_TAG); + + // 读写字节块到加密流缓冲区 + while ((read = fin.Read(bytes, 0, bytes.Length)) != 0) + { + cout.Write(bytes, 0, read); + chash.Write(bytes, 0, read); + value += read; + } + // 关闭加密流 + chash.Flush(); + chash.Close(); + + // 读取散列 + byte[] hash = hasher.Hash; + + // 输入文件写入散列 + cout.Write(hash, 0, hash.Length); + + // 关闭文件流 + cout.Flush(); + cout.Close(); + } + } + } + + /// + /// 解密文件 + /// + /// 待解密文件 + /// 解密后输出文件 + /// 解密密码 + public static void DecryptFile(string inFile, string outFile, string password) + { + // 创建打开文件流 + using (FileStream fin = File.OpenRead(inFile), fout = File.OpenWrite(outFile)) + { + int size = (int)fin.Length; + byte[] bytes = new byte[BUFFER_SIZE]; + int read = -1; + int value = 0; + int outValue = 0; + + byte[] IV = new byte[16]; + fin.Read(IV, 0, 16); + byte[] salt = new byte[16]; + fin.Read(salt, 0, 16); + + SymmetricAlgorithm sma = CreateRijndael(password, salt); + sma.IV = IV; + + value = 32; + long lSize = -1; + + // 创建散列对象, 校验文件 + HashAlgorithm hasher = SHA256.Create(); + + try + { + using (CryptoStream cin = new CryptoStream(fin, sma.CreateDecryptor(), CryptoStreamMode.Read), + chash = new CryptoStream(Stream.Null, hasher, CryptoStreamMode.Write)) + { + // 读取文件长度 + BinaryReader br = new BinaryReader(cin); + lSize = br.ReadInt64(); + ulong tag = br.ReadUInt64(); + + if (FC_TAG != tag) + throw new CryptoHelpException("文件被破坏"); + + long numReads = lSize / BUFFER_SIZE; + + long slack = (long)lSize % BUFFER_SIZE; + + for (int i = 0; i < numReads; ++i) + { + read = cin.Read(bytes, 0, bytes.Length); + fout.Write(bytes, 0, read); + chash.Write(bytes, 0, read); + value += read; + outValue += read; + } + + if (slack > 0) + { + read = cin.Read(bytes, 0, (int)slack); + fout.Write(bytes, 0, read); + chash.Write(bytes, 0, read); + value += read; + outValue += read; + } + + chash.Flush(); + chash.Close(); + + fout.Flush(); + fout.Close(); + + byte[] curHash = hasher.Hash; + + // 获取比较和旧的散列对象 + byte[] oldHash = new byte[hasher.HashSize / 8]; + read = cin.Read(oldHash, 0, oldHash.Length); + if ((oldHash.Length != read) || (!CheckByteArrays(oldHash, curHash))) + throw new CryptoHelpException("文件被破坏"); + } + } + catch (Exception e) + { + Console.WriteLine("DecryptFile()发生异常: {0}", e); + } + + if (outValue != lSize) + throw new CryptoHelpException("文件大小不匹配"); + } + } + + /// + /// 检验两个Byte数组是否相同 + /// + /// Byte数组 + /// Byte数组 + /// true-相等 + private static bool CheckByteArrays(byte[] b1, byte[] b2) + { + if (b1.Length == b2.Length) + { + for (int i = 0; i < b1.Length; ++i) + { + if (b1[i] != b2[i]) + return false; + } + return true; + } + return false; + } + + /// + /// 创建DebugLZQ ,http://www.cnblogs.com/DebugLZQ + /// + /// 密码 + /// + /// 加密对象 + private static SymmetricAlgorithm CreateRijndael(string password, byte[] salt) + { + PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, salt, "SHA256", 1000); + + SymmetricAlgorithm sma = Rijndael.Create(); + sma.KeySize = 256; + sma.Key = pdb.GetBytes(32); + sma.Padding = PaddingMode.PKCS7; + return sma; + } + + /// + /// 生成指定长度的随机Byte数组 + /// + /// Byte数组长度 + /// 随机Byte数组 + private static byte[] GenerateRandomBytes(int count) + { + // 加密文件随机数生成 + RandomNumberGenerator rand = new RNGCryptoServiceProvider(); + + byte[] bytes = new byte[count]; + rand.GetBytes(bytes); + return bytes; + } + + + /// + /// DES加密字符串 + /// + /// 待加密的字符串 + /// 加密密钥,要求为8位 + /// 加密成功返回加密后的字符串,失败返回源串 + public static string EncryptString(string encryptString, string encryptKey) + { + try + { + byte[] rgbKey = Encoding.UTF8.GetBytes(encryptKey.Substring(0, 8));//转换为字节 + byte[] rgbIV = Encoding.UTF8.GetBytes(encryptKey.Substring(0, 8)); + byte[] inputByteArray = Encoding.UTF8.GetBytes(encryptString); + DESCryptoServiceProvider dCSP = new DESCryptoServiceProvider();//实例化数据加密标准 + MemoryStream mStream = new MemoryStream();//实例化内存流 + //将数据流链接到加密转换的流 + CryptoStream cStream = new CryptoStream(mStream, dCSP.CreateEncryptor(rgbKey, rgbIV), CryptoStreamMode.Write); + cStream.Write(inputByteArray, 0, inputByteArray.Length); + cStream.FlushFinalBlock(); + // 转base64 + return Convert.ToBase64String(mStream.ToArray()); + } + catch (Exception e) + { + Console.WriteLine("EncryptString()发生异常: {0}", e); + return encryptString; + } + } + + /// + /// DES解密字符串 + /// + /// 待解密的字符串 + /// 解密密钥,要求为8位,和加密密钥相同 + /// 解密成功返回解密后的字符串,失败返源串 + public static string DecryptString(string decryptString, string decryptKey) + { + try + { + byte[] rgbKey = Encoding.UTF8.GetBytes(decryptKey); + byte[] rgbIV = Encoding.UTF8.GetBytes(decryptKey); + byte[] inputByteArray = Convert.FromBase64String(decryptString); + DESCryptoServiceProvider DCSP = new DESCryptoServiceProvider(); + MemoryStream mStream = new MemoryStream(); + CryptoStream cStream = new CryptoStream(mStream, DCSP.CreateDecryptor(rgbKey, rgbIV), CryptoStreamMode.Write); + cStream.Write(inputByteArray, 0, inputByteArray.Length); + cStream.FlushFinalBlock(); + return Encoding.UTF8.GetString(mStream.ToArray()); + } + catch (Exception e) + { + Console.WriteLine("DecryptString()发生异常: {0}", e); + return decryptString; + } + } + + /// + /// 计算字符串MD5值 + /// + /// + /// + public static string GetMd5Hash(string input) + { + if (input == null) + { + return null; + } + + MD5 md5Hash = MD5.Create(); + + // 将输入字符串转换为字节数组并计算哈希数据 + byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input)); + + // 创建一个 Stringbuilder 来收集字节并创建字符串 + StringBuilder sBuilder = new StringBuilder(); + + // 循环遍历哈希数据的每一个字节并格式化为十六进制字符串 + for (int i = 0; i < data.Length; i++) + { + sBuilder.Append(data[i].ToString("x2")); + } + + // 返回十六进制字符串 + return sBuilder.ToString(); + } + + + } + + /// + /// 异常处理类 + /// + public class CryptoHelpException : ApplicationException + { + public CryptoHelpException(string msg) : base(msg) { } + } + +} diff --git a/src/Core/FFmpegHelper.cs b/src/Core/FFmpegHelper.cs new file mode 100644 index 0000000..adfb4d7 --- /dev/null +++ b/src/Core/FFmpegHelper.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Windows; +using System.Windows.Controls; + +namespace Core +{ + public static class FFmpegHelper + { + + /// + /// 合并音频和视频 + /// + /// + /// + /// + public static bool MergeVideo(string video1, string video2, string destVideo) + { + string param = $"-i \"{video1}\" -i \"{video2}\" -acodec copy -vcodec copy -f mp4 \"{destVideo}\""; + if (video1 == null || !File.Exists(video1)) + { + param = $"-i \"{video2}\" -acodec copy -vcodec copy -f mp4 \"{destVideo}\""; + } + if (video2 == null || !File.Exists(video2)) + { + param = $"-i \"{video1}\" -acodec copy -vcodec copy -f mp4 \"{destVideo}\""; + } + if (!File.Exists(video1) && !File.Exists(video2)) { return false; } + + // 如果存在 + try { File.Delete(destVideo); } + catch (IOException e) + { + Console.WriteLine("MergeVideo()发生IO异常: {0}", e); + return false; + } + + ExcuteProcess("ffmpeg.exe", param, null, (s, e) => Console.WriteLine(e.Data)); + + try + { + if (video1 != null) { File.Delete(video1); } + if (video2 != null) { File.Delete(video2); } + } + catch (IOException e) + { + Console.WriteLine("MergeVideo()发生IO异常: {0}", e); + } + + return true; + } + + /// + /// 拼接多个视频 + /// + /// + /// + /// + /// + public static bool ConcatVideo(string workingDirectory, List flvFiles, string destVideo) + { + // contact的文件名,不包含路径 + string concatFileName = Guid.NewGuid().ToString("N") + "_concat.txt"; + try + { + string contact = ""; + foreach (string flv in flvFiles) + { + contact += $"file '{flv}'\n"; + } + + FileStream fileStream = new FileStream(workingDirectory + "/" + concatFileName, FileMode.Create); + StreamWriter streamWriter = new StreamWriter(fileStream); + //开始写入 + streamWriter.Write(contact); + //清空缓冲区 + streamWriter.Flush(); + //关闭流 + streamWriter.Close(); + fileStream.Close(); + } + catch (Exception e) + { + Console.WriteLine("ConcatVideo()发生异常: {0}", e); + return false; + } + + // ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mkv + // 加上-y,表示如果有同名文件,则默认覆盖 + string param = $"-f concat -safe 0 -i {concatFileName} -c copy \"{destVideo}\" -y"; + ExcuteProcess("ffmpeg.exe", param, workingDirectory, (s, e) => Console.WriteLine(e.Data)); + + // 删除临时文件 + try + { + // 删除concat文件 + File.Delete(workingDirectory + "/" + concatFileName); + + foreach (string flv in flvFiles) + { + File.Delete(flv); + } + } + catch (Exception e) + { + Console.WriteLine("ConcatVideo()发生异常: {0}", e); + } + + return true; + } + + /// + /// 去水印,非常消耗cpu资源 + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void Delogo(string video, string destVideo, int x, int y, int width, int height, TextBox output = null, Window window = null) + { + // ffmpeg -i "video.mp4" -vf delogo=x=1670:y=50:w=180:h=70:show=1 "delogo.mp4" + string param = $"-i \"{video}\" -vf delogo=x={x}:y={y}:w={width}:h={height} \"{destVideo}\" -hide_banner"; + ExcuteProcess("ffmpeg.exe", param, + null, (s, e) => + { + Console.WriteLine(e.Data); + if (output != null && window != null) + { + window.Dispatcher.Invoke(new Action(() => + { + output.Text += e.Data + "\n"; + output.ScrollToEnd(); + })); + } + }); + } + + /// + /// 从一个视频中仅提取音频 + /// + /// 源视频 + /// 目标音频 + /// 输出信息 + /// + public static void ExtractAudio(string video, string audio, TextBox output = null, Window window = null) + { + // 抽取音频命令 + // ffmpeg -i 3.mp4 -vn -y -acodec copy 3.aac + // ffmpeg -i 3.mp4 -vn -y -acodec copy 3.m4a + string param = $"-i \"{video}\" -vn -y -acodec copy \"{audio}\" -hide_banner"; + ExcuteProcess("ffmpeg.exe", param, + null, (s, e) => + { + Console.WriteLine(e.Data); + if (output != null && window != null) + { + window.Dispatcher.Invoke(new Action(() => + { + output.Text += e.Data + "\n"; + output.ScrollToEnd(); + })); + } + }); + } + + /// + /// 从一个视频中仅提取视频 + /// + /// 源视频 + /// 目标视频 + /// 输出信息 + /// + public static void ExtractVideo(string video, string destVideo, TextBox output = null, Window window = null) + { + // 提取视频 (Extract Video) + // ffmpeg -i Life.of.Pi.has.subtitles.mkv -vcodec copy –an videoNoAudioSubtitle.mp4 + string param = $"-i \"{video}\" -y -vcodec copy -an \"{destVideo}\" -hide_banner"; + ExcuteProcess("ffmpeg.exe", param, + null, (s, e) => + { + Console.WriteLine(e.Data); + if (output != null && window != null) + { + window.Dispatcher.Invoke(new Action(() => + { + output.Text += e.Data + "\n"; + output.ScrollToEnd(); + })); + } + }); + } + + /// + /// 提取视频的帧,输出为图片 + /// + /// + /// + /// + public static void ExtractFrame(string video, string image, uint number) + { + // 提取帧 + // ffmpeg -i caiyilin.wmv -vframes 1 wm.bmp + string param = $"-i \"{video}\" -y -vframes {number} \"{image}\""; + ExcuteProcess("ffmpeg.exe", param, null, (s, e) => Console.WriteLine(e.Data)); + } + + + /// + /// 执行一个控制台程序 + /// + /// 程序名称 + /// 参数 + /// 工作路径 + /// 输出重定向 + private static void ExcuteProcess(string exe, string arg, string workingDirectory, DataReceivedEventHandler output) + { + using (var p = new Process()) + { + p.StartInfo.FileName = exe; + p.StartInfo.Arguments = arg; + + // 工作目录 + if (workingDirectory != null) + { + p.StartInfo.WorkingDirectory = workingDirectory; + } + + p.StartInfo.UseShellExecute = false; //输出信息重定向 + p.StartInfo.CreateNoWindow = true; + p.StartInfo.RedirectStandardError = true; + p.StartInfo.RedirectStandardOutput = true; + + // 将 StandardErrorEncoding 改为 UTF-8 才不会出现中文乱码 + p.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8; + p.StartInfo.StandardErrorEncoding = System.Text.Encoding.UTF8; + + p.OutputDataReceived += output; + p.ErrorDataReceived += output; + + p.Start(); //启动线程 + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + p.WaitForExit(); //等待进程结束 + } + } + } + +} diff --git a/src/Core/FileDownloadUtil.cs b/src/Core/FileDownloadUtil.cs new file mode 100644 index 0000000..5501720 --- /dev/null +++ b/src/Core/FileDownloadUtil.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using System.Net; + +namespace Core +{ + // from https://www.jianshu.com/p/f31910ed8435 + public class FileDownloadUtil + { + private string url; //文件下载网络地址 + private string referer; //访问url的referer + private string path; //文件下载位置,如d:/download + private string filename; //文件名,如test.jpg + private string fileId; //文件ID,文件唯一标识,一般为UUID + + private System.Threading.Timer FileTimer; // 定时器 + private readonly int SPD_INTERVAL_SEC = 1; // 每隔多少秒计算一次速度 + private long FileTemp = 0; // 临时储存长度 + private float FileSpeed = 0; // //SPD_INTERVAL_SEC秒下载字节数 + + // 下载进度回调 + public delegate void ProgressChangedHandler(int progress, string fileId); + public event ProgressChangedHandler ProgressChanged; + protected virtual void OnProgressChanged(int progress, string fileId) + { + ProgressChanged?.Invoke(progress, fileId); + } + + public delegate void ProgressChanged2Handler(long totalBytes, long totalDownloadedByte, float speed, string fileId); + public event ProgressChanged2Handler ProgressChanged2; + protected virtual void OnProgressChanged2(long totalBytes, long totalDownloadedByte, float speed, string fileId) + { + ProgressChanged2?.Invoke(totalBytes, totalDownloadedByte, speed, fileId); + } + + // 下载结果回调 + public delegate void DownloadFinishHandler(bool isSuccess, string downloadPath, string fileId, string msg = null); + public event DownloadFinishHandler DownloadFinish; + protected virtual void OnDownloadFinish(bool isSuccess, string downloadPath, string fileId, string msg = null) + { + DownloadFinish?.Invoke(isSuccess, downloadPath, fileId, msg); + } + + //通过网络链接直接下载任意文件 + public FileDownloadUtil Init(string url, string referer, string path, string filename, string fileId) + { + this.url = url; + this.referer = referer; + this.path = path; + this.filename = filename; + this.fileId = fileId; + + return this; + } + + public void Download() + { + Download(url, path, filename, fileId); + } + + private void Download(string url, string path, string filename, string fileId) + { + + if (!Directory.Exists(path)) //判断文件夹是否存在 + Directory.CreateDirectory(path); + path = path + "\\" + filename; + + // 临时文件 "bilidownkyi\\" + string tempFile = Path.GetTempPath() + "downkyi." + Guid.NewGuid().ToString("N"); + //string tempFile = path + ".temp"; + + try + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); //存在则删除 + } + if (File.Exists(path)) + { + File.Delete(path); //存在则删除 + } + } + catch (IOException e) + { + Console.WriteLine("Download()发生IO异常: {0}", e); + } + + FileStream fs = null; + HttpWebRequest request = null; + HttpWebResponse response = null; + Stream responseStream = null; + try + { + //创建临时文件 + fs = new FileStream(tempFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + request = WebRequest.Create(url) as HttpWebRequest; + request.Method = "GET"; + request.Timeout = 60 * 1000; + request.UserAgent = Utils.GetUserAgent(); + //request.ContentType = "text/html;charset=UTF-8"; + request.Headers["accept-language"] = "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"; + //request.Headers["accept-encoding"] = "gzip, deflate, br"; + request.Headers["origin"] = "https://www.bilibili.com"; + request.Referer = referer; + + // 构造cookie + // web端请求视频链接时没有加入cookie + //if (!url.Contains("getLogin")) + //{ + // CookieContainer cookies = Login.GetLoginInfoCookies(); + // if (cookies != null) + // { + // request.CookieContainer = cookies; + // } + //} + + //发送请求并获取相应回应数据 + response = request.GetResponse() as HttpWebResponse; + //直到request.GetResponse()程序才开始向目标网页发送Post请求 + responseStream = response.GetResponseStream(); + byte[] bArr = new byte[1024]; + long totalBytes = response.ContentLength; //通过响应头获取文件大小,前提是响应头有文件大小 + int size = responseStream.Read(bArr, 0, (int)bArr.Length); //读取响应流到bArr,读取大小 + float percent = 0; //用来保存计算好的百分比 + long totalDownloadedByte = 0; //总共下载字节数 + + FileTimer = new System.Threading.Timer(SpeedTimer, null, 0, SPD_INTERVAL_SEC * 1000); + while (size > 0) //while (totalBytes > totalDownloadedByte) //while循环读取响应流 + { + fs.Write(bArr, 0, size); //写到临时文件 + // 定期刷新数据到磁盘,影响下载速度 + //fs.Flush(true); + + totalDownloadedByte += size; + FileTemp += size; + size = responseStream.Read(bArr, 0, (int)bArr.Length); + percent = (float)totalDownloadedByte / (float)totalBytes * 100; + + // 下载进度回调 + OnProgressChanged((int)percent, fileId); + OnProgressChanged2(totalBytes, totalDownloadedByte, FileSpeed, fileId); + } + + try + { + if (File.Exists(path)) + { + File.Delete(path); //存在则删除 + } + } + catch (IOException e) + { + Console.WriteLine("Download()发生IO异常: {0}", e); + } + + if (fs != null) + fs.Close(); + File.Move(tempFile, path); //重命名为正式文件 + OnDownloadFinish(true, path, fileId, null); //下载完成,成功回调 + } + catch (Exception ex) + { + Console.WriteLine("Download()发生异常: {0}", ex); + OnDownloadFinish(false, null, fileId, ex.Message); //下载完成,失败回调 + } + finally + { + if (fs != null) + fs.Close(); + if (request != null) + request.Abort(); + if (response != null) + response.Close(); + if (responseStream != null) + responseStream.Close(); + + try + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); //存在则删除 + } + } + catch (IOException e) + { + Console.WriteLine("Download()发生IO异常: {0}", e); + } + } + } + + private void SpeedTimer(object state) + { + FileSpeed = FileTemp / SPD_INTERVAL_SEC; //SPD_INTERVAL_SEC秒下载字节数, + FileTemp = 0; //清空临时储存 + } + + } +} diff --git a/src/Core/MachineCode.cs b/src/Core/MachineCode.cs new file mode 100644 index 0000000..e265cfc --- /dev/null +++ b/src/Core/MachineCode.cs @@ -0,0 +1,142 @@ +using System; +using System.Management; + +namespace Core +{ + public class MachineCode + { + private static MachineCode machineCode; + + /// + /// 获取机器码 + /// + /// + public static string GetMachineCodeString() + { + if (machineCode == null) + { + machineCode = new MachineCode(); + } + + string machineCodeString = "PC." + + machineCode.GetMainBordId() + "." + + machineCode.GetCpuInfo() + "." + + machineCode.GetDiskID();// + "." + + //machineCode.GetMoAddress(); + return machineCodeString.Replace(" ", ""); + } + + /// + /// 获取主板ID + /// + /// + public string GetMainBordId() + { + string strId = ""; + try + { + using (ManagementClass mc = new ManagementClass("Win32_BaseBoard")) + { + ManagementObjectCollection moc = mc.GetInstances(); + foreach (ManagementObject mo in moc) + { + strId = mo.Properties["SerialNumber"].Value.ToString(); + mo.Dispose(); + break; + } + } + } + catch (Exception) + { + return "unknown"; + //throw; + } + return strId; + } + + /// + /// 获取cpu序列号 + /// + /// + public string GetCpuInfo() + { + string cpuInfo = ""; + try + { + using (ManagementClass cimobject = new ManagementClass("Win32_Processor")) + { + ManagementObjectCollection moc = cimobject.GetInstances(); + + foreach (ManagementObject mo in moc) + { + cpuInfo = mo.Properties["ProcessorId"].Value.ToString(); + mo.Dispose(); + } + } + } + catch (Exception) + { + return "unknown"; + //throw; + } + return cpuInfo; + } + + /// + /// 获取硬盘ID + /// + /// + public string GetDiskID() + { + string diskName = ""; + string diskID = ""; + try + { + using (ManagementClass cimobject1 = new ManagementClass("Win32_DiskDrive")) + { + ManagementObjectCollection moc1 = cimobject1.GetInstances(); + foreach (ManagementObject mo in moc1) + { + diskName = mo.Properties["Model"].Value.ToString(); + diskID = mo.Properties["SerialNumber"].Value.ToString(); + mo.Dispose(); + } + } + } + catch (Exception) + { + return "unknown"; + //throw; + } + return diskName + diskID; + } + + /// + /// 获取网卡硬件地址 + /// + /// + public string GetMoAddress() + { + string MoAddress = ""; + try + { + using (ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration")) + { + ManagementObjectCollection moc2 = mc.GetInstances(); + foreach (ManagementObject mo in moc2) + { + if ((bool)mo["IPEnabled"] == true) + MoAddress = mo["MacAddress"].ToString(); + mo.Dispose(); + } + } + } + catch (Exception) + { + return "unknown"; + //throw; + } + return MoAddress; + } + } +} diff --git a/src/Core/UserSpaceOld.cs b/src/Core/UserSpaceOld.cs new file mode 100644 index 0000000..88bd659 --- /dev/null +++ b/src/Core/UserSpaceOld.cs @@ -0,0 +1,148 @@ +using Core.entity; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Core +{ + public static class UserSpaceOld + { + + /// + /// 获取我创建的收藏夹 + /// + /// + /// + public static FavFolderData GetCreatedFavFolder(long mid) + { + string url = $"https://api.bilibili.com/x/v3/fav/folder/created/list?up_mid={mid}&ps=50"; + return GetAllFavFolder(url); + } + + /// + /// 获取我收藏的收藏夹 + /// + /// + /// + public static FavFolderData GetCollectedFavFolder(long mid) + { + string url = $"https://api.bilibili.com/x/v3/fav/folder/collected/list?up_mid={mid}&ps=50"; + return GetAllFavFolder(url); + } + + private static FavFolderData GetAllFavFolder(string baseUrl) + { + FavFolderData userFavoriteData = new FavFolderData + { + count = 0, + list = new List() + }; + int i = 0; + while (true) + { + i++; + string url = baseUrl + $"&pn={i}"; + + var data = GetFavFolder(url); + if (data == null) + { break; } + if (data.count == 0 || data.list == null) + { break; } + + userFavoriteData.list.AddRange(data.list); + } + userFavoriteData.count = userFavoriteData.list.Count; + return userFavoriteData; + } + + private static FavFolderData GetFavFolder(string url) + { + string referer = "https://www.bilibili.com"; + string response = Utils.RequestWeb(url, referer); + + try + { + FavFolder favFolder = JsonConvert.DeserializeObject(response); + if (favFolder == null || favFolder.data == null) { return null; } + + return favFolder.data; + } + catch (Exception e) + { + Console.WriteLine("GetFavFolder()发生异常: {0}", e); + return null; + } + } + + /// + /// 获得某个收藏夹的内容 + /// + /// + /// + public static List GetAllFavResource(long mediaId) + { + string baseUrl = $"https://api.bilibili.com/x/v3/fav/resource/list?media_id={mediaId}&ps=20"; + List medias = new List(); + + int i = 0; + while (true) + { + i++; + string url = baseUrl + $"&pn={i}"; + + var data = GetFavResource(url); + if (data == null || data.Count == 0) + { break; } + + medias.AddRange(data); + } + return medias; + } + + private static List GetFavResource(string url) + { + string referer = "https://www.bilibili.com"; + string response = Utils.RequestWeb(url, referer); + + try + { + FavResource favResource = JsonConvert.DeserializeObject(response); + if (favResource == null || favResource.data == null) { return null; } + return favResource.data.medias; + } + catch (Exception e) + { + Console.WriteLine("GetFavResource()发生异常: {0}", e); + return null; + } + } + + + /// + /// 获取订阅番剧的数量 + /// 废弃 + /// + /// + /// + //public static int GetBangumiFollowList(long mid) + //{ + // string url = $"https://space.bilibili.com/ajax/Bangumi/getList?mid={mid}"; + // string referer = "https://www.bilibili.com"; + // string response = Utils.RequestWeb(url, referer); + + // try + // { + // BangumiList bangumiList = JsonConvert.DeserializeObject(response); + // if (bangumiList == null || bangumiList.data == null) { return -1; } + + // return bangumiList.data.count; + // } + // catch (Exception e) + // { + // Console.WriteLine("发生异常: {0}", e); + // return 0; + // } + //} + + } +} diff --git a/src/Core/Utils.cs b/src/Core/Utils.cs new file mode 100644 index 0000000..f27fc64 --- /dev/null +++ b/src/Core/Utils.cs @@ -0,0 +1,430 @@ +using Brotli; +using Core.api.login; +using Core.history; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; + +namespace Core +{ + public static class Utils + { + /// + /// 随机的UserAgent + /// + /// userAgent + public static string GetUserAgent() + { + string[] userAgents = { + // Chrome + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36", + + // 新版Edge + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 Edg/83.0.478.58", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.66", + + // IE 11 + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)", + + // 火狐 + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0", + + // Opera + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36 OPR/63.0.3368.43", + + // MacOS Chrome + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36", + + // MacOS Safari + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15" + }; + + var time = DateTime.Now; + + Random random = new Random(time.GetHashCode()); + int number = random.Next(0, userAgents.Length - 1); + + return userAgents[number]; + } + + /// + /// 发送get或post请求 + /// + /// + /// + /// + /// + /// + public static string RequestWeb(string url, string referer = null, string method = "GET", Dictionary parameters = null, int retry = 3) + { + // 重试次数 + if (retry <= 0) { return ""; } + + // post请求,发送参数 + if (method == "POST" && parameters != null) + { + StringBuilder builder = new StringBuilder(); + int i = 0; + foreach (var item in parameters) + { + if (i > 0) builder.Append("&"); + builder.AppendFormat("{0}={1}", item.Key, item.Value); + i++; + } + + url += "?" + builder.ToString(); + } + + try + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.Method = method; + request.Timeout = 30 * 1000; + request.UserAgent = GetUserAgent(); + //request.ContentType = "application/json,text/html,application/xhtml+xml,application/xml;charset=UTF-8"; + request.Headers["accept-language"] = "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"; + request.Headers["accept-encoding"] = "gzip, deflate, br"; + + //request.Headers["sec-fetch-dest"] = "empty"; + //request.Headers["sec-fetch-mode"] = "cors"; + //request.Headers["sec-fetch-site"] = "same-site"; + + // referer + if (referer != null) + { + request.Referer = referer; + } + + // 构造cookie + if (!url.Contains("getLogin")) + { + request.Headers["origin"] = "https://www.bilibili.com"; + + CookieContainer cookies = LoginHelper.GetInstance().GetLoginInfoCookies(); + if (cookies != null) + { + request.CookieContainer = cookies; + } + } + + //// post请求,发送参数 + //if (method == "POST" && parameters != null) + //{ + // StringBuilder builder = new StringBuilder(); + // int i = 0; + // foreach (var item in parameters) + // { + // if (i > 0) builder.Append("&"); + // builder.AppendFormat("{0}={1}", item.Key, item.Value); + // i++; + // } + // byte[] data = Encoding.UTF8.GetBytes(builder.ToString()); + // request.ContentLength = data.Length; + + // Stream reqStream = request.GetRequestStream(); + // reqStream.Write(data, 0, data.Length); + // reqStream.Close(); + + // Console.WriteLine("\n" + builder.ToString() + "\t" + data.Length + "\n"); + //} + + //HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + //Stream responseStream = response.GetResponseStream(); + //StreamReader streamReader = new StreamReader(responseStream, Encoding.UTF8); + //string str = streamReader.ReadToEnd(); + //streamReader.Close(); + //responseStream.Close(); + + string html = string.Empty; + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + if (response.ContentEncoding.ToLower().Contains("gzip")) + { + using (GZipStream stream = new GZipStream(response.GetResponseStream(), CompressionMode.Decompress)) + { + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + html = reader.ReadToEnd(); + } + } + } + else if (response.ContentEncoding.ToLower().Contains("deflate")) + { + using (DeflateStream stream = new DeflateStream(response.GetResponseStream(), CompressionMode.Decompress)) + { + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + html = reader.ReadToEnd(); + } + } + } + else if (response.ContentEncoding.ToLower().Contains("br")) + { + using (BrotliStream stream = new BrotliStream(response.GetResponseStream(), CompressionMode.Decompress)) + { + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + html = reader.ReadToEnd(); + } + } + } + else + { + using (Stream stream = response.GetResponseStream()) + { + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + html = reader.ReadToEnd(); + } + } + } + } + + return html; + } + catch (WebException e) + { + Console.WriteLine("RequestWeb()发生Web异常: {0}", e); + return RequestWeb(url, referer, method, parameters, retry - 1); + } + catch (IOException e) + { + Console.WriteLine("RequestWeb()发生IO异常: {0}", e); + return RequestWeb(url, referer, method, parameters, retry - 1); + } + catch (Exception e) + { + Console.WriteLine("RequestWeb()发生其他异常: {0}", e); + return RequestWeb(url, referer, method, parameters, retry - 1); + } + } + + /// + /// 解析二维码登录返回的url,用于设置cookie + /// + /// + /// + public static CookieContainer ParseCookie(string url) + { + CookieContainer cookieContainer = new CookieContainer(); + + if (url == null || url == "") { return cookieContainer; } + + string[] strList = url.Split('?'); + if (strList.Count() < 2) { return cookieContainer; } + + string[] strList2 = strList[1].Split('&'); + if (strList2.Count() == 0) { return cookieContainer; } + + // 获取expires + string expires = strList2.FirstOrDefault(it => it.Contains("Expires")).Split('=')[1]; + DateTime dateTime = DateTime.Now; + dateTime = dateTime.AddSeconds(int.Parse(expires)); + + foreach (var item in strList2) + { + string[] strList3 = item.Split('='); + if (strList3.Count() < 2) { continue; } + + string name = strList3[0]; + string value = strList3[1]; + + // 不需要 + if (name == "Expires" || name == "gourl") { continue; } + + // 添加cookie + cookieContainer.Add(new Cookie(name, value, "/", ".bilibili.com") { Expires = dateTime }); +#if DEBUG + Console.WriteLine(name + ": " + value + "\t" + cookieContainer.Count); +#endif + } + + return cookieContainer; + } + + /// + /// 将CookieContainer中的所有的Cookie读出来 + /// + /// + /// + public static List GetAllCookies(CookieContainer cc) + { + List lstCookies = new List(); + + Hashtable table = (Hashtable)cc.GetType().InvokeMember("m_domainTable", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.GetField | + System.Reflection.BindingFlags.Instance, null, cc, new object[] { }); + + foreach (object pathList in table.Values) + { + SortedList lstCookieCol = (SortedList)pathList.GetType().InvokeMember("m_list", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.GetField + | System.Reflection.BindingFlags.Instance, null, pathList, new object[] { }); + foreach (CookieCollection colCookies in lstCookieCol.Values) + foreach (Cookie c in colCookies) lstCookies.Add(c); + } + + return lstCookies; + } + + /// + /// 写入cookies到磁盘 + /// + /// + /// + /// + public static bool WriteCookiesToDisk(string file, CookieContainer cookieJar) + { + return WriteObjectToDisk(file, cookieJar); + } + + /// + /// 从磁盘读取cookie + /// + /// + /// + public static CookieContainer ReadCookiesFromDisk(string file) + { + return (CookieContainer)ReadObjectFromDisk(file); + } + + /// + /// 写入历史数据到磁盘 + /// + /// + /// + /// + public static bool WriteHistoryToDisk(string file, HistoryEntity history) + { + return WriteObjectToDisk(file, history); + } + + /// + /// 从磁盘读取历史数据 + /// + /// + /// + public static HistoryEntity ReadHistoryFromDisk(string file) + { + return (HistoryEntity)ReadObjectFromDisk(file); + } + + /// + /// 写入序列化对象到磁盘 + /// + /// + /// + /// + public static bool WriteObjectToDisk(string file, object obj) + { + try + { + using (Stream stream = File.Create(file)) + { +#if DEBUG + Console.Out.Write("Writing object to disk... "); +#endif + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(stream, obj); +#if DEBUG + Console.Out.WriteLine("Done."); +#endif + return true; + } + } + catch (IOException e) + { + Console.WriteLine("WriteObjectToDisk()发生IO异常: {0}", e); + return false; + } + catch (Exception e) + { + //Console.Out.WriteLine("Problem writing object to disk: " + e.GetType()); + Console.WriteLine("WriteObjectToDisk()发生异常: {0}", e); + return false; + } + } + + /// + /// 从磁盘读取序列化对象 + /// + /// + /// + public static object ReadObjectFromDisk(string file) + { + try + { + using (Stream stream = File.Open(file, FileMode.Open)) + { +#if DEBUG + Console.Out.Write("Reading object from disk... "); +#endif + BinaryFormatter formatter = new BinaryFormatter(); +#if DEBUG + Console.Out.WriteLine("Done."); +#endif + return formatter.Deserialize(stream); + } + } + catch (IOException e) + { + Console.WriteLine("ReadObjectFromDisk()发生IO异常: {0}", e); + return null; + } + catch (Exception e) + { + //Console.Out.WriteLine("Problem reading object from disk: " + e.GetType()); + Console.WriteLine("ReadObjectFromDisk()发生异常: {0}", e); + return null; + } + } + + /// + /// 生成二维码 + /// + /// 信息 + /// 版本 1 ~ 40 + /// 像素点大小 + /// 图标路径 + /// 图标尺寸 + /// 图标边框厚度 + /// 二维码白边 + /// 位图 + public static Bitmap EncodeQRCode(string msg, int version, int pixel, string icon_path, int icon_size, int icon_border, bool white_edge) + { + QRCoder.QRCodeGenerator code_generator = new QRCoder.QRCodeGenerator(); + + QRCoder.QRCodeData code_data = code_generator.CreateQrCode(msg, QRCoder.QRCodeGenerator.ECCLevel.H/* 这里设置容错率的一个级别 */, true, false, QRCoder.QRCodeGenerator.EciMode.Utf8, version); + + QRCoder.QRCode code = new QRCoder.QRCode(code_data); + + Bitmap icon; + if (icon_path == null || icon_path == "") + { + icon = null; + } + else + { + icon = new Bitmap(icon_path); + } + + Bitmap bmp = code.GetGraphic(pixel, Color.Black, Color.White, icon, icon_size, icon_border, white_edge); + return bmp; + } + + } + +} diff --git a/src/Core/VideoZone.cs b/src/Core/VideoZone.cs new file mode 100644 index 0000000..3a7a16d --- /dev/null +++ b/src/Core/VideoZone.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; + +namespace Core +{ + public class VideoZone + { + private static VideoZone that; + private readonly List zones = new List(); + + /// + /// 使用单例模式获取分区,注意未搜索到的分区需要额外处理 + /// + /// + public static VideoZone Instance() + { + if (that == null) + { + that = new VideoZone(); + } + return that; + } + + public List GetZone() + { + return zones; + } + + private VideoZone() + { + //动画 + zones.Add(new ZoneAttr(1, "douga", "动画")); // 主分区 + zones.Add(new ZoneAttr(24, "mad", "MAD·AMV", 1)); //具有一定制作程度的动画或静画的二次创作视频 + zones.Add(new ZoneAttr(25, "mmd", "MMD·3D", 1)); //使用MMD(MikuMikuDance)和其他3D建模类软件制作的视频 + zones.Add(new ZoneAttr(47, "voice", "短片·手书·配音", 1)); //追求创新并具有强烈特色的短片、手书(绘)及ACG相关配音 + zones.Add(new ZoneAttr(210, "garage_kit", "手办·模玩", 1)); //手办模玩的测评、改造或其他衍生内容 + zones.Add(new ZoneAttr(86, "tokusatsu", "特摄", 1)); //特摄相关衍生视频 + zones.Add(new ZoneAttr(27, "other", "综合", 1)); //以动画及动画相关内容为素材,包括但不仅限于音频替换、杂谈、排行榜等内容 + + //番剧 + zones.Add(new ZoneAttr(13, "anime", "番剧")); // 主分区 + zones.Add(new ZoneAttr(33, "serial", "连载动画", 13)); //当季连载的动画番剧 + zones.Add(new ZoneAttr(32, "finish", "完结动画", 13)); //已完结的动画番剧合集 + zones.Add(new ZoneAttr(51, "information", "资讯", 13)); //动画番剧相关资讯视频 + zones.Add(new ZoneAttr(152, "offical", "官方延伸", 13)); //动画番剧为主题的宣传节目、采访视频,及声优相关视频 + + //国创 + zones.Add(new ZoneAttr(167, "guochuang", "国创")); // 主分区 + zones.Add(new ZoneAttr(153, "chinese", "国产动画", 167)); //我国出品的PGC动画 + zones.Add(new ZoneAttr(168, "original", "国产原创相关", 167)); // + zones.Add(new ZoneAttr(169, "puppetry", "布袋戏", 167)); // + zones.Add(new ZoneAttr(195, "motioncomic", "动态漫·广播剧", 167)); // + zones.Add(new ZoneAttr(170, "information", "资讯", 167)); // + + //音乐 + zones.Add(new ZoneAttr(3, "music", "音乐")); // 主分区 + zones.Add(new ZoneAttr(28, "original", "原创音乐", 3)); //个人或团队制作以音乐为主要原创因素的歌曲或纯音乐 + zones.Add(new ZoneAttr(31, "cover", "翻唱", 3)); //一切非官方的人声再演绎歌曲作品 + 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(29, "live", "音乐现场", 3)); //音乐实况表演视频 + zones.Add(new ZoneAttr(130, "other", "音乐综合", 3)); //收录无法定义到其他音乐子分区的音乐视频 + + //舞蹈 + zones.Add(new ZoneAttr(129, "dance", "舞蹈")); // 主分区 + zones.Add(new ZoneAttr(20, "otaku", "宅舞", 129)); //与ACG相关的翻跳、原创舞蹈 + zones.Add(new ZoneAttr(198, "hiphop", "街舞", 129)); //收录街舞相关内容,包括赛事现场、舞室作品、个人翻跳、FREESTYLE等 + zones.Add(new ZoneAttr(199, "star", "明星舞蹈", 129)); //国内外明星发布的官方舞蹈及其翻跳内容 + zones.Add(new ZoneAttr(200, "china", "中国舞", 129)); //传承中国艺术文化的舞蹈内容,包括古典舞、民族民间舞、汉唐舞、古风舞等 + zones.Add(new ZoneAttr(154, "three_d", "舞蹈综合", 129)); //收录无法定义到其他舞蹈子分区的舞蹈视频 + zones.Add(new ZoneAttr(156, "demo", "舞蹈教程", 129)); //镜面慢速,动作分解,基础教程等具有教学意义的舞蹈视频 + + //游戏 + zones.Add(new ZoneAttr(4, "game", "游戏")); // 主分区 + zones.Add(new ZoneAttr(17, "stand_alone", "单机游戏", 4)); //以所有平台(PC、主机、移动端)的单机或联机游戏为主的视频内容,包括游戏预告、CG、实况解说及相关的评测、杂谈与视频剪辑等 + zones.Add(new ZoneAttr(171, "esports", "电子竞技", 4)); //具有高对抗性的电子竞技游戏项目,其相关的赛事、实况、攻略、解说、短剧等视频。 + zones.Add(new ZoneAttr(172, "mobile", "手机游戏", 4)); //以手机及平板设备为主要平台的游戏,其相关的实况、攻略、解说、短剧、演示等视频。 + zones.Add(new ZoneAttr(65, "online", "网络游戏", 4)); //由网络运营商运营的多人在线游戏,以及电子竞技的相关游戏内容。包括赛事、攻略、实况、解说等相关视频 + zones.Add(new ZoneAttr(173, "board", "桌游棋牌", 4)); //桌游、棋牌、卡牌对战等及其相关电子版游戏的实况、攻略、解说、演示等视频。 + zones.Add(new ZoneAttr(121, "gmv", "GMV", 4)); //由游戏素材制作的MV视频。以游戏内容或CG为主制作的,具有一定创作程度的MV类型的视频 + zones.Add(new ZoneAttr(136, "music", "音游", 4)); //各个平台上,通过配合音乐与节奏而进行的音乐类游戏视频 + zones.Add(new ZoneAttr(19, "mugen", "Mugen", 4)); //以Mugen引擎为平台制作、或与Mugen相关的游戏视频 + + //知识 + zones.Add(new ZoneAttr(36, "technology", "知识")); // 主分区 + 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(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(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(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(211, "food", "美食")); // 主分区 + zones.Add(new ZoneAttr(76, "make", "美食制作", 211)); //学做人间美味,展示精湛厨艺 + zones.Add(new ZoneAttr(212, "detective", "美食侦探", 211)); //寻找美味餐厅,发现街头美食 + zones.Add(new ZoneAttr(213, "measurement", "美食测评", 211)); //吃货世界,品尝世间美味 + zones.Add(new ZoneAttr(214, "rural", "田园美食", 211)); //品味乡野美食,寻找山与海的味道 + zones.Add(new ZoneAttr(215, "record", "美食记录", 211)); //记录一日三餐,给生活添一点幸福感 + + //动物圈 + zones.Add(new ZoneAttr(217, "animal", "动物圈")); // 主分区 + zones.Add(new ZoneAttr(218, "cat", "喵星人", 217)); //喵喵喵喵喵 + zones.Add(new ZoneAttr(219, "dog", "汪星人", 217)); //汪汪汪汪汪 + zones.Add(new ZoneAttr(220, "panda", "大熊猫", 217)); //芝麻汤圆营业中 + zones.Add(new ZoneAttr(221, "wild_animal", "野生动物", 217)); //内有“猛兽”出没 + zones.Add(new ZoneAttr(222, "reptiles", "爬宠", 217)); //鳞甲有灵 + zones.Add(new ZoneAttr(75, "animal_composite", "动物综合", 217)); //收录除上述子分区外,其余动物相关视频以及非动物主体或多个动物主体的动物相关延伸内容 + + //鬼畜 + zones.Add(new ZoneAttr(119, "kichiku", "鬼畜")); // 主分区 + zones.Add(new ZoneAttr(22, "guide", "鬼畜调教", 119)); //使用素材在音频、画面上做一定处理,达到与BGM一定的同步感 + zones.Add(new ZoneAttr(26, "mad", "音MAD", 119)); //使用素材音频进行一定的二次创作来达到还原原曲的非商业性质稿件 + zones.Add(new ZoneAttr(126, "manual_vocaloid", "人力VOCALOID", 119)); //将人物或者角色的无伴奏素材进行人工调音,使其就像VOCALOID一样歌唱的技术 + zones.Add(new ZoneAttr(216, "theatre", "鬼畜剧场", 119)); //使用素材进行人工剪辑编排的有剧情的作品 + zones.Add(new ZoneAttr(127, "course", "教程演示", 119)); //鬼畜相关的教程演示 + + //时尚 + 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(202, "information", "资讯")); // 主分区 + zones.Add(new ZoneAttr(203, "hotspot", "热点", 202)); //全民关注的时政热门资讯 + zones.Add(new ZoneAttr(204, "global", "环球", 202)); //全球范围内发生的具有重大影响力的事件动态 + zones.Add(new ZoneAttr(205, "social", "社会", 202)); //日常生活的社会事件、社会问题、社会风貌的报道 + zones.Add(new ZoneAttr(206, "multiple", "综合", 202)); //除上述领域外其它垂直领域的综合资讯 + + //娱乐 + zones.Add(new ZoneAttr(5, "ent", "娱乐")); // 主分区 + zones.Add(new ZoneAttr(71, "variety", "综艺", 5)); //国内外有趣的综艺和综艺相关精彩剪辑 + zones.Add(new ZoneAttr(137, "star", "明星", 5)); //娱乐圈动态、明星资讯相关 + + //影视 + zones.Add(new ZoneAttr(181, "cinephile", "影视")); // 主分区 + zones.Add(new ZoneAttr(182, "cinecism", "影视杂谈", 181)); //影视评论、解说、吐槽、科普等 + zones.Add(new ZoneAttr(183, "montage", "影视剪辑", 181)); //对影视素材进行剪辑再创作的视频 + zones.Add(new ZoneAttr(85, "shortfilm", "短片", 181)); //追求自我表达且具有特色的短片 + zones.Add(new ZoneAttr(184, "trailer_info", "预告·资讯", 181)); //影视类相关资讯,预告,花絮等视频 + + //纪录片 + zones.Add(new ZoneAttr(177, "documentary", "纪录片")); // 主分区 + zones.Add(new ZoneAttr(37, "history", "人文·历史", 177)); // + zones.Add(new ZoneAttr(178, "science", "科学·探索·自然", 177)); // + zones.Add(new ZoneAttr(179, "military", "军事", 177)); // + zones.Add(new ZoneAttr(180, "travel", "社会·美食·旅行", 177)); // + + //电影 + zones.Add(new ZoneAttr(23, "movie", "电影")); // 主分区 + zones.Add(new ZoneAttr(147, "chinese", "华语电影", 23)); // + zones.Add(new ZoneAttr(145, "west", "欧美电影", 23)); // + zones.Add(new ZoneAttr(146, "japan", "日本电影", 23)); // + zones.Add(new ZoneAttr(83, "movie", "其他国家", 23)); // + + //电视剧 + zones.Add(new ZoneAttr(11, "tv", "电视剧")); // 主分区 + zones.Add(new ZoneAttr(185, "mainland", "国产剧", 11)); // + zones.Add(new ZoneAttr(187, "overseas", "海外剧", 11)); // + + } + } + + public class ZoneAttr + { + public int Id { get; } + public string Type { get; } + public string Name { get; } + public int ParentId { get; } + + public ZoneAttr(int id, string type, string name, int parentId = 0) + { + Id = id; + Type = type; + Name = name; + ParentId = parentId; + } + + } + +} diff --git a/src/Core/api/danmaku/BiliDanmaku.cs b/src/Core/api/danmaku/BiliDanmaku.cs new file mode 100644 index 0000000..6218273 --- /dev/null +++ b/src/Core/api/danmaku/BiliDanmaku.cs @@ -0,0 +1,36 @@ +namespace Core.api.danmaku.entity +{ + public class BiliDanmaku + { + public long Id { get; set; } //弹幕dmID + public int Progress { get; set; } //出现时间 + public int Mode { get; set; } //弹幕类型 + public int Fontsize { get; set; } //文字大小 + public uint Color { get; set; } //弹幕颜色 + public string MidHash { get; set; } //发送者UID的HASH + public string Content { get; set; } //弹幕内容 + public long Ctime { get; set; } //发送时间 + public int Weight { get; set; } //权重 + //public string Action { get; set; } //动作? + public int Pool { get; set; } //弹幕池 + + public override string ToString() + { + //return base.ToString(); + + string separator = "\n"; + return $"id: {Id}{separator}" + + $"progress: {Progress}{separator}" + + $"mode: {Mode}{separator}" + + $"fontsize: {Fontsize}{separator}" + + $"color: {Color}{separator}" + + $"midHash: {MidHash}{separator}" + + $"content: {Content}{separator}" + + $"ctime: {Ctime}{separator}" + + $"weight: {Weight}{separator}" + + //$"action: {Action}{separator}" + + $"pool: {Pool}"; + } + + } +} diff --git a/src/Core/api/danmaku/DanmakuProtobuf.cs b/src/Core/api/danmaku/DanmakuProtobuf.cs new file mode 100644 index 0000000..e2c8180 --- /dev/null +++ b/src/Core/api/danmaku/DanmakuProtobuf.cs @@ -0,0 +1,115 @@ +using Core.api.danmaku.entity; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Core.api.danmaku +{ + /// + /// protobuf弹幕 + /// + public class DanmakuProtobuf + { + private static DanmakuProtobuf instance; + + /// + /// 获取DanmakuProtobuf实例 + /// + /// + public static DanmakuProtobuf GetInstance() + { + if (instance == null) + { + instance = new DanmakuProtobuf(); + } + return instance; + } + + /// + /// 隐藏DanmakuProtobuf()方法,必须使用单例模式 + /// + private DanmakuProtobuf() { } + + /// + /// 下载6分钟内的弹幕,返回弹幕列表 + /// + /// 稿件avID + /// 视频CID + /// 分包,每6分钟一包 + /// + public List GetDanmakuProto(long avid, long cid, int segmentIndex) + { + string url = $"https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid={cid}&pid={avid}&segment_index={segmentIndex}"; + string referer = "https://www.bilibili.com"; + + FileDownloadUtil fileDownload = new FileDownloadUtil(); + fileDownload.Init(url, referer, Path.GetTempPath() + "downkyi/danmaku", $"{cid}-{segmentIndex}.proto", "DanmakuProtobuf"); + fileDownload.Download(); + + var danmakuList = new List(); + + DmSegMobileReply danmakus; + try + { + using (var input = File.OpenRead(Path.GetTempPath() + $"downkyi/danmaku/{cid}-{segmentIndex}.proto")) + { + danmakus = DmSegMobileReply.Parser.ParseFrom(input); + if (danmakus == null || danmakus.Elems == null) + { + return danmakuList; + } + + foreach (var dm in danmakus.Elems) + { + var danmaku = new BiliDanmaku + { + Id = dm.Id, + Progress = dm.Progress, + Mode = dm.Mode, + Fontsize = dm.Fontsize, + Color = dm.Color, + MidHash = dm.MidHash, + Content = dm.Content, + Ctime = dm.Ctime, + Weight = dm.Weight, + //Action = dm.Action, + Pool = dm.Pool + }; + danmakuList.Add(danmaku); + } + } + } + catch (Exception e) + { +#if DEBUG + Console.WriteLine("发生异常: {0}", e); +#endif + return null; + } + + return danmakuList; + } + + /// + /// 下载所有弹幕,返回弹幕列表 + /// + /// 稿件avID + /// 视频CID + /// + public List GetAllDanmakuProto(long avid, long cid) + { + var danmakuList = new List(); + + int segmentIndex = 0; + while (true) + { + segmentIndex += 1; + var danmakus = GetDanmakuProto(avid, cid, segmentIndex); + if (danmakus == null) { break; } + danmakuList.AddRange(danmakus); + } + return danmakuList; + } + + } +} diff --git a/src/Core/api/danmaku/proto/Danmaku.cs b/src/Core/api/danmaku/proto/Danmaku.cs new file mode 100644 index 0000000..75fb7b6 --- /dev/null +++ b/src/Core/api/danmaku/proto/Danmaku.cs @@ -0,0 +1,814 @@ +// +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: Danmaku.proto +// +#pragma warning disable 1591, 0612, 3021 +#region Designer generated code + +using pb = global::Google.Protobuf; +using pbc = global::Google.Protobuf.Collections; +using pbr = global::Google.Protobuf.Reflection; +using scg = global::System.Collections.Generic; +/// Holder for reflection information generated from Danmaku.proto +public static partial class DanmakuReflection { + + #region Descriptor + /// File descriptor for Danmaku.proto + public static pbr::FileDescriptor Descriptor { + get { return descriptor; } + } + private static pbr::FileDescriptor descriptor; + + static DanmakuReflection() { + byte[] descriptorData = global::System.Convert.FromBase64String( + string.Concat( + "Cg1EYW5tYWt1LnByb3RvIsgBCgtEYW5tYWt1RWxlbRIKCgJpZBgBIAEoAxIQ", + "Cghwcm9ncmVzcxgCIAEoBRIMCgRtb2RlGAMgASgFEhAKCGZvbnRzaXplGAQg", + "ASgFEg0KBWNvbG9yGAUgASgNEg8KB21pZEhhc2gYBiABKAkSDwoHY29udGVu", + "dBgHIAEoCRINCgVjdGltZRgIIAEoAxIOCgZ3ZWlnaHQYCSABKAUSDgoGYWN0", + "aW9uGAogASgJEgwKBHBvb2wYCyABKAUSDQoFaWRTdHIYDCABKAkiLwoQRG1T", + "ZWdNb2JpbGVSZXBseRIbCgVlbGVtcxgBIAMoCzIMLkRhbm1ha3VFbGVtYgZw", + "cm90bzM=")); + descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, + new pbr::FileDescriptor[] { }, + new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { + new pbr::GeneratedClrTypeInfo(typeof(global::DanmakuElem), global::DanmakuElem.Parser, new[]{ "Id", "Progress", "Mode", "Fontsize", "Color", "MidHash", "Content", "Ctime", "Weight", "Action", "Pool", "IdStr" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::DmSegMobileReply), global::DmSegMobileReply.Parser, new[]{ "Elems" }, null, null, null, null) + })); + } + #endregion + +} +#region Messages +public sealed partial class DanmakuElem : pb::IMessage +#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage +#endif +{ + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new DanmakuElem()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::DanmakuReflection.Descriptor.MessageTypes[0]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public DanmakuElem() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public DanmakuElem(DanmakuElem other) : this() { + id_ = other.id_; + progress_ = other.progress_; + mode_ = other.mode_; + fontsize_ = other.fontsize_; + color_ = other.color_; + midHash_ = other.midHash_; + content_ = other.content_; + ctime_ = other.ctime_; + weight_ = other.weight_; + action_ = other.action_; + pool_ = other.pool_; + idStr_ = other.idStr_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public DanmakuElem Clone() { + return new DanmakuElem(this); + } + + /// Field number for the "id" field. + public const int IdFieldNumber = 1; + private long id_; + /// + ///弹幕dmID + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long Id { + get { return id_; } + set { + id_ = value; + } + } + + /// Field number for the "progress" field. + public const int ProgressFieldNumber = 2; + private int progress_; + /// + ///出现时间 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int Progress { + get { return progress_; } + set { + progress_ = value; + } + } + + /// Field number for the "mode" field. + public const int ModeFieldNumber = 3; + private int mode_; + /// + ///弹幕类型 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int Mode { + get { return mode_; } + set { + mode_ = value; + } + } + + /// Field number for the "fontsize" field. + public const int FontsizeFieldNumber = 4; + private int fontsize_; + /// + ///文字大小 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int Fontsize { + get { return fontsize_; } + set { + fontsize_ = value; + } + } + + /// Field number for the "color" field. + public const int ColorFieldNumber = 5; + private uint color_; + /// + ///弹幕颜色 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public uint Color { + get { return color_; } + set { + color_ = value; + } + } + + /// Field number for the "midHash" field. + public const int MidHashFieldNumber = 6; + private string midHash_ = ""; + /// + ///发送者UID的HASH + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string MidHash { + get { return midHash_; } + set { + midHash_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "content" field. + public const int ContentFieldNumber = 7; + private string content_ = ""; + /// + ///弹幕内容 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string Content { + get { return content_; } + set { + content_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "ctime" field. + public const int CtimeFieldNumber = 8; + private long ctime_; + /// + ///发送时间 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long Ctime { + get { return ctime_; } + set { + ctime_ = value; + } + } + + /// Field number for the "weight" field. + public const int WeightFieldNumber = 9; + private int weight_; + /// + ///权重 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int Weight { + get { return weight_; } + set { + weight_ = value; + } + } + + /// Field number for the "action" field. + public const int ActionFieldNumber = 10; + private string action_ = ""; + /// + ///动作? + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string Action { + get { return action_; } + set { + action_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "pool" field. + public const int PoolFieldNumber = 11; + private int pool_; + /// + ///弹幕池 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int Pool { + get { return pool_; } + set { + pool_ = value; + } + } + + /// Field number for the "idStr" field. + public const int IdStrFieldNumber = 12; + private string idStr_ = ""; + /// + ///弹幕dmID(字串形式) + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string IdStr { + get { return idStr_; } + set { + idStr_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as DanmakuElem); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(DanmakuElem other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Id != other.Id) return false; + if (Progress != other.Progress) return false; + if (Mode != other.Mode) return false; + if (Fontsize != other.Fontsize) return false; + if (Color != other.Color) return false; + if (MidHash != other.MidHash) return false; + if (Content != other.Content) return false; + if (Ctime != other.Ctime) return false; + if (Weight != other.Weight) return false; + if (Action != other.Action) return false; + if (Pool != other.Pool) return false; + if (IdStr != other.IdStr) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + if (Id != 0L) hash ^= Id.GetHashCode(); + if (Progress != 0) hash ^= Progress.GetHashCode(); + if (Mode != 0) hash ^= Mode.GetHashCode(); + if (Fontsize != 0) hash ^= Fontsize.GetHashCode(); + if (Color != 0) hash ^= Color.GetHashCode(); + if (MidHash.Length != 0) hash ^= MidHash.GetHashCode(); + if (Content.Length != 0) hash ^= Content.GetHashCode(); + if (Ctime != 0L) hash ^= Ctime.GetHashCode(); + if (Weight != 0) hash ^= Weight.GetHashCode(); + if (Action.Length != 0) hash ^= Action.GetHashCode(); + if (Pool != 0) hash ^= Pool.GetHashCode(); + if (IdStr.Length != 0) hash ^= IdStr.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (Id != 0L) { + output.WriteRawTag(8); + output.WriteInt64(Id); + } + if (Progress != 0) { + output.WriteRawTag(16); + output.WriteInt32(Progress); + } + if (Mode != 0) { + output.WriteRawTag(24); + output.WriteInt32(Mode); + } + if (Fontsize != 0) { + output.WriteRawTag(32); + output.WriteInt32(Fontsize); + } + if (Color != 0) { + output.WriteRawTag(40); + output.WriteUInt32(Color); + } + if (MidHash.Length != 0) { + output.WriteRawTag(50); + output.WriteString(MidHash); + } + if (Content.Length != 0) { + output.WriteRawTag(58); + output.WriteString(Content); + } + if (Ctime != 0L) { + output.WriteRawTag(64); + output.WriteInt64(Ctime); + } + if (Weight != 0) { + output.WriteRawTag(72); + output.WriteInt32(Weight); + } + if (Action.Length != 0) { + output.WriteRawTag(82); + output.WriteString(Action); + } + if (Pool != 0) { + output.WriteRawTag(88); + output.WriteInt32(Pool); + } + if (IdStr.Length != 0) { + output.WriteRawTag(98); + output.WriteString(IdStr); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (Id != 0L) { + output.WriteRawTag(8); + output.WriteInt64(Id); + } + if (Progress != 0) { + output.WriteRawTag(16); + output.WriteInt32(Progress); + } + if (Mode != 0) { + output.WriteRawTag(24); + output.WriteInt32(Mode); + } + if (Fontsize != 0) { + output.WriteRawTag(32); + output.WriteInt32(Fontsize); + } + if (Color != 0) { + output.WriteRawTag(40); + output.WriteUInt32(Color); + } + if (MidHash.Length != 0) { + output.WriteRawTag(50); + output.WriteString(MidHash); + } + if (Content.Length != 0) { + output.WriteRawTag(58); + output.WriteString(Content); + } + if (Ctime != 0L) { + output.WriteRawTag(64); + output.WriteInt64(Ctime); + } + if (Weight != 0) { + output.WriteRawTag(72); + output.WriteInt32(Weight); + } + if (Action.Length != 0) { + output.WriteRawTag(82); + output.WriteString(Action); + } + if (Pool != 0) { + output.WriteRawTag(88); + output.WriteInt32(Pool); + } + if (IdStr.Length != 0) { + output.WriteRawTag(98); + output.WriteString(IdStr); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + if (Id != 0L) { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(Id); + } + if (Progress != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(Progress); + } + if (Mode != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(Mode); + } + if (Fontsize != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(Fontsize); + } + if (Color != 0) { + size += 1 + pb::CodedOutputStream.ComputeUInt32Size(Color); + } + if (MidHash.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(MidHash); + } + if (Content.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Content); + } + if (Ctime != 0L) { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(Ctime); + } + if (Weight != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(Weight); + } + if (Action.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Action); + } + if (Pool != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(Pool); + } + if (IdStr.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(IdStr); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(DanmakuElem other) { + if (other == null) { + return; + } + if (other.Id != 0L) { + Id = other.Id; + } + if (other.Progress != 0) { + Progress = other.Progress; + } + if (other.Mode != 0) { + Mode = other.Mode; + } + if (other.Fontsize != 0) { + Fontsize = other.Fontsize; + } + if (other.Color != 0) { + Color = other.Color; + } + if (other.MidHash.Length != 0) { + MidHash = other.MidHash; + } + if (other.Content.Length != 0) { + Content = other.Content; + } + if (other.Ctime != 0L) { + Ctime = other.Ctime; + } + if (other.Weight != 0) { + Weight = other.Weight; + } + if (other.Action.Length != 0) { + Action = other.Action; + } + if (other.Pool != 0) { + Pool = other.Pool; + } + if (other.IdStr.Length != 0) { + IdStr = other.IdStr; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + Id = input.ReadInt64(); + break; + } + case 16: { + Progress = input.ReadInt32(); + break; + } + case 24: { + Mode = input.ReadInt32(); + break; + } + case 32: { + Fontsize = input.ReadInt32(); + break; + } + case 40: { + Color = input.ReadUInt32(); + break; + } + case 50: { + MidHash = input.ReadString(); + break; + } + case 58: { + Content = input.ReadString(); + break; + } + case 64: { + Ctime = input.ReadInt64(); + break; + } + case 72: { + Weight = input.ReadInt32(); + break; + } + case 82: { + Action = input.ReadString(); + break; + } + case 88: { + Pool = input.ReadInt32(); + break; + } + case 98: { + IdStr = input.ReadString(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + Id = input.ReadInt64(); + break; + } + case 16: { + Progress = input.ReadInt32(); + break; + } + case 24: { + Mode = input.ReadInt32(); + break; + } + case 32: { + Fontsize = input.ReadInt32(); + break; + } + case 40: { + Color = input.ReadUInt32(); + break; + } + case 50: { + MidHash = input.ReadString(); + break; + } + case 58: { + Content = input.ReadString(); + break; + } + case 64: { + Ctime = input.ReadInt64(); + break; + } + case 72: { + Weight = input.ReadInt32(); + break; + } + case 82: { + Action = input.ReadString(); + break; + } + case 88: { + Pool = input.ReadInt32(); + break; + } + case 98: { + IdStr = input.ReadString(); + break; + } + } + } + } + #endif + +} + +public sealed partial class DmSegMobileReply : pb::IMessage +#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage +#endif +{ + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new DmSegMobileReply()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor { + get { return global::DanmakuReflection.Descriptor.MessageTypes[1]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public DmSegMobileReply() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public DmSegMobileReply(DmSegMobileReply other) : this() { + elems_ = other.elems_.Clone(); + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public DmSegMobileReply Clone() { + return new DmSegMobileReply(this); + } + + /// Field number for the "elems" field. + public const int ElemsFieldNumber = 1; + private static readonly pb::FieldCodec _repeated_elems_codec + = pb::FieldCodec.ForMessage(10, global::DanmakuElem.Parser); + private readonly pbc::RepeatedField elems_ = new pbc::RepeatedField(); + /// + ///弹幕条目 + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public pbc::RepeatedField Elems { + get { return elems_; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) { + return Equals(other as DmSegMobileReply); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(DmSegMobileReply other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if(!elems_.Equals(other.elems_)) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() { + int hash = 1; + hash ^= elems_.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + elems_.WriteTo(output, _repeated_elems_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + elems_.WriteTo(ref output, _repeated_elems_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() { + int size = 0; + size += elems_.CalculateSize(_repeated_elems_codec); + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(DmSegMobileReply other) { + if (other == null) { + return; + } + elems_.Add(other.elems_); + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + elems_.AddEntriesFrom(input, _repeated_elems_codec); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + elems_.AddEntriesFrom(ref input, _repeated_elems_codec); + break; + } + } + } + } + #endif + +} + +#endregion + + +#endregion Designer generated code diff --git a/src/Core/api/danmaku/proto/Danmaku.proto b/src/Core/api/danmaku/proto/Danmaku.proto new file mode 100644 index 0000000..71f4f54 --- /dev/null +++ b/src/Core/api/danmaku/proto/Danmaku.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +message DanmakuElem { + int64 id = 1; //弹幕dmID + int32 progress = 2; //出现时间 + int32 mode = 3; //弹幕类型 + int32 fontsize = 4; //文字大小 + uint32 color = 5; //弹幕颜色 + string midHash = 6; //发送者UID的HASH + string content = 7; //弹幕内容 + int64 ctime = 8; //发送时间 + int32 weight = 9; //权重 + string action = 10; //动作? + int32 pool = 11; //弹幕池 + string idStr = 12; //弹幕dmID(字串形式) +} + +message DmSegMobileReply { + repeated DanmakuElem elems = 1; //弹幕条目 +} diff --git a/src/Core/api/fileDownload/FileDownloadConfig.cs b/src/Core/api/fileDownload/FileDownloadConfig.cs new file mode 100644 index 0000000..7bea76d --- /dev/null +++ b/src/Core/api/fileDownload/FileDownloadConfig.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace Core.api.fileDownload +{ + [Serializable] + internal class FileDownloadConfig + { + public long DownloadSize { get; set; } + public long TotalSize { get; set; } + public Queue DownloadQueue { get; set; } + + /// + /// 保存文件下载的配置信息,包括总字节数、已下载字节数、下载队列信息 + /// + /// + /// + /// + /// + public static void SaveConfig(string configFile, long downloadSize, long totalSize, Queue downloadQueue) + { + var config = new FileDownloadConfig() + { + DownloadSize = downloadSize, + TotalSize = totalSize, + DownloadQueue = downloadQueue + }; + Utils.WriteObjectToDisk(configFile, config); + } + + /// + /// 读取文件下载的配置信息,包括总字节数、已下载字节数、下载队列信息 + /// + /// + /// + public static FileDownloadConfig ReadConfig(string configFile) + { + return (FileDownloadConfig)Utils.ReadObjectFromDisk(configFile); + } + + } +} diff --git a/src/Core/api/fileDownload/FileDownloadEvent.cs b/src/Core/api/fileDownload/FileDownloadEvent.cs new file mode 100644 index 0000000..d641257 --- /dev/null +++ b/src/Core/api/fileDownload/FileDownloadEvent.cs @@ -0,0 +1,24 @@ +namespace Core.api.fileDownload +{ + public class FileDownloadEvent + { + + //private long _downloadSize; + //private long _totalSize; + public FileDownloadEvent() { } + + // 每秒下载的速度 B/s + public float Speed { get; set; } + + public float Percent + { + get { return DownloadSize * 100.0f / TotalSize; } + } + + public long DownloadSize { get; set; } + + public long TotalSize { get; set; } + + + } +} diff --git a/src/Core/api/fileDownload/FileDownloadHelper.cs b/src/Core/api/fileDownload/FileDownloadHelper.cs new file mode 100644 index 0000000..de0ad49 --- /dev/null +++ b/src/Core/api/fileDownload/FileDownloadHelper.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Core.api.fileDownload +{ + public class FileDownloadHelper + { + private const int DOWNLOAD_BUFFER_SIZE = 102400; //每次下载量 100KB + private const int THREAD_BUFFER_SIZE = 10485760; //每个线程每次最大下载大小 设为10MB 不能太小 否则会创建太多的request对象 + public delegate void ErrorMakedEventHandler(string errorstring); + public event ErrorMakedEventHandler ErrorMakedEvent; + public delegate void DownloadEventHandler(FileDownloadEvent e); + public event DownloadEventHandler DownloadEvent; + public delegate void StopEventHandler(); + public event StopEventHandler StopEvent; + private object locker = new object(); + private long downloadSize = 0; //已经下载的字节 + private CancellationTokenSource cancelTokenSource = new CancellationTokenSource(); + private ManualResetEvent mre = new ManualResetEvent(true); //初始化不等待 + private AutoResetEvent eventFinished = new AutoResetEvent(false); + + private void ThreadWork(string url, FileStream fs, Queue downQueue) + { + mre.WaitOne(); + if (cancelTokenSource.IsCancellationRequested) + { + return; + } + + Monitor.Enter(downQueue); + if (downQueue.Count == 0) + { + return; + } + ThreadDownloadInfo downInfo = downQueue.Dequeue(); + Monitor.Exit(downQueue); + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.AddRange(downInfo.StartLength); //设置Range值 + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + Stream ns = response.GetResponseStream(); + byte[] nbytes = new byte[DOWNLOAD_BUFFER_SIZE]; + int temp = 0; + int nReadSize = 0; + byte[] buffer = new byte[downInfo.Length]; //文件写入缓冲 + nReadSize = ns.Read(nbytes, 0, Math.Min(DOWNLOAD_BUFFER_SIZE, downInfo.Length)); + while (temp < downInfo.Length) + { + mre.WaitOne(); + Buffer.BlockCopy(nbytes, 0, buffer, temp, nReadSize); + lock (locker) + { + downloadSize += nReadSize; + } + temp += nReadSize; + nReadSize = ns.Read(nbytes, 0, Math.Min(DOWNLOAD_BUFFER_SIZE, downInfo.Length - temp)); + } + + lock (locker) + { + fs.Seek(downInfo.StartLength, SeekOrigin.Begin); + fs.Write(buffer, 0, buffer.Length); + } + + ns.Close(); + ThreadWork(url, fs, downQueue); + } + + public async void StartDownload(FileDownloadInfo info) + { + if (string.IsNullOrEmpty(info.DownLoadUrl)) + { throw new Exception("下载地址不能为空!"); } + if (!info.DownLoadUrl.ToLower().StartsWith("http://") || !info.DownLoadUrl.ToLower().StartsWith("https://")) + { throw new Exception("非法的下载地址!"); } + + downloadSize = 0; + await Task.Run(() => + { + try + { + long totalSize = 0; + long threadInitedLength = 0; //分配线程任务的下载量 + + #region 获取文件信息 + //打开网络连接 + HttpWebRequest initRequest = (HttpWebRequest)WebRequest.Create(info.DownLoadUrl); + WebResponse initResponse = initRequest.GetResponse(); + FileInfo fileMsg = FileInfo.GetFileMessage(initResponse); + totalSize = fileMsg.Length; + if ((!string.IsNullOrEmpty(fileMsg.FileName)) && info.LocalSaveFolder != null) + { + info.SavePath = Path.Combine(info.LocalSaveFolder, fileMsg.FileName); + } + //ReaderWriterLock readWriteLock = new ReaderWriterLock(); + #endregion + + #region 读取配置文件 + string configPath = info.SavePath.Substring(0, info.SavePath.LastIndexOf(".")) + ".cfg"; + FileDownloadConfig initInfo = null; + if (File.Exists(configPath) && (info.IsNew == false)) + { + initInfo = FileDownloadConfig.ReadConfig(configPath); + downloadSize = (long)initInfo.DownloadSize; + totalSize = (long)initInfo.TotalSize; + } + #endregion + + #region 计算速度 + //Stopwatch MyStopWatch = new Stopwatch(); + long lastDownloadSize = 0; //上次下载量 + bool isSendCompleteEvent = false; //是否完成 + Timer timer = new Timer(new TimerCallback((o) => + { + if (!isSendCompleteEvent) + { + FileDownloadEvent e = new FileDownloadEvent(); + e.DownloadSize = downloadSize; + e.TotalSize = totalSize; + if (totalSize > 0 && downloadSize == totalSize) + { + e.Speed = 0; + isSendCompleteEvent = true; + eventFinished.Set(); + } + else + { + e.Speed = downloadSize - lastDownloadSize; + lastDownloadSize = downloadSize; //更新上次下载量 + } + + DownloadEvent(e); + } + + }), null, 0, 1000); + #endregion + + string tempPath = info.SavePath.Substring(0, info.SavePath.LastIndexOf(".")) + ".dat"; + + #region 多线程下载 + //分配下载队列 + Queue downQueue = null; + if (initInfo == null || info.IsNew) + { + downQueue = new Queue(); //下载信息队列 + while (threadInitedLength < totalSize) + { + ThreadDownloadInfo downInfo = new ThreadDownloadInfo(); + downInfo.StartLength = threadInitedLength; + downInfo.Length = (int)Math.Min(Math.Min(THREAD_BUFFER_SIZE, totalSize - threadInitedLength), totalSize / info.ThreadCount); //下载量 + downQueue.Enqueue(downInfo); + threadInitedLength += downInfo.Length; + } + } + else + { + downQueue = initInfo.DownloadQueue; + } + + FileStream fs = new FileStream(tempPath, FileMode.OpenOrCreate); + fs.SetLength(totalSize); + int threads = info.ThreadCount; + + for (int i = 0; i < info.ThreadCount; i++) + { + ThreadPool.QueueUserWorkItem((state) => + { + ThreadWork(info.DownLoadUrl, fs, downQueue); + if (Interlocked.Decrement(ref threads) == 0) + { + (state as AutoResetEvent).Set(); + } + }, eventFinished); + } + + //等待所有线程完成 + eventFinished.WaitOne(); + if (fs != null) + { + fs.Close(); + } + fs = null; + if (File.Exists(info.SavePath)) + { + File.Delete(info.SavePath); + } + + if (downloadSize == totalSize) + { + File.Move(tempPath, info.SavePath); + File.Delete(configPath); + } + + if (cancelTokenSource.IsCancellationRequested && StopEvent != null) + { + StopEvent(); + //保存配置文件 + FileDownloadConfig.SaveConfig(configPath, downloadSize, totalSize, downQueue); + } + #endregion + + } + catch (Exception ex) + { + ErrorMakedEvent?.Invoke(ex.Message); + } + }); + + } + + public void Stop() + { + cancelTokenSource.Cancel(); + } + + public void Suspend() + { + mre.Reset(); + } + + public void Resume() + { + mre.Set(); + } + + //#region 获取文件信息 + //public class FileMessage + //{ + // public long Length { get; set; } + // public string FileName { get; set; } + //} + + //public FileMessage GetFileMessage(WebResponse response) + //{ + // FileMessage info = new FileMessage(); + + // if (response.Headers["Content-Disposition"] != null) + // { + // Match match = Regex.Match(response.Headers["Content-Disposition"], "filename=(.*)"); + // if (match.Success) + // { + // string fileName = match.Groups[1].Value; + // Encoding encoding = Encoding.UTF8; + // string str = (response as HttpWebResponse).CharacterSet; + // if (!string.IsNullOrEmpty(str)) + // { + // encoding = Encoding.GetEncoding(str); + // } + // info.FileName = System.Web.HttpUtility.UrlDecode(fileName, encoding); + // } + // } + + // if (response.Headers["Content-Length"] != null) + // { + // info.Length = long.Parse(response.Headers.Get("Content-Length")); + // } + // else + // { + // info.Length = response.ContentLength; + // } + + // return info; + //} + + + //#endregion + + //private void SaveConfig(string configPath, long downloadSize, long totalSize, Queue downQueue) + //{ + // stringBuilder sb = new stringBuilder(); + // sb.Append(downloadSize + ";" + totalSize + ";"); + // foreach (ThreadDownloadInfo info in downQueue) + // { + // sb.Append("(" + info.startLength + ","); + // sb.Append(info.length + ");"); + // } + + // byte[] buffer = Encoding.UTF8.GetBytes(sb.Tostring()); + // string str = Convert.ToBase64string(buffer); + // File.WriteAllText(configPath, str); + //} + + //private List ReadConfig(string configPath) + //{ + // List list = new List(); + // string str = File.ReadAllText(configPath); + // byte[] buffer = Convert.FromBase64string(str); + // str =Encoding.UTF8.Getstring(buffer); + // lock (locker) + // { + // string[] split = str.Split(';'); + // long downloadSize = Convert.ToInt64(split[0]); + // long totalSize = Convert.ToInt64(split[1]); + // Queue downQueue = new Queue(); //下载信息队列 + // foreach (Match match in Regex.Matches(str, "\\((\\d+),(\\d+)\\);")) + // { + // ThreadDownloadInfo downInfo = new ThreadDownloadInfo(); + // downInfo.startLength = Convert.ToInt64(match.Groups[1].Value); + // downInfo.length = Convert.ToInt32(match.Groups[2].Value); + // downQueue.Enqueue(downInfo); + // } + + // list.Add(downloadSize); + // list.Add(totalSize); + // list.Add(downQueue); + // } + + // return list; + //} + + + } + +} diff --git a/src/Core/api/fileDownload/FileDownloadInfo.cs b/src/Core/api/fileDownload/FileDownloadInfo.cs new file mode 100644 index 0000000..e40cbc6 --- /dev/null +++ b/src/Core/api/fileDownload/FileDownloadInfo.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; + +namespace Core.api.fileDownload +{ + public class FileDownloadInfo + { + + + // 下载地址 + public string DownLoadUrl { get; set; } + + private string _localSaveFolder; + // 本地保存路径 + public string LocalSaveFolder + { + get { return _localSaveFolder; } + set + { _localSaveFolder = value; } + } + + // 包含文件名的完整保存路径 + private string _savePath; + public string SavePath + { + get + { + if (_savePath == null) + { + if (_localSaveFolder == null) + { + throw new Exception("本地保存路径不能为空"); + } + + _savePath = Path.Combine(_localSaveFolder, Path.GetFileName(DownLoadUrl)); + + if (File.Exists(_savePath)) + { + if (IsNew) + { + if (IsOver) + { + File.Delete(_savePath); + } + else + { + _savePath = _savePath.Substring(0, _savePath.LastIndexOf(".")) + "(2)" + _savePath.Substring(_savePath.LastIndexOf(".")); + } + } + } + } + + return _savePath; + } + set + { + _savePath = value; + } + } + + private int _threadCount = 1; + ////// 线程数 + public int ThreadCount + { + get { return _threadCount; } + set { _threadCount = value; } + } + + ////// 是否覆盖已存在的文件 + public bool IsOver { get; set; } + + ////// 是否重新下载 + public bool IsNew { get; set; } + + + } +} diff --git a/src/Core/api/fileDownload/FileInfo.cs b/src/Core/api/fileDownload/FileInfo.cs new file mode 100644 index 0000000..934f84d --- /dev/null +++ b/src/Core/api/fileDownload/FileInfo.cs @@ -0,0 +1,43 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Core.api.fileDownload +{ + internal class FileInfo + { + public long Length { get; set; } + public string FileName { get; set; } + + /// + /// 获取远程文件的信息 + /// + /// + /// + public static FileInfo GetFileMessage(WebResponse response) + { + FileInfo info = new FileInfo(); + + if (response.Headers["Content-Disposition"] != null) + { + Match match = Regex.Match(response.Headers["Content-Disposition"], "filename=(.*)"); + if (match.Success) + { + string fileName = match.Groups[1].Value; + info.FileName = WebUtility.UrlDecode(fileName); + } + } + + if (response.Headers["Content-Length"] != null) + { + info.Length = long.Parse(response.Headers.Get("Content-Length")); + } + else + { + info.Length = response.ContentLength; + } + + return info; + } + + } +} diff --git a/src/Core/api/fileDownload/ThreadDownloadInfo.cs b/src/Core/api/fileDownload/ThreadDownloadInfo.cs new file mode 100644 index 0000000..f1e4364 --- /dev/null +++ b/src/Core/api/fileDownload/ThreadDownloadInfo.cs @@ -0,0 +1,11 @@ +namespace Core.api.fileDownload +{ + internal class ThreadDownloadInfo + { + // 开始位置 + public long StartLength { get; set; } + + // 下载量 + public int Length { get; set; } + } +} diff --git a/src/Core/api/login/Login.cs b/src/Core/api/login/Login.cs new file mode 100644 index 0000000..114ff90 --- /dev/null +++ b/src/Core/api/login/Login.cs @@ -0,0 +1,107 @@ +using Core.entity2.login; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Core.api.login +{ + public class Login + { + private static Login instance; + + /// + /// 获取Login实例 + /// + /// + public static Login GetInstance() + { + if (instance == null) + { + instance = new Login(); + } + return instance; + } + + /// + /// 隐藏Login()方法,必须使用单例模式 + /// + private Login() { } + + /// + /// 申请二维码URL及扫码密钥(web端) + /// + /// + public LoginUrlOrigin GetLoginUrl() + { + string getLoginUrl = "https://passport.bilibili.com/qrcode/getLoginUrl"; + string referer = "https://passport.bilibili.com/login"; + string response = Utils.RequestWeb(getLoginUrl, referer); + + try + { + var loginUrl = JsonConvert.DeserializeObject(response); + return loginUrl; + } + catch (Exception e) + { + Console.WriteLine("GetLoginUrl()发生异常: {0}", e); + return null; + } + } + + /// + /// 使用扫码登录(web端) + /// + /// + /// + /// + public LoginStatus GetLoginStatus(string oauthKey, string goUrl = "https://www.bilibili.com") + { + string url = "https://passport.bilibili.com/qrcode/getLoginInfo"; + string referer = "https://passport.bilibili.com/login"; + + Dictionary parameters = new Dictionary + { + { "oauthKey", oauthKey }, + { "gourl", goUrl } + }; + + string response = Utils.RequestWeb(url, referer, "POST", parameters); + var loginInfo = new LoginStatus(); + + try + { + if (response.Contains("\"code\":0") || response.Contains("\"code\": 0")) + { + var ready = JsonConvert.DeserializeObject(response); + if (ready == null) + { return null; } + + loginInfo.Code = ready.Code; + loginInfo.Status = ready.Status; + loginInfo.Message = "登录成功"; + loginInfo.Url = ready.Data.Url; + } + else + { + var scanning = JsonConvert.DeserializeObject(response); + if (scanning == null) + { return null; } + + loginInfo.Code = scanning.Data; + loginInfo.Status = scanning.Status; + loginInfo.Message = scanning.Message; + loginInfo.Url = ""; + } + + return loginInfo; + } + catch (Exception e) + { + Console.WriteLine("GetLoginInfo()发生异常: {0}", e); + return null; + } + } + + } +} diff --git a/src/Core/api/login/LoginHelper.cs b/src/Core/api/login/LoginHelper.cs new file mode 100644 index 0000000..7d23378 --- /dev/null +++ b/src/Core/api/login/LoginHelper.cs @@ -0,0 +1,231 @@ +using Core.settings; +using System; +using System.Drawing; +using System.IO; +using System.Net; +using System.Windows.Media.Imaging; + +namespace Core.api.login +{ + public class LoginHelper + { + private static LoginHelper instance; + + /// + /// 获取LoginHelper实例 + /// + /// + public static LoginHelper GetInstance() + { + if (instance == null) + { + instance = new LoginHelper(); + } + return instance; + } + + /// + /// 隐藏LoginHelper()方法,必须使用单例模式 + /// + private LoginHelper() { } + + private static readonly string LOCAL_LOGIN_INFO = Common.ConfigPath + "Login"; + private static readonly string SecretKey = "Ps*rB$TGaM#&JvOe"; // 16位密码,ps:密码位数没有限制,可任意设置 + + /// + /// 获得登录二维码 + /// + /// + public BitmapImage GetLoginQRCode() + { + try + { + string loginUrl = Login.GetInstance().GetLoginUrl().Data.Url; + return GetLoginQRCode(loginUrl); + } + catch (Exception e) + { + Console.WriteLine("GetLoginQRCode()发生异常: {0}", e); + return null; + } + } + + /// + /// 根据输入url生成二维码 + /// + /// + /// + public BitmapImage GetLoginQRCode(string url) + { + // 设置的参数影响app能否成功扫码 + Bitmap qrCode = Utils.EncodeQRCode(url, 10, 10, null, 0, 0, false); + + MemoryStream ms = new MemoryStream(); + qrCode.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp); + byte[] bytes = ms.GetBuffer(); + ms.Close(); + + BitmapImage image = new BitmapImage(); + image.BeginInit(); + image.StreamSource = new MemoryStream(bytes); + image.EndInit(); + return image; + } + + /// + /// 保存登录的cookies到文件 + /// + /// + /// + public bool SaveLoginInfoCookies(string url) + { + if (!Directory.Exists(Common.ConfigPath)) + { + Directory.CreateDirectory(Common.ConfigPath); + } + + string tempFile = LOCAL_LOGIN_INFO + "-" + Guid.NewGuid().ToString("N"); + CookieContainer cookieContainer = Utils.ParseCookie(url); + + bool isSucceed = Utils.WriteCookiesToDisk(tempFile, cookieContainer); + if (isSucceed) + { + // 加密密钥,增加机器码 + string password = SecretKey + MachineCode.GetMachineCodeString(); + + try + { + Encryptor.EncryptFile(tempFile, LOCAL_LOGIN_INFO, password); + } + catch (Exception e) + { + Console.WriteLine("SaveLoginInfoCookies()发生异常: {0}", e); + return false; + } + } + + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return isSucceed; + } + + /// + /// 获得登录的cookies + /// + /// + public CookieContainer GetLoginInfoCookies() + { + string tempFile = LOCAL_LOGIN_INFO + "-" + Guid.NewGuid().ToString("N"); + + if (File.Exists(LOCAL_LOGIN_INFO)) + { + try + { + // 加密密钥,增加机器码 + string password = SecretKey + MachineCode.GetMachineCodeString(); + Encryptor.DecryptFile(LOCAL_LOGIN_INFO, tempFile, password); + } + catch (CryptoHelpException e) + { + Console.WriteLine("GetLoginInfoCookies()发生异常: {0}", e); + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return null; + } + catch (FileNotFoundException e) + { + Console.WriteLine("GetLoginInfoCookies()发生异常: {0}", e); + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return null; + } + catch (DirectoryNotFoundException e) + { + Console.WriteLine("GetLoginInfoCookies()发生异常: {0}", e); + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return null; + } + catch (Exception e) + { + Console.WriteLine("GetLoginInfoCookies()发生异常: {0}", e); + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return null; + } + } + else { return null; } + + CookieContainer cookies = Utils.ReadCookiesFromDisk(tempFile); + + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return cookies; + } + + /// + /// 返回登录信息的cookies的字符串 + /// + /// + public string GetLoginInfoCookiesString() + { + var cookieContainer = GetLoginInfoCookies(); + if (cookieContainer == null) + { + return ""; + } + + var cookies = Utils.GetAllCookies(cookieContainer); + + string cookie = string.Empty; + foreach (var item in cookies) + { + cookie += item.ToString() + ";"; + } + return cookie.TrimEnd(';'); + } + + /// + /// 注销登录 + /// + /// + public bool Logout() + { + if (File.Exists(LOCAL_LOGIN_INFO)) + { + try + { + File.Delete(LOCAL_LOGIN_INFO); + + Settings.GetInstance().SetUserInfo(new UserInfoForSetting + { + Mid = -1, + Name = "", + IsLogin = false, + IsVip = false + }); + return true; + } + catch (IOException e) + { + Console.WriteLine("Logout()发生异常: {0}", e); + return false; + } + } + return false; + } + + } +} diff --git a/src/Core/api/users/UserSpace.cs b/src/Core/api/users/UserSpace.cs index 84421b8..4661fda 100644 --- a/src/Core/api/users/UserSpace.cs +++ b/src/Core/api/users/UserSpace.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace Core.api.users { @@ -99,24 +100,28 @@ namespace Core.api.users /// 视频分区 /// 搜索关键词 /// - public List GetAllPublication(long mid, PublicationOrder order = PublicationOrder.PUBDATE, int tid = 0, string keyword = "") + public async Task> GetAllPublication(long mid, int tid = 0, PublicationOrder order = PublicationOrder.PUBDATE, string keyword = "") { List result = new List(); - int i = 0; - while (true) + await Task.Run(() => { - i++; - int ps = 100; + int i = 0; + while (true) + { + i++; + int ps = 100; - var data = GetPublication(mid, i, ps, tid, order, keyword); - //if (data == null) { continue; } + var data = GetPublication(mid, i, ps, tid, order, keyword); + //if (data == null) { continue; } - if (data == null || data.Vlist == null || data.Vlist.Count == 0) - { break; } + if (data == null || data.Vlist == null || data.Vlist.Count == 0) + { break; } + + result.AddRange(data.Vlist); + } + }); - result.AddRange(data.Vlist); - } return result; } @@ -138,7 +143,20 @@ namespace Core.api.users try { - var spacePublication = JsonConvert.DeserializeObject(response); + // 忽略play的值为“--”时的类型错误 + var settings = new JsonSerializerSettings + { + Error = (sender, args) => + { + if (Equals(args.ErrorContext.Member, "play") && + args.ErrorContext.OriginalObject.GetType() == typeof(SpacePublicationListVideo)) + { + args.ErrorContext.Handled = true; + } + } + }; + + var spacePublication = JsonConvert.DeserializeObject(response, settings); if (spacePublication == null || spacePublication.Data == null) { return null; } return spacePublication.Data.List; } @@ -440,7 +458,7 @@ namespace Core.api.users /// public BangumiFollowData GetBangumiFollow(long mid, BangumiType type, int pn, int ps) { - string url = $"https://api.bilibili.com/x/space/bangumi/follow/list?vmid={mid}&type={type.ToString("D")}&pn={pn}&ps={ps}"; + string url = $"https://api.bilibili.com/x/space/bangumi/follow/list?vmid={mid}&type={type:D}&pn={pn}&ps={ps}"; string referer = "https://www.bilibili.com"; string response = Utils.RequestWeb(url, referer); diff --git a/src/Core/aria2cNet/Aria2c.cs b/src/Core/aria2cNet/Aria2c.cs new file mode 100644 index 0000000..6067942 --- /dev/null +++ b/src/Core/aria2cNet/Aria2c.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +// 代码来源 +// https://blog.csdn.net/haibin258/article/details/87905628 + +// Aria2详细信息请看 +// https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface + +// 废弃 +namespace Core.aria2cNet +{ + //定义一个类用于转码成JSON字符串发送给Aria2 json-rpc接口 + public class JsonClass + { + public string Jsonrpc { get; set; } + public string Id { get; set; } + public string Method { get; set; } + public List Params { get; set; } + } + + public class Aria2 + { + public static ClientWebSocket webSocket; // 用于连接到Aria2Rpc的客户端 + public static CancellationToken cancellationToken; // 传播有关应取消操作的通知 + + /// + /// 连接Aria2Rpc服务器 + /// + /// + /// + public static async Task ConnectServer(string uri) + { + webSocket = new ClientWebSocket(); // 用于连接到Aria2Rpc的客户端 + cancellationToken = new CancellationToken(); // 传播有关应取消操作的通知 + bool status = false; //储存连接状态 + try + { + //连接服务器 + await webSocket.ConnectAsync(new Uri(uri), cancellationToken); + } + catch + { + status = false; + } + //检查连接是否成功 + if (webSocket.State == WebSocketState.Open) + { + status = true; + } + return status; + } + + + /// + /// JsonClass类转为json格式 + /// + /// + /// + private static string ToJson(JsonClass json) + { + string str = "{" + "\"jsonrpc\":\"" + json.Jsonrpc + "\",\"id\":\"" + json.Id + "\",\"method\":\"" + json.Method + "\",\"params\":["; + for (int i = 0; i < json.Params.Count; i++) + { + if (json.Method == "aria2.addUri") + { + str += "[\"" + json.Params[i] + "\"]"; + } + else if (json.Method == "aria2.tellStatus") + { + if (i == 0) + { + str += "\"" + json.Params[i] + "\""; + } + else + { + str += "[\"" + json.Params[i] + "\"]"; + } + } + else + { + str += "\"" + json.Params[i] + "\""; + } + if (json.Params.Count - 1 > i) + { + str += ","; + } + } + str += "]}"; + //最后得到类似 + //{"jsonrpc":"2.0","id":"qwer","method":"aria2.addUri","params:"[["http://www.baidu.com"]]} + return str; + } + + + /// + /// 发送json并返回json消息 + /// + /// + /// + private static async Task SendAndReceive(JsonClass json) + { + string str = ToJson(json); + try + { + //发送json数据 + await webSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(str)), WebSocketMessageType.Text, true, cancellationToken); + var result = new byte[1024]; + //接收数据 + await webSocket.ReceiveAsync(new ArraySegment(result), new CancellationToken()); + str += "\r\n" + Encoding.UTF8.GetString(result, 0, result.Length) + "\r\n"; + } + catch + { + str = "连接错误"; + } + return str; + } + + + /// + /// 添加新的下载 + /// + /// + /// + public static async Task AddUri(string uri) + { + var json = new JsonClass + { + Jsonrpc = "2.0", + Id = "qwer", + Method = "aria2.addUri" + }; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(uri); + json.Params = paramslist; + string str = await SendAndReceive(json); + return str; + } + + + /// + /// 上传“.torrent”文件添加BitTorrent下载 + /// + /// + /// + public static async Task AddTorrent(string path) + { + string str = ""; + string fsbase64 = ""; + byte[] fs = File.ReadAllBytes(path); + fsbase64 = Convert.ToBase64String(fs); //转为Base64编码 + + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.addTorrent"; + List paramslist = new List(); + //添加“.torrent”文件本地地址 + paramslist.Add(fsbase64); + json.Params = paramslist; + str = await SendAndReceive(json); + return str; + } + + + /// + /// 删除已经停止的任务,强制删除请使用ForceRemove方法 + /// + /// + /// + public static async Task Remove(string gid) + { + string str = ""; + var json = new JsonClass + { + Jsonrpc = "2.0", + Id = "qwer", + Method = "aria2.remove" + }; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await SendAndReceive(json); + return str; + } + + + /// + /// 强制删除 + /// + /// + /// + public static async Task ForceRemove(string gid) + { + string str = ""; + var json = new JsonClass + { + Jsonrpc = "2.0", + Id = "qwer", + Method = "aria2.forceRemove" + }; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await SendAndReceive(json); + return str; + } + + + /// + /// 暂停下载,强制暂停请使用ForcePause方法 + /// + /// + /// + public static async Task Pause(string gid) + { + string str = ""; + var json = new JsonClass + { + Jsonrpc = "2.0", + Id = "qwer", + Method = "aria2.pause" + }; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await SendAndReceive(json); + return str; + } + + + /// + /// 暂停全部任务 + /// + /// + /// + public static async Task PauseAll() + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.pauseAll"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(""); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 强制暂停下载 + /// + /// + /// + public static async Task ForcePause(string gid) + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.forcePause"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 强制暂停全部下载 + /// + /// + /// + public static async Task ForcePauseAll() + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.forcePauseAll"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(""); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 把正在下载的任务状态改为等待下载 + /// + /// + /// + public static async Task PauseToWaiting(string gid) + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.unpause"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 把全部在下载的任务状态改为等待下载 + /// + /// + /// + public static async Task PauseToWaitingAll() + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.unpauseAll"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(""); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 返回下载进度 + /// + /// + /// + public static async Task TellStatus(string gid) + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.tellStatus"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + paramslist.Add("completedLength\", \"totalLength\",\"downloadSpeed"); + + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 返回全局统计信息,例如整体下载和上载速度 + /// + /// + /// + public static async Task GetGlobalStat(string gid) + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.getGlobalStat"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 返回由gid(字符串)表示的下载中使用的URI 。响应是一个结构数组 + /// + /// + /// + public static async Task GetUris(string gid) + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.getUris"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + + /// + /// 返回由gid(字符串)表示的下载文件列表 + /// + /// + /// + public static async Task GetFiles(string gid) + { + string str = ""; + var json = new JsonClass(); + json.Jsonrpc = "2.0"; + json.Id = "qwer"; + json.Method = "aria2.getFiles"; + List paramslist = new List(); + //添加下载地址 + paramslist.Add(gid); + json.Params = paramslist; + str = await Aria2.SendAndReceive(json); + return str; + } + + } +} diff --git a/src/Core/aria2cNet/AriaManager.cs b/src/Core/aria2cNet/AriaManager.cs index 8dd9369..a3132ff 100644 --- a/src/Core/aria2cNet/AriaManager.cs +++ b/src/Core/aria2cNet/AriaManager.cs @@ -14,14 +14,6 @@ namespace Core.aria2cNet TellStatus?.Invoke(totalLength, completedLength, speed, gid); } - // 全局下载状态 - public delegate void GetGlobalStatusHandler(long speed); - public event GetGlobalStatusHandler GetGlobalStatus; - protected virtual void OnGetGlobalStatus(long speed) - { - GetGlobalStatus?.Invoke(speed); - } - // 下载结果回调 public delegate void DownloadFinishHandler(bool isSuccess, string downloadPath, string gid, string msg = null); public event DownloadFinishHandler DownloadFinish; @@ -30,6 +22,19 @@ namespace Core.aria2cNet DownloadFinish?.Invoke(isSuccess, downloadPath, gid, msg); } + // 全局下载状态 + public delegate void GetGlobalStatusHandler(long speed); + public event GetGlobalStatusHandler GlobalStatus; + protected virtual void OnGlobalStatus(long speed) + { + GlobalStatus?.Invoke(speed); + } + + /// + /// 获取gid下载项的状态 + /// + /// + /// public DownloadStatus GetDownloadStatus(string gid) { string filePath = ""; @@ -83,12 +88,32 @@ namespace Core.aria2cNet } // 降低CPU占用 - Thread.Sleep(500); + Thread.Sleep(100); } OnDownloadFinish(true, filePath, gid, null); return DownloadStatus.SUCCESS; } + /// + /// 获取全局下载速度 + /// + public async void GetGlobalStatus() + { + while (true) + { + // 查询全局status + var globalStatus = await AriaClient.GetGlobalStatAsync(); + if (globalStatus == null || globalStatus.Result == null) { continue; } + + long globalSpeed = long.Parse(globalStatus.Result.DownloadSpeed); + // 回调 + OnGlobalStatus(globalSpeed); + + // 降低CPU占用 + Thread.Sleep(100); + } + } + //private async void Poll() //{ diff --git a/src/Core/aria2cNet/client/AriaClient.cs b/src/Core/aria2cNet/client/AriaClient.cs index 795c3cd..0605cae 100644 --- a/src/Core/aria2cNet/client/AriaClient.cs +++ b/src/Core/aria2cNet/client/AriaClient.cs @@ -1035,8 +1035,10 @@ namespace Core.aria2cNet.client /// private async static Task GetRpcResponseAsync(AriaSendData ariaSend) { + // 去掉null + var jsonSetting = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; // 转换为json字符串 - string sendJson = JsonConvert.SerializeObject(ariaSend); + string sendJson = JsonConvert.SerializeObject(ariaSend, Formatting.Indented, jsonSetting); // 向服务器请求数据 string result = string.Empty; await Task.Run(() => diff --git a/src/Core/aria2cNet/client/AriaClientWebSocket.cs b/src/Core/aria2cNet/client/AriaClientWebSocket.cs new file mode 100644 index 0000000..71e88e8 --- /dev/null +++ b/src/Core/aria2cNet/client/AriaClientWebSocket.cs @@ -0,0 +1,111 @@ +using Core.aria2cNet.client.entity; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Core.aria2cNet.client +{ + public class AriaClientWebSocket + { + private static readonly string JSONRPC = "2.0"; + private static readonly string TOKEN = "downkyi"; + + public static ClientWebSocket webSocket; // 用于连接到Aria2Rpc的客户端 + public static CancellationToken cancellationToken; // 传播有关应取消操作的通知 + public static bool Status; // 储存连接状态 + + /// + /// 连接Aria2Rpc服务器 + /// + /// + /// + public static async Task ConnectServerAsync(string uri) + { + webSocket = new ClientWebSocket(); // 用于连接到Aria2Rpc的客户端 + cancellationToken = new CancellationToken(); // 传播有关应取消操作的通知 + Status = false; // 储存连接状态 + + try + { + //连接服务器 + await webSocket.ConnectAsync(new Uri(uri), cancellationToken); + } + catch + { + Status = false; + } + //检查连接是否成功 + if (webSocket.State == WebSocketState.Open) + { + Status = true; + } + return Status; + } + + /// + /// add + /// + /// + /// + public static async Task AddUriAsync(List uris) + { + AriaSendOption option = new AriaSendOption + { + Out = "index测试.html", + Dir = "home/" + }; + + List ariaParams = new List + { + "token:" + TOKEN, + uris, + option + }; + + AriaSendData ariaSend = new AriaSendData + { + Id = Guid.NewGuid().ToString("N"), + Jsonrpc = JSONRPC, + Method = "aria2.addUri", + Params = ariaParams + }; + + string sendJson = JsonConvert.SerializeObject(ariaSend); + + string result = await SendAndReceiveAsync(sendJson); + return result; + } + + + /// + /// 发送并接受数据 + /// + /// + /// + private static async Task SendAndReceiveAsync(string sendJson) + { + string result; + try + { + //发送json数据 + await webSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(sendJson)), WebSocketMessageType.Text, true, cancellationToken); + byte[] receive = new byte[1024]; + //接收数据 + await webSocket.ReceiveAsync(new ArraySegment(receive), new CancellationToken()); + result = Encoding.UTF8.GetString(receive).TrimEnd('\0'); + } + catch + { + result = "{\"id\":null,\"jsonrpc\":\"2.0\",\"error\":{\"code\":-2020,\"message\":\"连接错误\"}}"; + } + return result; + } + + + + } +} diff --git a/src/Core/aria2cNet/server/AriaServer.cs b/src/Core/aria2cNet/server/AriaServer.cs index a5e57d2..9ff7fcd 100644 --- a/src/Core/aria2cNet/server/AriaServer.cs +++ b/src/Core/aria2cNet/server/AriaServer.cs @@ -62,7 +62,7 @@ namespace Core.aria2cNet.server try { var stream = File.Open(logFile, FileMode.Open); - if (stream.Length >= 1024 * 1024 * 1024L) + if (stream.Length >= 512 * 1024 * 1024L) { stream.SetLength(0); } @@ -83,7 +83,6 @@ namespace Core.aria2cNet.server headers += $"--header=\"{header}\" "; } } - Console.WriteLine(headers); ExcuteProcess("aria2c.exe", $"--enable-rpc --rpc-listen-all=true --rpc-allow-origin-all=true " + diff --git a/src/Core/danmaku2ass/Bilibili.cs b/src/Core/danmaku2ass/Bilibili.cs new file mode 100644 index 0000000..ada39a6 --- /dev/null +++ b/src/Core/danmaku2ass/Bilibili.cs @@ -0,0 +1,163 @@ +using Core.api.danmaku; +using System.Collections.Generic; + +namespace Core.danmaku2ass +{ + public class Bilibili + { + private static Bilibili instance; + + private Dictionary config = new Dictionary + { + { "top_filter", false }, + { "bottom_filter", false }, + { "scroll_filter", false } + }; + + private readonly Dictionary mapping = new Dictionary + { + { 1, "scroll" }, + { 2, "scroll" }, + { 3, "scroll" }, + { 4, "bottom" }, + { 5, "top" }, + { 6, "scroll" }, // 逆向滚动弹幕,还是当滚动处理 + { 7, "none" }, // 高级弹幕,暂时不要考虑 + { 8, "none" }, // 代码弹幕,暂时不要考虑 + { 9, "none" } // BAS弹幕,暂时不要考虑 + }; + + // 弹幕标准字体大小 + private readonly int normalFontSize = 25; + + /// + /// 获取Bilibili实例 + /// + /// + public static Bilibili GetInstance() + { + if (instance == null) + { + instance = new Bilibili(); + } + + return instance; + } + + /// + /// 隐藏Bilibili()方法,必须使用单例模式 + /// + private Bilibili() { } + + /// + /// 是否屏蔽顶部弹幕 + /// + /// + /// + public Bilibili SetTopFilter(bool isFilter) + { + config["top_filter"] = isFilter; + return this; + } + + /// + /// 是否屏蔽底部弹幕 + /// + /// + /// + public Bilibili SetBottomFilter(bool isFilter) + { + config["bottom_filter"] = isFilter; + return this; + } + + /// + /// 是否屏蔽滚动弹幕 + /// + /// + /// + public Bilibili SetScrollFilter(bool isFilter) + { + config["scroll_filter"] = isFilter; + return this; + } + + public void Create(long avid, long cid, Config subtitleConfig, string assFile) + { + // 弹幕转换 + var biliDanmakus = DanmakuProtobuf.GetInstance().GetAllDanmakuProto(avid, cid); + + // 按弹幕出现顺序排序 + biliDanmakus.Sort((x, y) => { return x.Progress.CompareTo(y.Progress); }); + + var danmakus = new List(); + foreach (var biliDanmaku in biliDanmakus) + { + var danmaku = new Danmaku + { + // biliDanmaku.Progress单位是毫秒,所以除以1000,单位变为秒 + Start = biliDanmaku.Progress / 1000.0f, + Style = mapping[biliDanmaku.Mode], + Color = (int)biliDanmaku.Color, + Commenter = biliDanmaku.MidHash, + Content = biliDanmaku.Content, + SizeRatio = 1.0f * biliDanmaku.Fontsize / normalFontSize + }; + + danmakus.Add(danmaku); + } + + // 弹幕预处理 + Producer producer = new Producer(config, danmakus); + producer.StartHandle(); + + // 字幕生成 + var keepedDanmakus = producer.KeepedDanmakus; + var studio = new Studio(subtitleConfig, keepedDanmakus); + studio.StartHandle(); + studio.CreateAssFile(assFile); + } + + public Dictionary GetResolution(int quality) + { + var resolution = new Dictionary + { + { "width", 0 }, + { "height", 0 } + }; + + switch (quality) + { + // 240P 极速(仅mp4方式) + case 6: + break; + // 360P 流畅 + case 16: + break; + // 480P 清晰 + case 32: + break; + // 720P 高清(登录) + case 64: + break; + // 720P60 高清(大会员) + case 74: + break; + // 1080P 高清(登录) + case 80: + break; + // 1080P+ 高清(大会员) + case 112: + break; + // 1080P60 高清(大会员) + case 116: + break; + // 4K 超清(大会员)(需要fourk=1) + case 120: + break; + } + return resolution; + } + + } +} diff --git a/src/Core/danmaku2ass/Collision.cs b/src/Core/danmaku2ass/Collision.cs new file mode 100644 index 0000000..8a78f1e --- /dev/null +++ b/src/Core/danmaku2ass/Collision.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Core.danmaku2ass +{ + /// + /// 碰撞处理 + /// + public class Collision + { + private readonly int lineCount; + private readonly List leaves; + + public Collision(int lineCount) + { + this.lineCount = lineCount; + leaves = Leaves(); + } + + private List Leaves() + { + var ret = new List(lineCount); + for (int i = 0; i < lineCount; i++) ret.Add(0); + return ret; + } + + /// + /// 碰撞检测 + /// 返回行号和时间偏移 + /// + /// + /// + public Tuple Detect(Display display) + { + List beyonds = new List(); + for (int i = 0; i < leaves.Count; i++) + { + float beyond = display.Danmaku.Start - leaves[i]; + // 某一行有足够空间,直接返回行号和 0 偏移 + if (beyond >= 0) + { + return Tuple.Create(i, 0f); + } + beyonds.Add(beyond); + } + + // 所有行都没有空间了,那么找出哪一行能在最短时间内让出空间 + float soon = beyonds.Max(); + int lineIndex = beyonds.IndexOf(soon); + float offset = -soon; + return Tuple.Create(lineIndex, offset); + } + + public void Update(float leave, int lineIndex, float offset) + { + leaves[lineIndex] = Utils.IntCeiling(leave + offset); + } + + } +} diff --git a/src/Core/danmaku2ass/Config.cs b/src/Core/danmaku2ass/Config.cs new file mode 100644 index 0000000..f9a2669 --- /dev/null +++ b/src/Core/danmaku2ass/Config.cs @@ -0,0 +1,55 @@ +using System; + +namespace Core.danmaku2ass +{ + public class Config + { + public string Title = "Downkyi"; + public int ScreenWidth = 1920; + public int ScreenHeight = 1080; + public string FontName = "黑体"; + public int BaseFontSize; // 字体大小,像素 + // 限制行数 + private int lineCount; + public int LineCount + { + get { return lineCount; } + set + { + if (value == 0) + { + lineCount = (int)Math.Floor(ScreenHeight / BaseFontSize * 1.0); + } + else + { + lineCount = value; + } + } + } + public string LayoutAlgorithm; // 布局算法,async/sync + public int TuneDuration; // 微调时长 + public int DropOffset; // 丢弃偏移 + public int BottomMargin; // 底部边距 + public int CustomOffset; // 自定义偏移 + public string HeaderTemplate = @"[Script Info] +; Script generated by Downkyi Danmaku Converter +; https://github.com/FlySelfLog/downkyi +Title: {title} +ScriptType: v4.00+ +Collisions: Normal +PlayResX: {width} +PlayResY: {height} +Timer: 10.0000 +WrapStyle: 2 +ScaledBorderAndShadow: no + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,{fontname},54,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0.00,0.00,1,2.00,0.00,2,30,30,120,0 +Style: Alternate,{fontname},36,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0.00,0.00,1,2.00,0.00,2,30,30,84,0 +Style: Danmaku,{fontname},{fontsize},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0.00,0.00,1,1.00,0.00,2,30,30,30,0 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"; + } +} diff --git a/src/Core/danmaku2ass/Creater.cs b/src/Core/danmaku2ass/Creater.cs new file mode 100644 index 0000000..6ce664a --- /dev/null +++ b/src/Core/danmaku2ass/Creater.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; + +namespace Core.danmaku2ass +{ + /// + /// 创建器 + /// + public class Creater + { + public Config Config; + public List Danmakus; + public List Subtitles; + public string Text; + + public Creater(Config config, List danmakus) + { + Config = config; + Danmakus = danmakus; + Subtitles = SetSubtitles(); + Text = SetText(); + } + + protected List SetSubtitles() + { + var scroll = new Collision(Config.LineCount); + var stayed = new Collision(Config.LineCount); + Dictionary collisions = new Dictionary + { + { "scroll", scroll }, + { "top", stayed }, + { "bottom", stayed } + }; + + List subtitles = new List(); + foreach (var danmaku in Danmakus) + { + // 丢弃不支持的 + if (danmaku.Style == "none") + { + continue; + } + + // 创建显示方式对象 + var display = Display.Factory(Config, danmaku); + var collision = collisions[danmaku.Style]; + var detect = collision.Detect(display); + int lineIndex = detect.Item1; + float waitingOffset = detect.Item2; + + // 超过容忍的偏移量,丢弃掉此条弹幕 + if (waitingOffset > Config.DropOffset) + { + continue; + } + + // 接受偏移,更新碰撞信息 + display.Relayout(lineIndex); + collision.Update(display.Leave, lineIndex, waitingOffset); + + // 再加上自定义偏移 + float offset = waitingOffset + Config.CustomOffset; + Subtitle subtitle = new Subtitle(danmaku, display, offset); + + subtitles.Add(subtitle); + } + return subtitles; + } + + protected string SetText() + { + string header = Config.HeaderTemplate + .Replace("{title}", Config.Title) + .Replace("{width}", Config.ScreenWidth.ToString()) + .Replace("{height}", Config.ScreenHeight.ToString()) + .Replace("{fontname}", Config.FontName) + .Replace("{fontsize}", Config.BaseFontSize.ToString()); + + string events = string.Empty; + foreach (var subtitle in Subtitles) + { + events += "\n" + subtitle.Text; + } + + return header + events; + } + + } +} diff --git a/src/Core/danmaku2ass/Danmaku.cs b/src/Core/danmaku2ass/Danmaku.cs new file mode 100644 index 0000000..4661318 --- /dev/null +++ b/src/Core/danmaku2ass/Danmaku.cs @@ -0,0 +1,12 @@ +namespace Core.danmaku2ass +{ + public class Danmaku + { + public float Start { get; set; } + public string Style { get; set; } + public int Color { get; set; } + public string Commenter { get; set; } + public string Content { get; set; } + public float SizeRatio { get; set; } + } +} diff --git a/src/Core/danmaku2ass/Dictionary.cs b/src/Core/danmaku2ass/Dictionary.cs new file mode 100644 index 0000000..839431b --- /dev/null +++ b/src/Core/danmaku2ass/Dictionary.cs @@ -0,0 +1,6 @@ +namespace Core.danmaku2ass +{ + public class Dictionary + { + } +} \ No newline at end of file diff --git a/src/Core/danmaku2ass/Display.cs b/src/Core/danmaku2ass/Display.cs new file mode 100644 index 0000000..5df85de --- /dev/null +++ b/src/Core/danmaku2ass/Display.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Core.danmaku2ass +{ + /// + /// 显示方式 + /// + public class Display + { + public Config Config; + public Danmaku Danmaku; + public int LineIndex; + + public int FontSize; + public bool IsScaled; + public int MaxLength; + public int Width; + public int Height; + + public Tuple Horizontal; + public Tuple Vertical; + + public int Duration; + public int Leave; + + protected Display() { } + + public Display(Config config, Danmaku danmaku) + { + Config = config; + Danmaku = danmaku; + LineIndex = 0; + + IsScaled = SetIsScaled(); + FontSize = SetFontSize(); + MaxLength = SetMaxLength(); + Width = SetWidth(); + Height = SetHeight(); + + Horizontal = SetHorizontal(); + Vertical = SetVertical(); + + Duration = SetDuration(); + Leave = SetLeave(); + } + + /// + /// 根据弹幕样式自动创建对应的 Display 类 + /// + /// + public static Display Factory(Config config, Danmaku danmaku) + { + Dictionary dict = new Dictionary + { + { "scroll", new ScrollDisplay(config, danmaku) }, + { "top", new TopDisplay(config, danmaku) }, + { "bottom", new BottomDisplay(config, danmaku) } + }; + return dict[danmaku.Style]; + } + + /// + /// 字体大小 + /// 按用户自定义的字体大小来缩放 + /// + /// + protected int SetFontSize() + { + if (IsScaled) + { + Console.WriteLine($"{Danmaku.SizeRatio}"); + } + return Utils.IntCeiling(Config.BaseFontSize * Danmaku.SizeRatio); + } + + /// + /// 字体是否被缩放过 + /// + /// + protected bool SetIsScaled() + { + return !Math.Round(Danmaku.SizeRatio, 2).Equals(1.0); + //return Danmaku.SizeRatio.Equals(1.0f); + } + + /// + /// 最长的行字符数 + /// + /// + protected int SetMaxLength() + { + string[] lines = Danmaku.Content.Split('\n'); + int maxLength = 0; + foreach (string line in lines) + { + int length = Utils.DisplayLength(line); + if (maxLength < length) + { + maxLength = length; + } + } + return maxLength; + } + + /// + /// 整条字幕宽度 + /// + /// + protected int SetWidth() + { + float charCount = MaxLength;// / 2; + return Utils.IntCeiling(FontSize * charCount); + } + + /// + /// 整条字幕高度 + /// + /// + protected int SetHeight() + { + int lineCount = Danmaku.Content.Split('\n').Length; + return lineCount * FontSize; + } + + /// + /// 出现和消失的水平坐标位置 + /// 默认在屏幕中间 + /// + /// + protected virtual Tuple SetHorizontal() + { + int x = (int)Math.Floor(Config.ScreenWidth / 2.0); + return Tuple.Create(x, x); + } + + /// + /// 出现和消失的垂直坐标位置 + /// 默认在屏幕中间 + /// + /// + protected virtual Tuple SetVertical() + { + int y = (int)Math.Floor(Config.ScreenHeight / 2.0); + return Tuple.Create(y, y); + } + + /// + /// 整条字幕的显示时间 + /// + /// + protected virtual int SetDuration() + { + int baseDuration = 3 + Config.TuneDuration; + if (baseDuration <= 0) + { + baseDuration = 0; + } + float charCount = MaxLength / 2; + + int value; + if (charCount < 6) + { + value = baseDuration + 1; + } + else if (charCount < 12) + { + value = baseDuration + 2; + } + else + { + value = baseDuration + 3; + } + return value; + } + + /// + /// 离开碰撞时间 + /// + /// + protected virtual int SetLeave() + { + return (int)(Danmaku.Start + Duration); + } + + /// + /// 按照新的行号重新布局 + /// + /// + public void Relayout(int lineIndex) + { + LineIndex = lineIndex; + Horizontal = SetHorizontal(); + Vertical = SetVertical(); + } + + } + + /// + /// 顶部 + /// + public class TopDisplay : Display + { + public TopDisplay(Config config, Danmaku danmaku) : base(config, danmaku) + { + Console.WriteLine("TopDisplay constructor."); + } + + /// + /// + /// + /// + protected override Tuple SetVertical() + { + // 这里 y 坐标为 0 就是最顶行了 + int y = LineIndex * Config.BaseFontSize; + return Tuple.Create(y, y); + } + } + + /// + /// 底部 + /// + public class BottomDisplay : Display + { + public BottomDisplay(Config config, Danmaku danmaku) : base(config, danmaku) + { + Console.WriteLine("BottomDisplay constructor."); + } + + /// + /// + /// + /// + protected override Tuple SetVertical() + { + // 要让字幕不超出底部,减去高度 + int y = Config.ScreenHeight - (LineIndex * Config.BaseFontSize) - Height; + // 再减去自定义的底部边距 + y -= Config.BottomMargin; + return Tuple.Create(y, y); + } + } + + /// + /// 滚动 + /// + public class ScrollDisplay : Display + { + public int Distance; + public int Speed; + + public ScrollDisplay(Config config, Danmaku danmaku) : base() + { + Console.WriteLine("ScrollDisplay constructor."); + + Config = config; + Danmaku = danmaku; + LineIndex = 0; + + IsScaled = SetIsScaled(); + FontSize = SetFontSize(); + MaxLength = SetMaxLength(); + Width = SetWidth(); + Height = SetHeight(); + + Horizontal = SetHorizontal(); + Vertical = SetVertical(); + + Distance = SetDistance(); + Speed = SetSpeed(); + + Duration = SetDuration(); + Leave = SetLeave(); + } + + /// + /// ASS 的水平位置参考点是整条字幕文本的中点 + /// + /// + protected override Tuple SetHorizontal() + { + int x1 = Config.ScreenWidth + (int)Math.Floor(Width / 2.0); + int x2 = 0 - (int)Math.Floor(Width / 2.0); + return Tuple.Create(x1, x2); + } + + protected override Tuple SetVertical() + { + int baseFontSize = Config.BaseFontSize; + + // 垂直位置,按基准字体大小算每一行的高度 + int y = (LineIndex + 1) * baseFontSize; + + // 个别弹幕可能字体比基准要大,所以最上的一行还要避免挤出顶部屏幕 + // 坐标不能小于字体大小 + if (y < FontSize) + { + y = FontSize; + } + return Tuple.Create(y, y); + } + + /// + /// 字幕坐标点的移动距离 + /// + /// + protected int SetDistance() + { + Tuple x = Horizontal; + return x.Item1 - x.Item2; + } + + /// + /// 字幕每个字的移动的速度 + /// + /// + protected int SetSpeed() + { + // 基准时间,就是每个字的移动时间 + // 12 秒加上用户自定义的微调 + int baseDuration = 12 + Config.TuneDuration; + if (baseDuration <= 0) + { + baseDuration = 1; + } + return Utils.IntCeiling(Config.ScreenWidth / baseDuration); + } + + /// + /// 计算每条弹幕的显示时长,同步方式 + /// 每个弹幕的滚动速度都一样,辨认度好,适合观看剧集类视频。 + /// + /// + public int SyncDuration() + { + return Distance / Speed; + } + + /// + /// 计算每条弹幕的显示时长,异步方式 + /// 每个弹幕的滚动速度都不一样,动态调整,辨认度低,适合观看 MTV 类视频。 + /// + /// + public int AsyncDuration() + { + int baseDuration = 6 + Config.TuneDuration; + if (baseDuration <= 0) + { + baseDuration = 0; + } + float charCount = MaxLength / 2; + + int value; + if (charCount < 6) + { + value = (int)(baseDuration + charCount); + } + else if (charCount < 12) + { + value = baseDuration + (int)(charCount / 2); + } + else if (charCount < 24) + { + value = baseDuration + (int)(charCount / 3); + } + else + { + value = baseDuration + 10; + } + return value; + } + + /// + /// 整条字幕的移动时间 + /// + /// + protected override int SetDuration() + { + string methodName = Config.LayoutAlgorithm.Substring(0, 1).ToUpper() + Config.LayoutAlgorithm.Substring(1); + methodName += "Duration"; + MethodInfo method = typeof(ScrollDisplay).GetMethod(methodName); + if (method != null) + { + return (int)method.Invoke(this, null); + } + return 0; + } + + /// + /// 离开碰撞时间 + /// + /// + protected override int SetLeave() + { + // 对于滚动样式弹幕来说,就是最后一个字符离开最右边缘的时间 + // 坐标是字幕中点,在屏幕外和内各有半个字幕宽度 + // 也就是跑过一个字幕宽度的路程 + float duration = Width / Speed; + return (int)(Danmaku.Start + duration); + } + + } + +} diff --git a/src/Core/danmaku2ass/Filter.cs b/src/Core/danmaku2ass/Filter.cs new file mode 100644 index 0000000..1fb6e23 --- /dev/null +++ b/src/Core/danmaku2ass/Filter.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace Core.danmaku2ass +{ + /// + /// 过滤器基类 + /// + public class Filter + { + public virtual List DoFilter(List danmakus) + { + throw new NotImplementedException("使用了过滤器的未实现的方法。"); + } + } + + /// + /// 顶部样式过滤器 + /// + public class TopFilter : Filter + { + public override List DoFilter(List danmakus) + { + List keep = new List(); + foreach (var danmaku in danmakus) + { + if (danmaku.Style == "top") + { + continue; + } + keep.Add(danmaku); + } + return keep; + } + } + + /// + /// 底部样式过滤器 + /// + public class BottomFilter : Filter + { + public override List DoFilter(List danmakus) + { + List keep = new List(); + foreach (var danmaku in danmakus) + { + if (danmaku.Style == "bottom") + { + continue; + } + keep.Add(danmaku); + } + return keep; + } + } + + /// + /// 滚动样式过滤器 + /// + public class ScrollFilter : Filter + { + public override List DoFilter(List danmakus) + { + List keep = new List(); + foreach (var danmaku in danmakus) + { + if (danmaku.Style == "scroll") + { + continue; + } + keep.Add(danmaku); + } + return keep; + } + } + + /// + /// 自定义过滤器 + /// + public class CustomFilter : Filter + { + public override List DoFilter(List danmakus) + { + // TODO + return base.DoFilter(danmakus); + } + } + +} diff --git a/src/Core/danmaku2ass/Producer.cs b/src/Core/danmaku2ass/Producer.cs new file mode 100644 index 0000000..2ebf885 --- /dev/null +++ b/src/Core/danmaku2ass/Producer.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Core.danmaku2ass +{ + public class Producer + { + public Dictionary Config; + public Dictionary Filters; + public List Danmakus; + public List KeepedDanmakus; + public Dictionary FilterDetail; + + public Producer(Dictionary config, List danmakus) + { + Config = config; + Danmakus = danmakus; + } + + public void StartHandle() + { + LoadFilter(); + ApplyFilter(); + } + + public void LoadFilter() + { + Filters = new Dictionary(); + if (Config["top_filter"]) + { + Filters.Add("top_filter", new TopFilter()); + } + if (Config["bottom_filter"]) + { + Filters.Add("bottom_filter", new BottomFilter()); + } + if (Config["scroll_filter"]) + { + Filters.Add("scroll_filter", new ScrollFilter()); + } + //if (Config["custom_filter"]) + //{ + // Filters.Add("custom_filter", new CustomFilter()); + //} + + } + + public void ApplyFilter() + { + Dictionary filterDetail = new Dictionary() { + { "top_filter",0}, + { "bottom_filter",0}, + { "scroll_filter",0}, + //{ "custom_filter",0} + }; + + List danmakus = Danmakus; + //string[] orders = { "top_filter", "bottom_filter", "scroll_filter", "custom_filter" }; + string[] orders = { "top_filter", "bottom_filter", "scroll_filter" }; + foreach (var name in orders) + { + Filter filter; + try + { + filter = Filters[name]; + } + catch (Exception e) + { + Console.WriteLine("ApplyFilter()发生异常: {0}", e); + continue; + } + + int count = danmakus.Count; + danmakus = filter.DoFilter(danmakus); + filterDetail[name] = count - danmakus.Count; + } + + KeepedDanmakus = danmakus; + FilterDetail = filterDetail; + } + + public Dictionary Report() + { + int blockedCount = 0; + foreach (int count in FilterDetail.Values) + { + blockedCount += count; + } + + int passedCount = KeepedDanmakus.Count; + int totalCount = blockedCount + passedCount; + + Dictionary ret = new Dictionary + { + { "blocked", blockedCount }, + { "passed", passedCount }, + { "total", totalCount } + }; + + return (Dictionary)ret.Concat(FilterDetail); + } + + } +} diff --git a/src/Core/danmaku2ass/Studio.cs b/src/Core/danmaku2ass/Studio.cs new file mode 100644 index 0000000..be471ec --- /dev/null +++ b/src/Core/danmaku2ass/Studio.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Core.danmaku2ass +{ + /// + /// 字幕工程类 + /// + public class Studio + { + public Config Config; + public List Danmakus; + + public Creater Creater; + public int KeepedCount; + public int DropedCount; + + public Studio(Config config, List danmakus) + { + Config = config; + Danmakus = danmakus; + } + + public void StartHandle() + { + Creater = SetCreater(); + KeepedCount = SetKeepedCount(); + DropedCount = SetDropedCount(); + } + + /// + /// ass 创建器 + /// + /// + protected Creater SetCreater() + { + return new Creater(Config, Danmakus); + } + + /// + /// 保留条数 + /// + /// + protected int SetKeepedCount() + { + return Creater.Subtitles.Count(); + } + + /// + /// 丢弃条数 + /// + /// + protected int SetDropedCount() + { + return Danmakus.Count - KeepedCount; + } + + /// + /// 创建 ass 字幕 + /// + /// + public void CreateAssFile(string fileName) + { + CreateFile(fileName, Creater.Text); + } + + public void CreateFile(string fileName, string text) + { + File.WriteAllText(fileName, text); + } + + public Dictionary Report() + { + return new Dictionary() + { + {"total", Danmakus.Count}, + {"droped", DropedCount}, + {"keeped", KeepedCount}, + }; + } + + } +} diff --git a/src/Core/danmaku2ass/Subtitle.cs b/src/Core/danmaku2ass/Subtitle.cs new file mode 100644 index 0000000..1f46e4c --- /dev/null +++ b/src/Core/danmaku2ass/Subtitle.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; + +namespace Core.danmaku2ass +{ + /// + /// 字幕 + /// + public class Subtitle + { + public Danmaku Danmaku; + public Display Display; + public float Offset; + + public float Start; + public float End; + public string Color; + public Dictionary Position; + public string StartMarkup; + public string EndMarkup; + public string ColorMarkup; + public string BorderMarkup; + public string FontSizeMarkup; + public string StyleMarkup; + public string LayerMarkup; + public string ContentMarkup; + public string Text; + + public Subtitle(Danmaku danmaku, Display display, float offset = 0) + { + Danmaku = danmaku; + Display = display; + Offset = offset; + + Start = SetStart(); + End = SetEnd(); + Color = SetColor(); + Position = SetPosition(); + StartMarkup = SetStartMarkup(); + EndMarkup = SetEndMarkup(); + ColorMarkup = SetColorMarkup(); + BorderMarkup = SetBorderMarkup(); + FontSizeMarkup = SetFontSizeMarkup(); + StyleMarkup = SetStyleMarkup(); + LayerMarkup = SetLayerMarkup(); + ContentMarkup = SetContentMarkup(); + Text = SetText(); + } + + protected float SetStart() + { + return Danmaku.Start + Offset; + } + + protected float SetEnd() + { + return Start + Display.Duration; + } + + protected string SetColor() + { + return Utils.Int2bgr(Danmaku.Color); + } + + protected Dictionary SetPosition() + { + Tuple x = Display.Horizontal; + Tuple y = Display.Vertical; + + Dictionary value = new Dictionary + { + { "x1", x.Item1 }, + { "x2", x.Item2 }, + { "y1", y.Item1 }, + { "y2", y.Item2 } + }; + return value; + } + + protected string SetStartMarkup() + { + return Utils.Second2hms(Start); + } + + protected string SetEndMarkup() + { + return Utils.Second2hms(End); + } + + protected string SetColorMarkup() + { + // 白色不需要加特别标记 + if (Color == "FFFFFF") + { + return ""; + } + return "\\c&H" + Color; + } + + protected string SetBorderMarkup() + { + // 暗色加个亮色边框,方便阅读 + if (Utils.IsDark(Danmaku.Color)) + { + //return "\\3c&HFFFFFF"; + return "\\3c&H000000"; + } + else + { + return "\\3c&H000000"; + } + //return ""; + } + + protected string SetFontSizeMarkup() + { + if (Display.IsScaled) + { + return $"\\fs{Display.FontSize}"; + } + return ""; + } + + protected string SetStyleMarkup() + { + if (Danmaku.Style == "scroll") + { + return $"\\move({Position["x1"]}, {Position["y1"]}, {Position["x2"]}, {Position["y2"]})"; + } + return $"\\a6\\pos({Position["x1"]}, {Position["y1"]})"; + } + + protected string SetLayerMarkup() + { + if (Danmaku.Style != "scroll") + { + return "-2"; + } + return "-1"; + } + + protected string SetContentMarkup() + { + string markup = StyleMarkup + ColorMarkup + BorderMarkup + FontSizeMarkup; + string content = Utils.CorrectTypos(Danmaku.Content); + return $"{{{markup}}}{content}"; + } + + protected string SetText() + { + return $"Dialogue: {LayerMarkup},{StartMarkup},{EndMarkup},Danmaku,,0000,0000,0000,,{ContentMarkup}"; + } + + } +} diff --git a/src/Core/danmaku2ass/Utils.cs b/src/Core/danmaku2ass/Utils.cs new file mode 100644 index 0000000..583cc89 --- /dev/null +++ b/src/Core/danmaku2ass/Utils.cs @@ -0,0 +1,228 @@ +using System; +using System.Linq; +using System.Text; + +namespace Core.danmaku2ass +{ + internal static class Utils + { + /// + /// 向上取整,返回int类型 + /// + /// + /// + public static int IntCeiling(float number) + { + return (int)Math.Ceiling(number); + } + + /// + /// 字符长度,1个汉字当2个英文 + /// + /// + /// + public static int DisplayLength(string text) + { + return Encoding.Default.GetBytes(text).Length; + } + + /// + /// 修正一些评论者的拼写错误 + /// + /// + /// + public static string CorrectTypos(string text) + { + text = text.Replace("/n", "\\N"); + text = text.Replace(">", ">"); + text = text.Replace("<", "<"); + return text; + } + + /// + /// 秒数转 时:分:秒 格式 + /// + /// + /// + public static string Second2hms(float seconds) + { + if (seconds < 0) + { + return "0:00:00.00"; + } + + 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); + + return $"{hour:D}:{min:D2}:{second:D2}.{dec:D2}"; + } + + /// + /// 时:分:秒 格式转 秒数 + /// + /// + /// + public static float Hms2second(string hms) + { + string[] numbers = hms.Split(':'); + float seconds = 0; + + for (int i = 0; i < numbers.Length; i++) + { + seconds += (float)(float.Parse(numbers[numbers.Length - i - 1]) * Math.Pow(60, i)); + } + return seconds; + } + + /// + /// 同Hms2second(string hms),不过可以用 +/- 符号来连接多个 + /// 即 3:00-2:30 相当于 30 秒 + /// + /// + /// + public static float Xhms2second(string xhms) + { + string[] args = xhms.Replace("+", " +").Replace("-", " -").Split(' '); + float result = 0; + foreach (string hms in args) + { + result += Hms2second(hms); + } + return result; + } + + /// + /// 颜色值,整型转 RGB + /// + /// + /// + public static string Int2rgb(int integer) + { + return integer.ToString("X").PadLeft(6, '0'); ; + } + + /// + /// 颜色值,整型转 BGR + /// + /// + /// + public static string Int2bgr(int integer) + { + string rgb = Int2rgb(integer); + string bgr = rgb.Substring(4, 2) + rgb.Substring(2, 2) + rgb.Substring(0, 2); + return bgr; + } + + /// + /// 颜色值,整型转 HLS + /// + /// + /// + public static float[] Int2hls(int integer) + { + string rgb = Int2rgb(integer); + int[] rgb_decimals = { 0, 0, 0 }; + rgb_decimals[0] = int.Parse(rgb.Substring(0, 2), System.Globalization.NumberStyles.HexNumber); + rgb_decimals[1] = int.Parse(rgb.Substring(2, 2), System.Globalization.NumberStyles.HexNumber); + rgb_decimals[2] = int.Parse(rgb.Substring(4, 2), System.Globalization.NumberStyles.HexNumber); + + int[] rgb_coordinates = { 0, 0, 0 }; + rgb_coordinates[0] = (int)Math.Floor(rgb_decimals[0] / 255.0); + rgb_coordinates[1] = (int)Math.Floor(rgb_decimals[1] / 255.0); + rgb_coordinates[2] = (int)Math.Floor(rgb_decimals[2] / 255.0); + float[] hls_corrdinates = Rgb2hls(rgb_coordinates); + + float[] hls = { 0, 0, 0 }; + hls[0] = hls_corrdinates[0] * 360; + hls[1] = hls_corrdinates[1] * 100; + hls[2] = hls_corrdinates[2] * 100; + return hls; + } + + /// + /// HLS: Hue, Luminance, Saturation + /// H: position in the spectrum + /// L: color lightness + /// S: color saturation + /// + /// + /// + private static float[] Rgb2hls(int[] rgb) + { + float[] hls = { 0, 0, 0 }; + int maxc = rgb.Max(); + int minc = rgb.Min(); + hls[1] = (minc + maxc) / 2.0f; + if (minc == maxc) + { + return hls; + } + + if (hls[1] <= 0.5) + { + hls[2] = (maxc - minc) / (maxc + minc); + } + else + { + hls[2] = (maxc - minc) / (2.0f - maxc - minc); + } + float rc = (maxc - rgb[0]) / (maxc - minc); + float gc = (maxc - rgb[1]) / (maxc - minc); + float bc = (maxc - rgb[2]) / (maxc - minc); + if (rgb[0] == maxc) + { + hls[0] = bc - gc; + } + else if (rgb[1] == maxc) + { + hls[0] = 2.0f + rc - bc; + } + else + { + hls[0] = 4.0f + gc - rc; + } + hls[0] = (hls[0] / 6.0f) % 1.0f; + return hls; + } + + /// + /// 是否属于暗色 + /// + /// + /// + public static bool IsDark(int integer) + { + if (integer == 0) + { + return true; + } + + float[] hls = Int2hls(integer); + float hue = hls[0]; + float lightness = hls[1]; + + // HSL 色轮见 + // http://zh.wikipedia.org/zh-cn/HSL和HSV色彩空间 + // 以下的数值都是我的主观判断认为是暗色 + if ((hue > 30 && hue < 210) && lightness < 33) + { + return true; + } + if ((hue < 30 || hue > 210) && lightness < 66) + { + return true; + } + return false; + } + + } +} diff --git a/src/Core/entity/BangumiMedia.cs b/src/Core/entity/BangumiMedia.cs new file mode 100644 index 0000000..a56f8f2 --- /dev/null +++ b/src/Core/entity/BangumiMedia.cs @@ -0,0 +1,35 @@ +// 注释掉未使用的属性 +namespace Core.entity +{ + // https://api.bilibili.com/pgc/review/user?media_id=28228367 + class BangumiMedia + { + //public int code { get; set; } + //public string message { get; set; } + public BangumiMediaData result { get; set; } + } + + public class BangumiMediaData + { + public BangumiMediaDataMedia media { get; set; } + //public BangumiMediaDataReview review { get; set; } + } + + public class BangumiMediaDataMedia + { + // TODO 暂时只实现部分字段 + //public long media_id { get; set; } + + public long season_id { get; set; } + //public string share_url { get; set; } + //public string title { get; set; } + //public string type_name { get; set; } + } + + //public class BangumiMediaDataReview + //{ + // public int is_coin { get; set; } + // public int is_open { get; set; } + //} + +} diff --git a/src/Core/entity/BangumiSeason.cs b/src/Core/entity/BangumiSeason.cs new file mode 100644 index 0000000..cc6dfa2 --- /dev/null +++ b/src/Core/entity/BangumiSeason.cs @@ -0,0 +1,199 @@ +using System.Collections.Generic; + +// 注释掉未使用的属性 +namespace Core.entity +{ + //https://api.bilibili.com/pgc/view/web/season?season_id=28324 + public class BangumiSeason + { + //public int code { get; set; } + //public string message { get; set; } + public BangumiSeasonResult result { get; set; } + } + + public class BangumiSeasonResult + { + //public BangumiSeasonResultActivity activity { get; set; } + //public string alias { get; set; } + //public string bkg_cover { get; set; } + public string cover { get; set; } + public List episodes { get; set; } + public string evaluate { get; set; } + //public string jp_title { get; set; } + //public string link { get; set; } + //public long media_id { get; set; } + //public int mode { get; set; } + //public BangumiSeasonResultNewEp new_ep { get; set; } + //public BangumiSeasonResultPositive positive { get; set; } + //public BangumiSeasonResultPublish publish { get; set; } + //public BangumiSeasonResultRating rating { get; set; } + //public string record { get; set; } + //public BangumiSeasonResultRights rights { get; set; } + //public long season_id { get; set; } + //public string season_title { get; set; } + //public List seasons { get; set; } + //public BangumiSeasonResultSeries series { get; set; } + //public string share_copy { get; set; } + //public string share_sub_title { get; set; } + //public string share_url { get; set; } + //public BangumiSeasonResultShow show { get; set; } + //public string square_cover { get; set; } + public BangumiSeasonResultStat stat { get; set; } + //public int status { get; set; } + //public string subtitle { get; set; } + public string title { get; set; } + //public int total { get; set; } + public int type { get; set; } + } + + //public class BangumiSeasonResultActivity + //{ + // public string head_bg_url { get; set; } + // public long id { get; set; } + // public string title { get; set; } + //} + + public class BangumiSeasonResultEpisodes + { + public long aid { get; set; } + //public string badge { get; set; } + //public BangumiSeasonResultBadgeInfo badge_info { get; set; } + //public int badge_type { get; set; } + public string bvid { get; set; } + public long cid { get; set; } + //public string cover { get; set; } + public BangumiSeasonResultEpisodesDimension dimension { get; set; } + //public string from { get; set; } + //public long id { get; set; } + //public string link { get; set; } + //public string long_title { get; set; } + //public long pub_time { get; set; } + //public string release_date { get; set; } + //public BangumiSeasonResultEpisodesRights rights { get; set; } + public string share_copy { get; set; } + //public string share_url { get; set; } + //public string short_link { get; set; } + //public int status { get; set; } + //public string subtitle { get; set; } + public string title { get; set; } + //public string vid { get; set; } + } + + //public class BangumiSeasonResultBadgeInfo + //{ + // public string bg_color { get; set; } + // public string bg_color_night { get; set; } + // public string text { get; set; } + //} + + public class BangumiSeasonResultEpisodesDimension + { + public int width { get; set; } + public int height { get; set; } + //public int rotate { get; set; } + } + + //public class BangumiSeasonResultEpisodesRights + //{ + // public int allow_dm { get; set; } + //} + + //public class BangumiSeasonResultNewEp + //{ + // public string desc { get; set; } + // public long id { get; set; } + // public int is_new { get; set; } + // public string title { get; set; } + //} + + //public class BangumiSeasonResultPositive + //{ + // public long id { get; set; } + // public string title { get; set; } + //} + + //public class BangumiSeasonResultPublish + //{ + // public int is_finish { get; set; } + // public int is_started { get; set; } + // public string pub_time { get; set; } + // public string pub_time_show { get; set; } + // public int unknow_pub_date { get; set; } + // public int weekday { get; set; } + //} + + //public class BangumiSeasonResultRating + //{ + // public long count { get; set; } + // public float score { get; set; } + //} + + //public class BangumiSeasonResultRights + //{ + // public int allow_bp { get; set; } + // public int allow_bp_rank { get; set; } + // public int allow_download { get; set; } + // public int allow_review { get; set; } + // public int area_limit { get; set; } + // public int ban_area_show { get; set; } + // public int can_watch { get; set; } + // public string copyright { get; set; } + // public int forbid_pre { get; set; } + // public int is_cover_show { get; set; } + // public int is_preview { get; set; } + // public int only_vip_download { get; set; } + // public string resource { get; set; } + // public int watch_platform { get; set; } + //} + + //public class BangumiSeasonResultSeasons + //{ + // public string badge { get; set; } + // public BangumiSeasonResultBadgeInfo badge_info { get; set; } + // public int badge_type { get; set; } + // public string cover { get; set; } + // public long media_id { get; set; } + // public BangumiSeasonResultSeasonsNewEp new_ep { get; set; } + // public long season_id { get; set; } + // public string season_title { get; set; } + // public int season_type { get; set; } + // public BangumiSeasonResultSeasonsStat stat { get; set; } + //} + + //public class BangumiSeasonResultSeasonsNewEp + //{ + // public string cover { get; set; } + // public long id { get; set; } + // public string index_show { get; set; } + //} + + //public class BangumiSeasonResultSeasonsStat + //{ + // public long favorites { get; set; } + // public long series_follow { get; set; } + // public long views { get; set; } + //} + + //public class BangumiSeasonResultSeries + //{ + // public long series_id { get; set; } + // public string series_title { get; set; } + //} + + //public class BangumiSeasonResultShow + //{ + // public int wide_screen { get; set; } + //} + + public class BangumiSeasonResultStat + { + public long coins { get; set; } + public long danmakus { get; set; } + public long favorites { get; set; } + public long likes { get; set; } + public long reply { get; set; } + public long share { get; set; } + public long views { get; set; } + } + +} diff --git a/src/Core/entity/CheeseList.cs b/src/Core/entity/CheeseList.cs new file mode 100644 index 0000000..905adec --- /dev/null +++ b/src/Core/entity/CheeseList.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Core.entity +{ + // https://api.bilibili.com/pugv/view/web/ep/list?season_id=112&pn=1 + public class CheeseList + { + public int code { get; set; } + public CheeseListData data { get; set; } + public string message { get; set; } + } + + public class CheeseListData + { + public List items { get; set; } + public CheeseListDataPage page { get; set; } + } + + public class CheeseListDataItem + { + public long aid { get; set; } + public long cid { get; set; } + public long duration { get; set; } + public string from { get; set; } + public long id { get; set; } // ep_id + public int index { get; set; } + public int page { get; set; } + public long play { get; set; } + public long release_date { get; set; } + public int status { get; set; } + public string title { get; set; } + public bool watched { get; set; } + public int watchedHistory { get; set; } + } + + public class CheeseListDataPage + { + public bool next { get; set; } // 是否还有下一页 + public int num { get; set; } // 当前页 + public int size { get; set; } // list大小 + public int total { get; set; } // 总的视频数量 + } + +} diff --git a/src/Core/entity/CheeseSeason.cs b/src/Core/entity/CheeseSeason.cs new file mode 100644 index 0000000..9933d8c --- /dev/null +++ b/src/Core/entity/CheeseSeason.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; + +namespace Core.entity +{ + // https://api.bilibili.com/pugv/view/web/season?ep_id=2126 + class CheeseSeason + { + //public int code { get; set; } + public CheeseSeasonData data { get; set; } + //public string message { get; set; } + } + + public class CheeseSeasonData + { + + public string cover { get; set; } + + public List episodes { get; set; } + + public long season_id { get; set; } + + public CheeseSeasonDataStat stat { get; set; } + + public string subtitle { get; set; } + public string title { get; set; } + + } + + public class CheeseSeasonDataEpisode + { + public long aid { get; set; } + public long cid { get; set; } + public long duration { get; set; } + public string from { get; set; } + public long id { get; set; } // ep_id + public int index { get; set; } + public int page { get; set; } + public long play { get; set; } + public long release_date { get; set; } + public int status { get; set; } + public string title { get; set; } + public bool watched { get; set; } + public int watchedHistory { get; set; } + } + + public class CheeseSeasonDataStat + { + public long play { get; set; } + public string play_desc { get; set; } + } + +} diff --git a/src/Core/entity/Danmu.cs b/src/Core/entity/Danmu.cs new file mode 100644 index 0000000..dae53c0 --- /dev/null +++ b/src/Core/entity/Danmu.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +// 注释掉未使用的属性 +namespace Core.entity +{ + + public class DanmuDate + { + //{"code":0,"message":"0","ttl":1,"data":["2020-07-01","2020-07-02","2020-07-03","2020-07-04","2020-07-05","2020-07-06","2020-07-07","2020-07-08"]} + //public int code { get; set; } + //public string message { get; set; } + //public int ttl { get; set; } + public List data { get; set; } + } + + public class DanmuFromWeb + { + //public int code { get; set; } + public string message { get; set; } + //public int ttl { get; set; } + } + + + +} diff --git a/src/Core/entity/FavFolder.cs b/src/Core/entity/FavFolder.cs new file mode 100644 index 0000000..166f66e --- /dev/null +++ b/src/Core/entity/FavFolder.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace Core.entity +{ + /// + /// https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=42018135 + /// https://api.bilibili.com/x/v3/fav/folder/collected/list?pn=1&ps=20&up_mid=42018135 + /// 上述两个网址都使用同一个class解析 + /// + public class FavFolder + { + //public int code { get; set; } + public FavFolderData data { get; set; } + //public string message { get; set; } + //public int ttl { get; set; } + } + + public class FavFolderData + { + public int count { get; set; } + public List list { get; set; } + } + + public class FavFolderDataList + { + public int attr { get; set; } + public string cover { get; set; } + public int cover_type { get; set; } + public long ctime { get; set; } + public int fav_state { get; set; } + public long fid { get; set; } + public long id { get; set; } + public string intro { get; set; } + public int media_count { get; set; } + public long mid { get; set; } + public long mtime { get; set; } + public int state { get; set; } + public string title { get; set; } + public FavFolderDataUpper upper { get; set; } + } + + public class FavFolderDataUpper + { + public string face { get; set; } + public long mid { get; set; } + public string name { get; set; } + } + +} diff --git a/src/Core/entity/FavResource.cs b/src/Core/entity/FavResource.cs new file mode 100644 index 0000000..1308cb6 --- /dev/null +++ b/src/Core/entity/FavResource.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +namespace Core.entity +{ + // https://api.bilibili.com/x/v3/fav/resource/list?media_id=94341835&pn=1&ps=20 + public class FavResource + { + //public int code { get; set; } + public FavResourceData data { get; set; } + //public string message { get; set; } + //public int ttl { get; set; } + } + + public class FavResourceData + { + //public FavResourceDataInfo info{get;set;} + public List medias { get; set; } + } + + public class FavResourceDataMedia + { + public int attr { get; set; } + public string bv_id { get; set; } + public string bvid { get; set; } + public FavResourceDataMediaCnt_info cnt_info { get; set; } + public string cover { get; set; } + public long ctime { get; set; } + public long duration { get; set; } + public long fav_time { get; set; } + public long id { get; set; } + public string intro { get; set; } + public string link { get; set; } + public int page { get; set; } + public long pubtime { get; set; } + public string title { get; set; } + public int type { get; set; } + public FavResourceDataMediaUpper upper { get; set; } + } + + public class FavResourceDataMediaCnt_info + { + public long collect { get; set; } + public long danmaku { get; set; } + public long play { get; set; } + } + + public class FavResourceDataMediaUpper + { + public string face { get; set; } + public long mid { get; set; } + public string name { get; set; } + } + +} diff --git a/src/Core/entity/LoginUrl.cs b/src/Core/entity/LoginUrl.cs new file mode 100644 index 0000000..a6638fd --- /dev/null +++ b/src/Core/entity/LoginUrl.cs @@ -0,0 +1,50 @@ +// 注释掉未使用的属性 +namespace Core.entity +{ + public class LoginUrl + { + //public int code { get; set; } + public LoginData data { get; set; } + public bool status { get; set; } + //public long ts { get; set; } + } + + public class LoginData + { + public string oauthKey { get; set; } + public string url { get; set; } + } + + + public class LoginInfo + { + public int code { get; set; } + public bool status { get; set; } + public string message { get; set; } + public string url { get; set; } + } + + public class LoginInfoScanning + { + public bool status { get; set; } + public int data { get; set; } + public string message { get; set; } + } + + public class LoginInfoReady + { + // {"code":0,"status":true,"ts":1594131179,"data":{"url":"https://passport.biligame.com/crossDomain?DedeUserID=42018135&DedeUserID__ckMd5=44e22fa30fe34ac4&Expires=15551000&SESSDATA=92334e44%2C1609683179%2C54db1%2A71&bili_jct=979b94fb3879c68574f02800d8a39484&gourl=https%3A%2F%2Fwww.bilibili.com"}} + + public int code { get; set; } + public bool status { get; set; } + + //public long ts { get; set; } + public LoginInfoData data { get; set; } + } + + public class LoginInfoData + { + public string url { get; set; } + } + +} diff --git a/src/Core/entity/MyInfo.cs b/src/Core/entity/MyInfo.cs new file mode 100644 index 0000000..b939db3 --- /dev/null +++ b/src/Core/entity/MyInfo.cs @@ -0,0 +1,96 @@ +namespace Core.entity +{ + // https://api.bilibili.com/x/space/myinfo + public class MyInfo + { + //public int code { get; set; } + public MyInfoData data { get; set; } + public string message { get; set; } + //public int ttl { get; set; } + } + + public class MyInfoData + { + public long birthday { get; set; } + public float coins { get; set; } + public int email_status { get; set; } + public string face { get; set; } + public int follower { get; set; } + public int following { get; set; } + public int identification { get; set; } + public int is_deleted { get; set; } + public int is_fake_account { get; set; } + public int is_tourist { get; set; } + public int jointime { get; set; } + public int level { get; set; } + public MyInfoDataLevelExp level_exp { get; set; } + public long mid { get; set; } + public int moral { get; set; } + public string name { get; set; } + public MyInfoDataNamePlate nameplate { get; set; } + public MyInfoDataOfficial official { get; set; } + public MyInfoDataPendant pendant { get; set; } + public int pin_prompting { get; set; } + public int rank { get; set; } + public string sex { get; set; } + public string sign { get; set; } + public int silence { get; set; } + public int tel_status { get; set; } + public MyInfoDataVip vip { get; set; } + } + + public class MyInfoDataLevelExp + { + public int current_exp { get; set; } + public int current_level { get; set; } + public int current_min { get; set; } + public int next_exp { get; set; } + } + + public class MyInfoDataNamePlate + { + public string condition { get; set; } + public string image { get; set; } + public string image_small { get; set; } + public string level { get; set; } + public string name { get; set; } + public int nid { get; set; } + } + + public class MyInfoDataOfficial + { + public string desc { get; set; } + public int role { get; set; } + public string title { get; set; } + public int type { get; set; } + } + + public class MyInfoDataPendant + { + public int expire { get; set; } + public string image { get; set; } + public string image_enhance { get; set; } + public string name { get; set; } + public int pid { get; set; } + } + + public class MyInfoDataVip + { + public int avatar_subscript { get; set; } + public long due_date { get; set; } + public MyInfoDataVipLabel label { get; set; } + public string nickname_color { get; set; } + public int status { get; set; } + public int theme_type { get; set; } + public int type { get; set; } + public int vip_pay_type { get; set; } + } + + public class MyInfoDataVipLabel + { + public string label_theme { get; set; } + public string path { get; set; } + public string text { get; set; } + } + +} diff --git a/src/Core/entity/Nav.cs b/src/Core/entity/Nav.cs new file mode 100644 index 0000000..23c33ab --- /dev/null +++ b/src/Core/entity/Nav.cs @@ -0,0 +1,88 @@ +// 注释掉未使用的属性 +namespace Core.entity +{ + public class Nav + { + //public int code { get; set; } + public NavData data { get; set; } + public string message { get; set; } + //public int ttl { get; set; } + } + + public class NavData + { + //public int allowance_count { get; set; } + //public int answer_status { get; set; } + //public int email_verified { get; set; } + public string face { get; set; } + //public bool has_shop { get; set; } + public bool isLogin { get; set; } + //public NavDataLevelInfo level_info { get; set; } + public long mid { get; set; } + //public int mobile_verified { get; set; } + public float money { get; set; } + //public int moral { get; set; } + //public NavDataOfficial official { get; set; } + //public NavDataOfficialVerify officialVerify { get; set; } + //public NavDataPendant pendant { get; set; } + //public int scores { get; set; } + //public string shop_url { get; set; } + public string uname { get; set; } + //public long vipDueDate { get; set; } + public int vipStatus { get; set; } + //public int vipType { get; set; } + //public int vip_avatar_subscript { get; set; } + //public NavDataVipLabel vip_label { get; set; } + //public string vip_nickname_color { get; set; } + //public int vip_pay_type { get; set; } + //public int vip_theme_type { get; set; } + public NavDataWallet wallet { get; set; } + } + + public class NavDataLevelInfo + { + public int current_exp { get; set; } + public int current_level { get; set; } + public int current_min { get; set; } + //public int next_exp { get; set; } // 当等级为6时,next_exp为string类型,值为"--" + } + + //public class NavDataOfficial + //{ + // public string desc { get; set; } + // public int role { get; set; } + // public string title { get; set; } + // public int type { get; set; } + //} + + //public class NavDataOfficialVerify + //{ + // public string desc { get; set; } + // public int type { get; set; } + //} + + //public class NavDataPendant + //{ + // public int expire { get; set; } + // public string image { get; set; } + // public string image_enhance { get; set; } + // public string name { get; set; } + // public int pid { get; set; } + //} + + //public class NavDataVipLabel + //{ + // public string label_theme { get; set; } + // public string path { get; set; } + // public string text { get; set; } + //} + + public class NavDataWallet + { + public float bcoin_balance { get; set; } + public float coupon_balance { get; set; } + public long coupon_due_time { get; set; } + public long mid { get; set; } + } + +} diff --git a/src/Core/entity/PlayUrl.cs b/src/Core/entity/PlayUrl.cs new file mode 100644 index 0000000..b9131c9 --- /dev/null +++ b/src/Core/entity/PlayUrl.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; + +// 注释掉未使用的属性 +namespace Core.entity +{ + public class PlayUrl + { + //public int code { get; set; } + public PlayUrlData data { get; set; } + public PlayUrlData result { get; set; } + //public string message { get; set; } + //public int ttl { get; set; } + } + + public class PlayUrlData + { + public List accept_description { get; set; } + //public string accept_format { get; set; } + public List accept_quality { get; set; } + public PlayUrlDataDash dash { get; set; } + //public string format { get; set; } + //public string from { get; set; } + //public string message { get; set; } + public int quality { get; set; } + //public string result { get; set; } + //public string seek_param { get; set; } + //public string seek_type { get; set; } + //public string timelength { get; set; } + //public int video_codecid { get; set; } + + public List durl { get; set; } + } + + public class PlayUrlDataDash + { + public List audio { get; set; } + public long duration { get; set; } + //public float minBufferTime { get; set; } + //public float min_buffer_time { get; set; } + public List video { get; set; } + } + + public class PlayUrlDataDashVideo + { + //public PlayUrlDataDashSegmentBaseCls SegmentBase { get; set; } + public List backupUrl { get; set; } + public List backup_url { get; set; } + //public long bandwidth { get; set; } + public string baseUrl { get; set; } + public string base_url { get; set; } + //public int codecid { get; set; } + public string codecs { get; set; } + public string frameRate { get; set; } + //public string frame_rate { get; set; } + public int height { get; set; } + public int id { get; set; } + public string mimeType { get; set; } + //public string mime_type { get; set; } + //public string sar { get; set; } + //public PlayUrlDataDashSegmentBaseCls2 segment_base { get; set; } + //public int startWithSap { get; set; } + //public int start_with_sap { get; set; } + public int width { get; set; } + } + + //public class PlayUrlDataDashSegmentBaseCls + //{ + // public string Initialization { get; set; } + // public string indexRange { get; set; } + //} + + //public class PlayUrlDataDashSegmentBaseCls2 + //{ + // public string initialization { get; set; } + // public string index_range { get; set; } + //} + + public class PlayUrlDUrl + { + //public int order { get; set; } + public long length { get; set; } + public long size { get; set; } + //public string ahead { get; set; } + //public string vhead { get; set; } + public string url { get; set; } + public List backup_url { get; set; } + } + +} diff --git a/src/Core/entity/Stat.cs b/src/Core/entity/Stat.cs new file mode 100644 index 0000000..60ca53c --- /dev/null +++ b/src/Core/entity/Stat.cs @@ -0,0 +1,21 @@ +namespace Core.entity +{ + // https://api.bilibili.com/x/relation/stat?vmid=42018135 + public class Stat + { + //public int code { get; set; } + public StatData data { get; set; } + //public string message { get; set; } + //public int ttl { get; set; } + } + + public class StatData + { + public int black { get; set; } // 黑名单 + public int follower { get; set; } // 粉丝数 + public int following { get; set; } // 关注数 + public long mid { get; set; } // 用户id + public int whisper { get; set; } // 悄悄关注数 + } + +} diff --git a/src/Core/entity/UserSettings.cs b/src/Core/entity/UserSettings.cs new file mode 100644 index 0000000..39102d0 --- /dev/null +++ b/src/Core/entity/UserSettings.cs @@ -0,0 +1,31 @@ +namespace Core.entity +{ + // https://space.bilibili.com/ajax/settings/getSettings?mid=42018135 + public class UserSettings + { + public UserSettingsData data { get; set; } + public bool status { get; set; } + } + + public class UserSettingsData + { + // …… + + public UserSettingsDataToutu toutu { get; set; } + } + + + public class UserSettingsDataToutu + { + public string android_img { get; set; } + public long expire { get; set; } + public string ipad_img { get; set; } + public string iphone_img { get; set; } + public string l_img { get; set; } + public int platform { get; set; } + public string s_img { get; set; } + public int sid { get; set; } + public string thumbnail_img { get; set; } + } + +} diff --git a/src/Core/entity/VideoDetail.cs b/src/Core/entity/VideoDetail.cs new file mode 100644 index 0000000..4c35beb --- /dev/null +++ b/src/Core/entity/VideoDetail.cs @@ -0,0 +1,28 @@ +namespace Core.entity +{ + // https://api.bilibili.com/x/web-interface/view/detail?bvid=BV1TJ411h7E6 + public class VideoDetail + { + //public int code { get; set; } + public VideoDetailData data { get; set; } + //public string message { get; set; } + //public int ttl { get; set; } + } + + public class VideoDetailData + { + //public VideoDetailDataCard Card { get; set; } + //public VideoDetailDataRelated Related { get; set; } + //public VideoDetailDataReply Reply { get; set; } + //public VideoDetailDataTags Tags { get; set; } + public VideoDetailDataView View { get; set; } + } + + public class VideoDetailDataView + { + // ... + public string redirect_url { get; set; } + // ... + } + +} diff --git a/src/Core/entity/VideoView.cs b/src/Core/entity/VideoView.cs new file mode 100644 index 0000000..0f6989b --- /dev/null +++ b/src/Core/entity/VideoView.cs @@ -0,0 +1,207 @@ +using System.Collections.Generic; + +// 注释掉未使用的属性 +namespace Core.entity +{ + public class VideoView + { + public int code { get; set; } + public VideoViewData data { get; set; } + public string message { get; set; } + public int ttl { get; set; } + } + + public class VideoViewData + { + public long aid { get; set; } + //public long attribute { get; set; } + public string bvid { get; set; } + //public long cid { get; set; } + //public int copyright { get; set; } + public long ctime { get; set; } + public string desc { get; set; } + //public VideoViewDataDimension dimension { get; set; } + //public long duration { get; set; } + //public string dynamic { get; set; } + //public VideoViewDataLabel label { get; set; } + //public long mission_id { get; set; } + //public bool no_cache { get; set; } + public VideoViewDataOwner owner { get; set; } + public List pages { get; set; } + public string pic { get; set; } + public long pubdate { get; set; } + //public VideoViewDataRights rights { get; set; } + //public long season_id { get; set; } + public VideoViewDataStat stat { get; set; } + //public int state { get; set; } + //public VideoViewDataSubtitle subtitle { get; set; } + public long tid { get; set; } + public string title { get; set; } + public string tname { get; set; } + //public VideoViewDataUgcSeason ugc_season { get; set; } + //public int videos { get; set; } + } + + public class VideoViewDataDimension + { + public int width { get; set; } + public int height { get; set; } + //public int rotate { get; set; } + } + + //public class VideoViewDataLabel + //{ + // public int type { get; set; } + //} + + public class VideoViewDataOwner + { + public string face { get; set; } + public long mid { get; set; } + public string name { get; set; } + } + + public class VideoViewDataPages + { + public long cid { get; set; } + public VideoViewDataDimension dimension { get; set; } + public long duration { get; set; } + //public string from { get; set; } + public int page { get; set; } + public string part { get; set; } + //public string vid { get; set; } + //public string weblink { get; set; } + } + + //public class VideoViewDataRights + //{ + // public int autoplay { get; set; } + // public int bp { get; set; } + // public int download { get; set; } + // public int elec { get; set; } + // public int hd5 { get; set; } + // public int is_cooperation { get; set; } + // public int movie { get; set; } + // public int no_background { get; set; } + // public int no_reprint { get; set; } + // public int pay { get; set; } + // public int ugc_pay { get; set; } + // public int ugc_pay_preview { get; set; } + //} + + public class VideoViewDataStat + { + public long aid { get; set; } + public long coin { get; set; } + public long danmaku { get; set; } + public long dislike { get; set; } + public string evaluation { get; set; } + public long favorite { get; set; } + public long his_rank { get; set; } + public long like { get; set; } + public long now_rank { get; set; } + public long reply { get; set; } + public long share { get; set; } + public long view { get; set; } + } + + //public class VideoViewDataSubtitle + //{ + // public bool allow_submit { get; set; } + // //public List list { get; set; } + //} + + //public class VideoViewDataUgcSeason + //{ + // public int attribute { get; set; } + // public string cover { get; set; } + // public int ep_count { get; set; } + // public long id { get; set; } + // public string intro { get; set; } + // public long mid { get; set; } + // public List sections { get; set; } + // public int sign_state { get; set; } + // public VideoViewDataStat stat { get; set; } + // public string title { get; set; } + //} + + //public class VideoViewDataUgcSeasonSections + //{ + // public List episodes { get; set; } + // public long id { get; set; } + // public long season_id { get; set; } + // public string title { get; set; } + // public int type { get; set; } + //} + + //public class VideoViewDataUgcSeasonStat + //{ + // public long coin { get; set; } + // public long danmaku { get; set; } + // public long favorite { get; set; } + // public long his_rank { get; set; } + // public long like { get; set; } + // public long now_rank { get; set; } + // public long reply { get; set; } + // public long season_id { get; set; } + // public long share { get; set; } + // public long view { get; set; } + //} + + //public class VideoViewDataUgcSeasonSectionsEpisodes + //{ + // public long aid { get; set; } + // public VideoViewDataUgcSeasonSectionsEpisodesArc arc { get; set; } + // public int attribute { get; set; } + // public long cid { get; set; } + // public long id { get; set; } + // public VideoViewDataUgcSeasonSectionsEpisodesPage page { get; set; } + // public long season_id { get; set; } + // public long section_id { get; set; } + // public string title { get; set; } + //} + + //public class VideoViewDataUgcSeasonSectionsEpisodesArc + //{ + // public long aid { get; set; } + // public int copyright { get; set; } + // public long ctime { get; set; } + // public string desc { get; set; } + // public VideoViewDataDimension dimension { get; set; } + // public long duration { get; set; } + // public string dynamic { get; set; } + // public VideoViewDataOwner owner { get; set; } + // public string pic { get; set; } + // public long pubdate { get; set; } + // public VideoViewDataRights rights { get; set; } + // public VideoViewDataStat stat { get; set; } + // public int state { get; set; } + // public long tid { get; set; } + // public string title { get; set; } + // public string tname { get; set; } + // public int videos { get; set; } + //} + + //public class VideoViewDataUgcSeasonSectionsEpisodesPage + //{ + // public long cid { get; set; } + // public VideoViewDataDimension dimension { get; set; } + // public long duration { get; set; } + // public string from { get; set; } + // public int page { get; set; } + // public string part { get; set; } + // public string vid { get; set; } + // public string weblink { get; set; } + //} + + + // https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp + public class Pagelist + { + public int code { get; set; } + public List data { get; set; } + public string message { get; set; } + public int ttl { get; set; } + } + +} diff --git a/src/Core/entity2/login/LoginStatus.cs b/src/Core/entity2/login/LoginStatus.cs new file mode 100644 index 0000000..4274f0a --- /dev/null +++ b/src/Core/entity2/login/LoginStatus.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json; + +namespace Core.entity2.login +{ + // https://passport.bilibili.com/qrcode/getLoginInfo + [JsonObject] + public class LoginStatus : BaseEntity + { + [JsonProperty("code")] + public int Code { get; set; } + [JsonProperty("status")] + public bool Status { get; set; } + [JsonProperty("message")] + public string Message { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + } + + [JsonObject] + public class LoginStatusScanning : BaseEntity + { + [JsonProperty("status")] + public bool Status { get; set; } + [JsonProperty("data")] + public int Data { get; set; } + [JsonProperty("message")] + public string Message { get; set; } + } + + [JsonObject] + public class LoginStatusReady : BaseEntity + { + [JsonProperty("code")] + public int Code { get; set; } + [JsonProperty("status")] + public bool Status { get; set; } + //public long ts { get; set; } + [JsonProperty("data")] + public LoginStatusData Data { get; set; } + } + + [JsonObject] + public class LoginStatusData : BaseEntity + { + [JsonProperty("url")] + public string Url { get; set; } + } + +} diff --git a/src/Core/entity2/login/LoginUrl.cs b/src/Core/entity2/login/LoginUrl.cs new file mode 100644 index 0000000..2f8ac90 --- /dev/null +++ b/src/Core/entity2/login/LoginUrl.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Core.entity2.login +{ + // https://passport.bilibili.com/qrcode/getLoginUrl + [JsonObject] + public class LoginUrlOrigin : BaseEntity + { + //public int code { get; set; } + [JsonProperty("data")] + public LoginUrl Data { get; set; } + [JsonProperty("status")] + public bool Status { get; set; } + //public long ts { get; set; } + } + + [JsonObject] + public class LoginUrl : BaseEntity + { + [JsonProperty("oauthKey")] + public string OauthKey { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + } + +} diff --git a/src/Core/entity2/users/SpacePublication.cs b/src/Core/entity2/users/SpacePublication.cs index d54afd5..13569cc 100644 --- a/src/Core/entity2/users/SpacePublication.cs +++ b/src/Core/entity2/users/SpacePublication.cs @@ -65,6 +65,8 @@ namespace Core.entity2.users public SpacePublicationListTypeVideoZone Technology { get; set; } [JsonProperty("188")] public SpacePublicationListTypeVideoZone Digital { get; set; } + [JsonProperty("223")] + public SpacePublicationListTypeVideoZone Car { get; set; } [JsonProperty("160")] public SpacePublicationListTypeVideoZone Life { get; set; } [JsonProperty("211")] diff --git a/src/Core/settings/ReadMe.md b/src/Core/settings/ReadMe.md new file mode 100644 index 0000000..87b3072 --- /dev/null +++ b/src/Core/settings/ReadMe.md @@ -0,0 +1,167 @@ +# 设置 + +Settings类位于`Core.settings`命名空间中,采用单例模式调用。设置属性使用json格式保存,并将加密后的字符串保存到文件。 + +| 文件名 | 内容 | +| ------------------- | -------------------------------------- | +| Settings.cs | Settings类的基本属性,单例模式的代码等 | +| Settings.class.cs | SettingsEntity类等 | +| Settings.base.cs | `基本`设置的代码 | +| Settings.network.cs | `网络`设置的代码 | +| Settings.video.cs | `视频`设置的代码 | +| Settings.about.cs | `关于`设置的代码 | + +## 基本 + +获取/设置下载完成后的操作。 + +```c# +public AfterDownloadOperation GetAfterDownloadOperation(); +public bool SetAfterDownloadOperation(AfterDownloadOperation afterDownload); +``` + +## 网络 + +获取/设置是否解除地区限制。 + +```c# +public LiftingOfRegion IsLiftingOfRegion(); +public bool IsLiftingOfRegion(LiftingOfRegion isLiftingOfRegion); +``` + +获取/设置Aria服务器的端口号。 + +```c# +public int GetAriaListenPort(); +public bool SetAriaListenPort(int ariaListenPort); +``` + +获取/设置Aria日志等级。 + +```c# +public AriaConfigLogLevel GetAriaLogLevel(); +public bool SetAriaLogLevel(AriaConfigLogLevel ariaLogLevel); +``` + +获取/设置Aria最大同时下载数(任务数)。 + +```c# +public int GetAriaMaxConcurrentDownloads(); +public bool SetAriaMaxConcurrentDownloads(int ariaMaxConcurrentDownloads); +``` + +获取/设置Aria单文件最大线程数。 + +```c# +public int GetAriaSplit(); +public bool SetAriaSplit(int ariaSplit); +``` + +获取/设置Aria下载速度限制。 + +```c# +public int GetAriaMaxOverallDownloadLimit(); +public bool SetAriaMaxOverallDownloadLimit(int ariaMaxOverallDownloadLimit); +``` + +获取/设置Aria下载单文件速度限制。 + +```c# +public int GetAriaMaxDownloadLimit(); +public bool SetAriaMaxDownloadLimit(int ariaMaxDownloadLimit); +``` + +获取/设置Aria文件预分配。 + +```c# +public AriaConfigFileAllocation GetAriaFileAllocation(); +public bool SetAriaFileAllocation(AriaConfigFileAllocation ariaFileAllocation); +``` + +获取/设置是否开启Aria http代理。 + +```c# +public AriaHttpProxy IsAriaHttpProxy(); +public bool IsAriaHttpProxy(AriaHttpProxy isAriaHttpProxy); +``` + +获取/设置Aria的http代理的地址。 + +```c# +public string GetAriaHttpProxy(); +public bool SetAriaHttpProxy(string ariaHttpProxy); +``` + +获取/设置Aria的http代理的端口。 + +```c# +public int GetAriaHttpProxyListenPort(); +public bool SetAriaHttpProxyListenPort(int ariaHttpProxyListenPort); +``` + +## 视频 + +获取/设置优先下载的视频编码。 + +```c# +public VideoCodecs GetVideoCodecs(); +public bool SetVideoCodecs(VideoCodecs videoCodecs); +``` + +获取/设置优先下载画质。 + +```c# +public int GetQuality(); +public bool SetQuality(int quality); +``` + +获取/设置是否给视频增加序号。 + +```c# +public AddOrder IsAddOrder(); +public bool IsAddOrder(AddOrder isAddOrder); +``` + +获取/设置是否下载flv视频后转码为mp4。 + +```c# +public TranscodingFlvToMp4 IsTranscodingFlvToMp4(); +public bool IsTranscodingFlvToMp4(TranscodingFlvToMp4 isTranscodingFlvToMp4); +``` + +获取/设置下载目录。 + +```c# +public string GetSaveVideoRootPath(); +public bool SetSaveVideoRootPath(string path); +``` + +获取/设置是否使用默认下载目录。 + +```c# +public UseSaveVideoRootPath IsUseSaveVideoRootPath(); +public bool IsUseSaveVideoRootPath(UseSaveVideoRootPath isUseSaveVideoRootPath); +``` + +获取/设置是否为不同视频分别创建文件夹。 + +```c# +public CreateFolderForMedia IsCreateFolderForMedia(); +public bool IsCreateFolderForMedia(CreateFolderForMedia isCreateFolderForMedia); +``` + +## 关于 + +获取/设置是否接收测试版更新。 + +```c# +public ReceiveBetaVersion IsReceiveBetaVersion(); +public bool IsReceiveBetaVersion(ReceiveBetaVersion isReceiveBetaVersion); +``` + +获取/设置是否允许启动时检查更新。 + +```c# +public AutoUpdateWhenLaunch GetAutoUpdateWhenLaunch(); +public bool SetAutoUpdateWhenLaunch(AutoUpdateWhenLaunch autoUpdateWhenLaunch); +``` diff --git a/src/Core/settings/Settings.about.cs b/src/Core/settings/Settings.about.cs new file mode 100644 index 0000000..abc3707 --- /dev/null +++ b/src/Core/settings/Settings.about.cs @@ -0,0 +1,65 @@ +namespace Core.settings +{ + public partial class Settings + { + // 是否接收测试版更新 + private readonly ALLOW_STATUS isReceiveBetaVersion = ALLOW_STATUS.NO; + + // 是否在启动时自动检查更新 + private readonly ALLOW_STATUS autoUpdateWhenLaunch = ALLOW_STATUS.YES; + + + /// + /// 获取是否接收测试版更新 + /// + /// + public ALLOW_STATUS IsReceiveBetaVersion() + { + if (settingsEntity.IsReceiveBetaVersion == 0) + { + // 第一次获取,先设置默认值 + IsReceiveBetaVersion(isReceiveBetaVersion); + return isReceiveBetaVersion; + } + return settingsEntity.IsReceiveBetaVersion; + } + + /// + /// 设置是否接收测试版更新 + /// + /// + /// + public bool IsReceiveBetaVersion(ALLOW_STATUS isReceiveBetaVersion) + { + settingsEntity.IsReceiveBetaVersion = isReceiveBetaVersion; + return SetEntity(); + } + + /// + /// 获取是否允许启动时检查更新 + /// + /// + public ALLOW_STATUS GetAutoUpdateWhenLaunch() + { + if (settingsEntity.AutoUpdateWhenLaunch == 0) + { + // 第一次获取,先设置默认值 + SetAutoUpdateWhenLaunch(autoUpdateWhenLaunch); + return autoUpdateWhenLaunch; + } + return settingsEntity.AutoUpdateWhenLaunch; + } + + /// + /// 设置是否允许启动时检查更新 + /// + /// + /// + public bool SetAutoUpdateWhenLaunch(ALLOW_STATUS autoUpdateWhenLaunch) + { + settingsEntity.AutoUpdateWhenLaunch = autoUpdateWhenLaunch; + return SetEntity(); + } + + } +} diff --git a/src/Core/settings/Settings.base.cs b/src/Core/settings/Settings.base.cs new file mode 100644 index 0000000..211a093 --- /dev/null +++ b/src/Core/settings/Settings.base.cs @@ -0,0 +1,123 @@ +namespace Core.settings +{ + public partial class Settings + { + // 默认下载完成后的操作 + private readonly AfterDownloadOperation afterDownload = AfterDownloadOperation.NONE; + + // 是否监听剪贴板 + private readonly ALLOW_STATUS isListenClipboard = ALLOW_STATUS.YES; + + // 视频详情页面是否自动解析 + private readonly ALLOW_STATUS isAutoParseVideo = ALLOW_STATUS.NO; + + // 下载完成列表排序 + private readonly DownloadFinishedSort finishedSort = DownloadFinishedSort.DOWNLOAD; + + + /// + /// 获取下载完成后的操作 + /// + /// + public AfterDownloadOperation GetAfterDownloadOperation() + { + if (settingsEntity.AfterDownload == 0) + { + // 第一次获取,先设置默认值 + SetAfterDownloadOperation(afterDownload); + return afterDownload; + } + return settingsEntity.AfterDownload; + } + + /// + /// 设置下载完成后的操作 + /// + /// + /// + public bool SetAfterDownloadOperation(AfterDownloadOperation afterDownload) + { + settingsEntity.AfterDownload = afterDownload; + return SetEntity(); + } + + /// + /// 是否监听剪贴板 + /// + /// + public ALLOW_STATUS IsListenClipboard() + { + if (settingsEntity.IsListenClipboard == 0) + { + // 第一次获取,先设置默认值 + IsListenClipboard(isListenClipboard); + return isListenClipboard; + } + return settingsEntity.IsListenClipboard; + } + + /// + /// 是否监听剪贴板 + /// + /// + /// + public bool IsListenClipboard(ALLOW_STATUS isListen) + { + settingsEntity.IsListenClipboard = isListen; + return SetEntity(); + } + + /// + /// 视频详情页面是否自动解析 + /// + /// + public ALLOW_STATUS IsAutoParseVideo() + { + if (settingsEntity.IsAutoParseVideo == 0) + { + // 第一次获取,先设置默认值 + IsAutoParseVideo(isAutoParseVideo); + return isAutoParseVideo; + } + return settingsEntity.IsAutoParseVideo; + } + + /// + /// 视频详情页面是否自动解析 + /// + /// + /// + public bool IsAutoParseVideo(ALLOW_STATUS IsAuto) + { + settingsEntity.IsAutoParseVideo = IsAuto; + return SetEntity(); + } + + /// + /// 获取下载完成列表排序 + /// + /// + public DownloadFinishedSort GetDownloadFinishedSort() + { + if (settingsEntity.DownloadFinishedSort == 0) + { + // 第一次获取,先设置默认值 + SetDownloadFinishedSort(finishedSort); + return finishedSort; + } + return settingsEntity.DownloadFinishedSort; + } + + /// + /// 设置下载完成列表排序 + /// + /// + /// + public bool SetDownloadFinishedSort(DownloadFinishedSort finishedSort) + { + settingsEntity.DownloadFinishedSort = finishedSort; + return SetEntity(); + } + + } +} diff --git a/src/Core/settings/Settings.class.cs b/src/Core/settings/Settings.class.cs new file mode 100644 index 0000000..02a1ccd --- /dev/null +++ b/src/Core/settings/Settings.class.cs @@ -0,0 +1,90 @@ +using Core.aria2cNet.server; + +namespace Core.settings +{ + public class SettingsEntity + { + public UserInfoForSetting UserInfo { get; set; } + + // 基本 + public AfterDownloadOperation AfterDownload { get; set; } + public ALLOW_STATUS IsListenClipboard { get; set; } + public ALLOW_STATUS IsAutoParseVideo { get; set; } + public DownloadFinishedSort DownloadFinishedSort { get; set; } + + // 网络 + public ALLOW_STATUS IsLiftingOfRegion { get; set; } + public int AriaListenPort { get; set; } + public AriaConfigLogLevel AriaLogLevel { get; set; } + public int AriaMaxConcurrentDownloads { get; set; } + public int AriaSplit { get; set; } + public int AriaMaxOverallDownloadLimit { get; set; } + public int AriaMaxDownloadLimit { get; set; } + public AriaConfigFileAllocation AriaFileAllocation { get; set; } + + public ALLOW_STATUS IsAriaHttpProxy { get; set; } + public string AriaHttpProxy { get; set; } + public int AriaHttpProxyListenPort { get; set; } + + // 视频 + public VideoCodecs VideoCodecs { get; set; } + public int Quality { get; set; } + public ALLOW_STATUS IsAddOrder { get; set; } + public ALLOW_STATUS IsTranscodingFlvToMp4 { get; set; } + public string SaveVideoRootPath { get; set; } + public ALLOW_STATUS IsUseSaveVideoRootPath { get; set; } + public ALLOW_STATUS IsCreateFolderForMedia { get; set; } + public ALLOW_STATUS IsDownloadDanmaku { get; set; } + public ALLOW_STATUS IsDownloadCover { get; set; } + + // 弹幕 + public ALLOW_STATUS DanmakuTopFilter { get; set; } + public ALLOW_STATUS DanmakuBottomFilter { get; set; } + public ALLOW_STATUS DanmakuScrollFilter { get; set; } + public ALLOW_STATUS IsCustomDanmakuResolution { get; set; } + public int DanmakuScreenWidth { get; set; } + public int DanmakuScreenHeight { get; set; } + public string DanmakuFontName { get; set; } + public int DanmakuFontSize { get; set; } + public int DanmakuLineCount { get; set; } + public DanmakuLayoutAlgorithm DanmakuLayoutAlgorithm { get; set; } + + // 关于 + public ALLOW_STATUS IsReceiveBetaVersion { get; set; } + public ALLOW_STATUS AutoUpdateWhenLaunch { get; set; } + } + + public class UserInfoForSetting + { + public long Mid { get; set; } + public string Name { get; set; } + public bool IsLogin { get; set; } // 是否登录 + public bool IsVip { get; set; } // 是否为大会员,未登录时为false + } + + public enum AfterDownloadOperation + { + NONE = 1, OPEN_FOLDER, CLOSE_APP, CLOSE_SYSTEM + } + + public enum ALLOW_STATUS + { + NONE = 0, YES, NO + } + + public enum DownloadFinishedSort + { + DOWNLOAD = 0, NUMBER + } + + public enum VideoCodecs + { + NONE = 0, AVC, HEVC + } + + public enum DanmakuLayoutAlgorithm + { + NONE = 0, ASYNC, SYNC + } + +} diff --git a/src/Core/settings/Settings.cs b/src/Core/settings/Settings.cs new file mode 100644 index 0000000..bfef7d8 --- /dev/null +++ b/src/Core/settings/Settings.cs @@ -0,0 +1,132 @@ +using Newtonsoft.Json; +using System; +using System.IO; + +namespace Core.settings +{ + public partial class Settings + { + private static Settings instance; + + /// + /// 获取Settings实例 + /// + /// + public static Settings GetInstance() + { + if (instance == null) + { + instance = new Settings(); + } + return instance; + } + + /// + /// 隐藏Settings()方法,必须使用单例模式 + /// + private Settings() + { + settingsEntity = GetEntity(); + } + + + // 内存中保存一份配置 + private readonly SettingsEntity settingsEntity; + + // 设置的配置文件 + private readonly string settingsName = Common.ConfigPath + "Settings"; + + // 密钥 + private readonly string password = "QA!M^gE@"; + + // 登录用户的mid + private readonly UserInfoForSetting userInfo = new UserInfoForSetting + { + Mid = -1, + Name = "", + IsLogin = false, + IsVip = false + }; + + private SettingsEntity GetEntity() + { + try + { + StreamReader streamReader = File.OpenText(settingsName); + string jsonWordTemplate = streamReader.ReadToEnd(); + streamReader.Close(); + + // 解密字符串 + jsonWordTemplate = Encryptor.DecryptString(jsonWordTemplate, password); + + return JsonConvert.DeserializeObject(jsonWordTemplate); + } + catch (FileNotFoundException e) + { + Console.WriteLine("GetEntity()发生异常: {0}", e); + return new SettingsEntity(); + } + catch (DirectoryNotFoundException e) + { + Console.WriteLine("GetEntity()发生异常: {0}", e); + return new SettingsEntity(); + } + catch (Exception e) + { + Console.WriteLine("GetEntity()发生异常: {0}", e); + return new SettingsEntity(); + } + } + + private bool SetEntity() + { + if (!Directory.Exists(Common.ConfigPath)) + { + Directory.CreateDirectory(Common.ConfigPath); + } + + string json = JsonConvert.SerializeObject(settingsEntity); + + // 加密字符串 + json = Encryptor.EncryptString(json, password); + try + { + File.WriteAllText(settingsName, json); + return true; + } + catch (IOException e) + { + Console.WriteLine("SetEntity()发生异常: {0}", e); + return false; + } + } + + /// + /// 获取登录用户信息 + /// + /// + public UserInfoForSetting GetUserInfo() + { + if (settingsEntity.UserInfo == null) + { + // 第一次获取,先设置默认值 + SetUserInfo(userInfo); + return userInfo; + } + return settingsEntity.UserInfo; + } + + /// + /// 设置中保存登录用户的信息,在index刷新用户状态时使用 + /// + /// + /// + public bool SetUserInfo(UserInfoForSetting userInfo) + { + settingsEntity.UserInfo = userInfo; + return SetEntity(); + } + + + } +} diff --git a/src/Core/settings/Settings.danmaku.cs b/src/Core/settings/Settings.danmaku.cs new file mode 100644 index 0000000..0adff0e --- /dev/null +++ b/src/Core/settings/Settings.danmaku.cs @@ -0,0 +1,298 @@ +namespace Core.settings +{ + public partial class Settings + { + // 是否屏蔽顶部弹幕 + private readonly ALLOW_STATUS danmakuTopFilter = ALLOW_STATUS.NO; + + // 是否屏蔽底部弹幕 + private readonly ALLOW_STATUS danmakuBottomFilter = ALLOW_STATUS.NO; + + // 是否屏蔽滚动弹幕 + private readonly ALLOW_STATUS danmakuScrollFilter = ALLOW_STATUS.NO; + + // 是否自定义分辨率 + private readonly ALLOW_STATUS isCustomDanmakuResolution = ALLOW_STATUS.NO; + + // 分辨率-宽 + private readonly int danmakuScreenWidth = 1920; + + // 分辨率-高 + private readonly int danmakuScreenHeight = 1080; + + // 弹幕字体 + private readonly string danmakuFontName = "黑体"; + + // 弹幕字体大小 + private readonly int danmakuFontSize = 50; + + // 弹幕限制行数 + private readonly int danmakuLineCount = 0; + + // 弹幕布局算法 + private readonly DanmakuLayoutAlgorithm danmakuLayoutAlgorithm = DanmakuLayoutAlgorithm.SYNC; + + + /// + /// 获取是否屏蔽顶部弹幕 + /// + /// + public ALLOW_STATUS GetDanmakuTopFilter() + { + if (settingsEntity.DanmakuTopFilter == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuTopFilter(danmakuTopFilter); + return danmakuTopFilter; + } + return settingsEntity.DanmakuTopFilter; + } + + /// + /// 设置是否屏蔽顶部弹幕 + /// + /// + /// + public bool SetDanmakuTopFilter(ALLOW_STATUS danmakuFilter) + { + settingsEntity.DanmakuTopFilter = danmakuFilter; + return SetEntity(); + } + + /// + /// 获取是否屏蔽底部弹幕 + /// + /// + public ALLOW_STATUS GetDanmakuBottomFilter() + { + if (settingsEntity.DanmakuBottomFilter == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuBottomFilter(danmakuBottomFilter); + return danmakuBottomFilter; + } + return settingsEntity.DanmakuBottomFilter; + } + + /// + /// 设置是否屏蔽底部弹幕 + /// + /// + /// + public bool SetDanmakuBottomFilter(ALLOW_STATUS danmakuFilter) + { + settingsEntity.DanmakuBottomFilter = danmakuFilter; + return SetEntity(); + } + + /// + /// 获取是否屏蔽滚动弹幕 + /// + /// + public ALLOW_STATUS GetDanmakuScrollFilter() + { + if (settingsEntity.DanmakuScrollFilter == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuScrollFilter(danmakuScrollFilter); + return danmakuScrollFilter; + } + return settingsEntity.DanmakuScrollFilter; + } + + /// + /// 设置是否屏蔽滚动弹幕 + /// + /// + /// + public bool SetDanmakuScrollFilter(ALLOW_STATUS danmakuFilter) + { + settingsEntity.DanmakuScrollFilter = danmakuFilter; + return SetEntity(); + } + + /// + /// 获取是否自定义分辨率 + /// + /// + public ALLOW_STATUS IsCustomDanmakuResolution() + { + if (settingsEntity.IsCustomDanmakuResolution == 0) + { + // 第一次获取,先设置默认值 + IsCustomDanmakuResolution(isCustomDanmakuResolution); + return isCustomDanmakuResolution; + } + return settingsEntity.IsCustomDanmakuResolution; + } + + /// + /// 设置是否自定义分辨率 + /// + /// + /// + public bool IsCustomDanmakuResolution(ALLOW_STATUS isCustomResolution) + { + settingsEntity.IsCustomDanmakuResolution = isCustomResolution; + return SetEntity(); + } + + /// + /// 获取分辨率-宽 + /// + /// + public int GetDanmakuScreenWidth() + { + if (settingsEntity.DanmakuScreenWidth == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuScreenWidth(danmakuScreenWidth); + return danmakuScreenWidth; + } + return settingsEntity.DanmakuScreenWidth; + } + + /// + /// 设置分辨率-宽 + /// + /// + /// + public bool SetDanmakuScreenWidth(int screenWidth) + { + settingsEntity.DanmakuScreenWidth = screenWidth; + return SetEntity(); + } + + /// + /// 获取分辨率-高 + /// + /// + public int GetDanmakuScreenHeight() + { + if (settingsEntity.DanmakuScreenHeight == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuScreenHeight(danmakuScreenHeight); + return danmakuScreenHeight; + } + return settingsEntity.DanmakuScreenHeight; + } + + /// + /// 设置分辨率-高 + /// + /// + /// + public bool SetDanmakuScreenHeight(int screenHeight) + { + settingsEntity.DanmakuScreenHeight = screenHeight; + return SetEntity(); + } + + /// + /// 获取弹幕字体 + /// + /// + public string GetDanmakuFontName() + { + if (settingsEntity.DanmakuFontName == null) + { + // 第一次获取,先设置默认值 + SetDanmakuFontName(danmakuFontName); + return danmakuFontName; + } + return settingsEntity.DanmakuFontName; + } + + /// + /// 设置弹幕字体 + /// + /// + /// + public bool SetDanmakuFontName(string danmakuFontName) + { + settingsEntity.DanmakuFontName = danmakuFontName; + return SetEntity(); + } + + /// + /// 获取弹幕字体大小 + /// + /// + public int GetDanmakuFontSize() + { + if (settingsEntity.DanmakuFontSize == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuFontSize(danmakuFontSize); + return danmakuFontSize; + } + return settingsEntity.DanmakuFontSize; + } + + /// + /// 设置弹幕字体大小 + /// + /// + /// + public bool SetDanmakuFontSize(int danmakuFontSize) + { + settingsEntity.DanmakuFontSize = danmakuFontSize; + return SetEntity(); + } + + /// + /// 获取弹幕限制行数 + /// + /// + public int GetDanmakuLineCount() + { + if (settingsEntity.DanmakuLineCount == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuLineCount(danmakuLineCount); + return danmakuLineCount; + } + return settingsEntity.DanmakuLineCount; + } + + /// + /// 设置弹幕限制行数 + /// + /// + /// + public bool SetDanmakuLineCount(int danmakuLineCount) + { + settingsEntity.DanmakuLineCount = danmakuLineCount; + return SetEntity(); + } + + /// + /// 获取弹幕布局算法 + /// + /// + public DanmakuLayoutAlgorithm GetDanmakuLayoutAlgorithm() + { + if (settingsEntity.DanmakuLayoutAlgorithm == 0) + { + // 第一次获取,先设置默认值 + SetDanmakuLayoutAlgorithm(danmakuLayoutAlgorithm); + return danmakuLayoutAlgorithm; + } + return settingsEntity.DanmakuLayoutAlgorithm; + } + + /// + /// 设置弹幕布局算法 + /// + /// + /// + public bool SetDanmakuLayoutAlgorithm(DanmakuLayoutAlgorithm danmakuLayoutAlgorithm) + { + settingsEntity.DanmakuLayoutAlgorithm = danmakuLayoutAlgorithm; + return SetEntity(); + } + + + } +} diff --git a/src/Core/settings/Settings.network.cs b/src/Core/settings/Settings.network.cs new file mode 100644 index 0000000..34a44d7 --- /dev/null +++ b/src/Core/settings/Settings.network.cs @@ -0,0 +1,324 @@ +using Core.aria2cNet.server; + +namespace Core.settings +{ + public partial class Settings + { + // 是否开启解除地区限制 + private readonly ALLOW_STATUS isLiftingOfRegion = ALLOW_STATUS.YES; + + // Aria服务器端口号 + private readonly int ariaListenPort = 6800; + + // Aria日志等级 + private readonly AriaConfigLogLevel ariaLogLevel = AriaConfigLogLevel.INFO; + + // Aria最大同时下载数(任务数) + private readonly int ariaMaxConcurrentDownloads = 3; + + // Aria单文件最大线程数 + private readonly int ariaSplit = 5; + + // Aria下载速度限制 + private readonly int ariaMaxOverallDownloadLimit = 0; + + // Aria下载单文件速度限制 + private readonly int ariaMaxDownloadLimit = 0; + + // Aria文件预分配 + private readonly AriaConfigFileAllocation ariaFileAllocation = AriaConfigFileAllocation.PREALLOC; + + // Aria HttpProxy代理 + private readonly ALLOW_STATUS isAriaHttpProxy = ALLOW_STATUS.NO; + private readonly string ariaHttpProxy = ""; + private readonly int ariaHttpProxyListenPort = 0; + + + /// + /// 获取是否解除地区限制 + /// + /// + public ALLOW_STATUS IsLiftingOfRegion() + { + if (settingsEntity.IsLiftingOfRegion == 0) + { + // 第一次获取,先设置默认值 + IsLiftingOfRegion(isLiftingOfRegion); + return isLiftingOfRegion; + } + return settingsEntity.IsLiftingOfRegion; + } + + /// + /// 设置是否解除地区限制 + /// + /// + /// + public bool IsLiftingOfRegion(ALLOW_STATUS isLiftingOfRegion) + { + settingsEntity.IsLiftingOfRegion = isLiftingOfRegion; + return SetEntity(); + } + + /// + /// 获取Aria服务器的端口号 + /// + /// + public int GetAriaListenPort() + { + if (settingsEntity.AriaListenPort == 0) + { + // 第一次获取,先设置默认值 + SetAriaListenPort(ariaListenPort); + return ariaListenPort; + } + return settingsEntity.AriaListenPort; + } + + /// + /// 设置Aria服务器的端口号 + /// + /// + /// + public bool SetAriaListenPort(int ariaListenPort) + { + settingsEntity.AriaListenPort = ariaListenPort; + return SetEntity(); + } + + /// + /// 获取Aria日志等级 + /// + /// + public AriaConfigLogLevel GetAriaLogLevel() + { + if (settingsEntity.AriaLogLevel == 0) + { + // 第一次获取,先设置默认值 + SetAriaLogLevel(ariaLogLevel); + return ariaLogLevel; + } + return settingsEntity.AriaLogLevel; + } + + /// + /// 设置Aria日志等级 + /// + /// + /// + public bool SetAriaLogLevel(AriaConfigLogLevel ariaLogLevel) + { + settingsEntity.AriaLogLevel = ariaLogLevel; + return SetEntity(); + } + + /// + /// 获取Aria最大同时下载数(任务数) + /// + /// + public int GetAriaMaxConcurrentDownloads() + { + if (settingsEntity.AriaMaxConcurrentDownloads == 0) + { + // 第一次获取,先设置默认值 + SetAriaMaxConcurrentDownloads(ariaMaxConcurrentDownloads); + return ariaMaxConcurrentDownloads; + } + return settingsEntity.AriaMaxConcurrentDownloads; + } + + /// + /// 设置Aria最大同时下载数(任务数) + /// + /// + /// + public bool SetAriaMaxConcurrentDownloads(int ariaMaxConcurrentDownloads) + { + settingsEntity.AriaMaxConcurrentDownloads = ariaMaxConcurrentDownloads; + return SetEntity(); + } + + /// + /// 获取Aria单文件最大线程数 + /// + /// + public int GetAriaSplit() + { + if (settingsEntity.AriaSplit == 0) + { + // 第一次获取,先设置默认值 + SetAriaSplit(ariaSplit); + return ariaSplit; + } + return settingsEntity.AriaSplit; + } + + /// + /// 设置Aria单文件最大线程数 + /// + /// + /// + public bool SetAriaSplit(int ariaSplit) + { + settingsEntity.AriaSplit = ariaSplit; + return SetEntity(); + } + + /// + /// 获取Aria下载速度限制 + /// + /// + public int GetAriaMaxOverallDownloadLimit() + { + if (settingsEntity.AriaMaxOverallDownloadLimit == 0) + { + // 第一次获取,先设置默认值 + SetAriaMaxOverallDownloadLimit(ariaMaxOverallDownloadLimit); + return ariaMaxOverallDownloadLimit; + } + return settingsEntity.AriaMaxOverallDownloadLimit; + } + + /// + /// 设置Aria下载速度限制 + /// + /// + /// + public bool SetAriaMaxOverallDownloadLimit(int ariaMaxOverallDownloadLimit) + { + settingsEntity.AriaMaxOverallDownloadLimit = ariaMaxOverallDownloadLimit; + return SetEntity(); + } + + /// + /// 获取Aria下载单文件速度限制 + /// + /// + public int GetAriaMaxDownloadLimit() + { + if (settingsEntity.AriaMaxDownloadLimit == 0) + { + // 第一次获取,先设置默认值 + SetAriaMaxDownloadLimit(ariaMaxDownloadLimit); + return ariaMaxDownloadLimit; + } + return settingsEntity.AriaMaxDownloadLimit; + } + + /// + /// 设置Aria下载单文件速度限制 + /// + /// + /// + public bool SetAriaMaxDownloadLimit(int ariaMaxDownloadLimit) + { + settingsEntity.AriaMaxDownloadLimit = ariaMaxDownloadLimit; + return SetEntity(); + } + + /// + /// 获取Aria文件预分配 + /// + /// + public AriaConfigFileAllocation GetAriaFileAllocation() + { + if (settingsEntity.AriaFileAllocation == 0) + { + // 第一次获取,先设置默认值 + SetAriaFileAllocation(ariaFileAllocation); + return ariaFileAllocation; + } + return settingsEntity.AriaFileAllocation; + } + + /// + /// 设置Aria文件预分配 + /// + /// + /// + public bool SetAriaFileAllocation(AriaConfigFileAllocation ariaFileAllocation) + { + settingsEntity.AriaFileAllocation = ariaFileAllocation; + return SetEntity(); + } + + /// + /// 获取是否开启Aria http代理 + /// + /// + public ALLOW_STATUS IsAriaHttpProxy() + { + if (settingsEntity.IsAriaHttpProxy == 0) + { + // 第一次获取,先设置默认值 + IsAriaHttpProxy(isAriaHttpProxy); + return isAriaHttpProxy; + } + return settingsEntity.IsAriaHttpProxy; + } + + /// + /// 设置是否开启Aria http代理 + /// + /// + /// + public bool IsAriaHttpProxy(ALLOW_STATUS isAriaHttpProxy) + { + settingsEntity.IsAriaHttpProxy = isAriaHttpProxy; + return SetEntity(); + } + + /// + /// 获取Aria的http代理的地址 + /// + /// + public string GetAriaHttpProxy() + { + if (settingsEntity.AriaHttpProxy == null) + { + // 第一次获取,先设置默认值 + SetAriaHttpProxy(ariaHttpProxy); + return ariaHttpProxy; + } + return settingsEntity.AriaHttpProxy; + } + + /// + /// 设置Aria的http代理的地址 + /// + /// + /// + public bool SetAriaHttpProxy(string ariaHttpProxy) + { + settingsEntity.AriaHttpProxy = ariaHttpProxy; + return SetEntity(); + } + + /// + /// 获取Aria的http代理的端口 + /// + /// + public int GetAriaHttpProxyListenPort() + { + if (settingsEntity.AriaHttpProxyListenPort == 0) + { + // 第一次获取,先设置默认值 + SetAriaHttpProxyListenPort(ariaHttpProxyListenPort); + return ariaHttpProxyListenPort; + } + return settingsEntity.AriaHttpProxyListenPort; + } + + /// + /// 设置Aria的http代理的端口 + /// + /// + /// + public bool SetAriaHttpProxyListenPort(int ariaHttpProxyListenPort) + { + settingsEntity.AriaHttpProxyListenPort = ariaHttpProxyListenPort; + return SetEntity(); + } + + } +} diff --git a/src/Core/settings/Settings.video.cs b/src/Core/settings/Settings.video.cs new file mode 100644 index 0000000..63e537b --- /dev/null +++ b/src/Core/settings/Settings.video.cs @@ -0,0 +1,273 @@ +namespace Core.settings +{ + public partial class Settings + { + // 设置优先下载的视频编码 + private readonly VideoCodecs videoCodecs = VideoCodecs.AVC; + + // 设置优先下载画质 + private readonly int quality = 120; + + // 是否在下载的视频前增加序号 + private readonly ALLOW_STATUS isAddOrder = ALLOW_STATUS.NO; + + // 是否下载flv视频后转码为mp4 + private readonly ALLOW_STATUS isTranscodingFlvToMp4 = ALLOW_STATUS.YES; + + // 默认下载目录 + private readonly string saveVideoRootPath = "./Media"; + + // 是否使用默认下载目录,如果是,则每次点击下载选中项时不再询问下载目录 + private readonly ALLOW_STATUS isUseSaveVideoRootPath = ALLOW_STATUS.NO; + + // 是否为不同视频分别创建文件夹 + private readonly ALLOW_STATUS isCreateFolderForMedia = ALLOW_STATUS.YES; + + // 是否在下载视频的同时下载弹幕 + private readonly ALLOW_STATUS isDownloadDanmaku = ALLOW_STATUS.YES; + + // 是否在下载视频的同时下载封面 + private readonly ALLOW_STATUS isDownloadCover = ALLOW_STATUS.YES; + + + /// + /// 获取优先下载的视频编码 + /// + /// + public VideoCodecs GetVideoCodecs() + { + if (settingsEntity.VideoCodecs == 0) + { + // 第一次获取,先设置默认值 + SetVideoCodecs(videoCodecs); + return videoCodecs; + } + return settingsEntity.VideoCodecs; + } + + /// + /// 设置优先下载的视频编码 + /// + /// + /// + public bool SetVideoCodecs(VideoCodecs videoCodecs) + { + settingsEntity.VideoCodecs = videoCodecs; + return SetEntity(); + } + + /// + /// 获取优先下载画质 + /// + /// + public int GetQuality() + { + if (settingsEntity.Quality == 0) + { + // 第一次获取,先设置默认值 + SetQuality(quality); + return quality; + } + return settingsEntity.Quality; + } + + /// + /// 设置优先下载画质 + /// + /// + /// + public bool SetQuality(int quality) + { + settingsEntity.Quality = quality; + return SetEntity(); + } + + /// + /// 获取是否给视频增加序号 + /// + /// + public ALLOW_STATUS IsAddOrder() + { + if (settingsEntity.IsAddOrder == 0) + { + // 第一次获取,先设置默认值 + IsAddOrder(isAddOrder); + return isAddOrder; + } + return settingsEntity.IsAddOrder; + } + + /// + /// 设置是否给视频增加序号 + /// + /// + /// + public bool IsAddOrder(ALLOW_STATUS isAddOrder) + { + settingsEntity.IsAddOrder = isAddOrder; + return SetEntity(); + } + + /// + /// 获取是否下载flv视频后转码为mp4 + /// + /// + public ALLOW_STATUS IsTranscodingFlvToMp4() + { + if (settingsEntity.IsTranscodingFlvToMp4 == 0) + { + // 第一次获取,先设置默认值 + IsTranscodingFlvToMp4(isTranscodingFlvToMp4); + return isTranscodingFlvToMp4; + } + return settingsEntity.IsTranscodingFlvToMp4; + } + + /// + /// 设置是否下载flv视频后转码为mp4 + /// + /// + /// + public bool IsTranscodingFlvToMp4(ALLOW_STATUS isTranscodingFlvToMp4) + { + settingsEntity.IsTranscodingFlvToMp4 = isTranscodingFlvToMp4; + return SetEntity(); + } + + /// + /// 获取下载目录 + /// + /// + public string GetSaveVideoRootPath() + { + if (settingsEntity.SaveVideoRootPath == null) + { + // 第一次获取,先设置默认值 + SetSaveVideoRootPath(saveVideoRootPath); + return saveVideoRootPath; + } + return settingsEntity.SaveVideoRootPath; + } + + /// + /// 设置下载目录 + /// + /// + /// + public bool SetSaveVideoRootPath(string path) + { + settingsEntity.SaveVideoRootPath = path; + return SetEntity(); + } + + /// + /// 获取是否使用默认下载目录 + /// + /// + public ALLOW_STATUS IsUseSaveVideoRootPath() + { + if (settingsEntity.IsUseSaveVideoRootPath == 0) + { + // 第一次获取,先设置默认值 + IsUseSaveVideoRootPath(isUseSaveVideoRootPath); + return isUseSaveVideoRootPath; + } + return settingsEntity.IsUseSaveVideoRootPath; + } + + /// + /// 设置是否使用默认下载目录 + /// + /// + /// + public bool IsUseSaveVideoRootPath(ALLOW_STATUS isUseSaveVideoRootPath) + { + settingsEntity.IsUseSaveVideoRootPath = isUseSaveVideoRootPath; + return SetEntity(); + } + + /// + /// 获取是否为不同视频分别创建文件夹 + /// + /// + public ALLOW_STATUS IsCreateFolderForMedia() + { + if (settingsEntity.IsCreateFolderForMedia == 0) + { + // 第一次获取,先设置默认值 + IsCreateFolderForMedia(isCreateFolderForMedia); + return isCreateFolderForMedia; + } + return settingsEntity.IsCreateFolderForMedia; + } + + /// + /// 设置是否为不同视频分别创建文件夹 + /// + /// + /// + public bool IsCreateFolderForMedia(ALLOW_STATUS isCreateFolderForMedia) + { + settingsEntity.IsCreateFolderForMedia = isCreateFolderForMedia; + return SetEntity(); + } + + /// + /// 获取是否在下载视频的同时下载弹幕 + /// + /// + public ALLOW_STATUS IsDownloadDanmaku() + { + if (settingsEntity.IsDownloadDanmaku == 0) + { + // 第一次获取,先设置默认值 + IsDownloadDanmaku(isDownloadDanmaku); + return isDownloadDanmaku; + } + return settingsEntity.IsDownloadDanmaku; + } + + /// + /// 设置是否在下载视频的同时下载弹幕 + /// + /// + /// + public bool IsDownloadDanmaku(ALLOW_STATUS isDownloadDanmaku) + { + settingsEntity.IsDownloadDanmaku = isDownloadDanmaku; + return SetEntity(); + } + + /// + /// 获取是否在下载视频的同时下载封面 + /// + /// + public ALLOW_STATUS IsDownloadCover() + { + if (settingsEntity.IsDownloadCover == 0) + { + // 第一次获取,先设置默认值 + IsDownloadCover(isDownloadCover); + return isDownloadCover; + } + return settingsEntity.IsDownloadCover; + } + + /// + /// 设置是否在下载视频的同时下载封面 + /// + /// + /// + public bool IsDownloadCover(ALLOW_STATUS isDownloadCover) + { + settingsEntity.IsDownloadCover = isDownloadCover; + return SetEntity(); + } + + + + + + + } +}