This commit is contained in:
2026-04-03 11:33:51 +02:00
parent 64922e1ae3
commit 0ed904319d
57 changed files with 2935 additions and 1377 deletions
+1 -1
View File
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
+59 -63
View File
@@ -7,7 +7,7 @@
> [!NOTE]
> 已添加对mpv内部 `mp.input`的支持在uosc不可用时通过键绑定调用此方式渲染菜单
>
>
> 欲启用此支持mpv最低版本要求0.39.0
## 项目简介
@@ -24,25 +24,38 @@
<details open>
1. 从弹弹play或自定义服务的API获取剧集及弹幕数据并根据用户选择的集数加载弹幕
2. 通过点击uosc control bar中的弹幕搜索按钮可以显示搜索菜单供用户选择需要的弹幕
3. 通过点击加入uosc control bar中的弹幕开关控件可以控制弹幕的开关
4. 通过点击加入uosc control bar中的[从源获取弹幕](#从弹幕源向当前弹幕添加新弹幕内容可选)按钮可以通过受支持的网络源或本地文件添加弹幕
5. 通过点击加入uosc control bar中的[弹幕样式](#实时修改弹幕样式可选)按钮可以打开uosc弹幕样式菜单供用户在视频播放时实时修改弹幕样式注意⚠未安装uosc框架时该功能不可用
6. 通过点击加入uosc control bar中的[弹幕设置](#弹幕设置总菜单可选)按钮可以打开多级功能复合菜单,包含了插件目前所有的图形化功能。
7. 通过点击加入uosc control bar中的[弹幕源延迟设置](#弹幕源延迟设置可选)按钮可以打开弹幕源延迟控制菜单可以独立控制每个弹幕源的延迟注意⚠未安装uosc框架时该功能不可用
8. 记忆型全自动弹幕填装,在为某个文件夹下的某一集番剧加载过一次弹幕后,加载过的弹幕会自动关联到该集;之后每次重新播放该文件就会自动加载弹幕,同时该文件对应的文件夹下的所有其他集数的文件都会在播放时自动加载弹幕,无需再重复手动输入番剧名进行搜索(注意⚠️:全自动弹幕填装默认关闭,如需开启请阅读[auto_load配置项说明](#auto_load)
9. 在没有手动加载过弹幕,没有填装自动弹幕记忆之前,通过文件哈希匹配的方式自动添加弹幕(~仅限本地文件~,现已支持网络视频),对于能够哈希匹配关联的文件不再需要手动搜索关联,实现全自动加载弹幕并添加记忆。该功能随记忆型全自动弹幕填装功能一起开启(哈希匹配自动加载准确率较低,如关联到错误的剧集请手动加载正确的剧集)
2. 通过点击uosc control bar中的弹幕搜索按钮可以显示搜索菜单供用户选择需要的弹幕
3. 通过点击加入uosc control bar中的弹幕开关控件可以控制弹幕的开关
4. 通过点击加入uosc control bar中的[从源获取弹幕](#从弹幕源向当前弹幕添加新弹幕内容可选)按钮可以通过受支持的网络源或本地文件添加弹幕
5. 通过点击加入uosc control bar中的[弹幕样式](#实时修改弹幕样式可选)按钮可以打开uosc弹幕样式菜单供用户在视频播放时实时修改弹幕样式注意⚠未安装uosc框架时该功能不可用
6. 通过点击加入uosc control bar中的[弹幕设置](#弹幕设置总菜单可选)按钮可以打开多级功能复合菜单,包含了插件目前所有的图形化功能。
7. 通过点击加入uosc control bar中的[弹幕源延迟设置](#弹幕源延迟设置可选)按钮可以打开弹幕源延迟控制菜单可以独立控制每个弹幕源的延迟注意⚠未安装uosc框架时该功能不可用
8. 记忆型全自动弹幕填装,在为某个文件夹下的某一集番剧加载过一次弹幕后,加载过的弹幕会自动关联到该集;之后每次重新播放该文件就会自动加载弹幕,同时该文件对应的文件夹下的所有其他集数的文件都会在播放时自动加载弹幕,无需再重复手动输入番剧名进行搜索(注意⚠️:全自动弹幕填装默认关闭,如需开启请阅读[auto_load配置项说明](#auto_load)
9. 在没有手动加载过弹幕,没有填装自动弹幕记忆之前,通过文件哈希匹配的方式自动添加弹幕(~仅限本地文件~,现已支持网络视频),对于能够哈希匹配关联的文件不再需要手动搜索关联,实现全自动加载弹幕并添加记忆。该功能随记忆型全自动弹幕填装功能一起开启(哈希匹配自动加载准确率较低,如关联到错误的剧集请手动加载正确的剧集)
> 哈希匹配功能需要 mpv 基于 LuaJIT 或 Lua 5.2 构建,不支持 Lua 5.1
>
10. 通过打开配置项load_more_danmaku可以爬取所有可用弹幕源获取更多弹幕注意⚠爬取所有可用弹幕源默认关闭如需开启请阅读[load_more_danmaku配置项说明](#load_more_danmaku)
11. 自动记忆弹幕开关情况,播放视频时保持上次关闭时的弹幕开关状态
12. 自定义默认播放弹幕样式(具体设置方法详见[自定义弹幕样式](#自定义弹幕样式相关配置)
13. 在使用如[Play-With-MPV](https://github.com/LuckyPuppy514/Play-With-MPV)或[ff2mpv](https://github.com/woodruffw/ff2mpv)等网络播放手段时自动加载弹幕注意⚠目前支持自动加载bilibili和巴哈姆特这两个网站的弹幕具体说明查看[autoload_for_url配置项说明](#autoload_for_url)
14. 保存当前弹幕到本地(详细功能说明见[save_danmaku配置项说明](#save_danmaku)
15. 可以合并一定时间段内同时出现的大量重复弹幕(具体设置方法详见[merge_tolerance配置项说明](#merge_tolerance)
16. 弹幕简体字繁体字转换,解决弹幕简繁混杂问题(具体设置方法详见[chConvert配置项说明](#chConvert)
17. 自定义插件相关提示的显示位置,可以自由调节距离画面左上角的两个维度的距离(具体设置方法详见[message_x配置项说明](#message_x)和[message_y配置项说明](#message_y)
10. 自动记忆弹幕开关情况,播放视频时保持上次关闭时的弹幕开关状态
11. 自定义默认播放弹幕样式(具体设置方法详见[自定义弹幕样式](#自定义弹幕样式相关配置)
12. 在使用如[Play-With-MPV](https://github.com/LuckyPuppy514/Play-With-MPV)或[ff2mpv](https://github.com/woodruffw/ff2mpv)等网络播放手段时自动加载弹幕注意⚠目前支持自动加载bilibili和巴哈姆特这两个网站的弹幕具体说明查看[autoload_for_url配置项说明](#autoload_for_url)
13. 保存当前弹幕到本地(详细功能说明见[save_danmaku配置项说明](#save_danmaku)
14. 可以合并一定时间段内同时出现的大量重复弹幕(具体设置方法详见[merge_tolerance配置项说明](#merge_tolerance)
15. 弹幕简体字繁体字转换,解决弹幕简繁混杂问题(具体设置方法详见[chConvert配置项说明](#chConvert)
16. 自定义插件相关提示的显示位置,可以自由调节距离画面左上角的两个维度的距离(具体设置方法详见[message_x配置项说明](#message_x)和[message_y配置项说明](#message_y)
无需亲自下载整合弹幕文件资源无需亲自处理文件格式转换在mpv播放器中一键加载包含了哔哩哔哩、巴哈姆特等弹幕网站弹幕的弹弹play的动画弹幕。
@@ -69,7 +82,7 @@
想要使用本插件,请将本插件完整地[下载](https://github.com/Tony15246/uosc_danmaku/releases)或者克隆到 `scripts`目录下即可使用,文件结构参阅下方
> [!IMPORTANT]
>
>
> 1. scripts目录下放置本插件的文件夹名称必须为uosc_danmaku否则必须参照uosc控件配置部分[修改uosc控件](#修改uosc控件可选)
> 2. 记得给bin文件夹下的文件赋予可执行权限
@@ -236,12 +249,12 @@ key script-message open_source_delay_menu
> #### 实时修改弹幕样式(可选)
依赖于[uosc UI框架](https://github.com/tomasklaen/uosc)实现**弹幕样式实时修改**,将打开弹幕样式修改图形化菜单供用户手动修改,该功能目前仅依靠 uosc 实现uosc不可用时无法使用此功能默认使用[自定义弹幕样式](#自定义弹幕样式相关配置)里的样式配置)。想要启用此功能,需要参照[uosc控件配置](#uosc控件配置)根据uosc版本添加 `button:danmaku_styles``command:palette:script-message open_setup_danmaku_menu?弹幕样式``uosc.conf`的controls配置项中。
依赖于[uosc UI框架](https://github.com/tomasklaen/uosc)实现**弹幕样式实时修改**,将打开弹幕样式修改图形化菜单供用户手动修改默认使用[自定义弹幕样式](#自定义弹幕样式相关配置)里的样式配置)。想要启用此功能,需要参照[uosc控件配置](#uosc控件配置)根据uosc版本添加 `button:danmaku_styles``command:palette:script-message open_danmaku_style_menu?弹幕样式``uosc.conf`的controls配置项中。
想要通过快捷键使用此功能,请添加类似下面的配置到 `input.conf`中。实时修改弹幕样式功能对应的脚本消息为 `open_setup_danmaku_menu`
想要通过快捷键使用此功能,请添加类似下面的配置到 `input.conf`中。实时修改弹幕样式功能对应的脚本消息为 `open_danmaku_style_menu`
```
key script-message open_setup_danmaku_menu
key script-message open_danmaku_style_menu
```
</details>
@@ -338,34 +351,6 @@ key script-message check-update
<!-- 下列是弹幕加载相关 -->
<details>
<summary>
load_more_danmaku
> 开关全量弹幕源加载
</summary>
### load_more_danmaku
#### 功能说明
由于弹弹Play默认对于弹幕较多的番剧加载并且整合弹幕的上限大约每集7000条而这7000条弹幕也不是均匀分配例如有时弹幕基本只来自于哔哩哔哩有时弹幕又只来自于巴哈姆特。这样的话弹幕观看体验就和直接在哔哩哔哩或者巴哈姆特观看没有区别了失去了弹弹Play整合全平台弹幕的优势。
因此,本人添加了配置选项 `load_more_danmaku`用来将从弹弹Play获取弹幕的逻辑更改为逐一搜索所有弹幕源下的全部弹幕并由本脚本整合加载。开启此选项可以获取到所有可用弹幕源下的所有弹幕。但是对于一些热门番剧来说弹幕数量可能破万如果接受不了屏幕上弹幕太多请不要开启此选项。不过本人看视频从来只会觉得弹幕多多益善
#### 使用方法
想要开启此选项请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并添加如下内容:
```
load_more_danmaku=yes
```
</details>
---
<details>
<summary>
auto_load
@@ -490,11 +475,11 @@ save_danmaku
当文件关闭时自动保存弹幕文件xml格式至视频同目录保存的弹幕文件名与对应的视频文件名相同。配合[autoload_local_danmaku选项](#autoload_local_danmaku)可以实现弹幕自动保存到本地并且下次播放时自动加载本地保存的弹幕。此功能默认禁用。
> **⚠NOTE**
>
>
> 当开启[autoload_local_danmaku选项](#autoload_local_danmaku)时,会自动加载播放文件同目录下同名的 xml 格式的弹幕文件,优先级高于一切其他自动加载弹幕功能。如果不希望每次播放都加载之前保存的本地弹幕,则请关闭[autoload_local_danmaku选项](#autoload_local_danmaku);或者在保存完弹幕之后转移弹幕文件至其他路径并关闭 `save_danmaku`选项。
>
>
> `save_danmaku`选项的打开和关闭可以运行时实时更新。在 `input.conf`中添加如下内容,可通过快捷键实时控制 `save_danmaku`选项的打开和关闭
>
>
> ```
> key cycle-values script-opts uosc_danmaku-save_danmaku=yes uosc_danmaku-save_danmaku=no
> ```
@@ -523,7 +508,7 @@ save_danmaku=yes
### add_from_source
> **⚠NOTE**
>
>
> 该可选配置项在Release v1.2.0之后已废除。现在通过 `从弹幕源向当前弹幕添加新弹幕内容`功能关联过的弹幕源被记录,并且下次播放同一个视频的时候自动关联并加载所有添加过的弹幕源,这样的行为已经成为了插件的默认行为,不需要再通过 `add_from_source`来开启。在[从源获取弹幕](#从弹幕源向当前弹幕添加新弹幕内容可选)菜单中可以可视化地管理所有添加过的弹幕源。
#### 功能说明
@@ -627,6 +612,7 @@ merge_tolerance=1
</details>
---
<details>
<summary>
max_screen_danmaku
@@ -730,8 +716,8 @@ api_server
允许自定义弹幕 API 的服务地址
> **⚠NOTE**
>
> 请确保自定义服务的 API 与弹弹play 的兼容,已知兼容:[l429609201/misaka_danmu_server](https://github.com/l429609201/misaka_danmu_server)[laozishen/abetsy](https://hub.docker.com/r/laozishen/abetsy)
>
> 请确保自定义服务的 API 与弹弹play 的兼容,已知兼容:[misaka_danmu_server](https://github.com/l429609201/misaka_danmu_server)[danmu_api](https://github.com/huangxd-/danmu_api)
#### 使用方法
@@ -757,18 +743,18 @@ fallback_server
#### 功能说明
自定义 b 站和爱腾优的弹幕获取的兜底服务器地址主要用于获取非动画弹幕只有在弹弹play无法解析视频源对应弹幕的情况下才会使用此处设置的服务器进行解析。兜底弹幕服务器可以自托管具体方法请参考此仓库https://github.com/lyz05/danmaku
自定义 b 站和爱腾优的弹幕获取的兜底服务器地址主要用于获取非动画弹幕只有在弹弹play无法解析视频源对应弹幕的情况下才会使用此处设置的服务器进行解析。可用: https://api.danmu.icuhttps://dmku.hls.one
> **⚠NOTE**
>
> 不设置此选项的情况下默认使用 `https://fc.lyz05.cn`作为兜底服务器,除非你自行部署了弹幕服务器,否则不建议自定义此选项。
>
> 不设置此选项的情况下默认使用 ` https://api.danmu.icu`作为兜底服务器
#### 使用方法
想要使用此选项请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并自定义如下内容:
```
fallback_server=https://fc.lyz05.cn
fallback_server= https://api.danmu.icu
```
</details>
@@ -790,7 +776,7 @@ tmdb_api_key
设置 tmdb 的 API Key用于获取非动画条目的中文信息(当搜索内容非中文时)。可以在 https://www.themoviedb.org 注册后去个人账号设置界面获取个人的tmdb 的 API Key。
> **⚠NOTE**
>
>
> 不设置此选项的情况下默认使用专为本项目申请的API Key。另外自定义此选项时还需要对获取到的 API Key 进行 base64 编码。
#### 使用方法
@@ -826,9 +812,9 @@ user_agent
想要使用此选项请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并自定义如下内容(不可为空):
> **⚠NOTE**
>
>
> User-Agent格式必须符合弹弹play的标准否则无法成功请求。具体格式要求见[弹弹play官方文档](https://github.com/kaedei/dandanplay-libraryindex/blob/master/api/OpenPlatform.md#5user-agent)
>
>
> 若想提高URL播放的哈希匹配成功率可以将此项设为 `mpv`或浏览器的User-Agent
```
@@ -1020,6 +1006,15 @@ outline=1
blacklist_path=
```
## 插件自定义属性
- `user-data/uosc_danmaku/danmaku-delay`
`user-data/uosc_danmaku/danmaku-delay`属性中可以获取到当前弹幕延迟的值,具体用法可以参考[此issue](https://github.com/Tony15246/uosc_danmaku/issues/77)
- `user-data/uosc_danmaku/has-danmaku`
`user-data/uosc_danmaku/has-danmaku`属性中可以获取到表示当前是否有弹幕在显示的布尔值,具体用法可以参考[此pr](https://github.com/Tony15246/uosc_danmaku/pull/276)
## 常见问题
### 来自弹弹play的弹幕源问题如何从根源进行调整解决
@@ -1060,3 +1055,4 @@ blacklist_path=
## 相关项目
- [slqy123/uosc_danmaku](https://github.com/slqy123/uosc_danmaku) 本项目的fork版本实现了通过dandanplay api发送弹幕的功能由于版本的兼容性以及功能的易用性问题未被合并具体讨论请参阅 [#220](https://github.com/Tony15246/uosc_danmaku/pull/220)
- [Loukyuu1120/uosc_danmaku](https://github.com/Loukyuu1120/uosc_danmaku) 本项目的fork版本实现了自定义多个 api_servers 与 弹幕来源选择菜单 功能,具体讨论请参阅 [#282](https://github.com/Tony15246/uosc_danmaku/issues/282)
+120 -242
View File
@@ -24,62 +24,43 @@ end
function set_episode_id(input, from_menu)
from_menu = from_menu or false
DANMAKU.source = "dandanplay"
local api_server = options.api_server
for url, source in pairs(DANMAKU.sources) do
if source.from == "api_server" then
if source.fname and file_exists(source.fname) then
os.remove(source.fname)
end
if not source.from_history then
DANMAKU.sources[url] = nil
else
DANMAKU.sources[url]["fname"] = nil
DANMAKU.sources[url]["data"] = nil
api_server = source.api_server or options.api_server
end
end
end
local episodeId = tonumber(input)
write_history(episodeId)
local main_url = api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
add_source_to_history(main_url, { from = "api_server", api_server = api_server })
write_history(episodeId, api_server)
set_danmaku_button()
if options.load_more_danmaku then
fetch_danmaku_all(episodeId, from_menu)
else
fetch_danmaku(episodeId, from_menu)
end
fetch_danmaku(episodeId, from_menu, api_server)
end
-- 回退使用额外的弹幕获取方式
function get_danmaku_fallback(query)
local url = options.fallback_server .. "/?url=" .. query
local url = options.fallback_server .. "/?ac=dm&url=" .. query
msg.verbose("尝试获取弹幕:" .. url)
local temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".xml"
local danmaku_xml = utils.join_path(DANMAKU_PATH, temp_file)
DANMAKU.count = DANMAKU.count + 1
local arg = {
"curl",
"-L",
"-s",
"--compressed",
"--user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"--output",
danmaku_xml,
url,
}
call_cmd_async(arg, function(error)
async_running = false
if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5)
msg.error(error)
local args = make_danmaku_request_args("GET", url)
if not args then return end
fetch_danmaku_data(args, function(data)
if not data or not data["comments"] or data["count"] <= 1 then
msg.info("备用服务器无数据或返回格式不正确")
show_message("备用服务器无数据或返回格式不正确", 3)
return
end
if file_exists(danmaku_xml) then
if query:find("iqiyi%.com") ~= nil then
DANMAKU.strict = true
end
save_danmaku_downloaded(query, danmaku_xml)
load_danmaku(true)
end
save_danmaku_data(data["comments"], query, "user_custom")
load_danmaku(true)
end)
end
@@ -122,11 +103,55 @@ function make_danmaku_request_args(method, url, headers, body)
table.insert(args, string.format('X-Timestamp: %s', time))
end
if options.proxy ~= "" then
table.insert(args, '-x')
table.insert(args, options.proxy)
end
table.insert(args, url)
return args
end
local function normalize_danmaku_response(d)
if not d then return d end
-- 已经是 comments/count 格式则直接返回
if d.comments or d.count then return d end
if d.danmuku and type(d.danmuku) == "table" then
local out = {}
for _, item in ipairs(d.danmuku) do
-- item 预期为数组,索引: 1=time, 2=pos(right/top/bottom), 3=color(hex), 5=content
local time = tonumber(item[1]) or 0
local pos = item[2] or "right"
local color = item[3] or ""
local content = item[5] or item[4] or ""
local mode = 1
if pos == "right" then
mode = 1
elseif pos == "top" then
mode = 4
elseif pos == "bottom" then
mode = 5
end
local colorDec = 16777215
if type(color) == "number" then
colorDec = color
elseif type(color) == "string" then
colorDec = hex_to_int_color(color)
end
local p = string.format("%.2f,%d,%d", time, mode, colorDec)
table.insert(out, { p = p, m = content })
end
return { comments = out, count = tonumber(d.danum) or #out }
end
return d
end
-- 尝试通过解析文件名匹配剧集
local function match_episode(animeTitle, bangumiId, episode_num)
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId
@@ -214,11 +239,15 @@ local function match_anime()
target_title = title .. "" .. number_to_chinese(season_num) .. ""
end
for _, anime in ipairs(animes) do
if anime.animeTitle:match("第一[季部]") and tonumber(season_num) == 1 then
local animeTitle = tostring(anime.animeTitle or "")
animeTitle = animeTitle:gsub("^%s*(.-)%s*$", "%1")
:gsub("%s*%(.-%)%s*$", "")
:gsub("%s*【.-】.*$", "")
if animeTitle:match("第一[季部]") and tonumber(season_num) == 1 then
target_title = title .. " 第一季"
end
local score = jaro_winkler(target_title, anime.animeTitle)
msg.debug(("候选: %s -> 相似度 %.3f"):format(anime.animeTitle, score))
local score = jaro_winkler(target_title, animeTitle)
msg.debug(("候选: %s -> 相似度 %.3f"):format(animeTitle, score))
if score > best_score then
best_score = score
best_match = anime
@@ -276,7 +305,7 @@ local function match_file(file_path, file_name, callback)
["Content-Type"] = "application/json"
}, {
fileName = file_name,
fileHash = hash or "",
fileHash = hash or "a1b2c3d4e5f67890abcd1234ef567890",
matchMode = "hashAndFileName"
}
)
@@ -291,7 +320,7 @@ local function match_file(file_path, file_name, callback)
return
end
local data = utils.parse_json(json)
if not data or not data.isMatched or #data.matches > 1 then
if not data or not data.isMatched then
callback("没有匹配的剧集")
return
end
@@ -314,48 +343,39 @@ function fetch_danmaku_data(args, callback)
return
end
local data = utils.parse_json(json)
data = normalize_danmaku_response(data)
callback(data)
end)
end
-- 保存弹幕数据
function save_danmaku_data(comments, query, danmaku_source)
local temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".json"
local danmaku_file = utils.join_path(DANMAKU_PATH, temp_file)
DANMAKU.count = DANMAKU.count + 1
local success = save_danmaku_json(comments, danmaku_file)
local danmaku_list = save_danmaku_to_list(comments)
if success then
if DANMAKU.sources[query] ~= nil then
if DANMAKU.sources[query].fname and file_exists(DANMAKU.sources[query].fname) then
os.remove(DANMAKU.sources[query].fname)
end
DANMAKU.sources[query]["fname"] = danmaku_file
else
DANMAKU.sources[query] = {from = danmaku_source, fname = danmaku_file}
end
if DANMAKU.sources[query] ~= nil then
DANMAKU.sources[query]["data"] = danmaku_list
else
DANMAKU.sources[query] = {from = danmaku_source, data = danmaku_list}
end
end
function save_danmaku_downloaded(url, downloaded_file)
local danmaku_list = parse_danmaku_file(downloaded_file)
if file_exists(downloaded_file) then
os.remove(downloaded_file)
end
if DANMAKU.sources[url] ~= nil then
if DANMAKU.sources[url].fname and file_exists(DANMAKU.sources[url].fname) then
os.remove(DANMAKU.sources[url].fname)
end
DANMAKU.sources[url]["fname"] = downloaded_file
DANMAKU.sources[url]["data"] = danmaku_list
else
DANMAKU.sources[url] = {from = "user_custom", fname = downloaded_file}
DANMAKU.sources[url] = {from = "user_custom", data = danmaku_list}
end
end
-- 处理弹幕数据
function handle_danmaku_data(query, data, from_menu)
local comments = data["comments"]
local count = data["count"]
-- 如果没有数据,进行重试
if count == 0 then
show_message("服务器无缓存数据,再次尝试请求", 30)
if not data or not data["comments"] or data["count"] <= 1 then
show_message("服务器无缓存数据,再次尝试请求", 10)
msg.verbose("服务器无缓存数据,再次尝试请求")
-- 等待 2 秒后重试
local start = os.time()
@@ -371,7 +391,7 @@ function handle_danmaku_data(query, data, from_menu)
end
fetch_danmaku_data(args, function(retry_data)
if not retry_data or not retry_data["comments"] or retry_data["count"] == 0 then
if not retry_data or not retry_data["comments"] or retry_data["count"] <= 1 then
get_danmaku_fallback(query)
return
end
@@ -379,87 +399,11 @@ function handle_danmaku_data(query, data, from_menu)
load_danmaku(from_menu)
end)
else
save_danmaku_data(comments, query, "user_custom")
save_danmaku_data(data["comments"], query, "user_custom")
load_danmaku(from_menu)
end
end
-- 处理第三方弹幕数据
function handle_related_danmaku(index, relateds, related, shift, callback)
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(related["url"])
show_message(string.format("正在从第三方库装填弹幕 [%d/%d]", index, #relateds), 30)
msg.verbose("正在从第三方库装填弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
local comments = {}
if data and data["comments"] then
if data["count"] == 0 then
-- 如果没有数据,稍等 2 秒重试
local start = os.time()
while os.time() - start < 2 do
-- 空循环,等待 2 秒
end
fetch_danmaku_data(args, function(data)
for _, comment in ipairs(data["comments"]) do
comment["shift"] = shift
table.insert(comments, comment)
end
callback(comments)
end)
else
for _, comment in ipairs(data["comments"]) do
comment["shift"] = shift
table.insert(comments, comment)
end
callback(comments)
end
else
show_message("无数据", 3)
msg.info("无数据")
callback(comments)
end
end)
end
-- 处理dandan库的弹幕数据
function handle_main_danmaku(url, from_menu)
show_message("正在从弹弹Play库装填弹幕", 30)
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
if not data or not data["comments"] then
show_message("无数据", 3)
msg.info("无数据")
return
end
local comments = data["comments"]
local count = data["count"]
if count == 0 then
if DANMAKU.sources[url] == nil then
DANMAKU.sources[url] = {from = "api_server"}
end
load_danmaku(from_menu)
return
end
save_danmaku_data(comments, url, "api_server")
load_danmaku(from_menu)
end)
end
-- 处理获取到的数据
function handle_fetched_danmaku(data, url, from_menu)
if data and data["comments"] then
@@ -481,8 +425,8 @@ end
-- 匹配弹幕库 comment, 仅匹配dandan本身弹幕库
-- 通过danmaku apiurl+id获取弹幕
function fetch_danmaku(episodeId, from_menu)
local url = options.api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
function fetch_danmaku(episodeId, from_menu, api_server)
local url = (api_server or options.api_server) .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
show_message("弹幕加载中...", 30)
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
@@ -496,57 +440,6 @@ function fetch_danmaku(episodeId, from_menu)
end)
end
-- 主函数:获取所有相关弹幕
function fetch_danmaku_all(episodeId, from_menu)
local url = options.api_server .. "/api/v2/related/" .. episodeId
show_message("弹幕加载中...", 30)
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
if not data or not data["relateds"] then
show_message("无数据", 3)
msg.info("无数据")
return
end
-- 处理所有的相关弹幕
local relateds = data["relateds"]
local function process_related(index)
if index > #relateds then
-- 所有相关弹幕加载完成后,开始加载主库弹幕
url = options.api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=false&chConvert=0"
handle_main_danmaku(url, from_menu)
return
end
local related = relateds[index]
local shift = related["shift"]
-- 处理当前的相关弹幕
handle_related_danmaku(index, relateds, related, shift, function(comments)
if #comments == 0 then
if DANMAKU.sources[related["url"]] == nil then
DANMAKU.sources[related["url"]] = {from = "api_server"}
end
else
save_danmaku_data(comments, related["url"], "api_server")
end
-- 继续处理下一个相关弹幕
process_related(index + 1)
end)
end
-- 从第一个相关库开始请求
process_related(1)
end)
end
-- 从用户添加过的弹幕源添加弹幕
function addon_danmaku(dir, from_menu)
if dir then
@@ -587,19 +480,16 @@ function add_danmaku_source_local(query, from_menu)
msg.warn("无效的文件路径")
return
end
if not (string.match(path, "%.xml$") or string.match(path, "%.json$") or string.match(path, "%.ass$")) then
if not (string.match(path, "%.xml$") or string.match(path, "%.json$")) then
msg.warn("仅支持弹幕文件")
return
end
if DANMAKU.sources[query] ~= nil then
if DANMAKU.sources[query].fname and file_exists(DANMAKU.sources[query].fname) then
os.remove(DANMAKU.sources[query].fname)
end
DANMAKU.sources[query]["from"] = "user_local"
DANMAKU.sources[query]["fname"] = path
DANMAKU.sources[query]["data"] = parse_danmaku_file(path)
else
DANMAKU.sources[query] = {from = "user_local", fname = path}
DANMAKU.sources[query] = {from = "user_local", data = parse_danmaku_file(path)}
end
set_danmaku_button()
@@ -619,53 +509,41 @@ function add_danmaku_source_online(query, from_menu)
end
fetch_danmaku_data(args, function(data)
if not data or not data["comments"] then
show_message("此源弹幕无法加载", 3)
msg.verbose("此源弹幕无法加载")
return
end
handle_danmaku_data(query, data, from_menu)
end)
end
-- 将弹幕转换为factory可读的json格式
function save_danmaku_json(comments, json_filename)
local temp_file = "danmaku-" .. PID .. ".json"
json_filename = json_filename or utils.join_path(DANMAKU_PATH, temp_file)
local json_file = io.open(json_filename, "w")
-- 将弹幕转换为 Lua table
function save_danmaku_to_list(comments)
local danmaku_list = {}
if json_file then
json_file:write("[\n")
for _, comment in ipairs(comments) do
local p = comment["p"]
local shift = comment["shift"]
if p then
local fields = split(p, ",")
if shift ~= nil then
fields[1] = tonumber(fields[1]) + tonumber(shift)
end
local c_value = string.format(
"%s,%s,%s,25,,,",
tostring(fields[1]), -- first field of p to first field of c
fields[3], -- third field of p to second field of c
fields[2] -- second field of p to third field of c
)
local m_value = comment["m"]
:gsub("[%z\1-\31]", "")
:gsub("\\", "")
:gsub("\"", "")
-- Write the JSON object as a single line, no spaces or extra formatting
local json_entry = string.format('{"c":"%s","m":"%s"},\n', c_value, m_value)
json_file:write(json_entry)
for _, comment in ipairs(comments) do
local p = comment["p"]
local shift = comment["shift"]
if p then
local fields = split(p, ",")
if shift ~= nil then
fields[1] = tonumber(fields[1]) + tonumber(shift)
end
local time = tonumber(fields[1])
local type = tonumber(fields[2])
local color = tonumber(fields[3]) or 0xFFFFFF
local size = 25
local m_value = comment["m"]
:gsub("[%z\1-\31]", "")
:gsub("\\", "")
:gsub("\"", "")
table.insert(danmaku_list, {
time = time,
type = type,
size = size,
color = color,
text = m_value
})
end
json_file:write("]")
json_file:close()
return true
end
return false
return danmaku_list
end
-- 通过文件前 16M 的 hash 值进行弹幕匹配
@@ -732,4 +610,4 @@ function get_danmaku_with_hash(file_name, file_path)
end
end)
end
end
end
+145 -57
View File
@@ -12,9 +12,18 @@ local function load_extra_danmaku(url, episode, number, class, id, site, title,
local play_url = nil
if url:match("^.-%.html") then
play_url = url:match("^(.-%.html).*")
elseif url:match("^https?://v%.youku%.com/") and url:match("[?&]vid=") then
-- 转换 youku 的短链接形式 video?vid=... 到真实播放页 v_show/id_*.html
local vid = url:match("[?&]vid=([^&]+)")
if vid then
play_url = "https://v.youku.com/v_show/id_" .. vid .. ".html"
else
play_url = url:gsub("%?bsource=360ogvys$",""):gsub("&.*$","")
end
else
play_url = url:gsub("%?bsource=360ogvys$","")
play_url = url:gsub("%?bsource=360ogvys$",""):gsub("&.*$","")
end
ENABLED = true
DANMAKU.anime = title .. " (" .. year .. ")"
DANMAKU.episode = "" .. episode .. ""
@@ -34,7 +43,7 @@ end
local function query_tmdb(title, class, menu)
local encoded_title = url_encode(title)
local url = string.format("https://api.themoviedb.org/3/search/%s?api_key=%s&query=%s&language=zh-CN",
local url = string.format("https://api.tmdb.org/3/search/%s?api_key=%s&query=%s&language=zh-CN",
class, Base64.decode(options.tmdb_api_key), encoded_title)
local cmd = {
@@ -98,6 +107,63 @@ local function get_number(cat, id, site)
return nil
end
local function get_episodes_v2(cat, id, site)
local s_param = string.format('[{"cat_id":"%s","ent_id":"%s","site":"%s"}]', tostring(cat), tostring(id), tostring(site))
local url = string.format("https://api.so.360kan.com/episodesv2?v_ap=1&s=%s", url_encode(s_param))
local cmd = { "curl", "-s", url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
})
if not res.status or res.status ~= 0 then
msg.error("Failed to fetch episodesv2: " .. (res.stderr or "unknown error"))
return nil
end
local data_text = res.stdout or ""
-- 兼容 JSONP 和 纯 JSON提取最外层括号内 JSON
local json_payload = data_text
local first_paren = data_text:find('%(')
local last_paren = data_text:match('.*()%)')
if first_paren and last_paren and last_paren > first_paren then
json_payload = data_text:sub(first_paren + 1, last_paren - 1)
end
local parsed = utils.parse_json(json_payload)
if not parsed then
msg.error("episodesv2: 解析返回失败: " .. (res.stdout or ""))
return nil
end
local episodes = {}
if parsed.code == 0 and parsed.data and #parsed.data > 0 then
local seriesHTML = parsed.data[1] and parsed.data[1].seriesHTML
if seriesHTML and seriesHTML.seriesPlaylinks then
for i, ep in ipairs(seriesHTML.seriesPlaylinks) do
local episode_url = nil
if type(ep) == 'string' then
episode_url = ep
elseif type(ep) == 'table' and ep.url then
episode_url = ep.url
end
if episode_url and episode_url ~= '' then
table.insert(episodes, { index = i, url = episode_url })
end
end
end
end
if #episodes == 0 then
return nil
end
return episodes
end
function get_details(class, id, site, title, year, number, episodenum)
local message = episodenum and "查询弹幕中..." or "加载数据中..."
local menu_type = "menu_details"
@@ -120,69 +186,91 @@ function get_details(class, id, site, title, year, number, episodenum)
cat = 4
end
if not number and cat ~= 0 then
number = get_number(cat, id, site)
end
if not number or cat == 0 then
local message = "无结果"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.verbose("无结果")
return
end
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&start=1&end=%s&site=%s",
cat, id, number, site)
local cmd = { "curl", "-s", url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
})
if not res.status or res.status ~= 0 then
local message = "无结果"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.verbose("无结果")
return
end
local result = utils.parse_json(res.stdout)
local items = {}
if result and result.data and result.data.allepidetail then
local data = result.data.allepidetail
local playurl, episode = nil, nil
local episodes = nil
if cat == 2 or cat == 4 then
episodes = get_episodes_v2(cat, id, site)
end
-- 统一构建 episode_rows优先使用 episodesv2 返回的数据,否则使用 v1/detail
local episode_rows = nil
if episodes then
episode_rows = {}
for _, ep in ipairs(episodes) do
table.insert(episode_rows, { index = tostring(ep.index), url = ep.url })
end
else
if not number and cat ~= 0 then
number = get_number(cat, id, site)
end
if not number or cat == 0 then
local message = "无结果"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.verbose("无结果")
return
end
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&start=1&end=%s&site=%s",
cat, id, number, site)
local cmd = { "curl", "-s", url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
})
if not res.status or res.status ~= 0 then
local message = "无结果"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.verbose("无结果")
return
end
local result = utils.parse_json(res.stdout)
if result and result.data and result.data.allepidetail and result.data.allepidetail[site] then
episode_rows = {}
for _, it in ipairs(result.data.allepidetail[site]) do
table.insert(episode_rows, { index = tostring(it.playlink_num), url = it.url })
end
end
end
if episode_rows and #episode_rows > 0 then
if episodenum then
for _, item in ipairs(data[site]) do
if tonumber(item.playlink_num) == tonumber(episodenum) then
playurl = item.url
episode = item.playlink_num
break
for _, ep in ipairs(episode_rows) do
if tonumber(ep.index) == tonumber(episodenum) then
load_extra_danmaku(ep.url, ep.index, number, class, id, site, title, year)
return
end
end
if playurl then
load_extra_danmaku(playurl, episode, number, class, id, site, title, year)
return
end
end
for _, item in ipairs(data[site]) do
table.insert(items, {
title = "← 返回搜索结果",
value = { "script-message-to", "uosc", "open-menu", latest_menu_anime },
keep_open = false,
selectable = true,
})
for _, ep in ipairs(episode_rows) do
table.insert(items, {
title = "" .. item.playlink_num .. "",
hint = item.playlink_num,
title = "" .. ep.index .. "",
hint = ep.index,
value = {
"script-message-to",
mp.get_script_name(),
"add-extra-event",
item.url, item.playlink_num, number, class, id, site, title, year
ep.url, ep.index, tostring(number), class, id, site, title, year
},
})
end
@@ -257,7 +345,7 @@ local function search_query(query, class, menu)
end
if #items > 0 then
if uosc_available then
update_menu_uosc(menu.type, menu.title, items, menu.footnote, menu.cmd, query)
latest_menu_anime = update_menu_uosc(menu.type, menu.title, items, menu.footnote, menu.cmd, query)
else
show_message("", 0)
mp.add_timeout(0.1, function()
@@ -344,4 +432,4 @@ mp.register_script_message("add-extra-event", function(url, episode, number, cla
mp.commandv("script-message-to", "uosc", "close-menu", "menu_details")
end
load_extra_danmaku(url, episode, number, class, id, site, title, year)
end)
end)
+1 -1
View File
@@ -3979,4 +3979,4 @@ return {
[""] = "",
["𫐖"] = "",
[""] = "",
}
}
+1 -1
View File
@@ -4112,4 +4112,4 @@ return {
["𪷓"] = "𣶭",
["𫒡"] = "𫓷",
["𫜦"] = "𫜫",
}
}
+114 -103
View File
@@ -25,11 +25,11 @@ DANMAKU_PATH = os.getenv("TEMP") or "/tmp/"
HISTORY_PATH = mp.command_native({"expand-path", options.history_path})
PID = utils.getpid()
DANMAKU = {sources = {}, count = 1}
DELAYS = {}
ENABLED, COMMENTS, DELAY = false, nil, 0
DELAY_PROPERTY = string.format("user-data/%s/danmaku-delay", mp.get_script_name())
mp.set_property_native(DELAY_PROPERTY, 0)
HAS_DANMAKU = string.format("user-data/%s/has-danmaku", mp.get_script_name())
mp.set_property_bool(HAS_DANMAKU, false)
KEY = table_to_zero_indexed({
0x00,0x01,0x02,0x03,0x04,
0x05,0x06,0x07,0x08,0x09,
@@ -58,6 +58,8 @@ PLATFORM = (function()
return "linux"
end)()
local rebuild_convert_timer = nil
function get_danmaku_visibility()
local history_json = read_file(HISTORY_PATH)
local history
@@ -140,21 +142,6 @@ local function extract_between_colons(input_string)
end
end
local function hex_to_int_color(hex_color)
-- 移除颜色代码中的'#'字符
hex_color = hex_color:sub(2) -- 只保留颜色代码部分
-- 提取R, G, B的十六进制值并转为整数
local r = tonumber(hex_color:sub(1, 2), 16)
local g = tonumber(hex_color:sub(3, 4), 16)
local b = tonumber(hex_color:sub(5, 6), 16)
-- 计算32位整数值
local color_int = (r * 256 * 256) + (g * 256) + b
return color_int
end
local function get_type_from_position(position)
if position == 0 then
return 1
@@ -170,11 +157,13 @@ end
function get_delay_for_time(delay_segments, time)
if not delay_segments or #delay_segments == 0 then return 0 end
table.sort(delay_segments, function(a, b) return a.start < b.start end)
local segs = {}
for i = 1, #delay_segments do segs[i] = delay_segments[i] end
table.sort(segs, function(a, b) return a.start < b.start end)
local applied_delay = 0
for i = 1, #delay_segments do
local seg = delay_segments[i]
for i = 1, #segs do
local seg = segs[i]
local delay = tonumber(seg.delay)
if time >= seg.start and delay then
applied_delay = applied_delay + delay
@@ -244,9 +233,29 @@ local function merge_delay_segments(segments)
return merged
end
local function set_danmaku_delay(dly, time)
for url, source in pairs(DANMAKU.sources) do
if source.fname and not source.blocked then
function parse_delay_input(text)
if not text then return nil end
local s = tostring(text):gsub("%s+", "")
if s == "" then return nil end
-- XmYs 格式,允许负号在分钟部分
local m, sec = string.match(s, "^(%-?%d+)m(%d+)s$")
if m and sec then
m = tonumber(m)
sec = tonumber(sec)
if not m or not sec then return nil end
if m < 0 then sec = -sec end
return m * 60 + sec
end
-- 普通数字(整数或小数),支持负数
local n = tonumber(s)
if n ~= nil then return n end
return nil
end
local function set_danmaku_delay(dly, time, specific_source)
if specific_source then
local source = DANMAKU.sources[specific_source]
if source and source.data and not source.blocked then
source.delay_segments = source.delay_segments or {}
if dly == 0 then
source.delay_segments = {}
@@ -255,32 +264,52 @@ local function set_danmaku_delay(dly, time)
else
table.insert(source.delay_segments, {start = 0, delay = dly})
end
source.delay = nil
table.sort(source.delay_segments, function(a, b) return a.start < b.start end)
add_source_to_history(url, source)
source.delay_segments = merge_delay_segments(source.delay_segments)
add_source_to_history(specific_source, source)
end
end
if time then
table.insert(DELAYS, {start = time, delay = dly})
else
table.insert(DELAYS, {start = 0, delay = dly})
for url, source in pairs(DANMAKU.sources) do
if source.data and not source.blocked then
source.delay_segments = source.delay_segments or {}
if dly == 0 then
source.delay_segments = {}
elseif time then
table.insert(source.delay_segments, {start = time, delay = dly})
else
table.insert(source.delay_segments, {start = 0, delay = dly})
end
source.delay = nil
source.delay_segments = merge_delay_segments(source.delay_segments)
add_source_to_history(url, source)
end
end
end
if dly == 0 then
DELAY = 0
DELAYS = {}
else
DELAY = DELAY + dly
end
DELAYS = merge_delay_segments(DELAYS)
if ENABLED and COMMENTS ~= nil then
render()
end
-- 防抖:批量重建 ASS 事件并渲染,避免频繁变更导致重复重建
if rebuild_convert_timer then
rebuild_convert_timer:kill()
rebuild_convert_timer = nil
end
rebuild_convert_timer = mp.add_timeout(0.1, function()
if convert_danmaku_to_ass_events then
convert_danmaku_to_ass_events(true)
end
render()
rebuild_convert_timer = nil
end)
show_message('设置弹幕延迟: ' .. string.format("%.1f", DELAY + 1e-10) .. ' s')
mp.set_property_native(DELAY_PROPERTY, DELAY)
end
@@ -299,9 +328,6 @@ local function clear_source()
for url, source in pairs(DANMAKU.sources) do
if source.from == "user_custom" then
if source.fname and file_exists(source.fname) then
os.remove(source.fname)
end
DANMAKU.sources[url] = nil
end
end
@@ -312,7 +338,7 @@ local function clear_source()
msg.verbose("已重置当前视频所有弹幕源更改")
end
function write_history(episodeid)
function write_history(episodeid, api_server)
local history = {}
local path = mp.get_property("path")
local dir = get_parent_directory(path)
@@ -353,6 +379,9 @@ function write_history(episodeid)
elseif DANMAKU.extra then
history[dir].extra = DANMAKU.extra
end
if api_server then
history[dir].api_server = api_server
end
write_json_file(HISTORY_PATH, history)
end
end
@@ -401,6 +430,11 @@ function add_source_to_history(add_url, add_source)
local record = history[path]["sources"][add_url]
record.from = add_source.from or "user_custom"
record.blocked = add_source.blocked or false
if record.from == "api_server" then
record.api_server = add_source.api_server or options.api_server
else
record.api_server = nil
end
local delay_segments = shallow_copy(add_source.delay_segments or {})
if #delay_segments > 0 then
@@ -455,6 +489,7 @@ function read_danmaku_source_record(path)
blocked = blocked,
delay_segments = delay_segments,
from_history = true,
api_server = data.api_server,
}
end
else
@@ -484,6 +519,7 @@ function read_danmaku_source_record(path)
blocked = blocked,
delay_segments = delay_segments,
from_history = true,
api_server = record.api_server,
}
upgraded_sources[source] = shallow_copy(DANMAKU.sources[source])
@@ -496,39 +532,8 @@ function read_danmaku_source_record(path)
end
end
-- 收集现有的弹幕文件和延迟记录
local function collect_danmaku_sources()
local danmaku_input = {}
local delays = {}
for _, source in pairs(DANMAKU.sources) do
if not source.blocked and source.fname then
if not file_exists(source.fname) then
show_message("未找到弹幕文件", 3)
msg.info("未找到弹幕文件")
return
end
table.insert(danmaku_input, source.fname)
if source.delay_segments and #source.delay_segments > 0 then
table.insert(delays, source.delay_segments)
end
end
end
return danmaku_input, delays
end
-- 视频播放时保存弹幕
function save_danmaku(not_forced)
local danmaku_input, delays = collect_danmaku_sources()
if #danmaku_input == 0 then
show_message("弹幕内容为空,无法保存", 3)
msg.verbose("弹幕内容为空,无法保存")
COMMENTS = {}
return
end
local path = mp.get_property("path")
local dir = get_parent_directory(path) or ""
local filename = mp.get_property('filename/no-ext')
@@ -544,7 +549,7 @@ function save_danmaku(not_forced)
msg.info("已存在同名弹幕文件:" .. danmaku_out)
return
else
convert_danmaku_to_xml(danmaku_input, danmaku_out, delays)
convert_danmaku_to_xml(danmaku_out)
end
end
end
@@ -552,19 +557,8 @@ end
-- 加载弹幕
function load_danmaku(from_menu, no_osd)
if not ENABLED then return end
local temp_file = "danmaku-" .. PID .. ".ass"
local danmaku_file = utils.join_path(DANMAKU_PATH, temp_file)
local danmaku_input, delays = collect_danmaku_sources()
-- 如果没有弹幕文件,退出加载
if #danmaku_input == 0 then
show_message("该集弹幕内容为空,结束加载", 3)
msg.verbose("该集弹幕内容为空,结束加载")
COMMENTS = {}
return
end
convert_danmaku_format(danmaku_input, danmaku_file, delays)
parse_danmaku(danmaku_file, from_menu, no_osd)
convert_danmaku_to_ass_events()
render_danmaku(from_menu, no_osd)
end
-- 为 bilibli 网站的视频播放加载弹幕
@@ -620,6 +614,11 @@ function load_danmaku_for_bilibili(path)
url,
}
if options.cookie_file and options.cookie_file ~= "" then
table.insert(arg, '-b')
table.insert(arg, mp.command_native({"expand-path", options.cookie_file}))
end
call_cmd_async(arg, function(error)
async_running = false
if error then
@@ -673,6 +672,11 @@ function load_danmaku_for_bahamut(path)
table.insert(arg, options.proxy)
end
if options.cookie_file and options.cookie_file ~= "" then
table.insert(arg, '-b')
table.insert(arg, mp.command_native({"expand-path", options.cookie_file}))
end
call_cmd_async(arg, function(error)
async_running = false
if error then
@@ -688,30 +692,33 @@ function load_danmaku_for_bahamut(path)
end
local comments_json = read_file(danmaku_json)
os.remove(danmaku_json)
local comments = utils.parse_json(comments_json)
if not comments then
return
end
local output_table = {}
for _, comment in ipairs(comments) do
local color = hex_to_int_color(comment["color"])
local mode = get_type_from_position(comment["position"])
local time = tonumber(comment["time"]) / 10
local c_param = string.format("%s,%s,%s,25,,,", time, color, mode)
table.insert(output_table, {
c = c_param,
m = comment["text"]
})
end
local final_json_str = utils.format_json(output_table)
temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".json"
local json_filename = utils.join_path(DANMAKU_PATH, temp_file)
DANMAKU.count = DANMAKU.count + 1
local json_file = io.open(json_filename, "w")
if json_file then
json_file:write("[\n")
for _, comment in ipairs(comments) do
local m = comment["text"]
local color = hex_to_int_color(comment["color"])
local mode = get_type_from_position(comment["position"])
local time = tonumber(comment["time"]) / 10
local c = time .. "," .. color .. "," .. mode .. ",25,,,"
-- Write the JSON object as a single line, no spaces or extra formatting
local json_entry = string.format('{"c":"%s","m":"%s"},\n', c, m)
json_file:write(json_entry)
end
json_file:write("]")
json_file:write(final_json_str)
json_file:close()
end
@@ -890,22 +897,27 @@ end)
mp.register_script_message("danmaku-delay", function(...)
local commands = {...}
local delay_str, time_str = commands[1], commands[2]
local dly = tonumber(delay_str)
local source_arg = commands[3]
local dly = parse_delay_input(delay_str)
local time = time_str and tonumber(time_str)
if type(dly) ~= "number" then
show_message("参数错误:缺少有效的延迟秒数", 3)
return
end
set_danmaku_delay(dly, time)
if source_arg and source_arg ~= "nil" then
set_danmaku_delay(dly, time, source_arg)
else
set_danmaku_delay(dly, time)
end
end)
mp.register_script_message("show_danmaku_keyboard", function()
ENABLED = not ENABLED
if ENABLED then
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
set_danmaku_visibility(true)
if COMMENTS == nil then
show_message("加载弹幕初始化...", 3)
set_danmaku_visibility(true)
local path = mp.get_property("path")
init(path)
else
@@ -915,7 +927,6 @@ mp.register_script_message("show_danmaku_keyboard", function()
else
show_message("关闭弹幕", 2)
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "off")
set_danmaku_visibility(false)
hide_danmaku_func()
end
end)
@@ -923,7 +934,7 @@ end)
mp.register_script_message("check-update", check_for_update)
mp.register_script_message("clear-source", clear_source)
mp.register_script_message("immediately_save_danmaku", save_danmaku)
mp.register_script_message("open_source_delay_menu", danmaku_delay_setup)
mp.register_script_message("open_source_delay_menu", open_delay_menu)
mp.register_script_message("open_search_danmaku_menu", open_input_menu)
mp.register_script_message("open_add_source_menu", open_add_menu)
mp.register_script_message("open_add_total_menu", open_add_total_menu)
mp.register_script_message("open_add_total_menu", open_add_total_menu)
+13 -11
View File
@@ -1,3 +1,5 @@
local unpack = unpack or table.unpack
-- Clean up media name
local function clean_name(name)
return name:gsub("^%[.-%]", " ")
@@ -19,12 +21,6 @@ local formatters = {
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第%s*(%d+)%s*[季部]+%s*[_%-%.%s]%s*[eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, season, episode)
@@ -32,7 +28,13 @@ local formatters = {
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*[eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)",
regex = "^(.-)%s*[_%-%.%s]%s*第%s*(%d+)%s*[季部]+%s*[_%-%.%s]%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. chinese_to_number(season) .. "E" .. episode:gsub("v%d+$","")
end
@@ -54,7 +56,7 @@ local formatters = {
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[eEpP]+[_%-%.%s]?(%d+%.?%d*)",
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[^%ddD][eEpP]+(%d+%.?%d*)",
format = function(name, year, episode)
return clean_name(name) .. " (" .. year .. ") E" .. episode
end
@@ -78,13 +80,13 @@ local formatters = {
end
},
{
regex = "^(.-)%s*[^dD][eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
regex = "^(.-)%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, episode, year)
return clean_name(name) .. " (" .. year .. ") E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[^dD][eEpP]+[_%-%.%s]?(%d+%.?%d*)",
regex = "^(.-)%s*[^%ddD][eEpP]+(%d+%.?%d*)",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
@@ -154,4 +156,4 @@ function format_filename(title)
return title
end
end
end
end
+1 -1
View File
@@ -189,4 +189,4 @@ local function sha256(message)
return result
end
return sha256
return sha256
+611 -80
View File
@@ -1,8 +1,10 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
local unpack = unpack or table.unpack
input_loaded, input = pcall(require, "mp.input")
uosc_available = false
latest_menu_anime = {}
-- 打开番剧数据匹配菜单
function get_animes(query)
@@ -69,10 +71,11 @@ function get_animes(query)
end
if uosc_available then
update_menu_uosc(menu_type, menu_title, items, footnote, menu_cmd, query)
latest_menu_anime = update_menu_uosc(menu_type, menu_title, items, footnote, menu_cmd, query)
elseif input_loaded then
show_message("", 0)
mp.add_timeout(0.1, function()
latest_menu_anime = utils.format_json(items)
open_menu_select(items)
end)
end
@@ -124,6 +127,13 @@ function get_episodes(animeTitle, bangumiId)
return
end
table.insert(items, {
title = "← 返回搜索结果",
value = { "script-message-to", mp.get_script_name(), "open-latest-menu-anime", latest_menu_anime },
keep_open = false,
selectable = true,
})
for _, episode in ipairs(response.bangumi.episodes) do
table.insert(items, {
title = episode.episodeTitle,
@@ -136,6 +146,7 @@ function get_episodes(animeTitle, bangumiId)
end
if uosc_available then
footnote = mp.get_property("filename")
update_menu_uosc(menu_type, menu_title, items, footnote)
elseif input_loaded then
mp.add_timeout(0.1, function()
@@ -154,6 +165,7 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
keep_open = true,
selectable = false,
align = "center",
icon = "spinner",
})
else
items = menu_item
@@ -171,6 +183,8 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
return json_props
end
function open_menu_select(menu_items, is_time)
@@ -182,10 +196,16 @@ function open_menu_select(menu_items, is_time)
end
mp.commandv('script-message-to', 'console', 'disable')
input.select({
prompt = '筛选:',
prompt = is_time and '筛选:' or '选择:',
items = item_titles,
submit = function(id)
mp.commandv(unpack(item_values[id]))
input.terminate()
local v = item_values[id]
if type(v) == 'table' then
mp.commandv(unpack(v))
elseif type(v) == 'string' then
mp.command(v)
end
end,
})
end
@@ -250,12 +270,114 @@ end
-- 打开弹幕源添加管理菜单
function open_add_menu_get()
mp.commandv('script-message-to', 'console', 'disable')
local menu_log, deal_value = {}, {}
-- 重建菜单内容函数
local function rebuild_menu_log(select_num)
deal_value = {}
menu_log = {
{ text = "【既有弹幕源】", style = "{\\c&H00CCFF&\\b1}" },
{ text = "----------------------------", style = "{\\c&H888888&}" }
}
local serial = 0
for url, source in pairs(DANMAKU.sources) do
if source.data then
serial = serial + 1
local action, text
if source.from == "api_server" then
action = source.blocked and "unblock" or "block"
text = string.format(" [%02d] %s [来源:弹幕服务器%s] ", serial, url,
source.blocked and "(已屏蔽)" or "(未屏蔽)")
else
action = "delete"
text = string.format(" [%02d] %s [来源:用户添加] ", serial, url)
end
local style = (tonumber(select_num) == serial) and
"{\\c&HFFDE7F&\\b1}" or (action == "unblock" and "{\\c&H4C4CC3&\\b0}" or "{\\c&HCCCCCC&\\b0}")
deal_value[serial] = {value = url, action = action}
table.insert(menu_log, {text = text, style = style})
end
end
if serial == 0 then
table.insert(menu_log, { text = "", style = "" })
end
end
-- 显示菜单
local function show_menu(extra_lines, select_num)
rebuild_menu_log(select_num)
local display = {}
for _, item in ipairs(menu_log) do table.insert(display, item) end
table.insert(display, { text = "----------------------------", style = "{\\c&H888888&}" })
if extra_lines then
if #extra_lines < 2 then table.insert(display, { text = "\n", style = "" }) end
for _, line in ipairs(extra_lines) do table.insert(display, line) end
else
table.insert(display, { text = "\n", style = "" })
table.insert(display, {
text = "提示: 输入【选项数字】可屏蔽或删除既有弹幕源",
style = "{\\c&H999999&}"
})
end
input.set_log(display)
end
-- 获取操作提示
local function get_hint(action)
local hints = {
block = "按回车执行,屏蔽该弹幕源",
unblock = "按回车执行,解除该弹幕源的屏蔽",
delete = "按回车执行,删除该弹幕源"
}
return hints[action] or "按回车执行获取输入源地址url的弹幕"
end
input.get({
prompt = 'Input url:',
keep_open = true,
prompt = "请在此输入源地址url: ",
opened = function() show_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
show_menu()
return
end
local num = tonumber(text)
local event = num and deal_value[num]
local hint = get_hint(event and event.action)
show_menu({
{ text = string.format("已输入: %s", text), style = "{\\c&HCCCCCC&}" },
{ text = hint, style = "{\\c&H999999&}" }
}, text)
end,
submit = function(text)
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "add-source-event", text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then return end
local num = tonumber(text)
local event = num and deal_value[num]
if event then
local args = string.format('{"type":"activate","value":"%s","action":"%s"}',
string.gsub(event.value, '\\', '\\\\'), event.action)
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-source", args)
else
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "add-source-event", text)
end
mp.add_timeout(0.1, show_menu)
end
})
end
@@ -263,19 +385,19 @@ end
function open_add_menu_uosc()
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname then
if source.data then
local item = {title = url, value = url, keep_open = true,}
if source.from == "api_server" then
if source.blocked then
item.hint = "来源:弹幕服务器(已屏蔽)"
item.actions = {{icon = "check", name = "unblock"},}
item.actions = {{icon = "check", name = "unblock", label = "解除屏蔽"},}
else
item.hint = "来源:弹幕服务器(未屏蔽)"
item.actions = {{icon = "not_interested", name = "block"},}
item.actions = {{icon = "not_interested", name = "block", label = "屏蔽"},}
end
else
item.hint = "来源:用户添加"
item.actions = {{icon = "delete", name = "delete"},}
item.actions = {{icon = "delete", name = "delete", label = "删除"},}
end
table.insert(sources, item)
end
@@ -312,13 +434,36 @@ function open_content_menu(pos)
if COMMENTS ~= nil then
for _, event in ipairs(COMMENTS) do
local text = event.clean_text:gsub("^m%s[mbl%s%-%d%.]+$", ""):gsub("^%s*(.-)%s*$", "%1")
local delay = get_delay_for_time(DELAYS, event.start_time)
local start_time = event.start_time + delay
local end_time = event.end_time + delay
local delay = event.delay
local start_time = event.start_time
local end_time = event.end_time
if text and text ~= "" and start_time >= 0 and start_time <= duration then
local delay_label_suffix = nil
local delay_num = delay and tonumber(delay)
if delay_num and math.abs(delay_num) > 0 then
delay_label_suffix = string.format("已存在延迟: %+0.1fs", delay_num)
end
local adjust_label = '调整弹幕延迟'
if delay_label_suffix then
adjust_label = adjust_label .. '' .. delay_label_suffix .. ''
end
table.insert(items, {
title = abbr_str(text, 60),
hint = seconds_to_time(start_time),
hint = seconds_to_time(start_time) .. " (" .. utf8_sub(remove_query(event.source), 1, 70) .. ")",
actions = {
{
name = 'block_source',
icon = 'block',
label = '屏蔽对应弹幕源'
},
{
name = 'adjust_delay',
icon = 'more_time',
label = adjust_label,
},
},
value = { "seek", start_time, "absolute" },
active = time_pos >= start_time and time_pos <= end_time,
})
@@ -330,7 +475,9 @@ function open_content_menu(pos)
type = "menu_content",
title = "弹幕内容",
footnote = "使用 / 打开搜索",
items = items
items = items,
item_actions_place = "outside",
callback = {mp.get_script_name(), 'handle-danmaku-content-action'},
}
local json_props = utils.format_json(menu_props)
@@ -361,12 +508,134 @@ local menu_items_config = {
local ordered_keys = {"bold", "fontsize", "outline", "shadow", "scrolltime", "opacity", "displayarea"}
-- 设置弹幕样式菜单
function add_danmaku_setup(actived, status)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
function open_style_menu_get(query, indicator)
mp.commandv('script-message-to', 'console', 'disable')
local menu_log = {}
local select_num = 0
local select_query = nil
if query then
if tonumber(query) ~= nil then
select_num = tonumber(query)
else
for i, v in ipairs(ordered_keys) do
if v == query then
select_num = i
end
end
end
select_query = ordered_keys[select_num]
end
local function build_menu(source)
menu_log = {
{ text = "【弹幕样式菜单】", style = "{\\c&H00CCFF&\\b1}" },
{ text = ("-"):rep(33), style = "{\\c&H888888&}" }
}
local serial = 0
for _, key in ipairs(ordered_keys) do
serial = serial + 1
local config = menu_items_config[key]
local text = string.format(" [%02d] %s [目前:%s] ", serial, config.title, config.hint)
text = config.hint ~= config.original and text .. "" or text
local style = serial == select_num and "{\\c&HFFDE7F&}" or "{\\c&HCCCCCC&}"
local item_config = { text = text, style = style }
table.insert(menu_log, item_config)
end
table.insert(menu_log, { text = ("-"):rep(33), style = "{\\c&H888888&}" })
if select_num == 0 then
table.insert(menu_log, {
text = "注: 样式更改仅在本次播放生效",
style = "{\\c&HFFDE7F&}"
})
table.insert(menu_log, {
text = "提示: 输入【w】可上移选项【s】可下移选项",
style = "{\\c&H999999&}"
})
else
local input_text = source and source or ""
local config = menu_items_config[select_query]
local suffix = ""
if config and config.hint ~= config.original then
suffix = "(输入\\r恢复默认配置"
end
input_text = string.format("已输入%s: %s", suffix, input_text)
local scope = config and config.footnote or ""
local hint_text = select_query == "bold" and "提示: 输入【y】切换状态" or "提示: " .. scope
local hint_style = "{\\c&H999999&}"
if source and source:lower() == "\\r" then
hint_text = string.format("提示: 回车将恢复默认配置 < %s >", config.original)
end
if indicator == "refresh" or indicator == "updata" then
indicator = ""
hint_text = "提示: 样式更改成功"
hint_style = "{\\c&HFFDE7F&}"
mp.add_timeout(1.5, build_menu)
elseif indicator == "error" then
indicator = ""
hint_text = "提示: 输入非数字字符或范围出错"
hint_style = "{\\c&H4C4CC3&}"
mp.add_timeout(1.5, build_menu)
end
table.insert(menu_log, { text = input_text, style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = hint_text, style = hint_style })
end
input.set_log(menu_log)
end
input.get({
keep_open = true,
prompt = "请在此输入操作w/s|上移/下移): ",
opened = function() build_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
build_menu()
return
end
if text:lower() == "w" or text:lower() == "s" then
input.terminate()
select_num = text:lower() == "w" and select_num - 1 or select_num + 1
select_num = (select_num > #ordered_keys) and 1 or (select_num <= 0 and #ordered_keys or select_num)
mp.add_timeout(0.01, function()
open_style_menu_get(select_num)
end)
return
end
build_menu(text)
end,
submit = function(text)
if select_query == nil then return end
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then return end
if text:lower() == "\\r" then
input.terminate()
local args = string.format('{"type":"activate","action":"%s","index":%d}', select_query, select_num)
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-style", args)
else
if menu_items_config[select_query]["scope"] ~= nil then
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-style", select_query, text)
elseif text:lower() == "y" and select_query == "bold" then
input.terminate()
local args = string.format('{"type":"activate","index":%d}', select_num)
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-style", args)
end
end
return
end
})
end
function open_style_menu_uosc(actived, status)
local items = {}
for _, key in ipairs(ordered_keys) do
local config = menu_items_config[key]
@@ -407,7 +676,7 @@ function add_danmaku_setup(actived, status)
elseif status == "error" then
menu_props.title = "输入非数字字符或范围出错"
-- 创建一个定时器在1秒后触发回调函数删除搜索栏错误信息
mp.add_timeout(1.0, function() add_danmaku_setup(actived, "updata") end)
mp.add_timeout(1.0, function() open_style_menu_uosc(actived, "updata") end)
end
menu_props.search_style = "palette"
menu_props.search_debounce = "submit"
@@ -419,8 +688,216 @@ function add_danmaku_setup(actived, status)
mp.commandv("script-message-to", "uosc", actions, json_props)
end
function open_style_menu(actived, status)
if uosc_available then
open_style_menu_uosc(actived, status)
elseif input_loaded then
mp.add_timeout(0.01, function()
open_style_menu_get(actived, status)
end)
else
show_message("无支持可用的 UI框架不支持使用该功能", 3)
end
end
-- 打开以指定时间为起点的延迟菜单
function open_delay_from_time_get(source, time, status)
mp.commandv('script-message-to', 'console', 'disable')
local menu_log = {}
local function build_menu(query, input_text)
menu_log = {
{ text = "【从该时间起调整弹幕延迟】", style = "{\\c&H00CCFF&\\b1}" },
{ text = ("-"):rep(33), style = "{\\c&H888888&}" }
}
table.insert(menu_log, { text = "\n", style = "" })
local hint_text = "提示:请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
local hint_style = "{\\c&H999999&}"
if status == "error" then
hint_text = "提示: 输入非数字字符或范围出错"
hint_style = "{\\c&H4C4CC3&}"
end
table.insert(menu_log, { text = input_text and ("已输入:" .. input_text) or "", style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = hint_text, style = hint_style })
input.set_log(menu_log)
end
input.get({
keep_open = true,
prompt = "请输入要设置的延迟(秒或 XmYs: ",
opened = function() build_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
build_menu()
return
end
build_menu(text)
end,
submit = function(text)
text = text and text:gsub("^%s*(.-)%s*$", "%1") or ""
if text == "" then return end
input.terminate()
local parsed = parse_delay_input(text)
if parsed ~= nil then
mp.commandv("script-message", "danmaku-delay", tostring(parsed), tostring(time), tostring(source))
else
open_delay_from_time(time, "error")
end
end
})
end
function open_delay_from_time_uosc(source, time, status)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
end
local menu_props = {
type = "menu_delay_from_time",
title = "从该时间起调整弹幕延迟",
search_style = "palette",
search_debounce = "submit",
footnote = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数",
items = {},
on_search = { "script-message-to", mp.get_script_name(), "setup-content-delay", tostring(time), tostring(source) },
}
if status == "error" then
menu_props.title = "输入非数字字符或范围出错"
mp.add_timeout(1.0, function() open_delay_from_time_uosc(source, time) end)
end
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_delay_from_time(source, time, status)
if uosc_available then
open_delay_from_time_uosc(source, time, status)
elseif input_loaded then
mp.add_timeout(0.01, function()
open_delay_from_time_get(source, time, status)
end)
else
show_message("无支持可用的 UI框架不支持使用该功能", 3)
end
end
-- 设置弹幕源延迟菜单
function danmaku_delay_setup(source_url)
function open_delay_menu_get(source, status)
mp.commandv('script-message-to', 'console', 'disable')
local menu_log = {}
local serial = 0
local select_num = 0
if source and tonumber(source) ~= nil then
select_num = tonumber(source)
end
local select_url = nil
local function build_menu(query, text)
menu_log = {
{ text = "【弹幕源延迟菜单】", style = "{\\c&H00CCFF&\\b1}" },
{ text = ("-"):rep(33), style = "{\\c&H888888&}" }
}
serial, select_num = 0, 0
for url, src in pairs(DANMAKU.sources) do
if src.data and not src.blocked then
local delay = 0
serial = serial + 1
select_num = (url == source) and serial or select_num
if src.delay_segments then
for _, seg in ipairs(src.delay_segments) do
if seg.start == 0 then
delay = seg.delay or 0
break
end
end
end
local hint = "当前弹幕源延迟: " .. string.format("%.1f", delay + 1e-10) .. ""
local text = string.format(" [%02d] %s [%s] ", serial, url, hint)
local style = (serial == select_num) and "{\\c&HFFDE7F&}" or "{\\c&HCCCCCC&}"
table.insert(menu_log, { text = text, style = style })
select_url = serial == select_num and url or select_url
end
end
if serial == 0 then
table.insert(menu_log, { text = "", style = "" })
end
table.insert(menu_log, { text = ("-"):rep(33), style = "{\\c&H888888&}" })
if select_num == 0 then
table.insert(menu_log, { text = "\n", style = "" })
table.insert(menu_log, {
text = "提示: 输入【w】可上移选项【s】可下移选项",
style = "{\\c&H999999&}"
})
else
local input_text = "已输入:" .. (text ~= nil and text or "")
local hint_text = "提示:请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
local hint_style = "{\\c&H999999&}"
if status == "refresh" then
status = ""
hint_text = "提示: 样式更改成功"
hint_style = "{\\c&HFFDE7F&}"
mp.add_timeout(1.5, build_menu)
elseif status == "error" then
status = ""
hint_text = "提示: 输入非数字字符或范围出错"
hint_style = "{\\c&H4C4CC3&}"
mp.add_timeout(1.5, build_menu)
end
-- table.insert(menu_log, { text = input_text, style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = input_text, style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = hint_text, style = hint_style })
end
input.set_log(menu_log)
end
input.get({
keep_open = true,
prompt = "请在此输入操作w/s|上移/下移): ",
opened = function() build_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
build_menu()
return
end
if text:lower() == "w" or text:lower() == "s" then
input.terminate()
select_num = text:lower() == "w" and select_num - 1 or select_num + 1
select_num = (select_num > serial) and 1 or (select_num <= 0 and serial or select_num)
mp.add_timeout(0.01, function()
open_delay_menu_get(select_num)
end)
return
end
build_menu(select_num, text)
end,
submit = function(text)
if select_url == nil then return end
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then return end
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "setup-source-delay", select_url, text)
return
end
})
end
function open_delay_menu_uosc(source_url, status)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
@@ -428,7 +905,7 @@ function danmaku_delay_setup(source_url)
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname and not source.blocked then
if source.data and not source.blocked then
local delay = 0
if source.delay_segments then
for _, seg in ipairs(source.delay_segments) do
@@ -453,7 +930,13 @@ function danmaku_delay_setup(source_url)
callback = {mp.get_script_name(), 'setup-source-delay'},
}
if source_url ~= nil then
menu_props.title = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
if status == "error" then
menu_props.title = "输入非数字字符或范围出错"
-- 创建一个定时器在1秒后触发回调函数删除搜索栏错误信息
mp.add_timeout(1.0, function() open_delay_menu_uosc(source_url) end)
else
menu_props.title = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
end
menu_props.search_style = "palette"
menu_props.search_debounce = "submit"
menu_props.on_search = { "script-message-to", mp.get_script_name(), "setup-source-delay", source_url }
@@ -463,18 +946,29 @@ function danmaku_delay_setup(source_url)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_delay_menu(source, status)
if uosc_available then
open_delay_menu_uosc(source, status)
elseif input_loaded then
mp.add_timeout(0.01, function()
open_delay_menu_get(source, status)
end)
else
show_message("无支持可用的 UI框架不支持使用该功能", 3)
end
end
-- 总集合弹幕菜单
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕源延迟设置", action = "open_source_delay_menu" },
{ title = "弹幕样式", action = "open_danmaku_style_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
function open_add_total_menu_uosc()
local items = {}
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕源延迟设置", action = "open_source_delay_menu" },
{ title = "弹幕样式", action = "open_setup_danmaku_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
if DANMAKU.anime and DANMAKU.episode then
local episode = DANMAKU.episode:gsub("%s.-$","")
@@ -509,11 +1003,6 @@ end
function open_add_total_menu_select()
local item_titles, item_values = {}, {}
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
for i, config in ipairs(total_menu_items_config) do
item_titles[i] = config.title
item_values[i] = { "script-message-to", mp.get_script_name(), config.action }
@@ -537,7 +1026,7 @@ function open_add_total_menu()
end
end
-- 添加 uosc 菜单栏按钮
mp.commandv(
"script-message-to",
"uosc",
@@ -570,7 +1059,7 @@ mp.commandv(
utils.format_json({
icon = "palette",
tooltip = "弹幕样式",
command = "script-message open_setup_danmaku_menu",
command = "script-message open_danmaku_style_menu",
})
)
@@ -611,8 +1100,8 @@ mp.register_script_message("set", function(prop, value)
if value == "on" then
ENABLED = true
set_danmaku_visibility(true)
if COMMENTS == nil then
set_danmaku_visibility(true)
local path = mp.get_property("path")
init(path)
else
@@ -622,7 +1111,6 @@ mp.register_script_message("set", function(prop, value)
else
show_message("关闭弹幕", 2)
ENABLED = false
set_danmaku_visibility(false)
hide_danmaku_func()
end
@@ -664,12 +1152,13 @@ mp.register_script_message("add-source-event", function(query)
add_danmaku_source(query, true)
end)
mp.register_script_message("open_setup_danmaku_menu", function()
mp.register_script_message("open_danmaku_style_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
end
add_danmaku_setup()
open_style_menu()
end)
mp.register_script_message("open_content_danmaku_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
@@ -677,6 +1166,17 @@ mp.register_script_message("open_content_danmaku_menu", function()
open_content_menu()
end)
mp.register_script_message("open-latest-menu-anime", function ()
if uosc_available then
mp.commandv("script-message-to", "uosc", "open-menu", latest_menu_anime)
elseif input_loaded then
show_message("", 0)
mp.add_timeout(0.1, function()
open_menu_select(utils.parse_json(latest_menu_anime))
end)
end
end)
mp.register_script_message("setup-danmaku-style", function(query, text)
local event = utils.parse_json(query)
if event ~= nil then
@@ -688,13 +1188,13 @@ mp.register_script_message("setup-danmaku-style", function(query, text)
menu_items_config.bold.hint = options.bold and "true" or "false"
end
-- "updata" 模式会保留输入框文字
add_danmaku_setup(ordered_keys[event.index], "updata")
open_style_menu(ordered_keys[event.index], "updata")
return
else
-- msg.info("event.action" .. event.action)
options[event.action] = menu_items_config[event.action]["original"]
menu_items_config[event.action]["hint"] = options[event.action]
add_danmaku_setup(event.action, "updata")
open_style_menu(event.action, "updata")
if event.action == "fontsize" or event.action == "scrolltime" then
load_danmaku(true)
end
@@ -718,14 +1218,14 @@ mp.register_script_message("setup-danmaku-style", function(query, text)
options[query] = tostring(num)
menu_items_config[query]["hint"] = options[query]
-- "refresh" 模式会清除输入框文字
add_danmaku_setup(query, "refresh")
open_style_menu(query, "refresh")
if query == "fontsize" or query == "scrolltime" then
load_danmaku(true, true)
end
return
end
end
add_danmaku_setup(query, "error")
open_style_menu(query, "error")
end
end)
@@ -734,14 +1234,10 @@ mp.register_script_message('setup-danmaku-source', function(json)
if event.type == 'activate' then
if event.action == "delete" then
local rm = DANMAKU.sources[event.value]["fname"]
if rm and file_exists(rm) and DANMAKU.sources[event.value]["from"] ~= "user_local" then
os.remove(rm)
end
DANMAKU.sources[event.value] = nil
remove_source_from_history(event.value)
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
open_add_menu()
load_danmaku(true)
end
@@ -749,7 +1245,7 @@ mp.register_script_message('setup-danmaku-source', function(json)
DANMAKU.sources[event.value]["blocked"] = true
add_source_to_history(event.value, DANMAKU.sources[event.value])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
open_add_menu()
load_danmaku(true)
end
@@ -757,7 +1253,7 @@ mp.register_script_message('setup-danmaku-source', function(json)
DANMAKU.sources[event.value]["blocked"] = false
add_source_to_history(event.value, DANMAKU.sources[event.value])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
open_add_menu()
load_danmaku(true)
end
end
@@ -768,39 +1264,74 @@ mp.register_script_message("setup-source-delay", function(query, text)
if event ~= nil then
-- item点击
if event.type == "activate" then
danmaku_delay_setup(event.value)
open_delay_menu(event.value)
end
else
-- 数值输入
if text == nil or text == "" then
return
end
local newText, _ = text:gsub("%s", "") -- 移除所有空白字符
local num = tonumber(newText)
local delay_segments = shallow_copy(DANMAKU.sources[query]["delay_segments"] or {})
for i = #delay_segments, 1, -1 do
if delay_segments[i].start == 0 then
table.remove(delay_segments, i)
end
local delay = parse_delay_input(text)
if delay ~= nil then
mp.commandv("script-message", "danmaku-delay", tostring(delay), "0")
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
mp.add_timeout(0.1, function()
open_delay_menu(query, "refresh")
end)
else
open_delay_menu(query, "error")
end
if num ~= nil then
table.insert(delay_segments, 1, { start = 0, delay = tonumber(num) })
DANMAKU.sources[query]["delay_segments"] = delay_segments
add_source_to_history(query, DANMAKU.sources[query])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
danmaku_delay_setup(query)
load_danmaku(true, true)
elseif newText:match("^%-?%d+m%d+s$") then
local minutes, seconds = string.match(newText, "^(%-?%d+)m(%d+)s$")
minutes = tonumber(minutes)
seconds = tonumber(seconds)
if minutes < 0 then seconds = -seconds end
table.insert(delay_segments, 1, { start = 0, delay = 60 * minutes + seconds })
DANMAKU.sources[query]["delay_segments"] = delay_segments
add_source_to_history(query, DANMAKU.sources[query])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
danmaku_delay_setup(query)
load_danmaku(true, true)
end
end)
mp.register_script_message('handle-danmaku-content-action', function(json)
local event = utils.parse_json(json)
if not event or event.type ~= 'activate' then return end
if event.action then
local d = COMMENTS[event.index]
if not d or not d.source then return end
if event.action == "block_source" then
DANMAKU.sources[d.source]["blocked"] = true
add_source_to_history(d.source, DANMAKU.sources[d.source])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_content")
load_danmaku(true)
elseif event.action == "adjust_delay" then
-- 打开以该弹幕时间为起点的延迟菜单(该延迟将作用于该时间点及之后的弹幕),仅针对该条弹幕的 source
mp.commandv("script-message", "open_content_delay_menu", d.source, tostring(d.start_time))
end
else
if event.value then
if type(event.value) == "table" then
mp.commandv(unpack(event.value))
else
mp.command(event.value)
end
mp.commandv("script-message-to", "uosc", "close-menu", "menu_content")
end
end
end)
mp.register_script_message("open_content_delay_menu", function(source, time)
open_delay_from_time(source, tonumber(time))
end)
mp.register_script_message("setup-content-delay", function(...)
local args = {...}
if #args == 1 then
return
end
if #args >= 2 then
local time = tonumber(args[1])
local source = args[2]
local delay_str = args[3]
local delay = parse_delay_input(delay_str)
if delay ~= nil then
mp.commandv("script-message", "danmaku-delay", tostring(delay), tostring(time), tostring(source))
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay_from_time")
else
open_delay_from_time(source, tonumber(time), "error")
end
end
end)
+5 -4
View File
@@ -5,19 +5,20 @@ options = {
-- 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
api_server = "https://api.dandanplay.net",
-- 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
-- 服务器可以自托管https://github.com/lyz05/danmaku
fallback_server = "https://fc.lyz05.cn",
-- 可用: https://api.danmu.icuhttps://dmku.hls.one
fallback_server = "https://api.danmu.icu",
-- 设置 tmdb 的 API Key用于获取非动画条目的中文信息(当搜索内容非中文时)
-- 可以在 https://www.themoviedb.org 注册后去个人账号设置界面获取
-- 注意:自定义此参数时还需要对获取到的 API Key 进行 base64 编码
tmdb_api_key = "NmJmYjIxOTZkNzIyN2UyMTIzMGM3Y2YzZjQ4MDNkZGM=",
load_more_danmaku = false,
auto_load = false,
autoload_local_danmaku = false,
autoload_for_url = false,
save_danmaku = false,
user_agent = "mpv_danmaku/1.0",
proxy = "",
-- 可选:向 HTTP 请求传递 cookie.txt 文件路径
cookie_file = "",
-- 使用 fps 视频滤镜,大幅提升弹幕平滑度。默认禁用
vf_fps = false,
-- 设置要使用的 fps 滤镜参数
@@ -73,4 +74,4 @@ options = {
]],
}
opt.read_options(options, mp.get_script_name(), function() end)
opt.read_options(options, mp.get_script_name(), function() end)
+303 -238
View File
@@ -40,10 +40,50 @@ local function load_blacklist_patterns(filepath)
return patterns
end
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then
table.insert(patterns, line)
if string.match(filepath, "%.xml$") then
-- xml文件格式示例
--<?xml version="1.0" encoding="utf-8"?>
--<filters>
-- <item enabled="true">t=卡在</item>
-- <item enabled="true">t=进度条</item>
--</filters>
print("加载黑名单文件: " .. filepath)
for line in file:lines() do
local pattern = line:match('<item%s+enabled="true">t=(.-)</item>')
if pattern then
print("加载黑名单模式: " .. pattern)
table.insert(patterns, pattern)
end
end
end
if string.match(filepath, "%.json$") then
-- json文件格式示例
-- [{"type":0,"filter":"开门","opened":true,"id":15628936}
-- ,{"type":0,"filter":"tony","opened":true,"id":15628939}
-- ,{"type":1,"filter":"0+.1","opened":true,"id":15628951}]
local content = read_file(filepath)
if content then
local json = utils.parse_json(content)
if json and type(json) == "table" then
for _, entry in ipairs(json) do
if entry.opened and entry.filter and entry.type == 0 then
table.insert(patterns, entry.filter)
end
end
end
end
end
if string.match(filepath, "%.txt$") then
-- 文本文件格式示例
-- 卡在
-- 进度条
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then
table.insert(patterns, line)
end
end
end
@@ -114,50 +154,62 @@ local function merge_duplicate_danmaku(danmakus, threshold)
local groups = {}
for _, d in ipairs(danmakus) do
local key = d.type .. "|" .. d.color .. "|" .. d.text
if not groups[key] then groups[key] = {} end
table.insert(groups[key], d)
local tkey = tostring(d.type or "")
local ckey = tostring(d.color or "")
local text = d.text or ""
groups[tkey] = groups[tkey] or {}
groups[tkey][ckey] = groups[tkey][ckey] or {}
groups[tkey][ckey][text] = groups[tkey][ckey][text] or {}
table.insert(groups[tkey][ckey][text], d)
end
local merged = {}
local abs = math.abs
for _, group in pairs(groups) do
table.sort(group, function(a, b) return a.time < b.time end)
for _, bytype in pairs(groups) do
for _, bycolor in pairs(bytype) do
for _, group in pairs(bycolor) do
table.sort(group, function(a, b) return a.time < b.time end)
local i = 1
while i <= #group do
local base = group[i]
local times = { base.time }
local count = 1
local j = i + 1
local i = 1
while i <= #group do
local base = group[i]
local times = { base.time }
local count = 1
local j = i + 1
while j <= #group and math.abs(group[j].time - base.time) <= threshold do
table.insert(times, group[j].time)
count = count + 1
j = j + 1
end
while j <= #group and abs(group[j].time - base.time) <= threshold do
times[#times+1] = group[j].time
count = count + 1
j = j + 1
end
local same_time = true
for k = 2, #times do
if times[k] ~= times[1] then
same_time = false
break
local same_time = true
for k = 2, #times do
if times[k] ~= times[1] then
same_time = false
break
end
end
local danmaku = {
time = base.time,
type = base.type,
size = base.size,
color = base.color,
text = base.text,
source = base.source,
orig_time = base.orig_time,
}
if count > 2 or not same_time then
danmaku.text = danmaku.text .. string.format("x%d", count)
end
table.insert(merged, danmaku)
i = j
end
end
local danmaku = {
time = base.time,
type = base.type,
size = base.size,
color = base.color,
text = base.text,
}
if count > 2 or not same_time then
danmaku.text = danmaku.text .. string.format("x%d", count)
end
table.insert(merged, danmaku)
i = j
end
end
@@ -207,9 +259,11 @@ local function limit_danmaku(danmakus, limit)
end
-- 解析 XML 弹幕
local function parse_xml_danmaku(xml_string, delay_segments)
local function parse_xml_danmaku(xml_string)
local danmakus = {}
for p_attr, text in xml_string:gmatch('<d p="([^"]+)">([^<]+)</d>') do
-- [^>]* 匹配其他 attributes
-- %f[^%s] 确保 p= 前面是空白字符
for p_attr, text in xml_string:gmatch('<d%s+[^>]*%f[^%s]p="([^"]+)"[^>]*>([^<]+)</d>') do
local params = {}
local i = 1
for val in p_attr:gmatch("([^,]+)") do
@@ -218,10 +272,8 @@ local function parse_xml_danmaku(xml_string, delay_segments)
end
if params[1] and params[2] and params[3] and params[4] then
local base_time = params[1]
local delay = get_delay_for_time(delay_segments, base_time)
table.insert(danmakus, {
time = base_time + delay,
time = params[1],
type = params[2] or 1,
size = params[3] or 25,
color = params[4] or 0xFFFFFF,
@@ -235,7 +287,7 @@ local function parse_xml_danmaku(xml_string, delay_segments)
end
-- 解析 JSON 弹幕
local function parse_json_danmaku(json_string, delay_segments)
local function parse_json_danmaku(json_string)
local danmakus = {}
if json_string:sub(1, 3) == "\239\187\191" then
json_string = json_string:sub(4)
@@ -259,10 +311,8 @@ local function parse_json_danmaku(json_string, delay_segments)
end
if params[1] and params[2] and params[3] and params[4] then
local base_time = params[1]
local delay = get_delay_for_time(delay_segments, base_time)
table.insert(danmakus, {
time = base_time + delay,
time = params[1],
color = params[2] or 0xFFFFFF,
type = params[3] or 1,
size = params[4] or 25,
@@ -277,64 +327,39 @@ local function parse_json_danmaku(json_string, delay_segments)
end
-- 解析弹幕文件
function parse_danmaku_files(danmaku_input, delays)
local DANMAKU_PATHs = {}
if type(danmaku_input) == "string" then
DANMAKU_PATHs = { danmaku_input }
else
for i, input in ipairs(danmaku_input) do
DANMAKU_PATHs[#DANMAKU_PATHs + 1] = input
end
end
function parse_danmaku_file(danmaku_input)
local danmakus = {}
local all_danmaku = {}
if file_exists(danmaku_input) then
local content = read_file(danmaku_input)
if content then
local parsed = {}
if danmaku_input:match("%.xml$") then
parsed = parse_xml_danmaku(content)
elseif danmaku_input:match("%.json$") then
parsed = parse_json_danmaku(content)
end
for i, DANMAKU_PATH in ipairs(DANMAKU_PATHs) do
if file_exists(DANMAKU_PATH) then
local content = read_file(DANMAKU_PATH)
if content then
local parsed = {}
local delay_segments = delays and delays[i] or {}
if DANMAKU_PATH:match("%.xml$") then
parsed = parse_xml_danmaku(content, delay_segments)
elseif DANMAKU_PATH:match("%.json$") then
parsed = parse_json_danmaku(content, delay_segments)
end
for _, d in ipairs(parsed) do
local matched, pattern = is_blacklisted(d.text, black_patterns)
if not matched then
d.text = ch_convert_cached(d.text)
table.insert(all_danmaku, d)
else
-- msg.debug("命中黑名单: " .. pattern)
end
end
else
msg.info("无法读取文件内容: " .. DANMAKU_PATH)
for _, d in ipairs(parsed) do
table.insert(danmakus, d)
end
else
msg.info("文件不存在: " .. DANMAKU_PATH)
msg.info("无法读取文件内容: " .. danmaku_input)
end
else
msg.info("文件不存在: " .. danmaku_input)
end
if #all_danmaku == 0 then
for _, d in ipairs(danmakus) do
if d.orig_time == nil then d.orig_time = d.time end
end
if #danmakus == 0 then
msg.info("未能解析任何弹幕")
return nil
end
if options.max_screen_danmaku > 0 and options.merge_tolerance <= 0 then
options.merge_tolerance = options.scrolltime
end
-- 按时间排序
table.sort(all_danmaku, function(a, b)
return a.time < b.time
end)
all_danmaku = merge_duplicate_danmaku(all_danmaku, options.merge_tolerance)
return all_danmaku
return danmakus
end
--# 弹幕数组与布局算法 (Danmaku Array & Layout Algorithms)
@@ -349,7 +374,7 @@ function DanmakuArray:new(res_x, res_y, font_size)
time_length_array = {}
}
for i = 1, obj.rows do
obj.time_length_array[i] = { time = -1, length = 0 }
obj.time_length_array[i] = { time = 0, length = 0, empty = true }
end
setmetatable(obj, self)
return obj
@@ -357,7 +382,7 @@ end
function DanmakuArray:set_time_length(row, time, length)
if row > 0 and row <= self.rows then
self.time_length_array[row] = { time = time, length = length }
self.time_length_array[row] = { time = time, length = length, empty = false }
end
end
@@ -375,15 +400,20 @@ function DanmakuArray:get_length(row)
return 0
end
function DanmakuArray:is_empty(row)
if row > 0 and row <= self.rows then
return self.time_length_array[row].empty
end
return false
end
-- 滚动弹幕 Y 坐标算法
function get_position_y(font_size, appear_time, text_length, resolution_x, roll_time, array)
local velocity = (text_length + resolution_x) / roll_time
local best_row = 0
local best_bias = -math.huge
for i = 1, array.rows do
local previous_appear_time = array:get_time(i)
if array:get_time(i) < 0 then
if array:is_empty(i) then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
@@ -391,7 +421,7 @@ function get_position_y(font_size, appear_time, text_length, resolution_x, roll_
local previous_length = array:get_length(i)
local previous_velocity = (previous_length + resolution_x) / roll_time
local delta_velocity = velocity - previous_velocity
local delta_x = (appear_time - previous_appear_time) * previous_velocity - (previous_length + text_length) / 2
local delta_x = (appear_time - previous_appear_time) * previous_velocity - previous_length
if delta_x >= 0 then
if delta_velocity <= 0 then
@@ -400,22 +430,10 @@ function get_position_y(font_size, appear_time, text_length, resolution_x, roll_
end
local delta_time = delta_x / delta_velocity
local bias = appear_time - previous_appear_time - delta_time
-- 判断:追及点是否在屏幕之外
local t_catch = previous_appear_time + delta_time
local distance_prev = previous_velocity * (t_catch - previous_appear_time)
if distance_prev > resolution_x then
-- 追及发生在屏幕之外,允许放置
if delta_time >= roll_time then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
if bias > 0 then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
elseif bias > best_bias then
best_bias = bias
best_row = i
end
end
end
-- 所有行都被占用,放弃渲染
@@ -424,8 +442,6 @@ end
-- 固定弹幕 Y 坐标算法
function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
local best_row = 0
local best_bias = -1
local row_start, row_end, row_step
if from_top then
row_start, row_end, row_step = 1, array.rows, 1
@@ -435,7 +451,7 @@ function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
for i = row_start, row_end, row_step do
local previous_appear_time = array:get_time(i)
if previous_appear_time < 0 then
if array:is_empty(i) then
array:set_time_length(i, appear_time, 0)
return (i - 1) * font_size + 1
else
@@ -443,9 +459,6 @@ function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
if delta_time > fixtime then
array:set_time_length(i, appear_time, 0)
return (i - 1) * font_size + 1
elseif delta_time > best_bias then
best_bias = delta_time
best_row = i
end
end
end
@@ -453,21 +466,155 @@ function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
return nil
end
-- 将弹幕转换为 ASS 格式
function convert_danmaku_to_ass(all_danmaku, danmaku_file)
if #all_danmaku == 0 then
msg.info("弹幕文件为空或解析失败")
-- 将弹幕转换为 XML 格式
function convert_danmaku_to_xml(danmaku_out)
local danmakus = {}
for _, source in pairs(DANMAKU.sources) do
if not source.blocked and source.data then
for _, d in ipairs(source.data) do
table.insert(danmakus, d)
end
end
end
if #danmakus == 0 then
show_message("弹幕内容为空,无法保存", 3)
msg.verbose("弹幕内容为空,无法保存")
COMMENTS = {}
return false
end
msg.info("已解析 " .. #all_danmaku .. " 条弹幕")
local alpha = string.format("%02X", (1 - tonumber(options.opacity)) * 255)
local bold = options.bold and "1" or "0"
-- 拼接为 XML 内容
local xml = { '<?xml version="1.0" encoding="UTF-8"?><i>\n' }
for _, d in ipairs(danmakus) do
local time = d.time
local type = d.type or 1
local size = d.size or 25
local color = d.color or 0xFFFFFF
local text = d.text or ""
text = text:gsub("&", "&amp;")
:gsub("<", "&lt;")
:gsub(">", "&gt;")
:gsub("\"", "&quot;")
:gsub("'", "&apos;")
table.insert(xml, string.format('<d p="%s,%s,%s,%s">%s</d>\n', time, type, size, color, text))
end
table.insert(xml, '</i>')
-- 写入 XML 文件
local file = io.open(danmaku_out, "w")
if not file then
show_message("无法写入目标 XML 文件", 3)
msg.info("无法写入目标 XML 文件: " .. danmaku_out)
return false
end
file:write(table.concat(xml))
file:close()
show_message("转换 XML 弹幕成功: " .. danmaku_out, 3)
msg.info("转换 XML 弹幕成功: " .. danmaku_out)
return true
end
function convert_danmaku_to_ass_events(force)
local per_source_lists = {}
for url, source in pairs(DANMAKU.sources) do
if not source.blocked and source.data then
local segments = nil
local prefix = nil
if source.delay_segments and #source.delay_segments > 0 then
segments = {}
for i, v in ipairs(source.delay_segments) do segments[i] = v end
table.sort(segments, function(a, b) return (a.start or 0) < (b.start or 0) end)
prefix = {}
local s = 0
for i, v in ipairs(segments) do
s = s + (v.delay or 0)
prefix[i] = s
end
end
local function get_cached_delay(t)
local segs = segments or {}
local pre = prefix or {}
if #segs == 0 then return 0 end
local idx = binary_search(segs, t, function(s) return (s and s.start) or 0 end)
local target = idx - 1
if target < 1 then return 0 end
return pre[target] or 0
end
local list = {}
for _, d in ipairs(source.data) do
local base_time = d.orig_time or d.time
if d.orig_time == nil then d.orig_time = base_time end
local adjusted_time = base_time + get_cached_delay(base_time)
local entry = {
orig_time = d.orig_time,
time = adjusted_time,
type = d.type,
size = d.size,
color = d.color,
text = d.text,
source = url,
}
if not is_blacklisted(d.text, black_patterns) then
table.insert(list, entry)
end
end
if #list > 0 then
table.sort(list, function(a, b) return a.time < b.time end)
per_source_lists[#per_source_lists + 1] = list
end
end
end
local danmakus = {}
local heap = new_min_heap()
for li = 1, #per_source_lists do
local lst = per_source_lists[li]
if lst and #lst > 0 then
heap.push({ time = lst[1].time, list_idx = li, pos = 1, entry = lst[1] })
end
end
while true do
local node = heap.pop()
if not node then break end
table.insert(danmakus, node.entry)
local li = node.list_idx
local next_pos = node.pos + 1
local lst = per_source_lists[li]
if lst and lst[next_pos] then
heap.push({ time = lst[next_pos].time, list_idx = li, pos = next_pos, entry = lst[next_pos] })
end
end
if options.max_screen_danmaku > 0 and options.merge_tolerance <= 0 then
options.merge_tolerance = options.scrolltime
end
danmakus = merge_duplicate_danmaku(danmakus, options.merge_tolerance)
if #danmakus == 0 then
if not force then
show_message("该集弹幕内容为空,结束加载", 3)
msg.verbose("该集弹幕内容为空,结束加载")
end
COMMENTS = {}
return
end
if not force then
msg.info("已解析 " .. #danmakus .. " 条弹幕")
end
local fontsize = tonumber(options.fontsize) or 50
local scrolltime = tonumber(options.scrolltime) or 15
local fixtime = tonumber(options.fixtime) or 5
local outline = tonumber(options.outline) or 1.0
local shadow = tonumber(options.shadow) or 0.0
local res_x = 1920
local res_y = 1080
@@ -475,33 +622,11 @@ function convert_danmaku_to_ass(all_danmaku, danmaku_file)
local roll_array = DanmakuArray:new(res_x, res_y, fontsize)
local top_array = DanmakuArray:new(res_x, res_y, fontsize)
local ass_header = string.format([[
[Script Info]
Title: DanmakuConvert for mpv
ScriptType: v4.00+
Collisions: Normal
PlayResX: %d
PlayResY: %d
Timer: 100.0000
WrapStyle: 2
ScaledBorderAndShadow: yes
[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: R2L,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,7,0,0,0,1
Style: TOP,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,8,0,0,0,1
Style: BTM,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,2,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
]], res_x, res_y, options.fontname, fontsize, alpha, alpha, bold, outline, shadow,
options.fontname, fontsize, alpha, alpha, bold, outline, shadow,
options.fontname, fontsize, alpha, alpha, bold, outline, shadow)
-- 预处理弹幕,先计算时间段以便进行数量限制
local pre_events = {}
for _, d in ipairs(all_danmaku) do
for _, d in ipairs(danmakus) do
local time = d.type == 1 and math.floor(d.time + 0.5) or d.time
local orig_time = d.type == 1 and math.floor(d.orig_time + 0.5) or d.orig_time
local appear_time = time
local danmaku_type = d.type
@@ -513,7 +638,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
end
if end_time then
table.insert(pre_events, {start_time = appear_time, end_time = end_time, danmaku = d})
table.insert(pre_events, {orig_time = orig_time, start_time = appear_time, end_time = end_time, danmaku = d})
end
end
@@ -526,7 +651,8 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
local d = ev.danmaku
local appear_time = ev.start_time
local danmaku_type = d.type
local text = ass_escape(decode_html_entities(d.text))
local clean_text = ch_convert_cached(decode_html_entities(d.text))
local text = ass_escape(clean_text)
:gsub("x(%d+)$", "{\\b1\\i1}x%1")
-- 颜色从十进制转为 BGR Hex
@@ -537,13 +663,11 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
local b = string.sub(color_hex, 5, 6)
local color_text = string.format("{\\c&H%s%s%s&}", b, g, r)
local start_time_str = seconds_to_time(appear_time)
local layer, end_time_str, style, effect
local style, effect
local pos, move = nil, nil
-- 滚动弹幕 (类型 1, 2, 3)
if danmaku_type >= 1 and danmaku_type <= 3 then
layer = 0
end_time_str = seconds_to_time(ev.end_time)
style = "R2L"
local text_length = get_str_width(text, fontsize)
local x1 = res_x + text_length / 2
@@ -551,105 +675,46 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
local y = get_position_y(fontsize, appear_time, text_length, res_x, scrolltime, roll_array)
if y then
effect = string.format("{\\move(%d, %d, %d, %d)}", x1, y, x2, y)
move = {x1, y, x2, y}
end
-- 顶部弹幕 (类型 5)
elseif danmaku_type == 5 then
layer = 1
end_time_str = seconds_to_time(ev.end_time)
style = "TOP"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, true)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
pos = {x, y}
end
-- 底部弹幕 (类型 4)
elseif danmaku_type == 4 then
layer = 1
end_time_str = seconds_to_time(ev.end_time)
style = "BTM"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, false)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
pos = {x, y}
end
end
if style then
local line = nil
if effect then
line = string.format("Dialogue: %d,%s,%s,%s,,0,0,0,,%s%s%s", layer, start_time_str, end_time_str, style, effect, color_text, text)
else
line = string.format("Comment: %d,%s,%s,%s,,0,0,0,,%s%s", layer, start_time_str, end_time_str, style, color_text, text)
end
table.insert(ass_events, line)
if style and effect then
text = effect .. color_text .. text
local event = {
orig_time = ev.orig_time,
start_time = ev.start_time,
end_time = ev.end_time,
delay = ev.start_time - (ev.orig_time or ev.start_time),
style = style,
text = text,
clean_text = clean_text,
pos = pos,
move = move,
source = d.source,
}
table.insert(ass_events, event)
COMMENTS = ass_events
end
end
local final_ass = ass_header .. table.concat(ass_events, "\n")
local ass_file = io.open(danmaku_file, "w")
if not ass_file then
msg.info("错误: 无法写入 ASS 弹幕文件")
return false
end
ass_file:write(final_ass)
ass_file:close()
msg.debug("已成功转换并写入 ASS" .. danmaku_file)
return true
end
-- 将弹幕转换为 XML 格式
function convert_danmaku_to_xml(danmaku_input, danmaku_out, delays)
local all_danmaku = parse_danmaku_files(danmaku_input, delays)
if not all_danmaku then
show_message("转换 XML 弹幕失败", 3)
msg.info("转换 XML 弹幕失败")
return
end
-- 拼接为 XML 内容
local xml = { '<?xml version="1.0" encoding="UTF-8"?><i>\n' }
for _, d in ipairs(all_danmaku) do
local time = d.time
local type = d.type or 1
local size = d.size or 25
local color = d.color or 0xFFFFFF
local text = d.text or ""
text = text:gsub("&", "&amp;")
:gsub("<", "&lt;")
:gsub(">", "&gt;")
:gsub("\"", "&quot;")
:gsub("'", "&apos;")
table.insert(xml, string.format('<d p="%s,%s,%s,%s">%s</d>\n', time, type, size, color, text))
end
table.insert(xml, '</i>')
-- 写入 XML 文件
local file = io.open(danmaku_out, "w")
if not file then
show_message("无法写入目标 XML 文件", 3)
msg.info("无法写入目标 XML 文件: " .. danmaku_out)
return false
end
file:write(table.concat(xml))
file:close()
show_message("转换 XML 弹幕成功: " .. danmaku_out, 3)
msg.info("转换 XML 弹幕成功: " .. danmaku_out)
return true
end
-- 解析和转换弹幕
function convert_danmaku_format(danmaku_input, danmaku_file, delays)
local all_danmaku = parse_danmaku_files(danmaku_input, delays)
if all_danmaku then
convert_danmaku_to_ass(all_danmaku, danmaku_file)
else
msg.info("未能解析对应的 .xml 或 .json 弹幕文件")
return false
end
end
end
+89 -126
View File
@@ -1,25 +1,15 @@
-- modified from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
local msg = require('mp.msg')
local utils = require("mp.utils")
local unpack = unpack or table.unpack
local INTERVAL = options.vf_fps and 0.01 or 0.001
local osd_width, osd_height, pause = 0, 0, true
local time_pos_observer_active = false
local overlay = mp.create_osd_overlay('ass-events')
-- 提取 \move 参数 (x1, y1, x2, y2) 并返回
local function parse_move_tag(text)
-- 匹配包括小数和负数在内的坐标值
local x1, y1, x2, y2 = text:match("\\move%((%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
if x1 and y1 and x2 and y2 then
return tonumber(x1), tonumber(y1), tonumber(x2), tonumber(y2)
end
return nil
end
local function parse_comment(event, pos, height, delay)
local x1, y1, x2, y2 = parse_move_tag(event.text)
local displayarea = tonumber(height * options.displayarea)
if not x1 then
local current_x, current_y = event.text:match("\\pos%((%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
local function realtime_position_text(event, pos, displayarea)
if not event.move then
local _, current_y = unpack(event.pos)
if not current_y or tonumber(current_y) > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\an8}%s", event.text)
@@ -28,9 +18,10 @@ local function parse_comment(event, pos, height, delay)
end
end
local x1, y1, x2, y2 = unpack(event.move)
-- 计算移动的时间范围
local duration = event.end_time - event.start_time --mean: options.scrolltime
local progress = (pos - event.start_time - delay) / duration -- 移动进度 [0, 1]
local progress = (pos - event.start_time) / duration -- 移动进度 [0, 1]
-- 计算当前坐标
local current_x = tonumber(x1 + (x2 - x1) * progress)
@@ -46,60 +37,28 @@ local function parse_comment(event, pos, height, delay)
end
end
-- 从 ASS 文件中解析样式和事件
local function parse_ass_events(ass_path, callback)
local ass_file = io.open(ass_path, "r")
if not ass_file then
callback("无法打开 ASS 文件")
function render(pos_arg)
if COMMENTS == nil then return end
local pos, err
if pos_arg == nil then
pos, err = mp.get_property_number('time-pos')
if err ~= nil then
return msg.error(err)
end
else
pos = pos_arg
end
if not pos then
overlay:remove()
return
end
local events = {}
local time_tolerance = options.merge_tolerance
for line in ass_file:lines() do
if line:match("^Dialogue:") then
local start_time, end_time, style, text = line:match("Dialogue:%s*[^,]*,%s*([^,]*),%s*([^,]*),%s*([^,]*),[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,(.*)")
if start_time and end_time and text then
local event = {
start_time = time_to_seconds(start_time),
end_time = time_to_seconds(end_time),
style = style,
text = text:gsub("%s+$", ""),
clean_text = text:gsub("\\h+", " "):gsub("{[\\=].-}", ""):gsub("^%s*(.-)%s*$", "%1"),
pos = text:match("\\pos"),
move = text:match("\\move"),
}
table.insert(events, event)
end
end
end
table.sort(events, function(a, b)
return a.start_time < b.start_time
end)
ass_file:close()
callback(nil, events)
end
local overlay = mp.create_osd_overlay('ass-events')
function render()
if COMMENTS == nil then return end
local pos, err = mp.get_property_number('time-pos')
if err ~= nil then
return msg.error(err)
end
local delay = get_delay_for_time(DELAYS, pos)
local fontname = options.fontname
local fontsize = options.fontsize
local alpha = string.format("%02X", (1 - tonumber(options.opacity)) * 255)
local opacity = tonumber(options.opacity)
local alpha = string.format("%02X", (1 - (opacity or 0)) * 255)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
@@ -109,23 +68,37 @@ function render()
end
local ass_events = {}
local max_display = math.max(options.scrolltime, options.fixtime)
local window_start = pos - max_display
for _, event in ipairs(COMMENTS) do
if pos >= event.start_time + delay and pos <= event.end_time + delay then
local text = parse_comment(event, pos, height, delay)
-- 跳过已结束的弹幕
local lo = binary_search(COMMENTS, window_start, function(item) return item.start_time end)
local re_entity = "&#%d+;"
local re_fs = "\\fs(%d+)"
local ass_prefix = string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}",
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0")
for i = lo, #COMMENTS do
local event = COMMENTS[i]
if not event then break end
if event.start_time > pos then break end -- 后续弹幕提前退出
if event.end_time >= pos then
local text = realtime_position_text(event, pos, height * options.displayarea)
if text then
text = text:gsub("&#%d+;","")
text = text:gsub(re_entity, "")
end
if text and text:match("\\fs%d+") then
text = text:gsub("\\fs(%d+)", function(size)
return string.format("\\fs%d", size * 1.5)
if text and text:match(re_fs) then
text = text:gsub(re_fs, function(size)
local n = tonumber(size) or 0
return string.format("\\fs%d", math.floor(n * 1.5))
end)
end
-- 构建 ASS 字符串
local ass_text = text and string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}%s",
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0", text)
local ass_text = text and (ass_prefix .. text)
table.insert(ass_events, ass_text)
end
@@ -137,27 +110,39 @@ function render()
overlay:update()
end
local timer = mp.add_periodic_timer(INTERVAL, render, true)
local function time_pos_callback(_, time_pos)
if time_pos then
render(time_pos)
else
overlay:remove()
end
end
function parse_danmaku(ass_file_path, from_menu, no_osd)
parse_ass_events(ass_file_path, function(err, events)
COMMENTS = events
if err then
msg.error("ASS 解析错误: " .. err)
return
end
local function start_time_observer()
if not time_pos_observer_active then
mp.observe_property('time-pos', 'number', time_pos_callback)
time_pos_observer_active = true
end
end
if ENABLED and (from_menu or get_danmaku_visibility()) then
if not no_osd then
show_loaded(true)
end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
show_danmaku_func()
else
show_message("")
hide_danmaku_func()
local function stop_time_observer()
if time_pos_observer_active then
mp.unobserve_property(time_pos_callback)
time_pos_observer_active = false
end
end
function render_danmaku(from_menu, no_osd)
if ENABLED and (from_menu or get_danmaku_visibility()) then
if not no_osd then
show_loaded(true)
end
end)
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
show_danmaku_func()
else
show_message("")
hide_danmaku_func()
end
end
local function filter_state(label, name)
@@ -172,9 +157,11 @@ local function filter_state(label, name)
end
function show_danmaku_func()
mp.set_property_bool(HAS_DANMAKU, true)
set_danmaku_visibility(true)
render()
if not pause then
timer:resume()
start_time_observer()
end
if options.vf_fps then
local display_fps = mp.get_property_number('display-fps')
@@ -189,7 +176,9 @@ function show_danmaku_func()
end
function hide_danmaku_func()
timer:kill()
stop_time_observer()
mp.set_property_bool(HAS_DANMAKU, false)
set_danmaku_visibility(false)
overlay:remove()
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
@@ -221,33 +210,15 @@ end
mp.observe_property('osd-width', 'number', function(_, value) osd_width = value or osd_width end)
mp.observe_property('osd-height', 'number', function(_, value) osd_height = value or osd_height end)
mp.observe_property('display-fps', 'number', function(_, value)
if value ~= nil then
local interval = 1 / value / 10
if interval > INTERVAL then
timer:kill()
timer = mp.add_periodic_timer(interval, render, true)
if ENABLED then
timer:resume()
end
else
timer:kill()
timer = mp.add_periodic_timer(INTERVAL, render, true)
if ENABLED then
timer:resume()
end
end
end
end)
mp.observe_property('pause', 'bool', function(_, value)
if value ~= nil then
pause = value
end
if ENABLED then
if pause then
timer:kill()
stop_time_observer()
elseif COMMENTS ~= nil then
timer:resume()
start_time_observer()
end
end
end)
@@ -263,7 +234,7 @@ end)
mp.add_hook("on_unload", 50, function()
COMMENTS, DELAY = nil, 0
timer:kill()
stop_time_observer()
overlay:remove()
mp.set_property_native(DELAY_PROPERTY, 0)
if filter_state("danmaku") then
@@ -271,13 +242,10 @@ mp.add_hook("on_unload", 50, function()
end
local files_to_remove = {
file1 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".json"),
file2 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".ass"),
file3 = utils.join_path(DANMAKU_PATH, "temp-" .. PID .. ".mp4"),
file4 = utils.join_path(DANMAKU_PATH, "bahamut-" .. PID .. ".json")
file1 = utils.join_path(DANMAKU_PATH, "temp-" .. PID .. ".mp4"),
}
if options.save_danmaku and file_exists(files_to_remove.file2) then
if options.save_danmaku then
save_danmaku(true)
end
@@ -287,10 +255,5 @@ mp.add_hook("on_unload", 50, function()
end
end
for _, source in pairs(DANMAKU.sources) do
if source.fname and source.from and source.from ~= "user_local" and file_exists(source.fname) then
os.remove(source.fname)
end
end
DANMAKU = {sources = {}, count = 1}
end)
+126 -12
View File
@@ -2,8 +2,10 @@ local msg = require('mp.msg')
local utils = require("mp.utils")
local repo = "Tony15246/uosc_danmaku"
local zip_file = utils.join_path(os.getenv("TEMP") or "/tmp/", "uosc_danmaku.zip")
local local_version = VERSION or "0.0.0"
local platform = mp.get_property("platform")
local function version_greater(v1, v2)
local function parse(ver)
@@ -29,25 +31,137 @@ local function get_latest_release(repo)
})
if not res or res.status ~= 0 then return nil end
local tag = res.stdout:match([["tag_name"%s*:%s*"([^"]+)"]])
return tag
local zip_url = res.stdout:match([["browser_download_url"%s*:%s*"([^"]+%.zip)"]])
return tag, zip_url
end
local function escape_ps(str)
return tostring(str):gsub("'", "''")
end
local function unzip_overwrite(zip_file)
local outpath = mp.get_script_directory()
-- 定义临时目录路径,用于安全更新
local tmpdir = utils.join_path(
(platform == "windows" and (os.getenv("TEMP") or "C:\\Windows\\Temp") or "/tmp"),
"uosc_update_" .. tostring(os.time())
)
local cmd_unzip = {}
msg.info("创建临时目录并解压: " .. tmpdir)
if platform == "windows" then
-- PowerShell: Expand-Archive (会自动创建目标目录)
local ps_script = string.format(
"Expand-Archive -LiteralPath '%s' -DestinationPath '%s' -Force",
escape_ps(zip_file),
escape_ps(tmpdir)
)
cmd_unzip = { "powershell", "-NoProfile", "-Command", ps_script }
else
-- Unix: unzip
cmd_unzip = { "unzip", "-o", zip_file, "-d", tmpdir }
end
local res = mp.command_native({
name = "subprocess",
args = cmd_unzip,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res or res.status ~= 0 then
msg.error("❌ 解压失败:\n" .. (res and (res.stdout .. res.stderr) or "未知错误"))
-- 清理残留的临时目录
if platform == "windows" then
mp.command_native({
name = "subprocess",
args = {"powershell", "-NoProfile", "-Command", "Remove-Item -LiteralPath '"..escape_ps(tmpdir).."' -Recurse -Force"}
})
else
os.execute("rm -rf \"" .. tmpdir .. "\"")
end
return false
end
msg.info("解压成功,准备替换旧目录...")
local cmd_swap = {}
if platform == "windows" then
-- Windows: 在一个 PowerShell 实例中执行删除和移动
local ps_swap = string.format(
"Remove-Item -LiteralPath '%s' -Recurse -Force -ErrorAction SilentlyContinue; Move-Item -LiteralPath '%s' -Destination '%s' -Force",
escape_ps(outpath),
escape_ps(tmpdir),
escape_ps(outpath)
)
cmd_swap = { "powershell", "-NoProfile", "-Command", ps_swap }
else
-- Unix: rm && mv
cmd_swap = { "sh", "-c", string.format("rm -rf \"%s\" && mv \"%s\" \"%s\"", outpath, tmpdir, outpath) }
end
local res_swap = mp.command_native({
name = "subprocess",
args = cmd_swap,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res_swap or res_swap.status ~= 0 then
msg.error("❌ 替换目录失败:\n" .. (res_swap and (res_swap.stdout .. res_swap.stderr) or ""))
return false
end
msg.info("更新完成")
return true
end
-- 仅检查并提示新版本,不自动下载/覆盖(避免 rm -rf 破坏配置)
function check_for_update()
local latest_version = get_latest_release(repo)
if not latest_version then
show_message("无法获取最新版本信息")
msg.warn("无法获取最新版本信息")
local latest_version, download_url = get_latest_release(repo)
if not latest_version or not download_url then
show_message("无法获取最新版本信息")
msg.warn("无法获取最新版本信息")
return
end
if not version_greater(latest_version, local_version) then
show_message("uosc_danmaku 已是最新版本 (" .. local_version .. ")")
msg.info("uosc_danmaku 已是最新版本 (" .. local_version .. ")")
show_message(" 已是最新版本 ("..local_version..")")
msg.info("✅ 已是最新版本")
return
end
local update_url = "https://github.com/" .. repo .. "/releases/tag/" .. latest_version
show_message("uosc_danmaku 有新版本: " .. latest_version .. " (当前: " .. local_version .. ")\n请手动更新: " .. update_url)
msg.info("uosc_danmaku 新版本: " .. latest_version .. " 下载地址: " .. update_url)
end
show_message("⬇️ 发现新版本: " .. latest_version .. ",正在下载...")
msg.info("⬇️ 发现新版本: " .. latest_version .. ",地址: " .. download_url)
local cmd = { "curl", "-L", "-o", zip_file, download_url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res or res.status ~= 0 then
show_message("❌ 下载失败!")
msg.warn("❌ 下载失败!")
return
end
show_message("📦 下载完成,开始解压覆盖...")
msg.info("📦 下载完成,开始解压覆盖...")
if unzip_overwrite(zip_file) then
os.remove(zip_file)
show_message("✅ 更新成功!请重启 mpv 以应用更新,当前版本为:" .. latest_version)
msg.info("✅ 更新成功,当前版本为:" .. latest_version)
else
os.remove(zip_file)
show_message("❌ 解压失败!请查看控制台日志")
msg.warn("❌ 解压失败!")
end
end
+127 -36
View File
@@ -1,4 +1,5 @@
local utils = require("mp.utils")
local unpack = unpack or table.unpack
-- from http://lua-users.org/wiki/LuaUnicode
local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'
@@ -210,6 +211,31 @@ function hex_to_char(x)
return string.char(tonumber(x, 16))
end
function hex_to_int_color(hex_color)
-- 移除颜色代码中的'#'字符
hex_color = hex_color:sub(2) -- 只保留颜色代码部分
-- 提取R, G, B的十六进制值并转为整数
local r = tonumber(hex_color:sub(1, 2), 16)
local g = tonumber(hex_color:sub(3, 4), 16)
local b = tonumber(hex_color:sub(5, 6), 16)
-- 计算32位整数值
local color_int = (r * 256 * 256) + (g * 256) + b
return color_int
end
local function get_type_from_position(position)
if position == 0 then
return 1
end
if position == 1 then
return 4
end
return 5
end
-- url编码转换
function url_encode(str)
-- 将非安全字符转换为百分号编码
@@ -318,6 +344,67 @@ function file_exists(path)
return false
end
function binary_search(tbl, target, key)
if not tbl or #tbl == 0 then return 1 end
key = key or function(x) return x end
local lo, hi = 1, #tbl
local res = #tbl + 1
while lo <= hi do
local mid = math.floor((lo + hi) / 2)
local v = tbl[mid]
local val = key(v)
if val >= target then
res = mid
hi = mid - 1
else
lo = mid + 1
end
end
return res
end
function new_min_heap()
local h = {}
local function swap(i, j)
h[i], h[j] = h[j], h[i]
end
local function up(i)
while i > 1 do
local p = math.floor(i/2)
if h[p].time <= h[i].time then break end
swap(p, i)
i = p
end
end
local function down(i)
local n = #h
while true do
local l = i * 2
local r = l + 1
local smallest = i
if l <= n and h[l].time < h[smallest].time then smallest = l end
if r <= n and h[r].time < h[smallest].time then smallest = r end
if smallest == i then break end
swap(i, smallest)
i = smallest
end
end
local function push(node)
h[#h + 1] = node
up(#h)
end
local function pop()
if #h == 0 then return nil end
local root = h[1]
if #h == 1 then h[1] = nil; return root end
h[1] = h[#h]
h[#h] = nil
down(1)
return root
end
return { push = push, pop = pop, size = function() return #h end }
end
function is_writable(path)
local file = io.open(path, "w")
if file then
@@ -386,10 +473,19 @@ local function split_by_numbers(filename)
return parts
end
-- 识别匹配前后剧集
local function compare_filenames(fname1, fname2)
-- 识别匹配前后剧集并提取集数
local function get_series_episodes(fname1, fname2)
local parts1 = split_by_numbers(fname1)
local parts2 = split_by_numbers(fname2)
local title1 = format_filename(fname1)
local title2 = format_filename(fname2)
if title1 and title2 then
local media_title1, season1, episode1 = title1:match("^(.-)%s*[sS](%d+)[eE](%d+)")
local media_title2, season2, episode2 = title2:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season1 and season2 and season1 ~= season2 then
return nil, nil
end
end
local min_len = math.min(#parts1, #parts2)
@@ -400,7 +496,7 @@ local function compare_filenames(fname1, fname2)
-- 比较数字前的字符是否相同
if part1.pre ~= part2.pre then
return false
return nil, nil
end
-- 比较数字部分
@@ -410,11 +506,36 @@ local function compare_filenames(fname1, fname2)
-- 比较数字后的字符是否相同
if part1.post ~= part2.post then
return false
return nil, nil
end
end
return false
return nil, nil
end
-- 获取当前文件名所包含的集数
function get_episode_number(filename, fname)
-- 尝试对比记录文件名来获取当前集数
if fname then
return get_series_episodes(fname, filename)
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local title = format_filename(filename)
if title then
local media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return tonumber(episode)
else
local media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return tonumber(episode)
end
end
end
return nil
end
-- 规范化路径
@@ -520,36 +641,6 @@ function parse_title()
return title_replace(title), season, episode
end
-- 获取当前文件名所包含的集数
function get_episode_number(filename, fname)
-- 尝试对比记录文件名来获取当前集数
if fname then
local episode_num1, episode_num2 = compare_filenames(fname, filename)
if episode_num1 and episode_num2 then
return episode_num1, episode_num2
else
return nil, nil
end
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local title = format_filename(filename)
if title then
local media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return tonumber(episode)
else
local media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return tonumber(episode)
end
end
end
return nil
end
local CHINESE_NUM_MAP = {
[""] = 0, [""] = 1, [""] = 2, [""] = 3, [""] = 4,
[""] = 5, [""] = 6, [""] = 7, [""] = 8, [""] = 9,
@@ -644,7 +735,7 @@ function call_cmd_async(args, callback)
name = 'subprocess',
capture_stderr = true,
capture_stdout = true,
playback_only = false,
playback_only = true,
args = args,
}, function(success, result, error)
if not success or not result or result.status ~= 0 then