弹幕转字幕

pull/252/head
flyself 3 years ago
parent 8f7b9df8f9
commit 396326ab55

@ -13,6 +13,7 @@ namespace DownKyi.Core.BiliApi.BiliUtils
/// 番剧电影、电视剧md号md28228367, MD28228367, https://www.bilibili.com/bangumi/media/md28228367 <para/>
/// 课程ss号https://www.bilibili.com/cheese/play/ss205 <para/>
/// 课程ep号https://www.bilibili.com/cheese/play/ep3489 <para/>
/// 收藏夹ml1329019876, ML1329019876, https://www.bilibili.com/medialist/detail/ml1329019876 <para/>
/// 用户空间uid928123, UID928123, uid:928123, UID:928123, https://space.bilibili.com/928123
/// </summary>
public static class ParseEntrance
@ -25,6 +26,7 @@ namespace DownKyi.Core.BiliApi.BiliUtils
public static readonly string BangumiUrl = $"{WwwUrl}/bangumi/play/";
public static readonly string BangumiMediaUrl = $"{WwwUrl}/bangumi/media/";
public static readonly string CheeseUrl = $"{WwwUrl}/cheese/play/";
public static readonly string FavoritesUrl = $"{WwwUrl}/medialist/detail/";
#region 视频
@ -290,6 +292,52 @@ namespace DownKyi.Core.BiliApi.BiliUtils
#endregion
#region 收藏夹
/// <summary>
/// 是否为收藏夹id
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static bool IsFavoritesId(string input)
{
return IsIntId(input, "ml");
}
/// <summary>
/// 是否为收藏夹url
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static bool IsFavoritesUrl(string input)
{
string favoritesId = GetId(input, FavoritesUrl);
return IsFavoritesId(favoritesId);
}
/// <summary>
/// 获取收藏夹id
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static long GetFavoritesId(string input)
{
if (IsFavoritesId(input))
{
return Number.GetInt(input.Remove(0, 2));
}
else if (IsFavoritesUrl(input))
{
return Number.GetInt(GetId(input, FavoritesUrl).Remove(0, 2));
}
else
{
return -1;
}
}
#endregion
#region 用户空间
/// <summary>

@ -0,0 +1,104 @@
using Bilibili.Community.Service.Dm.V1;
using DownKyi.Core.BiliApi.Danmaku.Models;
using System;
using System.Collections.Generic;
using System.IO;
namespace DownKyi.Core.BiliApi.Danmaku
{
public static class DanmakuProtobuf
{
/// <summary>
/// 下载6分钟内的弹幕返回弹幕列表
/// </summary>
/// <param name="avid">稿件avID</param>
/// <param name="cid">视频CID</param>
/// <param name="segmentIndex">分包每6分钟一包</param>
/// <returns></returns>
public static List<BiliDanmaku> 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";
string directory = Path.Combine(Storage.StorageManager.GetDanmaku(), $"{cid}");
string filePath = Path.Combine(directory, $"{segmentIndex}.proto");
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
try
{
System.Net.WebClient mywebclient = new System.Net.WebClient();
mywebclient.DownloadFile(url, filePath);
}
catch (Exception e)
{
Utils.Debug.Console.PrintLine("GetDanmakuProto()发生异常: {0}", e);
//Logging.LogManager.Error(e);
}
var danmakuList = new List<BiliDanmaku>();
try
{
using (var input = File.OpenRead(filePath))
{
DmSegMobileReply 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)
{
Utils.Debug.Console.PrintLine("GetDanmakuProto()发生异常: {0}", e);
//Logging.LogManager.Error(e);
return null;
}
return danmakuList;
}
/// <summary>
/// 下载所有弹幕,返回弹幕列表
/// </summary>
/// <param name="avid">稿件avID</param>
/// <param name="cid">视频CID</param>
/// <returns></returns>
public static List<BiliDanmaku> GetAllDanmakuProto(long avid, long cid)
{
var danmakuList = new List<BiliDanmaku>();
int segmentIndex = 0;
while (true)
{
segmentIndex += 1;
var danmakus = GetDanmakuProto(avid, cid, segmentIndex);
if (danmakus == null) { break; }
danmakuList.AddRange(danmakus);
}
return danmakuList;
}
}
}

@ -0,0 +1,33 @@
namespace DownKyi.Core.BiliApi.Danmaku.Models
{
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()
{
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}";
}
}
}

@ -0,0 +1,169 @@
using DownKyi.Core.BiliApi.Danmaku;
using System.Collections.Generic;
namespace DownKyi.Core.Danmaku2Ass
{
public class Bilibili
{
private static Bilibili instance;
private readonly Dictionary<string, bool> config = new Dictionary<string, bool>
{
{ "top_filter", false },
{ "bottom_filter", false },
{ "scroll_filter", false }
};
private readonly Dictionary<int, string> mapping = new Dictionary<int, string>
{
{ 0, "none" }, // 保留项
{ 1, "scroll" },
{ 2, "scroll" },
{ 3, "scroll" },
{ 4, "bottom" },
{ 5, "top" },
{ 6, "scroll" }, // 逆向滚动弹幕,还是当滚动处理
{ 7, "none" }, // 高级弹幕,暂时不要考虑
{ 8, "none" }, // 代码弹幕,暂时不要考虑
{ 9, "none" }, // BAS弹幕暂时不要考虑
{ 10, "none" }, // 未知,暂时不要考虑
{ 11, "none" }, // 保留项
{ 12, "none" }, // 保留项
{ 13, "none" }, // 保留项
{ 14, "none" }, // 保留项
{ 15, "none" }, // 保留项
};
// 弹幕标准字体大小
private readonly int normalFontSize = 25;
/// <summary>
/// 获取Bilibili实例
/// </summary>
/// <returns></returns>
public static Bilibili GetInstance()
{
if (instance == null)
{
instance = new Bilibili();
}
return instance;
}
/// <summary>
/// 隐藏Bilibili()方法,必须使用单例模式
/// </summary>
private Bilibili() { }
/// <summary>
/// 是否屏蔽顶部弹幕
/// </summary>
/// <param name="isFilter"></param>
/// <returns></returns>
public Bilibili SetTopFilter(bool isFilter)
{
config["top_filter"] = isFilter;
return this;
}
/// <summary>
/// 是否屏蔽底部弹幕
/// </summary>
/// <param name="isFilter"></param>
/// <returns></returns>
public Bilibili SetBottomFilter(bool isFilter)
{
config["bottom_filter"] = isFilter;
return this;
}
/// <summary>
/// 是否屏蔽滚动弹幕
/// </summary>
/// <param name="isFilter"></param>
/// <returns></returns>
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.GetAllDanmakuProto(avid, cid);
// 按弹幕出现顺序排序
biliDanmakus.Sort((x, y) => { return x.Progress.CompareTo(y.Progress); });
var danmakus = new List<Danmaku>();
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<string, int> GetResolution(int quality)
{
var resolution = new Dictionary<string, int>
{
{ "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;
}
}
}

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DownKyi.Core.Danmaku2Ass
{
/// <summary>
/// 碰撞处理
/// </summary>
public class Collision
{
private readonly int lineCount;
private readonly List<int> leaves;
public Collision(int lineCount)
{
this.lineCount = lineCount;
leaves = Leaves();
}
private List<int> Leaves()
{
var ret = new List<int>(lineCount);
for (int i = 0; i < lineCount; i++) ret.Add(0);
return ret;
}
/// <summary>
/// 碰撞检测
/// 返回行号和时间偏移
/// </summary>
/// <param name="display"></param>
/// <returns></returns>
public Tuple<int, float> Detect(Display display)
{
List<float> beyonds = new List<float>();
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);
}
}
}

@ -0,0 +1,57 @@
using System;
namespace DownKyi.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";
}
}

@ -0,0 +1,87 @@
using System.Collections.Generic;
namespace DownKyi.Core.Danmaku2Ass
{
/// <summary>
/// 创建器
/// </summary>
public class Creater
{
public Config Config;
public List<Danmaku> Danmakus;
public List<Subtitle> Subtitles;
public string Text;
public Creater(Config config, List<Danmaku> danmakus)
{
Config = config;
Danmakus = danmakus;
Subtitles = SetSubtitles();
Text = SetText();
}
protected List<Subtitle> SetSubtitles()
{
var scroll = new Collision(Config.LineCount);
var stayed = new Collision(Config.LineCount);
Dictionary<string, Collision> collisions = new Dictionary<string, Collision>
{
{ "scroll", scroll },
{ "top", stayed },
{ "bottom", stayed }
};
List<Subtitle> subtitles = new List<Subtitle>();
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;
}
}
}

@ -0,0 +1,12 @@
namespace DownKyi.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; }
}
}

@ -0,0 +1,406 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace DownKyi.Core.Danmaku2Ass
{
/// <summary>
/// 显示方式
/// </summary>
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<int, int> Horizontal;
public Tuple<int, int> 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();
}
/// <summary>
/// 根据弹幕样式自动创建对应的 Display 类
/// </summary>
/// <returns></returns>
public static Display Factory(Config config, Danmaku danmaku)
{
Dictionary<string, Display> dict = new Dictionary<string, Display>
{
{ "scroll", new ScrollDisplay(config, danmaku) },
{ "top", new TopDisplay(config, danmaku) },
{ "bottom", new BottomDisplay(config, danmaku) }
};
return dict[danmaku.Style];
}
/// <summary>
/// 字体大小
/// 按用户自定义的字体大小来缩放
/// </summary>
/// <returns></returns>
protected int SetFontSize()
{
if (IsScaled)
{
Console.WriteLine($"{Danmaku.SizeRatio}");
}
return Utils.IntCeiling(Config.BaseFontSize * Danmaku.SizeRatio);
}
/// <summary>
/// 字体是否被缩放过
/// </summary>
/// <returns></returns>
protected bool SetIsScaled()
{
return !Math.Round(Danmaku.SizeRatio, 2).Equals(1.0);
//return Danmaku.SizeRatio.Equals(1.0f);
}
/// <summary>
/// 最长的行字符数
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// 整条字幕宽度
/// </summary>
/// <returns></returns>
protected int SetWidth()
{
float charCount = MaxLength;// / 2;
return Utils.IntCeiling(FontSize * charCount);
}
/// <summary>
/// 整条字幕高度
/// </summary>
/// <returns></returns>
protected int SetHeight()
{
int lineCount = Danmaku.Content.Split('\n').Length;
return lineCount * FontSize;
}
/// <summary>
/// 出现和消失的水平坐标位置
/// 默认在屏幕中间
/// </summary>
/// <returns></returns>
protected virtual Tuple<int, int> SetHorizontal()
{
int x = (int)Math.Floor(Config.ScreenWidth / 2.0);
return Tuple.Create(x, x);
}
/// <summary>
/// 出现和消失的垂直坐标位置
/// 默认在屏幕中间
/// </summary>
/// <returns></returns>
protected virtual Tuple<int, int> SetVertical()
{
int y = (int)Math.Floor(Config.ScreenHeight / 2.0);
return Tuple.Create(y, y);
}
/// <summary>
/// 整条字幕的显示时间
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// 离开碰撞时间
/// </summary>
/// <returns></returns>
protected virtual int SetLeave()
{
return (int)(Danmaku.Start + Duration);
}
/// <summary>
/// 按照新的行号重新布局
/// </summary>
/// <param name="lineIndex"></param>
public void Relayout(int lineIndex)
{
LineIndex = lineIndex;
Horizontal = SetHorizontal();
Vertical = SetVertical();
}
}
/// <summary>
/// 顶部
/// </summary>
public class TopDisplay : Display
{
public TopDisplay(Config config, Danmaku danmaku) : base(config, danmaku)
{
Console.WriteLine("TopDisplay constructor.");
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected override Tuple<int, int> SetVertical()
{
// 这里 y 坐标为 0 就是最顶行了
int y = LineIndex * Config.BaseFontSize;
return Tuple.Create(y, y);
}
}
/// <summary>
/// 底部
/// </summary>
public class BottomDisplay : Display
{
public BottomDisplay(Config config, Danmaku danmaku) : base(config, danmaku)
{
Console.WriteLine("BottomDisplay constructor.");
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected override Tuple<int, int> SetVertical()
{
// 要让字幕不超出底部,减去高度
int y = Config.ScreenHeight - (LineIndex * Config.BaseFontSize) - Height;
// 再减去自定义的底部边距
y -= Config.BottomMargin;
return Tuple.Create(y, y);
}
}
/// <summary>
/// 滚动
/// </summary>
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();
}
/// <summary>
/// ASS 的水平位置参考点是整条字幕文本的中点
/// </summary>
/// <returns></returns>
protected override Tuple<int, int> 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<int, int> SetVertical()
{
int baseFontSize = Config.BaseFontSize;
// 垂直位置,按基准字体大小算每一行的高度
int y = (LineIndex + 1) * baseFontSize;
// 个别弹幕可能字体比基准要大,所以最上的一行还要避免挤出顶部屏幕
// 坐标不能小于字体大小
if (y < FontSize)
{
y = FontSize;
}
return Tuple.Create(y, y);
}
/// <summary>
/// 字幕坐标点的移动距离
/// </summary>
/// <returns></returns>
protected int SetDistance()
{
Tuple<int, int> x = Horizontal;
return x.Item1 - x.Item2;
}
/// <summary>
/// 字幕每个字的移动的速度
/// </summary>
/// <returns></returns>
protected int SetSpeed()
{
// 基准时间,就是每个字的移动时间
// 12 秒加上用户自定义的微调
int baseDuration = 12 + Config.TuneDuration;
if (baseDuration <= 0)
{
baseDuration = 1;
}
return Utils.IntCeiling(Config.ScreenWidth / baseDuration);
}
/// <summary>
/// 计算每条弹幕的显示时长,同步方式
/// 每个弹幕的滚动速度都一样,辨认度好,适合观看剧集类视频。
/// </summary>
/// <returns></returns>
public int SyncDuration()
{
return Distance / Speed;
}
/// <summary>
/// 计算每条弹幕的显示时长,异步方式
/// 每个弹幕的滚动速度都不一样,动态调整,辨认度低,适合观看 MTV 类视频。
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// 整条字幕的移动时间
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// 离开碰撞时间
/// </summary>
/// <returns></returns>
protected override int SetLeave()
{
// 对于滚动样式弹幕来说,就是最后一个字符离开最右边缘的时间
// 坐标是字幕中点,在屏幕外和内各有半个字幕宽度
// 也就是跑过一个字幕宽度的路程
float duration = Width / Speed;
return (int)(Danmaku.Start + duration);
}
}
}

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
namespace DownKyi.Core.Danmaku2Ass
{
/// <summary>
/// 过滤器基类
/// </summary>
public class Filter
{
public virtual List<Danmaku> DoFilter(List<Danmaku> danmakus)
{
throw new NotImplementedException("使用了过滤器的未实现的方法。");
}
}
/// <summary>
/// 顶部样式过滤器
/// </summary>
public class TopFilter : Filter
{
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
{
List<Danmaku> keep = new List<Danmaku>();
foreach (var danmaku in danmakus)
{
if (danmaku.Style == "top")
{
continue;
}
keep.Add(danmaku);
}
return keep;
}
}
/// <summary>
/// 底部样式过滤器
/// </summary>
public class BottomFilter : Filter
{
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
{
List<Danmaku> keep = new List<Danmaku>();
foreach (var danmaku in danmakus)
{
if (danmaku.Style == "bottom")
{
continue;
}
keep.Add(danmaku);
}
return keep;
}
}
/// <summary>
/// 滚动样式过滤器
/// </summary>
public class ScrollFilter : Filter
{
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
{
List<Danmaku> keep = new List<Danmaku>();
foreach (var danmaku in danmakus)
{
if (danmaku.Style == "scroll")
{
continue;
}
keep.Add(danmaku);
}
return keep;
}
}
/// <summary>
/// 自定义过滤器
/// </summary>
public class CustomFilter : Filter
{
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
{
// TODO
return base.DoFilter(danmakus);
}
}
}

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DownKyi.Core.Danmaku2Ass
{
public class Producer
{
public Dictionary<string, bool> Config;
public Dictionary<string, Filter> Filters;
public List<Danmaku> Danmakus;
public List<Danmaku> KeepedDanmakus;
public Dictionary<string, int> FilterDetail;
public Producer(Dictionary<string, bool> config, List<Danmaku> danmakus)
{
Config = config;
Danmakus = danmakus;
}
public void StartHandle()
{
LoadFilter();
ApplyFilter();
}
public void LoadFilter()
{
Filters = new Dictionary<string, Filter>();
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<string, int> filterDetail = new Dictionary<string, int>() {
{ "top_filter",0},
{ "bottom_filter",0},
{ "scroll_filter",0},
//{ "custom_filter",0}
};
List<Danmaku> 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<string, int> Report()
{
int blockedCount = 0;
foreach (int count in FilterDetail.Values)
{
blockedCount += count;
}
int passedCount = KeepedDanmakus.Count;
int totalCount = blockedCount + passedCount;
Dictionary<string, int> ret = new Dictionary<string, int>
{
{ "blocked", blockedCount },
{ "passed", passedCount },
{ "total", totalCount }
};
return (Dictionary<string, int>)ret.Concat(FilterDetail);
}
}
}

@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace DownKyi.Core.Danmaku2Ass
{
/// <summary>
/// 字幕工程类
/// </summary>
public class Studio
{
public Config Config;
public List<Danmaku> Danmakus;
public Creater Creater;
public int KeepedCount;
public int DropedCount;
public Studio(Config config, List<Danmaku> danmakus)
{
Config = config;
Danmakus = danmakus;
}
public void StartHandle()
{
Creater = SetCreater();
KeepedCount = SetKeepedCount();
DropedCount = SetDropedCount();
}
/// <summary>
/// ass 创建器
/// </summary>
/// <returns></returns>
protected Creater SetCreater()
{
return new Creater(Config, Danmakus);
}
/// <summary>
/// 保留条数
/// </summary>
/// <returns></returns>
protected int SetKeepedCount()
{
return Creater.Subtitles.Count();
}
/// <summary>
/// 丢弃条数
/// </summary>
/// <returns></returns>
protected int SetDropedCount()
{
return Danmakus.Count - KeepedCount;
}
/// <summary>
/// 创建 ass 字幕
/// </summary>
/// <param name="fileName"></param>
public void CreateAssFile(string fileName)
{
CreateFile(fileName, Creater.Text);
}
public void CreateFile(string fileName, string text)
{
File.WriteAllText(fileName, text);
}
public Dictionary<string, int> Report()
{
return new Dictionary<string, int>()
{
{"total", Danmakus.Count},
{"droped", DropedCount},
{"keeped", KeepedCount},
};
}
}
}

@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
namespace DownKyi.Core.Danmaku2Ass
{
/// <summary>
/// 字幕
/// </summary>
public class Subtitle
{
public Danmaku Danmaku;
public Display Display;
public float Offset;
public float Start;
public float End;
public string Color;
public Dictionary<string, int> 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<string, int> SetPosition()
{
Tuple<int, int> x = Display.Horizontal;
Tuple<int, int> y = Display.Vertical;
Dictionary<string, int> value = new Dictionary<string, int>
{
{ "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}";
}
}
}

@ -0,0 +1,227 @@
using System;
using System.Linq;
using System.Text;
namespace DownKyi.Core.Danmaku2Ass
{
internal static class Utils
{
/// <summary>
/// 向上取整返回int类型
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
public static int IntCeiling(float number)
{
return (int)Math.Ceiling(number);
}
/// <summary>
/// 字符长度1个汉字当2个英文
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static int DisplayLength(string text)
{
return Encoding.Default.GetBytes(text).Length;
}
/// <summary>
/// 修正一些评论者的拼写错误
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string CorrectTypos(string text)
{
text = text.Replace("/n", "\\N");
text = text.Replace("&gt;", ">");
text = text.Replace("&lt;", "<");
return text;
}
/// <summary>
/// 秒数转 时:分:秒 格式
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
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}";
}
/// <summary>
/// 时:分:秒 格式转 秒数
/// </summary>
/// <param name="hms"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 同Hms2second(string hms),不过可以用 +/- 符号来连接多个
/// 即 3:00-2:30 相当于 30 秒
/// </summary>
/// <param name="xhms"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 颜色值,整型转 RGB
/// </summary>
/// <param name="integer"></param>
/// <returns></returns>
public static string Int2rgb(int integer)
{
return integer.ToString("X").PadLeft(6, '0'); ;
}
/// <summary>
/// 颜色值,整型转 BGR
/// </summary>
/// <param name="integer"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 颜色值,整型转 HLS
/// </summary>
/// <param name="integer"></param>
/// <returns></returns>
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;
}
/// <summary>
/// HLS: Hue, Luminance, Saturation
/// H: position in the spectrum
/// L: color lightness
/// S: color saturation
/// </summary>
/// <param name="rgb"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 是否属于暗色
/// </summary>
/// <param name="integer"></param>
/// <returns></returns>
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;
}
}
}

@ -36,6 +36,9 @@
<Reference Include="Brotli.Core, Version=2.1.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Brotli.NET.2.1.1\lib\net45\Brotli.Core.dll</HintPath>
</Reference>
<Reference Include="Google.Protobuf, Version=3.18.1.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604, processorArchitecture=MSIL">
<HintPath>..\packages\Google.Protobuf.3.18.1\lib\net45\Google.Protobuf.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>
@ -45,13 +48,25 @@
<HintPath>..\packages\QRCoder.1.4.1\lib\net40\QRCoder.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Data.SQLite, Version=1.0.112.1, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
<HintPath>..\packages\System.Data.SQLite.Core.1.0.112.2\lib\net40\System.Data.SQLite.dll</HintPath>
</Reference>
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
<Private>True</Private>
@ -123,6 +138,8 @@
<Compile Include="BiliApi\Cheese\Models\CheeseStat.cs" />
<Compile Include="BiliApi\Cheese\Models\CheeseUpInfo.cs" />
<Compile Include="BiliApi\Cheese\Models\CheeseView.cs" />
<Compile Include="BiliApi\Danmaku\DanmakuProtobuf.cs" />
<Compile Include="BiliApi\Danmaku\Models\BiliDanmaku.cs" />
<Compile Include="BiliApi\Login\LoginHelper.cs" />
<Compile Include="BiliApi\Models\BaseModel.cs" />
<Compile Include="BiliApi\Login\LoginInfo.cs" />
@ -134,6 +151,7 @@
<Compile Include="BiliApi\Login\Models\UserInfoForNavigation.cs" />
<Compile Include="BiliApi\Models\Dimension.cs" />
<Compile Include="BiliApi\Models\VideoOwner.cs" />
<Compile Include="BiliApi\protobuf\bilibili\community\service\dm\v1\Dm.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrl.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrlDash.cs" />
<Compile Include="BiliApi\VideoStream\Models\PlayUrlDashVideo.cs" />
@ -161,6 +179,20 @@
<Compile Include="BiliApi\WebClient.cs" />
<Compile Include="BiliApi\Zone\VideoZone.cs" />
<Compile Include="BiliApi\Zone\ZoneAttr.cs" />
<Compile Include="Danmaku2Ass\Bilibili.cs" />
<Compile Include="Danmaku2Ass\Collision.cs" />
<Compile Include="Danmaku2Ass\Config.cs" />
<Compile Include="Danmaku2Ass\Creater.cs" />
<Compile Include="Danmaku2Ass\Danmaku.cs" />
<Compile Include="Danmaku2Ass\Display.cs" />
<Compile Include="Danmaku2Ass\Filter.cs" />
<Compile Include="Danmaku2Ass\Producer.cs" />
<Compile Include="Danmaku2Ass\Studio.cs" />
<Compile Include="Danmaku2Ass\Subtitle.cs" />
<Compile Include="Danmaku2Ass\Utils.cs" />
<Compile Include="Downloader\MultiThreadDownloader.cs" />
<Compile Include="Downloader\PartialDownloader.cs" />
<Compile Include="FFmpeg\FFmpegHelper.cs" />
<Compile Include="Logging\LogInfo.cs" />
<Compile Include="Logging\LogLevel.cs" />
<Compile Include="Logging\LogManager.cs" />
@ -210,6 +242,7 @@
<Compile Include="Utils\Web.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup />
@ -221,6 +254,8 @@
</PropertyGroup>
<Error Condition="!Exists('..\packages\Brotli.NET.2.1.1\build\Brotli.NET.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Brotli.NET.2.1.1\build\Brotli.NET.targets'))" />
<Error Condition="!Exists('..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets'))" />
<Error Condition="!Exists('..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets'))" />
</Target>
<Import Project="..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets" Condition="Exists('..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets')" />
<Import Project="..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets" Condition="Exists('..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets')" />
</Project>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -1,8 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Brotli.NET" version="2.1.1" targetFramework="net472" />
<package id="Google.Protobuf" version="3.18.1" targetFramework="net472" />
<package id="Google.Protobuf.Tools" version="3.18.1" targetFramework="net472" />
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
<package id="QRCoder" version="1.4.1" targetFramework="net472" />
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
<package id="System.Data.SQLite.Core" version="1.0.112.2" targetFramework="net472" />
<package id="System.Memory" version="4.5.4" targetFramework="net472" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="5.0.0" targetFramework="net472" />
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
</packages>
Loading…
Cancel
Save