summaryrefslogtreecommitdiff
path: root/.config/mpv
diff options
context:
space:
mode:
authorVito Graffagnino <vito@graffagnino.xyz>2020-09-08 18:10:49 +0100
committerVito Graffagnino <vito@graffagnino.xyz>2020-09-08 18:10:49 +0100
commit3b0142cedcde39e4c2097ecd916a870a3ced5ec6 (patch)
tree2116c49a845dfc0945778f2aa3e2118d72be428b /.config/mpv
parent8cc927e930d5b6aafe3e9862a61e81705479a1b4 (diff)
Added the relevent parts of the .config directory. Alss add ssh config
Diffstat (limited to '.config/mpv')
-rwxr-xr-x.config/mpv/input.conf11
-rwxr-xr-x.config/mpv/mpv.conf144
-rw-r--r--.config/mpv/mpvClipboard.log2
-rw-r--r--.config/mpv/scripts/SmartCopyPaste-II-2.2.lua406
-rw-r--r--.config/mpv/scripts/UndoRedo-1.5.2.lua222
-rw-r--r--.config/mpv/scripts/easycrop.lua253
-rw-r--r--.config/mpv/scripts/interactive-video.lua905
-rw-r--r--.config/mpv/scripts/mpvmenu454
-rw-r--r--.config/mpv/scripts/notify-send.lua99
-rw-r--r--.config/mpv/scripts/webtorrent-hook.lua136
-rwxr-xr-x.config/mpv/watch_later/0C1ADB3AF0B707724A2089855957CBD11
-rw-r--r--.config/mpv/watch_later/0F889B2F5365362796408B979967EE2E1
-rw-r--r--.config/mpv/watch_later/488683831FDE0EC6BCF9CBF1C7E744881
-rwxr-xr-x.config/mpv/watch_later/533BDF1A0D7E0CDF991484442A6339E41
-rw-r--r--.config/mpv/watch_later/5AAA9D654D3330AA0E70641D90273C821
-rw-r--r--.config/mpv/watch_later/6CFA806481681DD742151CEB8C869D113
-rw-r--r--.config/mpv/watch_later/729B1EA73253DFBDA2059A48708309C21
-rwxr-xr-x.config/mpv/watch_later/95E11B3A5B89BE9BB61A7CF58917C7031
-rw-r--r--.config/mpv/watch_later/A886B4B4658EC2EE3338EA97169D51C32
-rwxr-xr-x.config/mpv/watch_later/BDE1B7DD20A6156BF14F815048C8C9381
-rw-r--r--.config/mpv/watch_later/D06453D5ECC647181B9A40F626560C7C1
-rw-r--r--.config/mpv/watch_later/F1606355253CA7F0BEAC5248CD9E52533
22 files changed, 2649 insertions, 0 deletions
diff --git a/.config/mpv/input.conf b/.config/mpv/input.conf
new file mode 100755
index 0000000..dd0c6ef
--- /dev/null
+++ b/.config/mpv/input.conf
@@ -0,0 +1,11 @@
+Alt+RIGHT add video-rotate 90
+Alt+LEFT add video-rotate -90
+Alt+- add video-zoom -0.25
+Alt+= add video-zoom 0.25
+Alt+j add video-pan-x -0.05
+Alt+l add video-pan-x 0.05
+Alt+i add video-pan-y 0.05
+Alt+k add video-pan-y -0.05
+sub-file=~/tmp/subtitles/subtitles.srt
+osd-font-size=10
+geometry=0:+400
diff --git a/.config/mpv/mpv.conf b/.config/mpv/mpv.conf
new file mode 100755
index 0000000..575040e
--- /dev/null
+++ b/.config/mpv/mpv.conf
@@ -0,0 +1,144 @@
+#
+# Example mpv configuration file
+#
+# Warning:
+#
+# The commented example options usually do _not_ set the default values. Call
+# mpv with --list-options to see the default values for most options. There is
+# no builtin or example mpv.conf with all the defaults.
+#
+#
+# Configuration files are read system-wide from /usr/local/etc/mpv.conf
+# and per-user from ~/.config/mpv/mpv.conf, where per-user settings override
+# system-wide settings, all of which are overridden by the command line.
+#
+# Configuration file settings and the command line options use the same
+# underlying mechanisms. Most options can be put into the configuration file
+# by dropping the preceding '--'. See the man page for a complete list of
+# options.
+#
+# Lines starting with '#' are comments and are ignored.
+#
+# See the CONFIGURATION FILES section in the man page
+# for a detailed description of the syntax.
+#
+# Profiles should be placed at the bottom of the configuration file to ensure
+# that settings wanted as defaults are not restricted to specific profiles.
+
+##################
+# video settings #
+##################
+
+# Start in fullscreen mode by default.
+fs=no
+
+# force starting with centered window
+geometry=50%:50%
+#geometry=640+10+2000
+
+# don't allow a new window to have a size larger than 90% of the screen size
+autofit-larger=50%x50%
+
+# Do not close the window on exit.
+#keep-open=yes
+
+# Do not wait with showing the video window until it has loaded. (This will
+# resize the window once video is loaded. Also always shows a window with
+# audio.)
+#force-window=immediate
+
+# Disable the On Screen Controller (OSC).
+#osc=no
+
+# Keep the player window on top of all other windows.
+ontop=yes
+
+# Specify default video driver (see --vo=help for a list).
+# This one selects high quality video scaling etc. - can cause problems with
+# some drivers and GPUs.
+#vo=opengl-hq
+
+# Force video to lock on the display's refresh rate, and change video and audio
+# speed to some degree to ensure synchronous playback - can cause problems
+# with some drivers and desktop environments.
+#video-sync=display-resample
+
+# Enable hardware decoding if available. Often, this does not work with all
+# video outputs, but should work well with default settings on most systems.
+# If performance or energy usage is an issue, forcing the vdpau or vaapi VOs
+# may or may not help.
+#hwdec=auto
+
+##################
+# audio settings #
+##################
+
+# Specify default audio driver (see --ao=help for a list).
+#ao=alsa
+
+# Disable softvol usage, and always use the system mixer if available.
+#softvol=no
+
+# Do not filter audio to keep pitch when changing playback speed.
+audio-pitch-correction=no
+
+# Output 5.1 audio natively, and upmix/downmix audio with a different format.
+#audio-channels=5.1
+# Disable any automatic remix, _if_ the audio output accepts the audio format.
+# of the currently played file. See caveats mentioned in the manpage.
+# (This is the default.)
+#audio-channels=auto
+
+##################
+# other settings #
+##################
+
+# Pretend to be a web browser. Might fix playback with some streaming sites,
+# but also will break with shoutcast streams.
+#user-agent="Mozilla/5.0"
+
+# cache settings
+#
+# Use 150MB input cache by default. The cache is enabled for network streams only.
+#cache-default=153600
+#
+# Use 150MB input cache for everything, even local files.
+#cache=153600
+#
+# Disable the behavior that the player will pause if the cache goes below a
+# certain fill size.
+#cache-pause=no
+#
+# Read ahead about 5 seconds of audio and video packets.
+#demuxer-readahead-secs=5.0
+#
+# Raise readahead from demuxer-readahead-secs to this value if a cache is active.
+#cache-secs=50.0
+
+# Display English subtitles if available.
+#slang=en
+
+# Play Finnish audio if available, fall back to English otherwise.
+#alang=fi,en
+
+# Change subtitle encoding. For Arabic subtitles use 'cp1256'.
+# If the file seems to be valid UTF-8, prefer UTF-8.
+#sub-codepage=utf8:cp1256
+
+
+# You can also include other configuration files.
+#include=/path/to/the/file/you/want/to/include
+
+
+############
+# Profiles #
+############
+
+# The options declared as part of profiles override global default settings,
+# but only take effect when the profile is active.
+
+# The following profile can be enabled on the command line with: --profile=invert
+
+#[invert]
+# The profile forces this video filter:
+#vf-add=flip
diff --git a/.config/mpv/mpvClipboard.log b/.config/mpv/mpvClipboard.log
new file mode 100644
index 0000000..5a11944
--- /dev/null
+++ b/.config/mpv/mpvClipboard.log
@@ -0,0 +1,2 @@
+[25/May/20 15:13:57] /home/archlinux/vgg/macosxd3/Media/Linkin Park - Essentials (2020)/01. Linkin Park - In The End.mp3 |time=43
+[14/Aug/20 21:44:26] /home/archlinux/vgg/macosxd3/Media/Series/Watchmen/watchmen.s01e02.repack.web.h264-tbs[ettv].mkv |time=3
diff --git a/.config/mpv/scripts/SmartCopyPaste-II-2.2.lua b/.config/mpv/scripts/SmartCopyPaste-II-2.2.lua
new file mode 100644
index 0000000..4e2f33f
--- /dev/null
+++ b/.config/mpv/scripts/SmartCopyPaste-II-2.2.lua
@@ -0,0 +1,406 @@
+-- Copyright (c) 2020, Eisa AlAwadhi
+-- License: BSD 2-Clause License
+
+-- Creator: Eisa AlAwadhi
+-- Project: SmartCopyPaste-II
+-- Version: 2.2
+
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+local protocols
+local extensions
+local pasted = false
+
+----------------------------USER CUSTOMIZATION SETTINGS-----------------------------------
+--These settings are for users to manually change some options in the script.
+--Keybinds can be defined in the bottom of the script.
+
+local device = nil --nil is for automatic device detection, OR manually change to: 'windows' or 'mac' or 'linux'
+
+local linux_copy = 'xclip -silent -selection clipboard -in' --copy command that will be used in Linux. OR write a different command
+local linux_paste = 'xclip -selection clipboard -o' --paste command that will be used in Linux. OR write a different command
+
+local mac_copy = 'pbcopy' --copy command that will be used in MAC. OR write a different command
+local mac_paste = 'pbpaste' --paste command that will be used in MAC. OR write a different command
+
+local windows_copy = 'powershell' --'powershell' is for using windows powershell to copy. OR write the copy command, e.g: ' clip'
+local windows_paste = 'powershell' --'powershell' is for using windows powershell to paste. OR write the paste command
+
+local paste_anything = false --false is for specific paste based on the specified extensions and protocols. OR change to true so paste accepts anything (not recommended to change this).
+
+if not paste_anything then
+ protocols = { --add below (after a comma) any protocol you want SmartCopyPaste to work with; e.g: ,'ftp://'
+ 'https?://' ,'magnet:'
+ }
+ extensions = { --add below (after a comma) any extension you want SmartCopyPaste to work with; e.g: ,'pdf'
+ --video & audio
+ 'ac3', 'a52', 'eac3', 'mlp', 'dts', 'dts-hd', 'dtshd', 'true-hd', 'thd', 'truehd', 'thd+ac3', 'tta', 'pcm', 'wav', 'aiff', 'aif', 'aifc', 'amr', 'awb', 'au', 'snd', 'lpcm', 'yuv', 'y4m', 'ape', 'wv', 'shn', 'm2ts', 'm2t', 'mts', 'mtv', 'ts', 'tsv', 'tsa', 'tts', 'trp', 'adts', 'adt', 'mpa', 'm1a', 'm2a', 'mp1', 'mp2', 'mp3', 'mpeg', 'mpg', 'mpe', 'mpeg2', 'm1v', 'm2v', 'mp2v', 'mpv', 'mpv2', 'mod', 'tod', 'vob', 'vro', 'evob', 'evo', 'mpeg4', 'm4v', 'mp4', 'mp4v', 'mpg4', 'm4a', 'aac', 'h264', 'avc', 'x264', '264', 'hevc', 'h265', 'x265', '265', 'flac', 'oga', 'ogg', 'opus', 'spx', 'ogv', 'ogm', 'ogx', 'mkv', 'mk3d', 'mka', 'webm', 'weba', 'avi', 'vfw', 'divx', '3iv', 'xvid', 'nut', 'flic', 'fli', 'flc', 'nsv', 'gxf', 'mxf', 'wma', 'wm', 'wmv', 'asf', 'dvr-ms', 'dvr', 'wtv', 'dv', 'hdv', 'flv','f4v', 'f4a', 'qt', 'mov', 'hdmov', 'rm', 'rmvb', 'ra', 'ram', '3ga', '3ga2', '3gpp', '3gp', '3gp2', '3g2', 'ay', 'gbs', 'gym', 'hes', 'kss', 'nsf', 'nsfe', 'sap', 'spc', 'vgm', 'vgz', 'm3u', 'm3u8', 'pls', 'cue',
+ --images
+ "ase", "art", "bmp", "blp", "cd5", "cit", "cpt", "cr2", "cut", "dds", "dib", "djvu", "egt", "exif", "gif", "gpl", "grf", "icns", "ico", "iff", "jng", "jpeg", "jpg", "jfif", "jp2", "jps", "lbm", "max", "miff", "mng", "msp", "nitf", "ota", "pbm", "pc1", "pc2", "pc3", "pcf", "pcx", "pdn", "pgm", "PI1", "PI2", "PI3", "pict", "pct", "pnm", "pns", "ppm", "psb", "psd", "pdd", "psp", "px", "pxm", "pxr", "qfx", "raw", "rle", "sct", "sgi", "rgb", "int", "bw", "tga", "tiff", "tif", "vtf", "xbm", "xcf", "xpm", "3dv", "amf", "ai", "awg", "cgm", "cdr", "cmx", "dxf", "e2d", "egt", "eps", "fs", "gbr", "odg", "svg", "stl", "vrml", "x3d", "sxd", "v2d", "vnd", "wmf", "emf", "art", "xar", "png", "webp", "jxr", "hdp", "wdp", "cur", "ecw", "iff", "lbm", "liff", "nrrd", "pam", "pcx", "pgf", "sgi", "rgb", "rgba", "bw", "int", "inta", "sid", "ras", "sun", "tga",
+ --other types
+ 'torrent'
+ }
+---------------------------END OF USER CUSTOMIZATION SETTINGS------------------------
+else
+ protocols = {''}
+ extensions = {''}
+end
+
+if not device then
+ if os.getenv('windir') ~= nil then
+ device = 'windows'
+ elseif os.execute '[ -d "/Applications" ]' == 0 and os.execute '[ -d "/Library" ]' == 0 or os.execute '[ -d "/Applications" ]' == true and os.execute '[ -d "/Library" ]' == true then
+ device = 'mac'
+ else
+ device = 'linux'
+ end
+end
+
+function handleres(res, args)
+ if not res.error and res.status == 0 then
+ return res.stdout
+ else
+ msg.error("There was an error getting "..device.." clipboard: ")
+ msg.error(" Status: "..(res.status or ""))
+ msg.error(" Error: "..(res.error or ""))
+ msg.error(" stdout: "..(res.stdout or ""))
+ msg.error("args: "..utils.to_string(args))
+ return ''
+ end
+end
+
+function os.capture(cmd, raw)
+ local f = assert(io.popen(cmd, 'r'))
+ local s = assert(f:read('*a'))
+ f:close()
+ if raw then return s end
+ s = string.gsub(s, '^%s+', '')
+ s = string.gsub(s, '%s+$', '')
+ s = string.gsub(s, '[\n\r]+', ' ')
+ return s
+end
+
+local function get_extension(path)
+ match = string.match(path, '%.([^%.]+)$' )
+ if match == nil then
+ return 'nomatch'
+ else
+ return match
+ end
+end
+
+local function get_extentionpath(path)
+ match = string.match(path,'(.*)%.([^%.]+)$')
+ if match == nil then
+ return 'nomatch'
+ else
+ return match
+ end
+end
+
+local function has_extension (tab, val)
+ for index, value in ipairs(tab) do
+ if value == val then
+ return true
+ end
+ end
+
+ return false
+end
+
+local function starts_protocol (tab, val)
+ for index, value in ipairs(tab) do
+ if (val:find(value) == 1) then
+ return true
+ end
+ end
+ return false
+end
+
+
+function get_clipboard()
+local clip
+ if device == 'linux' then
+ clip = os.capture(linux_paste, false)
+ return clip
+ elseif device == 'windows' then
+ if windows_paste == 'powershell' then
+ local args = {
+ 'powershell', '-NoProfile', '-Command', [[& {
+ Trap {
+ Write-Error -ErrorRecord $_
+ Exit 1
+ }
+ $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
+ if ($clip) {
+ $clip = $clip
+ }
+ else {
+ $clip = Get-Clipboard -Raw -Format FileDropList
+ }
+ $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
+ [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
+ }]]
+ }
+ return handleres(utils.subprocess({ args = args, cancellable = false }), args)
+ else
+ clip = os.capture(windows_paste, false)
+ return clip
+ end
+ elseif device == 'mac' then
+ clip = os.capture(mac_paste, false)
+ return clip
+ end
+ return ''
+end
+
+
+function set_clipboard(text)
+ local pipe
+ if device == 'linux' then
+ pipe = io.popen(linux_copy, 'w')
+ pipe:write(text)
+ pipe:close()
+ elseif device == 'windows' then
+ if windows_copy == 'powershell' then
+ local res = utils.subprocess({ args = {
+ 'powershell', '-NoProfile', '-Command', string.format([[& {
+ Trap {
+ Write-Error -ErrorRecord $_
+ Exit 1
+ }
+ Add-Type -AssemblyName PresentationCore
+ [System.Windows.Clipboard]::SetText('%s')
+ }]], text)
+ } })
+ else
+ pipe = io.popen(windows_copy,'w')
+ pipe:write(text)
+ pipe:close()
+ end
+ elseif device == 'mac' then
+ pipe = io.popen(mac_copy,'w')
+ pipe:write(text)
+ pipe:close()
+ end
+ return ''
+end
+
+
+local function copy()
+ local filePath = mp.get_property_native('path')
+
+ if (filePath ~= nil) then
+ local time = math.floor(mp.get_property_number('time-pos'))
+ set_clipboard(filePath..' |time='..tostring(time))
+ mp.osd_message('Copied & Bookmarked:\n'..filePath..' |time='..tostring(time))
+
+ local copyLog = (os.getenv('APPDATA') or os.getenv('HOME')..'/.config')..'/mpv/mpvClipboard.log'
+ local copyLogAdd = io.open(copyLog, 'a+')
+
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), filePath..' |time='..tostring(time)))
+ copyLogAdd:close()
+ else
+ mp.osd_message('Failed to Copy\nNo Video Found')
+ end
+end
+
+
+local function copy_path()
+ local filePath = mp.get_property_native('path')
+
+ if (filePath ~= nil) then
+ set_clipboard(filePath)
+ mp.osd_message('Copied & Bookmarked Video Only:\n'..filePath)
+
+ local copyLog = (os.getenv('APPDATA') or os.getenv('HOME')..'/.config')..'/mpv/mpvClipboard.log'
+ local copyLogAdd = io.open(copyLog, 'a+')
+
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), filePath))
+ copyLogAdd:close()
+ else
+ return false
+ end
+end
+
+
+function paste()
+ local clip = get_clipboard()
+ clip = string.gsub(clip, "[\r\n]" , "")
+
+ local filePath = mp.get_property_native('path')
+ local time
+
+ if string.match(clip, '(.*) |time=') then
+ videoFile = string.match(clip, '(.*) |time=')
+ time = string.match(clip, ' |time=(.*)')
+ elseif string.match(clip, '^\"(.*)\"$') then
+ videoFile = string.match(clip, '^\"(.*)\"$')
+ else
+ videoFile = clip
+ end
+
+ local currentVideoExtension = string.lower(get_extension(videoFile))
+ local currentVideoExtensionPath = (get_extentionpath(videoFile))
+
+ local copyLog = (os.getenv('APPDATA') or os.getenv('HOME')..'/.config')..'/mpv/mpvClipboard.log'
+ local copyLogAdd = io.open(copyLog, 'a+')
+ local copyLogOpen = io.open(copyLog, 'r+')
+
+ local linePosition
+ local videoFound = ''
+ local logVideo
+ local logVideoTime
+
+ for line in copyLogOpen:lines() do
+
+ linePosition = line:find(']')
+ line = line:sub(linePosition + 2)
+
+ if line.match(line, '(.*) |time=') == filePath then
+ videoFound = line
+ end
+ end
+
+ logVideo = string.match(videoFound, '(.*) |time=')
+ logVideoTime = string.match(videoFound, ' |time=(.*)')
+
+ if (filePath == videoFile) and (time ~= nil) then
+ mp.commandv('seek', time, 'absolute', 'exact')
+ mp.osd_message('Resumed to Copied Time')
+ elseif (filePath == logVideo) and (logVideoTime ~= nil) then
+ mp.commandv('seek', logVideoTime, 'absolute', 'exact')
+ mp.osd_message('Resumed to Last Logged Time')
+ elseif (filePath ~= nil) and (logVideoTime == nil) then
+ mp.osd_message('No Copied Time Found')
+ elseif (filePath == nil) and has_extension(extensions, currentVideoExtension) and (currentVideoExtensionPath~= '') then
+ mp.commandv('loadfile', videoFile)
+ mp.osd_message('Pasted:\n'..videoFile)
+
+ if (time ~= nil) then
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), videoFile..' |time='..tostring(time)))
+ else
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), videoFile))
+ end
+ elseif (filePath == nil) and (starts_protocol(protocols, videoFile)) then
+ mp.commandv('loadfile', videoFile)
+ mp.osd_message('Pasted:\n'..videoFile)
+
+ if (time ~= nil) then
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), videoFile..' |time='..tostring(time)))
+ else
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), videoFile))
+ end
+ elseif (filePath == nil) and not has_extension(extensions, currentVideoExtension) and not (starts_protocol(protocols, videoFile)) then
+ copyLogLastOpen = io.open(copyLog, 'r+')
+
+ for line in copyLogLastOpen:lines() do
+ lastVideoFound = line
+ end
+
+ if (lastVideoFound ~= nil) then
+ linePosition = lastVideoFound:find(']')
+ lastVideoFound = lastVideoFound:sub(linePosition + 2)
+
+ if string.match(lastVideoFound, '(.*) |time=') then
+ videoFile = string.match(lastVideoFound, '(.*) |time=')
+ else
+ videoFile = lastVideoFound
+ end
+
+ mp.commandv('loadfile', videoFile)
+ mp.osd_message('Pasted Last Logged Item:\n'..videoFile)
+ else
+ mp.osd_message('Failed to Paste\nPasted Unsupported Item:\n'..clip)
+ end
+ copyLogLastOpen:close()
+ end
+
+ pasted = true
+ copyLogAdd:close()
+ copyLogOpen:close()
+end
+
+function paste_playlist()
+ local clip = get_clipboard()
+ clip = string.gsub(clip, "[\r\n]" , "")
+
+ local filePath = mp.get_property_native('path')
+ local time
+
+ if string.match(clip, '(.*) |time=') then
+ videoFile = string.match(clip, '(.*) |time=')
+ time = string.match(clip, ' |time=(.*)')
+ elseif string.match(clip, '^\"(.*)\"$') then
+ videoFile = string.match(clip, '^\"(.*)\"$')
+ else
+ videoFile = clip
+ end
+
+ local copyLog = (os.getenv('APPDATA') or os.getenv('HOME')..'/.config')..'/mpv/mpvClipboard.log'
+ local copyLogAdd = io.open(copyLog, 'a+')
+ local copyLogOpen = io.open(copyLog, 'r+')
+
+ local currentVideoExtension = string.lower(get_extension(videoFile))
+ local currentVideoExtensionPath = (get_extentionpath(videoFile))
+
+ if has_extension(extensions, currentVideoExtension) and (currentVideoExtensionPath~= '') or (starts_protocol(protocols, videoFile)) then
+ mp.commandv('loadfile', videoFile, 'append-play')
+ mp.osd_message('Pasted Into Playlist:\n'..videoFile)
+
+ if (time ~= nil) then
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), videoFile..' |time='..tostring(time)))
+ else
+ copyLogAdd:write(('[%s] %s\n'):format(os.date('%d/%b/%y %X'), videoFile))
+ end
+ else
+ mp.osd_message('Failed to Add Into Playlist\nPasted Unsupported Item:\n'..clip)
+ end
+
+ pasted = true
+ copyLogAdd:close()
+ copyLogOpen:close()
+end
+
+mp.register_event('end-file', function()
+ pasted = false
+end)
+
+mp.register_event('file-loaded', function()
+ if (pasted == true) then
+ local clip = get_clipboard()
+ local time = string.match(clip, ' |time=(.*)')
+ local videoFile = string.match(clip, '(.*) |time=')
+ local filePath = mp.get_property_native('path')
+
+ if (filePath == videoFile) and (time ~= nil) then
+ mp.commandv('seek', time, 'absolute', 'exact')
+ end
+ else
+ return false
+ end
+end)
+
+---------------------------KEYBINDS CUSTOMIZATION SETTINGS---------------------------------
+if device == 'mac' then --MAC OS Keybinds
+ mp.add_key_binding('Meta+c', 'copy', copy)
+ mp.add_key_binding('Meta+C', 'copyCaps', copy)
+ mp.add_key_binding('Meta+v', 'paste', paste)
+ mp.add_key_binding('Meta+V', 'pasteCaps', paste)
+
+ mp.add_key_binding('Meta+alt+c', 'copy-path', copy_path)
+ mp.add_key_binding('Meta+alt+C', 'copy-pathCaps', copy_path)
+ mp.add_key_binding('Meta+alt+v', 'paste-playlist', paste_playlist)
+ mp.add_key_binding('Meta+alt+V', 'paste-playlistCaps', paste_playlist)
+else --Windows and Linux Keybinds
+ mp.add_key_binding('ctrl+c', 'copy', copy)
+ mp.add_key_binding('ctrl+C', 'copyCaps', copy)
+ mp.add_key_binding('ctrl+v', 'paste', paste)
+ mp.add_key_binding('ctrl+V', 'pasteCaps', paste)
+
+ mp.add_key_binding('ctrl+alt+c', 'copy-path', copy_path)
+ mp.add_key_binding('ctrl+alt+C', 'copy-pathCaps', copy_path)
+ mp.add_key_binding('ctrl+alt+v', 'paste-playlist', paste_playlist)
+ mp.add_key_binding('ctrl+alt+V', 'paste-playlistCaps', paste_playlist)
+end
+---------------------END OF KEYBINDS CUSTOMIZATION SETTINGS---------------------------------
diff --git a/.config/mpv/scripts/UndoRedo-1.5.2.lua b/.config/mpv/scripts/UndoRedo-1.5.2.lua
new file mode 100644
index 0000000..3916e74
--- /dev/null
+++ b/.config/mpv/scripts/UndoRedo-1.5.2.lua
@@ -0,0 +1,222 @@
+-- Copyright (c) 2020, Eisa AlAwadhi
+-- License: BSD 2-Clause License
+
+-- Creator: Eisa AlAwadhi
+-- Project: UndoRedo
+-- Version: 1.5.2
+
+local utils = require 'mp.utils'
+local seconds = 0
+local countTimer = 0
+local seekTime = 0
+
+local seekNumber = 0
+local currentIndex = 0
+local seekTable = {}
+local seeking = 0
+
+local undoRedo = 0
+
+local pause = false
+
+seekTable[0] = 0
+
+mp.register_event('file-loaded', function()
+ filePath = mp.get_property('path')
+
+ timer = mp.add_periodic_timer(0.1, function()
+ seconds = seconds + 0.1
+ end)
+
+ if (pause == true) then
+ timer:stop()
+ else
+ timer:resume()
+ end
+
+ timer2 = mp.add_periodic_timer(0.1, function()
+ countTimer = countTimer + 0.1
+
+ if (countTimer == 0.6) then
+
+ if (seeking == 0) then
+
+ if (pause == true) then
+ seconds = seconds
+ else
+ seconds = seconds - 0.7
+ end
+
+ seekTable[currentIndex] = seekTable[currentIndex] + seconds
+ seconds = 0
+
+ seekNumber = currentIndex + 1
+ currentIndex = seekNumber
+ seekTime = math.floor(mp.get_property_number('time-pos'))
+ table.insert(seekTable, seekNumber, seekTime)
+
+ undoRedo = 0
+
+ elseif (seeking == 1) then
+ seeking = 0
+ end
+
+ end
+
+ end)
+
+ timer2:stop()
+end)
+
+
+mp.register_event('seek', function()
+ timer2:resume()
+ countTimer = 0
+end)
+
+mp.register_event('pause', function()
+ timer:stop()
+ pause = true
+end)
+
+mp.register_event('unpause', function()
+ timer:resume()
+ pause = false
+end)
+
+mp.register_event('end-file', function()
+ if timer ~= nil then
+ timer:kill()
+ end
+ if timer2 ~= nil then
+ timer2:kill()
+ end
+ seekNumber = 0
+ currentIndex = 0
+ undoRedo = 0
+ seconds = 0
+ countTimer = 0
+ seekTable[0] = 0
+end)
+
+local function undo()
+ if (filePath ~= nil) and (currentIndex > 0) and (seeking == 0) then
+
+ if (pause == true) then
+ seconds = seconds
+ else
+ seconds = seconds - 0.7
+ end
+
+ seekTable[currentIndex] = seekTable[currentIndex] + seconds
+ seconds=0
+
+ currentIndex = currentIndex - 1
+
+ if (seekTable[currentIndex] < 0) then
+ seekTable[currentIndex] = 0
+ end
+
+ mp.commandv('seek', seekTable[currentIndex], 'absolute', 'exact')
+
+ seeking = 1
+ undoRedo = 1
+
+ mp.osd_message('Undo')
+ elseif (filePath ~= nil) and (countTimer > 0) and (countTimer < 0.6) then
+ mp.osd_message('Seeking Still Running')
+ elseif (filePath ~= nil) and (currentIndex == 0) then
+ mp.osd_message('No Undo Found')
+ end
+end
+
+local function redo()
+ if (filePath ~= nil) and (currentIndex < seekNumber) and (seeking == 0) then
+
+ if (pause == true) then
+ seconds = seconds
+ else
+ seconds = seconds - 0.7
+ end
+
+ seekTable[currentIndex] = seekTable[currentIndex] + seconds
+ seconds = 0
+
+ currentIndex = currentIndex + 1
+
+ if (seekTable[currentIndex] < 0) then
+ seekTable[currentIndex] = 0
+ end
+
+ mp.commandv('seek', seekTable[currentIndex], 'absolute', 'exact')
+
+ seeking = 1
+ undoRedo = 0
+
+ mp.osd_message('Redo')
+ elseif (filePath ~= nil) and (countTimer > 0) and (countTimer < 0.6) then
+ mp.osd_message('Seeking Still Running')
+ elseif (filePath ~= nil) and (currentIndex == seekNumber) then
+ mp.osd_message('No Redo Found')
+ end
+end
+
+local function undoRedo()
+ if (filePath ~= nil) and (countTimer > 0.5) and (undoRedo == 0) then
+
+ if (pause == true) then
+ seconds = seconds
+ else
+ seconds = seconds - 0.7
+ end
+
+ seekTable[currentIndex] = seekTable[currentIndex] + seconds
+ seconds = 0
+
+ currentIndex = currentIndex - 1
+
+ if (seekTable[currentIndex] < 0) then
+ seekTable[currentIndex] = 0
+ end
+
+ mp.commandv('seek', seekTable[currentIndex], 'absolute', 'exact')
+ mp.osd_message('Undo')
+ seeking = 1
+ undoRedo = 1
+ elseif (filePath ~= nil) and (countTimer > 0.5) and (undoRedo == 1) then
+
+ if (pause == true) then
+ seconds = seconds
+ else
+ seconds = seconds - 0.7
+ end
+
+ seekTable[currentIndex] = seekTable[currentIndex] + seconds
+ seconds = 0
+
+ currentIndex = currentIndex + 1
+
+ if (seekTable[currentIndex] < 0) then
+ seekTable[currentIndex] = 0
+ end
+
+ mp.commandv('seek', seekTable[currentIndex], 'absolute', 'exact')
+ mp.osd_message('Redo')
+ seeking = 1
+ undoRedo = 0
+ elseif (filePath ~= nil) and (countTimer > 0) and (countTimer < 0.6) then
+ mp.osd_message('Seeking Still Running')
+ elseif (filePath ~= nil) and (countTimer == 0) then
+ mp.osd_message('No Undo Found')
+ end
+end
+
+
+mp.add_key_binding("ctrl+z", "undo", undo)
+mp.add_key_binding("ctrl+Z", "undoCaps", undo)
+
+mp.add_key_binding("ctrl+y", "redo", redo)
+mp.add_key_binding("ctrl+Y", "redoCaps", redo)
+
+mp.add_key_binding("ctrl+alt+z", "undoRedo", undoRedo)
+mp.add_key_binding("ctrl+alt+Z", "undoRedoCaps", undoRedo)
diff --git a/.config/mpv/scripts/easycrop.lua b/.config/mpv/scripts/easycrop.lua
new file mode 100644
index 0000000..b3a84a7
--- /dev/null
+++ b/.config/mpv/scripts/easycrop.lua
@@ -0,0 +1,253 @@
+local msg = require('mp.msg')
+local assdraw = require('mp.assdraw')
+
+local script_name = "easycrop"
+
+-- Number of crop points currently chosen (0 to 2)
+local points = {}
+-- True if in cropping selection mode
+local cropping = false
+-- Original value of osc property
+local osc_prop = false
+
+-- Helper that converts two points to top-left and bottom-right
+local swizzle_points = function (p1, p2)
+ if p1.x > p2.x then p1.x, p2.x = p2.x, p1.x end
+ if p1.y > p2.y then p1.y, p2.y = p2.y, p1.y end
+end
+
+local clamp = function (val, min, max)
+ assert(min <= max)
+ if val < min then return min end
+ if val > max then return max end
+ return val
+end
+
+local video_space_from_screen_space = function (ssp)
+ -- Video native dimensions and screen size
+ local vid_w = mp.get_property("width")
+ local vid_h = mp.get_property("height")
+ local osd_w = mp.get_property("osd-width")
+ local osd_h = mp.get_property("osd-height")
+
+ -- Factor by which the video is scaled to fit the screen
+ local scale = math.min(osd_w/vid_w, osd_h/vid_h)
+
+ -- Size video takes up in screen
+ local vid_sw, vid_sh = scale*vid_w, scale*vid_h
+
+ -- Video offset within screen
+ local off_x = math.floor((osd_w - vid_sw)/2)
+ local off_y = math.floor((osd_h - vid_sh)/2)
+
+ local vsp = {}
+
+ -- Move the point to within the video
+ vsp.x = clamp(ssp.x, off_x, off_x + vid_sw)
+ vsp.y = clamp(ssp.y, off_y, off_y + vid_sh)
+
+ -- Convert screen-space to video-space
+ vsp.x = math.floor((vsp.x - off_x) / scale)
+ vsp.y = math.floor((vsp.y - off_y) / scale)
+
+ return vsp
+end
+
+local screen_space_from_video_space = function (vsp)
+ -- Video native dimensions and screen size
+ local vid_w = mp.get_property("width")
+ local vid_h = mp.get_property("height")
+ local osd_w = mp.get_property("osd-width")
+ local osd_h = mp.get_property("osd-height")
+
+ -- Factor by which the video is scaled to fit the screen
+ local scale = math.min(osd_w/vid_w, osd_h/vid_h)
+
+ -- Size video takes up in screen
+ local vid_sw, vid_sh = scale*vid_w, scale*vid_h
+
+ -- Video offset within screen
+ local off_x = math.floor((osd_w - vid_sw)/2)
+ local off_y = math.floor((osd_h - vid_sh)/2)
+
+ local ssp = {}
+ ssp.x = vsp.x * scale + off_x
+ ssp.y = vsp.y * scale + off_y
+ return ssp
+end
+
+-- Wrapper that converts RRGGBB / RRGGBBAA to ASS format
+local ass_set_color = function (idx, color)
+ assert(color:len() == 8 or color:len() == 6)
+ local ass = ""
+
+ -- Set alpha value (if present)
+ if color:len() == 8 then
+ local alpha = 0xff - tonumber(color:sub(7, 8), 16)
+ ass = ass .. string.format("\\%da&H%X&", idx, alpha)
+ end
+
+ -- Swizzle RGB to BGR and build ASS string
+ color = color:sub(5, 6) .. color:sub(3, 4) .. color:sub(1, 2)
+ return "{" .. ass .. string.format("\\%dc&H%s&", idx, color) .. "}"
+end
+
+local draw_rect = function (p1, p2)
+ local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height")
+
+ ass = assdraw.ass_new()
+
+ -- Draw overlay over surrounding unselected region
+
+ ass:draw_start()
+ ass:pos(0, 0)
+
+ ass:append(ass_set_color(1, "000000aa"))
+ ass:append(ass_set_color(3, "00000000"))
+
+ local l = math.min(p1.x, p2.x)
+ local r = math.max(p1.x, p2.x)
+ local u = math.min(p1.y, p2.y)
+ local d = math.max(p1.y, p2.y)
+
+ ass:rect_cw(0, 0, l, osd_h)
+ ass:rect_cw(r, 0, osd_w, osd_h)
+ ass:rect_cw(l, 0, r, u)
+ ass:rect_cw(l, d, r, osd_h)
+
+ ass:draw_stop()
+
+ -- Draw border around selected region
+
+ ass:new_event()
+ ass:draw_start()
+ ass:pos(0, 0)
+
+ ass:append(ass_set_color(1, "00000000"))
+ ass:append(ass_set_color(3, "000000ff"))
+ ass:append("{\\bord2}")
+
+ ass:rect_cw(p1.x, p1.y, p2.x, p2.y)
+
+ ass:draw_stop()
+
+ mp.set_osd_ass(osd_w, osd_h, ass.text)
+end
+
+local draw_fill = function ()
+ local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height")
+
+ ass = assdraw.ass_new()
+ ass:draw_start()
+ ass:pos(0, 0)
+
+ ass:append(ass_set_color(1, "000000aa"))
+ ass:append(ass_set_color(3, "00000000"))
+ ass:rect_cw(0, 0, osd_w, osd_h)
+
+ ass:draw_stop()
+ mp.set_osd_ass(osd_w, osd_h, ass.text)
+end
+
+local draw_clear = function ()
+ local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height")
+ mp.set_osd_ass(osd_w, osd_h, "")
+end
+
+local draw_cropper = function ()
+ if #points == 1 then
+ local p1 = screen_space_from_video_space(points[1])
+ local p2 = {}
+ p2.x, p2.y = mp.get_mouse_pos()
+ draw_rect(p1, p2)
+ end
+end
+
+local uncrop = function ()
+ mp.command("no-osd vf del @" .. script_name .. ":crop")
+end
+
+local crop = function(p1, p2)
+ swizzle_points(p1, p2)
+
+ local w = p2.x - p1.x
+ local h = p2.y - p1.y
+ local ok, err = mp.command(string.format(
+ "no-osd vf add @%s:crop=%s:%s:%s:%s", script_name, w, h, p1.x, p1.y))
+
+ if not ok then
+ mp.osd_message("Cropping failed")
+ points = {}
+ end
+end
+
+local easycrop_stop = function ()
+ mp.set_property("osc", osc_prop)
+ cropping = false
+ mp.remove_key_binding("easycrop_mouse_btn0")
+ draw_clear()
+end
+
+local mouse_btn0_cb = function ()
+ if not cropping then
+ return
+ end
+
+ local mx, my = mp.get_mouse_pos()
+ table.insert(points, video_space_from_screen_space({ x = mx, y = my }))
+
+ if #points == 2 then
+ crop(points[1], points[2])
+ easycrop_stop()
+ end
+end
+
+local easycrop_start = function ()
+ -- Cropping requires swdec or hwdec with copy-back
+ local hwdec = mp.get_property("hwdec-current")
+ if hwdec == nil then
+ return mp.msg.error("Cannot determine current hardware decoder mode")
+ end
+ -- Check whitelist of ok values
+ local valid_hwdec = {
+ ["no"] = true, -- software decoding
+ -- Taken from mpv manual
+ ["videotoolbox-co"] = true,
+ ["vaapi-copy"] = true,
+ ["dxva2-copy"] = true,
+ ["d3d11va-copy"] = true,
+ ["mediacodec"] = true
+ }
+ if not valid_hwdec[hwdec] then
+ return mp.osd_message("Cropping requires swdec or hwdec with copy-back (see mpv manual)")
+ end
+
+ -- Just clear the current crop and return, if there is one
+ if #points ~= 0 then
+ uncrop()
+ points = {}
+ return
+ end
+
+ -- Hide OSC
+ osc_prop = mp.get_property("osc")
+ mp.set_property("osc", "no")
+
+ cropping = true
+ mp.add_forced_key_binding("mouse_btn0", "easycrop_mouse_btn0", mouse_btn0_cb)
+ draw_fill()
+end
+
+local easycrop_activate = function ()
+ if cropping then
+ easycrop_stop()
+ else
+ easycrop_start()
+ end
+end
+
+mp.add_key_binding("mouse_move", draw_cropper)
+mp.observe_property("osd-width", "native", draw_cropper)
+mp.observe_property("osd-height", "native", draw_cropper)
+
+mp.add_key_binding("c", "easy_crop", easycrop_activate)
diff --git a/.config/mpv/scripts/interactive-video.lua b/.config/mpv/scripts/interactive-video.lua
new file mode 100644
index 0000000..dff046a
--- /dev/null
+++ b/.config/mpv/scripts/interactive-video.lua
@@ -0,0 +1,905 @@
+local utils = require "mp.utils"
+local msg = require "mp.msg"
+
+
+--[[ Utility functions ]]------------------------------------------------------
+
+-- Change this to `msg.info' to see debug messages on mpv output.
+local msg_debug = msg.debug
+
+-- Count elements in a table.
+function count(tbl)
+ local n = 0
+ for _ in pairs(tbl) do n = n + 1 end
+ return n
+end
+
+-- Make a copy a table (but not of its elements).
+function shallow_copy(orig)
+ local copy
+ if type(orig) == "table" then
+ copy = {}
+ for key, val in pairs(orig) do
+ copy[key] = val
+ end
+ else
+ copy = orig
+ end
+ return copy
+end
+
+-- Round to nearest integer.
+math.round = function(x)
+ return math.floor(x + 0.5)
+end
+
+
+--[[ Time functions ]]---------------------------------------------------------
+
+-- Only used for rounding purposes: having the exact value is not necessary.
+-- Default to 25fps (i.e., 40ms).
+local frame_duration = 40
+
+-- Get frame duration from container-fps property.
+function update_frame_duration()
+ local fps = mp.get_property_number("container-fps") or 25
+ frame_duration = 1000 / (fps > 0 and fps or 25)
+end
+mp.register_event("file-loaded", update_frame_duration)
+
+-- Round time position (in ms) to a multiple of frame_duration.
+function round_to_frame(time)
+ return math.round(math.round(time / frame_duration) * frame_duration)
+end
+
+-- Time position of previous frame.
+function prev_frame(time)
+ return round_to_frame(time - frame_duration)
+end
+
+-- Format time position (in ms) as hh:mm:ss.msc.
+function time_to_string(time)
+ return string.format("%02d:%02d:%06.3f",
+ math.floor(time / 3600000),
+ math.floor(time / 60000) % 60,
+ (time % 60000) / 1000)
+end
+
+-- Retrieve the time-pos property (in ms) and round it to a frame position.
+function get_time_pos()
+ local t = (mp.get_property_number("time-pos") or 0) * 1000 - frame_duration
+ return round_to_frame(t)
+end
+
+-- Seek playback to given time position (in ms).
+function set_time_pos(time)
+ msg_debug("seek to: " .. time_to_string(time))
+ local t = round_to_frame(time + frame_duration) / 1000
+ mp.set_property_number("time-pos", t)
+end
+
+-- Retrieve the duration property (in ms) and round it to a frame position.
+function get_duration()
+ local t = (mp.get_property_number("duration") or 0) * 1000 - frame_duration
+ return round_to_frame(t)
+end
+
+
+--[[ Global data and state variables ]]----------------------------------------
+
+local initial_segment, segments, moments, preconditions, segment_groups
+local state = { active = false }
+
+
+--[[ Apply impression ]]-------------------------------------------------------
+
+function apply_impression(impression)
+ if impression == nil then
+ return
+
+ elseif impression.type == "userState" then
+ for var, val in pairs(impression.data.persistent or {}) do
+ msg_debug("set variable: " .. var .. " = " ..
+ utils.to_string(val) .. " (was: " ..
+ utils.to_string(state.vars[var]) .. ")")
+ state.vars[var] = val
+ end
+
+ else
+ msg.error("Invalid type of impression data: " ..
+ utils.to_string(impression.type))
+ end
+end
+
+
+--[[ Evaluate precondition ]]--------------------------------------------------
+
+function eval_precondition(cond)
+ if cond == nil then
+ return true
+
+ elseif type(cond) ~= "table" then
+ return cond
+
+ elseif #cond == 0 then
+ msg.error("Empty precondition expression")
+
+ elseif cond[1] == "not" and #cond == 2 then
+ return not eval_precondition(cond[2])
+
+ elseif cond[1] == "and" then
+ for i = 2, #cond do
+ if not eval_precondition(cond[i]) then return false end
+ end
+ return true
+
+ elseif cond[1] == "or" then
+ for i = 2, #cond do
+ if eval_precondition(cond[i]) then return true end
+ end
+ return false
+
+ elseif cond[1] == "eql" and #cond == 3 then
+ return eval_precondition(cond[2]) == eval_precondition(cond[3])
+
+ elseif cond[1] == "persistentState" and #cond == 2 then
+ return state.vars[cond[2]]
+
+ else
+ msg.error("Invalid precondition: " .. utils.to_string(cond))
+ end
+
+ return false
+end
+
+
+--[[ Format strings for displaying choices on OSD ]]---------------------------
+
+function format_osd_choices(choices)
+ local osd_choices = {}
+ for i = 1, #choices do
+ local str = ""
+ for j, ch in ipairs(choices) do
+ local b = i == j
+ str = str .. "{\\fscx70\\fscy70\\an5"
+ .. "\\pos(" .. ((2*j-1) / (2*#choices)) .. ",0.048)}"
+ .. (i == j and "{\\c&HFFFFFF&}[ " or "{\\c&H7F7F7F&}")
+ .. ch.text
+ .. (i == j and " ]" or "")
+ .. "\n"
+ end
+ table.insert(osd_choices, str)
+ end
+ return osd_choices
+end
+
+
+--[[ Update OSD according to current state ]]----------------------------------
+
+function update_osd()
+ -- Multiple-choice input.
+ if state.osd_choices and type(state.osd_input) == "number" then
+ mp.set_osd_ass(1, 1, state.osd_choices[state.osd_input])
+
+ -- Code entry input.
+ elseif state.osd_prompt and type(state.osd_input) == "string" then
+ mp.set_osd_ass(1, 1, "{\\fscx70\\fscy70\\an4\\pos(0.02,0.048)}" ..
+ state.osd_prompt .. " " .. state.osd_input .. "_\n")
+
+ -- Nothing to display.
+ else
+ mp.set_osd_ass(0, 0, "")
+ end
+end
+
+
+--[[ Navigation and input control functions ]]---------------------------------
+
+-- Dummy function.
+function cmd_nop()
+end
+
+-- Seek forward.
+function cmd_fwd(sec, skip)
+ return function()
+ process_events(get_time_pos() + sec * 1000, { no_skip = not skip,
+ ffwd = true })
+ end
+end
+
+-- Seek backward.
+function cmd_bwd(sec, skip)
+ return function()
+ rewind(sec * 1000, { no_skip = not skip })
+ end
+end
+
+-- Select next choice.
+function cmd_input_next()
+ if state.osd_input < #state.osd_choices then
+ state.osd_input = state.osd_input + 1
+ else
+ state.osd_input = 1
+ end
+ update_osd()
+end
+
+-- Select previous choice.
+function cmd_input_prev()
+ if state.osd_input > 1 then
+ state.osd_input = state.osd_input - 1
+ else
+ state.osd_input = #state.osd_choices
+ end
+ update_osd()
+end
+
+-- Input character.
+function cmd_input_char(char)
+ return function()
+ state.osd_input = state.osd_input .. char
+ update_osd()
+ end
+end
+
+-- Delete last input character.
+function cmd_input_backspace()
+ if string.len(state.osd_input) > 0 then
+ state.osd_input = string.sub(state.osd_input, 1, -2)
+ update_osd()
+ end
+end
+
+-- Submit current input.
+function cmd_input_submit()
+ state.osd_choices = nil
+ state.osd_prompt = nil
+ table.insert(state.events, 1, { time = get_time_pos(), type = "submit",
+ mom_id = state.osd_mom_id })
+ update_osd()
+ update_controls()
+end
+
+
+--[[ Key mappings for control functions ]]-------------------------------------
+
+-- Merge key mappings.
+-- Mappings in map1 take precedence over those in map2.
+function mapping_merge(map1, map2)
+ for key, ctrl in pairs(map2) do
+ map1[key] = map1[key] or ctrl
+ end
+end
+
+-- Key mapping for navigation.
+local nav_mapping = {
+ ["RIGHT"] = { "iv-nav-right", cmd_fwd( 10), {repeatable=true} },
+ ["SHIFT+RIGHT"] = { "iv-nav-sright", cmd_fwd( 10,true), {repeatable=true} },
+ ["LEFT"] = { "iv-nav-left", cmd_bwd( 10), {repeatable=true} },
+ ["SHIFT+LEFT"] = { "iv-nav-sleft", cmd_bwd( 10,true), {repeatable=true} },
+ ["UP"] = { "iv-nav-up", cmd_fwd( 60), {repeatable=true} },
+ ["SHIFT+UP"] = { "iv-nav-sup", cmd_fwd( 60,true), {repeatable=true} },
+ ["DOWN"] = { "iv-nav-down", cmd_bwd( 60), {repeatable=true} },
+ ["SHIFT+DOWN"] = { "iv-nav-sdown", cmd_bwd( 60,true), {repeatable=true} },
+ ["PGUP"] = { "iv-nav-pgup", cmd_fwd(1/0), {repeatable=true} },
+ ["SHIFT+PGUP"] = { "iv-nav-spgup", cmd_fwd(600,true), {repeatable=true} },
+ ["PGDWN"] = { "iv-nav-pgdwn", cmd_bwd(600), {repeatable=true} },
+ ["SHIFT+PGDWN"] = { "iv-nav-spgdwn", cmd_bwd(600,true), {repeatable=true} },
+}
+
+-- Control mapping for multiple-choice input.
+local input_choice_mapping = {
+ ["RIGHT"] = { "iv-input-right", cmd_input_next, { repeatable = true } },
+ ["LEFT"] = { "iv-input-left", cmd_input_prev, { repeatable = true } },
+ ["ENTER"] = { "iv-input-enter", cmd_input_submit },
+}
+for _, key in ipairs({ "UP", "DOWN", "PGUP", "PGDWN" }) do
+ input_choice_mapping[key] = { "iv-nop-"..key, cmd_nop }
+end
+mapping_merge(input_choice_mapping, nav_mapping)
+
+-- Control mapping for code entry input.
+local input_code_mapping = {
+ ["BS"] = { "iv-input-bs", cmd_input_backspace, { repeatable = true } },
+ ["ENTER"] = { "iv-input-enter", cmd_input_submit },
+}
+for _, key in ipairs({ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }) do
+ input_code_mapping[key] = { "iv-input-" ..key, cmd_input_char(key) }
+ input_code_mapping["KP"..key] = { "iv-input-kp"..key, cmd_input_char(key) }
+end
+for _, key in ipairs({ "RIGHT", "LEFT", "UP", "DOWN", "PGUP", "PGDWN" }) do
+ input_code_mapping[key] = { "iv-nop-" ..key, cmd_nop }
+end
+mapping_merge(input_code_mapping, nav_mapping)
+
+
+--[[ Update controls according to current state ]]-----------------------------
+
+local current_mapping
+
+function update_controls()
+ local new_mapping
+
+ if state.active then
+ -- Multiple-choice input.
+ if state.osd_choices and type(state.osd_input) == "number" then
+ new_mapping = input_choice_mapping
+
+ -- Code entry input.
+ elseif state.osd_prompt and type(state.osd_input) == "string" then
+ new_mapping = input_code_mapping
+
+ -- No input: only navigation controls.
+ else
+ new_mapping = nav_mapping
+ end
+ end
+
+ if new_mapping ~= current_mapping then
+ -- Remove current control bindings.
+ for key, ctrl in pairs(current_mapping or {}) do
+ mp.remove_key_binding(ctrl[1])
+ end
+
+ -- Register new control bindings.
+ current_mapping = new_mapping
+ for key, ctrl in pairs(current_mapping or {}) do
+ mp.add_forced_key_binding(key, table.unpack(ctrl))
+ end
+ end
+end
+
+
+--[[ Deactivate interactive video playback ]]----------------------------------
+
+function deactivate()
+ mp.unregister_event(on_tick)
+ state = { active = false }
+ update_osd()
+ update_controls()
+end
+
+
+--[[ Retrieve first valid segment from segment group ]]------------------------
+
+function resolve_segment_group(group_id)
+ for _, seg_id in ipairs(segment_groups[group_id] or {}) do
+ if type(seg_id) == "table" and seg_id.segmentGroup then
+ seg_id = resolve_segment_group(seg_id.segmentGroup)
+ if seg_id then return seg_id end
+ else
+ local cond_id = seg_id
+ if type(seg_id) == "table" then
+ cond_id = seg_id.precondition
+ seg_id = seg_id.segment
+ end
+ if eval_precondition(preconditions[cond_id]) then
+ return seg_id
+ end
+ end
+ end
+ return nil
+end
+
+
+--[[ Load segment and schedule all related events in the event queue ]]--------
+
+function load_segment(seg_id)
+ msg_debug("load segment: " .. seg_id)
+ state.seg_id = seg_id
+
+ state.events = {}
+
+ -- Add moment-related events.
+ for i, mom in ipairs(moments[state.seg_id] or {}) do
+ table.insert(state.events, { time = mom.startMs,
+ type = "start", mom_id = i })
+ table.insert(state.events, { time = prev_frame(mom.endMs),
+ type = "end", mom_id = i })
+ if string.sub(mom.type, 1, 6) == "scene:" then
+ table.insert(state.events, { time = mom.uiDisplayMS,
+ type = "display", mom_id = i })
+ table.insert(state.events, { time = prev_frame(mom.uiHideMS),
+ type = "hide", mom_id = i })
+ end
+ end
+
+ -- Sort moment-related events.
+ table.sort(state.events, function(ev1, ev2)
+ if ev1.time ~= ev2.time or ev1.mom_id ~= ev2.mom_id then
+ return ev1.time < ev2.time
+ elseif ev1.type ~= ev2.type then
+ return ev1.type == "start" or ev2.type == "end" or
+ (ev1.type == "display" and ev2.type == "hide")
+ else
+ return false
+ end
+ end)
+
+ -- Add segment start and end events.
+ local seg = segments[state.seg_id]
+ table.insert(state.events, 1, { time = seg.startTimeMs,
+ type = "start_seg" })
+ table.insert(state.events, { time = prev_frame(seg.endTimeMs or
+ get_duration()),
+ type = "end_seg" })
+end
+
+
+--[[ Process all pending events until given (or current) time position ]]------
+
+function process_events(time, flags)
+ time = time or get_time_pos()
+ flags = flags or {}
+
+ local seek = flags.ffwd
+
+ if not state.events then return end
+ while #state.events > 0 and time >= state.events[1].time do
+ -- Pop first event from queue.
+ local ev = state.events[1]
+ table.remove(state.events, 1)
+
+ msg_debug("process event: " .. time_to_string(ev.time) .. " " ..
+ string.format("%-10s", ev.type) ..
+ state.seg_id .. (ev.mom_id and ("/" .. ev.mom_id) or "") ..
+ ((ev.type == "submit" or ev.type == "end") and state.osd_input
+ and (" (" .. utils.to_string(state.osd_input) .. ")") or ""))
+
+ local next_seg_id = nil
+
+ -- Moment-related event?
+ if ev.mom_id then
+ local mom = moments[state.seg_id][ev.mom_id]
+
+ -- Start of new moment?
+ if ev.type == "start" then
+ -- Check precondition, and remove all moments related to this event
+ -- if it is not satisfied.
+ if not eval_precondition(mom.precondition) then
+ for i = #state.events, 1, -1 do
+ if state.events[i].mom_id == ev.mom_id then
+ table.remove(state.events, i)
+ end
+ end
+ else
+ apply_impression(mom.impressionData)
+ end
+
+ -- Scene-related event?
+ elseif string.sub(mom.type, 1, 6) == "scene:" then
+ -- Display user input controls.
+ if ev.type == "display" then
+ state.osd_mom_id = ev.mom_id
+ if mom.config.hasMultipleChoiceInput then
+ state.osd_prompt = "Enter code:"
+ state.osd_input = ""
+ else
+ state.osd_choices = format_osd_choices(mom.choices)
+ state.osd_input = mom.defaultChoiceIndex + 1
+ end
+ if state.hist[state.hist_idx].inputs[ev.mom_id] then
+ state.osd_input = state.hist[state.hist_idx].inputs[ev.mom_id]
+ end
+ if flags.no_skip and time > ev.time then
+ time = ev.time
+ seek = true
+ end
+
+ -- Hide user input controls.
+ elseif ev.type == "hide" then
+ state.osd_choices = nil
+ state.osd_prompt = nil
+ state.osd_mom_id = nil
+
+ -- Select branch according to user input.
+ elseif ev.type == "end" or
+ (ev.type == "submit" and
+ not mom.config.disableImmediateSceneTransition) then
+ -- Record input in history.
+ -- Clear all forward history if input different from recorded one.
+ local hist = state.hist[state.hist_idx]
+ if state.osd_input ~= hist.inputs[ev.mom_id] then
+ hist.inputs[ev.mom_id] = state.osd_input
+ for i, mi in ipairs(moments[state.seg_id]) do
+ if string.sub(mi.type, 1, 6) == "scene:" and
+ mi.startMs >= mom.endMs then
+ hist.inputs[i] = nil
+ end
+ end
+ for i = #state.hist, state.hist_idx+1, -1 do
+ state.hist[i] = nil
+ end
+ end
+
+ -- Find corresponding choice.
+ local choice
+ if mom.config.hasMultipleChoiceInput then
+ for _, ch in ipairs(mom.choices) do
+ if not ch.code or state.osd_input == ch.code then
+ choice = ch
+ break
+ end
+ end
+ if not choice then
+ msg.error("No choice available for input '" .. state.osd_input ..
+ "' in moment " .. state.seg_id .. "/" .. ev.mom_id)
+ deactivate()
+ return
+ end
+ else
+ choice = mom.choices[state.osd_input]
+ end
+ apply_impression(choice.impressionData)
+
+ -- Select next segment accordingly.
+ if not (mom.trackingInfo and
+ mom.trackingInfo.optionType == "fakeOption") then
+ next_seg_id = choice.segmentId or resolve_segment_group(choice.sg)
+ if not next_seg_id then
+ msg.error("No segment for choice '" .. choice.id .. "' " ..
+ "of moment " .. state.seg_id .. "/" .. ev.mom_id)
+ deactivate()
+ return
+ end
+ end
+ state.osd_input = nil
+ end
+ end
+
+ -- Start of current segment?
+ elseif ev.type == "start_seg" then
+ state.hist_idx = state.hist_idx + 1
+ if not state.hist[state.hist_idx] then
+ state.hist[state.hist_idx] = { seg_id = state.seg_id,
+ vars = shallow_copy(state.vars),
+ inputs = {} }
+ end
+
+ -- End of current segment?
+ elseif ev.type == "end_seg" then
+ next_seg_id = resolve_segment_group(state.seg_id) or
+ next(segments[state.seg_id].next)
+ if not next_seg_id then
+ msg.debug("No segment after " .. state.seg_id .. "; " ..
+ "assuming end of video")
+ deactivate()
+ mp.commandv("playlist-next", "force")
+ return
+ end
+ end
+
+ -- Load next segment?
+ if next_seg_id then
+ local cur_seg = segments[state.seg_id]
+ local next_seg = segments[next_seg_id]
+
+ -- If next segment does not directly follow current segment,
+ -- we need to jump.
+ if next_seg.startTimeMs ~= cur_seg.endTimeMs then
+ if flags.ffwd then
+ time = next_seg.startTimeMs + time - prev_frame(cur_seg.endTimeMs)
+ else
+ time = next_seg.startTimeMs
+ end
+ seek = true
+
+ -- Also jump if skipping end of current segment.
+ elseif time < next_seg.startTimeMs then
+ time = next_seg.startTimeMs
+ seek = true
+ end
+
+ load_segment(next_seg_id)
+ end
+ end
+
+ -- Jump to current time position, if required.
+ if seek then
+ set_time_pos(time)
+ end
+
+ -- Update OSD and controls according to current state.
+ update_osd()
+ update_controls()
+end
+
+
+--[[ Seek backwards and rewind history ]]--------------------------------------
+
+function rewind(delay, flags)
+ flags = flags or {}
+
+ if state.hist_idx == 0 then return end
+
+ local time = get_time_pos()
+ local seg_id = state.hist[state.hist_idx].seg_id
+ local first = true
+
+ -- Keep rewinding from segment to segment until delay is consumed.
+ while delay > 0 and (first or state.hist_idx > 1) do
+ if first then
+ first = false
+ else
+ state.hist_idx = state.hist_idx - 1
+ seg_id = state.hist[state.hist_idx].seg_id
+ time = segments[seg_id].endTimeMs
+ end
+
+ -- If in no-skip mode, find previous scene, if any.
+ if flags.no_skip then
+ local prev_mom
+ for _, mom in ipairs(moments[seg_id]) do
+ if string.sub(mom.type, 1, 6) == "scene:" and
+ time - delay <= mom.uiDisplayMS and mom.uiDisplayMS < time and
+ (not prev_mom or prev_mom.uiDisplayMS < mom.uiDisplayMS) then
+ prev_mom = mom
+ end
+ end
+ if prev_mom then
+ time = prev_mom.uiDisplayMS
+ delay = 0
+ break
+ end
+ end
+
+ -- Rewind until start of current segment.
+ delay = delay - time + segments[seg_id].startTimeMs
+ time = segments[seg_id].startTimeMs
+ end
+
+ -- Adjust actual time.
+ if delay < 0 then
+ time = time - delay
+ end
+
+ -- Load current segment and state variables from history.
+ load_segment(seg_id)
+ shallow_copy(state.vars, state.hist[state.hist_idx].vars)
+ state.hist_idx = state.hist_idx - 1
+ state.osd_choices = nil
+ state.osd_prompt = nil
+
+ -- Re-run current segment's history forward until actual time.
+ process_events(time, { ffwd = true })
+end
+
+
+--[[ Load and check JSON data files, if any, on file load ]]-------------------
+
+function on_start_file(_)
+ deactivate()
+
+ -- Look for JSON data files.
+
+ local dir = utils.split_path(mp.get_property("path"))
+ local base = mp.get_property("filename/no-ext")
+ local seg = utils.join_path(dir, base .. ".seg.json")
+ local ivm = utils.join_path(dir, base .. ".ivm.json")
+ if not (utils.file_info(seg) and utils.file_info(ivm)) then
+ return
+ end
+ msg.info("Found JSON data files for interactive video playback:\n" ..
+ " seg: " .. seg .. "\n" ..
+ " ivm: " .. ivm)
+
+ -- Read and parse JSON data files.
+
+ local file
+ file = io.open(seg, "r")
+ if not file then
+ msg.error("Cannot read from " .. seg)
+ return
+ end
+ seg = utils.parse_json(file:read("*a"))
+ file:close()
+
+ file = io.open(ivm, "r")
+ if not file then
+ msg.error("Cannot read from " .. ivm)
+ return
+ end
+ ivm = utils.parse_json(file:read("*a"))
+ file:close()
+
+ -- Initialize data from JSON structures.
+
+ segments = seg.segments
+ initial_segment = seg.initialSegment
+ local video_id = "" .. (seg.viewableId or "")
+
+ if not segments then
+ msg.error( "segments not found in JSON data files")
+ return
+ elseif not initial_segment then
+ msg.error("initialSegment not found in JSON data files")
+ return
+ elseif not video_id then
+ msg.error( "viewableId not found in JSON data files")
+ return
+ end
+
+ if ivm.videos and ivm.videos[video_id] and
+ ivm.videos[video_id].interactiveVideoMoments then
+ ivm = ivm.videos[video_id].interactiveVideoMoments.value
+ else
+ ivm = nil
+ end
+ if not ivm then
+ msg.error("interactiveVideoMoments not found in JSON data files")
+ return
+ end
+
+ moments = ivm.momentsBySegment
+ preconditions = ivm.preconditions
+ segment_groups = ivm.segmentGroups
+ state.vars = shallow_copy(ivm.stateHistory)
+
+ if not moments then
+ msg.error("momentsBySegment not found in JSON data files")
+ return
+ elseif not preconditions then
+ msg.error( "preconditions not found in JSON data files")
+ return
+ elseif not segment_groups then
+ msg.error( "segmentGroups not found in JSON data files")
+ return
+ elseif not state.vars then
+ msg.error( "stateHistory not found in JSON data files")
+ return
+ end
+
+ -- Sanity checks.
+
+ local fail = false
+
+ -- Check segment history.
+ if not (ivm.segmentHistory and #ivm.segmentHistory == 1 and
+ ivm.segmentHistory[1] == initial_segment) then
+ msg.error("Invalid segmentHistory" ..
+ ": expected " .. utils.to_string({ initial_segment }) ..
+ ", got " .. utils.to_string(ivm.segmentHistory))
+ fail = true
+ end
+
+ -- Check segments.
+ for k, seg in pairs(segments) do
+ if (seg.endTimeMs or 1/0) <= seg.startTimeMs then
+ msg.error("Segment " .. k .. " ends before it starts")
+ fail = true
+ end
+ local has_scene = false
+ for i, mom in ipairs(moments[k] or {}) do
+ if string.sub(mom.type, 1, 6) == "scene:" and
+ not (mom.trackingInfo and
+ mom.trackingInfo.optionType == "fakeOption") then
+ has_scene = true
+ break
+ end
+ end
+ if count(seg.next) > 1 and not has_scene and not segment_groups[k] then
+ msg.error("Segment " .. k .. " requires a branching moment " ..
+ "or a segment group")
+ fail = true
+ end
+ end
+
+ -- Check moments.
+ local n_moments = 0
+ for k, ml in pairs(moments) do
+ n_moments = n_moments + #ml
+ for i, mi in ipairs(ml) do
+ local ki = k .. "/" .. i
+
+ if mi.endMs <= mi.startMs then
+ msg.error("Moment " .. ki .. " ends before it starts")
+ fail = true
+ end
+ if mi.startMs < segments[k].startTimeMs then
+ msg.error("Moment " .. ki .. " starts before its segment does")
+ fail = true
+ end
+ if mi.endMs > (segments[k].endTimeMs or 1/0) then
+ msg.error("Moment " .. ki .. " ends after its segment does")
+ fail = true
+ end
+
+ -- Scene moments.
+ if string.sub(mi.type, 1, 6) == "scene:" then
+ if not mi.choices then
+ msg.error("Moment " .. ki .. " has no branching choices")
+ fail = true
+ end
+ if not (mi.uiDisplayMS and mi.uiHideMS) then
+ msg.error("Moment " .. ki .. " has no display interval")
+ fail = true
+ else
+ if mi.uiHideMS <= mi.uiDisplayMS then
+ msg.error("Moment " .. ki .. "'s display interval is empty")
+ fail = true
+ end
+ if mi.uiDisplayMS < mi.startMs or mi.uiHideMS >= mi.endMs then
+ msg.warn ("Moment " .. ki .. "'s display interval overflows " ..
+ "out of bounds")
+ end
+ end
+ if mi.endMs < (segments[k].endTimeMs or 1/0) and
+ not (mi.trackingInfo and
+ mi.trackingInfo.optionType == "fakeOption") then
+ msg.warn ("Moment " .. ki .. " ends before its segment does")
+ end
+
+ for j, mj in ipairs(ml) do
+ local kj = k .. "/" .. j
+ if i ~= j and mi.startMs <= mj.startMs and mj.startMs < mi.endMs then
+ msg.error("Moment " .. kj .. " starts while " .. ki ..
+ " is active")
+ fail = true
+ end
+ end
+
+ -- Notification moments.
+ elseif mi.type == "notification:playbackImpression" then
+ if mi.choices then
+ msg.error("Moment " .. ki .. " has branching choices")
+ fail = true
+ end
+ if not mi.impressionData then
+ msg.warn ("Moment " .. ki .. " has no impression data")
+ end
+
+ elseif mi.type == "notification:action" then
+ if mi.choices then
+ msg.error("Moment " .. ki .. " has branching choices")
+ fail = true
+ end
+
+ else
+ msg.error ("Moment " .. ki .. " has invalid type: " .. mi.type)
+ fail = true
+ end
+ end
+ end
+
+ if fail then
+ deactivate()
+ return
+ end
+
+ -- Display stats.
+
+ msg.verbose("Loaded JSON data files:\n" ..
+ " " .. count(segments) .. " segments\n" ..
+ " " .. n_moments .. " interactive moments\n" ..
+ " " .. count(preconditions) .. " preconditions\n" ..
+ " " .. count(segment_groups) .. " segment groups\n" ..
+ " " .. count(state.vars) .. " state variables")
+
+ state.active = true
+ state.hist_idx = 0
+ state.hist = {}
+ update_controls()
+end
+
+
+--[[ Jump to initial segment at beginning of playback ]]-----------------------
+
+function on_file_loaded(_)
+ if not state.active then return end
+ mp.register_event("tick", on_tick)
+ load_segment(initial_segment)
+ process_events(segments[initial_segment].startTimeMs, { ffwd = true })
+end
+
+
+--[[ Process queued events on each frame ]]------------------------------------
+
+function on_tick(_)
+ process_events()
+end
+
+
+--[[ Register script entry-point events ]]-------------------------------------
+
+mp.register_event("start-file", on_start_file)
+mp.register_event("file-loaded", on_file_loaded)
diff --git a/.config/mpv/scripts/mpvmenu b/.config/mpv/scripts/mpvmenu
new file mode 100644
index 0000000..5a7d4ce
--- /dev/null
+++ b/.config/mpv/scripts/mpvmenu
@@ -0,0 +1,454 @@
+#!/usr/bin/env python3
+
+import argparse
+import json
+import logging
+import os
+import os.path
+import signal
+import socket
+import subprocess
+import time
+
+import gi
+gi.require_version('Gtk', '3.0') # noqa
+from gi.repository import Gtk
+
+CONNECT_RETRY_DELAY = 2.0
+
+
+LOGGER = logging.getLogger(__name__)
+WORK_DIR = os.getcwd()
+
+post_menu_action = None
+
+
+class RPC:
+ def __init__(self, path):
+ self.path = path
+ self.socket = socket.socket(socket.AF_UNIX,
+ socket.SOCK_STREAM)
+ connected = False
+ while not connected:
+ try:
+ logging.debug("Attempting to connect to RPC.")
+ self.socket.connect(self.path)
+ connected = True
+ except socket.error:
+ logging.debug("Connection attempt failed.")
+ time.sleep(CONNECT_RETRY_DELAY)
+ self.file = self.socket.makefile("rw", 65536)
+
+ def send_line(self, s):
+ self.file.write(s + "\n")
+ self.file.flush()
+
+ def recv_line(self):
+ return self.file.readline(1024)
+
+ def send_cmd(self, *args):
+ self.send_line(json.dumps({"command": args}))
+
+ def recv_data(self):
+ s = self.recv_line()
+ if not s:
+ return None
+ return json.loads(s)
+
+ def get_result(self):
+ dat = {}
+ while not ("error" in dat):
+ dat = self.recv_data()
+ return dat
+
+ def command(self, *args):
+ self.send_cmd(*args)
+ dat = self.get_result()
+ success = (dat["error"] == "success")
+ return (success, dat["data"] if success else None)
+
+ def set_prop(self, prop, value):
+ self.send_cmd("set_property", prop, value)
+ return self.get_result()["error"] == "success"
+
+ def get_prop(self, prop):
+ self.send_cmd("get_property", prop)
+ dat = self.get_result()
+ return dat["data"] if dat["error"] == "success" else None
+
+
+class OPT:
+ NORMAL = 0
+ CHECK = 1
+ SEP = 2
+ SLIDER = 3
+
+ def __init__(self, name=None, typ=NORMAL,
+ init=None, activate=None):
+ self.name = name
+ self.typ = typ
+ self.init_ = init
+ self.activate_ = activate
+
+ def init(self):
+ if self.init_:
+ return self.init_()
+
+ def activate(self):
+ if self.activate_:
+ return self.activate_()
+ else:
+ LOGGER.debug("Option {} activated.".format(self.name))
+
+
+SEP = OPT(typ=OPT.SEP)
+
+
+class TOGGLE(OPT):
+ def __init__(self, name, prop):
+ OPT.__init__(self, name, OPT.CHECK)
+ self.prop = prop
+
+ def init(self):
+ self.state = rpc.get_prop(self.prop)
+ # In case we got None, because this property "vanished"
+ # for some reason, default to False.
+ if self.state is None:
+ LOGGER.warn("Can't get value for toggle property {}."
+ " No need to panic though.".format(self.prop))
+ self.state = False
+ LOGGER.debug("initial state for {} : {}."
+ .format(self.prop, self.state))
+
+ def activate(self):
+ rpc.set_prop(self.prop, not self.state)
+ LOGGER.debug("Property {} change: {} -> {}."
+ .format(self.prop, self.state, not self.state))
+
+
+class FILTER_TOGGLE(OPT):
+ def __init__(self, name, filter_type, filter_name, filter_opts):
+ OPT.__init__(self, name, OPT.CHECK)
+ self.ft = "af" if filter_type[0] == "a" else "vf"
+ self.filter_name = filter_name
+ self.filter_opts = filter_opts
+
+ def init(self):
+ self.state = (self.filter_name in map(lambda x: x["name"],
+ rpc.get_prop(self.ft)))
+
+ def activate(self):
+ # Note: af/vf toggle command might be removed/changed later.
+ rpc.command(self.ft, "toggle", self.filter_name+"="+self.filter_opts)
+
+
+class COMMAND(OPT):
+ def __init__(self, name, *args):
+ OPT.__init__(self, name, OPT.NORMAL)
+ self.args = args
+
+ def activate(self):
+ rpc.command(*self.args)
+
+
+class OPT_SET_PROP(OPT):
+ def __init__(self, name, prop, value):
+ OPT.__init__(self, name, OPT.NORMAL)
+ self.prop = prop
+ self.value = value
+
+ def activate(self):
+ rpc.set_prop(self.prop, self.value)
+
+
+def get_track_info():
+ info = {"video": [], "audio": [], "sub": []}
+ for i in range(rpc.get_prop("track-list/count")):
+ N = str(i)
+ type_ = rpc.get_prop("track-list/" + N + "/type")
+ track = {
+ "id": rpc.get_prop("track-list/" + N + "/id"),
+ "src-id": rpc.get_prop("track-list/" + N + "/src-id"),
+ "title": rpc.get_prop("track-list/" + N + "/title"),
+ "lang": rpc.get_prop("track-list/" + N + "/lang"),
+ "default": rpc.get_prop("track-list/" + N + "/default"),
+ }
+ if rpc.get_prop("track-list/" + N + "/external"):
+ track["filename"] = rpc.get_prop("track-list/" + N +
+ "/external-filename")
+ info[type_].append(track)
+ return info
+
+
+def to_abs_path(path):
+ return os.path.normpath(os.path.join(WORK_DIR, path))
+
+
+def load_file_run_dialog(title):
+ dialog = Gtk.FileChooserDialog(title, None,
+ Gtk.FileChooserAction.OPEN,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
+ dialog.set_current_folder(os.path.dirname(
+ to_abs_path(rpc.get_prop("path"))))
+ filename = ""
+ if (dialog.run() == Gtk.ResponseType.OK):
+ filename = dialog.get_filename()
+ dialog.destroy()
+ Gtk.main_quit()
+ return filename
+
+
+def load_sub_file():
+ filename = load_file_run_dialog("Choose a subtitle file")
+ if filename:
+ rpc.command("sub_add", filename)
+ LOGGER.debug("Subtitle file to load: {}.".format(filename))
+ else:
+ LOGGER.debug("Subtitle file load dlg canceled.")
+
+
+def load_file():
+ filename = load_file_run_dialog("Choose a subtitle file")
+ if filename:
+ rpc.command("loadfile", filename)
+
+
+def post_menu_action_factory(act):
+ def func():
+ global post_menu_action
+ post_menu_action = act
+ return func
+
+
+def dl_subs():
+ path = to_abs_path(rpc.get_prop("path"))
+ try:
+ logging.debug("Calling subdownloader.")
+ status = subprocess.call(["subdownloader", "--rename-video",
+ "-V", path])
+ logging.debug("Subdownloader exited with status: {}".format(status))
+ rpc.command("rescan-external-files")
+ except OSError:
+ dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO,
+ Gtk.ButtonsType.OK,
+ "Subdownloader required")
+ dialog.format_secondary_text("Currently, this option requires" +
+ "\nsubdownloader to be installed.")
+ dialog.run()
+ dialog.destroy()
+ Gtk.main_quit()
+
+
+def about():
+ dialog = Gtk.AboutDialog(program_name="MPVMenu",
+ comments="Popup menu for MPV",
+ logo_icon_name="")
+ dialog.run()
+ dialog.destroy()
+ Gtk.main_quit()
+
+
+class Layout:
+ def __init__(self, *args):
+ if len(args) > 1 and isinstance(args[0], str):
+ self.name = args[0]
+ args = args[1:]
+ self.items = args
+
+
+class TracklistLayout(Layout):
+ TYPE_VIDEO = 0
+ TYPE_AUDIO = 1
+ TYPE_SUB = 2
+
+ def __init__(self, name, typ):
+ self.typ = typ
+ self.name = name
+
+ @property
+ def items(self):
+ typ = ("video", "audio", "sub")[self.typ]
+ tracklist = get_track_info()[typ]
+ return list(map(self.track_info_to_opt, tracklist))
+
+ def track_info_to_opt(self, track):
+ prop = ("vid", "aid", "sid")[self.typ]
+ id_ = track["id"]
+ title = " "+track["title"] if track["title"] else " Untitled"
+ lang = " ("+track["lang"]+")" if track["lang"] else ""
+ default = " (default)" if track["default"] else ""
+ name = "{}{}{}{}".format(id_, title,
+ lang, default)
+ return OPT_SET_PROP(name, prop, track["id"])
+
+
+layout = Layout(
+ Layout(
+ "File",
+ OPT("Open file", activate=load_file),
+ SEP,
+ COMMAND("Quit mpv", "quit"),
+ COMMAND("Quit mpv (watch later)", "quit_watch_later"),
+ OPT("Quit mpvmenu", activate=exit)
+ ),
+ Layout(
+ "Playback",
+ TOGGLE("Pause", "pause"),
+ Layout(
+ "Rewind",
+ COMMAND("3 seconds", "seek", "-3"),
+ COMMAND("10 seconds", "seek", "-10"),
+ COMMAND("1 minute", "seek", "-60")
+ ),
+ Layout(
+ "Fast forward",
+ COMMAND("3 seconds", "seek", "3"),
+ COMMAND("10 seconds", "seek", "10"),
+ COMMAND("1 minute", "seek", "60")
+ ),
+ ),
+ Layout(
+ "Playlist",
+ COMMAND("Previous", "playlist_prev"),
+ COMMAND("Next", "playlist_next")
+ ),
+ Layout(
+ "Audio",
+ TracklistLayout("Select audio track",
+ TracklistLayout.TYPE_AUDIO),
+ TOGGLE("Mute", "mute"),
+ Layout(
+ "Audio Filters",
+ FILTER_TOGGLE("Dynamic Range Compression",
+ "a", "drc", "2:1")
+ )
+ ),
+ Layout(
+ "Video",
+ TracklistLayout("Select video track",
+ TracklistLayout.TYPE_VIDEO),
+ TOGGLE("Fullscreen", "fullscreen")
+ ),
+ Layout(
+ "Subtitles",
+ TracklistLayout("Select subtitle track",
+ TracklistLayout.TYPE_SUB),
+ TOGGLE("Enabled", "sub-visibility"),
+ OPT("Load subtitles from file", activate=load_sub_file),
+ OPT("Download subtitles",
+ activate=post_menu_action_factory(dl_subs))
+ ),
+ SEP,
+ Layout("Help", OPT("About", activate=about)),
+)
+
+
+class Menu(Gtk.Menu):
+ def __init__(self, layout):
+ Gtk.Menu.__init__(self)
+ self.process_layout(layout)
+ self.action = ""
+ self.show_all()
+ self.connect("selection-done",
+ self.on_selection_done)
+ self.popup(None, None,
+ None, None,
+ 2,
+ Gtk.get_current_event_time())
+ self.activation_handled = False
+
+ def on_selection_done(self, widget):
+ Gtk.main_quit()
+ return True
+
+ def on_menu_item_activate(self, widget):
+ global action
+ if not self.activation_handled:
+ widget.activate()
+ self.activation_handled = True
+ return True
+
+ def on_menu_item_btn(self, widget, evt):
+ if evt.button != 1:
+ return False
+ return self.on_menu_item_activate(widget)
+
+ def process_layout(self, layout, menu=None):
+ if not menu:
+ menu = self
+ for item in layout.items:
+ if isinstance(item, Layout):
+ menu_item = Gtk.MenuItem(item.name)
+ submenu = Gtk.Menu()
+ menu_item.set_submenu(submenu)
+ menu.append(menu_item)
+ self.process_layout(item, submenu)
+ else:
+ if item.typ == OPT.SEP:
+ menu_item = Gtk.SeparatorMenuItem()
+ else:
+ item.init()
+ if item.typ == OPT.CHECK:
+ menu_item = Gtk.CheckMenuItem(item.name)
+ menu_item.set_active(item.state)
+ else:
+ menu_item = Gtk.MenuItem(item.name)
+ menu_item.activate = item.activate
+ menu_item.connect("activate",
+ self.on_menu_item_activate)
+ # workaround for bug #695488
+ menu_item.connect("button-press-event",
+ self.on_menu_item_btn)
+ menu.append(menu_item)
+
+
+def run_client(script_message):
+ global rpc
+ global post_menu_action
+ rpc = RPC("/var/run/user/{}/mpv.sock".format(os.getuid()))
+
+ wd = rpc.get_prop("working-directory")
+ if wd:
+ WORK_DIR = wd # noqa
+ else:
+ LOGGER.warn("Can't get mpv's working directory,",
+ "using current working dir of this script.")
+
+ while True:
+ dat = rpc.recv_data()
+ if not dat:
+ break
+ # dispatch
+ if "event" in dat:
+ # event
+ if dat["event"] == "client-message" and \
+ dat["args"][0] == script_message:
+ menu = Menu(layout) # noqa
+ Gtk.main()
+ if post_menu_action:
+ logging.debug("Calling post menu action.")
+ post_menu_action()
+ post_menu_action = None
+
+
+if __name__ == "__main__":
+ arg_parser = argparse.ArgumentParser(description="Show a menu" +
+ " when an action occurs in MPV.")
+ arg_parser.add_argument("--log-level", type=str, choices=["debug", "info",
+ "warn", "error"],
+ default="warn", help="logging level")
+ arg_parser.add_argument("--script-message", type=str, default="popup_menu",
+ help="custom script message to trigger the menu")
+ args = arg_parser.parse_args()
+
+ logging.basicConfig(level=getattr(logging, args.log_level.upper()),
+ format="[%(asctime)s] %(levelname)s in " +
+ "%(funcName)s at %(lineno)d: %(message)s")
+
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ while True:
+ try:
+ run_client(args.script_message)
+ except (BrokenPipeError, ConnectionResetError):
+ pass
diff --git a/.config/mpv/scripts/notify-send.lua b/.config/mpv/scripts/notify-send.lua
new file mode 100644
index 0000000..e0c022d
--- /dev/null
+++ b/.config/mpv/scripts/notify-send.lua
@@ -0,0 +1,99 @@
+local utils = require "mp.utils"
+
+local cover_filenames = { "cover.png", "cover.jpg", "cover.jpeg",
+ "folder.jpg", "folder.png", "folder.jpeg",
+ "AlbumArtwork.png", "AlbumArtwork.jpg", "AlbumArtwork.jpeg" }
+
+function notify(summary, body, options)
+ local option_args = {}
+ for key, value in pairs(options or {}) do
+ table.insert(option_args, string.format("--%s=%s", key, value))
+ end
+ return mp.command_native({
+ "run", "notify-send", unpack(option_args),
+ summary, body,
+ })
+end
+
+function escape_pango_markup(str)
+ return string.gsub(str, "([\"'<>&])", function (char)
+ return string.format("&#%d;", string.byte(char))
+ end)
+end
+
+function notify_media(title, origin, thumbnail)
+ return notify(escape_pango_markup(title), origin, {
+ -- For some inscrutable reason, GNOME 3.24.2
+ -- nondeterministically fails to pick up the notification icon
+ -- if either of these two parameters are present.
+ --
+ -- urgency = "low",
+ -- ["app-name"] = "mpv",
+
+ -- ...and this one makes notifications nondeterministically
+ -- fail to appear altogether.
+ --
+ -- hint = "string:desktop-entry:mpv",
+
+ icon = thumbnail or "mpv",
+ })
+end
+
+function file_exists(path)
+ local info, _ = utils.file_info(path)
+ return info ~= nil
+end
+
+function find_cover(dir)
+ -- make dir an absolute path
+ if dir[1] ~= "/" then
+ dir = utils.join_path(utils.getcwd(), dir)
+ end
+
+ for _, file in ipairs(cover_filenames) do
+ local path = utils.join_path(dir, file)
+ if file_exists(path) then
+ return path
+ end
+ end
+
+ return nil
+end
+
+function notify_current_media()
+ local path = mp.get_property_native("path")
+
+ local dir, file = utils.split_path(path)
+
+ -- TODO: handle embedded covers and videos?
+ -- potential options: mpv's take_screenshot, ffprobe/ffmpeg, ...
+ -- hooking off existing desktop thumbnails would be good too
+ local thumbnail = find_cover(dir)
+
+ local title = file
+ local origin = dir
+
+ local metadata = mp.get_property_native("metadata")
+ if metadata then
+ function tag(name)
+ return metadata[string.upper(name)] or metadata[name]
+ end
+
+ title = tag("title") or title
+ origin = tag("artist_credit") or tag("artist") or ""
+
+ local album = tag("album")
+ if album then
+ origin = string.format("%s — %s", origin, album)
+ end
+
+ local year = tag("original_year") or tag("year")
+ if year then
+ origin = string.format("%s (%s)", origin, year)
+ end
+ end
+
+ return notify_media(title, origin, thumbnail)
+end
+
+mp.register_event("file-loaded", notify_current_media)
diff --git a/.config/mpv/scripts/webtorrent-hook.lua b/.config/mpv/scripts/webtorrent-hook.lua
new file mode 100644
index 0000000..cc66b56
--- /dev/null
+++ b/.config/mpv/scripts/webtorrent-hook.lua
@@ -0,0 +1,136 @@
+-- TODO prefetch if next in playlist?
+-- TODO handle torrent with multiple video files (if webtorrent can print json)
+-- - don't close kill webtorrent while still videos unplayed? or in playlist?
+-- - store titles/info when starting webtorrent and check stream-open-filename
+-- for any item in playlist to see if it matches stored entry
+
+local settings = {
+ close_webtorrent = true,
+ remove_files = true,
+ download_directory = "/tmp/webtorrent",
+ webtorrent_flags = "",
+ webtorrent_verbosity = "speed"
+}
+
+(require "mp.options").read_options(settings, "webtorrent-hook")
+
+local open_videos = {}
+
+-- http://lua-users.org/wiki/StringRecipes
+local function ends_with(str, ending)
+ return ending == "" or str:sub(-#ending) == ending
+end
+
+-- https://stackoverflow.com/questions/132397/get-back-the-output-of-os-execute-in-lua
+function os.capture(cmd, decolorize, raw)
+ if decolorize then
+ -- https://github.com/webtorrent/webtorrent-cli/issues/132
+ -- TODO webtorrent should have a way to just print json information with
+ -- no colors
+ -- https://stackoverflow.com/questions/19296667/remove-ansi-color-codes-from-a-text-file-using-bash/30938702#30938702
+ cmd = cmd .. " | sed -r 's/\\x1B\\[(([0-9]{1,2})?(;)?([0-9]{1,2})?)?[m,K,H,f,J]//g'"
+ end
+ local f = assert(io.popen(cmd, 'r'))
+ local s = assert(f:read('*a'))
+ f:close()
+ if raw then return s end
+ s = string.gsub(s, '^%s+', '')
+ s = string.gsub(s, '%s+$', '')
+ -- s = string.gsub(s, '[\n\r]+', ' ')
+ return s
+end
+
+function read_file(file)
+ local fh = assert(io.open(file, "rb"))
+ local contents = fh:read("*all")
+ fh:close()
+ return contents
+end
+
+function play_torrent()
+ local url = mp.get_property("stream-open-filename")
+ if (url:find("magnet:") == 1 or url:find("peerflix://") == 1
+ or url:find("webtorrent://") == 1 or ends_with(url, "torrent")) then
+ if url:find("webtorrent://") == 1 then
+ url = url:sub(14)
+ end
+ if url:find("peerflix://") == 1 then
+ url = url:sub(12)
+ end
+
+ os.execute("mkdir -p " .. settings.download_directory)
+ -- don't reuse files (so multiple mpvs works)
+ local output_file = settings.download_directory
+ .. "/webtorrent-output-" .. mp.get_time() .. ".log"
+ -- --keep-seeding is to prevent webtorrent from quitting once the download
+ -- is done
+ local webtorrent_command = "webtorrent "
+ .. settings.webtorrent_flags
+ .. " --out '" .. settings.download_directory .. "' --keep-seeding '"
+ .. url .. "' &> " .. output_file .. " & echo $!"
+ local pid = os.capture(webtorrent_command)
+ mp.msg.info("Waiting for webtorrent server")
+
+ local url_command = "tail -f " .. output_file
+ .. " | awk '/Server running at:/ {print $4; exit}'"
+ local url = os.capture(url_command, true)
+ mp.msg.info("Webtorrent server is up")
+
+ local title_command = "awk '/(Seeding|Downloading): / "
+ .. "{gsub(/(Seeding|Downloading): /, \"\"); print; exit}' "
+ .. output_file
+ local title = os.capture(title_command, true)
+ mp.msg.verbose("Setting media title to: " .. title)
+ mp.set_property("force-media-title", title)
+
+ local path
+ if title then
+ path = settings.download_directory .. "/" .. title
+ end
+ open_videos[url] = {title=title,path=path,pid=pid}
+
+ mp.set_property("stream-open-filename", url)
+
+ if settings.webtorrent_verbosity == "speed" then
+ local printer_pid
+ local printer_pid_file = settings.download_directory
+ .. "/webtorrent-printer-" .. mp.get_time() .. ".pid"
+ os.execute("tail -f " .. output_file
+ .. " | awk '/Speed:/' ORS='\r' & echo -n $! > "
+ .. printer_pid_file)
+ printer_pid = read_file(printer_pid_file)
+ mp.register_event("file-loaded",
+ function()
+ os.execute("kill " .. printer_pid)
+ end
+ )
+ end
+ end
+end
+
+function webtorrent_cleanup()
+ local url = mp.get_property("stream-open-filename")
+ if settings.close_webtorrent and open_videos[url] then
+ local title = open_videos[url].title
+ local path = open_videos[url].path
+ local pid = open_videos[url].pid
+
+ if pid then
+ mp.msg.verbose("Closing webtorrent for " .. title)
+ os.execute("kill " .. pid)
+ end
+
+ if settings.remove_files then
+ if path then
+ mp.msg.verbose("Removing media file for " .. title)
+ os.execute("rm -r '" .. path .. "'")
+ end
+ end
+
+ open_videos[url] = {}
+ end
+end
+
+mp.add_hook("on_load", 50, play_torrent)
+
+mp.add_hook("on_unload", 10, webtorrent_cleanup)
diff --git a/.config/mpv/watch_later/0C1ADB3AF0B707724A2089855957CBD1 b/.config/mpv/watch_later/0C1ADB3AF0B707724A2089855957CBD1
new file mode 100755
index 0000000..21e439c
--- /dev/null
+++ b/.config/mpv/watch_later/0C1ADB3AF0B707724A2089855957CBD1
@@ -0,0 +1 @@
+start=289.366667
diff --git a/.config/mpv/watch_later/0F889B2F5365362796408B979967EE2E b/.config/mpv/watch_later/0F889B2F5365362796408B979967EE2E
new file mode 100644
index 0000000..cf5f33e
--- /dev/null
+++ b/.config/mpv/watch_later/0F889B2F5365362796408B979967EE2E
@@ -0,0 +1 @@
+start=939.960000
diff --git a/.config/mpv/watch_later/488683831FDE0EC6BCF9CBF1C7E74488 b/.config/mpv/watch_later/488683831FDE0EC6BCF9CBF1C7E74488
new file mode 100644
index 0000000..67f42ba
--- /dev/null
+++ b/.config/mpv/watch_later/488683831FDE0EC6BCF9CBF1C7E74488
@@ -0,0 +1 @@
+start=2.969633
diff --git a/.config/mpv/watch_later/533BDF1A0D7E0CDF991484442A6339E4 b/.config/mpv/watch_later/533BDF1A0D7E0CDF991484442A6339E4
new file mode 100755
index 0000000..d5f7967
--- /dev/null
+++ b/.config/mpv/watch_later/533BDF1A0D7E0CDF991484442A6339E4
@@ -0,0 +1 @@
+start=1.400000
diff --git a/.config/mpv/watch_later/5AAA9D654D3330AA0E70641D90273C82 b/.config/mpv/watch_later/5AAA9D654D3330AA0E70641D90273C82
new file mode 100644
index 0000000..75647f9
--- /dev/null
+++ b/.config/mpv/watch_later/5AAA9D654D3330AA0E70641D90273C82
@@ -0,0 +1 @@
+start=1.334667
diff --git a/.config/mpv/watch_later/6CFA806481681DD742151CEB8C869D11 b/.config/mpv/watch_later/6CFA806481681DD742151CEB8C869D11
new file mode 100644
index 0000000..2b5c7b7
--- /dev/null
+++ b/.config/mpv/watch_later/6CFA806481681DD742151CEB8C869D11
@@ -0,0 +1,3 @@
+start=3734.792969
+osd-level=3
+pause=yes
diff --git a/.config/mpv/watch_later/729B1EA73253DFBDA2059A48708309C2 b/.config/mpv/watch_later/729B1EA73253DFBDA2059A48708309C2
new file mode 100644
index 0000000..bc85f3b
--- /dev/null
+++ b/.config/mpv/watch_later/729B1EA73253DFBDA2059A48708309C2
@@ -0,0 +1 @@
+start=3.795000
diff --git a/.config/mpv/watch_later/95E11B3A5B89BE9BB61A7CF58917C703 b/.config/mpv/watch_later/95E11B3A5B89BE9BB61A7CF58917C703
new file mode 100755
index 0000000..259193c
--- /dev/null
+++ b/.config/mpv/watch_later/95E11B3A5B89BE9BB61A7CF58917C703
@@ -0,0 +1 @@
+start=1.533333
diff --git a/.config/mpv/watch_later/A886B4B4658EC2EE3338EA97169D51C3 b/.config/mpv/watch_later/A886B4B4658EC2EE3338EA97169D51C3
new file mode 100644
index 0000000..37cdf27
--- /dev/null
+++ b/.config/mpv/watch_later/A886B4B4658EC2EE3338EA97169D51C3
@@ -0,0 +1,2 @@
+start=262.462200
+osd-level=3
diff --git a/.config/mpv/watch_later/BDE1B7DD20A6156BF14F815048C8C938 b/.config/mpv/watch_later/BDE1B7DD20A6156BF14F815048C8C938
new file mode 100755
index 0000000..0621c42
--- /dev/null
+++ b/.config/mpv/watch_later/BDE1B7DD20A6156BF14F815048C8C938
@@ -0,0 +1 @@
+start=1.866667
diff --git a/.config/mpv/watch_later/D06453D5ECC647181B9A40F626560C7C b/.config/mpv/watch_later/D06453D5ECC647181B9A40F626560C7C
new file mode 100644
index 0000000..f95dffe
--- /dev/null
+++ b/.config/mpv/watch_later/D06453D5ECC647181B9A40F626560C7C
@@ -0,0 +1 @@
+start=2745.432000
diff --git a/.config/mpv/watch_later/F1606355253CA7F0BEAC5248CD9E5253 b/.config/mpv/watch_later/F1606355253CA7F0BEAC5248CD9E5253
new file mode 100644
index 0000000..cf92384
--- /dev/null
+++ b/.config/mpv/watch_later/F1606355253CA7F0BEAC5248CD9E5253
@@ -0,0 +1,3 @@
+start=241.700000
+osd-level=3
+fullscreen=yes