init
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
--lite version of the code written by sorayuki
|
||||
--only keep the function to record the histroy and recover it
|
||||
|
||||
local mp = require 'mp'
|
||||
local utils = require 'mp.utils'
|
||||
local options = require 'mp.options'
|
||||
local msg = require 'mp.msg' -- this is for debugging
|
||||
|
||||
local o = {
|
||||
enabled = true,
|
||||
-- eng=English, chs=Chinese Simplified
|
||||
language = 'eng',
|
||||
timeout = 15,
|
||||
save_period = 30,
|
||||
-- Set '/:dir%mpvconf%/historybookmarks' to use mpv config directory
|
||||
-- OR change to '/:dir%script%/historybookmarks' for placing it in the same directory of script
|
||||
-- OR change to '~~/historybookmarks' for sub path of mpv portable_config directory
|
||||
-- OR write any variable using '/:var', such as: '/:var%APPDATA%/mpv/historybookmarks' or '/:var%HOME%/mpv/historybookmarks'
|
||||
-- OR specify the absolute path
|
||||
history_dir = "/:dir%mpvconf%/historybookmarks",
|
||||
-- specifies the extension of the history-bookmark file
|
||||
bookmark_ext = ".mpv.history",
|
||||
-- use hash to bookmark_name
|
||||
hash = true,
|
||||
-- set false to get playlist from directory
|
||||
use_playlist = true,
|
||||
-- specifies a whitelist of files to find in a directory
|
||||
whitelist = "3gp,amr,amv,asf,avi,avi,bdmv,f4v,flv,m2ts,m4v,mkv,mov,mp4,mpeg,mpg,ogv,rm,rmvb,ts,vob,webm,wmv",
|
||||
-- excluded directories for shared, #windows: ["X:", "Z:", "F:/Download/", "Download"]
|
||||
excluded_dir = [[
|
||||
[]
|
||||
]],
|
||||
included_dir = [[
|
||||
[]
|
||||
]]
|
||||
}
|
||||
options.read_options(o, _, function() end)
|
||||
|
||||
o.excluded_dir = utils.parse_json(o.excluded_dir)
|
||||
o.included_dir = utils.parse_json(o.included_dir)
|
||||
|
||||
local file_loaded = false
|
||||
|
||||
local locals = {
|
||||
['eng'] = {
|
||||
msg1 = 'Resume successfully',
|
||||
msg2 = 'Resume the last played file in current directory',
|
||||
msg3 = 'Press 1 to confirm, 0 to cancel',
|
||||
},
|
||||
['chs'] = {
|
||||
msg1 = '成功恢复上次播放',
|
||||
msg2 = '是否恢复当前目录的上次播放文件',
|
||||
msg3 = '按1确认,按0取消',
|
||||
}
|
||||
}
|
||||
|
||||
-- apply lang opts
|
||||
local texts = locals[o.language]
|
||||
|
||||
-- `pl` stands for playlist
|
||||
local path = nil
|
||||
local dir = nil
|
||||
local fname = nil
|
||||
local pl_count = 0
|
||||
local pl_name = nil
|
||||
local pl_path = nil
|
||||
local pl_list = {}
|
||||
local pl_idx = 1
|
||||
local current_idx = 1
|
||||
local bookmark_path = nil
|
||||
local history_dir = nil
|
||||
local normalize_path = nil
|
||||
|
||||
local wait_msg
|
||||
local on_key = false
|
||||
|
||||
if o.history_dir:find('^/:dir%%mpvconf%%') then
|
||||
history_dir = o.history_dir:gsub('/:dir%%mpvconf%%', mp.find_config_file('.'))
|
||||
elseif o.history_dir:find('^/:dir%%script%%') then
|
||||
history_dir = o.history_dir:gsub('/:dir%%script%%', mp.find_config_file('scripts'))
|
||||
elseif o.history_dir:find('/:var%%(.*)%%') then
|
||||
local os_variable = o.history_dir:match('/:var%%(.*)%%')
|
||||
history_dir = o.history_dir:gsub('/:var%%(.*)%%', os.getenv(os_variable))
|
||||
else
|
||||
history_dir = mp.command_native({ "expand-path", o.history_dir }) -- Expands both ~ and ~~
|
||||
end
|
||||
|
||||
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, detect path separator, windows uses backslashes
|
||||
--create history_dir if it doesn't exist
|
||||
if history_dir ~= '' then
|
||||
local meta = utils.file_info(history_dir)
|
||||
if not meta or not meta.is_dir then
|
||||
local windows_args = { 'powershell', '-NoProfile', '-Command', 'mkdir', string.format("\"%s\"", history_dir) }
|
||||
local unix_args = { 'mkdir', '-p', history_dir }
|
||||
local args = is_windows and windows_args or unix_args
|
||||
local res = mp.command_native({ name = "subprocess", capture_stdout = true, playback_only = false, args = args })
|
||||
if res.status ~= 0 then
|
||||
msg.error("Failed to create history_dir save directory " .. history_dir ..
|
||||
". Error: " .. (res.error or "unknown"))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function split(input)
|
||||
local ret = {}
|
||||
for str in string.gmatch(input, "([^,]+)") do
|
||||
ret[#ret + 1] = str
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local ext_whitelist = split(o.whitelist)
|
||||
|
||||
local function exclude(extension)
|
||||
if #ext_whitelist > 0 then
|
||||
for _, ext in pairs(ext_whitelist) do
|
||||
if extension == ext then
|
||||
return true
|
||||
end
|
||||
end
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local function is_protocol(path)
|
||||
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
|
||||
end
|
||||
|
||||
local function need_ignore(tab, val)
|
||||
for _, element in pairs(tab) do
|
||||
if string.find(val, element) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function tablelength(tab)
|
||||
local count = 0
|
||||
for _, _ in pairs(tab) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local message_overlay = mp.create_osd_overlay('ass-events')
|
||||
local message_timer = mp.add_timeout(1, function ()
|
||||
message_overlay:remove()
|
||||
end, true)
|
||||
|
||||
function show_message(text, time)
|
||||
message_timer:kill()
|
||||
message_timer.timeout = time or 1
|
||||
message_overlay.data = text
|
||||
message_overlay:update()
|
||||
message_timer:resume()
|
||||
end
|
||||
|
||||
local function normalize(path)
|
||||
if normalize_path ~= nil then
|
||||
if normalize_path then
|
||||
path = mp.command_native({"normalize-path", path})
|
||||
else
|
||||
local directory = mp.get_property("working-directory", "")
|
||||
path = utils.join_path(directory, path:gsub('^%.[\\/]',''))
|
||||
if is_windows then path = path:gsub("\\", "/") end
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
normalize_path = false
|
||||
|
||||
local commands = mp.get_property_native("command-list", {})
|
||||
for _, command in ipairs(commands) do
|
||||
if command.name == "normalize-path" then
|
||||
normalize_path = true
|
||||
break
|
||||
end
|
||||
end
|
||||
return normalize(path)
|
||||
end
|
||||
|
||||
function refresh_globals()
|
||||
path = mp.get_property("path")
|
||||
fname = mp.get_property("filename")
|
||||
pl_count = mp.get_property_number('playlist-count', 0)
|
||||
if path and not is_protocol(path) then
|
||||
path = normalize(path)
|
||||
dir = utils.split_path(path)
|
||||
else
|
||||
dir = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- for unix use only
|
||||
-- returns a table of command path and varargs, or nil if command was not found
|
||||
local function command_exists(command, ...)
|
||||
msg.debug("looking for command:", command)
|
||||
-- msg.debug("args:", )
|
||||
local process = mp.command_native({
|
||||
name = "subprocess",
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
playback_only = false,
|
||||
args = {"sh", "-c", "command -v -- " .. command}
|
||||
})
|
||||
|
||||
if process.status == 0 then
|
||||
local command_path = process.stdout:gsub("\n", "")
|
||||
msg.debug("command found:", command_path)
|
||||
return {command_path, ...}
|
||||
else
|
||||
msg.debug("command not found:", command)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- returns md5 hash of the full path of the current media file
|
||||
local function hash(path)
|
||||
if path == nil then
|
||||
msg.debug("something is wrong with the path, can't get full_path, can't hash it")
|
||||
return
|
||||
end
|
||||
|
||||
msg.debug("hashing:", path)
|
||||
|
||||
local cmd = {
|
||||
name = 'subprocess',
|
||||
capture_stdout = true,
|
||||
playback_only = false,
|
||||
}
|
||||
|
||||
local args = nil
|
||||
local is_unix = package.config:sub(1,1) == "/"
|
||||
if is_unix then
|
||||
local md5 = command_exists("md5sum") or command_exists("md5") or command_exists("openssl", "md5 | cut -d ' ' -f 2")
|
||||
if md5 == nil then
|
||||
msg.warn("no md5 command found, can't generate hash")
|
||||
return
|
||||
end
|
||||
md5 = table.concat(md5, " ")
|
||||
cmd["stdin_data"] = path
|
||||
args = {"sh", "-c", md5 .. " | cut -d ' ' -f 1 | tr '[:lower:]' '[:upper:]'" }
|
||||
else --windows
|
||||
-- https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.3
|
||||
local hash_command = [[
|
||||
$s = [System.IO.MemoryStream]::new();
|
||||
$w = [System.IO.StreamWriter]::new($s);
|
||||
$w.write(']] .. path .. [[');
|
||||
$w.Flush();
|
||||
$s.Position = 0;
|
||||
Get-FileHash -Algorithm MD5 -InputStream $s | Select-Object -ExpandProperty Hash
|
||||
]]
|
||||
|
||||
args = {"powershell", "-NoProfile", "-Command", hash_command}
|
||||
end
|
||||
cmd["args"] = args
|
||||
msg.debug("hash cmd:", utils.to_string(cmd))
|
||||
local process = mp.command_native(cmd)
|
||||
|
||||
if process.status == 0 then
|
||||
local hash = process.stdout:gsub("%s+", "")
|
||||
msg.debug("hash:", hash)
|
||||
return hash
|
||||
else
|
||||
msg.warn("hash function failed")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local function get_bookmark_path(dir)
|
||||
local fpath = string.sub(dir, 1, -2)
|
||||
local _, name = utils.split_path(fpath)
|
||||
local history_name = nil
|
||||
if o.hash then
|
||||
history_name = hash(dir)
|
||||
if history_name == nil then
|
||||
msg.warn("hash function failed, fallback to dirname")
|
||||
history_name = name
|
||||
end
|
||||
else
|
||||
history_name = name
|
||||
end
|
||||
local bookmark_name = history_name .. o.bookmark_ext
|
||||
bookmark_path = utils.join_path(history_dir, bookmark_name)
|
||||
if is_windows then bookmark_path = bookmark_path:gsub("\\", "/") end
|
||||
end
|
||||
|
||||
local function file_exist(path)
|
||||
local meta = utils.file_info(path)
|
||||
if not meta or not meta.is_file then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- get the content of the bookmark
|
||||
-- Arg: bookmark_file (path)
|
||||
-- Return: nil / content of the bookmark
|
||||
local function get_record(bookmark_path)
|
||||
local file = io.open(bookmark_path, 'r')
|
||||
local record = file:read()
|
||||
if record == nil then
|
||||
msg.verbose('No history record is found in the bookmark file.')
|
||||
return nil
|
||||
end
|
||||
msg.verbose('last play: ' .. record)
|
||||
file:close()
|
||||
return record
|
||||
end
|
||||
|
||||
----- winapi start -----
|
||||
-- in windows system, we can use the sorting function provided by the win32 API
|
||||
-- see https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-strcmplogicalw
|
||||
-- this function was taken from https://github.com/mpvnet-player/mpv.net/issues/575#issuecomment-1817413401
|
||||
local winapi = {}
|
||||
local is_windows = mp.get_property_native("platform") == "windows"
|
||||
|
||||
if is_windows then
|
||||
-- is_ffi_loaded is false usually means the mpv builds without luajit
|
||||
local is_ffi_loaded, ffi = pcall(require, "ffi")
|
||||
|
||||
if is_ffi_loaded then
|
||||
winapi = {
|
||||
ffi = ffi,
|
||||
C = ffi.C,
|
||||
CP_UTF8 = 65001,
|
||||
shlwapi = ffi.load("shlwapi"),
|
||||
}
|
||||
|
||||
-- ffi code from https://github.com/po5/thumbfast, Mozilla Public License Version 2.0
|
||||
ffi.cdef[[
|
||||
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr,
|
||||
int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||
int __stdcall StrCmpLogicalW(wchar_t *psz1, wchar_t *psz2);
|
||||
]]
|
||||
|
||||
winapi.utf8_to_wide = function(utf8_str)
|
||||
if utf8_str then
|
||||
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, nil, 0)
|
||||
|
||||
if utf16_len > 0 then
|
||||
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||
|
||||
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, utf16_str, utf16_len) > 0 then
|
||||
return utf16_str
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
end
|
||||
end
|
||||
----- winapi end -----
|
||||
|
||||
local function alphanumsort_windows(filenames)
|
||||
table.sort(filenames, function(a, b)
|
||||
local a_wide = winapi.utf8_to_wide(a)
|
||||
local b_wide = winapi.utf8_to_wide(b)
|
||||
return winapi.shlwapi.StrCmpLogicalW(a_wide, b_wide) == -1
|
||||
end)
|
||||
|
||||
return filenames
|
||||
end
|
||||
|
||||
-- alphanum sorting for humans in Lua
|
||||
-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
|
||||
local function alphanumsort_lua(filenames)
|
||||
local function padnum(n, d)
|
||||
return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
|
||||
or ("%03d%s"):format(#n, n)
|
||||
end
|
||||
|
||||
local tuples = {}
|
||||
for i, f in ipairs(filenames) do
|
||||
tuples[i] = {f:lower():gsub("0*(%d+)%.?(%d*)", padnum), f}
|
||||
end
|
||||
table.sort(tuples, function(a, b)
|
||||
return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
|
||||
end)
|
||||
for i, tuple in ipairs(tuples) do filenames[i] = tuple[2] end
|
||||
return filenames
|
||||
end
|
||||
|
||||
local function alphanumsort(filenames)
|
||||
local is_ffi_loaded = pcall(require, "ffi")
|
||||
if is_windows and is_ffi_loaded then
|
||||
alphanumsort_windows(filenames)
|
||||
else
|
||||
alphanumsort_lua(filenames)
|
||||
end
|
||||
end
|
||||
|
||||
local function create_playlist(dir)
|
||||
local pl_list = {}
|
||||
local file_list = utils.readdir(dir, 'files')
|
||||
for i = 1, #file_list do
|
||||
local file = file_list[i]
|
||||
local ext = file:match('%.([^./]+)$')
|
||||
if ext and exclude(ext:lower()) then
|
||||
table.insert(pl_list, file)
|
||||
msg.verbose("Adding " .. file)
|
||||
end
|
||||
end
|
||||
alphanumsort(pl_list)
|
||||
return pl_list
|
||||
end
|
||||
|
||||
local function get_playlist()
|
||||
local pl_list = {}
|
||||
local playlist = mp.get_property_native("playlist")
|
||||
for i = 0, #playlist - 1 do
|
||||
local filename = mp.get_property("playlist/" .. i .. "/filename")
|
||||
local _, file = utils.split_path(filename)
|
||||
table.insert(pl_list, file)
|
||||
end
|
||||
return pl_list
|
||||
end
|
||||
|
||||
-- get the index of the wanted file playlist
|
||||
-- if there is no playlist, return nil
|
||||
local function get_playlist_idx(dst_file)
|
||||
if dst_file == nil or dst_file == " " then
|
||||
return nil
|
||||
end
|
||||
|
||||
local idx = nil
|
||||
for i = 1, #pl_list do
|
||||
if (dst_file == pl_list[i]) then
|
||||
idx = i
|
||||
return idx
|
||||
end
|
||||
end
|
||||
return idx
|
||||
end
|
||||
|
||||
local function jump_resume()
|
||||
mp.unregister_event(jump_resume)
|
||||
show_message(texts.msg1, 2)
|
||||
end
|
||||
|
||||
local function unbind_key()
|
||||
msg.verbose('Unbinding keys')
|
||||
wait_jump_timer:kill()
|
||||
mp.remove_key_binding('key_jump')
|
||||
mp.remove_key_binding('key_cancel')
|
||||
end
|
||||
|
||||
local function key_jump()
|
||||
on_key = true
|
||||
wait_jump_timer:kill()
|
||||
unbind_key()
|
||||
current_idx = pl_idx
|
||||
mp.register_event('file-loaded', jump_resume)
|
||||
msg.verbose('Jumping to ' .. pl_path)
|
||||
mp.commandv('loadfile', pl_path)
|
||||
end
|
||||
|
||||
local function key_cancel()
|
||||
on_key = true
|
||||
wait_jump_timer:kill()
|
||||
unbind_key()
|
||||
end
|
||||
|
||||
local function bind_key()
|
||||
mp.add_forced_key_binding('1', 'key_jump', key_jump)
|
||||
mp.add_forced_key_binding('0', 'key_cancel', key_cancel)
|
||||
end
|
||||
|
||||
-- creat a .history file
|
||||
local function record_history()
|
||||
if not o.enabled or not file_loaded then return end
|
||||
refresh_globals()
|
||||
if not path or is_protocol(path) then return end
|
||||
get_bookmark_path(dir)
|
||||
local eof = mp.get_property_bool("eof-reached")
|
||||
local percent_pos = mp.get_property_number("percent-pos", 0)
|
||||
if not eof and percent_pos < 90 then
|
||||
if fname ~= nil then
|
||||
local file = io.open(bookmark_path, "w")
|
||||
file:write(fname .. "\n")
|
||||
file:close()
|
||||
end
|
||||
else
|
||||
local file = io.open(bookmark_path, "w")
|
||||
file:write(" " .. "\n")
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
local timeout = o.timeout
|
||||
local function wait_jumping()
|
||||
timeout = timeout - 1
|
||||
if timeout > 0 then
|
||||
if not on_key then
|
||||
local msg = string.format("%s -- %s? (%s) %02d", wait_msg, texts.msg2, texts.msg3, timeout)
|
||||
show_message(msg, 1)
|
||||
bind_key()
|
||||
else
|
||||
timeout = 0
|
||||
wait_jump_timer:kill()
|
||||
unbind_key()
|
||||
end
|
||||
else
|
||||
wait_jump_timer:kill()
|
||||
unbind_key()
|
||||
end
|
||||
end
|
||||
|
||||
-- record the file name when video is paused
|
||||
-- and stop the timer
|
||||
local function pause(_, paused)
|
||||
if paused then
|
||||
timer4saving_history:stop()
|
||||
record_history()
|
||||
else
|
||||
timer4saving_history:resume()
|
||||
end
|
||||
end
|
||||
|
||||
-- main function of the file
|
||||
local function record()
|
||||
if not o.enabled then return end
|
||||
refresh_globals()
|
||||
if pl_count and pl_count < 1 then return end
|
||||
if not path or is_protocol(path) or not file_exist(path) then return end
|
||||
if not dir or not fname then return end
|
||||
get_bookmark_path(dir)
|
||||
included_dir_count = tablelength(o.included_dir)
|
||||
if included_dir_count > 0 then
|
||||
if not need_ignore(o.included_dir, dir) then return end
|
||||
end
|
||||
if need_ignore(o.excluded_dir, dir) then return end
|
||||
|
||||
msg.verbose('folder -- ' .. dir)
|
||||
msg.verbose('playing -- ' .. fname)
|
||||
msg.verbose('bookmark path -- ' .. bookmark_path)
|
||||
|
||||
if (not file_exist(bookmark_path)) then
|
||||
pl_name = nil
|
||||
return
|
||||
else
|
||||
pl_name = get_record(bookmark_path)
|
||||
if pl_name then
|
||||
pl_path = utils.join_path(dir, pl_name)
|
||||
else
|
||||
pl_name = fname
|
||||
pl_path = path
|
||||
end
|
||||
end
|
||||
|
||||
if o.use_playlist or pl_count > 1 then
|
||||
pl_list = get_playlist()
|
||||
else
|
||||
pl_list = create_playlist(dir)
|
||||
end
|
||||
|
||||
pl_idx = get_playlist_idx(pl_name)
|
||||
if (pl_idx == nil) then
|
||||
msg.verbose('Playlist not found. Creating a new one...')
|
||||
else
|
||||
msg.verbose('playlist index --' .. pl_idx)
|
||||
end
|
||||
|
||||
current_idx = get_playlist_idx(fname)
|
||||
if current_idx then msg.verbose('current index -- ' .. current_idx) end
|
||||
|
||||
if current_idx and (pl_idx == nil) then
|
||||
pl_idx = current_idx
|
||||
pl_name = fname
|
||||
pl_path = path
|
||||
elseif current_idx and (pl_idx ~= current_idx) then
|
||||
wait_msg = pl_idx
|
||||
msg.verbose('Last watched episode -- ' .. wait_msg)
|
||||
wait_jump_timer = mp.add_periodic_timer(1, wait_jumping)
|
||||
end
|
||||
timer4saving_history = mp.add_periodic_timer(o.save_period, record_history)
|
||||
mp.observe_property("pause", "bool", pause)
|
||||
end
|
||||
|
||||
mp.register_event('file-loaded', function()
|
||||
file_loaded = true
|
||||
local path = mp.get_property("path")
|
||||
if not is_protocol(path) then
|
||||
path = normalize(path)
|
||||
directory = utils.split_path(path)
|
||||
else
|
||||
directory = nil
|
||||
end
|
||||
if directory ~= nil and directory ~= dir then
|
||||
mp.add_timeout(0.5, record)
|
||||
end
|
||||
end)
|
||||
|
||||
mp.add_hook("on_unload", 50, function()
|
||||
mp.unobserve_property(pause)
|
||||
record_history()
|
||||
file_loaded = false
|
||||
end)
|
||||
Reference in New Issue
Block a user