Initial commit
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
-- MIT License
|
||||
--
|
||||
-- Copyright (c) 2019 David Deprost
|
||||
--
|
||||
-- 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.
|
||||
|
||||
|
||||
--=============================================================================
|
||||
-->> SUBLIMINAL PATH:
|
||||
--=============================================================================
|
||||
-- This script uses Subliminal to download subtitles,
|
||||
-- so make sure to specify your system's Subliminal location below:
|
||||
local subliminal = 'C:\\Users\\oscar\\scoop\\apps\\python\\current\\Scripts\\subliminal.exe'
|
||||
--=============================================================================
|
||||
-->> SUBTITLE LANGUAGE:
|
||||
--=============================================================================
|
||||
-- Specify languages in this order:
|
||||
-- { 'language name', 'ISO-639-1', 'ISO-639-2' } !
|
||||
-- (See: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
|
||||
local languages = {
|
||||
-- If subtitles are found for the first language,
|
||||
-- other languages will NOT be downloaded,
|
||||
-- so put your preferred language first:
|
||||
{ 'English', 'en', 'eng' },
|
||||
-- { 'Dutch', 'nl', 'dut' },
|
||||
-- { 'Spanish', 'es', 'spa' },
|
||||
-- { 'French', 'fr', 'fre' },
|
||||
-- { 'German', 'de', 'ger' },
|
||||
-- { 'Italian', 'it', 'ita' },
|
||||
-- { 'Portuguese', 'pt', 'por' },
|
||||
-- { 'Polish', 'pl', 'pol' },
|
||||
-- { 'Russian', 'ru', 'rus' },
|
||||
-- { 'Chinese', 'zh', 'chi' },
|
||||
-- { 'Arabic', 'ar', 'ara' },
|
||||
}
|
||||
--=============================================================================
|
||||
-->> PROVIDER LOGINS:
|
||||
--=============================================================================
|
||||
-- These are completely optional and not required
|
||||
-- for the functioning of the script!
|
||||
-- If you use any of these services, simply uncomment it
|
||||
-- and replace 'USERNAME' and 'PASSWORD' with your own:
|
||||
local logins = {
|
||||
-- { '--addic7ed', 'USERNAME', 'PASSWORD' },
|
||||
-- { '--legendastv', 'USERNAME', 'PASSWORD' },
|
||||
-- { '--opensubtitles', 'USERNAME', 'PASSWORD' },
|
||||
-- { '--subscenter', 'USERNAME', 'PASSWORD' },
|
||||
}
|
||||
--=============================================================================
|
||||
-->> ADDITIONAL OPTIONS:
|
||||
--=============================================================================
|
||||
local bools = {
|
||||
auto = false, -- Automatically download subtitles, no hotkeys required
|
||||
debug = false, -- Use `--debug` in subliminal command for debug output
|
||||
force = true, -- Force download; will overwrite existing subtitle files
|
||||
utf8 = true, -- Save all subtitle files as UTF-8
|
||||
}
|
||||
local excludes = {
|
||||
-- Movies with a path containing any of these strings/paths
|
||||
-- will be excluded from auto-downloading subtitles.
|
||||
-- Full paths are also allowed, e.g.:
|
||||
-- '/home/david/Videos',
|
||||
'no-subs-dl',
|
||||
}
|
||||
local includes = {
|
||||
-- If anything is defined here, only the movies with a path
|
||||
-- containing any of these strings/paths will auto-download subtitles.
|
||||
-- Full paths are also allowed, e.g.:
|
||||
-- '/home/david/Videos',
|
||||
}
|
||||
--=============================================================================
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
|
||||
-- Download function: download the best subtitles in most preferred language
|
||||
function download_subs(language)
|
||||
language = language or languages[1]
|
||||
if #language == 0 then
|
||||
log('No Language found\n')
|
||||
return false
|
||||
end
|
||||
|
||||
log('Searching ' .. language[1] .. ' subtitles ...', 30)
|
||||
|
||||
-- Build the `subliminal` command, starting with the executable:
|
||||
local table = { args = { subliminal } }
|
||||
local a = table.args
|
||||
|
||||
for _, login in ipairs(logins) do
|
||||
a[#a + 1] = login[1]
|
||||
a[#a + 1] = login[2]
|
||||
a[#a + 1] = login[3]
|
||||
end
|
||||
if bools.debug then
|
||||
-- To see `--debug` output start MPV from the terminal!
|
||||
a[#a + 1] = '--debug'
|
||||
end
|
||||
|
||||
a[#a + 1] = 'download'
|
||||
if bools.force then
|
||||
a[#a + 1] = '-f'
|
||||
end
|
||||
if bools.utf8 then
|
||||
a[#a + 1] = '-e'
|
||||
a[#a + 1] = 'utf-8'
|
||||
end
|
||||
|
||||
a[#a + 1] = '-l'
|
||||
a[#a + 1] = language[2]
|
||||
a[#a + 1] = '-d'
|
||||
a[#a + 1] = directory
|
||||
a[#a + 1] = filename --> Subliminal command ends with the movie filename.
|
||||
|
||||
local result = utils.subprocess(table)
|
||||
|
||||
if string.find(result.stdout, 'Downloaded 1 subtitle') then
|
||||
-- When multiple external files are present,
|
||||
-- always activate the most recently downloaded:
|
||||
mp.set_property('slang', language[2])
|
||||
-- Subtitles are downloaded successfully, so rescan to activate them:
|
||||
mp.commandv('rescan_external_files')
|
||||
log(language[1] .. ' subtitles ready!')
|
||||
return true
|
||||
else
|
||||
log('No ' .. language[1] .. ' subtitles found\n')
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Manually download second language subs by pressing 'n':
|
||||
function download_subs2()
|
||||
download_subs(languages[2])
|
||||
end
|
||||
|
||||
-- Control function: only download if necessary
|
||||
function control_downloads()
|
||||
-- Make MPV accept external subtitle files with language specifier:
|
||||
mp.set_property('sub-auto', 'fuzzy')
|
||||
-- Set subtitle language preference:
|
||||
mp.set_property('slang', languages[1][2])
|
||||
mp.msg.warn('Reactivate external subtitle files:')
|
||||
mp.commandv('rescan_external_files')
|
||||
directory, filename = utils.split_path(mp.get_property('path'))
|
||||
|
||||
if not autosub_allowed() then
|
||||
return
|
||||
end
|
||||
|
||||
sub_tracks = {}
|
||||
for _, track in ipairs(mp.get_property_native('track-list')) do
|
||||
if track['type'] == 'sub' then
|
||||
sub_tracks[#sub_tracks + 1] = track
|
||||
end
|
||||
end
|
||||
if bools.debug then -- Log subtitle properties to terminal:
|
||||
for _, track in ipairs(sub_tracks) do
|
||||
mp.msg.warn('Subtitle track', track['id'], ':\n{')
|
||||
for k, v in pairs(track) do
|
||||
if type(v) == 'string' then v = '"' .. v .. '"' end
|
||||
mp.msg.warn(' "' .. k .. '":', v)
|
||||
end
|
||||
mp.msg.warn('}\n')
|
||||
end
|
||||
end
|
||||
|
||||
for _, language in ipairs(languages) do
|
||||
if should_download_subs_in(language) then
|
||||
if download_subs(language) then return end -- Download successful!
|
||||
else return end -- No need to download!
|
||||
end
|
||||
log('No subtitles were found')
|
||||
end
|
||||
|
||||
-- Check if subtitles should be auto-downloaded:
|
||||
function autosub_allowed()
|
||||
local duration = tonumber(mp.get_property('duration'))
|
||||
local active_format = mp.get_property('file-format')
|
||||
|
||||
if not bools.auto then
|
||||
mp.msg.warn('Automatic downloading disabled!')
|
||||
return false
|
||||
elseif duration < 900 then
|
||||
mp.msg.warn('Video is less than 15 minutes\n' ..
|
||||
'=> NOT auto-downloading subtitles')
|
||||
return false
|
||||
elseif directory:find('^http') then
|
||||
mp.msg.warn('Automatic subtitle downloading is disabled for web streaming')
|
||||
return false
|
||||
elseif active_format:find('^cue') then
|
||||
mp.msg.warn('Automatic subtitle downloading is disabled for cue files')
|
||||
return false
|
||||
else
|
||||
local not_allowed = {'aiff', 'ape', 'flac', 'mp3', 'ogg', 'wav', 'wv', 'tta'}
|
||||
|
||||
for _, file_format in pairs(not_allowed) do
|
||||
if file_format == active_format then
|
||||
mp.msg.warn('Automatic subtitle downloading is disabled for audio files')
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for _, exclude in pairs(excludes) do
|
||||
local escaped_exclude = exclude:gsub('%W','%%%0')
|
||||
local excluded = directory:find(escaped_exclude)
|
||||
|
||||
if excluded then
|
||||
mp.msg.warn('This path is excluded from auto-downloading subs')
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for i, include in ipairs(includes) do
|
||||
local escaped_include = include:gsub('%W','%%%0')
|
||||
local included = directory:find(escaped_include)
|
||||
|
||||
if included then break
|
||||
elseif i == #includes then
|
||||
mp.msg.warn('This path is not included for auto-downloading subs')
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if subtitles should be downloaded in this language:
|
||||
function should_download_subs_in(language)
|
||||
for i, track in ipairs(sub_tracks) do
|
||||
local subtitles = track['external'] and
|
||||
'subtitle file' or 'embedded subtitles'
|
||||
|
||||
if not track['lang'] and (track['external'] or not track['title'])
|
||||
and i == #sub_tracks then
|
||||
local status = track['selected'] and ' active' or ' present'
|
||||
log('Unknown ' .. subtitles .. status)
|
||||
mp.msg.warn('=> NOT downloading new subtitles')
|
||||
return false -- Don't download if 'lang' key is absent
|
||||
elseif track['lang'] == language[3] or track['lang'] == language[2] or
|
||||
(track['title'] and track['title']:lower():find(language[3])) then
|
||||
if not track['selected'] then
|
||||
mp.set_property('sid', track['id'])
|
||||
log('Enabled ' .. language[1] .. ' ' .. subtitles .. '!')
|
||||
else
|
||||
log(language[1] .. ' ' .. subtitles .. ' active')
|
||||
end
|
||||
mp.msg.warn('=> NOT downloading new subtitles')
|
||||
return false -- The right subtitles are already present
|
||||
end
|
||||
end
|
||||
mp.msg.warn('No ' .. language[1] .. ' subtitles were detected\n' ..
|
||||
'=> Proceeding to download:')
|
||||
return true
|
||||
end
|
||||
|
||||
-- Log function: log to both terminal and MPV OSD (On-Screen Display)
|
||||
function log(string, secs)
|
||||
secs = secs or 2.5 -- secs defaults to 2.5 when secs parameter is absent
|
||||
mp.msg.warn(string) -- This logs to the terminal
|
||||
mp.osd_message(string, secs) -- This logs to MPV screen
|
||||
end
|
||||
|
||||
|
||||
mp.add_key_binding('b', 'download_subs', download_subs)
|
||||
mp.add_key_binding('n', 'download_subs2', download_subs2)
|
||||
mp.register_event('file-loaded', control_downloads)
|
||||
@@ -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,518 @@
|
||||
-- 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 h = require('helpers')
|
||||
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')
|
||||
|
||||
-- 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)()
|
||||
|
||||
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("Error: no track of type '%s' selected", 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 h.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("Error: unsupported codec: %s", subtitle_track['codec']), "error", 3)
|
||||
end
|
||||
local temp_sub_fp = utils.join_path(os_temp(), 'autosubsync_extracted.' .. ext)
|
||||
notify("Extracting internal subtitles...", nil, 3)
|
||||
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
|
||||
}
|
||||
if ret == nil or ret.status ~= 0 then
|
||||
return notify("Couldn't extract internal subtitle.\nMake sure the video has internal subtitles.", "error", 7)
|
||||
end
|
||||
return temp_sub_fp
|
||||
end
|
||||
|
||||
local function dump(o)
|
||||
if type(o) == 'table' then
|
||||
local s = '{ '
|
||||
for k,v in pairs(o) do
|
||||
if type(k) ~= 'number' then k = '"'..k..'"' end
|
||||
s = s .. '['..k..'] = ' .. dump(v) .. ','
|
||||
end
|
||||
return s .. '} '
|
||||
else
|
||||
return tostring(o)
|
||||
end
|
||||
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 h.is_path(config.ffmpeg_path) and not h.file_exists(engine_path) then
|
||||
return notify(
|
||||
string.format("Can't find %s executable.\nPlease specify the correct path in the config.", engine_name),
|
||||
"error",
|
||||
5
|
||||
)
|
||||
end
|
||||
|
||||
if not h.file_exists(subtitle_path) then
|
||||
return notify(
|
||||
table.concat {
|
||||
"Subtitle synchronization failed:\nCouldn't find ",
|
||||
subtitle_path or "external subtitle file."
|
||||
},
|
||||
"error",
|
||||
3
|
||||
)
|
||||
end
|
||||
|
||||
local retimed_subtitle_path = mkfp_retimed(subtitle_path)
|
||||
|
||||
notify(string.format("Starting %s...", engine_name), nil, 2)
|
||||
|
||||
local ret
|
||||
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
|
||||
ret = subprocess(args)
|
||||
else
|
||||
ret = subprocess { config.alass_path, reference_file_path, subtitle_path, retimed_subtitle_path }
|
||||
end
|
||||
|
||||
if ret == nil then
|
||||
return notify("Parsing failed or no args passed.", "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("Subtitle synchronized.", nil, 2)
|
||||
mp.set_property("sub-delay", 0)
|
||||
if config.unload_old_sub then
|
||||
mp.commandv("sub_remove", old_sid)
|
||||
end
|
||||
else
|
||||
notify("Error: couldn't add synchronized subtitle.", "error", 3)
|
||||
end
|
||||
else
|
||||
notify("Subtitle synchronization failed.", "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
|
||||
if h.is_path(config.ffmpeg_path) and not h.file_exists(config.ffmpeg_path) then
|
||||
return notify("Can't find ffmpeg executable.\nPlease specify the correct path in the config.", "error", 5)
|
||||
end
|
||||
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("There were no manual timings set, nothing to do!", "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("Error: unsupported codec: %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("Manual timings saved, loading '%s'", s.filename), "info", 7)
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Menu actions & bindings
|
||||
|
||||
ref_selector = menu:new {
|
||||
items = { 'Sync to audio', 'Sync to another subtitle', 'Save current timings', 'Cancel' },
|
||||
last_choice = 'audio',
|
||||
pos_x = 50,
|
||||
pos_y = 50,
|
||||
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', 'Cancel' },
|
||||
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 { }
|
||||
|
||||
local function is_supported_format(track)
|
||||
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
|
||||
return supported_format
|
||||
end
|
||||
|
||||
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.secondary_sid = mp.get_property_native('secondary-sid')
|
||||
self.tracks = {}
|
||||
self.items = {}
|
||||
|
||||
for _, track in ipairs(self.all_sub_tracks) do
|
||||
if (not track.selected or track.id == self.secondary_sid) and is_supported_format(track) then
|
||||
table.insert(self.tracks, track)
|
||||
table.insert(
|
||||
self.items,
|
||||
string.format(
|
||||
"%s #%s - %s%s",
|
||||
(track.external and 'External' or 'Internal'),
|
||||
track['id'],
|
||||
(track.lang or (track.title and track.title:gsub('^.*%.', '') or 'unknown')),
|
||||
(track.selected and ' (active)' or '')
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if #self.items == 0 then
|
||||
notify("No supported subtitle tracks found.", "warn", 5)
|
||||
return
|
||||
end
|
||||
|
||||
table.insert(self.items, "Cancel")
|
||||
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] = h.is_empty(config[config_key]) and h.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,41 @@
|
||||
local utils = require('mp.utils')
|
||||
local self = {}
|
||||
|
||||
function self.is_empty(var)
|
||||
return var == nil or var == '' or (type(var) == 'table' and next(var) == nil)
|
||||
end
|
||||
|
||||
function self.file_exists(filepath)
|
||||
if not self.is_empty(filepath) then
|
||||
local info = utils.file_info(filepath)
|
||||
if info and info.is_file then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function self.alt_dirs()
|
||||
return {
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
utils.join_path(os.getenv("HOME") or "~", '.local/bin'),
|
||||
}
|
||||
end
|
||||
|
||||
function self.find_executable(name)
|
||||
local exec_path
|
||||
for _, path in pairs(self.alt_dirs()) do
|
||||
exec_path = utils.join_path(path, name)
|
||||
if self.file_exists(exec_path) then
|
||||
return exec_path
|
||||
end
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
function self.is_path(str)
|
||||
return not not string.match(str, '[/\\]')
|
||||
end
|
||||
|
||||
return self
|
||||
@@ -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
|
||||
+2741
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,675 @@
|
||||
-- repl.lua -- A graphical REPL for mpv input commands
|
||||
--
|
||||
-- © 2016, James Ross-Gowan
|
||||
--
|
||||
-- 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.
|
||||
|
||||
local utils = require 'mp.utils'
|
||||
local options = require 'mp.options'
|
||||
local assdraw = require 'mp.assdraw'
|
||||
|
||||
-- Default options
|
||||
local opts = {
|
||||
-- All drawing is scaled by this value, including the text borders and the
|
||||
-- cursor. Change it if you have a high-DPI display.
|
||||
scale = 1,
|
||||
-- Set the font used for the REPL and the console. This probably doesn't
|
||||
-- have to be a monospaced font.
|
||||
font = 'monospace',
|
||||
-- Set the font size used for the REPL and the console. This will be
|
||||
-- multiplied by "scale."
|
||||
['font-size'] = 16,
|
||||
}
|
||||
|
||||
function 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/cocoa-force-dedicated-gpu', o) ~= o then
|
||||
return 'macos'
|
||||
end
|
||||
return 'linux'
|
||||
end
|
||||
|
||||
-- Pick a better default font for Windows and macOS
|
||||
local platform = detect_platform()
|
||||
if platform == 'windows' then
|
||||
opts.font = 'Consolas'
|
||||
elseif platform == 'macos' then
|
||||
opts.font = 'Menlo'
|
||||
end
|
||||
|
||||
-- Apply user-set options
|
||||
options.read_options(opts)
|
||||
|
||||
-- Build a list of commands, properties and options for tab-completion
|
||||
local option_info = {
|
||||
'name', 'type', 'set-from-commandline', 'set-locally', 'default-value',
|
||||
'min', 'max', 'choices',
|
||||
}
|
||||
local cmd_list = {
|
||||
'ignore', 'seek', 'revert-seek', 'quit', 'quit-watch-later', 'stop',
|
||||
'frame-step', 'frame-back-step', 'playlist-next', 'playlist-prev',
|
||||
'playlist-shuffle', 'sub-step', 'sub-seek', 'osd', 'print-text',
|
||||
'show-text', 'show-progress', 'sub-add', 'sub-remove', 'sub-reload',
|
||||
'tv-last-channel', 'screenshot', 'screenshot-to-file', 'screenshot-raw',
|
||||
'loadfile', 'loadlist', 'playlist-clear', 'playlist-remove',
|
||||
'playlist-move', 'run', 'set', 'add', 'cycle', 'multiply', 'cycle-values',
|
||||
'enable-section', 'disable-section', 'define-section', 'ab-loop',
|
||||
'drop-buffers', 'af', 'af-command', 'ao-reload', 'vf', 'vf-command',
|
||||
'script-binding', 'script-message', 'script-message-to', 'overlay-add',
|
||||
'overlay-remove', 'write-watch-later-config', 'hook-add', 'hook-ack',
|
||||
'mouse', 'keypress', 'keydown', 'keyup', 'audio-add', 'audio-remove',
|
||||
'audio-reload', 'rescan-external-file', 'apply-profile', 'load-script',
|
||||
}
|
||||
local prop_list = mp.get_property_native('property-list')
|
||||
for _, opt in ipairs(mp.get_property_native('options')) do
|
||||
prop_list[#prop_list + 1] = 'options/' .. opt
|
||||
prop_list[#prop_list + 1] = 'file-local-options/' .. opt
|
||||
prop_list[#prop_list + 1] = 'option-info/' .. opt
|
||||
for _, p in ipairs(option_info) do
|
||||
prop_list[#prop_list + 1] = 'option-info/' .. opt .. '/' .. p
|
||||
end
|
||||
end
|
||||
|
||||
local repl_active = false
|
||||
local insert_mode = false
|
||||
local line = ''
|
||||
local cursor = 1
|
||||
local history = {}
|
||||
local history_pos = 1
|
||||
local log_ring = {}
|
||||
|
||||
-- Add a line to the log buffer (which is limited to 100 lines)
|
||||
function log_add(style, text)
|
||||
log_ring[#log_ring + 1] = { style = style, text = text }
|
||||
if #log_ring > 100 then
|
||||
table.remove(log_ring, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- Escape a string for verbatim display on the OSD
|
||||
function 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')
|
||||
return str
|
||||
end
|
||||
|
||||
-- Render the REPL and console as an ASS OSD
|
||||
function update()
|
||||
local screenx, screeny, aspect = mp.get_osd_size()
|
||||
screenx = screenx / opts.scale
|
||||
screeny = screeny / opts.scale
|
||||
|
||||
-- Clear the OSD if the REPL is not active
|
||||
if not repl_active then
|
||||
mp.set_osd_ass(screenx, screeny, '')
|
||||
return
|
||||
end
|
||||
|
||||
local ass = assdraw.ass_new()
|
||||
local style = '{\\r' ..
|
||||
'\\1a&H00&\\3a&H00&\\4a&H99&' ..
|
||||
'\\1c&Heeeeee&\\3c&H111111&\\4c&H000000&' ..
|
||||
'\\fn' .. opts.font .. '\\fs' .. opts['font-size'] ..
|
||||
'\\bord2\\xshad0\\yshad1\\fsp0\\q1}'
|
||||
-- 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 = opts['font-size'] * 8
|
||||
local cglyph = '{\\r' ..
|
||||
'\\1a&H44&\\3a&H44&\\4a&H99&' ..
|
||||
'\\1c&Heeeeee&\\3c&Heeeeee&\\4c&H000000&' ..
|
||||
'\\xbord0.5\\ybord0\\xshad0\\yshad1\\p4\\pbo24}' ..
|
||||
'm 0 0 l 1 0 l 1 ' .. cheight .. ' l 0 ' .. cheight ..
|
||||
'{\\p0}'
|
||||
local before_cur = ass_escape(line:sub(1, cursor - 1))
|
||||
local after_cur = ass_escape(line:sub(cursor))
|
||||
|
||||
-- Render log messages as ASS. This will render at most screeny / font-size
|
||||
-- messages.
|
||||
local log_ass = ''
|
||||
local log_messages = #log_ring
|
||||
local log_max_lines = math.ceil(screeny / opts['font-size'])
|
||||
if log_max_lines < log_messages then
|
||||
log_messages = log_max_lines
|
||||
end
|
||||
for i = #log_ring - log_messages + 1, #log_ring do
|
||||
log_ass = log_ass .. style .. log_ring[i].style .. ass_escape(log_ring[i].text)
|
||||
end
|
||||
|
||||
ass:new_event()
|
||||
ass:an(1)
|
||||
ass:pos(2, screeny - 2)
|
||||
ass:append(log_ass .. '\\N')
|
||||
ass:append(style .. '> ' .. before_cur)
|
||||
ass:append(cglyph)
|
||||
ass:append(style .. after_cur)
|
||||
|
||||
-- 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:pos(2, screeny - 2)
|
||||
ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
|
||||
ass:append(cglyph)
|
||||
ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
|
||||
|
||||
mp.set_osd_ass(screenx, screeny, ass.text)
|
||||
end
|
||||
|
||||
-- Set the REPL visibility (`, Esc)
|
||||
function set_active(active)
|
||||
if active == repl_active then return end
|
||||
if active then
|
||||
repl_active = true
|
||||
insert_mode = false
|
||||
mp.enable_key_bindings('repl-input', 'allow-hide-cursor+allow-vo-dragging')
|
||||
else
|
||||
repl_active = false
|
||||
mp.disable_key_bindings('repl-input')
|
||||
end
|
||||
update()
|
||||
end
|
||||
|
||||
-- Show the repl if hidden and replace its contents with 'text'
|
||||
-- (script-message-to repl type)
|
||||
function show_and_type(text)
|
||||
text = text or ''
|
||||
|
||||
-- Save the line currently being edited, just in case
|
||||
if line ~= text and line ~= '' and history[#history] ~= line then
|
||||
history[#history + 1] = line
|
||||
end
|
||||
|
||||
line = text
|
||||
cursor = line:len() + 1
|
||||
history_pos = #history + 1
|
||||
insert_mode = false
|
||||
if repl_active then
|
||||
update()
|
||||
else
|
||||
set_active(true)
|
||||
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 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 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 (' '-'~', Shift+Enter)
|
||||
function handle_char_input(c)
|
||||
if insert_mode then
|
||||
line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor))
|
||||
else
|
||||
line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
|
||||
end
|
||||
cursor = cursor + 1
|
||||
update()
|
||||
end
|
||||
|
||||
-- Remove the character behind the cursor (Backspace)
|
||||
function handle_backspace()
|
||||
if cursor <= 1 then return end
|
||||
local prev = prev_utf8(line, cursor)
|
||||
line = line:sub(1, prev - 1) .. line:sub(cursor)
|
||||
cursor = prev
|
||||
update()
|
||||
end
|
||||
|
||||
-- Remove the character in front of the cursor (Del)
|
||||
function handle_del()
|
||||
if cursor > line:len() then return end
|
||||
line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
|
||||
update()
|
||||
end
|
||||
|
||||
-- Toggle insert mode (Ins)
|
||||
function handle_ins()
|
||||
insert_mode = not insert_mode
|
||||
end
|
||||
|
||||
-- Move the cursor to the next character (Right)
|
||||
function next_char(amount)
|
||||
cursor = next_utf8(line, cursor)
|
||||
update()
|
||||
end
|
||||
|
||||
-- Move the cursor to the previous character (Left)
|
||||
function prev_char(amount)
|
||||
cursor = prev_utf8(line, cursor)
|
||||
update()
|
||||
end
|
||||
|
||||
-- Clear the current line (Ctrl+C)
|
||||
function clear()
|
||||
line = ''
|
||||
cursor = 1
|
||||
insert_mode = false
|
||||
history_pos = #history + 1
|
||||
update()
|
||||
end
|
||||
|
||||
-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D)
|
||||
function maybe_exit()
|
||||
if line == '' then
|
||||
set_active(false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Run the current command and clear the line (Enter)
|
||||
function handle_enter()
|
||||
if line == '' then
|
||||
return
|
||||
end
|
||||
if history[#history] ~= line then
|
||||
history[#history + 1] = line
|
||||
end
|
||||
|
||||
mp.command(line)
|
||||
clear()
|
||||
end
|
||||
|
||||
-- Go to the specified position in the command history
|
||||
function go_history(new_pos)
|
||||
local old_pos = history_pos
|
||||
history_pos = new_pos
|
||||
|
||||
-- Restrict the position to a legal value
|
||||
if history_pos > #history + 1 then
|
||||
history_pos = #history + 1
|
||||
elseif history_pos < 1 then
|
||||
history_pos = 1
|
||||
end
|
||||
|
||||
-- Do nothing if the history position didn't actually change
|
||||
if 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 == #history + 1 and line ~= '' and history[#history] ~= line then
|
||||
history[#history + 1] = line
|
||||
end
|
||||
|
||||
-- Now show the history line (or a blank line for #history + 1)
|
||||
if history_pos <= #history then
|
||||
line = history[history_pos]
|
||||
else
|
||||
line = ''
|
||||
end
|
||||
cursor = line:len() + 1
|
||||
insert_mode = false
|
||||
update()
|
||||
end
|
||||
|
||||
-- Go to the specified relative position in the command history (Up, Down)
|
||||
function move_history(amount)
|
||||
go_history(history_pos + amount)
|
||||
end
|
||||
|
||||
-- Go to the first command in the command history (PgUp)
|
||||
function handle_pgup()
|
||||
go_history(1)
|
||||
end
|
||||
|
||||
-- Stop browsing history and start editing a blank line (PgDown)
|
||||
function handle_pgdown()
|
||||
go_history(#history + 1)
|
||||
end
|
||||
|
||||
-- Move to the start of the current word, or if already at the start, the start
|
||||
-- of the previous word. (Ctrl+Left)
|
||||
function 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.
|
||||
cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1
|
||||
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 next_word()
|
||||
cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1
|
||||
update()
|
||||
end
|
||||
|
||||
-- List of tab-completions:
|
||||
-- pattern: A Lua pattern used in string:find. Should return the start and
|
||||
-- end positions of the word to be completed in the first and second
|
||||
-- capture groups (using the empty parenthesis notation "()")
|
||||
-- list: A list of candidate completion values.
|
||||
-- append: An extra string to be appended to the end of a successful
|
||||
-- completion. It is only appended if 'list' contains exactly one
|
||||
-- match.
|
||||
local completers = {
|
||||
{ pattern = '^%s*()[%w_-]+()$', list = cmd_list, append = ' ' },
|
||||
{ pattern = '^%s*set%s+()[%w_/-]+()$', list = prop_list, append = ' ' },
|
||||
{ pattern = '^%s*set%s+"()[%w_/-]+()$', list = prop_list, append = '" ' },
|
||||
{ pattern = '^%s*add%s+()[%w_/-]+()$', list = prop_list, append = ' ' },
|
||||
{ pattern = '^%s*add%s+"()[%w_/-]+()$', list = prop_list, append = '" ' },
|
||||
{ pattern = '^%s*cycle%s+()[%w_/-]+()$', list = prop_list, append = ' ' },
|
||||
{ pattern = '^%s*cycle%s+"()[%w_/-]+()$', list = prop_list, append = '" ' },
|
||||
{ pattern = '^%s*multiply%s+()[%w_/-]+()$', list = prop_list, append = ' ' },
|
||||
{ pattern = '^%s*multiply%s+"()[%w_/-]+()$', list = prop_list, append = '" ' },
|
||||
{ pattern = '${()[%w_/-]+()$', list = prop_list, append = '}' },
|
||||
}
|
||||
|
||||
-- Use 'list' to find possible tab-completions for 'part.' Returns the longest
|
||||
-- common prefix of all the matching list items and a flag that indicates
|
||||
-- whether the match was unique or not.
|
||||
function complete_match(part, list)
|
||||
local completion = nil
|
||||
local full_match = false
|
||||
|
||||
for _, candidate in ipairs(list) do
|
||||
if candidate:sub(1, part:len()) == part then
|
||||
if completion and completion ~= candidate then
|
||||
local prefix_len = part:len()
|
||||
while completion:sub(1, prefix_len + 1)
|
||||
== candidate:sub(1, prefix_len + 1) do
|
||||
prefix_len = prefix_len + 1
|
||||
end
|
||||
completion = candidate:sub(1, prefix_len)
|
||||
full_match = false
|
||||
else
|
||||
completion = candidate
|
||||
full_match = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return completion, full_match
|
||||
end
|
||||
|
||||
-- Complete the option or property at the cursor (TAB)
|
||||
function complete()
|
||||
local before_cur = line:sub(1, cursor - 1)
|
||||
local after_cur = line:sub(cursor)
|
||||
|
||||
-- Try the first completer that works
|
||||
for _, completer in ipairs(completers) do
|
||||
-- Completer patterns should return the start and end of the word to be
|
||||
-- completed as the first and second capture groups
|
||||
local _, _, s, e = before_cur:find(completer.pattern)
|
||||
if not s then
|
||||
-- Multiple input commands can be separated by semicolons, so all
|
||||
-- completions that are anchored at the start of the string with
|
||||
-- '^' can start from a semicolon as well. Replace ^ with ; and try
|
||||
-- to match again.
|
||||
_, _, s, e = before_cur:find(completer.pattern:gsub('^^', ';'))
|
||||
end
|
||||
if s then
|
||||
-- If the completer's pattern found a word, check the completer's
|
||||
-- list for possible completions
|
||||
local part = before_cur:sub(s, e)
|
||||
local c, full = complete_match(part, completer.list)
|
||||
if c then
|
||||
-- If there was only one full match from the list, add
|
||||
-- completer.append to the final string. This is normally a
|
||||
-- space or a quotation mark followed by a space.
|
||||
if full and completer.append then
|
||||
c = c .. completer.append
|
||||
end
|
||||
|
||||
-- Insert the completion and update
|
||||
before_cur = before_cur:sub(1, s - 1) .. c
|
||||
cursor = before_cur:len() + 1
|
||||
line = before_cur .. after_cur
|
||||
update()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Move the cursor to the beginning of the line (HOME)
|
||||
function go_home()
|
||||
cursor = 1
|
||||
update()
|
||||
end
|
||||
|
||||
-- Move the cursor to the end of the line (END)
|
||||
function go_end()
|
||||
cursor = line:len() + 1
|
||||
update()
|
||||
end
|
||||
|
||||
-- Delete from the cursor to the end of the word (Ctrl+W)
|
||||
function del_word()
|
||||
local before_cur = line:sub(1, cursor - 1)
|
||||
local after_cur = line:sub(cursor)
|
||||
|
||||
before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
|
||||
line = before_cur .. after_cur
|
||||
cursor = before_cur:len() + 1
|
||||
update()
|
||||
end
|
||||
|
||||
-- Delete from the cursor to the end of the line (Ctrl+K)
|
||||
function del_to_eol()
|
||||
line = line:sub(1, cursor - 1)
|
||||
update()
|
||||
end
|
||||
|
||||
-- Delete from the cursor back to the start of the line (Ctrl+U)
|
||||
function del_to_start()
|
||||
line = line:sub(cursor)
|
||||
cursor = 1
|
||||
update()
|
||||
end
|
||||
|
||||
-- Empty the log buffer of all messages (Ctrl+L)
|
||||
function clear_log_buffer()
|
||||
log_ring = {}
|
||||
update()
|
||||
end
|
||||
|
||||
-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
|
||||
function get_clipboard(clip)
|
||||
if platform == 'linux' 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 == '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 only.)
|
||||
function paste(clip)
|
||||
local text = get_clipboard(clip)
|
||||
local before_cur = line:sub(1, cursor - 1)
|
||||
local after_cur = line:sub(cursor)
|
||||
line = before_cur .. text .. after_cur
|
||||
cursor = cursor + text:len()
|
||||
update()
|
||||
end
|
||||
|
||||
-- The REPL has pretty specific requirements for key bindings that aren't
|
||||
-- really satisified by any of mpv's helper methods, since they must be in
|
||||
-- their own input section, but they must also raise events on key-repeat.
|
||||
-- Hence, this function manually creates an input section and puts a list of
|
||||
-- bindings in it.
|
||||
function add_repl_bindings(bindings)
|
||||
local cfg = ''
|
||||
for i, binding in ipairs(bindings) do
|
||||
local key = binding[1]
|
||||
local fn = binding[2]
|
||||
local name = '__repl_binding_' .. i
|
||||
mp.add_key_binding(nil, name, fn, 'repeatable')
|
||||
cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' ..
|
||||
name .. '\n'
|
||||
end
|
||||
mp.commandv('define-section', 'repl-input', cfg, 'force')
|
||||
end
|
||||
|
||||
-- Mapping from characters to mpv key names
|
||||
local binding_name_map = {
|
||||
[' '] = 'SPACE',
|
||||
['#'] = 'SHARP',
|
||||
}
|
||||
|
||||
-- List of input bindings. This is a weird mashup between common GUI text-input
|
||||
-- bindings and readline bindings.
|
||||
local bindings = {
|
||||
{ 'esc', function() set_active(false) end },
|
||||
{ 'enter', handle_enter },
|
||||
{ 'shift+enter', function() handle_char_input('\n') end },
|
||||
{ 'bs', handle_backspace },
|
||||
{ 'shift+bs', handle_backspace },
|
||||
{ 'del', handle_del },
|
||||
{ 'shift+del', handle_del },
|
||||
{ 'ins', handle_ins },
|
||||
{ 'shift+ins', function() paste(false) end },
|
||||
{ 'mouse_btn1', function() paste(false) end },
|
||||
{ 'left', function() prev_char() end },
|
||||
{ 'right', function() next_char() end },
|
||||
{ 'up', function() move_history(-1) end },
|
||||
{ 'axis_up', function() move_history(-1) end },
|
||||
{ 'mouse_btn3', function() move_history(-1) end },
|
||||
{ 'down', function() move_history(1) end },
|
||||
{ 'axis_down', function() move_history(1) end },
|
||||
{ 'mouse_btn4', function() move_history(1) end },
|
||||
{ 'axis_left', function() end },
|
||||
{ 'axis_right', function() end },
|
||||
{ 'ctrl+left', prev_word },
|
||||
{ 'ctrl+right', next_word },
|
||||
{ 'tab', complete },
|
||||
{ 'home', go_home },
|
||||
{ 'end', go_end },
|
||||
{ 'pgup', handle_pgup },
|
||||
{ 'pgdwn', handle_pgdown },
|
||||
{ 'ctrl+c', clear },
|
||||
{ 'ctrl+d', maybe_exit },
|
||||
{ 'ctrl+k', del_to_eol },
|
||||
{ 'ctrl+l', clear_log_buffer },
|
||||
{ 'ctrl+u', del_to_start },
|
||||
{ 'ctrl+v', function() paste(true) end },
|
||||
{ 'meta+v', function() paste(true) end },
|
||||
{ 'ctrl+w', del_word },
|
||||
}
|
||||
-- Add bindings for all the printable US-ASCII characters from ' ' to '~'
|
||||
-- inclusive. Note, this is a pretty hacky way to do text input. mpv's input
|
||||
-- system was designed for single-key key bindings rather than text input, so
|
||||
-- things like dead-keys and non-ASCII input won't work. This is probably okay
|
||||
-- though, since all mpv's commands and properties can be represented in ASCII.
|
||||
for b = (' '):byte(), ('~'):byte() do
|
||||
local c = string.char(b)
|
||||
local binding = binding_name_map[c] or c
|
||||
bindings[#bindings + 1] = {binding, function() handle_char_input(c) end}
|
||||
end
|
||||
add_repl_bindings(bindings)
|
||||
|
||||
-- Add a global binding for enabling the REPL. While it's enabled, its bindings
|
||||
-- will take over and it can be closed with ESC.
|
||||
mp.add_key_binding('`', 'repl-enable', function()
|
||||
set_active(true)
|
||||
end)
|
||||
|
||||
-- Add a script-message to show the REPL and fill it with the provided text
|
||||
mp.register_script_message('type', function(text)
|
||||
show_and_type(text)
|
||||
end)
|
||||
|
||||
-- Redraw the REPL when the OSD size changes. This is needed because the
|
||||
-- PlayRes of the OSD will need to be adjusted.
|
||||
mp.observe_property('osd-width', 'native', update)
|
||||
mp.observe_property('osd-height', 'native', update)
|
||||
|
||||
-- Watch for log-messages and print them in the REPL console
|
||||
mp.enable_messages('info')
|
||||
mp.register_event('log-message', function(e)
|
||||
-- Ignore log messages from the OSD because of paranoia, since writing them
|
||||
-- to the OSD could generate more messages in an infinite loop.
|
||||
if e.prefix:sub(1, 3) == 'osd' then return end
|
||||
|
||||
-- Use color for warn/error/fatal messages. Colors are stolen from base16
|
||||
-- Eighties by Chris Kempson.
|
||||
local style = ''
|
||||
if e.level == 'warn' then
|
||||
style = '{\\1c&H66ccff&}'
|
||||
elseif e.level == 'error' then
|
||||
style = '{\\1c&H7a77f2&}'
|
||||
elseif e.level == 'fatal' then
|
||||
style = '{\\1c&H5791f9&\\b1}'
|
||||
end
|
||||
|
||||
log_add(style, '[' .. e.prefix .. '] ' .. e.text)
|
||||
update()
|
||||
end)
|
||||
@@ -0,0 +1,940 @@
|
||||
-- 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 generation size in pixels (scaled down to fit)
|
||||
-- Values are scaled when hidpi is enabled
|
||||
max_height = 200,
|
||||
max_width = 200,
|
||||
|
||||
-- Scale factor for thumbnail display size (requires mpv 0.38+)
|
||||
-- Note that this is lower quality than increasing max_height and max_width
|
||||
scale_factor = 1,
|
||||
|
||||
-- Apply tone-mapping, no to disable
|
||||
tone_mapping = "auto",
|
||||
|
||||
-- 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"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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, y
|
||||
local last_x, last_y
|
||||
|
||||
local last_seek_time
|
||||
|
||||
local effective_w, effective_h = options.max_width, options.max_height
|
||||
local real_w, real_h
|
||||
local last_real_w, last_real_h
|
||||
|
||||
local script_name
|
||||
|
||||
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 tone_mappings = {["none"]=true, ["clip"]=true, ["linear"]=true, ["gamma"]=true, ["reinhard"]=true, ["hable"]=true, ["mobius"]=true}
|
||||
local last_tone_mapping
|
||||
|
||||
local last_vf_reset = ""
|
||||
local last_vf_runtime = ""
|
||||
|
||||
local last_rotate = 0
|
||||
|
||||
local par = ""
|
||||
local last_par = ""
|
||||
|
||||
local last_crop = nil
|
||||
|
||||
local last_has_vid = 0
|
||||
local has_vid = 0
|
||||
|
||||
local file_timer
|
||||
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 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
|
||||
|
||||
options.scale_factor = math.floor(options.scale_factor)
|
||||
|
||||
local mpv_path = options.mpv_path
|
||||
|
||||
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 vo_tone_mapping()
|
||||
local passes = mp.get_property_native("vo-passes")
|
||||
if passes and passes["fresh"] then
|
||||
for k, v in pairs(passes["fresh"]) do
|
||||
for k2, v2 in pairs(v) do
|
||||
if k2 == "desc" and v2 then
|
||||
local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map")
|
||||
if tone_mapping then
|
||||
return tone_mapping
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function vf_string(filters, full)
|
||||
local vf = ""
|
||||
local vf_table = properties["vf"]
|
||||
|
||||
if (properties["video-crop"] or "") ~= "" then
|
||||
vf = "lavfi-crop="..string.gsub(properties["video-crop"], "(%d*)x?(%d*)%+(%d+)%+(%d+)", "w=%1:h=%2:x=%3:y=%4")..","
|
||||
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 width and height then
|
||||
vf = string.gsub(vf, "w=:h=:", "w="..width..":h="..height..":")
|
||||
end
|
||||
end
|
||||
|
||||
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 and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then
|
||||
if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then
|
||||
local tone_mapping = options.tone_mapping
|
||||
if tone_mapping == "auto" then
|
||||
tone_mapping = last_tone_mapping or properties["tone-mapping"]
|
||||
if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then
|
||||
tone_mapping = vo_tone_mapping()
|
||||
end
|
||||
end
|
||||
if not tone_mappings[tone_mapping] then
|
||||
tone_mapping = "hable"
|
||||
end
|
||||
last_tone_mapping = tone_mapping
|
||||
vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap="..tone_mapping..",zscale=transfer=bt709,"
|
||||
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 image = properties["current-tracks/video"] and properties["current-tracks/video"]["image"]
|
||||
local albumart = image and properties["current-tracks/video"]["albumart"]
|
||||
|
||||
disabled = (w or 0) == 0 or (h or 0) == 0 or
|
||||
has_vid == 0 or
|
||||
(properties["demuxer-via-network"] and not options.network) 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 * options.scale_factor, height=h * options.scale_factor, scale_factor=options.scale_factor, 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", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal",
|
||||
"--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no", "--load-auto-profiles=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",
|
||||
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
|
||||
"--vd-lavc-skiploopfilter=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),
|
||||
"--sws-scaler=fast-bilinear",
|
||||
"--video-rotate="..last_rotate,
|
||||
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail
|
||||
}
|
||||
|
||||
if not pre_0_30_0 then
|
||||
table.insert(args, "--sws-allow-zimg=no")
|
||||
end
|
||||
|
||||
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)
|
||||
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
|
||||
options.tone_mapping = "no"
|
||||
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)
|
||||
if os_name == "windows" then
|
||||
mp.commandv("script-message-to", "mpvnet", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||
mp.commandv("script-message", "mpv.net", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||
end
|
||||
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
|
||||
local scale_w, scale_h = options.scale_factor ~= 1 and (w * options.scale_factor) or nil, options.scale_factor ~= 1 and (h * options.scale_factor) or nil
|
||||
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), scale_w, scale_h})
|
||||
else
|
||||
mp.command_native_async({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w), scale_w, scale_h}, function() end)
|
||||
end
|
||||
elseif script then
|
||||
local json, err = mp.utils.format_json({width=w, height=h, scale_factor=options.scale_factor, 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, last_y = x, 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 or last_crop ~= properties["video-crop"]
|
||||
|
||||
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_crop = properties["video-crop"]
|
||||
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
|
||||
if name == "tone-mapping" then
|
||||
last_tone_mapping = nil
|
||||
end
|
||||
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_tone_mapping = 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("demuxer-via-network", "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("video-crop", "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)
|
||||
Reference in New Issue
Block a user