From 3b0142cedcde39e4c2097ecd916a870a3ced5ec6 Mon Sep 17 00:00:00 2001 From: Vito Graffagnino Date: Tue, 8 Sep 2020 18:10:49 +0100 Subject: Added the relevent parts of the .config directory. Alss add ssh config --- .config/mpv/input.conf | 11 + .config/mpv/mpv.conf | 144 ++++ .config/mpv/mpvClipboard.log | 2 + .config/mpv/scripts/SmartCopyPaste-II-2.2.lua | 406 +++++++++ .config/mpv/scripts/UndoRedo-1.5.2.lua | 222 +++++ .config/mpv/scripts/easycrop.lua | 253 ++++++ .config/mpv/scripts/interactive-video.lua | 905 +++++++++++++++++++++ .config/mpv/scripts/mpvmenu | 454 +++++++++++ .config/mpv/scripts/notify-send.lua | 99 +++ .config/mpv/scripts/webtorrent-hook.lua | 136 ++++ .../watch_later/0C1ADB3AF0B707724A2089855957CBD1 | 1 + .../watch_later/0F889B2F5365362796408B979967EE2E | 1 + .../watch_later/488683831FDE0EC6BCF9CBF1C7E74488 | 1 + .../watch_later/533BDF1A0D7E0CDF991484442A6339E4 | 1 + .../watch_later/5AAA9D654D3330AA0E70641D90273C82 | 1 + .../watch_later/6CFA806481681DD742151CEB8C869D11 | 3 + .../watch_later/729B1EA73253DFBDA2059A48708309C2 | 1 + .../watch_later/95E11B3A5B89BE9BB61A7CF58917C703 | 1 + .../watch_later/A886B4B4658EC2EE3338EA97169D51C3 | 2 + .../watch_later/BDE1B7DD20A6156BF14F815048C8C938 | 1 + .../watch_later/D06453D5ECC647181B9A40F626560C7C | 1 + .../watch_later/F1606355253CA7F0BEAC5248CD9E5253 | 3 + 22 files changed, 2649 insertions(+) create mode 100755 .config/mpv/input.conf create mode 100755 .config/mpv/mpv.conf create mode 100644 .config/mpv/mpvClipboard.log create mode 100644 .config/mpv/scripts/SmartCopyPaste-II-2.2.lua create mode 100644 .config/mpv/scripts/UndoRedo-1.5.2.lua create mode 100644 .config/mpv/scripts/easycrop.lua create mode 100644 .config/mpv/scripts/interactive-video.lua create mode 100644 .config/mpv/scripts/mpvmenu create mode 100644 .config/mpv/scripts/notify-send.lua create mode 100644 .config/mpv/scripts/webtorrent-hook.lua create mode 100755 .config/mpv/watch_later/0C1ADB3AF0B707724A2089855957CBD1 create mode 100644 .config/mpv/watch_later/0F889B2F5365362796408B979967EE2E create mode 100644 .config/mpv/watch_later/488683831FDE0EC6BCF9CBF1C7E74488 create mode 100755 .config/mpv/watch_later/533BDF1A0D7E0CDF991484442A6339E4 create mode 100644 .config/mpv/watch_later/5AAA9D654D3330AA0E70641D90273C82 create mode 100644 .config/mpv/watch_later/6CFA806481681DD742151CEB8C869D11 create mode 100644 .config/mpv/watch_later/729B1EA73253DFBDA2059A48708309C2 create mode 100755 .config/mpv/watch_later/95E11B3A5B89BE9BB61A7CF58917C703 create mode 100644 .config/mpv/watch_later/A886B4B4658EC2EE3338EA97169D51C3 create mode 100755 .config/mpv/watch_later/BDE1B7DD20A6156BF14F815048C8C938 create mode 100644 .config/mpv/watch_later/D06453D5ECC647181B9A40F626560C7C create mode 100644 .config/mpv/watch_later/F1606355253CA7F0BEAC5248CD9E5253 (limited to '.config/mpv') 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 -- cgit v1.2.3