init (again)
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
|||||||
|
**/.git
|
||||||
|
.manager/*
|
||||||
|
!.manager/.gitkeep
|
||||||
|
cache/
|
||||||
|
|
||||||
|
files/*.log
|
||||||
|
files/*.json
|
||||||
|
files/*.jsonl
|
||||||
|
files/command_history.txt
|
||||||
|
files/watch_history.jsonl
|
||||||
|
files/historybookmarks/
|
||||||
|
files/screen/
|
||||||
|
files/animated/
|
||||||
|
files/cutfragments/
|
||||||
|
files/chapters/
|
||||||
|
files/danmaku-history.json
|
||||||
|
|
||||||
|
historybookmarks
|
||||||
|
|
||||||
|
*.log
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 外部依赖
|
||||||
|
|
||||||
|
## 必需
|
||||||
|
|
||||||
|
| 依赖 | 用途 | 安装 |
|
||||||
|
| --------- | --------------------------------------- | --------------------- |
|
||||||
|
| mpv | 播放器本体(需 gpu-next + vulkan 支持) | `pacman -S mpv` |
|
||||||
|
| yt-dlp | URL 解析与流媒体播放 | `pacman -S yt-dlp` |
|
||||||
|
| ffmpeg | 字幕导出、缩略图生成、字幕同步 | `pacman -S ffmpeg` |
|
||||||
|
| curl | 字幕搜索下载、更新检查 | `pacman -S curl` |
|
||||||
|
| trash-cli | 安全删除文件(移至回收站) | `pacman -S trash-cli` |
|
||||||
|
|
||||||
|
## 可选
|
||||||
|
|
||||||
|
| 依赖 | 用途 | 安装 |
|
||||||
|
| ---------- | ------------------------------------- | ---------------------------------------------- |
|
||||||
|
| alass | 字幕自动同步(autosubsync 脚本) | `paru -S alass` |
|
||||||
|
| ffsubsync | 字幕自动同步(alass 的替代) | `pip install ffsubsync` |
|
||||||
|
|
||||||
|
## 字体
|
||||||
|
|
||||||
|
弹幕中可能出现 emoji, 因此需要使用支持的字体, 例如将 Symbola 加到 Noto Sans CJK SC 的末尾:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="family">
|
||||||
|
<string>Noto Sans CJK SC</string>
|
||||||
|
</test>
|
||||||
|
<edit mode="append" name="family">
|
||||||
|
<string>Symbola</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
```
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# .config/mpv
|
||||||
|
|
||||||
|
一套 mpv 播放器配置,集成 uosc 界面、多种着色器方案和自动更新机制。
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
原始仓库为 [emoeem/mpv](https://github.com/emoeem/mpv)。根据我的需要精简了一些功能,同时做了一些修复。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
mpv.conf # 主配置(全局设置 + Profile 定义)
|
||||||
|
input.conf # 键位绑定与右键菜单
|
||||||
|
inputevent_key.conf # inputevent.lua 增强键位(单击/双击/长按)
|
||||||
|
profiles.conf # 场景预设(游戏/电影/动画/低功耗/直播等)
|
||||||
|
manager.json # 脚本和着色器的 git 自动更新源
|
||||||
|
scripts/ # Lua 脚本
|
||||||
|
script-opts/ # 脚本配置
|
||||||
|
shaders/ # GPU 着色器(NNEDI3/FSRCNNX/Ani4K/Anime4K/ravu/igv 等)
|
||||||
|
fonts/ # 字体
|
||||||
|
icc/ # ICC 色彩配置文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
- **界面**:uosc 界面 + 右键菜单 + 缩略图预览 + 命令面板
|
||||||
|
- **色彩管理**:ICC / Target 双模式,HDR 直通与色调映射,动态峰值检测
|
||||||
|
- **着色器**:NNEDI3、FSRCNNX、Ani4K、AniSD、Anime4K、ravu-zoom、SSIM
|
||||||
|
- **条件 Profile**:根据分辨率 / HDR / 帧率 / 刷新率 / 网络等自动调整参数
|
||||||
|
- **字幕**:自动加载、编码检测(GB18030)、在线搜索(assrt)、自动同步(autosubsync)
|
||||||
|
- **音频**:96kHz 重采样、独占模式、多声道自动下混调节、ReplayGain
|
||||||
|
- **播放管理**:历史记录(simplehistory)、进度保存、文件浏览器、播放列表管理
|
||||||
|
- **网络**:yt-dlp 集成、浏览器 Cookie、自动缓存优化
|
||||||
|
- **更新**:按 M 键通过 manager.lua 从 git 自动更新所有脚本和着色器
|
||||||
|
|
||||||
|
## 着色器方案
|
||||||
|
|
||||||
|
在 `mpv.conf` 的 Profile 激活区切换(取消注释对应行):
|
||||||
|
|
||||||
|
| Profile | 适用场景 | GPU 开销 |
|
||||||
|
| --------- | --------------- | -------- |
|
||||||
|
| NNEDI3 | 通用 | 中 |
|
||||||
|
| NNEDI3+ | 通用(64 变体) | 高 |
|
||||||
|
| ravu-zoom | 通用 | 中 |
|
||||||
|
| FSRCNNX | HD 内容 | 中 |
|
||||||
|
| FSRCNNX+ | SD 内容去伪影 | 中 |
|
||||||
|
| Ani4K | 动画 | 极高 |
|
||||||
|
| AniSD | SD 动画 | 极高 |
|
||||||
|
| Anime4K | 动画 | 低 |
|
||||||
|
| SSIM | 低性能需求 | 低 |
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- Ani4K / AniSD 着色器(`shaders/Ani4K/`)来自 [Sirosky/Upscale-Hub](https://github.com/Sirosky/Upscale-Hub)。
|
||||||
|
- nnedi3 / ravu 着色器(`shaders/nnedi3/`,`shaders/ravu/`)来自 [mpv-prescalers](https://github.com/bjin/mpv-prescalers)。
|
||||||
|
|
||||||
|
## 外部依赖
|
||||||
|
|
||||||
|
见 [DEPENDENCIES.md](DEPENDENCIES.md)。
|
||||||
|
|
||||||
|
## 更新流程
|
||||||
|
|
||||||
|
1. 在 mpv 中按 `M` 触发 manager.lua,观察控制台输出,确认无 `FAILED` 条目。可能会有其他报错如 `[manager] Fehler: externes Repository manager existiert bereits.`,这是正常的。只需要确保不出现全大写的 `FAILED` 即可。
|
||||||
|
2. 将 manager.lua 在 dest 目录产生的嵌套 `.git` 目录迁移到仓库内的 `.manager/`(避免根仓库误判为 submodule,幂等可重复执行):
|
||||||
|
```bash
|
||||||
|
REPO=$(git -C ~/.config/mpv rev-parse --show-toplevel)
|
||||||
|
GITSTORE="$REPO/.manager"
|
||||||
|
mkdir -p "$GITSTORE"
|
||||||
|
find -L ~/.config/mpv -mindepth 2 -name .git -type d | while read gitdir; do
|
||||||
|
dest="${gitdir%/.git}"
|
||||||
|
rel="${dest#$REPO/}"
|
||||||
|
name=$(echo "$rel" | tr '/' '-')
|
||||||
|
mv "$gitdir" "$GITSTORE/$name"
|
||||||
|
depth=$(echo "$rel" | tr -cd '/' | wc -c)
|
||||||
|
ups=$(printf '../%.0s' $(seq 1 $((depth + 1))))
|
||||||
|
echo "gitdir: ${ups}.manager/$name" > "$dest/.git"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
3. 重启 mpv,检查控制台有无 `unknown key` 或脚本加载失败的警告
|
||||||
|
4. 若有 `unknown key` 警告,说明对应脚本的配置项发生变化,找 `script-opts/` 下同名 `.conf` 对照脚本源码更新
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+321
@@ -0,0 +1,321 @@
|
|||||||
|
# 此文件定义 mpv 的快捷键绑定和 uosc 右键菜单项目
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:打开
|
||||||
|
o script-message-to uosc open-file #menu: 打开 > 打开内置浏览器
|
||||||
|
# script-message-to uosc playlist #menu: 打开 > 播放菜单
|
||||||
|
# script-message-to uosc chapters #menu: 打开 > 章节菜单
|
||||||
|
# script-message-to uosc editions #menu: 打开 > 版本菜单
|
||||||
|
# script-message-to uosc audio #menu: 打开 > 其他音轨
|
||||||
|
# script-message-to uosc subtitles #menu: 打开 > 其他字幕
|
||||||
|
# ignore #menu: 打开 > ---
|
||||||
|
` script-message-to simplehistory open-list;show-text '' #menu: 打开 > 历史 > 打开历史菜单
|
||||||
|
ALT+l script-message-to simplehistory history-incognito-mode #menu: 打开 > 历史 > 开/关 隐身历史
|
||||||
|
CTRL+L script-message-to simplehistory history-load-last #menu: 打开 > 历史 > 加载最后播放文件
|
||||||
|
CTRL+l script-message-to simplehistory history-resume #menu: 打开 > 历史 > 加载最后播放文件及进度
|
||||||
|
# ignore #menu: 打开 > ---
|
||||||
|
CTRL+c set clipboard/text ${path};show-text "已复制文件路径" #menu: 打开 > 复制 > 复制文件路径
|
||||||
|
CTRL+ALT+c set clipboard/text ${filename};show-text "已复制文件名" #menu: 打开 > 复制 > 复制文件名
|
||||||
|
# ignore #menu: 打开 > ---
|
||||||
|
CTRL+F script-message-to quality_menu video_formats_toggle #menu: 打开 > Youtube-dl > 开/关 ytdl 视频选择菜单
|
||||||
|
ALT+F script-message-to quality_menu audio_formats_toggle #menu: 打开 > Youtube-dl > 开/关 ytdl 音频选择菜单
|
||||||
|
# script-message-to quality_menu reload #menu: 打开 > Youtube-dl > ytdl 重新加载
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:文件
|
||||||
|
SHIFT+F11 stop #menu: 文件 > 停止
|
||||||
|
ALT+t cycle ontop;show-text "置顶:${ontop}" #menu: 文件 > 开/关 置顶状态 #@state=(ontop and 'checked')
|
||||||
|
ALT+b cycle window-maximized #menu: 文件 > 开/关 最大化 #@state=(window_maximized and 'checked')
|
||||||
|
f cycle fullscreen #menu: 文件 > 开/关 全屏 #@state=(fullscreen and 'checked')
|
||||||
|
i script-binding stats/display-stats #menu: 文件 > 临时显示统计信息
|
||||||
|
I script-binding stats/display-stats-toggle #menu: 文件 > 常驻显示统计信息
|
||||||
|
l ab-loop #menu: 文件 > 设定/清除 片段循环
|
||||||
|
L cycle-values loop-file inf no;show-text "循环播放:${loop-file}" #menu: 文件 > 开/关 循环播放 #@state=(loop_file and 'checked')
|
||||||
|
n script-message playlistmanager shuffle #menu: 文件 > 随机播放
|
||||||
|
[ no-osd add speed -0.1; script-message-to uosc flash-speed #menu: 文件 > 速度 > 速度 -0.1
|
||||||
|
] no-osd add speed 0.1; script-message-to uosc flash-speed #menu: 文件 > 速度 > 速度 +0.1
|
||||||
|
{ no-osd multiply speed 0.5; script-message-to uosc flash-speed #menu: 文件 > 速度 > 半速
|
||||||
|
} no-osd multiply speed 2.0; script-message-to uosc flash-speed #menu: 文件 > 速度 > 倍速
|
||||||
|
BS no-osd set speed 1.0; script-message-to uosc flash-speed #menu: 文件 > 速度 > 重置速度
|
||||||
|
ALT+o script-message-to uosc show-in-directory #menu: 文件 > 定位当前文件
|
||||||
|
DEL script-message-to delete_current_file delete-file 1 "请按 1 确认删除" #menu: 文件 > 删除当前文件
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:导航
|
||||||
|
# show-text ${track-list} 5000 #menu: 导航 > 打开 OSD 轨道信息
|
||||||
|
O no-osd cycle-values osd-level 3 1 #menu: 导航 > 开/关 显示 OSD 时间轴
|
||||||
|
F4 script-binding select/menu;show-text '' #menu: 导航 > OSD 交互菜单 > 综合菜单
|
||||||
|
F5 script-message-to playlistmanager showplaylist;show-text '' #menu: 导航 > OSD 交互菜单 > 播放列表
|
||||||
|
F6 script-binding select/select-audio-device;show-text '' #menu: 导航 > OSD 交互菜单 > 音频设备列表
|
||||||
|
# script-binding select/select-edition;show-text '' #menu: 导航 > OSD 交互菜单 > 版本列表
|
||||||
|
F7 script-binding select/select-chapter;show-text '' #menu: 导航 > OSD 交互菜单 > 章节列表
|
||||||
|
F8 script-binding select/select-track;show-text '' #menu: 导航 > OSD 交互菜单 > 轨道列表
|
||||||
|
F9 script-binding select/select-vid;show-text '' #menu: 导航 > OSD 交互菜单 > 视频轨列表
|
||||||
|
F10 script-binding select/select-aid;show-text '' #menu: 导航 > OSD 交互菜单 > 音频轨列表
|
||||||
|
F11 script-binding select/select-sid;show-text '' #menu: 导航 > OSD 交互菜单 > 字幕轨列表
|
||||||
|
ALT+c script-message-to chapter_make_read create_chapter #menu: 导航 > 章节制作 > 标记章节时间
|
||||||
|
ALT+e script-message-to chapter_make_read edit_chapter #menu: 导航 > 章节制作 > 编辑章节标题
|
||||||
|
ALT+r script-message-to chapter_make_read remove_chapter #menu: 导航 > 章节制作 > 删除当前章节
|
||||||
|
ALT+w script-message-to chapter_make_read write_chapter chp #menu: 导航 > 章节制作 > 创建 chp 章节文件
|
||||||
|
ALT+g script-message-to chapter_make_read write_chapter ogm #menu: 导航 > 章节制作 > 创建 ogm 章节文件
|
||||||
|
< playlist-prev;show-text "播放列表:${playlist-pos-1}/${playlist-count}" #menu: 导航 > 上个文件
|
||||||
|
> playlist-next;show-text "播放列表:${playlist-pos-1}/${playlist-count}" #menu: 导航 > 下个文件
|
||||||
|
PGDWN add chapter -1 #menu: 导航 > 上一章节
|
||||||
|
PGUP add chapter 1 #menu: 导航 > 下一章节
|
||||||
|
F3 script-message-to chapterskip skip-to-silence;show-text "跳到下一个静音位置" #menu: 导航 > 跳到下一个静音位置
|
||||||
|
ALT+q script-message-to chapterskip chapter-skip #menu: 导航 > 切换 章节跳过模式
|
||||||
|
ALT+n script-message-to chapterskip toggle-markskip #menu: 导航 > 标记片头片尾
|
||||||
|
, frame-back-step;show-text "当前帧:${estimated-frame-number}" #menu: 导航 > 前进后退 > 上一帧
|
||||||
|
. frame-step ;show-text "当前帧:${estimated-frame-number}" #menu: 导航 > 前进后退 > 下一帧
|
||||||
|
RIGHT seek 5 #menu: 导航 > 前进后退 > 前进 5 秒
|
||||||
|
LEFT seek -5 #menu: 导航 > 前进后退 > 后退 5 秒
|
||||||
|
UP no-osd add volume 5; script-message-to uosc flash-volume #menu: 音频 > 音量 > 音量 +5
|
||||||
|
DOWN no-osd add volume -5; script-message-to uosc flash-volume #menu: 音频 > 音量 > 音量 -5
|
||||||
|
SHIFT+RIGHT seek 1 exact #menu: 导航 > 前进后退 > 精准前进 1 秒
|
||||||
|
SHIFT+LEFT seek -1 exact #menu: 导航 > 前进后退 > 精准后退 1 秒
|
||||||
|
SHIFT+UP seek 85 exact #menu: 导航 > 前进后退 > 精准前进 80 秒
|
||||||
|
SHIFT+DOWN seek -85 exact #menu: 导航 > 前进后退 > 精准后退 80 秒
|
||||||
|
CTRL+z script-message-to undoredo undo #menu: 导航 > 跳转 > 撤消跳转
|
||||||
|
CTRL+x script-message-to undoredo redo #menu: 导航 > 跳转 > 重做跳转
|
||||||
|
CTRL+ALT+z script-message-to undoredo undoLoop #menu: 导航 > 跳转 > 循环跳转
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:画面
|
||||||
|
# set video-aspect-override "-1";show-text "宽高比:${video-aspect-override}" #menu: 画面 > 切换 宽高比 > 默认值
|
||||||
|
# set video-aspect-override "16:9";show-text "宽高比:${video-aspect-override}" #menu: 画面 > 切换 宽高比 > 16:9
|
||||||
|
# set video-aspect-override "4:3";show-text "宽高比:${video-aspect-override}" #menu: 画面 > 切换 宽高比 > 4:3
|
||||||
|
# set video-aspect-override "2.35:1";show-text "宽高比:${video-aspect-override}" #menu: 画面 > 切换 宽高比 > 2.35:1
|
||||||
|
A cycle-values video-aspect-override 16:9 4:3 2.35:1 -1;show-text "宽高比:${video-aspect-override}" #menu: 画面 > 切换 宽高比 > 循环切换
|
||||||
|
CTRL+LEFT cycle-values video-rotate 0 270 180 90;show-text "视频旋转:${video-rotate}" #menu: 画面 > 左旋转
|
||||||
|
CTRL+RIGHT cycle-values video-rotate 0 90 180 270;show-text "视频旋转:${video-rotate}" #menu: 画面 > 右旋转
|
||||||
|
CTRL+- add window-scale -0.1;show-text "窗口缩小:${window-scale}" #menu: 画面 > 画面缩放 > 缩小窗口
|
||||||
|
CTRL+= add window-scale 0.1;show-text "窗口放大:${window-scale}" #menu: 画面 > 画面缩放 > 放大窗口
|
||||||
|
ALT+- add video-zoom -0.1;show-text "画面缩小:${video-zoom}" #menu: 画面 > 画面缩放 > 画面缩小
|
||||||
|
ALT+= add video-zoom 0.1;show-text "画面放大:${video-zoom}" #menu: 画面 > 画面缩放 > 画面放大
|
||||||
|
ALT+LEFT add video-pan-x -0.1;show-text "画面左移动:${video-pan-x}" #menu: 画面 > 画面缩放 > 画面左移动
|
||||||
|
ALT+RIGHT add video-pan-x 0.1;show-text "画面右移动:${video-pan-x}" #menu: 画面 > 画面缩放 > 画面右移动
|
||||||
|
ALT+UP add video-pan-y -0.1;show-text "画面上移动:${video-pan-y}" #menu: 画面 > 画面缩放 > 画面上移动
|
||||||
|
ALT+DOWN add video-pan-y 0.1;show-text "画面下移动:${video-pan-y}" #menu: 画面 > 画面缩放 > 画面下移动
|
||||||
|
ALT+p cycle-values panscan 0.0 1.0;show-text "视频画面缩放:${panscan}" #menu: 画面 > 开/关 裁切填充 #@state=(panscan and 'checked')
|
||||||
|
# ignore #menu: 画面 > ---
|
||||||
|
ALT+BS set video-zoom 0;set panscan 0;set video-rotate 0;set video-pan-x 0;set video-pan-y 0;set video-aspect-override -1;show-text "重置画面操作" #menu: 画面 > 重置以上画面操作
|
||||||
|
# ignore #menu: 画面 > ---
|
||||||
|
CTRL+I cycle icc-profile-auto ;show-text "ICC 自动校色:${icc-profile-auto}" #menu: 画面 > 开/关 自动 ICC 校色 #@state=(icc_profile_auto and 'checked')
|
||||||
|
# cycle sigmoid-upscaling;show-text "非线性色彩转换:${sigmoid-upscaling}" #menu: 画面 > 开/关 非线性色彩转换 #@state=(sigmoid_upscaling and 'checked')
|
||||||
|
1 add contrast -1;show-text "对比度:${contrast}" #menu: 画面 > 调色 > 对比度 -1
|
||||||
|
2 add contrast 1;show-text "对比度:${contrast}" #menu: 画面 > 调色 > 对比度 +1
|
||||||
|
3 add brightness -1;show-text "明度:${brightness}" #menu: 画面 > 调色 > 明度 -1
|
||||||
|
4 add brightness 1;show-text "明度:${brightness}" #menu: 画面 > 调色 > 明度 +1
|
||||||
|
5 add gamma -1;show-text "伽马:${gamma}" #menu: 画面 > 调色 > 伽马 -1
|
||||||
|
6 add gamma 1;show-text "伽马:${gamma}" #menu: 画面 > 调色 > 伽马 +1
|
||||||
|
7 add saturation -1;show-text "饱和度:${saturation}" #menu: 画面 > 调色 > 饱和度 -1
|
||||||
|
8 add saturation 1;show-text "饱和度:${saturation}" #menu: 画面 > 调色 > 饱和度 +1
|
||||||
|
- add hue -1;show-text "色相:${hue}" #menu: 画面 > 调色 > 色相 -1
|
||||||
|
= add hue 1;show-text "色相:${hue}" #menu: 画面 > 调色 > 色相 +1
|
||||||
|
CTRL+BS set contrast 0;set brightness 0;set gamma 0;set saturation 0;set hue 0;show-text "重置调色" #menu: 画面 > 调色 > 重置
|
||||||
|
D cycle deband;show-text "去色带:${deband}" #menu: 画面 > 去色带 > deband 开关 #@state=(deband and 'checked')
|
||||||
|
ALT+z add deband-iterations +1;show-text "增加去色带强度:${deband-iterations}" #menu: 画面 > 去色带 > deband 强度 +1
|
||||||
|
ALT+x add deband-iterations -1;show-text "降低去色带强度:${deband-iterations}" #menu: 画面 > 去色带 > deband 强度 -1
|
||||||
|
h cycle-values tone-mapping auto spline bt.2390 hable bt.2446a st2094-40 st2094-10;show-text "HDR 映射曲线:${tone-mapping}" #menu: 画面 > HDR 相关 > 切换 HDR 映射曲线
|
||||||
|
ALT+h cycle-values hdr-compute-peak yes no;show-text "HDR 动态映射:${hdr-compute-peak}" #menu: 画面 > HDR 相关 > 切换 HDR 动态映射 #@state=(hdr_compute_peak and 'checked')
|
||||||
|
CTRL+h cycle target-colorspace-hint;show-text "HDR 直通模式:${target-colorspace-hint}" #menu: 画面 > HDR 相关 > 切换 HDR 直通模式 #@state=(target_colorspace_hint and 'checked')
|
||||||
|
CTRL+t cycle-values target-trc auto pq gamma2.2;show-text "显示器传输特性:${target-trc}" #menu: 画面 > HDR 相关 > 切换 显示器传输特性
|
||||||
|
CTRL+T cycle-values target-peak 100 203;show-text "映射目标峰值:${target-peak}" #menu: 画面 > HDR 相关 > 切换 映射目标峰值
|
||||||
|
CTRL+g cycle gamut-mapping-mode ;show-text "色域映射模式:${gamut-mapping-mode}" #menu: 画面 > HDR 相关 > 切换 色域映射模式
|
||||||
|
# cycle tone-mapping-visualize;show-text "色调映射可视化模式:${tone-mapping-visualize}" #menu: 画面 > HDR 相关 > 切换 色调映射可视化模式
|
||||||
|
# ignore #menu: ---
|
||||||
|
|
||||||
|
##⇘⇘menu 一级菜单:轨道
|
||||||
|
# ignore #menu: 菜单 > 轨道 #@tracks
|
||||||
|
# ignore #menu: 菜单 > 次字幕 #@tracks/sub-secondary
|
||||||
|
# ignore #menu: 菜单 > 章节列表 #@chapters
|
||||||
|
# ignore #menu: 菜单 > 版本列表 #@editions
|
||||||
|
# ignore #menu: ---
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:视频
|
||||||
|
# cycle video #menu: 视频 > 切换 视频轨
|
||||||
|
# set hwdec "no" #menu: 视频 > 切换 解码方式 > 软解
|
||||||
|
# set hwdec "auto-safe" #menu: 视频 > 切换 解码方式 > 自动选择硬解加速模式
|
||||||
|
# set hwdec "auto-copy-safe" #menu: 视频 > 切换 解码方式 > 自动选择 copy 硬解模式
|
||||||
|
# set hwdec "nvdec" #menu: 视频 > 切换 解码方式 > nvdec 硬解
|
||||||
|
# set hwdec "nvdec-copy" #menu: 视频 > 切换 解码方式 > nvdec-copy 硬解
|
||||||
|
# set video-sync audio;show-text "帧同步模式:${video-sync}" #menu: 视频 > 切换 帧同步模式 > audio
|
||||||
|
# set video-sync display-resample;show-text "帧同步模式:${video-sync}" #menu: 视频 > 切换 帧同步模式 > display-resample
|
||||||
|
# set video-sync display-tempo;show-text "帧同步模式:${video-sync}" #menu: 视频 > 切换 帧同步模式 > display-tempo
|
||||||
|
# set video-sync display-vdrop;show-text "帧同步模式:${video-sync}" #menu: 视频 > 切换 帧同步模式 > display-vdrop
|
||||||
|
# set video-sync display-resample-vdrop;show-text "帧同步模式:${video-sync}" #menu: 视频 > 切换 帧同步模式 > display-resample-vdrop
|
||||||
|
# cycle-values video-sync display-resample display-tempo audio display-vdrop display-resample-vdrop;show-text "帧同步模式:${video-sync}" #menu: 视频 > 切换 帧同步模式 > 循环切换
|
||||||
|
ALT+i cycle interpolation ;show-text "抖动补偿:${interpolation}" #menu: 视频 > 开/关 抖动补偿 #@state=(interpolation and 'checked')
|
||||||
|
d cycle deinterlace;show-text "去交错:${deinterlace}" #menu: 视频 > 开/关 反交错 #@state=(deinterlace and 'checked')
|
||||||
|
# cycle vd-lavc-assume-old-x264;show-text "兼容 x264 旧编码模式:${vd-lavc-assume-old-x264}" #menu: 视频 > 开/关 兼容 x264 旧编码模式 #@state=(vd_lavc_assume_old_x264 and 'checked')
|
||||||
|
# ignore #menu: 视频 > ---
|
||||||
|
s screenshot subtitles #menu: 视频 > 截屏 > 同源尺寸 - 有字幕 - 无 OSD-单帧
|
||||||
|
S screenshot video #menu: 视频 > 截屏 > 同源尺寸 - 无字幕 - 无 OSD-单帧
|
||||||
|
CTRL+s show-text "截屏" 400;script-message delay-command 0.5 screenshot window #menu: 视频 > 截屏 > 实际尺寸 - 有字幕 - 有 OSD-单帧
|
||||||
|
ALT+s screenshot subtitles+each-frame #menu: 视频 > 截屏 > 同源尺寸 - 有字幕 - 无 OSD-逐帧
|
||||||
|
ALT+S screenshot video+each-frame #menu: 视频 > 截屏 > 同源尺寸 - 无字幕 - 无 OSD-逐帧
|
||||||
|
CTRL+S show-text "逐帧截屏" 400;script-message delay-command 0.5 screenshot window+each-frame #menu: 视频 > 截屏 > 实际尺寸 - 有字幕 - 有 OSD-逐帧
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:音频
|
||||||
|
# script-message-to uosc audio-device #menu: 音频 > 音频设备列表 #@audio-devices
|
||||||
|
# ignore #menu: 音频 > ---
|
||||||
|
y cycle audio;show-text "音轨切换为:${audio}" #menu: 音频 > 切换 音频轨
|
||||||
|
m cycle mute;show-text "静音:${mute}" #menu: 音频 > 切换 静音 #@state=(mute and 'checked')
|
||||||
|
# ignore #menu: 音频 > ---
|
||||||
|
CTRL+, add audio-delay -0.1;show-text "音频延迟:${audio-delay}" #menu: 音频 > 延迟 -0.1
|
||||||
|
CTRL+. add audio-delay 0.1;show-text "音频预载:${audio-delay}" #menu: 音频 > 延迟 +0.1
|
||||||
|
; set audio-delay 0 ;show-text "重置音频延迟:${audio-delay}" #menu: 音频 > 延迟 重置
|
||||||
|
# ignore #menu: 音频 > ---
|
||||||
|
# cycle audio-normalize-downmix;show-text "音频规格化:${audio-normalize-downmix}" #menu: 音频 > 切换 音频规格化 #@state=(audio_normalize_downmix and 'checked')
|
||||||
|
CTRL+y cycle audio-exclusive ;show-text "音频独占模式:${audio-exclusive}" #menu: 音频 > 切换 音频独占模式 #@state=(audio_exclusive and 'checked')
|
||||||
|
CTRL+Y cycle hr-seek-framedrop;show-text "音频同步模式:${hr-seek-framedrop}" #menu: 音频 > 切换 音频同步模式 #@state=(hr_seek_framedrop and 'checked')
|
||||||
|
# set audio-channels "7.1";show-text "音频通道输出方式:${audio-channels}" #menu: 音频 > 音频通道输出方式 > 7.1 声道输出
|
||||||
|
# set audio-channels "5.1";show-text "音频通道输出方式:${audio-channels}" #menu: 音频 > 音频通道输出方式 > 5.1 声道输出
|
||||||
|
# set audio-channels "stereo";show-text "音频通道输出方式:${audio-channels}" #menu: 音频 > 音频通道输出方式 > 双通道输出
|
||||||
|
# set audio-channels "7.1,5.1,stereo";show-text "音频通道输出方式:${audio-channels}" #menu: 音频 > 音频通道输出方式 > 自动选择以上输出方式
|
||||||
|
ALT+y cycle-values audio-channels "7.1,5.1,stereo" "7.1" "5.1" "stereo" "auto-safe" "auto";show-text "音频通道输出方式:${audio-channels}" #menu: 音频 > 音频通道输出方式 > 循环切换
|
||||||
|
F2 cycle-values af "@dynaudnorm:lavfi=[dynaudnorm=f=500:g=31:p=0.5:m=5:r=0.9]" "@loudnorm:lavfi=[loudnorm=I=-16:TP=-1.5:LRA=11]" "" #menu: 音频 > 切换 下混滤镜
|
||||||
|
ALT+` af clr "" #menu: 音频 > 清空 af 滤镜
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:字幕
|
||||||
|
j cycle sub;show-text "字幕切换为:${sub}" #menu: 字幕 > 切换 字幕轨
|
||||||
|
k cycle secondary-sid;show-text "切换次字幕:${secondary-sid}" #menu: 字幕 > 切换 次字幕
|
||||||
|
v cycle sub-visibility;show-text "字幕可见性:${sub-visibility}" #menu: 字幕 > 切换 字幕可见性 #@state=(sub_visibility and 'checked')
|
||||||
|
ALT+V cycle secondary-sub-visibility;show-text "次字幕可见性:${secondary-sub-visibility}" #menu: 字幕 > 切换 次字幕可见性 #@state=(secondary_sub_visibility and 'checked')
|
||||||
|
u cycle sub-ass-override;show-text "字幕渲染样式:${sub-ass-override}" #menu: 字幕 > 切换 渲染样式
|
||||||
|
F cycle-values sub-font "Noto Sans CJK SC" "Noto Sans CJK KR" "Noto Serif CJK SC" "Noto Serif CJK KR";show-text "字幕字体:${sub-font}" #menu: 字幕 > 切换 默认字体
|
||||||
|
CTRL+r sub-reload;show-text "重载当前字幕" #menu: 字幕 > 重载当前字幕
|
||||||
|
# ignore #menu: 字幕 > ---
|
||||||
|
ALT+R cycle secondary-sub-ass-override;show-text "次字幕样式覆盖:${secondary-sub-ass-override}" #menu: 字幕 > 兼容性 > 切换 次字幕样式覆盖 #@state=(secondary_sub_ass_override and 'checked')
|
||||||
|
ALT+T cycle blend-subtitles;show-text "字幕混合视频帧:${blend-subtitles}" #menu: 字幕 > 兼容性 > 切换 字幕混合视频帧 #@state=(blend_subtitles and 'checked')
|
||||||
|
K cycle sub-fix-timing ;show-text "字幕时序修复:${sub-fix-timing}" #menu: 字幕 > 兼容性 > 切换 字幕时序修复 #@state=(sub_fix_timing and 'checked')
|
||||||
|
J cycle sub-ass-vsfilter-color-compat ;show-text "字幕颜色转换方式:${sub-ass-vsfilter-color-compat}" #menu: 字幕 > 兼容性 > 切换 字幕颜色转换方式
|
||||||
|
V cycle sub-ass-use-video-data ;show-text "使用视频信息:${sub-ass-use-video-data}" #menu: 字幕 > 兼容性 > 切换 使用视频信息
|
||||||
|
ALT+B cycle sub-vsfilter-bidi-compat ;show-text "bidi 双向检测兼容性:${sub-vsfilter-bidi-compat}" #menu: 字幕 > 兼容性 > 切换 bidi 双向检测兼容性 #@state=(sub_vsfilter_bidi_compat and 'checked')
|
||||||
|
ALT+X cycle-values sub-ass-style-overrides "ScaledBorderAndShadow=no" "ScaledBorderAndShadow=yes";show-text "强制替换 ass 样式:${sub-ass-style-overrides}" #menu: 字幕 > 兼容性 > 切换 ass 字幕阴影边框缩放
|
||||||
|
H cycle sub-ass-force-margins ;show-text "ass 字幕输出黑边:${sub-ass-force-margins}" #menu: 字幕 > 兼容性 > 切换 ass 字幕输出到黑边 #@state=(sub_ass_force_margins and 'checked')
|
||||||
|
ALT+Z cycle sub-use-margins ;show-text "srt 字幕输出黑边:${sub-use-margins}" #menu: 字幕 > 兼容性 > 切换 srt 字幕输出到黑边 #@state=(sub_use_margins and 'checked')
|
||||||
|
P cycle stretch-image-subs-to-screen ;show-text "pgs 字幕输出黑边:${stretch-image-subs-to-screen}" #menu: 字幕 > 兼容性 > 切换 pgs 字幕输出到黑边 #@state=(stretch_image_subs_to_screen and 'checked')
|
||||||
|
p cycle sub-gray;show-text "pgs 字幕灰度转换:${sub-gray}" #menu: 字幕 > 兼容性 > 切换 pgs 字幕灰度转换 #@state=(sub_gray and 'checked')
|
||||||
|
# ignore #menu: 字幕 > ---
|
||||||
|
Y script-message sub-select toggle #menu: 字幕 > 切换 字幕选择脚本
|
||||||
|
CTRL+f script-message-to sub_assrt sub-assrt #menu: 字幕 > 打开 字幕下载菜单
|
||||||
|
CTRL+m script-message-to autosubsync autosubsync-menu #menu: 字幕 > 打开 字幕同步菜单
|
||||||
|
CTRL+M script-binding select/select-subtitle-line #menu: 字幕 > 打开 字幕内容菜单
|
||||||
|
ALT+m script-message-to sub_export export-selected-subtitles #menu: 字幕 > 导出当前内封字幕
|
||||||
|
# ignore #menu: 字幕 > ---
|
||||||
|
r add sub-pos -1;show-text "字幕上移:${sub-pos}" #menu: 字幕 > 其他操作 > 字幕上移
|
||||||
|
t add sub-pos +1;show-text "字幕下移:${sub-pos}" #menu: 字幕 > 其他操作 > 字幕下移
|
||||||
|
R add secondary-sub-pos -1;show-text "次字幕上移:${secondary-sub-pos}" #menu: 字幕 > 其他操作 > 次字幕上移
|
||||||
|
T add secondary-sub-pos +1;show-text "次字幕下移:${secondary-sub-pos}" #menu: 字幕 > 其他操作 > 次字幕下移
|
||||||
|
z add sub-delay -0.1;show-text "字幕延迟:${sub-delay}" #menu: 字幕 > 其他操作 > 字幕延迟 -0.1
|
||||||
|
x add sub-delay 0.1;show-text "字幕预载:${sub-delay}" #menu: 字幕 > 其他操作 > 字幕延迟 +0.1
|
||||||
|
Z add secondary-sub-delay -0.1;show-text "次字幕延迟:${secondary-sub-delay}" #menu: 字幕 > 其他操作 > 次字幕延迟 -0.1
|
||||||
|
X add secondary-sub-delay 0.1;show-text "次字幕预载:${secondary-sub-delay}" #menu: 字幕 > 其他操作 > 次字幕延迟 +0.1
|
||||||
|
ALT+j add sub-scale -0.1;show-text "字幕缩小:${sub-scale}" #menu: 字幕 > 其他操作 > 字号 -0.1
|
||||||
|
ALT+k add sub-scale 0.1;show-text "字幕放大:${sub-scale}" #menu: 字幕 > 其他操作 > 字号 +0.1
|
||||||
|
CTRL+j sub-seek -1 #menu: 字幕 > 其他操作 > 跳转上一条字幕
|
||||||
|
CTRL+k sub-seek 1 #menu: 字幕 > 其他操作 > 跳转下一条字幕
|
||||||
|
SHIFT+BS set sub-pos 100;set sub-scale 1.0;set sub-delay 0;show-text "重置字幕状态" #menu: 字幕 > 其他操作 > 恢复初始
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:视频滤镜
|
||||||
|
CTRL+` vf clr "" #menu: 视频滤镜 > 清空
|
||||||
|
ALT+v vf toggle deblock=filter=weak:block=4 #menu: 视频滤镜 > 开/关 去色块滤镜
|
||||||
|
! vf toggle format=colorlevels=limited #menu: 视频滤镜 > 开/关 动态范围限制
|
||||||
|
@ vf toggle vflip #menu: 视频滤镜 > 开/关 垂直翻转
|
||||||
|
SHARP vf toggle hflip #menu: 视频滤镜 > 开/关 水平翻转
|
||||||
|
$ vf toggle rotate=angle=180*PI/180 #menu: 视频滤镜 > 开/关 旋转 180
|
||||||
|
% vf toggle format:gamma=gamma2.2 #menu: 视频滤镜 > 开/关 伽马修正 2.2
|
||||||
|
^ vf toggle fps=fps=60/1.001 #menu: 视频滤镜 > 开/关 强制帧数 59.94
|
||||||
|
* vf toggle pad=aspect=16/9:x=-1:y=-1 #menu: 视频滤镜 > 开/关 填充 16:9 的黑边并居中
|
||||||
|
& vf toggle colortemperature=temperature=6500 #menu: 视频滤镜 > 开/关 色温修正 6500
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:着色器
|
||||||
|
CTRL+0 change-list glsl-shaders clr "" #menu: 着色器 > 清空
|
||||||
|
CTRL+1 change-list glsl-shaders toggle "~~/shaders/igv/KrigBilateral.glsl" #menu: 着色器 > IGV > 开/关 KrigBilateral
|
||||||
|
CTRL+2 change-list glsl-shaders toggle "~~/shaders/igv/SSimSuperRes.glsl" #menu: 着色器 > IGV > 开/关 SSimSuperRes
|
||||||
|
CTRL+3 change-list glsl-shaders toggle "~~/shaders/igv/SSimDownscaler.glsl" #menu: 着色器 > IGV > 开/关 SSimDownscaler
|
||||||
|
CTRL+4 change-list glsl-shaders toggle "~~/shaders/igv/adaptive-sharpen.glsl" #menu: 着色器 > IGV > 开/关 自适应锐化
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/igv/adaptive-sharpen_luma.glsl" #menu: 着色器 > IGV > 开/关 自适应锐化-luma
|
||||||
|
CTRL+5 change-list glsl-shaders toggle "~~/shaders/igv/FSRCNNX_x2_8-0-4-1.glsl" #menu: 着色器 > FSRCNNX > 开/关 FSRCNNX
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/igv/FSRCNNX_x2_16-0-4-1.glsl" #menu: 着色器 > FSRCNNX > 开/关 FSRCNNX-16x
|
||||||
|
CTRL+6 change-list glsl-shaders toggle "~~/shaders/igv/FSRCNNX_x1_16-0-4-1_distort.glsl" #menu: 着色器 > FSRCNNX > 开/关 FSRCNNX-distort
|
||||||
|
CTRL+7 change-list glsl-shaders toggle "~~/shaders/nnedi3/nnedi3-nns32-win8x4.glsl" #menu: 着色器 > NNEDI3 > 开/关 nns32-4
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/nnedi3/nnedi3-nns32-win8x6.glsl" #menu: 着色器 > NNEDI3 > 开/关 nns32-6
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/nnedi3/nnedi3-nns64-win8x4.glsl" #menu: 着色器 > NNEDI3 > 开/关 nns64-4
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/nnedi3/nnedi3-nns64-win8x6.glsl" #menu: 着色器 > NNEDI3 > 开/关 nns64-6
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/nnedi3/nnedi3-nns128-win8x4.glsl" #menu: 着色器 > NNEDI3 > 开/关 nns128-4
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/nnedi3/nnedi3-nns128-win8x6.glsl" #menu: 着色器 > NNEDI3 > 开/关 nns128-6
|
||||||
|
CTRL+8 change-list glsl-shaders toggle "~~/shaders/ravu/ravu-zoom-ar-r3.glsl" #menu: 着色器 > RAVU > 开/关 ravu-zoom-ar
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/ravu/ravu-lite-ar-r3.glsl" #menu: 着色器 > RAVU > 开/关 ravu-lite-ar
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Restore/Anime4K_Clamp_Highlights.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 去伪影
|
||||||
|
CTRL+9 change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Restore/Anime4K_Restore_CNN_Soft_M.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 抗锯齿
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Restore/Anime4K_Restore_CNN_Soft_VL.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 中强度抗锯齿
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Restore/Anime4K_Restore_CNN_Soft_UL.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 高强度抗锯齿
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Experimental-Effects/Anime4K_Darken_HQ.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 深线
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Experimental-Effects/Anime4K_Thin_HQ.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 收线
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Denoise/Anime4K_Denoise_Bilateral_Mode.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 降噪
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Deblur/Anime4K_Deblur_DoG.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 去糊
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/Anime4K/glsl/Upscale/Anime4K_Upscale_CNN_x2_S.glsl" #menu: 着色器 > Anime4K > 开/关 Anime4K 放大
|
||||||
|
# change-list glsl-shaders toggle "~~/shaders/other/PixelClipper.glsl" #menu: 着色器 > Other > 开/关 抗振铃
|
||||||
|
# script-message cycle-commands "change-list glsl-shaders pre '~~/shaders/other/nlmeans_luma.glsl'" "change-list glsl-shaders remove '~~/shaders/other/nlmeans_luma.glsl'" #menu: 着色器 > Other > 开/关 nlmeans-luma 降噪
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:其它
|
||||||
|
CTRL+P script-message cycle-commands "apply-profile FSRCNNX;show-text 配置组:FSRCNNX" "apply-profile FSRCNNX+;show-text 配置组:FSRCNNX+" "apply-profile NNEDI3;show-text 配置组:NNEDI3" "apply-profile ravu-zoom;show-text 配置组:ravu-zoom" "apply-profile Anime4K;show-text 配置组:Anime4K" #menu: 其它 > 常规配置组 > 切换 指定配置组
|
||||||
|
ALT+1 apply-profile FSRCNNX;show-text "配置组:FSRCNNX" #menu: 其它 > 常规配置组 > 切换 FSRCNNX 配置
|
||||||
|
ALT+2 apply-profile FSRCNNX+;show-text "配置组:FSRCNNX+" #menu: 其它 > 常规配置组 > 切换 FSRCNNX+ 配置
|
||||||
|
ALT+3 apply-profile ravu-zoom;show-text "配置组:ravu-zoom" #menu: 其它 > 常规配置组 > 切换 Ravu-zoom 配置
|
||||||
|
ALT+4 apply-profile Ani4K;show-text "配置组:Ani4K" #menu: 其它 > 常规配置组 > 切换 Ani4K 配置
|
||||||
|
ALT+5 apply-profile AniSD;show-text "配置组:AniSD" #menu: 其它 > 常规配置组 > 切换 AniSD 配置
|
||||||
|
ALT+6 apply-profile Anime4K;show-text "配置组:Anime4K" #menu: 其它 > 常规配置组 > 切换 Anime4k 配置
|
||||||
|
ALT+7 apply-profile NNEDI3;show-text "配置组:NNEDI3" #menu: 其它 > 常规配置组 > 切换 NNEDI3 配置
|
||||||
|
ALT+8 apply-profile NNEDI3+;show-text "配置组:NNEDI3+" #menu: 其它 > 常规配置组 > 切换 NNEDI3+ 配置
|
||||||
|
# apply-profile SSIM;show-text "配置组:SSIM" #menu: 其它 > 常规配置组 > 切换 SSIM 配置
|
||||||
|
# cycle border;show-text "显示边框:${border}" #menu: 其它 > 切换 边框模式 #@state=(border and 'checked')
|
||||||
|
CTRL+B cycle title-bar;show-text "显示标题栏:${title-bar}" #menu: 其它 > 切换 标题栏 #@state=(title_bar and 'checked')
|
||||||
|
# cycle-values title ${media-title} ${filename} #menu: 其它 > 切换 媒体标题
|
||||||
|
~ script-binding console/enable #menu: 其它 > 打开 控制台
|
||||||
|
CTRL+R cycle-values reset-on-next-file "all" "no" "vf,af,loop-file,deinterlace,border,contrast,brightness,gamma,saturation,hue,video-zoom,video-rotate,video-pan-x,video-pan-y,panscan,speed,audio,sub,audio-delay,sub-pos,sub-scale,sub-delay,sub-speed,sub-visibility";show-text "播放下一个文件时重置以下选项:${reset-on-next-file}" #menu: 其它 > 重置播放中更改项
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单:工具
|
||||||
|
Ctrl+p script-message-to command_palette show-command-palette "Command Palette" #menu: 工具 > 打开命令面板
|
||||||
|
# set clipboard/text ${filename} #menu: 工具 > 复制文件信息 > 复制当前文件名
|
||||||
|
# set clipboard/text ${path} #menu: 工具 > 复制文件信息 > 复制当前文件路径
|
||||||
|
CTRL+ALT+t set clipboard/text ${time-pos} #menu: 工具 > 复制文件信息 > 复制当前时间
|
||||||
|
CTRL+ALT+s set clipboard/text ${sub-text} #menu: 工具 > 复制文件信息 > 复制当前字幕内容
|
||||||
|
# script-message-to uosc open-config-directory #menu: 工具 > 定位配置文件
|
||||||
|
M script-message manager-update-all ;show-text "更新脚本着色器" #menu: 工具 > 一键更新脚本和着色器
|
||||||
|
CTRL+d script-message-to uosc_danmaku show_danmaku_keyboard #menu: 工具 > 弹幕功能 > 开/关 弹幕显示
|
||||||
|
CTRL+D script-message-to uosc_danmaku open_add_total_menu #menu: 工具 > 弹幕功能 > 打开弹幕综合菜单
|
||||||
|
CTRL+ALT+d script-message danmaku-delay 1 ${=time-pos} #menu: 工具 > 弹幕功能 > 弹幕延迟 +1 秒
|
||||||
|
CTRL+ALT+a script-message danmaku-delay -1 ${=time-pos} #menu: 工具 > 弹幕功能 > 弹幕延迟 -1 秒
|
||||||
|
_ script-message-to uosc_danmaku immediately_save_danmaku #menu: 工具 > 弹幕功能 > 保存当前弹幕
|
||||||
|
# ignore #menu: ---
|
||||||
|
|
||||||
|
##⇘⇘uosc 一级菜单
|
||||||
|
b cycle window-minimized #menu: 最小化
|
||||||
|
q quit #menu: 退出程序
|
||||||
|
Q quit-watch-later # 退出并保存当前状态
|
||||||
|
|
||||||
|
##⇘⇘以下键位不显示在 uosc 菜单中
|
||||||
|
MENU script-message-to uosc menu-blurred # 开/关 uosc 菜单
|
||||||
|
POWER quit
|
||||||
|
PLAY cycle pause;script-message-to uosc flash-pause-indicator
|
||||||
|
PAUSE cycle pause;script-message-to uosc flash-pause-indicator
|
||||||
|
PLAYPAUSE cycle pause;script-message-to uosc flash-pause-indicator
|
||||||
|
STOP quit
|
||||||
|
FORWARD seek 30
|
||||||
|
REWIND seek -30
|
||||||
|
NEXT playlist-next
|
||||||
|
PREV playlist-prev
|
||||||
|
|
||||||
|
SPACE cycle pause;script-message-to uosc flash-pause-indicator
|
||||||
|
|
||||||
|
MBTN_LEFT cycle pause;script-message-to uosc flash-pause-indicator
|
||||||
|
MBTN_LEFT_DBL ignore
|
||||||
|
MBTN_Right script-message-to uosc menu-blurred # 开/关 uosc 菜单
|
||||||
|
MBTN_FORWARD playlist-next;show-text "播放列表:${playlist-pos-1}/${playlist-count}" # 前进键 切换到列表中的下个文件
|
||||||
|
MBTN_BACK playlist-prev;show-text "播放列表:${playlist-pos-1}/${playlist-count}" # 后退键 切换到列表中的上个文件
|
||||||
|
MBTN_Right_DBL ignore
|
||||||
|
Wheel_Up no-osd add volume 10; script-message-to uosc flash-volume
|
||||||
|
Wheel_Down no-osd add volume -10; script-message-to uosc flash-volume
|
||||||
|
ESC set fullscreen no;set window-maximized no
|
||||||
|
|
||||||
|
9 no-osd add volume -1; script-message-to uosc flash-volume # 音量 -1
|
||||||
|
0 no-osd add volume 1; script-message-to uosc flash-volume # 音量 +1
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
##⇘⇘以下为基于 inputevent.lua 脚本实现的增强式功能键位绑定
|
||||||
|
## '#event:click'用于指定同键位单击时的操作,'#event:double_click'用于指定同键位双击时的操作,'#event:press'用于指定同键位长按时的操作,'#event:release'用于指定同键位长按释放时的操作
|
||||||
|
## 更多的键位动作请参考脚本上游说明:https://github.com/zhongfly/InputEvent/tree/property-expansion
|
||||||
|
##! 注意:并非所有键位都具有以上不同的触发动作
|
||||||
|
## 同时脚本支持同一种动作根据条件触发不同的命令,条件的语法等同于 profile-cond(自动 profile 的条件语句)
|
||||||
|
## 即脚本语法升级为 '#event:动作|条件'(原先的#event:动作视为无条件执行命令),命令执行的优先级是从下到上按顺序,执行第一个满足条件的命令
|
||||||
|
|
||||||
|
RIGHT seek 5 #event:click
|
||||||
|
RIGHT script-message-to evafast speedup #event:press
|
||||||
|
RIGHT script-message-to evafast slowdown #event:release
|
||||||
|
|
||||||
|
TAB script-message-to uosc open-file #event:click
|
||||||
|
TAB script-message-to uosc toggle-ui #event:press
|
||||||
|
TAB script-message-to uosc toggle-ui #event:release
|
||||||
|
|
||||||
|
PGDWN add chapter -1 #event:click
|
||||||
|
PGDWN playlist-prev;show-text 播放列表:${playlist-pos-1}/${playlist-count} #event:click|chapter == 0
|
||||||
|
PGDWN playlist-prev;show-text 播放列表:${playlist-pos-1}/${playlist-count} #event:press
|
||||||
|
PGUP add chapter 1 #event:click
|
||||||
|
PGUP playlist-next;show-text 播放列表:${playlist-pos-1}/${playlist-count} #event:click|chapter+1 == chapters
|
||||||
|
PGUP playlist-next;show-text 播放列表:${playlist-pos-1}/${playlist-count} #event:press
|
||||||
|
|
||||||
|
SPACE cycle pause;script-message-to uosc flash-pause-indicator #event:click
|
||||||
|
SPACE script-binding simplehistory/history-load-last #event:click|idle_active
|
||||||
|
SPACE no-osd set speed 3; set pause no #event:press
|
||||||
|
SPACE ignore #event:release
|
||||||
|
|
||||||
|
MBTN_LEFT cycle pause;script-message-to uosc flash-pause-indicator #event:click
|
||||||
|
MBTN_LEFT cycle fullscreen #event:double_click
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"git": "https://github.com/po5/mpv_manager",
|
||||||
|
"branch": "master",
|
||||||
|
"whitelist": "manager%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/po5/evafast",
|
||||||
|
"branch": "rewrite",
|
||||||
|
"whitelist": "%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/stax76/mpv-scripts",
|
||||||
|
"branch": "main",
|
||||||
|
"whitelist": "delete_current_file%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/dyphire/mpv-scripts",
|
||||||
|
"branch": "main",
|
||||||
|
"blacklist": "license|%.md$|drcbox%.lua$|.-%-list%.lua$|mpv-torrserver.lua",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/dyphire/mpv-playlistmanager",
|
||||||
|
"branch": "dev",
|
||||||
|
"whitelist": "playlistmanager%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/dyphire/mpv-sub-assrt",
|
||||||
|
"whitelist": "%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/dyphire/chapterskip",
|
||||||
|
"branch": "dev",
|
||||||
|
"whitelist": "%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/dyphire/Eisa01_mpv-scripts",
|
||||||
|
"branch": "dev",
|
||||||
|
"whitelist": "undoredo%.lua$|simplehistory%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/dyphire/autosubsync-mpv",
|
||||||
|
"branch": "v0.33_CM",
|
||||||
|
"whitelist": "readme%.md$|%.lua$",
|
||||||
|
"dest": "~~/scripts/autosubsync"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/christoph-heinrich/mpv-quality-menu",
|
||||||
|
"whitelist": "quality%-menu%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/tomasklaen/uosc",
|
||||||
|
"branch": "main",
|
||||||
|
"whitelist": "src/uosc/",
|
||||||
|
"dest": "~~/scripts/uosc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/Tony15246/uosc_danmaku",
|
||||||
|
"branch": "main",
|
||||||
|
"blacklist": "^%.|^\"",
|
||||||
|
"dest": "~~/scripts/uosc_danmaku"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/CogentRedTester/mpv-sub-select",
|
||||||
|
"whitelist": "sub%-select%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/CogentRedTester/mpv-scripts",
|
||||||
|
"whitelist": "cycle%-commands%.lua$",
|
||||||
|
"dest": "~~/scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://gist.github.com/igv/8a77e4eb8276753b54bb94c1c50c317e",
|
||||||
|
"whitelist": "%.glsl$",
|
||||||
|
"dest": "~~/shaders/igv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://gist.github.com/igv/a015fc885d5c22e6891820ad89555637",
|
||||||
|
"whitelist": "%.glsl$",
|
||||||
|
"dest": "~~/shaders/igv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://gist.github.com/igv/2364ffa6e81540f29cb7ab4c9bc05b6b",
|
||||||
|
"whitelist": "%.glsl$",
|
||||||
|
"dest": "~~/shaders/igv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://gist.github.com/igv/36508af3ffc84410fe39761d6969be10",
|
||||||
|
"whitelist": "%.glsl$",
|
||||||
|
"dest": "~~/shaders/igv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/Artoriuz/glsl-joint-bilateral",
|
||||||
|
"branch": "main",
|
||||||
|
"whitelist": "jointbilateral%.glsl$",
|
||||||
|
"dest": "~~/shaders/other"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/Artoriuz/glsl-pixel-clipper",
|
||||||
|
"branch": "main",
|
||||||
|
"whitelist": "%.glsl$",
|
||||||
|
"dest": "~~/shaders/other"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"git": "https://github.com/bloc97/Anime4K",
|
||||||
|
"whitelist": "%.md$|%.glsl$",
|
||||||
|
"blacklist": "tensorflow",
|
||||||
|
"dest": "~~/shaders/Anime4K"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
# ===== 基础设置 =====
|
||||||
|
|
||||||
|
vo=gpu-next # 视频输出驱动
|
||||||
|
gpu-api=vulkan # 图形绘制接口
|
||||||
|
input-ime=no # 仅文本输入时激活 IME
|
||||||
|
autofit-smaller=40%x30% # 窗口最小占屏比例
|
||||||
|
idle=yes # 播放结束后保持运行
|
||||||
|
ontop # 窗口置顶
|
||||||
|
hr-seek=yes # 精确跳转
|
||||||
|
hr-seek-framedrop=no # 跳转时不丢帧,利于修正音频延迟
|
||||||
|
save-position-on-quit=yes # 退出时记住播放进度
|
||||||
|
write-filename-in-watch-later-config # 播放记录中写入文件名
|
||||||
|
resume-playback-check-mtime=yes # 校验文件修改时间防止误恢复
|
||||||
|
watch-later-dir="~~/cache/watch_later"
|
||||||
|
watch-later-options=start,vid,aid,sid
|
||||||
|
save-watch-history=yes # 保存播放历史(内置 select 脚本可浏览)
|
||||||
|
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
|
||||||
|
input-ipc-server=/tmp/mpvsocket # IPC 套接字
|
||||||
|
directory-mode=ignore # 打开目录时忽略子目录
|
||||||
|
metadata-codepage=auto # 元数据编码自动检测
|
||||||
|
msg-level=all=info,auto_profiles=warn
|
||||||
|
title=${?pause==yes:⏸}${?mute==yes:🔇}${?ontop==yes:📌}${?demuxer-via-network==yes:${media-title}}${?demuxer-via-network==no:${filename}}
|
||||||
|
|
||||||
|
# ===== OSD =====
|
||||||
|
|
||||||
|
no-osd-bar
|
||||||
|
osd-on-seek=msg-bar
|
||||||
|
osd-bar-w=100
|
||||||
|
osd-bar-h=2
|
||||||
|
osd-bar-align-y=-1
|
||||||
|
osd-font="Noto Sans CJK SC;Noto Color Emoji"
|
||||||
|
osd-font-size=24
|
||||||
|
osd-color="#FFFFFF"
|
||||||
|
osd-outline-size=1.0
|
||||||
|
osd-outline-color="#1C1B1F"
|
||||||
|
osd-shadow-offset=0
|
||||||
|
osd-back-color="#1C1B1F"
|
||||||
|
osd-border-style=outline-and-shadow
|
||||||
|
osd-playlist-entry=filename
|
||||||
|
osd-status-msg=${playback-time/full} / ${duration/full} (${percent-pos}%)\nframe: ${estimated-frame-number} / ${estimated-frame-count}
|
||||||
|
osd-fractions=yes
|
||||||
|
osd-duration=2000
|
||||||
|
|
||||||
|
# ===== 色彩管理 =====
|
||||||
|
|
||||||
|
icc-profile-auto # 自动检测系统 ICC 配置文件
|
||||||
|
icc-intent=0 # 感知度映射意图
|
||||||
|
icc-force-contrast=1000 # 解决校色文件对比度问题
|
||||||
|
icc-3dlut-size=128x128x128 # 3D LUT 精度
|
||||||
|
icc-cache-dir="~~/cache/icc_cache"
|
||||||
|
use-embedded-icc-profile=yes # 使用嵌入式 ICC 配置文件
|
||||||
|
inverse-tone-mapping=yes # SDR→HDR 反向映射(仅 HDR 显示器有效)
|
||||||
|
target-colorspace-hint=auto # 自动配置显示器输出色彩空间
|
||||||
|
hdr-contrast-recovery=0.30 # HDR 对比度恢复强度
|
||||||
|
hdr-compute-peak=yes # 动态峰值检测
|
||||||
|
|
||||||
|
# ===== 音频 =====
|
||||||
|
|
||||||
|
audio-device=auto
|
||||||
|
audio-channels=7.1,5.1,stereo # 按优先级回退,避免多声道下混丢声道
|
||||||
|
ao=alsa
|
||||||
|
audio-exclusive=yes # 独占音频通道
|
||||||
|
audio-samplerate=96000 # 重采样至 96kHz
|
||||||
|
audio-format=s32 # 32位有符号整数
|
||||||
|
replaygain=album # 专辑增益优先,无专辑增益时回退到曲目增益
|
||||||
|
gapless-audio=no # 避免采样率锁定导致音质下降
|
||||||
|
audio-file-auto=fuzzy # 自动加载同名外部音轨
|
||||||
|
audio-file-paths=audio;audios;audio 5.1;audios 5.1;audio 7.1;audios 7.1;音轨
|
||||||
|
alang=japanese,jpn,jap,ja,jp,english,eng,en
|
||||||
|
|
||||||
|
# ===== 字幕 =====
|
||||||
|
|
||||||
|
sub-codepage=gb18030 # 非 UTF-8 字幕首先尝试 GB18030
|
||||||
|
sub-auto=fuzzy # 自动加载同名外部字幕
|
||||||
|
sub-file-paths=sub;subs;subtitles;字幕
|
||||||
|
slang=chs,sc,zh-Hans,zh-CN,cht,tc,zh-Hant,zh-HK,zh-TW,chi,zho,zh
|
||||||
|
sub-font="Noto Sans CJK SC;Noto Color Emoji"
|
||||||
|
sub-font-size=50
|
||||||
|
sub-bold=yes
|
||||||
|
sub-color="#FFFFFF"
|
||||||
|
sub-outline-size=0.5
|
||||||
|
sub-outline-color="#000000"
|
||||||
|
sub-shadow-offset=0.5
|
||||||
|
sub-back-color="#000000"
|
||||||
|
|
||||||
|
# ===== 截图 =====
|
||||||
|
|
||||||
|
screenshot-format=webp
|
||||||
|
screenshot-webp-quality=85
|
||||||
|
screenshot-webp-compression=6
|
||||||
|
screenshot-tag-colorspace=no
|
||||||
|
screenshot-template="~~/files/screen/%{media-title}-%P-%n"
|
||||||
|
|
||||||
|
# ===== 脚本与着色器 =====
|
||||||
|
|
||||||
|
gpu-shader-cache-dir="~~/cache/shaders_cache"
|
||||||
|
osc=no # 禁用内置 OSC,使用 uosc
|
||||||
|
ytdl=yes
|
||||||
|
ytdl-format=bestvideo[height<=?2160][vcodec!=?vp9.2][vcodec!=?vp9]+bestaudio/best
|
||||||
|
ytdl-raw-options-append=cookies-from-browser=Firefox
|
||||||
|
input-default-bindings=no # 禁用内置键位,在 input.conf 中自定义
|
||||||
|
|
||||||
|
# ===== 外部配置 =====
|
||||||
|
|
||||||
|
include="~~/profiles.conf" # 场景预设(游戏/电影/动画等,手动激活)
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Profile 激活
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
# 色彩管理(ICC 和 Target 二选一)
|
||||||
|
#profile=ICC # ICC 色彩管理,使用系统 ICC 配置文件
|
||||||
|
#profile=ICC+ # ICC 色彩管理,使用自定义 ICC 文件
|
||||||
|
profile=Target # Target 色彩管理,直接指定目标色彩空间
|
||||||
|
|
||||||
|
profile=Dither # 抖动算法(fruit)
|
||||||
|
#profile=Dither+ # 抖动算法(error-diffusion,更耗能)
|
||||||
|
|
||||||
|
profile=Tscale # 时域插值(oversample)
|
||||||
|
#profile=Tscale+ # 时域插值(sphinx,更平滑但略模糊)
|
||||||
|
|
||||||
|
profile=HQ # 高质量缩放算法组合
|
||||||
|
profile=DeBand-low # 去色带(低强度)
|
||||||
|
#profile=DeBand-medium
|
||||||
|
#profile=DeBand-high
|
||||||
|
|
||||||
|
profile=HDR2SDR # HDR 映射 SDR 参数
|
||||||
|
#profile=SDR2HDR # SDR 反向映射 HDR(仅 HDR 显示器)
|
||||||
|
#profile=SWscaler # 软件缩放备选方案
|
||||||
|
|
||||||
|
# 着色器(选一)
|
||||||
|
#profile=NNEDI3 # 通用场景(NNEDI3-32)
|
||||||
|
profile=NNEDI3+ # 通用场景(NNEDI3-64,更耗能)
|
||||||
|
#profile=ravu-zoom # 通用场景(ravu-zoom)
|
||||||
|
#profile=FSRCNNX # HD 场景
|
||||||
|
#profile=FSRCNNX+ # SD 场景(FSRCNNX 去伪影)
|
||||||
|
#profile=Ani4K # 动画(Ani4K V2,高性能开销)
|
||||||
|
#profile=AniSD # SD 动画(AniSD,高性能开销)
|
||||||
|
#profile=Anime4K # 动画(Anime4K 修复 + 去伪影)
|
||||||
|
#profile=SSIM # 低性能消耗
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Profile 定义:常规
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
[ICC]
|
||||||
|
# 使用系统 ICC 配置文件进行色彩管理
|
||||||
|
icc-profile=""
|
||||||
|
icc-profile-auto
|
||||||
|
icc-intent=0
|
||||||
|
icc-force-contrast=1000
|
||||||
|
icc-3dlut-size=auto
|
||||||
|
icc-cache-dir="~~/cache/icc_cache"
|
||||||
|
|
||||||
|
[ICC+]
|
||||||
|
# 使用自定义 ICC 文件(如 color.org 泛 ICC 或专业校色文件)
|
||||||
|
icc-profile="~~/icc/ITU-RBT709ReferenceDisplay.icc"
|
||||||
|
icc-intent=0
|
||||||
|
icc-force-contrast=no
|
||||||
|
icc-3dlut-size=auto
|
||||||
|
icc-cache-dir="~~/cache/icc_cache"
|
||||||
|
|
||||||
|
[Target]
|
||||||
|
# 不使用 ICC,直接指定目标色彩空间参数
|
||||||
|
icc-profile=""
|
||||||
|
icc-profile-auto=no
|
||||||
|
target-prim=auto
|
||||||
|
target-contrast=auto
|
||||||
|
#target-trc=gamma2.2 # 非 OLED 显示器建议启用
|
||||||
|
#target-peak=203 # SDR 显示器默认 203 nit
|
||||||
|
|
||||||
|
[Dither]
|
||||||
|
dither-depth=auto
|
||||||
|
dither=fruit
|
||||||
|
dither-size-fruit=6
|
||||||
|
|
||||||
|
[Dither+]
|
||||||
|
dither-depth=auto
|
||||||
|
dither=error-diffusion
|
||||||
|
error-diffusion=floyd-steinberg
|
||||||
|
|
||||||
|
[Tscale]
|
||||||
|
video-sync=display-resample
|
||||||
|
interpolation
|
||||||
|
tscale=oversample
|
||||||
|
|
||||||
|
[Tscale+]
|
||||||
|
video-sync=display-resample
|
||||||
|
interpolation
|
||||||
|
tscale=sphinx
|
||||||
|
tscale-blur=0.65
|
||||||
|
|
||||||
|
[HQ]
|
||||||
|
scale=ewa_lanczossharp
|
||||||
|
cscale=bilinear
|
||||||
|
dscale=catmull_rom
|
||||||
|
scale-antiring=0.5
|
||||||
|
dscale-antiring=0.5
|
||||||
|
linear-upscaling=no
|
||||||
|
sigmoid-upscaling=yes
|
||||||
|
correct-downscaling=yes
|
||||||
|
linear-downscaling=no
|
||||||
|
deband=no
|
||||||
|
|
||||||
|
[DeBand-low]
|
||||||
|
deband-iterations=1
|
||||||
|
deband-threshold=48
|
||||||
|
deband-range=16
|
||||||
|
deband-grain=16
|
||||||
|
|
||||||
|
[DeBand-medium]
|
||||||
|
deband-iterations=2
|
||||||
|
deband-threshold=64
|
||||||
|
deband-range=16
|
||||||
|
deband-grain=24
|
||||||
|
|
||||||
|
[DeBand-high]
|
||||||
|
deband-iterations=3
|
||||||
|
deband-threshold=64
|
||||||
|
deband-range=16
|
||||||
|
deband-grain=24
|
||||||
|
|
||||||
|
[HDR2SDR]
|
||||||
|
tone-mapping=auto
|
||||||
|
gamut-mapping-mode=auto
|
||||||
|
hdr-contrast-recovery=0.30
|
||||||
|
hdr-compute-peak=auto
|
||||||
|
hdr-peak-percentile=99.995
|
||||||
|
hdr-peak-decay-rate=100
|
||||||
|
hdr-scene-threshold-low=5.5
|
||||||
|
hdr-scene-threshold-high=10
|
||||||
|
allow-delayed-peak-detect=no
|
||||||
|
|
||||||
|
[SDR2HDR]
|
||||||
|
# SDR 反向映射 HDR,仅 HDR 显示器有效
|
||||||
|
icc-profile=""
|
||||||
|
icc-profile-auto=no
|
||||||
|
target-prim=auto
|
||||||
|
target-trc=auto
|
||||||
|
target-peak=auto
|
||||||
|
target-colorspace-hint=auto
|
||||||
|
inverse-tone-mapping=yes
|
||||||
|
|
||||||
|
[SWscaler]
|
||||||
|
sws-allow-zimg=no
|
||||||
|
sws-scaler=bicublin
|
||||||
|
zimg-scaler-chroma=bicubic
|
||||||
|
zimg-scaler=bicubic
|
||||||
|
zimg-scaler-param-a=1/3
|
||||||
|
zimg-scaler-param-b=1/3
|
||||||
|
zimg-fast=no
|
||||||
|
|
||||||
|
# --- 着色器 Profile ---
|
||||||
|
|
||||||
|
[NNEDI3]
|
||||||
|
profile-desc=通用场景(NNEDI3-32 + 自适应锐化)
|
||||||
|
glsl-shaders-append="~~/shaders/nnedi3/nnedi3-nns32-win8x4.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/igv/adaptive-sharpen_luma.glsl"
|
||||||
|
|
||||||
|
[NNEDI3+]
|
||||||
|
profile-desc=通用场景(NNEDI3-64 + 自适应锐化)
|
||||||
|
glsl-shaders-append="~~/shaders/nnedi3/nnedi3-nns64-win8x4.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/igv/adaptive-sharpen_luma.glsl"
|
||||||
|
|
||||||
|
[ravu-zoom]
|
||||||
|
profile-desc=通用场景(ravu-zoom + 自适应锐化)
|
||||||
|
glsl-shaders-append="~~/shaders/ravu/ravu-zoom-ar-r3.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/igv/adaptive-sharpen_luma.glsl"
|
||||||
|
|
||||||
|
[FSRCNNX]
|
||||||
|
profile-desc=HD 场景(FSRCNNX + 自适应锐化)
|
||||||
|
glsl-shaders-append="~~/shaders/igv/FSRCNNX_x2_8-0-4-1.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/igv/adaptive-sharpen_luma.glsl"
|
||||||
|
|
||||||
|
[FSRCNNX+]
|
||||||
|
profile-desc=SD 场景(NNEDI3 + FSRCNNX 去伪影)
|
||||||
|
glsl-shaders-append="~~/shaders/nnedi3/nnedi3-nns32-win8x4.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/igv/FSRCNNX_x1_16-0-4-1_distort.glsl"
|
||||||
|
|
||||||
|
[Ani4K]
|
||||||
|
profile-desc=动画(Ani4K V2,高性能开销)
|
||||||
|
glsl-shaders-append="~~/shaders/Ani4K/Ani4Kv2_ArtCNN_C4F32_i2.glsl"
|
||||||
|
|
||||||
|
[AniSD]
|
||||||
|
profile-desc=SD 动画(AniSD,高性能开销)
|
||||||
|
glsl-shaders-append="~~/shaders/Ani4K/AniSD_ArtCNN_C4F32_i4.glsl"
|
||||||
|
|
||||||
|
[Anime4K]
|
||||||
|
profile-desc=动画(Anime4K 修复 + 去伪影)
|
||||||
|
glsl-shaders-append="~~/shaders/igv/KrigBilateral.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/Anime4K/glsl/Restore/Anime4K_Restore_CNN_Soft_M.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/Anime4K/glsl/Restore/Anime4K_Clamp_Highlights.glsl"
|
||||||
|
|
||||||
|
[SSIM]
|
||||||
|
profile-desc=低性能消耗(SSIM 超分 + 缩小)
|
||||||
|
glsl-shaders-append="~~/shaders/igv/SSimSuperRes.glsl"
|
||||||
|
glsl-shaders-append="~~/shaders/igv/SSimDownscaler.glsl"
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Profile 定义:条件触发
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
[SD]
|
||||||
|
# 低清源(≤720p)自动启用 FSRCNNX+ 增强
|
||||||
|
profile-cond=height <= 720 or width <= 720
|
||||||
|
profile-restore=copy
|
||||||
|
profile=FSRCNNX+
|
||||||
|
|
||||||
|
[Deband]
|
||||||
|
# YUV420P10 以下自动开启去色带
|
||||||
|
profile-cond=get("video-params/average-bpp") < 24
|
||||||
|
profile-restore=copy
|
||||||
|
deband=yes
|
||||||
|
|
||||||
|
[hdr-2390]
|
||||||
|
profile-cond=p.tone_mapping == "bt.2390" and p.current_vo == "gpu-next"
|
||||||
|
profile-restore=copy
|
||||||
|
tone-mapping-param=2.0
|
||||||
|
|
||||||
|
[peak-percentile]
|
||||||
|
profile-cond=get("video-params/avg-pq-y") < 0.21 and p.current_vo == "gpu-next"
|
||||||
|
profile-restore=copy
|
||||||
|
hdr-peak-percentile=99.8
|
||||||
|
|
||||||
|
[SDR-gamut]
|
||||||
|
# SDR 非 BT.709 内容的色域映射
|
||||||
|
profile-cond=get("video-params/primaries") ~= "bt.709" and get("video-params/gamma") == "bt.1886"
|
||||||
|
profile-restore=copy
|
||||||
|
gamut-mapping-mode=clip
|
||||||
|
|
||||||
|
[SDR-target]
|
||||||
|
# SDR 显示目标配置
|
||||||
|
profile-cond=not get("video-params/max-luma") and not get("inverse-tone-mapping") and p.current_vo == "gpu-next"
|
||||||
|
profile-restore=copy
|
||||||
|
target-colorspace-hint=no
|
||||||
|
|
||||||
|
[HDR]
|
||||||
|
# HDR 内容自动配置
|
||||||
|
profile-cond=get("video-params/max-luma") > 203
|
||||||
|
profile-restore=copy
|
||||||
|
target-peak=auto
|
||||||
|
target-colorspace-hint=auto
|
||||||
|
blend-subtitles=no
|
||||||
|
deband=no
|
||||||
|
vf=""
|
||||||
|
|
||||||
|
[HDR-PASS]
|
||||||
|
# HDR 直通(仅 HDR 显示模式下生效)
|
||||||
|
profile-cond=get("video-params/max-luma") > 203 and get("video-target-params/max-luma") > 203 and p.current_vo == "gpu-next"
|
||||||
|
profile-restore=copy
|
||||||
|
icc-profile=""
|
||||||
|
icc-profile-auto=no
|
||||||
|
target-colorspace-hint=auto
|
||||||
|
inverse-tone-mapping=no
|
||||||
|
target-prim=auto
|
||||||
|
target-trc=auto
|
||||||
|
target-peak=auto
|
||||||
|
target-contrast=auto
|
||||||
|
|
||||||
|
[video-sync]
|
||||||
|
# 音频调速超过阈值时切换同步模式,避免明显音高变化
|
||||||
|
profile-cond=(speed * audio_speed_correction) > 0 and ((speed * audio_speed_correction) < 0.96 or (speed * audio_speed_correction) > 1.04) and not (get("estimated-vf-fps") > 240 or get("display-fps") > 240)
|
||||||
|
profile-restore=copy-equal
|
||||||
|
video-sync=display-tempo
|
||||||
|
|
||||||
|
[fps-fix]
|
||||||
|
# 极高帧率/刷新率时使用 audio 同步避免异常
|
||||||
|
profile-cond=get("estimated-vf-fps") > 240 or get("display-fps") > 240
|
||||||
|
profile-restore=copy
|
||||||
|
video-sync=audio
|
||||||
|
|
||||||
|
[pgs-fix]
|
||||||
|
# 修复宽屏视频 PGS 字幕比例错位
|
||||||
|
profile-cond=get("video-params/aspect") > 16 / 9
|
||||||
|
profile-restore=copy
|
||||||
|
stretch-image-subs-to-screen=yes
|
||||||
|
|
||||||
|
[audio-filter]
|
||||||
|
# 多声道音频自动启用动态范围调节(适用于双声道设备)
|
||||||
|
profile-cond=get("audio-params/channel-count") > 2
|
||||||
|
profile-restore=copy-equal
|
||||||
|
af-pre=@dynaudnorm:lavfi=[dynaudnorm=f=500:g=31:p=0.5:m=5:r=0.9]
|
||||||
|
|
||||||
|
[pause]
|
||||||
|
# 暂停时取消置顶
|
||||||
|
profile-cond=pause
|
||||||
|
profile-restore=copy
|
||||||
|
ontop=no
|
||||||
|
|
||||||
|
[maximized]
|
||||||
|
# 最大化时禁止窗口自动调整大小
|
||||||
|
profile-cond=window_maximized and vid and not get("current-tracks/video/albumart")
|
||||||
|
profile-restore=copy-equal
|
||||||
|
auto-window-resize=no
|
||||||
|
|
||||||
|
[minimized]
|
||||||
|
# 最小化时自动暂停
|
||||||
|
profile-cond=window_minimized and vid and not get("current-tracks/video/albumart")
|
||||||
|
profile-restore=copy-equal
|
||||||
|
pause
|
||||||
|
|
||||||
|
[end]
|
||||||
|
# 播放列表结束后退出全屏
|
||||||
|
profile-cond=idle_active
|
||||||
|
no-fullscreen
|
||||||
|
no-window-maximized
|
||||||
|
|
||||||
|
[media-title]
|
||||||
|
# 网络/磁力链接显示媒体标题
|
||||||
|
profile-cond=path:find('://') ~= nil or path:find('^magnet:') ~= nil
|
||||||
|
profile-restore=copy
|
||||||
|
title=${?pause==yes:⏸}${?mute==yes:🔇}${?ontop==yes:📌}${media-title}
|
||||||
|
osd-playlist-entry=title
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Profile 定义:网络优化
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
[network-optimize]
|
||||||
|
# 网络播放时自动启用
|
||||||
|
profile-cond=demuxer_via_network
|
||||||
|
profile-restore=copy
|
||||||
|
cache=yes
|
||||||
|
cache-secs=600
|
||||||
|
demuxer-max-bytes=1GiB
|
||||||
|
demuxer-readahead-secs=120
|
||||||
|
network-timeout=60
|
||||||
|
stream-buffer-size=2MiB
|
||||||
|
prefetch-playlist=yes
|
||||||
|
stream-lavf-o-append=reconnect=1
|
||||||
|
stream-lavf-o-append=reconnect_at_eof=1
|
||||||
|
stream-lavf-o-append=reconnect_streamed=1
|
||||||
|
stream-lavf-o-append=multiple_requests=1
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
# 场景预设,手动激活:profile=game 等
|
||||||
|
|
||||||
|
[game]
|
||||||
|
profile-desc=低延迟,高性能
|
||||||
|
hwdec=auto-safe
|
||||||
|
video-sync=display-resample
|
||||||
|
interpolation=no
|
||||||
|
deband=no
|
||||||
|
video-latency-hacks=yes
|
||||||
|
opengl-pbo=yes
|
||||||
|
gpu-api=vulkan
|
||||||
|
gpu-context=wayland
|
||||||
|
cache=no
|
||||||
|
demuxer-max-bytes=8MiB
|
||||||
|
demuxer-max-back-bytes=4MiB
|
||||||
|
|
||||||
|
[movie]
|
||||||
|
profile-desc=高质量画质
|
||||||
|
hwdec=auto-copy-safe
|
||||||
|
video-sync=display-resample
|
||||||
|
interpolation=yes
|
||||||
|
deband=yes
|
||||||
|
deband-iterations=4
|
||||||
|
deband-threshold=35
|
||||||
|
deband-range=16
|
||||||
|
deband-grain=4
|
||||||
|
icc-profile-auto=yes
|
||||||
|
target-colorspace-hint=yes
|
||||||
|
hdr-compute-peak=yes
|
||||||
|
tone-mapping=hable
|
||||||
|
target-peak=100
|
||||||
|
|
||||||
|
[anime]
|
||||||
|
profile-desc=动画优化
|
||||||
|
hwdec=auto-safe
|
||||||
|
video-sync=display-resample
|
||||||
|
interpolation=yes
|
||||||
|
deband=yes
|
||||||
|
deband-iterations=3
|
||||||
|
deband-threshold=48
|
||||||
|
deband-range=16
|
||||||
|
deband-grain=6
|
||||||
|
icc-profile-auto=yes
|
||||||
|
glsl-shaders="~~/shaders/Anime4K/glsl/Restore/Anime4K_Restore_CNN_Soft_M.glsl;~~/shaders/Anime4K/glsl/Deblur/Anime4K_Deblur_DoG.glsl"
|
||||||
|
|
||||||
|
[lowpower]
|
||||||
|
profile-desc=省电模式
|
||||||
|
hwdec=no
|
||||||
|
video-sync=audio
|
||||||
|
interpolation=no
|
||||||
|
deband=no
|
||||||
|
icc-profile-auto=no
|
||||||
|
vd-lavc-threads=2
|
||||||
|
vd-lavc-skiploopfilter=all
|
||||||
|
vd-lavc-fast=yes
|
||||||
|
scale=bilinear
|
||||||
|
dscale=bilinear
|
||||||
|
cscale=bilinear
|
||||||
|
|
||||||
|
[stream]
|
||||||
|
profile-desc=网络流媒体优化
|
||||||
|
hwdec=auto-safe
|
||||||
|
video-sync=display-resample
|
||||||
|
cache=yes
|
||||||
|
cache-secs=300
|
||||||
|
demuxer-max-bytes=64MiB
|
||||||
|
demuxer-max-back-bytes=32MiB
|
||||||
|
network-timeout=60
|
||||||
|
ytdl=yes
|
||||||
|
ytdl-format=bestvideo[height<=?1080][vcodec!=?vp9]+bestaudio/best
|
||||||
|
|
||||||
|
[HDR-scene]
|
||||||
|
profile-desc=HDR 手动预设
|
||||||
|
hwdec=auto-copy-safe
|
||||||
|
target-colorspace-hint=yes
|
||||||
|
hdr-compute-peak=yes
|
||||||
|
tone-mapping=hable
|
||||||
|
target-peak=203
|
||||||
|
gamut-mapping-mode=relative
|
||||||
|
icc-profile-auto=yes
|
||||||
|
glsl-shaders="~~/shaders/other/PixelClipper.glsl"
|
||||||
|
|
||||||
|
[screenshot-hq]
|
||||||
|
profile-desc=高质量截图
|
||||||
|
screenshot-format=png
|
||||||
|
screenshot-png-compression=9
|
||||||
|
screenshot-png-filter=5
|
||||||
|
screenshot-tag-colorspace=yes
|
||||||
|
screenshot-high-bit-depth=yes
|
||||||
|
screenshot-sw=no
|
||||||
|
scale=lanczos
|
||||||
|
dscale=mitchell
|
||||||
|
cscale=mitchell
|
||||||
|
|
||||||
|
[live]
|
||||||
|
profile-desc=直播低延迟
|
||||||
|
hwdec=auto-safe
|
||||||
|
video-sync=display-resample
|
||||||
|
cache=no
|
||||||
|
demuxer-max-bytes=2MiB
|
||||||
|
demuxer-max-back-bytes=1MiB
|
||||||
|
video-latency-hacks=yes
|
||||||
|
opengl-pbo=yes
|
||||||
|
network-timeout=30
|
||||||
|
stream-lavf-o=reconnect_streamed=1,reconnect_delay_max=30
|
||||||
@@ -0,0 +1,932 @@
|
|||||||
|
-- https://github.com/Seme4eg/mpv-scripts/blob/master/script-modules/extended-menu.lua
|
||||||
|
|
||||||
|
local mp = require 'mp'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local assdraw = require 'mp.assdraw'
|
||||||
|
|
||||||
|
-- create namespace with default values
|
||||||
|
local em = {
|
||||||
|
|
||||||
|
-- customisable values ------------------------------------------------------
|
||||||
|
|
||||||
|
loop_when_navigating = false, -- Loop when navigating through list
|
||||||
|
lines_to_show = 17, -- NOT including search line
|
||||||
|
pause_on_open = true,
|
||||||
|
resume_on_exit = "only-if-was-paused", -- another possible value is true
|
||||||
|
|
||||||
|
-- styles (earlyer it was a table, but required many more steps to pass def-s
|
||||||
|
-- here from .conf file)
|
||||||
|
font_size = 21,
|
||||||
|
--font size scales by window
|
||||||
|
scale_by_window = false,
|
||||||
|
-- cursor 'width', useful to change if you have hidpi monitor
|
||||||
|
cursor_x_border = 0.3,
|
||||||
|
line_bottom_margin = 1, -- basically space between lines
|
||||||
|
text_color = {
|
||||||
|
default = 'ffffff',
|
||||||
|
accent = 'd8a07b',
|
||||||
|
current = 'aaaaaa',
|
||||||
|
comment = '636363',
|
||||||
|
},
|
||||||
|
menu_x_padding = 5, -- this padding for now applies only to 'left', not x
|
||||||
|
menu_y_padding = 2, -- but this one applies to both - top & bottom
|
||||||
|
|
||||||
|
|
||||||
|
-- values that should be passed from main script ----------------------------
|
||||||
|
|
||||||
|
search_heading = 'Default search heading',
|
||||||
|
-- 'full' is required from main script, 'current_i' is optional
|
||||||
|
-- others are 'private'
|
||||||
|
list = {
|
||||||
|
full = {}, filtered = {}, current_i = nil, pointer_i = 1, show_from_to = {}
|
||||||
|
},
|
||||||
|
-- field to compare with when searching for 'current value' by 'current_i'
|
||||||
|
index_field = 'index',
|
||||||
|
-- fields to use when searching for string match / any other custom searching
|
||||||
|
-- if value has 0 length, then search list item itself
|
||||||
|
filter_by_fields = {},
|
||||||
|
|
||||||
|
|
||||||
|
-- 'private' values that are not supposed to be changed from the outside ----
|
||||||
|
|
||||||
|
is_active = false,
|
||||||
|
-- https://mpv.io/manual/master/#lua-scripting-mp-create-osd-overlay(format)
|
||||||
|
ass = mp.create_osd_overlay("ass-events"),
|
||||||
|
was_paused = false, -- flag that indicates that vid was paused by this script
|
||||||
|
|
||||||
|
line = '',
|
||||||
|
-- if there was no cursor it wouldn't have been needed, but for now we need
|
||||||
|
-- variable below only to compare it with 'line' and see if we need to filter
|
||||||
|
prev_line = '',
|
||||||
|
cursor = 1,
|
||||||
|
history = {},
|
||||||
|
history_pos = 1,
|
||||||
|
key_bindings = {},
|
||||||
|
insert_mode = false,
|
||||||
|
|
||||||
|
-- used only in 'update' func to get error text msgs
|
||||||
|
error_codes = {
|
||||||
|
no_match = 'Match required',
|
||||||
|
no_submit_provided = 'No submit function provided'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- PRIVATE METHODS ------------------------------------------------------------
|
||||||
|
|
||||||
|
local ime_active = mp.get_property_native("input-ime")
|
||||||
|
|
||||||
|
-- declare constructor function
|
||||||
|
function em:new(o)
|
||||||
|
o = o or {}
|
||||||
|
setmetatable(o, self)
|
||||||
|
self.__index = self
|
||||||
|
|
||||||
|
-- some options might be customised by user in .conf file and read as strings
|
||||||
|
-- in that case parse those
|
||||||
|
if type(o.filter_by_fields) == 'string' then
|
||||||
|
o.filter_by_fields = utils.parse_json(o.filter_by_fields)
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(o.text_color) == 'string' then
|
||||||
|
o.text_color = utils.parse_json(o.text_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
-- this func is just a getter of a current list depending on search line
|
||||||
|
function em:current()
|
||||||
|
return self.line == '' and self.list.full or self.list.filtered
|
||||||
|
end
|
||||||
|
|
||||||
|
-- REVIEW: how to get rid of this wrapper and handle filter func sideeffects
|
||||||
|
-- in a more elegant way?
|
||||||
|
function em:filter_wrapper()
|
||||||
|
-- handles sideeffect that are needed to be run on filtering list
|
||||||
|
-- cuz the filter func may be redefined in main script and therefore needs
|
||||||
|
-- to be straight forward - only doing filtering and returning the table
|
||||||
|
|
||||||
|
-- passing current query just in case, so ppl can use it in their custom funcs
|
||||||
|
self.list.filtered = self:filter(self.line)
|
||||||
|
|
||||||
|
self.prev_line = self.line
|
||||||
|
self.list.pointer_i = 1
|
||||||
|
self:set_from_to(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
function em:set_from_to(reset_flag)
|
||||||
|
-- additional variables just for shorter var name
|
||||||
|
local i = self.list.pointer_i
|
||||||
|
local to_show = self.lines_to_show
|
||||||
|
local total = #self:current()
|
||||||
|
|
||||||
|
if reset_flag or to_show >= total then
|
||||||
|
self.list.show_from_to = { 1, math.min(to_show, total) }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If menu is opened with something already selected we want this 'selected'
|
||||||
|
-- to be displayed close to the middle of the menu. That's why 'show_from_to'
|
||||||
|
-- is not initially set, so we can know - if show_from_to length is 0 - it is
|
||||||
|
-- first call of this func in cur. init
|
||||||
|
if #self.list.show_from_to == 0 then
|
||||||
|
-- set show_from_to so chosen item will be displayed close to middle
|
||||||
|
local half_list = math.ceil(to_show / 2)
|
||||||
|
if i < half_list then
|
||||||
|
self.list.show_from_to = { 1, to_show }
|
||||||
|
elseif total - i < half_list then
|
||||||
|
self.list.show_from_to = { total - to_show + 1, total }
|
||||||
|
else
|
||||||
|
self.list.show_from_to = { i - half_list + 1, i - half_list + to_show }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.unpack = table.unpack or unpack -- 5.1 compatibility
|
||||||
|
local first, last = table.unpack(self.list.show_from_to)
|
||||||
|
|
||||||
|
-- handle cursor moving towards start / end bondary
|
||||||
|
if first ~= 1 and i - first < 2 then
|
||||||
|
self.list.show_from_to = { first - 1, last - 1 }
|
||||||
|
end
|
||||||
|
if last ~= total and last - i < 2 then
|
||||||
|
self.list.show_from_to = { first + 1, last + 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- handle index jumps from beginning to end and backwards
|
||||||
|
if i > last then
|
||||||
|
self.list.show_from_to = { i - to_show + 1, i }
|
||||||
|
end
|
||||||
|
if i < first then self.list.show_from_to = { 1, to_show } end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function em:change_selected_index(num)
|
||||||
|
self.list.pointer_i = self.list.pointer_i + num
|
||||||
|
if self.loop_when_navigating then
|
||||||
|
if self.list.pointer_i < 1 then
|
||||||
|
self.list.pointer_i = #self:current()
|
||||||
|
elseif self.list.pointer_i > #self:current() then
|
||||||
|
self.list.pointer_i = 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if self.list.pointer_i < 1 then
|
||||||
|
self.list.pointer_i = 1
|
||||||
|
elseif self.list.pointer_i > #self:current() then
|
||||||
|
self.list.pointer_i = #self:current()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:set_from_to()
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Render the REPL and console as an ASS OSD
|
||||||
|
function em:update(err_code)
|
||||||
|
-- ASS tags documentation here - https://aegi.vmoe.info/docs/3.0/ASS_Tags/
|
||||||
|
|
||||||
|
-- do not bother if function was called to close the menu..
|
||||||
|
if not self.is_active then
|
||||||
|
em.ass:remove()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local line_height = self.font_size + self.line_bottom_margin
|
||||||
|
local _, h, aspect = mp.get_osd_size()
|
||||||
|
local wh = self.scale_by_window and 720 or h
|
||||||
|
local ww = wh * aspect
|
||||||
|
|
||||||
|
-- '+ 1' below is a search string
|
||||||
|
local menu_y_pos =
|
||||||
|
wh - (line_height * (self.lines_to_show + 1) + self.menu_y_padding * 2)
|
||||||
|
|
||||||
|
-- didn't find better place to handle filtered list update
|
||||||
|
if self.line ~= self.prev_line then self:filter_wrapper() end
|
||||||
|
|
||||||
|
local function get_background()
|
||||||
|
local a = self:ass_new_wrapper()
|
||||||
|
a:append('{\\1c&H1c1c1c\\1a&H19}') -- background color & opacity
|
||||||
|
a:pos(0, 0)
|
||||||
|
a:draw_start()
|
||||||
|
a:rect_cw(0, menu_y_pos, ww, wh)
|
||||||
|
a:draw_stop()
|
||||||
|
return a.text
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_search_header()
|
||||||
|
local a = self:ass_new_wrapper()
|
||||||
|
|
||||||
|
a:pos(self.menu_x_padding, menu_y_pos + self.menu_y_padding)
|
||||||
|
|
||||||
|
local search_prefix = table.concat({
|
||||||
|
self:get_font_color('accent'),
|
||||||
|
(#self:current() ~= 0 and self.list.pointer_i or '!'),
|
||||||
|
'/', #self:current(), '\\h\\h', self.search_heading, ':\\h'
|
||||||
|
});
|
||||||
|
|
||||||
|
a:append(search_prefix)
|
||||||
|
-- reset font color after search prefix
|
||||||
|
a:append(self:get_font_color 'default')
|
||||||
|
|
||||||
|
-- Create the cursor glyph as an ASS drawing. ASS will draw the cursor
|
||||||
|
-- inline with the surrounding text, but it sets the advance to the width
|
||||||
|
-- of the drawing. So the cursor doesn't affect layout too much, make it as
|
||||||
|
-- thin as possible and make it appear to be 1px wide by giving it 0.5px
|
||||||
|
-- horizontal borders.
|
||||||
|
local cheight = self.font_size * 8
|
||||||
|
-- TODO: maybe do it using draw_rect from ass?
|
||||||
|
local cglyph = '{\\r' .. -- styles reset
|
||||||
|
'\\1c&Hffffff&\\3c&Hffffff' .. -- font color and border color
|
||||||
|
'\\xbord' .. self.cursor_x_border .. '\\p4\\pbo24}' .. -- xborder, scale x8 and baseline offset
|
||||||
|
'm 0 0 l 0 ' .. cheight .. -- drawing just a line
|
||||||
|
'{\\p0\\r}' -- finish drawing and reset styles
|
||||||
|
local before_cur = self:ass_escape(self.line:sub(1, self.cursor - 1))
|
||||||
|
local after_cur = self:ass_escape(self.line:sub(self.cursor))
|
||||||
|
|
||||||
|
a:append(table.concat({
|
||||||
|
before_cur, cglyph, self:reset_styles(),
|
||||||
|
self:get_font_color('default'), after_cur,
|
||||||
|
(err_code and '\\h' .. self.error_codes[err_code] or "")
|
||||||
|
}))
|
||||||
|
|
||||||
|
return a.text
|
||||||
|
|
||||||
|
-- NOTE: perhaps this commented code will some day help me in coding cursor
|
||||||
|
-- like in M-x emacs menu:
|
||||||
|
-- Redraw the cursor with the REPL text invisible. This will make the
|
||||||
|
-- cursor appear in front of the text.
|
||||||
|
-- ass:new_event()
|
||||||
|
-- ass:an(1)
|
||||||
|
-- ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
|
||||||
|
-- ass:append(cglyph)
|
||||||
|
-- ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_list()
|
||||||
|
local a = assdraw.ass_new()
|
||||||
|
|
||||||
|
local function apply_highlighting(y)
|
||||||
|
a:new_event()
|
||||||
|
a:append(self:reset_styles())
|
||||||
|
a:append('{\\1c&Hffffff\\1a&HE6}') -- background color & opacity
|
||||||
|
a:pos(0, 0)
|
||||||
|
a:draw_start()
|
||||||
|
a:rect_cw(0, y, ww, y + self.font_size)
|
||||||
|
a:draw_stop()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- REVIEW: maybe make another function 'get_line_str' and move there
|
||||||
|
-- everything from this for loop?
|
||||||
|
-- REVIEW: how to use something like table.unpack below?
|
||||||
|
for i = self.list.show_from_to[1], self.list.show_from_to[2] do
|
||||||
|
local value = assert(self:current()[i], 'no value with index ' .. i)
|
||||||
|
local y_offset = menu_y_pos + self.menu_y_padding +
|
||||||
|
(line_height * (i - self.list.show_from_to[1] + 1))
|
||||||
|
|
||||||
|
if i == self.list.pointer_i then apply_highlighting(y_offset) end
|
||||||
|
|
||||||
|
a:new_event()
|
||||||
|
a:append(self:reset_styles())
|
||||||
|
a:pos(self.menu_x_padding, y_offset)
|
||||||
|
a:append(self:get_line(i, value))
|
||||||
|
end
|
||||||
|
|
||||||
|
return a.text
|
||||||
|
end
|
||||||
|
|
||||||
|
em.ass.res_x = ww
|
||||||
|
em.ass.res_y = wh
|
||||||
|
em.ass.data = table.concat({
|
||||||
|
get_background(),
|
||||||
|
get_search_header(),
|
||||||
|
get_list()
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
em.ass:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- params:
|
||||||
|
-- - data : {list: {}, [current_i] : num}
|
||||||
|
function em:init(data)
|
||||||
|
self.list.full = data.list or {}
|
||||||
|
self.list.current_i = data.current_i or nil
|
||||||
|
self.list.pointer_i = data.current_i or 1
|
||||||
|
self:set_active(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
function em:exit()
|
||||||
|
self:undefine_key_bindings()
|
||||||
|
collectgarbage()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: write some idle func like this
|
||||||
|
-- function idle()
|
||||||
|
-- if pending_selection then
|
||||||
|
-- gallery:set_selection(pending_selection)
|
||||||
|
-- pending_selection = nil
|
||||||
|
-- end
|
||||||
|
-- if ass_changed or geometry_changed then
|
||||||
|
-- local ww, wh = mp.get_osd_size()
|
||||||
|
-- if geometry_changed then
|
||||||
|
-- geometry_changed = false
|
||||||
|
-- compute_geometry(ww, wh)
|
||||||
|
-- end
|
||||||
|
-- if ass_changed then
|
||||||
|
-- ass_changed = false
|
||||||
|
-- mp.set_osd_ass(ww, wh, ass)
|
||||||
|
-- end
|
||||||
|
-- end
|
||||||
|
-- end
|
||||||
|
-- ...
|
||||||
|
-- and handle it as follows
|
||||||
|
-- init():
|
||||||
|
-- mp.register_idle(idle)
|
||||||
|
-- idle()
|
||||||
|
-- exit():
|
||||||
|
-- mp.unregister_idle(idle)
|
||||||
|
-- idle()
|
||||||
|
-- And in these observers he is setting a flag, that's being checked in func above
|
||||||
|
-- mp.observe_property("osd-width", "native", mark_geometry_stale)
|
||||||
|
-- mp.observe_property("osd-height", "native", mark_geometry_stale)
|
||||||
|
|
||||||
|
-- PRIVATE METHODS END --------------------------------------------------------
|
||||||
|
|
||||||
|
-- PUBLIC METHODS -------------------------------------------------------------
|
||||||
|
|
||||||
|
function em:filter()
|
||||||
|
-- default filter func, might be redefined in main script
|
||||||
|
local result = {}
|
||||||
|
|
||||||
|
local function get_full_search_str(v)
|
||||||
|
local str = ''
|
||||||
|
for _, key in ipairs(self.filter_by_fields) do str = str .. (v[key] or '') end
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, v in ipairs(self.list.full) do
|
||||||
|
-- if filter_by_fields has 0 length, then search list item itself
|
||||||
|
if #self.filter_by_fields == 0 then
|
||||||
|
if self:search_method(v) then table.insert(result, v) end
|
||||||
|
else
|
||||||
|
-- NOTE: we might use search_method on fiels separately like this:
|
||||||
|
-- for _,key in ipairs(self.filter_by_fields) do
|
||||||
|
-- if self:search_method(v[key]) then table.insert(result, v) end
|
||||||
|
-- end
|
||||||
|
-- But since im planning to implement fuzzy search in future i need full
|
||||||
|
-- search string here
|
||||||
|
if self:search_method(get_full_search_str(v)) then
|
||||||
|
table.insert(result, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: implement fuzzy search and maybe match highlights
|
||||||
|
function em:search_method(str)
|
||||||
|
-- also might be redefined by main script
|
||||||
|
|
||||||
|
-- convert to string just to make sure..
|
||||||
|
return tostring(str):lower():find(self.line:lower(), 1, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- this module requires submit function to be defined in main script
|
||||||
|
function em:submit() self:update('no_submit_provided') end
|
||||||
|
|
||||||
|
function em:update_list(list)
|
||||||
|
-- for now this func doesn't handle cases when we have 'current_i' to update
|
||||||
|
-- it
|
||||||
|
self.list.full = list
|
||||||
|
if self.line ~= self.prev_line then self:filter_wrapper() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- PUBLIC METHODS END ---------------------------------------------------------
|
||||||
|
|
||||||
|
-- HELPER METHODS -------------------------------------------------------------
|
||||||
|
|
||||||
|
function em:get_line(_, v) -- [i]ndex, [v]alue
|
||||||
|
-- this func might be redefined in main script to get a custom-formatted line
|
||||||
|
-- default implementation of this func supposes that value.content field is a
|
||||||
|
-- String
|
||||||
|
local a = assdraw.ass_new()
|
||||||
|
local style = (self.list.current_i == v[self.index_field])
|
||||||
|
and 'current' or 'default'
|
||||||
|
|
||||||
|
a:append(self:reset_styles())
|
||||||
|
a:append(self:get_font_color(style))
|
||||||
|
-- content as default field, which is holding string
|
||||||
|
-- no point in moving it to main object since content itself is being
|
||||||
|
-- composed in THIS function, that might (and most likely, should) be
|
||||||
|
-- redefined in main script
|
||||||
|
a:append(v.content or 'Something is off in `get_line` func')
|
||||||
|
return a.text
|
||||||
|
end
|
||||||
|
|
||||||
|
-- REVIEW: for now i don't see normal way of mergin this func with below one
|
||||||
|
-- but it's being used only once
|
||||||
|
function em:reset_styles()
|
||||||
|
local a = assdraw.ass_new()
|
||||||
|
-- alignment top left, no word wrapping, border 0, shadow 0
|
||||||
|
a:append('{\\an7\\q2\\bord0\\shad0}')
|
||||||
|
a:append('{\\fs' .. self.font_size .. '}')
|
||||||
|
return a.text
|
||||||
|
end
|
||||||
|
|
||||||
|
-- function to get rid of some copypaste
|
||||||
|
function em:ass_new_wrapper()
|
||||||
|
local a = assdraw.ass_new()
|
||||||
|
a:new_event()
|
||||||
|
a:append(self:reset_styles())
|
||||||
|
return a
|
||||||
|
end
|
||||||
|
|
||||||
|
function em:get_font_color(style)
|
||||||
|
return '{\\1c&H' .. self.text_color[style] .. '}'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HELPER METHODS END ---------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The below code is a modified implementation of text input from mpv's console.lua:
|
||||||
|
https://github.com/mpv-player/mpv/blob/87c9eefb2928252497f6141e847b74ad1158bc61/player/lua/console.lua
|
||||||
|
|
||||||
|
I was too lazy to list all modifications i've done to the script, but if u
|
||||||
|
rly need to see those - do diff with the original code
|
||||||
|
]]
|
||||||
|
--
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- START ORIGINAL MPV CODE --
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Copyright (C) 2019 the mpv developers
|
||||||
|
--
|
||||||
|
-- Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
-- purpose with or without fee is hereby granted, provided that the above
|
||||||
|
-- copyright notice and this permission notice appear in all copies.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
function em:detect_platform()
|
||||||
|
local o = {}
|
||||||
|
-- Kind of a dumb way of detecting the platform but whatever
|
||||||
|
if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
|
||||||
|
return 'windows'
|
||||||
|
elseif mp.get_property_native('options/macos-force-dedicated-gpu', o) ~= o then
|
||||||
|
return 'macos'
|
||||||
|
elseif os.getenv('WAYLAND_DISPLAY') then
|
||||||
|
return 'wayland'
|
||||||
|
end
|
||||||
|
return 'x11'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Escape a string for verbatim display on the OSD
|
||||||
|
function em:ass_escape(str)
|
||||||
|
-- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
|
||||||
|
-- it isn't followed by a recognised character, so add a zero-width
|
||||||
|
-- non-breaking space
|
||||||
|
str = str:gsub('\\', '\\\239\187\191')
|
||||||
|
str = str:gsub('{', '\\{')
|
||||||
|
str = str:gsub('}', '\\}')
|
||||||
|
-- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
|
||||||
|
-- consecutive newlines
|
||||||
|
str = str:gsub('\n', '\239\187\191\\N')
|
||||||
|
-- Turn leading spaces into hard spaces to prevent ASS from stripping them
|
||||||
|
str = str:gsub('\\N ', '\\N\\h')
|
||||||
|
str = str:gsub('^ ', '\\h')
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set the REPL visibility ("enable", Esc)
|
||||||
|
function em:set_active(active)
|
||||||
|
if active == self.is_active then return end
|
||||||
|
if active then
|
||||||
|
if ime_active == false then
|
||||||
|
mp.set_property_bool("input-ime", true)
|
||||||
|
end
|
||||||
|
self.is_active = true
|
||||||
|
self.insert_mode = false
|
||||||
|
mp.enable_messages('terminal-default')
|
||||||
|
self:define_key_bindings()
|
||||||
|
|
||||||
|
-- set flag 'was_paused' only if vid wasn't paused before EM init
|
||||||
|
if self.pause_on_open and not mp.get_property_bool("pause", false) then
|
||||||
|
mp.set_property_bool("pause", true)
|
||||||
|
self.was_paused = true
|
||||||
|
end
|
||||||
|
|
||||||
|
self:set_from_to()
|
||||||
|
self:update()
|
||||||
|
else
|
||||||
|
-- no need to call 'update' in this block cuz 'clear' method is calling it
|
||||||
|
if ime_active == false then
|
||||||
|
mp.set_property_bool("input-ime", false)
|
||||||
|
end
|
||||||
|
self.is_active = false
|
||||||
|
self:undefine_key_bindings()
|
||||||
|
|
||||||
|
if self.resume_on_exit == true or
|
||||||
|
(self.resume_on_exit == "only-if-was-paused" and self.was_paused) then
|
||||||
|
mp.set_property_bool("pause", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:clear()
|
||||||
|
collectgarbage()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
|
||||||
|
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
|
||||||
|
function em:next_utf8(str, pos)
|
||||||
|
if pos > str:len() then return pos end
|
||||||
|
repeat
|
||||||
|
pos = pos + 1
|
||||||
|
until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
|
||||||
|
return pos
|
||||||
|
end
|
||||||
|
|
||||||
|
-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos'
|
||||||
|
function em:prev_utf8(str, pos)
|
||||||
|
if pos <= 1 then return pos end
|
||||||
|
repeat
|
||||||
|
pos = pos - 1
|
||||||
|
until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
|
||||||
|
return pos
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Insert a character at the current cursor position (any_unicode)
|
||||||
|
function em:handle_char_input(c)
|
||||||
|
if self.insert_mode then
|
||||||
|
self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self:next_utf8(self.line, self.cursor))
|
||||||
|
else
|
||||||
|
self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self.cursor)
|
||||||
|
end
|
||||||
|
self.cursor = self.cursor + #c
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove the character behind the cursor (Backspace)
|
||||||
|
function em:handle_backspace()
|
||||||
|
if self.cursor <= 1 then return end
|
||||||
|
local prev = self:prev_utf8(self.line, self.cursor)
|
||||||
|
self.line = self.line:sub(1, prev - 1) .. self.line:sub(self.cursor)
|
||||||
|
self.cursor = prev
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove the character in front of the cursor (Del)
|
||||||
|
function em:handle_del()
|
||||||
|
if self.cursor > self.line:len() then return end
|
||||||
|
self.line = self.line:sub(1, self.cursor - 1) .. self.line:sub(self:next_utf8(self.line, self.cursor))
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Toggle insert mode (Ins)
|
||||||
|
function em:handle_ins()
|
||||||
|
self.insert_mode = not self.insert_mode
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move the cursor to the next character (Right)
|
||||||
|
function em:next_char()
|
||||||
|
self.cursor = self:next_utf8(self.line, self.cursor)
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move the cursor to the previous character (Left)
|
||||||
|
function em:prev_char()
|
||||||
|
self.cursor = self:prev_utf8(self.line, self.cursor)
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear the current line (Ctrl+C)
|
||||||
|
function em:clear()
|
||||||
|
self.line = ''
|
||||||
|
self.prev_line = ''
|
||||||
|
|
||||||
|
self.list.current_i = nil
|
||||||
|
self.list.pointer_i = 1
|
||||||
|
self.list.filtered = {}
|
||||||
|
self.list.show_from_to = {}
|
||||||
|
|
||||||
|
self.was_paused = false
|
||||||
|
|
||||||
|
self.cursor = 1
|
||||||
|
self.insert_mode = false
|
||||||
|
self.history_pos = #self.history + 1
|
||||||
|
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run the current command and clear the line (Enter)
|
||||||
|
function em:handle_enter()
|
||||||
|
if #self:current() == 0 then
|
||||||
|
self:update('no_match')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.history[#self.history] ~= self.line then
|
||||||
|
self.history[#self.history + 1] = self.line
|
||||||
|
end
|
||||||
|
|
||||||
|
self:submit(self:current()[self.list.pointer_i])
|
||||||
|
self:set_active(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Go to the specified position in the command history
|
||||||
|
function em:go_history(new_pos)
|
||||||
|
local old_pos = self.history_pos
|
||||||
|
self.history_pos = new_pos
|
||||||
|
|
||||||
|
-- Restrict the position to a legal value
|
||||||
|
if self.history_pos > #self.history + 1 then
|
||||||
|
self.history_pos = #self.history + 1
|
||||||
|
elseif self.history_pos < 1 then
|
||||||
|
self.history_pos = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Do nothing if the history position didn't actually change
|
||||||
|
if self.history_pos == old_pos then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If the user was editing a non-history line, save it as the last history
|
||||||
|
-- entry. This makes it much less frustrating to accidentally hit Up/Down
|
||||||
|
-- while editing a line.
|
||||||
|
if old_pos == #self.history + 1 and self.line ~= '' and self.history[#self.history] ~= self.line then
|
||||||
|
self.history[#self.history + 1] = self.line
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Now show the history line (or a blank line for #history + 1)
|
||||||
|
if self.history_pos <= #self.history then
|
||||||
|
self.line = self.history[self.history_pos]
|
||||||
|
else
|
||||||
|
self.line = ''
|
||||||
|
end
|
||||||
|
self.cursor = self.line:len() + 1
|
||||||
|
self.insert_mode = false
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Go to the specified relative position in the command history (Up, Down)
|
||||||
|
function em:move_history(amount)
|
||||||
|
self:go_history(self.history_pos + amount)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Go to the first command in the command history (PgUp)
|
||||||
|
function em:handle_pgup()
|
||||||
|
-- Determine the number of items to move up (half a page)
|
||||||
|
local half_page = math.ceil(self.lines_to_show / 2)
|
||||||
|
|
||||||
|
-- Move the history position up by half a page
|
||||||
|
self:change_selected_index(-half_page)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stop browsing history and start editing a blank line (PgDown)
|
||||||
|
function em:handle_pgdown()
|
||||||
|
-- Determine the number of items to move down (half a page)
|
||||||
|
local half_page = math.ceil(self.lines_to_show / 2)
|
||||||
|
|
||||||
|
-- Move the history position down by half a page
|
||||||
|
self:change_selected_index(half_page)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move to the start of the current word, or if already at the start, the start
|
||||||
|
-- of the previous word. (Ctrl+Left)
|
||||||
|
function em:prev_word()
|
||||||
|
-- This is basically the same as next_word() but backwards, so reverse the
|
||||||
|
-- string in order to do a "backwards" find. This wouldn't be as annoying
|
||||||
|
-- to do if Lua didn't insist on 1-based indexing.
|
||||||
|
self.cursor = self.line:len() - select(2, self.line:reverse():find('%s*[^%s]*', self.line:len() - self.cursor + 2)) + 1
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move to the end of the current word, or if already at the end, the end of
|
||||||
|
-- the next word. (Ctrl+Right)
|
||||||
|
function em:next_word()
|
||||||
|
self.cursor = select(2, self.line:find('%s*[^%s]*', self.cursor)) + 1
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move the cursor to the beginning of the line (HOME)
|
||||||
|
function em:go_home()
|
||||||
|
self.cursor = 1
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move the cursor to the end of the line (END)
|
||||||
|
function em:go_end()
|
||||||
|
self.cursor = self.line:len() + 1
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Delete from the cursor to the beginning of the word (Ctrl+Backspace)
|
||||||
|
function em:del_word()
|
||||||
|
local before_cur = self.line:sub(1, self.cursor - 1)
|
||||||
|
local after_cur = self.line:sub(self.cursor)
|
||||||
|
|
||||||
|
before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
|
||||||
|
self.line = before_cur .. after_cur
|
||||||
|
self.cursor = before_cur:len() + 1
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Delete from the cursor to the end of the word (Ctrl+Del)
|
||||||
|
function em:del_next_word()
|
||||||
|
if self.cursor > self.line:len() then return end
|
||||||
|
|
||||||
|
local before_cur = self.line:sub(1, self.cursor - 1)
|
||||||
|
local after_cur = self.line:sub(self.cursor)
|
||||||
|
|
||||||
|
after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
|
||||||
|
self.line = before_cur .. after_cur
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Delete from the cursor to the end of the line (Ctrl+K)
|
||||||
|
function em:del_to_eol()
|
||||||
|
self.line = self.line:sub(1, self.cursor - 1)
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Delete from the cursor back to the start of the line (Ctrl+U)
|
||||||
|
function em:del_to_start()
|
||||||
|
self.line = self.line:sub(self.cursor)
|
||||||
|
self.cursor = 1
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
|
||||||
|
function em:get_clipboard(clip)
|
||||||
|
-- Pick a better default font for Windows and macOS
|
||||||
|
local platform = self:detect_platform()
|
||||||
|
|
||||||
|
if platform == 'x11' then
|
||||||
|
local res = utils.subprocess({
|
||||||
|
args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' },
|
||||||
|
playback_only = false,
|
||||||
|
})
|
||||||
|
if not res.error then
|
||||||
|
return res.stdout
|
||||||
|
end
|
||||||
|
elseif platform == 'wayland' then
|
||||||
|
local res = utils.subprocess({
|
||||||
|
args = { 'wl-paste', clip and '-n' or '-np' },
|
||||||
|
playback_only = false,
|
||||||
|
})
|
||||||
|
if not res.error then
|
||||||
|
return res.stdout
|
||||||
|
end
|
||||||
|
elseif platform == 'windows' then
|
||||||
|
local res = utils.subprocess({
|
||||||
|
args = { 'powershell', '-NoProfile', '-Command', [[& {
|
||||||
|
Trap {
|
||||||
|
Write-Error -ErrorRecord $_
|
||||||
|
Exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$clip = ""
|
||||||
|
if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
|
||||||
|
$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
|
||||||
|
} else {
|
||||||
|
Add-Type -AssemblyName PresentationCore
|
||||||
|
$clip = [Windows.Clipboard]::GetText()
|
||||||
|
}
|
||||||
|
|
||||||
|
$clip = $clip -Replace "`r",""
|
||||||
|
$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
|
||||||
|
[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
|
||||||
|
}]] },
|
||||||
|
playback_only = false,
|
||||||
|
})
|
||||||
|
if not res.error then
|
||||||
|
return res.stdout
|
||||||
|
end
|
||||||
|
elseif platform == 'macos' then
|
||||||
|
local res = utils.subprocess({
|
||||||
|
args = { 'pbpaste' },
|
||||||
|
playback_only = false,
|
||||||
|
})
|
||||||
|
if not res.error then
|
||||||
|
return res.stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Paste text from the window-system's clipboard. 'clip' determines whether the
|
||||||
|
-- clipboard or the primary selection buffer is used (on X11 and Wayland only.)
|
||||||
|
function em:paste(clip)
|
||||||
|
local text = self:get_clipboard(clip)
|
||||||
|
local before_cur = self.line:sub(1, self.cursor - 1)
|
||||||
|
local after_cur = self.line:sub(self.cursor)
|
||||||
|
self.line = before_cur .. text .. after_cur
|
||||||
|
self.cursor = self.cursor + text:len()
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- List of input bindings. This is a weird mashup between common GUI text-input
|
||||||
|
-- bindings and readline bindings.
|
||||||
|
function em:get_bindings()
|
||||||
|
local bindings = {
|
||||||
|
{ 'ctrl+[', function() self:set_active(false) end },
|
||||||
|
{ 'ctrl+g', function() self:set_active(false) end },
|
||||||
|
{ 'esc', function() self:set_active(false) end },
|
||||||
|
{ 'enter', function() self:handle_enter() end },
|
||||||
|
{ 'kp_enter', function() self:handle_enter() end },
|
||||||
|
{ 'ctrl+m', function() self:handle_enter() end },
|
||||||
|
{ 'bs', function() self:handle_backspace() end },
|
||||||
|
{ 'shift+bs', function() self:handle_backspace() end },
|
||||||
|
{ 'ctrl+h', function() self:handle_backspace() end },
|
||||||
|
{ 'del', function() self:handle_del() end },
|
||||||
|
{ 'shift+del', function() self:handle_del() end },
|
||||||
|
{ 'ins', function() self:handle_ins() end },
|
||||||
|
{ 'shift+ins', function() self:paste(false) end },
|
||||||
|
{ 'mbtn_mid', function() self:paste(false) end },
|
||||||
|
{ 'left', function() self:prev_char() end },
|
||||||
|
{ 'ctrl+b', function() self:prev_char() end },
|
||||||
|
{ 'right', function() self:next_char() end },
|
||||||
|
{ 'ctrl+f', function() self:next_char() end },
|
||||||
|
{ 'ctrl+k', function() self:change_selected_index(-1) end },
|
||||||
|
{ 'ctrl+p', function() self:change_selected_index(-1) end },
|
||||||
|
{ 'ctrl+j', function() self:change_selected_index(1) end },
|
||||||
|
{ 'ctrl+n', function() self:change_selected_index(1) end },
|
||||||
|
{ 'up', function() self:move_history(-1) end },
|
||||||
|
{ 'alt+p', function() self:move_history(-1) end },
|
||||||
|
{ 'wheel_up', function() self:move_history(-1) end },
|
||||||
|
{ 'down', function() self:move_history(1) end },
|
||||||
|
{ 'alt+n', function() self:move_history(1) end },
|
||||||
|
{ 'wheel_down', function() self:move_history(1) end },
|
||||||
|
{ 'wheel_left', function() end },
|
||||||
|
{ 'wheel_right', function() end },
|
||||||
|
{ 'ctrl+left', function() self:prev_word() end },
|
||||||
|
{ 'alt+b', function() self:prev_word() end },
|
||||||
|
{ 'ctrl+right', function() self:next_word() end },
|
||||||
|
{ 'alt+f', function() self:next_word() end },
|
||||||
|
{ 'ctrl+a', function() self:go_home() end },
|
||||||
|
{ 'home', function() self:go_home() end },
|
||||||
|
{ 'ctrl+e', function() self:go_end() end },
|
||||||
|
{ 'end', function() self:go_end() end },
|
||||||
|
{ 'ctrl+shift+f', function() self:handle_pgdown() end },
|
||||||
|
{ 'ctrl+shift+b', function() self:handle_pgup() end },
|
||||||
|
{ 'pgdwn', function() self:handle_pgdown() end },
|
||||||
|
{ 'pgup', function() self:handle_pgup() end },
|
||||||
|
{ 'ctrl+c', function() self:clear() end },
|
||||||
|
{ 'ctrl+d', function() self:handle_del() end },
|
||||||
|
{ 'ctrl+u', function() self:del_to_start() end },
|
||||||
|
{ 'ctrl+v', function() self:paste(true) end },
|
||||||
|
{ 'meta+v', function() self:paste(true) end },
|
||||||
|
{ 'ctrl+bs', function() self:del_word() end },
|
||||||
|
{ 'ctrl+w', function() self:del_word() end },
|
||||||
|
{ 'ctrl+del', function() self:del_next_word() end },
|
||||||
|
{ 'alt+d', function() self:del_next_word() end },
|
||||||
|
{ 'kp_dec', function() self:handle_char_input('.') end },
|
||||||
|
}
|
||||||
|
|
||||||
|
for i = 0, 9 do
|
||||||
|
bindings[#bindings + 1] =
|
||||||
|
{ 'kp' .. i, function() self:handle_char_input('' .. i) end }
|
||||||
|
end
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
end
|
||||||
|
|
||||||
|
function em:text_input(info)
|
||||||
|
if info.key_text and (info.event == "press" or info.event == "down"
|
||||||
|
or info.event == "repeat")
|
||||||
|
then
|
||||||
|
self:handle_char_input(info.key_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function em:define_key_bindings()
|
||||||
|
if #self.key_bindings > 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, bind in ipairs(self:get_bindings()) do
|
||||||
|
-- Generate arbitrary name for removing the bindings later.
|
||||||
|
local name = "search_" .. (#self.key_bindings + 1)
|
||||||
|
self.key_bindings[#self.key_bindings + 1] = name
|
||||||
|
mp.add_forced_key_binding(bind[1], name, bind[2], { repeatable = true })
|
||||||
|
end
|
||||||
|
mp.add_forced_key_binding("any_unicode", "search_input", function(...)
|
||||||
|
self:text_input(...)
|
||||||
|
end, { repeatable = true, complex = true })
|
||||||
|
self.key_bindings[#self.key_bindings + 1] = "search_input"
|
||||||
|
end
|
||||||
|
|
||||||
|
function em:undefine_key_bindings()
|
||||||
|
for _, name in ipairs(self.key_bindings) do
|
||||||
|
mp.remove_key_binding(name)
|
||||||
|
end
|
||||||
|
self.key_bindings = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- END ORIGINAL MPV CODE --
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
return em
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
### 该文件夹下存放mpv脚本的对应设置文件
|
||||||
|
|
||||||
|
通常脚本设置文件名与所属脚本文件同名,注意脚本文件名中的`-`默认需转译成`_`。实际以脚本开发者设定为准。
|
||||||
|
|
||||||
|
脚本设置文件切勿美化格式(例如加入无意义的空格);切勿在参数后注释(应单独另起一行写注释)。
|
||||||
|
|
||||||
|
脚本及其设置文件可能不支持windows的CRLF换行(尝试更改为LF)。
|
||||||
|
|
||||||
|
以上所述情况在自行修改的过程中都可能导致脚本设置文件(部分)失效。
|
||||||
|
|
||||||
|
以下为mpv内置脚本所使用的设置文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
console.conf
|
||||||
|
osc.conf
|
||||||
|
stats.conf
|
||||||
|
ytdl_hook.conf
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# 设置自动保存文件播放进度及状态的时间间隔,单位为秒。默认值:60 秒
|
||||||
|
save_interval=60
|
||||||
|
# 设置文件播放进度的百分比,满足时自动删除文件播放进度及状态。默认值:99
|
||||||
|
percent_pos=99
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
###不支持参数后注释,须另起一行
|
||||||
|
|
||||||
|
##禁用所有自动加载相关功能,默认:no
|
||||||
|
#disabled=yes
|
||||||
|
|
||||||
|
##是否自动加载当前目录(不含子目录)所有图片到播放列表,默认:yes
|
||||||
|
#images=no
|
||||||
|
|
||||||
|
##是否自动加载当前目录(不含子目录)所有视频到播放列表,默认:yes
|
||||||
|
#videos=no
|
||||||
|
|
||||||
|
##是否自动加载当前目录(不含子目录)所有音频到播放列表,默认:yes
|
||||||
|
#audio=no
|
||||||
|
|
||||||
|
##指定额外需要用于加载的图片、视频和音频扩展白名单
|
||||||
|
additional_image_exts=jfif
|
||||||
|
additional_video_exts=asf,f4v,rm,ts,vob
|
||||||
|
additional_audio_exts=dsf,spx
|
||||||
|
|
||||||
|
##<auto|recursive|lazy|默认 ignore> 打开文件时,选择递归、懒惰或忽略全部子目录
|
||||||
|
directory_mode=ignore
|
||||||
|
|
||||||
|
##是否掠过隐藏文件,默认:yes
|
||||||
|
#ignore_hidden=no
|
||||||
|
|
||||||
|
##指定需要略过的文件名模式,多个模式之间用逗号分隔。默认值为空
|
||||||
|
##支持 lua 模式写法,可使用 % 转义 ,
|
||||||
|
#ignore_patterns=^~,^bak-,%.bak$
|
||||||
|
|
||||||
|
#是否只自动载入相同类型的文件(视频、音频、图片),默认:no
|
||||||
|
same_type=yes
|
||||||
|
|
||||||
|
#是否只自动载入相似系列的文件,默认:no
|
||||||
|
#same_series=yes
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 手动指定可执行文件的绝对路径,如果以下程序不存在于环境变量中
|
||||||
|
|
||||||
|
# 1. ffmpeg
|
||||||
|
#ffmpeg_path=C:/Program Files/ffmpeg/bin/ffmpeg.exe
|
||||||
|
ffmpeg_path=ffmpeg
|
||||||
|
|
||||||
|
# 2. ffsubsync
|
||||||
|
#ffsubsync_path=C:/Program Files/ffsubsync/ffsubsync.exe
|
||||||
|
#ffsubsync_path=/home/user/.local/bin/ffsubsync
|
||||||
|
|
||||||
|
# 3. alass
|
||||||
|
#alass_path=C:/Program Files/ffmpeg/bin/alass.exe
|
||||||
|
#alass_path=/usr/bin/alass
|
||||||
|
##⇘⇘以下路径设置为在 mpv 程序所在的根目录下查找指定程序
|
||||||
|
alass_path=alass
|
||||||
|
|
||||||
|
# 首选的字幕同步工具。允许选项:'ffsubsync','alass','ask'.
|
||||||
|
# 如果设置为“ask”,脚本每次都会要求选择工具:
|
||||||
|
|
||||||
|
# 1. 用于与音频同步的首选工具。('ffsubsync','alass','ask')
|
||||||
|
audio_subsync_tool=alass
|
||||||
|
|
||||||
|
# 2. 用于与字幕同步的首选工具。('ffsubsync','alass','ask')
|
||||||
|
altsub_subsync_tool=alass
|
||||||
|
|
||||||
|
# 禁用原字幕 (yes,no)
|
||||||
|
# 尝试字幕同步操作完成后,告诉 mpv 忽略原来的字幕轨道
|
||||||
|
unload_old_sub=no
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 黑名单或白名单只需设置其中一种
|
||||||
|
|
||||||
|
# 白名单,只允许视频格式
|
||||||
|
#whitelist=3gp,amr,amv,asf,avi,avi,bdmv,f4v,flv,ifo,iso,m2ts,m4v,mkv,mov,mp4,mpeg,mpg,ogv,rm,rmvb,ts,vob,webm,wmv
|
||||||
|
|
||||||
|
# 或者,视频附近常见的黑名单格式
|
||||||
|
blacklist=mpls,mks,mka,weba,aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,sbv,srt,ssa,ssf,ttxt,txt,usf,vt,vtt
|
||||||
|
|
||||||
|
remove_files_without_extension=yes
|
||||||
|
|
||||||
|
# 脚本仅在播放开始时生效,禁用则在播放列表更改时也会生效
|
||||||
|
oneshot=no
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#是否启用自动读取并加载外部章节文件。默认:yes
|
||||||
|
autoload=yes
|
||||||
|
#是否启用自动导出章节文件 (当章节信息更改后)。默认:no
|
||||||
|
autosave=no
|
||||||
|
#是否使用外部章节信息覆盖视频内部章节信息。默认:no
|
||||||
|
force_overwrite=no
|
||||||
|
#指定外部章节文件的标识及扩展名
|
||||||
|
chapter_file_ext=.chp
|
||||||
|
#选择外部章节文件是否需要匹配源文件的扩展名。默认:yes
|
||||||
|
basename_with_ext=yes
|
||||||
|
#从视频文件同目录下的指定子目录读取外部章节文件
|
||||||
|
#注意:脚本优先从指定的子目录读取外部章节文件
|
||||||
|
#当子目录的文件不存在时会继续尝试在视频文件同目录中读取外部章节文件
|
||||||
|
external_chapter_subpath=chapters
|
||||||
|
#是否将章节文件统一存储在配置的全局目录中,网络文件将始终使用全局目录。默认:no
|
||||||
|
global_chapters=no
|
||||||
|
#指定章节文件的全局目录的路径。可以是 mpv 支持的相对路径或绝对路径
|
||||||
|
global_chapters_dir=~~/files/chapters
|
||||||
|
#是否在全局目录中使用哈希值保存章节文件名。默认:no
|
||||||
|
##如果设置为'no',章节文件将以相应的媒体文件命名,可能会导致冲突
|
||||||
|
##使用哈希可防止同名但位于不同目录中的媒体文件获取相同的章节文件
|
||||||
|
##但如果您将文件移动到不同的目录,哈希值将更改导致无法加载章节文件
|
||||||
|
hash=no
|
||||||
|
#设置创建新章节时是否默认打开重命名输入功能。默认:yes
|
||||||
|
ask_for_title=yes
|
||||||
|
#设置询问新章节标题时的占位符名称
|
||||||
|
placeholder_title=Chapter
|
||||||
|
#设置询问章节标题时是否暂停播放。默认:yes
|
||||||
|
pause_on_input=yes
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# 是否启用自动跳过。默认:no
|
||||||
|
enabled=no
|
||||||
|
# 每个章节名仅跳过一次。默认:yes
|
||||||
|
skip_once=yes
|
||||||
|
# 章节名匹配规则(Lua 正则)
|
||||||
|
categories=opening>^OP/ OP$/^[Oo]pening/[Oo]pening$/^Intro%s*Start/オープニング$/^片头$/片头开始$; ending>^ED/ ED$/^[Ee]nding/[Ee]nding$/エンディング$; credits>^[Cc]redits/[Cc]redits$; prologue>^[Pp]rologue/^[Ii]ntro$; preview>[Pp]review$/^[Pp]review/予告$/預告$; PartAB>Part [AB]/Ending 1; PartC>Part C
|
||||||
|
# 需要跳过的章节类别
|
||||||
|
skip=opening;ending;credits;prologue;preview
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#指定命令面板的字体大小,默认值:16
|
||||||
|
font_size=26
|
||||||
|
#指定字体大小是否随窗口大小缩放,默认值:no
|
||||||
|
scale_by_window=yes
|
||||||
|
#指定命令面板的菜单项的显示数量,默认值:12
|
||||||
|
lines_to_show=12
|
||||||
|
#指定是否在打开命令面板时暂停播放,默认值:no
|
||||||
|
pause_on_open=yes
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
###此配置不支持参数后注释,须另起一行
|
||||||
|
|
||||||
|
# 是否将命令历史记录保存到文件并加载它。默认:no
|
||||||
|
persist_history=yes
|
||||||
|
# 命令历史记录文件的路径。默认:~~state/command_history.txt
|
||||||
|
history_path=~~/files/command_history.txt
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
###此配置不支持参数后注释,须另起一行
|
||||||
|
|
||||||
|
# 指定控制台显示补全时使用的等宽字体。其他情况使用 --osd-font
|
||||||
|
#monospace_font=Noto Sans Mono CJK SC
|
||||||
|
# 字体大小默认 24。最终大小将与缩放率相乘
|
||||||
|
#font_size=24
|
||||||
|
# 设置用于 REPL 和控制台的字体边框大小。默认值:1.32
|
||||||
|
border_size=1.3
|
||||||
|
# 菜单背景的透明度。范围从 0(不透明)到 255(完全透明)。默认值:80
|
||||||
|
background_alpha=50
|
||||||
|
# 菜单的内边距。默认值:10
|
||||||
|
#padding=10
|
||||||
|
# 菜单边框的大小。默认值:0
|
||||||
|
#menu_outline_size=0
|
||||||
|
# 菜单边框的颜色。默认值:#FFFFFF
|
||||||
|
#menu_outline_color=#FFFFFF
|
||||||
|
# 菜单的圆角半径。默认值:8
|
||||||
|
#orner_radius=8
|
||||||
|
# 菜单所选项的颜色。默认值:#222222
|
||||||
|
#selected_color=#222222
|
||||||
|
# 菜单所选项的背景颜色。默认值:#FFFFFF
|
||||||
|
#selected_back_color=#FFFFFF
|
||||||
|
# 与搜索字符串匹配的字符的颜色。默认值:#0088FF
|
||||||
|
#match_color=#0088FF
|
||||||
|
# 是否使用窗口高度缩放控制台。可以是 yes、no 或 auto,取决于--osd-scale-by-window 选项。默认值:auto
|
||||||
|
#scale_with_window=auto
|
||||||
|
# 查询时是否使用精确搜索而非模糊搜索,默认值:no
|
||||||
|
# 在查询前加上 ' 字符可强制启用精确匹配
|
||||||
|
#exact_match=no
|
||||||
|
# 设置 Tab 自动补全是否区分大小写。仅适用于 ASCII 字符
|
||||||
|
## 默认值:在 Windows 上为 no,在其他平台上为 yes
|
||||||
|
#case_sensitive=no
|
||||||
|
# 删除历史记录中的重复条目,以便仅保留最新的条目。默认:yes
|
||||||
|
history_dedup=yes
|
||||||
|
# 设置字体高度与字体宽度的比率,调整完成建议的表格宽度。默认值:auto
|
||||||
|
## 1.8-2.5 范围内的值对于常见的等宽字体有用
|
||||||
|
#font_hw_ratio=auto
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
##! 注意这个菜单脚本只支持 windows 系统
|
||||||
|
## 指定是否使用 mpv 内部的上下文菜单实现。默认值为 yes
|
||||||
|
## 需使用包含上游提交 https://github.com/mpv-player/mpv/commit/3c1e983 的 mpv 版本
|
||||||
|
use_mpv_impl=no
|
||||||
|
### 指定是否启用 uosc 的菜单语法支持。与默认支持的 mpv.net 菜单语法不兼容,但是可以使用 uosc 的菜单语法。默认值为 no
|
||||||
|
uosc_syntax=yes
|
||||||
|
## 指定是否启用菜单标题转义。默认值为 yes
|
||||||
|
escape_title=yes
|
||||||
|
## 指定菜单标题的最大长度。默认值为 80。如果标题长度超过这个值,将会被截断
|
||||||
|
## 设为 0 表示不限制标题长度
|
||||||
|
max_title_length=80
|
||||||
|
## 指定播放列表菜单项的最大数量。默认值为 20
|
||||||
|
## 设为 0 表示禁用播放列表菜单
|
||||||
|
max_playlist_items=20
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 脚本键位按下时要跳多远
|
||||||
|
seek_distance=5
|
||||||
|
|
||||||
|
# 播放速度调整量,每 'speed_interval' 应用一次,直到达到上限
|
||||||
|
speed_increase=0.1
|
||||||
|
speed_decrease=0.1
|
||||||
|
|
||||||
|
# 以什么间隔应用速度调整量
|
||||||
|
speed_interval=0.05
|
||||||
|
|
||||||
|
# 播放速度上限
|
||||||
|
speed_cap=3
|
||||||
|
|
||||||
|
# 显示字幕时的播放速度上限,'no' 表示与 'speed_cap' 相同
|
||||||
|
subs_speed_cap=1.5
|
||||||
|
|
||||||
|
# 调整前将当前速度乘以速度调整量(指数加速)。默认:no
|
||||||
|
# 使用比默认值低得多的值,例如 speed_increase=0.05, speed_decrease=0.025
|
||||||
|
multiply_modifier=no
|
||||||
|
|
||||||
|
# 在 OSD 上显示当前速度(如果使用 uosc,则闪烁显示速度)。默认:yes
|
||||||
|
show_speed=yes
|
||||||
|
|
||||||
|
# 速度切换时在 OSD 上显示当前速度(如果使用 uosc,则闪烁显示速度)。默认:yes
|
||||||
|
show_speed_toggled=yes
|
||||||
|
|
||||||
|
# 在 osd 上显示搜索操作(如果使用 uosc,则闪烁显示时间线)。默认:yes
|
||||||
|
show_seek=yes
|
||||||
|
|
||||||
|
# 设置 'subs_speed_cap' 项时提前查看以实现更平滑的过渡。默认:no
|
||||||
|
subs_lookahead=no
|
||||||
|
|
||||||
|
# 设置 osd 消息显示的符号,示例即默认值
|
||||||
|
#osd_symbol={\fnmpv-osd-symbols} {\r}
|
||||||
|
#osd_rewind={\fnmpv-osd-symbols} {\r}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "WHEEL_UP",
|
||||||
|
"command": ["script-binding", "file_browser/dynamic/scroll_up"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "WHEEL_DOWN",
|
||||||
|
"command": ["script-binding", "file_browser/dynamic/scroll_down"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "MBTN_LEFT",
|
||||||
|
"command": ["script-binding", "file_browser/dynamic/down_dir"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "MBTN_RIGHT",
|
||||||
|
"command": ["script-binding", "file_browser/dynamic/up_dir"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "MBTN_MID",
|
||||||
|
"command": ["script-binding", "file_browser/dynamic/play"]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"key": "KP1",
|
||||||
|
"command": ["print-text", "files: %n"],
|
||||||
|
"filter": "file",
|
||||||
|
"multiselect": true,
|
||||||
|
"multi-type": "concat",
|
||||||
|
"concat-string": "\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP1",
|
||||||
|
"command": ["print-text", "directories: %n"],
|
||||||
|
"filter": "dir",
|
||||||
|
"multiselect": true,
|
||||||
|
"multi-type": "concat",
|
||||||
|
"concat-string": "\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP1",
|
||||||
|
"command": ["print-text", "%f"],
|
||||||
|
"passthrough": true,
|
||||||
|
"name": "thing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP2",
|
||||||
|
"command": ["print-text", "name: %n"],
|
||||||
|
"multiselect": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP3",
|
||||||
|
"command": ["print-text", "open directory: %p"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP4",
|
||||||
|
"command": ["print-text", "directory name: %d"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP5",
|
||||||
|
"command": ["print-text", "escape the code: %%f"],
|
||||||
|
"multiselect": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP6",
|
||||||
|
"command": ["print-text", "full filepath via concatenation: %p%n"],
|
||||||
|
"multiselect": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP7",
|
||||||
|
"command": ["print-text", "quote/escape filepath: %F"],
|
||||||
|
"multiselect": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KP8",
|
||||||
|
"command": ["print-text", "%r"]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"key": "Alt+DEL",
|
||||||
|
"command": ["run", "powershell", "-command", "rm", "%F"],
|
||||||
|
"filter": "file",
|
||||||
|
"multiselect": true,
|
||||||
|
"multi-type": "concat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+ENTER",
|
||||||
|
"command": ["run", "powershell", "-command", "mpv.exe", "%F"],
|
||||||
|
"multiselect": true,
|
||||||
|
"multi-type": "concat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+c",
|
||||||
|
"command": [
|
||||||
|
["run", "powershell", "-command", "Set-Clipboard", "%F"],
|
||||||
|
["print-text", "copied filepath to clipboard"]
|
||||||
|
],
|
||||||
|
"multiselect": true,
|
||||||
|
"delay": 0.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+v",
|
||||||
|
"command": ["run", "powershell", "-command", "cp", "-LiteralPath", "(Get-Clipboard)", "%P"],
|
||||||
|
"multiselect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+x",
|
||||||
|
"command": ["run", "powershell", "-command", "mv", "-LiteralPath", "(Get-Clipboard)", "%P"],
|
||||||
|
"multiselect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "INS",
|
||||||
|
"command": ["run", "powershell", "-command", "Set-Content", "-LiteralPath", "( %P + '/.ordered-chapters.m3u' )", "-Value", "( %N )"],
|
||||||
|
"multiselect": true,
|
||||||
|
"multi-type": "concat",
|
||||||
|
"concat-string": "+'\n'+"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+INS",
|
||||||
|
"command": ["run", "powershell", "-command", "rm", "-LiteralPath", "( %P + '/.ordered-chapters.m3u' )", "-Force"],
|
||||||
|
"multiselect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+o",
|
||||||
|
"command": ["run", "powershell", "-command", "explorer.exe", "(( %P ).TrimEnd('/') -replace '/', '\\' )"],
|
||||||
|
"multiselect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+O",
|
||||||
|
"command": ["run", "powershell", "-command", "explorer.exe", "(( %F ).TrimEnd('/') -replace '/', '\\' )"],
|
||||||
|
"filter": "dir",
|
||||||
|
"multiselect": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+O",
|
||||||
|
"command": ["run", "powershell", "-command", "explorer.exe", "'/select,'", "( %F -replace '/', '\\' )"],
|
||||||
|
"filter": "file",
|
||||||
|
"multiselect": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+o",
|
||||||
|
"command": ["run", "powershell", "-command", "& 'C:/Program Files/Mozilla Firefox/firefox.exe' %P"],
|
||||||
|
"multiselect": false,
|
||||||
|
"parser": "ftp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+O",
|
||||||
|
"command": ["run", "powershell", "-command", "& 'C:/Program Files/Mozilla Firefox/firefox.exe' %F"],
|
||||||
|
"filter": "dir",
|
||||||
|
"multiselect": true,
|
||||||
|
"parser": "ftp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DEL",
|
||||||
|
"command": [
|
||||||
|
["run", "powershell", "-command", "(New-Object -ComObject 'Shell.Application').NameSpace(0).ParseName((%F -replace '/', '\\' )).InvokeVerb('delete')"],
|
||||||
|
["script-message", "delay-command", "4", "script-binding", "file_browser/dynamic/reload"],
|
||||||
|
["show-text", "删除 %f"]
|
||||||
|
],
|
||||||
|
"multiselect": true,
|
||||||
|
"multi-type": "repeat"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"key": "F",
|
||||||
|
"command": ["script-message", "favourites/add_favourite", "%f"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "F",
|
||||||
|
"command": ["script-message", "favourites/remove_favourite", "%f"],
|
||||||
|
"parser": "favourites"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+UP",
|
||||||
|
"command": [
|
||||||
|
["script-message", "favourites/move_up", "%f"]
|
||||||
|
],
|
||||||
|
"parser": "favourites"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+DOWN",
|
||||||
|
"command": [
|
||||||
|
["script-message", "favourites/move_down", "%f"]
|
||||||
|
],
|
||||||
|
"parser": "favourites"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+r",
|
||||||
|
"command": [
|
||||||
|
["script-message", "winroot/import_root_drives"]
|
||||||
|
],
|
||||||
|
"parser": "root"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 指定脚本的工作方式,有三种模式可选:noth、pass、switch。默认值:noth
|
||||||
|
## noth:什么也不做
|
||||||
|
## pass:当显示器处于 HDR 模式时,为 HDR 内容传递 HDR 信号
|
||||||
|
## switch:根据 mpv 播放的视频内容在显示器的 HDR 模式和 SDR 模式之间自动切换,在 Windows 10 及更高版本的系统上可用
|
||||||
|
hdr_mode=noth
|
||||||
|
# 指定是否仅在全屏或窗口最大化时自动切换 HDR 模式。仅在 hdr_mode=switch 时生效,默认值:no
|
||||||
|
fullscreen_only=no
|
||||||
|
# 用于指定你的 HDR 显示器的目标峰值,默认值:203
|
||||||
|
#! 提醒:此项必须指定显示器的真实峰值亮度,否则会导致 HDR 内容显示不正确。默认值 203 会视为 SDR 显示器处理
|
||||||
|
target_peak=203
|
||||||
|
# 用于指定你的 HDR 显示器的最大对比度数值,默认值:auto,即无限对比度(OLED)
|
||||||
|
#! 提醒:此项必须指定为显示器的最大对比度,否则无法正确实施黑位补偿
|
||||||
|
## 例如 100000 表示显示器最大对比度为 100000:1
|
||||||
|
## OLED 显示器无需更改此项,使用默认值即可
|
||||||
|
target_contrast=auto
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#是否使用外部配置文件设置增强式键位动作,默认:no
|
||||||
|
enable_external_config=yes
|
||||||
|
#指定外部配置文件的路径,可以是 mpv 支持的相对路径或绝对路径
|
||||||
|
#注意:启用外部配置文件功能时请确保该文件存在
|
||||||
|
external_config=~~/inputevent_key.conf
|
||||||
|
#指定键位事件的识别前缀,默认:event
|
||||||
|
prefix=event
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#choose a layout(reduced/original/mid)
|
||||||
|
#layout=original
|
||||||
|
|
||||||
|
#show OSC when windowed? yes/no
|
||||||
|
#showwindowed=
|
||||||
|
|
||||||
|
#show OSC when fullscreen? yes/no
|
||||||
|
#showfullscreen=
|
||||||
|
|
||||||
|
#scaling of the controller when windowed
|
||||||
|
scalewindowed=1
|
||||||
|
|
||||||
|
#scaling of the controller when fullscreen
|
||||||
|
scalefullscreen=1
|
||||||
|
|
||||||
|
#scaling when rendered on a forced window
|
||||||
|
scaleforcedwindow=1.5
|
||||||
|
|
||||||
|
#scale the controller with the video? yes/no
|
||||||
|
#vidscale=
|
||||||
|
|
||||||
|
#duration in ms until the OSC hides if no mouse movement. enforced non-negative for the user but internally negative is 'always-on'.
|
||||||
|
#hidetimeout=500
|
||||||
|
|
||||||
|
#duration of fade out in ms 0=no fade
|
||||||
|
#fadeduration=200
|
||||||
|
|
||||||
|
#minimum amount of pixels the mouse has to move between ticks to make the OSC show up
|
||||||
|
#minmousemove=3
|
||||||
|
|
||||||
|
#use native mpv values and disable OSC internal track list management (and some functions that depend on it) yes/no
|
||||||
|
#amaprogrammer=
|
||||||
|
|
||||||
|
#default osc font
|
||||||
|
#font='mpv-osd-symbols'
|
||||||
|
|
||||||
|
#show seekrange overlay yes/no
|
||||||
|
#seekrange=
|
||||||
|
|
||||||
|
#color of seekbar and knot,there is no # before value
|
||||||
|
#seekbarfg_color="7FFFD4"
|
||||||
|
|
||||||
|
#transparency of seekranges
|
||||||
|
seekrangealpha=128
|
||||||
|
|
||||||
|
#use keyframes when dragging the seekbar yes/no
|
||||||
|
#seekbarkeyframes=
|
||||||
|
|
||||||
|
#string compatible with property-expansion to be shown as OSC title
|
||||||
|
#title='${media-title}'
|
||||||
|
|
||||||
|
#show osc and no hide timeout on pause yes/no
|
||||||
|
showonpause=no
|
||||||
|
|
||||||
|
#display timecodes with milliseconds yes/no
|
||||||
|
#timems=false
|
||||||
|
|
||||||
|
#display total time instead of remaining time? yes/no
|
||||||
|
#timetotal=no
|
||||||
|
|
||||||
|
#how mpv logo on idle
|
||||||
|
#idlescreen=yes
|
||||||
|
|
||||||
|
#only used at init to set visibility_mode(...) auto/always/never
|
||||||
|
#visibility=always
|
||||||
|
|
||||||
|
#whether to show window controls auto/yes/no
|
||||||
|
#windowcontrols=
|
||||||
|
|
||||||
|
#whether to show mute button and volumne slider yes/no
|
||||||
|
#volumecontrol=
|
||||||
|
|
||||||
|
#volume bar show processd volume yes/no
|
||||||
|
#processvolume=
|
||||||
|
|
||||||
|
#eng=English chs=Chinese eng/chs
|
||||||
|
#language=chs
|
||||||
|
|
||||||
|
#alpha of the background box,0 (opaque) to 255 (fully transparent)
|
||||||
|
boxalpha=128
|
||||||
|
|
||||||
|
#hight of deadzone,from bottom to top
|
||||||
|
#deadzone=200
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# options to pass to wget
|
||||||
|
## 设置 mpv 需全局记忆的选项状态
|
||||||
|
properties=volume
|
||||||
|
## 保存文件路径
|
||||||
|
properties_path=files/persistent_config.json
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
###此配置不支持在激活的参数后进行注释,如有注释需求应另起一行
|
||||||
|
|
||||||
|
#### ------- Mpv-Playlistmanager configuration ------- ####
|
||||||
|
|
||||||
|
#### ------- FUNCTIONAL ------- ####
|
||||||
|
|
||||||
|
#navigation 键绑定仅在播放列表可见时强制覆盖
|
||||||
|
#设置"no",则可以通过任何导航键显示播放列表
|
||||||
|
dynamic_binds=yes
|
||||||
|
|
||||||
|
#主菜单键位绑定
|
||||||
|
key_showplaylist=
|
||||||
|
|
||||||
|
#按住键位时显示播放列表
|
||||||
|
key_peek_at_playlist=
|
||||||
|
|
||||||
|
## 动态绑定键位 不应在 input.conf 中设置(不与静态绑定的键位冲突)
|
||||||
|
## 可以绑定多个键位,用空格分离
|
||||||
|
key_moveup=UP WHEEL_UP
|
||||||
|
key_movedown=DOWN WHEEL_DOWN
|
||||||
|
key_movepageup=PGUP MBTN_BACK
|
||||||
|
key_movepagedown=PGDWN MBTN_FORWARD
|
||||||
|
key_movebegin=HOME
|
||||||
|
key_moveend=END
|
||||||
|
key_selectfile=RIGHT
|
||||||
|
key_unselectfile=LEFT
|
||||||
|
key_playfile=ENTER MBTN_LEFT
|
||||||
|
key_removefile=DEL BS
|
||||||
|
key_closeplaylist=ESC MBTN_RIGHT
|
||||||
|
|
||||||
|
## 额外的功能键位动态绑定
|
||||||
|
## 可以绑定多个键位,用空格分离
|
||||||
|
key_sortplaylist=s
|
||||||
|
key_shuffleplaylist=r R
|
||||||
|
key_reverseplaylist=S
|
||||||
|
key_loadfiles=l L
|
||||||
|
key_saveplaylist=p P
|
||||||
|
|
||||||
|
# json 替换格式,请查看.lua 以获取说明
|
||||||
|
# example json=[{"ext":{"all":true},"rules":[{"_":" "}]},{"ext":{"mp4":true,"mkv":true},"rules":[{"^(.+)%..+$":"%1"},{"%s*[%[%(].-[%]%)]%s*":""},{"(%w)%.(%w)":"%1 %2"}]},{"protocol":{"http":true,"https":true},"rules":[{"^%a+://w*%.?":""}]}]
|
||||||
|
# 空值,无需更换
|
||||||
|
filename_replace=[{"protocol":{"all":true},"rules":[{"%%(%x%x)":"hex_to_char"}]},{"protocol":{"http":true,"https":true},"rules":[{"^%a[%a%d]+://localhost:.*/":""}]}]
|
||||||
|
|
||||||
|
## 指定从目录中搜索和加载的文件类型。注:此项与 autoload.lua 脚本功能重复
|
||||||
|
#loadfiles_filetypes=["3gp","amr","amv","asf","avi","avi","bdmv","f4v","flv","ifo","iso","m2ts","m4v","mkv","mov","mp4","mpeg","mpg","ogv","rm","rmvb","ts","vob","webm","wmv"]
|
||||||
|
#在启动时加载 1 个或多个文件到播放列表。注:此项与 autoload.lua 脚本功能重复
|
||||||
|
loadfiles_on_start=no
|
||||||
|
|
||||||
|
#空闲启动时从工作目录加载文件
|
||||||
|
loadfiles_on_idle_start=no
|
||||||
|
|
||||||
|
#总是在当前播放文件后放置加载的文件
|
||||||
|
loadfiles_always_append=no
|
||||||
|
|
||||||
|
#指定在初始加载后将任何文件添加到播放列表时是否进行自然排序
|
||||||
|
sortplaylist_on_file_add=no
|
||||||
|
|
||||||
|
#指定使用排序功能时需使用的排序方法,必须是其中之一:"name-asc", "name-desc", "date-asc", "date-desc", "size-asc", "size-desc".
|
||||||
|
default_sort=name-asc
|
||||||
|
|
||||||
|
#linux | windows | auto
|
||||||
|
system=auto
|
||||||
|
|
||||||
|
#Use ~ 用于主目录。留空以使用 mpv/playlists
|
||||||
|
playlist_savepath=
|
||||||
|
|
||||||
|
#播放列表打开时同步当前播放文件所在位置(pos)
|
||||||
|
sync_cursor_on_load=yes
|
||||||
|
|
||||||
|
#每次加载新文件时显示文件标题
|
||||||
|
show_title_on_file_load=no
|
||||||
|
#每次加载新文件时显示播放列表
|
||||||
|
show_playlist_on_file_load=no
|
||||||
|
#选择播放文件后关闭播放列表
|
||||||
|
close_playlist_on_playfile=no
|
||||||
|
|
||||||
|
##是否光标移动到末尾自动切换到首位条目
|
||||||
|
loop_cursor=yes
|
||||||
|
|
||||||
|
#当播放列表不可见时重置光标导航
|
||||||
|
reset_cursor_on_close=yes
|
||||||
|
|
||||||
|
#允许播放列表管理器在文件之间导航时编写稍后观看配置
|
||||||
|
allow_write_watch_later_config=no
|
||||||
|
|
||||||
|
#在保存、随机播放、反转播放列表时向 OSD 输出视觉反馈
|
||||||
|
display_osd_feedback=no
|
||||||
|
|
||||||
|
#prefer 以显示以下文件的标题:"all","url","none"。排序仍使用文件名
|
||||||
|
prefer_titles=url
|
||||||
|
|
||||||
|
#指定要用于标题解析的 ytdl 可执行文件,可以是绝对路径。默认:yt-dlp
|
||||||
|
youtube_dl_executable=yt-dlp
|
||||||
|
|
||||||
|
#使用 youtube-dl/yt-dlp 解析播放列表中网址的标题,默认:no
|
||||||
|
#prefer_titles 必须设置为 "url" 或"all" 才能正常工作
|
||||||
|
resolve_url_titles=yes
|
||||||
|
|
||||||
|
#指定播放列表中 url 标题解析的超时时长,默认值:15s
|
||||||
|
resolve_title_timeout=15
|
||||||
|
|
||||||
|
#指定同时解析 url 媒体标题的数量。较高的数字可能会导致卡明显的卡顿
|
||||||
|
concurrent_title_resolve_limit=10
|
||||||
|
|
||||||
|
##播放列表在 OSD 显示的时长,默认值:5s
|
||||||
|
playlist_display_timeout=5
|
||||||
|
|
||||||
|
##播放列表将呈现的最大行数。特殊值 -1 将自动计算,默认值:-1
|
||||||
|
showamount=13
|
||||||
|
|
||||||
|
##播放列表的字体样式
|
||||||
|
#example {\fnUbuntu\fs10\b0\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
|
||||||
|
#read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
|
||||||
|
#no 值默认为 mpv.conf 中的 OSD 设置
|
||||||
|
style_ass_tags={\fnNoto Sans CJK SC\fs26\b1\bord0.5}
|
||||||
|
|
||||||
|
##左上方的边距
|
||||||
|
text_padding_x=10
|
||||||
|
text_padding_y=15
|
||||||
|
|
||||||
|
#菜指定单打开时屏幕的不透明度,值:0.0 - 1.0(0 表示透明(默认),1 表示不透明)
|
||||||
|
curtain_opacity=0.0
|
||||||
|
|
||||||
|
##是否修改 MPV 的窗口标题
|
||||||
|
set_title_stripped=no
|
||||||
|
title_prefix=
|
||||||
|
title_suffix= - mpv
|
||||||
|
|
||||||
|
##是否切断长文件名,最大显示字符数
|
||||||
|
slice_longfilenames=yes
|
||||||
|
slice_longfilenames_amount=100
|
||||||
|
|
||||||
|
##播放列表标题
|
||||||
|
#%mediatitle or %filename = 播放文件的标题或名称
|
||||||
|
#%pos = 播放文件的位置
|
||||||
|
#%cursor = 导航的位置
|
||||||
|
#%plen = 播放列表长度
|
||||||
|
#%N = 换行符
|
||||||
|
playlist_header=播放列表 [%cursor/%plen]
|
||||||
|
|
||||||
|
##播放列表模板
|
||||||
|
#%pos = 播放文件的位置
|
||||||
|
#%name = 文件的标题或名称
|
||||||
|
#%N = 换行符
|
||||||
|
#也可以使用上面提到的 ASS 标签。例如:
|
||||||
|
# selected_file={\c&HFF00FF&}➔ %name | 为所选文件添加颜色
|
||||||
|
#使用 ASS 标签,你需要为每一行重置它们 (see https://github.com/jonniek/mpv-playlistmanager/issues/20)
|
||||||
|
normal_file={\c&HFFFFFF&}□ %name
|
||||||
|
hovered_file={\c&H33FFFF&}■ %name
|
||||||
|
selected_file={\c&C1C1FF&}☑ %name
|
||||||
|
playing_file={\c&HAAAAAA&}▷ %name
|
||||||
|
playing_hovered_file={\c&H00FF00&}▶ %name
|
||||||
|
playing_selected_file={\c&C1C1FF&}☑ %name
|
||||||
|
|
||||||
|
##播放列表被截断时的显示
|
||||||
|
playlist_sliced_prefix=▲
|
||||||
|
playlist_sliced_suffix=▼
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
###### 键绑定 ######
|
||||||
|
# 向上移动菜单光标
|
||||||
|
up_binding=UP WHEEL_UP
|
||||||
|
# 向下移动菜单光标
|
||||||
|
down_binding=DOWN WHEEL_DOWN
|
||||||
|
# 选择菜单项
|
||||||
|
select_binding=ENTER MBTN_LEFT
|
||||||
|
# 关闭质量菜单
|
||||||
|
close_menu_binding=ESC MBTN_RIGHT CTRL+F ALT+F
|
||||||
|
###### 键绑定 ######
|
||||||
|
|
||||||
|
# 字体大小按窗口缩放,如果为否需要更大的字体和填充大小
|
||||||
|
scale_playlist_by_window=yes
|
||||||
|
|
||||||
|
# 播放列表屁股样式覆盖大括号内。\keyvalue 是一个字段,额外 \ 用于 lua 中的转义
|
||||||
|
# example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
|
||||||
|
# 标签参考 https://aegi.vmoe.info/docs/3.0/ASS_Tags/
|
||||||
|
# 未声明的标签将使用默认的 osd 设置
|
||||||
|
# 这些样式将用于整个播放列表。更具体的样式实现方式并不优雅
|
||||||
|
# 建议使用等宽字体,保持样式美观
|
||||||
|
style_ass_tags={\\fnNoto Sans CJK SC,Noto Color Emoji\\fs25\\bord0.5}
|
||||||
|
|
||||||
|
# 自定义游标
|
||||||
|
# 也可以使用上面提到的 ASS 标签。例如:
|
||||||
|
# selected_and_inactive={\c&H33FFFF&}● - | 为所选格式添加颜色
|
||||||
|
# 使用 ASS 标签,你需要为每一行重置它们
|
||||||
|
selected_and_active={\c&H00FF00&}▶ -
|
||||||
|
selected_and_inactive={\c&H33FFFF&}● -
|
||||||
|
unselected_and_active={\c&HAAAAAA&}▷ -
|
||||||
|
unselected_and_inactive={\c&HFFFFFF&}○ -
|
||||||
|
|
||||||
|
# 移位绘图坐标。mpv.net 兼容性所必需的
|
||||||
|
shift_x=0
|
||||||
|
shift_y=0
|
||||||
|
|
||||||
|
# 左上角的填充
|
||||||
|
text_padding_x=5
|
||||||
|
text_padding_y=10
|
||||||
|
|
||||||
|
# 菜单打开时屏幕变暗
|
||||||
|
curtain_opacity=0.7
|
||||||
|
|
||||||
|
# 质量菜单超时的秒数
|
||||||
|
menu_timeout=6
|
||||||
|
|
||||||
|
# 使用 youtube-dl 获取可用格式的列表(覆盖 quality_strings),默认:yes
|
||||||
|
fetch_formats=yes
|
||||||
|
|
||||||
|
# 可供选择的 ytdl 格式字符串列表
|
||||||
|
quality_strings_video=[ {"4320p" : "bestvideo[height<=?4320p]"}, {"2160p" : "bestvideo[height<=?2160]"}, {"1440p" : "bestvideo[height<=?1440]"}, {"1080p" : "bestvideo[height<=?1080]"}, {"720p" : "bestvideo[height<=?720]"}, {"480p" : "bestvideo[height<=?480]"}, {"360p" : "bestvideo[height<=?360]"}, {"240p" : "bestvideo[height<=?240]"}, {"144p" : "bestvideo[height<=?144]"} ]
|
||||||
|
quality_strings_audio=[ {"default" : "bestaudio"} ]
|
||||||
|
|
||||||
|
# 打开网络视频后显示视频格式菜单,默认:yes
|
||||||
|
start_with_menu=no
|
||||||
|
|
||||||
|
# 在列表中包含未知格式,不幸的是,选择视频或音频格式并不总是完美的
|
||||||
|
# 设置为 yes 以确保您不会错过任何格式,但随后列表还可能包括实际上不是视频或音频的格式。已知不是视频或音频的格式仍会被过滤掉
|
||||||
|
include_unknown=no
|
||||||
|
|
||||||
|
# 隐藏所有格式都相同的列,默认:yes
|
||||||
|
hide_identical_columns=yes
|
||||||
|
|
||||||
|
# 指定列的属性以什么顺序显示,使用','分隔列表,属性前加'-'可使该列左对齐
|
||||||
|
# 对于 uosc 集成,可以将文本分成标题和提示
|
||||||
|
## 这是通过用'|'而不是逗号分隔两列来实现的
|
||||||
|
##可用属性有:
|
||||||
|
#resolution, width, height, fps, dynamic_range, tbr, vbr, abr, asr,
|
||||||
|
#filesize, filesize_approx, vcodec, acodec, ext, video_ext, audio_ext,
|
||||||
|
#language, format, format_note, quality
|
||||||
|
##以下属性经过特殊处理
|
||||||
|
#size, frame_rate, bitrate_total, bitrate_video, bitrate_audio,
|
||||||
|
#codec_video, codec_audio, audio_sample_rate
|
||||||
|
columns_video=-resolution,frame_rate|dynamic_range,bitrate_video,size,-codec_video,-ext
|
||||||
|
columns_audio=language,audio_sample_rate,bitrate_audio|size,-codec_audio,-ext
|
||||||
|
|
||||||
|
# 用于排序的列,有关可用列,请参阅'columns_video'
|
||||||
|
# 逗号分隔列表,前缀列带“-”以反转排序顺序
|
||||||
|
# 将此内容留空可保留 yt-dlp/youtube-dl 的顺序
|
||||||
|
# 注:拼写错误的列不会导致错误,但它们可能会影响结果
|
||||||
|
sort_video=
|
||||||
|
sort_audio=
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# options to pass to wget
|
||||||
|
#wget_opts=
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
###此配置不支持参数后注释,须另起一行
|
||||||
|
|
||||||
|
# 指定历史记录条目的日期格式。这被传递给 Lua 的 os.date
|
||||||
|
# 这使用与 strftime(3)相同的格式
|
||||||
|
history_date_format=%Y-%m-%d %H:%M:%S
|
||||||
|
# 是否仅显示具有相同路径的最后一个历史记录条目,默认:yes
|
||||||
|
hide_history_duplicates=yes
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
####------脚本设置-----####
|
||||||
|
|
||||||
|
#--打开 mpv 且没有加载视频/文件时自动运行列表。'none'表示禁用。或者选择:all, recents, distinct, protocols, fileonly, titleonly, timeonly, keywords.
|
||||||
|
auto_run_list_idle=none
|
||||||
|
|
||||||
|
#--mpv 启动且未加载任何内容时的行为。'none'表示禁用。'resume'以自动恢复您上次播放的项目。'resume-notime'以恢复您上次播放的项目,但从头开始
|
||||||
|
startup_idle_behavior=none
|
||||||
|
|
||||||
|
#--在打开和关闭菜单时隐藏 OSC 空闲屏幕消息(如果多个脚本触发 osc-空闲屏幕关闭,可能会导致意外行为)
|
||||||
|
toggle_idlescreen=no
|
||||||
|
|
||||||
|
#--更改为 0,以便项目从确切位置恢复,或减小值,以便在加载恢复点之前为您提供一些预览
|
||||||
|
resume_offset=-10
|
||||||
|
|
||||||
|
#--yes 用于在发生操作时显示 OSD 消息。更改为 no 将禁用从此脚本生成的所有 osd 消息
|
||||||
|
osd_messages=yes
|
||||||
|
|
||||||
|
#--none: :用于禁用。notification:将触发一条消息以恢复上一个到达的时间。force:根据阈值强制恢复上次播放
|
||||||
|
resume_option=none
|
||||||
|
|
||||||
|
#--0 在之前播放过同一视频时始终触发恢复选项,如果上次播放时间在视频的 5% 之后开始并在完成 5% 之前结束,则值(如 5)将仅触发恢复选项
|
||||||
|
resume_option_threshold=5
|
||||||
|
|
||||||
|
#--yes 用于将历史记录时间标记为章节。no 禁用标记为章节的行为
|
||||||
|
mark_history_as_chapter=no
|
||||||
|
|
||||||
|
#--yes: 反转黑名单为白名单,以便将诸如路径/网址之类的东西添加到 history_blacklist 中以保存到历史记录中
|
||||||
|
invert_history_blacklist=no
|
||||||
|
|
||||||
|
#--设置黑名单:Paths / URLs / Websites / Files / Protocols / Extensions, 黑名单中的类型或路径将不会添加到历史记录中
|
||||||
|
##例如:["c:\\users\\eisa01\\desktop", "c:\\users\\eisa01\\desktop\\*", "c:\\temp\\naruto-01.mp4", "youtube.com", "https://dailymotion.com/", "avi", "https://www.youtube.com/watch?v=e8YBesRKq_U", ".jpeg", "magnet:", "https://", "ftp"]
|
||||||
|
history_blacklist=[""]
|
||||||
|
|
||||||
|
#--键绑定,用于在没有视频播放时立即加载和恢复最后一项。如果视频正在播放,它将恢复到上次找到的位置
|
||||||
|
##! 注意:该绑定将覆盖 input.conf 中的同键位,推荐留空在 input.conf 中绑定该功能
|
||||||
|
history_resume_keybind=[""]
|
||||||
|
|
||||||
|
#--键绑定,用于在没有播放视频时立即加载最后一项而不恢复。如果视频正在播放,那么它将添加到播放列表中
|
||||||
|
##! 注意:该绑定将覆盖 input.conf 中的同键位,推荐留空在 input.conf 中绑定该功能
|
||||||
|
history_load_last_keybind=[""]
|
||||||
|
|
||||||
|
#--键绑定,将用于打开列表以及指定的筛选器。可用的过滤器:"all", "recents", "distinct", "protocols", "fileonly", "titleonly", "timeonly", "keywords".
|
||||||
|
##! 注意:该绑定将覆盖 input.conf 中的同键位,推荐留空在 input.conf 中绑定该功能
|
||||||
|
open_list_keybind=[ ["", "distinct"], ["", "recents"] ]
|
||||||
|
|
||||||
|
#--动态键绑定,在列表打开时使用以跳转到特定筛选器(它还允许按两次筛选器键绑定以关闭列表)。可用的过滤器:"all", "recents", "distinct", "protocols", "fileonly", "titleonly", "timeonly", "keywords".
|
||||||
|
list_filter_jump_keybind=[ ["a", "all"], ["r", "recents"], ["d", "distinct"], ["f", "fileonly"], ["p", "protocols"], ["t", "titleonly"], ["l", "playing"] ]
|
||||||
|
|
||||||
|
####------隐身设置-----####
|
||||||
|
|
||||||
|
#--指定是否在 MPV 启动时自动启动隐身模式
|
||||||
|
auto_run_incognito_mode=no
|
||||||
|
|
||||||
|
#--yes:以便自动从历史记录中删除触发隐身模式的文件,no:将文件保留在隐身模式触发的历史记录中
|
||||||
|
delete_incognito_entry=yes
|
||||||
|
|
||||||
|
#--"none"表示禁用,"deleted-restore"以便自动恢复进入隐身时删除的文件,"always"表示退出隐身模式后始终立即更新历史记录中的条目
|
||||||
|
restore_incognito_entry=always
|
||||||
|
|
||||||
|
#--键绑定,触发隐身模式。启用后播放的文件不会添加到历史记录中,直到禁用此模式
|
||||||
|
##! 注意:该绑定将覆盖 input.conf 中的同键位,推荐留空在 input.conf 中绑定该功能
|
||||||
|
history_incognito_mode_keybind=[""]
|
||||||
|
|
||||||
|
####------日志记录设置------####
|
||||||
|
|
||||||
|
#--指定书签日志文件的保存路径。更改为'/:dir%script%'以将其放置在脚本的同一目录中,或者更改为'/:dir%mpvconf%'以将其放置在 mpv portable_config 目录中。
|
||||||
|
##或者使用'/:var'写入任何变量,然后使用变量'/:var%APPDATA%',您也可以使用路径,例如:'/:var%APPDATA%\mpv'或'/:var%HOME%/mpv'或指定绝对路径,例如:'C:\Users\Eisa01\Desktop\'
|
||||||
|
log_path=/:dir%mpvconf%/files
|
||||||
|
|
||||||
|
#--名称 + 将用于存储日志数据的文件的扩展名
|
||||||
|
log_file=mpvHistory.log
|
||||||
|
|
||||||
|
#--日志中的日期格式(请参阅 lua 日期格式),例如:"%d/%m/%y %X" or "%d/%b/%y %X"
|
||||||
|
date_format=%A/%B %d/%m/%Y %X
|
||||||
|
|
||||||
|
#--在 all, protocols, none 中选择保存媒体标题的类型。此选项会将媒体标题存储在日志文件中,这对于网站/协议很有用,因为标题无法仅从链接中解析
|
||||||
|
file_title_logging=protocols
|
||||||
|
|
||||||
|
#--在下面(逗号后)添加您希望将其标题存储在日志文件中的任何协议。这仅对 (file_title_logging = "protocols" or file_title_logging = "all") 有效
|
||||||
|
logging_protocols=["://", "magnet:"]
|
||||||
|
|
||||||
|
#--指定显示文件名而不是标题的范围。在 local, protocols, all, 和 none 之间进行选择
|
||||||
|
## "local"更喜欢非协议视频的文件名。"protocols"将仅首选协议的文件名。"all"将始终使用文件名而不是标题。"none"将始终使用标题而不是文件名
|
||||||
|
prefer_filename_over_title=local
|
||||||
|
|
||||||
|
#--限制保存具有相同路径的条目:-1 表示无限制,0 将始终更新相同路径的条目,例如值 3 将限制为 3,然后它将在第 4 个条目开始更新旧值
|
||||||
|
same_entry_limit=2
|
||||||
|
|
||||||
|
####------列表设置-------####
|
||||||
|
|
||||||
|
#--设置是否启用光标循环滚动
|
||||||
|
loop_through_list=yes
|
||||||
|
|
||||||
|
#--设置是否在到达列表中间后更新显示新项目
|
||||||
|
list_middle_loader=yes
|
||||||
|
|
||||||
|
#--显示文件路径而不是媒体标题
|
||||||
|
show_paths=no
|
||||||
|
|
||||||
|
#--在显示其名称和值之前显示每个项目的编号
|
||||||
|
show_item_number=yes
|
||||||
|
|
||||||
|
#--设置是否按下面指定的字符数量对长文件名进行切片
|
||||||
|
slice_longfilenames=yes
|
||||||
|
|
||||||
|
#--用于切片长文件名的字符数量
|
||||||
|
slice_longfilenames_amount=80
|
||||||
|
|
||||||
|
#--更改最大数量以在当前列表显示更多项目
|
||||||
|
list_show_amount=10
|
||||||
|
|
||||||
|
#--是否启用动态选择键绑定,条目从 0 到 9,用于在列表打开时快速选择(list_show_amount = 10 是此功能工作的最大值)
|
||||||
|
quickselect_0to9_keybind=yes
|
||||||
|
|
||||||
|
#--是否启用双击主列表时退出列表的功能,即使列表是通过其他过滤器访问的
|
||||||
|
main_list_keybind_twice_exits=yes
|
||||||
|
|
||||||
|
#--巧妙地将搜索设置为不键入(当搜索框打开时),而无需按 ctrl+enter 键
|
||||||
|
search_not_typing_smartly=yes
|
||||||
|
|
||||||
|
#--"specific"查找日期、标题、路径/URL、时间的匹配项。"any"以根据日期,标题,路径/ URL 和时间的组合查找任何键入的搜索。"any-notime"根据日期、标题和路径/URL 的组合查找任何键入的搜索,但不查找时间(这是为了减少不需要的结果)
|
||||||
|
search_behavior=any
|
||||||
|
|
||||||
|
####------过滤器设置-------####
|
||||||
|
##--可用过滤器:"all"以显示所有项目。或"keybinds"以显示使用键绑定插槽过滤的列表。或"recents"以显示最近添加的要记录的项目而不重复。或"distinct"以显示不同路径中文件的最近保存条目。
|
||||||
|
##或"fileonly"以显示没有时间保存的文件。或"timeonly"以显示只有时间的文件。或"keywords"以显示具有配置中指定的匹配关键字的文件。或"playing"以显示当前播放文件的列表。
|
||||||
|
|
||||||
|
#--跳转到以下过滤器,并在通过左右键导航时按显示的顺序跳转。您可以更改顺序并删除不需要的筛选器
|
||||||
|
filters_and_sequence=["all", "recents", "distinct", "protocols", "playing", "fileonly", "titleonly", "keywords"]
|
||||||
|
|
||||||
|
#--键绑定,将用于根据 filters_and_sequence 跳转到下一个可用筛选器
|
||||||
|
next_filter_sequence_keybind=["RIGHT", "MBTN_FORWARD"]
|
||||||
|
|
||||||
|
#--键绑定,将用于根据 filters_and_sequence 跳转到上一个可用筛选器
|
||||||
|
previous_filter_sequence_keybind=["LEFT", "MBTN_BACK"]
|
||||||
|
|
||||||
|
#--是否启用循环访问过滤器的行为
|
||||||
|
loop_through_filters=yes
|
||||||
|
|
||||||
|
#--为您想要的"keywords"中创建一个过滤器,例如:youtube.com 将过滤掉 YouTube 上的视频。您还可以插入文件名或标题的一部分,或扩展名或路径的完整路径/部分。例如: ["youtube.com", "mp4", "naruto", "c:\\users\\eisa01\\desktop"]. 留空已禁用关键词过滤器
|
||||||
|
keywords_filter_list=["youtube.com"]
|
||||||
|
|
||||||
|
####------排序设置-------####
|
||||||
|
##--可用排序:added-asc 用于首先显示最新添加的项目。或者 added-desc 用于显示添加顺序。或者 alphanum-asc 用于 A 到 Z 方法,文件名和集数先降低。或者 alphanum-desc 是它的 Z 到 A 方法。或 time-asc,time-desc 根据时间对列表进行排序
|
||||||
|
|
||||||
|
#--指定列表中所有不同筛选器的默认排序方法。选择范围:added-asc, added-desc, time-asc, time-desc, alphanum-asc, alphanum-desc
|
||||||
|
list_default_sort=added-asc
|
||||||
|
|
||||||
|
#--指定特定过滤器的默认排序,例如:[ ["all", "alphanum-asc"], ["playing", "added-desc"] ]
|
||||||
|
list_filters_sort=[ ["keybinds", "keybind-asc"], ["fileonly", "alphanum-asc"], ["playing", "time-asc"] ]
|
||||||
|
|
||||||
|
#--键绑定,用于在列表打开时循环浏览不同的可用排序
|
||||||
|
list_cycle_sort_keybind=["alt+s"]
|
||||||
|
|
||||||
|
####------列表设计设置------####
|
||||||
|
|
||||||
|
#--指定列表的对齐方式,使用数字键盘位置从 1-9 中选择,或 0 以禁用。例如:7 左上对齐,8 中上对齐,9 右上角对齐
|
||||||
|
list_alignment=7
|
||||||
|
|
||||||
|
#--列表中项目显示的时间类型。选择:duration, length, remaining.
|
||||||
|
text_time_type=duration
|
||||||
|
|
||||||
|
#--指定在保存的时间之前显示的时间分隔符样式
|
||||||
|
time_seperator= 🕒
|
||||||
|
|
||||||
|
#--指定表示上面有更多项目的文本时的样式。\n 用于换行。\h 代表空格
|
||||||
|
list_sliced_prefix=...\h\N
|
||||||
|
|
||||||
|
#--指定表示下面有更多项目的文本时的样式
|
||||||
|
list_sliced_suffix=...
|
||||||
|
|
||||||
|
#--yes 启用前文本,用于在列表之前显示快速选择键绑定。no 禁用
|
||||||
|
quickselect_0to9_pre_text=no
|
||||||
|
|
||||||
|
#--指定列表的文本颜色,BGR 十六进制
|
||||||
|
text_color=ffffff
|
||||||
|
|
||||||
|
#--列表文本的字体大小
|
||||||
|
text_scale=80
|
||||||
|
|
||||||
|
#--列表文本的黑色边框大小
|
||||||
|
text_border=0.5
|
||||||
|
|
||||||
|
#--前光标位置的文本颜色,BGR 十六进制
|
||||||
|
text_cursor_color=ffbf7f
|
||||||
|
|
||||||
|
#--列表中当前光标位置的文本的字体大小
|
||||||
|
text_cursor_scale=90
|
||||||
|
|
||||||
|
#--列表中当前光标位置的文本的黑色边框大小
|
||||||
|
text_cursor_border=0.7
|
||||||
|
|
||||||
|
#--突出显示的多选项目的前置文本
|
||||||
|
text_highlight_pre_text=✅
|
||||||
|
|
||||||
|
#--在打字模式下搜索框的颜色
|
||||||
|
search_color_typing=ffffaa
|
||||||
|
|
||||||
|
#--处于打字模式且处于活动状态时搜索框的颜色
|
||||||
|
search_color_not_typing=00bfff
|
||||||
|
|
||||||
|
#--列表标题颜色,BGR 十六进制
|
||||||
|
header_color=00bfff
|
||||||
|
|
||||||
|
#--列表的标题文本大小
|
||||||
|
header_scale=100
|
||||||
|
|
||||||
|
#--列表标题的黑色边框大小
|
||||||
|
header_border=0.6
|
||||||
|
|
||||||
|
#--要显示为列表标题的文本
|
||||||
|
#--可用标头变量:%cursor%, %total%, %highlight%, %filter%, %search%, %listduration%, %listlength%, %listremaining%
|
||||||
|
#--仅在触发变量时显示的用户定义文本:%prefilter%, %afterfilter%, %prehighlight%, %afterhighlight% %presearch%, %aftersearch%, %prelistduration%, %afterlistduration%, %prelistlength%, %afterlistlength%, %prelistremaining%, %afterlistremaining%
|
||||||
|
#--变量说明:%cursor:显示列表中光标位置的数量。%total:当前列表中的项目总数。%highlight%:突出显示的项目总数。%filter:显示筛选器名称,%search:显示键入的搜索。仅在触发用户变量时才显示的用户定义文本示例:%prefilter:显示筛选器之前的用户定义文本,%afterfilter:显示筛选器后的用户定义文本
|
||||||
|
|
||||||
|
header_text=⌛ 历史菜单 [%cursor%/%total%]%prehighlight%%highlight%%afterhighlight%%prelistduration%%listduration%%afterlistduration%%prefilter%%filter%%afterfilter%%presort%%sort%%aftersort%%presearch%%search%%aftersearch%
|
||||||
|
|
||||||
|
#--指定使用 %sort% 变量时从标头中隐藏的排序方法
|
||||||
|
header_sort_hide_text=added-asc
|
||||||
|
|
||||||
|
#--设置在标头中触发变量之前或之后显示的文本
|
||||||
|
header_sort_pre_text= \{
|
||||||
|
header_sort_after_text=}
|
||||||
|
header_filter_pre_text= [Filter:
|
||||||
|
header_filter_after_text=]
|
||||||
|
header_search_pre_text=\h\N[Search=
|
||||||
|
header_search_after_text=..]
|
||||||
|
header_highlight_pre_text=✅
|
||||||
|
header_highlight_after_text=
|
||||||
|
header_list_duration_pre_text= 🕒
|
||||||
|
header_list_duration_after_text=
|
||||||
|
header_list_length_pre_text= 🕒
|
||||||
|
header_list_length_after_text=
|
||||||
|
header_list_remaining_pre_text= 🕒
|
||||||
|
header_list_remaining_after_text=
|
||||||
|
|
||||||
|
####-----时间格式设置-----####
|
||||||
|
##--在第一个参数中,您可以从可用样式中定义:default, hms, hms-full, timestamp, timestamp-concise。"default"以 HH:MM:SS.sss 格式显示。"hms"以 1h 2m 3.4s 格式显示。"hms-full"与 hms 相同,但当小时和分钟为 0 时保持恒定。"timestamp"将总时间显示为时间戳 123456.700 格式。"timestamp-concise"以 123456.7 格式显示总时间(根据可用性显示和隐藏小数)。
|
||||||
|
##--在第二个参数中,您可以定义是显示毫秒、舍入毫秒还是截断毫秒。可用选项:'truncate'以删除毫秒并保留秒数。0 删除毫秒并将秒舍入。1 或大于是要显示的毫秒数。默认值为 3 毫秒。
|
||||||
|
##--在第三个参数中,您可以在 hour:minute:second(小时:分钟:秒) 之间定义分隔符。"default"样式自动设置为":","hms","hms-full"自动设置为" "。您可以定义自己的。一些例子: ["default",3, "-"],["hms-full",5, "."],["hms", "truncate", ":"],["timestamp-concise"],["timestamp", ["timestamp",0],["timestamp", "truncate"],["timestamp",5]
|
||||||
|
|
||||||
|
osd_time_format=["default", "truncate"]
|
||||||
|
list_time_format=["default", "truncate"]
|
||||||
|
header_duration_time_format=["hms", "truncate", ":"]
|
||||||
|
header_length_time_format=["hms", "truncate", ":"]
|
||||||
|
header_remaining_time_format=["hms", "truncate", ":"]
|
||||||
|
|
||||||
|
|
||||||
|
####------列出键绑定设置------####
|
||||||
|
#--在下面(逗号后)添加要绑定的任何其他键绑定。或者更改引号内的字母以更改键绑定
|
||||||
|
#--更改和添加键绑定的示例:--从 ["b", "B"] 到 ["b"]. --从 [""] 到 ["alt+b"]. --从 [""] 到 ["a" "ctrl+a", "alt+a"]
|
||||||
|
|
||||||
|
#--键绑定,将用于在列表中向上导航
|
||||||
|
list_move_up_keybind=["UP", "WHEEL_UP"]
|
||||||
|
|
||||||
|
#--键绑定,将用于在列表中向下导航
|
||||||
|
list_move_down_keybind=["DOWN", "WHEEL_DOWN"]
|
||||||
|
|
||||||
|
#--键绑定,将用于转到列表上显示的页面的第一项
|
||||||
|
list_page_up_keybind=["PGUP"]
|
||||||
|
|
||||||
|
#--键绑定,将用于转到列表上显示的页面的最后一项
|
||||||
|
list_page_down_keybind=["PGDWN"]
|
||||||
|
|
||||||
|
#--键绑定,将用于导航到列表中的第一项
|
||||||
|
list_move_first_keybind=["HOME"]
|
||||||
|
|
||||||
|
#--密钥绑定,将用于导航到列表中的最后一项
|
||||||
|
list_move_last_keybind=["END"]
|
||||||
|
|
||||||
|
#--按键绑定,用于在按下导航键绑定时突出显示,按住 shift,然后按任何导航键绑定,例如:up, down, home, pgdwn 等。
|
||||||
|
list_highlight_move_keybind=["SHIFT"]
|
||||||
|
|
||||||
|
#--键绑定,将用于突出显示列表中所有显示的项目
|
||||||
|
list_highlight_all_keybind=["ctrl+a"]
|
||||||
|
|
||||||
|
#--键绑定,将用于从列表中删除所有当前突出显示的项目
|
||||||
|
list_unhighlight_all_keybind=["ctrl+d"]
|
||||||
|
|
||||||
|
#--键绑定,将用于根据光标位置加载条目
|
||||||
|
list_select_keybind=["ENTER", "MBTN_MID"]
|
||||||
|
|
||||||
|
#--键绑定,将用于根据光标位置向播放列表添加条目
|
||||||
|
list_add_playlist_keybind=["CTRL+ENTER"]
|
||||||
|
|
||||||
|
#--键绑定,将用于将所有突出显示的条目添加到播放列表
|
||||||
|
list_add_playlist_highlighted_keybind=["SHIFT+ENTER"]
|
||||||
|
|
||||||
|
#--将用于关闭列表的键绑定(如果搜索打开,则首先关闭搜索)
|
||||||
|
list_close_keybind=["ESC", "MBTN_RIGHT"]
|
||||||
|
|
||||||
|
#--键绑定,将用于根据光标位置删除条目
|
||||||
|
list_delete_keybind=["DEL"]
|
||||||
|
|
||||||
|
#--密钥绑定,将用于从列表中删除所有突出显示的条目
|
||||||
|
list_delete_highlighted_keybind=["SHIFT+DEL"]
|
||||||
|
|
||||||
|
#--将用于触发搜索的密钥绑定
|
||||||
|
list_search_activate_keybind=["ctrl+f"]
|
||||||
|
|
||||||
|
#--键绑定,将用于在保持搜索打开的同时退出搜索的键入模式
|
||||||
|
list_search_not_typing_mode_keybind=["ALT+ENTER"]
|
||||||
|
|
||||||
|
#--列表打开时忽略的键绑定
|
||||||
|
list_ignored_keybind=[""]
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
###不支持参数后注释,须另起一行
|
||||||
|
|
||||||
|
# -- 动态键位绑定(对应五个不同的信息页)
|
||||||
|
#key_page_1=1
|
||||||
|
#key_page_2=2
|
||||||
|
#key_page_3=3
|
||||||
|
#key_page_4=4
|
||||||
|
#key_page_0=0
|
||||||
|
|
||||||
|
# -- 动态键位绑定(部分页面支持上下翻页)
|
||||||
|
#key_scroll_up=UP
|
||||||
|
#key_scroll_down=DOWN
|
||||||
|
##设置滚动一次的行数。默认值:1
|
||||||
|
#scroll_lines=1
|
||||||
|
|
||||||
|
##短暂显示的持续时间(秒)。默认值:4
|
||||||
|
#duration=4
|
||||||
|
##常驻显示的数据刷新间隔(秒),设为 0 会有 bug。默认值:1
|
||||||
|
#redraw_delay=1
|
||||||
|
##文本格式化(ASS)。默认值:yes
|
||||||
|
##ass_formatting=yes
|
||||||
|
##禁止其它 OSD 文本覆盖 stats 信息。默认值:no
|
||||||
|
#persistent_overlay=yes
|
||||||
|
##设为 yes 将输出传递的完整信息。默认值:no
|
||||||
|
#print_perfdata_passes=no
|
||||||
|
##如果过滤器列表的长度超过这个数目,则每行显示一个过滤器。默认值:128
|
||||||
|
#filter_params_max_length=128
|
||||||
|
##启用调试输出。默认值:100
|
||||||
|
#debug=no
|
||||||
|
|
||||||
|
# -- 图形选项和样式
|
||||||
|
##显示性能数据的图表。默认值:no
|
||||||
|
#plot_perfdata=yes
|
||||||
|
##显示垂直同步和抖动值的图形(仅在统计信息常驻显示时)。默认值:no
|
||||||
|
#plot_vsync_ratio=yes
|
||||||
|
#plot_vsync_jitter=yes
|
||||||
|
##显示缓存值图表(第 3 页),仅在切换时显示。默认值:no
|
||||||
|
#plot_cache=no
|
||||||
|
##自动启用色调映射 LUT 可视化(仅在统计信息常驻显示时)。默认值:no
|
||||||
|
#plot_tonemapping_lut=no
|
||||||
|
|
||||||
|
#skip_frames=5
|
||||||
|
#global_max=yes
|
||||||
|
##切换时清除数据缓冲区。默认值:yes
|
||||||
|
#flush_graph_data=yes
|
||||||
|
#plot_bg_border_width=1.25
|
||||||
|
#plot_bg_border_color=0000FF
|
||||||
|
#plot_bg_color=262626
|
||||||
|
#plot_color=FFFFFF
|
||||||
|
|
||||||
|
##指定是否使用视频缩放文本和图形。no 尝试保持大小不变,auto 使用 OSD 缩放文本和图形
|
||||||
|
##而 OSD 使用 window 或保持恒定大小,具体取决于 --osd-scale-by-window 选项。默认值:auto
|
||||||
|
#vidscale=auto
|
||||||
|
|
||||||
|
# -- 字体相关设定
|
||||||
|
font=Noto Sans CJK SC,Noto Color Emoji
|
||||||
|
font_mono=Noto Sans CJK SC,Noto Color Emoji
|
||||||
|
##字体大小,默认 20
|
||||||
|
#font_size=20
|
||||||
|
##字体颜色
|
||||||
|
font_color=FFFFFF
|
||||||
|
##字体边框粗细,默认 1.2
|
||||||
|
border_size=1.5
|
||||||
|
##字体边框颜色,默认 262626
|
||||||
|
border_color=000000
|
||||||
|
shadow_x_offset=0.1
|
||||||
|
shadow_y_offset=0.1
|
||||||
|
shadow_color=000000
|
||||||
|
##<0-99> 字体透明度,似乎是百分比,默认 11
|
||||||
|
alpha=0
|
||||||
|
|
||||||
|
# -- 自定义标头,用于设置 ASS 标签的文本输出样式
|
||||||
|
# -- 指定此参数将忽略上面的文本样式值并使用这个字符串代替
|
||||||
|
#custom_header=
|
||||||
|
|
||||||
|
# -- 文本格式(ASS)
|
||||||
|
#ass_nl=\\N
|
||||||
|
#ass_indent=\\h\\h\\h\\h\\h
|
||||||
|
#ass_prefix_sep=\\h\\h
|
||||||
|
#ass_b1={\\b1}
|
||||||
|
#ass_b0={\\b0}
|
||||||
|
#ass_it1={\\i1}
|
||||||
|
#ass_it0={\\i0}
|
||||||
|
# -- Without ASS
|
||||||
|
#no_ass_nl=\n
|
||||||
|
#no_ass_indent=\t
|
||||||
|
#no_ass_prefix_sep=
|
||||||
|
#no_ass_b1=\027[1m
|
||||||
|
#no_ass_b0=\027[0m
|
||||||
|
#no_ass_it1=\027[3m
|
||||||
|
#no_ass_it0=\027[0m
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"alang": "*",
|
||||||
|
"slang": ["chs", "sc", "zho?%-cn", "zho?%-hans", "cht", "tc", "zho?%-hant", "zho?%-tw", "zho?%-hk", "zho?%-", "chi", "zho?", "und"],
|
||||||
|
"whitelist": ["chs&j[ap]n?"],
|
||||||
|
"condition": "sub.codec ~= 'null'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["sc&j[ap]n?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["cht&j[ap]n?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["ch&j[ap]n?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["tc&j[ap]n?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["zh&j[ap]n?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["chs&eng?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["sc&eng?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["cht&eng?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["ch&eng?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["tc&eng?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["zh&eng?"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["中日"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["中英"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["中上英下"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["简英"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["双语"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["特效"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["简体&英文"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["繁英"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["繁体&英文"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["繁體&英文"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["chs"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["sc"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["cn"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["hans"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["cht"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"blacklist": ["dutch"],
|
||||||
|
"whitelist": ["tc"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["hant"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["hk"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["tw"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["简"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["中"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["繁"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alang": "*",
|
||||||
|
"slang": "und",
|
||||||
|
"blacklist": [ "sign", "song", "comment", "danmaku", "danmu", "xml" ],
|
||||||
|
"condition": "sub.codec ~= 'null'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alang": "*",
|
||||||
|
"slang": ["chi", "zho?", "und"],
|
||||||
|
"whitelist": ["simplified"],
|
||||||
|
"condition": "sub.codec ~= 'null'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"whitelist": ["traditional"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alang": "*",
|
||||||
|
"slang": "zho?%-cn",
|
||||||
|
"condition": "sub.codec ~= 'null'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "zho?%-hans"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "zho?%-hant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "zho?%-hk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "zho?%-tw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "zho?%-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "chi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "zho?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "forced"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "j[ap]n?",
|
||||||
|
"blacklist": [ "sign", "song" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inherit": "^",
|
||||||
|
"slang": "eng?",
|
||||||
|
"blacklist": [ "sign", "song" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# API token, 可以在 https://assrt.net 上注册账号后在个人界面获取
|
||||||
|
#示例为脚本内预设的 key
|
||||||
|
#api_token=tNjXZUnOJWcHznHDyalNMYqqP6IdDdpQ
|
||||||
|
# 是否使用 https,默认 yes
|
||||||
|
#use_https=no
|
||||||
|
# 代理设置
|
||||||
|
#proxy=
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#ffmpeg 所在绝对路径,或者放入环境变量
|
||||||
|
ffmpeg_path=ffmpeg
|
||||||
|
#指定脚本在 OSD 和控制台显示的文本使用的语言,eng=English, chs=Chinese。默认值:eng
|
||||||
|
language=chs
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#######################################################
|
||||||
|
## Default configuration file for mpv-sub-select ##
|
||||||
|
## https://github.com/CogentRedTester/mpv-sub-select ##
|
||||||
|
#######################################################
|
||||||
|
|
||||||
|
# 强制启用脚本
|
||||||
|
#! 注意:这不会覆盖 sid 选项的显式指定
|
||||||
|
force_enable=yes
|
||||||
|
|
||||||
|
# 基于偏好 json 文件的实验性音轨选择
|
||||||
|
select_audio=no
|
||||||
|
|
||||||
|
#observe 音频开关,并在 alang 更改时重新选择字幕
|
||||||
|
observe_audio_switches=yes
|
||||||
|
|
||||||
|
# 仅选择用字幕轨道中明确说明的强制字幕。
|
||||||
|
# 默认情况下,在搜索特定语言的字幕轨道时,
|
||||||
|
# 强制字幕将包含在搜索结果中,并与其他曲目相同。
|
||||||
|
# 这意味着没有办法编写专门排除强制字幕轨道的规则
|
||||||
|
# 通过启用强制字幕,除非有规则在`slang`中明确包含"forced",否则永远不会选择强制字幕
|
||||||
|
explicit_forced_subs=no
|
||||||
|
|
||||||
|
# 指定包含 "sub-select.json" 文件的文件夹
|
||||||
|
config=~~/script-opts
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Socket 路径 (留空自动设置)
|
||||||
|
socket=
|
||||||
|
|
||||||
|
# 缩略图缓存路径 (留空自动设置)
|
||||||
|
thumbnail=
|
||||||
|
|
||||||
|
# 最大缩略图大小(以像素为单位)(缩小以适合)
|
||||||
|
# 当启用 hidpi 时此值会自适应缩放
|
||||||
|
max_height=200
|
||||||
|
max_width=200
|
||||||
|
|
||||||
|
# 叠加 ID
|
||||||
|
overlay_id=42
|
||||||
|
|
||||||
|
# 在文件加载时生成缩略图器,以更快地获得初始缩略图。默认禁用
|
||||||
|
spawn_first=no
|
||||||
|
|
||||||
|
# 是否退出超时未活动的缩略图进程(秒),默认 0 即禁用
|
||||||
|
quit_after_inactivity=0
|
||||||
|
|
||||||
|
# 在网络播放时启用。默认禁用
|
||||||
|
network=no
|
||||||
|
|
||||||
|
# 在音频播放时启用。默认禁用
|
||||||
|
audio=no
|
||||||
|
|
||||||
|
# 启用硬件解码生成缩略图。默认禁用
|
||||||
|
# 注意:硬解在高端显卡上可以加速生成缩略图,但在低端显卡上可能会出问题
|
||||||
|
hwdec=yes
|
||||||
|
|
||||||
|
# 仅限 Windows:使用原生 Windows API 写入管道(需要 LuaJIT)。默认禁用
|
||||||
|
direct_io=yes
|
||||||
|
|
||||||
|
# 指定 mpv 可执行文件的自定义路径。默认:mpv
|
||||||
|
mpv_path=mpv
|
||||||
|
|
||||||
|
# 指定需要忽略的视频扩展名黑名单,这些文件无法正常生成缩略图
|
||||||
|
blacklist_ext=bdmv,ifo
|
||||||
|
|
||||||
|
## 指定需忽略的共享盘(挂载盘)的路径/目录(缩略图生成性能差)
|
||||||
|
## windows 示例:excluded_dir=["X:", "Z:", "F:/Download/", "Download"]
|
||||||
|
#excluded_dir=[]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Options are slash-separated lists of words and languages
|
||||||
|
#是否启用脚本,默认 yes
|
||||||
|
enabled=yes
|
||||||
|
#同步选择曲目,与其他脚本一起更好地工作,默认 yes
|
||||||
|
hook=yes
|
||||||
|
#模仿 mpv 的曲目列表信息,以保留用户在文件中选择的曲目,默认 no
|
||||||
|
#注意此项必须设为 no,否则脚本功能无法真正生效
|
||||||
|
fingerprint=no
|
||||||
|
#覆盖用户的显式曲目选择,默认 no
|
||||||
|
force=no
|
||||||
|
#如果 mpv 无法做到这一点,请尝试重新选择最后一个轨道,例如当信息更改时,默认 no
|
||||||
|
smart_keep=no
|
||||||
|
## 指定需忽略的特殊协议
|
||||||
|
special_protocols=["://", "^magnet:"]
|
||||||
|
|
||||||
|
#视频轨道筛选项设置
|
||||||
|
#preferred_video_lang=
|
||||||
|
#excluded_video_words=
|
||||||
|
#expected_video_words=
|
||||||
|
|
||||||
|
#音频轨道筛选项设置
|
||||||
|
preferred_audio_lang=japanese/jpn/jap/ja/jp/english/eng/en
|
||||||
|
preferred_audio_channels=8/6/3/2
|
||||||
|
excluded_audio_words=commentary/cast/staff/dub/guide
|
||||||
|
expected_audio_words=
|
||||||
|
|
||||||
|
#字幕轨道筛选项设置
|
||||||
|
#preferred_sub_lang=chs/sc/zh-CN/zh-Hans/cht/tc/zh-Hant/zh-HK/zh-TW/chi/zh
|
||||||
|
#excluded_sub_words=
|
||||||
|
#expected_sub_words=中日/中英/中上英下/双语/特效/简/繁/中
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# 时间轴中当前位置的显示样式。可用:line, bar
|
||||||
|
timeline_style=bar
|
||||||
|
# 时间线(line)宽度(窗口/全屏模式)
|
||||||
|
timeline_line_width=2
|
||||||
|
# 进度条完全展开时的时间轴大小,以像素为单位,0 表示禁用
|
||||||
|
timeline_size=30
|
||||||
|
# 背景颜色的顶部边框,有助于在视觉上将时间轴与视频分开
|
||||||
|
timeline_border=1
|
||||||
|
# 指定在时间线上使用鼠标滚轮时,跳转的步进秒数。默认:5
|
||||||
|
# 默认使用快速查找。添加 '!' 后缀以启用精确查找。示例:'5!'
|
||||||
|
timeline_step=5
|
||||||
|
# 是否在时间轴上显示网络内容的渲染缓存指标,默认:yes
|
||||||
|
timeline_cache=yes
|
||||||
|
# 设置时间轴应始终可见的状态。使用逗号分隔,可用:paused, audio, image, video, idle, windowed, fullscreen
|
||||||
|
timeline_persistency=
|
||||||
|
# 设置何时显示始终可见的进度条(最小化时间线)。可以是:windowed(默认值), fullscreen, always, never
|
||||||
|
# 也可以使用 `toggle-progress` 命令按需切换
|
||||||
|
progress=windowed
|
||||||
|
progress_size=2
|
||||||
|
progress_line_width=10
|
||||||
|
|
||||||
|
# 以逗号分隔的项列表,用于构造时间轴上方的控制栏。设置为`never`以禁用
|
||||||
|
# 参数规范:括在`{}`中表示值,括在`[]`中表示可选
|
||||||
|
# 完整的条目语法:`[<[!]{disposition1}[,[!]{dispositionN}]>]{element}[:{paramN}][#{badge}[>{limit}]][?{tooltip}]`
|
||||||
|
#
|
||||||
|
# 常用属性参考:
|
||||||
|
# `{icon}` 指定图标名称的参数(例如 face)这里查询所有可用的值 https://fonts.google.com/icons?icon.platform=web&icon.set=Material+Icons&icon.style=Rounded
|
||||||
|
# `{element}`的参数及介绍:
|
||||||
|
# `{shorthand}` - 以下的可用值都是预配置好的快捷指令,可作为按钮:
|
||||||
|
# play-pause(播放/暂停)menu(菜单)subtitles(字幕轨列表)audio(音轨列表)video(视频轨列表)playlist(播放列表)chapters(章节列表)editions(版本列表)
|
||||||
|
# stream-quality(流式传输品质偏好)open-file(文件浏览器)items(播放列表/文件浏览器)
|
||||||
|
# next(跳转下一个)prev(上一个)first(首位)last(末位)audio-device(音频输出设备列表)
|
||||||
|
# fullscreen(切换全屏)loop-playlist(切换列表循环)loop-file(切换单曲循环)shuffle(切换乱序播放)autoload(自动加载文件)
|
||||||
|
#
|
||||||
|
# speed[:{scale}] (速度滑块 其中控件系数的尺寸,默认 1.3)
|
||||||
|
# command:{icon}:{command} (按下该按钮时执行的指令)
|
||||||
|
# toggle:{icon}:{prop} (切换 mpv 属性的按钮)
|
||||||
|
#
|
||||||
|
# cycle:{default_icon}:{prop}:{value1}[={icon1}][!]/{valueN}[={iconN}][!]
|
||||||
|
# 在不同 mpv 属性的值之间循环的按钮,每个值都可以选择不同的 {icon} 和激活标记。结尾处赋予可选的半角感叹号,将使该按钮成为可激活的样式。
|
||||||
|
#
|
||||||
|
# gap[:{scale}] (留出一个间隔 其中系数的尺寸,默认 0.3)
|
||||||
|
# space(填补上两个控件之间的所有空间,对于将条目右对齐非常有用。用多个 space 可在它们之间分配空间,可用于居中对齐)
|
||||||
|
#
|
||||||
|
# 控件条目的可见性控制:
|
||||||
|
## `<[!]{disposition1}[+[!]{dispositionN}][,{more_dispositions}]>` - 可选的前缀是用于控制 {element} 的可见性
|
||||||
|
# - `+` 创建 AND 条件,`,` 拆分为 OR 组。示例:`<foo,bar+baz>` -> `foo OR (bar AND baz)`
|
||||||
|
# - `{disposition}` 的可用值:
|
||||||
|
## idle 如果 mpv 处于空闲状态则为 true
|
||||||
|
## image 如果当前文件为单帧图片则为 true
|
||||||
|
## audio 如果当前文件为纯音频则为 true
|
||||||
|
## video 如果当前文件存在视频轨则为 true
|
||||||
|
## has_many_video 如果当前文件存在多个视频轨则为 true
|
||||||
|
## has_image 如果当前文件带有封面或其他图像轨道则为 true
|
||||||
|
## has_audio 如果当前文件存在音轨则为 true
|
||||||
|
## has_many_audio 如果当前文件存在多个音轨则为 true
|
||||||
|
## has_sub 如果当前文件存在字幕轨则为 true
|
||||||
|
## has_many_sub 如果当前文件存在多个字幕轨则为 true
|
||||||
|
## has_many_edition 如果当前文件存在多个版本则为 true
|
||||||
|
## has_chapter 如果当前文件存在章节则为 true
|
||||||
|
## stream 如果当前文件为流则为 true
|
||||||
|
## has_playlist 如果当前列表存在多个文件则为 true
|
||||||
|
## {mpv_prop} 任何 mpv 属性(注意:可以在脚本中设置 `user-data/foo` 以添加自定义属性)
|
||||||
|
## 可选的`!`前缀可用来反转所需的处理方式
|
||||||
|
## 示例:
|
||||||
|
## - `<stream>stream-quality` - 仅对流媒体显示“流式传输品质偏好”按钮
|
||||||
|
## - `<has_audio,!audio>audio` - 对所有存在音轨的文件显示“音轨列表”按钮,但不包括纯音频的文件
|
||||||
|
#
|
||||||
|
# 将 `#{badge}[>{limit}]` 放在 `{element}` 参数后,可赋予它一个徽章标记。可用的 `badge` 值:
|
||||||
|
## `sub`, `audio`, `video` - 轨道计数值
|
||||||
|
## `{mpv_prop}` - 如果 mpv 的属性值是一个数组,将显示其大小。可用的属性参见:https://mpv.io/manual/master/#property-list
|
||||||
|
## `>{limit}` 只有当它的数值高于此阈值时,才会显示徽章标记
|
||||||
|
## 示例:`#audio>1`
|
||||||
|
#
|
||||||
|
# 将 `?{tooltip}` 放在 `{element}` 的设置后面,赋予它一个工具提示
|
||||||
|
## 示例:`<stream>stream-quality?Stream quality`
|
||||||
|
#
|
||||||
|
# 一些可用的快捷指令的实现示范:
|
||||||
|
## menu = command:menu:script-binding uosc/menu-blurred?Menu
|
||||||
|
## subtitles = command:subtitles:script-binding uosc/subtitles#sub>1?Subtitles
|
||||||
|
## fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen
|
||||||
|
## loop-playlist = cycle:repeat:loop-playlist:no/inf!?Loop playlist
|
||||||
|
## toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes!
|
||||||
|
controls=menu,open-file,<idle>command:history:script-binding recentmenu/open?最近播放,<idle>command:bookmarks:script-binding simplebookmark/open-list?书签菜单,<idle>command:file_copy:script-binding smartcopypaste_II/open-list?剪贴菜单,gap,<!idle>command:analytics:script-binding stats/display-stats-toggle?统计,<stream>stream-quality,<audio,has_image>command:image:script-binding uosc/video#video?封面,<has_many_edition>editions,<video>video,<has_audio+!audio>audio,<video,audio>subtitles,<has_chapter>chapters,space,<has_chapter>command:skip_previous:add chapter -1?上一章节,<video,audio>speed,<has_chapter>command:skip_next:add chapter 1?下一章节,space,loop-file,shuffle,loop-playlist,gap,<has_playlist>prev,<has_playlist>playlist,<has_playlist>next,gap,audio-device,gap,fullscreen
|
||||||
|
controls_size=32
|
||||||
|
controls_margin=8
|
||||||
|
controls_spacing=2
|
||||||
|
controls_persistency=idle
|
||||||
|
|
||||||
|
# 显示音量控制的位置:none, left, right
|
||||||
|
# 设置音量控制条的样式
|
||||||
|
volume=right
|
||||||
|
volume_size=40
|
||||||
|
volume_border=1
|
||||||
|
volume_step=1
|
||||||
|
volume_persistency=
|
||||||
|
|
||||||
|
# 播放速度小部件:鼠标拖动或滚轮更改,单击以重置
|
||||||
|
speed_step=0.1
|
||||||
|
speed_step_is_factor=no
|
||||||
|
speed_persistency=
|
||||||
|
|
||||||
|
# 控制所有菜单,如上下文菜单,字幕加载器/选择器等
|
||||||
|
menu_item_height=36
|
||||||
|
menu_min_width=260
|
||||||
|
menu_padding=1
|
||||||
|
# 确定激活搜索需要`/`或`ctrl+f`或者是否键入任何文本就足够了
|
||||||
|
# 启用后,如果菜单是 unicode 字符,则无法再使用打开菜单的相同键关闭菜单
|
||||||
|
menu_type_to_search=no
|
||||||
|
|
||||||
|
|
||||||
|
# 顶栏的显隐逻辑(仅在无边框和全屏模式下显示),默认 no-border 其它可用的值:never, always
|
||||||
|
top_bar=no-border
|
||||||
|
top_bar_size=40
|
||||||
|
# 启用顶栏的右侧控制按钮,示例即默认值
|
||||||
|
top_bar_controls=yes
|
||||||
|
# 可以是:`no` (隐藏), `yes` (从 mpv.conf 继承标题), 或自定义模板字符串
|
||||||
|
top_bar_title=yes
|
||||||
|
# 使用模板字符串以启用替代顶部栏标题。如果替代标题与主标题匹配,它将被隐藏
|
||||||
|
# 提示:使用 '${media-title}' 表示主标题,使用 '${filename}' 表示替代标题
|
||||||
|
top_bar_alt_title=${?media-title:${media-title}}
|
||||||
|
# 可以是:
|
||||||
|
# `below` => 在主标题下方显示替代标题
|
||||||
|
# `toggle` => 通过单击顶部栏在主标题和 alt 之间切换顶部栏标题文本,或调用`toggle-title`绑定
|
||||||
|
top_bar_alt_title_place=below
|
||||||
|
# 加载以下类型的内容时闪烁顶部栏。可用:audio,image,video,chapter。默认:video,audio,特殊值 none 禁用闪烁
|
||||||
|
top_bar_flash_on=video,audio
|
||||||
|
top_bar_persistency=
|
||||||
|
|
||||||
|
# 边框模式下绘制的内边框和透明度
|
||||||
|
window_border_size=1
|
||||||
|
|
||||||
|
# 如果没有播放列表和文件结束,加载目录中符合 load_types 选项指定的下一个文件,默认:no
|
||||||
|
# 当该选项被启用时 uosc 将主动设置 mpv 选项状态:`keep-open=yes`;`keep-open-pause=no`
|
||||||
|
autoload=no
|
||||||
|
# 启用播放列表/目录导航随机播放,默认:no
|
||||||
|
# 这是实现按预期工作的洗牌所必需的,包括目录导航,因为 mpv 内置的“随机播放”选项基本上无法使用
|
||||||
|
shuffle=no
|
||||||
|
|
||||||
|
# 指定 UI 的缩放比例,请参考显示器的 HIDPI 缩放比例
|
||||||
|
scale=1
|
||||||
|
# 指定 UI 在全屏时的缩放比例。默认值:1.3
|
||||||
|
scale_fullscreen=1.3
|
||||||
|
# 自定义 UI 要使用的字体,推荐使用等宽字体以获得更好的显示效果。默认留空以使用'osd-font'
|
||||||
|
# font=Noto Sans CJK SC,Noto Color Emoji
|
||||||
|
# 调整文字缩放以适合您的字体
|
||||||
|
font_scale=1
|
||||||
|
# 指定直接在视频顶部绘制时文本和图标的边框。默认值:1.2
|
||||||
|
text_border=1.2
|
||||||
|
# 指定按钮、菜单和所有其他矩形的边框半径。默认值:4
|
||||||
|
border_radius=2
|
||||||
|
# 设置以逗号分隔的颜色覆盖列表,采用 RGB 十六进制格式:`rrggbb`
|
||||||
|
# 默认值: foreground=ffffff,foreground_text=000000,background=000000,background_text=ffffff,curtain=111111,match=69c5ff
|
||||||
|
color=foreground=FFFBFE,foreground_text=1C1B1F,background=1C1B1F,background_text=FFFBFE
|
||||||
|
# 设置以逗号分隔的不透明度覆盖列表,用于各种 UI 元素背景和形状。文本始终为 100%
|
||||||
|
# 示例: opacity=timeline=0.5,title=0.5
|
||||||
|
# 默认: timeline=0.9,position=1,chapters=0.8,slider=0.9,slider_gauge=1,controls=0,speed=0.6,menu=1,submenu=0.4,border=1,title=1,
|
||||||
|
# tooltip=1,thumbnail=1,curtain=0.8,idle_indicator=0.8,audio_indicator=0.5,buffering_indicator=0.3,playlist_position=0.8
|
||||||
|
opacity=menu=0.9,submenu=0.7,curtain=0.5
|
||||||
|
# 以逗号分隔的功能列表,以牺牲一些性能影响为代价进行优化。默认:空白
|
||||||
|
# text_width - 使用更准确的文本宽度测量,单独测量每个文本字符串而不是只测量一次已知字母的宽度并将它们相加
|
||||||
|
# sorting - 使用文件名排序可以更好地处理非英语语言,尤其是亚洲语言。目前,此功能仅在 Windows 上可用,对其他平台没有影响
|
||||||
|
refine=sorting
|
||||||
|
# 指定过渡动画的持续时间(以毫秒为单位)。默认值:100
|
||||||
|
animation_duration=100
|
||||||
|
# `flash-{element}`命令使用的闪存持续时间(以毫秒为单位)
|
||||||
|
flash_duration=1000
|
||||||
|
# 以像素为单位的距离,低于该像素的元素完全淡入/淡出
|
||||||
|
proximity_in=40
|
||||||
|
proximity_out=120
|
||||||
|
# 是否在整个 UI 中仅使用粗体字体粗细,默认:no
|
||||||
|
font_bold=no
|
||||||
|
# 指定按何种方式显示时间轴右侧时间:`total`, `playtime-remaining`(按当前速度缩放), `time-remaining` (文件的剩余长度)
|
||||||
|
destination_time=playtime-remaining
|
||||||
|
# 指定显示时间戳的亚秒级精度,精确等效到秒的小数点后的位数。默认 0
|
||||||
|
time_precision=3
|
||||||
|
# 在时间轴中显示流的缓冲时间,如果它低于此秒数,0 表示禁用
|
||||||
|
buffered_time_threshold=60
|
||||||
|
# 是否在 mpv 自动隐藏光标时隐藏 UI,默认:no
|
||||||
|
autohide=no
|
||||||
|
# 指定切换暂停状态时显示的指示器样式
|
||||||
|
# 可以是:none, flash, static, manual (由 flash-pause-indicator 和 decide-pause-indicator 命令控制)
|
||||||
|
pause_indicator=manual
|
||||||
|
# 指定在流质量菜单中列出的大小
|
||||||
|
stream_quality_options=4320,2160,1440,1080,720,480,360,240,144
|
||||||
|
# 指定导航媒体文件时要查找的文件类型
|
||||||
|
video_types=3g2,3gp,asf,avi,bdmv,f4v,flv,h264,h265,iso,ifo,m2ts,m4v,mkv,mov,mp4,mp4v,mpeg,mpg,ogm,ogv,rm,rmvb,ts,vob,webm,wmv,y4m
|
||||||
|
audio_types=aac,ac3,aiff,ape,au,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv
|
||||||
|
image_types=apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp
|
||||||
|
playlist_types=m3u,m3u8,pls,url,cue
|
||||||
|
# 指定加载外部字幕时要查找的文件类型
|
||||||
|
subtitle_types=aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,sbv,srt,ssa,ssf,ttxt,txt,usf,vt,vtt
|
||||||
|
# 自动加载或请求播放下一个文件时接受哪些类型作为下一个条目
|
||||||
|
# 可选:video,audio,image,playlist,same。特殊值 same 意味着只加载和当前文件属于同一类型的媒体文件
|
||||||
|
load_types=same
|
||||||
|
# 指定打开文件菜单时的默认目录,默认值:~/。使用特殊值 '{drives}' 打开 Windows 上的驱动器菜单(在 unix 上默认为 '/')
|
||||||
|
default_directory={drives}
|
||||||
|
# 读取目录时是否列出隐藏文件,默认:no。由于环境限制,这目前仅隐藏以'.'点开头的文件
|
||||||
|
# 不会隐藏 Windows 上的隐藏文件(我们无法判断它们是隐藏的)
|
||||||
|
show_hidden_files=no
|
||||||
|
# 删除文件时移动到回收站,默认:no
|
||||||
|
use_trash=yes
|
||||||
|
# 根据 UI 元素的可见性调整了 OSD 边距,默认:yes
|
||||||
|
adjust_osd_margins=no
|
||||||
|
|
||||||
|
|
||||||
|
# 将一些常见的章节类型转换为章节范围指标,该章节拥有的时间轴部分基于彩色
|
||||||
|
# 章节范围指标语法是逗号分隔的“{type}:{color}”
|
||||||
|
# `{type}` - 范围类型。目前支持有:
|
||||||
|
# - `openings` - 开场白和动画开场
|
||||||
|
# - `endings` - 外传和动画结局
|
||||||
|
# - `ads` - 由脚本创建的赞助商块:https://github.com/po5/mpv_sponsorblock
|
||||||
|
# `{color}` - 颜色代码,格式为 RRGGBB(AA)
|
||||||
|
#
|
||||||
|
# 要不转换任何范围类型,只需将其从列表中删除即可,全部留空即为不启用此特性
|
||||||
|
# 示例:chapter_ranges=openings:38869680,endings:38869680,ads:a5353580
|
||||||
|
chapter_ranges=openings:30abf964,endings:30abf964,intros:3fb95080,outros:3fb95080,ads:c54e4e80
|
||||||
|
# 补充额外的 lua 模式来识别章节范围的起始点(除`ads`外的所有章节)
|
||||||
|
# 语法:`{type}:{pattern}[,{patternN}][;{type}:{pattern}[,{patternN}]]`
|
||||||
|
# 目前可定义的章节范围`type`有:openings;endings;intros;outros
|
||||||
|
chapter_range_patterns=openings:^Intro%s*Start,オープニング$,^片头$,片头开始$;endings:^end$,^End$,エンディング$;intros:preview$,预告$,予告$;outros:credits$
|
||||||
|
|
||||||
|
# 指定本地化语言优先级,从高到低
|
||||||
|
# 内置语言可以在 'uosc/intl' 中找到。
|
||||||
|
# 'slang' 是从 '--slang' 属性继承值的关键字
|
||||||
|
# 支持自定义 json 文件的路径:`languages=~~/custom.json,slang,en`
|
||||||
|
languages=slang,en
|
||||||
|
|
||||||
|
# 默认情况下,将字幕下载到当前打开的文件的目录中
|
||||||
|
# 如果从 URL 播放文件,我们将使用此目录(扩展到 `{mpv_config_dir}/subtitles`)
|
||||||
|
# 在路径前加上`!`以强制将所有字幕保存在那里,示例:`!~~/subtitles`
|
||||||
|
subtitles_directory=~~/subtitles
|
||||||
|
|
||||||
|
# 指定要禁用的元素 ID 的列表,使用逗号分隔,默认留空。可用 ID:
|
||||||
|
# window_border, top_bar, timeline, controls, volume,
|
||||||
|
# idle_indicator, audio_indicator, buffering_indicator, pause_indicator
|
||||||
|
disable_elements=idle_indicator,audio_indicator
|
||||||
|
|
||||||
|
# 指定`ziggy`的可执行文件的路径。默认:default, 即 `~~/scripts/uosc/bin`
|
||||||
|
# 留空将在系统 PATH 中查找可执行文件,Windows 上还会在 mpv.exe 所在目录中查找
|
||||||
|
# ziggy_path=
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
## 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
|
||||||
|
api_server=https://api.dandanplay.net
|
||||||
|
## 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
|
||||||
|
## 服务器可以自托管:https://github.com/lyz05/danmaku
|
||||||
|
#fallback_server=https://fc.lyz05.cn
|
||||||
|
## 设置 tmdb 的 API Key,用于获取非动画条目的中文信息(当搜索内容非中文时)
|
||||||
|
## 可以在 https://www.themoviedb.org 注册后去个人账号设置界面获取
|
||||||
|
## 注意:自定义此参数时还需要对获取到的 API Key 进行 base64 编码
|
||||||
|
#tmdb_api_key=NmJmYjIxOTZkNzIyN2UyMTIzMGM3Y2YzZjQ4MDNkZGM=
|
||||||
|
## 加载更多来自弹幕服务器上第三方的弹幕
|
||||||
|
load_more_danmaku=yes
|
||||||
|
## 自动加载网络弹幕
|
||||||
|
auto_load=no
|
||||||
|
## 自动加载本地弹幕
|
||||||
|
autoload_local_danmaku=yes
|
||||||
|
## 为 URL 串流播放场景自动加载弹幕
|
||||||
|
autoload_for_url=yes
|
||||||
|
## 自动保存网络弹幕到本地(视频同目录下同名 xml 文件)
|
||||||
|
save_danmaku=yes
|
||||||
|
## 启用 fps 视频滤镜让弹幕滚动更平滑
|
||||||
|
##! 性能消耗较大
|
||||||
|
vf_fps=yes
|
||||||
|
## 设置要使用的 fps 滤镜参数
|
||||||
|
fps=60/1.001
|
||||||
|
## 指定合并重复弹幕的时间间隔的容差值,单位为秒。默认值:-1,表示禁用
|
||||||
|
merge_tolerance=0
|
||||||
|
## 限制同屏显示的最大弹幕数量,默认值 0 表示不限制
|
||||||
|
max_screen_danmaku=0
|
||||||
|
## 指定弹幕关联历史记录文件的路径,支持绝对路径和相对路径
|
||||||
|
history_path=~~/files/danmaku-history.json
|
||||||
|
|
||||||
|
## 中文简繁转换。0-不转换,1-转换为简体,2-转换为繁体
|
||||||
|
chConvert=1
|
||||||
|
## 滚动弹幕的显示时间。这会左右滚动速度
|
||||||
|
scrolltime=15
|
||||||
|
## 固定弹幕的显示时间
|
||||||
|
fixtime=5
|
||||||
|
## 字体
|
||||||
|
fontname=danmaku
|
||||||
|
## 字体大小
|
||||||
|
fontsize=24
|
||||||
|
## 透明度
|
||||||
|
opacity=0.6
|
||||||
|
## 粗体
|
||||||
|
bold=yes
|
||||||
|
## 描边 0-4
|
||||||
|
outline=1
|
||||||
|
## 阴影
|
||||||
|
shadow=0
|
||||||
|
## 全部弹幕的显示范围(0.0-1.0)
|
||||||
|
displayarea=0.85
|
||||||
|
|
||||||
|
## 指定弹幕屏蔽词文件路径(black.txt),支持绝对路径和相对路径。文件内容以换行分隔
|
||||||
|
## 支持 lua 的正则表达式写法
|
||||||
|
blacklist_path=~~/files/danmaku-blacklist.txt
|
||||||
|
## 自定义标题解析中的额外替换规则,内容格式为 JSON 字符串,替换模式为 lua 的 string.gsub 函数
|
||||||
|
##! 注意:由于 mpv 的 lua 版本限制,自定义规则只支持形如 %n 的捕获组写法,即示例用法,不支持直接替换字符的写法
|
||||||
|
title_replace=[{"rules":[{ "^〔(.-)〕": "%1"},{ "^.*《(.-)》": "%1" }]}]
|
||||||
|
## 指定哈希匹配中需忽略的共享盘(挂载盘)的路径/目录。支持绝对路径和相对路径,多个路径用逗号分隔
|
||||||
|
## 示例:["X:", "Z:", "F:/Download/", "Download"]
|
||||||
|
#excluded_path=
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
###不支持参数后注释,须另起一行
|
||||||
|
|
||||||
|
##首选尝试用 ytdl 解析 url(默认 no)。取决于 url 播放场景是否大多需要 ytdl 解析
|
||||||
|
try_ytdl_first=yes
|
||||||
|
|
||||||
|
##url 解析地址黑名单,格式解释见 https://mpv.io/manual/master/#options-exclude
|
||||||
|
##推荐在 try_ytdl_first=yes 的情况下使用,可合理加速网络地址的解析
|
||||||
|
exclude=%.avi$|%.flac$|%.flv$|%.mp3$|%.m3u$|%.m3u8$|%.m4a$|%.m4v$|%.mkv$|%.mp4$|%.ts$|%.VOB$|%.wav$|%.webm$|%.wmw$
|
||||||
|
|
||||||
|
##当 try_ytdl_first 为 no 时 mpv 应尝试优先使用 ytdl 解析的 URL 模式列表。匹配方式与 exclude 相同,示例为默认值
|
||||||
|
include=^%w+%.youtube%.com/|^youtube%.com/|^youtu%.be/|^%w+%.twitch%.tv/|^twitch%.tv/
|
||||||
|
|
||||||
|
##https://mpv.io/manual/master/#options-all-formats
|
||||||
|
#all_formats=no
|
||||||
|
#force_all_formats=yes
|
||||||
|
|
||||||
|
##允许切换源的轨道(出于性能原因默认禁用)
|
||||||
|
#use_manifests=no
|
||||||
|
|
||||||
|
##youtube-dl 最活跃的分支 yt-dlp 大有取代上游的趋势且开发活跃 https://github.com/yt-dlp/yt-dlp/releases
|
||||||
|
##须要 youtube-dl.exe / yt-dlp.exe 在环境变量 PATH 中,或位于 mpv.exe 所在路径的目录下
|
||||||
|
##yt-dlp.exe 更新命令:yt-dlp -U
|
||||||
|
#ytdl_path=yt-dlp
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
-- Runs write-watch-later-config periodically
|
||||||
|
|
||||||
|
local options = require 'mp.options'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
|
||||||
|
o = {
|
||||||
|
save_interval = 60,
|
||||||
|
percent_pos = 99,
|
||||||
|
}
|
||||||
|
options.read_options(o)
|
||||||
|
|
||||||
|
local can_delete = true
|
||||||
|
local can_save = true
|
||||||
|
local path = nil -- only set after file success load, reset to nil when file unload.
|
||||||
|
|
||||||
|
local function reset()
|
||||||
|
path = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set vars when file success load
|
||||||
|
local function init()
|
||||||
|
path = mp.get_property("path")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function save()
|
||||||
|
if not can_save then return end
|
||||||
|
local watch_later_list = mp.get_property("watch-later-options", {})
|
||||||
|
if mp.get_property_bool("save-position-on-quit") then
|
||||||
|
msg.debug("saving state")
|
||||||
|
if not watch_later_list:find("start") then
|
||||||
|
mp.commandv("change-list", "watch-later-options", "append", "start")
|
||||||
|
end
|
||||||
|
mp.command("write-watch-later-config")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function save_if_pause(_, pause)
|
||||||
|
if pause then save() end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pause_timer_while_paused(_, pause)
|
||||||
|
if pause then timer:stop() else timer:resume() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- save watch-later-config when file unloading
|
||||||
|
local function save_or_delete()
|
||||||
|
if not can_delete then return end
|
||||||
|
local eof = mp.get_property_bool("eof-reached")
|
||||||
|
local percent_pos = mp.get_property_number("percent-pos")
|
||||||
|
if eof or percent_pos and (percent_pos == 0 or percent_pos >= o.percent_pos) then
|
||||||
|
can_delete = true
|
||||||
|
if path ~= nil then
|
||||||
|
msg.debug("deleting state: percent_pos=0 or eof")
|
||||||
|
mp.commandv("delete-watch-later-config", path)
|
||||||
|
end
|
||||||
|
elseif path ~= nil then
|
||||||
|
save()
|
||||||
|
end
|
||||||
|
reset()
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message("skip-delete-state", function() can_delete = false end)
|
||||||
|
|
||||||
|
timer = mp.add_periodic_timer(o.save_interval, save)
|
||||||
|
mp.observe_property("pause", "bool", pause_timer_while_paused)
|
||||||
|
|
||||||
|
mp.observe_property("pause", "bool", save_if_pause)
|
||||||
|
|
||||||
|
mp.register_event("file-loaded", init)
|
||||||
|
mp.add_hook("on_unload", 50, save_or_delete) -- after mpv saving state
|
||||||
@@ -0,0 +1,610 @@
|
|||||||
|
-- This script automatically loads playlist entries before and after the
|
||||||
|
-- the currently played file. It does so by scanning the directory a file is
|
||||||
|
-- located in when starting playback. It sorts the directory entries
|
||||||
|
-- alphabetically, and adds entries before and after the current file to
|
||||||
|
-- the internal playlist. (It stops if it would add an already existing
|
||||||
|
-- playlist entry at the same position - this makes it "stable".)
|
||||||
|
-- Add at most 5000 * 2 files when starting a file (before + after).
|
||||||
|
|
||||||
|
--[[
|
||||||
|
To configure this script use file autoload.conf in directory script-opts (the "script-opts"
|
||||||
|
directory must be in the mpv configuration directory, typically ~/.config/mpv/).
|
||||||
|
|
||||||
|
Option `ignore_patterns` is a comma-separated list of patterns (see lua.org/pil/20.2.html).
|
||||||
|
Additionally to the standard lua patterns, you can also escape commas with `%`,
|
||||||
|
for example, the option `bak%,x%,,another` will be resolved as patterns `bak,x,` and `another`.
|
||||||
|
But it does not mean you need to escape all lua patterns twice,
|
||||||
|
so the option `bak%%,%。mp4,` will be resolved as two patterns `bak%%` and `%.mp4`.
|
||||||
|
|
||||||
|
Example configuration would be:
|
||||||
|
|
||||||
|
disabled=no
|
||||||
|
images=no
|
||||||
|
videos=yes
|
||||||
|
audio=yes
|
||||||
|
additional_image_exts=list,of,ext
|
||||||
|
additional_video_exts=list,of,ext
|
||||||
|
additional_audio_exts=list,of,ext
|
||||||
|
ignore_hidden=yes
|
||||||
|
same_type=yes
|
||||||
|
same_series=yes
|
||||||
|
directory_mode=recursive
|
||||||
|
ignore_patterns=^~,^bak-,%.bak$
|
||||||
|
|
||||||
|
--]]
|
||||||
|
|
||||||
|
MAXENTRIES = 5000
|
||||||
|
MAXDIRSTACK = 20
|
||||||
|
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local options = require 'mp.options'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
|
||||||
|
o = {
|
||||||
|
disabled = false,
|
||||||
|
images = true,
|
||||||
|
videos = true,
|
||||||
|
audio = true,
|
||||||
|
additional_image_exts = "",
|
||||||
|
additional_video_exts = "",
|
||||||
|
additional_audio_exts = "",
|
||||||
|
ignore_hidden = true,
|
||||||
|
same_type = false,
|
||||||
|
same_series = false,
|
||||||
|
directory_mode = "ignore",
|
||||||
|
ignore_patterns = ""
|
||||||
|
}
|
||||||
|
options.read_options(o, nil, function(list)
|
||||||
|
split_option_exts(list.additional_video_exts, list.additional_audio_exts, list.additional_image_exts)
|
||||||
|
if list.videos or list.additional_video_exts or
|
||||||
|
list.audio or list.additional_audio_exts or
|
||||||
|
list.images or list.additional_image_exts then
|
||||||
|
create_extensions()
|
||||||
|
end
|
||||||
|
if list.directory_mode then
|
||||||
|
validate_directory_mode()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
function Set (t)
|
||||||
|
local set = {}
|
||||||
|
for _, v in pairs(t) do set[v] = true end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
function SetUnion (a,b)
|
||||||
|
for k in pairs(b) do a[k] = true end
|
||||||
|
return a
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns first and last positions in string or past-to-end indices
|
||||||
|
function FindOrPastTheEnd (string, pattern, start_at)
|
||||||
|
local pos1, pos2 = string.find(string, pattern, start_at)
|
||||||
|
return pos1 or #string + 1,
|
||||||
|
pos2 or #string + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function Split (list)
|
||||||
|
local set = {}
|
||||||
|
|
||||||
|
local item_pos = 1
|
||||||
|
local item = ""
|
||||||
|
|
||||||
|
while item_pos <= #list do
|
||||||
|
local pos1, pos2 = FindOrPastTheEnd(list, "%%*,", item_pos)
|
||||||
|
|
||||||
|
local pattern_length = pos2 - pos1
|
||||||
|
local is_comma_escaped = pattern_length % 2
|
||||||
|
|
||||||
|
local pos_before_escape = pos1 - 1
|
||||||
|
local item_escape_count = pattern_length - is_comma_escaped
|
||||||
|
|
||||||
|
item = item .. string.sub(list, item_pos, pos_before_escape + item_escape_count)
|
||||||
|
|
||||||
|
if is_comma_escaped == 1 then
|
||||||
|
item = item .. ","
|
||||||
|
else
|
||||||
|
set[item] = true
|
||||||
|
item = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
item_pos = pos2 + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set[item] = true
|
||||||
|
|
||||||
|
-- exclude empty items
|
||||||
|
set[""] = nil
|
||||||
|
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
EXTENSIONS_VIDEO_DEFAULT = Set {
|
||||||
|
'3g2', '3gp', 'avi', 'flv', 'm2ts', 'm4v', 'mj2', 'mkv', 'mov',
|
||||||
|
'mp4', 'mpeg', 'mpg', 'ogv', 'rmvb', 'webm', 'wmv', 'y4m'
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTENSIONS_AUDIO_DEFAULT = Set {
|
||||||
|
'aiff', 'ape', 'au', 'flac', 'm4a', 'mka', 'mp3', 'oga', 'ogg',
|
||||||
|
'ogm', 'opus', 'wav', 'wma'
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTENSIONS_IMAGES_DEFAULT = Set {
|
||||||
|
'avif', 'bmp', 'gif', 'j2k', 'jp2', 'jpeg', 'jpg', 'jxl', 'png',
|
||||||
|
'svg', 'tga', 'tif', 'tiff', 'webp'
|
||||||
|
}
|
||||||
|
|
||||||
|
function split_option_exts(video, audio, image)
|
||||||
|
if video then o.additional_video_exts = Split(o.additional_video_exts) end
|
||||||
|
if audio then o.additional_audio_exts = Split(o.additional_audio_exts) end
|
||||||
|
if image then o.additional_image_exts = Split(o.additional_image_exts) end
|
||||||
|
end
|
||||||
|
split_option_exts(true, true, true)
|
||||||
|
|
||||||
|
function split_patterns()
|
||||||
|
o.ignore_patterns = Split(o.ignore_patterns)
|
||||||
|
end
|
||||||
|
split_patterns()
|
||||||
|
|
||||||
|
function create_extensions()
|
||||||
|
EXTENSIONS = {}
|
||||||
|
EXTENSIONS_VIDEO = {}
|
||||||
|
EXTENSIONS_AUDIO = {}
|
||||||
|
EXTENSIONS_IMAGES = {}
|
||||||
|
if o.videos then
|
||||||
|
SetUnion(SetUnion(EXTENSIONS_VIDEO, EXTENSIONS_VIDEO_DEFAULT), o.additional_video_exts)
|
||||||
|
SetUnion(EXTENSIONS, EXTENSIONS_VIDEO)
|
||||||
|
end
|
||||||
|
if o.audio then
|
||||||
|
SetUnion(SetUnion(EXTENSIONS_AUDIO, EXTENSIONS_AUDIO_DEFAULT), o.additional_audio_exts)
|
||||||
|
SetUnion(EXTENSIONS, EXTENSIONS_AUDIO)
|
||||||
|
end
|
||||||
|
if o.images then
|
||||||
|
SetUnion(SetUnion(EXTENSIONS_IMAGES, EXTENSIONS_IMAGES_DEFAULT), o.additional_image_exts)
|
||||||
|
SetUnion(EXTENSIONS, EXTENSIONS_IMAGES)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
create_extensions()
|
||||||
|
|
||||||
|
function validate_directory_mode()
|
||||||
|
if o.directory_mode ~= "recursive" and o.directory_mode ~= "lazy" and o.directory_mode ~= "ignore" then
|
||||||
|
o.directory_mode = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
validate_directory_mode()
|
||||||
|
|
||||||
|
function add_files(files)
|
||||||
|
local oldcount = mp.get_property_number("playlist-count", 1)
|
||||||
|
for i = 1, #files do
|
||||||
|
mp.commandv("loadfile", files[i][1], "append")
|
||||||
|
mp.commandv("playlist-move", oldcount + i - 1, files[i][2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_extension(path)
|
||||||
|
match = string.match(path, "%.([^%.]+)$" )
|
||||||
|
if match == nil then
|
||||||
|
return "nomatch"
|
||||||
|
else
|
||||||
|
return match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_filename_without_ext(filename)
|
||||||
|
local idx = filename:match(".+()%.%w+$")
|
||||||
|
if idx then
|
||||||
|
filename = filename:sub(1, idx - 1)
|
||||||
|
end
|
||||||
|
return filename
|
||||||
|
end
|
||||||
|
|
||||||
|
function utf8_char_bytes(str, i)
|
||||||
|
local char_byte = str:byte(i)
|
||||||
|
if char_byte < 0xC0 then
|
||||||
|
return 1
|
||||||
|
elseif char_byte < 0xE0 then
|
||||||
|
return 2
|
||||||
|
elseif char_byte < 0xF0 then
|
||||||
|
return 3
|
||||||
|
elseif char_byte < 0xF8 then
|
||||||
|
return 4
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function utf8_iter(str)
|
||||||
|
local byte_start = 1
|
||||||
|
return function()
|
||||||
|
local start = byte_start
|
||||||
|
if #str < start then return nil end
|
||||||
|
local byte_count = utf8_char_bytes(str, start)
|
||||||
|
byte_start = start + byte_count
|
||||||
|
return start, str:sub(start, start + byte_count - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function utf8_to_table(str)
|
||||||
|
local t = {}
|
||||||
|
for _, ch in utf8_iter(str) do
|
||||||
|
t[#t + 1] = ch
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
function jaro(s1, s2)
|
||||||
|
local match_window = math.floor(math.max(#s1, #s2) / 2.0) - 1
|
||||||
|
local matches1 = {}
|
||||||
|
local matches2 = {}
|
||||||
|
|
||||||
|
local m = 0;
|
||||||
|
local t = 0;
|
||||||
|
|
||||||
|
for i = 0, #s1, 1 do
|
||||||
|
local start = math.max(0, i - match_window)
|
||||||
|
local final = math.min(i + match_window + 1, #s2)
|
||||||
|
|
||||||
|
for k = start, final, 1 do
|
||||||
|
if not (matches2[k] or s1[i] ~= s2[k]) then
|
||||||
|
matches1[i] = true
|
||||||
|
matches2[k] = true
|
||||||
|
m = m + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if m == 0 then
|
||||||
|
return 0.0
|
||||||
|
end
|
||||||
|
|
||||||
|
local k = 0
|
||||||
|
for i = 0, #s1, 1 do
|
||||||
|
if matches1[i] then
|
||||||
|
while not matches2[k] do
|
||||||
|
k = k + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if s1[i] ~= s2[k] then
|
||||||
|
t = t + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
k = k + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
t = t / 2.0
|
||||||
|
|
||||||
|
return (m / #s1 + m / #s2 + (m - t) / m) / 3.0
|
||||||
|
end
|
||||||
|
|
||||||
|
function jaro_winkler_distance(s1, s2)
|
||||||
|
if #s1 + #s2 == 0 then
|
||||||
|
return 0.0
|
||||||
|
end
|
||||||
|
|
||||||
|
if s1 == s2 then
|
||||||
|
return 1.0
|
||||||
|
end
|
||||||
|
|
||||||
|
s1 = utf8_to_table(s1)
|
||||||
|
s2 = utf8_to_table(s2)
|
||||||
|
|
||||||
|
local d = jaro(s1, s2)
|
||||||
|
local p = 0.1
|
||||||
|
local l = 0;
|
||||||
|
while (s1[l] == s2[l] and l < 4) do
|
||||||
|
l = l + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return d + l * p * (1 - d)
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_same_series(f1, f2)
|
||||||
|
local f1, f2 = get_filename_without_ext(f1), get_filename_without_ext(f2)
|
||||||
|
if f1 ~= f2 then
|
||||||
|
-- by episode
|
||||||
|
local sub1 = f1:gsub("^[%[%(]+.-[%]%)]+[%s%[]*", ""):match("(.-%D+)0*%d+")
|
||||||
|
local sub2 = f2:gsub("^[%[%(]+.-[%]%)]+[%s%[]*", ""):match("(.-%D+)0*%d+")
|
||||||
|
if sub1 and sub2 and sub1 == sub2 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- by similarity
|
||||||
|
local threshold = 0.8
|
||||||
|
local similarity = jaro_winkler_distance(f1, f2)
|
||||||
|
if similarity > threshold then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_ignored(file)
|
||||||
|
for pattern, _ in pairs(o.ignore_patterns) do
|
||||||
|
if string.match(file, pattern) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
table.filter = function(t, iter)
|
||||||
|
for i = #t, 1, -1 do
|
||||||
|
if not iter(t[i]) then
|
||||||
|
table.remove(t, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.append = function(t1, t2)
|
||||||
|
local t1_size = #t1
|
||||||
|
for i = 1, #t2 do
|
||||||
|
t1[t1_size + i] = t2[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
----- winapi start -----
|
||||||
|
-- in windows system, we can use the sorting function provided by the win32 API
|
||||||
|
-- see https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-strcmplogicalw
|
||||||
|
-- this function was taken from https://github.com/mpvnet-player/mpv.net/issues/575#issuecomment-1817413401
|
||||||
|
local winapi = {}
|
||||||
|
local is_windows = mp.get_property_native("platform") == "windows"
|
||||||
|
|
||||||
|
if is_windows then
|
||||||
|
-- is_ffi_loaded is false usually means the mpv builds without luajit
|
||||||
|
local is_ffi_loaded, ffi = pcall(require, "ffi")
|
||||||
|
|
||||||
|
if is_ffi_loaded then
|
||||||
|
winapi = {
|
||||||
|
ffi = ffi,
|
||||||
|
C = ffi.C,
|
||||||
|
CP_UTF8 = 65001,
|
||||||
|
shlwapi = ffi.load("shlwapi"),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ffi code from https://github.com/po5/thumbfast, Mozilla Public License Version 2.0
|
||||||
|
ffi.cdef[[
|
||||||
|
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr,
|
||||||
|
int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||||
|
int __stdcall StrCmpLogicalW(wchar_t *psz1, wchar_t *psz2);
|
||||||
|
]]
|
||||||
|
|
||||||
|
winapi.utf8_to_wide = function(utf8_str)
|
||||||
|
if utf8_str then
|
||||||
|
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, nil, 0)
|
||||||
|
|
||||||
|
if utf16_len > 0 then
|
||||||
|
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||||
|
|
||||||
|
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, utf16_str, utf16_len) > 0 then
|
||||||
|
return utf16_str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
----- winapi end -----
|
||||||
|
|
||||||
|
function alphanumsort_windows(filenames)
|
||||||
|
table.sort(filenames, function(a, b)
|
||||||
|
local a_wide = winapi.utf8_to_wide(a)
|
||||||
|
local b_wide = winapi.utf8_to_wide(b)
|
||||||
|
return winapi.shlwapi.StrCmpLogicalW(a_wide, b_wide) == -1
|
||||||
|
end)
|
||||||
|
|
||||||
|
return filenames
|
||||||
|
end
|
||||||
|
|
||||||
|
-- alphanum sorting for humans in Lua
|
||||||
|
-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
|
||||||
|
function alphanumsort_lua(filenames)
|
||||||
|
local function padnum(n, d)
|
||||||
|
return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
|
||||||
|
or ("%03d%s"):format(#n, n)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tuples = {}
|
||||||
|
for i, f in ipairs(filenames) do
|
||||||
|
tuples[i] = {f:lower():gsub("0*(%d+)%.?(%d*)", padnum), f}
|
||||||
|
end
|
||||||
|
table.sort(tuples, function(a, b)
|
||||||
|
return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
|
||||||
|
end)
|
||||||
|
for i, tuple in ipairs(tuples) do filenames[i] = tuple[2] end
|
||||||
|
return filenames
|
||||||
|
end
|
||||||
|
|
||||||
|
function alphanumsort(filenames)
|
||||||
|
local is_ffi_loaded = pcall(require, "ffi")
|
||||||
|
if is_windows and is_ffi_loaded then
|
||||||
|
alphanumsort_windows(filenames)
|
||||||
|
else
|
||||||
|
alphanumsort_lua(filenames)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local autoloaded = nil
|
||||||
|
local added_entries = {}
|
||||||
|
local autoloaded_dir = nil
|
||||||
|
|
||||||
|
function scan_dir(path, current_file, dir_mode, separator, dir_depth, total_files, extensions)
|
||||||
|
if dir_depth == MAXDIRSTACK then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
msg.trace("scanning: " .. path)
|
||||||
|
local files = utils.readdir(path, "files") or {}
|
||||||
|
local dirs = dir_mode ~= "ignore" and utils.readdir(path, "dirs") or {}
|
||||||
|
local prefix = path == "." and "" or path
|
||||||
|
table.filter(files, function (v)
|
||||||
|
-- The current file could be a hidden file, ignoring it doesn't load other
|
||||||
|
-- files from the current directory.
|
||||||
|
local current = prefix .. v == current_file
|
||||||
|
if o.ignore_hidden and not current and string.match(v, "^%.") then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if not current and is_ignored(v) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local ext = get_extension(v)
|
||||||
|
if ext == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local name = mp.get_property("filename")
|
||||||
|
if o.same_series then
|
||||||
|
local name = mp.get_property("filename")
|
||||||
|
for ext, _ in pairs(extensions) do
|
||||||
|
if name:match(ext .. "$") ~= nil and v ~= name and
|
||||||
|
not is_same_series(name, v)
|
||||||
|
then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return extensions[string.lower(ext)]
|
||||||
|
end)
|
||||||
|
table.filter(dirs, function(d)
|
||||||
|
return not ((o.ignore_hidden and string.match(d, "^%.")))
|
||||||
|
end)
|
||||||
|
alphanumsort(files)
|
||||||
|
alphanumsort(dirs)
|
||||||
|
|
||||||
|
for i, file in ipairs(files) do
|
||||||
|
files[i] = prefix .. file
|
||||||
|
end
|
||||||
|
|
||||||
|
table.append(total_files, files)
|
||||||
|
if dir_mode == "recursive" then
|
||||||
|
for _, dir in ipairs(dirs) do
|
||||||
|
scan_dir(prefix .. dir .. separator, current_file, dir_mode,
|
||||||
|
separator, dir_depth + 1, total_files, extensions)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for i, dir in ipairs(dirs) do
|
||||||
|
dirs[i] = prefix .. dir
|
||||||
|
end
|
||||||
|
table.append(total_files, dirs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function find_and_add_entries()
|
||||||
|
local path = mp.get_property("path", "")
|
||||||
|
local dir, filename = utils.split_path(path)
|
||||||
|
msg.trace(("dir: %s, filename: %s"):format(dir, filename))
|
||||||
|
if o.disabled then
|
||||||
|
msg.debug("stopping: autoload disabled")
|
||||||
|
return
|
||||||
|
elseif #dir == 0 then
|
||||||
|
msg.debug("stopping: not a local path")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local pl_count = mp.get_property_number("playlist-count", 1)
|
||||||
|
this_ext = get_extension(filename)
|
||||||
|
-- check if this is a manually made playlist
|
||||||
|
if (pl_count > 1 and autoloaded == nil) or
|
||||||
|
(pl_count == 1 and EXTENSIONS[string.lower(this_ext)] == nil) then
|
||||||
|
msg.debug("stopping: manually made playlist")
|
||||||
|
return
|
||||||
|
else
|
||||||
|
if pl_count == 1 then
|
||||||
|
autoloaded = true
|
||||||
|
autoloaded_dir = dir
|
||||||
|
added_entries = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local extensions = {}
|
||||||
|
if o.same_type then
|
||||||
|
if EXTENSIONS_VIDEO[string.lower(this_ext)] ~= nil then
|
||||||
|
extensions = EXTENSIONS_VIDEO
|
||||||
|
elseif EXTENSIONS_AUDIO[string.lower(this_ext)] ~= nil then
|
||||||
|
extensions = EXTENSIONS_AUDIO
|
||||||
|
else
|
||||||
|
extensions = EXTENSIONS_IMAGES
|
||||||
|
end
|
||||||
|
else
|
||||||
|
extensions = EXTENSIONS
|
||||||
|
end
|
||||||
|
|
||||||
|
local pl = mp.get_property_native("playlist", {})
|
||||||
|
local pl_current = mp.get_property_number("playlist-pos-1", 1)
|
||||||
|
msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current,
|
||||||
|
utils.to_string(pl)))
|
||||||
|
|
||||||
|
local files = {}
|
||||||
|
do
|
||||||
|
local dir_mode = o.directory_mode or mp.get_property("directory-mode", "lazy")
|
||||||
|
local separator = mp.get_property_native("platform") == "windows" and "\\" or "/"
|
||||||
|
scan_dir(autoloaded_dir, path, dir_mode, separator, 0, files, extensions)
|
||||||
|
end
|
||||||
|
|
||||||
|
if next(files) == nil then
|
||||||
|
msg.debug("no other files or directories in directory")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find the current pl entry (dir+"/"+filename) in the sorted dir list
|
||||||
|
local current
|
||||||
|
for i = 1, #files do
|
||||||
|
if files[i] == path then
|
||||||
|
current = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if current == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
msg.trace("current file position in files: "..current)
|
||||||
|
|
||||||
|
-- treat already existing playlist entries, independent of how they got added
|
||||||
|
-- as if they got added by autoload
|
||||||
|
for _, entry in ipairs(pl) do
|
||||||
|
added_entries[entry.filename] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local append = {[-1] = {}, [1] = {}}
|
||||||
|
for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1
|
||||||
|
for i = 1, MAXENTRIES do
|
||||||
|
local pos = current + i * direction
|
||||||
|
local file = files[pos]
|
||||||
|
if file == nil or file[1] == "." then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- skip files that are/were already in the playlist
|
||||||
|
if not added_entries[file] then
|
||||||
|
if direction == -1 then
|
||||||
|
msg.verbose("Prepending " .. file)
|
||||||
|
table.insert(append[-1], 1, {file, pl_current + i * direction + 1})
|
||||||
|
else
|
||||||
|
msg.verbose("Adding " .. file)
|
||||||
|
if pl_count > 1 then
|
||||||
|
table.insert(append[1], {file, pl_current + i * direction - 1})
|
||||||
|
else
|
||||||
|
mp.commandv("loadfile", file, "append")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
added_entries[file] = true
|
||||||
|
end
|
||||||
|
if pl_count == 1 and direction == -1 and #append[-1] > 0 then
|
||||||
|
for i = 1, #append[-1] do
|
||||||
|
mp.commandv("loadfile", append[-1][i][1], "append")
|
||||||
|
end
|
||||||
|
mp.commandv("playlist-move", 0, current)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if pl_count > 1 then
|
||||||
|
add_files(append[1])
|
||||||
|
add_files(append[-1])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event("start-file", find_and_add_entries)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 joaquintorres
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# autosubsync-mpv
|
||||||
|
|
||||||
|
Automatic subtitle synchronization script for [mpv](https://wiki.archlinux.org/index.php/Mpv).
|
||||||
|
|
||||||
|
A demo can be viewed on <a target="_blank" href="https://www.youtube.com/watch?v=w1vwnUiF6Bc"><img src="https://user-images.githubusercontent.com/69171671/115097010-4bd13c80-9f17-11eb-83e9-2583658f73bc.png" width="80px"></a>
|
||||||
|
|
||||||
|
Supported backends:
|
||||||
|
* [ffsubsync](https://github.com/smacke/ffsubsync)
|
||||||
|
* [alass](https://github.com/kaegi/alass)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
0. Make sure you have mpv v0.33 or higher installed.
|
||||||
|
```
|
||||||
|
$ mpv --version
|
||||||
|
```
|
||||||
|
1. Install [FFmpeg](https://wiki.archlinux.org/index.php/FFmpeg):
|
||||||
|
```
|
||||||
|
$ pacman -S ffmpeg
|
||||||
|
```
|
||||||
|
Windows users have to manually install FFmpeg from [here](https://ffmpeg.zeranoe.com/builds/).
|
||||||
|
2. Install your retiming program of choice,
|
||||||
|
[ffsubsync](https://github.com/smacke/ffsubsync), [alass](https://github.com/kaegi/alass) or both:
|
||||||
|
```
|
||||||
|
$ pip install ffsubsync
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ trizen -S alass-git # for Arch-based distros
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Download the add-on and save it to your mpv scripts folder.
|
||||||
|
|
||||||
|
| GNU/Linux | Windows |
|
||||||
|
|---|---|
|
||||||
|
| `~/.config/mpv/scripts` | `%AppData%\mpv\scripts\` |
|
||||||
|
|
||||||
|
To do it in one command:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone 'https://github.com/Ajatt-Tools/autosubsync-mpv' ~/.config/mpv/scripts/autosubsync
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
You can skip this step if the add-on works out of the box.
|
||||||
|
|
||||||
|
Create a config file:
|
||||||
|
|
||||||
|
| GNU/Linux | Windows |
|
||||||
|
|---|---|
|
||||||
|
| `~/.config/mpv/script-opts/autosubsync.conf` | `%AppData%\mpv\script-opts\autosubsync.conf` |
|
||||||
|
|
||||||
|
Example config:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Absolute paths to the executables, if needed:
|
||||||
|
|
||||||
|
# 1. ffmpeg
|
||||||
|
ffmpeg_path=C:/Program Files/ffmpeg/bin/ffmpeg.exe
|
||||||
|
ffmpeg_path=/usr/bin/ffmpeg
|
||||||
|
|
||||||
|
# 2. ffsubsync
|
||||||
|
ffsubsync_path=C:/Program Files/ffsubsync/ffsubsync.exe
|
||||||
|
ffsubsync_path=/home/user/.local/bin/ffsubsync
|
||||||
|
|
||||||
|
# 3. alass
|
||||||
|
alass_path=C:/Program Files/ffmpeg/bin/alass.exe
|
||||||
|
alass_path=/usr/bin/alass
|
||||||
|
|
||||||
|
# Preferred retiming tool. Allowed options: 'ffsubsync', 'alass', 'ask'.
|
||||||
|
# If set to 'ask', the add-on will ask to choose the tool every time:
|
||||||
|
|
||||||
|
# 1. Preferred tool for syncing to audio.
|
||||||
|
audio_subsync_tool=ask
|
||||||
|
audio_subsync_tool=ffsubsync
|
||||||
|
audio_subsync_tool=alass
|
||||||
|
|
||||||
|
# 2. Preferred tool for syncing to another subtitle.
|
||||||
|
altsub_subsync_tool=ask
|
||||||
|
altsub_subsync_tool=ffsubsync
|
||||||
|
altsub_subsync_tool=alass
|
||||||
|
|
||||||
|
# Unload old subs (yes,no)
|
||||||
|
# After retiming, tell mpv to forget the original subtitle track.
|
||||||
|
unload_old_sub=yes
|
||||||
|
unload_old_sub=no
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
* On Windows, you need to use forward slashes or double backslashes for your path.
|
||||||
|
For example, `"C:\\Users\\YourPath\\Scripts\\ffsubsync"`
|
||||||
|
or `"C:/Users/YourPath/Scripts/ffsubsync"`,
|
||||||
|
or it might not work.
|
||||||
|
|
||||||
|
* On GNU/Linux you can use `which ffsubsync` to find out where it is.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
When you have an out of sync sub, press `n` to synchronize it.
|
||||||
|
|
||||||
|
`ffsubsync` can typically take up to about 20-30 seconds
|
||||||
|
to synchronize (I've seen it take as much as 2 minutes
|
||||||
|
with a very large file on a lower end computer), so it
|
||||||
|
would probably be faster to find another, properly
|
||||||
|
synchronized subtitle with `autosub` or `trueautosub`.
|
||||||
|
Many times this is just not possible, as all available
|
||||||
|
subs for your specific language are out of sync.
|
||||||
|
|
||||||
|
Take into account that using this script has the
|
||||||
|
same limitations as `ffsubsync`, so subtitles that have
|
||||||
|
a lot of extra text or are meant for an entirely different
|
||||||
|
version of the video might not sync properly. `alass` is supposed
|
||||||
|
to handle some edge cases better, but I haven't fully tested it yet,
|
||||||
|
obtaining similar results with both.
|
||||||
|
|
||||||
|
Note that the script will create a new subtitle file, in the same folder
|
||||||
|
as the original, with the `_retimed` suffix at the end.
|
||||||
|
|
||||||
|
## Issues and feedback
|
||||||
|
|
||||||
|
If you are having trouble getting it to work or you've found a bug,
|
||||||
|
feel free to [join our community](https://tatsumoto-ren.github.io/blog/join-our-community.html) to ask directly.
|
||||||
|
|
||||||
|
Try to check if
|
||||||
|
[ffsubsync](https://github.com/smacke/ffsubsync)
|
||||||
|
or
|
||||||
|
[alass](https://github.com/kaegi/alass)
|
||||||
|
works properly outside of `mpv` first.
|
||||||
|
If the retiming tool of choice isn't working, `autosubsync` will likely fail.
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
-- Usage:
|
||||||
|
-- default keybinding: n
|
||||||
|
-- add the following to your input.conf to change the default keybinding:
|
||||||
|
-- keyname script_binding autosubsync-menu
|
||||||
|
|
||||||
|
local mp = require('mp')
|
||||||
|
local utils = require('mp.utils')
|
||||||
|
local mpopt = require('mp.options')
|
||||||
|
local menu = require('menu')
|
||||||
|
local sub = require('subtitle')
|
||||||
|
local ref_selector
|
||||||
|
local engine_selector
|
||||||
|
local track_selector
|
||||||
|
|
||||||
|
-- Config
|
||||||
|
-- Options can be changed here or in a separate config file.
|
||||||
|
-- Config path: ~/.config/mpv/script-opts/autosubsync.conf
|
||||||
|
local config = {
|
||||||
|
-- Change the following lines if the locations of executables differ from the defaults
|
||||||
|
-- If set to empty, the path will be guessed.
|
||||||
|
ffmpeg_path = "",
|
||||||
|
ffsubsync_path = "",
|
||||||
|
alass_path = "",
|
||||||
|
|
||||||
|
-- Choose what tool to use. Allowed options: ffsubsync, alass, ask.
|
||||||
|
-- If set to ask, the add-on will ask to choose the tool every time.
|
||||||
|
audio_subsync_tool = "ask",
|
||||||
|
altsub_subsync_tool = "ask",
|
||||||
|
|
||||||
|
-- After retiming, tell mpv to forget the original subtitle track.
|
||||||
|
unload_old_sub = true,
|
||||||
|
}
|
||||||
|
mpopt.read_options(config, 'autosubsync')
|
||||||
|
|
||||||
|
local function is_empty(var)
|
||||||
|
return var == nil or var == '' or (type(var) == 'table' and next(var) == nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
----- string
|
||||||
|
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 esc_for_title(string)
|
||||||
|
string = string:gsub('^[%._%-%s]*', '')
|
||||||
|
:gsub('%.%w+$', '')
|
||||||
|
return string
|
||||||
|
end
|
||||||
|
|
||||||
|
local function esc_for_code(trackCode)
|
||||||
|
if trackCode:find("PGS") then trackCode = "PGS"
|
||||||
|
elseif trackCode:find("SUBRIP") then trackCode = "SRT"
|
||||||
|
elseif trackCode:find("VTT") then trackCode = "VTT"
|
||||||
|
elseif trackCode:find("DVD_SUB") then trackCode = "VOB_SUB"
|
||||||
|
elseif trackCode:find("DVB_SUB") then trackCode = "DVB_SUB"
|
||||||
|
elseif trackCode:find("DVB_TELE") then trackCode = "TELETEXT"
|
||||||
|
elseif trackCode:find("ARIB") then trackCode = "ARIB"
|
||||||
|
end
|
||||||
|
return trackCode
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Snippet borrowed from stackoverflow to get the operating system
|
||||||
|
-- originally found at: https://stackoverflow.com/a/30960054
|
||||||
|
local os_name = (function()
|
||||||
|
if os.getenv("HOME") == nil then
|
||||||
|
return function()
|
||||||
|
return "Windows"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return function()
|
||||||
|
return "*nix"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)()
|
||||||
|
|
||||||
|
local os_temp = (function()
|
||||||
|
if os_name() == "Windows" then
|
||||||
|
return function()
|
||||||
|
return os.getenv('TEMP')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return function()
|
||||||
|
return '/tmp/'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)()
|
||||||
|
|
||||||
|
-- Courtesy of https://stackoverflow.com/questions/4990990/check-if-a-file-exists-with-lua
|
||||||
|
local function file_exists(filepath)
|
||||||
|
if not filepath then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local f = io.open(filepath, "r")
|
||||||
|
if f ~= nil then
|
||||||
|
io.close(f)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
if file_exists(exec_path) then
|
||||||
|
return exec_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return fallback_path
|
||||||
|
end
|
||||||
|
|
||||||
|
local function notify(message, level, duration)
|
||||||
|
level = level or 'info'
|
||||||
|
duration = duration or 1
|
||||||
|
mp.msg[level](message)
|
||||||
|
mp.osd_message(message, duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function subprocess(args)
|
||||||
|
return mp.command_native {
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
args = args
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local url_decode = function(url)
|
||||||
|
local function hex_to_char(x)
|
||||||
|
return string.char(tonumber(x, 16))
|
||||||
|
end
|
||||||
|
if url ~= nil then
|
||||||
|
url = url:gsub("^file://", "")
|
||||||
|
url = url:gsub("+", " ")
|
||||||
|
url = url:gsub("%%(%x%x)", hex_to_char)
|
||||||
|
return url
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_loaded_tracks(track_type)
|
||||||
|
local result = {}
|
||||||
|
local track_list = mp.get_property_native('track-list')
|
||||||
|
for _, track in pairs(track_list) do
|
||||||
|
if track.type == track_type then
|
||||||
|
track['external-filename'] = track.external and url_decode(track['external-filename'])
|
||||||
|
table.insert(result, track)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_active_track(track_type)
|
||||||
|
local track_list = mp.get_property_native('track-list')
|
||||||
|
for num, track in ipairs(track_list) do
|
||||||
|
if track.type == track_type and track.selected == true then
|
||||||
|
if track.external then
|
||||||
|
track['external-filename'] = url_decode(track['external-filename'])
|
||||||
|
end
|
||||||
|
if not (track_type == 'sub' and track.id == mp.get_property_native('secondary-sid')) then
|
||||||
|
return num, track
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return notify(string.format("错误: 没有选择类型为 '%s' 的轨道", track_type), "error", 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_extension(filename)
|
||||||
|
return filename:gsub('%.%w+$', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_extension(filename)
|
||||||
|
return filename:match("^.+(%.%w+)$")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function startswith(str, prefix)
|
||||||
|
return string.sub(str, 1, string.len(prefix)) == prefix
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mkfp_retimed(sub_path)
|
||||||
|
if not startswith(sub_path, os_temp()) then
|
||||||
|
return table.concat { remove_extension(sub_path), '_retimed', get_extension(sub_path) }
|
||||||
|
else
|
||||||
|
return table.concat { remove_extension(mp.get_property("path")), '_retimed', get_extension(sub_path) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function engine_is_set()
|
||||||
|
local subsync_tool = ref_selector:get_subsync_tool()
|
||||||
|
if is_empty(subsync_tool) or subsync_tool == "ask" then
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function extract_to_file(subtitle_track)
|
||||||
|
local codec_ext_map = { subrip = "srt", ass = "ass" }
|
||||||
|
local ext = codec_ext_map[subtitle_track['codec']]
|
||||||
|
if ext == nil then
|
||||||
|
return notify(string.format("错误: 不支持的格式: %s", subtitle_track['codec']), "error", 3)
|
||||||
|
end
|
||||||
|
local temp_sub_fp = utils.join_path(os_temp(), 'autosubsync_extracted.' .. ext)
|
||||||
|
notify("提取内封字幕...", nil, 3)
|
||||||
|
local screenx, screeny, aspect = mp.get_osd_size()
|
||||||
|
mp.set_osd_ass(screenx, screeny, "{\\an9}● ")
|
||||||
|
local ret = subprocess {
|
||||||
|
config.ffmpeg_path,
|
||||||
|
"-hide_banner",
|
||||||
|
"-nostdin",
|
||||||
|
"-y",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
"-an",
|
||||||
|
"-vn",
|
||||||
|
"-i", mp.get_property("path"),
|
||||||
|
"-map", "0:" .. (subtitle_track and subtitle_track['ff-index'] or 's'),
|
||||||
|
"-f", ext,
|
||||||
|
temp_sub_fp
|
||||||
|
}
|
||||||
|
mp.set_osd_ass(screenx, screeny, "")
|
||||||
|
if ret == nil or ret.status ~= 0 then
|
||||||
|
return notify("无法提取内封字幕.\n请先确保在脚本配置文件中为 ffmpeg 指定了正确的路径\n并确保视频有内封字幕.", "error", 7)
|
||||||
|
end
|
||||||
|
return temp_sub_fp
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sync_subtitles(ref_sub_path)
|
||||||
|
local reference_file_path = ref_sub_path or mp.get_property("path")
|
||||||
|
local _, sub_track = get_active_track('sub')
|
||||||
|
if sub_track == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local subtitle_path = sub_track.external and sub_track['external-filename'] or extract_to_file(sub_track)
|
||||||
|
local engine_name = engine_selector:get_engine_name()
|
||||||
|
local engine_path = config[engine_name .. '_path']
|
||||||
|
|
||||||
|
if not file_exists(subtitle_path) then
|
||||||
|
return notify(
|
||||||
|
table.concat {
|
||||||
|
"字幕同步失败:\n无法找到 ",
|
||||||
|
subtitle_path or "外部字幕文件."
|
||||||
|
},
|
||||||
|
"error",
|
||||||
|
3
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local retimed_subtitle_path = mkfp_retimed(subtitle_path)
|
||||||
|
|
||||||
|
notify(string.format("开始 %s...", engine_name), nil, 2)
|
||||||
|
|
||||||
|
local ret
|
||||||
|
local screenx, screeny, aspect = mp.get_osd_size()
|
||||||
|
if engine_name == "ffsubsync" then
|
||||||
|
local args = { config.ffsubsync_path, reference_file_path, "-i", subtitle_path, "-o", retimed_subtitle_path }
|
||||||
|
if not ref_sub_path then
|
||||||
|
table.insert(args, '--reference-stream')
|
||||||
|
table.insert(args, '0:' .. get_active_track('audio'))
|
||||||
|
end
|
||||||
|
mp.set_osd_ass(screenx, screeny, "{\\an9}● ")
|
||||||
|
ret = subprocess(args)
|
||||||
|
mp.set_osd_ass(screenx, screeny, "")
|
||||||
|
else
|
||||||
|
mp.set_osd_ass(screenx, screeny, "{\\an9}● ")
|
||||||
|
ret = subprocess { config.alass_path, reference_file_path, subtitle_path, retimed_subtitle_path }
|
||||||
|
mp.set_osd_ass(screenx, screeny, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
if ret == nil then
|
||||||
|
return notify("解析失败或没有传递参数.", "fatal", 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
if ret.status == 0 then
|
||||||
|
local old_sid = mp.get_property("sid")
|
||||||
|
if mp.commandv("sub_add", retimed_subtitle_path) then
|
||||||
|
notify("字幕同步.", nil, 2)
|
||||||
|
mp.set_property("sub-delay", 0)
|
||||||
|
if config.unload_old_sub then
|
||||||
|
mp.commandv("sub_remove", old_sid)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
notify("错误: 不能添加同步字幕.", "error", 3)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
notify(string.format("字幕同步失败.\n请确保在脚本配置文件中为 %s 指定了正确的路径.\n或音轨提取失败", engine_name), "error", 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sync_to_subtitle()
|
||||||
|
local selected_track = track_selector:get_selected_track()
|
||||||
|
|
||||||
|
if selected_track and selected_track.external then
|
||||||
|
sync_subtitles(selected_track['external-filename'])
|
||||||
|
else
|
||||||
|
local temp_sub_fp = extract_to_file(selected_track)
|
||||||
|
if temp_sub_fp then
|
||||||
|
sync_subtitles(temp_sub_fp)
|
||||||
|
os.remove(temp_sub_fp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sync_to_manual_offset()
|
||||||
|
local _, track = get_active_track('sub')
|
||||||
|
local sub_delay = tonumber(mp.get_property("sub-delay"))
|
||||||
|
if tonumber(sub_delay) == 0 then
|
||||||
|
return notify("没有手动调整时轴,什么都做不了!", "error", 7)
|
||||||
|
end
|
||||||
|
local file_path = track.external and track['external-filename'] or extract_to_file(track)
|
||||||
|
if file_path == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ext = get_extension(file_path)
|
||||||
|
local codec_parser_map = { ass = sub.ASS, subrip = sub.SRT }
|
||||||
|
local parser = codec_parser_map[track['codec']]
|
||||||
|
if parser == nil then
|
||||||
|
return notify(string.format("错误: 不支持的格式: %s", track['codec']), "error", 3)
|
||||||
|
end
|
||||||
|
local s = parser:populate(file_path)
|
||||||
|
s:shift_timing(sub_delay)
|
||||||
|
if track.external == false then
|
||||||
|
os.remove(file_path)
|
||||||
|
s.filename = mp.get_property("filename/no-ext") .. "_manual_timing" .. ext
|
||||||
|
else
|
||||||
|
s.filename = remove_extension(s.filename) .. '_manual_timing' .. ext
|
||||||
|
end
|
||||||
|
s:save()
|
||||||
|
mp.commandv("sub_add", s.filename)
|
||||||
|
if config.unload_old_sub then
|
||||||
|
mp.commandv("sub_remove", track.id)
|
||||||
|
end
|
||||||
|
mp.set_property("sub-delay", 0)
|
||||||
|
return notify(string.format("手动同步保存,加载 '%s'", s.filename), "info", 7)
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Menu actions & bindings
|
||||||
|
|
||||||
|
ref_selector = menu:new {
|
||||||
|
items = { '与音频同步', '与其他字幕同步', '保存当前时轴', '退出' },
|
||||||
|
last_choice = 'audio',
|
||||||
|
pos_x = 50,
|
||||||
|
pos_y = 50,
|
||||||
|
rect_width = 400,
|
||||||
|
text_color = 'fff5da',
|
||||||
|
border_color = '2f1728',
|
||||||
|
active_color = 'ff6b71',
|
||||||
|
inactive_color = 'fff5da',
|
||||||
|
}
|
||||||
|
|
||||||
|
function ref_selector:get_keybindings()
|
||||||
|
return {
|
||||||
|
{ key = 'h', fn = function() self:close() end },
|
||||||
|
{ key = 'j', fn = function() self:down() end },
|
||||||
|
{ key = 'k', fn = function() self:up() end },
|
||||||
|
{ key = 'l', fn = function() self:act() end },
|
||||||
|
{ key = 'down', fn = function() self:down() end },
|
||||||
|
{ key = 'up', fn = function() self:up() end },
|
||||||
|
{ key = 'Enter', fn = function() self:act() end },
|
||||||
|
{ key = 'ESC', fn = function() self:close() end },
|
||||||
|
{ key = 'n', fn = function() self:close() end },
|
||||||
|
{ key = 'WHEEL_DOWN', fn = function() self:down() end },
|
||||||
|
{ key = 'WHEEL_UP', fn = function() self:up() end },
|
||||||
|
{ key = 'MBTN_LEFT', fn = function() self:act() end },
|
||||||
|
{ key = 'MBTN_RIGHT', fn = function() self:close() end },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ref_selector:new(o)
|
||||||
|
self.__index = self
|
||||||
|
o = o or {}
|
||||||
|
return setmetatable(o, self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ref_selector:get_ref()
|
||||||
|
if self.selected == 1 then
|
||||||
|
return 'audio'
|
||||||
|
elseif self.selected == 2 then
|
||||||
|
return 'sub'
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ref_selector:get_subsync_tool()
|
||||||
|
if self.selected == 1 then
|
||||||
|
return config.audio_subsync_tool
|
||||||
|
elseif self.selected == 2 then
|
||||||
|
return config.altsub_subsync_tool
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ref_selector:act()
|
||||||
|
self:close()
|
||||||
|
|
||||||
|
if self.selected == 3 then
|
||||||
|
return sync_to_manual_offset()
|
||||||
|
end
|
||||||
|
if self.selected == 4 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
engine_selector:init()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ref_selector:call_subsync()
|
||||||
|
if self.selected == 1 then
|
||||||
|
sync_subtitles()
|
||||||
|
elseif self.selected == 2 then
|
||||||
|
sync_to_subtitle()
|
||||||
|
elseif self.selected == 3 then
|
||||||
|
sync_to_manual_offset()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ref_selector:open()
|
||||||
|
self.selected = 1
|
||||||
|
for _, val in pairs(self:get_keybindings()) do
|
||||||
|
mp.add_forced_key_binding(val.key, val.key, val.fn)
|
||||||
|
end
|
||||||
|
self:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ref_selector:close()
|
||||||
|
for _, val in pairs(self:get_keybindings()) do
|
||||||
|
mp.remove_key_binding(val.key)
|
||||||
|
end
|
||||||
|
self:erase()
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Engine selector
|
||||||
|
|
||||||
|
engine_selector = ref_selector:new {
|
||||||
|
items = { 'ffsubsync', 'alass', '退出' },
|
||||||
|
last_choice = 'ffsubsync',
|
||||||
|
}
|
||||||
|
|
||||||
|
function engine_selector:init()
|
||||||
|
if not engine_is_set() then
|
||||||
|
engine_selector:open()
|
||||||
|
else
|
||||||
|
track_selector:init()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function engine_selector:get_engine_name()
|
||||||
|
return engine_is_set() and ref_selector:get_subsync_tool() or self.last_choice
|
||||||
|
end
|
||||||
|
|
||||||
|
function engine_selector:act()
|
||||||
|
self:close()
|
||||||
|
|
||||||
|
if self.selected == 1 then
|
||||||
|
self.last_choice = 'ffsubsync'
|
||||||
|
elseif self.selected == 2 then
|
||||||
|
self.last_choice = 'alass'
|
||||||
|
elseif self.selected == 3 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
track_selector:init()
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Track selector
|
||||||
|
|
||||||
|
track_selector = ref_selector:new { }
|
||||||
|
|
||||||
|
function track_selector:init()
|
||||||
|
self.selected = 0
|
||||||
|
|
||||||
|
if ref_selector:get_ref() == 'audio' then
|
||||||
|
return ref_selector:call_subsync()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.all_sub_tracks = get_loaded_tracks(ref_selector:get_ref())
|
||||||
|
self.tracks = {}
|
||||||
|
self.items = {}
|
||||||
|
|
||||||
|
local filename = mp.get_property_native('filename/no-ext')
|
||||||
|
for _, track in ipairs(self.all_sub_tracks) do
|
||||||
|
local supported_format = true
|
||||||
|
if track.external then
|
||||||
|
local ext = get_extension(track['external-filename'])
|
||||||
|
if ext ~= '.srt' and ext ~= '.ass' then
|
||||||
|
supported_format = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not track.selected and supported_format then
|
||||||
|
table.insert(self.tracks, track)
|
||||||
|
table.insert(
|
||||||
|
self.items,
|
||||||
|
string.format(
|
||||||
|
"%s #%s - %s%s%s",
|
||||||
|
(track.external and 'External' or 'Internal'),
|
||||||
|
track['id'],
|
||||||
|
(track.lang or (track.title and
|
||||||
|
esc_for_title(replace(track.title, filename, '')) or 'unknown')),
|
||||||
|
(track.codec and '[' .. esc_for_code(track.codec:upper()) .. ']' or ''),
|
||||||
|
(track.selected and ' (active)' or '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #self.items == 0 then
|
||||||
|
notify("没有找到受支持的字幕轨道.", "warn", 5)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(self.items, "退出")
|
||||||
|
self:open()
|
||||||
|
end
|
||||||
|
|
||||||
|
function track_selector:get_selected_track()
|
||||||
|
if self.selected < 1 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return self.tracks[self.selected]
|
||||||
|
end
|
||||||
|
|
||||||
|
function track_selector:act()
|
||||||
|
self:close()
|
||||||
|
|
||||||
|
if self.selected == #self.items then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
ref_selector:call_subsync()
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Initialize the addon
|
||||||
|
|
||||||
|
local function init()
|
||||||
|
for _, executable in pairs { 'ffmpeg', 'ffsubsync', 'alass' } do
|
||||||
|
local config_key = executable .. '_path'
|
||||||
|
config[config_key] = is_empty(config[config_key]) and find_executable(executable) or config[config_key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Entry point
|
||||||
|
|
||||||
|
init()
|
||||||
|
mp.add_key_binding("n", "autosubsync-menu", function() ref_selector:open() end)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
require('autosubsync')
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
------------------------------------------------------------
|
||||||
|
-- Menu visuals
|
||||||
|
|
||||||
|
local mp = require('mp')
|
||||||
|
local assdraw = require('mp.assdraw')
|
||||||
|
local Menu = assdraw.ass_new()
|
||||||
|
|
||||||
|
function Menu:new(o)
|
||||||
|
self.__index = self
|
||||||
|
o = o or {}
|
||||||
|
o.selected = o.selected or 1
|
||||||
|
o.canvas_width = o.canvas_width or 1280
|
||||||
|
o.canvas_height = o.canvas_height or 720
|
||||||
|
o.pos_x = o.pos_x or 0
|
||||||
|
o.pos_y = o.pos_y or 0
|
||||||
|
o.rect_width = o.rect_width or 320
|
||||||
|
o.rect_height = o.rect_height or 40
|
||||||
|
o.active_color = o.active_color or 'ffffff'
|
||||||
|
o.inactive_color = o.inactive_color or 'aaaaaa'
|
||||||
|
o.border_color = o.border_color or '000000'
|
||||||
|
o.text_color = o.text_color or 'ffffff'
|
||||||
|
|
||||||
|
return setmetatable(o, self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:set_position(x, y)
|
||||||
|
self.pos_x = x
|
||||||
|
self.pos_y = y
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:font_size(size)
|
||||||
|
self:append(string.format([[{\fs%s}]], size))
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:set_text_color(code)
|
||||||
|
self:append(string.format("{\\1c&H%s%s%s&\\1a&H05&}", code:sub(5, 6), code:sub(3, 4), code:sub(1, 2)))
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:set_border_color(code)
|
||||||
|
self:append(string.format("{\\3c&H%s%s%s&}", code:sub(5, 6), code:sub(3, 4), code:sub(1, 2)))
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:apply_text_color()
|
||||||
|
self:set_border_color(self.border_color)
|
||||||
|
self:set_text_color(self.text_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:apply_rect_color(i)
|
||||||
|
self:set_border_color(self.border_color)
|
||||||
|
if i == self.selected then
|
||||||
|
self:set_text_color(self.active_color)
|
||||||
|
else
|
||||||
|
self:set_text_color(self.inactive_color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:draw_text(i)
|
||||||
|
local padding = 5
|
||||||
|
local font_size = 25
|
||||||
|
|
||||||
|
self:new_event()
|
||||||
|
self:pos(self.pos_x + padding, self.pos_y + self.rect_height * (i - 1) + padding)
|
||||||
|
self:font_size(font_size)
|
||||||
|
self:apply_text_color(i)
|
||||||
|
self:append(self.items[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:draw_item(i)
|
||||||
|
self:new_event()
|
||||||
|
self:pos(self.pos_x, self.pos_y)
|
||||||
|
self:apply_rect_color(i)
|
||||||
|
self:draw_start()
|
||||||
|
self:rect_cw(0, 0 + (i - 1) * self.rect_height, self.rect_width, i * self.rect_height)
|
||||||
|
self:draw_stop()
|
||||||
|
self:draw_text(i)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:draw()
|
||||||
|
self.text = ''
|
||||||
|
for i, _ in ipairs(self.items) do
|
||||||
|
self:draw_item(i)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.set_osd_ass(self.canvas_width, self.canvas_height, self.text)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:erase()
|
||||||
|
mp.set_osd_ass(self.canvas_width, self.canvas_height, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:up()
|
||||||
|
self.selected = self.selected - 1
|
||||||
|
if self.selected == 0 then
|
||||||
|
self.selected = #self.items
|
||||||
|
end
|
||||||
|
self:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:down()
|
||||||
|
self.selected = self.selected + 1
|
||||||
|
if self.selected > #self.items then
|
||||||
|
self.selected = 1
|
||||||
|
end
|
||||||
|
self:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
return Menu
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
local P = {}
|
||||||
|
|
||||||
|
local TimeStamp = {}
|
||||||
|
local TimeStamp_mt = { __index = TimeStamp }
|
||||||
|
function TimeStamp:new(hours, minutes, seconds)
|
||||||
|
local new = {}
|
||||||
|
new.hours = hours
|
||||||
|
new.minutes = minutes
|
||||||
|
new.seconds = seconds
|
||||||
|
return setmetatable(new, TimeStamp_mt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TimeStamp.toTimeStamp(seconds)
|
||||||
|
local diff, h, m, s = seconds, 0, 0, 0
|
||||||
|
h = math.floor(diff / 3600)
|
||||||
|
diff = diff - (h * 3600)
|
||||||
|
m = math.floor(diff / 60)
|
||||||
|
diff = diff - (m * 60)
|
||||||
|
s = diff
|
||||||
|
return TimeStamp:new(h, m, s)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TimeStamp:toSeconds()
|
||||||
|
return (3600 * self.hours) + (60 * self.minutes) + self.seconds
|
||||||
|
end
|
||||||
|
|
||||||
|
function TimeStamp:adjustTime(seconds)
|
||||||
|
return self.toTimeStamp(self:toSeconds() + seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TimeStamp:toString(decimal_symbol)
|
||||||
|
local seconds_fmt = string.format("%06.3f", self.seconds):gsub("%.", decimal_symbol)
|
||||||
|
return string.format("%02d:%02d:%s", self.hours, self.minutes, seconds_fmt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TimeStamp.to_seconds(seconds, milliseconds)
|
||||||
|
return tonumber(string.format("%s.%s", seconds, milliseconds))
|
||||||
|
end
|
||||||
|
|
||||||
|
local AbstractSubtitle = {}
|
||||||
|
local AbstractSubtitle_mt = { __index = AbstractSubtitle }
|
||||||
|
|
||||||
|
function AbstractSubtitle:create()
|
||||||
|
local new = {}
|
||||||
|
return setmetatable(new, AbstractSubtitle_mt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function AbstractSubtitle:save()
|
||||||
|
print(string.format("Writing '%s' to file..", self.filename))
|
||||||
|
local f = io.open(self.filename, 'w')
|
||||||
|
f:write(self:toString())
|
||||||
|
f:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- strip Byte Order Mark from file, if it's present
|
||||||
|
function AbstractSubtitle:sanitize(line)
|
||||||
|
local bom_table = { 0xEF, 0xBB, 0xBF } -- TODO maybe add other ones (like UTF-16)
|
||||||
|
local function has_bom()
|
||||||
|
for i = 1, #bom_table do
|
||||||
|
if i > #line then return false end
|
||||||
|
local ch, byte = line:sub(i, i), line:byte(i, i)
|
||||||
|
if byte ~= bom_table[i] then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return has_bom() and string.sub(line, #bom_table + 1) or line
|
||||||
|
end
|
||||||
|
|
||||||
|
local function trim(s)
|
||||||
|
return s:match "^%s*(.-)%s*$"
|
||||||
|
end
|
||||||
|
|
||||||
|
function AbstractSubtitle:parse_file(filename)
|
||||||
|
local lines = {}
|
||||||
|
for line in io.lines(filename) do
|
||||||
|
if #lines == 0 then line = self:sanitize(line) end
|
||||||
|
line = line:gsub('\r\n?', '') -- make sure there's no carriage return
|
||||||
|
line = trim(line)
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
function AbstractSubtitle:shift_timing(diff_seconds)
|
||||||
|
for _, entry in pairs(self.entries) do
|
||||||
|
if self.valid_entry(entry) then
|
||||||
|
entry.start_time = entry.start_time:adjustTime(diff_seconds)
|
||||||
|
entry.end_time = entry.end_time:adjustTime(diff_seconds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function AbstractSubtitle.valid_entry(entry)
|
||||||
|
return entry ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function inheritsFrom (baseClass)
|
||||||
|
local new_class = {}
|
||||||
|
local class_mt = { __index = new_class }
|
||||||
|
|
||||||
|
function new_class:create(filename)
|
||||||
|
local instance = {
|
||||||
|
filename = filename,
|
||||||
|
language = nil,
|
||||||
|
header = nil, -- will be empty for srt, some stuff for ass
|
||||||
|
entries = {} -- list of entries
|
||||||
|
}
|
||||||
|
setmetatable(instance, class_mt)
|
||||||
|
return instance
|
||||||
|
end
|
||||||
|
|
||||||
|
if baseClass then
|
||||||
|
setmetatable(new_class, { __index = baseClass })
|
||||||
|
end
|
||||||
|
return new_class
|
||||||
|
end
|
||||||
|
|
||||||
|
local SRT = inheritsFrom(AbstractSubtitle)
|
||||||
|
function SRT.entry()
|
||||||
|
return { index = nil, start_time = nil, end_time = nil, text = {} }
|
||||||
|
end
|
||||||
|
|
||||||
|
function SRT:populate(filename)
|
||||||
|
local timestamp_fmt = "^(%d+):(%d+):(%d+),(%d+) %-%-> (%d+):(%d+):(%d+),(%d+)$"
|
||||||
|
local function parse_timestamp(timestamp)
|
||||||
|
local function to_seconds(seconds, milliseconds)
|
||||||
|
return tonumber(string.format("%s.%s", seconds, milliseconds))
|
||||||
|
end
|
||||||
|
local _, _, from_h, from_m, from_s, from_ms, to_h, to_m, to_s, to_ms = timestamp:find(timestamp_fmt)
|
||||||
|
return TimeStamp:new(from_h, from_m, to_seconds(from_s, from_ms)), TimeStamp:new(to_h, to_m, to_seconds(to_s, to_ms))
|
||||||
|
end
|
||||||
|
|
||||||
|
local new = self:create(filename)
|
||||||
|
local entry = self.entry()
|
||||||
|
local f_idx, idx = 1, 1
|
||||||
|
for _, line in pairs(self:parse_file(filename)) do
|
||||||
|
if idx == 1 and #line > 0 then
|
||||||
|
assert(line:match("^%d+$"), string.format("SRT FORMAT ERROR (line %d): expected a number but got '%s'", f_idx, line))
|
||||||
|
entry.index = line
|
||||||
|
elseif idx == 2 then
|
||||||
|
assert(line:match("^%d+:%d+:%d+,%d+ %-%-> %d+:%d+:%d+,%d+$"), string.format("SRT FORMAT ERROR (line %d): expected a timecode string but got '%s'", f_idx, line))
|
||||||
|
local t_start, t_end = parse_timestamp(line)
|
||||||
|
entry.start_time, entry.end_time = t_start, t_end
|
||||||
|
else
|
||||||
|
if #line == 0 then
|
||||||
|
-- end of text
|
||||||
|
if entry.index ~= nil then
|
||||||
|
table.insert(new.entries, entry)
|
||||||
|
end
|
||||||
|
entry = SRT.entry()
|
||||||
|
idx = 0
|
||||||
|
else
|
||||||
|
table.insert(entry.text, line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
idx = idx + 1
|
||||||
|
f_idx = f_idx + 1
|
||||||
|
end
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
function SRT:toString()
|
||||||
|
local stringbuilder = {}
|
||||||
|
local function append(s)
|
||||||
|
table.insert(stringbuilder, s)
|
||||||
|
end
|
||||||
|
for _, entry in pairs(self.entries) do
|
||||||
|
append(entry.index)
|
||||||
|
local timestamp_string = string.format("%s --> %s", entry.start_time:toString(","), entry.end_time:toString(","))
|
||||||
|
append(timestamp_string)
|
||||||
|
if type(entry.text) == 'table' then
|
||||||
|
append(table.concat(entry.text, "\n"))
|
||||||
|
else append(entry.text) end
|
||||||
|
append('')
|
||||||
|
end
|
||||||
|
return table.concat(stringbuilder, '\n')
|
||||||
|
end
|
||||||
|
|
||||||
|
local ASS = inheritsFrom(AbstractSubtitle)
|
||||||
|
ASS.header_mapper = { ["Start"] = "start_time", ["End"] = "end_time" }
|
||||||
|
|
||||||
|
function ASS.valid_entry(entry)
|
||||||
|
return entry['type'] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ASS:toString()
|
||||||
|
local stringbuilder = {}
|
||||||
|
local function append(s) table.insert(stringbuilder, s) end
|
||||||
|
append(self.header)
|
||||||
|
append('[Events]')
|
||||||
|
for i = 1, #self.entries do
|
||||||
|
if i == 1 then
|
||||||
|
-- stringbuilder for events header
|
||||||
|
local event_sb = {};
|
||||||
|
for _, v in pairs(self.event_header) do table.insert(event_sb, v) end
|
||||||
|
append(string.format("Format: %s", table.concat(event_sb, ", ")))
|
||||||
|
end
|
||||||
|
local entry = self.entries[i]
|
||||||
|
local entry_sb = {}
|
||||||
|
for _, col in pairs(self.event_header) do
|
||||||
|
local value = entry[col]
|
||||||
|
local timestamp_entry_column = self.header_mapper[col]
|
||||||
|
if timestamp_entry_column then
|
||||||
|
value = entry[timestamp_entry_column]:toString(".")
|
||||||
|
end
|
||||||
|
table.insert(entry_sb, value)
|
||||||
|
end
|
||||||
|
append(string.format("%s: %s", entry['type'], table.concat(entry_sb, ",")))
|
||||||
|
end
|
||||||
|
return table.concat(stringbuilder, '\n')
|
||||||
|
end
|
||||||
|
|
||||||
|
function ASS:populate(filename, language)
|
||||||
|
local header, events, parser = {}, {}, nil
|
||||||
|
for _, line in pairs(self:parse_file(filename)) do
|
||||||
|
local _, _, event = string.find(line, "^%[([^%]]+)%]%s*$")
|
||||||
|
if event then
|
||||||
|
if event == "Events" then
|
||||||
|
parser = function(x) table.insert(events, x) end
|
||||||
|
else
|
||||||
|
parser = function(x) table.insert(header, x) end
|
||||||
|
parser(line)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
parser(line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- create subtitle instance
|
||||||
|
local ev_regex = "^(%a+):%s(.+)$"
|
||||||
|
local function parse_event(header_columns, ev)
|
||||||
|
local function create_timestamp(timestamp_str)
|
||||||
|
local timestamp_fmt = "^(%d+):(%d+):(%d+).(%d+)"
|
||||||
|
local _, _, h, m, s, ms = timestamp_str:find(timestamp_fmt)
|
||||||
|
return TimeStamp:new(h, m, TimeStamp.to_seconds(s, ms))
|
||||||
|
end
|
||||||
|
local new_event = {}
|
||||||
|
local _, _, ev_type, ev_values = string.find(ev, ev_regex)
|
||||||
|
new_event['type'] = ev_type
|
||||||
|
-- skipping last column, since that's the text, which can contain commas
|
||||||
|
local last_idx = 0;
|
||||||
|
for i = 1, #header_columns - 1 do
|
||||||
|
local col = header_columns[i]
|
||||||
|
local idx = string.find(ev_values, ",", last_idx + 1)
|
||||||
|
local val = ev_values:sub(last_idx + 1, idx - 1)
|
||||||
|
local timestamp_entry_column = self.header_mapper[col]
|
||||||
|
if timestamp_entry_column then
|
||||||
|
new_event[timestamp_entry_column] = create_timestamp(val)
|
||||||
|
else
|
||||||
|
new_event[col] = val
|
||||||
|
end
|
||||||
|
last_idx = idx
|
||||||
|
end
|
||||||
|
new_event[header_columns[#header_columns]] = ev_values:sub(last_idx + 1)
|
||||||
|
return new_event
|
||||||
|
end
|
||||||
|
|
||||||
|
local sub = self:create(filename)
|
||||||
|
sub.header = table.concat(header, "\n")
|
||||||
|
sub.language = language
|
||||||
|
-- remove and process first entry in events, which is a header
|
||||||
|
local _, _, colstring = string.find(table.remove(events, 1), "^%a+:%s(.+)$")
|
||||||
|
local columns = {};
|
||||||
|
for i in colstring:gmatch("[^%,%s]+") do table.insert(columns, i) end
|
||||||
|
sub.event_header = columns
|
||||||
|
for _, event in pairs(events) do
|
||||||
|
if #event > 0 then
|
||||||
|
table.insert(sub.entries, parse_event(columns, event))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return sub
|
||||||
|
end
|
||||||
|
|
||||||
|
P.AbstractSubtitle = AbstractSubtitle
|
||||||
|
P.ASS = ASS
|
||||||
|
P.SRT = SRT
|
||||||
|
return P
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
opts = {
|
||||||
|
blacklist="",
|
||||||
|
whitelist="",
|
||||||
|
remove_files_without_extension = false,
|
||||||
|
oneshot = true,
|
||||||
|
}
|
||||||
|
(require 'mp.options').read_options(opts)
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
|
||||||
|
function split(input)
|
||||||
|
local ret = {}
|
||||||
|
for str in string.gmatch(input, "([^,]+)") do
|
||||||
|
ret[#ret + 1] = str
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.blacklist = split(opts.blacklist)
|
||||||
|
opts.whitelist = split(opts.whitelist)
|
||||||
|
|
||||||
|
local exclude
|
||||||
|
if #opts.whitelist > 0 then
|
||||||
|
exclude = function(extension)
|
||||||
|
for _, ext in pairs(opts.whitelist) do
|
||||||
|
if extension == ext then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
elseif #opts.blacklist > 0 then
|
||||||
|
exclude = function(extension)
|
||||||
|
for _, ext in pairs(opts.blacklist) do
|
||||||
|
if extension == ext then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
function should_remove(filename)
|
||||||
|
if string.find(filename, "://") then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local extension = string.match(filename, "%.([^%.]+)$")
|
||||||
|
if not extension and opts.remove_file_without_extension then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if extension and exclude(string.lower(extension)) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function process(playlist_count)
|
||||||
|
if playlist_count < 2 then return end
|
||||||
|
if opts.oneshot then
|
||||||
|
mp.unobserve_property(observe)
|
||||||
|
end
|
||||||
|
local playlist = mp.get_property_native("playlist")
|
||||||
|
local removed = 0
|
||||||
|
for i = #playlist, 1, -1 do
|
||||||
|
if should_remove(playlist[i].filename) then
|
||||||
|
mp.commandv("playlist-remove", i-1)
|
||||||
|
removed = removed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if removed == #playlist then
|
||||||
|
msg.warn("Removed eveything from the playlist")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function observe(k,v) process(v) end
|
||||||
|
|
||||||
|
mp.observe_property("playlist-count", "number", observe)
|
||||||
@@ -0,0 +1,618 @@
|
|||||||
|
--[[
|
||||||
|
* chapter-make-read.lua v.2025-03-01
|
||||||
|
*
|
||||||
|
* AUTHORS: dyphire
|
||||||
|
* License: MIT
|
||||||
|
* link: https://github.com/dyphire/mpv-scripts
|
||||||
|
--]]
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Copyright (c) 2023 dyphire <qimoge@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
--]]
|
||||||
|
|
||||||
|
-- Implementation read and automatically load the namesake external chapter file.
|
||||||
|
-- The external chapter files should conform to the following formats.
|
||||||
|
-- Note: The Timestamps should use the 12-bit format of 'hh:mm:ss.sss'.
|
||||||
|
-- Note: The file encoding should be UTF-8 and the linebreak should be Unix(LF).
|
||||||
|
-- Note: The script also supports reading OGM format and MediaInfo format in addition to the following formats.
|
||||||
|
--[[
|
||||||
|
00:00:00.000 A part
|
||||||
|
00:00:40.312 OP
|
||||||
|
00:02:00.873 B part
|
||||||
|
00:10:44.269 C part
|
||||||
|
00:22:40.146 ED
|
||||||
|
--]]
|
||||||
|
|
||||||
|
-- This script also supports manually load/refresh,marks,edits,remove and creates external chapter files, usage:
|
||||||
|
-- Note: It can also be used to export the existing chapter information of the playback file.
|
||||||
|
-- add bindings to input.conf:
|
||||||
|
-- key script-message-to chapter_make_read load_chapter
|
||||||
|
-- key script-message-to chapter_make_read create_chapter
|
||||||
|
-- key script-message-to chapter_make_read edit_chapter
|
||||||
|
-- key script-message-to chapter_make_read remove_chapter
|
||||||
|
-- key script-message-to chapter_make_read write_chapter chp
|
||||||
|
-- key script-message-to chapter_make_read write_chapter ogm
|
||||||
|
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local options = require "mp.options"
|
||||||
|
|
||||||
|
local o = {
|
||||||
|
autoload = true,
|
||||||
|
autosave = false,
|
||||||
|
force_overwrite = false,
|
||||||
|
-- Specifies the extension of the external chapter file.
|
||||||
|
chapter_file_ext = ".chp",
|
||||||
|
-- Select whether the external chapter file needs to match the extension of the source file.
|
||||||
|
basename_with_ext = true,
|
||||||
|
-- Specifies the subpath of the same directory as the playback file as the external chapter file path.
|
||||||
|
-- Note: The external chapter file is read from the subdirectory first.
|
||||||
|
-- If the file does not exist, it will next be read from the same directory as the playback file.
|
||||||
|
external_chapter_subpath = "chapters",
|
||||||
|
-- save all chapter files in a single global directory
|
||||||
|
global_chapters = false,
|
||||||
|
global_chapters_dir = "~~/chapters",
|
||||||
|
-- hash works only in global_chapters_dir
|
||||||
|
hash = false,
|
||||||
|
-- ask for title or leave it empty
|
||||||
|
ask_for_title = true,
|
||||||
|
-- placeholder when asking for title of a new chapter
|
||||||
|
placeholder_title = "Chapter ",
|
||||||
|
-- pause the playback when asking for chapter title
|
||||||
|
pause_on_input = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
options.read_options(o)
|
||||||
|
|
||||||
|
local input_loaded, input = pcall(require, "mp.input")
|
||||||
|
-- Requires: https://github.com/CogentRedTester/mpv-user-input
|
||||||
|
local user_input_loaded, user_input = pcall(require, "user-input-module")
|
||||||
|
|
||||||
|
local path = nil
|
||||||
|
local dir = nil
|
||||||
|
local fname = nil
|
||||||
|
local chapter_fullpath = nil
|
||||||
|
local all_chapters = {}
|
||||||
|
local chapter_count = 0
|
||||||
|
local chapters_modified = false
|
||||||
|
local paused = false
|
||||||
|
local protocol = false
|
||||||
|
|
||||||
|
local function is_protocol(path)
|
||||||
|
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
function url_decode(str)
|
||||||
|
local function hex_to_char(x)
|
||||||
|
return string.char(tonumber(x, 16))
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
str = str:gsub('[\\/:%?]*', '')
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--create global_chapters_dir if it doesn't exist
|
||||||
|
local global_chapters_dir = mp.command_native({ "expand-path", o.global_chapters_dir })
|
||||||
|
if global_chapters_dir and global_chapters_dir ~= '' then
|
||||||
|
local meta = utils.file_info(global_chapters_dir)
|
||||||
|
if not meta or not meta.is_dir then
|
||||||
|
local is_windows = package.config:sub(1, 1) == "\\"
|
||||||
|
local windows_args = { 'powershell', '-NoProfile', '-Command', 'mkdir', string.format("\"%s\"", global_chapters_dir) }
|
||||||
|
local unix_args = { 'mkdir', '-p', global_chapters_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 global_chapters_dir save directory " .. global_chapters_dir ..
|
||||||
|
". Error: " .. (res.error or "unknown"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_chapter(func)
|
||||||
|
local meta = utils.file_info(chapter_fullpath)
|
||||||
|
if not meta or not meta.is_file then return end
|
||||||
|
local f = io.open(chapter_fullpath, "r")
|
||||||
|
if not f then return end
|
||||||
|
local contents = {}
|
||||||
|
for line in f:lines() do
|
||||||
|
table.insert(contents, (func(line)))
|
||||||
|
end
|
||||||
|
f:close()
|
||||||
|
return contents
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_chapter_table()
|
||||||
|
local line_pos = 0
|
||||||
|
return read_chapter(function(line)
|
||||||
|
local h, m, s, t, n, l
|
||||||
|
local thin_space = string.char(0xE2, 0x80, 0x89)
|
||||||
|
local line = line:gsub(thin_space, " ")
|
||||||
|
if line:match("^%d+:%d+:%d+") ~= nil then
|
||||||
|
h, m, s = line:match("^(%d+):(%d+):(%d+[,%.]?%d+)")
|
||||||
|
s = s:gsub(',', '.')
|
||||||
|
t = h * 3600 + m * 60 + s
|
||||||
|
if line:match("^%d+:%d+:%d+[,%.]?%d+[,%s].*") ~= nil then
|
||||||
|
n = line:match("^%d+:%d+:%d+[,%.]?%d+[,%s](.*)")
|
||||||
|
n = n:gsub(":%s%a?%a?:", "")
|
||||||
|
:gsub("^%s*(.-)%s*$", "%1")
|
||||||
|
end
|
||||||
|
l = line
|
||||||
|
line_pos = line_pos + 1
|
||||||
|
elseif line:match("^%d+:%d+[,%.]?%d+[,%s].*") ~= nil then
|
||||||
|
m, s = line:match("^(%d+):(%d+[,%.]?%d+)")
|
||||||
|
s = s:gsub(',', '.')
|
||||||
|
t = m * 60 + s
|
||||||
|
if line:match("^%d+:%d+[,%.]?%d+[,%s].*") ~= nil then
|
||||||
|
n = line:match("^%d+:%d+[,%.]?%d+[,%s](.*)")
|
||||||
|
n = n:gsub(":%s%a?%a?:", "")
|
||||||
|
:gsub("^%s*(.-)%s*$", "%1")
|
||||||
|
end
|
||||||
|
l = line
|
||||||
|
line_pos = line_pos + 1
|
||||||
|
elseif line:match("^CHAPTER%d+=%d+:%d+:%d+") ~= nil then
|
||||||
|
h, m, s = line:match("^CHAPTER%d+=(%d+):(%d+):(%d+[,%.]?%d+)")
|
||||||
|
s = s:gsub(',', '.')
|
||||||
|
t = h * 3600 + m * 60 + s
|
||||||
|
l = line
|
||||||
|
line_pos = line_pos + 1
|
||||||
|
elseif line:match("^CHAPTER%d+NAME=.*") ~= nil then
|
||||||
|
n = line:gsub("^CHAPTER%d+NAME=", "")
|
||||||
|
n = n:gsub("^%s*(.-)%s*$", "%1")
|
||||||
|
l = line
|
||||||
|
line_pos = line_pos + 1
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return { found_title = n, found_time = t, found_line = l }
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function refresh_globals()
|
||||||
|
path = mp.get_property("path")
|
||||||
|
if path then
|
||||||
|
protocol = is_protocol(path)
|
||||||
|
dir = utils.split_path(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
if protocol then
|
||||||
|
fname = url_decode(mp.get_property("media-title"))
|
||||||
|
elseif o.basename_with_ext then
|
||||||
|
fname = mp.get_property("filename")
|
||||||
|
else
|
||||||
|
fname = mp.get_property("filename/no-ext")
|
||||||
|
end
|
||||||
|
|
||||||
|
all_chapters = mp.get_property_native("chapter-list")
|
||||||
|
chapter_count = mp.get_property_number("chapter-list/count")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function format_time(seconds)
|
||||||
|
local result = ""
|
||||||
|
local hours, mins, secs, msecs
|
||||||
|
if seconds <= 0 then
|
||||||
|
return "00:00:00.000";
|
||||||
|
else
|
||||||
|
hours = string.format("%02.f", math.floor(seconds / 3600))
|
||||||
|
mins = string.format("%02.f", math.floor(seconds / 60 - (hours * 60)))
|
||||||
|
secs = string.format("%02.f", math.floor(seconds - hours * 60 * 60 - mins * 60))
|
||||||
|
msecs = string.format("%03.f", seconds * 1000 - hours * 60 * 60 * 1000 - mins * 60 * 1000 - secs * 1000)
|
||||||
|
result = hours .. ":" .. mins .. ":" .. secs .. "." .. msecs
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- for unix use only
|
||||||
|
-- returns a table of command path and varargs, or nil if command was not found
|
||||||
|
local function command_exists(command, ...)
|
||||||
|
msg.debug("looking for command:", command)
|
||||||
|
-- msg.debug("args:", )
|
||||||
|
local process = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
playback_only = false,
|
||||||
|
args = {"sh", "-c", "command -v -- " .. command}
|
||||||
|
})
|
||||||
|
|
||||||
|
if process.status == 0 then
|
||||||
|
local command_path = process.stdout:gsub("\n", "")
|
||||||
|
msg.debug("command found:", command_path)
|
||||||
|
return {command_path, ...}
|
||||||
|
else
|
||||||
|
msg.debug("command not found:", command)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns md5 hash of the full path of the current media file
|
||||||
|
local function hash(path)
|
||||||
|
if path == nil then
|
||||||
|
msg.debug("something is wrong with the path, can't get full_path, can't hash it")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug("hashing:", path)
|
||||||
|
|
||||||
|
local cmd = {
|
||||||
|
name = 'subprocess',
|
||||||
|
capture_stdout = true,
|
||||||
|
playback_only = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local args = nil
|
||||||
|
local is_unix = package.config:sub(1,1) == "/"
|
||||||
|
if is_unix then
|
||||||
|
local md5 = command_exists("md5sum") or command_exists("md5") or command_exists("openssl", "md5 | cut -d ' ' -f 2")
|
||||||
|
if md5 == nil then
|
||||||
|
msg.warn("no md5 command found, can't generate hash")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
md5 = table.concat(md5, " ")
|
||||||
|
cmd["stdin_data"] = path
|
||||||
|
args = {"sh", "-c", md5 .. " | cut -d ' ' -f 1 | tr '[:lower:]' '[:upper:]'" }
|
||||||
|
else --windows
|
||||||
|
-- https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.3
|
||||||
|
local hash_command = [[
|
||||||
|
$s = [System.IO.MemoryStream]::new();
|
||||||
|
$w = [System.IO.StreamWriter]::new($s);
|
||||||
|
$w.write(']] .. path .. [[');
|
||||||
|
$w.Flush();
|
||||||
|
$s.Position = 0;
|
||||||
|
Get-FileHash -Algorithm MD5 -InputStream $s | Select-Object -ExpandProperty Hash
|
||||||
|
]]
|
||||||
|
|
||||||
|
args = {"powershell", "-NoProfile", "-Command", hash_command}
|
||||||
|
end
|
||||||
|
cmd["args"] = args
|
||||||
|
msg.debug("hash cmd:", utils.to_string(cmd))
|
||||||
|
local process = mp.command_native(cmd)
|
||||||
|
|
||||||
|
if process.status == 0 then
|
||||||
|
local hash = process.stdout:gsub("%s+", "")
|
||||||
|
msg.debug("hash:", hash)
|
||||||
|
return hash
|
||||||
|
else
|
||||||
|
msg.warn("hash function failed")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_chapter_filename(path)
|
||||||
|
name = hash(path)
|
||||||
|
if name == nil then
|
||||||
|
msg.warn("hash function failed, fallback to filename")
|
||||||
|
name = fname
|
||||||
|
end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mark_chapter(force_overwrite)
|
||||||
|
refresh_globals()
|
||||||
|
if not path then return end
|
||||||
|
|
||||||
|
local chapter_index = 0
|
||||||
|
local chapters_time = {}
|
||||||
|
local chapters_title = {}
|
||||||
|
local fpath = dir
|
||||||
|
if protocol then
|
||||||
|
fpath = global_chapters_dir
|
||||||
|
if o.hash then fname = get_chapter_filename(path) end
|
||||||
|
elseif o.external_chapter_subpath ~= '' then
|
||||||
|
fpath = utils.join_path(dir, o.external_chapter_subpath)
|
||||||
|
local meta = utils.file_info(fpath)
|
||||||
|
if not meta or not meta.is_dir then
|
||||||
|
fpath = dir
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if o.global_chapters and global_chapters_dir and global_chapters_dir ~= '' and not protocol then
|
||||||
|
fpath = global_chapters_dir
|
||||||
|
local meta = utils.file_info(fpath)
|
||||||
|
if meta and meta.is_dir then
|
||||||
|
if o.hash then
|
||||||
|
fname = get_chapter_filename(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local chapter_filename = fname .. o.chapter_file_ext
|
||||||
|
chapter_fullpath = utils.join_path(fpath, chapter_filename)
|
||||||
|
local fmeta = utils.file_info(chapter_fullpath)
|
||||||
|
if (not fmeta or not fmeta.is_file) and fpath ~= dir and not protocol then
|
||||||
|
if o.basename_with_ext then
|
||||||
|
fname = mp.get_property("filename")
|
||||||
|
else
|
||||||
|
fname = mp.get_property("filename/no-ext")
|
||||||
|
end
|
||||||
|
chapter_filename = fname .. o.chapter_file_ext
|
||||||
|
chapter_fullpath = utils.join_path(dir, chapter_filename)
|
||||||
|
end
|
||||||
|
local list_contents = read_chapter_table()
|
||||||
|
|
||||||
|
if not list_contents then return end
|
||||||
|
for i = 1, #list_contents do
|
||||||
|
local chapter_time = tonumber(list_contents[i].found_time)
|
||||||
|
if chapter_time ~= nil and chapter_time >= 0 then
|
||||||
|
table.insert(chapters_time, chapter_time)
|
||||||
|
end
|
||||||
|
if list_contents[i].found_title ~= nil then
|
||||||
|
table.insert(chapters_title, list_contents[i].found_title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not chapters_time[1] then return end
|
||||||
|
|
||||||
|
table.sort(chapters_time, function(a, b) return a < b end)
|
||||||
|
|
||||||
|
if force_overwrite then all_chapters = {} end
|
||||||
|
for i = 1, #chapters_time do
|
||||||
|
chapter_index = chapter_index + 1
|
||||||
|
all_chapters[chapter_index] = {
|
||||||
|
title = chapters_title[i] or ("Chapter " .. string.format("%02.f", chapter_index)),
|
||||||
|
time = chapters_time[i]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(all_chapters, function(a, b) return a['time'] < b['time'] end)
|
||||||
|
|
||||||
|
mp.set_property_native("chapter-list", all_chapters)
|
||||||
|
msg.info("load external chapter file successful: " .. chapter_filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function change_chapter_list(chapter_tltle, chapter_index)
|
||||||
|
local chapter_list = mp.get_property_native("chapter-list")
|
||||||
|
|
||||||
|
if chapter_index > mp.get_property_number("chapter-list/count") then
|
||||||
|
msg.warn("can't set chapter title")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
chapter_list[chapter_index].title = chapter_tltle
|
||||||
|
mp.set_property_native("chapter-list", chapter_list)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function change_title_callback(user_input, err, chapter_index)
|
||||||
|
if user_input == nil or err ~= nil then
|
||||||
|
if paused then return elseif o.pause_on_input then mp.set_property_native("pause", false) end
|
||||||
|
msg.warn("no chapter title provided:", err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
change_chapter_list(user_input, chapter_index)
|
||||||
|
if paused then return elseif o.pause_on_input then mp.set_property_native("pause", false) end
|
||||||
|
chapters_modified = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function input_title(default_input, cursor_pos, chapter_index)
|
||||||
|
input.get({
|
||||||
|
prompt = 'Chapter title:',
|
||||||
|
default_text = default_input,
|
||||||
|
cursor_position = cursor_pos,
|
||||||
|
submit = function(text)
|
||||||
|
input.terminate()
|
||||||
|
change_chapter_list(text, chapter_index)
|
||||||
|
end,
|
||||||
|
closed = function()
|
||||||
|
if paused then return elseif o.pause_on_input then mp.set_property_native("pause", false) end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function input_choice(title, chapter_index)
|
||||||
|
if not input_loaded and not user_input_loaded then
|
||||||
|
msg.error("no mpv-user-input, can't get user input, install: https://github.com/CogentRedTester/mpv-user-input")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if input_loaded then
|
||||||
|
input_title(title, #title + 1, chapter_index)
|
||||||
|
elseif user_input_loaded then
|
||||||
|
-- ask user for chapter title
|
||||||
|
-- (+1 because mpv indexes from 0, lua from 1)
|
||||||
|
user_input.get_user_input(change_title_callback, {
|
||||||
|
request_text = "Chapter title:",
|
||||||
|
default_input = title,
|
||||||
|
cursor_pos = #title + 1,
|
||||||
|
}, chapter_index)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function create_chapter()
|
||||||
|
refresh_globals()
|
||||||
|
if not path then return end
|
||||||
|
|
||||||
|
local time_pos = mp.get_property_number("time-pos")
|
||||||
|
local time_pos_osd = mp.get_property_osd("time-pos/full")
|
||||||
|
local current_chapter = mp.get_property_number("chapter")
|
||||||
|
mp.osd_message(time_pos_osd, 1)
|
||||||
|
|
||||||
|
if chapter_count == 0 then
|
||||||
|
all_chapters[1] = {
|
||||||
|
title = o.placeholder_title .. "01",
|
||||||
|
time = time_pos
|
||||||
|
}
|
||||||
|
-- We just set it to zero here so when we add 1 later it ends up as 1
|
||||||
|
-- otherwise it's probably "nil"
|
||||||
|
current_chapter = 0
|
||||||
|
-- note that mpv will treat the beginning of the file as all_chapters[0] when using pageup/pagedown
|
||||||
|
-- so we don't actually have to worry if the file doesn't start with a chapter
|
||||||
|
else
|
||||||
|
-- to insert a chapter we have to increase the index on all subsequent chapters
|
||||||
|
-- otherwise we'll end up with duplicate chapter IDs which will confuse mpv
|
||||||
|
-- +2 looks weird, but remember mpv indexes at 0 and lua indexes at 1
|
||||||
|
-- adding two will turn "current chapter" from mpv notation into "next chapter" from lua's notation
|
||||||
|
-- count down because these areas of memory overlap
|
||||||
|
for i = chapter_count, current_chapter + 2, -1 do
|
||||||
|
all_chapters[i + 1] = all_chapters[i]
|
||||||
|
end
|
||||||
|
all_chapters[current_chapter + 2] = {
|
||||||
|
title = o.placeholder_title .. string.format("%02.f", current_chapter + 2),
|
||||||
|
time = time_pos
|
||||||
|
}
|
||||||
|
end
|
||||||
|
mp.set_property_native("chapter-list", all_chapters)
|
||||||
|
mp.set_property_number("chapter", current_chapter + 1)
|
||||||
|
chapters_modified = true
|
||||||
|
|
||||||
|
if o.ask_for_title then
|
||||||
|
local chapter_index = mp.get_property_number("chapter") + 1
|
||||||
|
local title = o.placeholder_title .. string.format("%02.f", chapter_index)
|
||||||
|
|
||||||
|
input_choice(title, chapter_index)
|
||||||
|
|
||||||
|
if o.pause_on_input then
|
||||||
|
paused = mp.get_property_native("pause")
|
||||||
|
mp.set_property_bool("pause", true)
|
||||||
|
-- FIXME: for whatever reason osd gets hidden when we pause the
|
||||||
|
-- playback like that, workaround to make input prompt appear
|
||||||
|
-- right away without requiring mouse or keyboard action
|
||||||
|
mp.osd_message(" ", 0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function edit_chapter()
|
||||||
|
local chapter_index = mp.get_property_number("chapter") + 1
|
||||||
|
local chapter_list = mp.get_property_native("chapter-list")
|
||||||
|
local title = chapter_list[chapter_index + 1].title
|
||||||
|
if chapter_index == nil or chapter_index == -1 then
|
||||||
|
msg.verbose("no chapter selected, nothing to edit")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
input_choice(title, chapter_index)
|
||||||
|
|
||||||
|
if o.pause_on_input then
|
||||||
|
paused = mp.get_property_native("pause")
|
||||||
|
mp.set_property_bool("pause", true)
|
||||||
|
-- FIXME: for whatever reason osd gets hidden when we pause the
|
||||||
|
-- playback like that, workaround to make input prompt appear
|
||||||
|
-- right away without requiring mouse or keyboard action
|
||||||
|
mp.osd_message(" ", 0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_chapter()
|
||||||
|
local chapter_count = mp.get_property_number("chapter-list/count")
|
||||||
|
|
||||||
|
if chapter_count < 1 then
|
||||||
|
msg.verbose("no chapters to remove")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local chapter_list = mp.get_property_native("chapter-list")
|
||||||
|
-- +1 because mpv indexes from 0, lua from 1
|
||||||
|
local current_chapter = mp.get_property_number("chapter") + 1
|
||||||
|
|
||||||
|
table.remove(chapter_list, current_chapter)
|
||||||
|
msg.debug("removing chapter", current_chapter)
|
||||||
|
|
||||||
|
mp.set_property_native("chapter-list", chapter_list)
|
||||||
|
chapters_modified = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_chapter(format, force_write)
|
||||||
|
refresh_globals()
|
||||||
|
if not path or chapter_count == 0 or (not chapters_modified and not force_write) then
|
||||||
|
msg.debug("nothing to write")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if o.global_chapters then dir = global_chapters_dir end
|
||||||
|
if o.hash and o.global_chapters then fname = get_chapter_filename(path) end
|
||||||
|
local out_path = utils.join_path(dir, fname .. o.chapter_file_ext)
|
||||||
|
local chapters = ""
|
||||||
|
local next_chapter = nil
|
||||||
|
for i = 1, chapter_count, 1 do
|
||||||
|
local current_chapter = all_chapters[i]
|
||||||
|
local time_pos = format_time(current_chapter.time)
|
||||||
|
if format == "ogm" then
|
||||||
|
next_chapter = "CHAPTER" .. string.format("%02.f", i) .. "=" .. time_pos .. "\n" ..
|
||||||
|
"CHAPTER" .. string.format("%02.f", i) .. "NAME=" .. current_chapter.title .. "\n"
|
||||||
|
elseif format == "chp" then
|
||||||
|
next_chapter = time_pos .. " " .. current_chapter.title .. "\n"
|
||||||
|
else
|
||||||
|
msg.warn("please specify the correct chapter format: chp/ogm.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if i == 1 and (o.global_chapters or protocol) then
|
||||||
|
chapters = "# " .. path .. "\n\n" .. next_chapter
|
||||||
|
else
|
||||||
|
chapters = chapters .. next_chapter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = io.open(out_path, "w")
|
||||||
|
if file == nil then
|
||||||
|
dir = global_chapters_dir
|
||||||
|
fname = url_decode(mp.get_property("media-title"))
|
||||||
|
if o.hash then fname = get_chapter_filename(path) end
|
||||||
|
out_path = utils.join_path(dir, fname .. o.chapter_file_ext)
|
||||||
|
file = io.open(out_path, "w")
|
||||||
|
end
|
||||||
|
if file == nil then
|
||||||
|
mp.error("Could not open chapter file for writing.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
file:write(chapters)
|
||||||
|
file:close()
|
||||||
|
if not o.autosave then
|
||||||
|
mp.osd_message("Export chapter file to: " .. out_path, 3)
|
||||||
|
end
|
||||||
|
msg.info("Export chapter file to: " .. out_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HOOKS -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
if o.autoload then
|
||||||
|
mp.add_hook("on_preloaded", 50, function()
|
||||||
|
if o.force_overwrite then
|
||||||
|
mark_chapter(true)
|
||||||
|
else
|
||||||
|
mark_chapter(false)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
if o.autosave then
|
||||||
|
mp.add_hook("on_unload", 50, function()
|
||||||
|
write_chapter("chp", false)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
if user_input_loaded and not input_loaded then
|
||||||
|
mp.add_hook("on_unload", 50, function() user_input.cancel_user_input() end)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message("load_chapter", function() mark_chapter(true) end)
|
||||||
|
mp.register_script_message("create_chapter", create_chapter, { repeatable = true })
|
||||||
|
mp.register_script_message("remove_chapter", remove_chapter)
|
||||||
|
mp.register_script_message("edit_chapter", edit_chapter)
|
||||||
|
mp.register_script_message("write_chapter", function(format)
|
||||||
|
write_chapter(format, true)
|
||||||
|
end)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
-- chapterskip.lua
|
||||||
|
--
|
||||||
|
-- Ain't Nobody Got Time for That
|
||||||
|
--
|
||||||
|
-- This script skips chapters based on their title.
|
||||||
|
|
||||||
|
local categories = {
|
||||||
|
prologue = "^[Pp]rologue/^[Ii]ntro",
|
||||||
|
opening = "^OP/ OP$/^[Oo]pening/[Oo]pening$",
|
||||||
|
ending = "^ED/ ED$/^[Ee]nding/[Ee]nding$",
|
||||||
|
credits = "^[Cc]redits/[Cc]redits$",
|
||||||
|
preview = "[Pp]review$"
|
||||||
|
}
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
enabled = false,
|
||||||
|
skip_once = true,
|
||||||
|
categories = "",
|
||||||
|
skip = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.options = require "mp.options"
|
||||||
|
|
||||||
|
function matches(i, title)
|
||||||
|
for category in string.gmatch(options.skip, " *([^;]*[^; ]) *") do
|
||||||
|
if categories[category:lower()] then
|
||||||
|
if string.find(category:lower(), "^idx%-") == nil then
|
||||||
|
if title then
|
||||||
|
for pattern in string.gmatch(categories[category:lower()], "([^/]+)") do
|
||||||
|
if string.match(title, pattern) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for pattern in string.gmatch(categories[category:lower()], "([^/]+)") do
|
||||||
|
if tonumber(pattern) == i then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local skipped = {}
|
||||||
|
local parsed = {}
|
||||||
|
|
||||||
|
local function toggle_chapterskip()
|
||||||
|
options.enabled = not options.enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
function chapterskip(_, current)
|
||||||
|
mp.options.read_options(options, "chapterskip")
|
||||||
|
if not options.enabled then return end
|
||||||
|
for category in string.gmatch(options.categories, "([^;]+)") do
|
||||||
|
name, patterns = string.match(category, " *([^+>]*[^+> ]) *[+>](.*)")
|
||||||
|
if name then
|
||||||
|
categories[name:lower()] = patterns
|
||||||
|
elseif not parsed[category] then
|
||||||
|
mp.msg.warn("Improper category definition: " .. category)
|
||||||
|
end
|
||||||
|
parsed[category] = true
|
||||||
|
end
|
||||||
|
local chapters = mp.get_property_native("chapter-list")
|
||||||
|
local skip = false
|
||||||
|
for i, chapter in ipairs(chapters) do
|
||||||
|
if (not options.skip_once or not skipped[i]) and matches(i, chapter.title) then
|
||||||
|
if i == current + 1 or skip == i - 1 then
|
||||||
|
if skip then
|
||||||
|
skipped[skip] = true
|
||||||
|
end
|
||||||
|
skip = i
|
||||||
|
end
|
||||||
|
elseif skip then
|
||||||
|
mp.set_property("time-pos", chapter.time)
|
||||||
|
skipped[skip] = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if skip then
|
||||||
|
if mp.get_property_native("playlist-count") == mp.get_property_native("playlist-pos-1") then
|
||||||
|
return mp.set_property("time-pos", mp.get_property_native("duration"))
|
||||||
|
end
|
||||||
|
mp.commandv("playlist-next")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.observe_property("chapter", "number", chapterskip)
|
||||||
|
mp.register_event("file-loaded", function() skipped = {} end)
|
||||||
|
mp.register_script_message("chapter-skip", toggle_chapterskip)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
|||||||
|
--[[
|
||||||
|
script to cycle commands with a keybind, accomplished through script messages
|
||||||
|
available at: https://github.com/CogentRedTester/mpv-scripts
|
||||||
|
|
||||||
|
syntax:
|
||||||
|
script-message cycle-commands "command1 args" "command2 args" "command3 args"
|
||||||
|
|
||||||
|
The syntax of each command is identical to the standard input.conf syntax, but each command must be
|
||||||
|
a quoted string. Note that this may require you to nest (and potentially escape) quotes for the arguments.
|
||||||
|
Read the mpv documentation for how to do this: https://mpv.io/manual/master/#flat-command-syntax.
|
||||||
|
|
||||||
|
Semicolons also work exactly like they do normally, so you can easily send multiple commands each cycle.
|
||||||
|
|
||||||
|
Here are some examples of the same command using different quotes:
|
||||||
|
script-message cycle-commands "show-text one 1000 ; print-text two" "show-text \"three four\""
|
||||||
|
script-message cycle-commands 'show-text one 1000 ; print-text two' 'show-text "three four"'
|
||||||
|
script-message cycle-commands ``show-text one 1000 ; print-text two`` ``show-text "three four"``
|
||||||
|
|
||||||
|
This would, on keypress one, print 'one' to the OSD for 1 second and 'two' to the console,
|
||||||
|
and on keypress two 'three four' would be printed to the OSD.
|
||||||
|
Note that single (') and backtick (`) quoting was only added in mpv v0.34.
|
||||||
|
|
||||||
|
There are no limits to the number of commands, and the script message can be used as often as one wants.
|
||||||
|
The script stores the current iteration position for each unique set of command strings,
|
||||||
|
so there should be no overlap unless one binds the exact same set of strings (including spacing).
|
||||||
|
|
||||||
|
If the first command is `!reverse`, then the commands are cycled in the opposite direction.
|
||||||
|
If every subsequent command string is identical to a non-reversed cycle, then they share
|
||||||
|
their iteration position, making it possible to 'seek' forwards or backwards in the cycle:
|
||||||
|
script-message cycle-commands 'apply-profile profile1' 'apply-profile profile2' 'apply-profile profile3'
|
||||||
|
script-message cycle-commands !reverse 'apply-profile profile1' 'apply-profile profile2' 'apply-profile profile3'
|
||||||
|
|
||||||
|
Most commands should print messages to the OSD automatically, this can be controlled
|
||||||
|
by adding input prefixes to the commands: https://mpv.io/manual/master/#input-command-prefixes.
|
||||||
|
Some commands will not print an osd message even when told to, in this case you have two options:
|
||||||
|
you can add a show-text command to the cycle, or you can use the cycle-commands/osd script message
|
||||||
|
which will print the command string to the osd. For example:
|
||||||
|
script-message cycle-commands 'apply-profile profile1;show-text "applying profile1"' 'apply-profile profile2;show-text "applying profile2"'
|
||||||
|
script-message cycle-commands/osd 'apply-profile profile1' 'apply-profile profile2'
|
||||||
|
|
||||||
|
Any osd messages printed by the command will override the message sent by cycle-commands/osd.
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local mp = require 'mp'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
|
||||||
|
--keeps track of the current position for a specific cycle
|
||||||
|
local iterators = {}
|
||||||
|
|
||||||
|
--main function to identify and run the cycles
|
||||||
|
local function main(osd, ...)
|
||||||
|
local commands = {...}
|
||||||
|
|
||||||
|
local reverse = commands[1] == '!reverse'
|
||||||
|
if reverse then table.remove(commands, 1) end
|
||||||
|
|
||||||
|
--to identify the specific cycle we'll concatenate all the strings together to use as our table key
|
||||||
|
local str = ("%d> %s"):format(#commands, table.concat(commands, '|'))
|
||||||
|
msg.trace('recieved:', str)
|
||||||
|
|
||||||
|
-- we'll initialise the iterator at 0 (an invalid position) to support forward or backwards iteration
|
||||||
|
if iterators[str] == nil then
|
||||||
|
msg.debug('unknown cycle, creating iterator')
|
||||||
|
iterators[str] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
iterators[str] = iterators[str] + (reverse and -1 or 1)
|
||||||
|
if iterators[str] > #commands then iterators[str] = 1 end
|
||||||
|
if iterators[str] < 1 then iterators[str] = #commands end
|
||||||
|
|
||||||
|
--mp.command should run the commands exactly as if they were entered in input.conf.
|
||||||
|
--This should provide universal support for all input.conf command syntax
|
||||||
|
local cmd = commands[ iterators[str] ]
|
||||||
|
msg.verbose('sending command:', cmd)
|
||||||
|
if osd then mp.osd_message(cmd) end
|
||||||
|
mp.command(cmd)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message('cycle-commands', function(...) main(false, ...) end)
|
||||||
|
mp.register_script_message('cycle-commands/osd', function(...) main(true, ...) end)
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
|
||||||
|
--[[
|
||||||
|
|
||||||
|
https://github.com/stax76/mpv-scripts
|
||||||
|
|
||||||
|
This script instantly deletes the file that is currently playing
|
||||||
|
via keyboard shortcut, the file is moved to the recycle bin and
|
||||||
|
removed from the playlist.
|
||||||
|
|
||||||
|
On Linux the app trash-cli must be installed first.
|
||||||
|
On Ubuntu: sudo apt install trash-cli
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
Add bindings to input.conf:
|
||||||
|
|
||||||
|
# delete directly
|
||||||
|
KP0 script-message-to delete_current_file delete-file
|
||||||
|
|
||||||
|
# delete with confirmation
|
||||||
|
KP0 script-message-to delete_current_file delete-file KP1 "Press 1 to delete file"
|
||||||
|
|
||||||
|
Press KP0 to initiate the delete operation,
|
||||||
|
the script will ask to confirm by pressing KP1.
|
||||||
|
You may customize the the init and confirm key and the confirm message.
|
||||||
|
Confirm key and confirm message are optional.
|
||||||
|
|
||||||
|
Similar scripts:
|
||||||
|
https://github.com/zenyd/mpv-scripts#delete-file
|
||||||
|
|
||||||
|
]]--
|
||||||
|
|
||||||
|
key_bindings = {}
|
||||||
|
|
||||||
|
function file_exists(name)
|
||||||
|
if not name or name == '' then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local f = io.open(name, "r")
|
||||||
|
|
||||||
|
if f ~= nil then
|
||||||
|
io.close(f)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_protocol(path)
|
||||||
|
return type(path) == 'string' and (path:match('^%a[%a%d_-]+://'))
|
||||||
|
end
|
||||||
|
|
||||||
|
function delete_file(path)
|
||||||
|
local is_windows = package.config:sub(1,1) == "\\"
|
||||||
|
|
||||||
|
if is_protocol(path) or not file_exists(path) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_windows then
|
||||||
|
local ps_code = [[
|
||||||
|
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||||
|
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('__path__', 'OnlyErrorDialogs', 'SendToRecycleBin')
|
||||||
|
]]
|
||||||
|
|
||||||
|
local escaped_path = string.gsub(path, "'", "''")
|
||||||
|
escaped_path = string.gsub(escaped_path, "’", "’’")
|
||||||
|
escaped_path = string.gsub(escaped_path, "%%", "%%%%")
|
||||||
|
ps_code = string.gsub(ps_code, "__path__", escaped_path)
|
||||||
|
|
||||||
|
mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
detach = true,
|
||||||
|
args = { 'powershell', '-NoProfile', '-Command', ps_code },
|
||||||
|
})
|
||||||
|
else
|
||||||
|
mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
args = { 'trash', path },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function remove_current_file()
|
||||||
|
local count = mp.get_property_number("playlist-count")
|
||||||
|
local pos = mp.get_property_number("playlist-pos")
|
||||||
|
local new_pos = 0
|
||||||
|
|
||||||
|
if pos == count - 1 then
|
||||||
|
new_pos = pos - 1
|
||||||
|
else
|
||||||
|
new_pos = pos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.set_property_number("playlist-pos", new_pos)
|
||||||
|
|
||||||
|
if pos > -1 then
|
||||||
|
mp.command("playlist-remove " .. pos)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function handle_confirm_key()
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
|
||||||
|
if file_to_delete == path then
|
||||||
|
mp.commandv("show-text", "")
|
||||||
|
delete_file(file_to_delete)
|
||||||
|
remove_current_file()
|
||||||
|
remove_bindings()
|
||||||
|
file_to_delete = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function cleanup()
|
||||||
|
remove_bindings()
|
||||||
|
file_to_delete = ""
|
||||||
|
mp.commandv("show-text", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_bindings()
|
||||||
|
return {
|
||||||
|
{ confirm_key, handle_confirm_key },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function add_bindings()
|
||||||
|
if #key_bindings > 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local script_name = mp.get_script_name()
|
||||||
|
|
||||||
|
for _, bind in ipairs(get_bindings()) do
|
||||||
|
local name = script_name .. "_key_" .. (#key_bindings + 1)
|
||||||
|
key_bindings[#key_bindings + 1] = name
|
||||||
|
mp.add_forced_key_binding(bind[1], name, bind[2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function remove_bindings()
|
||||||
|
if #key_bindings == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, name in ipairs(key_bindings) do
|
||||||
|
mp.remove_key_binding(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
key_bindings = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function client_message(event)
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
|
||||||
|
if event.args[1] == "delete-file" and #event.args == 1 then
|
||||||
|
delete_file(path)
|
||||||
|
remove_current_file()
|
||||||
|
elseif event.args[1] == "delete-file" and #event.args == 3 and #key_bindings == 0 then
|
||||||
|
confirm_key = event.args[2]
|
||||||
|
mp.add_timeout(10, cleanup)
|
||||||
|
add_bindings()
|
||||||
|
file_to_delete = path
|
||||||
|
mp.commandv("show-text", event.args[3], "10000")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event("client-message", client_message)
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
|
||||||
|
--[[
|
||||||
|
|
||||||
|
https://github.com/stax76/mpv-scripts
|
||||||
|
|
||||||
|
This script instantly deletes the file that is currently playing
|
||||||
|
via keyboard shortcut, the file is moved to the recycle bin and
|
||||||
|
removed from the playlist.
|
||||||
|
|
||||||
|
On Linux the app trash-cli must be installed first.
|
||||||
|
On Ubuntu: sudo apt install trash-cli
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
Add bindings to input.conf:
|
||||||
|
|
||||||
|
# delete directly
|
||||||
|
KP0 script-message-to delete_current_file delete-file
|
||||||
|
|
||||||
|
# delete with confirmation
|
||||||
|
KP0 script-message-to delete_current_file delete-file KP1 "Press 1 to delete file"
|
||||||
|
|
||||||
|
Press KP0 to initiate the delete operation,
|
||||||
|
the script will ask to confirm by pressing KP1.
|
||||||
|
You may customize the the init and confirm key and the confirm message.
|
||||||
|
Confirm key and confirm message are optional.
|
||||||
|
|
||||||
|
Similar scripts:
|
||||||
|
https://github.com/zenyd/mpv-scripts#delete-file
|
||||||
|
|
||||||
|
]]--
|
||||||
|
|
||||||
|
key_bindings = {}
|
||||||
|
|
||||||
|
function file_exists(name)
|
||||||
|
if not name or name == '' then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local f = io.open(name, "r")
|
||||||
|
|
||||||
|
if f ~= nil then
|
||||||
|
io.close(f)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_protocol(path)
|
||||||
|
return type(path) == 'string' and (path:match('^%a[%a%d_-]+://'))
|
||||||
|
end
|
||||||
|
|
||||||
|
function delete_file(path)
|
||||||
|
local is_windows = package.config:sub(1,1) == "\\"
|
||||||
|
|
||||||
|
if is_protocol(path) or not file_exists(path) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_windows then
|
||||||
|
local ps_code = [[
|
||||||
|
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||||
|
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('__path__', 'OnlyErrorDialogs', 'SendToRecycleBin')
|
||||||
|
]]
|
||||||
|
|
||||||
|
local escaped_path = string.gsub(path, "'", "''")
|
||||||
|
escaped_path = string.gsub(escaped_path, "’", "’’")
|
||||||
|
escaped_path = string.gsub(escaped_path, "%%", "%%%%")
|
||||||
|
ps_code = string.gsub(ps_code, "__path__", escaped_path)
|
||||||
|
|
||||||
|
mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
detach = true,
|
||||||
|
args = { 'powershell', '-NoProfile', '-Command', ps_code },
|
||||||
|
})
|
||||||
|
else
|
||||||
|
mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
args = { 'trash', path },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function remove_current_file()
|
||||||
|
local count = mp.get_property_number("playlist-count")
|
||||||
|
local pos = mp.get_property_number("playlist-pos")
|
||||||
|
local new_pos = 0
|
||||||
|
|
||||||
|
if pos == count - 1 then
|
||||||
|
new_pos = pos - 1
|
||||||
|
else
|
||||||
|
new_pos = pos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.set_property_number("playlist-pos", new_pos)
|
||||||
|
|
||||||
|
if pos > -1 then
|
||||||
|
mp.command("playlist-remove " .. pos)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function handle_confirm_key()
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
|
||||||
|
if file_to_delete == path then
|
||||||
|
mp.commandv("show-text", "")
|
||||||
|
delete_file(file_to_delete)
|
||||||
|
remove_current_file()
|
||||||
|
remove_bindings()
|
||||||
|
file_to_delete = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function cleanup()
|
||||||
|
remove_bindings()
|
||||||
|
file_to_delete = ""
|
||||||
|
mp.commandv("show-text", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_bindings()
|
||||||
|
return {
|
||||||
|
{ confirm_key, handle_confirm_key },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function add_bindings()
|
||||||
|
if #key_bindings > 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local script_name = mp.get_script_name()
|
||||||
|
|
||||||
|
for _, bind in ipairs(get_bindings()) do
|
||||||
|
local name = script_name .. "_key_" .. (#key_bindings + 1)
|
||||||
|
key_bindings[#key_bindings + 1] = name
|
||||||
|
mp.add_forced_key_binding(bind[1], name, bind[2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function remove_bindings()
|
||||||
|
if #key_bindings == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, name in ipairs(key_bindings) do
|
||||||
|
mp.remove_key_binding(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
key_bindings = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function client_message(event)
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
|
||||||
|
if event.args[1] == "delete-file" and #event.args == 1 then
|
||||||
|
delete_file(path)
|
||||||
|
remove_current_file()
|
||||||
|
elseif event.args[1] == "delete-file" and #event.args == 3 and #key_bindings == 0 then
|
||||||
|
confirm_key = event.args[2]
|
||||||
|
mp.add_timeout(10, cleanup)
|
||||||
|
add_bindings()
|
||||||
|
file_to_delete = path
|
||||||
|
mp.commandv("show-text", event.args[3], "10000")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event("client-message", client_message)
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
-- evafast.lua
|
||||||
|
--
|
||||||
|
-- Much speed.
|
||||||
|
--
|
||||||
|
-- Jumps forwards when right arrow is tapped, speeds up when it's held.
|
||||||
|
-- Inspired by bilibili.com's player. Allows you to have both seeking and fast-forwarding on the same key.
|
||||||
|
-- Also supports toggling fastforward mode with a keypress.
|
||||||
|
-- Adjust --input-ar-delay to define when to start fastforwarding.
|
||||||
|
-- Define --hr-seek if you want accurate seeking.
|
||||||
|
-- If you just want a nicer fastforward.lua without hybrid key behavior, set seek_distance to 0.
|
||||||
|
-- Consider setting --sub-filter-regex="\`\s*\'" (on Linux) to ignore empty lines.
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
-- How far to jump on press, set to 0 to disable seeking and force fastforward
|
||||||
|
seek_distance = 5,
|
||||||
|
|
||||||
|
-- Playback speed modifier, applied once every speed_interval until cap is reached
|
||||||
|
speed_increase = 0.1,
|
||||||
|
speed_decrease = 0.1,
|
||||||
|
|
||||||
|
-- At what interval to apply speed modifiers
|
||||||
|
speed_interval = 0.05,
|
||||||
|
|
||||||
|
-- Playback speed cap
|
||||||
|
speed_cap = 2,
|
||||||
|
|
||||||
|
-- Playback speed cap when subtitles are displayed, ignored when equal to speed_cap
|
||||||
|
subs_speed_cap = 1.6,
|
||||||
|
|
||||||
|
-- Multiply current speed by modifier before adjustment (exponential speedup)
|
||||||
|
-- Use much lower values than default e.g. speed_increase=0.05, speed_decrease=0.025
|
||||||
|
multiply_modifier = false,
|
||||||
|
|
||||||
|
-- Show current speed on the osd (or flash speed if using uosc)
|
||||||
|
show_speed = true,
|
||||||
|
|
||||||
|
-- Show current speed on the osd when toggled (or flash speed if using uosc)
|
||||||
|
show_speed_toggled = true,
|
||||||
|
|
||||||
|
-- Show current speed on the osd when speeding up towards a target time (or flash speed if using uosc)
|
||||||
|
show_speed_target = false,
|
||||||
|
|
||||||
|
-- Show seek actions on the osd (or flash timeline if using uosc)
|
||||||
|
show_seek = true,
|
||||||
|
|
||||||
|
-- Look ahead for smoother transition when subs_speed_cap is set
|
||||||
|
subs_lookahead = true,
|
||||||
|
|
||||||
|
-- Symbols prepended to the osd message
|
||||||
|
osd_symbol = "{\\fnmpv-osd-symbols} {\\r}",
|
||||||
|
osd_rewind = "{\\fnmpv-osd-symbols} {\\r}"
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.options = require "mp.options"
|
||||||
|
mp.options.read_options(options, "evafast", function() end)
|
||||||
|
|
||||||
|
local uosc_available = false
|
||||||
|
local has_subtitle = true
|
||||||
|
local speedup_target = nil
|
||||||
|
local toggled_display = true
|
||||||
|
local toggled = false
|
||||||
|
local toggled_rewind = false
|
||||||
|
local speedup = false
|
||||||
|
local original_speed = 1
|
||||||
|
local next_sub_at = -1
|
||||||
|
local rewinding = false
|
||||||
|
local forced_slowdown = false
|
||||||
|
local file_duration = 0
|
||||||
|
local last_key_state = "up"
|
||||||
|
local was_rewinding = false
|
||||||
|
|
||||||
|
local ass_start = mp.get_property_osd("osd-ass-cc/0")
|
||||||
|
local ass_stop = mp.get_property_osd("osd-ass-cc/1")
|
||||||
|
|
||||||
|
local function speed_transition(current_speed, target_speed)
|
||||||
|
local speed_correction = current_speed >= target_speed and -options.speed_decrease or options.speed_increase
|
||||||
|
|
||||||
|
local time_for_correction = 0
|
||||||
|
local adjusted_speed = current_speed
|
||||||
|
|
||||||
|
while adjusted_speed ~= target_speed do
|
||||||
|
time_for_correction = time_for_correction + options.speed_interval * adjusted_speed
|
||||||
|
|
||||||
|
if options.multiply_modifier then
|
||||||
|
adjusted_speed = adjusted_speed + adjusted_speed * speed_correction
|
||||||
|
else
|
||||||
|
adjusted_speed = adjusted_speed + speed_correction
|
||||||
|
end
|
||||||
|
|
||||||
|
if (current_speed < target_speed and adjusted_speed > target_speed) or (current_speed > target_speed and adjusted_speed < target_speed) then
|
||||||
|
adjusted_speed = target_speed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return time_for_correction
|
||||||
|
end
|
||||||
|
|
||||||
|
local function next_sub(current_time)
|
||||||
|
local sub_delay = mp.get_property_native("sub-delay", 0)
|
||||||
|
local sub_visible = mp.get_property_bool("sub-visibility")
|
||||||
|
|
||||||
|
if sub_visible then
|
||||||
|
mp.set_property_bool("sub-visibility", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.command("no-osd sub-step 1")
|
||||||
|
|
||||||
|
local sub_next_delay = mp.get_property_native("sub-delay", 0)
|
||||||
|
mp.set_property("sub-delay", sub_delay)
|
||||||
|
|
||||||
|
if sub_visible then
|
||||||
|
mp.set_property_bool("sub-visibility", sub_visible)
|
||||||
|
end
|
||||||
|
|
||||||
|
if sub_delay - sub_next_delay == 0 then
|
||||||
|
return -2
|
||||||
|
end
|
||||||
|
|
||||||
|
local sub_next = current_time + sub_delay - sub_next_delay
|
||||||
|
|
||||||
|
normalized = math.floor(sub_next * 1000 + 0.5) / 1000
|
||||||
|
return normalized
|
||||||
|
end
|
||||||
|
|
||||||
|
local function flash_state(current_speed, display, forced)
|
||||||
|
local uosc_show = uosc_available and (display == nil or display == "uosc")
|
||||||
|
local osd_show = not uosc_available and (display == nil or display == "osd")
|
||||||
|
|
||||||
|
local show_special = (not speedup_target and options.show_speed_toggled) or (speedup_target and options.show_speed_target)
|
||||||
|
local show_toggled = show_special and (toggled or not speedup)
|
||||||
|
local show_regular = not toggled and toggled_display and options.show_speed
|
||||||
|
|
||||||
|
if current_speed and (show_regular or show_toggled or forced) then
|
||||||
|
if uosc_show then
|
||||||
|
mp.command("script-binding uosc/flash-speed")
|
||||||
|
elseif osd_show then
|
||||||
|
if current_speed == true then
|
||||||
|
current_speed = mp.get_property_number("speed", 1)
|
||||||
|
end
|
||||||
|
mp.osd_message(ass_start .. (was_rewinding and options.osd_rewind or options.osd_symbol) .. ass_stop .. string.format("x%.1f", current_speed))
|
||||||
|
end
|
||||||
|
elseif not current_speed and options.show_seek then
|
||||||
|
if uosc_show then
|
||||||
|
mp.command("script-binding uosc/flash-timeline")
|
||||||
|
elseif osd_show then
|
||||||
|
mp.osd_message(ass_start .. (was_rewinding and options.osd_rewind or options.osd_symbol))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_timer(reset)
|
||||||
|
if not reset and speed_timer:is_enabled() then return end
|
||||||
|
|
||||||
|
speed_timer.timeout = 0
|
||||||
|
speed_timer:resume()
|
||||||
|
speed_timer.timeout = options.speed_interval
|
||||||
|
end
|
||||||
|
|
||||||
|
local function evafast_speedup(toggle)
|
||||||
|
if not toggled and not speedup_target and not speed_timer:is_enabled() then
|
||||||
|
original_speed = mp.get_property_number("speed", 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
speedup = true
|
||||||
|
|
||||||
|
if toggle then
|
||||||
|
toggled = true
|
||||||
|
end
|
||||||
|
|
||||||
|
ensure_timer()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function evafast_slowdown(display)
|
||||||
|
forced_slowdown = false
|
||||||
|
if not display then
|
||||||
|
toggled_display = false
|
||||||
|
end
|
||||||
|
toggled = false
|
||||||
|
speedup = false
|
||||||
|
|
||||||
|
ensure_timer()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function evafast_toggle()
|
||||||
|
if toggled_rewind then
|
||||||
|
mp.set_property("play-dir", "+")
|
||||||
|
end
|
||||||
|
toggled_rewind = false
|
||||||
|
if speedup then
|
||||||
|
evafast_slowdown()
|
||||||
|
else
|
||||||
|
evafast_speedup(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function evafast_toggle_rewind()
|
||||||
|
rewinding = not speedup
|
||||||
|
mp.set_property("play-dir", rewinding and "-" or "+")
|
||||||
|
evafast_toggle()
|
||||||
|
toggled_rewind = rewinding
|
||||||
|
end
|
||||||
|
|
||||||
|
local function adjust_speed()
|
||||||
|
local current_time = mp.get_property_number("time-pos", 0)
|
||||||
|
local current_speed = mp.get_property_number("speed", 1)
|
||||||
|
local target_speed = original_speed
|
||||||
|
|
||||||
|
if speedup then
|
||||||
|
target_speed = options.speed_cap
|
||||||
|
|
||||||
|
if has_subtitle and target_speed ~= options.subs_speed_cap then
|
||||||
|
local sub_displayed = mp.get_property("sub-start") ~= nil
|
||||||
|
|
||||||
|
if sub_displayed then
|
||||||
|
target_speed = options.subs_speed_cap
|
||||||
|
elseif options.subs_lookahead then
|
||||||
|
if next_sub_at < current_time and next_sub_at ~= -2 then
|
||||||
|
next_sub_at = next_sub(current_time)
|
||||||
|
end
|
||||||
|
if target_speed ~= options.subs_speed_cap and next_sub_at > current_time then
|
||||||
|
local time_for_correction = speed_transition(options.speed_cap, options.subs_speed_cap)
|
||||||
|
if current_time + time_for_correction >= next_sub_at then
|
||||||
|
target_speed = options.subs_speed_cap
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if speedup_target ~= nil then
|
||||||
|
local effective_speedup_target = speedup_target >= 0 and speedup_target or (file_duration + speedup_target)
|
||||||
|
|
||||||
|
if current_time >= effective_speedup_target then
|
||||||
|
evafast_slowdown()
|
||||||
|
else
|
||||||
|
local time_for_correction = speed_transition(current_speed, original_speed)
|
||||||
|
if current_time + time_for_correction > effective_speedup_target or forced_slowdown then
|
||||||
|
forced_slowdown = true
|
||||||
|
speedup = false
|
||||||
|
target_speed = original_speed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if math.floor(target_speed * 1000 + 0.5) == math.floor(current_speed * 1000 + 0.5) then
|
||||||
|
if forced_slowdown or (not toggled and (not speedup or options.subs_speed_cap == options.speed_cap or (not has_subtitle and not speedup_target))) then
|
||||||
|
speed_timer:kill()
|
||||||
|
toggled_display = true
|
||||||
|
if speedup_target ~= nil then
|
||||||
|
evafast_slowdown()
|
||||||
|
end
|
||||||
|
speedup_target = nil
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_speed = current_speed
|
||||||
|
local speed_correction = 0
|
||||||
|
|
||||||
|
if options.multiply_modifier then
|
||||||
|
speed_correction = current_speed * options.speed_increase
|
||||||
|
else
|
||||||
|
speed_correction = options.speed_increase
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_speed > target_speed then
|
||||||
|
new_speed = math.max(current_speed - speed_correction, target_speed)
|
||||||
|
else
|
||||||
|
new_speed = math.min(current_speed + speed_correction, target_speed)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.set_property("speed", new_speed)
|
||||||
|
|
||||||
|
flash_state(new_speed)
|
||||||
|
end
|
||||||
|
|
||||||
|
speed_timer = mp.add_periodic_timer(100, adjust_speed)
|
||||||
|
speed_timer:kill()
|
||||||
|
|
||||||
|
local function evafast(keypress, rewind)
|
||||||
|
was_rewinding = false
|
||||||
|
if rewinding and not toggled_rewind and (not rewind or (keypress["event"] == "up" and last_key_state ~= "down")) then
|
||||||
|
rewinding = false
|
||||||
|
was_rewinding = true
|
||||||
|
mp.set_property("play-dir", "+")
|
||||||
|
end
|
||||||
|
if rewind then
|
||||||
|
was_rewinding = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if keypress["event"] == "down" then
|
||||||
|
if not speed_timer:is_enabled() then
|
||||||
|
if not toggled and not speedup_target then
|
||||||
|
original_speed = mp.get_property_number("speed", 1)
|
||||||
|
end
|
||||||
|
flash_state(nil, "osd")
|
||||||
|
flash_state(1, "uosc", true)
|
||||||
|
end
|
||||||
|
toggled_display = true
|
||||||
|
speed_timer:stop()
|
||||||
|
if options.seek_distance == 0 then
|
||||||
|
keypress["event"] = "repeat"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if keypress["event"] == "press" or keypress["event"] == "up" and last_key_state ~= "repeat" then
|
||||||
|
if not toggled and not speedup_target then
|
||||||
|
speed_timer:kill()
|
||||||
|
mp.set_property("speed", original_speed)
|
||||||
|
end
|
||||||
|
flash_state()
|
||||||
|
ensure_timer()
|
||||||
|
if rewind then
|
||||||
|
if not toggled_rewind then
|
||||||
|
rewinding = false
|
||||||
|
mp.set_property("play-dir", "+") -- unnecessary in some cases
|
||||||
|
end
|
||||||
|
mp.commandv("seek", -options.seek_distance)
|
||||||
|
else
|
||||||
|
mp.commandv("seek", options.seek_distance)
|
||||||
|
end
|
||||||
|
elseif keypress["event"] == "repeat" and last_key_state ~= "repeat" then
|
||||||
|
speedup = true
|
||||||
|
ensure_timer()
|
||||||
|
if rewind then
|
||||||
|
mp.set_property("play-dir", "-")
|
||||||
|
rewinding = true
|
||||||
|
end
|
||||||
|
elseif keypress["event"] == "up" and not toggled and not speedup_target then
|
||||||
|
evafast_slowdown(true)
|
||||||
|
ensure_timer(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_key_state = keypress["event"]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function evafast_rewind(keypress)
|
||||||
|
evafast(keypress, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.observe_property("duration", "native", function(prop, val)
|
||||||
|
file_duration = val or 0
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.observe_property("sid", "native", function(prop, val)
|
||||||
|
has_subtitle = (val or 0) ~= 0
|
||||||
|
next_sub_at = -1
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.observe_property("sub-start", "native", function(prop, val)
|
||||||
|
next_sub_at = -1
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_event("file-loaded", function()
|
||||||
|
next_sub_at = -1
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_event("seek", function()
|
||||||
|
next_sub_at = -1
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_script_message("uosc-version", function(version)
|
||||||
|
uosc_available = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_script_message("speedup-target", function(time)
|
||||||
|
local current_time = mp.get_property_number("time-pos", 0)
|
||||||
|
sign = string.sub(time, 1, 1)
|
||||||
|
time = tonumber(time) or 0
|
||||||
|
|
||||||
|
if sign == "+" then
|
||||||
|
time = current_time + time
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_time >= time and time >= 0 then
|
||||||
|
speedup_target = nil
|
||||||
|
evafast_slowdown()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
speedup_target = time
|
||||||
|
evafast_speedup()
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_script_message("get-version", function(script)
|
||||||
|
mp.commandv("script-message-to", script, "evafast-version", "2.0")
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.add_key_binding("RIGHT", "evafast", evafast, {repeatable = true, complex = true})
|
||||||
|
mp.add_key_binding(nil, "evafast-rewind", evafast_rewind, {repeatable = true, complex = true})
|
||||||
|
mp.add_key_binding(nil, "flash-speed", function() flash_state(true, nil, true) end)
|
||||||
|
mp.add_key_binding(nil, "speedup", evafast_speedup)
|
||||||
|
mp.add_key_binding(nil, "slowdown", evafast_slowdown)
|
||||||
|
mp.add_key_binding(nil, "toggle", evafast_toggle)
|
||||||
|
mp.add_key_binding(nil, "toggle-rewind", evafast_toggle_rewind)
|
||||||
|
|
||||||
|
mp.commandv("script-message-to", "uosc", "get-version", mp.get_script_name())
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
--[[
|
||||||
|
Fixed A/V sync when switching the audio output device with using audio filters
|
||||||
|
available at: https://github.com/dyphire/mpv-scripts
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local msg = require "mp.msg"
|
||||||
|
|
||||||
|
local function fix_avsync()
|
||||||
|
local paused = mp.get_property_bool("pause")
|
||||||
|
msg.info("fix A/V sync.")
|
||||||
|
mp.commandv("frame-back-step")
|
||||||
|
if paused then
|
||||||
|
return
|
||||||
|
else
|
||||||
|
mp.set_property_bool("pause", false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.observe_property("current-ao", "native", function(_, device)
|
||||||
|
local aid = mp.get_property_number("aid")
|
||||||
|
local has_af = mp.get_property("af", "") ~= ""
|
||||||
|
if device and aid and has_af then
|
||||||
|
fix_avsync()
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
-- Copyright (c) 2025 dyphire <qimoge@gmail.com>
|
||||||
|
-- License: MIT
|
||||||
|
-- link: https://github.com/dyphire/mpv-scripts
|
||||||
|
-- Automatically switches the display's SDR and HDR modes for HDR passthrough
|
||||||
|
-- based on the content of the video being played by the mpv, only works on Windows 10 and later systems
|
||||||
|
|
||||||
|
--! Required for use with mpv-display-plugin: https://github.com/dyphire/mpv-display-plugin
|
||||||
|
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local options = require 'mp.options'
|
||||||
|
|
||||||
|
local o = {
|
||||||
|
-- Specify the script working mode, value: noth, pass, switch. default: noth
|
||||||
|
-- noth: Do nothing
|
||||||
|
-- pass: Passing HDR signals for HDR content when the monitor is in HDR mode
|
||||||
|
-- switch: Automatically switch between HDR displays and SDR displays
|
||||||
|
-- on Windows 10 and later based on video specifications
|
||||||
|
hdr_mode = "noth",
|
||||||
|
-- Specify whether to switch HDR mode only when the window is in fullscreen or window maximized
|
||||||
|
-- only works with hdr_mode = "switch", default: false
|
||||||
|
fullscreen_only = false,
|
||||||
|
-- Specify the target peak of the HDR display, default: 203
|
||||||
|
-- must be the true peak brightness of the monitor,
|
||||||
|
-- otherwise it will cause HDR content to display incorrectly
|
||||||
|
target_peak = "203",
|
||||||
|
-- Specifies the measured contrast of the output display.
|
||||||
|
-- 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
|
||||||
|
-- OLED display do not need to change this, default: auto
|
||||||
|
target_contrast = "auto",
|
||||||
|
}
|
||||||
|
options.read_options(o, _, function() end)
|
||||||
|
|
||||||
|
local hdr_active = false
|
||||||
|
local hdr_supported = false
|
||||||
|
local first_switch_check = true
|
||||||
|
local file_loaded = false
|
||||||
|
|
||||||
|
local state = {
|
||||||
|
icc_profile = mp.get_property_native("icc-profile"),
|
||||||
|
icc_profile_auto = mp.get_property_native("icc-profile-auto"),
|
||||||
|
target_peak = mp.get_property_native("target-peak"),
|
||||||
|
target_prim = mp.get_property_native("target-prim"),
|
||||||
|
target_trc = mp.get_property_native("target-trc"),
|
||||||
|
target_contrast = mp.get_property_native("target_contrast"),
|
||||||
|
colorspace_hint = mp.get_property_native("target-colorspace-hint"),
|
||||||
|
inverse_mapping = mp.get_property_native("inverse-tone-mapping")
|
||||||
|
}
|
||||||
|
|
||||||
|
local function query_hdr_state()
|
||||||
|
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"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function switch_display_mode(enable)
|
||||||
|
if enable == hdr_active then return end
|
||||||
|
local arg = enable and "on" or "off"
|
||||||
|
mp.commandv('script-message', 'toggle-hdr-display', arg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_hdr_settings()
|
||||||
|
mp.set_property_native("icc-profile", "")
|
||||||
|
mp.set_property_native("icc-profile-auto", false)
|
||||||
|
mp.set_property_native("target-prim", "bt.2020")
|
||||||
|
mp.set_property_native("target-trc", "pq")
|
||||||
|
mp.set_property_native("target-peak", o.target_peak)
|
||||||
|
mp.set_property_native("target-contrast", o.target_contrast)
|
||||||
|
mp.set_property_native("target-colorspace-hint", "yes")
|
||||||
|
mp.set_property_native("inverse-tone-mapping", "no")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_sdr_settings()
|
||||||
|
mp.set_property_native("icc-profile", state.icc_profile)
|
||||||
|
mp.set_property_native("icc-profile-auto", state.icc_profile_auto)
|
||||||
|
mp.set_property_native("target-peak", "203")
|
||||||
|
mp.set_property_native("target-contrast", state.target_contrast)
|
||||||
|
mp.set_property_native("target-colorspace-hint", "no")
|
||||||
|
if state.target_prim ~= "bt.2020" then
|
||||||
|
mp.set_property_native("target-prim", state.target_prim)
|
||||||
|
else
|
||||||
|
mp.set_property_native("target-prim", "auto")
|
||||||
|
end
|
||||||
|
if state.target_trc ~= "pq" then
|
||||||
|
mp.set_property_native("target-trc", state.target_trc)
|
||||||
|
else
|
||||||
|
mp.set_property_native("target-trc", "auto")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reset_target_settings()
|
||||||
|
mp.set_property_native("target-peak", state.target_peak)
|
||||||
|
mp.set_property_native("target-prim", state.target_prim)
|
||||||
|
mp.set_property_native("target-trc", state.target_trc)
|
||||||
|
mp.set_property_native("target-contrast", state.target_contrast)
|
||||||
|
mp.set_property_native("target-colorspace-hint", state.colorspace_hint)
|
||||||
|
mp.set_property_native("inverse-tone-mapping", state.inverse_mapping)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pause_if_needed()
|
||||||
|
local paused = mp.get_property_native("pause")
|
||||||
|
if not paused then
|
||||||
|
mp.set_property_native("pause", true)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resume_if_needed(paused_before)
|
||||||
|
if paused_before then
|
||||||
|
mp.add_timeout(1, function()
|
||||||
|
mp.set_property_native("pause", false)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handle_hdr_logic(paused_before, target_peak, target_prim, target_trc)
|
||||||
|
query_hdr_state()
|
||||||
|
if hdr_active and o.hdr_mode ~= "noth" then
|
||||||
|
apply_hdr_settings()
|
||||||
|
resume_if_needed(paused_before)
|
||||||
|
elseif not hdr_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
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handle_sdr_logic(paused_before, target_peak, target_prim, target_trc)
|
||||||
|
query_hdr_state()
|
||||||
|
if not hdr_active or o.hdr_mode ~= "noth" then
|
||||||
|
if (not hdr_active or not state.inverse_mapping) and
|
||||||
|
(tonumber(target_peak) ~= 203 or target_prim == "bt.2020" or target_trc == "pq") then
|
||||||
|
apply_sdr_settings()
|
||||||
|
elseif hdr_active and state.inverse_mapping then
|
||||||
|
reset_target_settings()
|
||||||
|
end
|
||||||
|
resume_if_needed(paused_before)
|
||||||
|
end
|
||||||
|
if hdr_active and o.hdr_mode == "pass" and state.inverse_mapping then
|
||||||
|
reset_target_settings()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function should_switch_hdr(hdr_active, is_fullscreen)
|
||||||
|
if o.hdr_mode ~= "switch" then return false end
|
||||||
|
if not hdr_active and (not o.fullscreen_only or is_fullscreen) then
|
||||||
|
return true
|
||||||
|
elseif hdr_active and o.fullscreen_only and not is_fullscreen then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function switch_hdr()
|
||||||
|
query_hdr_state()
|
||||||
|
local params = mp.get_property_native("video-params")
|
||||||
|
local gamma = params and params["gamma"]
|
||||||
|
local max_luma = params and params["max-luma"]
|
||||||
|
local is_hdr = max_luma and max_luma > 203
|
||||||
|
if not gamma then return end
|
||||||
|
|
||||||
|
local current_state = is_hdr and "hdr" or "sdr"
|
||||||
|
local pause_changed = false
|
||||||
|
local fullscreen = mp.get_property_native("fullscreen")
|
||||||
|
local maximized = mp.get_property_native("window-maximized")
|
||||||
|
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")
|
||||||
|
local is_fullscreen = fullscreen or maximized
|
||||||
|
|
||||||
|
if current_state == "hdr" then
|
||||||
|
local function continue_hdr()
|
||||||
|
handle_hdr_logic(pause_changed, target_peak, target_prim, target_trc)
|
||||||
|
end
|
||||||
|
|
||||||
|
if first_switch_check and o.fullscreen_only and not is_fullscreen then
|
||||||
|
first_switch_check = false
|
||||||
|
elseif should_switch_hdr(hdr_active, is_fullscreen) then
|
||||||
|
pause_changed = pause_if_needed()
|
||||||
|
if hdr_active and o.fullscreen_only and not is_fullscreen then
|
||||||
|
msg.info("Switching to SDR output...")
|
||||||
|
switch_display_mode(false)
|
||||||
|
else
|
||||||
|
msg.info("Switching to HDR output...")
|
||||||
|
switch_display_mode(true)
|
||||||
|
end
|
||||||
|
mp.add_timeout(3, continue_hdr)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
handle_hdr_logic(false, target_peak, target_prim, target_trc)
|
||||||
|
|
||||||
|
elseif current_state == "sdr" then
|
||||||
|
local function continue_sdr()
|
||||||
|
handle_sdr_logic(pause_changed, target_peak, target_prim, target_trc)
|
||||||
|
end
|
||||||
|
|
||||||
|
if hdr_active and o.hdr_mode == "switch" and (not o.fullscreen_only or is_fullscreen) then
|
||||||
|
msg.info("Switching back to SDR output...")
|
||||||
|
pause_changed = pause_if_needed()
|
||||||
|
switch_display_mode(false)
|
||||||
|
mp.add_timeout(3, continue_sdr)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
handle_sdr_logic(false, target_peak, target_prim, target_trc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_paramet()
|
||||||
|
query_hdr_state()
|
||||||
|
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")
|
||||||
|
local target_contrast = mp.get_property_native("target-contrast")
|
||||||
|
local colorspace_hint = mp.get_property_native("target-colorspace-hint")
|
||||||
|
local inverse_mapping = mp.get_property_native("inverse-tone-mapping")
|
||||||
|
local params = mp.get_property_native("video-params")
|
||||||
|
local gamma = params and params["gamma"]
|
||||||
|
local max_luma = params and params["max-luma"]
|
||||||
|
local is_hdr = max_luma and max_luma > 203
|
||||||
|
if not gamma then return end
|
||||||
|
|
||||||
|
if is_hdr and hdr_active and o.hdr_mode ~= "noth" then
|
||||||
|
if target_peak ~= o.target_peak then
|
||||||
|
mp.set_property_native("target-peak", o.target_peak)
|
||||||
|
end
|
||||||
|
if target_contrast ~= o.target_contrast then
|
||||||
|
mp.set_property_native("target-contrast", o.target_contrast)
|
||||||
|
end
|
||||||
|
if target_prim ~= "bt.2020" then
|
||||||
|
mp.set_property_native("target-prim", "bt.2020")
|
||||||
|
end
|
||||||
|
if target_trc ~= "pq" then
|
||||||
|
mp.set_property_native("target-trc", "pq")
|
||||||
|
end
|
||||||
|
if colorspace_hint ~= "yes" then
|
||||||
|
mp.set_property_native("target-colorspace-hint", "yes")
|
||||||
|
end
|
||||||
|
if inverse_mapping then
|
||||||
|
mp.set_property_native("inverse-tone-mapping", "no")
|
||||||
|
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
|
||||||
|
|
||||||
|
local function on_start()
|
||||||
|
if o.hdr_mode == "noth" or tonumber(o.target_peak) <= 203 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local vo = mp.get_property("vo")
|
||||||
|
if vo and vo ~= "gpu-next" then
|
||||||
|
msg.warn("The current video output is not supported, please use gpu-next")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
file_loaded = true
|
||||||
|
query_hdr_state()
|
||||||
|
mp.observe_property("video-params", "native", switch_hdr)
|
||||||
|
mp.observe_property("target-peak", "native", check_paramet)
|
||||||
|
mp.observe_property("target-prim", "native", check_paramet)
|
||||||
|
mp.observe_property("target-trc", "native", check_paramet)
|
||||||
|
mp.observe_property("target-contrast", "native", check_paramet)
|
||||||
|
mp.observe_property("target-colorspace-hint", "native", check_paramet)
|
||||||
|
mp.observe_property("user-data/display-info/hdr-status", "native", switch_hdr)
|
||||||
|
if o.fullscreen_only then
|
||||||
|
mp.observe_property("fullscreen", "native", switch_hdr)
|
||||||
|
mp.observe_property("window-maximized", "native", switch_hdr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_end(event)
|
||||||
|
query_hdr_state()
|
||||||
|
first_switch_check = true
|
||||||
|
mp.unobserve_property(switch_hdr)
|
||||||
|
mp.unobserve_property(check_paramet)
|
||||||
|
if event["reason"] == "quit" and o.hdr_mode == "switch" then
|
||||||
|
if hdr_active then
|
||||||
|
msg.info("Restoring display to SDR on shutdown")
|
||||||
|
switch_display_mode(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
file_loaded = false
|
||||||
|
query_hdr_state()
|
||||||
|
if hdr_active then
|
||||||
|
msg.info("Restoring display to SDR on shutdown")
|
||||||
|
switch_display_mode(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event("start-file", on_start)
|
||||||
|
mp.register_event("end-file", on_end)
|
||||||
|
mp.observe_property("idle-active", "native", on_idle)
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
--lite version of the code written by sorayuki
|
||||||
|
--only keep the function to record the histroy and recover it
|
||||||
|
|
||||||
|
local mp = require 'mp'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local options = require 'mp.options'
|
||||||
|
local msg = require 'mp.msg' -- this is for debugging
|
||||||
|
|
||||||
|
local o = {
|
||||||
|
enabled = true,
|
||||||
|
-- eng=English, chs=Chinese Simplified
|
||||||
|
language = 'eng',
|
||||||
|
timeout = 15,
|
||||||
|
save_period = 30,
|
||||||
|
-- Set '/:dir%mpvconf%/historybookmarks' to use mpv config directory
|
||||||
|
-- OR change to '/:dir%script%/historybookmarks' for placing it in the same directory of script
|
||||||
|
-- OR change to '~~/historybookmarks' for sub path of mpv portable_config directory
|
||||||
|
-- OR write any variable using '/:var', such as: '/:var%APPDATA%/mpv/historybookmarks' or '/:var%HOME%/mpv/historybookmarks'
|
||||||
|
-- OR specify the absolute path
|
||||||
|
history_dir = "/:dir%mpvconf%/historybookmarks",
|
||||||
|
-- specifies the extension of the history-bookmark file
|
||||||
|
bookmark_ext = ".mpv.history",
|
||||||
|
-- use hash to bookmark_name
|
||||||
|
hash = true,
|
||||||
|
-- set false to get playlist from directory
|
||||||
|
use_playlist = true,
|
||||||
|
-- specifies a whitelist of files to find in a directory
|
||||||
|
whitelist = "3gp,amr,amv,asf,avi,avi,bdmv,f4v,flv,m2ts,m4v,mkv,mov,mp4,mpeg,mpg,ogv,rm,rmvb,ts,vob,webm,wmv",
|
||||||
|
-- excluded directories for shared, #windows: ["X:", "Z:", "F:/Download/", "Download"]
|
||||||
|
excluded_dir = [[
|
||||||
|
[]
|
||||||
|
]],
|
||||||
|
included_dir = [[
|
||||||
|
[]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
options.read_options(o, _, function() end)
|
||||||
|
|
||||||
|
o.excluded_dir = utils.parse_json(o.excluded_dir)
|
||||||
|
o.included_dir = utils.parse_json(o.included_dir)
|
||||||
|
|
||||||
|
local file_loaded = false
|
||||||
|
|
||||||
|
local locals = {
|
||||||
|
['eng'] = {
|
||||||
|
msg1 = 'Resume successfully',
|
||||||
|
msg2 = 'Resume the last played file in current directory',
|
||||||
|
msg3 = 'Press 1 to confirm, 0 to cancel',
|
||||||
|
},
|
||||||
|
['chs'] = {
|
||||||
|
msg1 = '成功恢复上次播放',
|
||||||
|
msg2 = '是否恢复当前目录的上次播放文件',
|
||||||
|
msg3 = '按1确认,按0取消',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- apply lang opts
|
||||||
|
local texts = locals[o.language]
|
||||||
|
|
||||||
|
-- `pl` stands for playlist
|
||||||
|
local path = nil
|
||||||
|
local dir = nil
|
||||||
|
local fname = nil
|
||||||
|
local pl_count = 0
|
||||||
|
local pl_name = nil
|
||||||
|
local pl_path = nil
|
||||||
|
local pl_list = {}
|
||||||
|
local pl_idx = 1
|
||||||
|
local current_idx = 1
|
||||||
|
local bookmark_path = nil
|
||||||
|
local history_dir = nil
|
||||||
|
local normalize_path = nil
|
||||||
|
|
||||||
|
local wait_msg
|
||||||
|
local on_key = false
|
||||||
|
|
||||||
|
if o.history_dir:find('^/:dir%%mpvconf%%') then
|
||||||
|
history_dir = o.history_dir:gsub('/:dir%%mpvconf%%', mp.find_config_file('.'))
|
||||||
|
elseif o.history_dir:find('^/:dir%%script%%') then
|
||||||
|
history_dir = o.history_dir:gsub('/:dir%%script%%', mp.find_config_file('scripts'))
|
||||||
|
elseif o.history_dir:find('/:var%%(.*)%%') then
|
||||||
|
local os_variable = o.history_dir:match('/:var%%(.*)%%')
|
||||||
|
history_dir = o.history_dir:gsub('/:var%%(.*)%%', os.getenv(os_variable))
|
||||||
|
else
|
||||||
|
history_dir = mp.command_native({ "expand-path", o.history_dir }) -- Expands both ~ and ~~
|
||||||
|
end
|
||||||
|
|
||||||
|
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, detect path separator, windows uses backslashes
|
||||||
|
--create history_dir if it doesn't exist
|
||||||
|
if history_dir ~= '' then
|
||||||
|
local meta = utils.file_info(history_dir)
|
||||||
|
if not meta or not meta.is_dir then
|
||||||
|
local windows_args = { 'powershell', '-NoProfile', '-Command', 'mkdir', string.format("\"%s\"", history_dir) }
|
||||||
|
local unix_args = { 'mkdir', '-p', history_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 history_dir save directory " .. history_dir ..
|
||||||
|
". Error: " .. (res.error or "unknown"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function split(input)
|
||||||
|
local ret = {}
|
||||||
|
for str in string.gmatch(input, "([^,]+)") do
|
||||||
|
ret[#ret + 1] = str
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
local ext_whitelist = split(o.whitelist)
|
||||||
|
|
||||||
|
local function exclude(extension)
|
||||||
|
if #ext_whitelist > 0 then
|
||||||
|
for _, ext in pairs(ext_whitelist) do
|
||||||
|
if extension == ext then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
for _, element in pairs(tab) do
|
||||||
|
if string.find(val, element) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tablelength(tab)
|
||||||
|
local count = 0
|
||||||
|
for _, _ in pairs(tab) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
local message_overlay = mp.create_osd_overlay('ass-events')
|
||||||
|
local message_timer = mp.add_timeout(1, function ()
|
||||||
|
message_overlay:remove()
|
||||||
|
end, true)
|
||||||
|
|
||||||
|
function show_message(text, time)
|
||||||
|
message_timer:kill()
|
||||||
|
message_timer.timeout = time or 1
|
||||||
|
message_overlay.data = text
|
||||||
|
message_overlay:update()
|
||||||
|
message_timer:resume()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalize(path)
|
||||||
|
if normalize_path ~= nil then
|
||||||
|
if normalize_path then
|
||||||
|
path = mp.command_native({"normalize-path", path})
|
||||||
|
else
|
||||||
|
local directory = mp.get_property("working-directory", "")
|
||||||
|
path = utils.join_path(directory, path:gsub('^%.[\\/]',''))
|
||||||
|
if is_windows then path = path:gsub("\\", "/") end
|
||||||
|
end
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
normalize_path = false
|
||||||
|
|
||||||
|
local commands = mp.get_property_native("command-list", {})
|
||||||
|
for _, command in ipairs(commands) do
|
||||||
|
if command.name == "normalize-path" then
|
||||||
|
normalize_path = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return normalize(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
function refresh_globals()
|
||||||
|
path = mp.get_property("path")
|
||||||
|
fname = mp.get_property("filename")
|
||||||
|
pl_count = mp.get_property_number('playlist-count', 0)
|
||||||
|
if path and not is_protocol(path) then
|
||||||
|
path = normalize(path)
|
||||||
|
dir = utils.split_path(path)
|
||||||
|
else
|
||||||
|
dir = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- for unix use only
|
||||||
|
-- returns a table of command path and varargs, or nil if command was not found
|
||||||
|
local function command_exists(command, ...)
|
||||||
|
msg.debug("looking for command:", command)
|
||||||
|
-- msg.debug("args:", )
|
||||||
|
local process = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
playback_only = false,
|
||||||
|
args = {"sh", "-c", "command -v -- " .. command}
|
||||||
|
})
|
||||||
|
|
||||||
|
if process.status == 0 then
|
||||||
|
local command_path = process.stdout:gsub("\n", "")
|
||||||
|
msg.debug("command found:", command_path)
|
||||||
|
return {command_path, ...}
|
||||||
|
else
|
||||||
|
msg.debug("command not found:", command)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns md5 hash of the full path of the current media file
|
||||||
|
local function hash(path)
|
||||||
|
if path == nil then
|
||||||
|
msg.debug("something is wrong with the path, can't get full_path, can't hash it")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug("hashing:", path)
|
||||||
|
|
||||||
|
local cmd = {
|
||||||
|
name = 'subprocess',
|
||||||
|
capture_stdout = true,
|
||||||
|
playback_only = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local args = nil
|
||||||
|
local is_unix = package.config:sub(1,1) == "/"
|
||||||
|
if is_unix then
|
||||||
|
local md5 = command_exists("md5sum") or command_exists("md5") or command_exists("openssl", "md5 | cut -d ' ' -f 2")
|
||||||
|
if md5 == nil then
|
||||||
|
msg.warn("no md5 command found, can't generate hash")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
md5 = table.concat(md5, " ")
|
||||||
|
cmd["stdin_data"] = path
|
||||||
|
args = {"sh", "-c", md5 .. " | cut -d ' ' -f 1 | tr '[:lower:]' '[:upper:]'" }
|
||||||
|
else --windows
|
||||||
|
-- https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.3
|
||||||
|
local hash_command = [[
|
||||||
|
$s = [System.IO.MemoryStream]::new();
|
||||||
|
$w = [System.IO.StreamWriter]::new($s);
|
||||||
|
$w.write(']] .. path .. [[');
|
||||||
|
$w.Flush();
|
||||||
|
$s.Position = 0;
|
||||||
|
Get-FileHash -Algorithm MD5 -InputStream $s | Select-Object -ExpandProperty Hash
|
||||||
|
]]
|
||||||
|
|
||||||
|
args = {"powershell", "-NoProfile", "-Command", hash_command}
|
||||||
|
end
|
||||||
|
cmd["args"] = args
|
||||||
|
msg.debug("hash cmd:", utils.to_string(cmd))
|
||||||
|
local process = mp.command_native(cmd)
|
||||||
|
|
||||||
|
if process.status == 0 then
|
||||||
|
local hash = process.stdout:gsub("%s+", "")
|
||||||
|
msg.debug("hash:", hash)
|
||||||
|
return hash
|
||||||
|
else
|
||||||
|
msg.warn("hash function failed")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_bookmark_path(dir)
|
||||||
|
local fpath = string.sub(dir, 1, -2)
|
||||||
|
local _, name = utils.split_path(fpath)
|
||||||
|
local history_name = nil
|
||||||
|
if o.hash then
|
||||||
|
history_name = hash(dir)
|
||||||
|
if history_name == nil then
|
||||||
|
msg.warn("hash function failed, fallback to dirname")
|
||||||
|
history_name = name
|
||||||
|
end
|
||||||
|
else
|
||||||
|
history_name = name
|
||||||
|
end
|
||||||
|
local bookmark_name = history_name .. o.bookmark_ext
|
||||||
|
bookmark_path = utils.join_path(history_dir, bookmark_name)
|
||||||
|
if is_windows then bookmark_path = bookmark_path:gsub("\\", "/") 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
|
||||||
|
|
||||||
|
-- get the content of the bookmark
|
||||||
|
-- Arg: bookmark_file (path)
|
||||||
|
-- Return: nil / content of the bookmark
|
||||||
|
local function get_record(bookmark_path)
|
||||||
|
local file = io.open(bookmark_path, 'r')
|
||||||
|
local record = file:read()
|
||||||
|
if record == nil then
|
||||||
|
msg.verbose('No history record is found in the bookmark file.')
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
msg.verbose('last play: ' .. record)
|
||||||
|
file:close()
|
||||||
|
return record
|
||||||
|
end
|
||||||
|
|
||||||
|
----- winapi start -----
|
||||||
|
-- in windows system, we can use the sorting function provided by the win32 API
|
||||||
|
-- see https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-strcmplogicalw
|
||||||
|
-- this function was taken from https://github.com/mpvnet-player/mpv.net/issues/575#issuecomment-1817413401
|
||||||
|
local winapi = {}
|
||||||
|
local is_windows = mp.get_property_native("platform") == "windows"
|
||||||
|
|
||||||
|
if is_windows then
|
||||||
|
-- is_ffi_loaded is false usually means the mpv builds without luajit
|
||||||
|
local is_ffi_loaded, ffi = pcall(require, "ffi")
|
||||||
|
|
||||||
|
if is_ffi_loaded then
|
||||||
|
winapi = {
|
||||||
|
ffi = ffi,
|
||||||
|
C = ffi.C,
|
||||||
|
CP_UTF8 = 65001,
|
||||||
|
shlwapi = ffi.load("shlwapi"),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ffi code from https://github.com/po5/thumbfast, Mozilla Public License Version 2.0
|
||||||
|
ffi.cdef[[
|
||||||
|
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr,
|
||||||
|
int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||||
|
int __stdcall StrCmpLogicalW(wchar_t *psz1, wchar_t *psz2);
|
||||||
|
]]
|
||||||
|
|
||||||
|
winapi.utf8_to_wide = function(utf8_str)
|
||||||
|
if utf8_str then
|
||||||
|
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, nil, 0)
|
||||||
|
|
||||||
|
if utf16_len > 0 then
|
||||||
|
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||||
|
|
||||||
|
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, utf16_str, utf16_len) > 0 then
|
||||||
|
return utf16_str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
----- winapi end -----
|
||||||
|
|
||||||
|
local function alphanumsort_windows(filenames)
|
||||||
|
table.sort(filenames, function(a, b)
|
||||||
|
local a_wide = winapi.utf8_to_wide(a)
|
||||||
|
local b_wide = winapi.utf8_to_wide(b)
|
||||||
|
return winapi.shlwapi.StrCmpLogicalW(a_wide, b_wide) == -1
|
||||||
|
end)
|
||||||
|
|
||||||
|
return filenames
|
||||||
|
end
|
||||||
|
|
||||||
|
-- alphanum sorting for humans in Lua
|
||||||
|
-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
|
||||||
|
local function alphanumsort_lua(filenames)
|
||||||
|
local function padnum(n, d)
|
||||||
|
return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
|
||||||
|
or ("%03d%s"):format(#n, n)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tuples = {}
|
||||||
|
for i, f in ipairs(filenames) do
|
||||||
|
tuples[i] = {f:lower():gsub("0*(%d+)%.?(%d*)", padnum), f}
|
||||||
|
end
|
||||||
|
table.sort(tuples, function(a, b)
|
||||||
|
return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
|
||||||
|
end)
|
||||||
|
for i, tuple in ipairs(tuples) do filenames[i] = tuple[2] end
|
||||||
|
return filenames
|
||||||
|
end
|
||||||
|
|
||||||
|
local function alphanumsort(filenames)
|
||||||
|
local is_ffi_loaded = pcall(require, "ffi")
|
||||||
|
if is_windows and is_ffi_loaded then
|
||||||
|
alphanumsort_windows(filenames)
|
||||||
|
else
|
||||||
|
alphanumsort_lua(filenames)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function create_playlist(dir)
|
||||||
|
local pl_list = {}
|
||||||
|
local file_list = utils.readdir(dir, 'files')
|
||||||
|
for i = 1, #file_list do
|
||||||
|
local file = file_list[i]
|
||||||
|
local ext = file:match('%.([^./]+)$')
|
||||||
|
if ext and exclude(ext:lower()) then
|
||||||
|
table.insert(pl_list, file)
|
||||||
|
msg.verbose("Adding " .. file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alphanumsort(pl_list)
|
||||||
|
return pl_list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_playlist()
|
||||||
|
local pl_list = {}
|
||||||
|
local playlist = mp.get_property_native("playlist")
|
||||||
|
for i = 0, #playlist - 1 do
|
||||||
|
local filename = mp.get_property("playlist/" .. i .. "/filename")
|
||||||
|
local _, file = utils.split_path(filename)
|
||||||
|
table.insert(pl_list, file)
|
||||||
|
end
|
||||||
|
return pl_list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- get the index of the wanted file playlist
|
||||||
|
-- if there is no playlist, return nil
|
||||||
|
local function get_playlist_idx(dst_file)
|
||||||
|
if dst_file == nil or dst_file == " " then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local idx = nil
|
||||||
|
for i = 1, #pl_list do
|
||||||
|
if (dst_file == pl_list[i]) then
|
||||||
|
idx = i
|
||||||
|
return idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return idx
|
||||||
|
end
|
||||||
|
|
||||||
|
local function jump_resume()
|
||||||
|
mp.unregister_event(jump_resume)
|
||||||
|
show_message(texts.msg1, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unbind_key()
|
||||||
|
msg.verbose('Unbinding keys')
|
||||||
|
wait_jump_timer:kill()
|
||||||
|
mp.remove_key_binding('key_jump')
|
||||||
|
mp.remove_key_binding('key_cancel')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function key_jump()
|
||||||
|
on_key = true
|
||||||
|
wait_jump_timer:kill()
|
||||||
|
unbind_key()
|
||||||
|
current_idx = pl_idx
|
||||||
|
mp.register_event('file-loaded', jump_resume)
|
||||||
|
msg.verbose('Jumping to ' .. pl_path)
|
||||||
|
mp.commandv('loadfile', pl_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function key_cancel()
|
||||||
|
on_key = true
|
||||||
|
wait_jump_timer:kill()
|
||||||
|
unbind_key()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bind_key()
|
||||||
|
mp.add_forced_key_binding('1', 'key_jump', key_jump)
|
||||||
|
mp.add_forced_key_binding('0', 'key_cancel', key_cancel)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- creat a .history file
|
||||||
|
local function record_history()
|
||||||
|
if not o.enabled or not file_loaded then return end
|
||||||
|
refresh_globals()
|
||||||
|
if not path or is_protocol(path) then return end
|
||||||
|
get_bookmark_path(dir)
|
||||||
|
local eof = mp.get_property_bool("eof-reached")
|
||||||
|
local percent_pos = mp.get_property_number("percent-pos", 0)
|
||||||
|
if not eof and percent_pos < 90 then
|
||||||
|
if fname ~= nil then
|
||||||
|
local file = io.open(bookmark_path, "w")
|
||||||
|
file:write(fname .. "\n")
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local file = io.open(bookmark_path, "w")
|
||||||
|
file:write(" " .. "\n")
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local timeout = o.timeout
|
||||||
|
local function wait_jumping()
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout > 0 then
|
||||||
|
if not on_key then
|
||||||
|
local msg = string.format("%s -- %s? (%s) %02d", wait_msg, texts.msg2, texts.msg3, timeout)
|
||||||
|
show_message(msg, 1)
|
||||||
|
bind_key()
|
||||||
|
else
|
||||||
|
timeout = 0
|
||||||
|
wait_jump_timer:kill()
|
||||||
|
unbind_key()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
wait_jump_timer:kill()
|
||||||
|
unbind_key()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- record the file name when video is paused
|
||||||
|
-- and stop the timer
|
||||||
|
local function pause(_, paused)
|
||||||
|
if paused then
|
||||||
|
timer4saving_history:stop()
|
||||||
|
record_history()
|
||||||
|
else
|
||||||
|
timer4saving_history:resume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- main function of the file
|
||||||
|
local function record()
|
||||||
|
if not o.enabled then return end
|
||||||
|
refresh_globals()
|
||||||
|
if pl_count and pl_count < 1 then return end
|
||||||
|
if not path or is_protocol(path) or not file_exist(path) then return end
|
||||||
|
if not dir or not fname then return end
|
||||||
|
get_bookmark_path(dir)
|
||||||
|
included_dir_count = tablelength(o.included_dir)
|
||||||
|
if included_dir_count > 0 then
|
||||||
|
if not need_ignore(o.included_dir, dir) then return end
|
||||||
|
end
|
||||||
|
if need_ignore(o.excluded_dir, dir) then return end
|
||||||
|
|
||||||
|
msg.verbose('folder -- ' .. dir)
|
||||||
|
msg.verbose('playing -- ' .. fname)
|
||||||
|
msg.verbose('bookmark path -- ' .. bookmark_path)
|
||||||
|
|
||||||
|
if (not file_exist(bookmark_path)) then
|
||||||
|
pl_name = nil
|
||||||
|
return
|
||||||
|
else
|
||||||
|
pl_name = get_record(bookmark_path)
|
||||||
|
if pl_name then
|
||||||
|
pl_path = utils.join_path(dir, pl_name)
|
||||||
|
else
|
||||||
|
pl_name = fname
|
||||||
|
pl_path = path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if o.use_playlist or pl_count > 1 then
|
||||||
|
pl_list = get_playlist()
|
||||||
|
else
|
||||||
|
pl_list = create_playlist(dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
pl_idx = get_playlist_idx(pl_name)
|
||||||
|
if (pl_idx == nil) then
|
||||||
|
msg.verbose('Playlist not found. Creating a new one...')
|
||||||
|
else
|
||||||
|
msg.verbose('playlist index --' .. pl_idx)
|
||||||
|
end
|
||||||
|
|
||||||
|
current_idx = get_playlist_idx(fname)
|
||||||
|
if current_idx then msg.verbose('current index -- ' .. current_idx) end
|
||||||
|
|
||||||
|
if current_idx and (pl_idx == nil) then
|
||||||
|
pl_idx = current_idx
|
||||||
|
pl_name = fname
|
||||||
|
pl_path = path
|
||||||
|
elseif current_idx and (pl_idx ~= current_idx) then
|
||||||
|
wait_msg = pl_idx
|
||||||
|
msg.verbose('Last watched episode -- ' .. wait_msg)
|
||||||
|
wait_jump_timer = mp.add_periodic_timer(1, wait_jumping)
|
||||||
|
end
|
||||||
|
timer4saving_history = mp.add_periodic_timer(o.save_period, record_history)
|
||||||
|
mp.observe_property("pause", "bool", pause)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event('file-loaded', function()
|
||||||
|
file_loaded = true
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
if not is_protocol(path) then
|
||||||
|
path = normalize(path)
|
||||||
|
directory = utils.split_path(path)
|
||||||
|
else
|
||||||
|
directory = nil
|
||||||
|
end
|
||||||
|
if directory ~= nil and directory ~= dir then
|
||||||
|
mp.add_timeout(0.5, record)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.add_hook("on_unload", 50, function()
|
||||||
|
mp.unobserve_property(pause)
|
||||||
|
record_history()
|
||||||
|
file_loaded = false
|
||||||
|
end)
|
||||||
@@ -0,0 +1,600 @@
|
|||||||
|
-- InputEvent
|
||||||
|
-- https://github.com/Natural-Harmonia-Gropius/InputEvent
|
||||||
|
|
||||||
|
local utils = require("mp.utils")
|
||||||
|
local opt = require("mp.options")
|
||||||
|
local msg = require("mp.msg")
|
||||||
|
local next = next
|
||||||
|
|
||||||
|
local watched_properties = {} -- indexed by property name (used as a set)
|
||||||
|
local cached_properties = {} -- property name -> last known raw value
|
||||||
|
local o = {
|
||||||
|
--enable external config
|
||||||
|
enable_external_config = false,
|
||||||
|
|
||||||
|
--external config file path
|
||||||
|
external_config = "~~/script-opts/inputevent_key.conf",
|
||||||
|
prefix = "event",
|
||||||
|
}
|
||||||
|
|
||||||
|
opt.read_options(o, _, function() end)
|
||||||
|
|
||||||
|
local bind_map = {}
|
||||||
|
|
||||||
|
local event_pattern = {
|
||||||
|
{ to = "penta_click", from = "down,up,down,up,down,up,down,up,down,up", length = 10 },
|
||||||
|
{ to = "quatra_click", from = "down,up,down,up,down,up,down,up", length = 8 },
|
||||||
|
{ to = "triple_click", from = "down,up,down,up,down,up", length = 6 },
|
||||||
|
{ to = "double_click", from = "down,up,down,up", length = 4 },
|
||||||
|
{ to = "click", from = "down,up", length = 2 },
|
||||||
|
{ to = "press", from = "down", length = 1 },
|
||||||
|
{ to = "release", from = "up", length = 1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
local supported_events = {
|
||||||
|
["repeat"] = true
|
||||||
|
}
|
||||||
|
for _, value in ipairs(event_pattern) do
|
||||||
|
supported_events[value.to] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- https://mpv.io/manual/master/#input-command-prefixes
|
||||||
|
local prefixes = { "osd-auto", "no-osd", "osd-bar", "osd-msg", "osd-msg-bar", "raw", "expand-properties",
|
||||||
|
"repeatable", "nonrepeatable", "async", "sync" }
|
||||||
|
|
||||||
|
-- https://mpv.io/manual/master/#list-of-input-commands
|
||||||
|
local commands = { "set", "cycle", "add", "multiply" }
|
||||||
|
|
||||||
|
function table:isEmpty()
|
||||||
|
if next(self) == nil then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function table:push(element)
|
||||||
|
self[#self + 1] = element
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function table:assign(source)
|
||||||
|
for key, value in pairs(source) do
|
||||||
|
self[key] = value
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function table:has(element)
|
||||||
|
for _, value in ipairs(self) do
|
||||||
|
if value == element then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function table:filter(filter)
|
||||||
|
local nt = {}
|
||||||
|
for index, value in ipairs(self) do
|
||||||
|
if (filter(index, value)) then
|
||||||
|
nt = table.push(nt, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nt
|
||||||
|
end
|
||||||
|
|
||||||
|
function table:join(separator)
|
||||||
|
local result = ""
|
||||||
|
for i, v in ipairs(self) do
|
||||||
|
local value = type(v) == "string" and v or tostring(v)
|
||||||
|
local semi = i == #self and "" or separator
|
||||||
|
result = result .. value .. semi
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function string:trim()
|
||||||
|
return (self:gsub("^%s*(.-)%s*$", "%1"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function string:replace(pattern, replacement)
|
||||||
|
local result, n = self:gsub(pattern, replacement)
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function string:split(separator)
|
||||||
|
local fields = {}
|
||||||
|
local separator = separator or ":"
|
||||||
|
local pattern = string.format("([^%s]+)", separator)
|
||||||
|
local copy = self:gsub(pattern, function(c) fields[#fields + 1] = c end)
|
||||||
|
return fields
|
||||||
|
end
|
||||||
|
|
||||||
|
local function debounce(func, wait)
|
||||||
|
func = type(func) == "function" and func or function() end
|
||||||
|
wait = type(wait) == "number" and wait / 1000 or 0
|
||||||
|
|
||||||
|
local timer = nil
|
||||||
|
local timer_end = function()
|
||||||
|
if timer then
|
||||||
|
timer:kill()
|
||||||
|
timer = nil
|
||||||
|
end
|
||||||
|
func()
|
||||||
|
end
|
||||||
|
|
||||||
|
return function()
|
||||||
|
if timer then
|
||||||
|
timer:kill()
|
||||||
|
end
|
||||||
|
timer = mp.add_timeout(wait, timer_end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function now()
|
||||||
|
return mp.get_time() * 1000
|
||||||
|
end
|
||||||
|
|
||||||
|
local function command(command)
|
||||||
|
if not command or command == '' then return true end
|
||||||
|
return mp.command(command)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function command_split(command)
|
||||||
|
local separator = { ";" }
|
||||||
|
local escape = { "\\" }
|
||||||
|
local quotation = { '"', "'" }
|
||||||
|
local quotation_stack = {}
|
||||||
|
local result = {}
|
||||||
|
local temp = ""
|
||||||
|
|
||||||
|
for i = 1, #command do
|
||||||
|
local char = command:sub(i, i)
|
||||||
|
|
||||||
|
if table.has(separator, char) and #quotation_stack == 0 then
|
||||||
|
result = table.push(result, temp)
|
||||||
|
temp = ""
|
||||||
|
elseif table.has(quotation, char) and not table.has(escape, temp:sub(#temp, #temp)) then
|
||||||
|
temp = temp .. char
|
||||||
|
if quotation_stack[#quotation_stack] == char then
|
||||||
|
quotation_stack = table.filter(quotation_stack, function(i, v) return i ~= #quotation_stack end)
|
||||||
|
else
|
||||||
|
quotation_stack = table.push(quotation_stack, char)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
temp = temp .. char
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #temp then
|
||||||
|
result = table.push(result, temp)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function command_invert(command)
|
||||||
|
local invert = ""
|
||||||
|
local command_list = command_split(command)
|
||||||
|
for i, v in ipairs(command_list) do
|
||||||
|
local trimed = v:trim()
|
||||||
|
local subs = trimed:split("%s*")
|
||||||
|
local prefix, command, property = "", nil, nil
|
||||||
|
for _, s in ipairs(subs) do
|
||||||
|
local sub = s:trim()
|
||||||
|
if not command and table.has(prefixes, sub) then
|
||||||
|
prefix = prefix .. " " .. sub
|
||||||
|
elseif not command then
|
||||||
|
if table.has(commands, sub) then
|
||||||
|
command = sub
|
||||||
|
else
|
||||||
|
msg.warn("\"" .. trimed .. "\" doesn't support auto restore.")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
elseif command and not property then
|
||||||
|
property = sub
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
repeat -- workaround continue
|
||||||
|
if not command or not property then
|
||||||
|
msg.warn("\"" .. trimed .. "\" doesn't support auto restore.")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
local value = mp.get_property(property)
|
||||||
|
if value then
|
||||||
|
local semi = i == #command_list and "" or ";"
|
||||||
|
invert = invert .. prefix:trim() .. " set " .. property .. " " .. value .. semi
|
||||||
|
else
|
||||||
|
msg.warn("\"" .. trimed .. "\" doesn't support auto restore.")
|
||||||
|
end
|
||||||
|
until true
|
||||||
|
end
|
||||||
|
msg.verbose("command_invert:" .. invert)
|
||||||
|
return invert
|
||||||
|
end
|
||||||
|
|
||||||
|
-- https://github.com/mpv-player/mpv/blob/master/player/lua/auto_profiles.lua
|
||||||
|
local function on_property_change(name, val)
|
||||||
|
cached_properties[name] = val
|
||||||
|
end
|
||||||
|
|
||||||
|
local function magic_get(name)
|
||||||
|
-- Lua identifiers can't contain "-", so in order to match with mpv
|
||||||
|
-- property conventions, replace "_" to "-"
|
||||||
|
name = string.gsub(name, "_", "-")
|
||||||
|
if not watched_properties[name] then
|
||||||
|
watched_properties[name] = true
|
||||||
|
local res, err = mp.get_property_native(name)
|
||||||
|
if err == "property not found" then
|
||||||
|
msg.error("Property '" .. name .. "' was not found.")
|
||||||
|
return default
|
||||||
|
end
|
||||||
|
cached_properties[name] = res
|
||||||
|
mp.observe_property(name, "native", on_property_change)
|
||||||
|
end
|
||||||
|
return cached_properties[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
local evil_magic = {}
|
||||||
|
setmetatable(evil_magic, {
|
||||||
|
__index = function(table, key)
|
||||||
|
-- interpret everything as property, unless it already exists as
|
||||||
|
-- a non-nil global value
|
||||||
|
local v = _G[key]
|
||||||
|
if type(v) ~= "nil" then
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
return magic_get(key)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
p = {}
|
||||||
|
setmetatable(p, {
|
||||||
|
__index = function(table, key)
|
||||||
|
return magic_get(key)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
local function compile_cond(name, s)
|
||||||
|
local code, chunkname = "return " .. s, "Event " .. name .. " condition"
|
||||||
|
local chunk, err
|
||||||
|
if setfenv then -- lua 5.1
|
||||||
|
chunk, err = loadstring(code, chunkname)
|
||||||
|
if chunk then
|
||||||
|
setfenv(chunk, evil_magic)
|
||||||
|
end
|
||||||
|
else -- lua 5.2
|
||||||
|
chunk, err = load(code, chunkname, "t", evil_magic)
|
||||||
|
end
|
||||||
|
if not chunk then
|
||||||
|
msg.error("Event '" .. name .. "' condition: " .. err)
|
||||||
|
chunk = function() return false end
|
||||||
|
end
|
||||||
|
return chunk
|
||||||
|
end
|
||||||
|
|
||||||
|
local InputEvent = {}
|
||||||
|
|
||||||
|
function InputEvent:new(key, on)
|
||||||
|
local Instance = {}
|
||||||
|
setmetatable(Instance, self);
|
||||||
|
self.__index = self;
|
||||||
|
|
||||||
|
Instance.key = key
|
||||||
|
Instance.on = table.assign({ click = {} }, on) -- event -> actions {cmd="",cond=function}
|
||||||
|
Instance.queue = {}
|
||||||
|
Instance.queue_max = { length = 0 }
|
||||||
|
Instance.duration = mp.get_property_number("input-doubleclick-time", 300)
|
||||||
|
Instance.ignored = {}
|
||||||
|
|
||||||
|
for _, event in ipairs(event_pattern) do
|
||||||
|
if Instance.on[event.to] and event.length > 1 then
|
||||||
|
Instance.queue_max = { event = event.to, length = event.length }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Instance
|
||||||
|
end
|
||||||
|
|
||||||
|
function InputEvent:evaluate(event)
|
||||||
|
msg.verbose("Evaluating event: " .. event)
|
||||||
|
local seleted = nil
|
||||||
|
local actions = self.on[event]
|
||||||
|
if not actions or table.isEmpty(actions) then return end
|
||||||
|
for _, action in ipairs(actions) do
|
||||||
|
msg.verbose("Evaluating comand: " .. action.cmd)
|
||||||
|
if type(action.cond) ~= "function" then
|
||||||
|
seleted = action.cmd
|
||||||
|
break
|
||||||
|
else
|
||||||
|
local status, res = pcall(action.cond)
|
||||||
|
if not status then
|
||||||
|
-- errors can be "normal", e.g. in case properties are unavailable
|
||||||
|
msg.verbose("Action condition error on evaluating: " .. res)
|
||||||
|
res = false
|
||||||
|
end
|
||||||
|
res = not not res
|
||||||
|
if res then
|
||||||
|
seleted = action.cmd
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return seleted
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cmd_filter(i,v) return (v.cmd ~= nil and v.cmd ~= "ignore") end
|
||||||
|
|
||||||
|
function InputEvent:emit(event)
|
||||||
|
if self.ignored[event] then
|
||||||
|
if now() - self.ignored[event] < self.duration then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.ignored[event] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "release" and (
|
||||||
|
self.on["release"] == nil or
|
||||||
|
table.isEmpty(self.on["release"]) or
|
||||||
|
table.isEmpty( table.filter(self.on["release"], cmd_filter) )
|
||||||
|
)
|
||||||
|
then
|
||||||
|
event = "release-auto"
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "repeat" and self.on[event] == "ignore" then
|
||||||
|
event = "click"
|
||||||
|
end
|
||||||
|
|
||||||
|
local cmd = self:evaluate(event)
|
||||||
|
if not cmd or cmd == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "press" and (
|
||||||
|
self.on["release"] == nil or
|
||||||
|
table.isEmpty(self.on["release"]) or
|
||||||
|
table.isEmpty( table.filter(self.on["release"], cmd_filter) )
|
||||||
|
)
|
||||||
|
then
|
||||||
|
self.on["release-auto"] = {{cmd = command_invert(cmd), cond = nil}}
|
||||||
|
end
|
||||||
|
|
||||||
|
local expand = mp.command_native({'expand-text', cmd})
|
||||||
|
if #command_split(cmd) == #command_split(expand) then
|
||||||
|
cmd = mp.command_native({'expand-text', cmd})
|
||||||
|
else
|
||||||
|
mp.msg.warn("Unsafe property-expansion detected.")
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.verbose("Apply comand: " .. cmd)
|
||||||
|
command(cmd)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InputEvent:handler(event)
|
||||||
|
if event == "press" then
|
||||||
|
self:handler("down")
|
||||||
|
self:handler("up")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "down" then
|
||||||
|
self:ignore("repeat")
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "repeat" then
|
||||||
|
self:emit(event)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "up" then
|
||||||
|
if #self.queue == 0 then
|
||||||
|
self:emit("release")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if #self.queue + 1 == self.queue_max.length then
|
||||||
|
self.queue = {}
|
||||||
|
self:emit(self.queue_max.event)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "cancel" then
|
||||||
|
if #self.queue == 0 then
|
||||||
|
self:emit("release")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
table.remove(self.queue)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.queue = table.push(self.queue, event)
|
||||||
|
self.exec_debounced()
|
||||||
|
end
|
||||||
|
|
||||||
|
function InputEvent:exec()
|
||||||
|
if #self.queue == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local separator = ","
|
||||||
|
|
||||||
|
local queue_string = table.join(self.queue, separator)
|
||||||
|
for _, v in ipairs(event_pattern) do
|
||||||
|
if self.on[v.to] then
|
||||||
|
queue_string = queue_string:replace(v.from, v.to)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.queue = queue_string:split(separator)
|
||||||
|
for _, event in ipairs(self.queue) do
|
||||||
|
self:emit(event)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.queue = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function InputEvent:ignore(event, timeout)
|
||||||
|
timeout = timeout or 0
|
||||||
|
|
||||||
|
self.ignored[event] = now() + timeout
|
||||||
|
end
|
||||||
|
|
||||||
|
function InputEvent:bind()
|
||||||
|
self.exec_debounced = debounce(function() self:exec() end, self.duration)
|
||||||
|
mp.add_forced_key_binding(self.key, self.key, function(e)
|
||||||
|
local event = e.canceled and "cancel" or e.event
|
||||||
|
self:handler(event)
|
||||||
|
end, { complex = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
function InputEvent:unbind()
|
||||||
|
mp.remove_key_binding(self.key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InputEvent:rebind(diff)
|
||||||
|
if type(diff) == "table" then
|
||||||
|
self = table.assign(self, diff)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:unbind()
|
||||||
|
self:bind()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bind(key, on)
|
||||||
|
key = #key == 1 and key or key:upper()
|
||||||
|
|
||||||
|
if type(on) == "string" then
|
||||||
|
on = utils.parse_json(on)
|
||||||
|
end
|
||||||
|
|
||||||
|
if bind_map[key] then
|
||||||
|
on = table.assign(bind_map[key].on, on)
|
||||||
|
bind_map[key]:unbind()
|
||||||
|
end
|
||||||
|
|
||||||
|
bind_map[key] = InputEvent:new(key, on)
|
||||||
|
bind_map[key]:bind()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unbind(key)
|
||||||
|
bind_map[key]:unbind()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bind_from_conf(conf)
|
||||||
|
local parsed = {}
|
||||||
|
for _, line in pairs(conf:split("\n")) do
|
||||||
|
line = line:trim()
|
||||||
|
if line ~= "" and line:sub(1, 1) ~= "#" then
|
||||||
|
local key, cmd, comment = line:trim():match("^([%S]+)%s+(.-)%s+#%s*(.-)$")
|
||||||
|
if comment then
|
||||||
|
local comments = {}
|
||||||
|
for _, item in ipairs(comment:split("#")) do
|
||||||
|
item = item:trim()
|
||||||
|
local prefix, value = item:match("^(.-)%s*:%s*(.-)$")
|
||||||
|
if not prefix then
|
||||||
|
prefix, value = item:match("^(%p)%s*(.-)$")
|
||||||
|
end
|
||||||
|
if prefix then
|
||||||
|
comments[prefix] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local event, cond = comments[o.prefix], nil
|
||||||
|
local parts = event and event:split("|")
|
||||||
|
if parts and #parts > 1 then
|
||||||
|
event, cond = event:match("(.-)%s*|%s*(.-)$")
|
||||||
|
end
|
||||||
|
|
||||||
|
if event and event ~= "" then
|
||||||
|
if not supported_events[event] then
|
||||||
|
event = "click"
|
||||||
|
end
|
||||||
|
if parsed[key] == nil then
|
||||||
|
parsed[key] = {}
|
||||||
|
end
|
||||||
|
if parsed[key][event] == nil then
|
||||||
|
parsed[key][event] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local index = table.isEmpty(parsed[key][event]) and 1 or #parsed[key][event]+1
|
||||||
|
local cond_name = string.format("%s-%s-%d", key, event, index)
|
||||||
|
table.insert(parsed[key][event], 1,{
|
||||||
|
cmd = cmd,
|
||||||
|
cond = cond ~= nil and compile_cond(cond_name, cond) or nil
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return parsed
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bind_content(content)
|
||||||
|
local parsed = bind_from_conf(content)
|
||||||
|
if parsed and not table.isEmpty(parsed) then
|
||||||
|
for key, on in pairs(parsed) do
|
||||||
|
bind(key, on)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_conf(path)
|
||||||
|
local content = ""
|
||||||
|
local meta, meta_error = utils.file_info(path)
|
||||||
|
if meta and meta.is_file then
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if file then
|
||||||
|
content = file:read("*all")
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return content
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_input_doubleclick_time_update(_, duration)
|
||||||
|
for _, binding in pairs(bind_map) do
|
||||||
|
binding:rebind({ duration = duration })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_focused_update(_, focused)
|
||||||
|
if not focused then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local binding = bind_map["MBTN_LEFT"]
|
||||||
|
if not binding then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
binding:ignore("click", binding.duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
mp.register_script_message("bind", bind)
|
||||||
|
mp.register_script_message("unbind", unbind)
|
||||||
|
mp.observe_property("input-doubleclick-time", "native", on_input_doubleclick_time_update)
|
||||||
|
mp.observe_property("focused", "native", on_focused_update)
|
||||||
|
|
||||||
|
local content = ""
|
||||||
|
local input_conf = mp.get_property_native("input-conf")
|
||||||
|
local input_conf_path = mp.command_native({ "expand-path", input_conf == "" and "~~/input.conf" or input_conf })
|
||||||
|
if o.enable_external_config then
|
||||||
|
local external_config_path = mp.command_native({ "expand-path", o.external_config })
|
||||||
|
content = read_conf(external_config_path)
|
||||||
|
elseif input_conf:match("^memory://") then
|
||||||
|
content = input_conf:replace("^memory://", "")
|
||||||
|
else
|
||||||
|
content = read_conf(input_conf_path)
|
||||||
|
end
|
||||||
|
if content ~= "" then bind_content(content) end
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
local utils = require "mp.utils"
|
||||||
|
local legacy = mp.command_native_async == nil
|
||||||
|
local config = {}
|
||||||
|
local dir_cache = {}
|
||||||
|
|
||||||
|
function run(args)
|
||||||
|
if legacy then
|
||||||
|
return utils.subprocess({args = args})
|
||||||
|
end
|
||||||
|
return mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
|
||||||
|
end
|
||||||
|
|
||||||
|
function parent(path)
|
||||||
|
return string.match(path, "(.*)[/\\]")
|
||||||
|
end
|
||||||
|
|
||||||
|
function cache(path)
|
||||||
|
local p_path = parent(path)
|
||||||
|
if p_path == nil or p_path == "" or dir_cache[p_path] then return end
|
||||||
|
cache(p_path)
|
||||||
|
dir_cache[path] = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function mkdir(path)
|
||||||
|
if dir_cache[path] then return end
|
||||||
|
cache(path)
|
||||||
|
run({"git", "init", path})
|
||||||
|
end
|
||||||
|
|
||||||
|
function match(str, patterns)
|
||||||
|
for pattern in string.gmatch(patterns, "[^|]+") do
|
||||||
|
if string.match(str, pattern) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function apply_defaults(info)
|
||||||
|
if info.git == nil then return false end
|
||||||
|
if info.whitelist == nil then info.whitelist = "" end
|
||||||
|
if info.blacklist == nil then info.blacklist = "" end
|
||||||
|
if info.dest == nil then info.dest = "~~/scripts" end
|
||||||
|
if info.branch == nil then info.branch = "master" end
|
||||||
|
return info
|
||||||
|
end
|
||||||
|
|
||||||
|
function update(info)
|
||||||
|
info = apply_defaults(info)
|
||||||
|
if not info then return false end
|
||||||
|
|
||||||
|
local base = nil
|
||||||
|
|
||||||
|
local e_dest = string.match(mp.command_native({"expand-path", info.dest}), "(.-)[/\\]?$")
|
||||||
|
mkdir(e_dest)
|
||||||
|
|
||||||
|
local files = {}
|
||||||
|
|
||||||
|
run({"git", "-C", e_dest, "remote", "add", "manager", info.git})
|
||||||
|
run({"git", "-C", e_dest, "remote", "set-url", "manager", info.git})
|
||||||
|
run({"git", "-C", e_dest, "fetch", "manager", info.branch})
|
||||||
|
|
||||||
|
for file in string.gmatch(run({"git", "-C", e_dest, "ls-tree", "-r", "--name-only", "remotes/manager/"..info.branch}).stdout, "[^\r\n]+") do
|
||||||
|
local l_file = string.lower(file)
|
||||||
|
if info.whitelist == "" or match(l_file, info.whitelist) then
|
||||||
|
if info.blacklist == "" or not match(l_file, info.blacklist) then
|
||||||
|
table.insert(files, file)
|
||||||
|
if base == nil then base = parent(l_file) or "" end
|
||||||
|
while string.match(base, l_file) == nil do
|
||||||
|
if l_file == "" then break end
|
||||||
|
l_file = parent(l_file) or ""
|
||||||
|
end
|
||||||
|
base = l_file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if base == nil then return false end
|
||||||
|
|
||||||
|
if base ~= "" then base = base.."/" end
|
||||||
|
|
||||||
|
if next(files) == nil then
|
||||||
|
print("no files matching patterns")
|
||||||
|
else
|
||||||
|
for _, file in ipairs(files) do
|
||||||
|
local based = string.sub(file, string.len(base)+1)
|
||||||
|
local p_based = parent(based)
|
||||||
|
if p_based and not info.flatten_folders then mkdir(e_dest.."/"..p_based) end
|
||||||
|
local c = string.match(run({"git", "-C", e_dest, "--no-pager", "show", "remotes/manager/"..info.branch..":"..file}).stdout, "(.-)[\r\n]?$")
|
||||||
|
local f = io.open(e_dest.."/"..(info.flatten_folders and file:match("[^/]+$") or based), "w")
|
||||||
|
f:write(c)
|
||||||
|
f:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function update_all()
|
||||||
|
local f = io.open(mp.command_native({"expand-path", "~~/manager.json"}), "r")
|
||||||
|
if f then
|
||||||
|
local json = f:read("*all")
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
local props = utils.parse_json(json or "")
|
||||||
|
if props then
|
||||||
|
config = props
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, info in ipairs(config) do
|
||||||
|
print("update"..i, update(info))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.add_key_binding(nil, "manager-update-all", update_all)
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
-- Original by Scheliux, Dragoner7 which was ported from Ruin0x11
|
||||||
|
-- Adapted to webp by DonCanjas
|
||||||
|
-- Modify_: https://github.com/dyphire/mpv-scripts
|
||||||
|
|
||||||
|
-- Create animated webps or gifs with mpv
|
||||||
|
-- Requires ffmpeg.
|
||||||
|
-- Adapted from https://github.com/Scheliux/mpv-gif-generator
|
||||||
|
-- Usage: "w" to set start frame, "W" to set end frame, "Ctrl+w" to create.
|
||||||
|
|
||||||
|
-- Note:
|
||||||
|
-- Requires FFmpeg in PATH environment variable or edit ffmpeg_path in the script options,
|
||||||
|
-- Note:
|
||||||
|
-- A small circle at the top-right corner is a sign that creat is happenning now.
|
||||||
|
|
||||||
|
require 'mp.options'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local utils = require "mp.utils"
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
type = "gif", -- gif or webp
|
||||||
|
ffmpeg_path = "ffmpeg",
|
||||||
|
dir = "~~desktop/",
|
||||||
|
rez = 600,
|
||||||
|
fps = 15,
|
||||||
|
lossless = 0,
|
||||||
|
quality = 90,
|
||||||
|
compression_level = 6,
|
||||||
|
loop = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
read_options(options)
|
||||||
|
|
||||||
|
|
||||||
|
local fps
|
||||||
|
local ext
|
||||||
|
local text
|
||||||
|
|
||||||
|
if options.type == "webp" then
|
||||||
|
ext = "webp"
|
||||||
|
text = "webP"
|
||||||
|
else
|
||||||
|
ext = "gif"
|
||||||
|
text = "GIF"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for invalid fps values
|
||||||
|
-- Can you believe Lua doesn't have a proper ternary operator in the year of our lord 2020?
|
||||||
|
if options.fps ~= nil and options.fps >= 1 and options.fps < 30 then
|
||||||
|
fps = options.fps
|
||||||
|
else
|
||||||
|
fps = 15
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set this to the filters to pass into ffmpeg's -vf option.
|
||||||
|
-- filters="fps=24,scale=320:-1:flags=spline"
|
||||||
|
filters=string.format("fps=%s,scale='trunc(ih*dar/2)*2:trunc(ih/2)*2',setsar=1/1,scale=%s:-1:flags=lanczos", fps, options.rez)
|
||||||
|
|
||||||
|
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, windows uses backslashes
|
||||||
|
-- Setup output directory
|
||||||
|
local output_directory = mp.command_native({ "expand-path", options.dir })
|
||||||
|
--create output_directory if it doesn't exist
|
||||||
|
if output_directory ~= '' then
|
||||||
|
local meta, meta_error = utils.file_info(output_directory)
|
||||||
|
if not meta or not meta.is_dir then
|
||||||
|
local windows_args = { 'powershell', '-NoProfile', '-Command', 'mkdir', string.format("\"%s\"", output_directory) }
|
||||||
|
local unix_args = { 'mkdir', '-p', output_directory }
|
||||||
|
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 animated_dir save directory "..output_directory..". Error: "..(res.error or "unknown"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
start_time = -1
|
||||||
|
end_time = -1
|
||||||
|
|
||||||
|
local function is_protocol(path)
|
||||||
|
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function make_animated_with_subtitles()
|
||||||
|
make_animated_internal(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
function make_animated()
|
||||||
|
make_animated_internal(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
function table_length(t)
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(t) do count = count + 1 end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function make_animated_internal(burn_subtitles)
|
||||||
|
local start_time_l = start_time
|
||||||
|
local end_time_l = end_time
|
||||||
|
if start_time_l == -1 or end_time_l == -1 or start_time_l >= end_time_l then
|
||||||
|
mp.osd_message("Invalid start/end time.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local trim_filters = filters
|
||||||
|
local position = start_time_l
|
||||||
|
local duration = end_time_l - start_time_l
|
||||||
|
local filename = mp.get_property("filename/no-ext")
|
||||||
|
|
||||||
|
msg.info("Creating " .. text)
|
||||||
|
mp.osd_message("Creating " .. text)
|
||||||
|
|
||||||
|
-- shell escape
|
||||||
|
function esc_for_sub(s)
|
||||||
|
s = string.gsub(s, "\\", "/")
|
||||||
|
s = string.gsub(s, '"', '\\"')
|
||||||
|
s = string.gsub(s, ":", "\\:")
|
||||||
|
s = string.gsub(s, "'", "\\'")
|
||||||
|
s = string.gsub(s, "%[", "\\%[")
|
||||||
|
s = string.gsub(s, "%]", "\\%]")
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
local pathname = mp.get_property("path", "")
|
||||||
|
local path = mp.get_property_native("path")
|
||||||
|
local cache = mp.get_property_native("cache")
|
||||||
|
local cache_state = mp.get_property_native("demuxer-cache-state")
|
||||||
|
local cache_ranges = cache_state and cache_state["seekable-ranges"] or {}
|
||||||
|
if path and is_protocol(path) or cache == "auto" and #cache_ranges > 0 then
|
||||||
|
local pid = mp.get_property_native('pid')
|
||||||
|
local temp_path = os.getenv("TEMP") or "/tmp/"
|
||||||
|
local temp_video_file = utils.join_path(temp_path, "mpv_dump_" .. pid .. ".mkv")
|
||||||
|
mp.commandv("dump-cache", start_time_l, end_time_l, temp_video_file)
|
||||||
|
position = 0
|
||||||
|
filename = mp.get_property("media-title")
|
||||||
|
pathname = temp_video_file
|
||||||
|
end
|
||||||
|
|
||||||
|
if burn_subtitles then
|
||||||
|
-- Determine currently active sub track
|
||||||
|
|
||||||
|
local i = 0
|
||||||
|
local tracks_count = mp.get_property_number("track-list/count")
|
||||||
|
local subs_array = {}
|
||||||
|
|
||||||
|
-- check for subtitle tracks
|
||||||
|
|
||||||
|
while i < tracks_count do
|
||||||
|
local type = mp.get_property(string.format("track-list/%d/type", i))
|
||||||
|
local selected = mp.get_property(string.format("track-list/%d/selected", i))
|
||||||
|
local external = mp.get_property(string.format("track-list/%d/external", i))
|
||||||
|
|
||||||
|
-- if it's a sub track, save it
|
||||||
|
|
||||||
|
if type == "sub" then
|
||||||
|
local length = table_length(subs_array)
|
||||||
|
if selected == "yes" and external == "yes" then
|
||||||
|
msg.info("Error: external subtitles have been selected")
|
||||||
|
mp.osd_message("Error: external subtitles have been selected", 2)
|
||||||
|
return
|
||||||
|
else
|
||||||
|
subs_array[length] = selected == "yes"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if table_length(subs_array) > 0 then
|
||||||
|
|
||||||
|
local correct_track = 0
|
||||||
|
|
||||||
|
-- iterate through saved subtitle tracks until the correct one is found
|
||||||
|
|
||||||
|
for index, is_selected in pairs(subs_array) do
|
||||||
|
if (is_selected) then
|
||||||
|
correct_track = index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
trim_filters = trim_filters .. string.format(",subtitles='%s':si=%s", esc_for_sub(pathname), correct_track)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- make the animated
|
||||||
|
local file_path = utils.join_path(output_directory, filename)
|
||||||
|
|
||||||
|
-- increment filename
|
||||||
|
for i = 0, 999 do
|
||||||
|
local fn = string.format('%s_%03d.%s', file_path, i, ext)
|
||||||
|
if not file_exists(fn) then
|
||||||
|
animated_name = fn
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not animated_name then
|
||||||
|
mp.osd_message('No available filenames!')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local copyts = ""
|
||||||
|
|
||||||
|
if burn_subtitles then
|
||||||
|
copyts = "-copyts"
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.type == "webp" then
|
||||||
|
arg = string.format("%s -y -hide_banner -loglevel error -ss %s %s -t %s -i '%s' -lavfi %s -lossless %s -q:v %s -compression_level %s -loop %s '%s'", options.ffmpeg_path, position, copyts, duration, pathname, trim_filters, options.lossless, options.quality, options.compression_level, options.loop, animated_name)
|
||||||
|
else
|
||||||
|
arg = string.format("%s -y -hide_banner -loglevel error -ss %s %s -t %s -i '%s' -lavfi %s,'split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse' -loop %s '%s'", options.ffmpeg_path, position, copyts, duration, pathname, trim_filters, options.loop, animated_name)
|
||||||
|
end
|
||||||
|
local windows_args = { 'powershell', '-NoProfile', '-Command', arg }
|
||||||
|
local unix_args = { '/bin/bash', '-c', arg }
|
||||||
|
local args = is_windows and windows_args or unix_args
|
||||||
|
local screenx, screeny, aspect = mp.get_osd_size()
|
||||||
|
mp.set_osd_ass(screenx, screeny, "{\\an9}● ")
|
||||||
|
local res = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
|
||||||
|
mp.set_osd_ass(screenx, screeny, "")
|
||||||
|
if res.status ~= 0 then
|
||||||
|
msg.info("Failed to creat " .. animated_name)
|
||||||
|
mp.osd_message("Error creating " .. text .. ", check console for more info.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
msg.info(animated_name .. " created.")
|
||||||
|
mp.osd_message(text .. " created.")
|
||||||
|
end
|
||||||
|
|
||||||
|
function set_animated_start()
|
||||||
|
start_time = mp.get_property_number("time-pos", -1)
|
||||||
|
mp.osd_message(text .. " Start: " .. start_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
function set_animated_end()
|
||||||
|
end_time = mp.get_property_number("time-pos", -1)
|
||||||
|
mp.osd_message(text .. " End: " .. end_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
function file_exists(name)
|
||||||
|
local f = io.open(name, "r")
|
||||||
|
if f ~= nil then io.close(f) return true else return false end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.add_key_binding("w", "set_animated_start", set_animated_start)
|
||||||
|
mp.add_key_binding("W", "set_animated_end", set_animated_end)
|
||||||
|
mp.add_key_binding("Ctrl+w", "make_animated", make_animated)
|
||||||
|
mp.add_key_binding("Ctrl+W", "make_animated_with_subtitles", make_animated_with_subtitles) --only works with srt for now
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
-- Copyright (c) 2022-2024 dyphire <qimoge@gmail.com>
|
||||||
|
-- License: MIT
|
||||||
|
-- link: https://github.com/dyphire/mpv-scripts
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The script calls up a window in mpv to quickly load the folder/files/iso/clipboard (support url)/other subtitles/other audio tracks/other video tracks.
|
||||||
|
Usage, add bindings to input.conf:
|
||||||
|
key script-message-to open_dialog import_folder
|
||||||
|
key script-message-to open_dialog import_files
|
||||||
|
key script-message-to open_dialog import_files <type> # vid, aid, sid (video/audio/subtitle track)
|
||||||
|
key script-message-to open_dialog import_clipboard
|
||||||
|
key script-message-to open_dialog import_clipboard <type> # vid, aid, sid (video/audio/subtitle track)
|
||||||
|
key script-message-to open_dialog set_clipboard <text> # text can be mpv properties as ${path}
|
||||||
|
|
||||||
|
Also supports open dialog to select folder/files for other scripts.
|
||||||
|
Scripting Example:
|
||||||
|
-- open a folder select dialog
|
||||||
|
mp.commandv('script-message-to', 'open_dialog', 'select_folder', mp.get_script_name())
|
||||||
|
-- receive the selected folder reply
|
||||||
|
mp.register_script_message('select_folder_reply', function(folder_path)
|
||||||
|
if folder_path and folder_path ~= '' then
|
||||||
|
-- do something with folder_path
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- open a xml file select dialog
|
||||||
|
mp.commandv('script-message-to', 'open_dialog', 'select_files', mp.get_script_name(), 'XML File|*.xml')
|
||||||
|
-- receive the selected files reply
|
||||||
|
mp.register_script_message('select_files_reply', function(file_paths)
|
||||||
|
for i, file_path in ipairs(utils.parse_json(file_paths)) do
|
||||||
|
-- do something with file_path
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local options = require 'mp.options'
|
||||||
|
|
||||||
|
o = {
|
||||||
|
video_types = '3g2,3gp,asf,avi,f4v,flv,h264,h265,m2ts,m4v,mkv,mov,mp4,mp4v,mpeg,mpg,ogm,ogv,rm,rmvb,ts,vob,webm,wmv,y4m',
|
||||||
|
audio_types = 'aac,ac3,aiff,ape,au,cue,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv',
|
||||||
|
image_types = 'apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp',
|
||||||
|
subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,sbv,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt',
|
||||||
|
playlist_types = 'm3u,m3u8,pls,cue',
|
||||||
|
iso_types = 'iso',
|
||||||
|
}
|
||||||
|
options.read_options(o)
|
||||||
|
|
||||||
|
local function split(input)
|
||||||
|
local ret = {}
|
||||||
|
for str in string.gmatch(input, "([^,]+)") do
|
||||||
|
ret[#ret + 1] = string.format("*.%s", str)
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
-- pre-defined file types
|
||||||
|
local file_types = {
|
||||||
|
video = table.concat(split(o.video_types), ';'),
|
||||||
|
audio = table.concat(split(o.audio_types), ';'),
|
||||||
|
image = table.concat(split(o.image_types), ';'),
|
||||||
|
iso = table.concat(split(o.iso_types), ';'),
|
||||||
|
subtitle = table.concat(split(o.subtitle_types), ';'),
|
||||||
|
playlist = table.concat(split(o.playlist_types), ';'),
|
||||||
|
}
|
||||||
|
|
||||||
|
local powershell = nil
|
||||||
|
|
||||||
|
local function pwsh_check()
|
||||||
|
local arg = {"cmd", "/c", "pwsh", "--version"}
|
||||||
|
local res = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = arg})
|
||||||
|
if res.status ~= 0 or res.stdout:match("^PowerShell") == nil then
|
||||||
|
powershell = "powershell"
|
||||||
|
else
|
||||||
|
powershell = "pwsh"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- escapes a string so that it can be inserted into powershell as a string literal
|
||||||
|
local function escape_powershell(str)
|
||||||
|
if not str then return '""' end
|
||||||
|
str = str:gsub('[$"`]', '`%1'):gsub('“', '`%1'):gsub('”', '`%1')
|
||||||
|
return '"'..str..'"'
|
||||||
|
end
|
||||||
|
|
||||||
|
local function end_file(event)
|
||||||
|
mp.unregister_event(end_file)
|
||||||
|
if event["reason"] == "eof" or event["reason"] == "stop" or event["reason"] == "error" then
|
||||||
|
local bd_device = mp.get_property_native("bluray-device")
|
||||||
|
local dvd_device = mp.get_property_native("dvd-device")
|
||||||
|
if event["reason"] == "error" and bd_device and bd_device ~= "" then
|
||||||
|
loaded_fail = true
|
||||||
|
else
|
||||||
|
loaded_fail = false
|
||||||
|
end
|
||||||
|
if bd_device then mp.set_property("bluray-device", "") end
|
||||||
|
if dvd_device then mp.set_property("dvd-device", "") end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- open bluray iso or dir
|
||||||
|
local function open_bluray(path)
|
||||||
|
mp.set_property('bluray-device', path)
|
||||||
|
mp.commandv('loadfile', 'bd://')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- open dvd iso or dir
|
||||||
|
local function open_dvd(path)
|
||||||
|
mp.set_property('dvd-device', path)
|
||||||
|
mp.commandv('loadfile', 'dvd://')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- open folder
|
||||||
|
local function open_folder(path, i)
|
||||||
|
local fpath, dir = utils.split_path(path)
|
||||||
|
if utils.file_info(utils.join_path(path, "BDMV")) then
|
||||||
|
open_bluray(path)
|
||||||
|
elseif utils.file_info(utils.join_path(path, "VIDEO_TS")) then
|
||||||
|
open_dvd(path)
|
||||||
|
elseif dir:upper() == "BDMV" then
|
||||||
|
open_bluray(fpath)
|
||||||
|
elseif dir:upper() == "VIDEO_TS" then
|
||||||
|
open_dvd(fpath)
|
||||||
|
else
|
||||||
|
mp.commandv('loadfile', path, i == 1 and 'replace' or 'append')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- open files
|
||||||
|
local function open_files(path, type, i, is_clip)
|
||||||
|
local ext = string.match(path, "%.([^%.]+)$"):lower()
|
||||||
|
if file_types['subtitle']:match(ext) then
|
||||||
|
mp.commandv('sub-add', path, 'cached')
|
||||||
|
elseif type == 'vid' and (not is_clip or (file_types['video']:match(ext) or file_types['image']:match(ext))) then
|
||||||
|
mp.commandv('video-add', path, 'cached')
|
||||||
|
elseif type == 'aid' and (not is_clip or file_types['audio']:match(ext)) then
|
||||||
|
mp.commandv('audio-add', path, 'cached')
|
||||||
|
elseif file_types['iso']:match(ext) then
|
||||||
|
local idle = mp.get_property('idle')
|
||||||
|
if idle ~= 'yes' then mp.set_property('idle', 'yes') end
|
||||||
|
mp.register_event("end-file", end_file)
|
||||||
|
open_bluray(path)
|
||||||
|
mp.add_timeout(1.0, function()
|
||||||
|
if idle ~= 'yes' then mp.set_property('idle', idle) end
|
||||||
|
if loaded_fail then
|
||||||
|
loaded_fail = false
|
||||||
|
open_dvd(path)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
mp.commandv('loadfile', path, i == 1 and 'replace' or 'append')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function select_folder()
|
||||||
|
if not powershell then pwsh_check() end
|
||||||
|
local powershell_script = string.format([[&{
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$fbd = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||||
|
$fbd.RootFolder = "Desktop"
|
||||||
|
$fbd.ShowNewFolderButton = $true
|
||||||
|
$owner = New-Object System.Windows.Forms.NativeWindow
|
||||||
|
$owner.AssignHandle((Get-Process -Id %d).MainWindowHandle)
|
||||||
|
try {
|
||||||
|
if ($fbd.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
|
$u8 = [System.Text.Encoding]::UTF8
|
||||||
|
$out = [Console]::OpenStandardOutput()
|
||||||
|
$selectedFolder = $fbd.SelectedPath
|
||||||
|
$u8selectedFolder = $u8.GetBytes("$selectedFolder`n")
|
||||||
|
$out.Write($u8selectedFolder, 0, $u8selectedFolder.Length)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$owner.ReleaseHandle()
|
||||||
|
$fbd.Dispose()
|
||||||
|
}
|
||||||
|
}]], mp.get_property_number('pid'))
|
||||||
|
local res = mp.command_native({
|
||||||
|
name = 'subprocess',
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
args = { powershell, '-NoProfile', '-Command', powershell_script },
|
||||||
|
})
|
||||||
|
if res.status ~= 0 then
|
||||||
|
mp.osd_message("Failed to open folder dialog.")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local folder_path = res.stdout:match("(.-)[\r\n]?$") -- Trim any trailing newline
|
||||||
|
return folder_path
|
||||||
|
end
|
||||||
|
|
||||||
|
local function select_files(filter)
|
||||||
|
if not powershell then pwsh_check() end
|
||||||
|
local powershell_script = string.format([[&{
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$ofd = New-Object System.Windows.Forms.OpenFileDialog
|
||||||
|
$ofd.Multiselect = $true
|
||||||
|
$ofd.Filter = %s
|
||||||
|
$owner = New-Object System.Windows.Forms.NativeWindow
|
||||||
|
$owner.AssignHandle((Get-Process -Id %d).MainWindowHandle)
|
||||||
|
try {
|
||||||
|
if ($ofd.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
|
$u8 = [System.Text.Encoding]::UTF8
|
||||||
|
$out = [Console]::OpenStandardOutput()
|
||||||
|
ForEach ($filename in $ofd.FileNames) {
|
||||||
|
$u8filename = $u8.GetBytes("$filename`n")
|
||||||
|
$out.Write($u8filename, 0, $u8filename.Length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$owner.ReleaseHandle()
|
||||||
|
$ofd.Dispose()
|
||||||
|
}
|
||||||
|
}]], escape_powershell(filter), mp.get_property_number('pid'))
|
||||||
|
local res = mp.command_native({
|
||||||
|
name = 'subprocess',
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
args = { powershell, '-NoProfile', '-Command', powershell_script },
|
||||||
|
})
|
||||||
|
local file_paths = {}
|
||||||
|
if res.status ~= 0 then
|
||||||
|
mp.osd_message("Failed to open files dialog.")
|
||||||
|
return file_paths
|
||||||
|
end
|
||||||
|
for file_path in string.gmatch(res.stdout, '[^\r\n]+') do
|
||||||
|
table.insert(file_paths, file_path)
|
||||||
|
end
|
||||||
|
return file_paths
|
||||||
|
end
|
||||||
|
|
||||||
|
-- import folder
|
||||||
|
local function import_folder()
|
||||||
|
local folder_path = select_folder()
|
||||||
|
if folder_path and folder_path ~= '' then open_folder(folder_path, 1) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- import files
|
||||||
|
local function import_files(type)
|
||||||
|
local filter = ''
|
||||||
|
if type == 'vid' then
|
||||||
|
filter = string.format("Video Files|%s|Image Files|%s", file_types['video'], file_types['image'])
|
||||||
|
elseif type == 'aid' then
|
||||||
|
filter = string.format("Audio Files|%s", file_types['audio'])
|
||||||
|
elseif type == 'sid' then
|
||||||
|
filter = string.format("Subtitle Files|%s", file_types['subtitle'])
|
||||||
|
else
|
||||||
|
filter = string.format("All Files (*.*)|*.*|Video Files|%s|Audio Files|%s|Image Files|%s|ISO Files|%s|Subtitle Files|%s|Playlist Files|%s",
|
||||||
|
file_types['video'], file_types['audio'], file_types['image'], file_types['iso'], file_types['subtitle'], file_types['playlist'])
|
||||||
|
end
|
||||||
|
for i, file_path in ipairs(select_files(filter)) do
|
||||||
|
open_files(file_path, type, i, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns a string of UTF-8 text from the clipboard
|
||||||
|
local function get_clipboard()
|
||||||
|
if mp.get_property('clipboard-backends') ~= nil or mp.get_property_bool('clipboard-enable') then
|
||||||
|
return mp.get_property('clipboard/text', '')
|
||||||
|
end
|
||||||
|
local res = mp.command_native({
|
||||||
|
name = 'subprocess',
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
args = { 'powershell', '-NoProfile', '-Command', [[& {
|
||||||
|
Trap {
|
||||||
|
Write-Error -ErrorRecord $_
|
||||||
|
Exit 1
|
||||||
|
}
|
||||||
|
$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
|
||||||
|
if (-not $clip) {
|
||||||
|
$clip = Get-Clipboard -Raw -Format FileDropList
|
||||||
|
}
|
||||||
|
$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
|
||||||
|
[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
|
||||||
|
}]] }
|
||||||
|
})
|
||||||
|
if not res.error then
|
||||||
|
return res.stdout
|
||||||
|
end
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
-- open files from clipboard
|
||||||
|
local function open_clipboard(path, type, i)
|
||||||
|
local path = path:gsub("^[\'\"]", ""):gsub("[\'\"]$", ""):gsub('^%s+', ''):gsub('%s+$', '')
|
||||||
|
if path:find('^%a[%w.+-]-://') then
|
||||||
|
mp.commandv('loadfile', path, i == 1 and 'replace' or 'append')
|
||||||
|
else
|
||||||
|
local meta = utils.file_info(path)
|
||||||
|
if not meta then
|
||||||
|
mp.osd_message('Clipboard path is invalid')
|
||||||
|
msg.warn('Clipboard path is invalid')
|
||||||
|
elseif meta.is_dir then
|
||||||
|
open_folder(path, i)
|
||||||
|
elseif meta.is_file then
|
||||||
|
open_files(path, type, i, true)
|
||||||
|
else
|
||||||
|
mp.osd_message('Clipboard path is invalid')
|
||||||
|
msg.warn('Clipboard path is invalid')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- import clipboard
|
||||||
|
local function import_clipboard(type)
|
||||||
|
local clip = get_clipboard()
|
||||||
|
if clip ~= '' then
|
||||||
|
local i = 0
|
||||||
|
for path in string.gmatch(clip, '[^\r\n]+') do
|
||||||
|
i = i + 1
|
||||||
|
open_clipboard(path, type, i)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
mp.osd_message('Clipboard is empty')
|
||||||
|
msg.warn('Clipboard is empty')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- sets the contents of the clipboard to the given string
|
||||||
|
local function set_clipboard(text)
|
||||||
|
msg.verbose('setting clipboard text:', text)
|
||||||
|
if mp.get_property('clipboard-backends') ~= nil or mp.get_property_bool('clipboard-enable') then
|
||||||
|
mp.commandv('set', 'clipboard/text', text)
|
||||||
|
else
|
||||||
|
mp.commandv('run', 'powershell', '-NoProfile', '-command', 'set-clipboard', escape_powershell(text))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message('import_folder', import_folder)
|
||||||
|
mp.register_script_message('import_files', import_files)
|
||||||
|
mp.register_script_message('import_clipboard', import_clipboard)
|
||||||
|
mp.register_script_message('set_clipboard', set_clipboard)
|
||||||
|
mp.register_script_message('select_folder', function(script_name)
|
||||||
|
local folder_path = select_folder()
|
||||||
|
mp.commandv('script-message-to', script_name, 'select_folder_reply', folder_path)
|
||||||
|
end)
|
||||||
|
mp.register_script_message('select_files', function(script_name, filter)
|
||||||
|
local file_paths = select_files(filter)
|
||||||
|
mp.commandv('script-message-to', script_name, 'select_files_reply', utils.format_json(file_paths))
|
||||||
|
end)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
-- Script home: https://github.com/d87/mpv-persist-properties
|
||||||
|
local utils = require "mp.utils"
|
||||||
|
local msg = require "mp.msg"
|
||||||
|
|
||||||
|
local opts = {
|
||||||
|
properties = "volume,sub-scale",
|
||||||
|
properties_path = 'persistent_config.json'
|
||||||
|
}
|
||||||
|
(require 'mp.options').read_options(opts, "persist_properties")
|
||||||
|
|
||||||
|
local CONFIG_ROOT = mp.find_config_file(".")
|
||||||
|
if not utils.file_info(CONFIG_ROOT) then
|
||||||
|
-- On Windows if using portable_config dir, APPDATA mpv folder isn't auto-created
|
||||||
|
-- In more recent mpv versions there's a mp.get_script_directory function, but i'm not using it for compatiblity
|
||||||
|
local mpv_conf_path = mp.find_config_file("scripts") -- finds where the scripts folder is located
|
||||||
|
local mpv_conf_dir = utils.split_path(mpv_conf_path)
|
||||||
|
CONFIG_ROOT = mpv_conf_dir
|
||||||
|
end
|
||||||
|
local PCONFIG = utils.join_path(CONFIG_ROOT, opts.properties_path);
|
||||||
|
|
||||||
|
local function split(input)
|
||||||
|
local ret = {}
|
||||||
|
for str in string.gmatch(input, "([^,]+)") do
|
||||||
|
table.insert(ret, str)
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
local persisted_properties = split(opts.properties)
|
||||||
|
|
||||||
|
local print = function(...)
|
||||||
|
-- return msg.log("info", ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- print("Config Root is "..CONFIG_ROOT)
|
||||||
|
|
||||||
|
local isInitialized = false
|
||||||
|
|
||||||
|
local properties
|
||||||
|
|
||||||
|
local function load_config(file)
|
||||||
|
local f = io.open(file, "r")
|
||||||
|
if f then
|
||||||
|
local jsonString = f:read()
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
if jsonString == nil then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local props = utils.parse_json(jsonString)
|
||||||
|
if props then
|
||||||
|
return props
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function save_config(file, properties)
|
||||||
|
local serialized_props = utils.format_json(properties)
|
||||||
|
|
||||||
|
local f = io.open(file, 'w+')
|
||||||
|
if f then
|
||||||
|
f:write(serialized_props)
|
||||||
|
f:close()
|
||||||
|
else
|
||||||
|
msg.log("error", string.format("Couldn't open file: %s", file))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local save_timer = nil
|
||||||
|
local got_unsaved_changed = false
|
||||||
|
|
||||||
|
local function onInitialLoad()
|
||||||
|
properties = load_config(PCONFIG)
|
||||||
|
|
||||||
|
for i, property in ipairs(persisted_properties) do
|
||||||
|
local name = property
|
||||||
|
local value = properties[name]
|
||||||
|
if value ~= nil then
|
||||||
|
mp.set_property_native(name, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, property in ipairs(persisted_properties) do
|
||||||
|
local property_type = nil
|
||||||
|
mp.observe_property(property, property_type, function(name)
|
||||||
|
if isInitialized then
|
||||||
|
local value = mp.get_property_native(name)
|
||||||
|
-- print(string.format("%s changed to %s at %s", name, value, os.time()))
|
||||||
|
|
||||||
|
properties[name] = value
|
||||||
|
|
||||||
|
if save_timer then
|
||||||
|
save_timer:kill()
|
||||||
|
save_timer:resume()
|
||||||
|
got_unsaved_changed = true
|
||||||
|
else
|
||||||
|
save_timer = mp.add_timeout(5, function()
|
||||||
|
save_config(PCONFIG, properties)
|
||||||
|
got_unsaved_changed = false
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
isInitialized = true
|
||||||
|
end
|
||||||
|
|
||||||
|
onInitialLoad()
|
||||||
|
mp.register_event("shutdown", function()
|
||||||
|
if got_unsaved_changed then
|
||||||
|
save_config(PCONFIG, properties)
|
||||||
|
end
|
||||||
|
end)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
|||||||
|
local msg = require "mp.msg"
|
||||||
|
local utils = require "mp.utils"
|
||||||
|
local options = require "mp.options"
|
||||||
|
|
||||||
|
local cut_pos = nil
|
||||||
|
local copy_audio = true
|
||||||
|
local ext_map = {
|
||||||
|
["mpegts"] = "ts",
|
||||||
|
}
|
||||||
|
local o = {
|
||||||
|
ffmpeg_path = "ffmpeg",
|
||||||
|
target_dir = "~~/cutfragments",
|
||||||
|
overwrite = false, -- whether to overwrite exist files
|
||||||
|
vcodec = "copy",
|
||||||
|
acodec = "copy",
|
||||||
|
debug = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
options.read_options(o)
|
||||||
|
|
||||||
|
Command = {}
|
||||||
|
|
||||||
|
local function is_protocol(path)
|
||||||
|
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Command:new(name)
|
||||||
|
local o = {}
|
||||||
|
setmetatable(o, self)
|
||||||
|
self.__index = self
|
||||||
|
o.name = ""
|
||||||
|
o.args = { "" }
|
||||||
|
if name then
|
||||||
|
o.name = name
|
||||||
|
o.args[1] = name
|
||||||
|
end
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
function Command:arg(...)
|
||||||
|
for _, v in ipairs({...}) do
|
||||||
|
self.args[#self.args + 1] = v
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
function Command:as_str()
|
||||||
|
return table.concat(self.args, " ")
|
||||||
|
end
|
||||||
|
function Command:run()
|
||||||
|
local res, err = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = self.args,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
})
|
||||||
|
return res, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local function file_format()
|
||||||
|
local fmt = mp.get_property("file-format")
|
||||||
|
if not fmt:find(',') then
|
||||||
|
return fmt
|
||||||
|
end
|
||||||
|
local path = mp.get_property('path')
|
||||||
|
if is_protocol(path) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local filename = mp.get_property('filename')
|
||||||
|
return filename:match('%.([^.]+)$')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_ext()
|
||||||
|
local fmt = file_format()
|
||||||
|
if fmt and ext_map[fmt] ~= nil then
|
||||||
|
return ext_map[fmt]
|
||||||
|
else
|
||||||
|
return fmt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function timestamp(duration)
|
||||||
|
local hours = math.floor(duration / 3600)
|
||||||
|
local minutes = math.floor(duration % 3600 / 60)
|
||||||
|
local seconds = duration % 60
|
||||||
|
return string.format("%02d:%02d:%06.3f", hours, minutes, seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function osd(str)
|
||||||
|
return mp.osd_message(str, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function info(s)
|
||||||
|
msg.info(s)
|
||||||
|
osd(s)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_outname(path, shift, endpos)
|
||||||
|
local name = mp.get_property("filename/no-ext")
|
||||||
|
if is_protocol(path) then
|
||||||
|
name = mp.get_property("media-title")
|
||||||
|
end
|
||||||
|
local ext = get_ext() or "mkv"
|
||||||
|
name = string.format("%s_%s-%s.%s", name, timestamp(shift), timestamp(endpos), ext)
|
||||||
|
return name:gsub(":", "-")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cut(shift, endpos)
|
||||||
|
local duration = endpos - shift
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
local inpath = mp.get_property("stream-open-filename")
|
||||||
|
local outpath = utils.join_path(
|
||||||
|
o.target_dir,
|
||||||
|
get_outname(path, shift, endpos)
|
||||||
|
)
|
||||||
|
|
||||||
|
local cache = mp.get_property_native("cache")
|
||||||
|
local cache_state = mp.get_property_native("demuxer-cache-state")
|
||||||
|
local cache_ranges = cache_state and cache_state["seekable-ranges"] or {}
|
||||||
|
if path and is_protocol(path) or cache == "auto" and #cache_ranges > 0 then
|
||||||
|
local pid = mp.get_property_native('pid')
|
||||||
|
local temp_path = os.getenv("TEMP") or "/tmp/"
|
||||||
|
local temp_video_file = utils.join_path(temp_path, "mpv_dump_" .. pid .. ".mkv")
|
||||||
|
mp.commandv("dump-cache", shift, endpos, temp_video_file)
|
||||||
|
shift = 0
|
||||||
|
inpath = temp_video_file
|
||||||
|
end
|
||||||
|
|
||||||
|
local cmds = Command:new(o.ffmpeg_path)
|
||||||
|
:arg("-v", "warning")
|
||||||
|
:arg(o.overwrite and "-y" or "-n")
|
||||||
|
:arg("-stats")
|
||||||
|
cmds:arg("-ss", tostring(shift))
|
||||||
|
cmds:arg("-accurate_seek")
|
||||||
|
cmds:arg("-i", inpath)
|
||||||
|
cmds:arg("-t", tostring(duration))
|
||||||
|
cmds:arg("-c:v", o.vcodec)
|
||||||
|
cmds:arg("-c:a", o.acodec)
|
||||||
|
cmds:arg("-c:s", "copy")
|
||||||
|
cmds:arg("-map", string.format("v:%s?", math.max(mp.get_property_number("current-tracks/video/id", 0) - 1, 0)))
|
||||||
|
cmds:arg("-map", string.format("a:%s?", math.max(mp.get_property_number("current-tracks/audio/id", 0) - 1, 0)))
|
||||||
|
cmds:arg("-map", string.format("s:%s?", math.max(mp.get_property_number("current-tracks/sub/id", 0) - 1, 0)))
|
||||||
|
cmds:arg(not copy_audio and "-an" or nil)
|
||||||
|
cmds:arg("-avoid_negative_ts", "make_zero")
|
||||||
|
cmds:arg("-async", "1")
|
||||||
|
cmds:arg(outpath)
|
||||||
|
msg.info("Run commands: " .. cmds:as_str())
|
||||||
|
local screenx, screeny, aspect = mp.get_osd_size()
|
||||||
|
mp.set_osd_ass(screenx, screeny, "{\\an9}● ")
|
||||||
|
local res, err = cmds:run()
|
||||||
|
mp.set_osd_ass(screenx, screeny, "")
|
||||||
|
if err then
|
||||||
|
msg.error(utils.to_string(err))
|
||||||
|
mp.osd_message("Failed. Refer console for details.")
|
||||||
|
elseif res.status ~= 0 then
|
||||||
|
if res.stderr ~= "" or res.stdout ~= "" then
|
||||||
|
msg.info("stderr: " .. (res.stderr:gsub("^%s*(.-)%s*$", "%1"))) -- trim stderr
|
||||||
|
msg.info("stdout: " .. (res.stdout:gsub("^%s*(.-)%s*$", "%1"))) -- trim stdout
|
||||||
|
mp.osd_message("Failed. Refer console for details.")
|
||||||
|
end
|
||||||
|
elseif res.status == 0 then
|
||||||
|
if o.debug and (res.stderr ~= "" or res.stdout ~= "") then
|
||||||
|
msg.info("stderr: " .. (res.stderr:gsub("^%s*(.-)%s*$", "%1"))) -- trim stderr
|
||||||
|
msg.info("stdout: " .. (res.stdout:gsub("^%s*(.-)%s*$", "%1"))) -- trim stdout
|
||||||
|
end
|
||||||
|
msg.info("Trim file successfully created: " .. outpath)
|
||||||
|
mp.add_timeout(1, function()
|
||||||
|
mp.osd_message("Trim file successfully created!")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toggle_mark()
|
||||||
|
local pos, err = mp.get_property_number("time-pos")
|
||||||
|
if not pos then
|
||||||
|
osd("Failed to get timestamp")
|
||||||
|
msg.error("Failed to get timestamp: " .. err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if cut_pos then
|
||||||
|
local shift, endpos = cut_pos, pos
|
||||||
|
if shift > endpos then
|
||||||
|
shift, endpos = endpos, shift
|
||||||
|
elseif shift == endpos then
|
||||||
|
osd("Cut fragment is empty")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
cut_pos = nil
|
||||||
|
info(string.format("Cut fragment: %s-%s", timestamp(shift), timestamp(endpos)))
|
||||||
|
cut(shift, endpos)
|
||||||
|
else
|
||||||
|
cut_pos = pos
|
||||||
|
info(string.format("Marked %s as start position", timestamp(pos)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toggle_audio()
|
||||||
|
copy_audio = not copy_audio
|
||||||
|
info("Audio capturing is " .. (copy_audio and "enabled" or "disabled"))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clear_toggle_mark()
|
||||||
|
cut_pos = nil
|
||||||
|
info("Cleared cut fragment")
|
||||||
|
end
|
||||||
|
|
||||||
|
o.target_dir = o.target_dir:gsub('"', "")
|
||||||
|
local file, _ = utils.file_info(mp.command_native({ "expand-path", o.target_dir }))
|
||||||
|
if not file then
|
||||||
|
--create target_dir if it doesn't exist
|
||||||
|
local savepath = mp.command_native({ "expand-path", o.target_dir })
|
||||||
|
local is_windows = package.config:sub(1, 1) == "\\"
|
||||||
|
local windows_args = { 'powershell', '-NoProfile', '-Command', 'mkdir', string.format("\"%s\"", savepath) }
|
||||||
|
local unix_args = { 'mkdir', '-p', savepath }
|
||||||
|
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 target_dir save directory "..savepath..". Error: "..(res.error or "unknown"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
elseif not file.is_dir then
|
||||||
|
osd("target_dir is a file")
|
||||||
|
msg.warn(string.format("target_dir `%s` is a file", o.target_dir))
|
||||||
|
end
|
||||||
|
o.target_dir = mp.command_native({ "expand-path", o.target_dir })
|
||||||
|
|
||||||
|
mp.add_key_binding("c", "slicing_mark", toggle_mark)
|
||||||
|
mp.add_key_binding("a", "slicing_audio", toggle_audio)
|
||||||
|
mp.add_key_binding("C", "clear_slicing_mark", clear_toggle_mark)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
-- sponsorblock_minimal.lua v 0.5.1
|
||||||
|
--
|
||||||
|
-- This script skip/mute sponsored segments of YouTube and bilibili videos
|
||||||
|
-- using data from https://github.com/ajayyy/SponsorBlock
|
||||||
|
-- and https://github.com/hanydd/BilibiliSponsorBlock
|
||||||
|
|
||||||
|
local opt = require 'mp.options'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
youtube_sponsor_server = "https://sponsor.ajay.app/api/skipSegments",
|
||||||
|
bilibili_sponsor_server = "https://bsbsb.top/api/skipSegments",
|
||||||
|
-- Categories to fetch
|
||||||
|
-- Perform skip/mute/mark chapter based on the 'actionType' returned
|
||||||
|
categories = '"sponsor"',
|
||||||
|
}
|
||||||
|
|
||||||
|
opt.read_options(options)
|
||||||
|
|
||||||
|
local ranges = nil
|
||||||
|
local video_id = nil
|
||||||
|
local sponsor_server = nil
|
||||||
|
local cache = {}
|
||||||
|
local mute = false
|
||||||
|
local ON = false
|
||||||
|
|
||||||
|
local function getranges(url)
|
||||||
|
local res = mp.command_native{
|
||||||
|
name = "subprocess",
|
||||||
|
capture_stdout = true,
|
||||||
|
playback_only = false,
|
||||||
|
args = {
|
||||||
|
"curl", "-L", "-s", "-g",
|
||||||
|
"-H", "origin: mpv-script/sponsorblock_minimal",
|
||||||
|
"-H", "x-ext-version: 0.5.1",
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.status ~= 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return utils.parse_json(res.stdout)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_chapter(ranges)
|
||||||
|
local chapters_time = {}
|
||||||
|
local chapters_title = {}
|
||||||
|
local chapter_index = 0
|
||||||
|
local all_chapters = mp.get_property_native("chapter-list")
|
||||||
|
for _, v in pairs(ranges) do
|
||||||
|
table.insert(chapters_time, v.segment[1])
|
||||||
|
table.insert(chapters_title, v.category)
|
||||||
|
table.insert(chapters_time, v.segment[2])
|
||||||
|
table.insert(chapters_title, "normal")
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #chapters_time do
|
||||||
|
chapter_index = chapter_index + 1
|
||||||
|
all_chapters[chapter_index] = {
|
||||||
|
title = chapters_title[i] or ("Chapter " .. string.format("%02.f", chapter_index)),
|
||||||
|
time = chapters_time[i]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(all_chapters, function(a, b) return a['time'] < b['time'] end)
|
||||||
|
mp.set_property_native("chapter-list", all_chapters)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function skip_ads(_, pos)
|
||||||
|
if pos ~= nil and ranges ~= nil then
|
||||||
|
for _, v in pairs(ranges) do
|
||||||
|
if v.actionType == "skip" and v.segment[1] <= pos and v.segment[2] > pos then
|
||||||
|
--this message may sometimes be wrong
|
||||||
|
--it only seems to be a visual thing though
|
||||||
|
local time = math.floor(v.segment[2] - pos)
|
||||||
|
mp.osd_message(string.format("[sponsorblock] skipping forward %ds", time))
|
||||||
|
--need to do the +0.01 otherwise mpv will start spamming skip sometimes
|
||||||
|
mp.set_property("time-pos", v.segment[2] + 0.01)
|
||||||
|
elseif v.actionType == "mute" then
|
||||||
|
if v.segment[1] <= pos and v.segment[2] >= pos then
|
||||||
|
cache[v.segment[2]] = nil
|
||||||
|
mp.set_property_bool("mute", true)
|
||||||
|
elseif pos > v.segment[2] and not cache[v.segment[2]] and mute ~= false then
|
||||||
|
cache[v.segment[2]] = true
|
||||||
|
mp.set_property_bool("mute", false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function file_loaded()
|
||||||
|
cache = {}
|
||||||
|
local video_path = mp.get_property("path", "")
|
||||||
|
local video_referer = mp.get_property("http-header-fields", ""):match("[Rr]eferer:%s*([^,\r\n]+)") or ""
|
||||||
|
local purl = mp.get_property("metadata/by-key/PURL", "")
|
||||||
|
local bilibili = video_path:match("bilibili.com/video") or video_referer:match("bilibili.com/video") or false
|
||||||
|
mute = mp.get_property_bool("mute")
|
||||||
|
|
||||||
|
local urls = {
|
||||||
|
"ytdl://youtu%.be/([%w-_]+).*",
|
||||||
|
"ytdl://w?w?w?%.?youtube%.com/v/([%w-_]+).*",
|
||||||
|
"ytdl://w?w?w?%.?bilibili%.com/video/([%w-_]+).*",
|
||||||
|
"https?://youtu%.be/([%w-_]+).*",
|
||||||
|
"https?://w?w?w?%.?youtube%.com/v/([%w-_]+).*",
|
||||||
|
"https?://w?w?w?%.?bilibili%.com/video/([%w-_]+).*",
|
||||||
|
"/watch.*[?&]v=([%w-_]+).*",
|
||||||
|
"/embed/([%w-_]+).*",
|
||||||
|
"^ytdl://([%w-_]+)$",
|
||||||
|
"-([%w-_]+)%."
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, url in ipairs(urls) do
|
||||||
|
video_id = video_id or video_path:match(url) or video_referer:match(url) or purl:match(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not video_id or string.len(video_id) < 11 then return end
|
||||||
|
|
||||||
|
if bilibili then
|
||||||
|
sponsor_server = options.bilibili_sponsor_server
|
||||||
|
video_id = string.sub(video_id, 1, 12)
|
||||||
|
else
|
||||||
|
sponsor_server = options.youtube_sponsor_server
|
||||||
|
video_id = string.sub(video_id, 1, 11)
|
||||||
|
end
|
||||||
|
|
||||||
|
local url = ("%s?videoID=%s&categories=[%s]"):format(sponsor_server, video_id, options.categories)
|
||||||
|
|
||||||
|
ranges = getranges(url)
|
||||||
|
if ranges ~= nil then
|
||||||
|
make_chapter(ranges)
|
||||||
|
ON = true
|
||||||
|
mp.observe_property("time-pos", "native", skip_ads)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function end_file()
|
||||||
|
if not ON then return end
|
||||||
|
mp.unobserve_property(skip_ads)
|
||||||
|
video_id = nil
|
||||||
|
cache = nil
|
||||||
|
ranges = nil
|
||||||
|
ON = false
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event("file-loaded", file_loaded)
|
||||||
|
mp.register_event("end-file", end_file)
|
||||||
@@ -0,0 +1,704 @@
|
|||||||
|
--[[
|
||||||
|
* sub-assrt.lua
|
||||||
|
*
|
||||||
|
* AUTHORS: dyphire
|
||||||
|
* License: MIT
|
||||||
|
* link: https://github.com/dyphire/mpv-sub-assrt
|
||||||
|
]]
|
||||||
|
|
||||||
|
local utils = require "mp.utils"
|
||||||
|
local msg = require "mp.msg"
|
||||||
|
local options = require("mp.options")
|
||||||
|
local input_loaded, input = pcall(require, "mp.input")
|
||||||
|
local uosc_available = false
|
||||||
|
|
||||||
|
local o = {
|
||||||
|
-- API token, 可以在 https://assrt.net 上注册账号后在个人界面获取
|
||||||
|
api_token = "tNjXZUnOJWcHznHDyalNMYqqP6IdDdpQ",
|
||||||
|
-- 是否使用 https
|
||||||
|
use_https = true,
|
||||||
|
-- 代理设置
|
||||||
|
proxy = "",
|
||||||
|
}
|
||||||
|
|
||||||
|
options.read_options(o, _, function() end)
|
||||||
|
|
||||||
|
local ASSRT_SEARCH_API = (o.use_https and "https" or "http") .. "://api.assrt.net/v1/sub/search"
|
||||||
|
local ASSRT_DETAIL_API = (o.use_https and "https" or "http") .. "://api.assrt.net/v1/sub/detail"
|
||||||
|
|
||||||
|
local TEMP_DIR = os.getenv("TEMP") or "/tmp"
|
||||||
|
local cache = {}
|
||||||
|
|
||||||
|
local function is_protocol(path)
|
||||||
|
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hex_to_char(x)
|
||||||
|
return string.char(tonumber(x, 16))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function url_encode(str)
|
||||||
|
if str then
|
||||||
|
str = str:gsub("([^%w%-%.%_%~])", function(c)
|
||||||
|
return string.format("%%%02X", string.byte(c))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
str = str:gsub('%?.+', '')
|
||||||
|
:gsub('%+', ' ')
|
||||||
|
return str
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_compressed_file(filename)
|
||||||
|
local ext_map = {
|
||||||
|
zip = true,
|
||||||
|
rar = true,
|
||||||
|
["7z"] = true,
|
||||||
|
gz = true,
|
||||||
|
tar = true,
|
||||||
|
bz2 = true,
|
||||||
|
xz = true,
|
||||||
|
tgz = true,
|
||||||
|
tbz2 = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local ext = filename:match("%.([%w]+)$"):lower()
|
||||||
|
if ext then
|
||||||
|
return ext_map[ext] or false
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function http_request(url)
|
||||||
|
local cmd = {
|
||||||
|
"curl",
|
||||||
|
"-s",
|
||||||
|
"-L",
|
||||||
|
"--max-redirs", "5",
|
||||||
|
"--connect-timeout", "10",
|
||||||
|
"--max-time", "30",
|
||||||
|
"--user-agent", "mpv",
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.proxy ~= "" then
|
||||||
|
table.insert(cmd, '-x')
|
||||||
|
table.insert(cmd, o.proxy)
|
||||||
|
end
|
||||||
|
|
||||||
|
local res = mp.command_native({ name = "subprocess", capture_stdout = true, capture_stderr = true, args = cmd })
|
||||||
|
if res.status == 0 then
|
||||||
|
return utils.parse_json(res.stdout)
|
||||||
|
else
|
||||||
|
msg.error("HTTP request failed: " .. res.stderr)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function file_exists(path)
|
||||||
|
if path then
|
||||||
|
local meta = utils.file_info(path)
|
||||||
|
return meta and meta.is_file
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function alphanumsort(a, b)
|
||||||
|
-- alphanum sorting for humans in Lua
|
||||||
|
-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
|
||||||
|
local function padnum(d)
|
||||||
|
local dec, n = string.match(d, "(%.?)0*(.+)")
|
||||||
|
return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n)
|
||||||
|
end
|
||||||
|
return tostring(a):lower():gsub("%.?%d+", padnum) .. ("%3d"):format(#b)
|
||||||
|
< tostring(b):lower():gsub("%.?%d+", padnum) .. ("%3d"):format(#a)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalize(path)
|
||||||
|
if normalize_path ~= nil then
|
||||||
|
if normalize_path then
|
||||||
|
path = mp.command_native({"normalize-path", path})
|
||||||
|
else
|
||||||
|
local directory = mp.get_property("working-directory", "")
|
||||||
|
path = utils.join_path(directory, path:gsub('^%.[\\/]',''))
|
||||||
|
if platform == "windows" then path = path:gsub("\\", "/") end
|
||||||
|
end
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
normalize_path = false
|
||||||
|
|
||||||
|
local commands = mp.get_property_native("command-list", {})
|
||||||
|
for _, command in ipairs(commands) do
|
||||||
|
if command.name == "normalize-path" then
|
||||||
|
normalize_path = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return normalize(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_sub(sub_file)
|
||||||
|
local tracks = mp.get_property_native("track-list")
|
||||||
|
local _, sub_title = utils.split_path(sub_file)
|
||||||
|
for _, track in ipairs(tracks) do
|
||||||
|
if track["type"] == "sub" and track["title"] == sub_title then
|
||||||
|
return true, track["id"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function append_sub(sub_file)
|
||||||
|
local sub, id = check_sub(sub_file)
|
||||||
|
if not sub then
|
||||||
|
mp.commandv('sub-add', sub_file)
|
||||||
|
else
|
||||||
|
mp.commandv('sub-reload', id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clean_name(name)
|
||||||
|
return name:gsub("^%[.-%]", " ")
|
||||||
|
:gsub("^%(.-%)", " ")
|
||||||
|
:gsub("[_%.%[%]]", " ")
|
||||||
|
:gsub("^%s*(.-)%s*$", "%1")
|
||||||
|
:gsub("[!@#%.%?%+%-%%&*_=,/~`]+$", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Formatters for media titles
|
||||||
|
local formatters = {
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)[_%.%s]%d%d[_%.%s]%d%d%s*[_%.%s]?(.-)%s*[_%.%s]%d+[pPkKxXbBfF]",
|
||||||
|
format = function(name, year, subtitle)
|
||||||
|
local title = clean_name(name)
|
||||||
|
if subtitle then
|
||||||
|
title = title .. ": " .. subtitle:gsub("%.", " "):gsub("^%s*(.-)%s*$", "%1")
|
||||||
|
end
|
||||||
|
return title .. " (" .. year .. ")"
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+%.?%d*)",
|
||||||
|
format = function(name, year, season, episode)
|
||||||
|
return clean_name(name) .. " (" .. year .. ") S" .. season .. "E" .. episode
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[eEpP]+(%d+%.?%d*)",
|
||||||
|
format = function(name, year, episode)
|
||||||
|
return clean_name(name) .. " (" .. year .. ") E" .. episode
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%-%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+[%.v]?%d*)%s*[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
|
||||||
|
format = function(name, season, episode, year)
|
||||||
|
return clean_name(name) .. " (" .. year .. ") S" .. season .. "E" .. episode:gsub("v%d+$","")
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%-%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+%.?%d*)",
|
||||||
|
format = function(name, season, episode)
|
||||||
|
return clean_name(name) .. " S" .. season .. "E" .. episode
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%.%s]%s*(%d+)[nrdsth]+[_%.%s]%s*[sS]eason[_%.%s]%s*%[(%d+[%.v]?%d*)%]",
|
||||||
|
format = function(name, season, episode)
|
||||||
|
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[^dD][eEpP]+(%d+[%.v]?%d*)[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
|
||||||
|
format = function(name, episode, year)
|
||||||
|
return clean_name(name) .. " (" .. year .. ") E" .. episode:gsub("v%d+$","")
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[^dD][eEpP]+(%d+%.?%d*)",
|
||||||
|
format = function(name, episode)
|
||||||
|
return clean_name(name) .. " E" .. episode
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*第%s*(%d+[%.v]?%d*)%s*[话回集]",
|
||||||
|
format = function(name, episode)
|
||||||
|
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%]",
|
||||||
|
format = function(name, episode)
|
||||||
|
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%(%a+%)%]",
|
||||||
|
format = function(name, episode)
|
||||||
|
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[%-#]%s*(%d+%.?%d*)%s*",
|
||||||
|
format = function(name, episode)
|
||||||
|
return clean_name(name) .. " E" .. episode
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[%[%(]([OVADSPs]+)[%]%)]",
|
||||||
|
format = function(name, sp)
|
||||||
|
return clean_name(name) .. " [" .. sp .. "]"
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%-%.%s]%s*(%d?%d)x(%d%d?%d?%d?)[^%dhHxXvVpPkKxXbBfF]",
|
||||||
|
format = function(name, season, episode)
|
||||||
|
return clean_name(name) .. " S" .. season .. "E" .. episode
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^%((%d%d%d%d)%.?%d?%d?%.?%d?%d?%)%s*(.-)%s*[%(%[]",
|
||||||
|
format = function(year, name)
|
||||||
|
return clean_name(name) .. " (" .. year .. ")"
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
|
||||||
|
format = function(name, year)
|
||||||
|
return clean_name(name) .. " (" .. year .. ")"
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^%[.-%]%s*%[?(.-)%]?%s*[%(%[]",
|
||||||
|
format = function(name)
|
||||||
|
return clean_name(name)
|
||||||
|
end
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function format_filename(title)
|
||||||
|
for _, formatter in ipairs(formatters) do
|
||||||
|
local matches = {title:match(formatter.regex)}
|
||||||
|
if #matches > 0 then
|
||||||
|
title = formatter.format(unpack(matches))
|
||||||
|
return title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
title = title:gsub("^%[.-%]", " ")
|
||||||
|
:gsub("^%(.-%)", " ")
|
||||||
|
:gsub("[_%.]", " ")
|
||||||
|
:gsub("^%s*(.-)%s*$", "%1")
|
||||||
|
:gsub("[!@#%.%?%+%-%%&*_=,/~`]+$", "")
|
||||||
|
return title
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_writable(path)
|
||||||
|
local file = io.open(path, "w")
|
||||||
|
if file then
|
||||||
|
file:close()
|
||||||
|
os.remove(path)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function download_file(url, fname)
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
local filename = mp.get_property("filename/no-ext")
|
||||||
|
local ext = fname:match('%.([^%.]+)$'):lower()
|
||||||
|
|
||||||
|
if is_protocol(path) then
|
||||||
|
sub_path = utils.join_path(TEMP_DIR, fname)
|
||||||
|
else
|
||||||
|
local dir = utils.split_path(normalize(path))
|
||||||
|
sub_path = utils.join_path(dir, filename .. ".assrt." .. ext)
|
||||||
|
if not is_writable(sub_path) then
|
||||||
|
sub_path = utils.join_path(TEMP_DIR, fname)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local message = "正在下载字幕..."
|
||||||
|
local type = "download_subtitle"
|
||||||
|
local title = "字幕下载菜单"
|
||||||
|
local footnote = "使用 / 打开筛选"
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote)
|
||||||
|
else
|
||||||
|
mp.osd_message(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
local cmd = {"curl", "-s", "--user-agent", "mpv", "-o", sub_path, url}
|
||||||
|
if o.proxy ~= "" then
|
||||||
|
table.insert(cmd, '-x')
|
||||||
|
table.insert(cmd, o.proxy)
|
||||||
|
end
|
||||||
|
local res = mp.command_native({ name = "subprocess", capture_stdout = true, capture_stderr = true, args = cmd })
|
||||||
|
if res.status == 0 then
|
||||||
|
if file_exists(sub_path) then
|
||||||
|
append_sub(sub_path)
|
||||||
|
local message = "字幕下载完成, 已载入"
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote)
|
||||||
|
-- 下载完字幕1.5秒后关闭面板
|
||||||
|
mp.add_timeout(1.5, function()
|
||||||
|
mp.commandv("script-message-to", "uosc", "close-menu", "download_subtitle")
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
mp.osd_message(message, 3)
|
||||||
|
end
|
||||||
|
msg.info("Subtitle downloaded: " .. sub_path)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local message = "字幕下载失败,查看控制台获取更多信息"
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote)
|
||||||
|
else
|
||||||
|
mp.osd_message(message, 3)
|
||||||
|
end
|
||||||
|
msg.error("Failed to download file: " .. res.stderr)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fetch_subtitle_details(sub_id)
|
||||||
|
local message = "正在加载字幕详细信息..."
|
||||||
|
local type = "subtitle_details"
|
||||||
|
local title = "字幕下载菜单"
|
||||||
|
local footnote = "使用 / 打开筛选"
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote)
|
||||||
|
else
|
||||||
|
mp.osd_message(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
local url = ASSRT_DETAIL_API .."?token=" .. o.api_token .. "&id=" .. (sub_id or 0)
|
||||||
|
local res = http_request(url)
|
||||||
|
if not res or res.status ~= 0 then
|
||||||
|
local message = "获取字幕详细信息失败,查看控制台获取更多信息"
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote)
|
||||||
|
else
|
||||||
|
mp.osd_message(message, 3)
|
||||||
|
end
|
||||||
|
msg.error("Failed to fetch subtitle details: " .. (res and res.errmsg or "Unknown error"))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local items = {}
|
||||||
|
items[#items + 1] = {
|
||||||
|
title = "..",
|
||||||
|
hint = "返回搜索结果",
|
||||||
|
value = {
|
||||||
|
"script-message-to",
|
||||||
|
mp.get_script_name(),
|
||||||
|
"search-subtitles-event",
|
||||||
|
"has_details", nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local subs = res.sub.subs[1]
|
||||||
|
for _, sub in ipairs(subs.filelist) do
|
||||||
|
table.insert(items, {
|
||||||
|
title = sub.f,
|
||||||
|
hint = sub.s,
|
||||||
|
value = {
|
||||||
|
"script-message-to",
|
||||||
|
mp.get_script_name(),
|
||||||
|
"download-file-event",
|
||||||
|
sub.url, sub.f,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if #items > 2 then
|
||||||
|
table.sort(items, function(a, b)
|
||||||
|
return alphanumsort(a.title, b.title)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #items == 0 and subs.url and not is_compressed_file(subs.filename) then
|
||||||
|
local size= subs.size / 1024
|
||||||
|
local sub_size = size > 1024 and string.format("%.2fMB", size / 1024) or string.format("%.2fKB", size)
|
||||||
|
table.insert(items, {
|
||||||
|
title = subs.filename,
|
||||||
|
hint = sub_size,
|
||||||
|
value = {
|
||||||
|
"script-message-to",
|
||||||
|
mp.get_script_name(),
|
||||||
|
"download-file-event",
|
||||||
|
subs.url, subs.filename,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, items, footnote)
|
||||||
|
elseif input_loaded then
|
||||||
|
mp.osd_message("")
|
||||||
|
mp.add_timeout(0.1, function()
|
||||||
|
open_menu_select(items)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function search_subtitles(pos, query)
|
||||||
|
local items = {}
|
||||||
|
local type = "menu_subtitle"
|
||||||
|
local title = "输入搜索内容"
|
||||||
|
local footnote = "使用enter或ctrl+enter进行搜索"
|
||||||
|
if pos ~= "has_details" and (query ~= cache.query or tonumber(pos) > 0) then
|
||||||
|
local pos = tonumber(pos)
|
||||||
|
local message = "正在搜索字幕..."
|
||||||
|
local cmd = { "script-message-to", mp.get_script_name(), "search-subtitles-event", tostring(pos) }
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote, cmd, query)
|
||||||
|
else
|
||||||
|
mp.osd_message(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
local url = ASSRT_SEARCH_API .. "?token=" .. o.api_token .. "&q=" .. url_encode(query) .. "&no_muxer=1&pos=" .. pos
|
||||||
|
local res = http_request(url)
|
||||||
|
if not res or res.status ~= 0 then
|
||||||
|
local message = "搜索字幕失败,查看控制台获取更多信息"
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote, cmd, query)
|
||||||
|
else
|
||||||
|
mp.osd_message(message, 3)
|
||||||
|
end
|
||||||
|
msg.error("Failed to search subtitles: " .. (res and res.errmsg or "Unknown error"))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local sub = res.sub
|
||||||
|
local subs = {}
|
||||||
|
if sub then subs = res.sub.subs end
|
||||||
|
if #subs == 0 then
|
||||||
|
local message = "未找到字幕,建议更改关键字尝试重新搜索"
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, message, footnote, cmd, query)
|
||||||
|
else
|
||||||
|
mp.osd_message(message, 3)
|
||||||
|
end
|
||||||
|
msg.info("No subtitles found.")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(items, {
|
||||||
|
title = "..",
|
||||||
|
hint = "返回搜索菜单",
|
||||||
|
value = {
|
||||||
|
"script-message-to",
|
||||||
|
mp.get_script_name(),
|
||||||
|
"open-search-menu",
|
||||||
|
0, query,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, sub in ipairs(subs) do
|
||||||
|
table.insert(items, {
|
||||||
|
title = sub.video_chinese_name and sub.video_chinese_name ~= '' and sub.video_chinese_name
|
||||||
|
or sub.native_name and sub.native_name ~= '' and sub.native_name or sub.videoname,
|
||||||
|
hint = sub.lang and sub.lang.desc ~= '' and sub.lang.desc
|
||||||
|
or sub.m_lang and sub.m_lang ~= '' and sub.m_lang:gsub(" ", " "),
|
||||||
|
value = {
|
||||||
|
"script-message-to",
|
||||||
|
mp.get_script_name(),
|
||||||
|
"fetch-details-event",
|
||||||
|
sub.id or sub.fileid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if #items == 16 then
|
||||||
|
pos = pos + 15
|
||||||
|
table.insert(items, {
|
||||||
|
title = "加载下一页",
|
||||||
|
value = {
|
||||||
|
"script-message-to",
|
||||||
|
mp.get_script_name(),
|
||||||
|
"search-subtitles-event",
|
||||||
|
tostring(pos), query,
|
||||||
|
},
|
||||||
|
italic = true,
|
||||||
|
bold = true,
|
||||||
|
align = "center",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
cache.query = query
|
||||||
|
cache.items = items
|
||||||
|
else
|
||||||
|
items = cache.items
|
||||||
|
end
|
||||||
|
|
||||||
|
if uosc_available then
|
||||||
|
update_menu_uosc(type, title, items, footnote)
|
||||||
|
elseif input_loaded then
|
||||||
|
mp.osd_message("")
|
||||||
|
mp.add_timeout(0.1, function()
|
||||||
|
open_menu_select(items)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function open_menu_select(menu_items)
|
||||||
|
local item_titles, item_values = {}, {}
|
||||||
|
for i, v in ipairs(menu_items) do
|
||||||
|
item_titles[i] = v.hint and v.title .. " (" .. v.hint .. ")" or v.title
|
||||||
|
item_values[i] = v.value
|
||||||
|
end
|
||||||
|
mp.commandv('script-message-to', 'console', 'disable')
|
||||||
|
input.select({
|
||||||
|
prompt = '筛选:',
|
||||||
|
items = item_titles,
|
||||||
|
submit = function(id)
|
||||||
|
mp.commandv(unpack(item_values[id]))
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function open_input_menu_get(pos, query)
|
||||||
|
mp.commandv('script-message-to', 'console', 'disable')
|
||||||
|
input.get({
|
||||||
|
prompt = '搜索字幕:',
|
||||||
|
default_text = query,
|
||||||
|
cursor_position = query and #query + 1,
|
||||||
|
submit = function(text)
|
||||||
|
input.terminate()
|
||||||
|
search_subtitles(pos, text)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function open_input_menu_uosc(pos, query)
|
||||||
|
local menu_props = {
|
||||||
|
type = "menu_subtitle",
|
||||||
|
title = "输入搜索内容",
|
||||||
|
search_style = "palette",
|
||||||
|
search_debounce = "submit",
|
||||||
|
search_suggestion = query,
|
||||||
|
on_search = {
|
||||||
|
"script-message-to",
|
||||||
|
mp.get_script_name(),
|
||||||
|
"search-subtitles-event",
|
||||||
|
tostring(pos),
|
||||||
|
},
|
||||||
|
footnote = "使用enter或ctrl+enter进行搜索",
|
||||||
|
items = {},
|
||||||
|
}
|
||||||
|
local json_props = utils.format_json(menu_props)
|
||||||
|
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
|
||||||
|
end
|
||||||
|
|
||||||
|
function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_cmd, query)
|
||||||
|
local items = {}
|
||||||
|
if type(menu_item) == "string" then
|
||||||
|
table.insert(items, {
|
||||||
|
title = menu_item,
|
||||||
|
value = "",
|
||||||
|
italic = true,
|
||||||
|
keep_open = true,
|
||||||
|
selectable = false,
|
||||||
|
align = "center",
|
||||||
|
})
|
||||||
|
else
|
||||||
|
items = menu_item
|
||||||
|
end
|
||||||
|
|
||||||
|
local menu_props = {
|
||||||
|
type = menu_type,
|
||||||
|
title = menu_title,
|
||||||
|
search_style = menu_cmd and "palette" or "on_demand",
|
||||||
|
search_debounce = menu_cmd and "submit" or 0,
|
||||||
|
on_search = menu_cmd,
|
||||||
|
footnote = menu_footnote,
|
||||||
|
search_suggestion = query,
|
||||||
|
items = items,
|
||||||
|
}
|
||||||
|
local json_props = utils.format_json(menu_props)
|
||||||
|
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sub_assrt()
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
local filename = mp.get_property("filename/no-ext")
|
||||||
|
local title = mp.get_property("media-title")
|
||||||
|
local thin_space = string.char(0xE2, 0x80, 0x89)
|
||||||
|
if not path then
|
||||||
|
msg.error("No file loaded.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_protocol(path) then
|
||||||
|
title = url_decode(title:gsub('%.[^%.]+$', ''))
|
||||||
|
elseif #title < #filename then
|
||||||
|
title = filename
|
||||||
|
end
|
||||||
|
|
||||||
|
local pos = 0
|
||||||
|
local title = title:gsub(thin_space, " ")
|
||||||
|
local query = format_filename(title):gsub("%s*E%d+$", "")
|
||||||
|
|
||||||
|
if cache.title and cache.title == query
|
||||||
|
and cache.items and #cache.items > 0 then
|
||||||
|
search_subtitles("has_details")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
cache.title = query
|
||||||
|
|
||||||
|
if uosc_available then
|
||||||
|
open_input_menu_uosc(pos, query)
|
||||||
|
elseif input_loaded then
|
||||||
|
open_input_menu_get(pos, query)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message('uosc-version', function()
|
||||||
|
uosc_available = true
|
||||||
|
mp.commandv('script-message-to', 'uosc', 'overwrite-binding', 'download-subtitles',
|
||||||
|
'script-message-to sub_assrt sub-assrt')
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_script_message("open-search-menu", function(pos, query)
|
||||||
|
if uosc_available then
|
||||||
|
mp.commandv("script-message-to", "uosc", "open-menu", "menu_subtitle")
|
||||||
|
end
|
||||||
|
if uosc_available then
|
||||||
|
open_input_menu_uosc(pos, query)
|
||||||
|
elseif input_loaded then
|
||||||
|
open_input_menu_get(pos, query)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_script_message("search-subtitles-event", function(pos, query)
|
||||||
|
if uosc_available then
|
||||||
|
mp.commandv("script-message-to", "uosc", "open-menu", "menu_subtitle")
|
||||||
|
end
|
||||||
|
search_subtitles(pos, query)
|
||||||
|
end)
|
||||||
|
mp.register_script_message("fetch-details-event", function(query)
|
||||||
|
if uosc_available then
|
||||||
|
mp.commandv("script-message-to", "uosc", "open-menu", "subtitle_details")
|
||||||
|
end
|
||||||
|
fetch_subtitle_details(query)
|
||||||
|
end)
|
||||||
|
mp.register_script_message("download-file-event", function(url, filename)
|
||||||
|
if uosc_available then
|
||||||
|
mp.commandv("script-message-to", "uosc", "open-menu", "download_subtitle")
|
||||||
|
end
|
||||||
|
download_file(url, filename)
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_script_message("sub-assrt", sub_assrt)
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
--[[
|
||||||
|
|
||||||
|
Automatically look for a fonts directory to use with `sub-fonts-dir`.
|
||||||
|
|
||||||
|
This mpv Lua script will automatically use the `sub-fonts-dir` option (to
|
||||||
|
override the default `~~/fonts` location) if it find a `Fonts` directory
|
||||||
|
alongside the currently playing file. (The name of the directory is
|
||||||
|
matched case-insensitively.)
|
||||||
|
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
|
||||||
|
Simply drop this script in your scripts configuration directory (usually
|
||||||
|
`~/.config/mpv/scripts/`).
|
||||||
|
|
||||||
|
|
||||||
|
REQUIREMENTS:
|
||||||
|
|
||||||
|
This script requires a version of mpv that includes the `sub-fonts-dir`
|
||||||
|
option.
|
||||||
|
|
||||||
|
|
||||||
|
NOTES:
|
||||||
|
|
||||||
|
- Any `--sub-fonts-dir` option passed on the command-line will override
|
||||||
|
this script.
|
||||||
|
|
||||||
|
- When going through a playlist, `sub-fonts-dir` will be dynamically
|
||||||
|
updated for each individual file.
|
||||||
|
|
||||||
|
- This script will output some additional information on higher verbosity
|
||||||
|
levels (`-v`). To increase the verbosity for this script only, use
|
||||||
|
`--msg-level=sub_fonts_dir_auto=v` (or `=debug` for more output).
|
||||||
|
|
||||||
|
|
||||||
|
AUTHOR:
|
||||||
|
|
||||||
|
Frédéric Brière (fbriere@fbriere.net)
|
||||||
|
|
||||||
|
Licensed under the GNU General Public License, version 2 or later.
|
||||||
|
|
||||||
|
--]]
|
||||||
|
|
||||||
|
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
-- msg.trace() was added in 0.28.0 -- define it ourselves if it's missing
|
||||||
|
if msg.trace == nil then
|
||||||
|
msg.trace = function(...) return mp.log("trace", ...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Directory name we are looking for (case-insensitive)
|
||||||
|
local FONTS_DIR_NAME = "Fonts"
|
||||||
|
-- Option name that we want to set
|
||||||
|
local OPTION_NAME = "sub-fonts-dir"
|
||||||
|
-- Make sure this option is available in this version of mpv
|
||||||
|
do
|
||||||
|
local _, err = mp.get_property(OPTION_NAME)
|
||||||
|
if err then
|
||||||
|
msg.error(string.format("This version of mpv does not support the %s option", OPTION_NAME))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Whether a path is a directory
|
||||||
|
local function isdir(path)
|
||||||
|
local meta, meta_error = utils.file_info(path)
|
||||||
|
if meta and meta.is_dir then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set an option's value for this file, without overriding the command-line
|
||||||
|
local function set_option(name, value)
|
||||||
|
if not mp.get_property_bool(string.format("option-info/%s/set-from-commandline", name)) then
|
||||||
|
msg.verbose(string.format("Setting %s to %q", name, value))
|
||||||
|
mp.set_property(string.format("file-local-options/%s", name), value)
|
||||||
|
else
|
||||||
|
msg.debug(string.format("Option %s was set on command-line -- leaving it as-is", name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find a "Fonts" directory under a single path
|
||||||
|
local function find_fonts_dir(path)
|
||||||
|
local fonts_path = utils.join_path(path, FONTS_DIR_NAME)
|
||||||
|
local meta, meta_error = utils.file_info(fonts_path)
|
||||||
|
if meta and meta.is_dir then
|
||||||
|
msg.trace("Match found")
|
||||||
|
return fonts_path
|
||||||
|
else
|
||||||
|
fonts_path = utils.join_path(path, FONTS_DIR_NAME:lower())
|
||||||
|
local fmeta, fmeta_error = utils.file_info(fonts_path)
|
||||||
|
if fmeta and fmeta.is_dir then
|
||||||
|
msg.trace("Match found")
|
||||||
|
return fonts_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
msg.trace("No match found")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "on_load" hook callback for when a file is about to be loaded.
|
||||||
|
local function on_load()
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
if isdir(path) then
|
||||||
|
msg.debug("Playing 'file' is actually a directory -- skipping")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local path_dir = utils.split_path(path)
|
||||||
|
-- Cosmetic nitpicking: That trailing "/" just looks annoying to me
|
||||||
|
path_dir = path_dir:gsub("(.)/+$", "%1")
|
||||||
|
|
||||||
|
msg.debug(string.format("Searching %q for fonts directory", path_dir))
|
||||||
|
local fonts_dir = find_fonts_dir(path_dir)
|
||||||
|
if fonts_dir then
|
||||||
|
msg.debug("Found fonts directory:", fonts_dir)
|
||||||
|
set_option(OPTION_NAME, fonts_dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
mp.add_hook("on_load", 50, on_load)
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
--[[
|
||||||
|
mpv-sub-select
|
||||||
|
|
||||||
|
This script allows you to configure advanced subtitle track selection based on
|
||||||
|
the current audio track and the names and language of the subtitle tracks.
|
||||||
|
|
||||||
|
https://github.com/CogentRedTester/mpv-sub-select
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local mp = require 'mp'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local opt = require 'mp.options'
|
||||||
|
|
||||||
|
local o = {
|
||||||
|
--forcibly enable the script regardless of the sid option
|
||||||
|
force_enable = false,
|
||||||
|
|
||||||
|
--experimental audio track selection based on the preferences.
|
||||||
|
select_audio = false,
|
||||||
|
|
||||||
|
--observe audio switches and reselect the subtitles when alang changes
|
||||||
|
observe_audio_switches = false,
|
||||||
|
|
||||||
|
--only select forced subtitles if they are explicitly included in slang
|
||||||
|
explicit_forced_subs = false,
|
||||||
|
|
||||||
|
--the folder that contains the 'sub-select.json' file
|
||||||
|
config = "~~/script-opts"
|
||||||
|
}
|
||||||
|
|
||||||
|
opt.read_options(o, "sub_select")
|
||||||
|
|
||||||
|
local prefs
|
||||||
|
|
||||||
|
local ENABLED = o.force_enable or true
|
||||||
|
local latest_audio = {}
|
||||||
|
local audio_tracks = {}
|
||||||
|
local sub_tracks = {}
|
||||||
|
|
||||||
|
-- represents when there is no audio or subtitle track selected
|
||||||
|
local NO_TRACK = {
|
||||||
|
id = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
--returns a table that stores the given table t as the __index in its metatable
|
||||||
|
--creates a prototypally inherited table
|
||||||
|
local function redirect_table(t, new)
|
||||||
|
return setmetatable(new or {}, { __index = t })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function type_check(val, t, required)
|
||||||
|
if val == nil then return not required end
|
||||||
|
if not t:find(type(val)) then return false end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setup_prefs()
|
||||||
|
local file = assert(io.open(mp.command_native({"expand-path", o.config .. "/sub-select.json"})))
|
||||||
|
local json = file:read("*all")
|
||||||
|
file:close()
|
||||||
|
prefs = utils.parse_json(json)
|
||||||
|
|
||||||
|
assert(prefs, "Invalid JSON format in sub-select.json.")
|
||||||
|
local reservedIDs = { ['^'] = true }
|
||||||
|
local IDs = {}
|
||||||
|
|
||||||
|
-- storing the ID in the first pass
|
||||||
|
for _, pref in ipairs(prefs) do
|
||||||
|
if pref.id then
|
||||||
|
assert(not reservedIDs[pref.id], 'using reserved ID '..pref.id)
|
||||||
|
assert(not IDs[pref.id], 'duplicate ID '..pref.id)
|
||||||
|
IDs[pref.id] = pref
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- doing a second pass to inherit prefs and type check
|
||||||
|
for i, pref in ipairs(prefs) do
|
||||||
|
local pref_str = 'pref_'..i..' '..utils.to_string(pref)
|
||||||
|
assert(type_check(pref.inherit, 'string'), '`inherit` must be a string: '..pref_str)
|
||||||
|
|
||||||
|
if pref.inherit then
|
||||||
|
local parent = pref.inherit == '^' and prefs[i-1] or IDs[pref.inherit]
|
||||||
|
assert(parent, 'failed to find matching id: '..pref_str)
|
||||||
|
pref = redirect_table(parent, pref)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- type checking the options
|
||||||
|
assert(type_check(pref.alang, 'string table', true), '`alang` must be a string or a table of strings: '..pref_str)
|
||||||
|
assert(type_check(pref.slang, 'string table', true), '`slang` must be a string or a table of strings: '..pref_str)
|
||||||
|
assert(type_check(pref.blacklist, 'table'), '`blacklist` must be a table: '..pref_str)
|
||||||
|
assert(type_check(pref.whitelist, 'table'), '`whitelist` must be a table: '..pref_str)
|
||||||
|
assert(type_check(pref.condition, 'string'), '`condition` must be a string: '..pref_str)
|
||||||
|
assert(type_check(pref.id, 'string'), '`id` must be a string: '..pref_str)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setup_prefs()
|
||||||
|
|
||||||
|
--evaluates and runs the given string in both Lua 5.1 and 5.2
|
||||||
|
--the name argument is used for error reporting
|
||||||
|
--provides the mpv modules and the fb module to the string
|
||||||
|
local function evaluate_string(str, env)
|
||||||
|
msg.trace('evaluating string '..str)
|
||||||
|
|
||||||
|
env = redirect_table(_G, env)
|
||||||
|
env.mp = redirect_table(mp)
|
||||||
|
env.msg = redirect_table(msg)
|
||||||
|
env.utils = redirect_table(utils)
|
||||||
|
|
||||||
|
local chunk, err
|
||||||
|
if setfenv then
|
||||||
|
chunk, err = loadstring(str)
|
||||||
|
if chunk then setfenv(chunk, env) end
|
||||||
|
else
|
||||||
|
chunk, err = load(str, nil, 't', env)
|
||||||
|
end
|
||||||
|
if not chunk then
|
||||||
|
msg.warn('failed to load string:', str)
|
||||||
|
msg.error(err)
|
||||||
|
chunk = function() return nil end
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, boolean = pcall(chunk)
|
||||||
|
if not success then msg.error(boolean) end
|
||||||
|
return boolean
|
||||||
|
end
|
||||||
|
|
||||||
|
--anticipates the default audio track
|
||||||
|
--returns the node for the predicted track
|
||||||
|
--this whole function can be skipped if the user decides to load the subtitles asynchronously instead,
|
||||||
|
--or if `--aid` is not set to `auto`
|
||||||
|
local function predict_audio()
|
||||||
|
--if the option is not set to auto then it is easy
|
||||||
|
local aid = mp.get_property("options/aid", "auto")
|
||||||
|
if aid == "no" then return NO_TRACK
|
||||||
|
elseif aid ~= "auto" then return audio_tracks[tonumber(aid)] end
|
||||||
|
|
||||||
|
local num_tracks = #audio_tracks
|
||||||
|
if num_tracks == 1 then return audio_tracks[1]
|
||||||
|
elseif num_tracks == 0 then return NO_TRACK end
|
||||||
|
|
||||||
|
local highest_priority = NO_TRACK
|
||||||
|
local priority_str = ""
|
||||||
|
local alangs = mp.get_property_native("alang", {})
|
||||||
|
|
||||||
|
--loop through the track list for any audio tracks
|
||||||
|
for _,track in ipairs(audio_tracks) do
|
||||||
|
|
||||||
|
--loop through the alang list to check if it has a preference
|
||||||
|
local pref = 0
|
||||||
|
for j,alang in ipairs(alangs) do
|
||||||
|
if track.lang == alang then
|
||||||
|
|
||||||
|
--a lower number j has higher priority, so flip the numbers around so the lowest j has highest preference
|
||||||
|
pref = 1000 - j
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--format the important preferences so that we can easily use a lexicographical comparison to find the default
|
||||||
|
local formatted_str = string.format("%d-%03d-%d-%02d",
|
||||||
|
track.forced and 1 or 0,
|
||||||
|
pref,
|
||||||
|
track.default and 1 or 0,
|
||||||
|
num_tracks - track.id
|
||||||
|
)
|
||||||
|
msg.trace("formatted track info: " .. formatted_str)
|
||||||
|
|
||||||
|
if formatted_str > priority_str then
|
||||||
|
priority_str = formatted_str
|
||||||
|
highest_priority = track
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.verbose("predicted audio track is "..tostring(highest_priority.id))
|
||||||
|
return highest_priority
|
||||||
|
end
|
||||||
|
|
||||||
|
--sets the subtitle track to the given sid
|
||||||
|
--this is a function to prepare for some upcoming functionality, but I've forgotten what that is
|
||||||
|
local function set_track(type, id)
|
||||||
|
msg.verbose("setting", type, "to", id)
|
||||||
|
if mp.get_property_number(type) == id then return end
|
||||||
|
mp.set_property('file-local-options/'..type, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
--checks if the given audio matches the given track preference
|
||||||
|
local function is_valid_audio(audio, pref)
|
||||||
|
local alangs = type(pref.alang) == "string" and {pref.alang} or pref.alang
|
||||||
|
|
||||||
|
for _,lang in ipairs(alangs) do
|
||||||
|
msg.trace("Checking for valid audio:", lang)
|
||||||
|
|
||||||
|
if audio == NO_TRACK then
|
||||||
|
if lang == "no" then return true end
|
||||||
|
else
|
||||||
|
if lang == '*' then
|
||||||
|
return true
|
||||||
|
elseif lang == "forced" then
|
||||||
|
if audio.forced then return true end
|
||||||
|
elseif lang == "default" then
|
||||||
|
if audio.default then return true end
|
||||||
|
else
|
||||||
|
if audio.lang and audio.lang:lower():find(lang) then return true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--checks if the given sub matches the given track preference
|
||||||
|
local function is_valid_sub(sub, slang, pref)
|
||||||
|
msg.trace("checking sub", slang, "against track", utils.to_string(sub))
|
||||||
|
|
||||||
|
-- Do not try to un-nest these if statements, it will break detection of default and forced tracks.
|
||||||
|
-- I've already had to un-nest these statements twice due to this mistake, don't let it happen again.
|
||||||
|
if sub == NO_TRACK then
|
||||||
|
return slang == 'no'
|
||||||
|
else
|
||||||
|
if slang == "default" then
|
||||||
|
if not sub.default then return false end
|
||||||
|
elseif slang == "forced" then
|
||||||
|
if not sub.forced then return false end
|
||||||
|
else
|
||||||
|
if sub.forced and o.explicit_forced_subs then return false end
|
||||||
|
if not sub.lang:lower():find(slang) and slang ~= "*" then return false end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local title = sub.title or ''
|
||||||
|
|
||||||
|
-- if the whitelist is not set then we don't need to find anything
|
||||||
|
local passes_whitelist = not pref.whitelist
|
||||||
|
local passes_blacklist = true
|
||||||
|
|
||||||
|
-- whitelist/blacklist handling
|
||||||
|
if pref.whitelist and title then
|
||||||
|
for _,word in ipairs(pref.whitelist) do
|
||||||
|
if title:lower():find(word) then passes_whitelist = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if pref.blacklist and title then
|
||||||
|
for _,word in ipairs(pref.blacklist) do
|
||||||
|
if title:lower():find(word) then passes_blacklist = false end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.trace(string.format("%s %s whitelist: %s | %s blacklist: %s",
|
||||||
|
title,
|
||||||
|
passes_whitelist and "passed" or "failed", utils.to_string(pref.whitelist),
|
||||||
|
passes_blacklist and "passed" or "failed", utils.to_string(pref.blacklist)
|
||||||
|
))
|
||||||
|
return passes_whitelist and passes_blacklist
|
||||||
|
end
|
||||||
|
|
||||||
|
--scans the track list and selects audio and subtitle tracks which match the track preferences
|
||||||
|
--if an audio track is provided to the function it will assume this track is the only audio
|
||||||
|
local function find_valid_tracks(manual_audio)
|
||||||
|
assert(manual_audio == nil or (type(manual_audio) == 'table' and manual_audio.id), 'argument must be an audio track or nil')
|
||||||
|
|
||||||
|
local sub_track_list = {NO_TRACK, unpack(sub_tracks)}
|
||||||
|
local audio_track_list
|
||||||
|
|
||||||
|
if manual_audio == nil then
|
||||||
|
audio_track_list = {NO_TRACK, unpack(audio_tracks)}
|
||||||
|
else
|
||||||
|
audio_track_list = {manual_audio}
|
||||||
|
end
|
||||||
|
|
||||||
|
if manual_audio then msg.debug("select subtitle for", utils.to_string(manual_audio))
|
||||||
|
else msg.debug('selecting audio and subtitles') end
|
||||||
|
|
||||||
|
--searching the selection presets for one that applies to this track
|
||||||
|
for _,pref in ipairs(prefs) do
|
||||||
|
msg.debug("checking pref:", utils.to_string(pref))
|
||||||
|
|
||||||
|
for _, audio_track in ipairs(audio_track_list) do
|
||||||
|
if is_valid_audio(audio_track, pref) then
|
||||||
|
local aid = audio_track and audio_track.id
|
||||||
|
|
||||||
|
--checks if any of the subtitle tracks match the preset for the current audio
|
||||||
|
local slangs = type(pref.slang) == "string" and {pref.slang} or pref.slang
|
||||||
|
msg.trace("valid audio preference found:", utils.to_string(pref.alang))
|
||||||
|
|
||||||
|
for _, slang in ipairs(slangs) do
|
||||||
|
msg.trace("checking for valid sub:", slang)
|
||||||
|
|
||||||
|
|
||||||
|
for _,sub_track in ipairs(sub_track_list) do
|
||||||
|
if is_valid_sub(sub_track, slang, pref)
|
||||||
|
and (not pref.condition or (evaluate_string('return '..pref.condition, {
|
||||||
|
audio = aid > 0 and audio_track or nil,
|
||||||
|
sub = sub_track.id > 0 and sub_track or nil
|
||||||
|
}) == true))
|
||||||
|
then
|
||||||
|
msg.verbose("valid audio preference found:", utils.to_string(pref.alang))
|
||||||
|
msg.verbose("valid subtitle preference found:", utils.to_string(pref.slang))
|
||||||
|
return aid, sub_track and sub_track.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--returns the audio node for the currently playing audio track
|
||||||
|
local function find_current_audio()
|
||||||
|
local aid = mp.get_property_number("aid", 0)
|
||||||
|
return audio_tracks[aid] or NO_TRACK
|
||||||
|
end
|
||||||
|
|
||||||
|
--extract the language code from an audio track node and pass it to select_subtitles
|
||||||
|
local function select_tracks(audio)
|
||||||
|
local aid, sid = find_valid_tracks(audio)
|
||||||
|
if sid then
|
||||||
|
set_track('sid', sid == 0 and 'no' or sid)
|
||||||
|
end
|
||||||
|
if aid and o.select_audio then
|
||||||
|
set_track('aid', aid == 0 and 'no' or aid)
|
||||||
|
end
|
||||||
|
|
||||||
|
latest_audio = audio or find_current_audio()
|
||||||
|
end
|
||||||
|
|
||||||
|
--select subtitles asynchronously after playback start
|
||||||
|
local function async_load()
|
||||||
|
select_tracks(not o.select_audio and find_current_audio() or nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
--select subtitles synchronously during the on_preloaded hook
|
||||||
|
local function preload()
|
||||||
|
if o.select_audio then return select_tracks() end
|
||||||
|
|
||||||
|
local audio = predict_audio()
|
||||||
|
select_tracks(audio)
|
||||||
|
latest_audio = audio
|
||||||
|
end
|
||||||
|
|
||||||
|
local track_auto_selection = true
|
||||||
|
mp.observe_property("track-auto-selection", "bool", function(_,b) track_auto_selection = b end)
|
||||||
|
|
||||||
|
local function selection_enabled()
|
||||||
|
if not ENABLED then return false end
|
||||||
|
if not track_auto_selection then return false end
|
||||||
|
if #sub_tracks == 0 then return false end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local INITIAL_LOAD = true
|
||||||
|
local ORIGINAL_SID = mp.get_property('options/sid')
|
||||||
|
|
||||||
|
mp.add_hook('on_load', 50, function()
|
||||||
|
INITIAL_LOAD = true
|
||||||
|
ORIGINAL_SID = mp.get_property('options/sid')
|
||||||
|
end)
|
||||||
|
|
||||||
|
--reselect the subtitles if the audio is different from what was last used
|
||||||
|
local function reselect_subtitles()
|
||||||
|
local initial = INITIAL_LOAD
|
||||||
|
INITIAL_LOAD = false
|
||||||
|
if not selection_enabled() then return end
|
||||||
|
local audio = find_current_audio()
|
||||||
|
if latest_audio.id ~= audio.id and (not initial or ORIGINAL_SID == 'auto') then
|
||||||
|
msg.info("detected audio change - reselecting subtitles")
|
||||||
|
select_tracks(audio)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--setups the audio and subtitle track lists to use for the rest of the script
|
||||||
|
local function read_track_list()
|
||||||
|
local track_list = mp.get_property_native("track-list", {})
|
||||||
|
audio_tracks = {}
|
||||||
|
sub_tracks = {}
|
||||||
|
for _,track in ipairs(track_list) do
|
||||||
|
if not track.lang then track.lang = "und" end
|
||||||
|
|
||||||
|
if track.type == "audio" then
|
||||||
|
table.insert(audio_tracks, track)
|
||||||
|
elseif track.type == "sub" then
|
||||||
|
table.insert(sub_tracks, track)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function observe_audio_switches()
|
||||||
|
mp.observe_property("aid", "string", reselect_subtitles)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unobserve_audio_switches()
|
||||||
|
mp.unobserve_property(reselect_subtitles)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.add_hook('on_preloaded', 25, read_track_list)
|
||||||
|
mp.add_hook('on_preloaded', 26, function() latest_audio = predict_audio() end)
|
||||||
|
|
||||||
|
--events for file loading
|
||||||
|
mp.add_hook('on_preloaded', 30, function()
|
||||||
|
if not selection_enabled() then return end
|
||||||
|
if mp.get_property('options/sid') ~= 'auto' then return end
|
||||||
|
preload()
|
||||||
|
end)
|
||||||
|
|
||||||
|
if o.observe_audio_switches then
|
||||||
|
mp.register_event('file-loaded', observe_audio_switches)
|
||||||
|
mp.add_hook('on_unload', 50, unobserve_audio_switches)
|
||||||
|
else
|
||||||
|
mp.register_event('file-loaded', reselect_subtitles)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.observe_property('track-list/count', 'number', read_track_list)
|
||||||
|
|
||||||
|
--force subtitle selection during playback
|
||||||
|
mp.register_script_message("select-subtitles", async_load)
|
||||||
|
|
||||||
|
--toggle sub-select during playback
|
||||||
|
mp.register_script_message("sub-select", function(arg)
|
||||||
|
if arg == "toggle" then ENABLED = not ENABLED
|
||||||
|
elseif arg == "enable" then ENABLED = true
|
||||||
|
elseif arg == "disable" then ENABLED = false end
|
||||||
|
local str = "sub-select: ".. (ENABLED and "enabled" or "disabled")
|
||||||
|
mp.osd_message(str)
|
||||||
|
|
||||||
|
if not selection_enabled() then return end
|
||||||
|
async_load()
|
||||||
|
end)
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
-- SOURCE: https://github.com/kelciour/mpv-scripts/blob/master/sub-export.lua
|
||||||
|
-- COMMIT: 29 Aug 2018 5039d8b
|
||||||
|
--
|
||||||
|
-- Usage:
|
||||||
|
-- add bindings to input.conf:
|
||||||
|
-- key script-message-to sub_export export-selected-subtitles
|
||||||
|
--
|
||||||
|
-- Note:
|
||||||
|
-- Requires FFmpeg in PATH environment variable or edit ffmpeg_path in the script options,
|
||||||
|
-- for example, by replacing "ffmpeg" with "C:\Programs\ffmpeg\bin\ffmpeg.exe"
|
||||||
|
-- Note:
|
||||||
|
-- The script support subtitles in srt, ass, and sup formats.
|
||||||
|
-- Note:
|
||||||
|
-- A small circle at the top-right corner is a sign that export is happenning now.
|
||||||
|
-- Note:
|
||||||
|
-- The exported subtitles will be automatically selected with visibility set to true.
|
||||||
|
-- Note:
|
||||||
|
-- It could take ~1-5 minutes to export subtitles.
|
||||||
|
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local options = require "mp.options"
|
||||||
|
|
||||||
|
---- Script Options ----
|
||||||
|
local o = {
|
||||||
|
ffmpeg_path = "ffmpeg",
|
||||||
|
-- eng=English, chs=Chinese
|
||||||
|
language = "eng",
|
||||||
|
}
|
||||||
|
|
||||||
|
options.read_options(o)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, windows uses backslashes
|
||||||
|
|
||||||
|
local TEMP_DIR = os.getenv("TEMP") or "/tmp"
|
||||||
|
local function is_writable(path)
|
||||||
|
local file = io.open(path, "w")
|
||||||
|
if file then
|
||||||
|
file:close()
|
||||||
|
os.remove(path)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function export_selected_subtitles()
|
||||||
|
local i = 0
|
||||||
|
local tracks_count = mp.get_property_number("track-list/count")
|
||||||
|
while i < tracks_count do
|
||||||
|
local track_type = mp.get_property(string.format("track-list/%d/type", i))
|
||||||
|
local track_index = mp.get_property_number(string.format("track-list/%d/ff-index", i))
|
||||||
|
local track_selected = mp.get_property(string.format("track-list/%d/selected", i))
|
||||||
|
local track_title = mp.get_property(string.format("track-list/%d/title", i))
|
||||||
|
local track_lang = mp.get_property(string.format("track-list/%d/lang", i))
|
||||||
|
local track_external = mp.get_property(string.format("track-list/%d/external", i))
|
||||||
|
local track_codec = mp.get_property(string.format("track-list/%d/codec", i))
|
||||||
|
local path = mp.get_property('path')
|
||||||
|
local dir, filename = utils.split_path(path)
|
||||||
|
local fname = mp.get_property("filename/no-ext")
|
||||||
|
local index = string.format("0:%d", track_index)
|
||||||
|
|
||||||
|
if track_type == "sub" and track_selected == "yes" then
|
||||||
|
if track_external == "yes" then
|
||||||
|
if o.language == 'chs' then
|
||||||
|
msg.info("错误:已选择外部字幕")
|
||||||
|
mp.osd_message("错误:已选择外部字幕", 2)
|
||||||
|
else
|
||||||
|
msg.info("Error: external subtitles have been selected")
|
||||||
|
mp.osd_message("Error: external subtitles have been selected", 2)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local video_file = utils.join_path(dir, filename)
|
||||||
|
|
||||||
|
local subtitles_ext = ".srt"
|
||||||
|
if string.find(track_codec, "ass") ~= nil then
|
||||||
|
subtitles_ext = ".ass"
|
||||||
|
elseif string.find(track_codec, "pgs") ~= nil then
|
||||||
|
subtitles_ext = ".sup"
|
||||||
|
end
|
||||||
|
|
||||||
|
if track_lang ~= nil then
|
||||||
|
if track_title ~= nil then
|
||||||
|
subtitles_ext = "." .. track_title .. "." .. track_lang .. subtitles_ext
|
||||||
|
else
|
||||||
|
subtitles_ext = "." .. track_lang .. subtitles_ext
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subtitles_file = utils.join_path(dir, fname .. subtitles_ext)
|
||||||
|
|
||||||
|
if not is_writable(subtitles_file) then
|
||||||
|
subtitles_file = utils.join_path(TEMP_DIR, fname .. subtitles_ext)
|
||||||
|
end
|
||||||
|
|
||||||
|
if o.language == 'chs' then
|
||||||
|
msg.info("正在导出当前字幕")
|
||||||
|
mp.osd_message("正在导出当前字幕")
|
||||||
|
else
|
||||||
|
msg.info("Exporting selected subtitles")
|
||||||
|
mp.osd_message("Exporting selected subtitles")
|
||||||
|
end
|
||||||
|
|
||||||
|
cmd = string.format("%s -y -hide_banner -loglevel error -i '%s' -map '%s' -vn -an -c:s copy '%s'",
|
||||||
|
o.ffmpeg_path, video_file, index, subtitles_file)
|
||||||
|
windows_args = { 'powershell', '-NoProfile', '-Command', cmd }
|
||||||
|
unix_args = { '/bin/bash', '-c', cmd }
|
||||||
|
args = is_windows and windows_args or unix_args
|
||||||
|
|
||||||
|
mp.add_timeout(mp.get_property_number("osd-duration") * 0.001, process)
|
||||||
|
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function process()
|
||||||
|
local screenx, screeny, aspect = mp.get_osd_size()
|
||||||
|
|
||||||
|
mp.set_osd_ass(screenx, screeny, "{\\an9}● ")
|
||||||
|
local res = mp.command_native({ name = "subprocess", capture_stdout = true, playback_only = false, args = args })
|
||||||
|
mp.set_osd_ass(screenx, screeny, "")
|
||||||
|
if res.status == 0 then
|
||||||
|
if o.language == 'chs' then
|
||||||
|
msg.info("当前字幕已导出")
|
||||||
|
mp.osd_message("当前字幕已导出")
|
||||||
|
else
|
||||||
|
msg.info("Finished exporting subtitles")
|
||||||
|
mp.osd_message("Finished exporting subtitles")
|
||||||
|
end
|
||||||
|
mp.commandv("sub-add", subtitles_file)
|
||||||
|
mp.set_property("sub-visibility", "yes")
|
||||||
|
else
|
||||||
|
if o.language == 'chs' then
|
||||||
|
msg.info("当前字幕导出失败")
|
||||||
|
mp.osd_message("当前字幕导出失败, 查看控制台获取更多信息.")
|
||||||
|
else
|
||||||
|
msg.info("Failed to export subtitles")
|
||||||
|
mp.osd_message("Failed to export subtitles, check console for more info.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message("export-selected-subtitles", export_selected_subtitles)
|
||||||
@@ -0,0 +1,947 @@
|
|||||||
|
-- thumbfast.lua
|
||||||
|
--
|
||||||
|
-- High-performance on-the-fly thumbnailer
|
||||||
|
--
|
||||||
|
-- Built for easy integration in third-party UIs.
|
||||||
|
|
||||||
|
--[[
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
-- Socket path (leave empty for auto)
|
||||||
|
socket = "",
|
||||||
|
|
||||||
|
-- Thumbnail path (leave empty for auto)
|
||||||
|
thumbnail = "",
|
||||||
|
|
||||||
|
-- Maximum thumbnail size in pixels (scaled down to fit)
|
||||||
|
-- Values are scaled when hidpi is enabled
|
||||||
|
max_height = 200,
|
||||||
|
max_width = 200,
|
||||||
|
|
||||||
|
-- Overlay id
|
||||||
|
overlay_id = 42,
|
||||||
|
|
||||||
|
-- Spawn thumbnailer on file load for faster initial thumbnails
|
||||||
|
spawn_first = false,
|
||||||
|
|
||||||
|
-- Close thumbnailer process after an inactivity period in seconds, 0 to disable
|
||||||
|
quit_after_inactivity = 0,
|
||||||
|
|
||||||
|
-- Enable on network playback
|
||||||
|
network = false,
|
||||||
|
|
||||||
|
-- Enable on audio playback
|
||||||
|
audio = false,
|
||||||
|
|
||||||
|
-- Enable hardware decoding
|
||||||
|
hwdec = false,
|
||||||
|
|
||||||
|
-- Windows only: use native Windows API to write to pipe (requires LuaJIT)
|
||||||
|
direct_io = false,
|
||||||
|
|
||||||
|
-- Custom path to the mpv executable
|
||||||
|
mpv_path = "mpv",
|
||||||
|
|
||||||
|
-- Specifies a blacklist of video extensions to ignore
|
||||||
|
blacklist_ext = "bdmv,ifo",
|
||||||
|
|
||||||
|
-- excluded directories for shared, #windows: ["X:", "Z:", "F:/Download/", "Download"]
|
||||||
|
excluded_dir = [[
|
||||||
|
[]
|
||||||
|
]],
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.utils = require "mp.utils"
|
||||||
|
mp.options = require "mp.options"
|
||||||
|
mp.options.read_options(options, "thumbfast")
|
||||||
|
|
||||||
|
local properties = {}
|
||||||
|
local pre_0_30_0 = mp.command_native_async == nil
|
||||||
|
local pre_0_33_0 = true
|
||||||
|
|
||||||
|
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, windows uses backslashes
|
||||||
|
|
||||||
|
local function split(input)
|
||||||
|
local ret = {}
|
||||||
|
for str in string.gmatch(input, "([^,]+)") do
|
||||||
|
ret[#ret + 1] = str
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
local function exclude(extension, tab)
|
||||||
|
if #tab > 0 then
|
||||||
|
for _, ext in pairs(tab) do
|
||||||
|
if extension == ext then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function need_ignore(tab, val)
|
||||||
|
for index, element in ipairs(tab) do
|
||||||
|
if string.find(val, element) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_protocol(path)
|
||||||
|
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
function subprocess(args, async, callback)
|
||||||
|
callback = callback or function() end
|
||||||
|
|
||||||
|
if not pre_0_30_0 then
|
||||||
|
if async then
|
||||||
|
return mp.command_native_async({name = "subprocess", playback_only = true, args = args}, callback)
|
||||||
|
else
|
||||||
|
return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if async then
|
||||||
|
return mp.utils.subprocess_detached({args = args}, callback)
|
||||||
|
else
|
||||||
|
return mp.utils.subprocess({args = args})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local winapi = {}
|
||||||
|
if options.direct_io then
|
||||||
|
local ffi_loaded, ffi = pcall(require, "ffi")
|
||||||
|
if ffi_loaded then
|
||||||
|
winapi = {
|
||||||
|
ffi = ffi,
|
||||||
|
C = ffi.C,
|
||||||
|
bit = require("bit"),
|
||||||
|
socket_wc = "",
|
||||||
|
|
||||||
|
-- WinAPI constants
|
||||||
|
CP_UTF8 = 65001,
|
||||||
|
GENERIC_WRITE = 0x40000000,
|
||||||
|
OPEN_EXISTING = 3,
|
||||||
|
FILE_FLAG_WRITE_THROUGH = 0x80000000,
|
||||||
|
FILE_FLAG_NO_BUFFERING = 0x20000000,
|
||||||
|
PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
|
||||||
|
|
||||||
|
INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
|
||||||
|
|
||||||
|
-- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
|
||||||
|
_lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
|
||||||
|
}
|
||||||
|
-- cache flags used in run() to avoid bor() call
|
||||||
|
winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
|
||||||
|
|
||||||
|
ffi.cdef[[
|
||||||
|
void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
|
||||||
|
bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
|
||||||
|
bool __stdcall CloseHandle(void *hObject);
|
||||||
|
bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
|
||||||
|
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||||
|
]]
|
||||||
|
|
||||||
|
winapi.MultiByteToWideChar = function(MultiByteStr)
|
||||||
|
if MultiByteStr then
|
||||||
|
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0)
|
||||||
|
if utf16_len > 0 then
|
||||||
|
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||||
|
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
|
||||||
|
return utf16_str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
options.direct_io = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = nil
|
||||||
|
local file_bytes = 0
|
||||||
|
local spawned = false
|
||||||
|
local disabled = false
|
||||||
|
local force_disabled = false
|
||||||
|
local spawn_waiting = false
|
||||||
|
local spawn_working = false
|
||||||
|
local script_written = false
|
||||||
|
|
||||||
|
local dirty = false
|
||||||
|
|
||||||
|
local x = nil
|
||||||
|
local y = nil
|
||||||
|
local last_x = x
|
||||||
|
local last_y = y
|
||||||
|
|
||||||
|
local last_seek_time = nil
|
||||||
|
|
||||||
|
local effective_w = options.max_width
|
||||||
|
local effective_h = options.max_height
|
||||||
|
local real_w = nil
|
||||||
|
local real_h = nil
|
||||||
|
local last_real_w = nil
|
||||||
|
local last_real_h = nil
|
||||||
|
|
||||||
|
local script_name = nil
|
||||||
|
|
||||||
|
local show_thumbnail = false
|
||||||
|
|
||||||
|
local filters_reset = {["lavfi-crop"]=true, ["crop"]=true}
|
||||||
|
local filters_runtime = {["hflip"]=true, ["vflip"]=true}
|
||||||
|
local filters_all = {["hflip"]=true, ["vflip"]=true, ["lavfi-crop"]=true, ["crop"]=true}
|
||||||
|
|
||||||
|
local last_vf_reset = ""
|
||||||
|
local last_vf_runtime = ""
|
||||||
|
|
||||||
|
local last_rotate = 0
|
||||||
|
|
||||||
|
local par = ""
|
||||||
|
local last_par = ""
|
||||||
|
|
||||||
|
local last_has_vid = 0
|
||||||
|
local has_vid = 0
|
||||||
|
|
||||||
|
local file_timer = nil
|
||||||
|
local file_check_period = 1/60
|
||||||
|
|
||||||
|
local allow_fast_seek = true
|
||||||
|
|
||||||
|
local client_script = [=[
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
MPV_IPC_FD=0; MPV_IPC_PATH="%s"
|
||||||
|
trap "kill 0" EXIT
|
||||||
|
while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
|
||||||
|
if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi
|
||||||
|
]=]
|
||||||
|
|
||||||
|
local cached_ranges = {}
|
||||||
|
local ext_blacklist = split(options.blacklist_ext)
|
||||||
|
local excluded_dir = mp.utils.parse_json(options.excluded_dir)
|
||||||
|
|
||||||
|
local function get_os()
|
||||||
|
local raw_os_name = ""
|
||||||
|
|
||||||
|
if jit and jit.os and jit.arch then
|
||||||
|
raw_os_name = jit.os
|
||||||
|
else
|
||||||
|
if package.config:sub(1,1) == "\\" then
|
||||||
|
-- Windows
|
||||||
|
local env_OS = os.getenv("OS")
|
||||||
|
if env_OS then
|
||||||
|
raw_os_name = env_OS
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raw_os_name = subprocess({"uname", "-s"}).stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
raw_os_name = (raw_os_name):lower()
|
||||||
|
|
||||||
|
local os_patterns = {
|
||||||
|
["windows"] = "windows",
|
||||||
|
["linux"] = "linux",
|
||||||
|
|
||||||
|
["osx"] = "darwin",
|
||||||
|
["mac"] = "darwin",
|
||||||
|
["darwin"] = "darwin",
|
||||||
|
|
||||||
|
["^mingw"] = "windows",
|
||||||
|
["^cygwin"] = "windows",
|
||||||
|
|
||||||
|
["bsd$"] = "darwin",
|
||||||
|
["sunos"] = "darwin"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Default to linux
|
||||||
|
local str_os_name = "linux"
|
||||||
|
|
||||||
|
for pattern, name in pairs(os_patterns) do
|
||||||
|
if raw_os_name:match(pattern) then
|
||||||
|
str_os_name = name
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return str_os_name
|
||||||
|
end
|
||||||
|
|
||||||
|
local os_name = mp.get_property("platform") or get_os()
|
||||||
|
|
||||||
|
local path_separator = os_name == "windows" and "\\" or "/"
|
||||||
|
|
||||||
|
if options.socket == "" then
|
||||||
|
if os_name == "windows" then
|
||||||
|
options.socket = "thumbfast"
|
||||||
|
else
|
||||||
|
options.socket = "/tmp/thumbfast"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.thumbnail == "" then
|
||||||
|
if os_name == "windows" then
|
||||||
|
options.thumbnail = os.getenv("TEMP").."\\thumbfast.out"
|
||||||
|
else
|
||||||
|
options.thumbnail = "/tmp/thumbfast.out"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local unique = mp.utils.getpid()
|
||||||
|
|
||||||
|
options.socket = options.socket .. unique
|
||||||
|
options.thumbnail = options.thumbnail .. unique
|
||||||
|
|
||||||
|
if options.direct_io then
|
||||||
|
if os_name == "windows" then
|
||||||
|
winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
if winapi.socket_wc == "" then
|
||||||
|
options.direct_io = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local mpv_path = options.mpv_path
|
||||||
|
|
||||||
|
if mpv_path == "mpv" then
|
||||||
|
local frontend_name = mp.get_property_native("user-data/frontend/name")
|
||||||
|
if frontend_name == "mpv.net" then
|
||||||
|
mpv_path = mp.get_property_native("user-data/frontend/process-path")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if mpv_path == "mpv" and os_name == "darwin" and unique then
|
||||||
|
-- TODO: look into ~~osxbundle/
|
||||||
|
mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "")
|
||||||
|
if mpv_path ~= "mpv" then
|
||||||
|
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
|
||||||
|
local mpv_bin = mp.utils.file_info("/usr/local/mpv")
|
||||||
|
if mpv_bin and mpv_bin.is_file then
|
||||||
|
mpv_path = "/usr/local/mpv"
|
||||||
|
else
|
||||||
|
local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
|
||||||
|
if mpv_app and mpv_app.is_file then
|
||||||
|
mp.msg.warn("symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||||
|
else
|
||||||
|
mp.msg.warn("drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vf_string(filters, full)
|
||||||
|
local vf = ""
|
||||||
|
local vf_table = properties["vf"]
|
||||||
|
|
||||||
|
if vf_table and #vf_table > 0 then
|
||||||
|
for i = #vf_table, 1, -1 do
|
||||||
|
if filters[vf_table[i].name] then
|
||||||
|
local args = ""
|
||||||
|
for key, value in pairs(vf_table[i].params) do
|
||||||
|
if args ~= "" then
|
||||||
|
args = args .. ":"
|
||||||
|
end
|
||||||
|
args = args .. key .. "=" .. value
|
||||||
|
end
|
||||||
|
vf = vf .. vf_table[i].name .. "=" .. args .. ","
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if full then
|
||||||
|
vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=-1:y=-1,format=bgra"
|
||||||
|
end
|
||||||
|
|
||||||
|
return vf
|
||||||
|
end
|
||||||
|
|
||||||
|
local function calc_dimensions()
|
||||||
|
local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
|
||||||
|
local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
|
||||||
|
if not width or not height then return end
|
||||||
|
|
||||||
|
local scale = properties["display-hidpi-scale"] or 1
|
||||||
|
|
||||||
|
if width / height > options.max_width / options.max_height then
|
||||||
|
effective_w = math.floor(options.max_width * scale + 0.5)
|
||||||
|
effective_h = math.floor(height / width * effective_w + 0.5)
|
||||||
|
else
|
||||||
|
effective_h = math.floor(options.max_height * scale + 0.5)
|
||||||
|
effective_w = math.floor(width / height * effective_h + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1
|
||||||
|
if v_par == 1 then
|
||||||
|
par = ":force_original_aspect_ratio=decrease"
|
||||||
|
else
|
||||||
|
par = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local info_timer = nil
|
||||||
|
|
||||||
|
local function info(w, h)
|
||||||
|
local rotate = properties["video-params"] and properties["video-params"]["rotate"]
|
||||||
|
local dovi_p5 = properties["video-params"] and properties["video-params"]["colormatrix"] == "dolbyvision" and properties["video-params"]["colorlevels"] == "full"
|
||||||
|
local image = properties["current-tracks/video"] and properties["current-tracks/video"]["image"]
|
||||||
|
local albumart = image and properties["current-tracks/video"]["albumart"]
|
||||||
|
local cache_state = properties["demuxer-cache-state"]
|
||||||
|
local dir = properties["path"] and mp.utils.split_path(properties["path"])
|
||||||
|
local file_ext = properties["path"] and properties["path"]:match("%.([^%.]+)$")
|
||||||
|
|
||||||
|
if is_windows and dir then dir = dir:gsub("\\", "/") end
|
||||||
|
if cache_state then cached_ranges = cache_state["seekable-ranges"] end
|
||||||
|
|
||||||
|
disabled = (w or 0) == 0 or (h or 0) == 0 or
|
||||||
|
has_vid == 0 or
|
||||||
|
(dir and need_ignore(excluded_dir, dir)) or
|
||||||
|
(file_ext and exclude(file_ext:lower(), ext_blacklist)) or
|
||||||
|
((properties["demuxer-via-network"] or is_protocol(properties["path"]) or (properties["cache"] == "auto" and #cached_ranges > 0)) and not options.network) or
|
||||||
|
(dovi_p5) or
|
||||||
|
(albumart and not options.audio) or
|
||||||
|
(image and not albumart) or
|
||||||
|
force_disabled
|
||||||
|
|
||||||
|
if info_timer then
|
||||||
|
info_timer:kill()
|
||||||
|
info_timer = nil
|
||||||
|
elseif has_vid == 0 or (rotate == nil and not disabled) then
|
||||||
|
info_timer = mp.add_timeout(0.05, function() info(w, h) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local json, err = mp.utils.format_json({width=w, height=h, disabled=disabled, available=true, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||||
|
if pre_0_30_0 then
|
||||||
|
mp.command_native({"script-message", "thumbfast-info", json})
|
||||||
|
else
|
||||||
|
mp.command_native_async({"script-message", "thumbfast-info", json}, function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_thumbnail_files()
|
||||||
|
if file then
|
||||||
|
file:close()
|
||||||
|
file = nil
|
||||||
|
file_bytes = 0
|
||||||
|
end
|
||||||
|
os.remove(options.thumbnail)
|
||||||
|
os.remove(options.thumbnail..".bgra")
|
||||||
|
end
|
||||||
|
|
||||||
|
local activity_timer
|
||||||
|
|
||||||
|
local function spawn(time)
|
||||||
|
if disabled then return end
|
||||||
|
|
||||||
|
local path = properties["path"]
|
||||||
|
if path == nil then return end
|
||||||
|
|
||||||
|
if options.quit_after_inactivity > 0 then
|
||||||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||||||
|
activity_timer:kill()
|
||||||
|
end
|
||||||
|
activity_timer:resume()
|
||||||
|
end
|
||||||
|
|
||||||
|
local open_filename = properties["stream-open-filename"]
|
||||||
|
local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename
|
||||||
|
if ytdl then
|
||||||
|
path = open_filename
|
||||||
|
end
|
||||||
|
|
||||||
|
remove_thumbnail_files()
|
||||||
|
|
||||||
|
local vid = properties["vid"]
|
||||||
|
has_vid = vid or 0
|
||||||
|
|
||||||
|
local args = {
|
||||||
|
mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--ao=null", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal",
|
||||||
|
"--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no",
|
||||||
|
"--load-auto-profiles=no", "--load-osd-console=no", "--load-select=no", "--autoload-files=no",
|
||||||
|
"--edition="..(properties["edition"] or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio",
|
||||||
|
"--start="..time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes",
|
||||||
|
"--gpu-dumb-mode=yes", "--dither-depth=no", "--hdr-compute-peak=no", "--target-colorspace-hint=no",
|
||||||
|
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
|
||||||
|
"--vd-lavc-skiploopfilter=all", "--vd-lavc-skipidct=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2",
|
||||||
|
"--hwdec="..(options.hwdec and "auto" or "no"),
|
||||||
|
"--vf="..vf_string(filters_all, true), "--audio-pitch-correction=no", "--deinterlace=no",
|
||||||
|
"--zimg-scaler=bilinear", "--zimg-fast=yes",
|
||||||
|
"--video-rotate="..last_rotate,
|
||||||
|
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--ocopy-metadata=no", "--o="..options.thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
if os_name == "darwin" and properties["macos-app-activation-policy"] then
|
||||||
|
table.insert(args, "--macos-app-activation-policy=accessory")
|
||||||
|
end
|
||||||
|
|
||||||
|
if os_name == "windows" or pre_0_33_0 then
|
||||||
|
table.insert(args, "--input-ipc-server="..options.socket)
|
||||||
|
local media_controls = mp.get_property_native("media-controls")
|
||||||
|
if media_controls ~= nil then
|
||||||
|
table.insert(args, "--media-controls=no")
|
||||||
|
end
|
||||||
|
elseif not script_written then
|
||||||
|
local client_script_path = options.socket..".run"
|
||||||
|
local script = io.open(client_script_path, "w+")
|
||||||
|
if script == nil then
|
||||||
|
mp.msg.error("client script write failed")
|
||||||
|
return
|
||||||
|
else
|
||||||
|
script_written = true
|
||||||
|
script:write(string.format(client_script, options.socket))
|
||||||
|
script:close()
|
||||||
|
subprocess({"chmod", "+x", client_script_path}, true)
|
||||||
|
table.insert(args, "--scripts="..client_script_path)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local client_script_path = options.socket..".run"
|
||||||
|
table.insert(args, "--scripts="..client_script_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(args, "--")
|
||||||
|
table.insert(args, path)
|
||||||
|
|
||||||
|
spawned = true
|
||||||
|
spawn_waiting = true
|
||||||
|
|
||||||
|
subprocess(args, true,
|
||||||
|
function(success, result)
|
||||||
|
if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then
|
||||||
|
spawned = false
|
||||||
|
spawn_waiting = false
|
||||||
|
mp.msg.error("mpv subprocess create failed")
|
||||||
|
if not spawn_working then -- notify users of required configuration
|
||||||
|
if options.mpv_path == "mpv" then
|
||||||
|
if properties["current-vo"] == "libmpv" then
|
||||||
|
if options.mpv_path == mpv_path then -- attempt to locate ImPlay
|
||||||
|
mpv_path = "ImPlay"
|
||||||
|
spawn(time)
|
||||||
|
else -- ImPlay not in path
|
||||||
|
if os_name ~= "darwin" then
|
||||||
|
force_disabled = true
|
||||||
|
info(real_w or effective_w, real_h or effective_h)
|
||||||
|
end
|
||||||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||||
|
-- found ImPlay but not defined in config
|
||||||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif success == true and (result.status == 0 or result.status == -2) then
|
||||||
|
if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then
|
||||||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||||
|
end
|
||||||
|
spawn_working = true
|
||||||
|
spawn_waiting = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function run(command)
|
||||||
|
if not spawned then return end
|
||||||
|
|
||||||
|
if options.direct_io then
|
||||||
|
local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil)
|
||||||
|
if hPipe ~= winapi.INVALID_HANDLE_VALUE then
|
||||||
|
local buf = command .. "\n"
|
||||||
|
winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
|
||||||
|
winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
|
||||||
|
winapi.C.CloseHandle(hPipe)
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local command_n = command.."\n"
|
||||||
|
|
||||||
|
if os_name == "windows" then
|
||||||
|
if file and file_bytes + #command_n >= 4096 then
|
||||||
|
file:close()
|
||||||
|
file = nil
|
||||||
|
file_bytes = 0
|
||||||
|
end
|
||||||
|
if not file then
|
||||||
|
file = io.open("\\\\.\\pipe\\"..options.socket, "r+b")
|
||||||
|
end
|
||||||
|
elseif pre_0_33_0 then
|
||||||
|
subprocess({"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket})
|
||||||
|
return
|
||||||
|
elseif not file then
|
||||||
|
file = io.open(options.socket, "r+")
|
||||||
|
end
|
||||||
|
if file then
|
||||||
|
file_bytes = file:seek("end")
|
||||||
|
file:write(command_n)
|
||||||
|
file:flush()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw(w, h, script)
|
||||||
|
if not w or not show_thumbnail then return end
|
||||||
|
if x ~= nil then
|
||||||
|
if pre_0_30_0 then
|
||||||
|
mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)})
|
||||||
|
else
|
||||||
|
mp.command_native_async({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)}, function() end)
|
||||||
|
end
|
||||||
|
elseif script then
|
||||||
|
local json, err = mp.utils.format_json({width=w, height=h, x=x, y=y, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||||
|
mp.commandv("script-message-to", script, "thumbfast-render", json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function real_res(req_w, req_h, filesize)
|
||||||
|
local count = filesize / 4
|
||||||
|
local diff = (req_w * req_h) - count
|
||||||
|
|
||||||
|
if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then
|
||||||
|
req_w, req_h = req_h, req_w
|
||||||
|
end
|
||||||
|
|
||||||
|
if diff == 0 then
|
||||||
|
return req_w, req_h
|
||||||
|
else
|
||||||
|
local threshold = 5 -- throw out results that change too much
|
||||||
|
local long_side, short_side = req_w, req_h
|
||||||
|
if req_h > req_w then
|
||||||
|
long_side, short_side = req_h, req_w
|
||||||
|
end
|
||||||
|
for a = short_side, short_side - threshold, -1 do
|
||||||
|
if count % a == 0 then
|
||||||
|
local b = count / a
|
||||||
|
if long_side - b < threshold then
|
||||||
|
if req_h < req_w then return b, a else return a, b end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function move_file(from, to)
|
||||||
|
if os_name == "windows" then
|
||||||
|
os.remove(to)
|
||||||
|
end
|
||||||
|
-- move the file because it can get overwritten while overlay-add is reading it, and crash the player
|
||||||
|
os.rename(from, to)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function seek(fast)
|
||||||
|
if last_seek_time then
|
||||||
|
run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local seek_period = 3/60
|
||||||
|
local seek_period_counter = 0
|
||||||
|
local seek_timer
|
||||||
|
seek_timer = mp.add_periodic_timer(seek_period, function()
|
||||||
|
if seek_period_counter == 0 then
|
||||||
|
seek(allow_fast_seek)
|
||||||
|
seek_period_counter = 1
|
||||||
|
else
|
||||||
|
if seek_period_counter == 2 then
|
||||||
|
if allow_fast_seek then
|
||||||
|
seek_timer:kill()
|
||||||
|
seek()
|
||||||
|
end
|
||||||
|
else seek_period_counter = seek_period_counter + 1 end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
seek_timer:kill()
|
||||||
|
|
||||||
|
local function request_seek()
|
||||||
|
if seek_timer:is_enabled() then
|
||||||
|
seek_period_counter = 0
|
||||||
|
else
|
||||||
|
seek_timer:resume()
|
||||||
|
seek(allow_fast_seek)
|
||||||
|
seek_period_counter = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_new_thumb()
|
||||||
|
-- the slave might start writing to the file after checking existance and
|
||||||
|
-- validity but before actually moving the file, so move to a temporary
|
||||||
|
-- location before validity check to make sure everything stays consistant
|
||||||
|
-- and valid thumbnails don't get overwritten by invalid ones
|
||||||
|
local tmp = options.thumbnail..".tmp"
|
||||||
|
move_file(options.thumbnail, tmp)
|
||||||
|
local finfo = mp.utils.file_info(tmp)
|
||||||
|
if not finfo then return false end
|
||||||
|
spawn_waiting = false
|
||||||
|
local w, h = real_res(effective_w, effective_h, finfo.size)
|
||||||
|
if w then -- only accept valid thumbnails
|
||||||
|
move_file(tmp, options.thumbnail..".bgra")
|
||||||
|
|
||||||
|
real_w, real_h = w, h
|
||||||
|
if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
|
||||||
|
last_real_w, last_real_h = real_w, real_h
|
||||||
|
info(real_w, real_h)
|
||||||
|
end
|
||||||
|
if not show_thumbnail then
|
||||||
|
file_timer:kill()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
file_timer = mp.add_periodic_timer(file_check_period, function()
|
||||||
|
if check_new_thumb() then
|
||||||
|
draw(real_w, real_h, script_name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
file_timer:kill()
|
||||||
|
|
||||||
|
local function clear()
|
||||||
|
file_timer:kill()
|
||||||
|
seek_timer:kill()
|
||||||
|
if options.quit_after_inactivity > 0 then
|
||||||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||||||
|
activity_timer:kill()
|
||||||
|
end
|
||||||
|
activity_timer:resume()
|
||||||
|
end
|
||||||
|
last_seek_time = nil
|
||||||
|
show_thumbnail = false
|
||||||
|
last_x = nil
|
||||||
|
last_y = nil
|
||||||
|
if script_name then return end
|
||||||
|
if pre_0_30_0 then
|
||||||
|
mp.command_native({"overlay-remove", options.overlay_id})
|
||||||
|
else
|
||||||
|
mp.command_native_async({"overlay-remove", options.overlay_id}, function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function quit()
|
||||||
|
activity_timer:kill()
|
||||||
|
if show_thumbnail then
|
||||||
|
activity_timer:resume()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
run("quit")
|
||||||
|
spawned = false
|
||||||
|
real_w, real_h = nil, nil
|
||||||
|
clear()
|
||||||
|
end
|
||||||
|
|
||||||
|
activity_timer = mp.add_timeout(options.quit_after_inactivity, quit)
|
||||||
|
activity_timer:kill()
|
||||||
|
|
||||||
|
local function thumb(time, r_x, r_y, script)
|
||||||
|
if disabled then return end
|
||||||
|
|
||||||
|
time = tonumber(time)
|
||||||
|
if time == nil then return end
|
||||||
|
|
||||||
|
if r_x == "" or r_y == "" then
|
||||||
|
x, y = nil, nil
|
||||||
|
else
|
||||||
|
x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
script_name = script
|
||||||
|
if last_x ~= x or last_y ~= y or not show_thumbnail then
|
||||||
|
show_thumbnail = true
|
||||||
|
last_x = x
|
||||||
|
last_y = y
|
||||||
|
draw(real_w, real_h, script)
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.quit_after_inactivity > 0 then
|
||||||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||||||
|
activity_timer:kill()
|
||||||
|
end
|
||||||
|
activity_timer:resume()
|
||||||
|
end
|
||||||
|
|
||||||
|
if time == last_seek_time then return end
|
||||||
|
last_seek_time = time
|
||||||
|
if not spawned then spawn(time) end
|
||||||
|
request_seek()
|
||||||
|
if not file_timer:is_enabled() then file_timer:resume() end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function watch_changes()
|
||||||
|
if not dirty or not properties["video-out-params"] then return end
|
||||||
|
dirty = false
|
||||||
|
|
||||||
|
local old_w = effective_w
|
||||||
|
local old_h = effective_h
|
||||||
|
|
||||||
|
calc_dimensions()
|
||||||
|
|
||||||
|
local vf_reset = vf_string(filters_reset)
|
||||||
|
local rotate = properties["video-rotate"] or 0
|
||||||
|
|
||||||
|
local resized = old_w ~= effective_w or
|
||||||
|
old_h ~= effective_h or
|
||||||
|
last_vf_reset ~= vf_reset or
|
||||||
|
(last_rotate % 180) ~= (rotate % 180) or
|
||||||
|
par ~= last_par
|
||||||
|
|
||||||
|
if resized then
|
||||||
|
last_rotate = rotate
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
elseif last_has_vid ~= has_vid and has_vid ~= 0 then
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
if spawned then
|
||||||
|
if resized then
|
||||||
|
-- mpv doesn't allow us to change output size
|
||||||
|
local seek_time = last_seek_time
|
||||||
|
run("quit")
|
||||||
|
clear()
|
||||||
|
spawned = false
|
||||||
|
spawn(seek_time or mp.get_property_number("time-pos", 0))
|
||||||
|
file_timer:resume()
|
||||||
|
else
|
||||||
|
if rotate ~= last_rotate then
|
||||||
|
run("set video-rotate "..rotate)
|
||||||
|
end
|
||||||
|
local vf_runtime = vf_string(filters_runtime)
|
||||||
|
if vf_runtime ~= last_vf_runtime then
|
||||||
|
run("vf set "..vf_string(filters_all, true))
|
||||||
|
last_vf_runtime = vf_runtime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
last_vf_runtime = vf_string(filters_runtime)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_vf_reset = vf_reset
|
||||||
|
last_rotate = rotate
|
||||||
|
last_par = par
|
||||||
|
last_has_vid = has_vid
|
||||||
|
|
||||||
|
if not spawned and not disabled and options.spawn_first and resized then
|
||||||
|
spawn(mp.get_property_number("time-pos", 0))
|
||||||
|
file_timer:resume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_property(name, value)
|
||||||
|
properties[name] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_property_dirty(name, value)
|
||||||
|
properties[name] = value
|
||||||
|
dirty = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_tracklist(name, value)
|
||||||
|
-- current-tracks shim
|
||||||
|
for _, track in ipairs(value) do
|
||||||
|
if track.type == "video" and track.selected then
|
||||||
|
properties["current-tracks/video"] = track
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sync_changes(prop, val)
|
||||||
|
update_property(prop, val)
|
||||||
|
if val == nil then return end
|
||||||
|
|
||||||
|
if type(val) == "boolean" then
|
||||||
|
if prop == "vid" then
|
||||||
|
has_vid = 0
|
||||||
|
last_has_vid = 0
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
clear()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
val = val and "yes" or "no"
|
||||||
|
end
|
||||||
|
|
||||||
|
if prop == "vid" then
|
||||||
|
has_vid = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if not spawned then return end
|
||||||
|
|
||||||
|
run("set "..prop.." "..val)
|
||||||
|
dirty = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function file_load()
|
||||||
|
clear()
|
||||||
|
spawned = false
|
||||||
|
real_w, real_h = nil, nil
|
||||||
|
last_real_w, last_real_h = nil, nil
|
||||||
|
last_seek_time = nil
|
||||||
|
if info_timer then
|
||||||
|
info_timer:kill()
|
||||||
|
info_timer = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
calc_dimensions()
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shutdown()
|
||||||
|
run("quit")
|
||||||
|
remove_thumbnail_files()
|
||||||
|
if os_name ~= "windows" then
|
||||||
|
os.remove(options.socket)
|
||||||
|
os.remove(options.socket..".run")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_duration(prop, val)
|
||||||
|
allow_fast_seek = (val or 30) >= 30
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.observe_property("current-tracks/video", "native", function(name, value)
|
||||||
|
if pre_0_33_0 then
|
||||||
|
mp.unobserve_property(update_tracklist)
|
||||||
|
pre_0_33_0 = false
|
||||||
|
end
|
||||||
|
update_property(name, value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.observe_property("track-list", "native", update_tracklist)
|
||||||
|
mp.observe_property("display-hidpi-scale", "native", update_property_dirty)
|
||||||
|
mp.observe_property("video-out-params", "native", update_property_dirty)
|
||||||
|
mp.observe_property("video-params", "native", update_property_dirty)
|
||||||
|
mp.observe_property("vf", "native", update_property_dirty)
|
||||||
|
mp.observe_property("tone-mapping", "native", update_property_dirty)
|
||||||
|
mp.observe_property("cache", "native", update_property)
|
||||||
|
mp.observe_property("demuxer-via-network", "native", update_property)
|
||||||
|
mp.observe_property('demuxer-cache-state', 'native', update_property)
|
||||||
|
mp.observe_property("stream-open-filename", "native", update_property)
|
||||||
|
mp.observe_property("macos-app-activation-policy", "native", update_property)
|
||||||
|
mp.observe_property("current-vo", "native", update_property)
|
||||||
|
mp.observe_property("video-rotate", "native", update_property)
|
||||||
|
mp.observe_property("path", "native", update_property)
|
||||||
|
mp.observe_property("vid", "native", sync_changes)
|
||||||
|
mp.observe_property("edition", "native", sync_changes)
|
||||||
|
mp.observe_property("duration", "native", on_duration)
|
||||||
|
|
||||||
|
mp.register_script_message("thumb", thumb)
|
||||||
|
mp.register_script_message("clear", clear)
|
||||||
|
|
||||||
|
mp.register_event("file-loaded", file_load)
|
||||||
|
mp.register_event("shutdown", shutdown)
|
||||||
|
|
||||||
|
mp.register_idle(watch_changes)
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
-- trackselect.lua
|
||||||
|
-- https://github.com/po5/trackselect
|
||||||
|
-- Because --slang isn't smart enough.
|
||||||
|
--
|
||||||
|
-- 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 utils = require 'mp.utils'
|
||||||
|
|
||||||
|
local defaults = {
|
||||||
|
audio = {
|
||||||
|
selected = nil,
|
||||||
|
best = {},
|
||||||
|
lang_score = nil,
|
||||||
|
channels_score = -math.huge,
|
||||||
|
preferred = "jpn/japanese",
|
||||||
|
excluded = "",
|
||||||
|
expected = "",
|
||||||
|
id = ""
|
||||||
|
},
|
||||||
|
video = {
|
||||||
|
selected = nil,
|
||||||
|
best = {},
|
||||||
|
lang_score = nil,
|
||||||
|
preferred = "",
|
||||||
|
excluded = "",
|
||||||
|
expected = "",
|
||||||
|
id = ""
|
||||||
|
},
|
||||||
|
sub = {
|
||||||
|
selected = nil,
|
||||||
|
best = {},
|
||||||
|
lang_score = nil,
|
||||||
|
preferred = "eng",
|
||||||
|
excluded = "sign",
|
||||||
|
expected = "",
|
||||||
|
id = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
enabled = true,
|
||||||
|
|
||||||
|
-- Do track selection synchronously, plays nicer with other scripts
|
||||||
|
hook = true,
|
||||||
|
|
||||||
|
-- Mimic mpv's track list fingerprint to preserve user-selected tracks across files
|
||||||
|
fingerprint = false,
|
||||||
|
|
||||||
|
-- Override user's explicit track selection
|
||||||
|
force = false,
|
||||||
|
|
||||||
|
-- Try to re-select the last track if mpv cannot do it e.g. when fingerprint changes
|
||||||
|
smart_keep = false,
|
||||||
|
|
||||||
|
--add above (after a comma) any protocol to disable
|
||||||
|
special_protocols = [[
|
||||||
|
["://", "^magnet:"]
|
||||||
|
]],
|
||||||
|
}
|
||||||
|
|
||||||
|
for _type, track in pairs(defaults) do
|
||||||
|
options["preferred_" .. _type .. "_lang"] = track.preferred
|
||||||
|
options["excluded_" .. _type .. "_words"] = track.excluded
|
||||||
|
options["expected_" .. _type .. "_words"] = track.expected
|
||||||
|
end
|
||||||
|
|
||||||
|
options["preferred_audio_channels"] = ""
|
||||||
|
|
||||||
|
local tracks = {}
|
||||||
|
local last = {}
|
||||||
|
local fingerprint = ""
|
||||||
|
|
||||||
|
opt.read_options(options, _, function() end)
|
||||||
|
|
||||||
|
options.special_protocols = utils.parse_json(options.special_protocols)
|
||||||
|
|
||||||
|
local function need_ignore(tab, val)
|
||||||
|
for index, element in ipairs(tab) do
|
||||||
|
if string.find(val, element) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function contains(track, words, attr)
|
||||||
|
if not track[attr] then return false end
|
||||||
|
local i = 0
|
||||||
|
if track.external then
|
||||||
|
i = 1
|
||||||
|
end
|
||||||
|
for word in string.gmatch(words:lower(), "([^/]+)") do
|
||||||
|
i = i - 1
|
||||||
|
if string.find(tostring(track[attr] or ""):lower(), word) then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function preferred(track, words, attr, title)
|
||||||
|
local score = contains(track, words, attr)
|
||||||
|
if not score then
|
||||||
|
if tracks[track.type][title] == nil then
|
||||||
|
tracks[track.type][title] = -math.huge
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if tracks[track.type][title] == nil or score > tracks[track.type][title] then
|
||||||
|
tracks[track.type][title] = score
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function preferred_or_equals(track, words, attr, title)
|
||||||
|
local score = contains(track, words, attr)
|
||||||
|
if not score then
|
||||||
|
if tracks[track.type][title] == nil or tracks[track.type][title] == -math.huge then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if tracks[track.type][title] == nil or score >= tracks[track.type][title] then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function copy(obj)
|
||||||
|
if type(obj) ~= "table" then return obj end
|
||||||
|
local res = {}
|
||||||
|
for k, v in pairs(obj) do res[k] = copy(v) end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function track_layout_hash(tracklist)
|
||||||
|
local t = {}
|
||||||
|
for _, track in ipairs(tracklist) do
|
||||||
|
t[#t + 1] = string.format("%s-%d-%s-%s-%s-%s", track.type, track.id, tostring(track.default),
|
||||||
|
tostring(track.external), track.lang or "", track.external and "" or (track.title or ""))
|
||||||
|
end
|
||||||
|
return table.concat(t, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
function trackselect()
|
||||||
|
local fpath = mp.get_property('path')
|
||||||
|
if not options.enabled then return end
|
||||||
|
if need_ignore(options.special_protocols, fpath) then return end
|
||||||
|
tracks = copy(defaults)
|
||||||
|
local filename = mp.get_property("filename/no-ext")
|
||||||
|
local tracklist = mp.get_property_native("track-list")
|
||||||
|
local tracklist_changed = false
|
||||||
|
local found_last = {}
|
||||||
|
if options.fingerprint then
|
||||||
|
local new_fingerprint = track_layout_hash(tracklist)
|
||||||
|
if new_fingerprint == fingerprint then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
fingerprint = new_fingerprint
|
||||||
|
tracklist_changed = true
|
||||||
|
end
|
||||||
|
for _, track in ipairs(tracklist) do
|
||||||
|
if options.smart_keep and last[track.type] ~= nil and last[track.type].lang == track.lang and
|
||||||
|
last[track.type].codec == track.codec and last[track.type].external == track.external and
|
||||||
|
last[track.type].title == track.title then
|
||||||
|
tracks[track.type].best = track
|
||||||
|
options["preferred_" .. track.type .. "_lang"] = ""
|
||||||
|
options["excluded_" .. track.type .. "_words"] = ""
|
||||||
|
options["expected_" .. track.type .. "_words"] = ""
|
||||||
|
options["preferred_" .. track.type .. "_channels"] = ""
|
||||||
|
found_last[track.type] = true
|
||||||
|
elseif not options.force and (tracklist_changed or not options.fingerprint) then
|
||||||
|
if tracks[track.type].id == "" then
|
||||||
|
tracks[track.type].id = mp.get_property(track.type:sub(1, 1) .. "id", "auto")
|
||||||
|
end
|
||||||
|
if tracks[track.type].id ~= "auto" then
|
||||||
|
options["preferred_" .. track.type .. "_lang"] = ""
|
||||||
|
options["excluded_" .. track.type .. "_words"] = ""
|
||||||
|
options["expected_" .. track.type .. "_words"] = ""
|
||||||
|
options["preferred_" .. track.type .. "_channels"] = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if options["preferred_" .. track.type .. "_lang"] ~= "" or options["excluded_" .. track.type .. "_words"] ~= ""
|
||||||
|
or options["expected_" .. track.type .. "_words"] ~= "" or
|
||||||
|
(options["preferred_" .. track.type .. "_channels"] or "") ~= "" then
|
||||||
|
if track.selected then
|
||||||
|
tracks[track.type].selected = track.id
|
||||||
|
if options.smart_keep then
|
||||||
|
last[track.type] = track
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if track.title then
|
||||||
|
track.title = string.gsub(string.gsub(track.title, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1"), filename, "")
|
||||||
|
end
|
||||||
|
if next(tracks[track.type].best) == nil or not (tracks[track.type].best.external
|
||||||
|
and tracks[track.type].best.lang ~= nil and not track.external) then
|
||||||
|
if options["excluded_" .. track.type .. "_words"] == "" or
|
||||||
|
not contains(track, options["excluded_" .. track.type .. "_words"], "title") then
|
||||||
|
if options["expected_" .. track.type .. "_words"] == "" or
|
||||||
|
contains(track, options["expected_" .. track.type .. "_words"], "title") then
|
||||||
|
local pass = true
|
||||||
|
local channels = false
|
||||||
|
local lang = false
|
||||||
|
if (options["preferred_" .. track.type .. "_channels"] or "") ~= "" and
|
||||||
|
preferred_or_equals(track, options["preferred_" .. track.type .. "_lang"], "lang",
|
||||||
|
"lang_score") then
|
||||||
|
channels = preferred(track, options["preferred_" .. track.type .. "_channels"],
|
||||||
|
"demux-channel-count", "channels_score")
|
||||||
|
pass = channels
|
||||||
|
end
|
||||||
|
if options["preferred_" .. track.type .. "_lang"] ~= "" then
|
||||||
|
lang = preferred(track, options["preferred_" .. track.type .. "_lang"], "lang", "lang_score")
|
||||||
|
end
|
||||||
|
if (options["preferred_" .. track.type .. "_lang"] == "" and pass) or channels or lang or
|
||||||
|
(track.external and track.lang == nil and
|
||||||
|
(not tracks[track.type].best.external or tracks[track.type].best.lang == nil)) then
|
||||||
|
tracks[track.type].best = track
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _type, track in pairs(tracks) do
|
||||||
|
if next(track.best) ~= nil and track.best.id ~= track.selected then
|
||||||
|
mp.set_property(_type:sub(1, 1) .. "id", track.best.id)
|
||||||
|
if options.smart_keep and found_last[track.best.type] then
|
||||||
|
last[track.best.type] = track.best
|
||||||
|
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
|
||||||
|
mp.add_hook("on_preloaded", 50, trackselect)
|
||||||
|
else
|
||||||
|
mp.register_event("file-loaded", trackselect)
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.smart_keep then
|
||||||
|
mp.register_event("track-switched", selected_tracks)
|
||||||
|
end
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
-- Copyright (c) 2021, Eisa AlAwadhi
|
||||||
|
-- License: BSD 2-Clause License
|
||||||
|
|
||||||
|
-- Creator: Eisa AlAwadhi
|
||||||
|
-- Project: UndoRedo
|
||||||
|
-- Version: 2.2
|
||||||
|
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local seconds = 0
|
||||||
|
local countTimer = -1
|
||||||
|
local seekTime = 0
|
||||||
|
local seekNumber = 0
|
||||||
|
local currentIndex = 0
|
||||||
|
local seekTable = {}
|
||||||
|
local seeking = 0
|
||||||
|
local undoRedo = 0
|
||||||
|
local pause = false
|
||||||
|
seekTable[0] = 0
|
||||||
|
|
||||||
|
----------------------------USER CUSTOMIZATION SETTINGS-----------------------------------
|
||||||
|
--These settings are for users to manually change some options in the script.
|
||||||
|
--Keybinds can be defined in the bottom of the script.
|
||||||
|
|
||||||
|
local osd_messages = true --true is for displaying osd messages when actions occur, Change to false will disable all osd messages generated from this script
|
||||||
|
|
||||||
|
---------------------------END OF USER CUSTOMIZATION SETTINGS---------------------
|
||||||
|
|
||||||
|
local function prepareUndoRedo()
|
||||||
|
if (pause == true) then
|
||||||
|
seconds = seconds
|
||||||
|
else
|
||||||
|
seconds = seconds - 0.5
|
||||||
|
end
|
||||||
|
seekTable[currentIndex] = seekTable[currentIndex] + seconds
|
||||||
|
seconds = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getUndoRedo()
|
||||||
|
if (seeking == 0) then
|
||||||
|
prepareUndoRedo()
|
||||||
|
|
||||||
|
seekNumber = currentIndex + 1
|
||||||
|
currentIndex = seekNumber
|
||||||
|
seekTime = math.floor(mp.get_property_number('time-pos'))
|
||||||
|
table.insert(seekTable, seekNumber, seekTime)
|
||||||
|
|
||||||
|
undoRedo = 0
|
||||||
|
|
||||||
|
elseif (seeking == 1) then
|
||||||
|
seeking = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event('file-loaded', function()
|
||||||
|
filePath = mp.get_property('path')
|
||||||
|
|
||||||
|
timer = mp.add_periodic_timer(0.1, function()
|
||||||
|
seconds = seconds + 0.1
|
||||||
|
end)
|
||||||
|
|
||||||
|
if (pause == true) then
|
||||||
|
timer:stop()
|
||||||
|
else
|
||||||
|
timer:resume()
|
||||||
|
end
|
||||||
|
|
||||||
|
timer2 = mp.add_periodic_timer(0.1, function()
|
||||||
|
countTimer = countTimer + 0.1
|
||||||
|
|
||||||
|
if (countTimer == 0.6) then
|
||||||
|
timer:resume()
|
||||||
|
getUndoRedo()
|
||||||
|
end
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
timer2:stop()
|
||||||
|
end)
|
||||||
|
|
||||||
|
|
||||||
|
mp.register_event('seek', function()
|
||||||
|
countTimer = 0
|
||||||
|
timer2:resume()
|
||||||
|
timer:stop()
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.observe_property('pause', 'bool', function(name, value)
|
||||||
|
if value then
|
||||||
|
if timer ~= nil then
|
||||||
|
timer:stop()
|
||||||
|
end
|
||||||
|
pause = true
|
||||||
|
else
|
||||||
|
if timer ~= nil then
|
||||||
|
timer:resume()
|
||||||
|
end
|
||||||
|
pause = false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.register_event('end-file', function()
|
||||||
|
if timer ~= nil then
|
||||||
|
timer:kill()
|
||||||
|
end
|
||||||
|
if timer2 ~= nil then
|
||||||
|
timer2:kill()
|
||||||
|
end
|
||||||
|
seekNumber = 0
|
||||||
|
currentIndex = 0
|
||||||
|
undoRedo = 0
|
||||||
|
seconds = 0
|
||||||
|
countTimer = -1
|
||||||
|
seekTable[0] = 0
|
||||||
|
end)
|
||||||
|
|
||||||
|
local function undo()
|
||||||
|
if (filePath ~= nil) and (countTimer >= 0) and (countTimer < 0.6) and (seeking == 0) then
|
||||||
|
timer2:stop()
|
||||||
|
|
||||||
|
getUndoRedo()
|
||||||
|
|
||||||
|
currentIndex = currentIndex - 1
|
||||||
|
if (currentIndex < 0) then
|
||||||
|
if (osd_messages == true) then
|
||||||
|
mp.osd_message('No Undo Found')
|
||||||
|
end
|
||||||
|
currentIndex = 0
|
||||||
|
msg.info('No undo found')
|
||||||
|
else
|
||||||
|
if (seekTable[currentIndex] < 0) then
|
||||||
|
seekTable[currentIndex] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
seeking = 1
|
||||||
|
|
||||||
|
mp.commandv('seek', seekTable[currentIndex], 'absolute', 'exact')
|
||||||
|
|
||||||
|
undoRedo = 1
|
||||||
|
if (osd_messages == true) then
|
||||||
|
mp.osd_message('Undo')
|
||||||
|
end
|
||||||
|
msg.info('Seeked using undo')
|
||||||
|
end
|
||||||
|
elseif (filePath ~= nil) and (currentIndex > 0) then
|
||||||
|
|
||||||
|
prepareUndoRedo()
|
||||||
|
currentIndex = currentIndex - 1
|
||||||
|
|
||||||
|
if (seekTable[currentIndex] < 0) then
|
||||||
|
seekTable[currentIndex] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
seeking = 1
|
||||||
|
mp.commandv('seek', seekTable[currentIndex], 'absolute', 'exact')
|
||||||
|
|
||||||
|
undoRedo = 1
|
||||||
|
if (osd_messages == true) then
|
||||||
|
mp.osd_message('Undo')
|
||||||
|
end
|
||||||
|
msg.info('Seeked using undo')
|
||||||
|
elseif (filePath ~= nil) and (currentIndex == 0) then
|
||||||
|
if (osd_messages == true) then
|
||||||
|
mp.osd_message('No Undo Found')
|
||||||
|
end
|
||||||
|
msg.info('No undo found')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function redo()
|
||||||
|
if (filePath ~= nil) and (currentIndex < seekNumber) then
|
||||||
|
|
||||||
|
prepareUndoRedo()
|
||||||
|
currentIndex = currentIndex + 1
|
||||||
|
|
||||||
|
if (seekTable[currentIndex] < 0) then
|
||||||
|
seekTable[currentIndex] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
seeking = 1
|
||||||
|
mp.commandv('seek', seekTable[currentIndex], 'absolute', 'exact')
|
||||||
|
|
||||||
|
undoRedo = 0
|
||||||
|
|
||||||
|
if (osd_messages == true) then
|
||||||
|
mp.osd_message('Redo')
|
||||||
|
end
|
||||||
|
msg.info('Seeked using redo')
|
||||||
|
elseif (filePath ~= nil) and (currentIndex == seekNumber) then
|
||||||
|
if (osd_messages == true) then
|
||||||
|
mp.osd_message('No Redo Found')
|
||||||
|
end
|
||||||
|
msg.info('No redo found')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function undoLoop()
|
||||||
|
if (filePath ~= nil) and (undoRedo == 0) then
|
||||||
|
undo()
|
||||||
|
elseif (filePath ~= nil) and (undoRedo == 1) then
|
||||||
|
redo()
|
||||||
|
elseif (filePath ~= nil) and (countTimer == -1) then
|
||||||
|
if (osd_messages == true) then
|
||||||
|
mp.osd_message('No Undo Found')
|
||||||
|
end
|
||||||
|
msg.info('No undo found')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
mp.add_key_binding("ctrl+z", "undo", undo)
|
||||||
|
mp.add_key_binding("ctrl+Z", "undoCaps", undo)
|
||||||
|
|
||||||
|
mp.add_key_binding("ctrl+y", "redo", redo)
|
||||||
|
mp.add_key_binding("ctrl+Y", "redoCaps", redo)
|
||||||
|
|
||||||
|
mp.add_key_binding("ctrl+alt+z", "undoLoop", undoLoop)
|
||||||
|
mp.add_key_binding("ctrl+alt+Z", "undoLoopCaps", undoLoop)
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply,
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
|
If you develop a new library, and you want it to be of the greatest
|
||||||
|
possible use to the public, we recommend making it free software that
|
||||||
|
everyone can redistribute and change. You can do so by permitting
|
||||||
|
redistribution under these terms (or, alternatively, under the terms of the
|
||||||
|
ordinary General Public License).
|
||||||
|
|
||||||
|
To apply these terms, attach the following notices to the library. It is
|
||||||
|
safest to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||||
|
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
||||||
@@ -0,0 +1,545 @@
|
|||||||
|
<div align="center">
|
||||||
|
<h1>uosc</h1>
|
||||||
|
<p>
|
||||||
|
Feature-rich minimalist proximity-based UI for <a href="https://mpv.io">MPV player</a>.
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<a href="https://user-images.githubusercontent.com/47283320/195073006-bfa72bcc-89d2-4dc7-b8dc-f3c13273910c.webm"><img src="https://github.com/tomasklaen/uosc/assets/47283320/9f99f2ae-3b65-4935-8af3-8b80c605f022" alt="Preview screenshot"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- UI elements hide and show based on their proximity to cursor instead of every time mouse moves. This provides 100% control over when you see the UI and when you don't. Click on the preview above to see it in action.
|
||||||
|
- When timeline is unused, it can minimize itself into a small discrete progress bar.
|
||||||
|
- Build your own context menu with nesting support by editing your `input.conf` file.
|
||||||
|
- Configurable controls bar.
|
||||||
|
- Fast and efficient thumbnails with [thumbfast](https://github.com/po5/thumbfast) integration.
|
||||||
|
- UIs for:
|
||||||
|
- Selecting subtitle/audio/video track.
|
||||||
|
- [Downloading subtitles](#download-subtitles) from [Open Subtitles](https://www.opensubtitles.com).
|
||||||
|
- Loading external subtitles.
|
||||||
|
- Selecting stream quality.
|
||||||
|
- Quick directory and playlist navigation.
|
||||||
|
- All menus are instantly searchable. Just start typing.
|
||||||
|
- Mouse scroll wheel does multiple things depending on what is the cursor hovering over:
|
||||||
|
- Timeline: seek by `timeline_step` seconds per scroll.
|
||||||
|
- Volume bar: change volume by `volume_step` per scroll.
|
||||||
|
- Speed bar: change speed by `speed_step` per scroll.
|
||||||
|
- Just hovering video with no UI widget below cursor: your configured wheel bindings from `input.conf`.
|
||||||
|
- Right click on volume or speed elements to reset them.
|
||||||
|
- Transforming chapters into timeline ranges (the red portion of the timeline in the preview).
|
||||||
|
- A lot of useful options and commands to bind keys to.
|
||||||
|
- [API for 3rd party scripts](https://github.com/tomasklaen/uosc/wiki) to extend, or use uosc to render their menus.
|
||||||
|
|
||||||
|
[Changelog](https://github.com/tomasklaen/uosc/releases).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. These commands will install or update **uosc** and place a default `uosc.conf` file into `script-opts` if it doesn't exist already.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
_Optional, needed to run a remote script the first time if not enabled already:_
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
_**NOTE**: If this command is run in an mpv installation directory with `portable_config`, it'll install there instead of `AppData`._
|
||||||
|
|
||||||
|
_**NOTE2**: The downloaded archive might trigger false positives in some antiviruses. This is explained in [FAQ below](#why-is-the-release-reported-as-malicious-by-some-antiviruses)._
|
||||||
|
|
||||||
|
### Linux & macOS
|
||||||
|
|
||||||
|
_Requires **curl** and **unzip**._
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux, we try to detect what package manager variant of the config location you're using, with precedent being:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.var/app/io.mpv.Mpv (flatpak)
|
||||||
|
~/snap/mpv
|
||||||
|
~/snap/mpv-wayland
|
||||||
|
~/.config/mpv
|
||||||
|
```
|
||||||
|
|
||||||
|
To install into any of these locations, make sure the ones above it don't exist.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
1. Download & extract [`uosc.zip`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip) into your mpv config directory. (_See the [documentation of mpv config locations](https://mpv.io/manual/master/#files)._)
|
||||||
|
|
||||||
|
2. If you don't have it already, download & extract [`uosc.conf`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf) into `script-opts` inside your mpv config directory. It contains all of uosc options along with their default values and documentation.
|
||||||
|
|
||||||
|
2. **OPTIONAL**: `mpv.conf` tweaks to better integrate with **uosc**:
|
||||||
|
|
||||||
|
```config
|
||||||
|
# uosc provides seeking & volume indicators (via flash-timeline and flash-volume commands)
|
||||||
|
# if you decide to use them, you don't need osd-bar
|
||||||
|
osd-bar=no
|
||||||
|
|
||||||
|
# uosc will draw its own window controls and border if you disable window border
|
||||||
|
border=no
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **OPTIONAL**: To have thumbnails in timeline, install [thumbfast](https://github.com/po5/thumbfast). No other step necessary, **uosc** integrates with it seamlessly.
|
||||||
|
|
||||||
|
4. **OPTIONAL**: If the UI feels sluggish/slow while playing video, you can remedy this _a bit_ by placing this in your `mpv.conf`:
|
||||||
|
|
||||||
|
```config
|
||||||
|
video-sync=display-resample
|
||||||
|
```
|
||||||
|
|
||||||
|
Though this does come at the cost of a little bit higher CPU/GPU load.
|
||||||
|
|
||||||
|
#### What is going on?
|
||||||
|
|
||||||
|
**uosc** places performance as one of its top priorities, but it might feel a bit sluggish because during a video playback, the UI rendering frequency is chained to its frame rate. To test this, you can pause the video which will switch refresh rate to be closer or match the frequency of your monitor, and the UI should feel smoother. This is mpv limitation, and not much we can do about it on our side.
|
||||||
|
|
||||||
|
#### Build instructions
|
||||||
|
|
||||||
|
To build ziggy (our utility binary) yourself, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/build ziggy
|
||||||
|
```
|
||||||
|
|
||||||
|
Which will run the `tools/build(.ps1)` script that builds it for each platform. It requires [go](https://go.dev/) to be installed. Source code is in `src/ziggy`.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
All of the available **uosc** options with their default values are documented in [`uosc.conf`](https://github.com/tomasklaen/uosc/blob/HEAD/src/uosc.conf) file ([download](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf)).
|
||||||
|
|
||||||
|
To change the font, **uosc** respects the mpv's `osd-font` configuration.
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
These bindings are active when any **uosc** menu is open (main menu, playlist, load/select subtitles,...):
|
||||||
|
|
||||||
|
- `up`, `down` - Select previous/next item.
|
||||||
|
- `enter` - Activate item or submenu.
|
||||||
|
- `bs` (backspace) - Activate parent menu.
|
||||||
|
- `esc` - Close menu.
|
||||||
|
- `wheel_up`, `wheel_down` - Scroll menu.
|
||||||
|
- `pgup`, `pgdwn`, `home`, `end` - Self explanatory.
|
||||||
|
- `ctrl+f` or `\` - In case `menu_type_to_search` config option is disabled, these two trigger the menu search instead.
|
||||||
|
- `ctrl+backspace` - Delete search query by word.
|
||||||
|
- `shift+backspace` - Clear search query.
|
||||||
|
- Holding `alt` while activating an item should prevent closing the menu (this is just a guideline, not all menus behave this way).
|
||||||
|
|
||||||
|
Each menu can also add its own shortcuts and bindings for special actions on items/menu, such as `del` to delete a playlist item, `ctrl+up/down/pgup/pgdwn/home/end` to move it around, etc. These are usually also exposed as item action buttons for you to find out about them that way.
|
||||||
|
|
||||||
|
Click on a faded parent menu to go back to it.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
**uosc** provides various commands with useful features to bind your preferred keys to, or populate your menu with. These are all unbound by default.
|
||||||
|
|
||||||
|
To add a keybind to one of this commands, open your `input.conf` file and add one on a new line. The command syntax is `script-binding uosc/{command-name}`.
|
||||||
|
|
||||||
|
Example to bind the `tab` key to toggle the ui visibility:
|
||||||
|
|
||||||
|
```
|
||||||
|
tab script-binding uosc/toggle-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
Available commands:
|
||||||
|
|
||||||
|
#### `toggle-ui`
|
||||||
|
|
||||||
|
Makes the whole UI visible until you call this command again. Useful for peeking remaining time and such while watching.
|
||||||
|
|
||||||
|
There's also a `toggle-elements <ids>` message you can send to toggle one or more specific elements by specifying their names separated by comma:
|
||||||
|
|
||||||
|
```
|
||||||
|
script-message-to uosc toggle-elements timeline,speed
|
||||||
|
```
|
||||||
|
|
||||||
|
Available element IDs: `timeline`, `controls`, `volume`, `top_bar`, `speed`
|
||||||
|
|
||||||
|
Under the hood, `toggle-ui` is using `toggle-elements`, and that is in turn using the `set-min-visibility <visibility> [<ids>]` message. `<visibility>` is a `0-1` floating point. Leave out `<ids>` to set it for all elements.
|
||||||
|
|
||||||
|
#### `toggle-progress`
|
||||||
|
|
||||||
|
Toggles the timeline progress mode on/off. Progress mode is an always visible thin version of timeline with no text labels. It can be configured using the `progress*` config options.
|
||||||
|
|
||||||
|
#### `toggle-title`
|
||||||
|
|
||||||
|
Toggles the top bar title between main and alternative title's. This can also be done by clicking on the top bar.
|
||||||
|
|
||||||
|
Only relevant if top bar is enabled, `top_bar_alt_title` is configured, and `top_bar_alt_title_place` is `toggle`.
|
||||||
|
|
||||||
|
#### `flash-ui`
|
||||||
|
|
||||||
|
Command(s) to briefly flash the whole UI. Elements are revealed for a second and then fade away.
|
||||||
|
|
||||||
|
To flash individual elements, you can use: `flash-timeline`, `flash-progress`, `flash-top-bar`, `flash-volume`, `flash-speed`, `flash-pause-indicator`, `decide-pause-indicator`
|
||||||
|
|
||||||
|
There's also a `flash-elements <ids>` message you can use to flash one or more specific elements. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
script-message-to uosc flash-elements timeline,speed
|
||||||
|
```
|
||||||
|
|
||||||
|
Available element IDs: `timeline`, `progress`, `controls`, `volume`, `top_bar`, `speed`, `pause_indicator`
|
||||||
|
|
||||||
|
This is useful in combination with other commands that modify values represented by flashed elements, for example: flashing volume element when changing the volume.
|
||||||
|
|
||||||
|
You can use it in your bindings like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
space cycle pause; script-binding uosc/flash-pause-indicator
|
||||||
|
right seek 5
|
||||||
|
left seek -5
|
||||||
|
shift+right seek 30; script-binding uosc/flash-timeline
|
||||||
|
shift+left seek -30; script-binding uosc/flash-timeline
|
||||||
|
m no-osd cycle mute; script-binding uosc/flash-volume
|
||||||
|
up no-osd add volume 10; script-binding uosc/flash-volume
|
||||||
|
down no-osd add volume -10; script-binding uosc/flash-volume
|
||||||
|
[ no-osd add speed -0.25; script-binding uosc/flash-speed
|
||||||
|
] no-osd add speed 0.25; script-binding uosc/flash-speed
|
||||||
|
\ no-osd set speed 1; script-binding uosc/flash-speed
|
||||||
|
> script-binding uosc/next; script-message-to uosc flash-elements top_bar,timeline
|
||||||
|
< script-binding uosc/prev; script-message-to uosc flash-elements top_bar,timeline
|
||||||
|
```
|
||||||
|
|
||||||
|
Case for `(flash/decide)-pause-indicator`: mpv handles frame stepping forward by briefly resuming the video, which causes pause indicator to flash, and none likes that when they are trying to compare frames. The solution is to enable manual pause indicator (`pause_indicator=manual`) and use `flash-pause-indicator` (for a brief flash) or `decide-pause-indicator` (for a static indicator) as a secondary command to appropriate bindings.
|
||||||
|
|
||||||
|
#### `menu`
|
||||||
|
|
||||||
|
Toggles default menu. Read [Menu](#menu-1) section below to find out how to fill it up with items you want there.
|
||||||
|
|
||||||
|
Note: there's also a `menu-blurred` command that opens a menu without pre-selecting the 1st item, suitable for commands triggered with a mouse, such as control bar buttons.
|
||||||
|
|
||||||
|
#### `subtitles`, `audio`, `video`
|
||||||
|
|
||||||
|
Menus to select a track of a requested type.
|
||||||
|
|
||||||
|
#### `load-subtitles`, `load-audio`, `load-video`
|
||||||
|
|
||||||
|
Displays a file explorer with directory navigation to load a requested track type.
|
||||||
|
|
||||||
|
For subtitles, the explorer only displays file types defined in `subtitle_types` option. For audio and video, the ones defined in `video_types` and `audio_types` are displayed.
|
||||||
|
|
||||||
|
#### `download-subtitles`
|
||||||
|
|
||||||
|
A menu to search and download subtitles from [Open Subtitles](https://www.opensubtitles.com). It can also be opened by selecting the **Download** option in `subtitles` menu.
|
||||||
|
|
||||||
|
We fetch results for languages defined in *uosc**'s `languages` option, which defaults to your mpv `slang` configuration.
|
||||||
|
|
||||||
|
We also hash the current file and send the hash to Open Subtitles so you can search even with empty query and if your file is known, you'll get subtitles exactly for it.
|
||||||
|
|
||||||
|
Subtitles will be downloaded to the same directory as currently opened file, or `~~/subtitles` (folder in your mpv config directory) if playing a URL.
|
||||||
|
|
||||||
|
Current Open Subtitles limit for unauthenticated requests is **5 download per day**, but searching is unlimited. Authentication raises downloads to 10, which doesn't feel like it's worth the effort of implementing it, so currently there's no way to authenticate. 5 downloads per day seems sufficient for most use cases anyway, as if you need more, you should probably just deal with it in the browser beforehand so you don't have to fiddle with the subtitle downloading menu every time you start playing a new file.
|
||||||
|
|
||||||
|
#### `playlist`
|
||||||
|
|
||||||
|
Playlist navigation.
|
||||||
|
|
||||||
|
#### `chapters`
|
||||||
|
|
||||||
|
Chapter navigation.
|
||||||
|
|
||||||
|
#### `editions`
|
||||||
|
|
||||||
|
Editions menu. Editions are different video cuts available in some mkv files.
|
||||||
|
|
||||||
|
#### `stream-quality`
|
||||||
|
|
||||||
|
Switch stream quality. This is just a basic re-assignment of `ytdl-format` mpv property from predefined options (configurable with `stream_quality_options`) and video reload, there is no fetching of available formats going on.
|
||||||
|
|
||||||
|
#### `keybinds`
|
||||||
|
|
||||||
|
Displays a command palette menu with all currently active keybindings (defined in your `input.conf` file, or registered by scripts). Useful to check what command is bound to what shortcut, or the other way around.
|
||||||
|
|
||||||
|
#### `open-file`
|
||||||
|
|
||||||
|
Open file menu. Browsing starts in current file directory, or user directory when file not available. The explorer only displays file types defined in the `video_types`, `audio_types`, and `image_types` options.
|
||||||
|
|
||||||
|
You can use `alt+enter` or `alt+click` to load the whole directory in mpv instead of navigating its contents.
|
||||||
|
You can also use `ctrl+enter` or `ctrl+click` to append a file or directory to the playlist.
|
||||||
|
|
||||||
|
#### `items`
|
||||||
|
|
||||||
|
Opens `playlist` menu when playlist exists, or `open-file` menu otherwise.
|
||||||
|
|
||||||
|
#### `next`, `prev`
|
||||||
|
|
||||||
|
Open next/previous item in playlist, or file in current directory when there is no playlist. Enable `loop-playlist` to loop around.
|
||||||
|
|
||||||
|
#### `first`, `last`
|
||||||
|
|
||||||
|
Open first/last item in playlist, or file in current directory when there is no playlist.
|
||||||
|
|
||||||
|
#### `next-file`, `prev-file`
|
||||||
|
|
||||||
|
Open next/prev file in current directory. Enable `loop-playlist` to loop around
|
||||||
|
|
||||||
|
#### `first-file`, `last-file`
|
||||||
|
|
||||||
|
Open first/last file in current directory.
|
||||||
|
|
||||||
|
#### `shuffle`
|
||||||
|
|
||||||
|
Toggle uosc's playlist/directory shuffle mode on or off.
|
||||||
|
|
||||||
|
This simply makes the next selected playlist or directory item be random, like the shuffle function of most other players. This does not modify the actual playlist in any way, in contrast to the mpv built-in command `playlist-shuffle`.
|
||||||
|
|
||||||
|
#### `delete-file-next`
|
||||||
|
|
||||||
|
Delete currently playing file and start next file in playlist (if there is a playlist) or current directory.
|
||||||
|
|
||||||
|
Useful when watching episodic content.
|
||||||
|
|
||||||
|
#### `delete-file-quit`
|
||||||
|
|
||||||
|
Delete currently playing file and quit mpv.
|
||||||
|
|
||||||
|
#### `show-in-directory`
|
||||||
|
|
||||||
|
Show current file in your operating systems' file explorer.
|
||||||
|
|
||||||
|
#### `audio-device`
|
||||||
|
|
||||||
|
Switch audio output device.
|
||||||
|
|
||||||
|
#### `paste`, `paste-to-open`, `paste-to-playlist`
|
||||||
|
|
||||||
|
Commands to paste path or URL in clipboard to either open immediately, or append to playlist.
|
||||||
|
|
||||||
|
`paste` will add to playlist if there's any (`playlist-count > 1`), or open immediately otherwise.
|
||||||
|
|
||||||
|
`paste-to-playlist` will also open the pasted file if mpv is idle (no file open).
|
||||||
|
|
||||||
|
Note: there are alternative ways to open stuff from clipboard without the need to bind these commands:
|
||||||
|
|
||||||
|
- When `open-file` menu is open → `ctrl+v` to open path/URL in clipboard.
|
||||||
|
- When `playlist` menu is open → `ctrl+v` to add path/URL in clipboard to playlist.
|
||||||
|
- When any track menu (`subtitles`, `audio`, `video`) is open → `ctrl+v` to add path/URL in clipboard as a new track.
|
||||||
|
|
||||||
|
#### `copy-to-clipboard`
|
||||||
|
|
||||||
|
Copy currently open path or URL to clipboard.
|
||||||
|
|
||||||
|
Additionally, you can also press `ctrl+c` to copy path of a selected item in `playlist` or all directory listing menus.
|
||||||
|
|
||||||
|
#### `open-config-directory`
|
||||||
|
|
||||||
|
Open directory with `mpv.conf` in file explorer.
|
||||||
|
|
||||||
|
#### `update`
|
||||||
|
|
||||||
|
Updates uosc to the latest stable release right from the UI. Available in the "Utils" section of default menu .
|
||||||
|
|
||||||
|
Supported environments:
|
||||||
|
|
||||||
|
| Env | Works | Note |
|
||||||
|
|:---|:---:|---|
|
||||||
|
| Windows | ✔️ | _Not tested on older PowerShell versions. You might need to `Set-ExecutionPolicy` from the install instructions and install with the terminal command first._ |
|
||||||
|
| Linux (apt) | ✔️ | |
|
||||||
|
| Linux (flatpak) | ✔️ | |
|
||||||
|
| Linux (snap) | ❌ | We're not allowed to access commands like `curl` even if they're installed. (Or at least this is what I think the issue is.) |
|
||||||
|
| MacOS | ❌ | `(23) Failed writing body` error, whatever that means. |
|
||||||
|
|
||||||
|
If you know about a solution to fix self-updater for any of the currently broken environments, please make an issue/PR and share it with us!
|
||||||
|
|
||||||
|
**Note:** The terminal commands from install instructions still work fine everywhere, so you can use those to update instead.
|
||||||
|
|
||||||
|
## Menu
|
||||||
|
|
||||||
|
**uosc** provides a way to build, display, and use your own menu. By default it displays a pre-configured menu with common actions.
|
||||||
|
|
||||||
|
To display the menu, add **uosc**'s `menu` command to a key of your choice. Example to bind it to **right click** and **menu** buttons:
|
||||||
|
|
||||||
|
```
|
||||||
|
mbtn_right script-binding uosc/menu
|
||||||
|
menu script-binding uosc/menu
|
||||||
|
```
|
||||||
|
|
||||||
|
To display a submenu, send a `show-submenu` message to **uosc** with first parameter specifying menu ID. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
R script-message-to uosc show-submenu "Utils > Aspect ratio"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The **menu** key is the one nobody uses between the **win** and **right_ctrl** keys (it might not be on your keyboard).
|
||||||
|
|
||||||
|
### Adding items to menu
|
||||||
|
|
||||||
|
Adding items to menu is facilitated by commenting your keybinds in `input.conf` with special comment syntax. **uosc** will than parse this file and build the context menu out of it.
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
|
||||||
|
Comment has to be at the end of the line with the binding.
|
||||||
|
|
||||||
|
Comment has to start with `#!` (or `#menu:`).
|
||||||
|
|
||||||
|
Text after `#!` is an item title.
|
||||||
|
|
||||||
|
Title can be split with `>` to define nested menus. There is no limit on nesting.
|
||||||
|
|
||||||
|
Use `#` instead of a key if you don't necessarily want to bind a key to a command, but still want it in the menu.
|
||||||
|
|
||||||
|
If multiple menu items with the same command are defined, **uosc** will concatenate them into one item and just display all available shortcuts as that items' hint, while using the title of the first defined item.
|
||||||
|
|
||||||
|
Menu items are displayed in the order they are defined in `input.conf` file.
|
||||||
|
|
||||||
|
The command `ignore` does not result in a menu item, however all the folders leading up to it will still be created.
|
||||||
|
This allows more flexible structuring of the `input.conf` file.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
Adds a menu item to load subtitles:
|
||||||
|
|
||||||
|
```
|
||||||
|
alt+s script-binding uosc/load-subtitles #! Load subtitles
|
||||||
|
```
|
||||||
|
|
||||||
|
Adds a stay-on-top toggle with no keybind:
|
||||||
|
|
||||||
|
```
|
||||||
|
# cycle ontop #! Toggle on-top
|
||||||
|
```
|
||||||
|
|
||||||
|
Define and display multiple shortcuts in single items' menu hint (items with same command get concatenated):
|
||||||
|
|
||||||
|
```
|
||||||
|
esc quit #! Quit
|
||||||
|
q quit #!
|
||||||
|
```
|
||||||
|
|
||||||
|
Define a folder without defining any of its contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
# ignore #! Folder title >
|
||||||
|
```
|
||||||
|
|
||||||
|
Define an un-selectable, muted, and italic title item by using `#` as key, and omitting the command:
|
||||||
|
|
||||||
|
```
|
||||||
|
# #! Title
|
||||||
|
# #! Section > Title
|
||||||
|
```
|
||||||
|
|
||||||
|
Define a separator between previous and next items by doing the same, but using `---` as title:
|
||||||
|
|
||||||
|
```
|
||||||
|
# #! ---
|
||||||
|
# #! Section > ---
|
||||||
|
```
|
||||||
|
|
||||||
|
Example context menu:
|
||||||
|
|
||||||
|
This is the default pre-configured menu if none is defined in your `input.conf`, but with added shortcuts. To both pause & move the window with left mouse button, so that you can have the menu on the right one, enable `click_threshold` in `uosc.conf` (see default `uosc.conf` for example/docs).
|
||||||
|
|
||||||
|
```
|
||||||
|
menu script-binding uosc/menu
|
||||||
|
mbtn_right script-binding uosc/menu
|
||||||
|
s script-binding uosc/subtitles #! Subtitles
|
||||||
|
a script-binding uosc/audio #! Audio tracks
|
||||||
|
q script-binding uosc/stream-quality #! Stream quality
|
||||||
|
p script-binding uosc/items #! Playlist
|
||||||
|
c script-binding uosc/chapters #! Chapters
|
||||||
|
> script-binding uosc/next #! Navigation > Next
|
||||||
|
< script-binding uosc/prev #! Navigation > Prev
|
||||||
|
alt+> script-binding uosc/delete-file-next #! Navigation > Delete file & Next
|
||||||
|
alt+< script-binding uosc/delete-file-prev #! Navigation > Delete file & Prev
|
||||||
|
alt+esc script-binding uosc/delete-file-quit #! Navigation > Delete file & Quit
|
||||||
|
o script-binding uosc/open-file #! Navigation > Open file
|
||||||
|
# set video-aspect-override "-1" #! Utils > Aspect ratio > Default
|
||||||
|
# set video-aspect-override "16:9" #! Utils > Aspect ratio > 16:9
|
||||||
|
# set video-aspect-override "4:3" #! Utils > Aspect ratio > 4:3
|
||||||
|
# set video-aspect-override "2.35:1" #! Utils > Aspect ratio > 2.35:1
|
||||||
|
# script-binding uosc/audio-device #! Utils > Audio devices
|
||||||
|
# script-binding uosc/editions #! Utils > Editions
|
||||||
|
ctrl+s async screenshot #! Utils > Screenshot
|
||||||
|
alt+i script-binding uosc/keybinds #! Utils > Key bindings
|
||||||
|
O script-binding uosc/show-in-directory #! Utils > Show in directory
|
||||||
|
# script-binding uosc/open-config-directory #! Utils > Open config directory
|
||||||
|
# script-binding uosc/update #! Utils > Update uosc
|
||||||
|
esc quit #! Quit
|
||||||
|
```
|
||||||
|
|
||||||
|
To see all the commands you can bind keys or menu items to, refer to [mpv's list of input commands documentation](https://mpv.io/manual/master/#list-of-input-commands).
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
**uosc** listens on some messages that can be sent with `script-message-to uosc` command. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
R script-message-to uosc show-submenu "Utils > Aspect ratio"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `show-submenu <menu_id>`, `show-submenu-blurred <menu_id>`
|
||||||
|
|
||||||
|
Opens one of the submenus defined in `input.conf` (read on how to build those in the Menu documentation above). To prevent 1st item being preselected, use `show-submenu-blurred` instead.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
|
||||||
|
##### `<menu_id>`
|
||||||
|
|
||||||
|
ID (title) of the submenu, including `>` subsections as defined in `input.conf`. It has to be match the title exactly.
|
||||||
|
|
||||||
|
## Scripting API
|
||||||
|
|
||||||
|
3rd party script developers can use our messaging API to integrate with uosc, or use it to render their menus. Documentation is available in [uosc Wiki](https://github.com/tomasklaen/uosc/wiki).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
If you want to help localizing uosc by either adding a new locale or fixing one that is not up to date, start by running this while in the repository root:
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/intl languagecode
|
||||||
|
```
|
||||||
|
|
||||||
|
`languagecode` can be any existing locale in `src/uosc/intl/` directory, or any [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag). If it doesn't exist yet, the `intl` tool will create it.
|
||||||
|
|
||||||
|
This will parse the codebase for localization strings and use them to either update existing locale by removing unused and setting untranslated strings to `null`, or create a new one with all `null` strings.
|
||||||
|
|
||||||
|
You can then navigate to `src/uosc/intl/languagecode.json` and start translating.
|
||||||
|
|
||||||
|
### Setting up binaries
|
||||||
|
|
||||||
|
If you want to test or work on something that involves ziggy (our multitool binary, currently handles searching & downloading subtitles), you first need to build it with:
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/build ziggy
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires [`go`](https://go.dev/dl/) to be installed and in path. If you don't want to bother with installing go, and there were no changes to ziggy, you can just use the binaries from [latest release](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip). Place folder `scripts/uosc/bin` from `uosc.zip` into `src/uosc/bin`.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
#### Why is the release zip size in megabytes? Isn't this just a lua script?
|
||||||
|
|
||||||
|
We are limited in what we can do in mpv's lua scripting environment. To work around this, we include a binary tool (one for each platform), that we call to handle stuff we can't do in lua. Currently this means searching & downloading subtitles, accessing clipboard data, and in future might improve self updating, and potentially other things.
|
||||||
|
|
||||||
|
Other scripts usually choose to go the route of adding python scripts and requiring users to install the runtime. I don't like this as I want the installation process to be as seamless and as painless as possible. I also don't want to contribute to potential python version mismatch issues, because one tool depends on 2.7, other latest 3, and this one 3.9 only and no newer (real world scenario that happened to me), now have fun reconciling this. Depending on external runtimes can be a mess, and shipping a stable, tiny, and fast binary that users don't even have to know about is imo more preferable than having unstable external dependencies and additional installation steps that force everyone to install and manage hundreds of megabytes big runtimes in global `PATH`.
|
||||||
|
|
||||||
|
#### Why don't you have `uosc-{platform}.zip` releases and only include binaries for the concerned platform in each?
|
||||||
|
|
||||||
|
Then you wouldn't be able to sync your mpv config between platforms and everything _just work_.
|
||||||
|
|
||||||
|
#### Why is the release reported as malicious by some antiviruses?
|
||||||
|
|
||||||
|
Some antiviruses find our binaries suspicious due to the way go packages them. This is a known issue with all go binaries (https://go.dev/doc/faq#virus). I think the only way to solve that would be to sign them (not 100% sure though), but I'm not paying to work on free stuff. If anyone is bothered by this, and would be willing to donate a code signing certificate, let me know.
|
||||||
|
|
||||||
|
If you want to check the binaries are safe, the code is in `src/ziggy`, and you can build them yourself by running `tools/build ziggy` in the repository root.
|
||||||
|
|
||||||
|
We might eventually rewrite it in something else.
|
||||||
|
|
||||||
|
#### Why _uosc_?
|
||||||
|
|
||||||
|
It stood for micro osc as it used to render just a couple rectangles before it grew to what it is today. And now it means a minimalist UI design direction where everything is out of your way until needed.
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
{
|
||||||
|
"a": "阿啊呵腌嗄锕錒",
|
||||||
|
"ai": "爱唉挨碍哀矮埃哎艾癌隘蔼嗳皑霭捱暧瑷娭砹锿嫒薆䔽㤅鴱㗨藹㕌磑礙硋䑂愛壒㘷叆靉䨠毐塧靄璦㱯䶣瞹䀳濭溰溾曖昹啀噯嘊㗒㝶䝽敱敳賹懓懝㢊銰鑀鱫鎄皧皚馤躷䅬㿄凒娾嬡伌㑸僾餲䬵譪譺",
|
||||||
|
"an": "安按案暗岸氨俺铵胺鞍黯庵桉谙鮟鹌咹犴广厂埯揞菴蓭荌萻葊隌㸩䮗㱘鵪豻貋腤雸堓垵䎨玵䁆洝晻唵啽㽢罯㟁屵䯥峖鞌䅁錌銨䅖馣痷鶕闇媕㜝盫儑侒䬓偣韽盦䎏䜙諳誝",
|
||||||
|
"ang": "昂肮盎䩕䒢䭹䭺䀚昻㦹㼜骯岇醠㭿枊䍩仰",
|
||||||
|
"ao": "傲熬凹遨嗷奥拗澳袄懊坳敖翱螯鳌鏖岙媪鏊骜艹聱獒廒蔜芺隞隩䮯厫磝䦋奡镺䐿䞝垇㘬墺㘭璈嗸嶅䫨㥿驁鰲鷔䵅摮嫯鼇謷䁱滶澚㕭軪䯠㟼㠂㠗岰慠㤇爊襖䥝獓翶擙抝䚫梎柪翺嶴䴈奧㿰㜜媼㜩㑃謸䜒",
|
||||||
|
"ba": "把八吧巴爸罢拔霸坝叭芭扒跋疤靶耙粑笆钯伯茇菝灞岜鲅捌魃䩻䩗夿䳊䃻㔜胈鼥壩垻豝玐㶚蚆㖠跁䟦哵罷軷㞎炦鈀鲃䥯䰾鮁䳁䱝魞釟抜㧊扷覇柭欛朳叐矲䆉羓䇑丷妭颰癹仈弝詙",
|
||||||
|
"bai": "白百摆败拜柏呗掰伯稗捭佰薭䒔㼣㓦瓸㗗㗑贁㠔䢙敗韛粨庍粺䙓襬猈拝㼟擺䳆挀㿟㧳栢䴽竡絔",
|
||||||
|
"ban": "办般半班板伴版搬斑扮颁瓣拌扳绊坂阪舨瘢柈钣癍靽䕰㚘瓪湴昄蝂岅肦怑粄䉽魬鈑鉡㩯㸞秚褩螌辦㪵闆辬姅頒鳻攽䬳絆斒",
|
||||||
|
"bang": "帮邦膀棒傍榜绑梆磅蚌旁镑谤浜蒡䧛䂜幚邫垹鞤幇幫䎧塝玤蜯䖫䟺髈䰷鎊挷捠搒㯁㭋㮄棓牓稖艕㔙㾦綁縍謗彭",
|
||||||
|
"bao": "保报包胞宝暴抱薄剥炮爆饱堡孢豹瀑刨鲍苞雹葆曝褒鸨褓煲龅趵勹蕔菢藵犦髱䨌䨔㙅靌報堢㙸虣珤㻄齙㵡㿺曓㫧蚫骲鳵怉䎂寳寚寶䴐宲窇䥤鑤㲒鮑䳈鉋铇勽䤖枹䈏㲏忁笣䪨闁媬儤賲䳰佨飹飽䭋駂鴇剝緥袌裦襃",
|
||||||
|
"bei": "被北备背倍悲贝杯辈臂卑碑呗狈惫钡悖孛蓓焙陂碚褙勃鞴鐾庳鹎邶䔒鞁藣苝犕㸬㸽牬盃愂䎬䎱琲㰆珼䁅輩䩀㻗㶔昁蛽䠙唄䡶㽡郥骳貝㤳糒禙鋇䰽狽㔨鉳㼎鵯揹柸桮梖椑㸢㓈㾱鄁軰㛝僃備憊㷶偹俻偝㣁䋳誖",
|
||||||
|
"ben": "本奔笨苯贲坌夯畚锛奙逩坋㱵渀漰泍㤓㡷錛撪㨧捹桳㮺翉楍栟犇倴䬱",
|
||||||
|
"beng": "崩蹦绷甭迸泵嘣甏菶䩬鞛奟䳞埲䨻塴埄琫㱶琣嗙嵭㷯䙀祊鏰镚甮揼逬䭰痭閍伻㑟綳䋽繃絣蚌",
|
||||||
|
"bi": "比必笔毕避壁秘闭鼻币彼逼辟臂泌碧弊蔽鄙毙弼痹庇陛璧婢敝匕俾裨荸吡哔蓖贲襞铋秕毖愎髀篦睥畀妣筚薜萆芘荜滗濞跸嬖狴箅舭鞸䩛蓽㳼萞苾䕗䎵聛䧗驆駜䮡夶髲䭮觱㗉皕䏢腷毴貏䏶賁堛䟆㙄㘩㻫豍珌㱸㻶㹃睤䁹䀣湢滭幤㵥獘斃鄨幣鷩潷䨆沘㡀畢鷝㪤䖩螕蜌啚蹕䟤躃䠋嗶咇罼奰㘠貱䯗畁㡙㠲贔赑怶愊韠䪐躄繴㵨鼊怭屄邲煏熚廦䊧粃襅袐襣禆䃾鲾鏎鐴鉍鰏鮅獙鎞㧙魓㪏柀楅䣥㯇㮿柲榌㮰䵄朼梐䫁篳馝䇷箆筆䄶閉閇㓖閟痺䦘疕疪㚰妼鵖嬶佊偪朇佖䬛䫾饆飶㢰䋔㢶弻彃縪鄪䌟綼㿫毞坒粊㢸䘡詖诐佛拂",
|
||||||
|
"bian": "变便边遍编辩辨鞭扁贬辫蝙匾卞鳊汴砭弁苄碥忭煸褊窆笾缏䒪鞕䛒藊萹䪻鴘㺹玣㻞䁵㳎覍汳㝸㴜昪䡢峅貶惼炞糄鯾鯿獱猵鍽㣐抃揙㭓牑邉邊釆籩艑䉸箯徧稨閞㵷㦚辡辧辮辯緶編㲢甂変諚變",
|
||||||
|
"biao": "表标彪裱婊飚飙镖膘鳔俵骠镳飑髟瘭䔸藨蔈骉驫飆猋嫑磦脿爂臕墂㯱滮淲瀌贆幖㟽㠒飊熛褾錶鑣鏢㧼摽㯹標檦穮儦飇颩颮颷飈諘謤䞄",
|
||||||
|
"bie": "别憋瘪鳖蹩虌莂䏟蟞鱉龞彆鼈蛂䠥別襒䋢徶䭱䉲癟㿜㢼",
|
||||||
|
"bin": "宾滨斌彬濒缤鬓槟殡摈膑邠玢份频髌豳镔傧髩鬢鬂臏䐔霦豩璸瑸殯頻虨瀕濱濵汃髕賓顮䚔賔鑌擯檳梹椕儐繽",
|
||||||
|
"bing": "并病兵冰饼丙柄炳秉禀屏槟摒邴鞆鞞苪䓑陃靐垪眪昞昺蛃䗒怲庰寎窉鈵鮩鋲鉼掤抦㨀棅栤䴵幷䈂並竝偋倂併仒傡餠餅仌䋑氷稟誁",
|
||||||
|
"bo": "波伯播剥博玻勃拨柏脖卜搏泊驳膊舶簿渤簸菠箔跛薄钵铂僰帛礴饽钹亳啵檗鹁踣擘䪇葧萡蘗蔔㹀䂍䮂䮀駁駮驋礡盋䰊䫊㝿砵䶈肑胉䑈䞳郣鵓㪍䢌㱟碆浡㴾溊淿謈㬧㬍䗚䟛蹳嚗㗘㖕䯋髉髆㟑嶓懪孹糪愽㶿煿袹襮袯䙏襏鑮䥬鈸鋍鲌馎鮊鱍鉑鎛镈鉢狛猼瓟瓝㩭挬㩧撥欂桲秡䢪缽簙牔䭯馛馞䒄艊䍨䪬䍸癷侼癶仢僠䭦䬪餑餺紴䊿袰譒佛",
|
||||||
|
"bu": "不部步布补捕卜哺埔怖簿埠钚卟逋晡钸醭瓿䪁荹蔀䏽㘵埗㙛㻉歨歩堡䴝鳪䀯㳍吥咘踄轐峬䝵䪔悑庯䊇廍補鈈鈽錻鸔抪捗㨐柨鵏䴺䍌䒈䑰篰㾟勏郶䳝㚴佈䬏餔餢䋠誧",
|
||||||
|
"ca": "擦嚓礤遪礸䟃䵽攃",
|
||||||
|
"cai": "才采材财彩菜裁猜蔡踩睬䰂毝䐆埰䞗啋跴財㥒寀採棌䴭䣋㒲婇倸偲䌨綵䌽纔縩",
|
||||||
|
"can": "参残餐惨蚕灿掺惭璨孱粲骖黪薒䣟朁蠶叄參㕘叅驂䏼蝅蠺䗞䘉殘㱚㻮㣓䝳㛑澯湌㘔喰㽩黲慙䳻㨻㥇憯慘慚㦧燦爘䙁䗝㺑穇䅟䑶㿊䍼飡䫮㜗嬠傪儏䬫謲䛹",
|
||||||
|
"cang": "藏苍仓舱沧臧伧蒼㶓㵴濸滄螥嵢賶鑶獊欌艙䅮凔仺鸧傖倉鶬䢢",
|
||||||
|
"cao": "草操曹槽糙嘈漕螬艚蓸䏆艸騲䐬鼜䎭曺鄵嶆愺慅慒懆褿襙䄚鏪撡㯥肏䒑㜖",
|
||||||
|
"ce": "策测侧册厕栅恻拆䔴荝萴萗蓛厠䜺測畟冊㥽夨惻憡廁粣䊂㨲拺㩍敇筴䇲䈟笧筞簎箣側",
|
||||||
|
"cen": "参岑涔䃡䨙埁㻸嵾䯔㞥䲋䤁䅾篸笒",
|
||||||
|
"ceng": "层曾蹭噌驓䁬㬝嶒層䉕㣒竲",
|
||||||
|
"cha": "差察查茶插叉诧岔刹喳茬嚓楂杈碴汊搽衩姹槎馇镲锸猹檫靫䕓䓭䒲䰈䑘垞䶪䁟嗏䟕蹅嵖㣾㤞㢒㢎㢉䆛銟䲦䤩鑔鍤扠剎挿揷査臿䊬秅䑡艖䡨疀奼侘偛餷詧紁㪯詫㛳㫅",
|
||||||
|
"chai": "差柴拆钗豺侪虿瘥茝芆䓱蠆䘍袃肞㼮祡㳗囆喍釵犲㾹儕㑪訍",
|
||||||
|
"chan": "产颤阐缠禅铲掺潺馋蝉搀蟾忏谄孱谗巉廛羼崭蒇骣觇澶躔冁婵单剗蕆䩶韂䵐苂䧯㹌䣑硟䐮䑎壥㙻㶣㙴刬䀡覘㢟䂁湹瀍瀺潹㵌灛滻浐㬄蟐蟬螹旵䠨囅丳嚵䡲磛䡪幝幨辿嵼懴䪜㦃懺煘鄽㢆燀裧襜䥀酁劖毚䤫䱿鑱镵㹽鋋鋓獑㺥鏟摻摲攙摌醦䤘䊲棎欃梴㯆䴼㸥艬簅闡閳産剷嬋儳饞儃緾繟纏纒產譂顫諂䜛讒誗讇斺",
|
||||||
|
"chang": "长常场厂唱昌肠偿尝倡畅倘敞淌猖怅嫦娼氅菖昶徜鲳惝苌鬯阊伥萇䩨䕋長䯴镸瓺兏厰腸膓㙊場鼚塲瑺瑒琩玚仧淐甞嘗㦂䗅暢㫤䠆䠀嚐畼悵韔廠焻裮鋹鲿錩锠鱨鯧椙閶倀仩僘償誯裳",
|
||||||
|
"chao": "朝超潮吵巢抄嘲剿炒钞绰晁焯耖怊焣㷅䏚䎐眧巣漅鼌鼂罺轈巐䬤煼㶤窲窼觘鈔䰫樔麨牊䄻鄛欩仯仦弨謿訬",
|
||||||
|
"che": "车彻撤扯澈掣尺屮砗坼莗㱌䧪聅䨁硨䞣䰩頙迠瞮䁤㵔蛼㬚唓車㥉爡烢䚢撦㨋硩㿭㯙䑲䒆勶徹㾝偖伡俥㔭䋲䛸䜠",
|
||||||
|
"chen": "称陈沉晨臣尘趁衬辰嗔琛抻伧谶碜宸郴谌忱龀榇茞蔯莀䢻莐薼䒞陳螴敶磣䣅㲀㫳䢈敐硶䫖夦䢅䐜墋趂霃齓齔瞋㴴鷐迧曟踸䟢趻㕴嚫軙贂賝䞋愖煁麎塵襯䆣鍖鈂䤟捵栕樄桭梣棽櫬䚘䑣瘎疢㽸㧱儭諶䜟謓訦諃讖沈",
|
||||||
|
"cheng": "成程称城承诚呈乘惩撑澄秤橙逞丞骋盛瞠铛塍柽埕琤净抢蛏裎铖酲枨荿䔲䧕阷㞼騁䮪騬郕䫆㼩碀脭爯頳䞓赪赬塖堘珹靗珵睈䀕䁎洆浾泟澂㲂牚瀓溗蟶晿䗊畻峸憆悜憕庱宬窚竀䆑䆵䆸䄇鋮鐣鏿鯎掁摚撐挰㨃揨檉棖檙橕棦朾乗筬稱罉穪䇸徎懲娍偁侱㐼饓僜絾緽誠椉",
|
||||||
|
"chi": "吃持池尺赤迟斥齿翅驰耻痴弛炽哧侈嗤叱敕啻饬笞踟柢呎茌褫鸱勅墀蚩蚩豉眵螭魑匙篪瘛媸傺荎䠠㔑䔟䧝妛恥欼馳䮻䮈肔胵腟胣䐤趩赿䞾灻垑漦雴鵄䜵䜻彨彲銐殦䶔齝齒歯瞝懘㳏湁蚇蚳喫噄䟷翤叺㽚㞿㞴貾㟂恜翄㓾遲翨杘遅遟憏迡烾㢁㶴粚㡿㢋熾裭䙙袳鴟㱀䤲鍉卶鉹㺈鉓瓻䰡抶摛攡㮛鶒慗鷘遫㓼麶勑䳵竾䑛䇪箎筂䈕䇼黐䪧痸癡侙䶵伬䬜飭㒆㘜䊼絺㢮訵㙜䜄謘袲誺䛂",
|
||||||
|
"chong": "重充冲虫崇涌宠憧忡舂铳种茺艟隀憃埫珫沖漴浺㳘蟲蝩蹖嘃罿㓽翀爞崈寵褈銃摏揰㧤䳯䖝衝㹐緟䌬",
|
||||||
|
"chou": "抽筹仇丑愁臭酬畴瞅绸稠踌惆帱瘳俦雠䓓薵菗䔏遚魗㦞㐜殠矁㵞躊吜疇幬㤽懤燽䊭裯䲖鮘㿧㩅皗搊㨶梼檮醻酧醜椆杽栦籌䇺臰篘䪮嬦㛶丒儔䀺偢犨讐雔雦犫讎䌧綢紬䌷絒詶",
|
||||||
|
"chu": "出处除初础助楚触畜储厨锄橱雏躇矗搐刍蜍怵滁黜绌杵蹰亍憷樗楮蒢蒭䢺㔘欪䧁䮞犓㕑㕏礎貙臅㙇埱趎耡䎤㼥䎝豠豖珿䜴璴齣齭齼敊䖏處泏濋㶆滀蟵䟞䠧躕䟣嘼㗰歜㡡幮岀㤕㤘廚䊰䙘䙕禇鶵芻雛鋤鉏㐥觸㹼摴斶櫉櫥䠂椘檚榋篨䅳処䦌竐竌閦媰俶儊儲傗絀諔鄐",
|
||||||
|
"chuai": "揣啜踹膪搋膗㪜㪓䦤䦟䦷",
|
||||||
|
"chuan": "传船穿川串喘椽氚钏舛遄舡巛荈堾玔瑏㱛䁣汌暷踳圌輲歂㼷賗釧猭㯌篅舩僢傳剶鶨",
|
||||||
|
"chuang": "创床窗闯疮怆磢䃥䚎刱䎫㵂噇䡴㡖愴窓窻摐牎牕䇬剙剏闖䚒牀瘡刅傸䭚創幢",
|
||||||
|
"chui": "吹垂锤椎炊捶槌陲棰菙㝽腄䞼䶴㓃錘鎚搥桘㩾䳠䍋埀䄲箠㥨龡倕顀",
|
||||||
|
"chun": "春纯唇醇蠢淳椿莼鹑蝽䔚䓐萶萅蓴蒓陙㸪犉脣䫃惷䐏䏝䐇䏛旾瑃睶㵮浱漘滣湻暙㖺輴賰䞐䄝䥎鰆鯙錞㿤鶞槆杶䣩䣨醕櫄橁箺䦮媋偆純㝄鶉㝇",
|
||||||
|
"chuo": "戳绰辍龊啜淖踔辶䓎歠䮕磭䃗辵趠繛齪逴涰嚽踀哾輟惙䆯鑡㚟㲋擉酫䂐䄪䍳䇍婥娖娕餟䋘綽",
|
||||||
|
"ci": "此次差词刺磁辞雌慈兹瓷赐伺疵呲糍祠茨鹚䓧㹂茦莿薋䦻㤵辝䰍䯸䂣礠㓨辭辤蛓趀䨏珁玼刾䧳㘹䖪㠿鮆鴜䳄飺泚濨蠀䗹螆跐㘂骴髊賜䛐㞖䲿㡹庛㢀皉㩞朿柌栨䆅䈘齹垐䳐餈鶿鷀甆嬨佌偨佽䭣縒絘詞",
|
||||||
|
"cong": "从匆丛聪葱囱淙熜琮苁骢璁枞藂䕺茐蔥蓯孮聦聰聡騘驄瑽瞛潈潀灇潨漗漎蟌暰䟲賩悰愡憁爜叢賨錝鍯怱鏓鏦欉樷樬樅棇徔悤囪徖䉘篵従從䳷㼻婃忩繱誴謥",
|
||||||
|
"cou": "凑腠辏楱湊㫶輳",
|
||||||
|
"cu": "促粗簇醋卒蹙猝蹴徂趣趋蔟殂䓚觕㗤顣䃚䢐脨趗鼀䠞踧踿䠓噈怚䎌憱麤䙯䥄麁䥘䟟㰗橻瘄瘯媨麄䬨縬蹵䛤誎",
|
||||||
|
"cuan": "窜篡攒蹿撺爨镩汆䰖㸑殩㵀躥㠝巑熶竄䆘鑹攛櫕欑㭫簒穳",
|
||||||
|
"cui": "催翠脆粹崔摧萃悴瘁璀啐淬毳榱䃀磪䂱膵膬䄟㯔臎脃脺趡墔琗㧘㱖㵏漼濢㳃啛嵟慛㥞忰翆㷃䊫粋㷪焠㝮襊竁鏙皠㯜槯䧽䆊凗疩伜倅紣縗缞綷顇衰",
|
||||||
|
"cun": "存村寸忖皴吋刌壿邨膥澊踆籿拵䍎竴侟",
|
||||||
|
"cuo": "错措挫搓撮磋锉蹉矬厝脞鹾鹾嵯痤蔖剒逪莡蒫莝遳蓌䂳䐣瑳䣜虘鹺睉䠡䟶㽨嵳㟇錯䱜鎈銼醝䴾酂酇㿷剉夎",
|
||||||
|
"da": "大打达答搭瘩嗒哒鞑沓耷惮靼跶褡怛笪妲荙韃䩢薘剳荅䃮迖羍迏䐛䐊垯墶㙮逹達溚蟽噠迚呾咑䵣䳴眔㟷燵炟匒鎝鐽鎉撘㯚笚䑽龖龘㾑㜓㿯畣繨詚亣畗",
|
||||||
|
"dai": "代带待袋戴呆贷逮歹岱傣玳怠黛殆迨甙棣呔诒埭毒大绐帶䒫貣㞭黱叇霴靆瑇帯㻖瀻蝳㫹曃蚮蹛跢軩軑轪軚獃懛廗襶䚟䚞鴏㯂簤艜䈆㿃垈帒貸柋㐲侢㶡紿緿",
|
||||||
|
"dan": "但单担弹蛋淡胆丹旦氮诞耽郸掸惮疸眈赕澹啖箪膻石萏聃殚瘅儋蓞䩥匰耼聸馾駳髧砃䃫㽎腅膽䨢霮䨵玬殫頕㴷単泹㵅鴠㫜啿㗖鄲單噉㕪啗嘾唌嚪黮黕黵帎賧贉刐饏疍憚憺㡺瓭沊㱽褝襌衴窞禫甔觛䱋狚㺗撣㲷抌擔撢酖柦䄷䉞蜑簞䉷躭癉癚媅妉僤伔䭛餤弾彈紞繵訑勯亶㔊誕",
|
||||||
|
"dang": "当党荡挡档裆铛宕噹菪砀凼谠蘯蕩礑碭䑗雼圵趤壋垱璫珰瞊澢灙盪璗䣊䣣當黨瓽潒逿蟷嵣氹愓襠鐺擋攩檔欓簜簹筜艡䦒闣㜭婸儅譡讜",
|
||||||
|
"dao": "到道导倒岛刀蹈稻盗捣叨悼祷焘氘捯纛刂忉菿陦隯﨩隝䧂䲽壔翿燾瓙盜螩翢嶹嶌嶋禱禂鱽島㠀魛釖擣搗椡槝檤朷稲軇艔衜舠衟㿒導噵䆃辺䌦",
|
||||||
|
"de": "的地得德嘚底锝㤫悳惪㥁䙸䙷淂㝵㥀鍀㯖棏徳恴",
|
||||||
|
"dei": "得嘚",
|
||||||
|
"deng": "等灯登邓瞪凳澄蹬噔磴戥嶝镫簦䒭隥䮴墱璒䠬燈鐙櫈艠竳嬁鄧㲪覴豋",
|
||||||
|
"di": "的地第提低底敌帝弟抵递滴迪堤蒂缔笛涤狄嘀谛娣嫡邸诋砥棣碲柢睇骶荻觌坻氐镝籴羝蔕䩘鞮靮䩚蔋苖菧慸遰菂苐蔐藡隄聜阺墬埅䮤馰牴㹍髢䯼磾厎奃䂡腣坘䞶趆覿䨤埞墑䶍豴玓珶眱䴞䀿坔滌螮蝃㼵䗖蝭旳踶䟡蹢嚁呧唙啲䵠軧䍕頔嶳埊廸岻怟鸐䊮䣌㡳焍袛祶禘鉪㪆䢑釱觝䏑鯳䱃䱱鏑摕逓遞掋拞䀸梊杕枤㭽梑樀楴㰅㣙彽秪䑭䑯糴䨀媂僀仾俤偙弤㢩締詆啇敵甋遆諦翟",
|
||||||
|
"dia": "嗲",
|
||||||
|
"dian": "点电典店淀颠殿垫奠甸碘佃滇惦巅癫掂踮玷靛钿癜阽坫簟蒧蕇䓦䧃驔厧磹㼭䟍顛㒹電墊琔齻奌敁㓠澱㵤㶘蜔蹎跕嚸㸃點敟巔嵮巓壂㞟㥆㝪鈿攧槙椣橂槇䍄癲瘨㚲婰婝傎顚扂",
|
||||||
|
"diao": "掉雕吊钓刁叼调碉凋貂鲷屌铞铫藋䔙蓧䂽奝䂪鼦雿琱㪕瞗汈蛁虭䵲彫鵰䘟窎窵鋽銱錭鑃鯛魡鮉銚釣㹿鈟扚䠼簓䉆竨瘹刟鳭㒛伄弔盄弴調訋",
|
||||||
|
"die": "爹跌叠蝶迭碟谍喋牒堞蹀垤耋鲽瓞㦶戜苵㲲䴑䮢镻胅䏲臷趃䞕耊褺䠟䲀䞇㻡殜眰眣蜨曡㬪螲㫼昳哋咥跮疂氎疊疉畳嵽峌幉㥈惵㦅恎㷸褋䘭㲳鰈䳀挕㩹㩸楪㭯鴩艓牃㑙絰绖諜詄佚",
|
||||||
|
"ding": "定顶丁订钉盯叮鼎锭啶腚仃町铤酊疔碇耵玎靪薡萣艼聢䦺矴磸碠鼑濎㴿㫀蝊虰帄嵿忊顁㝎鐤饤錠釘頂㼗㐉椗奵飣訂",
|
||||||
|
"diu": "丢铥丟銩",
|
||||||
|
"dong": "动东冬洞懂冻董咚栋侗峒恫胴氡鸫硐胨垌岽菄苳蕫駧䂢腖霘鼕䞒埬涷湩蝀昸㖦㗢戙迵㢥崬崠鯟鮗挏氭㨂東㼯鶇鶫棟動徚䅍箽笗㐑䳉䵔㓊凍䍶嬞姛㜱娻㑈倲働諌",
|
||||||
|
"dou": "都斗读豆抖兜陡逗窦蚪痘渎吋蔸篼钭䕱荳䕆阧脰郖毭㪷㐙鬦鬪鬥鬬鬭浢唗唞吺斣㞳㢄㷆竇䄈饾鈄㨮兠梪酘橷枓乧闘閗㛒餖䬦䛠",
|
||||||
|
"du": "度独读毒督渡杜肚堵赌嘟笃睹妒都镀竺犊渎牍蠹黩阇芏髑椟靯韇䪅匵䓯荰犢㸿騳䮷䀾䐗皾䢱蠧䲧覩剢瓄琽㱩殰殬裻錖瀆涜䟻黷䫳賭厾韣韥䙱䄍鑟鍍獨贕櫝醏螙篤牘䅊秺䈞凟闍㾄妬嬻豄讀讟読",
|
||||||
|
"duan": "断段短端锻缎煅椴簖葮碫腶塅㱭瑖躖䠪耑褍鍴鍛毈籪媏偳緞斷㫁",
|
||||||
|
"dui": "对队堆兑碓敦追怼镦憝䔪薱隊陮磓䨺䨴垖塠㙂㳔㵽濧瀩㬣轛䯟㠚㟋憞䊚對懟祋鐓䇤頧鴭痽䇏兊兌䬈䬽綐鐜対譵譈",
|
||||||
|
"dun": "盾顿吨蹲敦钝墩囤沌遁盹炖趸惇砘礅躉驐犜碷遯㬿逇頓潡蜳噸踲蹾㥫庉燉鈍䤜獤撴伅墪撉",
|
||||||
|
"duo": "多夺朵躲踱度堕惰哆舵跺垛咄掇铎剁哚柁裰缍䩔䩣䒳墮陏陊刴朶敠毲剟鵽敪鬌奲尮奪䐾垜㙍趓㙐埵㻧㻔畓㖼跥䠤喥嚉崜憜墯㥩剫䙃䙟䙤䤻鐸饳鈬䫂䤪挅㧷挆柮桗椯㔍軃躱䅜䑨㣞敚凙䍴痥㛆夛㛊敓飿綞嚲亸䯬隋",
|
||||||
|
"e": "额恶俄饿呃鹅扼厄蛾娥峨愕鳄鄂遏萼腭颚讹噩谔婀锷垩轭屙阿咹鹗苊莪锇䩹䳬䓊㼢蕚䔾䕏阨鵈娿阸騀頋阏砐砈㕎礘磀硆砨㼂妿䞩堨堮迗䝈豟堊蝁惡琧悪䫷㱦珴齶歺睋湂涐蚅歞噁卾㓵顎咢鶚遌覨㗁䣞遻㖾吪呝軛囮軶岋㡋崿㟧㠋㟯峉峩㦍㷈廅額頞䆓䄉鈪匎㔩鑩鍔䱮鰪鱷鰐䳗䳘魤鋨鈋擜搹㩵㼰皒搤㧖枙櫮㮙䙳齃頟䖸鵝鵞䑥䑪閼妸姶僫偔餓餩譌讍䛖諤戹誐訛哦",
|
||||||
|
"ei": "诶欸",
|
||||||
|
"en": "嗯恩摁蒽奀峎䊐煾䅰䭡䭓䬶",
|
||||||
|
"er": "而二儿尔耳饵迩洱贰鲕珥鸸铒佴荋貳弍薾聏陑毦隭刵䎶駬䮘髶髵耏鴯䏪胹兒趰弐貮邇爾児洏咡㖇唲輀轜峏粫袻鉺鮞㧫樲栮㮕栭䣵尓衈㛅䎟㜨㚷䎠㒃侕尒餌䋙䌺㢽䋩誀",
|
||||||
|
"fa": "发法乏罚伐阀筏砝珐垡䒥藅茷蕟髪髮䂲坺㘺墢琺沷㳒灋浌㕹罸罰峜彂鍅瞂䣹栰橃笩䇅冹疺閥㛲姂佱発發傠",
|
||||||
|
"fan": "反范饭犯翻繁凡泛番烦返贩帆藩梵樊蕃矾幡钒畈璠蘩燔蹯匥薠䒦㝃軬䮳颿䭵膰䐪墦䪤凣䀟㴀䀀氾滼瀿盕汎噃㕨輽䡊轓軓㠶販䪛㤆憣忛煩籵畨䊩襎㼝鱕㸋鐇㺕釩払䣲礬蠜䫶鐢棥橎柉杋笲䉊笵籓範勫飜鷭䉒舧舤凢瀪緐䌓㶗䋣㽹羳嬎㜶嬏奿仮飯飰繙䋦䛀旙旛訉拚",
|
||||||
|
"fang": "方放房防仿访芳纺妨肪坊彷舫鲂钫匚枋邡㯐牥䦈髣眆淓汸昘昉蚄趽㕫㤃錺魴䲱鈁㧍堏㑂倣鶭紡瓬䢍鴋旊訪",
|
||||||
|
"fei": "非飞费肥废肺匪菲沸啡妃吠斐翡诽绯蜚扉霏腓痱悱芾榧狒淝鲱镄镄篚萉蕜䕁䕠陫騑騛䰁厞朏蜰䑔鼣胇靅奜猆靟䩁剕㐟䨽棐婓餥渄濷㵒蟦暃昲曊䠊胐㥱屝飛飝䨾廃廢裶䚨䤵鯡鐨㩌杮㭭櫠䈈馡䆏䉬癈疿婔俷緋㔗費誹䛍",
|
||||||
|
"fen": "分份奋粉纷愤氛芬粪坟焚吩酚忿汾雰玢鼢瀵鲼棼偾蕡䩿棻蒶隫㸮奮膹朌鼖䴅墳豮豶瞓濆昐蚡㖹轒幩帉岎憤翂燌黺糞黂㥹衯鐼鱝魵獖鈖㮥橨梤燓㷊枌馩馚躮秎羵㿎朆竕羒妢僨弅餴饙蚠炃紛䯨訜",
|
||||||
|
"feng": "风封丰锋峰奉凤缝蜂冯逢疯讽枫沣烽俸砜葑唪酆䒠䩼飌蘴碸䏎堼犎霻靊堸鴌琒盽湗灃溄浲漨㵯沨渢䟪鄷豐崶㡝賵赗峯㦀焨煈寷䙜鎽鋒鏠猦摓檒桻覂楓麷夆蠭㷭篈艂馮瘋妦仹凮凨凬鳳僼鳯風偑綘縫諷",
|
||||||
|
"fo": "佛坲梻仏",
|
||||||
|
"fou": "否不缶鴀䳕雬殕缹缻妚紑",
|
||||||
|
"fu": "复服夫富府父负副福妇附符付幅伏浮腐腹傅扶辐肤抚覆辅赋赴甫缚弗咐俯俘孵拂斧敷脯腑袱芙氟孚蝠阜匐麸釜涪馥凫驸茯讣蝮蚨苻呋罘稃芾跗拊茀趺伕鄜莩菔莩阝砩郛滏蜉呒幞赙赙怫黻黼祓鳆鲋桴绂艴绋荂芣葍䕎䓛䔰萯荴蕧䧞䮛駙䭸䯱㬼䯽髴砆䩉㕊䂤㚕鵩胕䨗䞜䞯䞸䞞韨䘄㙏䨱垘坿䝾邞琈豧玞畐㽬鶝鬴巿玸鳺䫍膚虙㐢㜑澓洑泭㳇㫙蝜蜅蚹䗄蚥哹踾䟔䟮嘸㕮咈罦輻畉䡍䍖輔輹㟊賦帗賻㠅岪翇㤱䪙韍㤔烰粰糐焤炥冨䘠袚褔衭襆複袝襥䃽禣祔鍢鈇頫負鰒鳧鮲鮒鮄鍑鳬鉜鉘䎅捬撫郙棴尃酜枎盙乶椨榑椱覄栿柎麬麩麱柫旉懯箙筟㓡䫝甶䠵䘀蛗峊鴔簠秿復稪艀䒇䒀䑧䵗彿笰乀竎㵗癁䦣㾈娐妋嬔婏媍婦䵾怤姇釡俛偩俌颫紱綒綍䋹䌿刜㪄縛䌗緮䋨絥弣紨紼諨訃㚆詂佛",
|
||||||
|
"ga": "嘎伽尬噶旮咖夹尕尜钆嘠錷釓魀玍",
|
||||||
|
"gai": "改该概盖钙溉芥丐垓赅戤陔葢蓋荄䏗瓂豥㕢䀭漑晐畡乢峐賅䪱忋祴鈣匃匄㧉摡槩槪㮣姟侅絠絯郂㱾賌該",
|
||||||
|
"gan": "感赶敢甘杆干肝乾柑竿赣尴苷秆橄坩擀绀酐泔玕灨旰矸澉淦疳䔈芉皯䃭尷尲趕幹榦倝迀鳱䲺攼尶盰澸漧㽏汵䵟骭䯎忓粓衦鳡鱤㺂魐檊桿䇞簳稈筸贑䤗贛凎仠凲紺詌",
|
||||||
|
"gang": "刚钢港纲岗杠缸冈扛肛戆罡筻犅牨矼堽堈䴚㽘㟵崗㟠剛岡焵焹釭䚗鎠鋼摃㧏掆槓㭎棡罁疘冮戅戇綱",
|
||||||
|
"gao": "高告搞稿膏糕羔镐篙睾皋诰槁藁锆杲缟槔郜菒䔌藳㚏夰䗣鼛櫜峼韟祮祰禞鋯鎬鷎㚖皐槹橰檺勂吿臯鷱筶㾸餻縞髙槀稾稁誥",
|
||||||
|
"ge": "个合各革格歌哥隔割葛阁戈胳颌鸽搁咯疙蛤骼铬膈嗝镉圪鬲硌盖哿塥虼袼搿舸䪂䩐鞈䕻戓㦴茖呄䧄牫騔㷴䐙肐䨣䘁䪺䫦臵鞷㵧滆滒䗘蛒㗆嗰轕輵㠷愅韚韐裓㝓䆟觡鎘亇饹鴚鮯鎶獦鉻犵匌挌㨰擱槅戨㢦櫊䈓㪾敋箇笴閣鴿䢔個佫佮彁諽䛋䛿謌",
|
||||||
|
"gei": "给給",
|
||||||
|
"gen": "根跟亘艮茛哏亙㫔揯搄㮓䫀",
|
||||||
|
"geng": "更耕耿庚梗哽埂羹赓颈鲠绠莄菮堩刯郠浭畊骾峺焿鹒賡鶊䱍䱎鯁䱭䱴挭椩㾘羮絚綆䌄緪縆䋁",
|
||||||
|
"gong": "工公共功供攻宫贡巩弓恭拱躬龚汞蚣珙肱红廾觥龷慐貢㔶䢼拲㭟䂬鞏䡗㧬㼦碽厷髸塨䢚㺬㫒唝嗊輁幊愩㤨熕宮觵匔匑栱㯯杛篢躳䇨㓋龏龔侊糼糿",
|
||||||
|
"gou": "构够句购狗沟勾钩拘苟垢篝枸媾佝诟笱岣鞲遘觏彀缑冓覯芶䃓豿撀㜌㝅㨌坸耇耉耈玽溝㳶蚼㗕啂㽛購䝭䞀韝煹㝤褠袧雊鈎鉤夠㺃搆構簼䑦痀姤緱訽詬",
|
||||||
|
"gu": "古故固顾姑骨鼓股谷孤估雇咕呱辜菇沽锢贾钴梏臌箍蛄汩蛊轱诂牯崮鸪鹘瞽痼鲴毂菰牿嘏罟觚酤巭薣盬㠬䓢蓇苽巬㠫夃㚉䜼䮩尳鴣㼋䀇脵皷鼔堌㯏䅽皼榖穀糓轂䍍䐨䶜䀦䵻䀰濲瀔淈泒蠱啒唃唂軲䡩䍛罛軱鶻崓愲祻鈷錮馉鮕鯝鈲䀜㧽扢橭棝榾柧杚箛稒笟篐㒴㽽凅㾶羖嫴傦餶逧僱䊺縎詁顧",
|
||||||
|
"gua": "挂瓜寡刮褂呱卦剐胍鸹括栝诖䒷劀騧趏坬颪啩踻叧罣冎剮歄㒷煱掛桰鴰䈑颳絓緺詿",
|
||||||
|
"guai": "怪拐乖䂯㽇罫恠叏夬㷇㧔柺枴箉䊽",
|
||||||
|
"guan": "关观管官惯馆贯冠灌罐棺斡倌纶矜盥莞鳏鹳掼涫䩪䪀鸛觀雚蒄覌礶瓘璭琯矔卝泴㴦潅丱䗆䗰躀輨䏓䎚悺慣爟㮡悹䙮䘾䙛窤祼鑵鳤鱹鱞鰥䲘錧鏆摜欟樌罆観筦䦎癏瘝痯関關闗舘館䌯遦貫毌䝺",
|
||||||
|
"guang": "光广逛胱犷潢咣桄茪黆炗垙珖洸㫛炚輄臦臩廣烡広灮炛銧獷姯僙俇",
|
||||||
|
"gui": "规贵归鬼桂轨柜硅龟跪瑰闺诡傀匮圭刽桧鲑癸皈炅鳜珪匦眭晷刿庋宄簋妫茥鞼匭蓕蘬㔳陒雟㸵騩䰎厬胿䝿㙺攰邽㪈郌䳏䞨垝昋鬹規槼嫢璝鬶椝瓌劌瞡瞆瞶䁛氿湀㲹蟡蛫螝貴䠩軌䯣䞈巂嶲恑庪廆袿䙆襘祪禬鑎䣀㩻觤亀鐀鱖鮭䲅鱥䤥猤摫撌㨳㧪櫃槻樻槶椢櫷檜筀歸龜䇈攱閨䍷䍯癐䐴嬀姽媯劊佹䌆詭帰",
|
||||||
|
"gun": "滚棍辊衮磙丨鲧绲蓘蔉䎾䃂㙥㯻睔滾䵪輥惃鯀鮌袞緄緷㫎䜇謴",
|
||||||
|
"guo": "国过果郭锅裹蝈埚帼聒虢椁腘粿掴蜾崞猓馘菓蔮聝䂸㞅䆐腂膕䐸堝墎㳀㶁淉漍濄蟈褁㖪㕵嘓啯㗻國囯輠囻囶圀幗過惈慖䙨鈛鍋鐹馃㚍懖摑楇䴹槨簂瘑䤋䬎餜彉綶彍涡",
|
||||||
|
"ha": "哈蛤虾铪鉿紦",
|
||||||
|
"hai": "还海孩害嗨亥骇咳氦嗐骸胲醢㜾駴駭㦟塰咍䯐㤥烸䱺㺔㨟㧡酼䠽䠹䇋妎饚餀",
|
||||||
|
"han": "含汉喊寒汗旱韩函涵罕憾焊憨翰撼邯悍捍酣瀚鼾蚶颔晗菡犴旰顸焓厂邗撖䕿䓿㽉䓍蔊莟顄凾圅馯駻厈䫲丆䏷䶃䐄爳䨡䖔㙳頇㙈垾韓㲦螒鶾䮧雗㙔䎯䧲琀䁔睅甝㵄漢涆澏浫㵎浛暵蜬虷㪋晘蜭蛿㘕㖤哻㘚㘎唅輚䍐崡嵅屽䍑㟏㟔熯㶰㸁䗙䘶䤴䥁釬銲魽鋎猂㺖鋡㨔扞皔㮀梒䈄馠筨兯閈闬㽳嫨㜦娢傼佄㒈㑵谽豃頷㼨䌍㢨䛞譀",
|
||||||
|
"hang": "行航杭巷夯沆吭绗颃苀垳䀪蚢䣈䟘貥㤚裄䴂魧筕笐䘕䦳絎斻頏迒䲳",
|
||||||
|
"hao": "好号毫耗豪浩郝壕嚎皓镐蒿嗥濠昊貉薅颢灏蚝嚆薃䒵茠薧聕䧚䧫䝞毜㬶䝥㘪淏㵆灝澔滈昦㬔暤暭晧曍䯫顥暠蠔㙱䪽號㕺噑哠嘷㞻㠙乚悎鰝獆獔獋皞皡皥皜㩝椃秏籇竓恏㚪侴䬉䜰傐儫㝀䚽鄗譹皋",
|
||||||
|
"he": "和合何河呵核喝荷吓贺赫盒颌褐鹤禾嗬壑诃涸阂阖劾貉龢翮菏盖盍曷纥蠚鞨䕣萂䒩䓼㹇䃒碋礉盇賀䶅貈䞦䚂㷤靏靎垎靍鸖齕㕡龁澕渮㵑䳚㬞螛毼㔠鹖㓭䫘鶡㕰嚇啝咊㗿哬嗃䵱䢗峆䳽㥺䪚㦦翯煂熆爀焃㷎籺粭熇燺袔寉鶴鑉釛鲄饸魺狢鉌皬㿣抲㭱㪃㰤㮫楁覈柇㭘㮝麧䴳篕䎋惒盉䅂闔癋閤閡姀郃敆頜㪉欱餄紇鶮訶訸詥謞苛",
|
||||||
|
"hei": "黑嘿嗨潶黒",
|
||||||
|
"hen": "很恨狠痕鞎䓳拫㯊佷詪",
|
||||||
|
"heng": "衡横恒哼亨珩鸻蘅桁㔰䒛胻脝㶇涥啈䯒恆悙烆䄓鑅撗橫鴴鵆姮䬖䬝",
|
||||||
|
"hong": "红洪宏轰鸿哄虹烘弘泓竑訇讧闳薨蕻荭黉鞃䩑葓䲨葒苰䧆耾硔翃䫺硡䃔䂫㬴黌垬霟霐䞑䨎玒沗玜䀧鬨澒鴻汯渱潂浤渹晎叿吰呍嚝㖓䍔䡌軣轟輷䡏屸羾灴䉺㶹粠焢翝䆖宖銾鉷鈜魟鋐鍧撔揈篊閧闀閎䪦竤闂妅娂仜䫹谾䜫谹谼紅紘纮㢬彋綋紭訌",
|
||||||
|
"hou": "后候厚猴侯喉吼逅篌齁骺堠鲎糇後瘊茩葔䂉㸸㕈鱟䞧豞睺洉㫗㬋䗔㗋㖃吽帿翵㤧翭䙈矦鲘䪷鮜鯸䳧銗犼㺅鍭郈垕㮢鄇䫛餱",
|
||||||
|
"hu": "互乎护呼户忽胡湖虎糊弧狐壶沪蝴葫瑚浒惚唬扈琥瓠囫鹄唿斛祜滹鄠鹕醐猢和许核觳虍轷岵怙煳烀鹱槲笏冱戽䩴芐萀㸦蔛匢匫䔯苸蔰䕶㕆鬍鶘鶦䭌綔瓳㪶䎁怘䮸膴䞱豰壺嗀縠㺉螜壷垀雽䨥䨼戸䁫虖歑虝雐鍙瀫沍淴汻䲵泘滬滸䗂昒昈㗅䠒嘑嘝嚛喖䍓軤幠恗䪝䊀䉿焀熩粐㝬寣隺鍸䚛鳠錿鱯鸌鰗魱鯱曶㫚㹱乕摢抇搰㿥䰧㨭楜㯛枑槴箶衚頶鵠䧼䇘戶䈸䉉乯簄㾰頀媩嫮嫭婟俿䬍餬䭍䭅弖絗護謼帍鳸㦿䛎戏",
|
||||||
|
"hua": "化话花划画华滑哗桦猾铧骅砉華鷨蕐黊蘤㭉䔢蒊驊硴夻磆䏦埖㓰䶤澅螖嘩㕦䠉㕷㕲呚㠏崋㟆㦊㦎糀鏵錵觟釫釪鋘䱻㚌撶摦搳㩇樺椛槬㮯枠杹䅿舙嬅婲畵畫劃婳姡嫿繣譁誮諣諙䛡話譮豁",
|
||||||
|
"huai": "坏怀淮槐徊踝蘾蘹䃶壊耲壞䴜瀤咶㠢懐懷櫰䈭㜳褱褢",
|
||||||
|
"huan": "还环换欢缓患幻唤焕寰桓痪宦涣豢獾浣奂洹圜鬟鹮垸萑漶逭锾鲩擐缳荁萈酄歡藧㿪㕕驩䭴䮝㹖貛䝠貆肒堚豲瓛環瑍雈睆䀨䀓澣澴㶎㵹渙㬊㬇㼫嚾喛喚還轘嵈䯘峘鴅懽㦥愌㡲糫煥䴟鵍寏䆠鍰䥧鐶镮奐烉鰀鯶鯇獂狟犿攌換梙槵㣪䈠歓䍺闤阛羦䦡瘓㓉孉嬛緩絙繯綄讙㪱",
|
||||||
|
"huang": "黄皇荒慌晃煌惶簧谎恍蝗磺凰隍幌徨潢璜湟肓篁蟥遑鳇癀䪄黃鷬葟㞷䮲騜奛䐵㬻䐠䑟墴塃趪䞹堭瑝䁜兤滉曂晄喤㡆崲䍿愰怳㤺熿䊣熀炾䊗宺鐄鎤鱑鰉鍠锽獚皝皩䳨㿠㨪揘榥櫎楻穔䅣艎韹㾠㾮媓偟餭䌙縨謊朚巟㠵衁諻詤",
|
||||||
|
"hui": "会回挥灰汇绘恢辉毁慧惠悔溃徽讳卉秽贿晦诙彗晖蛔桧诲喙洄荟珲蕙烩茴睢迴麾咴隳恚虺蟪缋蘳蔧薉匯㰥䕇藱薈隓䜐䧥芔䃣㥣靧䩈㩓毀毇䏨噕璤恵豗㱱㻅璯睳顪翽瞺頮颒滙湏洃泋潓輝濊瀈蛕㬩暳蚘蜖暉嚖嘒噅䫭囬廽逥圚廻㞧屷賄囘翙屶懳㤬憓恛翚翬烠烣燬㷐㷄煇燴寭袆䙡䙌褘禈鏸鐬䤧灳鮰獩㨤㩨㨹拻撝揮櫘槥檓橞檅楎篲䂕穢鰴幑䇻䅏徻闠阓痐瘣㜇彚媈嬒婎㒑僡會㑹佪儶餯㑰繢彙絵繪譿詼譭䛼譓䜋䛛諱詯誨堕",
|
||||||
|
"hun": "婚混昏魂浑棍荤馄珲诨溷阍葷蔒䧰鼲䰟琿殙睴睧尡渾涽䫟圂慁轋䡣昬睯忶㥵惛焝觨䚠掍㨡棔䴷䅙䅱閽婫倱俒㑮餛䛰諢",
|
||||||
|
"huo": "和活或火获货伙惑霍祸豁夥蠖嚯镬藿劐耠灬钬锪攉㦯韄䰥蒦騞奯剨臛耯靃眓矆矐䂄䁨濩湱瀖沎漷曤嚄嚿喐咟吙㗲㘞䯏旤雘㦜邩㸌煷窢䄀禍䄑䄆鑊䱛鈥鍃獲掝擭捇㨯檴䣶㯉穫秮䉟秳艧秴癨䦚閄彠彟佸俰貨䋭謋",
|
||||||
|
"ji": "机几基己期济及级计即极技记集际积纪急激既继击奇季鸡迹剂辑绩吉寄疾挤肌籍祭寂脊饥忌冀藉稽畸棘鲫叽圾嫉姬讥妓汲系伎缉唧骥羁髻悸瘠箕暨矶麂岌蓟亟戟跻诘犄荠稷畿霁嵇嵴屐蒺觊笈玑楫偈鱀勣芨咭其齐芰蕺剞赍殛乩洎虮戢跽哜墼鲚掎笄彐佶齑䓫䩯蘎鞿蘻蘮葪薊茤旡蕀蔇虀薺䓽焏際隮㤂䲯﨤㹄䯂驥䮺鳮䰏㞆㚡朞卙䦇惎諅磼磯䐀鶏膌䐕䐚鷄雞叝䨖趌䟌䞘䟇塉郆霵賷坖䣢耤耭垍賫㙫㙨霽㒫䢋㱞㻷㻑璣璾䶩茍㦸䁒㭰㲺㴕㴉湒濈瀱漃㳵泲鹡鶺漈潗済濟䗁螏蝍暩蟣嗘踖躤踑蹟蹐䠏躋跡㘍㗊㖢喞㗱嘰嚌羇羈轚擊檕罽輯毄㚻繋撃䍤䝸覬㡇䶓嶯㠖㞦㠍㥛忣㠱㥍丮鵋㞛愱懻妀庴廭㸄㲅襀襋禝禨錤觙觭銈銡鱾䤠鍓魥鰿魝魢鯚鯽鰶鱭鑙犱鏶鐖鑇㔕撠刏鬾魕㰟裚揤曁旣皀卽皍擠㨈鸄覉覊極㮟樭橶枅䤒檝㮨梞槣槉楖㭲檵機櫅䇫彶䚐嵆徛簊稘筓積臮箿稩躸䪢刉艥䒁鷑穊穄穖穧兾㾊痵癪㽺㾒㾵癠塈堲䳭姞䢳伋亼偮㑧飢饑谻㞃僟亽雧級綨績緁緝紀彑䋟継紒㡮幾㡭繼計韲齏剤劑齎齌㧀記誋譤譏䜞给",
|
||||||
|
"jia": "家加价假架甲夹佳嫁驾嘉贾钾稼颊伽挟迦枷荚戛拮浃胛袈痂颉镓岬笳珈蛱跏瘕袷葭恝郏铗莢䩡䕛斚犌戞㕅郟夾頰鵊㼪脥駕毠乫㔖鴐腵貑鴶㪴耞圿豭玾頬䁍䀹䀫浹泇蛺䖬唊斝䑝幏叚忦糘麚䴥裌鋏鉫鉀鎵猳拁抸扴㮖榎梜賈椵榢槚檟徦㿓婽傢價䛟",
|
||||||
|
"jian": "间见建件坚简渐减检践健尖监艰键肩兼鉴浅箭碱剪剑舰奸歼俭拣荐贱茧柬捡煎溅涧谏睑堑腱毽笺缄饯硷翦犍謇鲣僭锏缣囝鞯菅蒹戋戬湔趼踺蹇裥搛枧楗笕鹣牮谫戔韉靬韀鞬堅䵖㔋監鋻鍳鑒㯺譼虃囏艱蔪繭薦藆蕑蕳葌菺䧖䮿礷碊礛鬋䶠䩆礀磵礆堿麉䶬趝墹䵤鳽雃戩臶幵瑊珔䵡豜豣殱殲瑐蠒玪鹸鹻鹼見瞷睷瞼㓺瀳減洊瀐䤔漸濺瀽㶕澗湕㳨瀸暕鵑踐䟰跈轞䟅䭕賤䯛䯡賎帴㦗惤熞熸糋寋弿襺袸襉襇鑑鑬鳒鏩鰹鰔鰜鰎鑳㺝猏鐗鐧䥜鍵鐱鑯㨴挸揀擶揃㨵撿樫檻椷栫榗梘㰄椾検檢櫼箋㣤㔓䄯牋筧䅐馢籛䇟篯艦簡䉍徤䵛覵間覸冿鶼姧姦俴剣劍劎剱劒劔餞䬻䭠餰䭈㦰倹儉緘絸繝彅縑諓䛳譛鵳諫譾謭旔詃槛",
|
||||||
|
"jiang": "将讲江降奖蒋港匠疆浆姜僵酱桨缰绛犟强茳礓耩豇洚糨匞韁薑顜葁蔣䕬㹔膙塂壃䞪䙹畺殭䁰滰疅畕嵹翞糡鳉鱂摪摾橿櫤㯍夅䉃䒂奨醤㢡奬獎醬漿螀螿槳將傋䋌䥒繮勥謽絳弜弶講",
|
||||||
|
"jiao": "教叫较交觉角脚焦胶郊缴骄娇轿搅浇嚼校剿礁椒矫狡绞蕉酵窖饺跤佼侥皎蛟茭醮姣铰湫鲛峤艽噍挢敫徼僬鹪茮斠藠驕膠腳膲趭璬珓䂃䣤䴛䁶㳅灚澆漖䀊滘潐㬭曒蟜暞晈蟭䠛踋劋嘂嘄噭呌嘦轇轎較嶠㠐峧賋嶕嶣䪒憿憍煍烄燋䘨䆗窌䚩鱎鮫䥞獥鉸鐎㩰敎皭攪撹皦撟捁挍摷㰾譥釂㭂敽鷮敿矯徺臫笅穚簥筊㽲㽱虠䢒䴔鵁勦嬓嬌孂㚣僥龣儌餃鷦燞繳纐絞訆譑䜈",
|
||||||
|
"jie": "结解接阶界价节介姐借街揭届洁杰截皆戒捷竭劫桔藉诫秸睫楷芥婕拮孑诘疥嗟颉疖桀碣羯讦偈蚧毑袷家她卩喈骱鲒䕙鞊鞂蓵䔿菨莭㔾階卪岊犗礍䂝䯰䂶㛃镼砎䃈脻丯刦刧刼頡㔛劼㓤迼堺堦䣠琾疌玠䀷䁓潔尐滐蠽湝昅蛶蠘蜐蛣䗻蝔唶踕跲喼吤畍嶻崨幯㠹巀嵥岕悈屆㞯㦢㸅庎煯㝌衱袺褯衸㝏䥛觧鉣㘶鍻鎅鮚䰺䱄䲙魪狤擮㨗掲擑㨩掶搩杢㮮楬楐檞桝榤㮞椄徣䂒䅥節蠞稭㓗㾏㿍楶癤痎䇒媎媫嫅媘㑘倢偼䲸傑飷結䌖鶛誡訐詰誱謯䛺",
|
||||||
|
"jin": "进金今近仅紧尽禁劲津斤晋锦浸筋巾谨襟靳矜瑾烬噤缙觐馑堇荩卺赆廑衿钅槿妗蓳荕菫緊覲㝻歏黅藎䒺巹㹏矝厪㰹砛䐶墐壗晉㬜琎瑨殣琻勁珒璶璡齽䶖鹶漌溍浕濅堻濜㴆㬐䗯唫嚍䝲贐惍㶦煡燼寖䘳䆮祲觔釿錦釒㨷劤搢䖐䤐枃䫴㱈㯲㯸䑤凚嫤㶳盡䀆賮嬧僅仐侭伒僸饉䭙儘進縉䋮䌝紟謹䥆",
|
||||||
|
"jing": "经精境京静竟惊景睛镜径警晶劲竞净敬井颈茎鲸荆靖兢痉憬泾菁粳阱胫腈迳旌璟儆箐刭肼靓獍婧弪荊莖葏㢣蟼憼驚䔔聙頚㣏㕋脛鼱㘫坓汬丼璥靜靚䴖鶄殌璄巠剄頸鵛逕坙梷淨汫瀞㵾涇澋浄曔暻㬌踁䵞䡖幜麠麖宑穽鯨㹵猄鏡坕桱橸稉徑秔凈痙竸競竫竧妌婙婛俓傹経弳經綡䜘鶁亰旍誩",
|
||||||
|
"jiong": "炯窘迥炅颎冂扃蘏蘔褧駫駉澃䐃坰埛㷡煛泂浻煚㖥囧冋㢠冏䢛燛㤯烱逈㷗㓏㑋僒侰絅䌹綗熲顈",
|
||||||
|
"jiu": "就究九久旧酒救纠舅揪灸疚臼鸠厩赳韭咎桕啾柩鹫鬏玖阄僦匶萛韮匛䓘舊牞镹䊆䳔䳎慦㺩㺵殧齨䰗鬮㲃汣䡂㠇丩乆䊘㡱廏廐廄㶭麔䆒鯦勼匓捄摎㧕揂㩆欍柾朻樛杦舏䅢揫㐇鳩奺倃糾乣糺紤鷲䛮",
|
||||||
|
"ju": "具据局举剧句居巨距聚拒柜菊矩惧俱拘桔咀锯鞠橘踞驹沮瞿炬踽疽遽掬枸飓榘苣裾龃榉倨狙钜莒且车苴鞫犋雎琚屦窭锔醵椐讵蘜䕮䢹乬巪蒟輂埾陱聥犑駏驧駶駒䃊砠㪺䢸舉㐦擧鴡貗腒䏱鼳鼰毩毱弆壉趜埧㘲耟㠪歫䶙齟䶥郹䴗鶪㮂狊䋰勮豦劇愳虡眗䡞洰㳥挙湨澽涺泦泃淗趄昛蚷㬬蜛䗇蹫跙㘌躆跼跔踘啹罝㽤巈岠岨崌㞫鵙怇鶋懅懼䪕㥌屨㞐凥烥粔焗粷寠袓襷䆽窶䄔鉅鐻邭鋸鋦鮔匊䱟鮈鵴䱡據㩴㩀㨿挶䰬抅㐝拠檋櫸欅䣰䤎椇梮椈秬簴筥躹䅓艍䈮䵕閰姖娵㜘婮婅倶侷颶䜯繘詎䛯諊渠",
|
||||||
|
"juan": "卷倦捐圈娟鹃绢眷涓镌蠲鄄狷锩桊蔨菤奆朘腃臇埍睊睠淃瓹呟罥羂䳪脧惓慻焆㷷裐隽鋑䥴獧錈鎸鐫捲䚈䣺㯞䅌䡓勌劵䄅龹䖭帣巻餋弮勬絭姢䌸㢧絹㢾讂㪻",
|
||||||
|
"jue": "决觉绝掘嚼爵诀厥倔攫崛蕨獗撅噘抉镢蹶谲角孓噱橛珏矍鳜桷钁劂爝觖匷㓸芵蕝孒䦼矡駃砄蹷蟨憠鷢橜䐘䏣臄貜䏐䁷覺趉䞵䞷赽瑴䝌玨㻕玦亅䀗覐㵐決覚泬灍蟩䖼蚗虳噊䟾躩䠇趹爴䡈㟲嶡嶥崫㤜憰戄屩屫刔鴂爑㷾熦焳䙠䘿䆕䆢氒鐍鐝觼觮䦆鈌鴃玃㹟㩱挗㸕捔撧㰐㭾㭈櫭䍊䇶欮疦瘚弡彏䋉㔢絶㔃絕譎斍訣",
|
||||||
|
"jun": "军均菌君俊峻钧郡骏竣隽浚筠麇儁皲捃莙葰䕑陖皹駿鵕㕙碅㓴埈䝍㻒珺䜭濬汮㴫晙蜠蚐呁㽙畯賐懏燇麏麕皸軍袀㝦寯鲪銞馂鵔鮶鍕銁鈞攈攟棞桾箟箘䇹姰頵鵘覠㒞餕㑺雋龟",
|
||||||
|
"ka": "卡咔咖咯喀佧胩垰裃鉲䘔",
|
||||||
|
"kai": "开凯慨恺揩楷铠忾闿锴岂蒈垲剀锎䒓奒䐩塏䁗暟嘅䡷輆剴颽凱㡁嵦愷愾炌烗鎧㚊鎎鐦鍇開闓勓欬",
|
||||||
|
"kan": "看刊堪砍坎勘嵌侃槛瞰龛阚磡戡莰凵顑歁墈栞䶫鬫矙轗輡嵁崁惂冚欿衎㸝䘓㸔䀍竷闞龕偘",
|
||||||
|
"kang": "抗康炕扛慷亢糠鱇伉钪闶匟砊漮䡉囥嵻忼㱂粇㝩鏮犺鈧槺躿穅閌嫝邟㰠",
|
||||||
|
"kao": "考靠烤铐拷犒尻栲䐧攷丂洘䯌嵪㸆銬鲓鮳鯌䯪髛",
|
||||||
|
"ke": "可科克客刻课颗壳棵渴咳柯磕苛坷瞌窠蝌轲颏恪稞髁珂氪缂岢嗑剋尅呵骒溘蚵锞钶疴薖萪匼騍牱犐礚碦勊勀砢㕉堁殼殻㵣渇顆敤㪙趷礊軻嶱嵑㞹嵙峇愘炣㪡愙䙐錁翗鈳搕揢榼醘㐓㪼㤩衉艐痾㾧牁娔樖緙課頦",
|
||||||
|
"kei": "剋尅",
|
||||||
|
"ken": "肯恳垦啃龈裉㸧硍墾懇貇豤肻肎褃錹掯",
|
||||||
|
"keng": "坑吭铿硻阬牼硁硜䡰鏗鍞銵摼挳妔誙劥",
|
||||||
|
"kong": "空控孔恐箜倥崆鞚硿埪涳㤟悾鵼錓躻㸜",
|
||||||
|
"kou": "口扣寇叩抠佝蔻芤眍筘剾蔲瞉鷇㲄瞘滱䳟怐冦宼㓂窛釦敂䳹摳劶㔚簆彄",
|
||||||
|
"ku": "苦哭库枯裤酷窟挎骷绔袴刳堀喾䧊郀矻嚳㱠跍圐㠸庫廤㐣焅褲鮬狜楛桍䇢秙䵈瘔㒂俈絝",
|
||||||
|
"kua": "跨夸垮挎胯侉咵趶骻䯞銙舿姱誇䋀",
|
||||||
|
"kuai": "会快块筷脍侩狯哙蒯浍郐䓒巜膾凷墤㙕㙗塊圦㱮欳澮㬮噲䯤㟴廥糩鲙鱠獪擓㧟㔞䈛鄶䭝儈旝",
|
||||||
|
"kuan": "款宽髋䕀臗髖寛寬窾窽䥗䲌鑧䤭㯘歀梡欵",
|
||||||
|
"kuang": "况矿狂框旷筐眶匡邝哐圹诳劻夼贶贶纩诓匩邼硄礦砿壙眖矌洭黋況曠昿軭軖軦軠岲貺恇忹懭鄺懬爌䊯鋛鑛鉱㤮鵟狅抂䵃筺穬儣絖纊絋誆誑",
|
||||||
|
"kui": "亏溃愧奎魁馈葵窥盔傀匮逵夔喟睽喹聩揆篑岿馗蒉蝰暌跬悝愦䕚蘷藈匱蕢䕫虁聵聭聧骙騤犪尯磈㚝膭頍㙓刲䖯殨㕟虧潰晆䠑䟸躨蹞嘳顝䯓巋巙憒煃窺頯鍷鍨㨒䫥楏䤆櫆楑籄簣䈐䦱闚䍪㛻嬇媿戣鄈䳫饋餽䧶謉",
|
||||||
|
"kun": "困昆捆坤锟崑鲲琨髡堃醌悃阃菎騉髨髠硱堒壼壸瑻睏涃潉蜫䖵晜㫻鹍鵾䠅崐焜熴鶤裩裍裈褌祵錕鯤猑㩲梱稇稛閸閫綑",
|
||||||
|
"kuo": "括扩阔廓蛞鞟鞹萿葀䯺髺鬠霩濶䟯㗥韕挄擴拡頢筈䦢闊",
|
||||||
|
"la": "拉啦腊辣蜡落喇垃剌旯邋砬瘌藞鞡䪉菈䏀鬎磖䂰㕇䃳臈臘䟑䝓䶛㻋㻝瓎溂䗶蝋蝲蠟嚹翋㸊爉鯻鑞镴搚揦攋䱫揧辢楋櫴柆䓥",
|
||||||
|
"lai": "来赖莱癞睐籁徕涞崃疠唻赉濑铼䓶藾萊䧒騋㚓䂾琜睞瀨瀬淶䠭㠣崍庲襰䄤䲚鯠錸猍梾頼賴鵣棶郲來賚顂鶆逨䚅麳筙㥎籟徠箂䅘癩㾢婡俫倈䋱",
|
||||||
|
"lan": "兰蓝烂览篮栏拦懒滥揽澜婪岚缆阑榄斓褴啉谰镧漤罱藍韊䪍覧覽擥蘫蘭葻䰐䃹䑌壈璼㱫瓓灆濫灠灡浨㳕瀾嚂囒躝㘓幱嵐㞩懢懶惏㦨爁爦爤糷䊖顲燗爛燷燣襤襽襕襴䆾钄䳿鑭㩜攬㨫攔欖㰖欗醂欄籃籣䦨闌㜮孏嬾㛦孄儖㑣㑑繿纜䌫䍀譋斕讕",
|
||||||
|
"lang": "浪朗郎狼廊琅螂啷榔鎯莨阆蒗锒稂䕞蓈蓢硠朤朖㙟埌㱢瑯䁁䀶蜋㫰䍚䡙䯖崀㟍㢃烺䆡㝗䱶鋃樃桹躴艆筤㾿閬㾗嫏郞塱㮾勆郒欴㓪斏誏",
|
||||||
|
"lao": "老劳落牢络捞姥烙唠涝佬潦痨酪崂醪乐耢铹铑栳荖䵏䕩硓磱嗠䝤朥耮耂㐗䳓珯澇労浶蛯蟧㗦咾嘮哰轑㟙㟹嶗㟉㞠恅憦顟粩䃕勞憥䝁窂銠鮱鐒䲏狫㧯撈㨓橯䇭躼軂簩癆嫪僗髝䜎",
|
||||||
|
"le": "了乐勒肋仂嘞鳓泐叻艻阞砳㔹玏氻㖀忇㦡鰳鱳扐楽樂簕竻韷餎",
|
||||||
|
"lei": "类累雷泪勒蕾垒肋擂磊儡镭耒羸嘞檑酹嫘缧缧诔䒹蕌蘲虆藟蘽蔂蘱絫厽㹎䮑礌礧磥㲕䐯鼺䨓靁㙼䢮䣂頛㼍瓃矋㵢洡灅㶟涙淚㴃蠝䍥䍣塁罍礨㔣壘壨畾纍轠鸓䴎櫐㡞類頪纇颣禷鐳銇鑸鑘鱩錑攂㭩䣦欙櫑樏䉪䉂䉓癗㿔㒍㑍㒦儽傫纝縲䛶誄讄",
|
||||||
|
"leng": "冷愣楞棱塄薐䮚碐堎睖踜㘄唥䚏䉄稜倰䬋",
|
||||||
|
"li": "里理力利立例离历李礼丽粒隶哩璃励黎厉厘梨莉吏栗犁鲤狸砾沥荔篱漓笠蛎痢俐锂俚雳逦戾镉罹栎蠡俪藜鹂骊砺蜊黧娌莅猁疠傈唳溧疬慄醴砬喱鬲苈澧蓠坜嫠郦呖跞轹詈粝鲡鳢枥篥缡藶蒚蒞荲䔆䔁䔣䔧蔾菞䔉苙茘䓞蘺䧉犡䮥䮋驪勵厲礪㔏礰鬁㻎砅䃯礫歴暦厯磿歷厤曆㻺㽁貍䤚蠫䴄脷壢靂隷䟐赲䟏靋塛孷釐剺斄㹈瓑珕蟸叓䣓䰛酈鸝邐䚕婯麗䴡㱹㡂㽝瓅瑮琍瓈䶘㮚䁻睙濿瀝浬浰沴涖灕蠇䘈曞蠣蛠㬏蝷蚸蟍蜧㒿嚦㘑囇躒㗚唎嚟㕸囄轣䡃轢䍠䍦豊巁屴峛峢㟳峲㠟岦㤦㤡㦒悧悷䊪爄糲糎爏廲粴麜㷰裡褵䙰禲禮䄜䥶觻䲞鋰鱱鳨鱺鯉鱧鯏㺡鏫鑗鉝瓥㼖攊㿨攦㸚擽皪搮㧰攭櫔櫪栛朸隸䣫欐䤙醨栃檪櫟鷅梸㰀㯤欚棙樆㰚䅄穲䖽䵩悡鋫䱘㴝犂睝䖿鯬鵹䊍邌錅䴻棃剓筣䉫秝艃䵓䅻籬癘竰癧䍽㿛㾐㾖鴗凓䇐孋㓯娳刕儮儷䬅䬆㑦㒧劙䗍盠盭䰜纚䋥綟縭讈裏離謧",
|
||||||
|
"lia": "俩",
|
||||||
|
"lian": "联连脸练炼恋莲怜链廉帘敛镰鲢涟殓濂梿奁裢潋楝蔹臁琏琏蠊裣匲蓮薕萰蘞匳蘝聨聫聯䏈聮奩鬑䃛磏臉䨬覝堜鄻璉㱨殮瑓䁠㶌瀮漣湅濓溓瀲澰㶑螊蹥嗹噒連㦁㡘慩翴㦑憐䙺㥕燫煉劆㢘熑褳襝鏈鰱鰊鐮錬鍊㺦䥥鎌㼓摙櫣㪝槤㼑㰈㯬㟀簾䆂䇜籢籨亷㾾㝺羷㜕嫾嬚媡㜃㜻斂㪘歛㰸僆䭑縺練䌞纞謰戀",
|
||||||
|
"liang": "两量亮良粮梁俩凉辆谅粱踉晾靓莨墚魉椋䩫䓣駺㹁脼㔝兩両涼湸蜽唡啢䠃喨哴輌輛輬辌㒳䝶悢糧裲䭪鍄掚魎䣼樑倆倞俍緉諒",
|
||||||
|
"liao": "了料疗辽僚聊廖缭寥撩燎撂瞭缪嘹潦寮镣蓼獠尥鹩钌藔䒿镽䩍尞鷯遼䨅㶫膫㙩璙䝀敹漻㵳暸蟟曢蹽蹘䍡嶚嶛髎嵺賿憭憀屪鄝䢧䎆廫膋爎㡻䉼炓㝋窷竂釕鐐爒㺒橑䄦簝䑠療嫽尦飉豂䜮繚䜍",
|
||||||
|
"lie": "列烈裂猎劣咧冽趔鬣埒洌躐捩茢䓟聗㸹犣鬛㼲脟㲱埓劽䴕㤠烮鮤鴷迾姴䁽浖毟蛚㬯哷䟹䟩㽟煭鱲猟獵㧜挒挘擸栵㭞㯿䅀䉭巤颲儠䜲",
|
||||||
|
"lin": "林临邻磷淋鳞霖麟琳拎凛吝粼赁蔺躏嶙啉璘廪檩遴膦瞵辚辚懔臨䕲菻藺隣阾厸驎䮼䫰碄壣瀶潾澟暽䗲晽躪蹸躙㖁轥疄轔崊恡悋懍燐㷠䢯鄰粦㔂亃翷斴甐麐廩冧㝝䚬鱗鏻獜撛㨆橉䫐檁箖䉮焛閵癝凜癛僯賃繗綝㐭",
|
||||||
|
"ling": "领另令灵零龄岭铃玲凌陵棱菱伶苓聆翎绫羚鲮呤棂蛉囹瓴酃泠柃䔖蘦䖅蕶蔆蓤䕘䧙駖㸳砱朎霊霗㪮䰱龗霝䴒䚖孁靈㲆䨩夌坽䴇霛琌㱥㻏齡羐鹷齢澪淩㬡昤㖫跉䡼䡿輘軨䯍崚岺嶺㦭㥄爧燯炩㡵䴫麢䙥裬袊祾䄥錂鯪魿狑鈴掕皊櫺欞㯪醽䉁䍅䉹䈊䉖䠲舲彾秢笭衑竛閝㾉婈姈鸰刢領鴒䌢綾紷詅〇",
|
||||||
|
"liu": "流六留刘硫柳溜瘤碌榴馏琉浏绺蹓遛镠骝鎏鹨熘镏锍旒蓅藰蒥䋷䭷驑駵駠騮磟磂䶉㙀塯霤㽌璢畱鬸珋瑠䰘澑畄瀏瑬蟉䗜㽞嚠疁罶嵧羀懰鷚翏雡熮㶯廇麍裗䄂䚧鐂鏐䱞䱖鰡鎦鋶鹠劉鶹㨨橊桺栁桞橮䉧癅嬼媹飗飂䬟飀飅餾綹㐬斿旈",
|
||||||
|
"long": "龙隆笼垄拢胧聋咙陇窿珑垅弄砻茏栊滝眬泷癃䪊蘢䃧隴䏊龓尨礲朧霳䥢鏧壠靇瓏矓漋㙙㴳湰瀧昽曨蠬哢躘嚨嶐㟖巃巄贚㦕㢅爖㝫襱竉鑨攏梇䙪櫳槞㚅䡁徿籠䆍篭聾礱龍壟龒蠪驡鸗㰍竜㛞㑝儱豅㡣",
|
||||||
|
"lou": "露楼漏陋搂喽篓娄镂偻髅蝼瘘耧蒌嵝鞻㔷蔞䮫㲎塿耬䝏剅瞜䁖漊溇螻嘍䣚䫫婁甊遱鷜㪹髏㟺嶁屚慺㥪廔熡䄛鏤䱾㺏摟樓簍䅹軁艛瘻瘺謱",
|
||||||
|
"lu": "路陆绿露录鲁炉卢芦鹿碌禄卤虏庐噜麓颅漉辘掳六赂鹭戮泸橹璐潞鲈撸蓼箓轳胪垆氇鸬渌辂镥栌簏舻逯虂䩮蘆蓾蕗蔍菉陸䎼騼䮉騄馿䰕磠硵䃙硉臚膔氌䐂壚塷趢塶圥勎坴鵱瓐㱺璷琭矑虜㪭盧顱鸕鹵睩淕瀘滷澛瀂淥曥蠦螰㫽踛嚧蹗鷺䟿嚕㖨黸䡜轤轆輅䡎髗㠠賂峍㟤㦇䎑勠剹㢚廬爐廘熝粶䴪㼾䘵祿錴鐪鑪鏀㔪鏴鯥䲐鱸魯鴼鵦䱚鏕魲鑥獹録錄鈩擄攎摝擼醁㯭櫨樐㯝樚櫓㯟㭔椂枦甪罏稑籚簬簵穋簶穞籙艣艫艪舮㓐㿖㛬㪖䚄盝㜙娽僇侓纑彔䌒㢳㪐謢玈",
|
||||||
|
"luan": "乱卵挛峦滦鸾孪栾銮脔娈䖂虊亂灤羉圞圝釠癴癵鵉孿㝈奱㡩灓曫巒鸞鑾攣欒孌臠㱍龻䜌",
|
||||||
|
"lun": "论轮伦仑沦纶抡囵崙菕芲陯磮碖腀耣埨淪溣蜦踚㖮圇輪崘惀㷍鯩錀㤻掄棆䑳稐䈁婨侖倫綸論",
|
||||||
|
"luo": "落罗逻洛络螺裸萝锣骆烙骡啰珞箩摞捋倮瘰猡硌荦脶漯泺镙椤雒蠃蘿蓏騾駱䯁硦覶頱腡㼈㱻覼䀩㴖濼曪囉囖邏羅峈㦬犖鏍鑼鮥玀㩡攞㰁欏洜㓢鵅籮躶䈷笿癳㿚㑩儸饠㒩纙絡䌱䌴驘臝䊨鸁䇔詻剆㽋咯",
|
||||||
|
"lv": "律率绿虑旅氯铝履吕捋驴滤侣屡缕榈褛偻闾稆膂藘葎䕡驢膢膟垏勴慮濾郘呂氀㠥嵂屢爈焒褸祣鑢鋁㲶捛挔櫖梠櫚穭箻閭儢侶僂絽縷緑綠繂膐",
|
||||||
|
"lve": "略掠锊寽㔀畧㨼圙鋢鋝稤",
|
||||||
|
"ma": "马吗妈麻嘛骂码抹玛蚂蟆犸嫲么杩蟇蔴䣕馬䣖遤碼鬕瑪睰溤螞䗫嗎駡嘜罵䯦犘㦄䳸祃禡鎷鰢鷌獁㨸榪㾺痲痳閁媽㜫㐷傌㑻摩",
|
||||||
|
"mai": "买卖麦脉埋迈霾荬劢唛薶勱邁蕒䮮脈霢霡䨪賣売䨫䁲嘪䚑鷶買麥衇䘑䈿㜥佅䜕",
|
||||||
|
"man": "满慢漫曼蛮瞒蔓馒螨幔缦鳗谩颟墁埋鞔熳镘䕕顢㒼蔄蘰鬗䯶鬘䰋䐽䝡䝢㙢䟂瞞満滿㵘澷蟎鄤㬅㗈㗄䡬㡢慲屘悗䊡襔鏋鏝鰻獌摱樠槾䅼䑱姏娨嫚㛧僈饅䜱縵謾䛲矕蠻",
|
||||||
|
"mang": "忙盲茫芒氓莽蟒铓牤邙硭漭䒎莾蘉茻牻駹厖硥壾㙁㻊䁳䀮盳浝汒蠎㬒蛖哤䟥䵨㟿㟐㟌㡛恾庬㝑鋩狵釯杧䅒笀䈍痝娏䖟杗吂",
|
||||||
|
"mao": "毛矛貌冒贸帽猫茂茅髦瑁锚牦铆卯懋袤昴峁眊茆瞀蟊蝥耄泖旄蓩鶜䓮芼鄚萺堥暓䖥愗髳冇貓䫉覒氂犛㲠㺺渵㴘冐毷㪞㒻㫯蝐罞軞䡚冃㡌戼㝟錨夘鉾䀤鉚乮鄮貿㧌㿞㧇皃㒵楙柕㮘枆酕䅦笷媢㚹䋃",
|
||||||
|
"me": "么濹嚰嚒",
|
||||||
|
"mei": "没美每妹梅煤眉霉媒枚酶镁媚魅玫昧莓糜楣寐湄嵋袂浼鹛镅猸䒽葿䓺苺脄腜脢堳坆㺳䜸瑂珻眛睸䀛湈沬沒渼䰪蝞跊嚜槑䵢黣䍙嵄郿鶥韎㶬䊊煝塺䊈燘禖祙鎇鋂鎂抺攗鬽挴楳㭑䤂栂䆀䰨躾黴徾篃毎䉋羙凂痗媺嬍媄睂旀",
|
||||||
|
"men": "们门闷瞒懑扪汶焖钔虋菛璊玧㱪懣㵍暪㡈䝧㥃㦖䊟穈燜䫒鍆㨺捫椚門悶閅們",
|
||||||
|
"meng": "梦蒙猛盟孟萌朦氓锰懵蟒勐檬濛蜢虻蠓矇瞢甍礞艨艋䓝鄸䒐䠢顭夢莔氋鹲鸏蕄䰒㚞䑅䑃䏵㙹靀霿霥矒溕曚䗈甿㠓幪懜懞冡鼆䀄䙩㝱䙦錳䴌䲛鯭鯍䥂獴䥰㩚掹擝橗䤓䴿䵆䉚㒱癦䇇㜴儚饛鄳夣蝱",
|
||||||
|
"mi": "密米秘迷蜜弥泌眯咪觅谜靡糜猕谧醚嘧弭脒幂麋縻汨蘼蘼芈敉宓冖祢糸蔝㰽蒾䕷蘪藌蔤葞䕳䮭镾覔㫘䪾覓㸓塓鸍羋瞇䖑濗漞濔㵋㳴㴵灖洣滵淧沵沕䌘渳瀰㳽羃䍘峚幎㠧㟜怽幦戂㥝㐘粎䊳麊熐麿爢㸏麛䴢冪宻鼏䁇冞㝥袮禰祕䱊銤獼㩢覛擟攠㨠䤍䤉釄醿醾䣾榓櫁樒簚䉾㜆孊侎䭩䭧䌩䌐㣆䥸彌㜷瓕䌕䋛䌏䛉謐䛑䛧謎詸",
|
||||||
|
"mian": "面免棉眠绵勉缅腼冕娩沔湎眄渑宀芇葂䏃䰓勔靦靣䃇㻰㤁丏麺䀎睌矈矏矊汅㴐澠蝒㬆喕愐糆㝰鮸緜㮌䤄杣㰃櫋麵麪麫檰䫵臱媔㛯婂嬵偭㒙緬絻綿",
|
||||||
|
"miao": "描苗妙秒庙渺瞄缪淼藐缈邈鹋眇喵杪鶓㦝䁧䖢㠺庿廟劰篎䅺竗媌嫹㑤緢緲玅",
|
||||||
|
"mie": "灭蔑篾咩乜蠛薎孭礣烕䩏䁾瀎滅䘊哶吀幭懱鴓鑖鱴搣櫗衊䈼㒝",
|
||||||
|
"min": "民敏闽皿悯抿泯岷闵苠珉玟黾愍鳘缗蠠䃉䂥碈砇垊琝瑉琘䁕盿湣潣旻旼䟨䡅罠䡑䡻㟭崏㞶䪸敯刡㥸鴖暋㟩敃惽怋憫忟鍲鈱䲄錉㨉捪笽笢簢勄慜鰵閩冺痻閔姄僶緡㢯䋋黽緍忞",
|
||||||
|
"ming": "明命名鸣铭冥螟茗瞑酩溟暝蓂眀眳洺㫥鳴朙㟰慏䊅鄍䒌䫤覭㝠䆩䆨䄙銘猽掵榠凕嫇姳佲詺",
|
||||||
|
"miu": "谬缪謬",
|
||||||
|
"mo": "么没模末默莫摸脉磨冒膜摩墨漠魔抹沫陌寞摹蓦蟆蘑馍谟茉貉秣殁貘万貊耱麽镆瘼嫫嬷嬷靺䒬莈驀㱳謩藦䮬砞䩋礳䃺䏞貃䳮塻圽歿歾瞙眜瞐䁼眽眿尛蛨黙昩䘃蟔嗼嚤䁿㱄髍帞帓懡糢㷬爅㷵䯢劘麼䜆庅鏌銆魹䱅魩獏㹮皌擵枺橅䴲䉑妺嫼嬤饃䬴饝纆絈謨嘿",
|
||||||
|
"mou": "某谋牟眸缪呣哞鍪蛑侔䥐劺鴾䏬䗋踎䍒恈䱕㭌麰繆謀",
|
||||||
|
"mu": "目母木模莫幕牧亩墓姆慕穆暮姥牡拇睦募沐牟缪苜钼毪坶仫莯䧔鞪䱯楘㜈牳砪氁胟雮霂畞䀲暯蚞踇畂畮峔幙慔毣炑䥈鉬狇鉧㣎㧅䑵艒㾇凩㒇縸䊾畆畝畒",
|
||||||
|
"na": "那哪拿纳娜呐捺衲钠内南肭镎靹蒳䖓乸䫱貀豽䏧雫䀑㴸䖧䟜吶㗙嗱軜䎎䪏袦鈉魶䱹鎿㨥䅞笝䇱郍䇣䈫拏妠搻納䛔",
|
||||||
|
"nai": "奶耐乃奈萘氖迺艿能鼐柰孻螚䘅䯮腉渿褦釢錼㮈㲡廼㮏疓㾍䍲嬭倷",
|
||||||
|
"nan": "难南男喃楠囡赧囝腩蝻䕼䔜萳戁難莮䔳遖䁪湳暔㫱㽖畘䶲煵揇抩枏柟䈒㓓婻娚侽諵䛁",
|
||||||
|
"nang": "囊囔曩馕攮䂇嚢灢㶞蠰乪擃欜齉儾㒄饢",
|
||||||
|
"nao": "脑闹恼挠瑙呶孬桡淖铙硇垴蛲猱夒䃩碙碯臑脳腦䑋䴃堖鬧蟯巎嶩悩憹怓惱鐃獶獿峱㺀㺁撓䄩閙嫐㞪㛴䫸㑎匘譊䛝詉䜀䜧",
|
||||||
|
"ne": "呢呐讷哪疒䭆䎪眲㕯抐訥",
|
||||||
|
"nei": "那内哪馁脮腇㼏㘨㖏䡾內䳖鮾䲎鯘錗㨅氞氝娞㐻餒",
|
||||||
|
"nen": "嫩恁㶧㯎㜛嫰",
|
||||||
|
"neng": "能䏻㲌㴰",
|
||||||
|
"ni": "你疑尼泥拟逆妮腻倪匿溺霓昵睨怩鲵铌旎呢坭猊伲䘌臡苨䕥薿孴聣隬䧇膩貎胒䝚郳㪒堄䁥齯惄眤㵫淣聻埿氼暱晲蜺蚭跜輗㞾㠜㥾㦐愵籾麑䘽䘦觬鈮鯢狔㹸掜屔抳䰯擬棿檷柅䭲馜秜䵒䵑屰䦵嫟嬺婗妳儞㲻伱儗㣇縌誽䛏",
|
||||||
|
"nian": "年念粘碾撵捻辗蔫拈埝黏鲶鲇辇廿䩞卄輦涊㲽淰躎蹍蹨哖唸㘝㞋惗焾鮎鯰攆撚䚓鵇秥簐䄭秊艌䄹姩䬯",
|
||||||
|
"niang": "娘酿䖆醸釀嬢孃",
|
||||||
|
"niao": "鸟尿溺袅脲茑嬲蔦䮍䦊䃵䐁㳮㠡㞙鳥䙚裊㭤樢嬝嫋㜵㒟褭",
|
||||||
|
"nie": "捏聂涅孽镍蹑蘖镊颞啮嗫摄乜陧臬糵㜸苶菍聶顳隉孼蠥糱櫱䯅䯀䯵齧㚔䂼㘿㙞摰槷湼㴪圼囁囓躡踙嚙踂噛踗㡪嵲嶭巕㸎䄒鑷鑈钀鎳錜揑㩶枿㮆籋臲篞㖖痆闑帇敜䌜䜓讘捻",
|
||||||
|
"nin": "您恁脌囜㤛拰䋻䚾䛘",
|
||||||
|
"ning": "宁凝拧狞咛柠泞佞聍甯䔭薴聹鬡㿦矃澝濘䗿嚀寕㝕㲰寍寜鸋寧寗鑏獰擰橣檸㣷嬣儜倿䭢侫",
|
||||||
|
"niu": "牛扭纽钮拗妞忸狃靵莥䒜牜䏔㺲䀔汼炄鈕杻䋴紐",
|
||||||
|
"nong": "农弄浓脓哝侬蕽鬞膿䢉䁸濃噥農燶㶶襛禯㺜挵挊醲檂欁辳齈穠秾䵜癑儂繷譨",
|
||||||
|
"nu": "努奴怒弩帑孥驽胬搙䢞笯駑砮㐐傉伮㚢",
|
||||||
|
"nuan": "暖䎡渜㬉煗煖䙇奻餪",
|
||||||
|
"nuo": "诺娜挪糯懦喏傩搦难锘逽㔮蹃㡅愞懧糥糑鍩䚥掿㰙梛榒橠稬穤㐡㛂儺㑚諾",
|
||||||
|
"nv": "女衄恧钕朒沑籹釹衂",
|
||||||
|
"nve": "虐疟硸䖋䖈瘧婩",
|
||||||
|
"o": "哦噢喔筽",
|
||||||
|
"ou": "偶呕鸥殴耦藕讴禺沤怄瓯区欧蕅毆鷗歐甌䚆鴎藲膒腢塸漚㼴嘔吘䯚慪熰鏂䳼櫙㛏㒖䌔䌂謳",
|
||||||
|
"pa": "怕爬帕扒啪趴琶耙杷葩钯筢䔤苩䯲䶕潖帊袙皅掱舥妑",
|
||||||
|
"pai": "派排迫拍牌湃徘俳哌蒎犤沠渒㵺䖰輫鎃猅棑㭛簲箄簰",
|
||||||
|
"pan": "判盘胖潘盼叛攀畔拌蹒泮蟠磐槃爿袢柈番襻丬萠蒰聁䰉䰔磻䃑䃲坢眅㳪溿沜瀊洀蹣跘炍鑻鋬牉䈲鞶幋縏盤鎜搫媻頖鵥冸詊拚",
|
||||||
|
"pang": "旁胖庞乓磅螃彷滂徬耪逄䮾厐龎肨膖胮霶㫄雱䨦眫㤶㥬炐龐鳑鰟舽䅭㜊嫎䒍覫",
|
||||||
|
"pao": "跑炮泡抛袍刨咆疱狍庖匏脬鞄䩝萢皰礟礮靤砲奅褜垉㘐軳麅麃炰拋爮㯡麭䶌㚿䛌",
|
||||||
|
"pei": "配培陪佩胚赔沛妃裴呸帔辔霈锫醅旆蓜阫馷䪹䲹䫠肧毰珮㳈浿㫲䣙賠㟝怌㤄䊃犻錇㧩衃姵俖伂轡裵斾",
|
||||||
|
"pen": "盆喷湓葐翸歕喯噴呠瓫",
|
||||||
|
"peng": "朋碰棚蓬膨捧篷鹏烹砰澎抨怦硼嘭彭堋蟛莑芃蘕駍騯鬅髼鬔䰃磞硑鵬蟚塜塳㼞淎泙踫輣軯䡫剻㥊憉恲熢袶䄘鑝錋匉捀皏掽樥槰椪䴶梈椖稝竼篣閛韸韼㛔倗傰纄弸苹亨",
|
||||||
|
"pi": "皮批屁披辟疲脾匹劈僻副罢譬啤琵坯癖毗痞枇霹噼裨媲否貔丕吡陂砒邳铍圮睥蜱疋鼙陴埤淠蚍罴甓庀擗郫仳纰苉鴄㓟隦阰駓髬㔻礔磇䏘豾脴腗䑀䑄膍肶豼噽嚭壀耚疈錃潎澼蚾蚽䠘㔥羆䡟毘岯嶏崥翍礕䴙憵鷿鸊悂炋焷螷蠯鈹銔鉟銢鲏鮍魾魮䤨釽錍狓狉鈚抷㨽揊䰦䫌䤏㯅秛秠稫篺笓鵧㿙闢嫓伾伓枈紕諀旇䚰䚹",
|
||||||
|
"pian": "片偏篇骗扁翩骈胼蹁便犏谝貵䮁騈駢騙腁䏒跰囨骿賆魸鍂楩楄覑㸤㾫㛹媥㓲騗鶣㼐諞",
|
||||||
|
"piao": "票飘漂瓢瞟缥剽嫖朴嘌骠慓殍螵薸䕯䏇驃犥㵱㬓䴩鰾㺓㹾皫㩠魒勡彯飄顠翲㼼醥徱篻闝僄飃縹旚",
|
||||||
|
"pie": "撇瞥苤氕丿暼鐅撆嫳覕䥕",
|
||||||
|
"pin": "品贫频拼聘拚嫔颦姘牝玭榀蘋薲驞礗砏琕顰䀻矉蠙嚬汖㰋馪穦嬪娦貧",
|
||||||
|
"ping": "平评凭瓶屏苹萍乒坪呯鲆枰娉俜蓱荓聠砯胓䶄塀玶㻂淜涄洴蚲蛢輧軿甹岼幈帲帡屛焩鮃檘缾䍈甁簈箳郱頩艵慿憑凴竮㺸評冯",
|
||||||
|
"po": "破迫婆坡颇泼朴泊魄粕珀鄱钋笸陂叵钷皤蔢尀蒪頗駊奤砶䞟䨰㨇洦湐溌潑昢哱嘙嚩岥䯙岶䪖烞鉕釙鏺廹敀櫇䣮䣪酦醱醗箥䎊䄸㰴㛘㔇繁",
|
||||||
|
"pou": "剖掊裒犃垺哣㧵抔捊抙箁咅娝婄",
|
||||||
|
"pu": "普铺扑谱朴葡仆浦蒲埔菩瀑圃噗曝匍蹼溥濮璞莆氆攴镤镨堡攵䔕䑑蒱䧤陠㹒暴圤墣㺪瞨潽㬥䗱圑贌烳炇㲫䴆菐鯆鏷䲕鋪獛鐠擈撲酺檏樸㯷䈻䈬穙痡暜舖舗㒒僕纀諩譜",
|
||||||
|
"qi": "起其气期器企七奇汽齐妻启旗弃骑欺漆棋岂凄契歧戚栖泣砌祈蹊乞迄崎祺鳍伎缉岐琦祁琪憩畦沏绮脐亟嘁荠杞麒颀耆啐蛴碛淇葺芪祇綦欹槭萋讫圻蕲揭萁芑骐亓丌柒汔蜞屺桤藄䩓䓅鄿䕤蘄䔇䒻炁芞藒䒗萕陭隑䏅䧘亝騹騎騏䭼䭶唘碶磩碕鬐䰇磧慼䫔䚉栔㓞㼤矵攲敧鵸碁䫏磜剘蜝㐞䳢棊肵䏠臍墄埼霋䞚䟄䎢璂䚍玘郪鶈䀙䶞盀盵濝淒呇滊湇湆蚑螧蚔蚚暣㫓蠐咠唭踦跂䟚噐呮罊蟿䡋軝䡔䢀㟢豈帺㟓岓嵜㠎邔慽㥓愭悽愒迉忯㞚㞓懠粸䉻麡䧵褀褄䙄禥䄎䄢鏚錡锜釮鲯鯕鶀䱈鰭䲬䰴夡玂猉鐑頎掑捿氣鬿魌気摖㩩櫀㯦㟚棲㩽榿檱㮑䣛桼憇諬䅲欫甈㣬䄫簯䅤䑴䉝艩簱籏㾨竒疧闙䀈婍娸傶僛倛䏌䬣㒅綺緀紪䭫䭬綥䌌斊棄䛴諆斉齊䶒䐡䁉䋯啔啟䏿䁈晵啓棨訖旂枝俟稽",
|
||||||
|
"qia": "恰洽掐卡髂拤袷咭葜鞐圶硈胢䨐殎䶝䶗䠍跒䯊峠㡊帢㤉擖酠冾㓣䜑",
|
||||||
|
"qian": "前千钱潜迁浅签纤牵欠遣铅歉谦乾倩嵌虔钳黔谴堑扦阡茜钎掮犍钤佥荨骞愆箝芡芊肷椠岍悭慊褰搴仟缱䪈韆䕭茾孯臤蜸掔婜蔳葥蕁蒨騚騝㸫鬜鬝厱膁㦮墘䥅亁乹圲䨿䁮䖍歬淺灊潛汧壍嬱汘濳蚈黚輤塹㟻槧㜞軡㡨岒慳悓忴粁䊴䞿騫錢鹐鵮銭鉗鑓鰬釺鎆鈆鉛鈐鏲㧄攑㩮拑皘㨜攐攓㩃拪揵扲撁橬檶遷棈榩櫏杄槏㯠圱刋谸籖䍉篏䈤䈴篟簽籤羬䇂䦲竏㪠䫡奷媊僉俔儙諐伣㐸偂傔䭤仱欦綪繾縴譴顅謙牽",
|
||||||
|
"qiang": "强枪墙抢腔羌呛跄锵蔷羟襁戕戗嫱樯蜣炝锖镪薔蘠蔃墻玱瑲溬漒蹡蹌啌嗴唴嗆嶈廧熗獇猐鏘鎗鏹摤㩖搶檣椌䵁槍艢䅚篬牆羥羗羻羫墏斨牄嬙㛨戧強彊繈繦謒疆",
|
||||||
|
"qiao": "巧桥悄瞧敲乔侨翘峭窍俏锹鞘憔跷撬樵荞橇壳雀诮峤鞒硗愀劁缲谯鞩鞽㤍䲾㚽菬荍藮蕎陗犞磽䃝䩌硚礄䂭翹墝㚁趬趫墽墧睄郻㴥踍蹺躈蹻嘺骹帩幧韒燆㢗㝯䆻竅釥鐰鄥䱁鄡鐈鍬撽櫵槗橋勪喬䀉䎗㡑鍫䇌頝癄嫶僺僑顦繰繑誚髜毃㪣髚譙",
|
||||||
|
"qie": "切且窃契怯砌伽茄妾惬趄锲箧挈郄苆㥦匧㰼聺㚗洯蛪㓶厒㤲㰰朅淁㫸䟙踥㗫愜悏竊鍥䤿鯜㹤癿篋笡籡穕㾜䦧㾀㛍㛙䬊",
|
||||||
|
"qin": "亲侵勤秦琴禽钦沁芹寝擒矜噙覃揿芩嗪衾螓吣锓檎菣靲䔷懃斳兓菳菦藽耹骎㮗駸肣㘦赾埐坅琹珡䖌澿瀙螼蠄昑蚙唚㞬嶜嵚嶔嵰懄慬吢㤈㢙庈㝲寴寢寑顉鈙鮼鵭欽鋟鈫抋捦撳㩒搇梫䠴笉䈜瘽䦦親㓎㾛嫀媇㪁鳹雂綅誛",
|
||||||
|
"qing": "情清亲青轻请倾庆氢晴顷卿蜻擎氰磬罄圊箐苘檠謦黥鲭綮葝䔛碃䌠硘埥殸漀㷫郬靘靑殑濪淸暒甠啨軽輕鑋䝼䞍慶檾庼廎寈錆鯖䲔夝擏掅氫㯳櫦棾樈凊儬傾頃請勍剠䋜䯧",
|
||||||
|
"qiong": "穷琼穹邛茕跫蛩銎筇卭㧭䓖藭藑蛬䊄䅃赹璚瓗㼇瓊瞏睘惸㷀煢焭熍焪䆳竆窮宆憌桏㮪橩笻䠻舼儝㒌䛪",
|
||||||
|
"qiu": "求球秋丘酋囚蚯邱裘鳅巯泅湫虬遒楸逑龟蝤赇糗犰鼽俅蓲鞦鞧莍萩蘒芁䎿毬肍䞭趥坵皳䣇盚㺫蟗玌璆殏巰㐀汓浗湭渞蛷虯䟵䟬䠗唒㕤賕㟈崷㞗㤹㥢恘秌煪觩觓銶䲡䤛䱸鰽鯄鮂鰍鰌釚釻㼒㧨搝扏梂逎㭝朹湬蝵鹙鶖醔媝穐篍龝蠤㷕丠頄㐤叴訄恷䜪紌絿緧䊵訅仇",
|
||||||
|
"qu": "去区取曲趣趋屈驱渠躯娶岖瞿祛蛐觑衢蛆龋黢癯苣蠼佉阒麯蘧蕖磲朐璩氍劬鸲麴诎葋䒧匤菃敺區䒼螶䧢阹驅駆駈厺髷胠刞臞䝣鼩㰦鼁坥䟊䞤趍趨耝璖麹䶚齲覰覻䁦䀠䂂戵鸜覷灈浀淭䖦㫢蠷蟝蝺呿䠐躣㖆軥㻃嶇㲘岴胊鶌憈翑焌爠粬煀袪鑺鴝斪䵶鰸魼鱋抾㭕㯫欔欋麮衐籧忂筁軀闃閴竘竬㜹佢伹紶㣄䋧絇詘詓誳㧁",
|
||||||
|
"quan": "全权圈泉劝拳券犬醛蜷痊颧铨荃诠筌鬈畎辁悛犭绻勸顴葲虇䄐騡駩犈牷犮硂䑏䟒埢瑔䀬湶洤蠸䠰踡跧啳圏輇巏㟨㟫峑恮䊎烇鳈鐉鰁銓搼權楾権棬椦勧箞㒰齤奍韏觠牶闎婘姾佺縓綣絟詮",
|
||||||
|
"que": "确却缺雀鹊炔瘸榷阙阕悫皵鵲䧿蒛碏礭確硞碻礐趞㱿㲉愨慤埆㱋塙琷㴶崅燩㕁搉㩁棤㰌缼䇎䦬㾡闕闋傕卻",
|
||||||
|
"qun": "群裙逡麇䭽夋囷峮宭㿏㪊裠帬羣",
|
||||||
|
"ran": "然染燃冉髯苒蚺䖄㲯蒅㸐䒣㿵髥肰䫇珃蚦呥嘫冄䎃衻袡袇䤡橪㯗䑙㾆媣姌㚩㜣䣸繎",
|
||||||
|
"rang": "让壤嚷攘瓤禳穰蘘鬤㚂壌瀼躟懹爙獽穣䉴儴勷譲讓",
|
||||||
|
"rao": "绕扰饶桡娆荛蕘隢㹛遶襓擾橈䫞嬈㑱饒繞",
|
||||||
|
"re": "热惹喏若熱",
|
||||||
|
"ren": "人认任忍仁韧刃妊纫壬饪仞衽荏稔轫亻靭靱荵芢㸾牣䏕䏰肕腍忈䀼軔㠴岃屻韌㣼䴦袵祍魜鈓銋扨梕杒栣朲棯忎躵秹䇮秂姙刄䋕鵀㶵栠飪餁䭃仭䌾紝纴綛紉絍認訒讱",
|
||||||
|
"reng": "仍扔芿陾辸礽㭁䄧㺱䚮",
|
||||||
|
"ri": "日䒤驲馹囸衵鈤釰釼",
|
||||||
|
"rong": "容溶荣融绒熔蓉茸戎榕冗嵘镕蝾肜狨茙㲨䩸氄駥毧㲓㲝坈瑢瀜栄螎曧蠑䠜㘇䡥䡆軵嶸峵烿爃嵤榮㣑䘬褣䇀宂㝐䢇㼸鎔㺎搑搈榵㭜䤊槦穁䇯穃䈶羢媶嬫嫆傇傛縙絨",
|
||||||
|
"rou": "肉柔揉蹂鞣糅葇鶔騥䰆腬脜䐓瑈瓇渘蝚㖻輮㽥禸韖煣粈宍鍒鰇楺䄾䧷媃厹譳",
|
||||||
|
"ru": "如入乳儒辱汝蠕茹褥濡嚅孺铷缛襦女蓐薷颥溽洳蕠蒘㹘㦺鄏肗䰰顬渪蝡曘嗕嶿袽鱬銣㨎擩扖醹杁筎㐈鳰邚鴑䋈媷嬬帤鴽挐桇侞縟繻",
|
||||||
|
"ruan": "软阮朊䓴碝礝耎腝堧壖瑌瓀輭㽭軟䞂撋䪭㼱媆偄㐾緛",
|
||||||
|
"rui": "瑞锐蕊兑睿芮蕤蚋枘蕋蘃蘂䓲㓹甤叡㪫㲊壡汭蜹繠橤鋭銳桵㮃䅑㛱緌䌼",
|
||||||
|
"run": "润闰膶瞤潤㠈橍閏閠䦞",
|
||||||
|
"ruo": "若弱偌箬鄀爇蒻叒䐞渃㘃嵶焫鰙鰯挼捼楉篛婼鶸",
|
||||||
|
"sa": "萨撒洒仨卅飒脎蕯薩靸躠隡馺䘮㪪灑㳐䊛䙣鈒钑摋櫒颯㽂㒎䬃訯",
|
||||||
|
"sai": "塞赛腮鳃噻毸毢嗮㗷嘥顋愢賽䚡鰓揌䈢簺僿思",
|
||||||
|
"san": "三参散伞叁糁毵馓弎㪚毿犙䫩鬖毶壭䀐潵㤾糤糣糝䊉䫅鏾鏒㧲㪔䉈閐厁俕饊傘繖",
|
||||||
|
"sang": "丧桑嗓搡颡磉顙䫙桒喪䡦褬鎟槡",
|
||||||
|
"sao": "扫嫂骚缫搔臊瘙埽鳋䕅騒騷䐹矂溞螦氉鰠鱢掻掃㿋㛐㛮颾繅髞梢",
|
||||||
|
"se": "色涩瑟啬塞铯穑槭䔼雭䨛嗇㱇璱㻭歮濇濏澁渋㴔洓瀒澀轖懎㥶銫鏼摵擌㮦栜穯穡䉢閪瘷歰飋㒊繬譅",
|
||||||
|
"sen": "森襂槮椮",
|
||||||
|
"seng": "僧鬙",
|
||||||
|
"sha": "沙杀啥纱砂傻刹厦杉莎煞鲨霎裟挲嗄唦痧唼铩歃萐蔱䮜髿䝊硰㲚㸺乷鯊桬啑喢帹翜翣廈粆魦鯋鎩猀毮閷殺榝樧㰱箑䶎䈉䵘閯㚫㛼儍倽紗繺",
|
||||||
|
"shai": "晒筛曬㬠㩄簛籭簁篩",
|
||||||
|
"shan": "山单善闪扇衫陕珊禅杉擅掺栅煽膳删姗汕赡跚掸讪缮舢疝嬗潸鳝搧鄯苫膻芟骟彡蟮钐陝剼騸㚒磰㪎脠赸墠圸墡㣌睒灗澘㶒晱蟺嘇軕刪邖幓贍炶煔覢熌䘰禪䄠釤銏䱉䱇鱓鯅鱔狦鐥䦅䥇䦂掞挻㨛樿柵檆椫䴮㣣笘䠾䆄痁閊㪨敾歚羴閃羶譱姍僐饍傓縿繕䚲訕謆",
|
||||||
|
"shang": "上商伤尚赏汤裳晌熵墒垧殇觞绱鞝蔏鬺殤丄尙賞漡滳螪贘慯恦禓觴鋿鏛鑜樉䬕傷緔扄謪",
|
||||||
|
"shao": "少烧绍召稍梢哨勺捎邵鞘芍韶筲艄苕劭潲杓莦萷䒚䔠蕱髾㪢䏴㲈玿輎㷹焼燒䘯袑鮹柖䈰䈾㸛娋卲綤䙼䬰弰紹旓",
|
||||||
|
"she": "社设射涉舍摄舌蛇折拾畲奢赦慑麝赊佘猞歙阇厍滠揲蔎騇厙奓䂠䁋䁯灄渉㴇㵃涻蠂虵蛥䵥畭輋䞌賒賖懾韘慴䀅䄕䤮攝摂捨欇㰒㭙檨䠶㒤舎畬䬷弽䌰㢵設",
|
||||||
|
"shen": "什身神深参甚审伸申沈渗婶肾慎绅呻娠砷蜃莘吲糁鯵诜谌瘆信葚胂渖哂矧谂蔘腎頣蓡薓葠駪㰮眘昚脤㥲堔珅眒瞫滲㵕㵊瀋涁蜄曑曋罧屾峷愼糂籸燊籶邥㔤審覾宷裑䆦穼罙祳鋠鲹鉮鰺鰰魫鯓氠扟䰠柛㰂榊兟甧甡鵢瘮㾕妽嬸㜤姺敒侺侁㑗紳弞矤訷谉讅詵諗訠",
|
||||||
|
"sheng": "生声省胜升盛圣剩乘牲绳笙甥嵊晟眚蕂苼䎴聖陞阩陹鼪勝賸榺墭聲殅珄渻湦泩䚇㼳晠琞曻昇㗂呏貹䞉憴焺鍟䱆鵿鉎狌斘橳枡剰䪿㾪竔偗䁞繉縄繩譝甸",
|
||||||
|
"shi": "是时实事十使什式世识食市史石始师失视示似适士势试施室释诗氏湿饰驶拾蚀尸逝侍誓矢狮匙柿硕嗜屎噬嘘栅拭峙仕恃虱轼舐耆螫豕谥弑奭殖蓍泽莳贳埘炻鲥鲺铈酾筮蒔貰䒨蒒葹䦹旹㱁乨駛䰄觢㸷䩃乭䂖䏡鼫鼭卋㔺邿塒㐊辻兘勢丗䴓鳾瑡亊䶡眎睗䁺眂眡溼溡浉湜濕㵓澨溮湤䖨㫑㫭時昰遈㒾呩㕜䟗㖷呞軾嵵崼峕忕蝨屍鸤䲩鳲恀烒煶䊓実寔宩冟襫襹褷䙾實祏視鉽釶鉐鉂䤱鮖鰣鯴鰘鰤鶳鉇鉃㹬㹝獅鈰鍦㹷銴弒揓栻枾釃榯榁柹㮶簭遾舓秲徥師釋釈笶籂箷竍䦠嬕姼餝䭄蝕餙飾飠䌳絁試詩諟戺䗐䛈適謚諡識",
|
||||||
|
"shou": "手受收首守授售寿瘦兽狩绶扌艏膄壽夀垨涭獣㖟獸㥅収㝊鏉龵痩䭭綬䛵",
|
||||||
|
"shu": "术数书属树述熟输束殊叔朱舒鼠疏署竖蔬抒枢淑暑薯梳俞蜀庶赎塾墅恕曙倏漱黍腧戍孰澍秫菽纾疋沭摅姝殳毹荗䩳䩱㷂竪豎䜿䝂薥䔫蒁藷陎㽰毺䑕䞖霔尌朮怷璹琡䜹㻿尗虪濖瀭潄潻㳆鼡㶖蠴暏䠱踈䟽咰數軗輸㟬贖䝪䎉疎屬庻糬襩裋襡䘤䆝鏣鮛鱪鱰錰鉥掓攄捒樞樹橾㯮杸䴰鶐䉀䢤術癙㾁書㛸婌㜐㣽鵨鄃侸跾倐儵焂㒔紓綀絉䃞",
|
||||||
|
"shua": "刷耍唰㕞誜",
|
||||||
|
"shuai": "率衰摔帅甩蟀帥䢦卛",
|
||||||
|
"shuan": "拴栓涮闩䧠腨閂",
|
||||||
|
"shuang": "双霜爽孀骦騻驦礵䫪鷞㼽㦼塽鹴鸘漺灀䗮䡯慡鏯欆艭㕠孇雙縔",
|
||||||
|
"shui": "谁水睡税说氵脽氺涚涗帨裞祱稅閖㽷䭨誰",
|
||||||
|
"shun": "顺瞬舜吮蕣䑞鬊䀵瞚䀢順㥧橓楯",
|
||||||
|
"shuo": "说烁硕朔数妁蒴铄搠槊矟碩䀥爍鑠獡鎙欶箾䌃説說",
|
||||||
|
"si": "思四死斯似司丝私饲寺撕祀肆嘶嗣厮俟泗咝巳鸶蛳驷锶汜伺食厶耜兕澌笥姒缌纟蕼䔮蕬㹑㸻牭騃駟騦磃蟴䏤鼶貄亖耛䎣㺨肂洍涘洠瀃泀泤㴲蟖螄㕽噝罳㟃孠覗廝燍䇁禗禩禠鈶鐁鋖鍶鈻鉰釲㺇銯虒枱杫梩柶楒㭒榹蜤㐌恖竢凘䦙䇃媤㚶㚸娰儩佀飔価俬颸飼飤緦糹㣈鷥絲",
|
||||||
|
"song": "送松宋颂讼耸诵淞嵩悚凇怂忪菘崧竦駷鬆硹濍㕬嵷憽㞞愯䢠庺梥鎹㧐㩳㨦檧楤㮸枩柗㣝䉥聳慫娀頌枀倯傱餸䜬誦訟䛦",
|
||||||
|
"sou": "搜艘嗽嗖擞飕馊薮螋叟溲瞍嗾锼蓃藪蒐䏂騪䮟鄋㵻㖩廋廀叜鎪獀捜擻摉摗醙櫢籔䉤䈹凁瘶傁颼䬒餿",
|
||||||
|
"su": "素速苏诉缩俗塑肃宿稣溯粟酥簌窣夙谡嗉僳愫蔌涑觫蘇莤藗䔎蘓㕖骕驌碿䃤膆塐趚甦殐璛珟玊溸㴑㴼泝潥潚㴋洬㬘囌蹜憟䘻㝛鯂穌鱐鋉䥔㨞㩋榡遬㔄樎櫯梀㯈樕䅇橚䑿愬遡㪩肅䎘鷫嫊鹔䏋粛㜚㓘傃㑛餗㑉縤䌚䛾謖訴",
|
||||||
|
"suan": "算酸蒜狻匴㔯祘筭笇痠",
|
||||||
|
"sui": "随虽岁碎遂髓穗隋绥隧邃祟燧睢荽濉谇䔹荾鞖䪎芕隨䢫鐆遀砕膸埣瓍㻟璲㻪㻽歲歳睟瀡㵦浽澻㴚滖哸䠔雖䡵嵗㞸䯝髄亗賥韢熣煫襚禭䥙鐩夊檖䉌穂䅗穟㒸嬘䭉倠綏繐繀繸䍁䜔旞譢誶尿",
|
||||||
|
"sun": "孙损笋荪狲飧榫隼蓀薞蕵孫飱䁚㡄㦏猻鎨搎損㔼槂簨箰筍鶽",
|
||||||
|
"suo": "所索缩锁梭嗦琐唆羧唢娑蓑挲些睃睃嗍桫䓾莏䂹䐝䞽趖琑瑣㪽䖛溹溑逤䣔暛蜶嗩䞆惢褨鎍鎖鮻獕鏁鎻挱乺摍䵀䅴䈗簔簑㛖傞䌇縮莎",
|
||||||
|
"ta": "他它她塔踏塌榻沓蹋嗒拓獭挞趿遢溻鳎铊闼鞳鞜䓠㿹牠䂿䶁䶀墖㳠㳫澾涾毾躂躢蹹嚃嚺㗳䵬䍝遝崉䎓粏褟祂禢錔鰨鮙鉈㺚㹺獺狧撻㧺搨榙橽㭼㯓䑜䍇㣵濌䈋䈳㣛闧闥闒阘㛥侤㒓傝䌈誻䜚譶",
|
||||||
|
"tai": "大太态台抬泰胎苔汰钛酞肽薹骀邰炱跆鲐䑓菭孡態㣍駘夳冭坮臺㙵溙汏汱㬃旲㘆囼㥭忲䢰燤炲㷘㸀鈦鮐擡檯䣭䈚籉箈舦嬯㒗㑷㑀儓颱",
|
||||||
|
"tan": "谈探弹碳坦叹滩炭摊坛贪谭潭痰毯瘫檀坍袒覃忐昙钽澹郯锬藫歎菼䕊䃪貚䏙䐺墵䞡壜埮墰㽑壇璮灘潬湠曇暺嘆嘽啴嗿惔憛憳憻顃㲜㲭㷋燂䊤襢䆱鉭錟擹攤醈醓醰橝榃舑舕罎罈䉡癱䦔痑婒怹倓僋貪談譚䜖譠",
|
||||||
|
"tang": "堂唐糖躺汤塘倘趟烫膛淌棠搪螳蹚羰傥溏帑醣耥瑭螗铴镗樘鞺薚蓎隚䧜磄膅鼞赯矘漟燙湯坣䣘劏曭蝪踼䟖嘡啺戃糛爣糃煻鄌㲥鶶㼺禟鐋鏜钂鎲镋鎕㿩摥㭻橖榶䉎篖䅯闛㜍伖㑽儻㒉偒傏饄餹䌅㙶",
|
||||||
|
"tao": "讨套逃陶桃萄掏涛淘滔叨韬啕绦洮饕跳鼗鞱鞉鞀㹗騊駣㚐夲瑫㴞濤蜪飸咷轁幍慆韜裪祹迯鋾匋搯槄醄䵚嫍絛䬞饀䬢弢縚綯绹縧詜謟討䚯䛬",
|
||||||
|
"te": "特忑忒慝铽脦蟘㥂鋱㧹",
|
||||||
|
"teng": "腾疼藤滕誊䕨虅驣䲢幐縢螣騰鰧謄膯鼟霯漛䠮熥籐籘䒅䲍駦痋邆儯",
|
||||||
|
"ti": "体提题替梯踢蹄惕啼剔剃涕屉嚏锑棣倜悌鹈逖醍缇绨䪆䔶薙蕛䧅㯩騠髰鬄鬀碮厗朑䨑趧趯䎮瑅殢瓋睼漽渧題鶗惖逷㗣嚔蹏鷤嗁㖒罤䯜體骵䝰㡗崹惿屜褆䙗褅禔禵䚣鳀鯷鷈鮷悐銻鍗䴘鷉掦挮揥擿笹䣽㬱䶑䅠躰軆徲籊稊㣢䣡䶏鵜媞偍䬾緹䌡綈䛱戻謕歒鶙弟",
|
||||||
|
"tian": "天田甜填添佃恬腆舔阗钿畑忝殄畋掭菾黇磌碵䩄胋鷏㙉甛塡靔靝瑱㐁琠璳兲睓沺淟湉晪䟧䠄唺㖭䡒䡘鴫䐌覥觍賟悿屇㥏㶺窴錪䥖搷㮇䣯酟䑚舚䄼䄽闐痶婖倎餂鷆緂㧂甸",
|
||||||
|
"tiao": "条调跳挑眺迢窕苕佻笤啁粜髫龆蜩祧鲦䒒萔芀蓚蓨糶聎䯾朓趒齠晀旫䟭㟘脁岧岹恌庣宨窱祒覜鰷䱔樤㸠䠷䎄䳂嬥鞗䩦䖺鯈鋚鎥條絩誂",
|
||||||
|
"tie": "铁贴帖餮萜聑驖䵿蛈呫貼怗鐵鐡䥫銕鉄䴴僣飻",
|
||||||
|
"ting": "听停庭挺厅廷亭艇烃婷蜓汀霆町铤葶莛梃鞓聴聽聤厛鼮脡䵺圢耓珽涏渟䗴蝏甼嵉聼廰廳烴庁烶䱓鋌㹶邒桯榳楟頲颋筳䦐閮娗侹䋼綎誔諪",
|
||||||
|
"tong": "同通统痛童铜筒桶桐佟侗酮捅瞳僮彤潼嗵恸峒茼砼仝蓪㼧㪌䂈䮵犝朣赨眮浵晍蚒䳋曈哃㠽峝峂㠉膧慟㤏烔粡庝炵燑䆚䆹鲖鉵銅鮦狪獞鉖樋㮔橦筩憅㣠秱㣚衕穜䶱勭氃䴀㼿痌㛚㸗餇絧統綂詷",
|
||||||
|
"tou": "头投透偷愉骰亠蘣斢黈䞬頭㰯䟝㖣㡏䵉㢏鋀䱏鍮㪗敨婾媮妵㓱偸紏緰㕻䚵",
|
||||||
|
"tu": "图土突途徒吐涂兔屠凸秃荼钍菟堍酴蒤葖莵鷋駼鼵迌腯㐋堗圡瑹㻬㻯㻠㻌䖘汢潳涋湥塗跿䠈唋圗圖図嶀㟮䣝鷵怢悇廜庩宊鶟鈯釷鵵鵌鍎鋵揬捸捈䤅㭸梌䅷馟兎禿稌筡鵚瘏痜凃䣄峹嵞䳜",
|
||||||
|
"tuan": "团湍疃抟彖䵎貒墥剸鷒漙湪団䵯團畽圕慱䊜糰煓褖鏄鷻猯摶㩛槫檲篿䜝揣",
|
||||||
|
"tui": "推退腿颓蜕褪忒煺藬蓷蘈隤駾㞂尵㦌䍾䀃㱣螁蛻蹪蹆骽㷟㢈㢑魋橔頺䅪頹䫋頽穨㿉㾯㾽㿗㾼弚娧俀僓弟",
|
||||||
|
"tun": "吞屯豚臀囤褪饨鲀氽暾芚朜霕坉㼊豘涒旽蛌㖔噋黗軘臋忳㞘焞魨㹠㩔呑飩",
|
||||||
|
"tuo": "脱托拖妥拓驼陀唾椭驮沱砣鸵佗坨跎箨柁柝橐沲鼍庹酡乇䓕萚蘀莌阤嶞陁馱駄䭾驝騨驒駝馲駞㸰㸱毻碢砤鼧鵎脫堶槖沰汑涶跅鼉咜咃䡐㟎岮䪑袥袉㼠饦䲊鰖鮀鴕魠䰿鮵狏扡拕捝挩橢楕杔䴱彵籜䍫㾃嫷媠毤侂仛侻飥紽詑託讬",
|
||||||
|
"wa": "瓦挖娃哇蛙凹洼袜佤娲腽韈聉䎳砙膃劸㰪鼃䵷邷漥溛咓䠚嗢嗗畖㼘韤襪窐窪穵窊㧚搲攨屲瓾媧䚴",
|
||||||
|
"wai": "外歪崴呙㖞喎咼䶐䠿顡竵",
|
||||||
|
"wan": "完万晚玩湾弯碗顽挽烷婉皖蔓腕丸宛惋蜿豌绾纨莞剜脘畹塆菀芄琬箢薍萖萬㿸䂺䩊脕埦頑㝴刓壪琓瞣睕澫涴潫汍灣蟃晥晼晩踠唍輐輓贎䯈岏貦帵贃䝹忨卐卍翫䗕䘼䖤盌㽜鋄錽䳃鋔䥑鎫抏捖捥杤椀梚䅋笂妧婠倇㸘綰綩紈䛷彎",
|
||||||
|
"wang": "往王望忘亡网旺汪妄枉惘罔辋辋魍朢菵莣尪迋尫瀇㲿㴏㳹蚟蛧蝄暀罒輞罖罓㓁䤑棢徍彺䰣徃兦仼亾尣尩䋞䋄網䛃誷",
|
||||||
|
"wei": "为维围委未微谓卫味唯威危伟尾违伪慰魏喂胃纬畏韦惟苇萎尉蔚巍薇偎帷娓渭桅圩倭痿崴猬诿猥潍煨葳韪帏嵬玮逶炜隈隗洧涠沩囗軎鲔艉闱位䔺蔿䪋苿菋蓶葨䵋荱藯葦蘶芛蒍隇䧦䮹熭碨硙䃬㞇硊㕒䑊腲鄬爲䙿壝墛霨䞔霺䝐瑋㱬琟覹矀濻潙韑瀢渨潿溦浘湋洈濰溈蝛㬙韙蝟暐蜲蜼喴踓㖐喡䡺轊囲䵳罻圍㠕骩骫骪幃嶉嵔㟪峗峞嶶崣屗㞑叞褽犚螱㷉韡䪘韋違㥜愄愇懀燰烓煟煒寪頠鏏厃鳂鳚鍡鍏鮪鰄鮇鰃䲁鮠䥩撱㨊揻揋捤㧑楲㭏醀椳欈梶椲䈧㣲徫躗躛㦣衛衞䘙讆讏䉠覣犩䭳痏闈癓媙媦媁儰僞偉䬑颹䬐饖餵䬿餧偽縅緭緯㢻維䗽詴亹斖䜜謂䜅為諉",
|
||||||
|
"wen": "文问温闻稳纹吻蚊紊瘟韫雯汶刎璺阌鞰莬芠䎽駇馼鼤脗肳塭豱瑥䰚殟珳渂溫㳷昷㗃呡㖧呅輼辒轀蟁炆顐㝧鳁鎾鰛鰮魰揾搵抆榅榲桽穩穏䎹聞閿闅䦩閺問闦瘒妏㒚伆饂繧紋彣䘇螡蚉㐎鴍鳼",
|
||||||
|
"weng": "翁嗡瓮蓊蕹聬㹙㹚䐥䤰塕奣瞈滃暡螉㘢嵡䱵鎓攚齆䈵㜲勜鹟鶲罋甕",
|
||||||
|
"wo": "我握窝卧沃涡斡蜗喔倭挝龌渥莴幄硪肟臥萵䰀臒腛㦱瓁㱧齷䁊瞃濣渦涹蝸䠎踒唩㠛焥窩猧捰捾㧴枂楃婐媉婑仴偓",
|
||||||
|
"wu": "物无五务舞武屋误恶午吴伍污乌雾悟吾呜侮唔巫勿梧诬捂晤兀於芜戊毋鹉妩钨邬坞蜈婺鹜忤骛牾庑杌亡芴阢鼯圬浯鋈怃焐寤迕痦仵莁靰蘁茣蕪鹀鵐陚䎸隖奦務㡔嵍熃騖鶩䳱敄䮏鴮碔矹䃖䑁㬳霧雺霚塢墲鵡珷珸郚㻍㐚逜㐏忢瑦卼玝璑瞴洿汚汙洖溩㵲潕螐旿蟱䟼躌吳呉嗚䡧䍢峿屼嵨岉剭悮悞憮乄熓粅廡㷻窏窹祦鋙铻鄔鯃烏鰞歍鎢㹳扤摀㐅杇啎無鷡橆甒鼿齀箼䒉㽾䦜䫓䦍娬娪嫵娒倵俉㐳儛㑄弙䳇誣誈䛩誤譕",
|
||||||
|
"xi": "系西细习息吸喜戏析希席洗稀惜悉袭腊溪媳牺锡嘻夕隙晰栖膝熙昔烯熄禧鳃徙嬉犀蟋奚兮曦蜥汐翕玺唏螅铣淅硒皙熹窸羲矽檄郗忾僖屣歙樨觋娭豨咭葸菥蓰隰鼷舄浠粞裼穸禊饩欷醯舾阋㐂葈蕮蒵䩤䓇匸煕蓆莃薂蒠覡隵隟䧍䢄枲騱驨騽䮎犔犠犧磶磎礂䲪䙽㚛䐼䏮貕舃肸肹谿䫣㙾霼趘䨳趇欯囍憙歖霫赩赥豯卌琋壐璽瞦䀘鬩戲䖒矖戱卥戯睎盻覤㳧澙渓潟鸂虩漝㵿漇潝螇暿蟢蠵晞嚱躧蹝呬㗩㕧焁唽噏喺繫黖㽯嵠巇㠄嶍酅㔒忚㤴慀恄憘㤸怬屃屓屭㥡㦻悕習飁恓㞒屖焟熺糦㸍焬熂燨爔熻邜鐊觿觽觹鳛錫鑴饻鱚鰼鯑鉨釸鈢㹫㺣鎴釳鏭狶鉩扱鵗㩗忥氥扸墍㯕榽䙵橲槢桸晳惁椞㮩㭡厀椺橀怸熈㷩稧徯㣟䈪郋鄎徆襲㿇凞瘜闟䊠㜎衋嬆傒翖俙㑶係饎餼餏郤豀縘繥緆細縰綌绤謑䜁譆諰焈謵䛥䜣䚷洒蹊",
|
||||||
|
"xia": "下夏吓狭辖霞峡瞎厦虾暇匣唬遐侠黠呷瑕罅狎瘕硖柙蕸陿陜䖎騢硤碬磍夓埉圷㙤赮丅乤珨睱䖖虲蝦㗇㽠㘡翈轄峽懗䫗㰺䪗舝炠煆烚鶷䘥祫鎼鏬鍜魻鰕鎋狹梺筪敮舺閕䦖疜閜傄俠颬谺縖諕䛅",
|
||||||
|
"xian": "现先线显限县鲜险献宪陷仙闲纤腺弦贤嫌掀咸衔羡掺涎娴见酰舷藓馅锨铣冼霰暹籼苋痫氙蚬岘莶燹跹跣祆猃筅鹇藖韅䁂賢贒莧䵌㔵蘚䒸䕔薟苮䧟䧋䧮陥険險礥䃱尠䃸臔姭䏹鼸毨胘韯壏塪赻䨘垷埳䨷現豏珗䶢䶟獻睍縣鹹県盷瞯涀灦㳭瀗㶍㳄鍌㵪澖湺䝨尟㫫晛蜆䗾顕䘆㬗蛝顯㬎蚿㘋咁咞嘕哯蹮躚啣㘅嗛輱䞁幰峴㡉崄嶮㦓忺憪憸糮粯廯䵇烍㡾麙鶱憲褼襳禒鑦臽䚚䀏鋧䥪䱤鱻䲗鮮銽錎䤼鍁銛铦銑獮玁狝㺌獫㧋搟攇㩈㧥撊撏挦攕㮭醎枮櫶杴㭠橌橺麲㭹㯀䉯䢾㪇箲馦秈銜䉳衘稴屳閒鷳羨鷼閑鷴㜪䦥㿅癇癎甉㛾娊奾嫺嫻嬐孅娹妶仚僊僲僩僴餡韱佡伭綫纎繊線缐絤㢺纖婱絃諴誢䜢譣誸洗",
|
||||||
|
"xiang": "想相象向响像项乡降香羊享箱祥详湘橡翔巷厢镶襄饷骧芗飨衖葙蟓庠鲞缃缃項瓨䔗萫䢽薌驤䐟膷䜶珦瓖晑䖮曏跭㟟嶑㟄䊑廂麘襐勨鱌鱶鱜鐌銄鑲栙楿欀缿稥忀鮝鯗姠佭餉饟緗鄊蚃鄕郷鄉蠁響嚮㗽饗絴纕亯㖜㐔詳",
|
||||||
|
"xiao": "小消笑效校销削晓肖硝萧孝啸潇俏嚣哮淆宵箫霄筱逍骁姣枭哓鴞蛸崤魈枵绡绡䒕虈䕧䒝蕭藃驍硣膮斅斆㬵毊瀟揱涍㕾敩洨蠨蟏暁曉蟂蟰嘵嘋鸮踃嚻囂呺嘐㗛咲嘯嘨髐髇憢㤊恔庨焇灲熽䊥灱宯窙銷鴵䥵梟㹲猇獢郩殽皢皛撨櫹穘鷍筿簫簘篠痚痟効㔅歗婋虓侾翛㑾烋颵俲傚綃彇謏誟歊誵訤詨",
|
||||||
|
"xie": "些解写协谢械鞋斜谐胁泄歇邪契携卸屑泻蟹懈挟蝎偕楔勰亵燮鲑撷颉榭邂缬澥瀣廨躞叶薤渫獬榍绁靾鞢鞵䕵䩧䢡藛薢䕈䔑㔎㕐絜脅脇劦膎協㙝奊翓塮暬垥瑎齛齥齘禼卨䪥韰㱔㳦洩㴮瀉㵼㴬㴽㳿蝢旪蠍蠏㖑嚡噧㖿嗋䵦䡡峫嶰屟恊愶屧㞕㥟㦪灺緳熁燲糏炨炧䊝冩寫㝍褉䙎襭䙊祄㙰䲒䥱䥾猲揳挾拹㨙擷攜㨝烲焎娎㩉㩦擕㩪㰔䉣缷徢齂㣯䉏㣰䦑㸉㓔䦏媟孈脋伳偞偰龤㙦㒠㰡僁䭎紲緤綊纈絏縀繲絬衺䚳䙝褻讗爕夑㽊謝䚸諧血",
|
||||||
|
"xin": "心新信欣辛薪锌芯馨鑫衅昕訢忻莘炘歆囟忄镡䒖阠孞馸舋釁脪盺噷噺軐惞廞焮襑鈊䰼鐔鋅邤㭄杺枔馫顖嬜妡㛛㚯㐰伈俽伩䜗䚱訫䛨",
|
||||||
|
"xing": "行性形兴型星省幸醒刑姓杏猩腥邢惺悻荥陉擤荇硎饧䓷莕葕陘骍騂臖興㐩㓝㼬㙚垶㼛郉瑆䣆䁄睲涬洐蛵曐哘䳙煋滎㝭觲觪䤯鈃钘鉶铏銒鋞鯹鮏㨘䰢皨㮐䂔㣜箵篂㓑嬹婞娙倖侀餳緈䛭謃",
|
||||||
|
"xiong": "雄兄胸凶熊汹匈芎熋䧺洶焽焸哅賯恟忷夐敻胷匂兇詗诇詾訩讻㐫",
|
||||||
|
"xiu": "修秀休袖臭羞绣朽锈嗅溴貅岫咻宿髹庥馐鸺苬髤脙璓臹珛㱙琇潃滫螑嚊㗜峀糔烌鱃鮴鏥銹鏽鎀鏅銝樇齅㾋脩鵂俢飍饈綉繡繍褎褏",
|
||||||
|
"xu": "许需须续序虚徐绪叙蓄吁絮婿嘘旭栩墟畜浒戌胥圩恤煦蓿酗顼诩魆洫盱砉溆勖糈醑芧蕦藇藚㰲蒣聓䔓㜿䦽㞊䳳㷦㕛㐨䂆驉㚜㦽鬚䢕盨媭嬃須㘧壻垿珬頊珝殈㺷瞁虛歔虗汿沀㵰湑潊漵朂晇暊勗旴冔蝑昫㖅噓㗵呴喣盢㞰賉怴㤢㥠慉燸烼歘欻烅裇䙒禑銊鑐欨鱮䱬獝揟魖䣱䣴楈槒聟䅡鄦卹䘏欰稰稸疞㾥䦗䍱姁㜅㑔㑯敍敘伵偦䬔侐俆䋶續続緒緖縃綇䜡訏譃諿詡諝谞訹許䛙休邪",
|
||||||
|
"xuan": "选宣旋悬玄喧轩绚眩炫渲漩暄萱癣煊镟璇县碹泫铉揎楦痃儇谖萲䩰鞙䩙蓒蕿藼蘐蔙䧎駽䮄塇璿琄瑄琁玹懸睻眴矎贙䁢㳙㳬晅昍蠉暅蝖蜁暶昡咺䠣吅軒翾䴉㘣䍗䝮愋懁選愃怰烜翧䘩袨禤䚭䚙鋗䴋鰚䲂鍹㹡鏇鉉㧦楥梋檈箮衒䍻癬㾌媗嫙颴弲繏絢縼諼譞諠䗠䲻券",
|
||||||
|
"xue": "学血雪削穴薛靴谑踅噱鳕泶蒆鞾茓辥膤學觷壆澩嶨燢鷽䨮趐坹瞲㔧辪㶅瀥峃鸴㗾㖸吷轌㞽㡜岤䎀袕鱈䱑狘㧒㿱乴樰䤕桖艝疶䫻䬂䫼䭥斈謔",
|
||||||
|
"xun": "训迅寻循讯巡询旬逊驯勋熏汛殉荀薰峋洵浚鲟徇浔醺窨荨埙巽蕈孙曛恂郇獯蘍薫愻遜馴駨顨奞毥臐壦攳坃塤壎殾燅珣璕矄潠潯畃䖲蟳勛噀嚑噚䞊卂巺㽦爋燻燖䙉㝁迿㰬鱏鱘鑂狥㨚灥揗㰊杊栒樳桪稄勲勳鄩尋廵焄㜄侚伨偱㒐䭀紃䋸纁㢲訓訊詢䛜訙",
|
||||||
|
"ya": "压呀亚牙雅芽鸭押崖哑鸦讶丫涯轧衙娅伢蚜桠氩垭碣琊疋迓邪砑睚吖岈揠痖蕥䪵鴉聐孲厊圧厓䃁壓厑䝟劜堐埡圠玡亞鵶䢝㰳亜襾齖齾漄啞唖圔䵝軋鴨崕䯉㿿庌䊦庘㝞窫錏鐚铔䰲犽猰猚㧎掗氬挜枒椏覀笌䄰稏䅉冴疨瘂䦪婭俹訝",
|
||||||
|
"yan": "眼研验言严演烟沿盐延颜岩炎燕掩厌艳咽焰铅宴衍殷阎雁淹砚檐焉彦蜒俨奄谚腌堰晏胭嫣阉湮筵兖妍偃唁鼹恹琰赝魇滟酽焱餍甗郾菸厣埏鄢罨崦剡闫谳讠鹽匽鶠䕾酀㬫鷰㷼䴏嬊莚萒蔅䓂隁隒驠騴騐験驗牪硽黡䊙揅硏硯夵魘厭厴懕黶檿嬮饜䣍剦礹䂩鳫贗鴈贋㷳䶮䂴臙䑍鼴墕壧䎦䀋塩壛㿼䢥珚琂齞齴䖗鬳䁙覎䀽虤沇厳漹灔灎灧灩淊溎渷㶄㳂渰蝘曣㦔猒䗡暥曮鷃曕妟䳛昖㫟嚥嚈囐嚴碞喦嵒㘙啱㗴喭㘖噞黭黫黬黤艶艷豓豔巘巚巌嵓巖巗觃嵃嶖愝懨熖㷔焑敥炏焔煙烻㢂爓㢛麣戭褗裺鴳䄋䤷觾燄鰋䲓䱲狿抁揜椻㭺歅醼醃釅醶欕棪樮椼櫩楌篶郔䗺躽軅簷䅧䇾閆閹龑䢭兗乵閻顔遃㿕嬿㛪姸孍㚧姲娫娮傿弇顩㕣儼偐䭘酓㓧䳺䨄縯䊻綖䌪讌䜩顏彥訮詽讞扊諺㫃訁",
|
||||||
|
"yang": "样养阳洋氧央杨扬羊仰秧痒漾疡佯殃鸯怏鞅恙徉炀暘泱蛘烊陽阦駚礢胦䑆霷雵坱垟珜䁑眏眻瀁䬗昜敭蝆䖹旸㬕咉䵮輰軮㿮崸䒋鴦崵岟㟅懩慃煬炴鍈卬鍚鉠钖鰑㺊氜揚氱抰㨾攁楧鸉楊柍様樣䇦劷羏㔦羕飬養瘍鴹癢姎佒飏颺䬬䬺䭐傟紻諹詇详",
|
||||||
|
"yao": "要药摇腰咬耀遥邀瑶姚窑妖谣钥尧么乐吆肴夭侥舀幺徭珧杳窕窈鹞繇曜爻约轺崾鳐䔄蘨靿薬藥葽蓔苭葯騕磘㞁䂚䍃颻鷂飖尭垚顤堯瑤殀䋤䶧齩䁘㔽矅覞䁏眑㴭溔滧㵸㿢暚䖴㫐嗂喓鷕軺峣嶢嶤岆㟱愮熎燿烑㢓䴠宎㝔䙅袎窰䆙窅䆞穾窯窔祅鎐鰩鱙猺遙獟狕䚻䢣䌛邎揺抭搖㨱摿榣柼㮁楆枖榚鴁鼼䉰筄䑬艞㿑闄媱婹傜倄偠仸䬙餆餚鴢䌊䋂纅謡謠訞㫏䚺讑詏疟",
|
||||||
|
"ye": "也业夜叶液爷野喝页冶耶咽邪拽曳腋椰掖噎晔谒揶射邺靥吔烨铘䓉葉枼䧨驜靨擪㪑頁礏墷枽㙪㐖璍瑘殗瞸瞱潱澲漜洂曄曅蠮暍曵曗嘢㗼㖶㖡㙒嶪嶫燁煠㥷爗鄴鸈業㱉㝣鐷鋣釾䥺鍱鎁䤶鎑馌䲜䥟䥡䤳擛皣捓抴擫歋㩎捙擨㭨壄埜䈎㸣僷倻爺䭟餣饁謁亪亱鵺",
|
||||||
|
"yi": "一以义意已艺易议咦依益衣异医移遗疑亦宜仪忆伊倚乙亿抑役毅译椅翼姨蚁泄谊疫逸矣溢夷疙绎尾蛾怡胰贻裔彝邑奕翌屹臆颐诣驿熠咿蜴漪沂呓揖弋轶迤懿悒佚羿噫铱弈壹肄翳癔缢刈旖苡怿痍猗诒峄食射荑薏埸圯殪眙嗌黟嶷嶷衤饴钇镱镒挹酏劓舣瘗翊仡佾蘙芅匜䩟藝蓺虉弌頤巸媐䖁䓃㔴䔬苢勚勩萓苅殹㙠醫鹥瞖繄䗟贀悘鷖黳嫛毉瑿萟䓈藙䓹䕍䬥隿耴迆阣䧧㹓瓵䮊驛駅䭿逘礒䝝帠肊䐖䐅鶂膉貖䝘敼㰻霬墿夁亄㦤鷧㱅壱坄㙯埶㺿玴珆豷豛䰙鹝鷊辷㱲殔鴺乁頥齮齸頉㵩浳㶠渏沶㴁洟浥潩㳑瀷㲼泆浂澺洢㵝㴒湙曀蛦晹䗑曎螘蛡敡鶍螔蟻䗷螠蛜暆囈呭㘊跇遺跠㖂唈㘁呹吚㕥㘈異欭輢黓睪斁歝圛軼轙畩貤貽䞅骮䯆顗峓幆嶧䝯嶬崺怈㦉恞㠯䎈郼䢃懌乛㞔㰝㥴忔攺憶㡼廙熼燡㢞熤燚熪燱炈庡焲宧冝宐㝖襼袣䘝衪裿褹袘寱䘸䆿迱寲䄁祎禕釴鈘釔鉯䱌鶃鮧鯣䱒鳦鸃䲑鮨鏔匇迻狋㹭獈鐿鎰鈠銥撎䖊㣻拸乂㩘枻杙杝槸䣧醳醷桋栧椬栘柂檥檍榏枍䴬㰘椸檹棭䄬劮鄓㓷䇵䄿䇩穓顊稦笖簃乊䉨艗艤秇垼篒籎瘞瘱豙䴊義羛羠鷾痬䦴竩兿鹢鷁嫕㚤㜒嬄㚦㛕彛彞嬟嬑㜋㛄佁侇㑥俋伿㐹㑜䬁儀億飴饐䭂䭞䬮䭇伇㥋偯㑊弬㣂㡫䋵繹䋚䌻彜觺㽈繶縊讛詍裛詒旑訲讉譯㦾扅悥扆訳帟誼誃謻㫊議譩詣蛇",
|
||||||
|
"yin": "因音引印银阴隐饮姻吟殷荫淫尹茵寅蚓瘾龈垠胤喑氤窨鄞吲圻狺铟茚霪堙洇廴夤夤蘟蔭䕃鞇靷荶蔩䓄蒑䒡隱檃櫽隠阥陻隂陰骃駰㹜碒磤㕂㥯㸒䨸霒趛赺韾堷霠烎璌殥慭珢齗齦龂䖜濦滛濥垽㴈峾溵乑湚泿洕朄螾蟫噖嚚㖗噾䡛囙輑圁嶾湮㡥崟崯㞤訔㦩懚㥼愔廕粌㝙㪦冘裀禋䤺䲟淾銦鮣犾鈝㹞銀鈏㼉㧢斦慇㐆㧈檼垔䤃酳鷣栶檭猌㙬憖憗筃秵㣧䇙癮癊䪩㾙闉凐瘖訚誾婬婣飲侌㱃飮䌥絪緸讔䚿諲訡",
|
||||||
|
"ying": "应影英营映迎硬盈婴鹰颖赢荧蝇莹莺樱瑛萤鹦萦缨膺瀛荥璎嘤媵罂瘿茔楹郢滢颍嬴景蓥潆撄萾㲟鶧蘡藀䕦盁孾碤礯䃷朠膡䑉霙䨍珱瓔㼆䁐䀴鷪渶溁溋㵬浧㴄瀴濴瀠㶈瀯濙灐濚瀅蛍営鴬灜暎蝧蝿蠳蠅嚶甖巊鑍鸎罌嬰鸚賏譻巆愥煐㢍廮応罃褮塋䁝禜縈螢䪯營熒鶯覮鎣嫈瑩甇謍鶑噟應鷹譍䙬锳鐛鱦䤝㨕攖摬攍桜梬櫻櫿矨軈籝籯韺癭㿘媖孆偀僌㑞䭊䭗緓绬頴颕潁纓㯋穎贏",
|
||||||
|
"yo": "哟唷喲",
|
||||||
|
"yong": "用永勇拥涌庸泳佣咏雍踊蛹臃俑甬壅鳙恿痈邕喁慵湧墉镛饔苚蒏㦷勈硧砽惥埇䞻塎㙲慂滽㴩灉澭顒颙䗤踴嗈噰㞲嵱愑悀愹怺㶲醟鄘鷛廱彮㝘鲬鯒鱅鰫鏞郺擁柡栐㷏牅癰癕雝嫞傭㽫詠",
|
||||||
|
"you": "有又由优油友游右幼尤犹忧邮幽诱悠铀佑黝柚囿蚴酉釉疣猷莠攸祐鱿繇鼬蚰牖呦莜莸尢卣蝣宥铕侑苃䒴蕕䢊聈牰駀䀁鄾迶憂䳑肬貁䞥耰丣㻀逌䚃瀀沋㳺湵滺浟泑蜏䖻哊嚘㕱唀㘥輏輶㽕峟甴峳懮㤑怞怮庮麀䆜禉銪鲉鈾魷鮋䱂㹨狖㺠猶逰㮋栯櫌櫾酭梄槱楢郵怣牗㰶秞䅎䑻㕗羑㾞羪姷㚭優佦㒡㛜䬀偤纋孧㓜訧亴䛻遊誘䢟㫍",
|
||||||
|
"yu": "于与语育鱼余雨预域玉遇予欲宇愈渔誉郁羽狱御裕愉豫愚喻娱寓浴吁舆尉榆俞禹屿淤逾峪谕於迂虞瘀驭芋隅渝瑜阈毓盂汩熨禺腴揄臾煜钰彧鹬鬻谀馀聿纡竽伛龉觎圄欤妪玙邪蓣萸舁雩蜮昱蝓圉嵛庾庾燠窳窬饫狳瘐妤肀俣鹆蕷蘛㔱䩒芌蕍茰蒮䔡䖇萭薁蓹蘌茟匬萮陓隃䂊矞預鷸遹䮙驈馭䮇騟䂛戫礖砡㝼礇䃋硢硲䏸礜轝㦛鸒歟與譽輿䐳雤貐斞霱堣䨒迃亐圫䨞堬堉琙璵邘㺮㪀玗䢩敔䜽鳿瑀齬齵鸆䁌䲣䱷䁩睮歶淢㳚潏滪澦㳛盓澞湡漁灪淯㶛虶㬂䗨欥㬰蜟噳踰喅喩唹罭㽣輍骬嶼㠘髃嵎嶎䍞㠨崳惐䣁忬頨懙㥚㥥㤤㥔燏㡰粖庽㷒爩麌焴䢓㲾䴁䵫寙㝢䙔衧褕䆷穻鴧鴪䄏祤鈺鍝魣鱊鰅鴥鷠䰻魚鮽鯲㺞鐭䥏㺄獄銉鋊錥㼌扝扜挧魊扵棫櫲桙楰醧杅酑鬰欎欝鬱楀楡棛棜稢䈅稶穥籅䍂䄨䘘牏鄅㙑軉秗禦䉛篽籞艅艈籲込箊閾瘉羭癒䘱嫗嬩㚥䢖媀娛娯傴伃僪㒜儥兪覦歈㼶悆雓俁㑨㒁䬄偊饇飫餘螸慾鵒俼緎紆䋖㣃逳袬諛謣語斔䛕旟諭乻吾奥粥",
|
||||||
|
"yuan": "原员远元院源愿圆园缘援袁怨冤渊猿宛苑垣媛鸳辕沅爰橼塬鸢圜螈垸瑗鼋湲芫眢掾蒝薳䩩薗蒬茒葾鳶㹉䏍貟贠騵厡厵願鶢䳒䳣遠鼘逺邧䲮黿㤪盶溒渁鼝淵渆渕灁蝯蚖蜵蜎䖠蝝肙剈噮鶰員圎園轅囦圓㟶円㥳悁鹓惌鵷寃褑褤裫裷禐駌夗鴛妴鎱鈨魭鋺猨㭇榞榬杬酛棩櫞笎衏邍羱䅈䬇嫄媴嬽傆㥐䬧䬼䨊縁緣謜䛄䛇",
|
||||||
|
"yue": "月约越跃阅悦曰岳乐粤兑钥栎钺说刖瀹哕樾龠䖃戉蘥㹊玥䢲泧㬦蚏蚎䟠噦跀躍䠯啘䢁黦䡇軏岄曱嶽恱悅爚礿禴鉞鈅䥃鸑䤦鑰抈捳㰛籆矱籰粵籥篗箹閲閱嬳㜧妜㜰鸙䶳䋐約",
|
||||||
|
"yun": "运云允匀韵孕晕蕴芸陨酝韫耘恽纭熨愠氲筠郓郧殒员昀狁蕓䩵荺蕰薀蒀蒷蒕藴蘊阭耺隕馻夽奫磒腪䢵䨶䲰雲䞫霣㚃鋆殞齫齳眃沄澐涢溳蝹暈鄖䚋喗囩䵴䡝畇賱㟦㞌韗韞愪慍惲煴熉熅鄆運褞䆬鈗䤞勻抎氳抣枟橒醖醞秐䉙馧筼篔䦾䇖韻㾓㚺妘㛣㜏䪳伝傊餫紜緼缊縜縕贇赟",
|
||||||
|
"za": "杂咱扎咋砸咂匝拶䕹臜臢䞙䪞帀迊沯沞囋囃襍鉔魳桚韴雑雥雜",
|
||||||
|
"zai": "在再载灾仔栽宰崽哉甾䏁䮨載䵧烖㦲酨㱰睵渽溨洅㴓㞨賳扗畠䣬災傤儎縡",
|
||||||
|
"zan": "咱赞暂攒簪瓒錾糌趱昝㔆兂趲瓉鄼賛瓚濽灒噆喒暫蹔鏨㟛寁襸禶鐕鵤鐟撍攅攢揝橵贊簮㜺儧儹偺㤰饡㣅讃讚",
|
||||||
|
"zang": "藏脏葬赃臧奘驵蔵塟匨駔臟臓羘㘸贜贓髒賍賘弉銺牂",
|
||||||
|
"zao": "造早遭藻燥糟灶躁枣凿噪皂澡蚤唣薻䥣㲧趮栆璪璅䖣䗢蹧喿唕慥㷮煰鑿竃竈䲃皁醩棗梍簉艁䒃傮䜊譟",
|
||||||
|
"ze": "则责择泽侧啧仄赜咋昃帻箦迮舴蔶賾䕪䕉矠礋責齰䶦齚歵瞔㳻㳁澤溭沢滜泎汄蠌昗㖽嘖鸅幘則崱庂襗䰹皟捑擇択樍䇥簀㣱嫧諎謮",
|
||||||
|
"zei": "贼蠈賊戝鲗鱡鰂",
|
||||||
|
"zen": "怎谮䫈譖",
|
||||||
|
"zeng": "增综赠憎曾锃甑罾缯鬵磳増䰝璔囎㽪贈熷䙢鋥鱛橧矰鄫曽繒譄",
|
||||||
|
"zha": "扎炸眨渣闸喳榨诈栅札乍楂喋蚱柞铡咤查咋砟哳吒揸齄痄䕢䃎厏䞢耫霅㱜皻㪥㗬㴙溠䖳灹㡸宱觰鲊鍘鮓䥷抯摣紥挓搾拃柤醡樝皶蚻紮䵵牐齇劄箚䵙㷢閘鲝鮺偧㒀䋾譇䛽詐譗",
|
||||||
|
"zhai": "摘窄债宅寨斋翟砦责择侧祭齐瘵㡯鉙粂捚㩟榸檡夈債斎齋",
|
||||||
|
"zhan": "展战站占粘颤沾崭盏斩毡湛瞻栈辗詹绽蘸谵旃霑搌㠭菚虦盞䪌薝驏驙䩅氊趈䟋琖㻵虥䁴惉戦魙䗃蛅戰噡輾轏斬覱㟞岾嶃嶄嶦嶘㞡䎒䘺鳣䱠䱼鱣㺘棧桟醆枬榐栴橏䡀㣶閚嫸偡佔僝飐颭飦饘䋎綻詀讝氈鹯鸇邅譧譫旜",
|
||||||
|
"zhang": "长张章掌丈障涨帐仗胀账杖璋彰樟瘴漳蟑嶂鱆獐幛鄣嫜仉蔁騿礃脹墇㙣瞕涱漲暲㕩賬帳幥慞粻粀麞鏱扙痮㽴遧瘬傽餦張",
|
||||||
|
"zhao": "找照招召赵着兆昭沼诏朝钊肇濯啁棹罩爪嘲笊䮓駋㕚䃍㐍䝖爫趙垗瑵瞾曌㷖䍜羄燳㡽炤鍣釗鮡狣㺐鉊㨄櫂枛罀箌䈇䈃䍮㐒巶妱㑿佋皽肈肁旐詔",
|
||||||
|
"zhe": "这着者折哲遮浙蔗褶辙锗辄蛰蜇赭柘鹧摺螫谪著磔䩾䓆䎲㪿䮰䂞矺厇砓詟䐑䐲䏳喆嚞乽蟄謺䝕䝃歽淛蟅晣虴啫踷䠦嗻輙輒䵭轍㞏䗪鷓粍籷䊞襵袩銸鍺鮿埑晢啠悊㯰樜讋嫬這謫讁",
|
||||||
|
"zhei": "这",
|
||||||
|
"zhen": "真针阵镇振珍震诊侦贞枕圳砧斟疹臻甄祯桢朕赈帧榛缜箴畛稹填蓁胗溱浈轸鸩椹葴蒖䑐䫬薽萙塦陣聄㓄駗碪鬒䂧䂦㪛䏖䨯瑧殝珎遉貞眹眕㴨湞潧澵昣䟴辴轃黰甽軫賑帪幀䝩屒䲴寊䪴鴆裖袗禛禎鍼鋴針鎮錱覙鱵獉鉁鎭挋䳲揕搸抮㮳酙楨樼㯢栚籈姫嫃侲㐱偵弫䊶縥絼縝㣀眞紾紖纼誫診",
|
||||||
|
"zheng": "正政争整证征丁蒸症郑睁挣怔拯铮筝狰峥诤徵钲聇脀烝氶䂻鬇爭㱏埩靕鴊䥭睜眐塣晸踭䡕崢崝幁㡧㡠炡䥌鉦錚猙鏳掙揁掟抍撜愸篜箏徰䈣䦛䦶鄭㽀癥姃媜佂凧䋊䋫糽䛫証諍證",
|
||||||
|
"zhi": "之只制质知指直至志织支值致职止植置纸智执殖枝脂秩肢滞拓汁旨址稚芝吱帜蜘挚掷侄趾治识酯窒峙炙桎栉雉祗芷咫痣栀氏胝祇跖踯鸷蛭枳帙痔徵贽姪沚陟骘陟膣豸埴郅踬轾轵忮黹祉觯卮摭絷夂彘蘵芖䓌䛗䓜迣茋䓡藢䕌聀阯騭隲䏄㝂䎺職犆馶駤馽厔㕄砋礩䐭䐈䏯胑䑇乿膱墆鳷䧴坧䟈㙷覟墌疐坁垁漐縶贄慹騺鷙䥍摯執瓡驇臸瓆璏歭㫖淽滯滍汥洔淔洷㴛潌汦泜潪瀄晊蟙跱蹠躓躑㗌㗧㘉畤䡹輊軹豑豒剬䞃幟崻懥懫翐恉庤庢廌㡶熫寘衼襧衹袟禃祬祑帋觶觗䚦鋕銍铚䳅䱨鯯㩼锧鑕狾猘釞劧㨁貭搘挃㨖巵㧻抧摨搱扺扻劕質䭁擳擲梽榰㮹梔櫍椥柣櫛䵂栺樴㲛䝷鼅䵹鴙䅩秓徝稙憄䉅秷製䱥䄺䇛徏軄徴筫穉䆈稺秖䇽䉜㣥瘈痓䦯疻疷㜼娡㛿妷嬂値俧凪傂儨倁偫䬹隻綕緻䌤鴲紙紩織誌訨袠戠䫕旘",
|
||||||
|
"zhong": "中种重众终钟忠肿仲衷踵盅冢锺忪螽舯茽蔠刣尰鼨腫塚堹歱泈汷蚛蜙蹱喠眾幒煄炂衶衳祌銿鈡䱰鴤鍾狆㹣鐘㲴柊衆籦種㣫徸彸筗瘇妕媑妐偅伀㐺終螤諥蚣",
|
||||||
|
"zhou": "周州洲宙轴骤皱昼舟咒粥肘帚绉胄纣诌妯繇啁调荮碡酎籀䩜菷葤㔌䎻驟駎騆駲䐍䶇霌盩珘睭淍䖞晭嚋呪喌噣咮輖軸輈辀冑郮週㼙賙赒㥮粙炿烐皺鯞銂矪徟甃籒籕鸼鵃箒䈙䇠疛㾭晝婤㛩伷㑳㑇僽侜紂縐䋓诪譸詋䛆謅",
|
||||||
|
"zhu": "主住注助逐著宁筑诸珠猪竹朱柱祝驻株贮嘱煮铸烛蛛瞩竺蛀拄伫褚诛侏澍潴箸渚炷躅铢瘃苎术属茱翥洙麈橥杼槠邾舳疰丶茿莇藸蓫䕽苧蕏陼䎷逫馵䮱駯駐劯硃砫䐢墸䟉壴坾䬡煑䝒豬櫫矚眝㵭瀦濐灟乼蝫蠋曯蠾蠩跦跓囑鸀罜軴帾貯嵀䝬劚斸㤖㔉㫂燝燭爥炢麆㝉㿾䘢袾窋宔祩鋳鑄钃鯺鱁鮢䥮㺛銖㹥鉒拀㧣柷欘樦櫧笁篫築笜筯䍆鴸鼄篴䇧簗䇡秼㾻竚羜孎㑏佇䭖飳䰞䌵纻紵絑紸諸迬殶詝誅註",
|
||||||
|
"zhua": "抓爪髽膼撾檛簻挝",
|
||||||
|
"zhuai": "拽转跩",
|
||||||
|
"zhuan": "转专砖赚撰篆传颛馔啭蒃孨磚磗膞腞塼堟瑼鄟專甎叀専瑑蟤囀䡱転轉顓賺灷襈鱄篹籑䉵竱嫥僎饌縳諯譔",
|
||||||
|
"zhuang": "状装庄壮撞桩妆幢僮奘戆庒荘莊壵湷糚粧樁梉狀壯焋娤裝妝",
|
||||||
|
"zhui": "追缀椎坠锥赘惴骓隹缒墜騅硾礈腏膇贅沝畷䄌錣鑆鵻錐醊甀笍娷綴縋諈揣",
|
||||||
|
"zhun": "准谆淳屯肫窀埻迍準啍㡒宒衠稕凖綧訰諄",
|
||||||
|
"zhuo": "捉桌著卓着浊灼啄琢拙酌镯茁斫濯淖涿棹擢焯浞禚倬诼斮斲䕴䪼叕硺䶂龺圴斱琸鵫灂濁汋晫蠗啅罬斀劅㣿㪬蠿烵炪丵窡窧鐯鋜鐲㺟犳斵擆撯棳椓㭬槕櫡棁梲穱籗籱篧彴䅵穛娺妰諁諑謶鷟缴",
|
||||||
|
"zi": "自子资字紫仔姿滋兹姊籽咨孜渍梓髭恣滓谘淄呲孳鲻龇辎甾眦秭赀吱齐茈趑耔觜訾嵫锱笫粢缁芓蓻茡荢䔂茊葘菑茲孖牸矷頾頿胏䐉胾嗭赼趦鼒㺭剚鄑㱴㰷齜眥呰啙貲胔鈭㰣姕漬澬湽虸吇嗞輺輜崰䘣禌釨鰦鯔鎡镃鍿錙㧗杍橴榟椔秄䅆稵資栥秶㾅㜽姉鶅倳紎緇緕纃訿齍諮孶玆",
|
||||||
|
"zong": "总宗综纵踪棕粽鬃熜偬从腙葼蓗骔騌騣惣㹅鬉䰌碂磫朡堫䝋豵鬷昮蝬䗥蹤踨䍟嵏嵕嵸惾翪燪糭㷓糉㢔焧鑁鯮鯼鍐猔猣㚇揔摠搃捴㯶椶稯熧瘲疭倧傯倊綜緫緵總繌縦縱縂総緃",
|
||||||
|
"zou": "走奏邹揍陬鄹驺鲰诹菆郰棸騶赱㔿齱齺㵵䠫黀鄒鯫鯐掫棷箃緅諏",
|
||||||
|
"zu": "组族足祖阻租卒诅镞俎菹靻䔃蒩葅䯿珇䖕唨踤哫㞺崒崪䚝䱣鎺鏃爼椊䅸箤卆組䘚詛㲞㰵",
|
||||||
|
"zuan": "钻攥纂躜缵繤䂎躦鑚鉆鑽䤸劗籫纉纘䌣",
|
||||||
|
"zui": "最嘴罪醉咀蕞䮔厜璻蟕晬嗺噿嶵㠑嶊冣㝡䘹祽鋷錊酻酔樶檌㰎栬槜檇辠䘒稡纗絊",
|
||||||
|
"zun": "尊遵樽鳟撙墫噂嶟鶎銌鱒鐏捘罇鷷僔繜譐",
|
||||||
|
"zuo": "作做坐左座昨佐琢撮柞唑祚捽阼胙嘬怍酢笮葄葃蓙䔘苲莋㸲㝾䞰䎰咗㘀㘴岝岞䝫糳袏鈼㭮稓穝秨筰㛗㑅飵侳繓䋏"
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
local Element = require('elements/Element')
|
||||||
|
|
||||||
|
---@class BufferingIndicator : Element
|
||||||
|
local BufferingIndicator = class(Element)
|
||||||
|
|
||||||
|
function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end
|
||||||
|
function BufferingIndicator:init()
|
||||||
|
Element.init(self, 'buffering_indicator', {ignores_curtain = true, render_order = 2})
|
||||||
|
self.enabled = false
|
||||||
|
self:decide_enabled()
|
||||||
|
end
|
||||||
|
|
||||||
|
function BufferingIndicator:decide_enabled()
|
||||||
|
local cache = state.cache_underrun or state.cache_buffering and state.cache_buffering < 100
|
||||||
|
local player = state.core_idle and not state.eof_reached
|
||||||
|
if self.enabled then
|
||||||
|
if not player or (state.pause and not cache) then self.enabled = false end
|
||||||
|
elseif player and cache and state.uncached_ranges then
|
||||||
|
self.enabled = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function BufferingIndicator:on_prop_pause() self:decide_enabled() end
|
||||||
|
function BufferingIndicator:on_prop_core_idle() self:decide_enabled() end
|
||||||
|
function BufferingIndicator:on_prop_eof_reached() self:decide_enabled() end
|
||||||
|
function BufferingIndicator:on_prop_uncached_ranges() self:decide_enabled() end
|
||||||
|
function BufferingIndicator:on_prop_cache_buffering() self:decide_enabled() end
|
||||||
|
function BufferingIndicator:on_prop_cache_underrun() self:decide_enabled() end
|
||||||
|
|
||||||
|
function BufferingIndicator:render()
|
||||||
|
local ass = assdraw.ass_new()
|
||||||
|
ass:rect(0, 0, display.width, display.height, {color = bg, opacity = config.opacity.buffering_indicator})
|
||||||
|
local size = round(30 + math.min(display.width, display.height) / 10)
|
||||||
|
local opacity = (Elements.menu and Elements.menu:is_alive()) and 0.3 or 0.8
|
||||||
|
ass:spinner(display.width / 2, display.height / 2, size, {color = fg, opacity = opacity})
|
||||||
|
return ass
|
||||||
|
end
|
||||||
|
|
||||||
|
return BufferingIndicator
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
local Element = require('elements/Element')
|
||||||
|
|
||||||
|
---@alias ButtonProps {icon: string; on_click?: function; is_clickable?: boolean; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
|
||||||
|
|
||||||
|
---@class Button : Element
|
||||||
|
local Button = class(Element)
|
||||||
|
|
||||||
|
---@param id string
|
||||||
|
---@param props ButtonProps
|
||||||
|
function Button:new(id, props) return Class.new(self, id, props) --[[@as Button]] end
|
||||||
|
---@param id string
|
||||||
|
---@param props ButtonProps
|
||||||
|
function Button:init(id, props)
|
||||||
|
self.icon = props.icon
|
||||||
|
self.active = props.active
|
||||||
|
self.tooltip = props.tooltip
|
||||||
|
self.badge = props.badge
|
||||||
|
self.foreground = props.foreground or fg
|
||||||
|
self.background = props.background or bg
|
||||||
|
self.is_clickable = true
|
||||||
|
---@type fun()|nil
|
||||||
|
self.on_click = props.on_click
|
||||||
|
Element.init(self, id, props)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end
|
||||||
|
function Button:handle_cursor_click()
|
||||||
|
if not self.on_click or not self.is_clickable then return end
|
||||||
|
-- We delay the callback to next tick, otherwise we are risking race
|
||||||
|
-- conditions as we are in the middle of event dispatching.
|
||||||
|
-- For example, handler might add a menu to the end of the element stack, and that
|
||||||
|
-- than picks up this click event we are in right now, and instantly closes itself.
|
||||||
|
mp.add_timeout(0.01, self.on_click)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Button:render()
|
||||||
|
local visibility = self:get_visibility()
|
||||||
|
if visibility <= 0 then return end
|
||||||
|
cursor:zone('primary_down', self, function() self:handle_cursor_click() end)
|
||||||
|
|
||||||
|
local ass = assdraw.ass_new()
|
||||||
|
local is_clickable = self.is_clickable and self.on_click ~= nil
|
||||||
|
local is_hover = self.proximity_raw <= 0
|
||||||
|
local foreground = self.active and self.background or self.foreground
|
||||||
|
local background = self.active and self.foreground or self.background
|
||||||
|
local background_opacity = self.active and 1 or config.opacity.controls
|
||||||
|
|
||||||
|
if is_hover and is_clickable and background_opacity < 0.3 then background_opacity = 0.3 end
|
||||||
|
|
||||||
|
-- Background
|
||||||
|
if background_opacity > 0 then
|
||||||
|
ass:rect(self.ax, self.ay, self.bx, self.by, {
|
||||||
|
color = (self.active or not is_hover) and background or foreground,
|
||||||
|
radius = state.radius,
|
||||||
|
opacity = visibility * background_opacity,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tooltip on hover
|
||||||
|
if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end
|
||||||
|
|
||||||
|
-- Badge
|
||||||
|
local icon_clip
|
||||||
|
if self.badge then
|
||||||
|
local badge_font_size = self.font_size * 0.6
|
||||||
|
local badge_opts = {size = badge_font_size, color = background, opacity = visibility}
|
||||||
|
local badge_width = text_width(self.badge, badge_opts)
|
||||||
|
local width, height = math.ceil(badge_width + (badge_font_size / 7) * 2), math.ceil(badge_font_size * 0.93)
|
||||||
|
local bx, by = self.bx - 1, self.by - 1
|
||||||
|
ass:rect(bx - width, by - height, bx, by, {
|
||||||
|
color = foreground,
|
||||||
|
radius = state.radius,
|
||||||
|
opacity = visibility,
|
||||||
|
border = self.active and 0 or 1,
|
||||||
|
border_color = background,
|
||||||
|
})
|
||||||
|
ass:txt(bx - width / 2, by - height / 2, 5, self.badge, badge_opts)
|
||||||
|
|
||||||
|
local clip_border = math.max(self.font_size / 20, 1)
|
||||||
|
local clip_path = assdraw.ass_new()
|
||||||
|
clip_path:round_rect_cw(
|
||||||
|
math.floor((bx - width) - clip_border), math.floor((by - height) - clip_border), bx, by, 3
|
||||||
|
)
|
||||||
|
icon_clip = '\\iclip(' .. clip_path.scale .. ', ' .. clip_path.text .. ')'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Icon
|
||||||
|
local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2)
|
||||||
|
ass:icon(x, y, self.font_size, self.icon, {
|
||||||
|
color = foreground,
|
||||||
|
border = self.active and 0 or options.text_border * state.scale,
|
||||||
|
border_color = background,
|
||||||
|
opacity = visibility,
|
||||||
|
clip = icon_clip,
|
||||||
|
})
|
||||||
|
|
||||||
|
return ass
|
||||||
|
end
|
||||||
|
|
||||||
|
return Button
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
local Element = require('elements/Element')
|
||||||
|
local Button = require('elements/Button')
|
||||||
|
local CycleButton = require('elements/CycleButton')
|
||||||
|
local ManagedButton = require('elements/ManagedButton')
|
||||||
|
local Speed = require('elements/Speed')
|
||||||
|
|
||||||
|
-- sizing:
|
||||||
|
-- static - shrink, have highest claim on available space, disappear when there's not enough of it
|
||||||
|
-- dynamic - shrink to make room for static elements until they reach their ratio_min, then disappear
|
||||||
|
-- gap - shrink if there's no space left
|
||||||
|
-- space - expands to fill available space, shrinks as needed
|
||||||
|
-- scale - `options.controls_size` scale factor.
|
||||||
|
-- ratio - Width/height ratio of a static or dynamic element.
|
||||||
|
-- ratio_min Min ratio for 'dynamic' sized element.
|
||||||
|
---@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic' | 'gap'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: {[string]: boolean}[]}
|
||||||
|
|
||||||
|
---@class Controls : Element
|
||||||
|
local Controls = class(Element)
|
||||||
|
|
||||||
|
function Controls:new() return Class.new(self) --[[@as Controls]] end
|
||||||
|
function Controls:init()
|
||||||
|
Element.init(self, 'controls', {render_order = 6})
|
||||||
|
---@type ControlItem[] All control elements serialized from `options.controls`.
|
||||||
|
self.controls = {}
|
||||||
|
---@type ControlItem[] Only controls that match current dispositions.
|
||||||
|
self.layout = {}
|
||||||
|
|
||||||
|
self:init_options()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Controls:destroy()
|
||||||
|
self:destroy_elements()
|
||||||
|
Element.destroy(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Controls:init_options()
|
||||||
|
-- Serialize control elements
|
||||||
|
local shorthands = {
|
||||||
|
['play-pause'] = 'cycle:pause:pause:no/yes=play_arrow?' .. t('Play/Pause'),
|
||||||
|
menu = 'command:menu:script-binding uosc/menu-blurred?' .. t('Menu'),
|
||||||
|
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?' .. t('Subtitles'),
|
||||||
|
audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?' .. t('Audio'),
|
||||||
|
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?' .. t('Audio device'),
|
||||||
|
video = 'command:theaters:script-binding uosc/video#video>1?' .. t('Video'),
|
||||||
|
playlist = 'command:list_alt:script-binding uosc/playlist?' .. t('Playlist'),
|
||||||
|
chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?' .. t('Chapters'),
|
||||||
|
['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?' .. t('Editions'),
|
||||||
|
['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?' .. t('Stream quality'),
|
||||||
|
['open-file'] = 'command:file_open:script-binding uosc/open-file?' .. t('Open file'),
|
||||||
|
['items'] = 'command:list_alt:script-binding uosc/items?' .. t('Playlist/Files'),
|
||||||
|
prev = 'command:arrow_back_ios:script-binding uosc/prev?' .. t('Previous'),
|
||||||
|
next = 'command:arrow_forward_ios:script-binding uosc/next?' .. t('Next'),
|
||||||
|
first = 'command:first_page:script-binding uosc/first?' .. t('First'),
|
||||||
|
last = 'command:last_page:script-binding uosc/last?' .. t('Last'),
|
||||||
|
['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?' .. t('Loop playlist'),
|
||||||
|
['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?' .. t('Loop file'),
|
||||||
|
shuffle = 'toggle:shuffle:shuffle?' .. t('Shuffle'),
|
||||||
|
autoload = 'toggle:hdr_auto:autoload@uosc?' .. t('Autoload'),
|
||||||
|
fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?' .. t('Fullscreen'),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Parse out disposition/config pairs
|
||||||
|
local items = {}
|
||||||
|
local in_disposition = false
|
||||||
|
local current_item = nil
|
||||||
|
for c in options.controls:gmatch('.') do
|
||||||
|
if not current_item then current_item = {disposition = '', config = ''} end
|
||||||
|
if c == '<' and #current_item.config == 0 then
|
||||||
|
in_disposition = true
|
||||||
|
elseif c == '>' and #current_item.config == 0 then
|
||||||
|
in_disposition = false
|
||||||
|
elseif c == ',' and not in_disposition then
|
||||||
|
items[#items + 1] = current_item
|
||||||
|
current_item = nil
|
||||||
|
else
|
||||||
|
local prop = in_disposition and 'disposition' or 'config'
|
||||||
|
current_item[prop] = current_item[prop] .. c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
items[#items + 1] = current_item
|
||||||
|
|
||||||
|
-- Create controls
|
||||||
|
self.controls = {}
|
||||||
|
for i, item in ipairs(items) do
|
||||||
|
local config = shorthands[item.config] and shorthands[item.config] or item.config
|
||||||
|
local config_tooltip = split(config, ' *%? *')
|
||||||
|
local tooltip = config_tooltip[2]
|
||||||
|
config = shorthands[config_tooltip[1]]
|
||||||
|
and split(shorthands[config_tooltip[1]], ' *%? *')[1] or config_tooltip[1]
|
||||||
|
local config_badge = split(config, ' *# *')
|
||||||
|
config = config_badge[1]
|
||||||
|
local badge = config_badge[2]
|
||||||
|
local parts = split(config, ' *: *')
|
||||||
|
local kind, params = parts[1], itable_slice(parts, 2)
|
||||||
|
|
||||||
|
-- Serialize dispositions into OR groups of AND conditions
|
||||||
|
---@type {[string]: boolean}[]
|
||||||
|
local dispositions = {}
|
||||||
|
---@type string[]
|
||||||
|
local disposition_props = {}
|
||||||
|
for _, or_group in ipairs(comma_split(item.disposition)) do
|
||||||
|
local group = {}
|
||||||
|
for _, condition in ipairs(split(or_group, ' *+ *')) do
|
||||||
|
if #condition > 0 then
|
||||||
|
local value = condition:sub(1, 1) ~= '!'
|
||||||
|
local name = not value and condition:sub(2) or condition
|
||||||
|
if name:sub(1, 4) == 'has_' or itable_has({'idle', 'image', 'audio', 'video', 'stream'}, name) then
|
||||||
|
local prop = name:sub(1, 4) == 'has_' and name or 'is_' .. name
|
||||||
|
group[prop] = value
|
||||||
|
else
|
||||||
|
disposition_props[#disposition_props + 1] = name
|
||||||
|
group[name] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
dispositions[#dispositions + 1] = group
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Convert toggles into cycles
|
||||||
|
if kind == 'toggle' then
|
||||||
|
kind = 'cycle'
|
||||||
|
params[#params + 1] = 'no/yes!'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create a control element
|
||||||
|
local control = {dispositions = dispositions, kind = kind}
|
||||||
|
|
||||||
|
if kind == 'space' then
|
||||||
|
control.sizing = 'space'
|
||||||
|
elseif kind == 'gap' then
|
||||||
|
table_assign(control, {sizing = 'gap', scale = 1, ratio = params[1] or 0.3, ratio_min = 0})
|
||||||
|
elseif kind == 'command' then
|
||||||
|
if #params ~= 2 then
|
||||||
|
mp.error(string.format(
|
||||||
|
'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/')
|
||||||
|
))
|
||||||
|
else
|
||||||
|
local element = Button:new('control_' .. i, {
|
||||||
|
render_order = self.render_order,
|
||||||
|
icon = params[1],
|
||||||
|
anchor_id = 'controls',
|
||||||
|
on_click = function() mp.command(params[2]) end,
|
||||||
|
tooltip = tooltip,
|
||||||
|
count_prop = 'sub',
|
||||||
|
})
|
||||||
|
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
||||||
|
if badge then self:register_badge_updater(badge, element) end
|
||||||
|
end
|
||||||
|
elseif kind == 'cycle' then
|
||||||
|
if #params ~= 3 then
|
||||||
|
mp.error(string.format(
|
||||||
|
'cycle button needs 3 parameters, %d received: %s',
|
||||||
|
#params, table.concat(params, '/')
|
||||||
|
))
|
||||||
|
else
|
||||||
|
local state_configs = split(params[3], ' */ *')
|
||||||
|
local states = {}
|
||||||
|
|
||||||
|
for _, state_config in ipairs(state_configs) do
|
||||||
|
local active = false
|
||||||
|
if state_config:sub(-1) == '!' then
|
||||||
|
active = true
|
||||||
|
state_config = state_config:sub(1, -2)
|
||||||
|
end
|
||||||
|
local state_params = split(state_config, ' *= *')
|
||||||
|
local value, icon = state_params[1], state_params[2] or params[1]
|
||||||
|
states[#states + 1] = {value = value, icon = icon, active = active}
|
||||||
|
end
|
||||||
|
|
||||||
|
local element = CycleButton:new('control_' .. i, {
|
||||||
|
render_order = self.render_order,
|
||||||
|
prop = params[2],
|
||||||
|
anchor_id = 'controls',
|
||||||
|
states = states,
|
||||||
|
tooltip = tooltip,
|
||||||
|
})
|
||||||
|
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
||||||
|
if badge then self:register_badge_updater(badge, element) end
|
||||||
|
end
|
||||||
|
elseif kind == 'button' then
|
||||||
|
if #params ~= 1 then
|
||||||
|
mp.error(string.format(
|
||||||
|
'managed button needs 1 parameter, %d received: %s', #params, table.concat(params, '/')
|
||||||
|
))
|
||||||
|
else
|
||||||
|
local element = ManagedButton:new('control_' .. i, {
|
||||||
|
name = params[1],
|
||||||
|
render_order = self.render_order,
|
||||||
|
anchor_id = 'controls',
|
||||||
|
on_hide = function() self:reflow() end,
|
||||||
|
})
|
||||||
|
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
||||||
|
end
|
||||||
|
elseif kind == 'speed' then
|
||||||
|
if not Elements.speed then
|
||||||
|
local element = Speed:new({anchor_id = 'controls', render_order = self.render_order})
|
||||||
|
local scale = tonumber(params[1]) or 1.3
|
||||||
|
table_assign(control, {
|
||||||
|
element = element, sizing = 'dynamic', scale = scale, ratio = 3.5, ratio_min = 2,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
msg.error('there can only be 1 speed slider')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
msg.error('unknown element kind "' .. kind .. '"')
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
if control.element then
|
||||||
|
for _, prop in ipairs(disposition_props) do
|
||||||
|
control.element:observe_mp_property(prop, function() self:reflow() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.controls[#self.controls + 1] = control
|
||||||
|
end
|
||||||
|
|
||||||
|
self:reflow()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Controls:reflow()
|
||||||
|
-- Populate the layout only with items that are not hidden and match current disposition
|
||||||
|
self.layout = {}
|
||||||
|
for _, control in ipairs(self.controls) do
|
||||||
|
local matches = false
|
||||||
|
local conditions_num = 0
|
||||||
|
|
||||||
|
-- Check against OR groups of AND conditions
|
||||||
|
for _, group in pairs(control.dispositions) do
|
||||||
|
local group_matches = true
|
||||||
|
for prop, value in pairs(group) do
|
||||||
|
conditions_num = conditions_num + 1
|
||||||
|
---@type boolean
|
||||||
|
local current_value
|
||||||
|
if prop:sub(1, 4) == 'has_' or prop:sub(1, 3) == 'is_' then
|
||||||
|
current_value = state[prop]
|
||||||
|
else
|
||||||
|
current_value = mp.get_property_bool(prop, false)
|
||||||
|
end
|
||||||
|
if current_value ~= value then
|
||||||
|
group_matches = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if group_matches then
|
||||||
|
matches = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if conditions_num == 0 then matches = true end
|
||||||
|
local show = matches and (not control.element or control.element.hide ~= true)
|
||||||
|
if control.element then control.element.enabled = show end
|
||||||
|
if show then self.layout[#self.layout + 1] = control end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:update_dimensions()
|
||||||
|
Elements:trigger('controls_reflow')
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param badge string
|
||||||
|
---@param element Element An element that supports `badge` property.
|
||||||
|
function Controls:register_badge_updater(badge, element)
|
||||||
|
local prop_and_limit = split(badge, ' *> *')
|
||||||
|
local prop, limit = prop_and_limit[1], tonumber(prop_and_limit[2] or -1)
|
||||||
|
local observable_name, serializer, is_external_prop = prop, nil, false
|
||||||
|
|
||||||
|
if itable_index_of({'sub', 'audio', 'video'}, prop) then
|
||||||
|
observable_name = 'track-list'
|
||||||
|
serializer = function(value)
|
||||||
|
local count = 0
|
||||||
|
for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local parts = split(prop, '@')
|
||||||
|
-- Support both new `prop@owner` and old `@prop` syntaxes
|
||||||
|
if #parts > 1 then prop, is_external_prop = parts[1] ~= '' and parts[1] or parts[2], true end
|
||||||
|
serializer = function(value) return value and (type(value) == 'table' and #value or tostring(value)) or nil end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handler(_, value)
|
||||||
|
local new_value = serializer(value) --[[@as nil|string|integer]]
|
||||||
|
local value_number = tonumber(new_value)
|
||||||
|
if value_number then new_value = value_number > limit and value_number or nil end
|
||||||
|
element.badge = new_value
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_external_prop then
|
||||||
|
element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end
|
||||||
|
else
|
||||||
|
element:observe_mp_property(observable_name, handler)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Controls:get_visibility()
|
||||||
|
return Elements:v('speed', 'dragging') and 1 or Elements:maybe('timeline', 'get_is_hovered')
|
||||||
|
and -1 or Element.get_visibility(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Controls:update_dimensions()
|
||||||
|
local window_border = Elements:v('window_border', 'size', 0)
|
||||||
|
local size = round(options.controls_size * state.scale)
|
||||||
|
local spacing = round(options.controls_spacing * state.scale)
|
||||||
|
local margin = round(options.controls_margin * state.scale)
|
||||||
|
|
||||||
|
-- Disable when not enough space
|
||||||
|
local available_space = display.height - window_border * 2 - Elements:v('top_bar', 'size', 0)
|
||||||
|
- Elements:v('timeline', 'size', 0)
|
||||||
|
self.enabled = available_space > size + 10
|
||||||
|
|
||||||
|
-- Reset hide/enabled flags
|
||||||
|
for c, control in ipairs(self.layout) do
|
||||||
|
control.hide = false
|
||||||
|
if control.element then control.element.enabled = self.enabled end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.enabled then return end
|
||||||
|
|
||||||
|
-- Container
|
||||||
|
self.bx = display.width - window_border - margin
|
||||||
|
self.by = Elements:v('timeline', 'ay', display.height - window_border) - margin
|
||||||
|
self.ax, self.ay = window_border + margin, self.by - size
|
||||||
|
|
||||||
|
-- Controls
|
||||||
|
local available_width, statics_width = self.bx - self.ax, 0
|
||||||
|
local min_content_width = statics_width
|
||||||
|
local max_dynamics_width, dynamic_units, spaces, gaps = 0, 0, 0, 0
|
||||||
|
|
||||||
|
-- Calculate statics_width, min_content_width, and count spaces & gaps
|
||||||
|
for c, control in ipairs(self.layout) do
|
||||||
|
if control.sizing == 'space' then
|
||||||
|
spaces = spaces + 1
|
||||||
|
elseif control.sizing == 'gap' then
|
||||||
|
gaps = gaps + control.scale * control.ratio
|
||||||
|
elseif control.sizing == 'static' then
|
||||||
|
local width = size * control.scale * control.ratio + (c ~= #self.layout and spacing or 0)
|
||||||
|
statics_width = statics_width + width
|
||||||
|
min_content_width = min_content_width + width
|
||||||
|
elseif control.sizing == 'dynamic' then
|
||||||
|
local spacing = (c ~= #self.layout and spacing or 0)
|
||||||
|
statics_width = statics_width + spacing
|
||||||
|
min_content_width = min_content_width + size * control.scale * control.ratio_min + spacing
|
||||||
|
max_dynamics_width = max_dynamics_width + size * control.scale * control.ratio
|
||||||
|
dynamic_units = dynamic_units + control.scale * control.ratio
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hide & disable elements in the middle until we fit into available width
|
||||||
|
if min_content_width > available_width then
|
||||||
|
local i = math.ceil(#self.layout / 2 + 0.1)
|
||||||
|
for a = 0, #self.layout - 1, 1 do
|
||||||
|
i = i + (a * (a % 2 == 0 and 1 or -1))
|
||||||
|
local control = self.layout[i]
|
||||||
|
|
||||||
|
if control.sizing ~= 'gap' and control.sizing ~= 'space' then
|
||||||
|
control.hide = true
|
||||||
|
if control.element then control.element.enabled = false end
|
||||||
|
if control.sizing == 'static' then
|
||||||
|
local width = size * control.scale * control.ratio
|
||||||
|
min_content_width = min_content_width - width - spacing
|
||||||
|
statics_width = statics_width - width - spacing
|
||||||
|
elseif control.sizing == 'dynamic' then
|
||||||
|
statics_width = statics_width - spacing
|
||||||
|
min_content_width = min_content_width - size * control.scale * control.ratio_min - spacing
|
||||||
|
max_dynamics_width = max_dynamics_width - size * control.scale * control.ratio
|
||||||
|
dynamic_units = dynamic_units - control.scale * control.ratio
|
||||||
|
end
|
||||||
|
|
||||||
|
if min_content_width < available_width then break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Lay out the elements
|
||||||
|
local current_x = self.ax
|
||||||
|
local width_for_dynamics = available_width - statics_width
|
||||||
|
local empty_space_width = width_for_dynamics - max_dynamics_width
|
||||||
|
local width_for_gaps = math.min(empty_space_width, size * gaps)
|
||||||
|
local individual_space_width = spaces > 0 and ((empty_space_width - width_for_gaps) / spaces) or 0
|
||||||
|
|
||||||
|
for c, control in ipairs(self.layout) do
|
||||||
|
if not control.hide then
|
||||||
|
local sizing, element, scale, ratio = control.sizing, control.element, control.scale, control.ratio
|
||||||
|
local width, height = 0, 0
|
||||||
|
|
||||||
|
if sizing == 'space' then
|
||||||
|
if individual_space_width > 0 then width = individual_space_width end
|
||||||
|
elseif sizing == 'gap' then
|
||||||
|
if width_for_gaps > 0 then width = width_for_gaps * (ratio / gaps) end
|
||||||
|
elseif sizing == 'static' then
|
||||||
|
height = size * scale
|
||||||
|
width = height * ratio
|
||||||
|
elseif sizing == 'dynamic' then
|
||||||
|
height = size * scale
|
||||||
|
width = max_dynamics_width < width_for_dynamics
|
||||||
|
and height * ratio or width_for_dynamics * ((scale * ratio) / dynamic_units)
|
||||||
|
end
|
||||||
|
|
||||||
|
local bx = current_x + width
|
||||||
|
if element then element:set_coordinates(round(current_x), round(self.by - height), bx, self.by) end
|
||||||
|
current_x = element and bx + spacing or bx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Elements:update_proximities()
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Controls:on_dispositions() self:reflow() end
|
||||||
|
function Controls:on_display() self:update_dimensions() end
|
||||||
|
function Controls:on_prop_border() self:update_dimensions() end
|
||||||
|
function Controls:on_prop_title_bar() self:update_dimensions() end
|
||||||
|
function Controls:on_prop_fullormaxed() self:update_dimensions() end
|
||||||
|
function Controls:on_timeline_enabled() self:update_dimensions() end
|
||||||
|
|
||||||
|
function Controls:destroy_elements()
|
||||||
|
for _, control in ipairs(self.controls) do
|
||||||
|
if control.element then control.element:destroy() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Controls:on_options()
|
||||||
|
self:destroy_elements()
|
||||||
|
self:init_options()
|
||||||
|
end
|
||||||
|
|
||||||
|
return Controls
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
local Element = require('elements/Element')
|
||||||
|
|
||||||
|
---@class Curtain : Element
|
||||||
|
local Curtain = class(Element)
|
||||||
|
|
||||||
|
function Curtain:new() return Class.new(self) --[[@as Curtain]] end
|
||||||
|
function Curtain:init()
|
||||||
|
Element.init(self, 'curtain', {render_order = 999})
|
||||||
|
self.opacity = 0
|
||||||
|
---@type string[]
|
||||||
|
self.dependents = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param id string
|
||||||
|
function Curtain:register(id)
|
||||||
|
self.dependents[#self.dependents + 1] = id
|
||||||
|
if #self.dependents == 1 then self:tween_property('opacity', self.opacity, 1) end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param id string
|
||||||
|
function Curtain:unregister(id)
|
||||||
|
self.dependents = itable_filter(self.dependents, function(item) return item ~= id end)
|
||||||
|
if #self.dependents == 0 then self:tween_property('opacity', self.opacity, 0) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Curtain:render()
|
||||||
|
if self.opacity == 0 or config.opacity.curtain == 0 then return end
|
||||||
|
local ass = assdraw.ass_new()
|
||||||
|
ass:rect(0, 0, display.width, display.height, {
|
||||||
|
color = config.color.curtain, opacity = config.opacity.curtain * self.opacity,
|
||||||
|
})
|
||||||
|
return ass
|
||||||
|
end
|
||||||
|
|
||||||
|
return Curtain
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
local Button = require('elements/Button')
|
||||||
|
|
||||||
|
---@alias CycleState {value: any; icon: string; active?: boolean}
|
||||||
|
---@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string}
|
||||||
|
|
||||||
|
local function yes_no_to_boolean(value)
|
||||||
|
if type(value) ~= 'string' then return value end
|
||||||
|
local lowercase = trim(value):lower()
|
||||||
|
if lowercase == 'yes' or lowercase == 'no' then
|
||||||
|
return lowercase == 'yes'
|
||||||
|
else
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class CycleButton : Button
|
||||||
|
local CycleButton = class(Button)
|
||||||
|
|
||||||
|
---@param id string
|
||||||
|
---@param props CycleButtonProps
|
||||||
|
function CycleButton:new(id, props) return Class.new(self, id, props) --[[@as CycleButton]] end
|
||||||
|
---@param id string
|
||||||
|
---@param props CycleButtonProps
|
||||||
|
function CycleButton:init(id, props)
|
||||||
|
local is_state_prop = itable_index_of({'shuffle'}, props.prop)
|
||||||
|
self.prop = props.prop
|
||||||
|
self.states = props.states
|
||||||
|
|
||||||
|
Button.init(self, id, props)
|
||||||
|
|
||||||
|
self.icon = self.states[1].icon
|
||||||
|
self.active = self.states[1].active
|
||||||
|
self.current_state_index = 1
|
||||||
|
self.on_click = function()
|
||||||
|
local new_state = self.states[self.current_state_index + 1] or self.states[1]
|
||||||
|
local new_value = new_state.value
|
||||||
|
if self.owner == 'uosc' then
|
||||||
|
if type(options[self.prop]) == 'number' then
|
||||||
|
options[self.prop] = tonumber(new_value) or 0
|
||||||
|
else
|
||||||
|
options[self.prop] = yes_no_to_boolean(new_value)
|
||||||
|
end
|
||||||
|
handle_options({[self.prop] = options[self.prop]})
|
||||||
|
elseif self.owner then
|
||||||
|
mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value)
|
||||||
|
elseif is_state_prop then
|
||||||
|
set_state(self.prop, yes_no_to_boolean(new_value))
|
||||||
|
else
|
||||||
|
mp.set_property(self.prop, new_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handle_change(name, value)
|
||||||
|
-- Removes unnecessary floating point digits from values like `2.00000`.
|
||||||
|
-- This happens when observing properties like `speed`.
|
||||||
|
if type(value) == 'string' and string.match(value, '^[%+%-]?%d+%.%d+$') then
|
||||||
|
value = tonumber(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
value = type(value) == 'boolean' and (value and 'yes' or 'no') or tostring(value or '')
|
||||||
|
local index = itable_find(self.states, function(state) return state.value == value end)
|
||||||
|
self.current_state_index = index or 1
|
||||||
|
self.icon = self.states[self.current_state_index].icon
|
||||||
|
self.active = self.states[self.current_state_index].active
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
local prop_parts = split(self.prop, '@')
|
||||||
|
if #prop_parts == 2 then -- External prop with a script owner
|
||||||
|
self.prop, self.owner = prop_parts[1], prop_parts[2]
|
||||||
|
if self.owner == 'uosc' then
|
||||||
|
self['on_options'] = function() handle_change(self.prop, options[self.prop]) end
|
||||||
|
handle_change(self.prop, options[self.prop])
|
||||||
|
else
|
||||||
|
self['on_external_prop_' .. self.prop] = function(_, value) handle_change(self.prop, value) end
|
||||||
|
handle_change(self.prop, external[self.prop])
|
||||||
|
end
|
||||||
|
elseif is_state_prop then -- uosc's state props
|
||||||
|
self['on_prop_' .. self.prop] = function(self, value) handle_change(self.prop, value) end
|
||||||
|
handle_change(self.prop, state[self.prop])
|
||||||
|
else
|
||||||
|
self:observe_mp_property(self.prop, 'string', handle_change)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return CycleButton
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
---@alias ElementProps {enabled?: boolean; render_order?: number; ax?: number; ay?: number; bx?: number; by?: number; ignores_curtain?: boolean; anchor_id?: string;}
|
||||||
|
|
||||||
|
-- Base class all elements inherit from.
|
||||||
|
---@class Element : Class
|
||||||
|
local Element = class()
|
||||||
|
|
||||||
|
---@param id string
|
||||||
|
---@param props? ElementProps
|
||||||
|
function Element:init(id, props)
|
||||||
|
self.id = id
|
||||||
|
self.render_order = 1
|
||||||
|
-- `false` means element won't be rendered, or receive events
|
||||||
|
self.enabled = true
|
||||||
|
-- Element coordinates
|
||||||
|
self.ax, self.ay, self.bx, self.by = 0, 0, 0, 0
|
||||||
|
-- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range.
|
||||||
|
self.proximity = 0
|
||||||
|
-- Raw proximity in pixels.
|
||||||
|
self.proximity_raw = math.huge
|
||||||
|
---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility.
|
||||||
|
self.min_visibility = 0
|
||||||
|
---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations
|
||||||
|
self.forced_visibility = nil
|
||||||
|
---@type boolean Show this element even when curtain is visible.
|
||||||
|
self.ignores_curtain = false
|
||||||
|
---@type nil|string ID of an element from which this one should inherit visibility.
|
||||||
|
self.anchor_id = nil
|
||||||
|
---@type fun()[] Disposer functions called when element is destroyed.
|
||||||
|
self._disposers = {}
|
||||||
|
---@type table<string,table<string, boolean>> Namespaced active key bindings. Default namespace is `_`.
|
||||||
|
self._key_bindings = {}
|
||||||
|
|
||||||
|
if props then table_assign(self, props) end
|
||||||
|
|
||||||
|
-- Flash timer
|
||||||
|
self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
|
||||||
|
local function getTo() return self.proximity end
|
||||||
|
local function onTweenEnd() self.forced_visibility = nil end
|
||||||
|
if self.enabled then
|
||||||
|
self:tween_property('forced_visibility', self:get_visibility(), getTo, onTweenEnd)
|
||||||
|
else
|
||||||
|
onTweenEnd()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
self._flash_out_timer:kill()
|
||||||
|
|
||||||
|
Elements:add(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Element:destroy()
|
||||||
|
self:dispose()
|
||||||
|
self.destroyed = true
|
||||||
|
self:remove_key_bindings()
|
||||||
|
Elements:remove(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calls all disposers registered for this element (usually mpv events/prop observers).
|
||||||
|
function Element:dispose()
|
||||||
|
for _, disposer in ipairs(self._disposers) do disposer() end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Element:reset_proximity() self.proximity, self.proximity_raw = 0, math.huge end
|
||||||
|
|
||||||
|
---@param ax number
|
||||||
|
---@param ay number
|
||||||
|
---@param bx number
|
||||||
|
---@param by number
|
||||||
|
function Element:set_coordinates(ax, ay, bx, by)
|
||||||
|
self.ax, self.ay, self.bx, self.by = ax, ay, bx, by
|
||||||
|
Elements:update_proximities()
|
||||||
|
self:maybe('on_coordinates')
|
||||||
|
end
|
||||||
|
|
||||||
|
function Element:update_proximity()
|
||||||
|
if cursor.hidden then
|
||||||
|
self:reset_proximity()
|
||||||
|
else
|
||||||
|
local range = options.proximity_out - options.proximity_in
|
||||||
|
self.proximity_raw = get_point_to_rectangle_proximity(cursor, self)
|
||||||
|
self.proximity = 1 - (clamp(0, self.proximity_raw - options.proximity_in, range) / range)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Element:is_persistent()
|
||||||
|
local persist = config[self.id .. '_persistency']
|
||||||
|
return persist and (
|
||||||
|
(persist.audio and state.is_audio)
|
||||||
|
or (
|
||||||
|
persist.paused and state.pause
|
||||||
|
and (not Elements.timeline or not Elements.timeline.pressed or Elements.timeline.pressed.pause)
|
||||||
|
)
|
||||||
|
or (persist.video and state.is_video)
|
||||||
|
or (persist.image and state.is_image)
|
||||||
|
or (persist.idle and state.is_idle)
|
||||||
|
or (persist.windowed and not state.fullormaxed)
|
||||||
|
or (persist.fullscreen and state.fullormaxed)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Decide elements visibility based on proximity and various other factors
|
||||||
|
function Element:get_visibility()
|
||||||
|
-- Hide when curtain is visible, unless this elements ignores it
|
||||||
|
local min_order = (Elements.curtain.opacity > 0 and not self.ignores_curtain) and Elements.curtain.render_order or 0
|
||||||
|
if self.render_order < min_order then return 0 end
|
||||||
|
|
||||||
|
-- Persistency
|
||||||
|
if self:is_persistent() then return 1 end
|
||||||
|
|
||||||
|
-- Forced visibility
|
||||||
|
if self.forced_visibility then return math.max(self.forced_visibility, self.min_visibility) end
|
||||||
|
|
||||||
|
-- Anchor inheritance
|
||||||
|
-- If anchor returns -1, it means all attached elements should force hide.
|
||||||
|
local anchor = self.anchor_id and Elements[self.anchor_id]
|
||||||
|
local anchor_visibility = anchor and anchor:get_visibility() or 0
|
||||||
|
|
||||||
|
return anchor_visibility == -1 and 0 or math.max(self.proximity, anchor_visibility, self.min_visibility)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Call method if it exists
|
||||||
|
function Element:maybe(name, ...)
|
||||||
|
if self[name] then return self[name](self, ...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Attach a tweening animation to this element
|
||||||
|
---@param from number
|
||||||
|
---@param to number|fun():number
|
||||||
|
---@param setter fun(value: number)
|
||||||
|
---@param duration_or_callback? number|fun() Duration in milliseconds or a callback function.
|
||||||
|
---@param callback? fun() Called either on animation end, or when animation is killed.
|
||||||
|
function Element:tween(from, to, setter, duration_or_callback, callback)
|
||||||
|
self:tween_stop()
|
||||||
|
self._kill_tween = self.enabled and tween(
|
||||||
|
from, to, setter, duration_or_callback,
|
||||||
|
function()
|
||||||
|
self._kill_tween = nil
|
||||||
|
if callback then callback() end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Element:is_tweening() return self and self._kill_tween end
|
||||||
|
function Element:tween_stop() self:maybe('_kill_tween') end
|
||||||
|
|
||||||
|
-- Animate an element property between 2 values.
|
||||||
|
---@param prop string
|
||||||
|
---@param from number
|
||||||
|
---@param to number|fun():number
|
||||||
|
---@param duration_or_callback? number|fun() Duration in milliseconds or a callback function.
|
||||||
|
---@param callback? fun() Called either on animation end, or when animation is killed.
|
||||||
|
function Element:tween_property(prop, from, to, duration_or_callback, callback)
|
||||||
|
self:tween(from, to, function(value) self[prop] = value end, duration_or_callback, callback)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
function Element:trigger(name, ...)
|
||||||
|
local result = self:maybe('on_' .. name, ...)
|
||||||
|
request_render()
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Briefly flashes the element for `options.flash_duration` milliseconds.
|
||||||
|
-- Useful to visualize changes of volume and timeline when changed via hotkeys.
|
||||||
|
function Element:flash()
|
||||||
|
if self.enabled and options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
|
||||||
|
self:tween_stop()
|
||||||
|
self.forced_visibility = 1
|
||||||
|
request_render()
|
||||||
|
self._flash_out_timer.timeout = options.flash_duration / 1000
|
||||||
|
self._flash_out_timer:kill()
|
||||||
|
self._flash_out_timer:resume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Register disposer to be called when element is destroyed.
|
||||||
|
---@param disposer fun()
|
||||||
|
function Element:register_disposer(disposer)
|
||||||
|
if not itable_index_of(self._disposers, disposer) then
|
||||||
|
self._disposers[#self._disposers + 1] = disposer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Automatically registers disposer for the passed callback.
|
||||||
|
---@param event string
|
||||||
|
---@param callback fun()
|
||||||
|
function Element:register_mp_event(event, callback)
|
||||||
|
mp.register_event(event, callback)
|
||||||
|
self:register_disposer(function() mp.unregister_event(callback) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Automatically registers disposer for the observer.
|
||||||
|
---@param name string
|
||||||
|
---@param type_or_callback string|fun(name: string, value: any)
|
||||||
|
---@param callback_maybe nil|fun(name: string, value: any)
|
||||||
|
function Element:observe_mp_property(name, type_or_callback, callback_maybe)
|
||||||
|
local callback = type(type_or_callback) == 'function' and type_or_callback or callback_maybe
|
||||||
|
local prop_type = type(type_or_callback) == 'string' and type_or_callback or 'native'
|
||||||
|
mp.observe_property(name, prop_type, callback)
|
||||||
|
self:register_disposer(function() mp.unobserve_property(callback) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Adds a keybinding for the lifetime of the element, or until removed manually.
|
||||||
|
---@param key string mpv key identifier.
|
||||||
|
---@param fnFlags fun()|string|table<fun()|string> Callback, or `{callback, flags}` tuple. Callback can be just a method name, in which case it'll be wrapped in `create_action(callback)`.
|
||||||
|
---@param namespace? string Keybinding namespace. Default is `_`.
|
||||||
|
function Element:add_key_binding(key, fnFlags, namespace)
|
||||||
|
local name = self.id .. '-' .. key
|
||||||
|
local isTuple = type(fnFlags) == 'table'
|
||||||
|
local fn = (isTuple and fnFlags[1] or fnFlags)
|
||||||
|
local flags = isTuple and fnFlags[2] or nil
|
||||||
|
namespace = namespace or '_'
|
||||||
|
local names = self._key_bindings[namespace]
|
||||||
|
if not names then
|
||||||
|
names = {}
|
||||||
|
self._key_bindings[namespace] = names
|
||||||
|
end
|
||||||
|
names[name] = true
|
||||||
|
if type(fn) == 'string' then
|
||||||
|
fn = self:create_action(fn)
|
||||||
|
end
|
||||||
|
mp.add_forced_key_binding(key, name, fn, flags)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove all or only keybindings belonging to a specific namespace.
|
||||||
|
---@param namespace? string Optional keybinding namespace to remove.
|
||||||
|
function Element:remove_key_bindings(namespace)
|
||||||
|
local namespaces = namespace and {namespace} or table_keys(self._key_bindings)
|
||||||
|
for _, namespace in ipairs(namespaces) do
|
||||||
|
local names = self._key_bindings[namespace]
|
||||||
|
if names then
|
||||||
|
for name, _ in pairs(names) do
|
||||||
|
mp.remove_key_binding(name)
|
||||||
|
end
|
||||||
|
self._key_bindings[namespace] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Checks if there are any (at all or namespaced) keybindings for this element.
|
||||||
|
---@param namespace? string Only check this namespace.
|
||||||
|
function Element:has_keybindings(namespace)
|
||||||
|
if namespace then
|
||||||
|
return self._key_bindings[namespace] ~= nil
|
||||||
|
else
|
||||||
|
return #table_keys(self._key_bindings) > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if element is not destroyed or otherwise disabled.
|
||||||
|
-- Intended to be overridden by inheriting elements to add more checks.
|
||||||
|
function Element:is_alive() return not self.destroyed end
|
||||||
|
|
||||||
|
-- Wraps a function into a callback that won't run if element is destroyed or otherwise disabled.
|
||||||
|
---@param fn fun(...)|string Function or a name of a method on this class to call.
|
||||||
|
function Element:create_action(fn)
|
||||||
|
if type(fn) == 'string' then
|
||||||
|
local method = fn
|
||||||
|
fn = function(...) self[method](self, ...) end
|
||||||
|
end
|
||||||
|
return function(...)
|
||||||
|
if self:is_alive() then fn(...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Element
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
local Elements = {_all = {}}
|
||||||
|
|
||||||
|
---@param element Element
|
||||||
|
function Elements:add(element)
|
||||||
|
if not element.id then
|
||||||
|
msg.error('attempt to add element without "id" property')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if self:has(element.id) then Elements:remove(element.id) end
|
||||||
|
|
||||||
|
self._all[#self._all + 1] = element
|
||||||
|
self[element.id] = element
|
||||||
|
|
||||||
|
-- Sort by render order
|
||||||
|
table.sort(self._all, function(a, b) return a.render_order < b.render_order end)
|
||||||
|
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Elements:remove(idOrElement)
|
||||||
|
if not idOrElement then return end
|
||||||
|
local id = type(idOrElement) == 'table' and idOrElement.id or idOrElement
|
||||||
|
local element = Elements[id]
|
||||||
|
if element then
|
||||||
|
if not element.destroyed then element:destroy() end
|
||||||
|
element.enabled = false
|
||||||
|
self._all = itable_delete_value(self._all, self[id])
|
||||||
|
self[id] = nil
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Elements:update_proximities()
|
||||||
|
local curtain_render_order = Elements.curtain.opacity > 0 and Elements.curtain.render_order or 0
|
||||||
|
local mouse_leave_elements = {}
|
||||||
|
local mouse_enter_elements = {}
|
||||||
|
|
||||||
|
-- Calculates proximities for all elements
|
||||||
|
for _, element in self:ipairs() do
|
||||||
|
if element.enabled then
|
||||||
|
local previous_proximity_raw = element.proximity_raw
|
||||||
|
|
||||||
|
-- If curtain is open, we disable all elements set to rendered below it
|
||||||
|
if not element.ignores_curtain and element.render_order < curtain_render_order then
|
||||||
|
element:reset_proximity()
|
||||||
|
else
|
||||||
|
element:update_proximity()
|
||||||
|
end
|
||||||
|
|
||||||
|
if element.proximity_raw <= 0 then
|
||||||
|
-- Mouse entered element area
|
||||||
|
if previous_proximity_raw > 0 then
|
||||||
|
mouse_enter_elements[#mouse_enter_elements + 1] = element
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Mouse left element area
|
||||||
|
if previous_proximity_raw <= 0 then
|
||||||
|
mouse_leave_elements[#mouse_leave_elements + 1] = element
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trigger `mouse_leave` and `mouse_enter` events
|
||||||
|
for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end
|
||||||
|
for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Toggles passed elements' min visibilities between 0 and 1.
|
||||||
|
---@param ids string[] IDs of elements to peek.
|
||||||
|
function Elements:toggle(ids)
|
||||||
|
local has_invisible = itable_find(ids, function(id)
|
||||||
|
return Elements[id] and Elements[id].enabled and (Elements[id].min_visibility or 0) ~= 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
self:set_min_visibility(has_invisible and 1 or 0, ids)
|
||||||
|
|
||||||
|
-- Reset proximities when toggling off. Has to happen after `set_min_visibility`,
|
||||||
|
-- as that is using proximity as a tween starting point.
|
||||||
|
if not has_invisible then
|
||||||
|
for _, id in ipairs(ids) do
|
||||||
|
if Elements[id] then Elements[id]:reset_proximity() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set (animate) elements' min visibilities to passed value.
|
||||||
|
---@param visibility number 0-1 floating point.
|
||||||
|
---@param ids string[] IDs of elements to peek.
|
||||||
|
function Elements:set_min_visibility(visibility, ids)
|
||||||
|
for _, id in ipairs(ids) do
|
||||||
|
local element = Elements[id]
|
||||||
|
if element then
|
||||||
|
local from = math.max(0, element:get_visibility())
|
||||||
|
element:tween_property('min_visibility', from, visibility)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flash passed elements.
|
||||||
|
---@param ids string[] IDs of elements to peek.
|
||||||
|
function Elements:flash(ids)
|
||||||
|
local elements = itable_filter(self._all, function(element) return itable_has(ids, element.id) end)
|
||||||
|
for _, element in ipairs(elements) do element:flash() end
|
||||||
|
|
||||||
|
-- Special case for 'progress' since it's a state of timeline, not an element
|
||||||
|
if itable_has(ids, 'progress') and not itable_has(ids, 'timeline') then
|
||||||
|
Elements:maybe('timeline', 'flash_progress')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string Event name.
|
||||||
|
function Elements:trigger(name, ...)
|
||||||
|
for _, element in self:ipairs() do element:trigger(name, ...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trigger two events, `name` and `global_name`, depending on element-cursor proximity.
|
||||||
|
-- Disabled elements don't receive these events.
|
||||||
|
---@param name string Event name.
|
||||||
|
function Elements:proximity_trigger(name, ...)
|
||||||
|
for i = #self._all, 1, -1 do
|
||||||
|
local element = self._all[i]
|
||||||
|
if element.enabled then
|
||||||
|
if element.proximity_raw <= 0 then
|
||||||
|
if element:trigger(name, ...) == 'stop_propagation' then break end
|
||||||
|
end
|
||||||
|
if element:trigger('global_' .. name, ...) == 'stop_propagation' then break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns a property of an element with a passed `id` if it exists, with an optional fallback.
|
||||||
|
---@param id string
|
||||||
|
---@param prop string
|
||||||
|
---@param fallback any
|
||||||
|
function Elements:v(id, prop, fallback)
|
||||||
|
if self[id] and self[id].enabled and self[id][prop] ~= nil then return self[id][prop] end
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calls a method on an element with passed `id` if it exists.
|
||||||
|
---@param id string
|
||||||
|
---@param method string
|
||||||
|
function Elements:maybe(id, method, ...)
|
||||||
|
if self[id] then return self[id]:maybe(method, ...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Elements:has(id) return self[id] ~= nil end
|
||||||
|
function Elements:ipairs() return ipairs(self._all) end
|
||||||
|
|
||||||
|
return Elements
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
local Button = require('elements/Button')
|
||||||
|
|
||||||
|
---@alias ManagedButtonProps {name: string; anchor_id?: string; render_order?: number; hide?: boolean}
|
||||||
|
|
||||||
|
---@class ManagedButton : Button
|
||||||
|
local ManagedButton = class(Button)
|
||||||
|
|
||||||
|
---@param id string
|
||||||
|
---@param props ManagedButtonProps
|
||||||
|
function ManagedButton:new(id, props) return Class.new(self, id, props) --[[@as ManagedButton]] end
|
||||||
|
---@param id string
|
||||||
|
---@param props ManagedButtonProps
|
||||||
|
function ManagedButton:init(id, props)
|
||||||
|
---@type string | table | nil
|
||||||
|
self.command = nil
|
||||||
|
---@type boolean
|
||||||
|
self.hide = nil
|
||||||
|
---@type fun(hide: boolean) | nil
|
||||||
|
self.on_hide = nil
|
||||||
|
Button.init(self, id, table_assign({}, props, {on_click = function() execute_command(self.command) end}))
|
||||||
|
self:update(buttons:get(props.name))
|
||||||
|
self:register_disposer(buttons:subscribe(props.name, function(data) self:update(data) end))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ManagedButton:update(data)
|
||||||
|
local hide_before = self.hide
|
||||||
|
for _, prop in ipairs({'icon', 'active', 'badge', 'command', 'tooltip', 'hide'}) do
|
||||||
|
self[prop] = data[prop]
|
||||||
|
end
|
||||||
|
self.is_clickable = self.command ~= nil
|
||||||
|
if self.hide ~= hide_before and self.on_hide then
|
||||||
|
self.on_hide(self.hide)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ManagedButton
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
|||||||
|
local Element = require('elements/Element')
|
||||||
|
|
||||||
|
---@class PauseIndicator : Element
|
||||||
|
local PauseIndicator = class(Element)
|
||||||
|
|
||||||
|
function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end
|
||||||
|
function PauseIndicator:init()
|
||||||
|
Element.init(self, 'pause_indicator', {render_order = 3})
|
||||||
|
self.ignores_curtain = true
|
||||||
|
self.paused = state.pause
|
||||||
|
self.opacity = 0
|
||||||
|
self.fadeout = false
|
||||||
|
self:init_options()
|
||||||
|
end
|
||||||
|
|
||||||
|
function PauseIndicator:init_options()
|
||||||
|
self.base_icon_opacity = config.opacity.pause_indicator or (options.pause_indicator == 'flash' and 1) or 0.8
|
||||||
|
self.type = options.pause_indicator
|
||||||
|
self:on_prop_pause()
|
||||||
|
end
|
||||||
|
|
||||||
|
function PauseIndicator:flash()
|
||||||
|
-- Can't wait for pause property event listener to set this, because when this is used inside a binding like:
|
||||||
|
-- cycle pause; script-binding uosc/flash-pause-indicator
|
||||||
|
-- The pause event is not fired fast enough, and indicator starts rendering with old icon.
|
||||||
|
self.paused = mp.get_property_native('pause')
|
||||||
|
self.fadeout, self.opacity = false, 1
|
||||||
|
self:tween_property('opacity', 1, 0, 300)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Decides whether static indicator should be visible or not.
|
||||||
|
function PauseIndicator:decide()
|
||||||
|
self.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary
|
||||||
|
self.fadeout, self.opacity = self.paused, self.paused and 1 or 0
|
||||||
|
request_render()
|
||||||
|
|
||||||
|
-- Workaround for an mpv race condition bug during pause on windows builds, which causes osd updates to be ignored.
|
||||||
|
-- .03 was still loosing renders, .04 was fine, but to be safe I added 10ms more
|
||||||
|
mp.add_timeout(.05, function() osd:update() end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PauseIndicator:on_prop_pause()
|
||||||
|
if Elements:v('timeline', 'pressed') then return end
|
||||||
|
if options.pause_indicator == 'flash' then
|
||||||
|
if self.paused ~= state.pause then self:flash() end
|
||||||
|
elseif options.pause_indicator == 'static' then
|
||||||
|
self:decide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function PauseIndicator:on_options()
|
||||||
|
self:init_options()
|
||||||
|
if self.type == 'flash' then self.opacity = 0 end
|
||||||
|
end
|
||||||
|
|
||||||
|
function PauseIndicator:render()
|
||||||
|
if self.opacity == 0 then return end
|
||||||
|
|
||||||
|
local ass = assdraw.ass_new()
|
||||||
|
|
||||||
|
-- Background fadeout
|
||||||
|
if self.fadeout then
|
||||||
|
ass:rect(0, 0, display.width, display.height, {color = bg, opacity = self.opacity * 0.3})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Icon
|
||||||
|
local size = round(math.min(display.width, display.height) * (self.fadeout and 0.20 or 0.15))
|
||||||
|
size = size + size * (1 - self.opacity)
|
||||||
|
|
||||||
|
if self.paused then
|
||||||
|
ass:icon(display.width / 2, display.height / 2, size, 'pause',
|
||||||
|
{border = 1, opacity = self.base_icon_opacity * self.opacity}
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ass:icon(display.width / 2, display.height / 2, size * 1.2, 'play_arrow',
|
||||||
|
{border = 1, opacity = self.base_icon_opacity * self.opacity}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ass
|
||||||
|
end
|
||||||
|
|
||||||
|
return PauseIndicator
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
local Element = require('elements/Element')
|
||||||
|
|
||||||
|
---@alias Dragging { start_time: number; start_x: number; distance: number; speed_distance: number; start_speed: number; }
|
||||||
|
|
||||||
|
---@class Speed : Element
|
||||||
|
local Speed = class(Element)
|
||||||
|
|
||||||
|
---@param props? ElementProps
|
||||||
|
function Speed:new(props) return Class.new(self, props) --[[@as Speed]] end
|
||||||
|
function Speed:init(props)
|
||||||
|
Element.init(self, 'speed', props)
|
||||||
|
|
||||||
|
self.width = 0
|
||||||
|
self.height = 0
|
||||||
|
self.notches = 10
|
||||||
|
self.notch_every = 0.1
|
||||||
|
---@type number
|
||||||
|
self.notch_spacing = nil
|
||||||
|
---@type number
|
||||||
|
self.font_size = nil
|
||||||
|
---@type Dragging|nil
|
||||||
|
self.dragging = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Speed:get_visibility()
|
||||||
|
return Elements:maybe('timeline', 'get_is_hovered') and -1 or Element.get_visibility(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Speed:on_coordinates()
|
||||||
|
self.height, self.width = self.by - self.ay, self.bx - self.ax
|
||||||
|
self.notch_spacing = self.width / (self.notches + 1)
|
||||||
|
self.font_size = round(self.height * 0.48 * options.font_scale)
|
||||||
|
end
|
||||||
|
function Speed:on_options() self:on_coordinates() end
|
||||||
|
|
||||||
|
function Speed:speed_step(speed, up)
|
||||||
|
if options.speed_step_is_factor then
|
||||||
|
if up then
|
||||||
|
return speed * options.speed_step
|
||||||
|
else
|
||||||
|
return speed * 1 / options.speed_step
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if up then
|
||||||
|
return speed + options.speed_step
|
||||||
|
else
|
||||||
|
return speed - options.speed_step
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Speed:handle_cursor_down()
|
||||||
|
self:tween_stop() -- Stop and cleanup possible ongoing animations
|
||||||
|
self.dragging = {
|
||||||
|
start_time = mp.get_time(),
|
||||||
|
start_x = cursor.x,
|
||||||
|
distance = 0,
|
||||||
|
speed_distance = 0,
|
||||||
|
start_speed = state.speed,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Speed:on_global_mouse_move()
|
||||||
|
if not self.dragging then return end
|
||||||
|
|
||||||
|
self.dragging.distance = cursor.x - self.dragging.start_x
|
||||||
|
self.dragging.speed_distance = (-self.dragging.distance / self.notch_spacing * self.notch_every)
|
||||||
|
|
||||||
|
local speed_current = state.speed
|
||||||
|
local speed_drag_current = self.dragging.start_speed + self.dragging.speed_distance
|
||||||
|
speed_drag_current = clamp(0.01, speed_drag_current, 100)
|
||||||
|
local drag_dir_up = speed_drag_current > speed_current
|
||||||
|
|
||||||
|
local speed_step_next = speed_current
|
||||||
|
local speed_drag_diff = math.abs(speed_drag_current - speed_current)
|
||||||
|
while math.abs(speed_step_next - speed_current) < speed_drag_diff do
|
||||||
|
speed_step_next = self:speed_step(speed_step_next, drag_dir_up)
|
||||||
|
end
|
||||||
|
local speed_step_prev = self:speed_step(speed_step_next, not drag_dir_up)
|
||||||
|
|
||||||
|
local speed_new = speed_step_prev
|
||||||
|
local speed_next_diff = math.abs(speed_drag_current - speed_step_next)
|
||||||
|
local speed_prev_diff = math.abs(speed_drag_current - speed_step_prev)
|
||||||
|
if speed_next_diff < speed_prev_diff then
|
||||||
|
speed_new = speed_step_next
|
||||||
|
end
|
||||||
|
|
||||||
|
if speed_new ~= speed_current then
|
||||||
|
mp.set_property_native('speed', speed_new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Speed:handle_cursor_up()
|
||||||
|
self.dragging = nil
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Speed:on_global_mouse_leave()
|
||||||
|
self.dragging = nil
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Speed:handle_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end
|
||||||
|
function Speed:handle_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end
|
||||||
|
|
||||||
|
function Speed:render()
|
||||||
|
local visibility = self:get_visibility()
|
||||||
|
local opacity = self.dragging and 1 or visibility
|
||||||
|
|
||||||
|
if opacity <= 0 then return end
|
||||||
|
|
||||||
|
cursor:zone('primary_down', self, function()
|
||||||
|
self:handle_cursor_down()
|
||||||
|
cursor:once('primary_up', function() self:handle_cursor_up() end)
|
||||||
|
end)
|
||||||
|
cursor:zone('secondary_click', self, function() mp.set_property_native('speed', 1) end)
|
||||||
|
cursor:zone('wheel_down', self, function() self:handle_wheel_down() end)
|
||||||
|
cursor:zone('wheel_up', self, function() self:handle_wheel_up() end)
|
||||||
|
|
||||||
|
local ass = assdraw.ass_new()
|
||||||
|
|
||||||
|
-- Background
|
||||||
|
ass:rect(self.ax, self.ay, self.bx, self.by, {
|
||||||
|
color = bg, radius = state.radius, opacity = opacity * config.opacity.speed,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Coordinates
|
||||||
|
local ax, ay = self.ax, self.ay
|
||||||
|
local bx, by = self.bx, ay + self.height
|
||||||
|
local half_width = (self.width / 2)
|
||||||
|
local half_x = ax + half_width
|
||||||
|
|
||||||
|
-- Notches
|
||||||
|
local speed_at_center = state.speed
|
||||||
|
if self.dragging then
|
||||||
|
speed_at_center = self.dragging.start_speed + self.dragging.speed_distance
|
||||||
|
speed_at_center = clamp(0.01, speed_at_center, 100)
|
||||||
|
end
|
||||||
|
local nearest_notch_speed = round(speed_at_center / self.notch_every) * self.notch_every
|
||||||
|
local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / self.notch_every) * self.notch_spacing)
|
||||||
|
local guide_size = math.floor(self.height / 7.5)
|
||||||
|
local notch_by = by - guide_size
|
||||||
|
local notch_ay_big = ay + round(self.font_size * 1.1)
|
||||||
|
local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2)
|
||||||
|
local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4)
|
||||||
|
local from_to_index = math.floor(self.notches / 2)
|
||||||
|
|
||||||
|
for i = -from_to_index, from_to_index do
|
||||||
|
local notch_speed = nearest_notch_speed + (i * self.notch_every)
|
||||||
|
|
||||||
|
if notch_speed >= 0 and notch_speed <= 100 then
|
||||||
|
local notch_x = nearest_notch_x + (i * self.notch_spacing)
|
||||||
|
local notch_thickness = 1
|
||||||
|
local notch_ay = notch_ay_small
|
||||||
|
if (notch_speed % (self.notch_every * 10)) < 0.00000001 then
|
||||||
|
notch_ay = notch_ay_big
|
||||||
|
notch_thickness = 1.5
|
||||||
|
elseif (notch_speed % (self.notch_every * 5)) < 0.00000001 then
|
||||||
|
notch_ay = notch_ay_medium
|
||||||
|
end
|
||||||
|
|
||||||
|
ass:rect(notch_x - notch_thickness, notch_ay, notch_x + notch_thickness, notch_by, {
|
||||||
|
color = fg,
|
||||||
|
border = 1,
|
||||||
|
border_color = bg,
|
||||||
|
opacity = math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1) * opacity,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Center guide
|
||||||
|
ass:new_event()
|
||||||
|
ass:append('{\\rDefault\\an7\\blur0\\bord1\\shad0\\1c&H' .. fg .. '\\3c&H' .. bg .. '}')
|
||||||
|
ass:opacity(opacity)
|
||||||
|
ass:pos(0, 0)
|
||||||
|
ass:draw_start()
|
||||||
|
ass:move_to(half_x, by - 2 - guide_size)
|
||||||
|
ass:line_to(half_x + guide_size, by - 2)
|
||||||
|
ass:line_to(half_x - guide_size, by - 2)
|
||||||
|
ass:draw_stop()
|
||||||
|
|
||||||
|
-- Speed value
|
||||||
|
local speed_text = (round(state.speed * 100) / 100) .. 'x'
|
||||||
|
ass:txt(half_x, ay + (notch_ay_big - ay) / 2, 5, speed_text, {
|
||||||
|
size = self.font_size,
|
||||||
|
color = bgt,
|
||||||
|
border = options.text_border * state.scale,
|
||||||
|
border_color = bg,
|
||||||
|
opacity = opacity,
|
||||||
|
})
|
||||||
|
|
||||||
|
return ass
|
||||||
|
end
|
||||||
|
|
||||||
|
return Speed
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user