开发下载服务中,保存工作

croire 3 years ago
parent 35fa8d6cd5
commit be6b0d47a5

@ -0,0 +1,175 @@
using Brotli;
using DownKyi.Core.BiliApi.Login;
using DownKyi.Core.BiliApi.Models.Json;
using DownKyi.Core.BiliApi.VideoStream;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Text;
namespace DownKyi.Core.Test.BiliApi
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
var player = VideoStream.PlayerV2(464087531, "BV1HL411u765", 439599721);
foreach (var subtitle in player.Subtitle.Subtitles)
{
string referer = "https://www.bilibili.com";
string response = RequestWeb($"https:{subtitle.SubtitleUrl}", referer);
try
{
//Console.WriteLine(subtitle.SubtitleUrl);
//string json = @"D:\test.json";
//WebClient mywebclient = new WebClient();
//mywebclient.DownloadFile($"https:{subtitle.SubtitleUrl}", json);
//StreamReader streamReader = File.OpenText(json);
//string jsonWordTemplate = streamReader.ReadToEnd();
//streamReader.Close();
var subtitleJson = JsonConvert.DeserializeObject<SubtitleJson>(response);
if (subtitleJson == null) { return; }
string srt = subtitleJson.ToSubRip();
File.WriteAllText($"D:/{subtitle.LanDoc}.srt", srt);
}
catch (Exception e)
{
Utils.Debugging.Console.PrintLine("PlayerV2()发生异常: {0}", e);
}
}
}
public string RequestWeb(string url, string referer = null, string method = "GET", Dictionary<string, string> 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 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36";
//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";
// referer
if (referer != null)
{
request.Referer = referer;
}
// 构造cookie
if (!url.Contains("getLogin"))
{
request.Headers["origin"] = "https://www.bilibili.com";
CookieContainer cookies = LoginHelper.GetLoginInfoCookies();
if (cookies != null)
{
request.CookieContainer = cookies;
}
}
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);
Logging.LogManager.Error(e);
return RequestWeb(url, referer, method, parameters, retry - 1);
}
catch (IOException e)
{
Console.WriteLine("RequestWeb()发生IO异常: {0}", e);
Logging.LogManager.Error(e);
return RequestWeb(url, referer, method, parameters, retry - 1);
}
catch (Exception e)
{
Console.WriteLine("RequestWeb()发生其他异常: {0}", e);
Logging.LogManager.Error(e);
return RequestWeb(url, referer, method, parameters, retry - 1);
}
}
}
}

@ -45,10 +45,14 @@
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.2.1.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="BiliApi\UnitTest1.cs" />
<Compile Include="BiliApi\Video\DynamicTest.cs" />
<Compile Include="BiliApi\Video\RankingTest.cs" />
<Compile Include="BiliApi\Video\VideoInfoTest.cs" />
@ -60,6 +64,10 @@
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Brotli.Core\Brotli.Core.csproj">
<Project>{3107CD63-E257-455E-AE81-1FD3582B067A}</Project>
<Name>Brotli.Core</Name>
</ProjectReference>
<ProjectReference Include="..\DownKyi.Core\DownKyi.Core.csproj">
<Project>{4fde0364-f65b-4812-bfe8-34e886624fbd}</Project>
<Name>DownKyi.Core</Name>

@ -2,4 +2,5 @@
<packages>
<package id="MSTest.TestAdapter" version="2.1.1" targetFramework="net472" />
<package id="MSTest.TestFramework" version="2.1.1" targetFramework="net472" />
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
</packages>

@ -1,5 +1,6 @@
using DownKyi.Core.Aria2cNet.Client;
using DownKyi.Core.Logging;
using System;
using System.Threading;
namespace DownKyi.Core.Aria2cNet
@ -32,10 +33,14 @@ namespace DownKyi.Core.Aria2cNet
/// <summary>
/// 获取gid下载项的状态
///
/// TODO
/// 对于下载的不同状态的返回值的测试
/// </summary>
/// <param name="gid"></param>
/// <param name="action"></param>
/// <returns></returns>
public DownloadStatus GetDownloadStatus(string gid)
public DownloadResult GetDownloadStatus(string gid, Action action = null)
{
string filePath = "";
while (true)
@ -48,7 +53,7 @@ namespace DownKyi.Core.Aria2cNet
if (status.Result.Error.Message.Contains("is not found"))
{
OnDownloadFinish(false, null, gid, status.Result.Error.Message);
return DownloadStatus.ABORT;
return DownloadResult.ABORT;
}
}
@ -60,9 +65,16 @@ namespace DownKyi.Core.Aria2cNet
long totalLength = long.Parse(status.Result.Result.TotalLength);
long completedLength = long.Parse(status.Result.Result.CompletedLength);
long speed = long.Parse(status.Result.Result.DownloadSpeed);
// 回调
OnTellStatus(totalLength, completedLength, speed, gid);
// 在外部执行
if (action != null)
{
action.Invoke();
}
if (status.Result.Result.Status == "complete")
{
break;
@ -86,14 +98,14 @@ namespace DownKyi.Core.Aria2cNet
// 返回回调信息,退出函数
OnDownloadFinish(false, null, gid, status.Result.Result.ErrorMessage);
return DownloadStatus.FAILED;
return DownloadResult.FAILED;
}
// 降低CPU占用
Thread.Sleep(100);
}
OnDownloadFinish(true, filePath, gid, null);
return DownloadStatus.SUCCESS;
return DownloadResult.SUCCESS;
}
/// <summary>

@ -3,7 +3,7 @@
/// <summary>
/// 下载状态
/// </summary>
public enum DownloadStatus
public enum DownloadResult
{
SUCCESS = 1,
FAILED,

@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace DownKyi.Core.BiliApi.Models.Json
{
public class SubRipText : BaseModel
{
[JsonProperty("lan")]
public string Lan { get; set; }
[JsonProperty("lan_doc")]
public string LanDoc { get; set; }
[JsonProperty("srtString")]
public string SrtString { get; set; }
}
}

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace DownKyi.Core.BiliApi.Models.Json
{
public class Subtitle : BaseModel
{
[JsonProperty("from")]
public float From { get; set; }
[JsonProperty("to")]
public float To { get; set; }
[JsonProperty("location")]
public int Location { get; set; }
[JsonProperty("content")]
public string Content { get; set; }
}
}

@ -0,0 +1,69 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace DownKyi.Core.BiliApi.Models.Json
{
public class SubtitleJson : BaseModel
{
[JsonProperty("font_size")]
public float FontSize { get; set; }
[JsonProperty("font_color")]
public string FontColor { get; set; }
[JsonProperty("background_alpha")]
public float BackgroundAlpha { get; set; }
[JsonProperty("background_color")]
public string BackgroundColor { get; set; }
[JsonProperty("Stroke")]
public string Stroke { get; set; }
[JsonProperty("body")]
public List<Subtitle> Body { get; set; }
/// <summary>
/// srt格式字幕
/// </summary>
/// <returns></returns>
public string ToSubRip()
{
string subRip = string.Empty;
for (int i = 0; i < Body.Count; i++)
{
subRip += $"{i + 1}\n";
subRip += $"{Second2hms(Body[i].From)} --> {Second2hms(Body[i].To)}\n";
subRip += $"{Body[i].Content}\n";
subRip += "\n";
}
return subRip;
}
/// <summary>
/// 秒数转 时:分:秒 格式
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
private static string Second2hms(float seconds)
{
if (seconds < 0)
{
return "00:00:00,000";
}
int i = (int)Math.Floor(seconds / 1.0);
int dec = (int)(Math.Round(seconds % 1.0f, 2) * 100);
if (dec >= 100)
{
dec = 99;
}
int min = (int)Math.Floor(i / 60.0);
int second = (int)(i % 60.0f);
int hour = (int)Math.Floor(min / 60.0);
min = (int)Math.Floor(min % 60.0f);
return $"{hour:D2}:{min:D2}:{second:D2},{dec:D3}";
}
}
}

@ -0,0 +1,34 @@
using DownKyi.Core.BiliApi.Models;
using Newtonsoft.Json;
namespace DownKyi.Core.BiliApi.VideoStream.Models
{
// https://api.bilibili.com/x/player/v2?cid={cid}&aid={avid}&bvid={bvid}
public class PlayerV2Origin : BaseModel
{
//[JsonProperty("code")]
//public int Code { get; set; }
//[JsonProperty("message")]
//public string Message { get; set; }
//[JsonProperty("ttl")]
//public int Ttl { get; set; }
[JsonProperty("data")]
public PlayerV2 Data { get; set; }
}
public class PlayerV2 : BaseModel
{
[JsonProperty("aid")]
public long Aid { get; set; }
[JsonProperty("bvid")]
public string Bvid { get; set; }
// allow_bp
// no_share
[JsonProperty("cid")]
public long Cid { get; set; }
// ...
[JsonProperty("subtitle")]
public SubtitleInfo Subtitle { get; set; }
}
}

@ -0,0 +1,25 @@
using DownKyi.Core.BiliApi.Models;
using Newtonsoft.Json;
namespace DownKyi.Core.BiliApi.VideoStream.Models
{
public class Subtitle : BaseModel
{
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("lan")]
public string Lan { get; set; }
[JsonProperty("lan_doc")]
public string LanDoc { get; set; }
[JsonProperty("is_lock")]
public bool IsLock { get; set; }
[JsonProperty("author_mid")]
public long AuthorMid { get; set; }
[JsonProperty("subtitle_url")]
public string SubtitleUrl { get; set; }
[JsonProperty("type")]
public int Type { get; set; }
[JsonProperty("id_str")]
public string IdStr { get; set; }
}
}

@ -0,0 +1,18 @@
using DownKyi.Core.BiliApi.Models;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace DownKyi.Core.BiliApi.VideoStream.Models
{
public class SubtitleInfo : BaseModel
{
[JsonProperty("allow_submit")]
public bool AllowSubmit { get; set; }
[JsonProperty("lan")]
public string Lan { get; set; }
[JsonProperty("lan_doc")]
public string LanDoc { get; set; }
[JsonProperty("subtitles")]
public List<Subtitle> Subtitles { get; set; }
}
}

@ -1,13 +1,82 @@
using DownKyi.Core.BiliApi.VideoStream.Models;
using DownKyi.Core.BiliApi.Models.Json;
using DownKyi.Core.BiliApi.VideoStream.Models;
using DownKyi.Core.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace DownKyi.Core.BiliApi.VideoStream
{
public static class VideoStream
{
/// <summary>
/// 获取播放器信息web端
/// </summary>
/// <param name="avid"></param>
/// <param name="bvid"></param>
/// <param name="cid"></param>
/// <returns></returns>
public static PlayerV2 PlayerV2(long avid, string bvid, long cid)
{
string url = $"https://api.bilibili.com/x/player/v2?cid={cid}&aid={avid}&bvid={bvid}";
string referer = "https://www.bilibili.com";
string response = WebClient.RequestWeb(url, referer);
try
{
var playUrl = JsonConvert.DeserializeObject<PlayerV2Origin>(response);
return playUrl?.Data;
}
catch (Exception e)
{
Utils.Debugging.Console.PrintLine("PlayerV2()发生异常: {0}", e);
LogManager.Error("PlayerV2()", e);
return null;
}
}
/// <summary>
/// 获取所有字幕
/// </summary>
/// <param name="avid"></param>
/// <param name="bvid"></param>
/// <param name="cid"></param>
/// <returns></returns>
public static List<SubRipText> GetSubtitle(long avid, string bvid, long cid)
{
List<SubRipText> subRipTexts = new List<SubRipText>();
// 获取播放器信息
var player = PlayerV2(avid, bvid, cid);
foreach (var subtitle in player.Subtitle.Subtitles)
{
string referer = "https://www.bilibili.com";
string response = WebClient.RequestWeb($"https:{subtitle.SubtitleUrl}", referer);
try
{
var subtitleJson = JsonConvert.DeserializeObject<SubtitleJson>(response);
if (subtitleJson == null) { continue; }
subRipTexts.Add(new SubRipText
{
Lan = subtitle.Lan,
LanDoc = subtitle.LanDoc,
SrtString = subtitleJson.ToSubRip()
});
}
catch (Exception e)
{
Utils.Debugging.Console.PrintLine("GetSubtitle()发生异常: {0}", e);
LogManager.Error("GetSubtitle()", e);
}
}
return subRipTexts;
}
/// <summary>
/// 获取普通视频的视频流
/// </summary>
@ -92,7 +161,7 @@ namespace DownKyi.Core.BiliApi.VideoStream
catch (Exception e)
{
Utils.Debugging.Console.PrintLine("GetPlayUrl()发生异常: {0}", e);
LogManager.Error("GetPlayUrl", e);
LogManager.Error("GetPlayUrl()", e);
return null;
}
}

@ -111,7 +111,7 @@
<Compile Include="Aria2cNet\Client\Entity\SystemMulticall.cs" />
<Compile Include="Aria2cNet\Client\Entity\SystemMulticallMathod.cs" />
<Compile Include="Aria2cNet\Client\HowChangePosition.cs" />
<Compile Include="Aria2cNet\DownloadStatus.cs" />
<Compile Include="Aria2cNet\DownloadResult.cs" />
<Compile Include="Aria2cNet\Server\AriaConfig.cs" />
<Compile Include="Aria2cNet\Server\AriaConfigFileAllocation.cs" />
<Compile Include="Aria2cNet\Server\AriaConfigLogLevel.cs" />
@ -160,13 +160,19 @@
<Compile Include="BiliApi\Login\Models\LoginUrl.cs" />
<Compile Include="BiliApi\Login\Models\UserInfoForNavigation.cs" />
<Compile Include="BiliApi\Models\Dimension.cs" />
<Compile Include="BiliApi\Models\Json\SubRipText.cs" />
<Compile Include="BiliApi\Models\Json\Subtitle.cs" />
<Compile Include="BiliApi\Models\Json\SubtitleJson.cs" />
<Compile Include="BiliApi\Models\VideoOwner.cs" />
<Compile Include="BiliApi\protobuf\bilibili\community\service\dm\v1\Dm.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayerV2.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrl.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrlDash.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrlDashVideo.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrlDurl.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrlSupportFormat.cs" />
<Compile Include="BiliApi\VideoStream\Models\Subtitle.cs" />
<Compile Include="BiliApi\VideoStream\Models\SubtitleInfo.cs" />
<Compile Include="BiliApi\VideoStream\VideoStream.cs" />
<Compile Include="BiliApi\Video\Dynamic.cs" />
<Compile Include="BiliApi\Video\Models\RankingVideoView.cs" />
@ -203,6 +209,9 @@
<Compile Include="Downloader\MultiThreadDownloader.cs" />
<Compile Include="Downloader\PartialDownloader.cs" />
<Compile Include="FFmpeg\FFmpegHelper.cs" />
<Compile Include="FileName\FileName.cs" />
<Compile Include="FileName\FileNamePart.cs" />
<Compile Include="FileName\HyphenSeparated.cs" />
<Compile Include="Logging\LogInfo.cs" />
<Compile Include="Logging\LogLevel.cs" />
<Compile Include="Logging\LogManager.cs" />

@ -0,0 +1,117 @@
using System.Collections.Generic;
namespace DownKyi.Core.FileName
{
public class FileName
{
private readonly List<FileNamePart> nameParts;
private int order = -1;
private string mainTitle = "MAIN_TITLE";
private string pageTitle = "PAGE_TITLE";
private string videoZone = "VIDEO_ZONE";
private string audioQuality = "AUDIO_QUALITY";
private string videoQuality = "VIDEO_QUALITY";
private string videoCodec = "VIDEO_CODEC";
private FileName(List<FileNamePart> nameParts)
{
this.nameParts = nameParts;
}
public static FileName Builder(List<FileNamePart> nameParts)
{
return new FileName(nameParts);
}
public FileName SetOrder(int order)
{
this.order = order;
return this;
}
public FileName SetMainTitle(string mainTitle)
{
this.mainTitle = mainTitle;
return this;
}
public FileName SetPageTitle(string pageTitle)
{
this.pageTitle = pageTitle;
return this;
}
public FileName SetVideoZone(string videoZone)
{
this.videoZone = videoZone;
return this;
}
public FileName SetAudioQuality(string audioQuality)
{
this.audioQuality = audioQuality;
return this;
}
public FileName SetVideoQuality(string videoQuality)
{
this.videoQuality = videoQuality;
return this;
}
public FileName SetVideoCodec(string videoCodec)
{
this.videoCodec = videoCodec;
return this;
}
public string RelativePath()
{
string path = string.Empty;
foreach (FileNamePart part in nameParts)
{
switch (part)
{
case FileNamePart.ORDER:
if (order != -1)
{
path += order;
}
else
{
path += "ORDER";
}
break;
case FileNamePart.MAIN_TITLE:
path += mainTitle;
break;
case FileNamePart.PAGE_TITLE:
path += pageTitle;
break;
case FileNamePart.VIDEO_ZONE:
path += videoZone;
break;
case FileNamePart.AUDIO_QUALITY:
path += audioQuality;
break;
case FileNamePart.VIDEO_QUALITY:
path += videoQuality;
break;
case FileNamePart.VIDEO_CODEC:
path += videoCodec;
break;
}
if (((int)part) >= 100)
{
path += HyphenSeparated.Hyphen[(int)part];
}
}
// 避免以斜杠开头和结尾的情况
return path.TrimEnd('/').TrimStart('/');
}
}
}

@ -0,0 +1,34 @@
namespace DownKyi.Core.FileName
{
public enum FileNamePart
{
// Video
ORDER = 1,
MAIN_TITLE,
PAGE_TITLE,
VIDEO_ZONE,
AUDIO_QUALITY,
VIDEO_QUALITY,
VIDEO_CODEC,
// 斜杠
SLASH = 100,
// HyphenSeparated
UNDERSCORE = 101, // 下划线
HYPHEN, // 连字符
PLUS, // 加号
COMMA, // 逗号
PERIOD, // 句号
AND, // and
NUMBER, // #
OPEN_PAREN, // 左圆括号
CLOSE_PAREN, // 右圆括号
OPEN_BRACKET, // 左方括号
CLOSE_BRACKET, // 右方括号
OPEN_BRACE, // 左花括号
CLOSE_brace, // 右花括号
BLANK, // 空白符
}
}

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace DownKyi.Core.FileName
{
/// <summary>
/// 文件名的分隔符
/// </summary>
public static class HyphenSeparated
{
public static Dictionary<int, string> Hyphen = new Dictionary<int, string>()
{
{ 101, "/" },
{ 101, "_" },
{ 102, "-" },
{ 103, "+" },
{ 104, "," },
{ 105, "." },
{ 106, "&" },
{ 107, "#" },
{ 108, "(" },
{ 109, ")" },
{ 110, "[" },
{ 111, "]" },
{ 112, "{" },
{ 113, "}" },
{ 114, " " },
};
}
}

@ -1,4 +1,6 @@
using DownKyi.Utils;
using DownKyi.Models;
using DownKyi.Services.Download;
using DownKyi.Utils;
using DownKyi.ViewModels;
using DownKyi.ViewModels.Dialogs;
using DownKyi.ViewModels.DownloadManager;
@ -11,6 +13,7 @@ using DownKyi.Views.Settings;
using DownKyi.Views.Toolbox;
using Prism.Ioc;
using System;
using System.Collections.ObjectModel;
using System.Windows;
namespace DownKyi
@ -20,6 +23,12 @@ namespace DownKyi
/// </summary>
public partial class App
{
public static ObservableCollection<DownloadingItem> DownloadingList { get; set; }
public static ObservableCollection<DownloadedItem> DownloadedList { get; set; }
// 下载服务
private IDownloadService downloadService;
protected override Window CreateShell()
{
// 设置主题
@ -30,9 +39,26 @@ namespace DownKyi
DictionaryResource.LoadLanguage("Default");
//DictionaryResource.LoadLanguage("en_US");
// 初始化数据
// TODO 从数据库读取
DownloadingList = new ObservableCollection<DownloadingItem>();
DownloadedList = new ObservableCollection<DownloadedItem>();
// 启动下载服务
downloadService = new AriaDownloadService(DownloadingList, DownloadedList);
downloadService.Start();
return Container.Resolve<MainWindow>();
}
protected override void OnExit(ExitEventArgs e)
{
// 关闭下载服务
downloadService.End();
base.OnExit(e);
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// pages

@ -87,9 +87,13 @@
<Compile Include="Images\LogoIcon.cs" />
<Compile Include="Images\NavigationIcon.cs" />
<Compile Include="Images\NormalIcon.cs" />
<Compile Include="Models\DownloadedItem.cs" />
<Compile Include="Models\DownloadingItem.cs" />
<Compile Include="Models\DownloadStatus.cs" />
<Compile Include="Models\Favorites.cs" />
<Compile Include="Models\FavoritesMedia.cs" />
<Compile Include="Models\ParseScopeDisplay.cs" />
<Compile Include="Models\PlayStreamType.cs" />
<Compile Include="Models\Resolution.cs" />
<Compile Include="Models\TabHeader.cs" />
<Compile Include="Models\VideoQuality.cs" />
@ -98,7 +102,10 @@
<Compile Include="Models\VideoSection.cs" />
<Compile Include="Services\BangumiInfoService.cs" />
<Compile Include="Services\CheeseInfoService.cs" />
<Compile Include="Services\Download\AriaDownloadService.cs" />
<Compile Include="Services\Download\DownloadService.cs" />
<Compile Include="Services\FavoritesService.cs" />
<Compile Include="Services\Download\IDownloadService.cs" />
<Compile Include="Services\IFavoritesService.cs" />
<Compile Include="Services\IInfoService.cs" />
<Compile Include="Services\IResolutionService.cs" />

@ -0,0 +1,12 @@
namespace DownKyi.Models
{
public enum DownloadStatus
{
NOT_STARTED, // 未开始,从未开始下载
WAIT_FOR_DOWNLOAD, // 等待下载,下载过,但是启动本次下载周期未开始,如重启程序后未开始
PAUSE, // 暂停
DOWNLOADING, // 下载中
DOWNLOAD_SUCCEED, // 下载成功
DOWNLOAD_FAILED, // 下载失败
}
}

@ -0,0 +1,13 @@
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DownKyi.Models
{
public class DownloadedItem : BindableBase
{
}
}

@ -0,0 +1,91 @@
using DownKyi.Core.BiliApi.VideoStream.Models;
using Prism.Mvvm;
namespace DownKyi.Models
{
public class DownloadingItem : BindableBase
{
public PlayUrl PlayUrl { get; set; }
// 此条下载项的id
public string Uuid { get; }
// 文件路径,不包含扩展名,所有内容均以此路径下载
public string FilePath { get; set; }
// Aria相关
public string Gid { get; set; }
// 视频类别
public PlayStreamType PlayStreamType { get; set; }
// 视频的id
public string Bvid { get; set; }
public long Avid { get; set; }
public long Cid { get; set; }
public long EpisodeId { get; set; }
// 视频封面的url
public string CoverUrl { get; set; }
// 视频序号
private int order;
public int Order
{
get { return order; }
set { SetProperty(ref order, value); }
}
// 视频主标题
private string mainTitle;
public string MainTitle
{
get { return mainTitle; }
set { SetProperty(ref mainTitle, value); }
}
// 视频标题
private string name;
public string Name
{
get { return name; }
set { SetProperty(ref name, value); }
}
// 音频编码
public int AudioCodecId { get; set; }
private string audioCodecName;
public string AudioCodecName
{
get { return audioCodecName; }
set { SetProperty(ref audioCodecName, value); }
}
// 视频编码
public int VideoCodecId { get; set; }
private string videoCodecName;
public string VideoCodecName
{
get { return videoCodecName; }
set { SetProperty(ref videoCodecName, value); }
}
// 下载内容(音频、视频、弹幕、字幕、封面)
private string downloadContent;
public string DownloadContent
{
get { return downloadContent; }
set { SetProperty(ref downloadContent, value); }
}
// 下载状态
public DownloadStatus DownloadStatus { get; set; }
private string downloadStatusTitle;
public string DownloadStatusTitle
{
get { return downloadStatusTitle; }
set { SetProperty(ref downloadStatusTitle, value); }
}
}
}

@ -0,0 +1,9 @@
namespace DownKyi.Models
{
public enum PlayStreamType
{
VIDEO = 1, // 普通视频
BANGUMI, // 番剧、电影、电视剧等
CHEESE, // 课程
}
}

@ -8,6 +8,7 @@ using DownKyi.Models;
using DownKyi.Utils;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows.Media.Imaging;
namespace DownKyi.Services
@ -63,21 +64,28 @@ namespace DownKyi.Services
string name;
// 判断title是否为数字如果是则将share_copy作为name否则将title作为name
if (int.TryParse(episode.Title, out int result))
{
name = episode.ShareCopy;
}
else
{
if (episode.LongTitle != null && episode.LongTitle != "")
{
name = $"{episode.Title} {episode.LongTitle}";
}
else
{
name = episode.Title;
}
}
//if (int.TryParse(episode.Title, out int result))
//{
// name = Regex.Replace(episode.ShareCopy, @"《.*?》", "");
// //name = episode.ShareCopy;
//}
//else
//{
// if (episode.LongTitle != null && episode.LongTitle != "")
// {
// name = $"{episode.Title} {episode.LongTitle}";
// }
// else
// {
// name = episode.Title;
// }
//}
// 将share_copy作为name删除《》中的标题
name = Regex.Replace(episode.ShareCopy, @"^《.*?》", "");
// 删除前后空白符
name = name.Trim();
VideoPage page = new VideoPage
{

@ -0,0 +1,405 @@
using DownKyi.Core.Aria2cNet;
using DownKyi.Core.Aria2cNet.Client;
using DownKyi.Core.Aria2cNet.Client.Entity;
using DownKyi.Core.Aria2cNet.Server;
using DownKyi.Core.BiliApi.Login;
using DownKyi.Core.BiliApi.VideoStream;
using DownKyi.Core.Danmaku2Ass;
using DownKyi.Core.Logging;
using DownKyi.Core.Settings;
using DownKyi.Core.Storage;
using DownKyi.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace DownKyi.Services.Download
{
/// <summary>
/// 音视频采用Aria下载其余采用WebClient下载
/// </summary>
public class AriaDownloadService : DownloadService, IDownloadService
{
private CancellationTokenSource tokenSource;
public AriaDownloadService(ObservableCollection<DownloadingItem> downloadingList, ObservableCollection<DownloadedItem> downloadedList) : base(downloadingList, downloadedList)
{
Tag = "AriaDownloadService";
}
public string DownloadAudio(DownloadingItem downloading)
{
// 如果没有Dash返回null
if(downloading.PlayUrl.Dash == null) { return null; }
// 如果audio列表没有内容则返回null
if (downloading.PlayUrl.Dash.Audio == null) { return null; }
else if (downloading.PlayUrl.Dash.Audio.Count == 0) { return null; }
throw new NotImplementedException();
}
/// <summary>
/// 下载封面
/// </summary>
/// <param name="downloading"></param>
public void DownloadCover(DownloadingItem downloading)
{
// 查询、保存封面
StorageCover storageCover = new StorageCover();
string cover = storageCover.GetCover(downloading.Avid, downloading.Bvid, downloading.Cid, downloading.CoverUrl);
if (cover == null)
{
return;
}
// 图片的扩展名
string[] temp = downloading.CoverUrl.Split('.');
string fileExtension = temp[temp.Length - 1];
// 图片的地址
string coverPath = $"{StorageManager.GetCover()}/{cover}";
// 复制图片到指定位置
try
{
File.Copy(coverPath, $"{downloading.FilePath}.{fileExtension}");
}
catch (Exception e)
{
Core.Utils.Debugging.Console.PrintLine(e);
LogManager.Error(Tag, e);
}
}
/// <summary>
/// 下载弹幕
/// </summary>
/// <param name="downloading"></param>
public void DownloadDanmaku(DownloadingItem downloading)
{
string title = $"{downloading.Name}";
string assFile = $"{downloading.FilePath}.ass";
int screenWidth = SettingsManager.GetInstance().GetDanmakuScreenWidth();
int screenHeight = SettingsManager.GetInstance().GetDanmakuScreenHeight();
//if (SettingsManager.GetInstance().IsCustomDanmakuResolution() != AllowStatus.YES)
//{
// if (downloadingEntity.Width > 0 && downloadingEntity.Height > 0)
// {
// screenWidth = downloadingEntity.Width;
// screenHeight = downloadingEntity.Height;
// }
//}
// 字幕配置
var subtitleConfig = new Config
{
Title = title,
ScreenWidth = screenWidth,
ScreenHeight = screenHeight,
FontName = SettingsManager.GetInstance().GetDanmakuFontName(),
BaseFontSize = SettingsManager.GetInstance().GetDanmakuFontSize(),
LineCount = SettingsManager.GetInstance().GetDanmakuLineCount(),
LayoutAlgorithm = SettingsManager.GetInstance().GetDanmakuLayoutAlgorithm().ToString("G").ToLower(), // async/sync
TuneDuration = 0,
DropOffset = 0,
BottomMargin = 0,
CustomOffset = 0
};
Core.Danmaku2Ass.Bilibili.GetInstance()
.SetTopFilter(SettingsManager.GetInstance().GetDanmakuTopFilter() == AllowStatus.YES)
.SetBottomFilter(SettingsManager.GetInstance().GetDanmakuBottomFilter() == AllowStatus.YES)
.SetScrollFilter(SettingsManager.GetInstance().GetDanmakuScrollFilter() == AllowStatus.YES)
.Create(downloading.Avid, downloading.Cid, subtitleConfig, assFile);
}
/// <summary>
/// 下载字幕
/// </summary>
/// <param name="downloading"></param>
public void DownloadSubtitle(DownloadingItem downloading)
{
var subRipTexts = VideoStream.GetSubtitle(downloading.Avid, downloading.Bvid, downloading.Cid);
foreach (var subRip in subRipTexts)
{
string srtFile = $"{downloading.FilePath}_{subRip.LanDoc}.srt";
try
{
File.WriteAllText(srtFile, subRip.SrtString);
}
catch (Exception e)
{
Core.Utils.Debugging.Console.PrintLine("DownloadSubtitle()发生异常: {0}", e);
LogManager.Error("DownloadSubtitle()", e);
}
}
}
public string DownloadVideo(DownloadingItem downloading)
{
throw new NotImplementedException();
}
/// <summary>
/// 停止下载服务
/// </summary>
public void End()
{
// TODO something
// 关闭Aria服务器
CloseAriaServer();
// 结束任务
tokenSource.Cancel();
}
public void MixedFlow(DownloadingItem downloading, string audioUid, string videoUid)
{
throw new NotImplementedException();
}
/// <summary>
/// 解析视频流的下载链接
/// </summary>
/// <param name="downloading"></param>
public void Parse(DownloadingItem downloading)
{
if (downloading.PlayUrl != null && downloading.DownloadStatus == DownloadStatus.NOT_STARTED)
{
return;
}
switch (downloading.PlayStreamType)
{
case PlayStreamType.VIDEO:
downloading.PlayUrl = VideoStream.GetVideoPlayUrl(downloading.Avid, downloading.Bvid, downloading.Cid);
break;
case PlayStreamType.BANGUMI:
downloading.PlayUrl = VideoStream.GetBangumiPlayUrl(downloading.Avid, downloading.Bvid, downloading.Cid);
break;
case PlayStreamType.CHEESE:
downloading.PlayUrl = VideoStream.GetCheesePlayUrl(downloading.Avid, downloading.Bvid, downloading.Cid, downloading.EpisodeId);
break;
default:
break;
}
}
/// <summary>
/// 启动下载服务
/// </summary>
public async void Start()
{
// 启动Aria服务器
StartAriaServer();
await Task.Run(DoWork, (tokenSource = new CancellationTokenSource()).Token);
}
/// <summary>
/// 执行任务
/// </summary>
private async void DoWork()
{
CancellationToken cancellationToken = tokenSource.Token;
while (true)
{
int maxDownloading = SettingsManager.GetInstance().GetAriaMaxConcurrentDownloads();
int countDownloading = 0;
bool isOutOfMaxDownloading = false;
foreach (DownloadingItem downloading in downloadingList)
{
// 对正在下载的元素计数
if (downloading.DownloadStatus == DownloadStatus.DOWNLOADING)
{
countDownloading++;
}
// 正在下载数量等于最大下载数量,退出本次循环
if (countDownloading == maxDownloading)
{
break;
}
// 正在下载数量大于最大下载数量
if (countDownloading > maxDownloading)
{
isOutOfMaxDownloading = true;
}
// 将超过下载数量的元素暂停
if (isOutOfMaxDownloading)
{
if (downloading.DownloadStatus == DownloadStatus.DOWNLOADING)
{
downloading.DownloadStatus = DownloadStatus.PAUSE;
}
}
await Task.Run(new Action(() =>
{
downloading.DownloadStatus = DownloadStatus.DOWNLOADING;
// 依次下载音频、视频、弹幕、字幕、封面等内容
Parse(downloading);
string audioUid = DownloadAudio(downloading);
string videoUid = DownloadVideo(downloading);
DownloadDanmaku(downloading);
DownloadSubtitle(downloading);
DownloadCover(downloading);
MixedFlow(downloading, audioUid, videoUid);
}),
(tokenSource = new CancellationTokenSource()).Token);
}
// 判断是否该结束线程若为true跳出while循环
if (cancellationToken.IsCancellationRequested)
{
Core.Utils.Debugging.Console.PrintLine("AriaDownloadService: 下载服务结束跳出while循环");
LogManager.Debug(Tag, "下载服务结束");
break;
}
// 降低CPU占用
Thread.Sleep(500);
}
}
/// <summary>
/// 启动Aria服务器
/// </summary>
private async void StartAriaServer()
{
List<string> header = new List<string>
{
$"Cookie: {LoginHelper.GetLoginInfoCookiesString()}",
$"Origin: https://www.bilibili.com",
$"Referer: https://www.bilibili.com",
$"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 91.0.4472.77 Safari / 537.36"
};
AriaConfig config = new AriaConfig()
{
ListenPort = SettingsManager.GetInstance().GetAriaListenPort(),
Token = "downkyi",
LogLevel = SettingsManager.GetInstance().GetAriaLogLevel(),
MaxConcurrentDownloads = SettingsManager.GetInstance().GetAriaMaxConcurrentDownloads(),
MaxConnectionPerServer = 16, // 最大取16
Split = SettingsManager.GetInstance().GetAriaSplit(),
//MaxTries = 5,
MinSplitSize = 10, // 10MB
MaxOverallDownloadLimit = SettingsManager.GetInstance().GetAriaMaxOverallDownloadLimit() * 1024L, // 输入的单位是KB/s所以需要乘以1024
MaxDownloadLimit = SettingsManager.GetInstance().GetAriaMaxDownloadLimit() * 1024L, // 输入的单位是KB/s所以需要乘以1024
MaxOverallUploadLimit = 0,
MaxUploadLimit = 0,
ContinueDownload = true,
FileAllocation = SettingsManager.GetInstance().GetAriaFileAllocation(),
Headers = header
};
var task = await AriaServer.StartServerAsync(config);
if (task) { Console.WriteLine("Start ServerAsync Completed"); }
Console.WriteLine("Start ServerAsync end");
// 恢复所有下载
//var ariaPause = await AriaClient.UnpauseAllAsync();
//if (ariaPause != null)
//{
// Core.Utils.Debugging.Console.PrintLine(ariaPause.ToString());
//}
}
/// <summary>
/// 关闭Aria服务器
/// </summary>
private void CloseAriaServer()
{
new Thread(() =>
{
// 暂停所有下载
var ariaPause = AriaClient.PauseAllAsync();
Core.Utils.Debugging.Console.PrintLine(ariaPause.ToString());
// 关闭服务器
bool close = AriaServer.CloseServer();
Core.Utils.Debugging.Console.PrintLine(close);
})
{ IsBackground = false }
.Start();
}
/// <summary>
/// 采用Aria下载文件
/// </summary>
/// <param name="downloading"></param>
/// <returns></returns>
private DownloadResult DownloadByAria(DownloadingItem downloading, List<string> urls, string localFileName)
{
string[] temp = downloading.FilePath.Split('/');
string path = downloading.FilePath.Replace(temp[temp.Length - 1], "");
AriaSendOption option = new AriaSendOption
{
//HttpProxy = $"http://{Settings.GetAriaHttpProxy()}:{Settings.GetAriaHttpProxyListenPort()}",
Dir = path,
Out = localFileName
//Header = $"cookie: {Login.GetLoginInfoCookiesString()}\nreferer: https://www.bilibili.com",
//UseHead = "true",
//UserAgent = Utils.GetUserAgent()
};
// 如果设置了代理则增加HttpProxy
if (SettingsManager.GetInstance().IsAriaHttpProxy() == AllowStatus.YES)
{
option.HttpProxy = $"http://{SettingsManager.GetInstance().GetAriaHttpProxy()}:{SettingsManager.GetInstance().GetAriaHttpProxyListenPort()}";
}
// 添加一个下载
var ariaAddUri = AriaClient.AddUriAsync(urls, option);
if (ariaAddUri == null || ariaAddUri.Result == null || ariaAddUri.Result.Result == null)
{
return DownloadResult.FAILED;
}
// 保存gid
string gid = ariaAddUri.Result.Result;
downloading.Gid = gid;
// 管理下载
AriaManager ariaManager = new AriaManager();
ariaManager.TellStatus += AriaTellStatus;
ariaManager.DownloadFinish += AriaDownloadFinish;
return ariaManager.GetDownloadStatus(gid, new Action(() =>
{
switch (downloading.DownloadStatus)
{
case DownloadStatus.PAUSE:
var ariaPause = AriaClient.PauseAsync(downloading.Gid);
// TODO
break;
case DownloadStatus.DOWNLOADING:
var ariaUnpause = AriaClient.UnpauseAsync(downloading.Gid);
// TODO
break;
}
}));
}
private void AriaTellStatus(long totalLength, long completedLength, long speed, string gid)
{
throw new NotImplementedException();
}
private void AriaDownloadFinish(bool isSuccess, string downloadPath, string gid, string msg)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,25 @@
using DownKyi.Models;
using System.Collections.ObjectModel;
namespace DownKyi.Services.Download
{
public class DownloadService
{
protected string Tag = "DownloadService";
protected ObservableCollection<DownloadingItem> downloadingList;
protected ObservableCollection<DownloadedItem> downloadedList;
/// <summary>
/// 初始化
/// </summary>
/// <param name="downloading"></param>
/// <returns></returns>
public DownloadService(ObservableCollection<DownloadingItem> downloadingList, ObservableCollection<DownloadedItem> downloadedList)
{
this.downloadingList = downloadingList;
this.downloadedList = downloadedList;
}
}
}

@ -0,0 +1,18 @@
using DownKyi.Models;
namespace DownKyi.Services.Download
{
public interface IDownloadService
{
void Parse(DownloadingItem downloading);
string DownloadAudio(DownloadingItem downloading);
string DownloadVideo(DownloadingItem downloading);
void DownloadDanmaku(DownloadingItem downloading);
void DownloadSubtitle(DownloadingItem downloading);
void DownloadCover(DownloadingItem downloading);
void MixedFlow(DownloadingItem downloading, string audioUid, string videoUid);
void Start();
void End();
}
}

@ -1,8 +1,10 @@
using Prism.Commands;
using DownKyi.Models;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace DownKyi.ViewModels.DownloadManager
@ -11,9 +13,20 @@ namespace DownKyi.ViewModels.DownloadManager
{
public const string Tag = "PageDownloadManagerDownloadFinished";
public ViewDownloadFinishedViewModel(IEventAggregator eventAggregator) : base(eventAggregator)
#region 页面属性申明
private ObservableCollection<DownloadedItem> downloadedList;
public ObservableCollection<DownloadedItem> DownloadedList
{
get { return downloadedList; }
set { SetProperty(ref downloadedList, value); }
}
#endregion
public ViewDownloadFinishedViewModel(IEventAggregator eventAggregator) : base(eventAggregator)
{
DownloadedList = App.DownloadedList;
}
}
}

@ -1,8 +1,10 @@
using Prism.Commands;
using DownKyi.Models;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace DownKyi.ViewModels.DownloadManager
@ -11,9 +13,20 @@ namespace DownKyi.ViewModels.DownloadManager
{
public const string Tag = "PageDownloadManagerDownloading";
public ViewDownloadingViewModel(IEventAggregator eventAggregator) : base(eventAggregator)
#region 页面属性申明
private ObservableCollection<DownloadingItem> downloadingList;
public ObservableCollection<DownloadingItem> DownloadingList
{
get { return downloadingList; }
set { SetProperty(ref downloadingList, value); }
}
#endregion
public ViewDownloadingViewModel(IEventAggregator eventAggregator) : base(eventAggregator)
{
DownloadingList = App.DownloadingList;
}
}
}

@ -304,6 +304,7 @@ namespace DownKyi.ViewModels
/// <param name="e"></param>
private void OnClipboardUpdated(object sender, EventArgs e)
{
#region 执行第二遍时跳过
times += 1;
DispatcherTimer timer = new DispatcherTimer
{
@ -319,6 +320,8 @@ namespace DownKyi.ViewModels
return;
}
#endregion
AllowStatus isListenClipboard = SettingsManager.GetInstance().IsListenClipboard();
if (isListenClipboard != AllowStatus.YES)
{

Loading…
Cancel
Save