This commit is contained in:
2026-08-15 17:50:48 +02:00
parent 22246060e6
commit 4dc6687de7
35 changed files with 4978 additions and 1509 deletions
+9 -2
View File
@@ -60,21 +60,28 @@ icc/ # ICC 色彩配置文件
## 更新流程 ## 更新流程
1. 在 mpv 中按 `M` 触发 manager.lua,观察控制台输出,确认无 `FAILED` 条目。可能会有其他报错如 `[manager] Fehler: externes Repository manager existiert bereits.`,这是正常的。只需要确保不出现全大写的 `FAILED` 即可。 1. 在 mpv 中按 `M` 触发 manager.lua,观察控制台输出,确认无 `FAILED` 条目。可能会有其他报错如 `[manager] error: remote manager already exists.`,这是正常的。只需要确保不出现全大写的 `FAILED` 即可。
2. 将 manager.lua 在 dest 目录产生的嵌套 `.git` 目录迁移到仓库内的 `.manager/`(避免根仓库误判为 submodule,幂等可重复执行): 2. 将 manager.lua 在 dest 目录产生的嵌套 `.git` 目录迁移到仓库内的 `.manager/`(避免根仓库误判为 submodule,幂等可重复执行):
```bash ```bash
REPO=$(git -C ~/.config/mpv rev-parse --show-toplevel) REPO=$(git -C ~/.config/mpv rev-parse --show-toplevel)
GITSTORE="$REPO/.manager" GITSTORE="$REPO/.manager"
mkdir -p "$GITSTORE" mkdir -p "$GITSTORE"
find -L ~/.config/mpv -mindepth 2 -name .git -type d | while read gitdir; do find "$REPO" -mindepth 2 -name .git -type d -not -path "$GITSTORE/*" | while read gitdir; do
dest="${gitdir%/.git}" dest="${gitdir%/.git}"
rel="${dest#$REPO/}" rel="${dest#$REPO/}"
name=$(echo "$rel" | tr '/' '-') name=$(echo "$rel" | tr '/' '-')
[ -n "$name" ] || continue
[ -d "$GITSTORE/$name" ] && rm -rf "${GITSTORE:?}/$name"
mv "$gitdir" "$GITSTORE/$name" mv "$gitdir" "$GITSTORE/$name"
depth=$(echo "$rel" | tr -cd '/' | wc -c) depth=$(echo "$rel" | tr -cd '/' | wc -c)
ups=$(printf '../%.0s' $(seq 1 $((depth + 1)))) ups=$(printf '../%.0s' $(seq 1 $((depth + 1))))
echo "gitdir: ${ups}.manager/$name" > "$dest/.git" echo "gitdir: ${ups}.manager/$name" > "$dest/.git"
done done
``` ```
校验(应无输出):
```bash
find "$REPO" -mindepth 2 -name .git -type f -printf '%h\0' \
| xargs -0 -I{} sh -c 'git -C "{}" rev-parse --git-dir >/dev/null 2>&1 || echo "BROKEN: {}"'
```
3. 重启 mpv,检查控制台有无 `unknown key` 或脚本加载失败的警告 3. 重启 mpv,检查控制台有无 `unknown key` 或脚本加载失败的警告
4. 若有 `unknown key` 警告,说明对应脚本的配置项发生变化,找 `script-opts/` 下同名 `.conf` 对照脚本源码更新 4. 若有 `unknown key` 警告,说明对应脚本的配置项发生变化,找 `script-opts/` 下同名 `.conf` 对照脚本源码更新
+2 -2
View File
@@ -1,6 +1,6 @@
# ===== 基础设置 ===== # ===== 基础设置 =====
vo=gpu-next # 视频输出驱动 vo=gpu-next # 视频输出驱动
gpu-api=vulkan # 图形绘制接口 gpu-api=vulkan # 图形绘制接口
input-ime=no # 仅文本输入时激活 IME input-ime=no # 仅文本输入时激活 IME
autofit-smaller=40%x30% # 窗口最小占屏比例 autofit-smaller=40%x30% # 窗口最小占屏比例
@@ -16,7 +16,7 @@ watch-later-options=start,vid,aid,sid
save-watch-history=yes # 保存播放历史(内置 select 脚本可浏览) save-watch-history=yes # 保存播放历史(内置 select 脚本可浏览)
watch-history-path=~~/files/watch_history.jsonl watch-history-path=~~/files/watch_history.jsonl
reset-on-next-file=vid,aid,sid,secondary-sid,vf,af,loop-file,deinterlace,contrast,brightness,gamma,saturation,hue,video-zoom,video-rotate,video-pan-x,video-pan-y,panscan,speed,audio-delay,sub-pos,sub-scale,sub-delay,sub-speed,sub-visibility,secondary-sub-visibility reset-on-next-file=vid,aid,sid,secondary-sid,vf,af,loop-file,deinterlace,contrast,brightness,gamma,saturation,hue,video-zoom,video-rotate,video-pan-x,video-pan-y,panscan,speed,audio-delay,sub-pos,sub-scale,sub-delay,sub-speed,sub-visibility,secondary-sub-visibility
input-ipc-server=/tmp/mpvsocket # IPC 套接字 input-ipc-server=/tmp/mpvsocket # IPC 套接字
directory-mode=ignore # 打开目录时忽略子目录 directory-mode=ignore # 打开目录时忽略子目录
metadata-codepage=auto # 元数据编码自动检测 metadata-codepage=auto # 元数据编码自动检测
msg-level=all=info,auto_profiles=warn msg-level=all=info,auto_profiles=warn
+1 -1
View File
@@ -11,7 +11,7 @@ force=no
#如果 mpv 无法做到这一点,请尝试重新选择最后一个轨道,例如当信息更改时,默认 no #如果 mpv 无法做到这一点,请尝试重新选择最后一个轨道,例如当信息更改时,默认 no
smart_keep=no smart_keep=no
## 指定需忽略的特殊协议 ## 指定需忽略的特殊协议
special_protocols=["://", "^magnet:"] #special_protocols=["://", "^magnet:"]
#视频轨道筛选项设置 #视频轨道筛选项设置
#preferred_video_lang= #preferred_video_lang=
+3 -4
View File
@@ -1,5 +1,5 @@
## 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api ## 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
api_server=https://api.dandanplay.net api_server=https://danmaku-api.152468.xyz
## 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕 ## 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
## 服务器可以自托管:https://github.com/lyz05/danmaku ## 服务器可以自托管:https://github.com/lyz05/danmaku
#fallback_server=https://fc.lyz05.cn #fallback_server=https://fc.lyz05.cn
@@ -8,7 +8,7 @@ api_server=https://api.dandanplay.net
## 注意:自定义此参数时还需要对获取到的 API Key 进行 base64 编码 ## 注意:自定义此参数时还需要对获取到的 API Key 进行 base64 编码
#tmdb_api_key=NmJmYjIxOTZkNzIyN2UyMTIzMGM3Y2YzZjQ4MDNkZGM= #tmdb_api_key=NmJmYjIxOTZkNzIyN2UyMTIzMGM3Y2YzZjQ4MDNkZGM=
## 加载更多来自弹幕服务器上第三方的弹幕 ## 加载更多来自弹幕服务器上第三方的弹幕
load_more_danmaku=yes #load_more_danmaku=yes
## 自动加载网络弹幕 ## 自动加载网络弹幕
auto_load=no auto_load=no
## 自动加载本地弹幕 ## 自动加载本地弹幕
@@ -38,7 +38,7 @@ fixtime=5
## 字体 ## 字体
fontname=danmaku fontname=danmaku
## 字体大小 ## 字体大小
fontsize=24 fontsize=28
## 透明度 ## 透明度
opacity=0.6 opacity=0.6
## 粗体 ## 粗体
@@ -59,4 +59,3 @@ title_replace=[{"rules":[{ "^(.-)": "%1"},{ "^.*《(.-)》": "%1" }]}]
## 指定哈希匹配中需忽略的共享盘(挂载盘)的路径/目录。支持绝对路径和相对路径,多个路径用逗号分隔 ## 指定哈希匹配中需忽略的共享盘(挂载盘)的路径/目录。支持绝对路径和相对路径,多个路径用逗号分隔
## 示例:["X:", "Z:", "F:/Download/", "Download"] ## 示例:["X:", "Z:", "F:/Download/", "Download"]
#excluded_path= #excluded_path=
+10 -6
View File
@@ -100,11 +100,11 @@ local function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil) return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end end
function url_decode(str) local function hex_to_char(x)
local function hex_to_char(x) return string.char(tonumber(x, 16))
return string.char(tonumber(x, 16)) end
end
local function url_decode(str)
if str ~= nil then if str ~= nil then
str = str:gsub('^%a[%a%d-_]+://', '') str = str:gsub('^%a[%a%d-_]+://', '')
:gsub('^%a[%a%d-_]+:\\?', '') :gsub('^%a[%a%d-_]+:\\?', '')
@@ -112,9 +112,13 @@ function url_decode(str)
if str:find('://localhost:?') then if str:find('://localhost:?') then
str = str:gsub('^.*/', '') str = str:gsub('^.*/', '')
end end
str = str:gsub('[\\/:%?]*', '') str = str:gsub("%?.+", ""):gsub("%+", " ")
return str local last_pos = str:match('.*[\\/:%?]()')
if last_pos then
str = str:sub(last_pos)
end
end end
return str
end end
--create global_chapters_dir if it doesn't exist --create global_chapters_dir if it doesn't exist
+8 -1
View File
@@ -5,6 +5,11 @@
local msg = require "mp.msg" local msg = require "mp.msg"
local function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end
local function fix_avsync() local function fix_avsync()
local paused = mp.get_property_bool("pause") local paused = mp.get_property_bool("pause")
msg.info("fix A/V sync.") msg.info("fix A/V sync.")
@@ -19,7 +24,9 @@ end
mp.observe_property("current-ao", "native", function(_, device) mp.observe_property("current-ao", "native", function(_, device)
local aid = mp.get_property_number("aid") local aid = mp.get_property_number("aid")
local has_af = mp.get_property("af", "") ~= "" local has_af = mp.get_property("af", "") ~= ""
if device and aid and has_af then local path = mp.get_property("path")
local time = mp.get_property_native("time-pos")
if device and aid and has_af and time > 0 and not is_protocol(path) then
fix_avsync() fix_avsync()
end end
end) end)
+60 -34
View File
@@ -20,14 +20,18 @@ local o = {
-- Specify whether to switch HDR mode only when the window is in fullscreen or window maximized -- Specify whether to switch HDR mode only when the window is in fullscreen or window maximized
-- only works with hdr_mode = "switch", default: false -- only works with hdr_mode = "switch", default: false
fullscreen_only = false, fullscreen_only = false,
-- Specify the target peak of the HDR display, default: 203 -- Specify the target peak of the HDR display, default: 0
-- must be the true peak brightness of the monitor, -- By default, the display peak information property provided by mpv-display-plugin
-- otherwise it will cause HDR content to display incorrectly -- will be used: "user-data/display-info/max-luminance"
target_peak = "203", -- Specific values can also be specified to override internal detection,
-- but it must be the true peak brightness of the monitor,
target_peak = "0",
-- Specifies the measured contrast of the output display. -- Specifies the measured contrast of the output display.
-- Used in black point compensation during HDR tone-mapping and HDR passthrough. -- Used in black point compensation during HDR tone-mapping and HDR passthrough.
-- Must be the true contrast information of the display, e.g. 100000 means 100000:1 maximum contrast -- Must be the true contrast information of the display, e.g. 100000 means 100000:1 maximum contrast
-- OLED display do not need to change this, default: auto -- The default value auto will try to use the display black level attribute provided
-- by mpv-display-plugin to calculate the correct value: "user-data/display-info/min-luminance"
-- OLED displays can specify the special value "inf" to apply infinite contrast
target_contrast = "auto", target_contrast = "auto",
} }
options.read_options(o, _, function() end) options.read_options(o, _, function() end)
@@ -48,6 +52,10 @@ local state = {
inverse_mapping = mp.get_property_native("inverse-tone-mapping") inverse_mapping = mp.get_property_native("inverse-tone-mapping")
} }
local function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end
local function query_hdr_state() local function query_hdr_state()
hdr_supported = mp.get_property_native("user-data/display-info/hdr-supported") hdr_supported = mp.get_property_native("user-data/display-info/hdr-supported")
hdr_active = mp.get_property_native("user-data/display-info/hdr-status") == "on" hdr_active = mp.get_property_native("user-data/display-info/hdr-status") == "on"
@@ -64,7 +72,7 @@ local function apply_hdr_settings()
mp.set_property_native("icc-profile-auto", false) mp.set_property_native("icc-profile-auto", false)
mp.set_property_native("target-prim", "bt.2020") mp.set_property_native("target-prim", "bt.2020")
mp.set_property_native("target-trc", "pq") mp.set_property_native("target-trc", "pq")
mp.set_property_native("target-peak", o.target_peak) mp.set_property_native("target-peak", tonumber(o.target_peak))
mp.set_property_native("target-contrast", o.target_contrast) mp.set_property_native("target-contrast", o.target_contrast)
mp.set_property_native("target-colorspace-hint", "yes") mp.set_property_native("target-colorspace-hint", "yes")
mp.set_property_native("inverse-tone-mapping", "no") mp.set_property_native("inverse-tone-mapping", "no")
@@ -116,27 +124,26 @@ end
local function handle_hdr_logic(paused_before, target_peak, target_prim, target_trc) local function handle_hdr_logic(paused_before, target_peak, target_prim, target_trc)
query_hdr_state() query_hdr_state()
if hdr_active and o.hdr_mode ~= "noth" then if hdr_active then
apply_hdr_settings() apply_hdr_settings()
resume_if_needed(paused_before) resume_if_needed(paused_before)
elseif not hdr_active and o.hdr_mode ~= "noth" and elseif (tonumber(target_peak) ~= 203 or target_prim == "bt.2020" or
(tonumber(target_peak) ~= 203 or target_prim == "bt.2020" or target_trc == "pq") then target_trc == "pq") then
apply_sdr_settings() apply_sdr_settings()
end end
end end
local function handle_sdr_logic(paused_before, target_peak, target_prim, target_trc) local function handle_sdr_logic(paused_before, target_peak, target_prim, target_trc)
query_hdr_state() query_hdr_state()
if not hdr_active or o.hdr_mode ~= "noth" then if not hdr_active then
if (not hdr_active or not state.inverse_mapping) and if not state.inverse_mapping and (tonumber(target_peak) ~= 203 or
(tonumber(target_peak) ~= 203 or target_prim == "bt.2020" or target_trc == "pq") then target_prim == "bt.2020" or target_trc == "pq") then
apply_sdr_settings() apply_sdr_settings()
elseif hdr_active and state.inverse_mapping then elseif state.inverse_mapping then
reset_target_settings() reset_target_settings()
end end
resume_if_needed(paused_before) resume_if_needed(paused_before)
end elseif o.hdr_mode == "pass" and state.inverse_mapping then
if hdr_active and o.hdr_mode == "pass" and state.inverse_mapping then
reset_target_settings() reset_target_settings()
end end
end end
@@ -153,11 +160,26 @@ end
local function switch_hdr() local function switch_hdr()
query_hdr_state() query_hdr_state()
local params = mp.get_property_native("video-params") local path = mp.get_property("path")
local params = mp.get_property_native("video-out-params")
local gamma = params and params["gamma"] local gamma = params and params["gamma"]
local max_luma = params and params["max-luma"] local max_luma = params and params["max-luma"]
local target_max_luma = mp.get_property_native("user-data/display-info/max-luminance", "203")
local target_min_luma = mp.get_property_native("user-data/display-info/min-luminance")
local is_hdr = max_luma and max_luma > 203 local is_hdr = max_luma and max_luma > 203
if not gamma then return end if not path or not gamma then return end
if tonumber(o.target_peak) <= 203 and tonumber(target_max_luma) > 203 then
o.target_peak = math.floor(tonumber(target_max_luma))
end
if o.target_contrast == "auto" and tonumber(o.target_peak) > 203 and target_min_luma then
if tonumber(target_min_luma) == 0 then
o.target_contrast = "inf"
else
o.target_contrast = math.floor(tonumber(o.target_peak) / tonumber(target_min_luma))
end
end
local current_state = is_hdr and "hdr" or "sdr" local current_state = is_hdr and "hdr" or "sdr"
local pause_changed = false local pause_changed = false
@@ -168,8 +190,16 @@ local function switch_hdr()
local target_trc = mp.get_property_native("target-trc") local target_trc = mp.get_property_native("target-trc")
local is_fullscreen = fullscreen or maximized local is_fullscreen = fullscreen or maximized
if current_state == "hdr" then if current_state == "hdr" and tonumber(o.target_peak) > 203 then
local function continue_hdr() local function continue_hdr()
local time = mp.get_property_number("time-pos")
local paused = mp.get_property_native("pause")
if time > 0 and not is_protocol(path) then
mp.commandv("frame-back-step")
if not paused then
pause_changed = true
end
end
handle_hdr_logic(pause_changed, target_peak, target_prim, target_trc) handle_hdr_logic(pause_changed, target_peak, target_prim, target_trc)
end end
@@ -189,9 +219,16 @@ local function switch_hdr()
end end
handle_hdr_logic(false, target_peak, target_prim, target_trc) handle_hdr_logic(false, target_peak, target_prim, target_trc)
elseif current_state == "sdr" then elseif current_state == "sdr" then
local function continue_sdr() local function continue_sdr()
local time = mp.get_property_native("time-pos")
local paused = mp.get_property_native("pause")
if time > 0 and not is_protocol(path) then
mp.commandv("frame-back-step")
if not paused then
pause_changed = true
end
end
handle_sdr_logic(pause_changed, target_peak, target_prim, target_trc) handle_sdr_logic(pause_changed, target_peak, target_prim, target_trc)
end end
@@ -215,13 +252,13 @@ local function check_paramet()
local target_contrast = mp.get_property_native("target-contrast") local target_contrast = mp.get_property_native("target-contrast")
local colorspace_hint = mp.get_property_native("target-colorspace-hint") local colorspace_hint = mp.get_property_native("target-colorspace-hint")
local inverse_mapping = mp.get_property_native("inverse-tone-mapping") local inverse_mapping = mp.get_property_native("inverse-tone-mapping")
local params = mp.get_property_native("video-params") local params = mp.get_property_native("video-out-params")
local gamma = params and params["gamma"] local gamma = params and params["gamma"]
local max_luma = params and params["max-luma"] local max_luma = params and params["max-luma"]
local is_hdr = max_luma and max_luma > 203 local is_hdr = max_luma and max_luma > 203
if not gamma then return end if not gamma then return end
if is_hdr and hdr_active and o.hdr_mode ~= "noth" then if is_hdr and hdr_active and o.hdr_mode ~= "noth" and tonumber(o.target_peak) > 203 then
if target_peak ~= o.target_peak then if target_peak ~= o.target_peak then
mp.set_property_native("target-peak", o.target_peak) mp.set_property_native("target-peak", o.target_peak)
end end
@@ -241,14 +278,10 @@ local function check_paramet()
mp.set_property_native("inverse-tone-mapping", "no") mp.set_property_native("inverse-tone-mapping", "no")
end end
end end
if not is_hdr and o.hdr_mode ~= "noth" and not state.inverse_mapping
and (tonumber(target_peak) ~= 203 or target_prim == "bt.2020" or target_trc == "pq") then
apply_sdr_settings()
end
end end
local function on_start() local function on_start()
if o.hdr_mode == "noth" or tonumber(o.target_peak) <= 203 then if o.hdr_mode == "noth" then
return return
end end
local vo = mp.get_property("vo") local vo = mp.get_property("vo")
@@ -258,7 +291,7 @@ local function on_start()
end end
file_loaded = true file_loaded = true
query_hdr_state() query_hdr_state()
mp.observe_property("video-params", "native", switch_hdr) mp.observe_property("video-out-params", "native", switch_hdr)
mp.observe_property("target-peak", "native", check_paramet) mp.observe_property("target-peak", "native", check_paramet)
mp.observe_property("target-prim", "native", check_paramet) mp.observe_property("target-prim", "native", check_paramet)
mp.observe_property("target-trc", "native", check_paramet) mp.observe_property("target-trc", "native", check_paramet)
@@ -285,13 +318,6 @@ local function on_end(event)
end end
local function on_idle(_, active) local function on_idle(_, active)
local target_peak = mp.get_property_native("target-peak")
local target_prim = mp.get_property_native("target-prim")
local target_trc = mp.get_property_native("target-trc")
if active and o.hdr_mode ~= "noth" and
(tonumber(target_peak) ~= 203 or target_prim == "bt.2020" or target_trc == "pq") then
apply_sdr_settings()
end
if active and file_loaded and o.hdr_mode == "switch" then if active and file_loaded and o.hdr_mode == "switch" then
file_loaded = false file_loaded = false
query_hdr_state() query_hdr_state()
+670
View File
@@ -0,0 +1,670 @@
--[[
SOURCE_: https://github.com/WatanabeChika/mpv-lines-meme-generator
Modify_: https://github.com/dyphire/mpv-scripts
create long graph of lines with mpv
requires ffmpeg.
Usage: add bindings to input.conf
-- key script-message crop-screenshot
-- key script-message take-screenshot
-- key script-message stitch-images
]]
require("mp.options")
local utils = require("mp.utils")
local assdraw = require 'mp.assdraw'
local msg = require 'mp.msg'
local options = {
ffmpeg_path = "ffmpeg",
screenshot_dir = "~~/screenshots", -- your path to save screenshots
lossless = false, -- use lossless screenshots
ffmpeg_loglevel = "error", -- ffmpeg log level
}
read_options(options, _, function() end)
local screenshot_dir = mp.command_native({ "expand-path", options.screenshot_dir })
local screenshot_format = options.lossless and ".png" or ".jpg"
local screenshot_count = 0
local subtitle_top, subtitle_bottom = 0, 0
local screenshots = {}
-- detect path separator, detect path separator, windows uses backslashes
local is_windows = package.config:sub(1, 1) == "\\"
--create screenshot_dir if it doesn't exist
if screenshot_dir ~= '' then
local meta = utils.file_info(screenshot_dir)
if not meta or not meta.is_dir then
local windows_args = { 'powershell', '-NoProfile', '-Command', 'mkdir',
string.format("\"%s\"", screenshot_dir) }
local unix_args = { 'mkdir', '-p', screenshot_dir }
local args = is_windows and windows_args or unix_args
local res = mp.command_native({
name = "subprocess",
capture_stdout = true,
playback_only = false,
args = args
})
if res.status ~= 0 then
msg.error("Failed to create screenshot_dir save directory " .. screenshot_dir ..
". Error: " .. (res.error or "unknown"))
return
end
end
end
local function file_exist(path)
local meta = utils.file_info(path)
if not meta or not meta.is_file then
return false
end
return true
end
-- helper: crop an input image file into output using mpv
local function mpv_crop_file(input_path, out_path, crop_arg)
local ENCODER_MAP = {
[".png"] = "png",
[".jpg"] = "mjpeg",
}
local ovc = ENCODER_MAP[screenshot_format] or "png"
local cmd = { "mpv", input_path, "--no-config" }
if crop_arg and crop_arg ~= "" then
table.insert(cmd, "--vf=" .. crop_arg)
end
table.insert(cmd, "--frames=1")
table.insert(cmd, "--ovc=" .. ovc)
table.insert(cmd, "-o")
table.insert(cmd, out_path)
local res = utils.subprocess({ args = cmd, capture_stdout = true, capture_stderr = true })
if res and res.status ~= 0 then
msg.error("mpv crop failed: status=" .. tostring(res.status) .. " error=" .. tostring(res.error))
if res.stdout then msg.error("mpv stdout: " .. tostring(res.stdout)) end
if res.stderr then msg.error("mpv stderr: " .. tostring(res.stderr)) end
end
return res
end
-- take and crop screenshots
local function perform_capture(idx, crop_arg_local, line)
-- take original shots
local screenshot_file = utils.join_path(screenshot_dir, "temp_screenshot_" .. idx .. screenshot_format)
mp.commandv("screenshot-to-file", screenshot_file, "subtitles")
-- crop the shots
local processed_file = line and
utils.join_path(screenshot_dir, string.format("line_shot_%03d" .. screenshot_format, idx)) or
utils.join_path(screenshot_dir, string.format("crop_shot_%s%s", os.date("%Y%m%d_%H%M%S"), screenshot_format))
-- use helper to crop/export the temporary screenshot into final output
local result = mpv_crop_file(screenshot_file, processed_file, crop_arg_local)
if result and result.status == 0 then
mp.osd_message("Shot saved: " .. processed_file)
msg.verbose("Shot saved: " .. processed_file)
if line then
table.insert(screenshots, processed_file)
end
else
mp.osd_message("Cropping failed")
msg.verbose("Cropping failed: " .. (result and result.error or "unknown"))
end
-- delete the original shots
os.remove(screenshot_file)
end
-- Minimal DisplayState derived from mpv_crop_script's implementation
local DisplayState = {}
DisplayState.__index = DisplayState
function DisplayState.new()
local self = setmetatable({}, DisplayState)
self.screen = { width = 0, height = 0 }
self.video = { width = 0, height = 0 }
self.bounds = { left = 0, top = 0, right = 0, bottom = 0, width = 0, height = 0 }
self.scale = { x = 1, y = 1 }
self.current_state = nil
self.screen_ready = false
self.video_ready = false
return self
end
function DisplayState:_collect_display_state()
local screen_w, screen_h = mp.get_osd_size()
local state = {
screen_w = screen_w,
screen_h = screen_h,
video_w = mp.get_property_number("dwidth"),
video_h = mp.get_property_number("dheight"),
video_w_raw = mp.get_property_number("video-out-params/w"),
video_h_raw = mp.get_property_number("video-out-params/h"),
video_aspect_override = mp.get_property_native("video-aspect-override"),
panscan = mp.get_property_native("panscan"),
video_zoom = mp.get_property_native("video-zoom"),
video_unscaled = mp.get_property_native("video-unscaled"),
video_align_x = mp.get_property_native("video-align-x"),
video_align_y = mp.get_property_native("video-align-y"),
video_pan_x = mp.get_property_native("video-pan-x"),
video_pan_y = mp.get_property_native("video-pan-y"),
fullscreen = mp.get_property_native("fullscreen"),
keepaspect = mp.get_property_native("keepaspect"),
keepaspect_window = mp.get_property_native("keepaspect-window"),
window_maximized = mp.get_property_native("window-maximized")
}
return state
end
function DisplayState:_aspect_calc_panscan(state)
local f_width = state.screen_w
local f_height = (state.screen_w / state.video_w) * state.video_h
if f_height > state.screen_h or f_height < state.video_h_raw then
local tmp_w = (state.screen_h / state.video_h) * state.video_w
if tmp_w <= state.screen_w then
f_height = state.screen_h
f_width = tmp_w
end
end
local vo_panscan_area = state.screen_h - f_height
local f_w = f_width / f_height
local f_h = 1
if (vo_panscan_area == 0) then
vo_panscan_area = state.screen_w - f_width
f_w = 1
f_h = f_height / f_width
end
if state.video_unscaled then
vo_panscan_area = 0
if state.video_unscaled ~= "downscale-big" or ((state.video_w <= state.screen_w)
and (state.video_h <= state.screen_h)) then
f_width = state.video_w
f_height = state.video_h
end
end
local scaled_w = math.floor( f_width + vo_panscan_area * state.panscan * f_w )
local scaled_h = math.floor( f_height + vo_panscan_area * state.panscan * f_h )
return scaled_w, scaled_h
end
function DisplayState:_split_scaling(dst_size, scaled_src_size, zoom, align, pan)
scaled_src_size = math.floor(scaled_src_size * 2^zoom)
align = (align + 1) / 2
local dst_start = (dst_size - scaled_src_size) * align + pan * scaled_src_size
local dst_end = dst_start + scaled_src_size
return math.floor(dst_start), math.floor(dst_end)
end
function DisplayState:recalculate_bounds(forced)
local new_state = self:_collect_display_state()
if not (forced or self.current_state == nil) then
local changed = false
for k in pairs(new_state) do
if new_state[k] ~= self.current_state[k] then
changed = true
break
end
end
if not changed then return self.screen_ready end
end
self.current_state = new_state
self.screen.width = new_state.screen_w
self.screen.height = new_state.screen_h
if new_state.video_w and new_state.video_h then
self.video.width = new_state.video_w
self.video.height = new_state.video_h
-- Apply video-aspect-override when present (e.g. "16:9" or "1.7777") by adjusting raw output dims
local vao = new_state.video_aspect_override
if vao and vao ~= false and tostring(vao) ~= "no" then
local s = tostring(vao)
-- Try parse A:B style
local na, nb = s:match("^(%d+)%s*[:xX]%s*(%d+)$")
local ar = nil
if na and nb then
ar = tonumber(na) / tonumber(nb)
else
local f = tonumber(s)
if f and f > 0 then ar = f end
end
if ar then
-- Prefer to keep video_h_raw and compute video_w_raw to match aspect
if new_state.video_h_raw and new_state.video_h_raw > 0 then
new_state.video_w_raw = math.floor(new_state.video_h_raw * ar + 0.5)
elseif new_state.video_w_raw and new_state.video_w_raw > 0 then
new_state.video_h_raw = math.floor(new_state.video_w_raw / ar + 0.5)
else
-- fallback to reported dwidth/dheight
if new_state.video_h and new_state.video_h > 0 then
new_state.video_w_raw = math.floor(new_state.video_h * ar + 0.5)
elseif new_state.video_w and new_state.video_w > 0 then
new_state.video_h_raw = math.floor(new_state.video_w / ar + 0.5)
end
end
-- Also update video_w/video_h (dwidth/dheight) to reflect the override so
-- bounds calculation (which uses video_w/video_h) stays consistent with
-- the adjusted raw output dimensions. Prefer keeping video_h and
-- computing video_w, mirroring the logic above.
if new_state.video_h and new_state.video_h > 0 then
new_state.video_w = math.floor(new_state.video_h * ar + 0.5)
elseif new_state.video_w and new_state.video_w > 0 then
new_state.video_h = math.floor(new_state.video_w / ar + 0.5)
end
end
end
if new_state.keepaspect then
local scaled_w, scaled_h = self:_aspect_calc_panscan(new_state)
local video_left, video_right = self:_split_scaling(new_state.screen_w, scaled_w,
new_state.video_zoom, new_state.video_align_x, new_state.video_pan_x)
local video_top, video_bottom = self:_split_scaling(new_state.screen_h, scaled_h,
new_state.video_zoom, new_state.video_align_y, new_state.video_pan_y)
self.bounds = { left = video_left, right = video_right, top = video_top, bottom = video_bottom,
width = video_right - video_left, height = video_bottom - video_top }
else
self.bounds = { left = 0, top = 0, right = self.screen.width, bottom = self.screen.height,
width = self.screen.width, height = self.screen.height }
end
-- Use the same source for scale as was used to compute bounds (prefer dwidth/dheight)
local basis_w = new_state.video_w or new_state.video_w_raw
local basis_h = new_state.video_h or new_state.video_h_raw
self.scale.x = basis_w / self.bounds.width
self.scale.y = basis_h / self.bounds.height
self.video_ready = true
end
self.screen_ready = true
return self.screen_ready
end
function DisplayState:screen_to_video(x, y)
local nx = (x - self.bounds.left) * self.scale.x
local ny = (y - self.bounds.top) * self.scale.y
return nx, ny
end
function DisplayState:video_to_screen(x, y)
local nx = (x / self.scale.x) + self.bounds.left
local ny = (y / self.scale.y) + self.bounds.top
return nx, ny
end
-- create one instance for use
display_state = DisplayState.new()
-- store observer ids in a local table so we can unobserve on unload
local display_observer_ids = {}
-- Register observers for properties that affect display mapping.
-- When any of these change, recalculate the display bounds once.
local function register_display_observers()
local props = {
"dwidth", "dheight",
"video-out-params/w", "video-out-params/h",
"video-aspect-override",
"panscan", "video-zoom", "video-unscaled",
"video-align-x", "video-align-y",
"video-pan-x", "video-pan-y",
"keepaspect", "keepaspect-window",
"fullscreen", "window-maximized"
}
for _, p in ipairs(props) do
local cb = function(name, value)
if display_state and display_state.recalculate_bounds then
display_state:recalculate_bounds(true)
end
end
local id = mp.observe_property(p, "native", cb)
table.insert(display_observer_ids, id)
end
end
local function unregister_display_observers()
if not display_observer_ids then return end
for _, id in ipairs(display_observer_ids) do
mp.unobserve_property(id)
end
display_observer_ids = {}
end
local function osd_to_video_coords(x, y)
-- Use cached DisplayState
if display_state and display_state.recalculate_bounds then
-- Ensure display state is up-to-date and video mapping is ready
display_state:recalculate_bounds()
if display_state.video_ready then
local vx, vy = display_state:screen_to_video(x, y)
return math.floor(vx + 0.5), math.floor(vy + 0.5)
end
end
return nil
end
-- Simplified interactive ASS cropper
local asscrop_active = false
local asscrop_start_pos = nil
local asscrop_end_pos = nil
local asscrop_timer = nil
local asscrop_count = 0
local asscrop_callback = nil
local function asscrop_update()
if not asscrop_overlay then return end
local ass = assdraw.ass_new()
if asscrop_start_pos then
local sx, sy = asscrop_start_pos[1], asscrop_start_pos[2]
local mx, my = mp.get_mouse_pos()
local ex, ey
if asscrop_end_pos then
ex, ey = asscrop_end_pos[1], asscrop_end_pos[2]
else
ex, ey = mx, my
end
local x0 = math.min(sx, ex)
local y0 = math.min(sy, ey)
local x1 = math.max(sx, ex)
local y1 = math.max(sy, ey)
local osd_w, osd_h = mp.get_osd_size()
-- dim outside area using inverse clip so the interior stays untouched
local overlay_transparency = 160 -- 0-255
local overlay_lightness = 0
local format_dim = string.format("{\\bord0\\1a&H%02X&\\1c&H%02X%02X%02X&}",
overlay_transparency,
overlay_lightness,
overlay_lightness,
overlay_lightness)
ass:new_event()
ass:append(string.format("{\\iclip(%d,%d,%d,%d)}", x0, y0, x1, y1))
ass:pos(0,0)
ass:append(format_dim)
ass:draw_start()
ass:rect_cw(0,0, osd_w, osd_h)
ass:draw_stop()
-- Draw border using transparent primary (no fill) and \\3c for outline color
local line_color = 220
local box_format = string.format("{\\1a&HFF&\\1c&H000000&\\3c&H%02X%02X%02X&\\bord1}",
line_color,
line_color,
line_color)
ass:new_event()
ass:pos(0,0)
ass:append(box_format)
ass:draw_start()
ass:rect_cw(x0, y0, x1, y1)
ass:draw_stop()
-- Handles: transparent fill + white border
local hs = 10
local handle_format = "{\\1a&HFF&\\1c&H000000&\\3c&HFFFFFF&\\bord1}"
ass:new_event()
ass:pos(0,0)
ass:append(handle_format)
ass:draw_start()
ass:rect_cw(x0-hs, y0-hs, x0+hs, y0+hs)
ass:rect_cw(x1-hs, y0-hs, x1+hs, y0+hs)
ass:rect_cw(x0-hs, y1-hs, x0+hs, y1+hs)
ass:rect_cw(x1-hs, y1-hs, x1+hs, y1+hs)
ass:draw_stop()
-- Size text
local sw = math.max(0, x1 - x0)
local sh = math.max(0, y1 - y0)
ass:new_event()
ass:pos(x1 - 6, y1 + 14)
ass:an(9)
ass:append(string.format("{\\fs16\\bord2\\shad0} %dx%d", sw, sh))
else
ass:new_event()
ass:pos(10,10)
ass:append("{\\fs20\\bord2}Click once to set first corner, click again to set second. Enter to crop, Esc to cancel.")
end
local osd_w, osd_h = mp.get_osd_size()
mp.set_osd_ass(osd_w, osd_h, ass.text)
end
local function asscrop_cancel(msg)
asscrop_start_pos = nil
asscrop_end_pos = nil
asscrop_callback = nil
if asscrop_timer then asscrop_timer:kill() asscrop_timer = nil end
mp.set_osd_ass(0,0, "")
mp.remove_key_binding("lines_asscrop_mouse")
mp.remove_key_binding("lines_asscrop_enter")
mp.remove_key_binding("lines_asscrop_esc")
asscrop_active = false
if msg then
mp.osd_message("Crop canceled")
end
mp.commandv('script-message-to', 'uosc', 'disable-elements', mp.get_script_name(), '')
end
local function asscrop_finish()
if not (asscrop_start_pos and asscrop_end_pos) then
mp.osd_message("No crop area set")
msg.info("No crop area set")
return
end
-- compute video-space crop
local x1s, y1s = asscrop_start_pos[1], asscrop_start_pos[2]
local x2s, y2s = asscrop_end_pos[1], asscrop_end_pos[2]
local x0 = math.min(x1s, x2s)
local y0 = math.min(y1s, y2s)
local x1 = math.max(x1s, x2s)
local y1 = math.max(y1s, y2s)
local vx0, vy0
local vw, vh
do
local tx0, ty0 = osd_to_video_coords(x0, y0)
local tx1, ty1 = osd_to_video_coords(x1, y1)
if not tx0 or not tx1 then
mp.osd_message("Unable to determine video coordinates")
msg.warn("Unable to determine video coordinates")
return
end
vx0, vy0 = tx0, ty0
vw = math.abs(tx1 - tx0)
vh = math.abs(ty1 - ty0)
end
if vw <= 0 or vh <= 0 then
mp.osd_message("Bad crop size")
msg.warn("Bad crop size")
return
end
-- If a callback is registered (used by take_screenshot first-shot flow),
-- call it with crop and skip mpv crop
if asscrop_callback then
local cb = asscrop_callback
asscrop_callback = nil
cb({ x = vx0, y = vy0, w = vw, h = vh })
asscrop_cancel(false)
return
end
local crop_arg = string.format("crop=%d:%d:%d:%d", vw, vh, vx0, vy0)
asscrop_count = asscrop_count + 1
perform_capture(asscrop_count, crop_arg)
asscrop_cancel(false)
end
local function asscrop_mouse(e)
if not e or not e.event then return end
if e.event == "down" then
local mx, my = mp.get_mouse_pos()
if not asscrop_start_pos then
asscrop_start_pos = {mx, my}
if not asscrop_timer then asscrop_timer = mp.add_periodic_timer(1/60, asscrop_update) end
-- immediate update so rectangle follows cursor
asscrop_update()
else
asscrop_end_pos = {mx, my}
asscrop_update()
end
end
end
local function asscrop_begin()
local path = mp.get_property("path")
if not path then
return
end
if asscrop_active then
asscrop_cancel(true)
return
end
asscrop_active = true
asscrop_start_pos = nil
asscrop_end_pos = nil
asscrop_overlay = mp.create_osd_overlay("ass-events")
asscrop_timer = nil
-- disable certain uosc UI elements
mp.commandv('script-message-to', 'uosc', 'disable-elements', mp.get_script_name(),
'top_bar,timeline,controls,volume,idle_indicator,audio_indicator,buffering_indicator,pause_indicator')
-- forced mouse binding to capture clicks
mp.add_forced_key_binding("MBTN_LEFT", "lines_asscrop_mouse", asscrop_mouse, {complex=true})
mp.add_forced_key_binding("ENTER", "lines_asscrop_enter", function() asscrop_finish() end)
mp.add_forced_key_binding("ESC", "lines_asscrop_esc", function() asscrop_cancel(true) end)
mp.osd_message("Crop: Click first corner, then second. Enter to confirm, Esc to cancel.")
end
local function take_screenshot()
local crop_arg = ""
local path = mp.get_property("path")
if not path then
return
end
local v_w = mp.get_property_number("dwidth") or mp.get_property_number("width", 0)
local v_h = mp.get_property_number("dheight") or mp.get_property_number("height", 0)
screenshot_count = screenshot_count + 1
if subtitle_top == 0 then
screenshot_count = 1
end
-- If first shot (or subtitle not set), defer actual capture until after ASS cropper callback
if screenshot_count == 1 then
asscrop_callback = function(crop)
-- crop is video-space x,y,w,h
subtitle_top = crop.y / v_h
subtitle_bottom = (v_h - (crop.y + crop.h)) / v_h
if subtitle_top < 0 or subtitle_bottom < 0 then
subtitle_top = 0
subtitle_bottom = 0
mp.osd_message("Invalid subtitle margins\nplease make sure the subtitles are within the video frame")
msg.warn("Invalid subtitle margins, please make sure the subtitles are within the video frame")
return
end
mp.osd_message(string.format("Subtitle region set: top=%.3f bottom=%.3f", subtitle_top, subtitle_bottom))
msg.verbose(string.format("Subtitle region set: top=%.3f bottom=%.3f", subtitle_top, subtitle_bottom))
-- now perform the actual capture using the newly set margins
local effective_subtitle_top = 1 - subtitle_bottom
local crop_h = math.max(1, math.floor(v_h * effective_subtitle_top + 0.5))
local crop_w = v_w
local crop_x = 0
local crop_y = 0
crop_arg = string.format("crop=%d:%d:%d:%d", crop_w, crop_h, crop_x, crop_y)
perform_capture(screenshot_count, crop_arg, true)
end
-- Launch the ASS cropper UI and wait for callback to do the capture
asscrop_begin()
else
local crop_height_frac = 1 - subtitle_top - subtitle_bottom
if crop_height_frac <= 0 then
screenshots = {}
screenshot_count = 0
subtitle_top = 0
subtitle_bottom = 0
mp.osd_message("Invalid subtitle margins, skipping crop")
msg.warn("Invalid subtitle margins, skipping crop")
else
local crop_h = math.max(1, math.floor(v_h * crop_height_frac + 0.5))
local crop_w = v_w
local crop_x = 0
local crop_y = math.max(0, math.floor(v_h * subtitle_top + 0.5))
crop_arg = string.format("crop=%d:%d:%d:%d", crop_w, crop_h, crop_x, crop_y)
perform_capture(screenshot_count, crop_arg, true)
end
end
end
-- stitch the cropped screenshots together
local function stitch_images()
if #screenshots <= 1 then
mp.osd_message("No shots to stitch!")
return
end
-- remove entries that no longer exist on disk
for i = #screenshots, 1, -1 do
if not file_exist(screenshots[i]) then
table.remove(screenshots, i)
end
end
local command = { options.ffmpeg_path, "-loglevel", options.ffmpeg_loglevel, "-y" }
local output_file = utils.join_path(screenshot_dir, "stitched_screenshot_" ..
os.date("%Y%m%d_%H%M%S") .. screenshot_format)
local filter_expr = "vstack=" .. #screenshots
-- add input files, filter, output filename, one by one
for i = 1, #screenshots do
table.insert(command, "-i")
table.insert(command, screenshots[i])
end
table.insert(command, "-filter_complex")
table.insert(command, filter_expr)
table.insert(command, output_file)
-- run the command
local result = utils.subprocess({ args = command })
if result.status == 0 then
mp.osd_message("Stitched shot saved: " .. output_file)
msg.verbose("Stitched shot saved: " .. output_file)
else
mp.osd_message("Stitching failed")
msg.verbose("Stitching failed: " .. (result and result.error or "unknown"))
return
end
-- delete the cropped shots and reset the counter
for _, img in ipairs(screenshots) do
os.remove(img)
end
screenshots = {}
screenshot_count = 0
subtitle_top = 0
subtitle_bottom = 0
end
mp.register_event("file-loaded", function()
if display_state then
display_state:recalculate_bounds(true)
end
register_display_observers()
end)
mp.add_hook("on_unload", 50, function()
unregister_display_observers()
end)
-- bindings
mp.add_key_binding(nil, "take-screenshot", take_screenshot)
mp.add_key_binding(nil, "stitch-images", stitch_images)
mp.add_key_binding(nil, "crop-screenshot", asscrop_begin)
+224
View File
@@ -0,0 +1,224 @@
-- Install [Torrserver](https://github.com/YouROK/TorrServer)
-- then add "script-opts-append=mpv_torrserver-server=http://[TorrServer ip]:[port]" to mpv.conf
local utils = require 'mp.utils'
local opts = {
server = "http://localhost:8090",
torrserver_init = false,
torrserver_path = "TorrServer",
search_for_external_tracks = true
}
(require 'mp.options').read_options(opts)
local luacurl_available, cURL = pcall(require, 'cURL')
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, windows uses backslashes
local function find_executable(name)
local os_path = os.getenv("PATH") or ""
local fallback_path = utils.join_path("/usr/bin", name)
local exec_path
for path in os_path:gmatch("[^:]+") do
exec_path = utils.join_path(path, name)
local meta, meta_error = utils.file_info(exec_path)
if meta and meta.is_file then
return exec_path
end
end
if not is_windows then return fallback_path end
return name -- fallback to just the name, hoping it's in PATH
end
local function init()
local exec_path = find_executable(opts.torrserver_path)
local windows_args = { 'powershell', '-NoProfile', '-Command', exec_path }
local unix_args = { '/bin/bash', '-c', exec_path }
local args = is_windows and windows_args or unix_args
local res = mp.command_native_async({ name = "subprocess", capture_stdout = true, playback_only = false, args = args })
if res.status == 0 then
mp.msg.error("TorrServer failed to start: ")
end
end
local char_to_hex = function(c)
return string.format("%%%02X", string.byte(c))
end
local function urlencode(url)
if url == nil then
return
end
url = url:gsub("\n", "\r\n")
url = url:gsub("([^%w ])", char_to_hex)
url = url:gsub(" ", "+")
return url
end
local function get_magnet_info(url)
local info_url = opts.server .. "/stream?stat&link=" .. urlencode(url)
local res
if not (luacurl_available) then
-- if Lua-cURL is not available on this system
local curl_cmd = {
"curl",
"-L",
"--silent",
"--max-time", "10",
info_url
}
local cmd = mp.command_native {
name = "subprocess",
capture_stdout = true,
playback_only = false,
args = curl_cmd
}
res = cmd.stdout
else
-- otherwise use Lua-cURL (binding to libcurl)
local buf = {}
local c = cURL.easy_init()
c:setopt_followlocation(1)
c:setopt_url(info_url)
c:setopt_writefunction(function(chunk)
table.insert(buf, chunk);
return true;
end)
c:perform()
res = table.concat(buf)
end
if res and res ~= "" then
return (require 'mp.utils').parse_json(res)
else
return nil, "no info response (timeout?)"
end
end
local function edlencode(url)
return "%" .. string.len(url) .. "%" .. url
end
local function guess_type_by_extension(ext)
if ext == "mkv" or ext == "mp4" or ext == "avi" or ext == "wmv" or ext == "vob" or ext == "m2ts" or ext == "ogm" then
return "video"
end
if ext == "mka" or ext == "mp3" or ext == "aac" or ext == "flac" or ext == "ogg" or ext == "wma" or ext == "mpg"
or ext == "wav" or ext == "wv" or ext == "opus" or ext == "ac3" then
return "audio"
end
if ext == "ass" or ext == "srt" or ext == "vtt" then
return "sub"
end
return "other";
end
local function string_replace(str, match, replace)
local s, e = string.find(str, match, 1, true)
if s == nil or e == nil then
return str
end
return string.sub(str, 1, s - 1) .. replace .. string.sub(str, e + 1)
end
-- https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst
local function generate_m3u(magnet_uri, files)
for _, fileinfo in ipairs(files) do
-- strip top directory
if fileinfo.path:find("/", 1, true) then
fileinfo.fullpath = string.sub(fileinfo.path, fileinfo.path:find("/", 1, true) + 1)
else
fileinfo.fullpath = fileinfo.path
end
fileinfo.path = {}
for w in fileinfo.fullpath:gmatch("([^/]+)") do table.insert(fileinfo.path, w) end
local ext = string.match(fileinfo.path[#fileinfo.path], "%.(%w+)$")
fileinfo.type = guess_type_by_extension(ext)
end
table.sort(files, function(a, b)
-- make top-level files appear first in the playlist
if (#a.path == 1 or #b.path == 1) and #a.path ~= #b.path then
return #a.path < #b.path
end
-- make videos first
if (a.type == "video" or b.type == "video") and a.type ~= b.type then
return a.type == "video"
end
-- otherwise sort by path
return a.fullpath < b.fullpath
end);
local infohash = magnet_uri:match("^magnet:%?xt=urn:bt[im]h:(%w+)") or urlencode(magnet_uri)
local playlist = { '#EXTM3U' }
for _, fileinfo in ipairs(files) do
if fileinfo.processed ~= true then
table.insert(playlist, '#EXTINF:0,' .. fileinfo.fullpath)
local basename = string.match(fileinfo.path[#fileinfo.path], '^(.+)%.%w+$')
local url = opts.server .. "/stream/" .. urlencode(fileinfo.fullpath) .."?play&index=" .. fileinfo.id .. "&link=" .. infohash
local hdr = { "!new_stream", "!no_clip",
--"!track_meta,title=" .. edlencode(basename),
edlencode(url)
}
local edl = "edl://" .. table.concat(hdr, ";") .. ";"
local external_tracks = 0
fileinfo.processed = true
if opts.search_for_external_tracks and basename ~= nil and fileinfo.type == "video" then
mp.msg.info("!" .. basename)
for _, fileinfo2 in ipairs(files) do
if #fileinfo2.path > 0 and
fileinfo2.type ~= "other" and
fileinfo2.processed ~= true and
string.find(fileinfo2.path[#fileinfo2.path], basename, 1, true) ~= nil
then
mp.msg.info("->" .. fileinfo2.fullpath)
local title = string_replace(fileinfo2.fullpath, basename, "%")
local url = opts.server .. "/stream/" .. urlencode(fileinfo2.fullpath).."?play&index=" .. fileinfo2.id .. "&link=" .. infohash
local hdr = { "!new_stream", "!no_clip", "!no_chapters",
"!delay_open,media_type=" .. fileinfo2.type,
"!track_meta,title=" .. edlencode(title),
edlencode(url)
}
edl = edl .. table.concat(hdr, ";") .. ";"
fileinfo2.processed = true
external_tracks = external_tracks + 1
end
end
end
if external_tracks == 0 then -- dont use edl
table.insert(playlist, url)
else
table.insert(playlist, edl)
end
end
end
return table.concat(playlist, '\n')
end
mp.add_hook("on_load", 5, function()
local url = mp.get_property("stream-open-filename")
if url:find("^magnet:") == 1 or (url:find("^https?://") == 1 and url:find("%.torrent$") ~= nil) then
mp.set_property_bool("file-local-options/ytdl", false)
if opts.torrserver_init then init() end
local magnet_info, err = get_magnet_info(url)
if type(magnet_info) == "table" then
if magnet_info.file_stats then
-- torrent has multiple files. open as playlist
mp.set_property("stream-open-filename", "memory://" .. generate_m3u(url, magnet_info.file_stats))
return
end
-- if not a playlist and has a name
if magnet_info.name then
mp.set_property("stream-open-filename", "memory://#EXTM3U\n" ..
"#EXTINF:0," .. magnet_info.name .. "\n" ..
opts.server .. "/stream?play&index=1&link=" .. urlencode(url))
return
end
else
mp.msg.warn("error: " .. err)
end
mp.set_property("stream-open-filename", opts.server .. "/stream?m3u&link=" .. urlencode(url))
end
end)
+126
View File
@@ -0,0 +1,126 @@
-- original by Wanakachi
-- suitable for Windows operating systems
-- picture-in-picture (PiP) mode for mpv
local options = require "mp.options"
local o = {
autofit = "25%x25%",
autofit_restore_larger = "100%x100%",
autofit_restore_smaller = "40%x30%",
geometry = "100%:100%", -- pip position, bottom-right: 100%:100%
geometry_restore = "50%:50%", -- restore position, center: 50%:50%
geometry_delay = 0.05 -- delay to avoid geometry race on state change
}
options.read_options(o, _, function() end)
local pip_enabled = false
local geometry_timer = nil
local original_options = {}
-- save original window properties
local function save_original_options()
original_options.fullscreen = mp.get_property_bool("fullscreen")
original_options.window_maximized = mp.get_property_bool("window-maximized")
original_options.force_window_position = mp.get_property_bool("force-window-position")
original_options.keepaspect_window = mp.get_property_bool("keepaspect-window")
original_options.border = mp.get_property_bool("border")
original_options.ontop = mp.get_property_bool("ontop")
original_options.window_scale = mp.get_property("window_scale")
original_options.autofit = mp.get_property("autofit")
original_options.autofit_larger = mp.get_property("autofit-larger") ~= "" and mp.get_property("autofit-larger") or o.autofit_restore_larger
original_options.autofit_smaller = mp.get_property("autofit-smaller") ~= "" and mp.get_property("autofit-smaller") or o.autofit_restore_smaller
original_options.geometry = mp.get_property("geometry") ~= "" and mp.get_property("geometry") or o.geometry_restore
end
-- restore original window properties
local function restore_original_options()
if original_options.fullscreen ~= nil then mp.set_property_bool("fullscreen", original_options.fullscreen) end
if original_options.window_maximized ~= nil then mp.set_property_bool("window-maximized", original_options.window_maximized) end
if original_options.force_window_position ~= nil then mp.set_property_bool("force-window-position", original_options.force_window_position) end
if original_options.border ~= nil then mp.set_property_bool("border", original_options.border) end
if original_options.ontop ~= nil then mp.set_property_bool("ontop", original_options.ontop) end
if original_options.keepaspect_window ~= nil then mp.set_property_bool("keepaspect-window", original_options.keepaspect_window) end
if original_options.window_scale ~= nil then mp.set_property("window_scale", original_options.window_scale) end
if original_options.autofit ~= nil then mp.set_property("autofit", original_options.autofit) end
if original_options.autofit_larger ~= nil then mp.set_property("autofit-larger", original_options.autofit_larger) end
if original_options.autofit_smaller ~= nil then mp.set_property("autofit-smaller", original_options.autofit_smaller) end
if original_options.geometry ~= nil then mp.set_property("geometry", original_options.geometry) end
end
local function cancel_geometry_timer()
if geometry_timer then
geometry_timer:kill()
geometry_timer = nil
end
end
local function set_geometry_delay(value)
cancel_geometry_timer()
geometry_timer = mp.add_timeout(o.geometry_delay, function()
geometry_timer = nil
mp.set_property("geometry", value)
end)
end
-- enter PiP mode
local function enable_pip()
if pip_enabled then return end
save_original_options()
mp.set_property_bool("fullscreen", false) -- exit fullscreen if necessary
mp.set_property_bool("window-minimized", false)
mp.set_property_bool("force-window-position", false)
mp.set_property_bool("border", false) -- remove decorations
mp.set_property_bool("ontop", true) -- keep above other windows
mp.set_property_bool("keepaspect-window", true)
mp.set_property("autofit", o.autofit)
mp.set_property("autofit-larger", "")
mp.set_property("autofit-smaller", "")
mp.set_property("window_scale", "")
set_geometry_delay(o.geometry)
pip_enabled = true
mp.osd_message("PiP enabled")
end
-- leave PiP mode
local function disable_pip()
if not pip_enabled then return end
cancel_geometry_timer()
restore_original_options()
pip_enabled = false
mp.osd_message("PiP disabled")
end
-- toggle PiP on/off
local function toggle_pip()
if pip_enabled then
disable_pip()
else
enable_pip()
end
end
-- monitor fullscreen state changes and exit PiP first
mp.observe_property("fullscreen", "bool", function(_, value)
if value and pip_enabled then
disable_pip()
mp.set_property_bool("fullscreen", true)
end
end)
-- monitor window-maximized state changes (on some systems maximize and fullscreen are separate)
mp.observe_property("window-maximized", "bool", function(_, value)
if value and pip_enabled then
disable_pip()
mp.set_property_bool("window-maximized", true)
end
end)
-- binding
mp.add_key_binding(nil, "toggle-pip", toggle_pip)
+6 -5
View File
@@ -54,12 +54,13 @@ local function url_decode(str)
if str:find('://localhost:?') then if str:find('://localhost:?') then
str = str:gsub('^.*/', '') str = str:gsub('^.*/', '')
end end
str = str:gsub('%?.+', '') str = str:gsub("%?.+", ""):gsub("%+", " ")
:gsub('%+', ' ') local last_pos = str:match('.*[\\/:%?]()')
return str if last_pos then
else str = str:sub(last_pos)
return end
end end
return str
end end
local function is_compressed_file(filename) local function is_compressed_file(filename)
+176 -74
View File
@@ -1,10 +1,13 @@
-- trackselect.lua --[[
-- https://github.com/po5/trackselect SOURCE_: https://github.com/po5/trackselect
-- Because --slang isn't smart enough. Modify_: https://github.com/dyphire/mpv-scripts
--
-- This script tries to select non-dub -- Because --slang isn't smart enough.
-- audio and subtitle tracks. --
-- Idea from https://github.com/siikamiika/scripts/blob/master/mpv%20scripts/dualaudiofix.lua -- This script tries to select non-dub
-- audio and subtitle tracks.
-- Idea from https://github.com/siikamiika/scripts/blob/master/mpv%20scripts/dualaudiofix.lua
]]
local opt = require 'mp.options' local opt = require 'mp.options'
local utils = require 'mp.utils' local utils = require 'mp.utils'
@@ -14,9 +17,10 @@ local defaults = {
selected = nil, selected = nil,
best = {}, best = {},
lang_score = nil, lang_score = nil,
expected_score = nil,
channels_score = -math.huge, channels_score = -math.huge,
preferred = "jpn/japanese", preferred = "jpn/japanese",
excluded = "", excluded = "commentary/cast/staff/dub/guide",
expected = "", expected = "",
id = "" id = ""
}, },
@@ -24,6 +28,7 @@ local defaults = {
selected = nil, selected = nil,
best = {}, best = {},
lang_score = nil, lang_score = nil,
expected_score = nil,
preferred = "", preferred = "",
excluded = "", excluded = "",
expected = "", expected = "",
@@ -33,8 +38,9 @@ local defaults = {
selected = nil, selected = nil,
best = {}, best = {},
lang_score = nil, lang_score = nil,
expected_score = nil,
preferred = "eng", preferred = "eng",
excluded = "sign", excluded = "sign/song",
expected = "", expected = "",
id = "" id = ""
} }
@@ -54,11 +60,6 @@ local options = {
-- Try to re-select the last track if mpv cannot do it e.g. when fingerprint changes -- Try to re-select the last track if mpv cannot do it e.g. when fingerprint changes
smart_keep = false, smart_keep = false,
--add above (after a comma) any protocol to disable
special_protocols = [[
["://", "^magnet:"]
]],
} }
for _type, track in pairs(defaults) do for _type, track in pairs(defaults) do
@@ -72,41 +73,82 @@ options["preferred_audio_channels"] = ""
local tracks = {} local tracks = {}
local last = {} local last = {}
local fingerprint = "" local fingerprint = ""
local trackselect_ran = false
opt.read_options(options, _, function() end) opt.read_options(options, _, function() end)
options.special_protocols = utils.parse_json(options.special_protocols) local function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end
local function need_ignore(tab, val) local function hex_to_char(x)
for index, element in ipairs(tab) do return string.char(tonumber(x, 16))
if string.find(val, element) then end
return true
local function url_decode(str)
if str ~= nil then
str = str:gsub('^%a[%a%d-_]+://', '')
:gsub('^%a[%a%d-_]+:\\?', '')
:gsub('%%(%x%x)', hex_to_char)
if str:find('://localhost:?') then
str = str:gsub('^.*/', '')
end end
str = str:gsub("%?.+", ""):gsub("%+", " ")
local last_pos = str:match('.*[\\/:%?]()')
if last_pos then
str = str:sub(last_pos)
end
end
return str
end
local function is_empty(input)
if input == nil or input == "" then
return true
end
end
local function replace(str, what, with)
if is_empty(str) then return "" end
if is_empty(what) then return str end
if with == nil then with = "" end
what = string.gsub(what, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1")
with = string.gsub(with, "[%%]", "%%%%")
return string.gsub(str, what, with)
end
local function comparison(str1, str2)
if not str1 and not str2 then
return true
end
if str1 and str2 then
return str1:lower() == str2:lower()
end end
return false return false
end end
function contains(track, words, attr) local function contains(track, words, attr)
if not track[attr] then return false end if not track[attr] then return false end
local i = 0 local i = 0
if track.external then if track.external then
i = 1 i = 1
end end
for word in string.gmatch(words:lower(), "([^/]+)") do for word in string.gmatch(words:lower(), "([^/]+)") do
local w = word:match("^%s*(.-)%s*$")
i = i - 1 i = i - 1
if string.find(tostring(track[attr] or ""):lower(), word) then if w ~= "" and string.find(tostring(track[attr] or ""):lower(), w) then
return i return i
end end
end end
return false return false
end end
function preferred(track, words, attr, title) local function preferred(track, words, attr, title)
local score = contains(track, words, attr) local score = contains(track, words, attr)
if not score then if not score then
if tracks[track.type][title] == nil then if tracks[track.type][title] == nil then
tracks[track.type][title] = -math.huge tracks[track.type][title] = -math.huge
return true return false
end end
return false return false
end end
@@ -117,10 +159,11 @@ function preferred(track, words, attr, title)
return false return false
end end
function preferred_or_equals(track, words, attr, title) local function preferred_or_equals(track, words, attr, title)
local score = contains(track, words, attr) local score = contains(track, words, attr)
if not score then if not score then
if tracks[track.type][title] == nil or tracks[track.type][title] == -math.huge then if (tracks[track.type][title] == nil or
tracks[track.type][title] == -math.huge) and track.default then
return true return true
end end
return false return false
@@ -131,14 +174,14 @@ function preferred_or_equals(track, words, attr, title)
return false return false
end end
function copy(obj) local function copy(obj)
if type(obj) ~= "table" then return obj end if type(obj) ~= "table" then return obj end
local res = {} local res = {}
for k, v in pairs(obj) do res[k] = copy(v) end for k, v in pairs(obj) do res[k] = copy(v) end
return res return res
end end
function track_layout_hash(tracklist) local function track_layout_hash(tracklist)
local t = {} local t = {}
for _, track in ipairs(tracklist) do for _, track in ipairs(tracklist) do
t[#t + 1] = string.format("%s-%d-%s-%s-%s-%s", track.type, track.id, tostring(track.default), t[#t + 1] = string.format("%s-%d-%s-%s-%s-%s", track.type, track.id, tostring(track.default),
@@ -147,12 +190,51 @@ function track_layout_hash(tracklist)
return table.concat(t, "\n") return table.concat(t, "\n")
end end
function trackselect() local function sanitize_title(track, path, filename, media_title)
local fpath = mp.get_property('path') if not track or not track.title then return end
if not options.enabled then return end local title = filename
if need_ignore(options.special_protocols, fpath) then return end if is_protocol(path) then
tracks = copy(defaults) title = url_decode(media_title):gsub("%.?([^%.]+)$", "")
track.title = url_decode(track.title)
end
track.title = replace(track.title, title, "")
end
local function selected_tracks()
if not trackselect_ran then
return
end
last = {}
local tracklist = mp.get_property_native("track-list")
local path = mp.get_property("path")
local filename = mp.get_property("filename/no-ext") local filename = mp.get_property("filename/no-ext")
local media_title = mp.get_property("media-title", "")
for _, track in ipairs(tracklist) do
if track.selected then
sanitize_title(track, path, filename, media_title)
last[track.type] = track
end
end
end
local function trackselect()
if options.smart_keep then
-- observe specific current-tracks sub-properties
local props = { 'current-tracks/video', 'current-tracks/audio', 'current-tracks/sub' }
for _, p in ipairs(props) do
mp.observe_property(p, 'native', selected_tracks)
end
end
trackselect_ran = true
if not options.enabled then
return
end
tracks = copy(defaults)
local path = mp.get_property("path")
local filename = mp.get_property("filename/no-ext")
local media_title = mp.get_property("media-title", "")
local tracklist = mp.get_property_native("track-list") local tracklist = mp.get_property_native("track-list")
local tracklist_changed = false local tracklist_changed = false
local found_last = {} local found_last = {}
@@ -164,10 +246,14 @@ function trackselect()
fingerprint = new_fingerprint fingerprint = new_fingerprint
tracklist_changed = true tracklist_changed = true
end end
for _, track in ipairs(tracklist) do for _, track in ipairs(tracklist) do
if options.smart_keep and last[track.type] ~= nil and last[track.type].lang == track.lang and sanitize_title(track, path, filename, media_title)
last[track.type].codec == track.codec and last[track.type].external == track.external and if options.smart_keep and last[track.type] ~= nil and
last[track.type].title == track.title then comparison(last[track.type].lang, track.lang) and
track.codec ~= "null" and last[track.type].codec == track.codec and
last[track.type].external == track.external and
comparison(last[track.type].title, track.title) then
tracks[track.type].best = track tracks[track.type].best = track
options["preferred_" .. track.type .. "_lang"] = "" options["preferred_" .. track.type .. "_lang"] = ""
options["excluded_" .. track.type .. "_words"] = "" options["excluded_" .. track.type .. "_words"] = ""
@@ -185,40 +271,64 @@ function trackselect()
options["preferred_" .. track.type .. "_channels"] = "" options["preferred_" .. track.type .. "_channels"] = ""
end end
end end
if options["preferred_" .. track.type .. "_lang"] ~= "" or options["excluded_" .. track.type .. "_words"] ~= "" if track.codec ~= "null" and options["preferred_" .. track.type .. "_lang"] ~= "" or
or options["expected_" .. track.type .. "_words"] ~= "" or options["excluded_" .. track.type .. "_words"] ~= "" or
options["expected_" .. track.type .. "_words"] ~= "" or
(options["preferred_" .. track.type .. "_channels"] or "") ~= "" then (options["preferred_" .. track.type .. "_channels"] or "") ~= "" then
local preferred_lang = options["preferred_" .. track.type .. "_lang"]
local excluded_words = options["excluded_" .. track.type .. "_words"]
local expected_words = options["expected_" .. track.type .. "_words"]
local preferred_channels = options["preferred_" .. track.type .. "_channels"]
if preferred_lang == "" then
if track.type == "video" then
preferred_lang = mp.get_property("vlang", ""):gsub(",", "/")
elseif track.type == "audio" then
preferred_lang = mp.get_property("alang", ""):gsub(",", "/")
elseif track.type == "sub" then
preferred_lang = mp.get_property("slang", ""):gsub(",", "/")
end
end
if track.selected then if track.selected then
tracks[track.type].selected = track.id tracks[track.type].selected = track.id
if options.smart_keep then if options.smart_keep then
last[track.type] = track last[track.type] = track
end end
end end
if track.title then
track.title = string.gsub(string.gsub(track.title, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1"), filename, "") if (next(tracks[track.type].best) == nil or not (tracks[track.type].best.external
end and tracks[track.type].best.lang ~= nil and not track.external)) then
if next(tracks[track.type].best) == nil or not (tracks[track.type].best.external if excluded_words == "" or not contains(track, excluded_words, "title") then
and tracks[track.type].best.lang ~= nil and not track.external) then local channels = false
if options["excluded_" .. track.type .. "_words"] == "" or local lang = false
not contains(track, options["excluded_" .. track.type .. "_words"], "title") then if (preferred_channels or "") ~= "" and
if options["expected_" .. track.type .. "_words"] == "" or preferred_or_equals(track, preferred_lang, "lang", "lang_score") then
contains(track, options["expected_" .. track.type .. "_words"], "title") then channels = preferred(track, preferred_channels, "demux-channel-count", "channels_score")
local pass = true end
local channels = false if preferred_lang ~= "" then
local lang = false lang = preferred(track, preferred_lang, "lang", "lang_score")
if (options["preferred_" .. track.type .. "_channels"] or "") ~= "" and end
preferred_or_equals(track, options["preferred_" .. track.type .. "_lang"], "lang",
"lang_score") then local exp_score = nil
channels = preferred(track, options["preferred_" .. track.type .. "_channels"], if expected_words then
"demux-channel-count", "channels_score") exp_score = contains(track, expected_words, "title")
pass = channels end
if exp_score then
local lang_score = nil
local existing_lang = nil
local existing_exp = tracks[track.type].expected_score
if next(tracks[track.type].best) ~= nil and preferred_lang ~= "" then
existing_lang = contains(tracks[track.type].best, preferred_lang, "lang")
lang_score = contains(track, preferred_lang, "lang")
end end
if options["preferred_" .. track.type .. "_lang"] ~= "" then if existing_exp == nil or exp_score > existing_exp or (exp_score == existing_exp and
lang = preferred(track, options["preferred_" .. track.type .. "_lang"], "lang", "lang_score") ((lang_score or -math.huge) > (existing_lang or -math.huge) or channels)) then
tracks[track.type].expected_score = exp_score
tracks[track.type].best = track
end end
if (options["preferred_" .. track.type .. "_lang"] == "" and pass) or channels or lang or elseif tracks[track.type].expected_score == nil then
(track.external and track.lang == nil and if (preferred_lang == "" and track.default) or channels or lang or
(not tracks[track.type].best.external or tracks[track.type].best.lang == nil)) then (track.external and track.lang == nil and next(tracks[track.type].best) == nil) then
tracks[track.type].best = track tracks[track.type].best = track
end end
end end
@@ -226,9 +336,10 @@ function trackselect()
end end
end end
end end
for _type, track in pairs(tracks) do for _type, track in pairs(tracks) do
if next(track.best) ~= nil and track.best.id ~= track.selected then if next(track.best) ~= nil and track.best.id ~= track.selected then
mp.set_property(_type:sub(1, 1) .. "id", track.best.id) mp.set_property('file-local-options/'.. _type:sub(1, 1) .. "id", track.best.id)
if options.smart_keep and found_last[track.best.type] then if options.smart_keep and found_last[track.best.type] then
last[track.best.type] = track.best last[track.best.type] = track.best
end end
@@ -236,22 +347,13 @@ function trackselect()
end end
end end
function selected_tracks()
local tracklist = mp.get_property_native("track-list")
last = {}
for _, track in ipairs(tracklist) do
if track.selected then
last[track.type] = track
end
end
end
if options.hook then if options.hook then
mp.add_hook("on_preloaded", 50, trackselect) mp.add_hook("on_preloaded", 50, trackselect)
else else
mp.register_event("file-loaded", trackselect) mp.register_event("file-loaded", trackselect)
end end
if options.smart_keep then mp.add_hook("on_unload", 50, function()
mp.register_event("track-switched", selected_tracks) trackselect_ran = false
end mp.unobserve_property(selected_tracks)
end)
+1 -1
View File
@@ -36,7 +36,7 @@ end
function Button:render() function Button:render()
local visibility = self:get_visibility() local visibility = self:get_visibility()
if visibility <= 0 then return end if visibility <= 0 then return end
cursor:zone('primary_down', self, function() self:handle_cursor_click() end) cursor:zone('primary_click', self, function() self:handle_cursor_click() end)
local ass = assdraw.ass_new() local ass = assdraw.ass_new()
local is_clickable = self.is_clickable and self.on_click ~= nil local is_clickable = self.is_clickable and self.on_click ~= nil
+13 -9
View File
@@ -108,6 +108,7 @@ function Menu:init(data, callback, opts)
self.opts = opts or {} self.opts = opts or {}
self.offset_x = 0 -- Used for submenu transition animation. self.offset_x = 0 -- Used for submenu transition animation.
self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items
self.mouse_hovered_index = nil -- Current menu's item being hovered by mouse, ignoring it's selectable or not.
self.item_height = nil self.item_height = nil
self.min_width = nil self.min_width = nil
self.item_spacing = 1 self.item_spacing = 1
@@ -700,7 +701,8 @@ end
---@param shortcut? Shortcut ---@param shortcut? Shortcut
function Menu:handle_cursor_up(shortcut) function Menu:handle_cursor_up(shortcut)
if self.proximity_raw <= -self.padding and self.drag_last_y and not self.is_dragging then if self.proximity_raw <= -self.padding and self.drag_last_y and not self.is_dragging
and self.mouse_hovered_index == self.current.selected_index then
self:activate_selected_item(shortcut, true) self:activate_selected_item(shortcut, true)
end end
if self.is_dragging then if self.is_dragging then
@@ -1456,14 +1458,16 @@ function Menu:render()
} }
-- Select hovered item -- Select hovered item
if is_current and self.mouse_nav and item.selectable ~= false if is_current and self.mouse_nav
-- Do not select items if cursor is moving towards a submenu
and (not submenu_rect or not cursor:direction_to_rectangle_distance(submenu_rect))
and (submenu_is_hovered or get_point_to_rectangle_proximity(cursor, item_rect_hitbox) <= 0) then and (submenu_is_hovered or get_point_to_rectangle_proximity(cursor, item_rect_hitbox) <= 0) then
menu.selected_index = index self.mouse_hovered_index = index
if not is_selected then -- Do not select items if cursor is moving towards a submenu
is_selected = true if item.selectable ~= false and (not submenu_rect or not cursor:direction_to_rectangle_distance(submenu_rect)) then
request_render() menu.selected_index = index
if not is_selected then
is_selected = true
request_render()
end
end end
end end
@@ -1555,7 +1559,7 @@ function Menu:render()
-- Select action on cursor hover -- Select action on cursor hover
if self.mouse_nav and get_point_to_rectangle_proximity(cursor, rect) <= 0 then if self.mouse_nav and get_point_to_rectangle_proximity(cursor, rect) <= 0 then
cursor:zone('primary_down', rect, self:create_action(function(shortcut) cursor:zone('primary_click', rect, self:create_action(function(shortcut)
self:activate_selected_item(shortcut, true) self:activate_selected_item(shortcut, true)
end)) end))
blur_action_index = false blur_action_index = false
+4 -4
View File
@@ -249,7 +249,7 @@ function TopBar:render()
local button_fg = is_hover and (button.hover_fg or bg) or fg local button_fg = is_hover and (button.hover_fg or bg) or fg
local button_bg = is_hover and (button.hover_bg or fg) or bg local button_bg = is_hover and (button.hover_bg or fg) or bg
cursor:zone('primary_down', rect, button.command) cursor:zone('primary_click', rect, button.command)
local bg_size = self.size - margin local bg_size = self.size - margin
local bg_ax, bg_ay = rect.ax + (is_left and margin or 0), rect.ay + margin local bg_ax, bg_ay = rect.ax + (is_left and margin or 0), rect.ay + margin
@@ -302,7 +302,7 @@ function TopBar:render()
if left_aligned then title_bx = rect.ax - margin else title_ax = rect.bx + margin end if left_aligned then title_bx = rect.ax - margin else title_ax = rect.bx + margin end
-- Click action -- Click action
cursor:zone('primary_down', rect, function() mp.command('script-binding uosc/playlist') end) cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/playlist') end)
end end
-- Skip rendering titles if there's not enough horizontal space -- Skip rendering titles if there's not enough horizontal space
@@ -325,7 +325,7 @@ function TopBar:render()
local title_rect = {ax = ax, ay = title_ay, bx = ax + rect_width, by = by} local title_rect = {ax = ax, ay = title_ay, bx = ax + rect_width, by = by}
if options.top_bar_alt_title_place == 'toggle' then if options.top_bar_alt_title_place == 'toggle' then
cursor:zone('primary_down', title_rect, function() self:toggle_title() end) cursor:zone('primary_click', title_rect, function() self:toggle_title() end)
end end
ass:rect(title_rect.ax, title_rect.ay, title_rect.bx, title_rect.by, { ass:rect(title_rect.ax, title_rect.ay, title_rect.bx, title_rect.by, {
@@ -415,7 +415,7 @@ function TopBar:render()
-- Click action -- Click action
rect.bx = time_bx rect.bx = time_bx
cursor:zone('primary_down', rect, function() mp.command('script-binding uosc/chapters') end) cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/chapters') end)
title_ay = rect.by + self.title_spacing title_ay = rect.by + self.title_spacing
end end
+30 -2
View File
@@ -4,6 +4,8 @@ local cursor = {
x = math.huge, x = math.huge,
y = math.huge, y = math.huge,
hidden = true, hidden = true,
disabled = false,
disablers = {}, -- List of ids. When not empty, the cursor handling is disabled.
distance = 0, -- Distance traveled during current move. Reset by `cursor.distance_reset_timer`. distance = 0, -- Distance traveled during current move. Reset by `cursor.distance_reset_timer`.
last_hover = false, -- Stores `mouse.hover` boolean of the last mouse event for enter/leave detection. last_hover = false, -- Stores `mouse.hover` boolean of the last mouse event for enter/leave detection.
-- Event handlers that are only fired on zones defined during render loop. -- Event handlers that are only fired on zones defined during render loop.
@@ -404,11 +406,27 @@ function cursor:direction_to_rectangle_distance(rect)
return get_ray_to_rectangle_distance(self.x, self.y, end_x, end_y, rect) return get_ray_to_rectangle_distance(self.x, self.y, end_x, end_y, rect)
end end
---@param id string
function cursor:register_disabler(id)
self.disablers[#self.disablers + 1] = id
if #self.disablers > 0 then
cursor.disabled = true
self:leave()
end
end
---@param id string
function cursor:unregister_disabler(id)
self.disablers = itable_filter(self.disablers, function(item) return item ~= id end)
if #self.disablers == 0 then cursor.disabled = false end
end
---@param event string ---@param event string
---@param shortcut Shortcut ---@param shortcut Shortcut
---@param cb? fun(shortcut: Shortcut) ---@param cb? fun(shortcut: Shortcut)
function cursor:create_handler(event, shortcut, cb) function cursor:create_handler(event, shortcut, cb)
return function() return function()
if self.disabled then return end
if cb then cb(shortcut) end if cb then cb(shortcut) end
self:trigger(event, shortcut) self:trigger(event, shortcut)
end end
@@ -416,7 +434,7 @@ end
-- Movement -- Movement
local function handle_mouse_pos(_, mouse) local function handle_mouse_pos(_, mouse)
if not mouse then return end if not mouse or cursor.disabled then return end
if cursor.last_hover and not mouse.hover then if cursor.last_hover and not mouse.hover then
cursor:leave() cursor:leave()
elseif not (cursor.last_hover == false and mouse.hover == false) then -- filters out duplicate mouse out events elseif not (cursor.last_hover == false and mouse.hover == false) then -- filters out duplicate mouse out events
@@ -426,7 +444,7 @@ local function handle_mouse_pos(_, mouse)
end end
local function handle_touch_pos(_, touches) local function handle_touch_pos(_, touches)
if not touches then return end if not touches or cursor.disabled then return end
local touch = touches[1] local touch = touches[1]
if touch then if touch then
cursor:move(touch.x, touch.y) cursor:move(touch.x, touch.y)
@@ -466,4 +484,14 @@ mp.set_key_bindings({
{'wheel_down', cursor:create_handler('wheel_down', create_shortcut('wheel_down'))}, {'wheel_down', cursor:create_handler('wheel_down', create_shortcut('wheel_down'))},
}, 'wheel', 'force') }, 'wheel', 'force')
-- Monitor mpv UI's and disable uosc's cursor handling when any are open
mp.observe_property('user-data/mpv/context-menu/open', 'bool', function(_, value)
if value == true then cursor:register_disabler('context-menu')
else cursor:unregister_disabler('context-menu') end
end)
mp.observe_property('user-data/mpv/console/open', 'bool', function(_, value)
if value == true then cursor:register_disabler('console')
else cursor:unregister_disabler('console') end
end)
return cursor return cursor
+3 -4
View File
@@ -54,15 +54,14 @@ do
-- alphanum sorting for humans in Lua -- alphanum sorting for humans in Lua
-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua -- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
local function padnum(n, d) local function padnum(n)
return #d > 0 and ('%03d%s%.12f'):format(#n, n, tonumber(d) / (10 ^ #d)) return ("%03d%s"):format(#n, n)
or ('%03d%s'):format(#n, n)
end end
local function sort_lua(strings) local function sort_lua(strings)
local tuples = {} local tuples = {}
for i, f in ipairs(strings) do for i, f in ipairs(strings) do
tuples[i] = {f:lower():gsub('0*(%d+)%.?(%d*)', padnum), f} tuples[i] = {f:lower():gsub('0*(%d+)', padnum), f}
end end
table.sort(tuples, function(a, b) table.sort(tuples, function(a, b)
return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1] return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
+99 -29
View File
@@ -5,6 +5,9 @@
> [!WARNING] > [!WARNING]
> Release1.2.0及Release1.2.0之前的发行版,都由于弹弹play接口使用政策改版,部分功能无法使用。如果发现插件功能异常,比如搜索弹幕总是显示无结果,请拉取或下载主分支最新源代码;或下载[最新发行版](https://github.com/Tony15246/uosc_danmaku/releases/latest) > 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] > [!NOTE]
> 已添加对mpv内部 `mp.input`的支持,在uosc不可用时通过键绑定调用此方式渲染菜单 > 已添加对mpv内部 `mp.input`的支持,在uosc不可用时通过键绑定调用此方式渲染菜单
> >
@@ -63,12 +66,44 @@
</details> </details>
## 目录
- [项目简介](#项目简介)
- [主要功能](#主要功能)
- [安装](#安装)
- [下载](#下载)
- [基本配置](#基本配置)
- [uosc控件配置](#uosc控件配置)
- [绑定快捷键(可选)](#绑定快捷键可选)
- [拓展功能(可选)](#拓展功能可选)
- [从弹幕源向当前弹幕添加新弹幕内容(从网络url或本地添加弹幕)](#从弹幕源向当前弹幕添加新弹幕内容可选)
- [弹幕源延迟设置](#弹幕源延迟设置可选)
- [实时修改弹幕样式](#实时修改弹幕样式可选)
- [弹幕设置总菜单](#弹幕设置总菜单可选)
- [设置弹幕延迟(可选)](#设置弹幕延迟可选)
- [保存当前视频弹幕(可选)](#保存当前视频弹幕可选)
- [清空当前视频关联的弹幕源(可选)](#清空当前视频关联的弹幕源可选)
- [检查脚本更新(可选)](#检查脚本更新可选)
- [可配置选项(可选)](#可配置选项可选)
- [弹幕加载相关](#弹幕加载相关)
- [弹幕显示相关](#弹幕显示相关)
- [弹幕解析服务相关](#弹幕解析服务相关)
- [插件配置相关](#插件配置相关)
- [自定义弹幕样式相关配置](#自定义弹幕样式相关配置)
- [插件自定义属性](#插件自定义属性)
- [常见问题](#常见问题)
- [特别感谢](#特别感谢)
- [相关项目](#相关项目)
## 安装 ## 安装
### 下载 ### 下载
一般的mpv配置目录结构大致如下 一般的mpv配置目录结构大致如下
> [!NOTE]
> Windows 系统上等价全局配置路径`%APPDATA%/mpv/`,也可使用 mpv.exe 所在目录的 portable_config 文件夹(便携配置路径)
``` ```
~/.config/mpv ~/.config/mpv
├── fonts ├── 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` 想要通过快捷键使用此功能,请添加类似下面的配置到 `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** > **⚠️NOTE**
> >
@@ -492,35 +527,30 @@ save_danmaku
save_danmaku=yes save_danmaku=yes
``` ```
</details> 如需保存到指定文件夹,请提前创建目标文件夹,然后配置 `save_danmaku_path``save_danmaku_path_mode`。插件不会自动创建目录。
--- 保存本地媒体到指定文件夹时,文件名会包含视频父目录名,用于减少不同目录下同名视频的弹幕文件冲突。例如 `动画/01.mkv` 会保存为类似 `动画_01.xml`。网络媒体保存到指定文件夹时仍使用媒体标题作为文件名。
<details> `save_danmaku_path_mode` 可选值如下:
<summary>
~~add_from_source~~ - `local`:默认值,仅本地媒体保存到 `save_danmaku_path`,网络媒体仍不保存
- `url`:仅网络媒体保存到 `save_danmaku_path`,本地媒体仍保存到视频同目录
- `all`:本地媒体和网络媒体都保存到 `save_danmaku_path`
> ~~开关记录通过 从弹幕源向当前弹幕添加新弹幕内容 关联过的弹幕源并自动加载(已废除)~~ 例如,仅将网络媒体的弹幕保存到 `~~/danmaku`
</summary>
### add_from_source
> **⚠️NOTE**
>
> 该可选配置项在Release v1.2.0之后已废除。现在通过 `从弹幕源向当前弹幕添加新弹幕内容`功能关联过的弹幕源被记录,并且下次播放同一个视频的时候自动关联并加载所有添加过的弹幕源,这样的行为已经成为了插件的默认行为,不需要再通过 `add_from_source`来开启。在[从源获取弹幕](#从弹幕源向当前弹幕添加新弹幕内容可选)菜单中可以可视化地管理所有添加过的弹幕源。
#### 功能说明
开启此选项后,通过 `从弹幕源向当前弹幕添加新弹幕内容`功能关联过的弹幕源会被记录,并且下次播放同一个视频的时候会自动关联并加载添加过的弹幕源。
#### 使用方法
想要开启此选项,请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并添加如下内容:
``` ```
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> </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> <details>
<summary> <summary>
max_screen_danmaku 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** > **⚠️NOTE**
> >
> 请确保自定义服务的 API 与弹弹play 的兼容,已知兼容:[misaka_danmu_server](https://github.com/l429609201/misaka_danmu_server)[danmu_api](https://github.com/huangxd-/danmu_api) > 请确保自定义服务的 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`文件并自定义如下内容: 想要使用此选项,请在mpv配置文件夹下的 `script-opts`中创建 `uosc_danmaku.conf`文件并自定义如下内容:
``` ```
api_server=https://api.dandanplay.net api_server=https://danmaku-api.152468.xyz
``` ```
</details> </details>
@@ -813,7 +878,7 @@ user_agent
> **⚠️NOTE** > **⚠️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 > 若想提高URL播放的哈希匹配成功率,可以将此项设为 `mpv`或浏览器的User-Agent
@@ -1015,6 +1080,9 @@ blacklist_path=
- `user-data/uosc_danmaku/has-danmaku` - `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/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的弹幕源问题如何从根源进行调整解决 ### 来自弹弹play的弹幕源问题如何从根源进行调整解决
@@ -1049,10 +1117,12 @@ blacklist_path=
- 弹幕格式解析转换:[DanmakuConvert](https://github.com/timerring/DanmakuConvert) - 弹幕格式解析转换:[DanmakuConvert](https://github.com/timerring/DanmakuConvert)
- 简繁转换:[OpenCC](https://github.com/BYVoid/OpenCC) - 简繁转换:[OpenCC](https://github.com/BYVoid/OpenCC)
- lua原生md5计算实现:https://github.com/rkscv/danmaku - 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) - 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) - 巴哈姆特在线播放弹幕获取实现参考:[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) - [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)~~ 相关功能主仓库已实现
+240 -119
View File
@@ -21,47 +21,114 @@ end
-- 写入history.json -- 写入history.json
-- 读取episodeId获取danmaku -- 读取episodeId获取danmaku
function set_episode_id(input, from_menu) function set_episode_id(input, from_menu, api_server)
from_menu = from_menu or false from_menu = from_menu or false
DANMAKU.source = "dandanplay" DANMAKU.source = "dandanplay"
local api_server = options.api_server local selected_server = api_server
for url, source in pairs(DANMAKU.sources) do for url, source in pairs(DANMAKU.sources) do
if source.from == "api_server" then if source.from == "api_server" then
if not source.from_history then if not source.from_history then
DANMAKU.sources[url] = nil DANMAKU.sources[url] = nil
else else
DANMAKU.sources[url]["data"] = nil DANMAKU.sources[url]["data"] = nil
api_server = source.api_server or options.api_server
end end
end end
end end
local episodeId = tonumber(input)
local main_url = api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0" if not api_server then
add_source_to_history(main_url, { from = "api_server", api_server = api_server }) if DANMAKU.api_server ~= nil then
write_history(episodeId, api_server) 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() set_danmaku_button()
fetch_danmaku(episodeId, from_menu, api_server) fetch_danmaku(episodeId, from_menu, selected_server)
end end
-- 回退使用额外的弹幕获取方式 -- 回退使用额外的弹幕获取方式
function get_danmaku_fallback(query) function get_danmaku_fallback(query)
local url = options.fallback_server .. "/?ac=dm&url=" .. query local function do_fallback()
msg.verbose("尝试获取弹幕:" .. url) 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) local args = make_danmaku_request_args("GET", url)
if not args then return end if not args then return end
fetch_danmaku_data(args, function(data) fetch_danmaku_data(args, function(data)
if not data or not data["comments"] or data["count"] <= 1 then if data ~= nil and data["xml"] ~= nil then
msg.info("备用服务器无数据或返回格式不正确") if DANMAKU.sources[query] ~= nil then
show_message("备用服务器无数据或返回格式不正确", 3) DANMAKU.sources[query]["data"] = data["xml"]
return else
end DANMAKU.sources[query] = {from = "user_custom", data = data["xml"]}
end
load_danmaku(true)
return
end
save_danmaku_data(data["comments"], query, "user_custom") if not data or not data["comments"] or data["count"] <= 1 then
load_danmaku(true) msg.info("备用服务器无数据或返回格式不正确")
end) 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 end
-- 返回弹幕请求参数 -- 返回弹幕请求参数
@@ -91,6 +158,8 @@ function make_danmaku_request_args(method, url, headers, body)
table.insert(args, 'Content-Type: application/json') table.insert(args, 'Content-Type: application/json')
end end
table.insert(args, '--compressed')
if url:find("api%.dandanplay%.") then if url:find("api%.dandanplay%.") then
local time = os.time() local time = os.time()
local appid = "UgjRIH45lE1BBLNmir1WKw==" local appid = "UgjRIH45lE1BBLNmir1WKw=="
@@ -153,8 +222,8 @@ local function normalize_danmaku_response(d)
end end
-- 尝试通过解析文件名匹配剧集 -- 尝试通过解析文件名匹配剧集
local function match_episode(animeTitle, bangumiId, episode_num) local function match_episode(animeTitle, bangumiId, episode_num, api_server)
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId local url = api_server .. "/api/v2/bangumi/" .. bangumiId
local args = make_danmaku_request_args("GET", url) local args = make_danmaku_request_args("GET", url)
if args == nil then if args == nil then
@@ -162,7 +231,6 @@ local function match_episode(animeTitle, bangumiId, episode_num)
end end
call_cmd_async(args, function(error, json) call_cmd_async(args, function(error, json)
async_running = false
if error then if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5) show_message("HTTP 请求失败,打开控制台查看详情", 5)
msg.error(error) 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 if ep_num and ep_num == tonumber(episode_num) then
DANMAKU.anime = animeTitle DANMAKU.anime = animeTitle
DANMAKU.episode = episode.episodeTitle DANMAKU.episode = episode.episodeTitle
set_episode_id(episode.episodeId) set_episode_id(episode.episodeId, nil, api_server)
break break
end end
end end
@@ -188,9 +256,7 @@ local function match_episode(animeTitle, bangumiId, episode_num)
end end
local function match_anime() local function match_anime()
local animes = {}
local anime_type = "tvseries" local anime_type = "tvseries"
local type_count = 0
local title, season_num, episode_num = parse_title() local title, season_num, episode_num = parse_title()
if not episode_num then if not episode_num then
msg.info("无法解析剧集信息") msg.info("无法解析剧集信息")
@@ -201,44 +267,49 @@ local function match_anime()
anime_type = "ova" anime_type = "ova"
end end
-- 并发在多个 api_server 上搜索,遇到第一个可接受的匹配就取消其余请求
local encoded_query = url_encode(title) local encoded_query = url_encode(title)
local url = options.api_server .. "/api/v2/search/anime" local servers = get_api_server_list(options.api_server)
local params = "keyword=" .. encoded_query
local full_url = url .. "?" .. params
local args = make_danmaku_request_args("GET", full_url)
if not args then return end local matched = false
local cancel_fn = nil
call_cmd_async(args, function(error, json) local function build_args(server)
async_running = false local url = server .. "/api/v2/search/anime"
if error then local full_url = url .. "?keyword=" .. encoded_query
show_message("HTTP 请求失败,打开控制台查看详情", 5) return make_danmaku_request_args("GET", full_url)
msg.error(error) 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 return
end end
local data = utils.parse_json(out)
local data = utils.parse_json(json)
if not data or not data.animes then if not data or not data.animes then
msg.info("无结果")
return return
end end
local local_candidates = {}
for _, anime in ipairs(data.animes) do for _, anime in ipairs(data.animes) do
if anime.type == anime_type then if anime.type == anime_type then
type_count = type_count + 1 table.insert(local_candidates, anime)
table.insert(animes, anime)
end end
end end
if #local_candidates == 1 then
if type_count == 1 then matched = true
match_episode(animes[1].animeTitle, animes[1].bangumiId, episode_num) local a = local_candidates[1]
elseif type_count > 1 and season_num then 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 best_match, best_score = nil, -1
local target_title = title local target_title = title
if tonumber(season_num) > 1 then if tonumber(season_num) > 1 then
target_title = title .. "" .. number_to_chinese(season_num) .. "" target_title = title .. "" .. number_to_chinese(season_num) .. ""
end end
for _, anime in ipairs(animes) do for _, anime in ipairs(local_candidates) do
local animeTitle = tostring(anime.animeTitle or "") local animeTitle = tostring(anime.animeTitle or "")
animeTitle = animeTitle:gsub("^%s*(.-)%s*$", "%1") animeTitle = animeTitle:gsub("^%s*(.-)%s*$", "%1")
:gsub("%s*%(.-%)%s*$", "") :gsub("%s*%(.-%)%s*$", "")
@@ -253,17 +324,24 @@ local function match_anime()
best_match = anime best_match = anime
end end
end end
if best_match and best_score >= 0.75 then if best_match and best_score >= 0.75 then
matched = true
msg.info(("模糊匹配选中: %s (score=%.2f)"):format(best_match.animeTitle, best_score)) msg.info(("模糊匹配选中: %s (score=%.2f)"):format(best_match.animeTitle, best_score))
match_episode(best_match.animeTitle, best_match.bangumiId, episode_num) match_episode(best_match.animeTitle, best_match.bangumiId, episode_num, server)
else if cancel_fn then pcall(cancel_fn) end
msg.info("匹配到多个结果,但相似度不足,请手动搜索") return
end end
else end
-- 未找到可接受匹配,继续等待其他服务器的返回
end
local function final_cb()
if not matched then
msg.info("没有找到合适的匹配结果") msg.info("没有找到合适的匹配结果")
end end
end) end
cancel_fn = parallel_requests(servers, build_args, per_response, final_cb, { concurrency = 5, per_request_timeout = 15 })
end end
-- 执行哈希匹配获取弹幕 -- 执行哈希匹配获取弹幕
@@ -271,7 +349,7 @@ local function match_file(file_path, file_name, callback)
-- 计算文件哈希 -- 计算文件哈希
local hash = nil local hash = nil
local file_info = utils.file_info(file_path) 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') local file, error = io.open(normalize(file_path), 'rb')
if file and not error then if file and not error then
local m = MD5.new() local m = MD5.new()
@@ -300,50 +378,66 @@ local function match_file(file_path, file_name, callback)
file_name = title file_name = title
end end
local url = options.api_server .. "/api/v2/match" local servers = get_api_server_list(options.api_server)
local args = make_danmaku_request_args("POST", url, {
["Content-Type"] = "application/json" 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, fileName = file_name,
fileHash = hash or "a1b2c3d4e5f67890abcd1234ef567890", fileHash = hash or "a1b2c3d4e5f67890abcd1234ef567890",
matchMode = "hashAndFileName" matchMode = "hashAndFileName"
} })
) end
if not args then return end local function per_response(server, err, out)
if matched then return end
call_cmd_async(args, function(error, json) if err then
async_running = false msg.debug(("match failed for %s: %s"):format(server, tostring(err)))
if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5)
callback(error)
return return
end end
local data = utils.parse_json(json) local data = utils.parse_json(out)
if not data or not data.isMatched then if not data or not data.isMatched then
callback("没有匹配的剧集")
return return
end end
matched = true
DANMAKU.anime = data.matches[1].animeTitle DANMAKU.anime = data.matches[1].animeTitle
DANMAKU.episode = data.matches[1].episodeTitle DANMAKU.episode = data.matches[1].episodeTitle
-- 获取并加载弹幕数据 set_episode_id(data.matches[1].episodeId, nil, server)
set_episode_id(data.matches[1].episodeId) if cancel_fn then pcall(cancel_fn) end
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 end
-- 异步获取弹幕数据 -- 异步获取弹幕数据
function fetch_danmaku_data(args, callback) function fetch_danmaku_data(args, callback)
call_cmd_async(args, function(error, json) call_cmd_async(args, function(error, json)
async_running = false
if error then if error then
show_message("获取数据失败", 3) show_message("获取数据失败", 3)
msg.error("HTTP 请求失败:" .. error) msg.error("HTTP 请求失败:" .. error)
return return
end end
local data = utils.parse_json(json) 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) callback(data)
end) end)
end end
@@ -359,6 +453,26 @@ function save_danmaku_data(comments, query, danmaku_source)
end end
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) function save_danmaku_downloaded(url, downloaded_file)
local danmaku_list = parse_danmaku_file(downloaded_file) local danmaku_list = parse_danmaku_file(downloaded_file)
if file_exists(downloaded_file) then if file_exists(downloaded_file) then
@@ -371,39 +485,6 @@ function save_danmaku_downloaded(url, downloaded_file)
end end
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) function handle_fetched_danmaku(data, url, from_menu)
if data and data["comments"] then if data and data["comments"] then
@@ -426,7 +507,7 @@ end
-- 匹配弹幕库 comment, 仅匹配dandan本身弹幕库 -- 匹配弹幕库 comment, 仅匹配dandan本身弹幕库
-- 通过danmaku apiurl+id获取弹幕 -- 通过danmaku apiurl+id获取弹幕
function fetch_danmaku(episodeId, from_menu, api_server) 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) show_message("弹幕加载中...", 30)
msg.verbose("尝试获取弹幕:" .. url) msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url) local args = make_danmaku_request_args("GET", url)
@@ -499,18 +580,60 @@ end
--通过输入源url获取弹幕库 --通过输入源url获取弹幕库
function add_danmaku_source_online(query, from_menu) function add_danmaku_source_online(query, from_menu)
set_danmaku_button() set_danmaku_button()
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(query)
show_message("弹幕加载中...", 30) show_message("弹幕加载中...", 30)
msg.verbose("尝试获取弹幕:" .. url) msg.verbose("尝试获取弹幕:" .. query)
local args = make_danmaku_request_args("GET", url)
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 return
end end
fetch_danmaku_data(args, function(data) local matched = false
handle_danmaku_data(query, data, from_menu) local cancel_fn = nil
end)
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 end
-- 将弹幕转换为 Lua table -- 将弹幕转换为 Lua table
@@ -578,8 +701,6 @@ function get_danmaku_with_hash(file_name, file_path)
end end
call_cmd_async(arg, function(error) call_cmd_async(arg, function(error)
async_running = false
file_path = utils.join_path(DANMAKU_PATH, temp_file) file_path = utils.join_path(DANMAKU_PATH, temp_file)
match_file(file_path, file_name, function(error) match_file(file_path, file_name, function(error)
+141 -70
View File
@@ -1,8 +1,11 @@
local utils = require 'mp.utils' local utils = require 'mp.utils'
local msg = require 'mp.msg' local msg = require 'mp.msg'
local cached_series_playlinks = {}
local Source = { local Source = {
["b 站"] = "bilibili1", ["b 站"] = "bilibili1",
["芒果TV"] = "imgo",
["腾讯"] = "qq", ["腾讯"] = "qq",
["爱奇艺"] = "qiyi", ["爱奇艺"] = "qiyi",
["优酷"] = "youku", ["优酷"] = "youku",
@@ -83,6 +86,31 @@ local function query_tmdb(title, class, menu)
end end
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 function get_number(cat, id, site)
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&site=%s", local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&site=%s",
cat, id, site) cat, id, site)
@@ -107,6 +135,53 @@ local function get_number(cat, id, site)
return nil return nil
end 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 function get_episodes_v2(cat, id, site)
local s_param = string.format('[{"cat_id":"%s","ent_id":"%s","site":"%s"}]', tostring(cat), tostring(id), tostring(site)) local 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 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 return nil
end end
local data_text = res.stdout or "" local parsed = utils.parse_json(res.stdout)
-- 兼容 JSONP 和 纯 JSON:提取最外层括号内 JSON
local json_payload = data_text
local first_paren = data_text:find('%(')
local last_paren = data_text:match('.*()%)')
if first_paren and last_paren and last_paren > first_paren then
json_payload = data_text:sub(first_paren + 1, last_paren - 1)
end
local parsed = utils.parse_json(json_payload)
if not parsed then if not parsed then
msg.error("episodesv2: 解析返回失败: " .. (res.stdout or "")) msg.warn("episodesv2: 解析返回失败: " .. (res.stdout or ""))
return nil return nil
end 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 if parsed.code == 0 and parsed.data and #parsed.data > 0 then
local seriesHTML = parsed.data[1] and parsed.data[1].seriesHTML local seriesHTML = parsed.data[1] and parsed.data[1].seriesHTML
if seriesHTML and seriesHTML.seriesPlaylinks then if seriesHTML and seriesHTML.seriesPlaylinks then
for i, ep in ipairs(seriesHTML.seriesPlaylinks) do local rows = build_episode_rows(seriesHTML.seriesPlaylinks)
local episode_url = nil if rows then
if type(ep) == 'string' then for _, r in ipairs(rows) do
episode_url = ep table.insert(episodes, { index = tonumber(r.index), url = r.url })
elseif type(ep) == 'table' and ep.url then
episode_url = ep.url
end
if episode_url and episode_url ~= '' then
table.insert(episodes, { index = i, url = episode_url })
end end
end end
end end
@@ -170,7 +231,7 @@ function get_details(class, id, site, title, year, number, episodenum)
local menu_title = "剧集信息" local menu_title = "剧集信息"
local footnote = "使用 / 打开筛选" local footnote = "使用 / 打开筛选"
if uosc_available and not episodenum then 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 else
show_message(message, 3) show_message(message, 3)
end end
@@ -188,59 +249,57 @@ function get_details(class, id, site, title, year, number, episodenum)
local items = {} local items = {}
local episodes = nil local episodes = nil
local episode_rows = nil
-- 优先尝试使用搜索时缓存的 seriesPlaylinks(若存在且站点匹配)
if cat == 2 or cat == 4 then 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 end
-- 统一构建 episode_rows:优先使用 episodesv2 返回的数据,否则使用 v1/detail -- 若未命中缓存,则继续使用 episodesv2/v1 的原有流程
local episode_rows = nil if not episode_rows then
if episodes then if cat == 2 or cat == 4 then
episode_rows = {} episodes = get_episodes_v2(cat, id, site)
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 end
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&start=1&end=%s&site=%s", -- 统一构建 episode_rows:优先使用 episodesv2 返回的数据,否则使用 v1/detail
cat, id, number, site) if episodes then
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 = {} episode_rows = {}
for _, it in ipairs(result.data.allepidetail[site]) do for _, ep in ipairs(episodes) do
table.insert(episode_rows, { index = tostring(it.playlink_num), url = it.url }) 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 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 if result and result.data.longData and result.data.longData.rows then
for _, item in ipairs(result.data.longData.rows) do for _, item in ipairs(result.data.longData.rows) do
if item.playlinks then 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 for source_name, source_id in pairs(Source) do
if item.playlinks[source_id] then if item.playlinks[source_id] then
table.insert(items, { table.insert(items, {
@@ -375,7 +446,7 @@ function query_extra(name, class)
} }
menu.cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" } menu.cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" }
if uosc_available then 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 else
show_message(message, 30) show_message(message, 30)
end end
+74 -238
View File
@@ -1,4 +1,4 @@
VERSION = "2.0.0" VERSION = "2.2.0"
mp.commandv('script-message', 'uosc_danmaku-version', VERSION) mp.commandv('script-message', 'uosc_danmaku-version', VERSION)
@@ -21,6 +21,13 @@ require("modules/update")
require("apis/dandanplay") require("apis/dandanplay")
require('apis/extra') 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/" DANMAKU_PATH = os.getenv("TEMP") or "/tmp/"
HISTORY_PATH = mp.command_native({"expand-path", options.history_path}) HISTORY_PATH = mp.command_native({"expand-path", options.history_path})
PID = utils.getpid() 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) mp.set_property_native(DELAY_PROPERTY, 0)
HAS_DANMAKU = string.format("user-data/%s/has-danmaku", mp.get_script_name()) HAS_DANMAKU = string.format("user-data/%s/has-danmaku", mp.get_script_name())
mp.set_property_bool(HAS_DANMAKU, false) 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({ KEY = table_to_zero_indexed({
0x00,0x01,0x02,0x03,0x04, 0x00,0x01,0x02,0x03,0x04,
0x05,0x06,0x07,0x08,0x09, 0x05,0x06,0x07,0x08,0x09,
@@ -92,7 +103,7 @@ end
function set_danmaku_button() function set_danmaku_button()
if get_danmaku_visibility() then if get_danmaku_visibility() then
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on") toggle_danmaku_switch("on")
end end
end end
@@ -105,51 +116,7 @@ function show_loaded(init)
else else
show_message("弹幕加载成功,共计" .. #COMMENTS .. "条弹幕", 3) show_message("弹幕加载成功,共计" .. #COMMENTS .. "条弹幕", 3)
end end
end mp.set_property_native(DANMAKU_COUNT, #COMMENTS)
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
end end
-- 获取指定时间的延迟 -- 获取指定时间的延迟
@@ -287,10 +254,12 @@ local function set_danmaku_delay(dly, time, specific_source)
end end
end end
if dly == 0 then if not specific_source then
DELAY = 0 if dly == 0 then
else DELAY = 0
DELAY = DELAY + dly else
DELAY = DELAY + dly
end
end end
if ENABLED and COMMENTS ~= nil then if ENABLED and COMMENTS ~= nil then
@@ -310,8 +279,14 @@ local function set_danmaku_delay(dly, time, specific_source)
rebuild_convert_timer = nil rebuild_convert_timer = nil
end) end)
show_message('设置弹幕延迟: ' .. string.format("%.1f", DELAY + 1e-10) .. ' s') if specific_source then
mp.set_property_native(DELAY_PROPERTY, DELAY) 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 end
local function clear_source() local function clear_source()
@@ -430,11 +405,6 @@ function add_source_to_history(add_url, add_source)
local record = history[path]["sources"][add_url] local record = history[path]["sources"][add_url]
record.from = add_source.from or "user_custom" record.from = add_source.from or "user_custom"
record.blocked = add_source.blocked or false 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 {}) local delay_segments = shallow_copy(add_source.delay_segments or {})
if #delay_segments > 0 then if #delay_segments > 0 then
@@ -460,6 +430,7 @@ function read_danmaku_source_record(path)
local history = utils.parse_json(history_json) or {} local history = utils.parse_json(history_json) or {}
local record = history[path] local record = history[path]
if not record or not record.sources then return end if not record or not record.sources then return end
local sources = record.sources local sources = record.sources
@@ -489,7 +460,6 @@ function read_danmaku_source_record(path)
blocked = blocked, blocked = blocked,
delay_segments = delay_segments, delay_segments = delay_segments,
from_history = true, from_history = true,
api_server = data.api_server,
} }
end end
else else
@@ -519,7 +489,6 @@ function read_danmaku_source_record(path)
blocked = blocked, blocked = blocked,
delay_segments = delay_segments, delay_segments = delay_segments,
from_history = true, from_history = true,
api_server = record.api_server,
} }
upgraded_sources[source] = shallow_copy(DANMAKU.sources[source]) upgraded_sources[source] = shallow_copy(DANMAKU.sources[source])
@@ -532,14 +501,48 @@ function read_danmaku_source_record(path)
end end
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) function save_danmaku(not_forced)
local path = mp.get_property("path") local path = mp.get_property("path")
local dir = get_parent_directory(path) or "" local filename = is_protocol(path) and mp.get_property("media-title") or mp.get_property('filename/no-ext')
local filename = mp.get_property('filename/no-ext') local danmaku_out = get_save_danmaku_output(path, filename)
local danmaku_out = utils.join_path(dir, filename .. ".xml") if not danmaku_out or (not file_exists(danmaku_out)
-- 排除网络播放场景
if not path or is_protocol(path) or (not file_exists(danmaku_out)
and not is_writable(danmaku_out)) then and not is_writable(danmaku_out)) then
show_message("此弹幕文件不支持保存至本地") show_message("此弹幕文件不支持保存至本地")
msg.warn("此弹幕文件不支持保存至本地") msg.warn("此弹幕文件不支持保存至本地")
@@ -561,176 +564,6 @@ function load_danmaku(from_menu, no_osd)
render_danmaku(from_menu, no_osd) render_danmaku(from_menu, no_osd)
end 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) function load_danmaku_for_url(path)
if path:find('bilibili.com') or path:find('bilivideo.c[nom]+') then if path:find('bilibili.com') or path:find('bilivideo.c[nom]+') then
load_danmaku_for_bilibili(path) 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_id = history_dir.episodeId
local history_fname = history_dir.fname local history_fname = history_dir.fname
local history_extra = history_dir.extra local history_extra = history_dir.extra
local history_api_server = history_dir.api_server
local playing_number = nil local playing_number = nil
if history_fname then if history_fname then
@@ -794,6 +628,7 @@ function auto_load_danmaku(path, dir, filename, number)
if playing_number ~= nil then if playing_number ~= nil then
local x = playing_number - history_number --获取集数差值 local x = playing_number - history_number --获取集数差值
DANMAKU.episode = episode_number and string.format("第%s话", episode_number + x) or history_dir.episodeTitle 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) show_message("自动加载上次匹配的弹幕", 3)
msg.verbose("自动加载上次匹配的弹幕") msg.verbose("自动加载上次匹配的弹幕")
if history_id then if history_id then
@@ -881,7 +716,7 @@ mp.register_event("file-loaded", function()
return return
end end
if ENABLED and COMMENTS == nil and not async_running then if ENABLED and COMMENTS == nil and not is_async_running() then
init(path) init(path)
end end
end) end)
@@ -914,7 +749,8 @@ end)
mp.register_script_message("show_danmaku_keyboard", function() mp.register_script_message("show_danmaku_keyboard", function()
ENABLED = not ENABLED ENABLED = not ENABLED
if ENABLED then if ENABLED then
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on") toggle_danmaku_switch("on")
if COMMENTS == nil then if COMMENTS == nil then
show_message("加载弹幕初始化...", 3) show_message("加载弹幕初始化...", 3)
set_danmaku_visibility(true) set_danmaku_visibility(true)
@@ -926,7 +762,7 @@ mp.register_script_message("show_danmaku_keyboard", function()
end end
else else
show_message("关闭弹幕", 2) show_message("关闭弹幕", 2)
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "off") toggle_danmaku_switch("off")
hide_danmaku_func() hide_danmaku_func()
end end
end) end)
+490 -490
View File
@@ -1,491 +1,491 @@
-- modified from https://github.com/idiomic/Lua_AES -- modified from https://github.com/idiomic/Lua_AES
--[[ --[[
Copyright 2019 Tyler Richard Hoyer Copyright 2019 Tyler Richard Hoyer
Copyright 2025 dyphire Copyright 2025 dyphire
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
]] ]]
local unpack = unpack or table.unpack local unpack = unpack or table.unpack
local GF8x2 = { local GF8x2 = {
[0]=0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5
} }
local GF8x3 = { local GF8x3 = {
[0]=0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a
} }
local GF8x9 = { local GF8x9 = {
[0]=0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46
} }
local GF8x11 = { local GF8x11 = {
[0]=0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3
} }
local GF8x13 = { local GF8x13 = {
[0]=0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97
} }
local GF8x14 = { local GF8x14 = {
[0]=0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d
} }
local s = { local s = {
[0]=0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16
} }
local si = { local si = {
[0]=0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D
} }
local rcon = { local rcon = {
0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d
} }
local xor4 = { local xor4 = {
[0]=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15, [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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
} }
local function xor8(a, b) local function xor8(a, b)
local al = a % 16 local al = a % 16
local bl = b % 16 local bl = b % 16
return 16 * xor4[a - al + (b - bl) / 16] + xor4[16 * al + bl] return 16 * xor4[a - al + (b - bl) / 16] + xor4[16 * al + bl]
end end
local function xor_blocks_8(a, b) local function xor_blocks_8(a, b)
local res = {} local res = {}
for i = 1, 16 do for i = 1, 16 do
res[i] = xor8(a[i], b[i]) res[i] = xor8(a[i], b[i])
end end
return res return res
end end
local function addRoundKey(state, key) local function addRoundKey(state, key)
for i, byte in next, state do for i, byte in next, state do
state[i] = xor8(byte, key[i]) state[i] = xor8(byte, key[i])
end end
end end
local function subBytes(state, s_box) local function subBytes(state, s_box)
for i, byte in next, state do for i, byte in next, state do
state[i] = s_box[byte] state[i] = s_box[byte]
end end
end end
local function shiftRows(state) local function shiftRows(state)
state[2], state[6], state[10], state[14] = state[2], state[6], state[10], state[14] =
state[6], state[10], state[14], state[2] state[6], state[10], state[14], state[2]
state[3], state[7], state[11], state[15] = state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7] state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] = state[4], state[8], state[12], state[16] =
state[16], state[4], state[8], state[12] state[16], state[4], state[8], state[12]
end end
local function inv_shiftRows(state) local function inv_shiftRows(state)
state[2], state[6], state[10], state[14] = state[2], state[6], state[10], state[14] =
state[14], state[2], state[6], state[10] state[14], state[2], state[6], state[10]
state[3], state[7], state[11], state[15] = state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7] state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] = state[4], state[8], state[12], state[16] =
state[8], state[12], state[16], state[4] state[8], state[12], state[16], state[4]
end end
local function mixColumns(state) local function mixColumns(state)
for i = 0, 3 do for i = 0, 3 do
local cur = i*4+1 local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3] 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 + 0] = xor8(xor8(xor8(GF8x2[a], GF8x3[b]), c), d)
state[cur + 1] = xor8(xor8(xor8(a, GF8x2[b]), GF8x3[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 + 2] = xor8(xor8(xor8(a, b), GF8x2[c]), GF8x3[d])
state[cur + 3] = xor8(xor8(xor8(GF8x3[a], b), c), GF8x2[d]) state[cur + 3] = xor8(xor8(xor8(GF8x3[a], b), c), GF8x2[d])
end end
end end
local function inv_mixColumns(state) -- TODO: fix local function inv_mixColumns(state) -- TODO: fix
for i = 0, 3 do for i = 0, 3 do
local cur = i*4+1 local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3] 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 + 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 + 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 + 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]) state[cur + 3] = xor8(xor8(xor8(GF8x11[a], GF8x13[b]), GF8x9[c]), GF8x14[d])
end end
end end
-- 256-bit key constants -- 256-bit key constants
local n = 32 -- number of bytes in the 256-bit encryption key 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 b = 240 -- number of bytes in 15 128-bit round keys
local function schedule256(key) local function schedule256(key)
local expanded = {} local expanded = {}
for c = 0, n-1 do for c = 0, n-1 do
expanded[c] = key[c] expanded[c] = key[c]
end end
local i = 1 local i = 1
local c = n local c = n
local t1, t2, t3, t4 --t local t1, t2, t3, t4 --t
while c < b do while c < b do
t1 = expanded[c-4] t1 = expanded[c-4]
t2 = expanded[c-3] t2 = expanded[c-3]
t3 = expanded[c-2] t3 = expanded[c-2]
t4 = expanded[c-1] t4 = expanded[c-1]
if (c % n == 0) then if (c % n == 0) then
t1, t2, t3, t4 = xor8(rcon[i+1], s[t2]), s[t3], s[t4], s[t1] t1, t2, t3, t4 = xor8(rcon[i+1], s[t2]), s[t3], s[t4], s[t1]
i = i + 1 i = i + 1
end end
if (c % n == 16) then if (c % n == 16) then
t1 = s[t1] t1 = s[t1]
t2 = s[t2] t2 = s[t2]
t3 = s[t3] t3 = s[t3]
t4 = s[t4] t4 = s[t4]
end end
t1 = xor8(t1, expanded[c - n]) t1 = xor8(t1, expanded[c - n])
expanded[c] = t1 expanded[c] = t1
c = c + 1 c = c + 1
t2 = xor8(t2, expanded[c - n]) t2 = xor8(t2, expanded[c - n])
expanded[c] = t2 expanded[c] = t2
c = c + 1 c = c + 1
t3 = xor8(t3, expanded[c - n]) t3 = xor8(t3, expanded[c - n])
expanded[c] = t3 expanded[c] = t3
c = c + 1 c = c + 1
t4 = xor8(t4, expanded[c - n]) t4 = xor8(t4, expanded[c - n])
expanded[c] = t4 expanded[c] = t4
c = c + 1 c = c + 1
end end
local roundKeys = {} local roundKeys = {}
for round = 0, 14 do for round = 0, 14 do
local roundKey = {} local roundKey = {}
for byte = 0, 15 do for byte = 0, 15 do
roundKey[byte+1] = expanded[round * 16 + byte] roundKey[byte+1] = expanded[round * 16 + byte]
end end
roundKeys[round] = roundKey roundKeys[round] = roundKey
end end
return roundKeys return roundKeys
end end
local function chunks(text, i) local function chunks(text, i)
local first = i * 16 + 1 local first = i * 16 + 1
if first > #text then if first > #text then
return return
end end
i = i + 1 i = i + 1
local chunk = {text:byte(first, first + 15)} local chunk = {text:byte(first, first + 15)}
for j = #chunk + 1, 16 do for j = #chunk + 1, 16 do
chunk[j] = 0 chunk[j] = 0
end end
return i, chunk return i, chunk
end end
local function pkcs7_unpad(str) local function pkcs7_unpad(str)
local len = #str local len = #str
if len == 0 then return str end if len == 0 then return str end
local pad_len = string.byte(str, len) local pad_len = string.byte(str, len)
if pad_len < 1 or pad_len > 16 then if pad_len < 1 or pad_len > 16 then
return nil return nil
end end
for i = len - pad_len + 1, len do for i = len - pad_len + 1, len do
if string.byte(str, i) ~= pad_len then if string.byte(str, i) ~= pad_len then
return nil return nil
end end
end end
return string.sub(str, 1, len - pad_len) return string.sub(str, 1, len - pad_len)
end end
local function zero_unpad(str) local function zero_unpad(str)
local len = #str local len = #str
while len > 0 and string.byte(str, len) == 0 do while len > 0 and string.byte(str, len) == 0 do
len = len - 1 len = len - 1
end end
return string.sub(str, 1, len) return string.sub(str, 1, len)
end end
local function unpad(str) local function unpad(str)
local unpadded = pkcs7_unpad(str) local unpadded = pkcs7_unpad(str)
if unpadded then if unpadded then
return unpadded return unpadded
else else
return zero_unpad(str) return zero_unpad(str)
end end
end end
local function encrypt(state, roundKeys) local function encrypt(state, roundKeys)
addRoundKey(state, roundKeys[0]) addRoundKey(state, roundKeys[0])
for round = 1, 13 do for round = 1, 13 do
subBytes(state, s) subBytes(state, s)
shiftRows(state) shiftRows(state)
mixColumns(state) mixColumns(state)
addRoundKey(state, roundKeys[round]) addRoundKey(state, roundKeys[round])
end end
subBytes(state, s) subBytes(state, s)
shiftRows(state) shiftRows(state)
addRoundKey(state, roundKeys[14]) addRoundKey(state, roundKeys[14])
end end
local function decrypt(state, roundKeys) local function decrypt(state, roundKeys)
addRoundKey(state, roundKeys[14]) addRoundKey(state, roundKeys[14])
inv_shiftRows(state) inv_shiftRows(state)
subBytes(state, si) subBytes(state, si)
for round = 13, 1, -1 do for round = 13, 1, -1 do
addRoundKey(state, roundKeys[round]) addRoundKey(state, roundKeys[round])
inv_mixColumns(state) inv_mixColumns(state)
inv_shiftRows(state) inv_shiftRows(state)
subBytes(state, si) subBytes(state, si)
end end
addRoundKey(state, roundKeys[0]) addRoundKey(state, roundKeys[0])
end end
local function ECB_encrypt(key, originaltext) local function ECB_encrypt(key, originaltext)
local text = {} local text = {}
local roundKeys = schedule256(key) local roundKeys = schedule256(key)
local i = 0 local i = 0
while true do while true do
i, state = chunks(originaltext, i) i, state = chunks(originaltext, i)
if not state then break end if not state then break end
encrypt(state, roundKeys) encrypt(state, roundKeys)
text[i] = string.char(unpack(state)) text[i] = string.char(unpack(state))
end end
return table.concat(text) return table.concat(text)
end end
local function ECB_decrypt(key, ciphertext) local function ECB_decrypt(key, ciphertext)
local text = {} local text = {}
local roundKeys = schedule256(key) local roundKeys = schedule256(key)
local i = 0 local i = 0
while true do while true do
i, state = chunks(ciphertext, i) i, state = chunks(ciphertext, i)
if not state then break end if not state then break end
decrypt(state, roundKeys) decrypt(state, roundKeys)
text[i] = string.char(unpack(state)) text[i] = string.char(unpack(state))
end end
return unpad(table.concat(text)) return unpad(table.concat(text))
end end
local function CBC_encrypt(key, iv, originaltext) local function CBC_encrypt(key, iv, originaltext)
local roundKeys = schedule256(key) local roundKeys = schedule256(key)
local text = {} local text = {}
local prev_block = {unpack(iv)} local prev_block = {unpack(iv)}
local i = 0 local i = 0
while true do while true do
i, block = chunks(originaltext, i) i, block = chunks(originaltext, i)
if not block then break end if not block then break end
local xored = xor_blocks_8(block, prev_block) local xored = xor_blocks_8(block, prev_block)
encrypt(xored, roundKeys) encrypt(xored, roundKeys)
text[i] = string.char(unpack(xored)) text[i] = string.char(unpack(xored))
prev_block = xored prev_block = xored
end end
return table.concat(text) return table.concat(text)
end end
local function CBC_decrypt(key, iv, ciphertext) local function CBC_decrypt(key, iv, ciphertext)
local roundKeys = schedule256(key) local roundKeys = schedule256(key)
local text = {} local text = {}
local prev_block = {unpack(iv)} local prev_block = {unpack(iv)}
local i = 0 local i = 0
while true do while true do
i, block = chunks(ciphertext, i) i, block = chunks(ciphertext, i)
if not block then break end if not block then break end
local decrypted = {unpack(block)} local decrypted = {unpack(block)}
decrypt(decrypted, roundKeys) decrypt(decrypted, roundKeys)
local plain_block = xor_blocks_8(decrypted, prev_block) local plain_block = xor_blocks_8(decrypted, prev_block)
text[i] = string.char(unpack(plain_block)) text[i] = string.char(unpack(plain_block))
prev_block = block prev_block = block
end end
local result = table.concat(text) local result = table.concat(text)
return unpad(result) return unpad(result)
end end
return { return {
ECB = { ECB = {
encrypt = ECB_encrypt; encrypt = ECB_encrypt;
decrypt = ECB_decrypt; decrypt = ECB_decrypt;
}; };
CBC = { CBC = {
encrypt = CBC_encrypt; encrypt = CBC_encrypt;
decrypt = CBC_decrypt; decrypt = CBC_decrypt;
}; };
} }
+192 -192
View File
@@ -1,192 +1,192 @@
--[[ --[[
sha256 -- public domain Lua SHA-256 implementation sha256 -- public domain Lua SHA-256 implementation
no warranty implied; use at your own risk no warranty implied; use at your own risk
author: dyphire author: dyphire
COMPATIBILITY COMPATIBILITY
Lua 5.1+, LuaJIT Lua 5.1+, LuaJIT
LICENSE: MIT License LICENSE: MIT License
--]] --]]
local unpack = unpack or table.unpack local unpack = unpack or table.unpack
local function band(a,b) local function band(a,b)
local res = 0 local res = 0
local bit = 1 local bit = 1
for i = 0,31 do for i = 0,31 do
local aa = a % 2 local aa = a % 2
local bb = b % 2 local bb = b % 2
if aa == 1 and bb == 1 then if aa == 1 and bb == 1 then
res = res + bit res = res + bit
end end
a = (a - aa) / 2 a = (a - aa) / 2
b = (b - bb) / 2 b = (b - bb) / 2
bit = bit * 2 bit = bit * 2
end end
return res return res
end end
local function bor(a,b) local function bor(a,b)
local res = 0 local res = 0
local bit = 1 local bit = 1
for i = 0,31 do for i = 0,31 do
local aa = a % 2 local aa = a % 2
local bb = b % 2 local bb = b % 2
if aa == 1 or bb == 1 then if aa == 1 or bb == 1 then
res = res + bit res = res + bit
end end
a = (a - aa) / 2 a = (a - aa) / 2
b = (b - bb) / 2 b = (b - bb) / 2
bit = bit * 2 bit = bit * 2
end end
return res return res
end end
local function bxor(a,b) local function bxor(a,b)
local res = 0 local res = 0
local bit = 1 local bit = 1
for i = 0,31 do for i = 0,31 do
local aa = a % 2 local aa = a % 2
local bb = b % 2 local bb = b % 2
if (aa + bb) == 1 then if (aa + bb) == 1 then
res = res + bit res = res + bit
end end
a = (a - aa) / 2 a = (a - aa) / 2
b = (b - bb) / 2 b = (b - bb) / 2
bit = bit * 2 bit = bit * 2
end end
return res return res
end end
local function bnot(a) local function bnot(a)
return 0xFFFFFFFF - a return 0xFFFFFFFF - a
end end
local function lshift(a,n) local function lshift(a,n)
return (a * 2^n) % 2^32 return (a * 2^n) % 2^32
end end
local function rshift(a,n) local function rshift(a,n)
return math.floor(a / 2^n) % 2^32 return math.floor(a / 2^n) % 2^32
end end
local function bit_ror(x, n) local function bit_ror(x, n)
return bor(rshift(x, n), lshift(x, 32 - n)) return bor(rshift(x, n), lshift(x, 32 - n))
end end
local function sha256(message) local function sha256(message)
local k = { local k = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
} }
local function preprocess(msg) local function preprocess(msg)
local len = #msg local len = #msg
local bitLen = len * 8 local bitLen = len * 8
msg = msg .. "\128" msg = msg .. "\128"
local zeroPad = 64 - ((len + 9) % 64) local zeroPad = 64 - ((len + 9) % 64)
if zeroPad ~= 64 then if zeroPad ~= 64 then
msg = msg .. string.rep("\0", zeroPad) msg = msg .. string.rep("\0", zeroPad)
end end
msg = msg .. string.char( msg = msg .. string.char(
rshift(bitLen, 56) % 256, rshift(bitLen, 56) % 256,
rshift(bitLen, 48) % 256, rshift(bitLen, 48) % 256,
rshift(bitLen, 40) % 256, rshift(bitLen, 40) % 256,
rshift(bitLen, 32) % 256, rshift(bitLen, 32) % 256,
rshift(bitLen, 24) % 256, rshift(bitLen, 24) % 256,
rshift(bitLen, 16) % 256, rshift(bitLen, 16) % 256,
rshift(bitLen, 8) % 256, rshift(bitLen, 8) % 256,
bitLen % 256 bitLen % 256
) )
return msg return msg
end end
local function chunkify(msg) local function chunkify(msg)
local chunks = {} local chunks = {}
for i = 1, #msg, 64 do for i = 1, #msg, 64 do
table.insert(chunks, msg:sub(i, i + 63)) table.insert(chunks, msg:sub(i, i + 63))
end end
return chunks return chunks
end end
local function processChunk(chunk, hash) local function processChunk(chunk, hash)
local w = {} local w = {}
for i = 1, 64 do for i = 1, 64 do
if i <= 16 then if i <= 16 then
w[i] = lshift(string.byte(chunk, (i - 1) * 4 + 1), 24) + 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 + 2), 16) +
lshift(string.byte(chunk, (i - 1) * 4 + 3), 8) + lshift(string.byte(chunk, (i - 1) * 4 + 3), 8) +
string.byte(chunk, (i - 1) * 4 + 4) string.byte(chunk, (i - 1) * 4 + 4)
else else
local s0 = bxor(bxor(bit_ror(w[i - 15], 7), bit_ror(w[i - 15], 18)), rshift(w[i - 15], 3)) 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)) 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 w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % 2^32
end end
end end
local a, b, c, d, e, f, g, h = unpack(hash) local a, b, c, d, e, f, g, h = unpack(hash)
for i = 1, 64 do for i = 1, 64 do
local s1 = bxor(bxor(bit_ror(e, 6), bit_ror(e, 11)), bit_ror(e, 25)) 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 ch = bxor(band(e, f), band(bnot(e), g))
local temp1 = (h + s1 + ch + k[i] + w[i]) % 2^32 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 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 maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
local temp2 = (s0 + maj) % 2^32 local temp2 = (s0 + maj) % 2^32
h = g h = g
g = f g = f
f = e f = e
e = (d + temp1) % 2^32 e = (d + temp1) % 2^32
d = c d = c
c = b c = b
b = a b = a
a = (temp1 + temp2) % 2^32 a = (temp1 + temp2) % 2^32
end end
return return
(hash[1] + a) % 2^32, (hash[1] + a) % 2^32,
(hash[2] + b) % 2^32, (hash[2] + b) % 2^32,
(hash[3] + c) % 2^32, (hash[3] + c) % 2^32,
(hash[4] + d) % 2^32, (hash[4] + d) % 2^32,
(hash[5] + e) % 2^32, (hash[5] + e) % 2^32,
(hash[6] + f) % 2^32, (hash[6] + f) % 2^32,
(hash[7] + g) % 2^32, (hash[7] + g) % 2^32,
(hash[8] + h) % 2^32 (hash[8] + h) % 2^32
end end
message = preprocess(message) message = preprocess(message)
local chunks = chunkify(message) local chunks = chunkify(message)
local hash = { local hash = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
} }
for _, chunk in ipairs(chunks) do for _, chunk in ipairs(chunks) do
hash = {processChunk(chunk, hash)} hash = {processChunk(chunk, hash)}
end end
local result = "" local result = ""
for _, h in ipairs(hash) do for _, h in ipairs(hash) do
result = result .. string.format("%08x", h) result = result .. string.format("%08x", h)
end end
return result return result
end end
return sha256 return sha256
+322
View File
@@ -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
+372 -127
View File
@@ -5,84 +5,267 @@ local unpack = unpack or table.unpack
input_loaded, input = pcall(require, "mp.input") input_loaded, input = pcall(require, "mp.input")
uosc_available = false uosc_available = false
latest_menu_anime = {} 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) function get_animes(query)
local encoded_query = url_encode(query) local encoded_query = url_encode(query)
local url = options.api_server .. "/api/v2/search/anime" local server_metas = get_api_server_list(options.api_server, true)
local params = "keyword=" .. encoded_query local servers = {}
local full_url = url .. "?" .. params 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 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 message = "加载数据中..."
local menu_type = "menu_anime" local menu_type = "menu_anime"
local menu_title = "在此处输入番剧名称" local menu_title = "在此处输入番剧名称"
local footnote = "使用enter或ctrl+enter进行搜索" local footnote = "使用enter或ctrl+enter进行搜索"
local menu_cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" } 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 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 else
show_message(message, 30) show_message(initial_message, 30)
end
msg.verbose("尝试获取番剧数据:" .. full_url)
local args = make_danmaku_request_args("GET", full_url)
if args == nil then
return
end 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 build_args = make_build_args(encoded_query)
local message = "获取数据失败"
if uosc_available then -- 构造 ctx,用于 handle_response 闭包访问和修改共享状态
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query) local ctx = {
else items = items,
show_message(message, 3) 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 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 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 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 end
msg.info("无结果") end, { concurrency = 5, per_request_timeout = 30 })
return active_request_cancel = cancel_fn
end active_request_type = menu_type
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 end
function get_episodes(animeTitle, bangumiId) function get_episodes(animeTitle, bangumiId, api_server)
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId local url = api_server .. "/api/v2/bangumi/" .. bangumiId
local items = {} local items = {}
local message = "加载数据中..." local message = "加载数据中..."
@@ -91,7 +274,9 @@ function get_episodes(animeTitle, bangumiId)
local footnote = "使用 / 打开筛选" local footnote = "使用 / 打开筛选"
if uosc_available then 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 else
show_message(message, 30) show_message(message, 30)
end end
@@ -102,60 +287,70 @@ function get_episodes(animeTitle, bangumiId)
return return
end end
local res = mp.command_native({ name = 'subprocess', capture_stdout = true, capture_stderr = true, args = args }) request_cancelled = false
active_request_type = menu_type
if not res.status or res.status ~= 0 then active_request_cancel = call_cmd_async(args, function(err, stdout)
local message = "获取数据失败" active_request_cancel = nil
if uosc_available then if request_cancelled then
update_menu_uosc(menu_type, menu_title, message, footnote) return
else
show_message(message, 3)
end end
msg.error("HTTP 请求失败:" .. res.stderr)
end
local response = utils.parse_json(res.stdout) if err then
local message = "获取数据失败"
if not response or not response.bangumi or not response.bangumi.episodes then if uosc_available then
local message = "无结果" update_menu_uosc(menu_type, menu_title, message, footnote)
if uosc_available then else
update_menu_uosc(menu_type, menu_title, message, footnote) show_message(message, 3)
else end
show_message(message, 3) msg.error("HTTP 请求失败:" .. tostring(err))
return
end end
msg.info("无结果")
return
end
table.insert(items, { local response = utils.parse_json(stdout)
title = "← 返回搜索结果", if not response or not response.bangumi or not response.bangumi.episodes then
value = { "script-message-to", mp.get_script_name(), "open-latest-menu-anime", latest_menu_anime }, local message = "无结果"
keep_open = false, if uosc_available then
selectable = true, 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, { table.insert(items, {
title = episode.episodeTitle, title = "← 返回搜索结果",
hint = episode.episodeNumber, value = { "script-message-to", mp.get_script_name(), "open-latest-menu-anime", latest_menu_anime },
value = { "script-message-to", mp.get_script_name(), "load-danmaku",
animeTitle, episode.episodeTitle, episode.episodeId },
keep_open = false, keep_open = false,
selectable = true, selectable = true,
}) })
end
if uosc_available then for _, episode in ipairs(response.bangumi.episodes) do
footnote = mp.get_property("filename") table.insert(items, {
update_menu_uosc(menu_type, menu_title, items, footnote) title = episode.episodeTitle,
elseif input_loaded then hint = episode.episodeNumber,
mp.add_timeout(0.1, function() value = { "script-message-to", mp.get_script_name(), "load-danmaku",
open_menu_select(items) animeTitle, episode.episodeTitle, episode.episodeId, api_server },
end) keep_open = false,
end 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 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 = {} local items = {}
if type(menu_item) == "string" then if type(menu_item) == "string" then
table.insert(items, { table.insert(items, {
@@ -165,7 +360,7 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
keep_open = true, keep_open = true,
selectable = false, selectable = false,
align = "center", align = "center",
icon = "spinner", icon = message_icon or "",
}) })
else else
items = menu_item items = menu_item
@@ -181,8 +376,19 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
search_suggestion = query, search_suggestion = query,
items = items, 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) 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 return json_props
end end
@@ -200,6 +406,7 @@ function open_menu_select(menu_items, is_time)
items = item_titles, items = item_titles,
submit = function(id) submit = function(id)
input.terminate() input.terminate()
perform_cancel_active_request()
local v = item_values[id] local v = item_values[id]
if type(v) == 'table' then if type(v) == 'table' then
mp.commandv(unpack(v)) mp.commandv(unpack(v))
@@ -207,6 +414,11 @@ function open_menu_select(menu_items, is_time)
mp.command(v) mp.command(v)
end end
end, end,
closed = function()
show_message("", 0)
input.terminate()
perform_cancel_active_request()
end,
}) })
end end
@@ -264,7 +476,9 @@ function open_input_menu()
if uosc_available then if uosc_available then
open_input_menu_uosc() open_input_menu_uosc()
elseif input_loaded then elseif input_loaded then
open_input_menu_get() mp.add_timeout(0.01, function()
open_input_menu_get()
end)
end end
end end
@@ -283,23 +497,29 @@ function open_add_menu_get()
local serial = 0 local serial = 0
for url, source in pairs(DANMAKU.sources) do for url, source in pairs(DANMAKU.sources) do
if source.data then if source.data then
serial = serial + 1
local action, text
if source.from == "api_server" then if source.from == "api_server" then
action = source.blocked and "unblock" or "block" serial = serial + 1
text = string.format(" [%02d] %s [来源:弹幕服务器%s] ", serial, url, local action = source.blocked and "unblock" or "block"
local text = string.format(" [%02d] %s [来源:弹幕服务器%s] ", serial, url,
source.blocked and "(已屏蔽)" or "(未屏蔽)") 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 else
action = "delete" serial = serial + 1
text = string.format(" [%02d] %s [来源:用户添加] ", serial, url) 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 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
end end
@@ -386,7 +606,7 @@ function open_add_menu_uosc()
local sources = {} local sources = {}
for url, source in pairs(DANMAKU.sources) do for url, source in pairs(DANMAKU.sources) do
if source.data then 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.from == "api_server" then
if source.blocked then if source.blocked then
item.hint = "来源:弹幕服务器(已屏蔽)" item.hint = "来源:弹幕服务器(已屏蔽)"
@@ -397,7 +617,17 @@ function open_add_menu_uosc()
end end
else else
item.hint = "来源:用户添加" 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 end
table.insert(sources, item) table.insert(sources, item)
end end
@@ -421,7 +651,9 @@ function open_add_menu()
if uosc_available then if uosc_available then
open_add_menu_uosc() open_add_menu_uosc()
elseif input_loaded then elseif input_loaded then
open_add_menu_get() mp.add_timeout(0.01, function()
open_add_menu_get()
end)
end end
end end
@@ -915,7 +1147,7 @@ function open_delay_menu_uosc(source_url, status)
end end
end 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.hint = "当前弹幕源延迟:" .. string.format("%.1f", delay + 1e-10) .. ""
item.active = url == source_url item.active = url == source_url
table.insert(sources, item) table.insert(sources, item)
@@ -1013,7 +1245,10 @@ function open_add_total_menu_select()
prompt = '选择:', prompt = '选择:',
items = item_titles, items = item_titles,
submit = function(id) 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,
}) })
end end
@@ -1026,6 +1261,10 @@ function open_add_total_menu()
end end
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( mp.commandv(
"script-message-to", "script-message-to",
@@ -1114,11 +1353,12 @@ mp.register_script_message("set", function(prop, value)
hide_danmaku_func() hide_danmaku_func()
end end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", value) toggle_danmaku_switch(value)
end) end)
-- 注册函数给 uosc 按钮使用 -- 注册函数给 uosc 按钮使用
mp.register_script_message("search-anime-event", function(query) mp.register_script_message("search-anime-event", function(query)
perform_cancel_active_request()
if uosc_available then if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_danmaku") mp.commandv("script-message-to", "uosc", "close-menu", "menu_danmaku")
end end
@@ -1129,19 +1369,20 @@ mp.register_script_message("search-anime-event", function(query)
get_animes(query) get_animes(query)
end end
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 if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime") mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime")
end end
get_episodes(animeTitle, bangumiId)
get_episodes(animeTitle, bangumiId, api_server)
end) end)
-- Register script message to show the input menu mp.register_script_message("load-danmaku", function(animeTitle, episodeTitle, episodeId, api_server)
mp.register_script_message("load-danmaku", function(animeTitle, episodeTitle, episodeId)
ENABLED = true ENABLED = true
DANMAKU.anime = animeTitle DANMAKU.anime = animeTitle
DANMAKU.episode = episodeTitle DANMAKU.episode = episodeTitle
set_episode_id(episodeId, true) set_episode_id(episodeId, true, api_server)
end) end)
mp.register_script_message("add-source-event", function(query) mp.register_script_message("add-source-event", function(query)
@@ -1177,6 +1418,10 @@ mp.register_script_message("open-latest-menu-anime", function ()
end end
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) mp.register_script_message("setup-danmaku-style", function(query, text)
local event = utils.parse_json(query) local event = utils.parse_json(query)
if event ~= nil then if event ~= nil then
@@ -1273,7 +1518,7 @@ mp.register_script_message("setup-source-delay", function(query, text)
end end
local delay = parse_delay_input(text) local delay = parse_delay_input(text)
if delay ~= nil then 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.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
mp.add_timeout(0.1, function() mp.add_timeout(0.1, function()
open_delay_menu(query, "refresh") open_delay_menu(query, "refresh")
+9 -1
View File
@@ -3,7 +3,9 @@ local opt = require("mp.options")
-- 选项 -- 选项
options = { options = {
-- 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api -- 指定弹幕服务器地址,自定义服务需兼容 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 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕 -- 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
-- 可用: https://api.danmu.icuhttps://dmku.hls.one -- 可用: https://api.danmu.icuhttps://dmku.hls.one
fallback_server = "https://api.danmu.icu", fallback_server = "https://api.danmu.icu",
@@ -15,6 +17,10 @@ options = {
autoload_local_danmaku = false, autoload_local_danmaku = false,
autoload_for_url = false, autoload_for_url = false,
save_danmaku = false, save_danmaku = false,
-- 指定弹幕保存目录。为空时保存到视频同目录;目录需要用户提前创建
save_danmaku_path = "",
-- 指定 save_danmaku_path 的应用范围:local / url / all
save_danmaku_path_mode = "local",
user_agent = "mpv_danmaku/1.0", user_agent = "mpv_danmaku/1.0",
proxy = "", proxy = "",
-- 可选:向 HTTP 请求传递 cookie.txt 文件路径 -- 可选:向 HTTP 请求传递 cookie.txt 文件路径
@@ -25,6 +31,8 @@ options = {
fps = "60/1.001", fps = "60/1.001",
-- 指定合并重复弹幕的时间间隔的容差值,单位为秒。默认值: -1,表示禁用 -- 指定合并重复弹幕的时间间隔的容差值,单位为秒。默认值: -1,表示禁用
merge_tolerance = -1, merge_tolerance = -1,
-- 合并重复弹幕时是否强制合并类型和颜色不同的弹幕。默认值: false,表示仅合并类型和颜色相同的弹幕
merge_without_style = false,
-- 指定弹幕关联历史记录文件的路径,支持绝对路径和相对路径 -- 指定弹幕关联历史记录文件的路径,支持绝对路径和相对路径
history_path = "~~/danmaku-history.json", history_path = "~~/danmaku-history.json",
open_search_danmaku_menu_key = "Ctrl+d", open_search_danmaku_menu_key = "Ctrl+d",
+133 -71
View File
@@ -10,6 +10,33 @@ local function ass_escape(text)
:gsub("\n", "\\N") :gsub("\n", "\\N")
end 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) local function xml_unescape(str)
return str:gsub("&quot;", "\"") return str:gsub("&quot;", "\"")
:gsub("&apos;", "'") :gsub("&apos;", "'")
@@ -154,62 +181,103 @@ local function merge_duplicate_danmaku(danmakus, threshold)
local groups = {} local groups = {}
for _, d in ipairs(danmakus) do for _, d in ipairs(danmakus) do
local tkey = tostring(d.type or "") local tkey = options.merge_without_style and "any" or tostring(d.type or "")
local ckey = tostring(d.color or "")
local text = d.text or "" local text = d.text or ""
groups[tkey] = groups[tkey] or {} groups[text] = groups[text] or {}
groups[tkey][ckey] = groups[tkey][ckey] or {} groups[text][tkey] = groups[text][tkey] or {}
groups[tkey][ckey][text] = groups[tkey][ckey][text] or {} table.insert(groups[text][tkey], d)
table.insert(groups[tkey][ckey][text], 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 end
local merged = {} local merged = {}
local abs = math.abs local abs = math.abs
for _, bytype in pairs(groups) do for _, group in ipairs(final_groups) do
for _, bycolor in pairs(bytype) do table.sort(group, function(a, b) return a.time < b.time end)
for _, group in pairs(bycolor) do
table.sort(group, function(a, b) return a.time < b.time end)
local i = 1 local i = 1
while i <= #group do while i <= #group do
local base = group[i] local base = group[i]
local times = { base.time } local times = { base.time }
local count = 1 local count = 1
local j = i + 1 local j = i + 1
while j <= #group and abs(group[j].time - base.time) <= threshold do while j <= #group and abs(group[j].time - base.time) <= threshold do
times[#times+1] = group[j].time times[#times+1] = group[j].time
count = count + 1 count = count + 1
j = j + 1 j = j + 1
end end
local same_time = true local same_time = true
for k = 2, #times do for k = 2, #times do
if times[k] ~= times[1] then if times[k] ~= times[1] then
same_time = false same_time = false
break 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
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
end end
@@ -259,8 +327,11 @@ local function limit_danmaku(danmakus, limit)
end end
-- 解析 XML 弹幕 -- 解析 XML 弹幕
local function parse_xml_danmaku(xml_string) function parse_xml_danmaku(xml_string)
local danmakus = {} local danmakus = {}
if not xml_string then
return danmakus
end
-- [^>]* 匹配其他 attributes -- [^>]* 匹配其他 attributes
-- %f[^%s] 确保 p= 前面是空白字符 -- %f[^%s] 确保 p= 前面是空白字符
for p_attr, text in xml_string:gmatch('<d%s+[^>]*%f[^%s]p="([^"]+)"[^>]*>([^<]+)</d>') do 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 end
-- 解析 JSON 弹幕 -- 解析 JSON 弹幕
local function parse_json_danmaku(json_string) function parse_json_danmaku(json_string)
local danmakus = {} local danmakus = {}
if json_string:sub(1, 3) == "\239\187\191" then if json_string:sub(1, 3) == "\239\187\191" then
json_string = json_string:sub(4) json_string = json_string:sub(4)
@@ -469,10 +540,22 @@ end
-- 将弹幕转换为 XML 格式 -- 将弹幕转换为 XML 格式
function convert_danmaku_to_xml(danmaku_out) function convert_danmaku_to_xml(danmaku_out)
local danmakus = {} 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 if not source.blocked and source.data then
local get_cached_delay = make_delay_lookup(source)
for _, d in ipairs(source.data) do 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 end
end end
@@ -521,29 +604,7 @@ function convert_danmaku_to_ass_events(force)
local per_source_lists = {} local per_source_lists = {}
for url, source in pairs(DANMAKU.sources) do for url, source in pairs(DANMAKU.sources) do
if not source.blocked and source.data then if not source.blocked and source.data then
local segments = nil local get_cached_delay = make_delay_lookup(source)
local prefix = nil
if source.delay_segments and #source.delay_segments > 0 then
segments = {}
for i, v in ipairs(source.delay_segments) do segments[i] = v end
table.sort(segments, function(a, b) return (a.start or 0) < (b.start or 0) end)
prefix = {}
local s = 0
for i, v in ipairs(segments) do
s = s + (v.delay or 0)
prefix[i] = s
end
end
local function get_cached_delay(t)
local segs = segments or {}
local pre = prefix or {}
if #segs == 0 then return 0 end
local idx = binary_search(segs, t, function(s) return (s and s.start) or 0 end)
local target = idx - 1
if target < 1 then return 0 end
return pre[target] or 0
end
local list = {} local list = {}
for _, d in ipairs(source.data) do for _, d in ipairs(source.data) do
@@ -711,6 +772,7 @@ function convert_danmaku_to_ass_events(force)
clean_text = clean_text, clean_text = clean_text,
pos = pos, pos = pos,
move = move, move = move,
layer = (style == "R2L") and 0 or 1,
source = d.source, source = d.source,
} }
table.insert(ass_events, event) table.insert(ass_events, event)
+33 -13
View File
@@ -5,7 +5,8 @@ local unpack = unpack or table.unpack
local osd_width, osd_height, pause = 0, 0, true local osd_width, osd_height, pause = 0, 0, true
local time_pos_observer_active = false 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) local function realtime_position_text(event, pos, displayarea)
if not event.move then if not event.move then
@@ -51,7 +52,8 @@ function render(pos_arg)
end end
if not pos then if not pos then
overlay:remove() overlay_low:remove()
overlay_high:remove()
return return
end end
@@ -67,7 +69,8 @@ function render(pos_arg)
fontsize = options.fontsize - ratio * 2 fontsize = options.fontsize - ratio * 2
end end
local ass_events = {} local ass_events_low = {}
local ass_events_high = {}
local max_display = math.max(options.scrolltime, options.fixtime) local max_display = math.max(options.scrolltime, options.fixtime)
local window_start = pos - max_display local window_start = pos - max_display
@@ -99,22 +102,36 @@ function render(pos_arg)
-- 构建 ASS 字符串 -- 构建 ASS 字符串
local ass_text = text and (ass_prefix .. text) local ass_text = text and (ass_prefix .. text)
if ass_text then
table.insert(ass_events, ass_text) 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
end end
overlay.res_x = width -- 写入低层(滚动)和高层(顶/底)overlay,并设置 z 值以控制堆叠
overlay.res_y = height overlay_low.res_x = width
overlay.data = table.concat(ass_events, '\n') overlay_low.res_y = height
overlay:update() 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 end
local function time_pos_callback(_, time_pos) local function time_pos_callback(_, time_pos)
if time_pos then if time_pos then
render(time_pos) render(time_pos)
else else
overlay:remove() overlay_low:remove()
overlay_high:remove()
end end
end end
@@ -137,7 +154,7 @@ function render_danmaku(from_menu, no_osd)
if not no_osd then if not no_osd then
show_loaded(true) show_loaded(true)
end end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on") toggle_danmaku_switch("on")
show_danmaku_func() show_danmaku_func()
else else
show_message("") show_message("")
@@ -179,7 +196,8 @@ function hide_danmaku_func()
stop_time_observer() stop_time_observer()
mp.set_property_bool(HAS_DANMAKU, false) mp.set_property_bool(HAS_DANMAKU, false)
set_danmaku_visibility(false) set_danmaku_visibility(false)
overlay:remove() overlay_low:remove()
overlay_high:remove()
if filter_state("danmaku") then if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku") mp.commandv("vf", "remove", "@danmaku")
end end
@@ -235,7 +253,8 @@ end)
mp.add_hook("on_unload", 50, function() mp.add_hook("on_unload", 50, function()
COMMENTS, DELAY = nil, 0 COMMENTS, DELAY = nil, 0
stop_time_observer() stop_time_observer()
overlay:remove() overlay_low:remove()
overlay_high:remove()
mp.set_property_native(DELAY_PROPERTY, 0) mp.set_property_native(DELAY_PROPERTY, 0)
if filter_state("danmaku") then if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku") mp.commandv("vf", "remove", "@danmaku")
@@ -256,4 +275,5 @@ mp.add_hook("on_unload", 50, function()
end end
DANMAKU = {sources = {}, count = 1} DANMAKU = {sources = {}, count = 1}
mp.set_property_native(DANMAKU_COUNT, 0)
end) end)
+247 -10
View File
@@ -201,6 +201,18 @@ function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil) return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end 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) function hex_to_bin(hexstr)
return (hexstr:gsub('..', function (cc) return (hexstr:gsub('..', function (cc)
return string.char(tonumber(cc, 16)) return string.char(tonumber(cc, 16))
@@ -214,6 +226,9 @@ end
function hex_to_int_color(hex_color) function hex_to_int_color(hex_color)
-- 移除颜色代码中的'#'字符 -- 移除颜色代码中的'#'字符
hex_color = hex_color:sub(2) -- 只保留颜色代码部分 hex_color = hex_color:sub(2) -- 只保留颜色代码部分
if not hex_color:match("^%x%x%x%x%x%x$") then
return 16777215
end
-- 提取R, G, B的十六进制值并转为整数 -- 提取R, G, B的十六进制值并转为整数
local r = tonumber(hex_color:sub(1, 2), 16) local r = tonumber(hex_color:sub(1, 2), 16)
@@ -226,14 +241,14 @@ function hex_to_int_color(hex_color)
return color_int return color_int
end end
local function get_type_from_position(position) function color_dist(c1, c2)
if position == 0 then local r1 = math.floor(c1 / 65536) % 256
return 1 local g1 = math.floor(c1 / 256) % 256
end local b1 = c1 % 256
if position == 1 then local r2 = math.floor(c2 / 65536) % 256
return 4 local g2 = math.floor(c2 / 256) % 256
end local b2 = c2 % 256
return 5 return math.sqrt((r1-r2)^2 + (g1-g2)^2 + (b1-b2)^2)
end end
-- url编码转换 -- url编码转换
@@ -424,6 +439,42 @@ function contains_any(tab, val)
return false return false
end 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 --读history 和 写history
function read_file(file_path) function read_file(file_path)
local file = io.open(file_path, "r") -- 打开文件,"r"表示只读模式 local file = io.open(file_path, "r") -- 打开文件,"r"表示只读模式
@@ -727,10 +778,29 @@ function number_to_chinese(num)
return result return result
end 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 函数,用于取消异步命令 -- 同时返回 abort 函数,用于取消异步命令
function call_cmd_async(args, callback) function call_cmd_async(args, callback)
async_running = true -- 标记异步开始
mark_async_start()
local abort_signal = mp.command_native_async({ local abort_signal = mp.command_native_async({
name = 'subprocess', name = 'subprocess',
capture_stderr = true, capture_stderr = true,
@@ -738,9 +808,14 @@ function call_cmd_async(args, callback)
playback_only = true, playback_only = true,
args = args, args = args,
}, function(success, result, error) }, function(success, result, error)
-- 标记异步结束
mark_async_end()
if not success or not result or result.status ~= 0 then if not success or not result or result.status ~= 0 then
local exit_code = (result and result.status or 'unknown') 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, {}) callback('Calling failed. Exit code: ' .. exit_code .. ' Error: ' .. message, {})
return return
end end
@@ -752,4 +827,166 @@ function call_cmd_async(args, callback)
return function() return function()
mp.abort_async_command(abort_signal) mp.abort_async_command(abort_signal)
end 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 end
+118
View File
@@ -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
+246
View File
@@ -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
+290
View File
@@ -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
+146
View File
@@ -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
+157
View File
@@ -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
+323
View File
@@ -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