update
This commit is contained in:
@@ -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.
|
||||
@@ -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.icu,https://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)
|
||||
@@ -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 api(url)+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
|
||||
@@ -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)
|
||||
@@ -3979,4 +3979,4 @@ return {
|
||||
["誉"] = "譽",
|
||||
["𫐖"] = "轇",
|
||||
["输"] = "輸",
|
||||
}
|
||||
}
|
||||
@@ -4112,4 +4112,4 @@ return {
|
||||
["𪷓"] = "𣶭",
|
||||
["𫒡"] = "𫓷",
|
||||
["𫜦"] = "𫜫",
|
||||
}
|
||||
}
|
||||
+114
-103
@@ -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)
|
||||
@@ -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
|
||||
@@ -189,4 +189,4 @@ local function sha256(message)
|
||||
return result
|
||||
end
|
||||
|
||||
return sha256
|
||||
return sha256
|
||||
@@ -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,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.icu,https://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)
|
||||
@@ -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("&", "&")
|
||||
:gsub("<", "<")
|
||||
:gsub(">", ">")
|
||||
:gsub("\"", """)
|
||||
:gsub("'", "'")
|
||||
|
||||
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("&", "&")
|
||||
:gsub("<", "<")
|
||||
:gsub(">", ">")
|
||||
:gsub("\"", """)
|
||||
:gsub("'", "'")
|
||||
|
||||
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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user