mirror of
https://github.com/cxfksword/jellyfin-plugin-danmu.git
synced 2026-02-02 17:59:58 +08:00
Add generate ass
This commit is contained in:
43
Jellyfin.Plugin.Danmu.Test/ToAssTest.cs
Normal file
43
Jellyfin.Plugin.Danmu.Test/ToAssTest.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Danmaku2Ass;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Test
|
||||
{
|
||||
[TestClass]
|
||||
public class ToAssTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestToAss()
|
||||
{
|
||||
var xml = @"<?xml version=""1.0"" encoding=""UTF-8""?><i><chatserver>chat.bilibili.com</chatserver><chatid>13113033</chatid><mission>0</mission><maxlimit>3000</maxlimit><state>0</state><real_name>0</real_name><source>k-v</source>
|
||||
<d p=""253.09700,1,30,16777215,1665229999,0,e890f28,1158792239837718272,11"">杨笑汝有受到羽海野千花影响啦</d>
|
||||
<d p=""225.85500,1,25,16777215,1665229198,0,e890f28,1158785518893433856,11"">风间笑死我了</d>
|
||||
<d p=""213.22500,1,25,16777215,1665229172,0,e890f28,1158785301788090624,11"">有搬运车的</d>
|
||||
<d p=""253.71600,1,25,16777215,1663566786,0,4cd9e142,1144840193279505664,11"">杨笑汝最喜欢的就是野海羽千花!</d>
|
||||
<d p=""634.75000,1,25,16777215,1663517171,0,43c9cec0,1144424000059966464,11"">给女儿买衣服哈哈哈哈哈</d>
|
||||
<d p=""1110.38100,1,25,16777215,1662232214,0,e8930b5b,1133644991965657344,11"">kdhr这会才15吧</d>
|
||||
<d p=""1167.49900,1,25,16777215,1662112456,0,9589ad2c,1132640394031491584,11"">真田情感好细腻</d>
|
||||
<d p=""55.38000,1,25,16777215,1660413677,0,9c28a5a9,1118390004910248704,11"">这个op看得我好迷茫</d></i>
|
||||
";
|
||||
|
||||
var ass = Bilibili.GetInstance().ToASS(xml, new Config());
|
||||
Console.WriteLine(ass);
|
||||
Assert.IsNotNull(ass);
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestToAssFile()
|
||||
{
|
||||
var xml = File.ReadAllText(@"F:\ddd\11111.xml");
|
||||
|
||||
|
||||
Bilibili.GetInstance().Create(xml, new Config(), @"F:\ddd\11111.ass");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,10 +123,11 @@ namespace Jellyfin.Plugin.Danmu.Api
|
||||
throw new Exception($"Request fail. cid={cid}");
|
||||
}
|
||||
|
||||
// 数据太小可能是已经被b站下架
|
||||
// 数据太小可能是已经被b站下架,返回了出错信息
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (bytes == null || bytes.Length < 1000)
|
||||
if (bytes == null || bytes.Length < 2000)
|
||||
{
|
||||
this._logger.LogWarning("弹幕获取失败,可能视频已下架或弹幕太少. url: {0}", url);
|
||||
throw new Exception($"Request fail. cid={cid}");
|
||||
}
|
||||
|
||||
|
||||
@@ -28,30 +28,43 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
// set default options here
|
||||
Options = SomeOptions.AnotherOption;
|
||||
TrueFalseSetting = true;
|
||||
AnInteger = 2;
|
||||
AString = "string";
|
||||
ToAss = false;
|
||||
AssFont = string.Empty;
|
||||
AssFontSize = string.Empty;
|
||||
AssLineCount = string.Empty;
|
||||
AssSpeed = string.Empty;
|
||||
AssTextOpacity = string.Empty;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether some true or false setting is enabled..
|
||||
/// 是否同时生成ASS格式弹幕.
|
||||
/// </summary>
|
||||
public bool TrueFalseSetting { get; set; }
|
||||
public bool ToAss { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an integer setting.
|
||||
/// <summary>
|
||||
/// 字体.
|
||||
/// </summary>
|
||||
public int AnInteger { get; set; }
|
||||
public string AssFont { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a string setting.
|
||||
/// <summary>
|
||||
/// 字体大小.
|
||||
/// </summary>
|
||||
public string AString { get; set; }
|
||||
public string AssFontSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an enum option.
|
||||
/// <summary>
|
||||
/// 限制行数.
|
||||
/// </summary>
|
||||
public SomeOptions Options { get; set; }
|
||||
public string AssLineCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 移动速度.
|
||||
/// </summary>
|
||||
public string AssSpeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度.
|
||||
/// </summary>
|
||||
public string AssTextOpacity { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -9,28 +9,37 @@
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<form id="TemplateConfigForm">
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="Options">Several Options</label>
|
||||
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
|
||||
<option id="optOneOption" value="OneOption">One Option</option>
|
||||
<option id="optAnotherOption" value="AnotherOption">Another Option</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label>
|
||||
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">A Description</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="TrueFalseSetting" name="TrueFalseCheckBox" type="checkbox" is="emby-checkbox" />
|
||||
<span>A Checkbox</span>
|
||||
<input id="ToAss" name="ToAss" type="checkbox" is="emby-checkbox" />
|
||||
<span>同时生成ASS格式弹幕</span>
|
||||
</label>
|
||||
<div class="fieldDescription">勾选后,会在视频目录下生成ass格式的弹幕,命名格式:[视频名].danmu.ass</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputeLabel inputLabelUnfocused" for="AString">A String</label>
|
||||
<input id="AString" name="AString" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">Another Description</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssFont">ass弹幕字体</label>
|
||||
<input id="AssFont" name="AssFont" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">可为空,默认黑体.</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssFontSize">ass弹幕字体大小</label>
|
||||
<input id="AssFontSize" name="AssFontSize" type="number" is="emby-input" min="25" />
|
||||
<div class="fieldDescription">可为空,默认60,可以此为基准,增大或缩小.</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssTextOpacity">ass弹幕字体透明度</label>
|
||||
<input id="AssTextOpacity" name="AssTextOpacity" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">可为空,默认1,表示不透明,数值在0.0~1.0之间</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssLineCount">ass弹幕显示行数</label>
|
||||
<input id="AssLineCount" name="AssLineCount" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">可为空,默认全屏显示,1/4屏可填5,半屏可填9</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssSpeed">ass弹幕移动速度</label>
|
||||
<input id="AssSpeed" name="AssSpeed" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">可为空,默认8秒</div>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
@@ -49,10 +58,12 @@
|
||||
.addEventListener('pageshow', function() {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
document.querySelector('#Options').value = config.Options;
|
||||
document.querySelector('#AnInteger').value = config.AnInteger;
|
||||
document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting;
|
||||
document.querySelector('#AString').value = config.AString;
|
||||
document.querySelector('#ToAss').checked = config.ToAss;
|
||||
document.querySelector('#AssFont').value = config.AssFont;
|
||||
document.querySelector('#AssFontSize').value = config.AssFontSize;
|
||||
document.querySelector('#AssTextOpacity').value = config.AssTextOpacity;
|
||||
document.querySelector('#AssLineCount').value = config.AssLineCount;
|
||||
document.querySelector('#AssSpeed').value = config.AssSpeed;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
@@ -61,10 +72,12 @@
|
||||
.addEventListener('submit', function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
config.Options = document.querySelector('#Options').value;
|
||||
config.AnInteger = document.querySelector('#AnInteger').value;
|
||||
config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked;
|
||||
config.AString = document.querySelector('#AString').value;
|
||||
config.ToAss = document.querySelector('#ToAss').checked;
|
||||
config.AssFont = document.querySelector('#AssFont').value;
|
||||
config.AssFontSize = document.querySelector('#AssFontSize').value;
|
||||
config.AssTextOpacity = document.querySelector('#AssTextOpacity').value;
|
||||
config.AssLineCount = document.querySelector('#AssLineCount').value;
|
||||
config.AssSpeed = document.querySelector('#AssSpeed').value;
|
||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
|
||||
33
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/BiliDanmaku.cs
Normal file
33
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/BiliDanmaku.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Jellyfin.Plugin.Danmu.Core.Danmaku2Ass
|
||||
{
|
||||
public class BiliDanmaku
|
||||
{
|
||||
public long Id { get; set; } //弹幕dmID
|
||||
public int Progress { get; set; } //出现时间(单位ms)
|
||||
public int Mode { get; set; } //弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
277
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Bilibili.cs
Normal file
277
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Bilibili.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using Jellyfin.Plugin.Danmu.Core.Danmaku2Ass;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
|
||||
namespace 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 void Create(string xml, Config subtitleConfig, string assFile)
|
||||
{
|
||||
var danmakus = ParseXml(xml);
|
||||
|
||||
// 弹幕预处理
|
||||
Producer producer = new Producer(config, danmakus);
|
||||
producer.StartHandle();
|
||||
|
||||
// 字幕生成
|
||||
var keepedDanmakus = producer.KeepedDanmakus;
|
||||
var studio = new Studio(subtitleConfig, keepedDanmakus);
|
||||
studio.StartHandle();
|
||||
studio.CreateAssFile(assFile);
|
||||
}
|
||||
|
||||
public string ToASS(string xml, Config subtitleConfig)
|
||||
{
|
||||
var danmakus = ParseXml(xml);
|
||||
|
||||
// 弹幕预处理
|
||||
Producer producer = new Producer(config, danmakus);
|
||||
producer.StartHandle();
|
||||
|
||||
// 字幕生成
|
||||
var keepedDanmakus = producer.KeepedDanmakus;
|
||||
var studio = new Studio(subtitleConfig, keepedDanmakus);
|
||||
studio.StartHandle();
|
||||
return studio.GetText();
|
||||
}
|
||||
|
||||
public List<Danmaku> ParseXml(string xml)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
|
||||
var calFontSizeDict = new Dictionary<int, int>();
|
||||
var biliDanmakus = new List<BiliDanmaku>();
|
||||
var nodes = doc.GetElementsByTagName("d");
|
||||
foreach (XmlNode node in nodes)
|
||||
{
|
||||
// bilibili弹幕格式:
|
||||
// <d p="944.95400,5,25,16707842,1657598634,0,ece5c9d1,1094775706690331648,11">今天的风儿甚是喧嚣</d>
|
||||
// time, mode, size, color, create, pool, sender, id, weight(屏蔽等级)
|
||||
var p = node.Attributes["p"];
|
||||
if (p == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var danmaku = new BiliDanmaku();
|
||||
var arr = p.Value.Split(",");
|
||||
danmaku.Progress = (int)(Convert.ToDouble(arr[0]) * 1000);
|
||||
danmaku.Mode = Convert.ToInt32(arr[1]);
|
||||
danmaku.Fontsize = Convert.ToInt32(arr[2]);
|
||||
danmaku.Color = Convert.ToUInt32(arr[3]);
|
||||
danmaku.Ctime = Convert.ToInt64(arr[4]);
|
||||
danmaku.Pool = Convert.ToInt32(arr[5]);
|
||||
danmaku.MidHash = arr[6];
|
||||
danmaku.Id = Convert.ToInt64(arr[7]);
|
||||
danmaku.Weight = Convert.ToInt32(arr[8]);
|
||||
danmaku.Content = node.InnerText;
|
||||
|
||||
biliDanmakus.Add(danmaku);
|
||||
|
||||
if (calFontSizeDict.ContainsKey(danmaku.Fontsize))
|
||||
{
|
||||
calFontSizeDict[danmaku.Fontsize]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
calFontSizeDict[danmaku.Fontsize] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 按弹幕出现顺序排序
|
||||
biliDanmakus.Sort((x, y) => { return x.Progress.CompareTo(y.Progress); });
|
||||
|
||||
// 获取使用最多的字体大小
|
||||
var mostUsedFontSize = this.normalFontSize;
|
||||
if (calFontSizeDict.Count > 0)
|
||||
{
|
||||
mostUsedFontSize = calFontSizeDict.OrderByDescending(x => x.Value).First().Key;
|
||||
}
|
||||
|
||||
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 / mostUsedFontSize
|
||||
};
|
||||
|
||||
danmakus.Add(danmaku);
|
||||
}
|
||||
|
||||
return danmakus;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Collision.cs
Normal file
60
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Collision.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Config.cs
Normal file
84
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Config.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Jellyfin.Plugin.Danmu.Core.Danmaku2Ass;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Danmaku2Ass
|
||||
{
|
||||
public class Config
|
||||
{
|
||||
public string Title = "Danmaku";
|
||||
public int ScreenWidth = 1920;
|
||||
public int ScreenHeight = 1080;
|
||||
public string FontName = "黑体";
|
||||
public int BaseFontSize = 60; // 字体大小,像素
|
||||
public float _textOpacity = 1.0f; // 透明度,0完全透明,1不透明
|
||||
public float TextOpacity
|
||||
{
|
||||
get { return _textOpacity; }
|
||||
set {
|
||||
if (value > 0 && value <= 1.0)
|
||||
{
|
||||
_textOpacity = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_textOpacity = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String FontColor
|
||||
{
|
||||
get
|
||||
{
|
||||
var c = Color.FromArgb((byte)((1.0f - TextOpacity) * byte.MaxValue), 255, 255, 255);
|
||||
return string.Format("&H{0}{1}{2}{3}", c.A.ToString("X2"), c.R.ToString("X2"), c.G.ToString("X2"), c.B.ToString("X2"));
|
||||
}
|
||||
}
|
||||
|
||||
// 限制行数
|
||||
private int lineCount;
|
||||
public int LineCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (lineCount == 0)
|
||||
{
|
||||
return (int)Math.Floor(ScreenHeight / BaseFontSize * 1.0);
|
||||
}
|
||||
return lineCount;
|
||||
}
|
||||
set
|
||||
{
|
||||
lineCount = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DanmakuLayoutAlgorithm LayoutAlgorithm = DanmakuLayoutAlgorithm.Sync; // 布局算法,async/sync
|
||||
public int TuneDuration; // 微调时长
|
||||
public int DropOffset; // 丢弃偏移
|
||||
public int BottomMargin; // 底部边距
|
||||
public int CustomOffset; // 自定义偏移
|
||||
public string HeaderTemplate = @"[Script Info]
|
||||
; Script generated by jellyfin-plugin-danmu
|
||||
; https://github.com/cxfksword/jellyfin-plugin-danmu
|
||||
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},{fontcolor},{fontcolor},&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";
|
||||
}
|
||||
}
|
||||
88
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Creater.cs
Normal file
88
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Creater.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace 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())
|
||||
.Replace("{fontcolor}", Config.FontColor);
|
||||
|
||||
string events = string.Empty;
|
||||
foreach (var subtitle in Subtitles)
|
||||
{
|
||||
events += "\n" + subtitle.Text;
|
||||
}
|
||||
|
||||
return header + events;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Danmaku.cs
Normal file
12
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Danmaku.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace 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,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Core.Danmaku2Ass
|
||||
{
|
||||
public enum DanmakuLayoutAlgorithm
|
||||
{
|
||||
None = 0,
|
||||
Async,
|
||||
Sync
|
||||
}
|
||||
}
|
||||
407
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Display.cs
Normal file
407
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Display.cs
Normal file
@@ -0,0 +1,407 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace 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()
|
||||
{
|
||||
// 基准时间,就是每个字的移动时间
|
||||
// 8 秒加上用户自定义的微调
|
||||
int baseDuration = 8 + 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()
|
||||
{
|
||||
var layoutAlgorithm = Config.LayoutAlgorithm.ToString("G");
|
||||
string methodName = layoutAlgorithm.Substring(0, 1).ToUpper() + 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
88
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Filter.cs
Normal file
88
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Filter.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Producer.cs
Normal file
99
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Producer.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace 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 (string name in orders)
|
||||
{
|
||||
Filter filter;
|
||||
if (!this.Filters.TryGetValue(name, out filter))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Studio.cs
Normal file
94
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Studio.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace 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 string GetText()
|
||||
{
|
||||
return Creater.Text;
|
||||
}
|
||||
|
||||
public void CreateFile(string fileName, string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(fileName, text);
|
||||
}
|
||||
catch (Exception)
|
||||
{ }
|
||||
}
|
||||
|
||||
public Dictionary<string, int> Report()
|
||||
{
|
||||
return new Dictionary<string, int>()
|
||||
{
|
||||
{"total", Danmakus.Count},
|
||||
{"droped", DropedCount},
|
||||
{"keeped", KeepedCount},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
154
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Subtitle.cs
Normal file
154
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Subtitle.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
228
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Utils.cs
Normal file
228
Jellyfin.Plugin.Danmu/Core/Danmaku2Ass/Utils.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace 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(">", ">");
|
||||
text = text.Replace("<", "<");
|
||||
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);
|
||||
min = (int)Math.Floor(min % 60.0f);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,17 @@ namespace Jellyfin.Plugin.Danmu.Core
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static float ToFloat(this string s)
|
||||
{
|
||||
float val;
|
||||
if (float.TryParse(s, out val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using Danmaku2Ass;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Jellyfin.Plugin.Danmu.Api.Entity;
|
||||
using Jellyfin.Plugin.Danmu.Core;
|
||||
@@ -320,19 +324,10 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
// 视频也支持指定的BV号
|
||||
if (providerVal.StartsWith("BV", StringComparison.CurrentCulture))
|
||||
{
|
||||
var bvid = providerVal;
|
||||
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await _api.GetDanmaContentAsync(bvid, CancellationToken.None).ConfigureAwait(false);
|
||||
var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml");
|
||||
await File.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
var bvid = providerVal;
|
||||
|
||||
// 下载弹幕xml文件
|
||||
await this.DownloadDanmu(item, bvid).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -340,21 +335,12 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
|
||||
if (epId <= 0)
|
||||
{
|
||||
DeleteOldDanmu(item);
|
||||
this.DeleteOldDanmu(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await _api.GetDanmaContentAsync(epId, CancellationToken.None).ConfigureAwait(false);
|
||||
var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml");
|
||||
await File.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
await this.DownloadDanmu(item, epId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 延迟200毫秒,避免请求太频繁
|
||||
@@ -624,19 +610,10 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
if (epId <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await _api.GetDanmaContentAsync(epId, CancellationToken.None).ConfigureAwait(false);
|
||||
var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml");
|
||||
await File.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
}
|
||||
|
||||
// 下载弹幕xml文件
|
||||
await this.DownloadDanmu(item, epId).ConfigureAwait(false);
|
||||
|
||||
// 延迟200毫秒,避免请求太频繁
|
||||
Thread.Sleep(200);
|
||||
@@ -657,21 +634,12 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
// 新影片,判断是否设置epId,没的话,尝试搜索填充
|
||||
if (epId <= 0)
|
||||
{
|
||||
DeleteOldDanmu(item);
|
||||
this.DeleteOldDanmu(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await _api.GetDanmaContentAsync(epId, CancellationToken.None).ConfigureAwait(false);
|
||||
var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml");
|
||||
await File.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
await this.DownloadDanmu(item, epId).ConfigureAwait(false);
|
||||
|
||||
// 延迟200毫秒,避免请求太频繁
|
||||
Thread.Sleep(200);
|
||||
@@ -704,7 +672,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
if (eventType == EventType.Update || eventType == EventType.Add || eventType == EventType.Refresh)
|
||||
{
|
||||
var episodes = season.GetEpisodes(null, new DtoOptions(false));
|
||||
var video = await _api.GetVideoByBvidAsync(bvid, CancellationToken.None).ConfigureAwait(false);
|
||||
var video = await this._api.GetVideoByBvidAsync(bvid, CancellationToken.None).ConfigureAwait(false);
|
||||
if (video == null)
|
||||
{
|
||||
_logger.LogInformation("获取不到b站视频信息:bvid={0}", bvid);
|
||||
@@ -713,21 +681,14 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
|
||||
foreach (var (episode, idx) in episodes.WithIndex())
|
||||
{
|
||||
if (video.Pages.Length == episodes.Count)
|
||||
// 分片的集数不规范,采用大于jellyfin集数方式判断.
|
||||
if (video.Pages.Length >= episodes.Count)
|
||||
{
|
||||
var cid = video.Pages[idx].Cid;
|
||||
_logger.LogInformation("视频分片成功匹配. {0} -> index: {1}", episode.Name, idx);
|
||||
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await _api.GetDanmaContentByCidAsync(cid, CancellationToken.None).ConfigureAwait(false);
|
||||
var danmuPath = Path.Combine(episode.ContainingFolderPath, episode.FileNameWithoutExtension + ".xml");
|
||||
await File.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
await this.DownloadDanmuByCid(episode, cid).ConfigureAwait(false);
|
||||
|
||||
// 延迟200毫秒,避免请求太频繁
|
||||
Thread.Sleep(200);
|
||||
@@ -803,6 +764,94 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
_logger.LogInformation("更新b站epid到元数据完成。item数:{0}", queue.Count);
|
||||
}
|
||||
|
||||
private async Task DownloadDanmu(BaseItem item, long epId)
|
||||
{
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await this._api.GetDanmaContentAsync(epId, CancellationToken.None).ConfigureAwait(false);
|
||||
await this.DownloadDanmuInternal(item, bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadDanmu(BaseItem item, string bvid)
|
||||
{
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await this._api.GetDanmaContentAsync(bvid, CancellationToken.None).ConfigureAwait(false);
|
||||
await this.DownloadDanmuInternal(item, bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadDanmuByCid(BaseItem item, long cid)
|
||||
{
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var bytes = await this._api.GetDanmaContentByCidAsync(cid, CancellationToken.None).ConfigureAwait(false);
|
||||
await this.DownloadDanmuInternal(item, bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadDanmuInternal(BaseItem item, byte[] bytes)
|
||||
{
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
{
|
||||
var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml");
|
||||
await File.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var config = Plugin.Instance.Configuration;
|
||||
if (config.ToAss && bytes.Length > 0)
|
||||
{
|
||||
var assConfig = new Danmaku2Ass.Config();
|
||||
assConfig.Title = item.Name;
|
||||
if (!string.IsNullOrEmpty(config.AssFont.Trim()))
|
||||
{
|
||||
assConfig.FontName = config.AssFont;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssFontSize.Trim()))
|
||||
{
|
||||
assConfig.BaseFontSize = config.AssFontSize.Trim().ToInt();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssTextOpacity.Trim()))
|
||||
{
|
||||
assConfig.TextOpacity = config.AssTextOpacity.Trim().ToFloat();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssLineCount.Trim()))
|
||||
{
|
||||
assConfig.LineCount = config.AssLineCount.Trim().ToInt();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssSpeed.Trim()))
|
||||
{
|
||||
assConfig.TuneDuration = config.AssSpeed.Trim().ToInt() - 8;
|
||||
}
|
||||
|
||||
var assPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".danmu.ass");
|
||||
Bilibili.GetInstance().Create(Encoding.UTF8.GetString(bytes), assConfig, assPath);
|
||||
}
|
||||
|
||||
this._logger.LogInformation("弹幕下载成功:name={0}", item.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Exception handled download danmu file");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string GetSearchSeasonName(string seriesName, int seasonIndexNumber)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user