update
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
> [!WARNING]
|
||||
> Release1.2.0及Release1.2.0之前的发行版,都由于弹弹play接口使用政策改版,部分功能无法使用。如果发现插件功能异常,比如搜索弹幕总是显示无结果,请拉取或下载主分支最新源代码;或下载[最新发行版](https://github.com/Tony15246/uosc_danmaku/releases/latest)
|
||||
|
||||
> [!NOTE]
|
||||
> 插件默认通过项目维护的 API 代理 `https://danmaku-api.152468.xyz` 访问弹弹play开放弹幕网络。普通用户无需注册账号,也无需配置或持有弹弹play的 AppId/AppSecret。代理包含缓存和访问频率限制,请勿用于批量抓取。
|
||||
|
||||
> [!NOTE]
|
||||
> 已添加对mpv内部 `mp.input`的支持,在uosc不可用时通过键绑定调用此方式渲染菜单
|
||||
>
|
||||
@@ -63,12 +66,44 @@
|
||||
|
||||
</details>
|
||||
|
||||
## 目录
|
||||
|
||||
- [项目简介](#项目简介)
|
||||
- [主要功能](#主要功能)
|
||||
- [安装](#安装)
|
||||
- [下载](#下载)
|
||||
- [基本配置](#基本配置)
|
||||
- [uosc控件配置](#uosc控件配置)
|
||||
- [绑定快捷键(可选)](#绑定快捷键可选)
|
||||
- [拓展功能(可选)](#拓展功能可选)
|
||||
- [从弹幕源向当前弹幕添加新弹幕内容(从网络url或本地添加弹幕)](#从弹幕源向当前弹幕添加新弹幕内容可选)
|
||||
- [弹幕源延迟设置](#弹幕源延迟设置可选)
|
||||
- [实时修改弹幕样式](#实时修改弹幕样式可选)
|
||||
- [弹幕设置总菜单](#弹幕设置总菜单可选)
|
||||
- [设置弹幕延迟(可选)](#设置弹幕延迟可选)
|
||||
- [保存当前视频弹幕(可选)](#保存当前视频弹幕可选)
|
||||
- [清空当前视频关联的弹幕源(可选)](#清空当前视频关联的弹幕源可选)
|
||||
- [检查脚本更新(可选)](#检查脚本更新可选)
|
||||
- [可配置选项(可选)](#可配置选项可选)
|
||||
- [弹幕加载相关](#弹幕加载相关)
|
||||
- [弹幕显示相关](#弹幕显示相关)
|
||||
- [弹幕解析服务相关](#弹幕解析服务相关)
|
||||
- [插件配置相关](#插件配置相关)
|
||||
- [自定义弹幕样式相关配置](#自定义弹幕样式相关配置)
|
||||
- [插件自定义属性](#插件自定义属性)
|
||||
- [常见问题](#常见问题)
|
||||
- [特别感谢](#特别感谢)
|
||||
- [相关项目](#相关项目)
|
||||
|
||||
## 安装
|
||||
|
||||
### 下载
|
||||
|
||||
一般的mpv配置目录结构大致如下
|
||||
|
||||
> [!NOTE]
|
||||
> Windows 系统上等价全局配置路径`%APPDATA%/mpv/`,也可使用 mpv.exe 所在目录的 portable_config 文件夹(便携配置路径)
|
||||
|
||||
```
|
||||
~/.config/mpv
|
||||
├── fonts
|
||||
@@ -301,7 +336,7 @@ key script-message danmaku-delay <seconds> ${=time-pos}
|
||||
|
||||
> #### 保存当前视频弹幕(可选)
|
||||
|
||||
在视频播放时手动保存弹幕至视频所在文件夹,保存格式为 `xml`(注:此功能将保存为视频同名弹幕,若视频文件夹下存在同名文件将不会执行该功能)
|
||||
在视频播放时手动保存弹幕,保存格式为 `xml`(注:此功能将保存为视频同名弹幕,若目标文件夹下存在同名文件将不会执行该功能)。默认保存至视频所在文件夹,也可以通过 `save_danmaku_path` 和 `save_danmaku_path_mode` 保存到指定文件夹。
|
||||
|
||||
想要通过快捷键使用此功能,请添加类似下面的配置到 `input.conf`中。从源添加弹幕功能对应的脚本消息为 `immediately_save_danmaku`。
|
||||
|
||||
@@ -472,7 +507,7 @@ save_danmaku
|
||||
|
||||
#### 功能说明
|
||||
|
||||
当文件关闭时自动保存弹幕文件(xml格式)至视频同目录,保存的弹幕文件名与对应的视频文件名相同。配合[autoload_local_danmaku选项](#autoload_local_danmaku)可以实现弹幕自动保存到本地并且下次播放时自动加载本地保存的弹幕。此功能默认禁用。
|
||||
当文件关闭时自动保存弹幕文件(xml格式),保存的弹幕文件名与对应的视频文件名相同。默认保存至视频同目录,配合[autoload_local_danmaku选项](#autoload_local_danmaku)可以实现弹幕自动保存到本地并且下次播放时自动加载本地保存的弹幕。此功能默认禁用。
|
||||
|
||||
> **⚠️NOTE!**
|
||||
>
|
||||
@@ -492,35 +527,30 @@ save_danmaku
|
||||
save_danmaku=yes
|
||||
```
|
||||
|
||||
</details>
|
||||
如需保存到指定文件夹,请提前创建目标文件夹,然后配置 `save_danmaku_path` 和 `save_danmaku_path_mode`。插件不会自动创建目录。
|
||||
|
||||
---
|
||||
保存本地媒体到指定文件夹时,文件名会包含视频父目录名,用于减少不同目录下同名视频的弹幕文件冲突。例如 `动画/01.mkv` 会保存为类似 `动画_01.xml`。网络媒体保存到指定文件夹时仍使用媒体标题作为文件名。
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
`save_danmaku_path_mode` 可选值如下:
|
||||
|
||||
~~add_from_source~~
|
||||
- `local`:默认值,仅本地媒体保存到 `save_danmaku_path`,网络媒体仍不保存
|
||||
- `url`:仅网络媒体保存到 `save_danmaku_path`,本地媒体仍保存到视频同目录
|
||||
- `all`:本地媒体和网络媒体都保存到 `save_danmaku_path`
|
||||
|
||||
> ~~开关记录通过 从弹幕源向当前弹幕添加新弹幕内容 关联过的弹幕源并自动加载(已废除)~~
|
||||
|
||||
</summary>
|
||||
|
||||
### add_from_source
|
||||
|
||||
> **⚠️NOTE!**
|
||||
>
|
||||
> 该可选配置项在Release v1.2.0之后已废除。现在通过 `从弹幕源向当前弹幕添加新弹幕内容`功能关联过的弹幕源被记录,并且下次播放同一个视频的时候自动关联并加载所有添加过的弹幕源,这样的行为已经成为了插件的默认行为,不需要再通过 `add_from_source`来开启。在[从源获取弹幕](#从弹幕源向当前弹幕添加新弹幕内容可选)菜单中可以可视化地管理所有添加过的弹幕源。
|
||||
|
||||
#### 功能说明
|
||||
|
||||
开启此选项后,通过 `从弹幕源向当前弹幕添加新弹幕内容`功能关联过的弹幕源会被记录,并且下次播放同一个视频的时候会自动关联并加载添加过的弹幕源。
|
||||
|
||||
#### 使用方法
|
||||
|
||||
想要开启此选项,请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并添加如下内容:
|
||||
例如,仅将网络媒体的弹幕保存到 `~~/danmaku`:
|
||||
|
||||
```
|
||||
add_from_source=yes
|
||||
save_danmaku=yes
|
||||
save_danmaku_path=~~/danmaku
|
||||
save_danmaku_path_mode=url
|
||||
```
|
||||
|
||||
例如,将所有媒体的弹幕都保存到 `~~/danmaku`:
|
||||
|
||||
```
|
||||
save_danmaku=yes
|
||||
save_danmaku_path=~~/danmaku
|
||||
save_danmaku_path_mode=all
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -613,6 +643,35 @@ merge_tolerance=1
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
merge_without_style
|
||||
|
||||
> 决定在合并重复弹幕时是否忽略类型和颜色差异
|
||||
|
||||
</summary>
|
||||
|
||||
### merge_without_style
|
||||
|
||||
#### 功能说明
|
||||
|
||||
配合 `merge_tolerance` 使用。默认值: `no`(关闭)
|
||||
|
||||
当开启此选项时,即使属于不同位置类型(如顶部、底部、滚动)或不同颜色(无论色差多大),只要弹幕文本内容相同并且在 `merge_tolerance` 指定的时间容差范围内,就会被强制合并成一条弹幕。
|
||||
关闭时,仅当弹幕内容相同、位置类型相同、且颜色肉眼不可区分(色差极小)时,才会进行弹幕合并。
|
||||
|
||||
#### 使用方法
|
||||
|
||||
想要使用此选项,请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并自定义如下内容:
|
||||
|
||||
```
|
||||
merge_without_style=yes
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
max_screen_danmaku
|
||||
@@ -713,18 +772,24 @@ api_server
|
||||
|
||||
#### 功能说明
|
||||
|
||||
允许自定义弹幕 API 的服务地址
|
||||
允许自定义弹幕 API 的服务地址。默认使用项目维护的 `https://danmaku-api.152468.xyz`,由代理完成弹弹play API 鉴权,插件用户无需配置密钥。
|
||||
|
||||
可指定多个用逗号分隔的有序 api_server 列表。
|
||||
|
||||
支持每项使用 '|' 或 '#' 分隔备注,例如: "https://a.example.com|备用A" 或 "https://b.example.com#备用B"
|
||||
|
||||
> **⚠️NOTE!**
|
||||
>
|
||||
> 请确保自定义服务的 API 与弹弹play 的兼容,已知兼容:[misaka_danmu_server](https://github.com/l429609201/misaka_danmu_server),[danmu_api](https://github.com/huangxd-/danmu_api)
|
||||
>
|
||||
> 通过默认 API 代理访问弹弹play时,无需配置弹弹play的 AppId/AppSecret;如需使用个人申请的弹弹play AppId/AppSecret 凭据,可以自行部署服务端代理,并将 `api_server` 指向该代理。
|
||||
|
||||
#### 使用方法
|
||||
|
||||
想要使用此选项,请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并自定义如下内容:
|
||||
|
||||
```
|
||||
api_server=https://api.dandanplay.net
|
||||
api_server=https://danmaku-api.152468.xyz
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -813,7 +878,7 @@ user_agent
|
||||
|
||||
> **⚠️NOTE!**
|
||||
>
|
||||
> User-Agent格式必须符合弹弹play的标准,否则无法成功请求。具体格式要求见[弹弹play官方文档](https://github.com/kaedei/dandanplay-libraryindex/blob/master/api/OpenPlatform.md#5user-agent)
|
||||
> 使用默认 API 代理时无需为弹弹play鉴权而修改 User-Agent。直接接入弹弹play官方 API 或其他自定义服务时,请遵循对应服务的 User-Agent 要求。
|
||||
>
|
||||
> 若想提高URL播放的哈希匹配成功率,可以将此项设为 `mpv`或浏览器的User-Agent
|
||||
|
||||
@@ -1015,6 +1080,9 @@ blacklist_path=
|
||||
- `user-data/uosc_danmaku/has-danmaku`
|
||||
从`user-data/uosc_danmaku/has-danmaku`属性中可以获取到表示当前是否有弹幕在显示的布尔值,具体用法可以参考[此pr](https://github.com/Tony15246/uosc_danmaku/pull/276)
|
||||
|
||||
- `user-data/uosc_danmaku/danmaku-switch-on`
|
||||
从`user-data/uosc_danmaku/danmaku-switch-on`属性中可以获取到表示当前弹幕开关状态的布尔值,具体用法可以参考[此issue](https://github.com/Tony15246/uosc_danmaku/issues/362)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 来自弹弹play的弹幕源问题如何从根源进行调整解决
|
||||
@@ -1049,10 +1117,12 @@ blacklist_path=
|
||||
- 弹幕格式解析转换:[DanmakuConvert](https://github.com/timerring/DanmakuConvert)
|
||||
- 简繁转换:[OpenCC](https://github.com/BYVoid/OpenCC)
|
||||
- lua原生md5计算实现:https://github.com/rkscv/danmaku
|
||||
- lua原生zip解压缩实现:[lua-inflate](https://github.com/TohruMKDM/lua-inflate)
|
||||
- 爱优腾及芒果TV的弹幕解析参考:https://github.com/lyz05/danmaku
|
||||
- b站在线播放弹幕获取实现参考:[MPV-Play-BiliBili-Comments](https://github.com/itKelis/MPV-Play-BiliBili-Comments)
|
||||
- 巴哈姆特在线播放弹幕获取实现参考:[MPV-Play-BAHA-Comments](https://github.com/s594569321/MPV-Play-BAHA-Comments)
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [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)
|
||||
- ~~[Loukyuu1120/uosc_danmaku](https://github.com/Loukyuu1120/uosc_danmaku) 本项目的fork版本,实现了自定义多个 api_servers 与 弹幕来源选择菜单 功能,具体讨论请参阅 [#282](https://github.com/Tony15246/uosc_danmaku/issues/282)~~ 相关功能主仓库已实现
|
||||
@@ -21,47 +21,114 @@ end
|
||||
|
||||
-- 写入history.json
|
||||
-- 读取episodeId获取danmaku
|
||||
function set_episode_id(input, from_menu)
|
||||
function set_episode_id(input, from_menu, api_server)
|
||||
from_menu = from_menu or false
|
||||
DANMAKU.source = "dandanplay"
|
||||
local api_server = options.api_server
|
||||
local selected_server = api_server
|
||||
for url, source in pairs(DANMAKU.sources) do
|
||||
if source.from == "api_server" then
|
||||
if not source.from_history then
|
||||
DANMAKU.sources[url] = nil
|
||||
else
|
||||
DANMAKU.sources[url]["data"] = nil
|
||||
api_server = source.api_server or options.api_server
|
||||
end
|
||||
end
|
||||
end
|
||||
local episodeId = tonumber(input)
|
||||
|
||||
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)
|
||||
if not api_server then
|
||||
if DANMAKU.api_server ~= nil then
|
||||
selected_server = DANMAKU.api_server
|
||||
else
|
||||
local servers = get_api_server_list(options.api_server)
|
||||
if servers and #servers > 0 then
|
||||
selected_server = servers[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
DANMAKU.api_server = selected_server
|
||||
|
||||
local episodeId = tonumber(input)
|
||||
write_history(episodeId, selected_server)
|
||||
set_danmaku_button()
|
||||
fetch_danmaku(episodeId, from_menu, api_server)
|
||||
fetch_danmaku(episodeId, from_menu, selected_server)
|
||||
end
|
||||
|
||||
-- 回退使用额外的弹幕获取方式
|
||||
function get_danmaku_fallback(query)
|
||||
local url = options.fallback_server .. "/?ac=dm&url=" .. query
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local function do_fallback()
|
||||
if options.fallback_server == "" then return end
|
||||
local url = options.fallback_server .. "/?ac=dm&url=" .. query
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
if not args then return end
|
||||
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
|
||||
fetch_danmaku_data(args, function(data)
|
||||
if data ~= nil and data["xml"] ~= nil then
|
||||
if DANMAKU.sources[query] ~= nil then
|
||||
DANMAKU.sources[query]["data"] = data["xml"]
|
||||
else
|
||||
DANMAKU.sources[query] = {from = "user_custom", data = data["xml"]}
|
||||
end
|
||||
load_danmaku(true)
|
||||
return
|
||||
end
|
||||
|
||||
save_danmaku_data(data["comments"], query, "user_custom")
|
||||
load_danmaku(true)
|
||||
end)
|
||||
if not data or not data["comments"] or data["count"] <= 1 then
|
||||
msg.info("备用服务器无数据或返回格式不正确")
|
||||
show_message("备用服务器无数据或返回格式不正确", 3)
|
||||
return
|
||||
end
|
||||
|
||||
save_danmaku_data(data["comments"], query, "user_custom")
|
||||
load_danmaku(true)
|
||||
end)
|
||||
end
|
||||
|
||||
if query:find('bilibili.com') or query:find('bilivideo.c[nom]+') then
|
||||
load_danmaku_for_bilibili(query, function(success)
|
||||
if not success then do_fallback() end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if query:find('bahamut.akamaized.net') then
|
||||
load_danmaku_for_bahamut(query, function(success)
|
||||
if not success then do_fallback() end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if query:find('mgtv.com') then
|
||||
load_danmaku_for_mgtv(query, function(success)
|
||||
if not success then do_fallback() end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if query:find('iqiyi.com') then
|
||||
load_danmaku_for_iqiyi(query, function(success)
|
||||
if not success then do_fallback() end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if query:find('v.qq.com') then
|
||||
load_danmaku_for_tencent(query, function(success)
|
||||
if not success then do_fallback() end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if query:find('v.youku.com') then
|
||||
load_danmaku_for_youku(query, function(success)
|
||||
if not success then do_fallback() end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
do_fallback()
|
||||
end
|
||||
|
||||
-- 返回弹幕请求参数
|
||||
@@ -91,6 +158,8 @@ function make_danmaku_request_args(method, url, headers, body)
|
||||
table.insert(args, 'Content-Type: application/json')
|
||||
end
|
||||
|
||||
table.insert(args, '--compressed')
|
||||
|
||||
if url:find("api%.dandanplay%.") then
|
||||
local time = os.time()
|
||||
local appid = "UgjRIH45lE1BBLNmir1WKw=="
|
||||
@@ -153,8 +222,8 @@ local function normalize_danmaku_response(d)
|
||||
end
|
||||
|
||||
-- 尝试通过解析文件名匹配剧集
|
||||
local function match_episode(animeTitle, bangumiId, episode_num)
|
||||
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId
|
||||
local function match_episode(animeTitle, bangumiId, episode_num, api_server)
|
||||
local url = api_server .. "/api/v2/bangumi/" .. bangumiId
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
@@ -162,7 +231,6 @@ local function match_episode(animeTitle, bangumiId, episode_num)
|
||||
end
|
||||
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
@@ -180,7 +248,7 @@ local function match_episode(animeTitle, bangumiId, episode_num)
|
||||
if ep_num and ep_num == tonumber(episode_num) then
|
||||
DANMAKU.anime = animeTitle
|
||||
DANMAKU.episode = episode.episodeTitle
|
||||
set_episode_id(episode.episodeId)
|
||||
set_episode_id(episode.episodeId, nil, api_server)
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -188,9 +256,7 @@ local function match_episode(animeTitle, bangumiId, episode_num)
|
||||
end
|
||||
|
||||
local function match_anime()
|
||||
local animes = {}
|
||||
local anime_type = "tvseries"
|
||||
local type_count = 0
|
||||
local title, season_num, episode_num = parse_title()
|
||||
if not episode_num then
|
||||
msg.info("无法解析剧集信息")
|
||||
@@ -201,44 +267,49 @@ local function match_anime()
|
||||
anime_type = "ova"
|
||||
end
|
||||
|
||||
-- 并发在多个 api_server 上搜索,遇到第一个可接受的匹配就取消其余请求
|
||||
local encoded_query = url_encode(title)
|
||||
local url = options.api_server .. "/api/v2/search/anime"
|
||||
local params = "keyword=" .. encoded_query
|
||||
local full_url = url .. "?" .. params
|
||||
local args = make_danmaku_request_args("GET", full_url)
|
||||
local servers = get_api_server_list(options.api_server)
|
||||
|
||||
if not args then return end
|
||||
local matched = false
|
||||
local cancel_fn = nil
|
||||
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
local function build_args(server)
|
||||
local url = server .. "/api/v2/search/anime"
|
||||
local full_url = url .. "?keyword=" .. encoded_query
|
||||
return make_danmaku_request_args("GET", full_url)
|
||||
end
|
||||
|
||||
local function per_response(server, err, out)
|
||||
if matched then return end
|
||||
if err then
|
||||
msg.debug(("search anime failed for %s: %s"):format(server, tostring(err)))
|
||||
return
|
||||
end
|
||||
|
||||
local data = utils.parse_json(json)
|
||||
local data = utils.parse_json(out)
|
||||
if not data or not data.animes then
|
||||
msg.info("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
local local_candidates = {}
|
||||
for _, anime in ipairs(data.animes) do
|
||||
if anime.type == anime_type then
|
||||
type_count = type_count + 1
|
||||
table.insert(animes, anime)
|
||||
table.insert(local_candidates, anime)
|
||||
end
|
||||
end
|
||||
|
||||
if type_count == 1 then
|
||||
match_episode(animes[1].animeTitle, animes[1].bangumiId, episode_num)
|
||||
elseif type_count > 1 and season_num then
|
||||
if #local_candidates == 1 then
|
||||
matched = true
|
||||
local a = local_candidates[1]
|
||||
match_episode(a.animeTitle, a.bangumiId, episode_num, server)
|
||||
if cancel_fn then pcall(cancel_fn) end
|
||||
return
|
||||
end
|
||||
if #local_candidates > 1 and season_num then
|
||||
local best_match, best_score = nil, -1
|
||||
local target_title = title
|
||||
if tonumber(season_num) > 1 then
|
||||
target_title = title .. " 第" .. number_to_chinese(season_num) .. "季"
|
||||
end
|
||||
for _, anime in ipairs(animes) do
|
||||
for _, anime in ipairs(local_candidates) do
|
||||
local animeTitle = tostring(anime.animeTitle or "")
|
||||
animeTitle = animeTitle:gsub("^%s*(.-)%s*$", "%1")
|
||||
:gsub("%s*%(.-%)%s*$", "")
|
||||
@@ -253,17 +324,24 @@ local function match_anime()
|
||||
best_match = anime
|
||||
end
|
||||
end
|
||||
|
||||
if best_match and best_score >= 0.75 then
|
||||
matched = true
|
||||
msg.info(("模糊匹配选中: %s (score=%.2f)"):format(best_match.animeTitle, best_score))
|
||||
match_episode(best_match.animeTitle, best_match.bangumiId, episode_num)
|
||||
else
|
||||
msg.info("匹配到多个结果,但相似度不足,请手动搜索")
|
||||
match_episode(best_match.animeTitle, best_match.bangumiId, episode_num, server)
|
||||
if cancel_fn then pcall(cancel_fn) end
|
||||
return
|
||||
end
|
||||
else
|
||||
end
|
||||
-- 未找到可接受匹配,继续等待其他服务器的返回
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
if not matched then
|
||||
msg.info("没有找到合适的匹配结果")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
cancel_fn = parallel_requests(servers, build_args, per_response, final_cb, { concurrency = 5, per_request_timeout = 15 })
|
||||
end
|
||||
|
||||
-- 执行哈希匹配获取弹幕
|
||||
@@ -271,7 +349,7 @@ local function match_file(file_path, file_name, callback)
|
||||
-- 计算文件哈希
|
||||
local hash = nil
|
||||
local file_info = utils.file_info(file_path)
|
||||
if file_info and file_info.size > 16 * 1024 * 1024 then
|
||||
if file_info and file_info.size >= 16 * 1024 * 1024 then
|
||||
local file, error = io.open(normalize(file_path), 'rb')
|
||||
if file and not error then
|
||||
local m = MD5.new()
|
||||
@@ -300,50 +378,66 @@ local function match_file(file_path, file_name, callback)
|
||||
file_name = title
|
||||
end
|
||||
|
||||
local url = options.api_server .. "/api/v2/match"
|
||||
local args = make_danmaku_request_args("POST", url, {
|
||||
["Content-Type"] = "application/json"
|
||||
}, {
|
||||
local servers = get_api_server_list(options.api_server)
|
||||
|
||||
local matched = false
|
||||
local cancel_fn = nil
|
||||
|
||||
local function build_args(server)
|
||||
local url = server .. "/api/v2/match"
|
||||
return make_danmaku_request_args("POST", url, { ["Content-Type"] = "application/json" }, {
|
||||
fileName = file_name,
|
||||
fileHash = hash or "a1b2c3d4e5f67890abcd1234ef567890",
|
||||
matchMode = "hashAndFileName"
|
||||
}
|
||||
)
|
||||
})
|
||||
end
|
||||
|
||||
if not args then return end
|
||||
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
callback(error)
|
||||
local function per_response(server, err, out)
|
||||
if matched then return end
|
||||
if err then
|
||||
msg.debug(("match failed for %s: %s"):format(server, tostring(err)))
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
local data = utils.parse_json(out)
|
||||
if not data or not data.isMatched then
|
||||
callback("没有匹配的剧集")
|
||||
return
|
||||
end
|
||||
|
||||
matched = true
|
||||
DANMAKU.anime = data.matches[1].animeTitle
|
||||
DANMAKU.episode = data.matches[1].episodeTitle
|
||||
|
||||
-- 获取并加载弹幕数据
|
||||
set_episode_id(data.matches[1].episodeId)
|
||||
end)
|
||||
set_episode_id(data.matches[1].episodeId, nil, server)
|
||||
if cancel_fn then pcall(cancel_fn) end
|
||||
if callback then pcall(callback) end
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
if not matched then
|
||||
callback("没有匹配的剧集")
|
||||
end
|
||||
end
|
||||
|
||||
cancel_fn = parallel_requests(servers, build_args, per_response, final_cb, { concurrency = 5, per_request_timeout = 15 })
|
||||
end
|
||||
|
||||
-- 异步获取弹幕数据
|
||||
function fetch_danmaku_data(args, callback)
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("获取数据失败", 3)
|
||||
msg.error("HTTP 请求失败:" .. error)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
data = normalize_danmaku_response(data)
|
||||
if data ~= nil then
|
||||
data = normalize_danmaku_response(data)
|
||||
else
|
||||
local danmaku = parse_xml_danmaku(json)
|
||||
if #danmaku > 0 then
|
||||
data = {}
|
||||
data["xml"] = danmaku
|
||||
end
|
||||
end
|
||||
callback(data)
|
||||
end)
|
||||
end
|
||||
@@ -359,6 +453,26 @@ function save_danmaku_data(comments, query, danmaku_source)
|
||||
end
|
||||
end
|
||||
|
||||
function save_danmaku_xml(url, xml_string)
|
||||
local danmaku_list = parse_xml_danmaku(xml_string)
|
||||
|
||||
if DANMAKU.sources[url] ~= nil then
|
||||
DANMAKU.sources[url]["data"] = danmaku_list
|
||||
else
|
||||
DANMAKU.sources[url] = {from = "user_custom", data = danmaku_list}
|
||||
end
|
||||
end
|
||||
|
||||
function save_danmaku_json(url, json_string)
|
||||
local danmaku_list = parse_json_danmaku(json_string)
|
||||
|
||||
if DANMAKU.sources[url] ~= nil then
|
||||
DANMAKU.sources[url]["data"] = danmaku_list
|
||||
else
|
||||
DANMAKU.sources[url] = {from = "user_custom", 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
|
||||
@@ -371,39 +485,6 @@ function save_danmaku_downloaded(url, downloaded_file)
|
||||
end
|
||||
end
|
||||
|
||||
-- 处理弹幕数据
|
||||
function handle_danmaku_data(query, data, from_menu)
|
||||
-- 如果没有数据,进行重试
|
||||
if not data or not data["comments"] or data["count"] <= 1 then
|
||||
show_message("服务器无缓存数据,再次尝试请求", 10)
|
||||
msg.verbose("服务器无缓存数据,再次尝试请求")
|
||||
-- 等待 2 秒后重试
|
||||
local start = os.time()
|
||||
while os.time() - start < 2 do
|
||||
-- 空循环,等待 2 秒
|
||||
end
|
||||
-- 重新发起请求
|
||||
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(query)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(retry_data)
|
||||
if not retry_data or not retry_data["comments"] or retry_data["count"] <= 1 then
|
||||
get_danmaku_fallback(query)
|
||||
return
|
||||
end
|
||||
save_danmaku_data(retry_data["comments"], query, "user_custom")
|
||||
load_danmaku(from_menu)
|
||||
end)
|
||||
else
|
||||
save_danmaku_data(data["comments"], query, "user_custom")
|
||||
load_danmaku(from_menu)
|
||||
end
|
||||
end
|
||||
|
||||
-- 处理获取到的数据
|
||||
function handle_fetched_danmaku(data, url, from_menu)
|
||||
if data and data["comments"] then
|
||||
@@ -426,7 +507,7 @@ end
|
||||
-- 匹配弹幕库 comment, 仅匹配dandan本身弹幕库
|
||||
-- 通过danmaku api(url)+id获取弹幕
|
||||
function fetch_danmaku(episodeId, from_menu, api_server)
|
||||
local url = (api_server or options.api_server) .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
|
||||
local url = api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
|
||||
show_message("弹幕加载中...", 30)
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
@@ -499,18 +580,60 @@ end
|
||||
--通过输入源url获取弹幕库
|
||||
function add_danmaku_source_online(query, from_menu)
|
||||
set_danmaku_button()
|
||||
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(query)
|
||||
show_message("弹幕加载中...", 30)
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
msg.verbose("尝试获取弹幕:" .. query)
|
||||
|
||||
if args == nil then
|
||||
local servers = get_api_server_list(options.api_server)
|
||||
|
||||
-- 过滤掉指向 dandanplay.net 的服务器
|
||||
local filtered = {}
|
||||
for _, s in ipairs(servers) do
|
||||
if type(s) == "string" and not s:lower():find("dandanplay%.net") then
|
||||
table.insert(filtered, s)
|
||||
end
|
||||
end
|
||||
servers = filtered
|
||||
if #servers == 0 then
|
||||
get_danmaku_fallback(query)
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(data)
|
||||
handle_danmaku_data(query, data, from_menu)
|
||||
end)
|
||||
local matched = false
|
||||
local cancel_fn = nil
|
||||
|
||||
local function build_args(server)
|
||||
local url = server .. "/api/v2/extcomment?url=" .. url_encode(query)
|
||||
return make_danmaku_request_args("GET", url)
|
||||
end
|
||||
|
||||
local function per_response(server, err, out)
|
||||
if matched then return end
|
||||
if err then
|
||||
msg.debug(("extcomment failed for %s: %s"):format(server, tostring(err)))
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
data = normalize_danmaku_response(data)
|
||||
if not data or not data["comments"] or data["count"] <= 1 then
|
||||
return
|
||||
end
|
||||
matched = true
|
||||
-- 保存并加载弹幕
|
||||
save_danmaku_data(data["comments"], query, "user_custom")
|
||||
load_danmaku(from_menu)
|
||||
-- 取消其他未完成请求
|
||||
if cancel_fn then pcall(cancel_fn) end
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
if not matched then
|
||||
-- 所有服务器都未返回有效弹幕,回退到备用服务器
|
||||
msg.info("所有服务器均无有效弹幕,尝试备用服务器")
|
||||
get_danmaku_fallback(query)
|
||||
end
|
||||
end
|
||||
|
||||
cancel_fn = parallel_requests(servers, build_args, per_response, final_cb, { concurrency = 3, per_request_timeout = 30 })
|
||||
end
|
||||
|
||||
-- 将弹幕转换为 Lua table
|
||||
@@ -578,8 +701,6 @@ function get_danmaku_with_hash(file_name, file_path)
|
||||
end
|
||||
|
||||
call_cmd_async(arg, function(error)
|
||||
async_running = false
|
||||
|
||||
file_path = utils.join_path(DANMAKU_PATH, temp_file)
|
||||
|
||||
match_file(file_path, file_name, function(error)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
local utils = require 'mp.utils'
|
||||
local msg = require 'mp.msg'
|
||||
|
||||
local cached_series_playlinks = {}
|
||||
|
||||
local Source = {
|
||||
["b 站"] = "bilibili1",
|
||||
["芒果TV"] = "imgo",
|
||||
["腾讯"] = "qq",
|
||||
["爱奇艺"] = "qiyi",
|
||||
["优酷"] = "youku",
|
||||
@@ -83,6 +86,31 @@ local function query_tmdb(title, class, menu)
|
||||
end
|
||||
end
|
||||
|
||||
-- 从单个 seriesPlaylinks 项解析出有效的播放 URL
|
||||
local function extract_episode_url(item, playlink)
|
||||
if not item then return nil end
|
||||
if type(item) == 'string' then
|
||||
return playlink or item
|
||||
elseif type(item) == 'table' then
|
||||
return item.url or nil
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 将 seriesPlaylinks 转换为统一的 episode_rows 列表:{ index=string, url=string }
|
||||
local function build_episode_rows(seriesPlaylinks, playlink)
|
||||
if not seriesPlaylinks or type(seriesPlaylinks) ~= 'table' then return nil end
|
||||
local rows = {}
|
||||
for i, it in ipairs(seriesPlaylinks) do
|
||||
local url = extract_episode_url(it, playlink)
|
||||
if url and url ~= '' then
|
||||
table.insert(rows, { index = tostring(i), url = url })
|
||||
end
|
||||
end
|
||||
if #rows == 0 then return nil end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function get_number(cat, id, site)
|
||||
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&site=%s",
|
||||
cat, id, site)
|
||||
@@ -107,6 +135,53 @@ local function get_number(cat, id, site)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 使用 /v1/detail 分批获取集数(每批最多200集)
|
||||
local function get_episodes_v1(cat, id, site, number)
|
||||
if not number or tonumber(number) == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local batch_size = 200
|
||||
local start_idx = 1
|
||||
local episodes = {}
|
||||
while start_idx <= tonumber(number) do
|
||||
local end_idx = math.min(start_idx + batch_size - 1, tonumber(number))
|
||||
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&start=%s&end=%s&site=%s",
|
||||
cat, id, start_idx, end_idx, 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
|
||||
msg.error(string.format("Failed to fetch detail batch %d-%d: %s", start_idx, end_idx, res.stderr or "unknown"))
|
||||
if start_idx == 1 then
|
||||
return nil
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local result = utils.parse_json(res.stdout)
|
||||
if result and result.data and result.data.allepidetail and result.data.allepidetail[site] then
|
||||
for _, it in ipairs(result.data.allepidetail[site]) do
|
||||
table.insert(episodes, { index = tostring(it.playlink_num), url = it.url })
|
||||
end
|
||||
end
|
||||
|
||||
start_idx = end_idx + 1
|
||||
end
|
||||
|
||||
if #episodes == 0 then
|
||||
return nil
|
||||
end
|
||||
return episodes
|
||||
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))
|
||||
|
||||
@@ -121,22 +196,13 @@ local function get_episodes_v2(cat, id, site)
|
||||
})
|
||||
|
||||
if not res.status or res.status ~= 0 then
|
||||
msg.error("Failed to fetch episodesv2: " .. (res.stderr or "unknown error"))
|
||||
msg.warn("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)
|
||||
local parsed = utils.parse_json(res.stdout)
|
||||
if not parsed then
|
||||
msg.error("episodesv2: 解析返回失败: " .. (res.stdout or ""))
|
||||
msg.warn("episodesv2: 解析返回失败: " .. (res.stdout or ""))
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -144,15 +210,10 @@ local function get_episodes_v2(cat, id, site)
|
||||
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 })
|
||||
local rows = build_episode_rows(seriesHTML.seriesPlaylinks)
|
||||
if rows then
|
||||
for _, r in ipairs(rows) do
|
||||
table.insert(episodes, { index = tonumber(r.index), url = r.url })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -170,7 +231,7 @@ function get_details(class, id, site, title, year, number, episodenum)
|
||||
local menu_title = "剧集信息"
|
||||
local footnote = "使用 / 打开筛选"
|
||||
if uosc_available and not episodenum then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote, nil, nil, "spinner")
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
@@ -188,59 +249,57 @@ function get_details(class, id, site, title, year, number, episodenum)
|
||||
|
||||
local items = {}
|
||||
local episodes = nil
|
||||
local episode_rows = nil
|
||||
|
||||
-- 优先尝试使用搜索时缓存的 seriesPlaylinks(若存在且站点匹配)
|
||||
if cat == 2 or cat == 4 then
|
||||
episodes = get_episodes_v2(cat, id, site)
|
||||
local cid = tostring(id)
|
||||
local cached = cached_series_playlinks[cid]
|
||||
if cached and cached.seriesPlaylinks and cached.seriesSite and tostring(cached.seriesSite) == tostring(site) then
|
||||
local rows = build_episode_rows(cached.seriesPlaylinks, cached.playlink)
|
||||
if rows then
|
||||
episode_rows = rows
|
||||
end
|
||||
end
|
||||
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
|
||||
-- 若未命中缓存,则继续使用 episodesv2/v1 的原有流程
|
||||
if not episode_rows then
|
||||
if cat == 2 or cat == 4 then
|
||||
episodes = get_episodes_v2(cat, id, site)
|
||||
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:优先使用 episodesv2 返回的数据,否则使用 v1/detail
|
||||
if episodes then
|
||||
episode_rows = {}
|
||||
for _, it in ipairs(result.data.allepidetail[site]) do
|
||||
table.insert(episode_rows, { index = tostring(it.playlink_num), url = it.url })
|
||||
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
|
||||
|
||||
episode_rows = get_episodes_v1(cat, id, site, number)
|
||||
if not episode_rows or #episode_rows == 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
|
||||
end
|
||||
end
|
||||
@@ -325,6 +384,18 @@ local function search_query(query, class, menu)
|
||||
if result and result.data.longData and result.data.longData.rows then
|
||||
for _, item in ipairs(result.data.longData.rows) do
|
||||
if item.playlinks then
|
||||
-- 如果搜索结果中包含 seriesPlaylinks,则缓存它(使用 en_id 作为 key)
|
||||
if item.seriesPlaylinks and item.en_id then
|
||||
local playlink = nil
|
||||
if item.playlinks and item.seriesSite then
|
||||
playlink = item.playlinks[item.seriesSite]
|
||||
end
|
||||
cached_series_playlinks[tostring(item.en_id)] = {
|
||||
seriesPlaylinks = item.seriesPlaylinks,
|
||||
seriesSite = item.seriesSite,
|
||||
playlink = playlink,
|
||||
}
|
||||
end
|
||||
for source_name, source_id in pairs(Source) do
|
||||
if item.playlinks[source_id] then
|
||||
table.insert(items, {
|
||||
@@ -375,7 +446,7 @@ function query_extra(name, class)
|
||||
}
|
||||
menu.cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" }
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, name)
|
||||
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, name, "spinner")
|
||||
else
|
||||
show_message(message, 30)
|
||||
end
|
||||
|
||||
+74
-238
@@ -1,4 +1,4 @@
|
||||
VERSION = "2.0.0"
|
||||
VERSION = "2.2.0"
|
||||
|
||||
mp.commandv('script-message', 'uosc_danmaku-version', VERSION)
|
||||
|
||||
@@ -21,6 +21,13 @@ require("modules/update")
|
||||
require("apis/dandanplay")
|
||||
require('apis/extra')
|
||||
|
||||
require("sites/bilibili")
|
||||
require("sites/bahamut")
|
||||
require("sites/iqiyi")
|
||||
require("sites/mgtv")
|
||||
require("sites/tencentvideo")
|
||||
require("sites/youku")
|
||||
|
||||
DANMAKU_PATH = os.getenv("TEMP") or "/tmp/"
|
||||
HISTORY_PATH = mp.command_native({"expand-path", options.history_path})
|
||||
PID = utils.getpid()
|
||||
@@ -30,6 +37,10 @@ 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)
|
||||
DANMAKU_SWITCH_ON = string.format("user-data/%s/danmaku-switch-on", mp.get_script_name())
|
||||
mp.set_property_bool(DANMAKU_SWITCH_ON, false)
|
||||
DANMAKU_COUNT = string.format("user-data/%s/danmaku-count", mp.get_script_name())
|
||||
mp.set_property_native(DANMAKU_COUNT, 0)
|
||||
KEY = table_to_zero_indexed({
|
||||
0x00,0x01,0x02,0x03,0x04,
|
||||
0x05,0x06,0x07,0x08,0x09,
|
||||
@@ -92,7 +103,7 @@ end
|
||||
|
||||
function set_danmaku_button()
|
||||
if get_danmaku_visibility() then
|
||||
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
|
||||
toggle_danmaku_switch("on")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,51 +116,7 @@ function show_loaded(init)
|
||||
else
|
||||
show_message("弹幕加载成功,共计" .. #COMMENTS .. "条弹幕", 3)
|
||||
end
|
||||
end
|
||||
|
||||
local function get_cid()
|
||||
local cid, danmaku_id = nil, nil
|
||||
local tracks = mp.get_property_native("track-list")
|
||||
for _, track in ipairs(tracks) do
|
||||
if track["lang"] == "danmaku" then
|
||||
cid = track["external-filename"]:match("/(%d-)%.xml$")
|
||||
danmaku_id = track["id"]
|
||||
break
|
||||
end
|
||||
end
|
||||
return cid, danmaku_id
|
||||
end
|
||||
|
||||
local function extract_between_colons(input_string)
|
||||
local start_index = 0
|
||||
local end_index = 0
|
||||
local count = 0
|
||||
for i = 1, #input_string do
|
||||
if input_string:sub(i, i) == ":" then
|
||||
count = count + 1
|
||||
if count == 2 then
|
||||
start_index = i
|
||||
elseif count == 3 then
|
||||
end_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if start_index > 0 and end_index > 0 then
|
||||
return input_string:sub(start_index + 1, end_index - 1)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local function get_type_from_position(position)
|
||||
if position == 0 then
|
||||
return 1
|
||||
end
|
||||
if position == 1 then
|
||||
return 4
|
||||
end
|
||||
return 5
|
||||
mp.set_property_native(DANMAKU_COUNT, #COMMENTS)
|
||||
end
|
||||
|
||||
-- 获取指定时间的延迟
|
||||
@@ -287,10 +254,12 @@ local function set_danmaku_delay(dly, time, specific_source)
|
||||
end
|
||||
end
|
||||
|
||||
if dly == 0 then
|
||||
DELAY = 0
|
||||
else
|
||||
DELAY = DELAY + dly
|
||||
if not specific_source then
|
||||
if dly == 0 then
|
||||
DELAY = 0
|
||||
else
|
||||
DELAY = DELAY + dly
|
||||
end
|
||||
end
|
||||
|
||||
if ENABLED and COMMENTS ~= nil then
|
||||
@@ -310,8 +279,14 @@ local function set_danmaku_delay(dly, time, specific_source)
|
||||
rebuild_convert_timer = nil
|
||||
end)
|
||||
|
||||
show_message('设置弹幕延迟: ' .. string.format("%.1f", DELAY + 1e-10) .. ' s')
|
||||
mp.set_property_native(DELAY_PROPERTY, DELAY)
|
||||
if specific_source then
|
||||
local source = DANMAKU.sources[specific_source]
|
||||
local source_delay = get_delay_for_time(source and source.delay_segments, time or 0)
|
||||
show_message('设置弹幕源延迟: ' .. string.format("%.1f", source_delay + 1e-10) .. ' s')
|
||||
else
|
||||
show_message('设置弹幕延迟: ' .. string.format("%.1f", DELAY + 1e-10) .. ' s')
|
||||
mp.set_property_native(DELAY_PROPERTY, DELAY)
|
||||
end
|
||||
end
|
||||
|
||||
local function clear_source()
|
||||
@@ -430,11 +405,6 @@ 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
|
||||
@@ -460,6 +430,7 @@ function read_danmaku_source_record(path)
|
||||
|
||||
local history = utils.parse_json(history_json) or {}
|
||||
local record = history[path]
|
||||
|
||||
if not record or not record.sources then return end
|
||||
|
||||
local sources = record.sources
|
||||
@@ -489,7 +460,6 @@ function read_danmaku_source_record(path)
|
||||
blocked = blocked,
|
||||
delay_segments = delay_segments,
|
||||
from_history = true,
|
||||
api_server = data.api_server,
|
||||
}
|
||||
end
|
||||
else
|
||||
@@ -519,7 +489,6 @@ 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])
|
||||
@@ -532,14 +501,48 @@ function read_danmaku_source_record(path)
|
||||
end
|
||||
end
|
||||
|
||||
local function get_save_danmaku_output(path, filename)
|
||||
if not path or not filename then return nil end
|
||||
|
||||
local custom_dir = options.save_danmaku_path ~= "" and
|
||||
mp.command_native({"expand-path", options.save_danmaku_path}) or nil
|
||||
local is_url = is_protocol(path)
|
||||
local mode = options.save_danmaku_path_mode
|
||||
local dir = nil
|
||||
|
||||
if mode ~= "url" and mode ~= "all" then
|
||||
mode = "local"
|
||||
end
|
||||
|
||||
if custom_dir and (mode == "all" or (mode == "url" and is_url) or (mode == "local" and not is_url)) then
|
||||
local output_name = sanitize_filename(filename)
|
||||
if not is_url then
|
||||
dir = get_parent_directory(path)
|
||||
local _, parent_name = dir and utils.split_path(dir:sub(1, -2))
|
||||
if parent_name and parent_name ~= "" then
|
||||
output_name = sanitize_filename(parent_name .. "_" .. filename)
|
||||
end
|
||||
end
|
||||
return utils.join_path(custom_dir, output_name .. ".xml")
|
||||
end
|
||||
|
||||
if is_url then
|
||||
return nil
|
||||
end
|
||||
|
||||
dir = get_parent_directory(path)
|
||||
if not dir then
|
||||
return nil
|
||||
end
|
||||
return utils.join_path(dir, filename .. ".xml")
|
||||
end
|
||||
|
||||
-- 视频播放时保存弹幕
|
||||
function save_danmaku(not_forced)
|
||||
local path = mp.get_property("path")
|
||||
local dir = get_parent_directory(path) or ""
|
||||
local filename = mp.get_property('filename/no-ext')
|
||||
local danmaku_out = utils.join_path(dir, filename .. ".xml")
|
||||
-- 排除网络播放场景
|
||||
if not path or is_protocol(path) or (not file_exists(danmaku_out)
|
||||
local filename = is_protocol(path) and mp.get_property("media-title") or mp.get_property('filename/no-ext')
|
||||
local danmaku_out = get_save_danmaku_output(path, filename)
|
||||
if not danmaku_out or (not file_exists(danmaku_out)
|
||||
and not is_writable(danmaku_out)) then
|
||||
show_message("此弹幕文件不支持保存至本地")
|
||||
msg.warn("此弹幕文件不支持保存至本地")
|
||||
@@ -561,176 +564,6 @@ function load_danmaku(from_menu, no_osd)
|
||||
render_danmaku(from_menu, no_osd)
|
||||
end
|
||||
|
||||
-- 为 bilibli 网站的视频播放加载弹幕
|
||||
function load_danmaku_for_bilibili(path)
|
||||
local cid, danmaku_id = get_cid()
|
||||
if danmaku_id ~= nil then
|
||||
mp.commandv('sub-remove', danmaku_id)
|
||||
end
|
||||
|
||||
if cid == nil then
|
||||
cid = mp.get_opt('cid')
|
||||
if not cid then
|
||||
local patterns = {
|
||||
"bilivideo%.c[nom]+.*/resource/(%d+)%D+.*",
|
||||
"bilivideo%.c[nom]+.*/(%d+)-%d+-%d+%..*%?",
|
||||
}
|
||||
local urls = {
|
||||
path,
|
||||
mp.get_property("stream-open-filename", ''),
|
||||
}
|
||||
|
||||
for _, pattern in ipairs(patterns) do
|
||||
for _, url in ipairs(urls) do
|
||||
if url:find(pattern) then
|
||||
cid = url:match(pattern)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if cid == nil and path:match("/video/BV.-") then
|
||||
if path:match("video/BV.-/.*") then
|
||||
path = path:gsub("/[^/]+$", "")
|
||||
end
|
||||
add_danmaku_source_online(path, true)
|
||||
return
|
||||
end
|
||||
if cid ~= nil then
|
||||
local url = "https://comment.bilibili.com/" .. cid .. ".xml"
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
return
|
||||
end
|
||||
if file_exists(danmaku_xml) then
|
||||
save_danmaku_downloaded(path, danmaku_xml)
|
||||
load_danmaku(true)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- 为 bahamut 网站的视频播放加载弹幕
|
||||
function load_danmaku_for_bahamut(path)
|
||||
local path = path:gsub('%%(%x%x)', hex_to_char)
|
||||
local sn = extract_between_colons(path)
|
||||
if sn == nil then
|
||||
return
|
||||
end
|
||||
local url = "https://ani.gamer.com.tw/ajax/danmuGet.php"
|
||||
local temp_file = "bahamut-" .. PID .. ".json"
|
||||
local danmaku_json = utils.join_path(DANMAKU_PATH, temp_file)
|
||||
local arg = {
|
||||
"curl",
|
||||
"-X",
|
||||
"POST",
|
||||
"-d",
|
||||
"sn=" .. sn,
|
||||
"-L",
|
||||
"-s",
|
||||
"--user-agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
|
||||
"--header",
|
||||
"Origin: https://ani.gamer.com.tw",
|
||||
"--header",
|
||||
"Content-Type: application/x-www-form-urlencoded;charset=utf-8",
|
||||
"--header",
|
||||
"Accept: application/json",
|
||||
"--header",
|
||||
"Authority: ani.gamer.com.tw",
|
||||
"--output",
|
||||
danmaku_json,
|
||||
url,
|
||||
}
|
||||
|
||||
if options.proxy ~= "" then
|
||||
table.insert(arg, '-x')
|
||||
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
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
return
|
||||
end
|
||||
if not file_exists(danmaku_json) then
|
||||
url = "https://ani.gamer.com.tw/animeVideo.php?sn=" .. sn
|
||||
ENABLED = true
|
||||
add_danmaku_source_online(url, true)
|
||||
return
|
||||
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(final_json_str)
|
||||
json_file:close()
|
||||
end
|
||||
|
||||
if file_exists(json_filename) then
|
||||
save_danmaku_downloaded(
|
||||
"https://ani.gamer.com.tw/animeVideo.php?sn=" .. sn,
|
||||
json_filename)
|
||||
load_danmaku(true)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function load_danmaku_for_url(path)
|
||||
if path:find('bilibili.com') or path:find('bilivideo.c[nom]+') then
|
||||
load_danmaku_for_bilibili(path)
|
||||
@@ -776,6 +609,7 @@ function auto_load_danmaku(path, dir, filename, number)
|
||||
local history_id = history_dir.episodeId
|
||||
local history_fname = history_dir.fname
|
||||
local history_extra = history_dir.extra
|
||||
local history_api_server = history_dir.api_server
|
||||
local playing_number = nil
|
||||
|
||||
if history_fname then
|
||||
@@ -794,6 +628,7 @@ function auto_load_danmaku(path, dir, filename, number)
|
||||
if playing_number ~= nil then
|
||||
local x = playing_number - history_number --获取集数差值
|
||||
DANMAKU.episode = episode_number and string.format("第%s话", episode_number + x) or history_dir.episodeTitle
|
||||
DANMAKU.api_server = history_api_server or nil
|
||||
show_message("自动加载上次匹配的弹幕", 3)
|
||||
msg.verbose("自动加载上次匹配的弹幕")
|
||||
if history_id then
|
||||
@@ -881,7 +716,7 @@ mp.register_event("file-loaded", function()
|
||||
return
|
||||
end
|
||||
|
||||
if ENABLED and COMMENTS == nil and not async_running then
|
||||
if ENABLED and COMMENTS == nil and not is_async_running() then
|
||||
init(path)
|
||||
end
|
||||
end)
|
||||
@@ -914,7 +749,8 @@ 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")
|
||||
toggle_danmaku_switch("on")
|
||||
|
||||
if COMMENTS == nil then
|
||||
show_message("加载弹幕初始化...", 3)
|
||||
set_danmaku_visibility(true)
|
||||
@@ -926,7 +762,7 @@ mp.register_script_message("show_danmaku_keyboard", function()
|
||||
end
|
||||
else
|
||||
show_message("关闭弹幕", 2)
|
||||
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "off")
|
||||
toggle_danmaku_switch("off")
|
||||
hide_danmaku_func()
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -1,491 +1,491 @@
|
||||
-- modified from https://github.com/idiomic/Lua_AES
|
||||
--[[
|
||||
Copyright 2019 Tyler Richard Hoyer
|
||||
Copyright 2025 dyphire
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
]]
|
||||
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local GF8x2 = {
|
||||
[0]=0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,
|
||||
0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,
|
||||
0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,
|
||||
0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,
|
||||
0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,
|
||||
0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,
|
||||
0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,
|
||||
0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,
|
||||
0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,
|
||||
0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,
|
||||
0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,
|
||||
0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,
|
||||
0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,
|
||||
0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,
|
||||
0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,
|
||||
0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5
|
||||
}
|
||||
|
||||
local GF8x3 = {
|
||||
[0]=0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,
|
||||
0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,
|
||||
0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,
|
||||
0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,
|
||||
0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,
|
||||
0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,
|
||||
0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,
|
||||
0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,
|
||||
0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,
|
||||
0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,
|
||||
0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,
|
||||
0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,
|
||||
0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,
|
||||
0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,
|
||||
0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,
|
||||
0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a
|
||||
}
|
||||
|
||||
local GF8x9 = {
|
||||
[0]=0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77,
|
||||
0x90,0x99,0x82,0x8b,0xb4,0xbd,0xa6,0xaf,0xd8,0xd1,0xca,0xc3,0xfc,0xf5,0xee,0xe7,
|
||||
0x3b,0x32,0x29,0x20,0x1f,0x16,0x0d,0x04,0x73,0x7a,0x61,0x68,0x57,0x5e,0x45,0x4c,
|
||||
0xab,0xa2,0xb9,0xb0,0x8f,0x86,0x9d,0x94,0xe3,0xea,0xf1,0xf8,0xc7,0xce,0xd5,0xdc,
|
||||
0x76,0x7f,0x64,0x6d,0x52,0x5b,0x40,0x49,0x3e,0x37,0x2c,0x25,0x1a,0x13,0x08,0x01,
|
||||
0xe6,0xef,0xf4,0xfd,0xc2,0xcb,0xd0,0xd9,0xae,0xa7,0xbc,0xb5,0x8a,0x83,0x98,0x91,
|
||||
0x4d,0x44,0x5f,0x56,0x69,0x60,0x7b,0x72,0x05,0x0c,0x17,0x1e,0x21,0x28,0x33,0x3a,
|
||||
0xdd,0xd4,0xcf,0xc6,0xf9,0xf0,0xeb,0xe2,0x95,0x9c,0x87,0x8e,0xb1,0xb8,0xa3,0xaa,
|
||||
0xec,0xe5,0xfe,0xf7,0xc8,0xc1,0xda,0xd3,0xa4,0xad,0xb6,0xbf,0x80,0x89,0x92,0x9b,
|
||||
0x7c,0x75,0x6e,0x67,0x58,0x51,0x4a,0x43,0x34,0x3d,0x26,0x2f,0x10,0x19,0x02,0x0b,
|
||||
0xd7,0xde,0xc5,0xcc,0xf3,0xfa,0xe1,0xe8,0x9f,0x96,0x8d,0x84,0xbb,0xb2,0xa9,0xa0,
|
||||
0x47,0x4e,0x55,0x5c,0x63,0x6a,0x71,0x78,0x0f,0x06,0x1d,0x14,0x2b,0x22,0x39,0x30,
|
||||
0x9a,0x93,0x88,0x81,0xbe,0xb7,0xac,0xa5,0xd2,0xdb,0xc0,0xc9,0xf6,0xff,0xe4,0xed,
|
||||
0x0a,0x03,0x18,0x11,0x2e,0x27,0x3c,0x35,0x42,0x4b,0x50,0x59,0x66,0x6f,0x74,0x7d,
|
||||
0xa1,0xa8,0xb3,0xba,0x85,0x8c,0x97,0x9e,0xe9,0xe0,0xfb,0xf2,0xcd,0xc4,0xdf,0xd6,
|
||||
0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46
|
||||
}
|
||||
|
||||
local GF8x11 = {
|
||||
[0]=0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69,
|
||||
0xb0,0xbb,0xa6,0xad,0x9c,0x97,0x8a,0x81,0xe8,0xe3,0xfe,0xf5,0xc4,0xcf,0xd2,0xd9,
|
||||
0x7b,0x70,0x6d,0x66,0x57,0x5c,0x41,0x4a,0x23,0x28,0x35,0x3e,0x0f,0x04,0x19,0x12,
|
||||
0xcb,0xc0,0xdd,0xd6,0xe7,0xec,0xf1,0xfa,0x93,0x98,0x85,0x8e,0xbf,0xb4,0xa9,0xa2,
|
||||
0xf6,0xfd,0xe0,0xeb,0xda,0xd1,0xcc,0xc7,0xae,0xa5,0xb8,0xb3,0x82,0x89,0x94,0x9f,
|
||||
0x46,0x4d,0x50,0x5b,0x6a,0x61,0x7c,0x77,0x1e,0x15,0x08,0x03,0x32,0x39,0x24,0x2f,
|
||||
0x8d,0x86,0x9b,0x90,0xa1,0xaa,0xb7,0xbc,0xd5,0xde,0xc3,0xc8,0xf9,0xf2,0xef,0xe4,
|
||||
0x3d,0x36,0x2b,0x20,0x11,0x1a,0x07,0x0c,0x65,0x6e,0x73,0x78,0x49,0x42,0x5f,0x54,
|
||||
0xf7,0xfc,0xe1,0xea,0xdb,0xd0,0xcd,0xc6,0xaf,0xa4,0xb9,0xb2,0x83,0x88,0x95,0x9e,
|
||||
0x47,0x4c,0x51,0x5a,0x6b,0x60,0x7d,0x76,0x1f,0x14,0x09,0x02,0x33,0x38,0x25,0x2e,
|
||||
0x8c,0x87,0x9a,0x91,0xa0,0xab,0xb6,0xbd,0xd4,0xdf,0xc2,0xc9,0xf8,0xf3,0xee,0xe5,
|
||||
0x3c,0x37,0x2a,0x21,0x10,0x1b,0x06,0x0d,0x64,0x6f,0x72,0x79,0x48,0x43,0x5e,0x55,
|
||||
0x01,0x0a,0x17,0x1c,0x2d,0x26,0x3b,0x30,0x59,0x52,0x4f,0x44,0x75,0x7e,0x63,0x68,
|
||||
0xb1,0xba,0xa7,0xac,0x9d,0x96,0x8b,0x80,0xe9,0xe2,0xff,0xf4,0xc5,0xce,0xd3,0xd8,
|
||||
0x7a,0x71,0x6c,0x67,0x56,0x5d,0x40,0x4b,0x22,0x29,0x34,0x3f,0x0e,0x05,0x18,0x13,
|
||||
0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3
|
||||
}
|
||||
|
||||
local GF8x13 = {
|
||||
[0]=0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b,
|
||||
0xd0,0xdd,0xca,0xc7,0xe4,0xe9,0xfe,0xf3,0xb8,0xb5,0xa2,0xaf,0x8c,0x81,0x96,0x9b,
|
||||
0xbb,0xb6,0xa1,0xac,0x8f,0x82,0x95,0x98,0xd3,0xde,0xc9,0xc4,0xe7,0xea,0xfd,0xf0,
|
||||
0x6b,0x66,0x71,0x7c,0x5f,0x52,0x45,0x48,0x03,0x0e,0x19,0x14,0x37,0x3a,0x2d,0x20,
|
||||
0x6d,0x60,0x77,0x7a,0x59,0x54,0x43,0x4e,0x05,0x08,0x1f,0x12,0x31,0x3c,0x2b,0x26,
|
||||
0xbd,0xb0,0xa7,0xaa,0x89,0x84,0x93,0x9e,0xd5,0xd8,0xcf,0xc2,0xe1,0xec,0xfb,0xf6,
|
||||
0xd6,0xdb,0xcc,0xc1,0xe2,0xef,0xf8,0xf5,0xbe,0xb3,0xa4,0xa9,0x8a,0x87,0x90,0x9d,
|
||||
0x06,0x0b,0x1c,0x11,0x32,0x3f,0x28,0x25,0x6e,0x63,0x74,0x79,0x5a,0x57,0x40,0x4d,
|
||||
0xda,0xd7,0xc0,0xcd,0xee,0xe3,0xf4,0xf9,0xb2,0xbf,0xa8,0xa5,0x86,0x8b,0x9c,0x91,
|
||||
0x0a,0x07,0x10,0x1d,0x3e,0x33,0x24,0x29,0x62,0x6f,0x78,0x75,0x56,0x5b,0x4c,0x41,
|
||||
0x61,0x6c,0x7b,0x76,0x55,0x58,0x4f,0x42,0x09,0x04,0x13,0x1e,0x3d,0x30,0x27,0x2a,
|
||||
0xb1,0xbc,0xab,0xa6,0x85,0x88,0x9f,0x92,0xd9,0xd4,0xc3,0xce,0xed,0xe0,0xf7,0xfa,
|
||||
0xb7,0xba,0xad,0xa0,0x83,0x8e,0x99,0x94,0xdf,0xd2,0xc5,0xc8,0xeb,0xe6,0xf1,0xfc,
|
||||
0x67,0x6a,0x7d,0x70,0x53,0x5e,0x49,0x44,0x0f,0x02,0x15,0x18,0x3b,0x36,0x21,0x2c,
|
||||
0x0c,0x01,0x16,0x1b,0x38,0x35,0x22,0x2f,0x64,0x69,0x7e,0x73,0x50,0x5d,0x4a,0x47,
|
||||
0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97
|
||||
}
|
||||
|
||||
local GF8x14 = {
|
||||
[0]=0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a,
|
||||
0xe0,0xee,0xfc,0xf2,0xd8,0xd6,0xc4,0xca,0x90,0x9e,0x8c,0x82,0xa8,0xa6,0xb4,0xba,
|
||||
0xdb,0xd5,0xc7,0xc9,0xe3,0xed,0xff,0xf1,0xab,0xa5,0xb7,0xb9,0x93,0x9d,0x8f,0x81,
|
||||
0x3b,0x35,0x27,0x29,0x03,0x0d,0x1f,0x11,0x4b,0x45,0x57,0x59,0x73,0x7d,0x6f,0x61,
|
||||
0xad,0xa3,0xb1,0xbf,0x95,0x9b,0x89,0x87,0xdd,0xd3,0xc1,0xcf,0xe5,0xeb,0xf9,0xf7,
|
||||
0x4d,0x43,0x51,0x5f,0x75,0x7b,0x69,0x67,0x3d,0x33,0x21,0x2f,0x05,0x0b,0x19,0x17,
|
||||
0x76,0x78,0x6a,0x64,0x4e,0x40,0x52,0x5c,0x06,0x08,0x1a,0x14,0x3e,0x30,0x22,0x2c,
|
||||
0x96,0x98,0x8a,0x84,0xae,0xa0,0xb2,0xbc,0xe6,0xe8,0xfa,0xf4,0xde,0xd0,0xc2,0xcc,
|
||||
0x41,0x4f,0x5d,0x53,0x79,0x77,0x65,0x6b,0x31,0x3f,0x2d,0x23,0x09,0x07,0x15,0x1b,
|
||||
0xa1,0xaf,0xbd,0xb3,0x99,0x97,0x85,0x8b,0xd1,0xdf,0xcd,0xc3,0xe9,0xe7,0xf5,0xfb,
|
||||
0x9a,0x94,0x86,0x88,0xa2,0xac,0xbe,0xb0,0xea,0xe4,0xf6,0xf8,0xd2,0xdc,0xce,0xc0,
|
||||
0x7a,0x74,0x66,0x68,0x42,0x4c,0x5e,0x50,0x0a,0x04,0x16,0x18,0x32,0x3c,0x2e,0x20,
|
||||
0xec,0xe2,0xf0,0xfe,0xd4,0xda,0xc8,0xc6,0x9c,0x92,0x80,0x8e,0xa4,0xaa,0xb8,0xb6,
|
||||
0x0c,0x02,0x10,0x1e,0x34,0x3a,0x28,0x26,0x7c,0x72,0x60,0x6e,0x44,0x4a,0x58,0x56,
|
||||
0x37,0x39,0x2b,0x25,0x0f,0x01,0x13,0x1d,0x47,0x49,0x5b,0x55,0x7f,0x71,0x63,0x6d,
|
||||
0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d
|
||||
}
|
||||
|
||||
local s = {
|
||||
[0]=0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76,
|
||||
0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0,
|
||||
0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15,
|
||||
0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75,
|
||||
0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84,
|
||||
0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF,
|
||||
0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8,
|
||||
0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2,
|
||||
0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73,
|
||||
0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB,
|
||||
0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79,
|
||||
0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08,
|
||||
0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A,
|
||||
0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E,
|
||||
0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF,
|
||||
0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16
|
||||
}
|
||||
|
||||
local si = {
|
||||
[0]=0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB,
|
||||
0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB,
|
||||
0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E,
|
||||
0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25,
|
||||
0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92,
|
||||
0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84,
|
||||
0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06,
|
||||
0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B,
|
||||
0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73,
|
||||
0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E,
|
||||
0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B,
|
||||
0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4,
|
||||
0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F,
|
||||
0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF,
|
||||
0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61,
|
||||
0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D
|
||||
}
|
||||
|
||||
local rcon = {
|
||||
0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,
|
||||
0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,
|
||||
0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,
|
||||
0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,
|
||||
0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,
|
||||
0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,
|
||||
0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,
|
||||
0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,
|
||||
0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,
|
||||
0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,
|
||||
0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,
|
||||
0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,
|
||||
0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,
|
||||
0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,
|
||||
0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,
|
||||
0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d
|
||||
}
|
||||
|
||||
local xor4 = {
|
||||
[0]=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
|
||||
1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,
|
||||
2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,
|
||||
3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,
|
||||
4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,
|
||||
5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,
|
||||
6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,
|
||||
7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,
|
||||
8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
|
||||
9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,
|
||||
10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,
|
||||
11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,
|
||||
12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,
|
||||
13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,
|
||||
14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,
|
||||
15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
|
||||
}
|
||||
|
||||
local function xor8(a, b)
|
||||
local al = a % 16
|
||||
local bl = b % 16
|
||||
return 16 * xor4[a - al + (b - bl) / 16] + xor4[16 * al + bl]
|
||||
end
|
||||
|
||||
local function xor_blocks_8(a, b)
|
||||
local res = {}
|
||||
for i = 1, 16 do
|
||||
res[i] = xor8(a[i], b[i])
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function addRoundKey(state, key)
|
||||
for i, byte in next, state do
|
||||
state[i] = xor8(byte, key[i])
|
||||
end
|
||||
end
|
||||
|
||||
local function subBytes(state, s_box)
|
||||
for i, byte in next, state do
|
||||
state[i] = s_box[byte]
|
||||
end
|
||||
end
|
||||
|
||||
local function shiftRows(state)
|
||||
state[2], state[6], state[10], state[14] =
|
||||
state[6], state[10], state[14], state[2]
|
||||
|
||||
state[3], state[7], state[11], state[15] =
|
||||
state[11], state[15], state[3], state[7]
|
||||
|
||||
state[4], state[8], state[12], state[16] =
|
||||
state[16], state[4], state[8], state[12]
|
||||
end
|
||||
|
||||
local function inv_shiftRows(state)
|
||||
state[2], state[6], state[10], state[14] =
|
||||
state[14], state[2], state[6], state[10]
|
||||
|
||||
state[3], state[7], state[11], state[15] =
|
||||
state[11], state[15], state[3], state[7]
|
||||
|
||||
state[4], state[8], state[12], state[16] =
|
||||
state[8], state[12], state[16], state[4]
|
||||
end
|
||||
|
||||
local function mixColumns(state)
|
||||
for i = 0, 3 do
|
||||
local cur = i*4+1
|
||||
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
|
||||
state[cur + 0] = xor8(xor8(xor8(GF8x2[a], GF8x3[b]), c), d)
|
||||
state[cur + 1] = xor8(xor8(xor8(a, GF8x2[b]), GF8x3[c]), d)
|
||||
state[cur + 2] = xor8(xor8(xor8(a, b), GF8x2[c]), GF8x3[d])
|
||||
state[cur + 3] = xor8(xor8(xor8(GF8x3[a], b), c), GF8x2[d])
|
||||
end
|
||||
end
|
||||
|
||||
local function inv_mixColumns(state) -- TODO: fix
|
||||
for i = 0, 3 do
|
||||
local cur = i*4+1
|
||||
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
|
||||
state[cur + 0] = xor8(xor8(xor8(GF8x14[a], GF8x11[b]), GF8x13[c]), GF8x9[d])
|
||||
state[cur + 1] = xor8(xor8(xor8(GF8x9[a], GF8x14[b]), GF8x11[c]), GF8x13[d])
|
||||
state[cur + 2] = xor8(xor8(xor8(GF8x13[a], GF8x9[b]), GF8x14[c]), GF8x11[d])
|
||||
state[cur + 3] = xor8(xor8(xor8(GF8x11[a], GF8x13[b]), GF8x9[c]), GF8x14[d])
|
||||
end
|
||||
end
|
||||
|
||||
-- 256-bit key constants
|
||||
local n = 32 -- number of bytes in the 256-bit encryption key
|
||||
local b = 240 -- number of bytes in 15 128-bit round keys
|
||||
local function schedule256(key)
|
||||
local expanded = {}
|
||||
for c = 0, n-1 do
|
||||
expanded[c] = key[c]
|
||||
end
|
||||
|
||||
local i = 1
|
||||
local c = n
|
||||
local t1, t2, t3, t4 --t
|
||||
while c < b do
|
||||
t1 = expanded[c-4]
|
||||
t2 = expanded[c-3]
|
||||
t3 = expanded[c-2]
|
||||
t4 = expanded[c-1]
|
||||
|
||||
if (c % n == 0) then
|
||||
t1, t2, t3, t4 = xor8(rcon[i+1], s[t2]), s[t3], s[t4], s[t1]
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
if (c % n == 16) then
|
||||
t1 = s[t1]
|
||||
t2 = s[t2]
|
||||
t3 = s[t3]
|
||||
t4 = s[t4]
|
||||
end
|
||||
|
||||
t1 = xor8(t1, expanded[c - n])
|
||||
expanded[c] = t1
|
||||
c = c + 1
|
||||
|
||||
t2 = xor8(t2, expanded[c - n])
|
||||
expanded[c] = t2
|
||||
c = c + 1
|
||||
|
||||
t3 = xor8(t3, expanded[c - n])
|
||||
expanded[c] = t3
|
||||
c = c + 1
|
||||
|
||||
t4 = xor8(t4, expanded[c - n])
|
||||
expanded[c] = t4
|
||||
c = c + 1
|
||||
end
|
||||
|
||||
local roundKeys = {}
|
||||
for round = 0, 14 do
|
||||
local roundKey = {}
|
||||
for byte = 0, 15 do
|
||||
roundKey[byte+1] = expanded[round * 16 + byte]
|
||||
end
|
||||
roundKeys[round] = roundKey
|
||||
end
|
||||
return roundKeys
|
||||
end
|
||||
|
||||
local function chunks(text, i)
|
||||
local first = i * 16 + 1
|
||||
if first > #text then
|
||||
return
|
||||
end
|
||||
i = i + 1
|
||||
|
||||
local chunk = {text:byte(first, first + 15)}
|
||||
for j = #chunk + 1, 16 do
|
||||
chunk[j] = 0
|
||||
end
|
||||
|
||||
return i, chunk
|
||||
end
|
||||
|
||||
local function pkcs7_unpad(str)
|
||||
local len = #str
|
||||
if len == 0 then return str end
|
||||
|
||||
local pad_len = string.byte(str, len)
|
||||
if pad_len < 1 or pad_len > 16 then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = len - pad_len + 1, len do
|
||||
if string.byte(str, i) ~= pad_len then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return string.sub(str, 1, len - pad_len)
|
||||
end
|
||||
|
||||
local function zero_unpad(str)
|
||||
local len = #str
|
||||
while len > 0 and string.byte(str, len) == 0 do
|
||||
len = len - 1
|
||||
end
|
||||
return string.sub(str, 1, len)
|
||||
end
|
||||
|
||||
local function unpad(str)
|
||||
local unpadded = pkcs7_unpad(str)
|
||||
if unpadded then
|
||||
return unpadded
|
||||
else
|
||||
return zero_unpad(str)
|
||||
end
|
||||
end
|
||||
|
||||
local function encrypt(state, roundKeys)
|
||||
addRoundKey(state, roundKeys[0])
|
||||
for round = 1, 13 do
|
||||
subBytes(state, s)
|
||||
shiftRows(state)
|
||||
mixColumns(state)
|
||||
addRoundKey(state, roundKeys[round])
|
||||
end
|
||||
subBytes(state, s)
|
||||
shiftRows(state)
|
||||
addRoundKey(state, roundKeys[14])
|
||||
end
|
||||
|
||||
local function decrypt(state, roundKeys)
|
||||
addRoundKey(state, roundKeys[14])
|
||||
inv_shiftRows(state)
|
||||
subBytes(state, si)
|
||||
for round = 13, 1, -1 do
|
||||
addRoundKey(state, roundKeys[round])
|
||||
inv_mixColumns(state)
|
||||
inv_shiftRows(state)
|
||||
subBytes(state, si)
|
||||
end
|
||||
addRoundKey(state, roundKeys[0])
|
||||
end
|
||||
|
||||
local function ECB_encrypt(key, originaltext)
|
||||
local text = {}
|
||||
local roundKeys = schedule256(key)
|
||||
local i = 0
|
||||
while true do
|
||||
i, state = chunks(originaltext, i)
|
||||
if not state then break end
|
||||
encrypt(state, roundKeys)
|
||||
text[i] = string.char(unpack(state))
|
||||
end
|
||||
return table.concat(text)
|
||||
end
|
||||
|
||||
local function ECB_decrypt(key, ciphertext)
|
||||
local text = {}
|
||||
local roundKeys = schedule256(key)
|
||||
local i = 0
|
||||
while true do
|
||||
i, state = chunks(ciphertext, i)
|
||||
if not state then break end
|
||||
decrypt(state, roundKeys)
|
||||
text[i] = string.char(unpack(state))
|
||||
end
|
||||
return unpad(table.concat(text))
|
||||
end
|
||||
|
||||
local function CBC_encrypt(key, iv, originaltext)
|
||||
local roundKeys = schedule256(key)
|
||||
local text = {}
|
||||
local prev_block = {unpack(iv)}
|
||||
local i = 0
|
||||
|
||||
while true do
|
||||
i, block = chunks(originaltext, i)
|
||||
if not block then break end
|
||||
|
||||
local xored = xor_blocks_8(block, prev_block)
|
||||
encrypt(xored, roundKeys)
|
||||
text[i] = string.char(unpack(xored))
|
||||
prev_block = xored
|
||||
end
|
||||
|
||||
return table.concat(text)
|
||||
end
|
||||
|
||||
local function CBC_decrypt(key, iv, ciphertext)
|
||||
local roundKeys = schedule256(key)
|
||||
local text = {}
|
||||
local prev_block = {unpack(iv)}
|
||||
local i = 0
|
||||
|
||||
while true do
|
||||
i, block = chunks(ciphertext, i)
|
||||
if not block then break end
|
||||
|
||||
local decrypted = {unpack(block)}
|
||||
|
||||
decrypt(decrypted, roundKeys)
|
||||
local plain_block = xor_blocks_8(decrypted, prev_block)
|
||||
text[i] = string.char(unpack(plain_block))
|
||||
prev_block = block
|
||||
end
|
||||
|
||||
local result = table.concat(text)
|
||||
return unpad(result)
|
||||
end
|
||||
|
||||
return {
|
||||
ECB = {
|
||||
encrypt = ECB_encrypt;
|
||||
decrypt = ECB_decrypt;
|
||||
};
|
||||
CBC = {
|
||||
encrypt = CBC_encrypt;
|
||||
decrypt = CBC_decrypt;
|
||||
};
|
||||
-- modified from https://github.com/idiomic/Lua_AES
|
||||
--[[
|
||||
Copyright 2019 Tyler Richard Hoyer
|
||||
Copyright 2025 dyphire
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
]]
|
||||
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local GF8x2 = {
|
||||
[0]=0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,
|
||||
0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,
|
||||
0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,
|
||||
0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,
|
||||
0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,
|
||||
0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,
|
||||
0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,
|
||||
0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,
|
||||
0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,
|
||||
0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,
|
||||
0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,
|
||||
0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,
|
||||
0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,
|
||||
0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,
|
||||
0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,
|
||||
0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5
|
||||
}
|
||||
|
||||
local GF8x3 = {
|
||||
[0]=0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,
|
||||
0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,
|
||||
0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,
|
||||
0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,
|
||||
0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,
|
||||
0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,
|
||||
0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,
|
||||
0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,
|
||||
0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,
|
||||
0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,
|
||||
0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,
|
||||
0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,
|
||||
0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,
|
||||
0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,
|
||||
0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,
|
||||
0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a
|
||||
}
|
||||
|
||||
local GF8x9 = {
|
||||
[0]=0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77,
|
||||
0x90,0x99,0x82,0x8b,0xb4,0xbd,0xa6,0xaf,0xd8,0xd1,0xca,0xc3,0xfc,0xf5,0xee,0xe7,
|
||||
0x3b,0x32,0x29,0x20,0x1f,0x16,0x0d,0x04,0x73,0x7a,0x61,0x68,0x57,0x5e,0x45,0x4c,
|
||||
0xab,0xa2,0xb9,0xb0,0x8f,0x86,0x9d,0x94,0xe3,0xea,0xf1,0xf8,0xc7,0xce,0xd5,0xdc,
|
||||
0x76,0x7f,0x64,0x6d,0x52,0x5b,0x40,0x49,0x3e,0x37,0x2c,0x25,0x1a,0x13,0x08,0x01,
|
||||
0xe6,0xef,0xf4,0xfd,0xc2,0xcb,0xd0,0xd9,0xae,0xa7,0xbc,0xb5,0x8a,0x83,0x98,0x91,
|
||||
0x4d,0x44,0x5f,0x56,0x69,0x60,0x7b,0x72,0x05,0x0c,0x17,0x1e,0x21,0x28,0x33,0x3a,
|
||||
0xdd,0xd4,0xcf,0xc6,0xf9,0xf0,0xeb,0xe2,0x95,0x9c,0x87,0x8e,0xb1,0xb8,0xa3,0xaa,
|
||||
0xec,0xe5,0xfe,0xf7,0xc8,0xc1,0xda,0xd3,0xa4,0xad,0xb6,0xbf,0x80,0x89,0x92,0x9b,
|
||||
0x7c,0x75,0x6e,0x67,0x58,0x51,0x4a,0x43,0x34,0x3d,0x26,0x2f,0x10,0x19,0x02,0x0b,
|
||||
0xd7,0xde,0xc5,0xcc,0xf3,0xfa,0xe1,0xe8,0x9f,0x96,0x8d,0x84,0xbb,0xb2,0xa9,0xa0,
|
||||
0x47,0x4e,0x55,0x5c,0x63,0x6a,0x71,0x78,0x0f,0x06,0x1d,0x14,0x2b,0x22,0x39,0x30,
|
||||
0x9a,0x93,0x88,0x81,0xbe,0xb7,0xac,0xa5,0xd2,0xdb,0xc0,0xc9,0xf6,0xff,0xe4,0xed,
|
||||
0x0a,0x03,0x18,0x11,0x2e,0x27,0x3c,0x35,0x42,0x4b,0x50,0x59,0x66,0x6f,0x74,0x7d,
|
||||
0xa1,0xa8,0xb3,0xba,0x85,0x8c,0x97,0x9e,0xe9,0xe0,0xfb,0xf2,0xcd,0xc4,0xdf,0xd6,
|
||||
0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46
|
||||
}
|
||||
|
||||
local GF8x11 = {
|
||||
[0]=0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69,
|
||||
0xb0,0xbb,0xa6,0xad,0x9c,0x97,0x8a,0x81,0xe8,0xe3,0xfe,0xf5,0xc4,0xcf,0xd2,0xd9,
|
||||
0x7b,0x70,0x6d,0x66,0x57,0x5c,0x41,0x4a,0x23,0x28,0x35,0x3e,0x0f,0x04,0x19,0x12,
|
||||
0xcb,0xc0,0xdd,0xd6,0xe7,0xec,0xf1,0xfa,0x93,0x98,0x85,0x8e,0xbf,0xb4,0xa9,0xa2,
|
||||
0xf6,0xfd,0xe0,0xeb,0xda,0xd1,0xcc,0xc7,0xae,0xa5,0xb8,0xb3,0x82,0x89,0x94,0x9f,
|
||||
0x46,0x4d,0x50,0x5b,0x6a,0x61,0x7c,0x77,0x1e,0x15,0x08,0x03,0x32,0x39,0x24,0x2f,
|
||||
0x8d,0x86,0x9b,0x90,0xa1,0xaa,0xb7,0xbc,0xd5,0xde,0xc3,0xc8,0xf9,0xf2,0xef,0xe4,
|
||||
0x3d,0x36,0x2b,0x20,0x11,0x1a,0x07,0x0c,0x65,0x6e,0x73,0x78,0x49,0x42,0x5f,0x54,
|
||||
0xf7,0xfc,0xe1,0xea,0xdb,0xd0,0xcd,0xc6,0xaf,0xa4,0xb9,0xb2,0x83,0x88,0x95,0x9e,
|
||||
0x47,0x4c,0x51,0x5a,0x6b,0x60,0x7d,0x76,0x1f,0x14,0x09,0x02,0x33,0x38,0x25,0x2e,
|
||||
0x8c,0x87,0x9a,0x91,0xa0,0xab,0xb6,0xbd,0xd4,0xdf,0xc2,0xc9,0xf8,0xf3,0xee,0xe5,
|
||||
0x3c,0x37,0x2a,0x21,0x10,0x1b,0x06,0x0d,0x64,0x6f,0x72,0x79,0x48,0x43,0x5e,0x55,
|
||||
0x01,0x0a,0x17,0x1c,0x2d,0x26,0x3b,0x30,0x59,0x52,0x4f,0x44,0x75,0x7e,0x63,0x68,
|
||||
0xb1,0xba,0xa7,0xac,0x9d,0x96,0x8b,0x80,0xe9,0xe2,0xff,0xf4,0xc5,0xce,0xd3,0xd8,
|
||||
0x7a,0x71,0x6c,0x67,0x56,0x5d,0x40,0x4b,0x22,0x29,0x34,0x3f,0x0e,0x05,0x18,0x13,
|
||||
0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3
|
||||
}
|
||||
|
||||
local GF8x13 = {
|
||||
[0]=0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b,
|
||||
0xd0,0xdd,0xca,0xc7,0xe4,0xe9,0xfe,0xf3,0xb8,0xb5,0xa2,0xaf,0x8c,0x81,0x96,0x9b,
|
||||
0xbb,0xb6,0xa1,0xac,0x8f,0x82,0x95,0x98,0xd3,0xde,0xc9,0xc4,0xe7,0xea,0xfd,0xf0,
|
||||
0x6b,0x66,0x71,0x7c,0x5f,0x52,0x45,0x48,0x03,0x0e,0x19,0x14,0x37,0x3a,0x2d,0x20,
|
||||
0x6d,0x60,0x77,0x7a,0x59,0x54,0x43,0x4e,0x05,0x08,0x1f,0x12,0x31,0x3c,0x2b,0x26,
|
||||
0xbd,0xb0,0xa7,0xaa,0x89,0x84,0x93,0x9e,0xd5,0xd8,0xcf,0xc2,0xe1,0xec,0xfb,0xf6,
|
||||
0xd6,0xdb,0xcc,0xc1,0xe2,0xef,0xf8,0xf5,0xbe,0xb3,0xa4,0xa9,0x8a,0x87,0x90,0x9d,
|
||||
0x06,0x0b,0x1c,0x11,0x32,0x3f,0x28,0x25,0x6e,0x63,0x74,0x79,0x5a,0x57,0x40,0x4d,
|
||||
0xda,0xd7,0xc0,0xcd,0xee,0xe3,0xf4,0xf9,0xb2,0xbf,0xa8,0xa5,0x86,0x8b,0x9c,0x91,
|
||||
0x0a,0x07,0x10,0x1d,0x3e,0x33,0x24,0x29,0x62,0x6f,0x78,0x75,0x56,0x5b,0x4c,0x41,
|
||||
0x61,0x6c,0x7b,0x76,0x55,0x58,0x4f,0x42,0x09,0x04,0x13,0x1e,0x3d,0x30,0x27,0x2a,
|
||||
0xb1,0xbc,0xab,0xa6,0x85,0x88,0x9f,0x92,0xd9,0xd4,0xc3,0xce,0xed,0xe0,0xf7,0xfa,
|
||||
0xb7,0xba,0xad,0xa0,0x83,0x8e,0x99,0x94,0xdf,0xd2,0xc5,0xc8,0xeb,0xe6,0xf1,0xfc,
|
||||
0x67,0x6a,0x7d,0x70,0x53,0x5e,0x49,0x44,0x0f,0x02,0x15,0x18,0x3b,0x36,0x21,0x2c,
|
||||
0x0c,0x01,0x16,0x1b,0x38,0x35,0x22,0x2f,0x64,0x69,0x7e,0x73,0x50,0x5d,0x4a,0x47,
|
||||
0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97
|
||||
}
|
||||
|
||||
local GF8x14 = {
|
||||
[0]=0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a,
|
||||
0xe0,0xee,0xfc,0xf2,0xd8,0xd6,0xc4,0xca,0x90,0x9e,0x8c,0x82,0xa8,0xa6,0xb4,0xba,
|
||||
0xdb,0xd5,0xc7,0xc9,0xe3,0xed,0xff,0xf1,0xab,0xa5,0xb7,0xb9,0x93,0x9d,0x8f,0x81,
|
||||
0x3b,0x35,0x27,0x29,0x03,0x0d,0x1f,0x11,0x4b,0x45,0x57,0x59,0x73,0x7d,0x6f,0x61,
|
||||
0xad,0xa3,0xb1,0xbf,0x95,0x9b,0x89,0x87,0xdd,0xd3,0xc1,0xcf,0xe5,0xeb,0xf9,0xf7,
|
||||
0x4d,0x43,0x51,0x5f,0x75,0x7b,0x69,0x67,0x3d,0x33,0x21,0x2f,0x05,0x0b,0x19,0x17,
|
||||
0x76,0x78,0x6a,0x64,0x4e,0x40,0x52,0x5c,0x06,0x08,0x1a,0x14,0x3e,0x30,0x22,0x2c,
|
||||
0x96,0x98,0x8a,0x84,0xae,0xa0,0xb2,0xbc,0xe6,0xe8,0xfa,0xf4,0xde,0xd0,0xc2,0xcc,
|
||||
0x41,0x4f,0x5d,0x53,0x79,0x77,0x65,0x6b,0x31,0x3f,0x2d,0x23,0x09,0x07,0x15,0x1b,
|
||||
0xa1,0xaf,0xbd,0xb3,0x99,0x97,0x85,0x8b,0xd1,0xdf,0xcd,0xc3,0xe9,0xe7,0xf5,0xfb,
|
||||
0x9a,0x94,0x86,0x88,0xa2,0xac,0xbe,0xb0,0xea,0xe4,0xf6,0xf8,0xd2,0xdc,0xce,0xc0,
|
||||
0x7a,0x74,0x66,0x68,0x42,0x4c,0x5e,0x50,0x0a,0x04,0x16,0x18,0x32,0x3c,0x2e,0x20,
|
||||
0xec,0xe2,0xf0,0xfe,0xd4,0xda,0xc8,0xc6,0x9c,0x92,0x80,0x8e,0xa4,0xaa,0xb8,0xb6,
|
||||
0x0c,0x02,0x10,0x1e,0x34,0x3a,0x28,0x26,0x7c,0x72,0x60,0x6e,0x44,0x4a,0x58,0x56,
|
||||
0x37,0x39,0x2b,0x25,0x0f,0x01,0x13,0x1d,0x47,0x49,0x5b,0x55,0x7f,0x71,0x63,0x6d,
|
||||
0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d
|
||||
}
|
||||
|
||||
local s = {
|
||||
[0]=0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76,
|
||||
0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0,
|
||||
0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15,
|
||||
0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75,
|
||||
0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84,
|
||||
0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF,
|
||||
0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8,
|
||||
0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2,
|
||||
0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73,
|
||||
0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB,
|
||||
0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79,
|
||||
0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08,
|
||||
0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A,
|
||||
0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E,
|
||||
0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF,
|
||||
0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16
|
||||
}
|
||||
|
||||
local si = {
|
||||
[0]=0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB,
|
||||
0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB,
|
||||
0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E,
|
||||
0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25,
|
||||
0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92,
|
||||
0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84,
|
||||
0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06,
|
||||
0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B,
|
||||
0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73,
|
||||
0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E,
|
||||
0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B,
|
||||
0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4,
|
||||
0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F,
|
||||
0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF,
|
||||
0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61,
|
||||
0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D
|
||||
}
|
||||
|
||||
local rcon = {
|
||||
0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,
|
||||
0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,
|
||||
0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,
|
||||
0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,
|
||||
0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,
|
||||
0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,
|
||||
0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,
|
||||
0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,
|
||||
0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,
|
||||
0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,
|
||||
0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,
|
||||
0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,
|
||||
0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,
|
||||
0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,
|
||||
0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,
|
||||
0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d
|
||||
}
|
||||
|
||||
local xor4 = {
|
||||
[0]=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
|
||||
1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,
|
||||
2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,
|
||||
3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,
|
||||
4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,
|
||||
5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,
|
||||
6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,
|
||||
7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,
|
||||
8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
|
||||
9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,
|
||||
10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,
|
||||
11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,
|
||||
12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,
|
||||
13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,
|
||||
14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,
|
||||
15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
|
||||
}
|
||||
|
||||
local function xor8(a, b)
|
||||
local al = a % 16
|
||||
local bl = b % 16
|
||||
return 16 * xor4[a - al + (b - bl) / 16] + xor4[16 * al + bl]
|
||||
end
|
||||
|
||||
local function xor_blocks_8(a, b)
|
||||
local res = {}
|
||||
for i = 1, 16 do
|
||||
res[i] = xor8(a[i], b[i])
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function addRoundKey(state, key)
|
||||
for i, byte in next, state do
|
||||
state[i] = xor8(byte, key[i])
|
||||
end
|
||||
end
|
||||
|
||||
local function subBytes(state, s_box)
|
||||
for i, byte in next, state do
|
||||
state[i] = s_box[byte]
|
||||
end
|
||||
end
|
||||
|
||||
local function shiftRows(state)
|
||||
state[2], state[6], state[10], state[14] =
|
||||
state[6], state[10], state[14], state[2]
|
||||
|
||||
state[3], state[7], state[11], state[15] =
|
||||
state[11], state[15], state[3], state[7]
|
||||
|
||||
state[4], state[8], state[12], state[16] =
|
||||
state[16], state[4], state[8], state[12]
|
||||
end
|
||||
|
||||
local function inv_shiftRows(state)
|
||||
state[2], state[6], state[10], state[14] =
|
||||
state[14], state[2], state[6], state[10]
|
||||
|
||||
state[3], state[7], state[11], state[15] =
|
||||
state[11], state[15], state[3], state[7]
|
||||
|
||||
state[4], state[8], state[12], state[16] =
|
||||
state[8], state[12], state[16], state[4]
|
||||
end
|
||||
|
||||
local function mixColumns(state)
|
||||
for i = 0, 3 do
|
||||
local cur = i*4+1
|
||||
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
|
||||
state[cur + 0] = xor8(xor8(xor8(GF8x2[a], GF8x3[b]), c), d)
|
||||
state[cur + 1] = xor8(xor8(xor8(a, GF8x2[b]), GF8x3[c]), d)
|
||||
state[cur + 2] = xor8(xor8(xor8(a, b), GF8x2[c]), GF8x3[d])
|
||||
state[cur + 3] = xor8(xor8(xor8(GF8x3[a], b), c), GF8x2[d])
|
||||
end
|
||||
end
|
||||
|
||||
local function inv_mixColumns(state) -- TODO: fix
|
||||
for i = 0, 3 do
|
||||
local cur = i*4+1
|
||||
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
|
||||
state[cur + 0] = xor8(xor8(xor8(GF8x14[a], GF8x11[b]), GF8x13[c]), GF8x9[d])
|
||||
state[cur + 1] = xor8(xor8(xor8(GF8x9[a], GF8x14[b]), GF8x11[c]), GF8x13[d])
|
||||
state[cur + 2] = xor8(xor8(xor8(GF8x13[a], GF8x9[b]), GF8x14[c]), GF8x11[d])
|
||||
state[cur + 3] = xor8(xor8(xor8(GF8x11[a], GF8x13[b]), GF8x9[c]), GF8x14[d])
|
||||
end
|
||||
end
|
||||
|
||||
-- 256-bit key constants
|
||||
local n = 32 -- number of bytes in the 256-bit encryption key
|
||||
local b = 240 -- number of bytes in 15 128-bit round keys
|
||||
local function schedule256(key)
|
||||
local expanded = {}
|
||||
for c = 0, n-1 do
|
||||
expanded[c] = key[c]
|
||||
end
|
||||
|
||||
local i = 1
|
||||
local c = n
|
||||
local t1, t2, t3, t4 --t
|
||||
while c < b do
|
||||
t1 = expanded[c-4]
|
||||
t2 = expanded[c-3]
|
||||
t3 = expanded[c-2]
|
||||
t4 = expanded[c-1]
|
||||
|
||||
if (c % n == 0) then
|
||||
t1, t2, t3, t4 = xor8(rcon[i+1], s[t2]), s[t3], s[t4], s[t1]
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
if (c % n == 16) then
|
||||
t1 = s[t1]
|
||||
t2 = s[t2]
|
||||
t3 = s[t3]
|
||||
t4 = s[t4]
|
||||
end
|
||||
|
||||
t1 = xor8(t1, expanded[c - n])
|
||||
expanded[c] = t1
|
||||
c = c + 1
|
||||
|
||||
t2 = xor8(t2, expanded[c - n])
|
||||
expanded[c] = t2
|
||||
c = c + 1
|
||||
|
||||
t3 = xor8(t3, expanded[c - n])
|
||||
expanded[c] = t3
|
||||
c = c + 1
|
||||
|
||||
t4 = xor8(t4, expanded[c - n])
|
||||
expanded[c] = t4
|
||||
c = c + 1
|
||||
end
|
||||
|
||||
local roundKeys = {}
|
||||
for round = 0, 14 do
|
||||
local roundKey = {}
|
||||
for byte = 0, 15 do
|
||||
roundKey[byte+1] = expanded[round * 16 + byte]
|
||||
end
|
||||
roundKeys[round] = roundKey
|
||||
end
|
||||
return roundKeys
|
||||
end
|
||||
|
||||
local function chunks(text, i)
|
||||
local first = i * 16 + 1
|
||||
if first > #text then
|
||||
return
|
||||
end
|
||||
i = i + 1
|
||||
|
||||
local chunk = {text:byte(first, first + 15)}
|
||||
for j = #chunk + 1, 16 do
|
||||
chunk[j] = 0
|
||||
end
|
||||
|
||||
return i, chunk
|
||||
end
|
||||
|
||||
local function pkcs7_unpad(str)
|
||||
local len = #str
|
||||
if len == 0 then return str end
|
||||
|
||||
local pad_len = string.byte(str, len)
|
||||
if pad_len < 1 or pad_len > 16 then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = len - pad_len + 1, len do
|
||||
if string.byte(str, i) ~= pad_len then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return string.sub(str, 1, len - pad_len)
|
||||
end
|
||||
|
||||
local function zero_unpad(str)
|
||||
local len = #str
|
||||
while len > 0 and string.byte(str, len) == 0 do
|
||||
len = len - 1
|
||||
end
|
||||
return string.sub(str, 1, len)
|
||||
end
|
||||
|
||||
local function unpad(str)
|
||||
local unpadded = pkcs7_unpad(str)
|
||||
if unpadded then
|
||||
return unpadded
|
||||
else
|
||||
return zero_unpad(str)
|
||||
end
|
||||
end
|
||||
|
||||
local function encrypt(state, roundKeys)
|
||||
addRoundKey(state, roundKeys[0])
|
||||
for round = 1, 13 do
|
||||
subBytes(state, s)
|
||||
shiftRows(state)
|
||||
mixColumns(state)
|
||||
addRoundKey(state, roundKeys[round])
|
||||
end
|
||||
subBytes(state, s)
|
||||
shiftRows(state)
|
||||
addRoundKey(state, roundKeys[14])
|
||||
end
|
||||
|
||||
local function decrypt(state, roundKeys)
|
||||
addRoundKey(state, roundKeys[14])
|
||||
inv_shiftRows(state)
|
||||
subBytes(state, si)
|
||||
for round = 13, 1, -1 do
|
||||
addRoundKey(state, roundKeys[round])
|
||||
inv_mixColumns(state)
|
||||
inv_shiftRows(state)
|
||||
subBytes(state, si)
|
||||
end
|
||||
addRoundKey(state, roundKeys[0])
|
||||
end
|
||||
|
||||
local function ECB_encrypt(key, originaltext)
|
||||
local text = {}
|
||||
local roundKeys = schedule256(key)
|
||||
local i = 0
|
||||
while true do
|
||||
i, state = chunks(originaltext, i)
|
||||
if not state then break end
|
||||
encrypt(state, roundKeys)
|
||||
text[i] = string.char(unpack(state))
|
||||
end
|
||||
return table.concat(text)
|
||||
end
|
||||
|
||||
local function ECB_decrypt(key, ciphertext)
|
||||
local text = {}
|
||||
local roundKeys = schedule256(key)
|
||||
local i = 0
|
||||
while true do
|
||||
i, state = chunks(ciphertext, i)
|
||||
if not state then break end
|
||||
decrypt(state, roundKeys)
|
||||
text[i] = string.char(unpack(state))
|
||||
end
|
||||
return unpad(table.concat(text))
|
||||
end
|
||||
|
||||
local function CBC_encrypt(key, iv, originaltext)
|
||||
local roundKeys = schedule256(key)
|
||||
local text = {}
|
||||
local prev_block = {unpack(iv)}
|
||||
local i = 0
|
||||
|
||||
while true do
|
||||
i, block = chunks(originaltext, i)
|
||||
if not block then break end
|
||||
|
||||
local xored = xor_blocks_8(block, prev_block)
|
||||
encrypt(xored, roundKeys)
|
||||
text[i] = string.char(unpack(xored))
|
||||
prev_block = xored
|
||||
end
|
||||
|
||||
return table.concat(text)
|
||||
end
|
||||
|
||||
local function CBC_decrypt(key, iv, ciphertext)
|
||||
local roundKeys = schedule256(key)
|
||||
local text = {}
|
||||
local prev_block = {unpack(iv)}
|
||||
local i = 0
|
||||
|
||||
while true do
|
||||
i, block = chunks(ciphertext, i)
|
||||
if not block then break end
|
||||
|
||||
local decrypted = {unpack(block)}
|
||||
|
||||
decrypt(decrypted, roundKeys)
|
||||
local plain_block = xor_blocks_8(decrypted, prev_block)
|
||||
text[i] = string.char(unpack(plain_block))
|
||||
prev_block = block
|
||||
end
|
||||
|
||||
local result = table.concat(text)
|
||||
return unpad(result)
|
||||
end
|
||||
|
||||
return {
|
||||
ECB = {
|
||||
encrypt = ECB_encrypt;
|
||||
decrypt = ECB_decrypt;
|
||||
};
|
||||
CBC = {
|
||||
encrypt = CBC_encrypt;
|
||||
decrypt = CBC_decrypt;
|
||||
};
|
||||
}
|
||||
@@ -1,192 +1,192 @@
|
||||
--[[
|
||||
|
||||
sha256 -- public domain Lua SHA-256 implementation
|
||||
no warranty implied; use at your own risk
|
||||
|
||||
author: dyphire
|
||||
|
||||
COMPATIBILITY
|
||||
|
||||
Lua 5.1+, LuaJIT
|
||||
|
||||
LICENSE: MIT License
|
||||
|
||||
--]]
|
||||
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local function band(a,b)
|
||||
local res = 0
|
||||
local bit = 1
|
||||
for i = 0,31 do
|
||||
local aa = a % 2
|
||||
local bb = b % 2
|
||||
if aa == 1 and bb == 1 then
|
||||
res = res + bit
|
||||
end
|
||||
a = (a - aa) / 2
|
||||
b = (b - bb) / 2
|
||||
bit = bit * 2
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function bor(a,b)
|
||||
local res = 0
|
||||
local bit = 1
|
||||
for i = 0,31 do
|
||||
local aa = a % 2
|
||||
local bb = b % 2
|
||||
if aa == 1 or bb == 1 then
|
||||
res = res + bit
|
||||
end
|
||||
a = (a - aa) / 2
|
||||
b = (b - bb) / 2
|
||||
bit = bit * 2
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function bxor(a,b)
|
||||
local res = 0
|
||||
local bit = 1
|
||||
for i = 0,31 do
|
||||
local aa = a % 2
|
||||
local bb = b % 2
|
||||
if (aa + bb) == 1 then
|
||||
res = res + bit
|
||||
end
|
||||
a = (a - aa) / 2
|
||||
b = (b - bb) / 2
|
||||
bit = bit * 2
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function bnot(a)
|
||||
return 0xFFFFFFFF - a
|
||||
end
|
||||
|
||||
local function lshift(a,n)
|
||||
return (a * 2^n) % 2^32
|
||||
end
|
||||
|
||||
local function rshift(a,n)
|
||||
return math.floor(a / 2^n) % 2^32
|
||||
end
|
||||
|
||||
local function bit_ror(x, n)
|
||||
return bor(rshift(x, n), lshift(x, 32 - n))
|
||||
end
|
||||
|
||||
local function sha256(message)
|
||||
local k = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||
}
|
||||
|
||||
local function preprocess(msg)
|
||||
local len = #msg
|
||||
local bitLen = len * 8
|
||||
msg = msg .. "\128"
|
||||
|
||||
local zeroPad = 64 - ((len + 9) % 64)
|
||||
if zeroPad ~= 64 then
|
||||
msg = msg .. string.rep("\0", zeroPad)
|
||||
end
|
||||
|
||||
msg = msg .. string.char(
|
||||
rshift(bitLen, 56) % 256,
|
||||
rshift(bitLen, 48) % 256,
|
||||
rshift(bitLen, 40) % 256,
|
||||
rshift(bitLen, 32) % 256,
|
||||
rshift(bitLen, 24) % 256,
|
||||
rshift(bitLen, 16) % 256,
|
||||
rshift(bitLen, 8) % 256,
|
||||
bitLen % 256
|
||||
)
|
||||
return msg
|
||||
end
|
||||
|
||||
local function chunkify(msg)
|
||||
local chunks = {}
|
||||
for i = 1, #msg, 64 do
|
||||
table.insert(chunks, msg:sub(i, i + 63))
|
||||
end
|
||||
return chunks
|
||||
end
|
||||
|
||||
local function processChunk(chunk, hash)
|
||||
local w = {}
|
||||
|
||||
for i = 1, 64 do
|
||||
if i <= 16 then
|
||||
w[i] = lshift(string.byte(chunk, (i - 1) * 4 + 1), 24) +
|
||||
lshift(string.byte(chunk, (i - 1) * 4 + 2), 16) +
|
||||
lshift(string.byte(chunk, (i - 1) * 4 + 3), 8) +
|
||||
string.byte(chunk, (i - 1) * 4 + 4)
|
||||
else
|
||||
local s0 = bxor(bxor(bit_ror(w[i - 15], 7), bit_ror(w[i - 15], 18)), rshift(w[i - 15], 3))
|
||||
local s1 = bxor(bxor(bit_ror(w[i - 2], 17), bit_ror(w[i - 2], 19)), rshift(w[i - 2], 10))
|
||||
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % 2^32
|
||||
end
|
||||
end
|
||||
|
||||
local a, b, c, d, e, f, g, h = unpack(hash)
|
||||
|
||||
for i = 1, 64 do
|
||||
local s1 = bxor(bxor(bit_ror(e, 6), bit_ror(e, 11)), bit_ror(e, 25))
|
||||
local ch = bxor(band(e, f), band(bnot(e), g))
|
||||
local temp1 = (h + s1 + ch + k[i] + w[i]) % 2^32
|
||||
local s0 = bxor(bxor(bit_ror(a, 2), bit_ror(a, 13)), bit_ror(a, 22))
|
||||
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
|
||||
local temp2 = (s0 + maj) % 2^32
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + temp1) % 2^32
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (temp1 + temp2) % 2^32
|
||||
end
|
||||
|
||||
return
|
||||
(hash[1] + a) % 2^32,
|
||||
(hash[2] + b) % 2^32,
|
||||
(hash[3] + c) % 2^32,
|
||||
(hash[4] + d) % 2^32,
|
||||
(hash[5] + e) % 2^32,
|
||||
(hash[6] + f) % 2^32,
|
||||
(hash[7] + g) % 2^32,
|
||||
(hash[8] + h) % 2^32
|
||||
end
|
||||
|
||||
message = preprocess(message)
|
||||
local chunks = chunkify(message)
|
||||
|
||||
local hash = {
|
||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
||||
}
|
||||
|
||||
for _, chunk in ipairs(chunks) do
|
||||
hash = {processChunk(chunk, hash)}
|
||||
end
|
||||
|
||||
local result = ""
|
||||
for _, h in ipairs(hash) do
|
||||
result = result .. string.format("%08x", h)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return sha256␍
|
||||
--[[
|
||||
|
||||
sha256 -- public domain Lua SHA-256 implementation
|
||||
no warranty implied; use at your own risk
|
||||
|
||||
author: dyphire
|
||||
|
||||
COMPATIBILITY
|
||||
|
||||
Lua 5.1+, LuaJIT
|
||||
|
||||
LICENSE: MIT License
|
||||
|
||||
--]]
|
||||
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local function band(a,b)
|
||||
local res = 0
|
||||
local bit = 1
|
||||
for i = 0,31 do
|
||||
local aa = a % 2
|
||||
local bb = b % 2
|
||||
if aa == 1 and bb == 1 then
|
||||
res = res + bit
|
||||
end
|
||||
a = (a - aa) / 2
|
||||
b = (b - bb) / 2
|
||||
bit = bit * 2
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function bor(a,b)
|
||||
local res = 0
|
||||
local bit = 1
|
||||
for i = 0,31 do
|
||||
local aa = a % 2
|
||||
local bb = b % 2
|
||||
if aa == 1 or bb == 1 then
|
||||
res = res + bit
|
||||
end
|
||||
a = (a - aa) / 2
|
||||
b = (b - bb) / 2
|
||||
bit = bit * 2
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function bxor(a,b)
|
||||
local res = 0
|
||||
local bit = 1
|
||||
for i = 0,31 do
|
||||
local aa = a % 2
|
||||
local bb = b % 2
|
||||
if (aa + bb) == 1 then
|
||||
res = res + bit
|
||||
end
|
||||
a = (a - aa) / 2
|
||||
b = (b - bb) / 2
|
||||
bit = bit * 2
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function bnot(a)
|
||||
return 0xFFFFFFFF - a
|
||||
end
|
||||
|
||||
local function lshift(a,n)
|
||||
return (a * 2^n) % 2^32
|
||||
end
|
||||
|
||||
local function rshift(a,n)
|
||||
return math.floor(a / 2^n) % 2^32
|
||||
end
|
||||
|
||||
local function bit_ror(x, n)
|
||||
return bor(rshift(x, n), lshift(x, 32 - n))
|
||||
end
|
||||
|
||||
local function sha256(message)
|
||||
local k = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||
}
|
||||
|
||||
local function preprocess(msg)
|
||||
local len = #msg
|
||||
local bitLen = len * 8
|
||||
msg = msg .. "\128"
|
||||
|
||||
local zeroPad = 64 - ((len + 9) % 64)
|
||||
if zeroPad ~= 64 then
|
||||
msg = msg .. string.rep("\0", zeroPad)
|
||||
end
|
||||
|
||||
msg = msg .. string.char(
|
||||
rshift(bitLen, 56) % 256,
|
||||
rshift(bitLen, 48) % 256,
|
||||
rshift(bitLen, 40) % 256,
|
||||
rshift(bitLen, 32) % 256,
|
||||
rshift(bitLen, 24) % 256,
|
||||
rshift(bitLen, 16) % 256,
|
||||
rshift(bitLen, 8) % 256,
|
||||
bitLen % 256
|
||||
)
|
||||
return msg
|
||||
end
|
||||
|
||||
local function chunkify(msg)
|
||||
local chunks = {}
|
||||
for i = 1, #msg, 64 do
|
||||
table.insert(chunks, msg:sub(i, i + 63))
|
||||
end
|
||||
return chunks
|
||||
end
|
||||
|
||||
local function processChunk(chunk, hash)
|
||||
local w = {}
|
||||
|
||||
for i = 1, 64 do
|
||||
if i <= 16 then
|
||||
w[i] = lshift(string.byte(chunk, (i - 1) * 4 + 1), 24) +
|
||||
lshift(string.byte(chunk, (i - 1) * 4 + 2), 16) +
|
||||
lshift(string.byte(chunk, (i - 1) * 4 + 3), 8) +
|
||||
string.byte(chunk, (i - 1) * 4 + 4)
|
||||
else
|
||||
local s0 = bxor(bxor(bit_ror(w[i - 15], 7), bit_ror(w[i - 15], 18)), rshift(w[i - 15], 3))
|
||||
local s1 = bxor(bxor(bit_ror(w[i - 2], 17), bit_ror(w[i - 2], 19)), rshift(w[i - 2], 10))
|
||||
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % 2^32
|
||||
end
|
||||
end
|
||||
|
||||
local a, b, c, d, e, f, g, h = unpack(hash)
|
||||
|
||||
for i = 1, 64 do
|
||||
local s1 = bxor(bxor(bit_ror(e, 6), bit_ror(e, 11)), bit_ror(e, 25))
|
||||
local ch = bxor(band(e, f), band(bnot(e), g))
|
||||
local temp1 = (h + s1 + ch + k[i] + w[i]) % 2^32
|
||||
local s0 = bxor(bxor(bit_ror(a, 2), bit_ror(a, 13)), bit_ror(a, 22))
|
||||
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
|
||||
local temp2 = (s0 + maj) % 2^32
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + temp1) % 2^32
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (temp1 + temp2) % 2^32
|
||||
end
|
||||
|
||||
return
|
||||
(hash[1] + a) % 2^32,
|
||||
(hash[2] + b) % 2^32,
|
||||
(hash[3] + c) % 2^32,
|
||||
(hash[4] + d) % 2^32,
|
||||
(hash[5] + e) % 2^32,
|
||||
(hash[6] + f) % 2^32,
|
||||
(hash[7] + g) % 2^32,
|
||||
(hash[8] + h) % 2^32
|
||||
end
|
||||
|
||||
message = preprocess(message)
|
||||
local chunks = chunkify(message)
|
||||
|
||||
local hash = {
|
||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
||||
}
|
||||
|
||||
for _, chunk in ipairs(chunks) do
|
||||
hash = {processChunk(chunk, hash)}
|
||||
end
|
||||
|
||||
local result = ""
|
||||
for _, h in ipairs(hash) do
|
||||
result = result .. string.format("%08x", h)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return sha256
|
||||
@@ -0,0 +1,322 @@
|
||||
--[[lit-meta
|
||||
name = 'TohruMKDM/inflate'
|
||||
version = '2.0.0'
|
||||
homepage = 'https://github.com/TohruMKDM/lua-inflate'
|
||||
description = 'ZIP archive inflation in pure Lua.'
|
||||
tags = {'zlib', 'inflate', 'inflation', 'compression'}
|
||||
license = 'MIT'
|
||||
author = {name = 'Tohru~ (トール)', email = 'admin@ikaros.pw'}
|
||||
contributors = {'zerkman', 'samhocevar'}
|
||||
]]
|
||||
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local bitOp = bit32 or bit
|
||||
if not bitOp then
|
||||
error('This library requires bit operations to work property.')
|
||||
end
|
||||
|
||||
local rshift, lshift, band, bxor, bnot = bitOp.rshift, bitOp.lshift, bitOp.band, bitOp.bxor, bitOp.bnot
|
||||
local byte, char, sub, find = string.byte, string.char, string.sub, string.find
|
||||
local concat, unpack = table.concat, unpack or table.unpack
|
||||
local min = math.min
|
||||
|
||||
local ORDER = {17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16}
|
||||
local CRC32 = {[0] = 0, 1996959894, -301047508, -1727442502, 124634137, 1886057615, -379345611, -1637575261, 249268274, 2044508324, -522852066, -1747789432, 162941995, 2125561021, -407360249, -1866523247, 498536548, 1789927666, -205950648, -2067906082, 450548861, 1843258603, -187386543, -2083289657, 325883990, 1684777152, -43845254,-1973040660, 335633487, 1661365465, -99664541, -1928851979, 997073096, 1281953886, -715111964, -1570279054, 1006888145, 1258607687, -770865667, -1526024853, 901097722, 1119000684, -608450090, -1396901568, 853044451, 1172266101, -589951537, -1412350631, 651767980, 1373503546, -925412992, -1076862698, 565507253, 1454621731, -809855591, -1195530993, 671266974, 1594198024, -972236366, -1324619484, 795835527, 1483230225, -1050600021, -1234817731, 1994146192, 31158534, -1731059524, -271249366, 1907459465, 112637215, -1614814043, -390540237, 2013776290, 251722036, -1777751922, -519137256, 2137656763, 141376813, -1855689577, -429695999, 1802195444, 476864866, -2056965928, -228458418, 1812370925, 453092731, -2113342271, -183516073, 1706088902, 314042704, -1950435094, -54949764, 1658658271, 366619977, -1932296973, -69972891, 1303535960, 984961486, -1547960204, -725929758, 1256170817, 1037604311, -1529756563, -740887301, 1131014506, 879679996, -1385723834, -631195440, 1141124467, 855842277, -1442165665, -586318647, 1342533948, 654459306, -1106571248, -921952122, 1466479909, 544179635, -1184443383, -832445281, 1591671054, 702138776, -1328506846, -942167884, 1504918807, 783551873, -1212326853, -1061524307, -306674912, -1698712650, 62317068, 1957810842, -355121351, -1647151185, 81470997,1943803523, -480048366, -1805370492, 225274430, 2053790376, -468791541, -1828061283, 167816743, 2097651377, -267414716, -2029476910, 503444072, 1762050814, -144550051, -2140837941, 426522225, 1852507879, -19653770, -1982649376, 282753626, 1742555852, -105259153, -1900089351, 397917763, 1622183637, -690576408, -1580100738, 953729732, 1340076626, -776247311, -1497606297, 1068828381, 1219638859, -670225446, -1358292148, 906185462, 1090812512, -547295293, -1469587627, 829329135, 1181335161, -882789492, -1134132454, 628085408, 1382605366, -871598187, -1156888829, 570562233, 1426400815, -977650754, -1296233688, 733239954, 1555261956, -1026031705, -1244606671, 752459403, 1541320221, -1687895376, -328994266, 1969922972, 40735498, -1677130071, -351390145, 1913087877, 83908371, -1782625662, -491226604, 2075208622, 213261112, -1831694693, -438977011, 2094854071, 198958881, -2032938284, -237706686, 1759359992, 534414190, -2118248755, -155638181, 1873836001, 414664567, -2012718362, -15766928, 1711684554, 285281116, -1889165569, -127750551, 1634467795, 376229701, -1609899400, -686959890, 1308918612, 956543938, -1486412191, -799009033, 1231636301, 1047427035, -1362007478, -640263460, 1088359270, 936918000, -1447252397, -558129467, 1202900863, 817233897, -1111625188, -893730166, 1404277552, 615818150, -1160759803, -841546093, 1423857449, 601450431, -1285129682, -1000256840, 1567103746, 711928724, -1274298825, -1022587231, 1510334235, 755167117}
|
||||
local NBT = {2, 3, 7}
|
||||
local CNT = {144, 112, 24, 8}
|
||||
local DPT = {8, 9, 7, 8}
|
||||
local STATIC_HUFFMAN = {[0] = 5, 261, 133, 389, 69, 325, 197, 453, 37, 293, 165, 421, 101, 357, 229, 485, 21, 277, 149, 405, 85, 341, 213, 469, 53, 309, 181, 437, 117, 373, 245, 501}
|
||||
local STATIC_BITS = 5
|
||||
local CHUNK_SIZE = 4096
|
||||
|
||||
local function generateCrc32(input)
|
||||
local crc = -1
|
||||
for i = 1, #input do
|
||||
local c = byte(input, i)
|
||||
crc = bxor(CRC32[bxor(c, band(crc, 255))], rshift(crc, 8))
|
||||
end
|
||||
crc = bnot(crc)
|
||||
if crc < 0 then
|
||||
crc = crc + 4294967296
|
||||
end
|
||||
return crc
|
||||
end
|
||||
|
||||
local function flushBits(stream, int)
|
||||
stream.bits = rshift(stream.bits, int)
|
||||
stream.count = stream.count - int
|
||||
end
|
||||
|
||||
local function peekBits(stream, int)
|
||||
local buffer, bits, count, position = stream.buffer, stream.bits, stream.count, stream.position
|
||||
while count < int do
|
||||
bits = bits + lshift(byte(buffer, position), count)
|
||||
position = position + 1
|
||||
count = count + 8
|
||||
end
|
||||
stream.bits = bits
|
||||
stream.position = position
|
||||
stream.count = count
|
||||
return band(bits, lshift(1, int) - 1)
|
||||
end
|
||||
|
||||
local function getBits(stream, int)
|
||||
local result = peekBits(stream, int)
|
||||
stream.bits = rshift(stream.bits, int)
|
||||
stream.count = stream.count - int
|
||||
return result
|
||||
end
|
||||
|
||||
local function getElement(stream, hufftable, int)
|
||||
local element = hufftable[peekBits(stream, int)]
|
||||
local length = band(element, 15)
|
||||
local result = rshift(element, 4)
|
||||
stream.bits = rshift(stream.bits, length)
|
||||
stream.count = stream.count - length
|
||||
return result
|
||||
end
|
||||
|
||||
local function huffman(depths)
|
||||
local size = #depths
|
||||
local blocks, codes, hufftable = {[0] = 0}, {}, {}
|
||||
local bits, code = 1, 0
|
||||
for i = 1, size do
|
||||
local depth = depths[i]
|
||||
if depth > bits then
|
||||
bits = depth
|
||||
end
|
||||
blocks[depth] = (blocks[depth] or 0) + 1
|
||||
end
|
||||
for i = 1, bits do
|
||||
code = (code + (blocks[i - 1] or 0)) * 2
|
||||
codes[i] = code
|
||||
end
|
||||
for i = 1, size do
|
||||
local depth = depths[i]
|
||||
if depth > 0 then
|
||||
local element = (i - 1) * 16 + depth
|
||||
local rcode = 0
|
||||
for j = 1, depth do
|
||||
rcode = rcode + lshift(band(1, rshift(codes[depth], j - 1)), depth - j)
|
||||
end
|
||||
for j = 0, 2 ^ bits - 1, 2 ^ depth do
|
||||
hufftable[j + rcode] = element
|
||||
end
|
||||
codes[depth] = codes[depth] + 1
|
||||
end
|
||||
end
|
||||
return hufftable, bits
|
||||
end
|
||||
|
||||
local function loop(output, stream, litTable, litBits, distTable, distBits)
|
||||
local index = #output + 1
|
||||
local lit
|
||||
repeat
|
||||
lit = getElement(stream, litTable, litBits)
|
||||
if lit < 256 then
|
||||
output[index] = lit
|
||||
index = index + 1
|
||||
elseif lit > 256 then
|
||||
local bits, size, dist = 0, 3, 1
|
||||
if lit < 265 then
|
||||
size = size + lit - 257
|
||||
elseif lit < 285 then
|
||||
bits = rshift(lit - 261, 2)
|
||||
size = size + lshift(band(lit - 261, 3) + 4, bits)
|
||||
else
|
||||
size = 258
|
||||
end
|
||||
if bits > 0 then
|
||||
size = size + getBits(stream, bits)
|
||||
end
|
||||
local element = getElement(stream, distTable, distBits)
|
||||
if element < 4 then
|
||||
dist = dist + element
|
||||
else
|
||||
bits = rshift(element - 2, 1)
|
||||
dist = dist + lshift(band(element, 1) + 2, bits) + getBits(stream, bits)
|
||||
end
|
||||
local position = index - dist
|
||||
repeat
|
||||
output[index] = output[position]
|
||||
index = index + 1
|
||||
position = position + 1
|
||||
size = size - 1
|
||||
until size == 0
|
||||
end
|
||||
until lit == 256
|
||||
end
|
||||
|
||||
local function dynamic(output, stream)
|
||||
local lit, dist, length = 257 + getBits(stream, 5), 1 + getBits(stream, 5), 4 + getBits(stream, 4)
|
||||
local depths = {}
|
||||
for i = 1, length do
|
||||
depths[ORDER[i]] = getBits(stream, 3)
|
||||
end
|
||||
for i = length + 1, 19 do
|
||||
depths[ORDER[i]] = 0
|
||||
end
|
||||
local lengthTable, lengthBits = huffman(depths)
|
||||
local i = 1
|
||||
local total = lit + dist + 1
|
||||
repeat
|
||||
local element = getElement(stream, lengthTable, lengthBits)
|
||||
if element < 16 then
|
||||
depths[i] = element
|
||||
i = i + 1
|
||||
elseif element < 19 then
|
||||
local int = NBT[element - 15]
|
||||
local count = 0
|
||||
local num = 3 + getBits(stream, int)
|
||||
if element == 16 then
|
||||
count = depths[i - 1]
|
||||
elseif element == 18 then
|
||||
num = num + 8
|
||||
end
|
||||
for _ = 1, num do
|
||||
depths[i] = count
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
until i == total
|
||||
local litDepths, distDepths = {}, {}
|
||||
for j = 1, lit do
|
||||
litDepths[j] = depths[j]
|
||||
end
|
||||
for j = lit + 1, #depths do
|
||||
distDepths[#distDepths + 1] = depths[j]
|
||||
end
|
||||
local litTable, litBits = huffman(litDepths)
|
||||
local distTable, distBits = huffman(distDepths)
|
||||
loop(output, stream, litTable, litBits, distTable, distBits)
|
||||
end
|
||||
|
||||
local function static(output, stream)
|
||||
local depths = {}
|
||||
for i = 1, 4 do
|
||||
local depth = DPT[i]
|
||||
for _ = 1, CNT[i] do
|
||||
depths[#depths + 1] = depth
|
||||
end
|
||||
end
|
||||
local litTable, litBits = huffman(depths)
|
||||
loop(output, stream, litTable, litBits, STATIC_HUFFMAN, STATIC_BITS)
|
||||
end
|
||||
|
||||
local function uncompressed(output, stream)
|
||||
flushBits(stream, band(stream.count, 7))
|
||||
local length = getBits(stream, 16); getBits(stream, 16)
|
||||
local buffer, position = stream.buffer, stream.position
|
||||
for i = position, position + length - 1 do
|
||||
output[#output + 1] = byte(buffer, i, i)
|
||||
end
|
||||
stream.position = position + length
|
||||
end
|
||||
|
||||
local function int2le(buffer, position)
|
||||
local a, b = byte(buffer, position, position + 1)
|
||||
return b * 256 + a
|
||||
end
|
||||
|
||||
local function int4le(buffer, position)
|
||||
local a, b, c, d = byte(buffer, position, position + 3)
|
||||
return ((d * 256 + c) * 256 + b) * 256 + a
|
||||
end
|
||||
|
||||
local inflate = {}
|
||||
|
||||
---@class BitStream
|
||||
---@field buffer string Character Buffer
|
||||
---@field position integer Position in the character buffer
|
||||
---@field bits integer Bits buffer
|
||||
---@field count integer Number of bits in the buffer
|
||||
local BitStream = {}
|
||||
BitStream.__index = BitStream
|
||||
|
||||
---Creates a new bitstream object with the specified buffer
|
||||
---@param buffer string The character buffer to use for the bitstream.
|
||||
---@return BitStream
|
||||
function inflate.new(buffer)
|
||||
local EOCD
|
||||
local pos = 1
|
||||
repeat
|
||||
local i, j = find(buffer, 'PK\5\6', pos, true)
|
||||
if i then
|
||||
EOCD = i
|
||||
pos = j + 1
|
||||
end
|
||||
until not i
|
||||
if EOCD then
|
||||
buffer = sub(buffer, 1, EOCD + 19)..'\0\0'
|
||||
end
|
||||
local object = {buffer = buffer, position = 0, bits = 0, count = 0}
|
||||
return setmetatable(object, BitStream)
|
||||
end
|
||||
---Update the chunk size to be used in inflation. Defaults to `4096`
|
||||
---@param size integer The new chunk size, should be a power of 2.
|
||||
function inflate.setChunkSize(size)
|
||||
CHUNK_SIZE = size
|
||||
end
|
||||
|
||||
---Returns an iterator that spans the list of files in the stream
|
||||
---@return fun(): name: string?, offset: integer?, size: integer?, packed: boolean?, crc: integer?
|
||||
function BitStream:files()
|
||||
local buffer = self.buffer
|
||||
local position = int4le(buffer, #buffer - 5) + 1
|
||||
return function()
|
||||
if int4le(buffer, position) ~= 33639248 then
|
||||
return
|
||||
end
|
||||
local packed = int2le(buffer, position + 10) ~= 0
|
||||
local crc = int4le(buffer, position + 16)
|
||||
local length = int2le(buffer, position + 28)
|
||||
local offset = int4le(buffer, position + 42) + 1
|
||||
local name = sub(buffer, position + 46, position + 45 + length)
|
||||
position = position + 46 + length + int2le(buffer, position + 30) + int2le(buffer, position + 32)
|
||||
return name, offset + 30 + length + int2le(buffer, offset + 28), int4le(buffer, offset + 18), packed, crc
|
||||
end
|
||||
end
|
||||
---Inflates the bitstream starting from the specified offset and optionally performs checksum verification
|
||||
---@param offset integer The position at which to begin inflating.
|
||||
---@param crc? integer The checksum value to use for verification.
|
||||
---@return string
|
||||
function BitStream:inflate(offset, crc)
|
||||
local output, buffer = {}, {}
|
||||
local last, typ
|
||||
self.bits = 0
|
||||
self.count = 0
|
||||
self.position = offset
|
||||
repeat
|
||||
last, typ = getBits(self, 1), getBits(self, 2)
|
||||
typ = typ == 0 and uncompressed(output, self) or typ == 1 and static(output, self) or typ == 2 and dynamic(output, self)
|
||||
until last == 1
|
||||
local size = #output
|
||||
for i = 1, size, CHUNK_SIZE do
|
||||
buffer[#buffer + 1] = char(unpack(output, i, min(i + CHUNK_SIZE - 1, size)))
|
||||
end
|
||||
local result = concat(buffer)
|
||||
if crc and crc ~= generateCrc32(result) then
|
||||
error("Checksum verification failed: the computed CRC does not match the expected value.", 2)
|
||||
end
|
||||
return result
|
||||
end
|
||||
---Extracts a specific file from the bitstream
|
||||
---@param filepath string The file to unzip.
|
||||
---@param verify? boolean Whether or not to perform checksum verification.
|
||||
---@return string
|
||||
function BitStream:unzip(filepath, verify)
|
||||
for name, offset, size, packed, crc in self:files() do
|
||||
if name == filepath then
|
||||
return packed and self:inflate(offset, verify and crc or nil) or sub(self.buffer, offset, offset + size - 1)
|
||||
end
|
||||
end
|
||||
error('File "'..filepath..'" not found in ZIP archive.')
|
||||
end
|
||||
--- Extracts unpacked contents from the bitstream at the specified offset and size
|
||||
--- @param offset integer The starting position from which to extract the contents.
|
||||
--- @param size integer The size of the contents to extract.
|
||||
function BitStream:extract(offset, size)
|
||||
return sub(self.buffer, offset, offset + size - 1)
|
||||
end
|
||||
|
||||
return inflate
|
||||
@@ -5,84 +5,267 @@ local unpack = unpack or table.unpack
|
||||
input_loaded, input = pcall(require, "mp.input")
|
||||
uosc_available = false
|
||||
latest_menu_anime = {}
|
||||
local active_request_cancel = nil
|
||||
local active_request_type = nil
|
||||
local request_cancelled = false
|
||||
|
||||
-- 如果 latest_menu_anime 中存在首项为加载占位,移除它(兼容完整 menu props 或 items 数组)
|
||||
local function strip_loading_from_latest_menu_anime()
|
||||
if not latest_menu_anime or #latest_menu_anime == 0 then return end
|
||||
local parsed = utils.parse_json(latest_menu_anime)
|
||||
if not parsed or type(parsed) ~= "table" then return end
|
||||
local function is_loading_item(it)
|
||||
if not it or type(it) ~= "table" then return false end
|
||||
if it.title == "加载数据中..." then return true end
|
||||
if it.italic == true and it.icon == "spinner" then return true end
|
||||
return false
|
||||
end
|
||||
if parsed.items and type(parsed.items) == "table" then
|
||||
if #parsed.items > 0 and is_loading_item(parsed.items[1]) then
|
||||
table.remove(parsed.items, 1)
|
||||
latest_menu_anime = utils.format_json(parsed)
|
||||
end
|
||||
else
|
||||
if #parsed > 0 and is_loading_item(parsed[1]) then
|
||||
table.remove(parsed, 1)
|
||||
latest_menu_anime = utils.format_json(parsed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 统一取消并发请求:设置取消标志、剥离加载占位并调用实际取消函数
|
||||
local function perform_cancel_active_request(expected_type)
|
||||
if expected_type and active_request_type and tostring(expected_type) ~= tostring(active_request_type) then
|
||||
return
|
||||
end
|
||||
if active_request_cancel then
|
||||
request_cancelled = true
|
||||
strip_loading_from_latest_menu_anime()
|
||||
pcall(active_request_cancel)
|
||||
active_request_cancel = nil
|
||||
active_request_type = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function make_build_args(encoded_query)
|
||||
return function(server)
|
||||
local url = server .. "/api/v2/search/anime"
|
||||
local full_url = url .. "?keyword=" .. encoded_query
|
||||
return make_danmaku_request_args("GET", full_url)
|
||||
end
|
||||
end
|
||||
|
||||
local function make_handle_response(ctx)
|
||||
return function(server, err, out)
|
||||
if request_cancelled then
|
||||
ctx.remaining.n = math.max(0, ctx.remaining.n - 1)
|
||||
return
|
||||
end
|
||||
|
||||
local function do_final_update()
|
||||
local final_items = {}
|
||||
-- 按配置的 server 顺序拼接每个 server 的结果
|
||||
for _, srv in ipairs(ctx.server_order or {}) do
|
||||
local list = ctx.server_items and ctx.server_items[srv]
|
||||
if list and type(list) == 'table' then
|
||||
for _, v in ipairs(list) do table.insert(final_items, v) end
|
||||
end
|
||||
end
|
||||
if request_cancelled then return end
|
||||
if uosc_available then
|
||||
latest_menu_anime = update_menu_uosc(ctx.menu_type, ctx.menu_title, final_items, ctx.footnote, ctx.menu_cmd, ctx.query)
|
||||
else
|
||||
latest_menu_anime = utils.format_json(final_items)
|
||||
if input_loaded then
|
||||
input.terminate()
|
||||
mp.add_timeout(0.1, function()
|
||||
open_menu_select(final_items)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if err then
|
||||
msg.debug(("search anime failed for %s: %s"):format(server, tostring(err)))
|
||||
ctx.remaining.n = math.max(0, ctx.remaining.n - 1)
|
||||
if ctx.remaining.n == 0 then pcall(do_final_update) end
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
if not data or not data.animes then
|
||||
ctx.remaining.n = math.max(0, ctx.remaining.n - 1)
|
||||
if ctx.remaining.n == 0 then pcall(do_final_update) end
|
||||
return
|
||||
end
|
||||
for _, anime in ipairs(data.animes) do
|
||||
local key = anime.bangumiId or (anime.animeTitle and anime.animeTitle:gsub("%s+", " ") or nil)
|
||||
if key and not ctx.seen[key] then
|
||||
ctx.seen[key] = true
|
||||
ctx.server_items[server] = ctx.server_items[server] or {}
|
||||
local note = (ctx.server_notes and ctx.server_notes[server]) or nil
|
||||
local hint = anime.typeDescription
|
||||
if note and note ~= "" then
|
||||
if hint and hint ~= "" then
|
||||
hint = hint .. "|" .. note
|
||||
else
|
||||
hint = note
|
||||
end
|
||||
end
|
||||
table.insert(ctx.server_items[server], {
|
||||
title = anime.animeTitle,
|
||||
hint = hint,
|
||||
value = { "script-message-to", mp.get_script_name(), "search-episodes-event", anime.animeTitle, anime.bangumiId, server },
|
||||
})
|
||||
ctx.total_count = (ctx.total_count or 0) + 1
|
||||
end
|
||||
end
|
||||
local display_items = {}
|
||||
if ctx.remaining.n > 0 then
|
||||
local progress_msg = ctx.message or ""
|
||||
if ctx.total_servers and ctx.total_servers > 1 and ctx.remaining and ctx.remaining.n then
|
||||
local completed = math.max(0, (ctx.total_servers - ctx.remaining.n) + 1)
|
||||
progress_msg = tostring(progress_msg):gsub("%.+$", "")
|
||||
progress_msg = progress_msg .. string.format("(%d/%d)...", completed, ctx.total_servers)
|
||||
end
|
||||
table.insert(display_items, {
|
||||
title = progress_msg,
|
||||
value = "",
|
||||
italic = true,
|
||||
keep_open = true,
|
||||
selectable = false,
|
||||
icon = "spinner",
|
||||
})
|
||||
end
|
||||
-- 按 server_order 拼接当前已收到的结果
|
||||
for _, srv in ipairs(ctx.server_order or {}) do
|
||||
local list = ctx.server_items and ctx.server_items[srv]
|
||||
if list and type(list) == 'table' then
|
||||
for _, v in ipairs(list) do table.insert(display_items, v) end
|
||||
end
|
||||
end
|
||||
|
||||
if uosc_available then
|
||||
latest_menu_anime = update_menu_uosc(ctx.menu_type, ctx.menu_title, display_items, ctx.footnote, ctx.menu_cmd, ctx.query,
|
||||
{ "script-message-to", mp.get_script_name(), "cancel-active-request", ctx.menu_type })
|
||||
else
|
||||
if not ctx.first_opened.val and input_loaded and (ctx.total_count or 0) > 0 then
|
||||
ctx.first_opened.val = true
|
||||
show_message("", 0)
|
||||
input.terminate()
|
||||
mp.add_timeout(0.1, function()
|
||||
latest_menu_anime = utils.format_json(display_items)
|
||||
open_menu_select(display_items)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
ctx.remaining.n = math.max(0, ctx.remaining.n - 1)
|
||||
if ctx.remaining.n == 0 then
|
||||
pcall(do_final_update)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 打开番剧数据匹配菜单
|
||||
function get_animes(query)
|
||||
local encoded_query = url_encode(query)
|
||||
local url = options.api_server .. "/api/v2/search/anime"
|
||||
local params = "keyword=" .. encoded_query
|
||||
local full_url = url .. "?" .. params
|
||||
local server_metas = get_api_server_list(options.api_server, true)
|
||||
local servers = {}
|
||||
local server_notes = {}
|
||||
for _, m in ipairs(server_metas) do
|
||||
table.insert(servers, m.url)
|
||||
if m.note and m.note ~= '' then
|
||||
server_notes[m.url] = m.note
|
||||
end
|
||||
end
|
||||
|
||||
local items = {}
|
||||
local seen = {}
|
||||
local first_opened = false
|
||||
local remaining = #servers
|
||||
local total_servers = remaining
|
||||
local server_items = {}
|
||||
local server_order = servers
|
||||
local total_count = 0
|
||||
request_cancelled = false
|
||||
|
||||
local message = "加载数据中..."
|
||||
local menu_type = "menu_anime"
|
||||
local menu_title = "在此处输入番剧名称"
|
||||
local footnote = "使用enter或ctrl+enter进行搜索"
|
||||
local menu_cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" }
|
||||
|
||||
local function strip_trailing_dots(s)
|
||||
if not s then return "" end
|
||||
return tostring(s):gsub("%.+$", "")
|
||||
end
|
||||
|
||||
local initial_message = message
|
||||
if total_servers and total_servers > 1 then
|
||||
initial_message = strip_trailing_dots(message) .. string.format("(%d/%d)...", 0, total_servers)
|
||||
end
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
|
||||
active_request_type = menu_type
|
||||
update_menu_uosc(menu_type, menu_title, initial_message, footnote, menu_cmd, query, "spinner",
|
||||
{ "script-message-to", mp.get_script_name(), "cancel-active-request", menu_type })
|
||||
else
|
||||
show_message(message, 30)
|
||||
end
|
||||
msg.verbose("尝试获取番剧数据:" .. full_url)
|
||||
|
||||
local args = make_danmaku_request_args("GET", full_url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
show_message(initial_message, 30)
|
||||
end
|
||||
|
||||
local res = mp.command_native({ name = 'subprocess', capture_stdout = true, capture_stderr = true, args = args })
|
||||
msg.verbose("尝试获取番剧数据,servers: " .. table.concat(servers, ", ") .. " query: " .. query)
|
||||
|
||||
if not res.status or res.status ~= 0 then
|
||||
local message = "获取数据失败"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
|
||||
else
|
||||
show_message(message, 3)
|
||||
local build_args = make_build_args(encoded_query)
|
||||
|
||||
-- 构造 ctx,用于 handle_response 闭包访问和修改共享状态
|
||||
local ctx = {
|
||||
items = items,
|
||||
seen = seen,
|
||||
first_opened = { val = first_opened },
|
||||
remaining = { n = remaining },
|
||||
message = message,
|
||||
total_servers = total_servers,
|
||||
menu_type = menu_type,
|
||||
menu_title = menu_title,
|
||||
footnote = footnote,
|
||||
menu_cmd = menu_cmd,
|
||||
query = query,
|
||||
server_items = server_items,
|
||||
server_order = server_order,
|
||||
server_notes = server_notes,
|
||||
total_count = total_count,
|
||||
}
|
||||
|
||||
local handle_response = make_handle_response(ctx)
|
||||
|
||||
local cancel_fn = parallel_requests(servers, build_args, handle_response, function()
|
||||
if request_cancelled then return end
|
||||
local final_items = {}
|
||||
for _, srv in ipairs(ctx.server_order or {}) do
|
||||
local list = ctx.server_items and ctx.server_items[srv]
|
||||
if list and type(list) == 'table' then
|
||||
for _, v in ipairs(list) do table.insert(final_items, v) end
|
||||
end
|
||||
end
|
||||
msg.error("HTTP 请求失败:" .. res.stderr)
|
||||
end
|
||||
|
||||
local response = utils.parse_json(res.stdout)
|
||||
|
||||
if not response or not response.animes then
|
||||
local message = "无结果"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
|
||||
latest_menu_anime = update_menu_uosc(ctx.menu_type, ctx.menu_title, final_items, ctx.footnote, ctx.menu_cmd, ctx.query)
|
||||
else
|
||||
show_message(message, 3)
|
||||
latest_menu_anime = utils.format_json(final_items)
|
||||
if not ctx.first_opened.val and input_loaded and #final_items > 0 then
|
||||
ctx.first_opened.val = true
|
||||
show_message("", 0)
|
||||
input.terminate()
|
||||
mp.add_timeout(0.1, function()
|
||||
open_menu_select(final_items)
|
||||
end)
|
||||
end
|
||||
end
|
||||
msg.info("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
for _, anime in ipairs(response.animes) do
|
||||
table.insert(items, {
|
||||
title = anime.animeTitle,
|
||||
hint = anime.typeDescription,
|
||||
value = {
|
||||
"script-message-to",
|
||||
mp.get_script_name(),
|
||||
"search-episodes-event",
|
||||
anime.animeTitle, anime.bangumiId,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
if uosc_available then
|
||||
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
|
||||
end, { concurrency = 5, per_request_timeout = 30 })
|
||||
active_request_cancel = cancel_fn
|
||||
active_request_type = menu_type
|
||||
end
|
||||
|
||||
function get_episodes(animeTitle, bangumiId)
|
||||
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId
|
||||
function get_episodes(animeTitle, bangumiId, api_server)
|
||||
local url = api_server .. "/api/v2/bangumi/" .. bangumiId
|
||||
local items = {}
|
||||
|
||||
local message = "加载数据中..."
|
||||
@@ -91,7 +274,9 @@ function get_episodes(animeTitle, bangumiId)
|
||||
local footnote = "使用 / 打开筛选"
|
||||
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
active_request_type = menu_type
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote, nil, nil, "spinner",
|
||||
{ "script-message-to", mp.get_script_name(), "cancel-active-request", menu_type })
|
||||
else
|
||||
show_message(message, 30)
|
||||
end
|
||||
@@ -102,60 +287,70 @@ function get_episodes(animeTitle, bangumiId)
|
||||
return
|
||||
end
|
||||
|
||||
local res = mp.command_native({ name = 'subprocess', capture_stdout = true, capture_stderr = true, args = args })
|
||||
|
||||
if not res.status or res.status ~= 0 then
|
||||
local message = "获取数据失败"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
request_cancelled = false
|
||||
active_request_type = menu_type
|
||||
active_request_cancel = call_cmd_async(args, function(err, stdout)
|
||||
active_request_cancel = nil
|
||||
if request_cancelled then
|
||||
return
|
||||
end
|
||||
msg.error("HTTP 请求失败:" .. res.stderr)
|
||||
end
|
||||
|
||||
local response = utils.parse_json(res.stdout)
|
||||
|
||||
if not response or not response.bangumi or not response.bangumi.episodes then
|
||||
local message = "无结果"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
if err then
|
||||
local message = "获取数据失败"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.error("HTTP 请求失败:" .. tostring(err))
|
||||
return
|
||||
end
|
||||
msg.info("无结果")
|
||||
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,
|
||||
})
|
||||
local response = utils.parse_json(stdout)
|
||||
if not response or not response.bangumi or not response.bangumi.episodes then
|
||||
local message = "无结果"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.info("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
for _, episode in ipairs(response.bangumi.episodes) do
|
||||
table.insert(items, {
|
||||
title = episode.episodeTitle,
|
||||
hint = episode.episodeNumber,
|
||||
value = { "script-message-to", mp.get_script_name(), "load-danmaku",
|
||||
animeTitle, episode.episodeTitle, episode.episodeId },
|
||||
title = "← 返回搜索结果",
|
||||
value = { "script-message-to", mp.get_script_name(), "open-latest-menu-anime", latest_menu_anime },
|
||||
keep_open = false,
|
||||
selectable = true,
|
||||
})
|
||||
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()
|
||||
open_menu_select(items)
|
||||
end)
|
||||
end
|
||||
for _, episode in ipairs(response.bangumi.episodes) do
|
||||
table.insert(items, {
|
||||
title = episode.episodeTitle,
|
||||
hint = episode.episodeNumber,
|
||||
value = { "script-message-to", mp.get_script_name(), "load-danmaku",
|
||||
animeTitle, episode.episodeTitle, episode.episodeId, api_server },
|
||||
keep_open = false,
|
||||
selectable = true,
|
||||
})
|
||||
end
|
||||
|
||||
if uosc_available then
|
||||
footnote = mp.get_property("filename")
|
||||
update_menu_uosc(menu_type, menu_title, items, footnote)
|
||||
elseif input_loaded then
|
||||
show_message("", 0)
|
||||
input.terminate()
|
||||
mp.add_timeout(0.1, function()
|
||||
open_menu_select(items)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
active_request_type = menu_type
|
||||
end
|
||||
|
||||
function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_cmd, query)
|
||||
function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_cmd, query, message_icon, on_close)
|
||||
local items = {}
|
||||
if type(menu_item) == "string" then
|
||||
table.insert(items, {
|
||||
@@ -165,7 +360,7 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
|
||||
keep_open = true,
|
||||
selectable = false,
|
||||
align = "center",
|
||||
icon = "spinner",
|
||||
icon = message_icon or "",
|
||||
})
|
||||
else
|
||||
items = menu_item
|
||||
@@ -181,8 +376,19 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
|
||||
search_suggestion = query,
|
||||
items = items,
|
||||
}
|
||||
|
||||
if on_close ~= nil then
|
||||
menu_props.on_close = on_close
|
||||
end
|
||||
|
||||
local current_menu_type = mp.get_property_native('user-data/uosc/menu/type')
|
||||
local cmd = "open-menu"
|
||||
if current_menu_type and tostring(current_menu_type) == tostring(menu_type) then
|
||||
cmd = "update-menu"
|
||||
end
|
||||
|
||||
local json_props = utils.format_json(menu_props)
|
||||
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
|
||||
mp.commandv("script-message-to", "uosc", cmd, json_props)
|
||||
|
||||
return json_props
|
||||
end
|
||||
@@ -200,6 +406,7 @@ function open_menu_select(menu_items, is_time)
|
||||
items = item_titles,
|
||||
submit = function(id)
|
||||
input.terminate()
|
||||
perform_cancel_active_request()
|
||||
local v = item_values[id]
|
||||
if type(v) == 'table' then
|
||||
mp.commandv(unpack(v))
|
||||
@@ -207,6 +414,11 @@ function open_menu_select(menu_items, is_time)
|
||||
mp.command(v)
|
||||
end
|
||||
end,
|
||||
closed = function()
|
||||
show_message("", 0)
|
||||
input.terminate()
|
||||
perform_cancel_active_request()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -264,7 +476,9 @@ function open_input_menu()
|
||||
if uosc_available then
|
||||
open_input_menu_uosc()
|
||||
elseif input_loaded then
|
||||
open_input_menu_get()
|
||||
mp.add_timeout(0.01, function()
|
||||
open_input_menu_get()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -283,23 +497,29 @@ function open_add_menu_get()
|
||||
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,
|
||||
serial = serial + 1
|
||||
local action = source.blocked and "unblock" or "block"
|
||||
local text = string.format(" [%02d] %s [来源:弹幕服务器%s] ", serial, url,
|
||||
source.blocked and "(已屏蔽)" or "(未屏蔽)")
|
||||
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})
|
||||
else
|
||||
action = "delete"
|
||||
text = string.format(" [%02d] %s [来源:用户添加] ", serial, url)
|
||||
serial = serial + 1
|
||||
local action1 = source.blocked and "unblock" or "block"
|
||||
local text1 = string.format(" [%02d] %s [来源:用户添加]%s ", serial, url, source.blocked and " (已屏蔽)" or "(未屏蔽)")
|
||||
local style1 = (tonumber(select_num) == serial) and "{\\c&HFFDE7F&\\b1}" or (action1 == "unblock" and "{\\c&H4C4CC3&\\b0}" or "{\\c&HCCCCCC&\\b0}")
|
||||
deal_value[serial] = {value = url, action = action1}
|
||||
table.insert(menu_log, {text = text1, style = style1})
|
||||
|
||||
serial = serial + 1
|
||||
local action2 = "delete"
|
||||
local text2 = string.format(" [%02d] %s [来源:用户添加] (删除) ", serial, url)
|
||||
local style2 = (tonumber(select_num) == serial) and "{\\c&HFFDE7F&\\b1}" or "{\\c&HCCCCCC&\\b0}"
|
||||
deal_value[serial] = {value = url, action = action2}
|
||||
table.insert(menu_log, {text = text2, style = style2})
|
||||
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
|
||||
|
||||
@@ -386,7 +606,7 @@ function open_add_menu_uosc()
|
||||
local sources = {}
|
||||
for url, source in pairs(DANMAKU.sources) do
|
||||
if source.data then
|
||||
local item = {title = url, value = url, keep_open = true,}
|
||||
local item = {title = utf8_sub(url, 1, 100), value = url, keep_open = true,}
|
||||
if source.from == "api_server" then
|
||||
if source.blocked then
|
||||
item.hint = "来源:弹幕服务器(已屏蔽)"
|
||||
@@ -397,7 +617,17 @@ function open_add_menu_uosc()
|
||||
end
|
||||
else
|
||||
item.hint = "来源:用户添加"
|
||||
item.actions = {{icon = "delete", name = "delete", label = "删除"},}
|
||||
if source.blocked then
|
||||
item.actions = {
|
||||
{icon = "check", name = "unblock", label = "解除屏蔽"},
|
||||
{icon = "delete", name = "delete", label = "删除"},
|
||||
}
|
||||
else
|
||||
item.actions = {
|
||||
{icon = "not_interested", name = "block", label = "屏蔽"},
|
||||
{icon = "delete", name = "delete", label = "删除"},
|
||||
}
|
||||
end
|
||||
end
|
||||
table.insert(sources, item)
|
||||
end
|
||||
@@ -421,7 +651,9 @@ function open_add_menu()
|
||||
if uosc_available then
|
||||
open_add_menu_uosc()
|
||||
elseif input_loaded then
|
||||
open_add_menu_get()
|
||||
mp.add_timeout(0.01, function()
|
||||
open_add_menu_get()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -915,7 +1147,7 @@ function open_delay_menu_uosc(source_url, status)
|
||||
end
|
||||
end
|
||||
end
|
||||
local item = {title = url, value = url, keep_open = true,}
|
||||
local item = {title = utf8_sub(url, 1, 100), value = url, keep_open = true,}
|
||||
item.hint = "当前弹幕源延迟:" .. string.format("%.1f", delay + 1e-10) .. "秒"
|
||||
item.active = url == source_url
|
||||
table.insert(sources, item)
|
||||
@@ -1013,7 +1245,10 @@ function open_add_total_menu_select()
|
||||
prompt = '选择:',
|
||||
items = item_titles,
|
||||
submit = function(id)
|
||||
mp.commandv(unpack(item_values[id]))
|
||||
local cmd = item_values[id]
|
||||
mp.add_timeout(0.1, function()
|
||||
mp.commandv(unpack(cmd))
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
@@ -1026,6 +1261,10 @@ function open_add_total_menu()
|
||||
end
|
||||
end
|
||||
|
||||
function toggle_danmaku_switch(value)
|
||||
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", value)
|
||||
mp.set_property_bool(DANMAKU_SWITCH_ON, value == "on")
|
||||
end
|
||||
|
||||
mp.commandv(
|
||||
"script-message-to",
|
||||
@@ -1114,11 +1353,12 @@ mp.register_script_message("set", function(prop, value)
|
||||
hide_danmaku_func()
|
||||
end
|
||||
|
||||
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", value)
|
||||
toggle_danmaku_switch(value)
|
||||
end)
|
||||
|
||||
-- 注册函数给 uosc 按钮使用
|
||||
mp.register_script_message("search-anime-event", function(query)
|
||||
perform_cancel_active_request()
|
||||
if uosc_available then
|
||||
mp.commandv("script-message-to", "uosc", "close-menu", "menu_danmaku")
|
||||
end
|
||||
@@ -1129,19 +1369,20 @@ mp.register_script_message("search-anime-event", function(query)
|
||||
get_animes(query)
|
||||
end
|
||||
end)
|
||||
mp.register_script_message("search-episodes-event", function(animeTitle, bangumiId)
|
||||
mp.register_script_message("search-episodes-event", function(animeTitle, bangumiId, api_server)
|
||||
perform_cancel_active_request()
|
||||
if uosc_available then
|
||||
mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime")
|
||||
end
|
||||
get_episodes(animeTitle, bangumiId)
|
||||
|
||||
get_episodes(animeTitle, bangumiId, api_server)
|
||||
end)
|
||||
|
||||
-- Register script message to show the input menu
|
||||
mp.register_script_message("load-danmaku", function(animeTitle, episodeTitle, episodeId)
|
||||
mp.register_script_message("load-danmaku", function(animeTitle, episodeTitle, episodeId, api_server)
|
||||
ENABLED = true
|
||||
DANMAKU.anime = animeTitle
|
||||
DANMAKU.episode = episodeTitle
|
||||
set_episode_id(episodeId, true)
|
||||
set_episode_id(episodeId, true, api_server)
|
||||
end)
|
||||
|
||||
mp.register_script_message("add-source-event", function(query)
|
||||
@@ -1177,6 +1418,10 @@ mp.register_script_message("open-latest-menu-anime", function ()
|
||||
end
|
||||
end)
|
||||
|
||||
mp.register_script_message('cancel-active-request', function(menu_type)
|
||||
perform_cancel_active_request(menu_type)
|
||||
end)
|
||||
|
||||
mp.register_script_message("setup-danmaku-style", function(query, text)
|
||||
local event = utils.parse_json(query)
|
||||
if event ~= nil then
|
||||
@@ -1273,7 +1518,7 @@ mp.register_script_message("setup-source-delay", function(query, text)
|
||||
end
|
||||
local delay = parse_delay_input(text)
|
||||
if delay ~= nil then
|
||||
mp.commandv("script-message", "danmaku-delay", tostring(delay), "0")
|
||||
mp.commandv("script-message", "danmaku-delay", tostring(delay), "0", tostring(query))
|
||||
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
|
||||
mp.add_timeout(0.1, function()
|
||||
open_delay_menu(query, "refresh")
|
||||
|
||||
@@ -3,7 +3,9 @@ local opt = require("mp.options")
|
||||
-- 选项
|
||||
options = {
|
||||
-- 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
|
||||
api_server = "https://api.dandanplay.net",
|
||||
-- 可指定多个用逗号分隔的有序 api_server 列表
|
||||
-- 支持每项使用 '|' 或 '#' 分隔备注,例如: "https://a.example.com|备用A" 或 "https://b.example.com#备用B"
|
||||
api_server = "https://danmaku-api.152468.xyz",
|
||||
-- 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
|
||||
-- 可用: https://api.danmu.icu,https://dmku.hls.one
|
||||
fallback_server = "https://api.danmu.icu",
|
||||
@@ -15,6 +17,10 @@ options = {
|
||||
autoload_local_danmaku = false,
|
||||
autoload_for_url = false,
|
||||
save_danmaku = false,
|
||||
-- 指定弹幕保存目录。为空时保存到视频同目录;目录需要用户提前创建
|
||||
save_danmaku_path = "",
|
||||
-- 指定 save_danmaku_path 的应用范围:local / url / all
|
||||
save_danmaku_path_mode = "local",
|
||||
user_agent = "mpv_danmaku/1.0",
|
||||
proxy = "",
|
||||
-- 可选:向 HTTP 请求传递 cookie.txt 文件路径
|
||||
@@ -25,6 +31,8 @@ options = {
|
||||
fps = "60/1.001",
|
||||
-- 指定合并重复弹幕的时间间隔的容差值,单位为秒。默认值: -1,表示禁用
|
||||
merge_tolerance = -1,
|
||||
-- 合并重复弹幕时是否强制合并类型和颜色不同的弹幕。默认值: false,表示仅合并类型和颜色相同的弹幕
|
||||
merge_without_style = false,
|
||||
-- 指定弹幕关联历史记录文件的路径,支持绝对路径和相对路径
|
||||
history_path = "~~/danmaku-history.json",
|
||||
open_search_danmaku_menu_key = "Ctrl+d",
|
||||
|
||||
@@ -10,6 +10,33 @@ local function ass_escape(text)
|
||||
:gsub("\n", "\\N")
|
||||
end
|
||||
|
||||
-- 构建 per-source 的延迟查询函数
|
||||
local function make_delay_lookup(source)
|
||||
local segments = nil
|
||||
local prefix = nil
|
||||
if source and 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
|
||||
|
||||
return function(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
|
||||
end
|
||||
|
||||
local function xml_unescape(str)
|
||||
return str:gsub(""", "\"")
|
||||
:gsub("'", "'")
|
||||
@@ -154,62 +181,103 @@ local function merge_duplicate_danmaku(danmakus, threshold)
|
||||
local groups = {}
|
||||
|
||||
for _, d in ipairs(danmakus) do
|
||||
local tkey = tostring(d.type or "")
|
||||
local ckey = tostring(d.color or "")
|
||||
local tkey = options.merge_without_style and "any" or tostring(d.type 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)
|
||||
groups[text] = groups[text] or {}
|
||||
groups[text][tkey] = groups[text][tkey] or {}
|
||||
table.insert(groups[text][tkey], d)
|
||||
end
|
||||
|
||||
local final_groups = {}
|
||||
|
||||
-- 颜色合并
|
||||
for _, type_groups in pairs(groups) do
|
||||
for _, list in pairs(type_groups) do
|
||||
if options.merge_without_style then
|
||||
table.insert(final_groups, list)
|
||||
else
|
||||
local color_groups = {}
|
||||
-- 利用弹幕同色大量重复的特征,缓存 颜色值 -> color_groups 索引
|
||||
local exact_color_cache = {}
|
||||
|
||||
for _, d in ipairs(list) do
|
||||
local c = d.color or 16777215
|
||||
local g_idx = exact_color_cache[c]
|
||||
|
||||
if g_idx then
|
||||
-- 命中缓存,直接插入
|
||||
table.insert(color_groups[g_idx], d)
|
||||
else
|
||||
-- 未命中的颜色值,计算它是否落入已有代表颜色的 15 容差范围内
|
||||
local found = false
|
||||
for i, cg in ipairs(color_groups) do
|
||||
if color_dist(cg[1].color or 16777215, c) <= 15 then
|
||||
table.insert(cg, d)
|
||||
-- 将该颜色值缓存为这个代表颜色的索引,后续遇到完全相同的颜色值可以直接命中
|
||||
exact_color_cache[c] = i
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- 超出容差范围的全新颜色代表,开辟新组
|
||||
if not found then
|
||||
table.insert(color_groups, {d})
|
||||
exact_color_cache[c] = #color_groups
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, cg in ipairs(color_groups) do
|
||||
table.insert(final_groups, cg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local merged = {}
|
||||
local abs = math.abs
|
||||
|
||||
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)
|
||||
for _, group in ipairs(final_groups) 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 abs(group[j].time - base.time) <= threshold do
|
||||
times[#times+1] = 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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -259,8 +327,11 @@ local function limit_danmaku(danmakus, limit)
|
||||
end
|
||||
|
||||
-- 解析 XML 弹幕
|
||||
local function parse_xml_danmaku(xml_string)
|
||||
function parse_xml_danmaku(xml_string)
|
||||
local danmakus = {}
|
||||
if not xml_string then
|
||||
return danmakus
|
||||
end
|
||||
-- [^>]* 匹配其他 attributes
|
||||
-- %f[^%s] 确保 p= 前面是空白字符
|
||||
for p_attr, text in xml_string:gmatch('<d%s+[^>]*%f[^%s]p="([^"]+)"[^>]*>([^<]+)</d>') do
|
||||
@@ -287,7 +358,7 @@ local function parse_xml_danmaku(xml_string)
|
||||
end
|
||||
|
||||
-- 解析 JSON 弹幕
|
||||
local function parse_json_danmaku(json_string)
|
||||
function parse_json_danmaku(json_string)
|
||||
local danmakus = {}
|
||||
if json_string:sub(1, 3) == "\239\187\191" then
|
||||
json_string = json_string:sub(4)
|
||||
@@ -469,10 +540,22 @@ end
|
||||
-- 将弹幕转换为 XML 格式
|
||||
function convert_danmaku_to_xml(danmaku_out)
|
||||
local danmakus = {}
|
||||
for _, source in pairs(DANMAKU.sources) do
|
||||
for url, source in pairs(DANMAKU.sources) do
|
||||
if not source.blocked and source.data then
|
||||
local get_cached_delay = make_delay_lookup(source)
|
||||
|
||||
for _, d in ipairs(source.data) do
|
||||
table.insert(danmakus, d)
|
||||
local base_time = d.orig_time or d.time
|
||||
local adjusted_time = base_time + get_cached_delay(base_time)
|
||||
table.insert(danmakus, {
|
||||
orig_time = d.orig_time,
|
||||
time = adjusted_time,
|
||||
type = d.type,
|
||||
size = d.size,
|
||||
color = d.color,
|
||||
text = d.text,
|
||||
source = url,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -521,29 +604,7 @@ 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 get_cached_delay = make_delay_lookup(source)
|
||||
|
||||
local list = {}
|
||||
for _, d in ipairs(source.data) do
|
||||
@@ -711,6 +772,7 @@ function convert_danmaku_to_ass_events(force)
|
||||
clean_text = clean_text,
|
||||
pos = pos,
|
||||
move = move,
|
||||
layer = (style == "R2L") and 0 or 1,
|
||||
source = d.source,
|
||||
}
|
||||
table.insert(ass_events, event)
|
||||
|
||||
@@ -5,7 +5,8 @@ local unpack = unpack or table.unpack
|
||||
|
||||
local osd_width, osd_height, pause = 0, 0, true
|
||||
local time_pos_observer_active = false
|
||||
local overlay = mp.create_osd_overlay('ass-events')
|
||||
local overlay_low = mp.create_osd_overlay('ass-events')
|
||||
local overlay_high = mp.create_osd_overlay('ass-events')
|
||||
|
||||
local function realtime_position_text(event, pos, displayarea)
|
||||
if not event.move then
|
||||
@@ -51,7 +52,8 @@ function render(pos_arg)
|
||||
end
|
||||
|
||||
if not pos then
|
||||
overlay:remove()
|
||||
overlay_low:remove()
|
||||
overlay_high:remove()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -67,7 +69,8 @@ function render(pos_arg)
|
||||
fontsize = options.fontsize - ratio * 2
|
||||
end
|
||||
|
||||
local ass_events = {}
|
||||
local ass_events_low = {}
|
||||
local ass_events_high = {}
|
||||
local max_display = math.max(options.scrolltime, options.fixtime)
|
||||
local window_start = pos - max_display
|
||||
|
||||
@@ -99,22 +102,36 @@ function render(pos_arg)
|
||||
|
||||
-- 构建 ASS 字符串
|
||||
local ass_text = text and (ass_prefix .. text)
|
||||
|
||||
table.insert(ass_events, ass_text)
|
||||
if ass_text then
|
||||
if event.layer == nil or tonumber(event.layer) == 0 then
|
||||
table.insert(ass_events_low, ass_text)
|
||||
else
|
||||
table.insert(ass_events_high, ass_text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
overlay.res_x = width
|
||||
overlay.res_y = height
|
||||
overlay.data = table.concat(ass_events, '\n')
|
||||
overlay:update()
|
||||
-- 写入低层(滚动)和高层(顶/底)overlay,并设置 z 值以控制堆叠
|
||||
overlay_low.res_x = width
|
||||
overlay_low.res_y = height
|
||||
overlay_low.z = 0
|
||||
overlay_low.data = table.concat(ass_events_low, '\n')
|
||||
overlay_low:update()
|
||||
|
||||
overlay_high.res_x = width
|
||||
overlay_high.res_y = height
|
||||
overlay_high.z = 1
|
||||
overlay_high.data = table.concat(ass_events_high, '\n')
|
||||
overlay_high:update()
|
||||
end
|
||||
|
||||
local function time_pos_callback(_, time_pos)
|
||||
if time_pos then
|
||||
render(time_pos)
|
||||
else
|
||||
overlay:remove()
|
||||
overlay_low:remove()
|
||||
overlay_high:remove()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -137,7 +154,7 @@ function render_danmaku(from_menu, no_osd)
|
||||
if not no_osd then
|
||||
show_loaded(true)
|
||||
end
|
||||
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
|
||||
toggle_danmaku_switch("on")
|
||||
show_danmaku_func()
|
||||
else
|
||||
show_message("")
|
||||
@@ -179,7 +196,8 @@ function hide_danmaku_func()
|
||||
stop_time_observer()
|
||||
mp.set_property_bool(HAS_DANMAKU, false)
|
||||
set_danmaku_visibility(false)
|
||||
overlay:remove()
|
||||
overlay_low:remove()
|
||||
overlay_high:remove()
|
||||
if filter_state("danmaku") then
|
||||
mp.commandv("vf", "remove", "@danmaku")
|
||||
end
|
||||
@@ -235,7 +253,8 @@ end)
|
||||
mp.add_hook("on_unload", 50, function()
|
||||
COMMENTS, DELAY = nil, 0
|
||||
stop_time_observer()
|
||||
overlay:remove()
|
||||
overlay_low:remove()
|
||||
overlay_high:remove()
|
||||
mp.set_property_native(DELAY_PROPERTY, 0)
|
||||
if filter_state("danmaku") then
|
||||
mp.commandv("vf", "remove", "@danmaku")
|
||||
@@ -256,4 +275,5 @@ mp.add_hook("on_unload", 50, function()
|
||||
end
|
||||
|
||||
DANMAKU = {sources = {}, count = 1}
|
||||
mp.set_property_native(DANMAKU_COUNT, 0)
|
||||
end)
|
||||
@@ -201,6 +201,18 @@ function is_protocol(path)
|
||||
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
|
||||
end
|
||||
|
||||
function sanitize_filename(name)
|
||||
if not name or name == "" then
|
||||
return "danmaku"
|
||||
end
|
||||
name = name:gsub('[/\\:*?"<>|]', "_")
|
||||
:gsub('^%s*(.-)%s*$', '%1')
|
||||
if name == "" then
|
||||
return "danmaku"
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
function hex_to_bin(hexstr)
|
||||
return (hexstr:gsub('..', function (cc)
|
||||
return string.char(tonumber(cc, 16))
|
||||
@@ -214,6 +226,9 @@ end
|
||||
function hex_to_int_color(hex_color)
|
||||
-- 移除颜色代码中的'#'字符
|
||||
hex_color = hex_color:sub(2) -- 只保留颜色代码部分
|
||||
if not hex_color:match("^%x%x%x%x%x%x$") then
|
||||
return 16777215
|
||||
end
|
||||
|
||||
-- 提取R, G, B的十六进制值并转为整数
|
||||
local r = tonumber(hex_color:sub(1, 2), 16)
|
||||
@@ -226,14 +241,14 @@ function hex_to_int_color(hex_color)
|
||||
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
|
||||
function color_dist(c1, c2)
|
||||
local r1 = math.floor(c1 / 65536) % 256
|
||||
local g1 = math.floor(c1 / 256) % 256
|
||||
local b1 = c1 % 256
|
||||
local r2 = math.floor(c2 / 65536) % 256
|
||||
local g2 = math.floor(c2 / 256) % 256
|
||||
local b2 = c2 % 256
|
||||
return math.sqrt((r1-r2)^2 + (g1-g2)^2 + (b1-b2)^2)
|
||||
end
|
||||
|
||||
-- url编码转换
|
||||
@@ -424,6 +439,42 @@ function contains_any(tab, val)
|
||||
return false
|
||||
end
|
||||
|
||||
-- 将一个逗号分隔的 api_server 字符串解析为有序列表
|
||||
function get_api_server_list(api_server_str, meta)
|
||||
local want_meta = meta or false
|
||||
local metas = {}
|
||||
if not api_server_str or api_server_str == "" then
|
||||
if want_meta then return metas end
|
||||
return {}
|
||||
end
|
||||
|
||||
for part in string.gmatch(api_server_str, "[^,]+") do
|
||||
local s = part:gsub('^%s*(.-)%s*$', '%1')
|
||||
if s ~= '' then
|
||||
local u, n = s:match('^(.-)[|#](.*)$')
|
||||
local url = nil
|
||||
local note = nil
|
||||
if u then
|
||||
url = u:gsub('^%s*(.-)%s*$', '%1')
|
||||
note = n and n:gsub('^%s*(.-)%s*$', '%1') or nil
|
||||
else
|
||||
url = s:gsub('^%s*(.-)%s*$', '%1')
|
||||
note = nil
|
||||
end
|
||||
table.insert(metas, { url = url, note = note })
|
||||
end
|
||||
end
|
||||
|
||||
if #metas == 0 and api_server_str ~= '' then
|
||||
table.insert(metas, { url = api_server_str, note = nil })
|
||||
end
|
||||
|
||||
if want_meta then return metas end
|
||||
local urls = {}
|
||||
for _, m in ipairs(metas) do table.insert(urls, m.url) end
|
||||
return urls
|
||||
end
|
||||
|
||||
--读history 和 写history
|
||||
function read_file(file_path)
|
||||
local file = io.open(file_path, "r") -- 打开文件,"r"表示只读模式
|
||||
@@ -727,10 +778,29 @@ function number_to_chinese(num)
|
||||
return result
|
||||
end
|
||||
|
||||
-- 内部的异步运行计数
|
||||
local async_running_count = 0
|
||||
|
||||
function mark_async_start()
|
||||
async_running_count = async_running_count + 1
|
||||
end
|
||||
|
||||
function mark_async_end()
|
||||
if async_running_count > 0 then
|
||||
async_running_count = async_running_count - 1
|
||||
end
|
||||
end
|
||||
|
||||
function is_async_running()
|
||||
return async_running_count > 0
|
||||
end
|
||||
|
||||
-- 异步执行命令
|
||||
-- 同时返回 abort 函数,用于取消异步命令
|
||||
function call_cmd_async(args, callback)
|
||||
async_running = true
|
||||
-- 标记异步开始
|
||||
mark_async_start()
|
||||
|
||||
local abort_signal = mp.command_native_async({
|
||||
name = 'subprocess',
|
||||
capture_stderr = true,
|
||||
@@ -738,9 +808,14 @@ function call_cmd_async(args, callback)
|
||||
playback_only = true,
|
||||
args = args,
|
||||
}, function(success, result, error)
|
||||
-- 标记异步结束
|
||||
mark_async_end()
|
||||
|
||||
if not success or not result or result.status ~= 0 then
|
||||
local exit_code = (result and result.status or 'unknown')
|
||||
local message = error or (result and result.stdout .. result.stderr) or ''
|
||||
local message = error or (
|
||||
result and ((result.stdout or '') .. (result.stderr or ''))
|
||||
) or ''
|
||||
callback('Calling failed. Exit code: ' .. exit_code .. ' Error: ' .. message, {})
|
||||
return
|
||||
end
|
||||
@@ -752,4 +827,166 @@ function call_cmd_async(args, callback)
|
||||
return function()
|
||||
mp.abort_async_command(abort_signal)
|
||||
end
|
||||
end
|
||||
|
||||
local function yield_once()
|
||||
local co = coroutine.running()
|
||||
mp.add_timeout(0, function()
|
||||
coroutine.resume(co)
|
||||
end)
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
local function make_safe_resume(co, timer_ref)
|
||||
local resumed = false
|
||||
|
||||
return function(...)
|
||||
if resumed then return end
|
||||
resumed = true
|
||||
|
||||
if timer_ref.timer then
|
||||
timer_ref.timer:kill()
|
||||
timer_ref.timer = nil
|
||||
end
|
||||
|
||||
local ok, err = coroutine.resume(co, ...)
|
||||
if not ok then
|
||||
mp.msg.warn("resume failed: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function await_call_cmd(args, timeout, on_start)
|
||||
local co = coroutine.running()
|
||||
|
||||
local timer_ref = { timer = nil }
|
||||
local safe_resume = make_safe_resume(co, timer_ref)
|
||||
|
||||
local abort_fn = call_cmd_async(args, function(err, out)
|
||||
safe_resume(err, out)
|
||||
end)
|
||||
|
||||
if on_start and type(on_start) == 'function' then
|
||||
pcall(on_start, abort_fn)
|
||||
end
|
||||
|
||||
if timeout and type(timeout) == 'number' and timeout > 0 then
|
||||
timer_ref.timer = mp.add_timeout(timeout, function()
|
||||
if abort_fn then pcall(abort_fn) end
|
||||
safe_resume("timeout", nil)
|
||||
end)
|
||||
end
|
||||
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
-- 并行请求调度器
|
||||
-- servers: { "url1", "url2", ... }
|
||||
-- build_args_fn(server) -> args
|
||||
-- per_response_cb(server, err, stdout)
|
||||
-- final_cb() 可选
|
||||
-- opts: { concurrency=3, per_request_timeout=10 }
|
||||
function parallel_requests(servers, build_args_fn, per_response_cb, final_cb, opts)
|
||||
if type(final_cb) == 'table' and opts == nil then
|
||||
opts = final_cb
|
||||
final_cb = nil
|
||||
end
|
||||
|
||||
opts = opts or {}
|
||||
local concurrency = opts.concurrency or 3
|
||||
local timeout = opts.per_request_timeout or 10
|
||||
|
||||
local in_flight_abort = {}
|
||||
local in_flight_count = 0
|
||||
local aborted = false
|
||||
local idx = 1
|
||||
local monitor = nil
|
||||
|
||||
-- worker 协程
|
||||
local function worker()
|
||||
while true do
|
||||
if aborted then return end
|
||||
|
||||
local i = idx
|
||||
if i > #servers then return end
|
||||
idx = idx + 1
|
||||
|
||||
local server = servers[i]
|
||||
local args = build_args_fn(server)
|
||||
|
||||
if not args then
|
||||
if not aborted then
|
||||
pcall(per_response_cb, server, "no_args", nil)
|
||||
end
|
||||
|
||||
yield_once()
|
||||
else
|
||||
local ok, err, out = pcall(function()
|
||||
return await_call_cmd(args, timeout, function(ab)
|
||||
in_flight_abort[i] = ab
|
||||
in_flight_count = in_flight_count + 1
|
||||
end)
|
||||
end)
|
||||
|
||||
-- 请求结束
|
||||
if in_flight_abort[i] then
|
||||
in_flight_abort[i] = nil
|
||||
in_flight_count = in_flight_count - 1
|
||||
end
|
||||
|
||||
if aborted then return end
|
||||
|
||||
if not ok then
|
||||
pcall(per_response_cb, server, tostring(err), nil)
|
||||
else
|
||||
pcall(per_response_cb, server, err, out)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 启动 worker
|
||||
local workers = {}
|
||||
for i = 1, math.min(concurrency, #servers) do
|
||||
local co = coroutine.create(worker)
|
||||
local ok, res = coroutine.resume(co)
|
||||
if not ok then
|
||||
mp.msg.warn("worker start failed: " .. tostring(res))
|
||||
end
|
||||
workers[#workers + 1] = co
|
||||
end
|
||||
|
||||
-- monitor(完成检测)
|
||||
monitor = mp.add_periodic_timer(0.05, function()
|
||||
if aborted then
|
||||
monitor:kill()
|
||||
return
|
||||
end
|
||||
|
||||
local all_assigned = (idx > #servers)
|
||||
local any_inflight = (in_flight_count > 0)
|
||||
|
||||
if all_assigned and not any_inflight then
|
||||
monitor:kill()
|
||||
if final_cb then pcall(final_cb) end
|
||||
end
|
||||
end)
|
||||
|
||||
-- 返回取消函数
|
||||
return function()
|
||||
if aborted then return end
|
||||
aborted = true
|
||||
|
||||
for i, abort_fn in pairs(in_flight_abort) do
|
||||
if abort_fn then pcall(abort_fn) end
|
||||
in_flight_abort[i] = nil
|
||||
end
|
||||
|
||||
in_flight_count = 0
|
||||
|
||||
if monitor then
|
||||
monitor:kill()
|
||||
monitor = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local function resolve_bahamut_sn(input_string)
|
||||
local start_index = 0
|
||||
local end_index = 0
|
||||
local count = 0
|
||||
for i = 1, #input_string do
|
||||
if input_string:sub(i, i) == ":" then
|
||||
count = count + 1
|
||||
if count == 2 then
|
||||
start_index = i
|
||||
elseif count == 3 then
|
||||
end_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if start_index > 0 and end_index > 0 then
|
||||
return input_string:sub(start_index + 1, end_index - 1)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
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
|
||||
|
||||
-- 为 bahamut 网站的视频播放加载弹幕
|
||||
function load_danmaku_for_bahamut(path, callback)
|
||||
callback = callback or function() end
|
||||
local path = path:gsub('%%(%x%x)', hex_to_char)
|
||||
local sn = resolve_bahamut_sn(path)
|
||||
if sn == nil then
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
local url = "https://ani.gamer.com.tw/ajax/danmuGet.php"
|
||||
local temp_file = "bahamut-" .. PID .. ".json"
|
||||
local danmaku_json = utils.join_path(DANMAKU_PATH, temp_file)
|
||||
local arg = {
|
||||
"curl",
|
||||
"-X",
|
||||
"POST",
|
||||
"-d",
|
||||
"sn=" .. sn,
|
||||
"-L",
|
||||
"-s",
|
||||
"--user-agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
|
||||
"--header",
|
||||
"Origin: https://ani.gamer.com.tw",
|
||||
"--header",
|
||||
"Content-Type: application/x-www-form-urlencoded;charset=utf-8",
|
||||
"--header",
|
||||
"Accept: application/json",
|
||||
"--header",
|
||||
"Authority: ani.gamer.com.tw",
|
||||
"--output",
|
||||
danmaku_json,
|
||||
url,
|
||||
}
|
||||
|
||||
if options.proxy ~= "" then
|
||||
table.insert(arg, '-x')
|
||||
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)
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
if not file_exists(danmaku_json) then
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local comments_json = read_file(danmaku_json)
|
||||
os.remove(danmaku_json)
|
||||
local comments = utils.parse_json(comments_json)
|
||||
if not comments then
|
||||
callback(false)
|
||||
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)
|
||||
save_danmaku_json("https://ani.gamer.com.tw/animeVideo.php?sn=" .. sn, final_json_str)
|
||||
load_danmaku(true)
|
||||
callback(true)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,246 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local 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'
|
||||
|
||||
local function build_curl_args(url, extra_headers)
|
||||
local args = {
|
||||
'curl', '-L', '-s', '--compressed', '--user-agent', user_agent
|
||||
}
|
||||
extra_headers = extra_headers or {}
|
||||
for _, h in ipairs(extra_headers) do
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, h)
|
||||
end
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
if options.proxy and options.proxy ~= '' then
|
||||
table.insert(args, '-x')
|
||||
table.insert(args, options.proxy)
|
||||
end
|
||||
table.insert(args, url)
|
||||
return args
|
||||
end
|
||||
|
||||
local function get_cid()
|
||||
local cid, danmaku_id = nil, nil
|
||||
local tracks = mp.get_property_native("track-list")
|
||||
for _, track in ipairs(tracks) do
|
||||
if track["lang"] == "danmaku" then
|
||||
cid = track["external-filename"]:match("/(%d-)%.xml$")
|
||||
danmaku_id = track["id"]
|
||||
break
|
||||
end
|
||||
end
|
||||
return cid, danmaku_id
|
||||
end
|
||||
|
||||
local function get_bilibili_id_and_page(path)
|
||||
local bvid, aid, page = nil, nil, nil
|
||||
if not bvid then
|
||||
bvid = path:match("/video/(BV[%w]+)")
|
||||
or path:match("[?&]bvid=(BV[%w]+)")
|
||||
end
|
||||
if not aid then
|
||||
aid = path:match("/video/av([%d]+)")
|
||||
end
|
||||
if not page then
|
||||
page = tonumber(path:match("[?&]p=(%d+)"))
|
||||
end
|
||||
return bvid, aid, page or 1
|
||||
end
|
||||
|
||||
local function get_bilibili_pagelist_args(bvid, aid)
|
||||
local url
|
||||
if bvid ~= nil then
|
||||
url = "https://api.bilibili.com/x/player/pagelist?bvid=" .. bvid
|
||||
else
|
||||
url = "https://api.bilibili.com/x/player/pagelist?aid=" .. aid
|
||||
end
|
||||
local headers = {"Referer: https://www.bilibili.com/video/" .. (bvid or ("av" .. aid))}
|
||||
return build_curl_args(url, headers)
|
||||
end
|
||||
|
||||
local function resolve_bilibili_cid(path, callback)
|
||||
-- 扩展支持:普通视频(BV/av)、番剧(ep)、课程(cheese)
|
||||
local api_bangumi_season = "https://api.bilibili.com/pgc/view/web/season"
|
||||
local api_cheese_season = "https://api.bilibili.com/pugv/view/web/season"
|
||||
|
||||
local q = path
|
||||
-- 解析普通投稿视频
|
||||
local bvid, aid, p = get_bilibili_id_and_page(q)
|
||||
if bvid or aid then
|
||||
local args = get_bilibili_pagelist_args(bvid, aid)
|
||||
call_cmd_async(args, function(error, json)
|
||||
if error then
|
||||
msg.warn("Failed to request bilibili pagelist: " .. tostring(error))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
local pages = data and data["data"]
|
||||
if type(pages) ~= "table" then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local page_info = pages[p] or pages[1]
|
||||
local cid = page_info and page_info["cid"]
|
||||
local part = page_info and page_info["part"]
|
||||
callback(cid and tostring(cid) or nil, part and tostring(part) or nil)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- 番剧、番外等(含 ep)
|
||||
if q:find("bangumi/") and q:find("ep") then
|
||||
local epid = q:match("ep(%d+)") or q:match("ep(%d+)$")
|
||||
if not epid then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local url = api_bangumi_season .. "?ep_id=" .. epid
|
||||
local arg = build_curl_args(url)
|
||||
call_cmd_async(arg, function(error, json)
|
||||
if error then
|
||||
msg.warn("Failed to request bilibili bangumi info: " .. tostring(error))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
if not data or data.code ~= 0 or not data.result then
|
||||
msg.warn("bilibili bangumi api returned error")
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
-- 查找正片
|
||||
local episodes = data.result.episodes or {}
|
||||
for _, ep in ipairs(episodes) do
|
||||
if tostring(ep.id) == tostring(epid) then
|
||||
callback(tostring(ep.cid), tostring(ep.share_copy or ep.title or ""))
|
||||
return
|
||||
end
|
||||
end
|
||||
-- 查找 section(花絮等)
|
||||
if type(data.result.section) == "table" then
|
||||
for _, sec in ipairs(data.result.section) do
|
||||
if sec.episodes then
|
||||
for _, ep in ipairs(sec.episodes) do
|
||||
if tostring(ep.id) == tostring(epid) then
|
||||
callback(tostring(ep.cid), tostring(ep.share_copy or ep.title or ""))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
callback(nil)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- cheese 课程
|
||||
if q:find("cheese/") and q:find("ep") then
|
||||
local epid = q:match("ep(%d+)") or q:match("ep(%d+)$")
|
||||
if not epid then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local url = api_cheese_season .. "?ep_id=" .. epid
|
||||
local arg = build_curl_args(url)
|
||||
call_cmd_async(arg, function(error, json)
|
||||
if error then
|
||||
msg.warn("Failed to request bilibili cheese info: " .. tostring(error))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
if not data or data.code ~= 0 or not data.data then
|
||||
msg.warn("bilibili cheese api returned error")
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local episodes = data.data.episodes or {}
|
||||
for _, ep in ipairs(episodes) do
|
||||
if tostring(ep.id) == tostring(epid) then
|
||||
callback(tostring(ep.cid), tostring(ep.title or ""))
|
||||
return
|
||||
end
|
||||
end
|
||||
callback(nil)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- 其它情况返回 nil
|
||||
callback(nil)
|
||||
end
|
||||
|
||||
local function download_bilibili_danmaku(path, cid, from_menu, callback)
|
||||
local url = "https://comment.bilibili.com/" .. cid .. ".xml"
|
||||
local args = build_curl_args(url)
|
||||
|
||||
call_cmd_async(args, function(error, out)
|
||||
if error then
|
||||
show_message("HTTP request failed, see console for details", 5)
|
||||
msg.error(error)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
if not out or out == '' then
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
save_danmaku_xml(path, out)
|
||||
load_danmaku(from_menu == nil and true or from_menu)
|
||||
callback(true)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 为 bilibli 网站的视频播放加载弹幕
|
||||
function load_danmaku_for_bilibili(path, callback)
|
||||
callback = callback or function() end
|
||||
local cid, danmaku_id = get_cid()
|
||||
if danmaku_id ~= nil then
|
||||
mp.commandv('sub-remove', danmaku_id)
|
||||
end
|
||||
|
||||
if cid == nil then
|
||||
cid = mp.get_opt('cid')
|
||||
if not cid then
|
||||
local patterns = {
|
||||
"bilivideo%.c[nom]+.*/resource/(%d+)%D+.*",
|
||||
"bilivideo%.c[nom]+.*/(%d+)-%d+-%d+%..*%?",
|
||||
}
|
||||
local urls = {
|
||||
path,
|
||||
mp.get_property("stream-open-filename", ''),
|
||||
}
|
||||
|
||||
for _, pattern in ipairs(patterns) do
|
||||
for _, url in ipairs(urls) do
|
||||
if url:find(pattern) then
|
||||
cid = url:match(pattern)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if cid == nil then
|
||||
resolve_bilibili_cid(path, function(resolved_cid)
|
||||
if resolved_cid then
|
||||
download_bilibili_danmaku(path, resolved_cid, true, callback)
|
||||
else
|
||||
show_message("获取哔哩哔哩视频cid失败", 3)
|
||||
msg.error("获取哔哩哔哩视频cid失败")
|
||||
callback(false)
|
||||
end
|
||||
end)
|
||||
return
|
||||
end
|
||||
if cid ~= nil then
|
||||
download_bilibili_danmaku(path, cid, true, callback)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,290 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
local inflate = require('modules/inflate')
|
||||
|
||||
local 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'
|
||||
|
||||
local function build_curl_args(url, extra_headers)
|
||||
local args = {
|
||||
'curl', '-L', '-s', '--compressed', '--user-agent', user_agent
|
||||
}
|
||||
extra_headers = extra_headers or {}
|
||||
for _, h in ipairs(extra_headers) do
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, h)
|
||||
end
|
||||
table.insert(args, url)
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
local function get_tvid_from_url(url, callback)
|
||||
local id = url:match('v_(%w+)')
|
||||
if not id then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local api = string.format('https://pcw-api.iq.com/api/decode/%s?platformId=3&modeCode=intl&langCode=sg', id)
|
||||
call_cmd_async(build_curl_args(api), function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('Iqiyi decode request failed: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
if not data or not data['data'] then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local raw = data['data']
|
||||
local tvid_str
|
||||
if type(raw) == 'number' then
|
||||
tvid_str = string.format('%.0f', raw)
|
||||
else
|
||||
tvid_str = tostring(raw)
|
||||
end
|
||||
callback(tvid_str)
|
||||
end)
|
||||
end
|
||||
|
||||
local function get_video_info(tvid, callback)
|
||||
local api = string.format('https://pcw-api.iqiyi.com/video/video/baseinfo/%s', tvid)
|
||||
call_cmd_async(build_curl_args(api), function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('Iqiyi baseinfo request failed: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
callback(data and data['data'] or nil)
|
||||
end)
|
||||
end
|
||||
|
||||
local function extract_tags(xml, tag)
|
||||
local res = {}
|
||||
if not xml then return res end
|
||||
for v in xml:gmatch('<'..tag..'>(.-)</'..tag..'>') do
|
||||
table.insert(res, v)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function parse_xml_and_append(xml, contents, total_files)
|
||||
local danmaku = extract_tags(xml, 'content')
|
||||
local showTime = extract_tags(xml, 'showTime')
|
||||
local color = extract_tags(xml, 'color')
|
||||
|
||||
if #danmaku == 0 then return end
|
||||
local step = math.ceil(#danmaku * (total_files or 1) / 10000)
|
||||
if step < 1 then step = 1 end
|
||||
|
||||
for i = 1, #danmaku, step do
|
||||
local content = {}
|
||||
local timepoint = tonumber(showTime[i]) or 0
|
||||
local col = tonumber((color[i] or ''), 16) or 16777215
|
||||
local txt = danmaku[i] or ''
|
||||
content.c = string.format('%s,%s,%s,25,,,', timepoint, col, 1)
|
||||
content.m = txt
|
||||
table.insert(contents, content)
|
||||
end
|
||||
end
|
||||
|
||||
local function try_decompress_file(path)
|
||||
local f, err = io.open(path, 'rb')
|
||||
if not f then return nil, err end
|
||||
local data = f:read('*a')
|
||||
f:close()
|
||||
if not data or #data == 0 then return nil, 'empty' end
|
||||
local b1,b2 = data:byte(1,2)
|
||||
-- zip archive (PK..)
|
||||
if b1 == 0x50 and b2 == 0x4b then
|
||||
local bs = inflate.new(data)
|
||||
if bs then
|
||||
local it = bs:files()
|
||||
local name = it()
|
||||
if name then
|
||||
local res = bs:unzip(name, true)
|
||||
if res then return res end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- gzip
|
||||
if b1 == 0x1f and b2 == 0x8b then
|
||||
local function parse_gzip_start(d)
|
||||
local flg = d:byte(4) or 0
|
||||
local pos = 11
|
||||
if (flg % 8) >= 4 then
|
||||
local xlen = (d:byte(11) or 0) + ((d:byte(12) or 0) * 256)
|
||||
pos = pos + 2 + xlen
|
||||
end
|
||||
if (flg % 16) >= 8 then
|
||||
while d:byte(pos) and d:byte(pos) ~= 0 do pos = pos + 1 end
|
||||
pos = pos + 1
|
||||
end
|
||||
if (flg % 32) >= 16 then
|
||||
while d:byte(pos) and d:byte(pos) ~= 0 do pos = pos + 1 end
|
||||
pos = pos + 1
|
||||
end
|
||||
if (flg % 4) >= 2 then
|
||||
pos = pos + 2
|
||||
end
|
||||
return pos
|
||||
end
|
||||
local start = parse_gzip_start(data)
|
||||
if start and start < #data - 8 then
|
||||
local deflated = data:sub(start, #data - 8)
|
||||
local bs = inflate.new(deflated)
|
||||
if bs then
|
||||
local res = bs:inflate(1)
|
||||
if res then return res end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- zlib (check header checksum mod31)
|
||||
if ((b1*256 + (b2 or 0)) % 31) == 0 and #data > 6 then
|
||||
local deflated = data:sub(3, #data - 4)
|
||||
local bs = inflate.new(deflated)
|
||||
if bs then
|
||||
local res = bs:inflate(1)
|
||||
if res then return res end
|
||||
end
|
||||
end
|
||||
-- fallback: return raw data
|
||||
return data
|
||||
end
|
||||
|
||||
local function save_output_and_load(output_table, source_url)
|
||||
if #output_table == 0 then
|
||||
show_message('未获取到任何弹幕', 3)
|
||||
return
|
||||
end
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(source_url, final_json_str)
|
||||
load_danmaku(true)
|
||||
end
|
||||
|
||||
-- 辅助函数:构造 curl 参数
|
||||
local function build_args_for_server(server, referer)
|
||||
local headers = {
|
||||
'Referer: ' .. (referer or ''),
|
||||
'Accept: */*',
|
||||
}
|
||||
local args = build_curl_args(server.url, headers)
|
||||
local last = args[#args]
|
||||
args[#args] = '--output'
|
||||
table.insert(args, server.zfile)
|
||||
table.insert(args, last)
|
||||
return args
|
||||
end
|
||||
|
||||
-- 辅助函数:处理单个分段的响应并解压解析
|
||||
local function handle_server_response(server, err, out, output_table, servers, referer)
|
||||
if err then
|
||||
msg.warn('请求弹幕段失败: ' .. tostring(server) .. ' 错误: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
|
||||
if type(out) == 'string' and out:find('<') then
|
||||
parse_xml_and_append(out, output_table, #servers)
|
||||
return
|
||||
end
|
||||
|
||||
if type(server) == 'table' and server.zfile and utils.file_info(server.zfile) then
|
||||
local content, derr = try_decompress_file(server.zfile)
|
||||
if not content then
|
||||
msg.warn('文件解压失败: ' .. tostring(server.url) .. ' 错误: ' .. tostring(derr))
|
||||
os.remove(server.zfile)
|
||||
return
|
||||
end
|
||||
if not content or content == '' then
|
||||
msg.warn('文件解压后为空: ' .. tostring(server.url))
|
||||
os.remove(server.zfile)
|
||||
return
|
||||
end
|
||||
if type(content) == 'string' and content:find('<') then
|
||||
parse_xml_and_append(content, output_table, #servers)
|
||||
else
|
||||
msg.warn('无法解析弹幕段内容: ' .. tostring(server.url))
|
||||
end
|
||||
os.remove(server.zfile)
|
||||
return
|
||||
end
|
||||
msg.warn('无法处理弹幕段响应: ' .. tostring(server))
|
||||
end
|
||||
|
||||
-- 辅助函数:处理已知 tvid 的主流程
|
||||
local function process_iqiyi_with_tvid(tvid, url, callback)
|
||||
get_video_info(tvid, function(videoInfo)
|
||||
if not videoInfo then
|
||||
show_message('获取爱奇艺视频信息失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local title = videoInfo['name'] or videoInfo['tvName'] or ''
|
||||
local duration = tonumber(videoInfo['durationSec']) or 0
|
||||
local albumid = videoInfo['albumId']
|
||||
local categoryid = videoInfo['channelId'] or videoInfo['categoryId']
|
||||
if title and title ~= '' then
|
||||
DANMAKU.title = title
|
||||
end
|
||||
|
||||
local page = math.ceil(duration / (60 * 5))
|
||||
if page < 1 then page = 1 end
|
||||
msg.verbose(string.format('tvid: %s duration: %s pages: %d', tvid, tostring(duration), page))
|
||||
|
||||
local servers = {}
|
||||
for i = 0, page - 1 do
|
||||
local part1 = tvid:sub(-4, -3) or ''
|
||||
local part2 = tvid:sub(-2) or ''
|
||||
local api_url = string.format('https://cmts.iqiyi.com/bullet/%s/%s/%s_300_%d.z', part1, part2, tvid, i + 1)
|
||||
local qs = '?rn=0.0123456789123456&business=danmu&is_iqiyi=true&is_video_page=true&tvid='..url_encode(tvid)
|
||||
if albumid then qs = qs .. '&albumid=' .. url_encode(tostring(albumid)) end
|
||||
if categoryid then qs = qs .. '&categoryid=' .. url_encode(tostring(categoryid)) end
|
||||
qs = qs .. '&qypid=01010021010000000000'
|
||||
local full = api_url .. qs
|
||||
local tmp_file = 'iqiyi_' .. PID .. '_' .. tostring(os.time()) .. '_' .. tostring(i) .. '.z'
|
||||
local tmp_path = utils.join_path(DANMAKU_PATH, tmp_file)
|
||||
table.insert(servers, { url = full, zfile = tmp_path, idx = i + 1 })
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function build_args_fn(server)
|
||||
return build_args_for_server(server, url)
|
||||
end
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
return handle_server_response(server, err, out, output_table, servers, url)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
local ok = #output_table > 0
|
||||
save_output_and_load(output_table, url)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
parallel_requests(servers, build_args_fn, per_response_cb, final_cb, {concurrency = 6, per_request_timeout = 15})
|
||||
end)
|
||||
end
|
||||
|
||||
-- 为爱奇艺加载弹幕
|
||||
function load_danmaku_for_iqiyi(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = path or mp.get_property('stream-open-filename', '')
|
||||
if not url or url == '' then
|
||||
msg.error('无有效 URL')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
get_tvid_from_url(url, function(tvid)
|
||||
if not tvid then
|
||||
show_message('获取爱奇艺 tvid 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
process_iqiyi_with_tvid(tvid, url, callback)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
|
||||
local 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'
|
||||
|
||||
-- 解析多种 time 表示为秒数(支持数字、带小数、HH:MM:SS、MM:SS)
|
||||
local function parse_mgtv_time(time_str)
|
||||
if time_str == nil then return 0 end
|
||||
if type(time_str) == 'number' then return time_str end
|
||||
local s = tostring(time_str):gsub('^%s*(.-)%s*$', '%1')
|
||||
local n = tonumber(s)
|
||||
if n then return n end
|
||||
local h, m, sec = s:match('^(%d+):(%d+):([%d%.]+)$')
|
||||
if h and m and sec then
|
||||
return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(sec)
|
||||
end
|
||||
local mm, ss = s:match('^(%d+):([%d%.]+)$')
|
||||
if mm and ss then
|
||||
return tonumber(mm) * 60 + tonumber(ss)
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- 生成分段请求列表(每段以 ms 为单位的起点)
|
||||
local function generate_mgtv_segments(api_base, total_seconds, step_ms)
|
||||
local segments = {}
|
||||
local end_time_ms = math.floor(total_seconds * 1000)
|
||||
for i = 0, math.max(0, end_time_ms - 1), step_ms do
|
||||
table.insert(segments, api_base .. tostring(i))
|
||||
end
|
||||
return segments
|
||||
end
|
||||
|
||||
-- 构建通用 curl 请求参数
|
||||
local function build_mgtv_curl_args(target_url)
|
||||
local args = {
|
||||
'curl', '-s', '-L', '--compressed',
|
||||
'--user-agent', user_agent,
|
||||
'-H', 'Accept: application/json',
|
||||
target_url,
|
||||
}
|
||||
return args
|
||||
end
|
||||
|
||||
-- 解析单个分段返回并把弹幕追加到 output_table
|
||||
local function parse_mgtv_segment(out, output_table)
|
||||
if not out then return end
|
||||
local j = utils.parse_json(out)
|
||||
if not j or not j.data or not j.data.items then return end
|
||||
for _, item in ipairs(j.data.items) do
|
||||
local t_ms = tonumber(item.time) or 0
|
||||
local time_s = t_ms / 1000
|
||||
local content = item.content or ''
|
||||
local mode = 1
|
||||
local color = 16777215
|
||||
local c_param = string.format('%.2f,%d,%d,25,,,', time_s, color, mode)
|
||||
table.insert(output_table, { c = c_param, m = content })
|
||||
end
|
||||
end
|
||||
|
||||
local function extract_mgtv_ids(path)
|
||||
if not path then return nil, nil end
|
||||
-- 常见格式: /b/<cid>/<vid>.html
|
||||
local cid, vid = path:match('/b/(%d+)/([%w%._-]+)%.html')
|
||||
if cid and vid then
|
||||
vid = vid:match('([^.]+)') or vid
|
||||
return cid, vid
|
||||
end
|
||||
|
||||
-- 回退:取最后两个 path segment
|
||||
local segs = {}
|
||||
for seg in path:gmatch('/([^/]+)') do table.insert(segs, seg) end
|
||||
if #segs >= 2 then
|
||||
cid = segs[#segs - 1]
|
||||
vid = segs[#segs]
|
||||
vid = vid and vid:match('([^.]+)') or nil
|
||||
return cid, vid
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- 为 芒果TV 加载弹幕
|
||||
function load_danmaku_for_mgtv(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = path or mp.get_property('stream-open-filename', '')
|
||||
if not url or url == '' then
|
||||
msg.error('mgtv: 无效的 url')
|
||||
return
|
||||
end
|
||||
|
||||
local cid, vid = extract_mgtv_ids(url)
|
||||
if not cid or not vid then
|
||||
msg.error('mgtv: 无法解析 cid/vid: ' .. tostring(url))
|
||||
return
|
||||
end
|
||||
|
||||
local info_api = 'https://pcweb.api.mgtv.com/video/info?cid=' .. cid .. '&vid=' .. vid
|
||||
local api_danmaku_base = 'https://galaxy.bz.mgtv.com/rdbarrage?vid=' .. vid .. '&cid=' .. cid .. '&time='
|
||||
|
||||
local args = build_mgtv_curl_args(info_api)
|
||||
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err then
|
||||
msg.error('mgtv: 请求 video/info 失败: ' .. tostring(err))
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
if not data or data.code ~= 200 or not data.data or not data.data.info then
|
||||
msg.info('mgtv: video/info 返回无效')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
local time_str = data.data.info.time
|
||||
local total_seconds = parse_mgtv_time(time_str)
|
||||
|
||||
local step = 60 * 1000
|
||||
local segments = generate_mgtv_segments(api_danmaku_base, total_seconds, step)
|
||||
|
||||
if #segments == 0 then
|
||||
msg.info('mgtv: 未生成任何弹幕分段请求')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
if err then
|
||||
msg.debug('mgtv segment request failed: ' .. tostring(server) .. ' err: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
parse_mgtv_segment(out, output_table)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
local ok = #output_table > 0
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(url, final_json_str)
|
||||
load_danmaku(true)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
parallel_requests(segments, build_mgtv_curl_args, per_response_cb, final_cb, { concurrency = 6, per_request_timeout = 10 })
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,157 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
|
||||
local 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'
|
||||
|
||||
-- 将 URL 中的百分号编码解码为字符
|
||||
local function normalize_url(path)
|
||||
if not path then return '' end
|
||||
return (path:gsub('%%(%x%x)', hex_to_char))
|
||||
end
|
||||
|
||||
-- 从 URL 提取 vid
|
||||
local function extract_vid(url)
|
||||
if not url then return nil end
|
||||
local vid = url:match('[?&]vid=([^&?#]+)')
|
||||
if not vid then
|
||||
local last = nil
|
||||
for seg in url:gmatch('/([^/?#]+)') do
|
||||
last = seg
|
||||
end
|
||||
if last then
|
||||
vid = last:match('([^%.]+)')
|
||||
end
|
||||
end
|
||||
return vid
|
||||
end
|
||||
|
||||
-- 构造 curl 请求参数(通用)
|
||||
local function build_curl_args(target_url)
|
||||
local args = {
|
||||
'curl',
|
||||
'-L',
|
||||
'-s',
|
||||
'--compressed',
|
||||
'--user-agent',
|
||||
user_agent,
|
||||
target_url,
|
||||
}
|
||||
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
-- 解析单个 segment 返回并把弹幕追加到 output_table
|
||||
local function parse_segment_to_output(seg_json, output_table)
|
||||
if not seg_json or not seg_json['barrage_list'] then return end
|
||||
for _, item in ipairs(seg_json['barrage_list']) do
|
||||
local time = tonumber(item['time_offset']) and tonumber(item['time_offset']) / 1000 or 0
|
||||
local color = 16777215
|
||||
if item['content_style'] and item['content_style']['color'] then
|
||||
local col = item['content_style']['color']
|
||||
if type(col) == 'string' and col:match('^#') then
|
||||
color = hex_to_int_color(col)
|
||||
end
|
||||
end
|
||||
local mode = 1
|
||||
local c_param = string.format('%s,%s,%s,25,,,', time, color, mode)
|
||||
table.insert(output_table, {c = c_param, m = item['content'] or ''})
|
||||
end
|
||||
end
|
||||
|
||||
-- 保存并加载最终弹幕 JSON
|
||||
local function save_output_and_load(output_table, source_url)
|
||||
if #output_table == 0 then
|
||||
show_message('未获取到任何弹幕', 3)
|
||||
return
|
||||
end
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(source_url, final_json_str)
|
||||
load_danmaku(true)
|
||||
end
|
||||
|
||||
-- 为 腾讯视频 加载弹幕
|
||||
function load_danmaku_for_tencent(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = normalize_url(path)
|
||||
if not url or url == '' then
|
||||
url = mp.get_property('stream-open-filename', '')
|
||||
end
|
||||
|
||||
local vid = extract_vid(url)
|
||||
if not vid then
|
||||
msg.error('无法从 URL 中解析 vid: ' .. tostring(url))
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local api_base = 'https://dm.video.qq.com/barrage/base/' .. vid
|
||||
local api_segment_base = 'https://dm.video.qq.com/barrage/segment/' .. vid .. '/'
|
||||
|
||||
local base_args = build_curl_args(api_base)
|
||||
|
||||
call_cmd_async(base_args, function(err, out)
|
||||
if err then
|
||||
msg.error('请求腾讯弹幕 base 失败: ' .. tostring(err))
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local base_json = utils.parse_json(out)
|
||||
if not base_json or not base_json['segment_index'] then
|
||||
show_message('好像没有弹幕哦', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
-- 构造 segment 请求列表
|
||||
local segments = {}
|
||||
local seg_index = base_json['segment_index']
|
||||
if type(seg_index) == 'table' then
|
||||
for k, v in pairs(seg_index) do
|
||||
local seg_name = nil
|
||||
if type(v) == 'table' and v['segment_name'] then
|
||||
seg_name = v['segment_name']
|
||||
elseif type(k) == 'string' then
|
||||
seg_name = k
|
||||
end
|
||||
if seg_name then
|
||||
table.insert(segments, api_segment_base .. seg_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #segments == 0 then
|
||||
show_message('没有找到弹幕分段', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function build_args_fn(server)
|
||||
return build_curl_args(server)
|
||||
end
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
if err then
|
||||
msg.warn('请求段失败: ' .. tostring(server) .. ' 错误: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
local seg_json = utils.parse_json(out)
|
||||
parse_segment_to_output(seg_json, output_table)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
local ok = #output_table > 0
|
||||
save_output_and_load(output_table, url)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
-- 并行请求 segments
|
||||
parallel_requests(segments, build_args_fn, per_response_cb, final_cb, {concurrency = 6, per_request_timeout = 15})
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,323 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
|
||||
local 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'
|
||||
|
||||
local function build_curl_args(url, extra_headers)
|
||||
local args = {'curl', '-L', '-s', '--compressed', '--user-agent', user_agent}
|
||||
extra_headers = extra_headers or {}
|
||||
for _, h in ipairs(extra_headers) do
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, h)
|
||||
end
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
table.insert(args, url)
|
||||
return args
|
||||
end
|
||||
|
||||
local function parse_set_cookie_headers(text)
|
||||
local cookies = {}
|
||||
if not text or text == '' then return cookies end
|
||||
for line in text:gmatch('[^\r\n]+') do
|
||||
local low = line:lower()
|
||||
if low:find('set%-cookie:') then
|
||||
local cookie_str = line:match(':%s*(.*)') or ''
|
||||
for kv in cookie_str:gmatch('([^;]+)') do
|
||||
local k, v = kv:match('^%s*(.-)%s*=%s*(.*)')
|
||||
if k and v then
|
||||
cookies[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return cookies
|
||||
end
|
||||
|
||||
local function get_cna(callback)
|
||||
local api = 'https://log.mmstat.com/eg.js'
|
||||
local args = build_curl_args(api)
|
||||
table.insert(args, #args, '-i')
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('get_cna failed: ' .. tostring(err))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local cookies = parse_set_cookie_headers(out)
|
||||
callback(cookies['cna'])
|
||||
end)
|
||||
end
|
||||
|
||||
local function get_tk_enc(callback)
|
||||
local api_url = 'https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788'
|
||||
local args = build_curl_args(api_url)
|
||||
table.insert(args, #args, '-i')
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('get_tk_enc failed: ' .. tostring(err))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local cookies = parse_set_cookie_headers(out)
|
||||
callback(cookies)
|
||||
end)
|
||||
end
|
||||
|
||||
local function yk_msg_sign(msg)
|
||||
return MD5.sum(msg .. 'MkmC9SoIw6xCkSKHhJ7b5D2r51kBiREr')
|
||||
end
|
||||
|
||||
local function yk_t_sign(token, t, appkey, data)
|
||||
local text = table.concat({token, tostring(t), appkey, data}, '&')
|
||||
return MD5.sum(text)
|
||||
end
|
||||
|
||||
local function get_vinfos_by_video_id(url, callback)
|
||||
local vid = url:match('/v_show/id_([%w=]+)') or url:match('id_([%w=]+)') or url:match("[?&]vid=([^&]+)")
|
||||
if not vid then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local api_url = 'https://openapi.youku.com/v2/videos/show.json'
|
||||
local params = '?client_id=53e6cc67237fc59a&video_id=' .. url_encode(vid) .. '&package=com.huawei.hwvplayer.youku&ext=show'
|
||||
local args = build_curl_args(api_url .. params)
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('youku show.json failed: ' .. tostring(err))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
if not data then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local duration = tonumber(data.duration) or 0
|
||||
local title = data.title or ''
|
||||
if title and title ~= '' then DANMAKU.title = title end
|
||||
callback(vid, duration)
|
||||
end)
|
||||
end
|
||||
|
||||
local function build_query_string(params)
|
||||
local parts = {}
|
||||
for k, v in pairs(params) do
|
||||
table.insert(parts, k .. '=' .. url_encode(tostring(v)))
|
||||
end
|
||||
return table.concat(parts, '&')
|
||||
end
|
||||
|
||||
-- Helper: 构造 curl 参数(POST form data)
|
||||
local function youku_build_curl_args_for_server(server)
|
||||
local headers = {
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'Referer: https://v.youku.com',
|
||||
'User-Agent: ' .. user_agent,
|
||||
'Cookie: ' .. server.cookie
|
||||
}
|
||||
local args = build_curl_args(server.url, headers)
|
||||
table.insert(args, '--data-urlencode')
|
||||
table.insert(args, 'data=' .. server.data)
|
||||
msg.info('youku build args mat=' .. tostring(server.mat) .. ' sign=' .. tostring(server.sign) .. ' msg_b64_prefix=' .. string.sub(server.msg_b64 or '', 1, 16))
|
||||
msg.verbose('youku curl args: ' .. table.concat(args, ' '))
|
||||
return args
|
||||
end
|
||||
|
||||
-- Helper: 解析单个 youku 响应并把 danmu 加入 output_table
|
||||
local function youku_parse_response(out, server, output_table)
|
||||
if not out or out == '' then return 0 end
|
||||
|
||||
-- 优先解析外层 JSON,再解析内层被转义的 result
|
||||
local parsed = nil
|
||||
local outer = utils.parse_json(out)
|
||||
if outer and type(outer) == 'table' then
|
||||
if outer.data then
|
||||
if type(outer.data) == 'string' then
|
||||
local inner = utils.parse_json(outer.data)
|
||||
if inner and type(inner) == 'table' then parsed = inner end
|
||||
elseif type(outer.data) == 'table' then
|
||||
if type(outer.data.result) == 'string' then
|
||||
local inner = utils.parse_json(outer.data.result)
|
||||
if inner and type(inner) == 'table' then parsed = inner end
|
||||
elseif type(outer.data.result) == 'table' then
|
||||
parsed = outer.data
|
||||
end
|
||||
end
|
||||
elseif type(outer.result) == 'string' then
|
||||
local inner = utils.parse_json(outer.result)
|
||||
if inner and type(inner) == 'table' then parsed = inner end
|
||||
elseif type(outer.result) == 'table' then
|
||||
parsed = outer
|
||||
end
|
||||
end
|
||||
|
||||
if not parsed then
|
||||
local s, e = out:find('%b{}')
|
||||
if s and e then
|
||||
local snippet = out:sub(s, e)
|
||||
local sn = utils.parse_json(snippet)
|
||||
if sn and type(sn) == 'table' then parsed = sn end
|
||||
end
|
||||
end
|
||||
|
||||
if not parsed then
|
||||
msg.warn('youku parse failed for response; skipping')
|
||||
return 0
|
||||
end
|
||||
|
||||
if tostring(parsed.code) == '-1' then return 0 end
|
||||
local danmus = parsed.data and parsed.data.result or parsed.result
|
||||
if not danmus then return 0 end
|
||||
|
||||
local added = 0
|
||||
for _, d in ipairs(danmus) do
|
||||
local content = {}
|
||||
local timepoint = tonumber(d.playat) and tonumber(d.playat)/1000 or 0
|
||||
local properties = {}
|
||||
if d.propertis then
|
||||
local ps = utils.parse_json(d.propertis)
|
||||
if ps then properties = ps end
|
||||
end
|
||||
local color = tonumber(properties.color) or 16777215
|
||||
content.c = string.format('%s,%s,%s,25,,,', timepoint, color, 1)
|
||||
content.m = d.content or ''
|
||||
table.insert(output_table, content)
|
||||
added = added + 1
|
||||
end
|
||||
|
||||
return added
|
||||
end
|
||||
|
||||
-- Helper: 最终保存并加载弹幕
|
||||
local function youku_final_save(output_table, url, callback)
|
||||
local ok = #output_table > 0
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(url, final_json_str)
|
||||
load_danmaku(true)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
-- Helper: 根据 mat/guid/tk/vid 构造单个 server 条目
|
||||
local function youku_make_server_entry(mat, guid, tk, vid)
|
||||
local api_url = 'https://acs.youku.com/h5/mopen.youku.danmu.list/1.0/'
|
||||
local msg_obj = {
|
||||
ctime = os.time() * 1000,
|
||||
ctype = 10004,
|
||||
cver = 'v1.0',
|
||||
guid = guid,
|
||||
mat = mat,
|
||||
mcount = 1,
|
||||
pid = 0,
|
||||
sver = '3.1.0',
|
||||
type = 1,
|
||||
vid = vid
|
||||
}
|
||||
local data_json = utils.format_json(msg_obj)
|
||||
local msg_b64 = Base64.encode(data_json)
|
||||
msg_obj.msg = msg_b64
|
||||
msg_obj.sign = yk_msg_sign(msg_b64)
|
||||
local data_wrapper = utils.format_json(msg_obj)
|
||||
local t = tostring(os.time() * 1000)
|
||||
local token_raw = tk['_m_h5_tk'] or ''
|
||||
local token = token_raw:sub(1, 32)
|
||||
local params = {
|
||||
jsv = '2.5.6',
|
||||
appKey = '24679788',
|
||||
t = t,
|
||||
sign = yk_t_sign(token, t, '24679788', data_wrapper),
|
||||
api = 'mopen.youku.danmu.list',
|
||||
v = '1.0',
|
||||
type = 'originaljson',
|
||||
dataType = 'jsonp',
|
||||
timeout = '20000',
|
||||
jsonpIncPrefix = 'utility'
|
||||
}
|
||||
local qs = build_query_string(params)
|
||||
local full_url = api_url .. '?' .. qs
|
||||
local cookie_header = '_m_h5_tk=' .. (tk['_m_h5_tk'] or '') .. ';_m_h5_tk_enc=' .. (tk['_m_h5_tk_enc'] or '') .. ';'
|
||||
return {url = full_url, data = data_wrapper, cookie = cookie_header, mat = mat, sign = msg_obj.sign, msg_b64 = msg_b64}
|
||||
end
|
||||
|
||||
-- Helper: 并行获取 cna 和 tk,然后调用回调 cb({cna=..., tk=...})
|
||||
local function gather_cna_and_tk(cb)
|
||||
local res = {}
|
||||
local remaining = 2
|
||||
local function maybe_done()
|
||||
remaining = remaining - 1
|
||||
if remaining == 0 then cb(res) end
|
||||
end
|
||||
get_cna(function(cna)
|
||||
res.cna = cna
|
||||
maybe_done()
|
||||
end)
|
||||
get_tk_enc(function(tk)
|
||||
res.tk = tk
|
||||
maybe_done()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Helper: 从已知参数发起 youku 弹幕请求并处理结果
|
||||
local function start_youku_requests(source_url, vid, duration, cna, tk, callback)
|
||||
local max_mat = math.floor(duration / 60) + 1
|
||||
if max_mat < 1 then max_mat = 1 end
|
||||
msg.verbose(string.format('vid: %s duration: %s mats: %d', vid, tostring(duration), max_mat))
|
||||
|
||||
local servers = {}
|
||||
for mat = 0, max_mat - 1 do
|
||||
table.insert(servers, youku_make_server_entry(mat, cna, tk, vid))
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
if err then
|
||||
msg.warn('youku request failed: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
if not out or out == '' then return end
|
||||
youku_parse_response(out, server, output_table)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
youku_final_save(output_table, source_url, callback)
|
||||
end
|
||||
|
||||
parallel_requests(servers, youku_build_curl_args_for_server, per_response_cb, final_cb, {concurrency = 6, per_request_timeout = 20})
|
||||
end
|
||||
|
||||
function load_danmaku_for_youku(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = path or mp.get_property('stream-open-filename', '')
|
||||
if not url or url == '' then
|
||||
msg.error('无有效 URL')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
get_vinfos_by_video_id(url, function(vid, duration)
|
||||
if not vid then
|
||||
show_message('获取优酷 video_id 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
gather_cna_and_tk(function(res)
|
||||
local cna = res.cna
|
||||
local tk = res.tk
|
||||
if not cna then
|
||||
show_message('获取优酷 cna 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
if not tk then
|
||||
show_message('获取优酷 tk_enc 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
start_youku_requests(url, vid, duration, cna, tk, callback)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user