mirror of
https://github.com/gaitas13/dotfiles.git
synced 2025-12-06 07:15:37 +01:00
421 lines
13 KiB
Lua
421 lines
13 KiB
Lua
--[[
|
||
mpv-dvd-browser
|
||
|
||
This script uses the `lsdvd` commandline utility to allow users to view and select titles
|
||
for DVDs from directly within mpv. The browser is interractive and allows for both playing
|
||
the selected title, or appending it to the playlist.
|
||
|
||
For full documentation see: https://github.com/CogentRedTester/mpv-dvd-browser
|
||
]]--
|
||
|
||
local mp = require 'mp'
|
||
local msg = require 'mp.msg'
|
||
local opt = require 'mp.options'
|
||
local utils = require 'mp.utils'
|
||
|
||
local o = {
|
||
lsdvd = 'lsdvd',
|
||
|
||
--path to the dvd device to send to lsdvd, leaving this blank will set the script to
|
||
--use the --dvd-device option.
|
||
--It is recommended that this be left blank unless using wsl
|
||
dvd_device = "",
|
||
|
||
--number of titles to display on the screen at once
|
||
num_entries = 20,
|
||
|
||
--by default the player enters an infinite loop, usually of the DVD menu screen, after moving
|
||
--past the last second of the file. If this option is enabled, then the script will
|
||
--automatically configure mpv to end playback before entering the loop
|
||
escape_loop = true,
|
||
|
||
--changes default mpv behaviour and loads the first title instead of the longest when
|
||
--the title isn't specified
|
||
start_from_first_title = true,
|
||
|
||
---------------------
|
||
--playlist options:
|
||
---------------------
|
||
|
||
--adds the previous and subsequent titles to the playlist when playing a dvd
|
||
--only does this when there is only one item in the playlist
|
||
create_playlist = true,
|
||
|
||
--when dvd:// (no specified title) is loaded the script will always insert all of the
|
||
--titles into the playlist, regardless of the playlist length
|
||
--similar to loading a directory or playlist file
|
||
treat_root_as_playlist = true,
|
||
|
||
------------------------------------------
|
||
--wsl options for limitted windows support
|
||
------------------------------------------
|
||
wsl = false,
|
||
wsl_password = "",
|
||
|
||
--------------
|
||
--ass options
|
||
---------------
|
||
ass_header = "{\\q2\\fs35\\c&00ccff&}",
|
||
ass_body = "{\\q2\\fs25\\c&Hffffff&}",
|
||
ass_selected = "{\\c&Hfce788&}",
|
||
ass_playing = "{\\c&H33ff66&}",
|
||
ass_footerheader = "{\\c&00ccff&\\fs16}",
|
||
ass_cursor = "{\\c&00ccff&}",
|
||
ass_length = "{\\fs20\\c&aaaaaa&}"
|
||
}
|
||
|
||
opt.read_options(o, 'dvd_browser')
|
||
|
||
--[[
|
||
lsdvd returns a JSON object with a number of details about the dvd
|
||
some notable values are:
|
||
title = title of the dvd
|
||
longest_track = longest track on the dvd
|
||
device = path to the dvd mount point
|
||
track = array of 'titles'/tracks on the disc
|
||
length = length of each title
|
||
ix = numerical id of the title starting from 1
|
||
chapter = array of chapters in the title
|
||
num_chapters = length of chapters array (added by me)
|
||
]]--
|
||
|
||
--if the script name includes file_browser, then the script is being loaded
|
||
--as an addon, and hence we can skip loading several things
|
||
local STANDALONE = not mp.get_script_name():find("file_browser")
|
||
|
||
local list = {}
|
||
if STANDALONE then
|
||
package.path = mp.command_native( {"expand-path", "~~/script-modules/?.lua;" } ) .. package.path
|
||
list = require "scroll-list"
|
||
|
||
list.header_style = o.ass_header
|
||
list.list_style = o.ass_body
|
||
list.wrapper_style = o.ass_footerheader
|
||
list.empty_text = "insert DVD"
|
||
end
|
||
|
||
local dvd = {}
|
||
local state = {
|
||
playing_disc = false,
|
||
selected = 1,
|
||
flag_update = false
|
||
}
|
||
|
||
--automatically match to the current dvd device
|
||
if (o.dvd_device == "") then
|
||
mp.observe_property('dvd-device', 'string', function(_, device)
|
||
if device == "" then device = "/dev/dvd" end
|
||
o.dvd_device = device
|
||
|
||
--we set this to false to force a dvd rescan
|
||
state.playing_disc = false
|
||
end)
|
||
end
|
||
|
||
local function get_header_str()
|
||
local title
|
||
if dvd == nil then title = ""
|
||
else title = dvd.title end
|
||
return '📀 dvd://'..title
|
||
end
|
||
|
||
local function get_line_str(v)
|
||
return "Title "..(v.ix-1)..o.ass_length.." ["..v.length.."] "..v.num_chapters.." chapters"
|
||
end
|
||
|
||
function list:format_header()
|
||
self.ass.data = o.ass_header..get_header_str().."\\N ---------------------------------------------------- \\N"
|
||
end
|
||
|
||
--simple function to append to the ass string
|
||
function list:format_line(i, v)
|
||
self:append(o.ass_body)
|
||
|
||
--the below text contains unicode whitespace characters
|
||
if i == list.selected then self:append(o.ass_cursor..[[➤\h]]..o.ass_selected)
|
||
else self:append([[\h\h\h\h]]) end
|
||
|
||
--prints the currently-playing icon and style
|
||
if mp.get_property('filename', "0") == tostring(i-1) then
|
||
self:append(o.ass_playing)
|
||
end
|
||
|
||
self:append(get_line_str(v))
|
||
self:newline()
|
||
end
|
||
|
||
--sends a call to lsdvd to read the contents of the disc
|
||
local function read_disc()
|
||
msg.verbose('reading contents of ' .. o.dvd_device)
|
||
|
||
local args
|
||
if o.wsl then
|
||
msg.verbose('wsl compatibility mode enabled')
|
||
|
||
--if wsl password is not set then we'll assume the user has mounted manually
|
||
if o.wsl_password ~= "" then
|
||
local dvd_device = mp.get_property('dvd-device', ''):gsub([[\]], [[/]])
|
||
msg.verbose('mounting '..dvd_device..' at '..o.dvd_device)
|
||
|
||
mp.command_native({
|
||
name = 'subprocess',
|
||
playback_only = false,
|
||
args = {'wsl', 'echo', o.wsl_password, '|', 'sudo', '-S', 'mount', '-t', 'drvfs', dvd_device, o.dvd_device}
|
||
})
|
||
end
|
||
|
||
--setting wsl arguments
|
||
args = {'wsl', o.lsdvd, o.dvd_device, '-Oy', '-c'}
|
||
else
|
||
args = {o.lsdvd, o.dvd_device, '-Oy', '-c'}
|
||
end
|
||
|
||
local cmd = mp.command_native({
|
||
name = 'subprocess',
|
||
playback_only = false,
|
||
capture_stdout = true,
|
||
capture_stderr = true,
|
||
args = args
|
||
})
|
||
|
||
--making the python string JSON compatible
|
||
local result = cmd.stdout:gsub("'", '"')
|
||
result = result:gsub('lsdvd = ', '')
|
||
dvd = utils.parse_json(result)
|
||
|
||
if (not dvd) then
|
||
msg.error(cmd.stderr)
|
||
state.playing_disc = false
|
||
return
|
||
end
|
||
msg.trace(utils.to_string(dvd))
|
||
|
||
--creating a fallback for the title
|
||
-- if dvd.title == "unknown" then dvd.title = "dvd://" end
|
||
|
||
--making modifications to all the entries
|
||
for i = 1, #dvd.track do
|
||
local v = dvd.track[i]
|
||
|
||
--saving the chapter count
|
||
v.num_chapters = #v.chapter
|
||
|
||
--modifying the length
|
||
local l = v.length
|
||
local lstr = tostring(l)
|
||
|
||
--adding the microseconds as is
|
||
local index = tostring(l):find([[.[^.]*$]])
|
||
local str
|
||
if index == 1 then str = "00"
|
||
else
|
||
str = tostring(lstr:sub(index+1))
|
||
str = string.format('%02d', str)
|
||
end
|
||
l = math.floor(l)
|
||
|
||
local seconds = l%60
|
||
str = string.format('%02d', seconds) .. '.' .. str
|
||
l = (l - seconds)/60
|
||
|
||
local mins = l%60
|
||
str = string.format('%02d', mins) .. ':' .. str
|
||
l = (l-mins)/60
|
||
|
||
local hours = l%24
|
||
str = string.format('%02d', hours) .. ':' .. str
|
||
|
||
msg.debug('changing length string for title '..(i-1)..' to '..str)
|
||
v.length = str
|
||
end
|
||
|
||
state.playing_disc = true
|
||
list.list = dvd.track
|
||
end
|
||
|
||
--this function updates dvd information and updates the browser
|
||
local function update()
|
||
read_disc()
|
||
list:update()
|
||
end
|
||
|
||
--appends the specified playlist item along with the desired options
|
||
local function load_dvd_title(title, flag)
|
||
local i = title.ix-1
|
||
mp.commandv("loadfile", "dvd://"..i, flag)
|
||
end
|
||
|
||
--handles actions when dvd:// paths are played directly
|
||
--updates dvd information and inserts disc titles into the playlist
|
||
local function load_disc()
|
||
local path = mp.get_property('stream-open-filename', '')
|
||
|
||
if path:find('dvd://') ~= 1 then
|
||
state.playing_disc = false
|
||
return
|
||
end
|
||
msg.verbose('playing dvd')
|
||
|
||
--if we have not stopped playing a disc then there's no need to parse the disc again
|
||
if not state.playing_disc then read_disc() end
|
||
|
||
--if we still can't detect a disc then return
|
||
if (not state.playing_disc) then return end
|
||
|
||
--if we successfully loaded info about the disc it's time to do some other stuff:
|
||
--this code block finds the default title of the disc
|
||
local curr_title
|
||
|
||
--if the user specified a title number we use that
|
||
if path ~= "dvd://" then
|
||
--treating whatever comes after "dvd://" as the title number
|
||
curr_title = tonumber(path:sub(7))
|
||
|
||
--if dvd:// was sent and this option is set we set the default ourselves
|
||
elseif o.start_from_first_title then
|
||
mp.set_property('stream-open-filename', "dvd://0")
|
||
curr_title = 0
|
||
|
||
--otherwise if just dvd:// was sent we need to find the longest title
|
||
else
|
||
curr_title = dvd.longest_track
|
||
end
|
||
|
||
mp.set_property('file-local-options/title', dvd.title.." - Title "..curr_title)
|
||
|
||
--if o.create_playlist is false then the function can end here
|
||
if not o.create_playlist then return end
|
||
|
||
--offsetting curr_title by one to account for lua arrays being 1-based
|
||
curr_title = curr_title+1
|
||
local length = mp.get_property_number('playlist-count', 1)
|
||
|
||
--load files in the playlist under the specified conditions
|
||
if (path == "dvd://" and o.treat_root_as_playlist) or length == 1 then
|
||
local pos = mp.get_property_number('playlist-pos', 1)
|
||
|
||
--add all of the files to the playlist
|
||
for i = 1, #dvd.track do
|
||
if i ~= curr_title then
|
||
load_dvd_title(dvd.track[i], "append")
|
||
length = length + 1
|
||
|
||
--we need slightly different behaviour when prepending vs appending a playlist entry
|
||
if (i < curr_title) then
|
||
mp.commandv("playlist-move", length-1, pos)
|
||
pos = pos+1
|
||
elseif (i > curr_title) then
|
||
mp.commandv("playlist-move", length-1, pos+(i-curr_title))
|
||
end
|
||
end
|
||
end
|
||
|
||
--if the path is dvd, then we actually need to fully replace this entry in the playlist,
|
||
--otherwise the whole disc will be added to the playlist again if moving back to this entry
|
||
if (path == "dvd://") then
|
||
msg.verbose('replacing dvd:// with playlist')
|
||
|
||
load_dvd_title(dvd.track[curr_title], "append")
|
||
length = length+1
|
||
mp.commandv('playlist-move', length-1, pos+1)
|
||
mp.commandv('playlist-remove', 'current')
|
||
end
|
||
end
|
||
end
|
||
|
||
--opens the currently selected file
|
||
local function open_file(flag)
|
||
load_dvd_title(dvd.track[list.selected], flag)
|
||
|
||
if flag == 'replace' then
|
||
list:close()
|
||
end
|
||
end
|
||
|
||
--opens the browser and declares dynamic keybinds
|
||
function list:open()
|
||
self.hidden = false
|
||
if not state.playing_disc then
|
||
update()
|
||
else
|
||
self:open_list()
|
||
end
|
||
self:add_keybinds()
|
||
end
|
||
|
||
list.keybinds = {
|
||
{"ESC", "exit", function() list:close() end, {}},
|
||
{"ENTER", "open", function() open_file('replace') end, {}},
|
||
{"Shift+ENTER", "append_playlist", function() open_file('append') end, {}},
|
||
{'DOWN', 'scroll_down', function() list:scroll_down() end, {repeatable = true}},
|
||
{'UP', 'scroll_up', function() list:scroll_up() end, {repeatable = true}},
|
||
{'Ctrl+r', 'reload', function() read_disc() ; list:update() end, {}}
|
||
}
|
||
|
||
--modifies track length to escape infinite loop
|
||
if o.escape_loop then
|
||
mp.add_hook('on_preloaded', 50, function()
|
||
if mp.get_property("path", ""):find("dvd://") ~= 1 then return end
|
||
if mp.get_property('end', 'none') ~= 'none' then return end
|
||
|
||
local chapters = mp.get_property_native('chapter-list')
|
||
if not chapters then return end
|
||
|
||
local num_chapters = #chapters
|
||
|
||
-- occurs if there are no chapters
|
||
if not chapters[num_chapters] then return end
|
||
if (mp.get_property_number('duration', 0) - (chapters[num_chapters].time or 0)) > 1 then return end
|
||
|
||
msg.verbose('modifying end of the title to escape infinite loop')
|
||
mp.set_property('file-local-options/end', "#"..num_chapters)
|
||
end)
|
||
end
|
||
|
||
--if we're playing a disc then read it and modify playlist appropriately
|
||
mp.add_hook('on_load', 50, load_disc)
|
||
|
||
--if these events are disabled then the list gui functions are never called
|
||
if STANDALONE then
|
||
mp.observe_property('path', 'string', function(_,path)
|
||
if state.playing_disc then list:update() end
|
||
end)
|
||
|
||
mp.register_script_message('browse-dvd', function() list:open() end)
|
||
|
||
mp.add_key_binding('MENU', 'dvd-browser', function() list:toggle() end)
|
||
end
|
||
|
||
--module functions when loading as file_browser addon
|
||
local dvd_module = {
|
||
priority = 50
|
||
}
|
||
|
||
function dvd_module:can_parse(directory)
|
||
return directory:sub(1,6) == "dvd://" or directory == self.get_dvd_device()
|
||
end
|
||
|
||
function dvd_module:parse()
|
||
read_disc()
|
||
local list = {}
|
||
|
||
if dvd then
|
||
for i = 1, #dvd.track do
|
||
list[i] = {
|
||
ass = get_line_str(dvd.track[i]),
|
||
name = tostring(dvd.track[i].ix-1),
|
||
path = "dvd://"..(dvd.track[i].ix-1),
|
||
type = "file"
|
||
}
|
||
end
|
||
end
|
||
|
||
return list, {
|
||
empty_text = "insert DVD",
|
||
directory_label = get_header_str(),
|
||
filtered = true,
|
||
sorted = true
|
||
}
|
||
end
|
||
|
||
return dvd_module
|