summary refs log tree commit diff
path: root/.config/mpv
diff options
context:
space:
mode:
Diffstat (limited to '.config/mpv')
-rw-r--r--.config/mpv/.editorconfig11
-rw-r--r--.config/mpv/config155
-rwxr-xr-x.config/mpv/hooks/mako.sh10
-rw-r--r--.config/mpv/input.conf186
-rw-r--r--.config/mpv/mvi/input.conf109
-rw-r--r--.config/mpv/mvi/mpv.conf49
-rw-r--r--.config/mpv/mvi/script-opts/detect_image.conf17
-rw-r--r--.config/mpv/mvi/script-opts/image_positioning.conf14
-rw-r--r--.config/mpv/mvi/script-opts/minimap.conf19
-rw-r--r--.config/mpv/mvi/script-opts/ruler.conf21
-rw-r--r--.config/mpv/mvi/script-opts/status_line.conf14
-rw-r--r--.config/mpv/mvi/scripts/detect-image.lua73
-rw-r--r--.config/mpv/mvi/scripts/equalizer.lua176
-rw-r--r--.config/mpv/mvi/scripts/freeze-window.lua14
-rw-r--r--.config/mpv/mvi/scripts/image-positioning.lua277
-rw-r--r--.config/mpv/mvi/scripts/minimap.lua201
l---------.config/mpv/mvi/scripts/mouse_coords.lua1
-rw-r--r--.config/mpv/mvi/scripts/ruler.lua314
-rw-r--r--.config/mpv/mvi/scripts/status-line.lua142
-rw-r--r--.config/mpv/script-opts/playlist_view.conf16
-rw-r--r--.config/mpv/script-opts/subs2srs.conf13
-rw-r--r--.config/mpv/script-opts/thumbfast.conf19
-rw-r--r--.config/mpv/script-opts/uosc.conf10
-rw-r--r--.config/mpv/script-opts/webm.conf3
-rw-r--r--.config/mpv/scripts/.stylua.toml6
-rw-r--r--.config/mpv/scripts/hooks.lua25
-rw-r--r--.config/mpv/scripts/idle_inhibit.lua5
-rw-r--r--.config/mpv/scripts/mouse_coords.lua41
-rw-r--r--.config/mpv/scripts/openuri.lua42
-rw-r--r--.config/mpv/scripts/whisper.lua543
-rw-r--r--.config/mpv/scripts/yanky.lua37
-rwxr-xr-x.config/mpv/update-uosc.sh20
-rw-r--r--.config/mpv/vapoursynth/interpolate.vpy84
33 files changed, 2667 insertions, 0 deletions
diff --git a/.config/mpv/.editorconfig b/.config/mpv/.editorconfig
new file mode 100644
index 0000000..c0b69e9
--- /dev/null
+++ b/.config/mpv/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+tab_width = 8
+end_of_line = lf
+insert_final_newline = true
+
+[*.lua]
+max_line_length = 100
diff --git a/.config/mpv/config b/.config/mpv/config
new file mode 100644
index 0000000..41b10cb
--- /dev/null
+++ b/.config/mpv/config
@@ -0,0 +1,155 @@
+vo = gpu-next
+hwdec = auto
+
+screenshot-directory = ~/pictures/screenshots/mpv
+screenshot-template = '%tY%tm%td-%tH%tM%tS-%F-%p'
+screenshot-format = png
+screenshot-png-compression = 9
+
+#
+# profiles
+#
+
+[fullscreen]
+vo=dmabuf-wayland
+
+[protocol.http]
+profile-desc="Optimized for streaming video"
+glsl-shaders-clr
+title="${media-title}"
+# ytdl-format=bv[height<=?1080]+ba / b[height<=?1080]
+ytdl-format=(bv[vcodec~='^av01']/bv)[height<=?1080]+ba / b[height<=?1080]
+demuxer-seekable-cache=yes
+cache-pause=no
+hls-bitrate=max
+
+[protocol.https]
+profile=protocol.http
+
+[reset]
+profile-desc="Reset everything to vanilla mpv"
+profile=default
+load-scripts=no
+osc=yes
+osd-bar=yes
+
+[bigcache]
+demuxer-readahead-secs=20
+demuxer-max-bytes=512M
+demuxer-max-back-bytes=512M
+
+[gpuhq]
+profile-desc="Optimized for HQ rendering"
+profile-cond=height <= 1440
+profile=gpu-hq
+
+[scale]
+profile-desc="Use fancy scaling algorithms"
+profile-cond=height <= 1440
+glsl-shaders-clr
+scale=ewa_lanczossharp
+dscale=ewa_lanczossharp
+correct-downscaling=yes
+sigmoid-upscaling=yes
+
+[webcam]
+profile-desc="Optimized for showing real-time webcam feed"
+profile-cond=(not not string.match(get('path'), '/dev/video*'))
+load-scripts=no
+osc=no
+profile=low-latency
+untimed
+video-sync=display-resample
+no-demuxer-thread
+opengl-glfinish=yes
+opengl-swapinterval=0
+vf-add=hflip
+
+[webcam/local]
+profile-cond=(not not string.match(get('path'), '/dev/video0'))
+profile=webcam
+demuxer-lavf-o=input_format=mjpeg
+
+[downmix]
+ad-lavc-downmix=no
+# == pan ==
+# <output channel layout> | <output channel definition>
+# FL = front left
+# FC = front center
+# BL = back left
+# SL = side left
+# LFE = low frequency
+#
+# == dynaudnorm ==
+# g (gausssize): 3-301 (must be odd; default: 31), window size (in frames) of smoothing filter around current frame
+#   larger: stronger smoothing -> less gain variation
+# f (framelen): 10-8000 ms (default: 500), length of the section of audio used to detect peaks
+# r (targetrms): 0.0-1.0 (default: 0.0 disabled), use RMS (root mean square) of signal instead of peak - better approx. of "perceived loudness"
+#   adjusts frames to a constant RMS value
+# p (peak): (default: 0.95 leaves 5% headroom), target peak value
+#af-add='lavfi=[pan=2.1| FL < FC+0.8FL+0.1*BL+0.1*SL | FR < FC+0.8*FR+0.1*BR+0.1*SR | LFE < LFE,dynaudnorm=g=41:f=700:r=0.9]'
+#af-add='lavfi=[pan=5.1| FL < 0.8*FL | FR < 0.8*FR | LFE < LFE | FC < FC | BL < BL | BR < BR,dynaudnorm=g=41:f=700:r=0.9]'
+af-add='lavfi=[pan=5.1| FL = 0.5*FL | FR = 0.5*FR | LFE = LFE | FC = FC | BL = 0.3*BL | BR = 0.3*BR]'
+
+[fastforward]
+profile-restore=copy-equal
+profile=fast
+scale=bilinear
+dscale=bilinear
+cscale=bilinear
+vd-lavc-skiploopfilter=all
+vd-lavc-skipframe=all
+
+[compressor]
+profile-desc="Apply audio compression"
+input-commands-add=load-script '/usr/share/mpv/lua/acompressor.lua'
+script-opts-add=acompressor-default_enable=yes
+
+# mpv image viewer settings
+[mvi]
+profile-desc="mpv image viewer"
+profile-restore=copy-equal
+profile=gpu-hq
+profile=scale
+sub-auto=no
+audio-file-auto=no
+term-status-msg=
+title="${?media-title:${media-title}}${!media-title:No file}"
+video-aspect-override=no
+loop-file=inf
+image-display-duration=inf
+input-conf=~~/mvi/input.conf
+input-commands-add=load-script '~~/mvi/scripts/ruler.lua'
+input-commands-add=load-script '~~/mvi/scripts/detect-image.lua'
+input-commands-add=load-script '~~/mvi/scripts/freeze-window.lua'
+input-commands-add=load-script '~~/mvi/scripts/image-positioning.lua'
+input-commands-add=load-script '~~/mvi/scripts/minimap.lua'
+input-commands-add=load-script '~~/mvi/scripts/ruler.lua'
+script-opts-add=uosc-volume=none
+#script-opts-add=uosc-controls=no
+#script-opts-add=uosc-progress=no
+script-opts-add=uosc-timeline_size=0
+directory-filter-types=image
+
+[extension.jpg]
+profile=mvi
+[extension.png]
+profile=mvi
+[extension.gif]
+profile=mvi
+
+# mpv audio player
+[mpa]
+profile-desc="mpv audio player"
+profile=pseudo-gui
+script-opts-add=uosc-volume=none
+directory-filter-types=audio
+
+[extension.ogg]
+profile=mpa
+[extension.flac]
+profile=mpa
+[extension.wav]
+profile=mpa
+[extension.mp3]
+profile=mpa
diff --git a/.config/mpv/hooks/mako.sh b/.config/mpv/hooks/mako.sh
new file mode 100755
index 0000000..60e5804
--- /dev/null
+++ b/.config/mpv/hooks/mako.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eux
+
+# TODO: figure out why this fucks mako
+exit 0
+
+case "${1:-}" in
+file-loaded) fyi -a mpv 'mpv hooks' 'dnd on' ;;
+shutdown) fyi -a mpv 'mpv hooks' 'dnd off' ;;
+esac
diff --git a/.config/mpv/input.conf b/.config/mpv/input.conf
new file mode 100644
index 0000000..b67dab7
--- /dev/null
+++ b/.config/mpv/input.conf
@@ -0,0 +1,186 @@
+# MPV INPUT CONFIG
+
+# taken from the default config
+ESC set fullscreen no
+f cycle fullscreen
+
+SPACE cycle pause  #! pause/play
+m cycle mute       #! controls > mute
+
+# ARROW KEY BINDINGS
+LEFT  seek -10 exact
+RIGHT seek  10 exact
+DOWN  seek -60 exact
+UP    seek  60 exact
+
+WHEEL_UP         seek -10 exact
+WHEEL_DOWN       seek  10 exact
+Shift+WHEEL_UP   seek -60 exact
+Shift+WHEEL_DOWN seek  60 exact
+
+h seek -10 keyframes
+j seek -60 keyframes
+k seek  60 keyframes
+l seek  10 keyframes
+
+HOME    seek 0 absolute-percent
+0 seek 0 absolute-percent
+
+END     seek 100 absolute-percent
+$ seek 100 absolute-percent
+
+Shift+RIGHT     playlist-next
+n playlist-next  #! controls > playlist next
+Shift+LEFT      playlist-prev
+p playlist-prev  #! controls > playlist prev
+Alt+Shift+s playlist-shuffle  #! controls > playlist shuffle
+
+#Alt+RIGHT       frame-step
+#Alt+LEFT        frame-back-step
+> frame-step
+< frame-back-step
+Ctrl+< seek -1 keyframes
+Ctrl+> seek  1 keyframes
+
+Shift+UP   add volume 10
+Shift+DOWN add volume -10
+Alt+UP     add volume 5
+Alt+DOWN   add volume -5
+
+L           add chapter 1  #! controls > chapter next
+H           add chapter -1  #! controls > chapter prev
+PGUP        add chapter 1
+PGDWN       add chapter -1
+
+a cycle audio
+A script-binding uosc/audio #! select > audio
+D cycle deinterlace
+v script-binding uosc/video #! select > video
+
+s cycle sub
+S script-binding uosc/subtitles           #! select > sub
+Ctrl+s script-binding uosc/load-subtitles #! select > load sub...
+e add sub-scale +0.1                      #! sub > scale up
+w add sub-scale -0.1                      #! sub > scale down
+r add sub-pos +1                          #! sub > pos up
+t add sub-pos -1                          #! sub > pos down
+
+z add sub-delay -0.1                      #! delay > sub -0.1
+x add sub-delay +0.1                      #! delay > sub +0.1
+Z add audio-delay -0.1                    #! delay > audio -0.1
+X add audio-delay +0.1                    #! delay > audio +0.1
+Ctrl+z sub-step -1                        #! delay > sub-step -1
+Ctrl+x sub-step 1                         #! delay > sub-step +1
+
+U cycle-values sub-ass-override "scale" "force" "strip" "no"  #! sub > ass override
+V cycle sub-visibility  #! sub > visibility
+# Meta+s cycle sub-ass-force-margins
+
+# PLAYER BINDINGS
+i script-binding stats/display-stats-toggle                   #! stats
+Ctrl+g screenshot                                             #! utils > screenshot > default
+Alt+g screenshot video                                        #! utils > screenshot > video
+
+Ctrl+a cycle-values video-aspect-override "16:9" "4:3" "21:9" "2.35:1" "-1" #! controls > aspect-ratio
+Alt+s cycle-values audio-channels "auto-safe" "auto" "stereo" #! controls > audio channels
+Alt+a cycle-values video-rotate 0 90 180 270                  #! controls > video-rotate
+# T cycle ontop
+
+q stop
+Q quit-watch-later  #! watch later
+
+# Ctrl+BS revert-seek mark-permanent
+
+Alt+] script-binding skip2silence #! utils > skip2silence
+Alt+} script-binding skip2scene #! utils > skip2scene
+u revert-seek mark-permanent #! controls > undo/revert seek
+
+= add volume 10
+Shift+= add volume 5
+- add volume -10
+Shift+- add volume -5
+
+Alt+[ cycle-values speed 0.75 0.5 0.25  #! controls > speed > slower
+Alt+] cycle-values speed 1.25 1.5 2.0   #! controls > speed > faster
+Alt+BS set speed 1.0                    #! utils > speed > reset
+
+M cycle-values vf setpts=(2/1)*PTS setpts=(4/1)*PTS setpts=(1/2)*PTS setpts=(1/4)*PTS setpts=(1/8)*PTS ""
+
+Alt+l-f cycle-values loop-file yes no #! loop > file
+Ctrl+l ab-loop                       #! loop > A-B
+
+# VAPOURSYNTH/FILTER BINDINGS
+b ignore # debanding
+B ignore
+F1 af toggle asubcut   #! fx > a:subcut
+F2 af toggle loudnorm  #! fx > a:loudnorm
+F3 af toggle earwax    #! fx > a:earwax
+F6 af toggle @skip-silence   #! fx > a:@skip-silence
+F4 vf toggle bwdif     #! fx > v:deinterlace
+F5 vf toggle @deband   #! fx > v:@deband
+F7 vf toggle @no-3d    #! fx > v:@no-3d
+
+# uosc
+? script-binding uosc/keybinds
+Ctrl+SPACE script-binding uosc/toggle-ui
+MBTN_MID script-binding uosc/menu
+: script-binding uosc/menu
+Enter script-binding uosc/menu
+P script-binding uosc/items     #! select > playlist
+C script-binding uosc/chapters  #! select > chapters
+o script-binding uosc/open-file #! select > file...
+Ctrl+o script-binding openuri/openuri #! select > uri...
+Ctrl+Shift+o script-binding openuri/pasteuri #! select > paste
+Ctrl+n script-binding uosc/next-file
+Ctrl+p script-binding uosc/prev-file
+
+# various
+Alt+w script-binding webm/display-webm-encoder   #! utils > webm encoder
+# Alt+s-a script-binding slicing/slicing_audio     #! utils > slicing > audio
+# Alt+s-m script-binding slicing/slicing_mark      #! utils > slicing > mark
+Alt+a-d script-binding autodeint                 #! utils > autodeint
+Alt+a-c script-binding autocrop/toggle_crop      #! utils > autocrop
+Alt+m-c script-binding mouse_coords              #! utils > mouse coords
+Alt+b script-binding blur/toggle-blur            #! utils > blur
+
+# gallery
+g ignore
+Shift+g script-message playlist-view-toggle  #! utils > playlist/gallery view
+
+# yanktime
+y-y script_binding yanky/yanktime  #! utils > yank > time
+y-p script_binding yanky/yankpath  #! utils > yank > path
+y-s script_binding yanky/yanksubs  #! utils > yank > subs
+
+# sponsorblock
+Alt+b-g script_binding sponsorblock/set_segment         #! sponsorblock > set
+Alt+b-G script_binding sponsorblock/submit_segment      #! sponsorblock > submit
+Alt+b-u script_binding sponsorblock/upvote_segment      #! sponsorblock > upvote
+Alt+b-UP script_binding sponsorblock/upvote_segment     #! sponsorblock > upvote
+Alt+b-d script_binding sponsorblock/downvote_segment    #! sponsorblock > downvote
+Alt+b-DOWN script_binding sponsorblock/downvote_segment #! sponsorblock > downvote
+
+# mpvacious
+Alt+a   script-binding mpvacious-menu-open #! mpvacious > open
+Ctrl+n  script-binding mpvacious-export-note #! mpvacious > export note
+
+# 1 ignore
+# 2 ignore
+# 3 ignore
+4 ignore
+# 5 ignore
+6 ignore
+7 ignore
+8 ignore
+9 ignore
+
+3 script-message whisper/sub-whisper
+5 cycle-values gamma-factor 1.0 1.1 1.2
+
+I apply-profile interpolate   #! profiles > interpolate
+
+Ctrl+Shift+0 apply-profile reset         #! profiles > reset
+Ctrl+Shift+1 apply-profile interpolate   #! profiles > interpolate
+Ctrl+Shift+2 apply-profile cache         #! profiles > cache
+Ctrl+Shift+3 apply-profile scale         #! profiles > scale
+Ctrl+Shift+4 apply-profile compressor    #! profiles > compressor
diff --git a/.config/mpv/mvi/input.conf b/.config/mpv/mvi/input.conf
new file mode 100644
index 0000000..1d5b989
--- /dev/null
+++ b/.config/mpv/mvi/input.conf
@@ -0,0 +1,109 @@
+1 change-list script-opts append image_positioning-drag_to_pan_margin=200
+2 change-list script-opts append ruler-exit_bindings=8
+3 change-list script-opts append ruler-line_color=FF
+4 change-list script-opts append ruler-scale=25
+5 change-list script-opts append ruler-max_size=20,20
+
+SPACE repeatable playlist-next
+alt+SPACE repeatable playlist-prev
+
+# familiar binds
+n playlist-next
+p playlist-prev
+
+UP ignore
+DOWN ignore
+LEFT repeatable playlist-prev
+RIGHT repeatable playlist-next
+
+# simple reminder of default bindings
+#1 add contrast -1
+#2 add contrast 1
+#3 add brightness -1
+#4 add brightness 1
+#5 add gamma -1
+#6 add gamma 1
+#7 add saturation -1
+#8 add saturation 1
+
+# mouse-centric bindings
+MBTN_RIGHT script-binding drag-to-pan
+MBTN_LEFT  script-binding pan-follows-cursor
+MBTN_LEFT_DBL ignore
+WHEEL_UP   script-message cursor-centric-zoom 0.1
+WHEEL_DOWN script-message cursor-centric-zoom -0.1
+
+# panning with the keyboard:
+# pan-image takes the following arguments
+# pan-image AXIS AMOUNT ZOOM_INVARIANT IMAGE_CONSTRAINED
+#            ^            ^                  ^
+#          x or y         |                  |
+#                         |                  |
+#   if yes, will pan by the same         if yes, stops panning if the image
+#     amount regardless of zoom             would go outside of the window
+
+ctrl+down  repeatable script-message pan-image y -0.1 yes yes
+ctrl+up    repeatable script-message pan-image y +0.1 yes yes
+ctrl+right repeatable script-message pan-image x -0.1 yes yes
+ctrl+left  repeatable script-message pan-image x +0.1 yes yes
+
+# now with more precision
+alt+down   repeatable script-message pan-image y -0.01 yes yes
+alt+up     repeatable script-message pan-image y +0.01 yes yes
+alt+right  repeatable script-message pan-image x -0.01 yes yes
+alt+left   repeatable script-message pan-image x +0.01 yes yes
+
+# replace at will with h,j,k,l if you prefer vim-style bindings
+
+# on a trackpad you may want to use these
+#WHEEL_UP    repeatable script-message pan-image y -0.02 yes yes
+#WHEEL_DOWN  repeatable script-message pan-image y +0.02 yes yes
+#WHEEL_LEFT  repeatable script-message pan-image x -0.02 yes yes
+#WHEEL_RIGHT repeatable script-message pan-image x +0.02 yes yes
+
+# align the border of the image to the border of the window
+# align-border takes the following arguments:
+# align-border ALIGN_X ALIGN_Y
+# any value for ALIGN_* is accepted, -1 and 1 map to the border of the window
+ctrl+shift+right script-message align-border -1 ""
+ctrl+shift+left  script-message align-border 1 ""
+ctrl+shift+down  script-message align-border "" -1
+ctrl+shift+up    script-message align-border "" 1
+
+# reset the image
+ctrl+0  no-osd set video-pan-x 0; no-osd set video-pan-y 0; no-osd set video-zoom 0
+
++ add video-zoom 0.5
+- add video-zoom -0.5; script-message reset-pan-if-visible
+= no-osd set video-zoom 0; script-message reset-pan-if-visible
+
+e script-message equalizer-toggle
+alt+e script-message equalizer-reset
+
+h no-osd vf toggle hflip; show-text "Horizontal flip"
+v no-osd vf toggle vflip; show-text "Vertical flip"
+
+r script-message rotate-video 90; show-text "Clockwise rotation"
+R script-message rotate-video -90; show-text "Counter-clockwise rotation"
+alt+r no-osd set video-rotate 0; show-text "Reset rotation"
+
+d script-message ruler
+
+# Toggling between pixel-exact reproduction and interpolation
+a cycle-values scale nearest ewa_lanczossharp
+
+# Toggle color management on or off
+c cycle icc-profile-auto
+
+# Screenshot of the window output
+S screenshot window
+
+# Toggle aspect ratio information on and off
+A cycle-values video-aspect-override "-1" "no"
+
+P script-message force-print-filename
+
+# ADVANCED: you can define bindings that belong to a "section" (named "image-viewer" here) like so:
+#alt+SPACE {image-viewer} repeatable playlist-prev
+#SPACE     {image-viewer} repeatable playlist-next
+# to load them conditionally with a command. See scripts-opts/image_viewer.conf for how you can do this
diff --git a/.config/mpv/mvi/mpv.conf b/.config/mpv/mvi/mpv.conf
new file mode 100644
index 0000000..973ba42
--- /dev/null
+++ b/.config/mpv/mvi/mpv.conf
@@ -0,0 +1,49 @@
+## IMAGE
+# classic opengl-hq parameter, change at will
+scale=spline36
+cscale=spline36
+dscale=mitchell
+dither-depth=auto
+correct-downscaling
+sigmoid-upscaling
+# debanding seems rarely useful with images
+#deband
+# dark grey background instead of pure black
+background=color
+background-color=0.2
+
+## MISC
+mute=yes
+# the osc is mostly useful for videos
+osc=no
+# don't try to autoload subtitles or audio files
+sub-auto=no
+audio-file-auto=no
+# get rid of the useless V: 00:00:00 / 00:00:00 line
+term-status-msg=
+
+# replace mpv with mvi in the window title
+title="${?media-title:${media-title}}${!media-title:No file} - mvi"
+
+# don't slideshow by default
+image-display-duration=inf
+# loop files in case of webms or gifs
+loop-file=inf
+# and loop the whole playlist
+loop-playlist=inf
+
+# you need this if you plan to use drag-to-pan or pan-follows-cursor with MOUSE_LEFT 
+window-dragging=no
+
+#according to haasn, aspect ratio info for PNG and JPG is "universally bust"
+[extension.png]
+video-aspect-override=no
+
+[extension.jpg]
+video-aspect-override=no
+
+[extension.jpeg]
+profile=extension.jpg
+
+[silent]
+msg-level=all=no
diff --git a/.config/mpv/mvi/script-opts/detect_image.conf b/.config/mpv/mvi/script-opts/detect_image.conf
new file mode 100644
index 0000000..586fb95
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/detect_image.conf
@@ -0,0 +1,17 @@
+# commands to execute when a file detected as an image (1 frame, no audio) is loaded or unloaded
+
+# an image was loaded, and the previous file was not an image (or there was no previous file)
+command_on_first_image_loaded=
+# an image was loaded (regardless of what the previous file was)
+command_on_image_loaded=
+# a non-image was loaded, and the previous file was an image
+command_on_non_image_loaded=
+
+# the purpose of these "hooks" is to let you change bindings, profiles, reset properties...
+# see https://mpv.io/manual/master/#list-of-input-commands for general command information
+# note that there is no such thing as "unloading a profile", to emulate this you must create an opposite profile and load that
+
+# example possible values:
+#command_on_first_image_loaded=apply-profile image; enable-section image-viewer; script-message status-line-enable
+#command_on_image_loaded=no-osd set video-pan-x 0; script-message align-border "" -1
+#command_on_non_image_loaded=disable-section image-viewer; no-osd set video-pan-x 0; no-osd set video-pan-y 0; no-osd set video-zoom 0; script-message status-line-disable
diff --git a/.config/mpv/mvi/script-opts/image_positioning.conf b/.config/mpv/mvi/script-opts/image_positioning.conf
new file mode 100644
index 0000000..998ec64
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/image_positioning.conf
@@ -0,0 +1,14 @@
+# size of the margins with drag-to-pan
+drag_to_pan_margin=50
+drag_to_pan_move_if_full_view=no
+
+# size of the margins with pan-follows-cursor
+pan_follows_cursor_margin=50
+
+# size of the margins with cursor-centric-zoom
+cursor_centric_zoom_margin=50
+# if the borders would show up, move the image 
+# this makes it not exactly cursor-centric in some cases
+cursor_centric_zoom_auto_center=yes
+# allow zooming out if the image can already fully fit in the window
+cursor_centric_zoom_dezoom_if_full_view=no
diff --git a/.config/mpv/mvi/script-opts/minimap.conf b/.config/mpv/mvi/script-opts/minimap.conf
new file mode 100644
index 0000000..72a05ea
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/minimap.conf
@@ -0,0 +1,19 @@
+# whether to show by default
+enabled=yes
+# the position of the center of the minimap, in percentage of the window (x, y)
+center=92,92
+# the scale of the minimap (i.e. the view rectangle is scale / 100 times the size of the window)
+scale=12
+# the cutoff size of the minimap (i.e. the image rectangle is clipped if it falls outside of the this zone)
+max_size=16,16
+# opacity of the "image" (from 00=opaque to FF=transparent)
+image_opacity=88
+# color of the "image" (#BBGGRR where each component rages from 00 to FF)
+image_color=BBBBBB
+# opacity of the "view"
+view_opacity=BB
+view_color=222222
+# whether the view should be drawn above the image
+view_above_image=yes
+# whether to show the minimap if the current image is fully visible
+hide_when_full_image_in_view=yes
diff --git a/.config/mpv/mvi/script-opts/ruler.conf b/.config/mpv/mvi/script-opts/ruler.conf
new file mode 100644
index 0000000..5e6f48d
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/ruler.conf
@@ -0,0 +1,21 @@
+# whether to show the length of the lines between the two points
+show_distance=yes
+# whether to show the coordinates of the two points
+show_coordinates=yes
+# the coordinate space of the text shown. Can be "image", "window", "both"
+coordinates_space=image
+# can be "degrees", "radians", "both", or "no"
+show_angles=degrees
+line_width=2
+dots_radius=3
+font_size=36
+# ranges from 00 (black) to FF (white)
+line_color=33
+# bindings used to set points. The binding to trigger ruler mode can also be used. Comma-separated list
+confirm_bindings=MBTN_LEFT,ENTER
+# bindings used to set points. The binding to trigger ruler mode can also be used. Comma-separated list
+exit_bindings=ESC
+# if yes, the first point will be immediately set at the cursor position when calling 'ruler'
+set_first_point_on_begin=no
+# if yes, the ruler overlay will be immediately cleared when setting the second point
+clear_on_second_point_set=no
diff --git a/.config/mpv/mvi/script-opts/status_line.conf b/.config/mpv/mvi/script-opts/status_line.conf
new file mode 100644
index 0000000..ec15108
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/status_line.conf
@@ -0,0 +1,14 @@
+# whether to show by default
+enabled=yes
+# its font size
+size=36
+# distance of the text to the borders
+margin=10
+# the text to be expanded
+# see property expansion: https://mpv.io/manual/master/#property-expansion
+# \N can be used for line breaks
+# you can also use ass tags, see here: http://docs.aegisub.org/3.2/ASS_Tags/
+text_top_left=
+text_top_right=
+text_bottom_left=${filename} [${playlist-pos-1}/${playlist-count}]
+text_bottom_right=[${dwidth:X}x${dheight:X}]
diff --git a/.config/mpv/mvi/scripts/detect-image.lua b/.config/mpv/mvi/scripts/detect-image.lua
new file mode 100644
index 0000000..bfe52c2
--- /dev/null
+++ b/.config/mpv/mvi/scripts/detect-image.lua
@@ -0,0 +1,73 @@
+local opts = {
+    command_on_first_image_loaded="",
+    command_on_image_loaded="",
+    command_on_non_image_loaded="",
+}
+local options = require 'mp.options'
+local msg = require 'mp.msg'
+
+options.read_options(opts, nil, function() end)
+
+function run_maybe(str)
+    if str ~= "" then
+        mp.command(str)
+    end
+end
+
+local was_image = false
+
+function set_image(is_image)
+    if is_image and not was_image then
+        msg.info("First image detected")
+        run_maybe(opts.command_on_first_image_loaded)
+    end
+    if is_image then
+        msg.info("Image detected")
+        run_maybe(opts.command_on_image_loaded)
+    end
+    if not is_image and was_image then
+        msg.info("Non-image detected")
+        run_maybe(opts.command_on_non_image_loaded)
+    end
+    was_image = is_image
+end
+
+local properties = {}
+
+function properties_changed()
+    local dwidth = properties["dwidth"]
+    local tracks = properties["track-list"]
+    local path = properties["path"]
+    local framecount = properties["estimated-frame-count"]
+
+    if not path or path == "" then return end
+    if not tracks or #tracks == 0 then return end
+    local audio_tracks = 0
+    for _, track in ipairs(tracks) do
+        if track.type == "audio" then
+            audio_tracks = audio_tracks + 1
+        end
+    end
+
+    -- only do things when state is consistent
+    if not framecount and audio_tracks > 0 then
+        set_image(false)
+    elseif framecount and dwidth and dwidth > 0 then
+        -- png have 0 frames, jpg 1 ¯\_(ツ)_/¯
+        set_image((framecount == 0 or framecount == 1) and audio_tracks == 0)
+    end
+end
+
+function observe(propname)
+    mp.observe_property(propname, "native", function(_, val)
+        if val ~= properties[propname] then
+            properties[propname] = val
+            msg.verbose("Property " .. propname .. " changed")
+            properties_changed()
+        end
+    end)
+end
+observe("estimated-frame-count")
+observe("track-list")
+observe("dwidth")
+observe("path")
diff --git a/.config/mpv/mvi/scripts/equalizer.lua b/.config/mpv/mvi/scripts/equalizer.lua
new file mode 100644
index 0000000..f7a50ab
--- /dev/null
+++ b/.config/mpv/mvi/scripts/equalizer.lua
@@ -0,0 +1,176 @@
+local opts = {
+    bars = 'brightness,contrast,gamma,saturation,hue',
+    draw_icons = true,
+}
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+options.read_options(opts, nil, function(c)
+end)
+
+local enabled = false
+local active_bars = {}
+local bar_being_dragged = nil
+local stale = false
+
+function split_comma(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = str
+    end
+    return ret
+end
+
+function get_position_normalized(x, y, bar)
+    return (x - bar.x) / bar.w, (y - bar.y) / bar.h
+end
+
+function handle_mouse_move()
+    if not bar_being_dragged then return end
+    local bar = bar_being_dragged
+    local mx, my = mp.get_mouse_pos()
+    local nx, _ = get_position_normalized(mx, my, bar)
+    nx = math.max(0, math.min(nx, 1))
+    local val = math.floor(nx * (bar.max_value - bar.min_value) + bar.min_value + 0.5)
+    mp.set_property_number(bar.property, val)
+    -- the observe_property call will take care of setting the value
+end
+
+function handle_mouse_left(table)
+    if table["event"] == "down" then
+        local mx, my = mp.get_mouse_pos()
+        for _, bar in ipairs(active_bars) do
+            local nx, ny = get_position_normalized(mx, my, bar)
+            if nx >= 0 and ny >= 0 and nx <= 1 and ny <= 1 then
+                bar_being_dragged = bar
+                local val = math.floor(nx * (bar.max_value - bar.min_value) + bar.min_value + 0.5)
+                mp.set_property_number(bar.property, val)
+                mp.add_forced_key_binding("mouse_move", "mouse_move", handle_mouse_move)
+                break
+            end
+        end
+    elseif table["event"] == "up" then
+        mp.remove_key_binding("mouse_move")
+        bar_being_dragged = nil
+    end
+end
+
+function property_changed(prop, val)
+    for _, bar in ipairs(active_bars) do
+        if bar.property == prop then
+            bar.value = val
+            stale = true
+            break
+        end
+    end
+end
+
+function idle_handler()
+    if not stale then return end
+    stale = false
+    local a = assdraw.ass_new()
+    a:new_event()
+    a:append(string.format('{\\an0\\bord2\\shad0\\1a&00&\\1c&%s&}', '888888'))
+    a:pos(0, 0)
+    a:draw_start()
+    for _, bar in ipairs(active_bars) do
+        a:rect_cw(bar.x, bar.y, bar.x + bar.w, bar.y + bar.h)
+    end
+    a:new_event()
+    a:append(string.format('{\\an0\\bord2\\shad0\\1a&00&\\1c&%s&}', 'dddddd'))
+    a:pos(0, 0)
+    a:draw_start()
+    for _, bar in ipairs(active_bars) do
+        if bar.value > bar.min_value then
+            local val_norm = (bar.value - bar.min_value) / (bar.max_value - bar.min_value)
+            a:rect_cw(bar.x, bar.y, bar.x + val_norm * bar.w, bar.y + bar.h)
+        end
+    end
+    for _, bar in ipairs(active_bars) do
+        a:new_event()
+        a:append("{\\an6\\fs40\\bord2}")
+        a:pos(bar.x - 8, bar.y + bar.h/2 - 2)
+        a:append(bar.property:sub(1,1):upper() .. bar.property:sub(2,-1))
+    end
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, a.text)
+end
+
+function fix_position()
+    local ww, wh = mp.get_osd_size()
+    for i, bar in ipairs(active_bars) do
+        bar.x = ww / 5
+        bar.y = wh / 2 + i * 50
+        bar.w = ww - 2 * (ww / 5)
+        bar.h = 30
+    end
+end
+
+function dimensions_changed()
+    stale = true
+    fix_position()
+end
+
+function enable()
+    if enabled then return end
+    enabled = true
+    mp.add_forced_key_binding("MBTN_LEFT", "mouse_left", handle_mouse_left, {complex=true})
+    for i, prop in ipairs(split_comma(opts.bars)) do
+        local prop_info = mp.get_property_native("option-info/" .. prop)
+        if not prop_info then
+            msg.warn("Property \'" .. prop .. "\' does not exist")
+        elseif not prop_info.type == 'Integer' then
+            msg.warn("Property \'" .. prop .. "\' is not an integer")
+        else
+            mp.observe_property(prop, 'native', property_changed)
+            active_bars[#active_bars + 1] = {
+                property = prop,
+                value = mp.get_property_number(prop),
+                min_value = prop_info.min,
+                max_value = prop_info.max,
+            }
+        end
+    end
+    stale = true
+    fix_position()
+    mp.observe_property("osd-dimensions", "native", dimensions_changed)
+    mp.register_idle(idle_handler)
+end
+
+function disable()
+    if not enabled then return end
+    enabled = false
+    active_bars = {}
+    bar_being_dragged = nil
+    mp.remove_key_binding("mouse_left")
+    mp.remove_key_binding("mouse_move")
+    mp.unobserve_property(property_changed)
+    mp.unobserve_property(dimensions_changed)
+    mp.unregister_idle(idle_handler)
+    mp.set_osd_ass(1280, 720, "")
+end
+
+function toggle()
+    if enabled then
+        disable()
+    else
+        enable()
+    end
+end
+
+function reset()
+    for _, prop in ipairs(split_comma(opts.bars)) do
+        local prop_info = mp.get_property_native("option-info/" .. prop)
+        if prop_info and prop_info["default-value"] then
+            mp.set_property(prop_info["name"], prop_info["default-value"])
+        end
+    end
+end
+
+mp.add_key_binding(nil, "equalizer-enable", enable)
+mp.add_key_binding(nil, "equalizer-disable", disable)
+mp.add_key_binding(nil, "equalizer-toggle", toggle)
+mp.add_key_binding(nil, "equalizer-reset", reset)
diff --git a/.config/mpv/mvi/scripts/freeze-window.lua b/.config/mpv/mvi/scripts/freeze-window.lua
new file mode 100644
index 0000000..693c717
--- /dev/null
+++ b/.config/mpv/mvi/scripts/freeze-window.lua
@@ -0,0 +1,14 @@
+-- credit to TheAMM
+
+local size_changed = false
+
+mp.register_idle(function()
+    if not size_changed then return end
+    local ww, wh = mp.get_osd_size()
+    if not ww or ww <= 0 or not wh or wh <= 0 then return end
+    mp.set_property("geometry", string.format("%dx%d", ww, wh))
+    size_changed = false
+end)
+
+mp.observe_property("osd-width", "native", function() size_changed = true end)
+mp.observe_property("osd-height", "native", function() size_changed = true end)
diff --git a/.config/mpv/mvi/scripts/image-positioning.lua b/.config/mpv/mvi/scripts/image-positioning.lua
new file mode 100644
index 0000000..c0036d9
--- /dev/null
+++ b/.config/mpv/mvi/scripts/image-positioning.lua
@@ -0,0 +1,277 @@
+local opts = {
+    drag_to_pan_margin = 50,
+    drag_to_pan_move_if_full_view=false,
+
+    pan_follows_cursor_margin = 50,
+
+    cursor_centric_zoom_margin = 50,
+    cursor_centric_zoom_auto_center = true,
+    cursor_centric_zoom_dezoom_if_full_view = false,
+}
+local options = require 'mp.options'
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+options.read_options(opts, nil, function() end)
+
+function clamp(value, low, high)
+    if value <= low then
+        return low
+    elseif value >= high then
+        return high
+    else
+        return value
+    end
+end
+
+
+local cleanup = nil -- function set up by drag-to-pan/pan-follows cursor and must be called to clean lingering state
+
+function drag_to_pan_handler(table)
+    if cleanup then
+        cleanup()
+        cleanup = nil
+    end
+    if table["event"] == "down" then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        local mouse_pos_origin, video_pan_origin = {}, {}
+        local moved = false
+        mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos()
+        video_pan_origin[1] = mp.get_property_number("video-pan-x")
+        video_pan_origin[2] = mp.get_property_number("video-pan-y")
+        local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+        local margin = opts.drag_to_pan_margin
+        local move_up = true
+        local move_lateral = true
+        if not opts.drag_to_pan_move_if_full_view then
+            if dim.ml >= 0 and dim.mr >= 0 then
+                move_lateral = false
+            end
+            if dim.mt >= 0 and dim.mb >= 0 then
+                move_up = false
+            end
+        end
+        if not move_up and not move_lateral then return end
+        local idle = function()
+            if moved then
+                local mX, mY = mp.get_mouse_pos()
+                local pX = video_pan_origin[1]
+                local pY = video_pan_origin[2]
+                if move_lateral then
+                    pX = video_pan_origin[1] + (mX - mouse_pos_origin[1]) / video_size[1]
+                    if 2 * margin > dim.ml + dim.mr then
+                        pX = clamp(pX,
+                            (-margin + dim.w / 2) / video_size[1] - 0.5,
+                            (margin - dim.w / 2) / video_size[1] + 0.5)
+                    else
+                        pX = clamp(pX,
+                            (margin - dim.w / 2) / video_size[1] + 0.5,
+                            (-margin + dim.w / 2) / video_size[1] - 0.5)
+                    end
+                end
+                if move_up then
+                    pY = video_pan_origin[2] + (mY - mouse_pos_origin[2]) / video_size[2]
+                    if 2 * margin > dim.mt + dim.mb then
+                        pY = clamp(pY,
+                            (-margin + dim.h / 2) / video_size[2] - 0.5,
+                            (margin - dim.h / 2) / video_size[2] + 0.5)
+                    else
+                        pY = clamp(pY,
+                            (margin - dim.h / 2) / video_size[2] + 0.5,
+                            (-margin + dim.h / 2) / video_size[2] - 0.5)
+                    end
+                end
+                mp.command("no-osd set video-pan-x " .. clamp(pX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(pY, -3, 3))
+                moved = false
+            end
+        end
+        mp.register_idle(idle)
+        mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end)
+        cleanup = function()
+            mp.remove_key_binding("image-viewer-mouse-move")
+            mp.unregister_idle(idle)
+        end
+    end
+end
+
+function pan_follows_cursor_handler(table)
+    if cleanup then
+        cleanup()
+        cleanup = nil
+    end
+    if table["event"] == "down" then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+        local moved = true
+        local idle = function()
+            if moved then
+                local mX, mY = mp.get_mouse_pos()
+                local x = math.min(1, math.max(- 2 * mX / dim.w + 1, -1))
+                local y = math.min(1, math.max(- 2 * mY / dim.h + 1, -1))
+                local command = ""
+                local margin = opts.pan_follows_cursor_margin
+                if dim.ml + dim.mr < 0 then
+                    command = command .. "no-osd set video-pan-x " .. clamp(x * (2 * margin - dim.ml - dim.mr) / (2 * video_size[1]), -3, 3) .. ";"
+                elseif mp.get_property_number("video-pan-x") ~= 0 then
+                    command = command .. "no-osd set video-pan-x " .. "0;"
+                end
+                if dim.mt + dim.mb < 0 then
+                    command = command .. "no-osd set video-pan-y " .. clamp(y * (2 * margin - dim.mt - dim.mb) / (2 * video_size[2]), -3, 3) .. ";"
+                elseif mp.get_property_number("video-pan-y") ~= 0 then
+                    command = command .. "no-osd set video-pan-y " .. "0;"
+                end
+                if command ~= "" then
+                    mp.command(command)
+                end
+                moved = false
+            end
+        end
+        mp.register_idle(idle)
+        mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end)
+        cleanup = function()
+            mp.remove_key_binding("image-viewer-mouse-move")
+            mp.unregister_idle(idle)
+        end
+    end
+end
+
+function cursor_centric_zoom_handler(amt)
+    local zoom_inc = tonumber(amt)
+    if not zoom_inc or zoom_inc == 0 then return end
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then return end
+
+    local margin = opts.cursor_centric_zoom_margin
+
+    local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+
+    -- the size in pixels of the (in|de)crement
+    local diff_width  = (2 ^ zoom_inc - 1) * video_size[1]
+    local diff_height = (2 ^ zoom_inc - 1) * video_size[2]
+    if not opts.cursor_centric_zoom_dezoom_if_full_view and
+        zoom_inc < 0 and
+        video_size[1] + diff_width + 2 * margin <= dim.w and
+        video_size[2] + diff_height + 2 * margin <= dim.h
+    then
+        -- the zoom decrement is too much, reduce it such that the full image is visible, no more, no less
+        -- in addition, this should take care of trying too zoom out while everything is already visible
+        local new_zoom_inc_x = math.log((dim.w - 2 * margin) / video_size[1]) / math.log(2)
+        local new_zoom_inc_y = math.log((dim.h - 2 * margin) / video_size[2]) / math.log(2)
+        local new_zoom_inc = math.min(0, math.min(new_zoom_inc_x, new_zoom_inc_y))
+        zoom_inc = new_zoom_inc
+        diff_width  = (2 ^ zoom_inc - 1) * video_size[1]
+        diff_height = (2 ^ zoom_inc - 1) * video_size[2]
+    end
+    local new_width = video_size[1] + diff_width
+    local new_height = video_size[2] + diff_height
+
+    local mouse_pos_origin = {}
+    mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos()
+    local new_pan_x, new_pan_y
+
+    -- some additional constraints:
+    -- if image can be fully visible (in either direction), set pan to 0
+    -- if border would show on either side, then prefer adjusting the pan even if not cursor-centric
+    local auto_c = opts.cursor_centric_zoom_auto_center
+    if auto_c and new_width <= dim.w then
+        new_pan_x = 0
+    else
+        local pan_x = mp.get_property("video-pan-x")
+        local rx = (dim.ml + video_size[1] / 2 - mouse_pos_origin[1]) / (video_size[1] / 2)
+        new_pan_x = (pan_x * video_size[1] + rx * diff_width / 2) / new_width
+        if auto_c then
+            new_pan_x = clamp(new_pan_x, (dim.w - 2 * margin) / (2 * new_width) - 0.5, - (dim.w - 2 * margin) / (2 * new_width) + 0.5)
+        end
+    end
+
+    if auto_c and new_height <= dim.h then
+        new_pan_y = 0
+    else
+        local pan_y = mp.get_property("video-pan-y")
+        local ry = (dim.mt + video_size[2] / 2 - mouse_pos_origin[2]) / (video_size[2] / 2)
+        new_pan_y = (pan_y * video_size[2] + ry * diff_height / 2) / new_height
+        if auto_c then
+            new_pan_y = clamp(new_pan_y, (dim.h - 2 * margin) / (2 * new_height) - 0.5, - (dim.h - 2 * margin) / (2 * new_height) + 0.5)
+        end
+    end
+
+    local zoom_origin = mp.get_property("video-zoom")
+    mp.command("no-osd set video-zoom " .. zoom_origin + zoom_inc .. "; no-osd set video-pan-x " .. clamp(new_pan_x, -3, 3) .. "; no-osd set video-pan-y " .. clamp(new_pan_y, -3, 3))
+end
+
+function align_border(x, y)
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then return end
+    local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+    local x, y = tonumber(x), tonumber(y)
+    local command = ""
+    if x then
+        command = command .. "no-osd set video-pan-x " .. clamp(- x * (dim.ml + dim.mr) / (2 * video_size[1]), -3, 3) .. ";"
+    end
+    if y then
+        command = command .. "no-osd set video-pan-y " .. clamp(- y * (dim.mt + dim.mb) / (2 * video_size[2]), -3, 3) .. ";"
+    end
+    if command ~= "" then
+        mp.command(command)
+    end
+end
+
+function pan_image(axis, amount, zoom_invariant, image_constrained)
+    amount = tonumber(amount)
+    if not amount or amount == 0 or axis ~= "x" and axis ~= "y" then return end
+    if zoom_invariant == "yes" then
+        amount = amount / 2 ^ mp.get_property_number("video-zoom")
+    end
+    local prop = "video-pan-" .. axis
+    local old_pan = mp.get_property_number(prop)
+    if image_constrained == "yes" then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        local margin =
+            (axis == "x" and amount > 0) and dim.ml
+            or (axis == "x" and amount < 0) and dim.mr
+            or (amount > 0) and dim.mt
+            or (amount < 0) and dim.mb
+        local vid_size = (axis == "x") and (dim.w - dim.ml - dim.mr) or (dim.h - dim.mt - dim.mb)
+        local pixels_moved = math.abs(amount) * vid_size
+        -- the margin is already visible, no point going further
+        if margin >= 0 then
+            return
+        elseif margin + pixels_moved > 0 then
+            amount = -(math.abs(amount) / amount) * margin / vid_size
+        end
+    end
+    mp.set_property_number(prop, old_pan + amount)
+end
+
+function rotate_video(amt)
+    local rot = mp.get_property_number("video-rotate")
+    rot = (rot + amt) % 360
+    mp.set_property_number("video-rotate", rot)
+end
+
+function reset_pan_if_visible()
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then return end
+    local command = ""
+    if (dim.ml + dim.mr >= 0) then
+        command = command .. "no-osd set video-pan-x 0" .. ";"
+    end
+    if (dim.mt + dim.mb >= 0) then
+        command = command .. "no-osd set video-pan-y 0" .. ";"
+    end
+    if command ~= "" then
+        mp.command(command)
+    end
+end
+
+mp.add_key_binding(nil, "drag-to-pan", drag_to_pan_handler, {complex = true})
+mp.add_key_binding(nil, "pan-follows-cursor", pan_follows_cursor_handler, {complex = true})
+mp.add_key_binding(nil, "cursor-centric-zoom", cursor_centric_zoom_handler)
+mp.add_key_binding(nil, "align-border", align_border)
+mp.add_key_binding(nil, "pan-image", pan_image)
+mp.add_key_binding(nil, "rotate-video", rotate_video)
+mp.add_key_binding(nil, "reset-pan-if-visible", reset_pan_if_visible)
+mp.add_key_binding(nil, "force-print-filename", force_print_filename)
diff --git a/.config/mpv/mvi/scripts/minimap.lua b/.config/mpv/mvi/scripts/minimap.lua
new file mode 100644
index 0000000..f1b9a7d
--- /dev/null
+++ b/.config/mpv/mvi/scripts/minimap.lua
@@ -0,0 +1,201 @@
+local opts = {
+    enabled = true,
+    center = "92,92",
+    scale = 12,
+    max_size = "16,16",
+    image_opacity = "88",
+    image_color = "BBBBBB",
+    view_opacity = "BB",
+    view_color = "222222",
+    view_above_image = true,
+    hide_when_full_image_in_view = true,
+}
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+local options = require 'mp.options'
+
+options.read_options(opts, nil, function(c)
+    if c["enabled"] then
+        if opts.enabled then
+            enable()
+        else
+            disable()
+        end
+    end
+    mark_stale()
+end)
+
+function split_comma(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = tonumber(str)
+    end
+    return ret
+end
+
+local active = false
+local refresh = true
+
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+function mark_stale()
+    refresh = true
+end
+
+function refresh_minimap()
+    if not refresh then return end
+    refresh = false
+
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then
+        draw_ass("")
+        return
+    end
+    local ww, wh = dim.w, dim.h
+
+    if not (ww > 0 and wh > 0) then return end
+    if opts.hide_when_full_image_in_view then
+        if dim.mt >= 0 and dim.mb >= 0 and dim.ml >= 0 and dim.mr >= 0 then
+            draw_ass("")
+            return
+        end
+    end
+
+    local center = split_comma(opts.center)
+    center[1] = center[1] * 0.01 * ww
+    center[2] = center[2] * 0.01 * wh
+    local cutoff = split_comma(opts.max_size)
+    cutoff[1] = cutoff[1] * 0.01 * ww * 0.5
+    cutoff[2] = cutoff[2] * 0.01 * wh * 0.5
+
+    local a = assdraw.ass_new()
+    local draw = function(x, y, w, h, opacity, color)
+        a:new_event()
+        a:pos(center[1], center[2])
+        a:append("{\\bord0}")
+        a:append("{\\shad0}")
+        a:append("{\\c&" .. color .. "&}")
+        a:append("{\\2a&HFF}")
+        a:append("{\\3a&HFF}")
+        a:append("{\\4a&HFF}")
+        a:append("{\\1a&H" .. opacity .. "}")
+        w = w * 0.5
+        h = h * 0.5
+        a:draw_start()
+        local rounded = {true,true,true,true} -- tl, tr, br, bl
+        local x0,y0,x1,y1 = x-w, y-h, x+w, y+h
+        if x0 < -cutoff[1] then
+            x0 = -cutoff[1]
+            rounded[4] = false
+            rounded[1] = false
+        end
+        if y0 < -cutoff[2] then
+            y0 = -cutoff[2]
+            rounded[1] = false
+            rounded[2] = false
+        end
+        if x1 > cutoff[1] then
+            x1 = cutoff[1]
+            rounded[2] = false
+            rounded[3] = false
+        end
+        if y1 > cutoff[2] then
+            y1 = cutoff[2]
+            rounded[3] = false
+            rounded[4] = false
+        end
+
+        local r = 3
+        local c = 0.551915024494 * r
+        if rounded[0] then
+            a:move_to(x0 + r, y0)
+        else
+            a:move_to(x0,y0)
+        end
+        if rounded[1] then
+            a:line_to(x1 - r, y0)
+            a:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r)
+        else
+            a:line_to(x1, y0)
+        end
+        if rounded[2] then
+            a:line_to(x1, y1 - r)
+            a:bezier_curve(x1, y1 - r + c, x1 - r + c, y1, x1 - r, y1)
+        else
+            a:line_to(x1, y1)
+        end
+        if rounded[3] then
+            a:line_to(x0 + r, y1)
+            a:bezier_curve(x0 + r - c, y1, x0, y1 - r + c, x0, y1 - r)
+        else
+            a:line_to(x0, y1)
+        end
+        if rounded[4] then
+            a:line_to(x0, y0 + r)
+            a:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0)
+        else
+            a:line_to(x0, y0)
+        end
+        a:draw_stop()
+    end
+    local image = function()
+        draw((dim.ml/2 - dim.mr/2) / opts.scale,
+             (dim.mt/2 - dim.mb/2) / opts.scale,
+             (ww - dim.ml - dim.mr) / opts.scale,
+             (wh - dim.mt - dim.mb) / opts.scale,
+             opts.image_opacity,
+             opts.image_color)
+    end
+    local view = function()
+        draw(0,
+             0,
+             ww / opts.scale,
+             wh / opts.scale,
+             opts.view_opacity,
+             opts.view_color)
+    end
+    if opts.view_above_image then
+        image()
+        view()
+    else
+        view()
+        image()
+    end
+    draw_ass(a.text)
+end
+
+function enable()
+    if active then return end
+    active = true
+    mp.observe_property("osd-dimensions", nil, mark_stale)
+    mp.register_idle(refresh_minimap)
+    mark_stale()
+end
+
+function disable()
+    if not active then return end
+    active = false
+    mp.unobserve_property(mark_stale)
+    mp.unregister_idle(refresh_minimap)
+    draw_ass("")
+end
+
+function toggle()
+    if active then
+        disable()
+    else
+        enable()
+    end
+end
+
+if opts.enabled then
+    enable()
+end
+
+mp.add_key_binding(nil, "minimap-enable", enable)
+mp.add_key_binding(nil, "minimap-disable", disable)
+mp.add_key_binding(nil, "minimap-toggle", toggle)
diff --git a/.config/mpv/mvi/scripts/mouse_coords.lua b/.config/mpv/mvi/scripts/mouse_coords.lua
new file mode 120000
index 0000000..ecd3896
--- /dev/null
+++ b/.config/mpv/mvi/scripts/mouse_coords.lua
@@ -0,0 +1 @@
+../../scripts/mouse_coords.lua
\ No newline at end of file
diff --git a/.config/mpv/mvi/scripts/ruler.lua b/.config/mpv/mvi/scripts/ruler.lua
new file mode 100644
index 0000000..2cfcd0f
--- /dev/null
+++ b/.config/mpv/mvi/scripts/ruler.lua
@@ -0,0 +1,314 @@
+local opts = {
+    show_distance = true,
+    show_coordinates = true,
+    coordinates_space = "image",
+    show_angles = "degrees",
+    line_width = 2,
+    dots_radius = 3,
+    font_size = 36,
+    line_color = "33",
+    confirm_bindings = "MBTN_LEFT,ENTER",
+    exit_bindings = "ESC",
+    set_first_point_on_begin = false,
+    clear_on_second_point_set = false,
+}
+
+local options = require 'mp.options'
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+local state = 0 -- {0,1,2,3} = {inactive,setting first point,setting second point,done}
+local first_point = nil -- in normalized video space coordinates
+local second_point = nil -- in normalized video space coordinates
+local video_dimensions_stale = false
+
+function split(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = str
+    end
+    return ret
+end
+
+local confirm_bindings = split(opts.confirm_bindings)
+local exit_bindings = split(opts.exit_bindings)
+
+options.read_options(opts, nil, function()
+    if state ~= 0 then
+        remove_bindings()
+    end
+    confirm_bindings = split(opts.confirm_bindings)
+    exit_bindings = split(opts.exit_bindings)
+    if state ~= 0 then
+        add_bindings()
+        mark_stale()
+    end
+end)
+
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+
+function cursor_video_space_normalized(dim)
+    local mx, my = mp.get_mouse_pos()
+    local ret = {}
+    ret[1] = (mx - dim.ml) / (dim.w - dim.ml - dim.mr)
+    ret[2] = (my - dim.mt) / (dim.h - dim.mt - dim.mb)
+    return ret
+end
+
+function refresh()
+    if not video_dimensions_stale then return end
+    video_dimensions_stale = false
+
+    local dim = mp.get_property_native("osd-dimensions")
+    local out_params = mp.get_property_native("video-out-params")
+    if not dim or not out_params then
+        draw_ass("")
+        return
+    end
+    local vid_width = out_params.dw
+    local vid_height = out_params.dh
+
+    function video_space_normalized_to_video(point)
+        local ret = {}
+        ret[1] = point[1] * vid_width
+        ret[2] = point[2] * vid_height
+        return ret
+    end
+    function video_space_normalized_to_screen(point)
+        local ret = {}
+        ret[1] = point[1] * (dim.w - dim.ml - dim.mr) + dim.ml
+        ret[2] = point[2] * (dim.h - dim.mt - dim.mb) + dim.mt
+        return ret
+    end
+
+    local line_start = {}
+    local line_end = {}
+    if second_point then
+        line_start.image = video_space_normalized_to_video(first_point)
+        line_start.screen = video_space_normalized_to_screen(first_point)
+        line_end.image = video_space_normalized_to_video(second_point)
+        line_end.screen = video_space_normalized_to_screen(second_point)
+    elseif first_point then
+        line_start.image = video_space_normalized_to_video(first_point)
+        line_start.screen = video_space_normalized_to_screen(first_point)
+        line_end.image = video_space_normalized_to_video(cursor_video_space_normalized(dim))
+        line_end.screen = {}
+        line_end.screen[1], line_end.screen[2] = mp.get_mouse_pos()
+    else
+        local mx, my = mp.get_mouse_pos()
+        line_start.image = video_space_normalized_to_video(cursor_video_space_normalized(dim))
+        line_start.screen = {}
+        line_start.screen[1], line_start.screen[2] = mp.get_mouse_pos()
+        line_end = line_start
+    end
+    local distinct = (math.abs(line_start.screen[1] - line_end.screen[1]) >= 1
+                   or math.abs(line_start.screen[2] - line_end.screen[2]) >= 1)
+
+    local a = assdraw:ass_new()
+    local draw_setup = function(bord)
+        a:new_event()
+        a:pos(0,0)
+        a:append("{\\bord" .. bord .. "}")
+        a:append("{\\shad0}")
+        local r = opts.line_color
+        a:append("{\\3c&H".. r .. r .. r .. "&}")
+        a:append("{\\1a&HFF}")
+        a:append("{\\2a&HFF}")
+        a:append("{\\3a&H00}")
+        a:append("{\\4a&HFF}")
+        a:draw_start()
+    end
+    local dot = function(pos, size)
+        draw_setup(size)
+        a:move_to(pos[1], pos[2]-0.5)
+        a:line_to(pos[1], pos[2]+0.5)
+    end
+    local line = function(from, to, size)
+        draw_setup(size)
+        a:move_to(from[1], from[2])
+        a:line_to(to[1], to[2])
+    end
+    if distinct then
+        dot(line_start.screen, opts.dots_radius)
+        line(line_start.screen, line_end.screen, opts.line_width)
+        dot(line_end.screen, opts.dots_radius)
+    else
+        dot(line_start.screen, opts.dots_radius)
+    end
+
+    local line_info = function()
+        if not opts.show_distance then return end
+        a:new_event()
+        a:append("{\\fs36}{\\bord1}")
+        a:pos((line_start.screen[1] + line_end.screen[1]) / 2, (line_start.screen[2] + line_end.screen[2]) / 2)
+        local an = 1
+        if line_start.image[1] < line_end.image[1] then an = an + 2 end
+        if line_start.image[2] < line_end.image[2] then an = an + 6 end
+        a:an(an)
+        local image = math.sqrt(math.pow(line_start.image[1] - line_end.image[1], 2) + math.pow(line_start.image[2] - line_end.image[2], 2))
+        local screen = math.sqrt(math.pow(line_start.screen[1] - line_end.screen[1], 2) + math.pow(line_start.screen[2] - line_end.screen[2], 2))
+        if opts.coordinates_space == "both" then
+            a:append(string.format("image: %.1f\\Nscreen: %.1f", image, screen))
+        elseif opts.coordinates_space == "image" then
+            a:append(string.format("%.1f", image))
+        elseif opts.coordinates_space == "window" then
+            a:append(string.format("%.1f", screen))
+        end
+    end
+    local dot_info = function(pos, opposite)
+        if not opts.show_coordinates then return end
+        a:new_event()
+        a:append("{\\fs" .. opts.font_size .."}{\\bord1}")
+        a:pos(pos.screen[1], pos.screen[2])
+        local an
+        if distinct then
+            an = 1
+            if line_start.image[1] > line_end.image[1] then an = an + 2 end
+            if line_start.image[2] < line_end.image[2] then an = an + 6 end
+        else
+            an = 7
+        end
+        if opposite then
+            an = 9 + 1 - an
+        end
+        a:an(an)
+        if opts.coordinates_space == "both" then
+            a:append(string.format("image: %.1f, %.1f\\Nscreen: %i, %i",
+                pos.image[1], pos.image[2], pos.screen[1], pos.screen[2]))
+        elseif opts.coordinates_space == "image" then
+            a:append(string.format("%.1f, %.1f", pos.image[1], pos.image[2]))
+        elseif opts.coordinates_space == "window" then
+            a:append(string.format("%i, %i", pos.screen[1], pos.screen[2]))
+        end
+    end
+    dot_info(line_start, true)
+    if distinct then
+        line_info()
+        dot_info(line_end, false)
+    end
+    if distinct and opts.show_angles ~= "no" then
+        local dist = 50
+        local pos_from_angle = function(mult, angle)
+            return {
+                line_start.screen[1] + mult * dist * math.cos(angle),
+                line_start.screen[2] + mult * dist * math.sin(angle),
+            }
+        end
+        local extended = { line_start.screen[1], line_start.screen[2] }
+        if line_end.screen[1] > line_start.screen[1] then
+            extended[1] = extended[1] + dist
+        else
+            extended[1] = extended[1] - dist
+        end
+        line(line_start.screen, extended, math.max(0, opts.line_width-0.5))
+        local angle = math.atan(math.abs(line_start.image[2] - line_end.image[2]) / math.abs(line_start.image[1] - line_end.image[1]))
+        local fix_angle
+        local an
+        if line_end.image[2] < line_start.image[2] and line_end.image[1] > line_start.image[1] then
+            -- upper-right
+            an = 4
+            fix_angle = function(angle) return - angle end
+        elseif line_end.image[2] < line_start.image[2] then
+            -- upper-left
+            an = 6
+            fix_angle = function(angle) return math.pi + angle end
+        elseif line_end.image[1] < line_start.image[1] then
+            -- bottom-left
+            an = 6
+            fix_angle = function(angle) return math.pi - angle end
+        else
+            -- bottom-right
+            an = 4
+            fix_angle = function(angle) return angle end
+        end
+        -- should implement this https://math.stackexchange.com/questions/873224/calculate-control-points-of-cubic-bezier-curve-approximating-a-part-of-a-circle
+        local cp1 = pos_from_angle(1, fix_angle(angle*1/4))
+        local cp2 = pos_from_angle(1, fix_angle(angle*3/4))
+        local p2 = pos_from_angle(1, fix_angle(angle))
+        a:bezier_curve(cp1[1], cp1[2], cp2[1], cp2[2], p2[1], p2[2])
+
+        a:new_event()
+        a:append("{\\fs" .. opts.font_size .."}{\\bord1}")
+        local text_pos = pos_from_angle(1.1, fix_angle(angle*2/3)) -- you'd think /2 would make more sense, but *2/3  looks better
+        a:pos(text_pos[1], text_pos[2])
+        a:an(an)
+        if opts.show_angles == "both" then
+            a:append(string.format("%.2f\\N%.1f°", angle, angle / math.pi * 180))
+        elseif opts.show_angles == "degrees" then
+            a:append(string.format("%.1f°", angle / math.pi * 180))
+        elseif opts.show_angles == "radians" then
+            a:append(string.format("%.2f", angle))
+        end
+    end
+
+    draw_ass(a.text)
+end
+
+function mark_stale()
+    video_dimensions_stale = true
+end
+
+function add_bindings()
+    mp.add_forced_key_binding("mouse_move", "ruler-mouse-move", mark_stale)
+    for _, key in ipairs(confirm_bindings) do
+        mp.add_forced_key_binding(key, "ruler-next-" .. key, next_step)
+    end
+    for _, key in ipairs(exit_bindings) do
+        mp.add_forced_key_binding(key, "ruler-stop-" .. key, stop)
+    end
+end
+
+function remove_bindings()
+    for _, key in ipairs(confirm_bindings) do
+        mp.remove_key_binding("ruler-next-" .. key)
+    end
+    for _, key in ipairs(exit_bindings) do
+        mp.remove_key_binding("ruler-stop-" .. key)
+    end
+    mp.remove_key_binding("ruler-mouse-move")
+end
+
+function next_step()
+    if state == 0 then
+        state = 1
+        mp.register_idle(refresh)
+        mp.observe_property("osd-dimensions", nil, mark_stale)
+        mark_stale()
+        add_bindings()
+        if opts.set_first_point_on_begin then
+            next_step()
+        end
+    elseif state == 1 then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        state = 2
+        first_point = cursor_video_space_normalized(dim)
+    elseif state == 2 then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        state = 3
+        second_point = cursor_video_space_normalized(dim)
+        if opts.clear_on_second_point_set then
+            next_step()
+        end
+    else
+        stop()
+    end
+end
+
+function stop()
+    if state == 0 then return end
+    mp.unregister_idle(refresh)
+    mp.unobserve_property(mark_stale)
+    remove_bindings()
+    state = 0
+    first_point = nil
+    second_point = nil
+    draw_ass("")
+end
+
+mp.add_key_binding(nil, "ruler", next_step)
diff --git a/.config/mpv/mvi/scripts/status-line.lua b/.config/mpv/mvi/scripts/status-line.lua
new file mode 100644
index 0000000..7ac5168
--- /dev/null
+++ b/.config/mpv/mvi/scripts/status-line.lua
@@ -0,0 +1,142 @@
+local opts = {
+    enabled = true,
+    size = 36,
+    margin = 10,
+    text_top_left = "",
+    text_top_right = "",
+    text_bottom_left = "${filename} [${playlist-pos-1}/${playlist-count}]",
+    text_bottom_right = "[${dwidth:X}x${dheight:X}]",
+}
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+local options = require 'mp.options'
+
+options.read_options(opts, nil, function(c)
+    if c["enabled"] then
+        if opts.enabled then
+            enable()
+        else
+            disable()
+        end
+    end
+    if c["size"] or c["margin"] then
+        mark_stale()
+    end
+    if c["text_top_left"] or
+       c["text_top_right"] or
+       c["text_bottom_left"] or
+       c["text_bottom_right"]
+    then
+        observe_properties()
+        mark_stale()
+    end
+end)
+
+local stale = true
+local active = false
+
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+function refresh()
+    if not stale then return end
+    stale = false
+    local a = assdraw:ass_new()
+    local draw_text = function(text, an, x, y)
+        if text == "" then return end
+        local expanded = mp.command_native({ "expand-text", text})
+        if not expanded then
+            msg.error("Error expanding status-line")
+            return
+        end
+        msg.verbose("Status-line changed to: " .. expanded)
+        a:new_event()
+        a:an(an)
+        a:pos(x,y)
+        a:append("{\\fs".. opts.size.. "}{\\bord1.0}")
+        a:append(expanded)
+    end
+    local w,h = mp.get_osd_size()
+    local m = opts.margin
+    draw_text(opts.text_top_left, 7, m, m)
+    draw_text(opts.text_top_right, 9, w-m, m)
+    draw_text(opts.text_bottom_left, 1, m, h-m)
+    draw_text(opts.text_bottom_right, 3, w-m, h-m)
+    draw_ass(a.text)
+end
+
+function mark_stale()
+    stale = true
+end
+
+function observe_properties()
+    mp.unobserve_property(mark_stale)
+    if not active then return end
+    for _, str in ipairs({
+        opts.text_top_left,
+        opts.text_top_right,
+        opts.text_bottom_left,
+        opts.text_bottom_right,
+    }) do
+        local start = 0
+        while true do
+            local s, e, cap = string.find(str, "%${[?!]?([%l%d-/]*)", start)
+            if not s then break end
+            msg.verbose("Observing property " .. cap)
+            mp.observe_property(cap, nil, mark_stale)
+            start = e
+        end
+    end
+    mp.observe_property("osd-width", nil, mark_stale)
+    mp.observe_property("osd-height", nil, mark_stale)
+end
+
+function enable()
+    if active then return end
+    active = true
+    observe_properties()
+    mp.register_idle(refresh)
+    mark_stale()
+end
+
+
+function disable()
+    if not active then return end
+    active = false
+    observe_properties()
+    mp.unregister_idle(refresh)
+    draw_ass("")
+end
+
+function toggle()
+    if active then
+        disable()
+    else
+        enable()
+    end
+end
+
+if opts.enabled then
+    enable()
+end
+
+mp.add_key_binding(nil, "status-line-enable", enable)
+mp.add_key_binding(nil, "status-line-disable", disable)
+mp.add_key_binding(nil, "status-line-toggle", toggle)
+
+-- TODO remove
+mp.add_key_binding(nil, "enable-status-line", function()
+    msg.warn("This binding is deprecated, use 'status-line-enable' instead")
+    enable()
+end)
+mp.add_key_binding(nil, "disable-status-line", function()
+    msg.warn("This binding is deprecated, use 'status-line-disable' instead")
+    disable()
+end)
+mp.add_key_binding(nil, "toggle-status-line", function()
+    msg.warn("This binding is deprecated, use 'status-line-toggle' instead")
+    toggle()
+end)
diff --git a/.config/mpv/script-opts/playlist_view.conf b/.config/mpv/script-opts/playlist_view.conf
new file mode 100644
index 0000000..fcec2f0
--- /dev/null
+++ b/.config/mpv/script-opts/playlist_view.conf
@@ -0,0 +1,16 @@
+start_on_file_end=no
+
+gallery_position={ 0, 0 }
+gallery_size={ ww, wh }
+
+background_color=101010
+background_opacity=33
+
+UP=k
+DOWN=j
+LEFT=h
+RIGHT=l
+PAGE_UP=PGUP
+PAGE_DOWN=PGDWN
+FIRST=g-g
+LAST=G
diff --git a/.config/mpv/script-opts/subs2srs.conf b/.config/mpv/script-opts/subs2srs.conf
new file mode 100644
index 0000000..09e2af4
--- /dev/null
+++ b/.config/mpv/script-opts/subs2srs.conf
@@ -0,0 +1,13 @@
+autoclip=yes
+
+deck_name=subs2srs
+model_name=Japanese sentences
+sentence_field=SentKanji
+secondary_field=SentEng
+audio_field=SentAudio
+image_field=Image
+note_tag=subs2srs
+
+use_ffmpeg=yes
+snapshot_format=webp
+opus_container=webm
diff --git a/.config/mpv/script-opts/thumbfast.conf b/.config/mpv/script-opts/thumbfast.conf
new file mode 100644
index 0000000..5f5a2a6
--- /dev/null
+++ b/.config/mpv/script-opts/thumbfast.conf
@@ -0,0 +1,19 @@
+# Apply tone-mapping, no to disable
+# TODO: no required, see https://github.com/po5/thumbfast/issues/135
+tone_mapping=no
+
+# Enable on network playback
+network=no
+
+# Enable hardware decoding
+hwdec=yes
+
+# Spawn thumbnailer earlier for faster loads
+spawn_first=yes
+
+# Make smaller thumbnails
+max_height=150
+max_width=150
+
+# Increase display size
+scale_factor=1.2
diff --git a/.config/mpv/script-opts/uosc.conf b/.config/mpv/script-opts/uosc.conf
new file mode 100644
index 0000000..0ed5f6e
--- /dev/null
+++ b/.config/mpv/script-opts/uosc.conf
@@ -0,0 +1,10 @@
+border_radius=1
+controls=menu,gap,subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,<stream>stream-quality,<has_chapter>chapters,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next
+controls_size=24
+pause_indicator=none
+timeline_size=32
+top_bar=always
+top_bar_size=32
+top_bar_alt_title=${filename}
+top_bar_flash_on=audio,image,video
+volume_size=32
diff --git a/.config/mpv/script-opts/webm.conf b/.config/mpv/script-opts/webm.conf
new file mode 100644
index 0000000..90b4aae
--- /dev/null
+++ b/.config/mpv/script-opts/webm.conf
@@ -0,0 +1,3 @@
+output_format=av1
+output_directory=~
+output_template=[%s-%e]--%T
diff --git a/.config/mpv/scripts/.stylua.toml b/.config/mpv/scripts/.stylua.toml
new file mode 100644
index 0000000..616d360
--- /dev/null
+++ b/.config/mpv/scripts/.stylua.toml
@@ -0,0 +1,6 @@
+column_width = 100
+line_endings = "Unix"
+indent_type = "Spaces"
+indent_width = 2
+quote_style = "AutoPreferSingle"
+#call_parentheses = "Always"
diff --git a/.config/mpv/scripts/hooks.lua b/.config/mpv/scripts/hooks.lua
new file mode 100644
index 0000000..81660f7
--- /dev/null
+++ b/.config/mpv/scripts/hooks.lua
@@ -0,0 +1,25 @@
+local mp = require("mp")
+local utils = require("mp.utils")
+
+local function run(...)
+	mp.msg.warn('running hooks: ' .. table.concat({ ... }, " "))
+	utils.subprocess_detached({
+		args = { "sh", "-c", table.concat({ ... }, " ") .. " >/dev/null 2>&1" },
+		max_size = 0,
+		cancellable = false,
+	})
+end
+
+mp.register_event("file-loaded", function()
+	local function cb(n, vo)
+		if vo == true then
+			run("run-parts", "-v", "--arg=file-loaded", "~/.config/mpv/hooks")
+			mp.unobserve_property(cb)
+		end
+	end
+	mp.observe_property("vo-configured", "bool", cb)
+end)
+
+mp.register_event("shutdown", function()
+	run("run-parts", "-v", "--arg=shutdown", "~/.config/mpv/hooks")
+end)
diff --git a/.config/mpv/scripts/idle_inhibit.lua b/.config/mpv/scripts/idle_inhibit.lua
new file mode 100644
index 0000000..6c9ba1b
--- /dev/null
+++ b/.config/mpv/scripts/idle_inhibit.lua
@@ -0,0 +1,5 @@
+mp.observe_property("pause", "bool", function(_, paused)
+	local value = paused and "no" or "always"
+	mp.msg.warn(value)
+	mp.set_property("stop-screensaver", value)
+end)
diff --git a/.config/mpv/scripts/mouse_coords.lua b/.config/mpv/scripts/mouse_coords.lua
new file mode 100644
index 0000000..b2a2a65
--- /dev/null
+++ b/.config/mpv/scripts/mouse_coords.lua
@@ -0,0 +1,41 @@
+local msg = require("mp.msg")
+
+-- local x, y = 0, 0
+
+-- function set_marker()
+--     x, y = mp.get_mouse_pos()
+-- end
+
+-- function measure()
+--     x2, y2 = mp.get_mouse_pos()
+--     w = x2-x
+--     h = y2-y
+
+--     pos = "x: " .. x .. ", y:" .. y
+--     dimensions = "w: " .. h .. ", h:" .. h
+--     output = pos .. " | " .. dimensions
+
+--     mp.msg.warn(output)
+--     mp.osd_message(output)
+-- end
+
+function print_pos()
+	dw = mp.get_property_number("dwidth")
+	dh = mp.get_property_number("dheight")
+	ow, oh = mp.get_osd_size()
+
+	wr = math.floor(ow / dw)
+	hr = math.floor(oh / dh)
+
+	mx, my = mp.get_mouse_pos()
+	x = math.floor(mx / wr)
+	y = math.floor(my / hr)
+
+	pos = dw .. "x" .. dh .. " " .. x .. "," .. y
+	mp.msg.warn(pos)
+	mp.osd_message(pos)
+end
+
+mp.add_key_binding("0", mp.get_script_name(), print_pos)
+-- mp.add_key_binding("8", mp.get_script_name().."/set", set_marker)
+-- mp.add_key_binding("9", mp.get_script_name().."/measure", measure)
diff --git a/.config/mpv/scripts/openuri.lua b/.config/mpv/scripts/openuri.lua
new file mode 100644
index 0000000..61b20e5
--- /dev/null
+++ b/.config/mpv/scripts/openuri.lua
@@ -0,0 +1,42 @@
+mp = require("mp")
+format_json = require("mp.utils").format_json
+
+function open_uri()
+    local json = format_json({
+        type = "open-uri",
+        title = "https://mpv.io",
+        items = {
+            {
+                title = "Enter a network url",
+                align = "left",
+                value = "ignore",
+                muted = true,
+                selectable = false
+            }
+        },
+        on_search = { "script-message-to", mp.get_script_name(), "openuri-cb" },
+        palette = true,
+        search_style = "palette",
+        search_debounce = "submit"
+    }) or "{}"
+    mp.commandv("script-message-to", "uosc", "open-menu", json)
+end
+
+mp.register_script_message("openuri-cb", function(path)
+    mp.commandv("script-message-to", "uosc", "close-menu", "openuri")
+    mp.commandv("loadfile", path, "append-play")
+end)
+
+mp.add_key_binding("ctrl+o", "openuri", open_uri)
+
+mp.add_key_binding("ctrl+shift+o", "pasteuri", function()
+    local r = mp.command_native({
+        name = "subprocess",
+        playback_only = false,
+        capture_stdout = true,
+        args = { "wl-paste" },
+    })
+    if r.status == 0 then
+        mp.commandv("script-message-to", mp.get_script_name(), "openuri-cb", r.stdout)
+    end
+end)
diff --git a/.config/mpv/scripts/whisper.lua b/.config/mpv/scripts/whisper.lua
new file mode 100644
index 0000000..424edf8
--- /dev/null
+++ b/.config/mpv/scripts/whisper.lua
@@ -0,0 +1,543 @@
+--[[
+    * whisper.lua
+    *
+    * AUTHORS: dyphire,robertgzr
+    * License: MIT
+]]
+
+local msg = require('mp.msg')
+local utils = require('mp.utils')
+local options = require('mp.options')
+
+---- Script Options ----
+local o = {
+  ffmpeg_path = 'ffmpeg',
+  model = '~/src/edl-toolbox/whisper-ggml-small.bin',
+  language = 'auto',
+  queue = '3',
+  use_gpu = 'true',
+  gpu_device = '0',
+  -- Specify output path, supports absolute and relative paths
+  -- Special value: "source" saves the subtitle file to the directory
+  -- where the video file is located
+  output_path = 'source',
+  -- Specify how many subtitles are generated before updating
+  -- to avoid frequent flickering of subtitles
+  update_interval = 20,
+  -- Segment duration in seconds
+  segment_duration = 20,
+}
+
+options.read_options(o, _, function() end)
+------------------------
+
+o.ffmpeg_path = mp.command_native({ 'expand-path', o.ffmpeg_path })
+o.output_path = mp.command_native({ 'expand-path', o.output_path })
+
+local pid = mp.get_property_native('pid')
+local temp_path = os.getenv('TEMP') or '/tmp/'
+
+local subtitles_file
+local subtitle_count = 1
+local append_subtitle_count = 1
+local subtitles_written = false
+local whisper_running = false
+local state = {}
+local time_ranges = {}
+
+local is_windows = package.config:sub(1, 1) == '\\'
+
+local function is_protocol(path)
+  return type(path) == 'string'
+    and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
+end
+
+local function file_exists(path)
+  if path then
+    local meta = utils.file_info(path)
+    return meta and meta.is_file
+  end
+  return false
+end
+
+local function is_writable(path)
+  local file = io.open(path, 'w')
+  if file then
+    file:close()
+    os.remove(path)
+    return true
+  end
+  return false
+end
+
+local function check_and_remove_empty_file(file_path)
+  if file_exists(file_path) then
+    local file = io.open(file_path, 'r')
+    if file then
+      local content = file:read('*all')
+      file:close()
+      if content == '' then
+        os.remove(file_path)
+      end
+    end
+  end
+end
+
+local function normalize(path)
+  if normalize_path ~= nil then
+    if normalize_path then
+      path = mp.command_native({ 'normalize-path', path })
+    else
+      local directory = mp.get_property('working-directory', '')
+      path = utils.join_path(directory, path:gsub('^%.[\\/]', ''))
+      if is_windows then
+        path = path:gsub('\\', '/')
+      end
+    end
+    return path
+  end
+
+  normalize_path = false
+
+  local commands = mp.get_property_native('command-list', {})
+  for _, command in ipairs(commands) do
+    if command.name == 'normalize-path' then
+      normalize_path = true
+      break
+    end
+  end
+  return normalize(path)
+end
+
+local function format_time(time_str)
+  local h, m, s, ms = nil, nil, nil, nil
+  if time_str:match('^%d+:%d+:%d+[%.:]%d+$') then
+    h, m, s, ms = time_str:match('(%d+):(%d+):(%d+)[%.:](%d+)')
+  elseif time_str:match('^%d+:%d+[%.:]%d+$') then
+    h = 0
+    m, s, ms = time_str:match('(%d+):(%d+)[%.:](%d+)')
+  else
+    return time_str
+  end
+
+  return string.format('%02d:%02d:%02d,%03d', h, m, s, ms)
+end
+
+local function timestamp_to_seconds(timestamp)
+  local h, m, s, ms = timestamp:match('(%d+):(%d+):(%d+),(%d+)')
+  return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s) + tonumber(ms) / 1000
+end
+
+local function seconds_to_timestamp(seconds)
+  local h = math.floor(seconds / 3600)
+  local m = math.floor(seconds / 60) % 60
+  local s = math.floor(seconds % 60)
+  local ms = math.floor((seconds - math.floor(seconds)) * 1000)
+  return string.format('%02d:%02d:%02d,%03d', h, m, s, ms)
+end
+
+local function check_sub(sub_file)
+  local tracks = mp.get_property_native('track-list')
+  local _, sub_title = utils.split_path(sub_file)
+  for _, track in ipairs(tracks) do
+    local external_filename = track['external-filename']
+    local track_title = track['title']
+    if external_filename then
+      _, track_title = utils.split_path(external_filename)
+    end
+
+    if track['type'] == 'sub' and track_title == sub_title then
+      return true, track['id']
+    end
+  end
+  return false, nil
+end
+
+local function append_sub(sub_file, auto)
+  local sub, id = check_sub(sub_file)
+  if not sub then
+    if auto then
+      mp.commandv('sub-add', sub_file, 'auto')
+    else
+      mp.commandv('sub-add', sub_file)
+    end
+  else
+    mp.commandv('sub-reload', id)
+  end
+end
+
+local function shift_subtitle_timestamps(temp_srt, srt_file, subtitle_count, start_time)
+  local temp_file = io.open(temp_srt, 'r')
+  local main_file = io.open(srt_file, 'a')
+
+  if not temp_file or not main_file then
+    msg.error('Failed to open temporary or main SRT file.')
+    return subtitle_count
+  end
+
+  local subtitle_number = subtitle_count
+  if subtitle_number == 1 then
+    mp.osd_message('AI subtitles are loaded and updated in real time', 5)
+    msg.info('AI subtitles are loaded and updated in real time')
+  end
+  for line in temp_file:lines() do
+    if line:match('%d+:%d+:%d+,%d+%D+%d+:%d+:%d+,%d+') then
+      local start_ts, end_ts = line:match('(%d+:%d+:%d+,%d+)%D+(%d+:%d+:%d+,%d+)')
+      if start_ts and end_ts then
+        local start_seconds = timestamp_to_seconds(start_ts) + start_time
+        local end_seconds = timestamp_to_seconds(end_ts) + start_time
+        main_file:write(subtitle_number .. '\n')
+        main_file:write(
+          seconds_to_timestamp(start_seconds)
+            .. ' --> '
+            .. seconds_to_timestamp(end_seconds)
+            .. '\n'
+        )
+        subtitle_number = subtitle_number + 1
+      end
+    elseif line ~= '' and not tonumber(line) then
+      main_file:write(line .. '\n')
+    end
+  end
+
+  temp_file:close()
+  main_file:close()
+
+  os.remove(temp_srt)
+
+  return subtitle_number
+end
+------------------------
+
+local function process_audio_segment(video_path, temp_srt_path, start_time_str, segment_duration)
+  local whisper_filter = {
+    'model=' .. normalize(o.model),
+    'language=' .. o.language,
+    'use_gpu=' .. o.use_gpu,
+    'gpu_device=' .. o.gpu_device,
+    'queue=' .. o.queue,
+    'format=srt',
+    'destination=' .. temp_srt_path,
+  }
+  local args = {
+    o.ffmpeg_path,
+    '-hide_banner',
+    '-nostdin',
+    '-y',
+    '-loglevel',
+    'quiet',
+    '-i',
+    video_path,
+    '-ss',
+    start_time_str,
+    '-t',
+    utils.to_string(segment_duration),
+    '-map',
+    string.format('a:%s?', mp.get_property_number('current-tracks/audio/id', 0) - 1),
+    '-vn',
+    '-sn',
+    '-af',
+    'whisper=' .. table.concat(whisper_filter, ':'),
+    '-f',
+    'null',
+    '-',
+  }
+  local res = mp.command_native({
+    name = 'subprocess',
+    capture_stdout = true,
+    capture_stderr = true,
+    args = args,
+  })
+
+  if res and res.status ~= 0 then
+    msg.error('Processing failed for: ' .. video_path .. '\n' .. res.stdout .. ' -- ' .. res.stderr)
+    return false
+  end
+
+  return true
+end
+
+local function process_video_incrementally(video_path, srt_file, segment_duration)
+  local start_time = 0
+  local segment_index = 1
+  local file_duration = mp.get_property_number('duration')
+
+  msg.info('hi')
+
+  while true do
+    if start_time >= file_duration then
+      break
+    end
+    if start_time + segment_duration > file_duration then
+      segment_duration = file_duration - start_time
+    end
+
+    local temp_srt_file = utils.join_path(temp_path, 'whisper-' .. pid .. '.srt')
+    if file_exists(temp_srt_file) then
+      os.remove(temp_srt_file)
+    end
+
+    local start_time_str = string.format(
+      '%02d:%02d:%02d',
+      math.floor(start_time / 3600),
+      math.floor(start_time / 60) % 60,
+      start_time % 60
+    )
+    msg.verbose(
+      string.format('Processing segment: %d, Start Time: %s', segment_index, start_time_str)
+    )
+    local success =
+      process_audio_segment(video_path, temp_srt_file, start_time_str, segment_duration)
+    if not success or not file_exists(temp_srt_file) then
+      msg.verbose('Segment processing completed or failed.')
+      break
+    end
+
+    if file_exists(temp_srt_file) then
+      subtitle_count =
+        shift_subtitle_timestamps(temp_srt_file, srt_file, subtitle_count, start_time)
+    end
+
+    if file_exists(srt_file) then
+      append_sub(srt_file)
+    end
+
+    start_time = start_time + segment_duration
+    segment_index = segment_index + 1
+  end
+end
+
+local function whisper_segment()
+  local path = mp.get_property('path')
+  local fname = mp.get_property('filename/no-ext')
+  if not path or is_protocol(path) then
+    return
+  end
+  if path then
+    path = normalize(path)
+    dir = utils.split_path(path)
+  end
+
+  if o.output_path ~= 'source' then
+    subtitles_file = utils.join_path(o.output_path, fname .. '.srt')
+  else
+    subtitles_file = utils.join_path(dir, fname .. '.srt')
+  end
+
+  if file_exists(subtitles_file) then
+    msg.info('Subtitles file already exists: ' .. subtitles_file)
+    return
+  end
+
+  if not is_writable(subtitles_file) then
+    subtitles_file = utils.join_path(temp_path, fname .. '.srt')
+  end
+
+  mp.osd_message('Subtitle generation in progress', 9)
+  msg.info('Subtitle generation in progress')
+  msg.verbose('Subtitle file => ' .. subtitles_file)
+
+  whisper_running = true
+  process_video_incrementally(path, subtitles_file, o.segment_duration)
+  whisper_running = false
+
+  if file_exists(subtitles_file) then
+    mp.osd_message('Subtitles successfully generated', 5)
+    msg.info('Subtitles successfully generated')
+    append_sub(subtitles_file)
+  end
+end
+------------------------
+
+local function adjust_time_range(strat_time, end_time)
+  for _, range in ipairs(time_ranges) do
+    if not (end_time <= range.start or strat_time >= range.finish) then
+      if strat_time >= range.start and end_time <= range.finish then
+        return nil, nil
+      end
+      if strat_time < range.finish and end_time > range.start then
+        if strat_time < range.finish then
+          strat_time = range.finish
+        end
+        if end_time > range.start then
+          end_time = range.start
+        end
+      end
+    end
+  end
+  return strat_time, end_time
+end
+
+local function get_time_range(strat_time, end_time)
+  strat_time, end_time = adjust_time_range(strat_time, end_time)
+  if strat_time and strat_time < end_time then
+    return true, strat_time, end_time
+  else
+    return false
+  end
+end
+
+local function whisper_cache(current_pos, subtitle_count)
+  local temp_video_file = utils.join_path(temp_path, 'whisper-' .. pid .. '.mkv')
+  local srt_file = utils.join_path(temp_path, 'whisper.srt')
+  local file_duration = mp.get_property_number('duration')
+  local cache_state = mp.get_property_native('demuxer-cache-state')
+  local cache_ranges = cache_state and cache_state['seekable-ranges'] or {}
+  local cache_start = cache_ranges[1] and cache_ranges[1]['start'] or current_pos
+  local cache_end = cache_ranges[1] and cache_ranges[1]['end'] or current_pos
+
+  if current_pos < cache_start or cache_start < state.pos then
+    current_pos = cache_start
+    state.pos = current_pos
+  end
+
+  if current_pos >= file_duration then
+    if file_exists(srt_file) then
+      append_sub(srt_file)
+    end
+    return
+  end
+
+  local valid_range, strat_time, end_time = get_time_range(current_pos, cache_end)
+  if strat_time and end_time then
+    current_pos = strat_time
+    cache_end = end_time
+  end
+
+  if not valid_range or cache_end <= current_pos then
+    mp.add_timeout(1, function()
+      whisper_cache(current_pos, subtitle_count)
+    end)
+    return
+  end
+
+  if subtitle_count == 0 then
+    mp.osd_message('Subtitle generation in progress (from cache)', 9)
+    msg.info('Subtitle generation in progress (from cache)')
+    local files_to_remove = {
+      temp_srt_file1 = utils.join_path(temp_path, 'whisper.srt'),
+      temp_srt_file2 = utils.join_path(temp_path, 'whisper-' .. pid .. '.srt'),
+    }
+
+    for _, file in pairs(files_to_remove) do
+      if file_exists(file) then
+        os.remove(file)
+      end
+    end
+  end
+
+  whisper_running = true
+  mp.commandv('dump-cache', math.ceil(current_pos), math.floor(cache_end), temp_video_file)
+  local temp_srt = srt_file .. '-1.srt'
+  local success =
+    process_audio_segment(temp_video_file, temp_srt, current_pos, (cache_end - current_pos))
+  if not success then
+    msg.verbose('Segment processing completed or failed.')
+  end
+  whisper_running = false
+
+  if file_exists(temp_srt) then
+    subtitle_number = shift_subtitle_timestamps(temp_srt, srt_file, subtitle_number, current_pos)
+  end
+
+  if file_exists(srt_file) then
+    subtitle_count = subtitle_count + 1
+    append_sub(srt_file)
+  end
+
+  table.insert(time_ranges, { start = current_pos, finish = cache_end })
+  table.sort(time_ranges, function(a, b)
+    return a.start < b.start
+  end)
+
+  current_pos = cache_end
+
+  -- Callback
+  mp.add_timeout(1, function()
+    whisper_cache(current_pos, subtitle_count)
+  end)
+end
+------------------------
+
+local function whisper_main()
+  if whisper_running then
+    return
+  end
+  local path = mp.get_property_native('path')
+  local cache = mp.get_property_native('cache')
+  local cache_state = mp.get_property_native('demuxer-cache-state')
+  local cache_ranges = cache_state and cache_state['seekable-ranges'] or {}
+  if path and is_protocol(path) or cache == 'auto' and #cache_ranges > 0 then
+    time_ranges = {}
+    subtitle_count = 0
+    local current_pos = mp.get_property_native('time-pos')
+    local cache_start = cache_ranges[1]['start']
+    state.pos = cache_start or current_pos
+    whisper_cache(cache_start, subtitle_count)
+    return
+  end
+  whisper_segment()
+end
+
+-- mp.register_event('log-message', function(e)
+--   if e.prefix ~= mp.get_script_name() then
+--     return
+--   end
+--
+--   local file = subtitles_file and io.open(subtitles_file, 'a')
+--   if file and e.text and e.text ~= '' then
+--     local text_pattern = '%[([%d+:]?%d+:%d+%.%d+)%D+([%d+:]?%d+:%d+%.%d+)%]%s*(.*)'
+--     local start_time_srt, end_time_srt, subtitle_text = e.text:match(text_pattern)
+--     if start_time_srt and end_time_srt and subtitle_text then
+--       local start_time = format_time(start_time_srt)
+--       local end_time = format_time(end_time_srt)
+--
+--       file:write(subtitle_count .. '\n')
+--       file:write(start_time .. ' --> ' .. end_time .. '\n')
+--       file:write(subtitle_text .. '\n')
+--       file:close()
+--
+--       subtitle_count = subtitle_count + 1
+--       subtitles_written = true
+--     end
+--     if subtitle_count % o.update_interval == 1 and subtitles_written then
+--       if append_subtitle_count == 1 then
+--         mp.osd_message('Subtitles are loaded and updated in real time', 5)
+--         msg.info('Subtitles are loaded and updated in real time')
+--       end
+--       append_sub(subtitles_file)
+--       subtitles_written = false
+--       append_subtitle_count = append_subtitle_count + 1
+--     end
+--   end
+-- end)
+
+mp.add_hook('on_unload', 50, function()
+  start_index = 1
+  in_progress_batches = 0
+  time_ranges = nil
+  progress_cache = nil
+  collectgarbage()
+  time_ranges = {}
+  progress_cache = {}
+
+  temp_path = os.getenv('TEMP') or '/tmp/'
+  local path = mp.get_property('path')
+  local dir = utils.split_path(path)
+  local filename = mp.get_property('filename/no-ext')
+  local files_to_remove = {
+    temp_video_file = utils.join_path(temp_path, 'whisper-' .. pid .. '.mkv'),
+    temp_srt_file1 = utils.join_path(temp_path, 'whisper.srt'),
+    temp_srt_file2 = utils.join_path(temp_path, 'whisper-' .. pid .. '.srt'),
+  }
+
+  for _, file in pairs(files_to_remove) do
+    if file_exists(file) then
+      os.remove(file)
+    end
+  end
+
+  check_and_remove_empty_file(subtitles_file)
+end)
+
+mp.register_script_message('whisper/sub-whisper', whisper_main)
diff --git a/.config/mpv/scripts/yanky.lua b/.config/mpv/scripts/yanky.lua
new file mode 100644
index 0000000..be9d308
--- /dev/null
+++ b/.config/mpv/scripts/yanky.lua
@@ -0,0 +1,37 @@
+local mp = require("mp")
+
+function yank(payload)
+	mp.commandv("run", "wl-copy", payload)
+	mp.msg.info("yanked: " .. payload)
+	mp.osd_message("yanked: " .. payload)
+end
+
+function yankproperty(prop)
+	yank(mp.get_property_native(prop))
+end
+
+function yankpath()
+	local path = mp.get_property_native("path")
+	if path ~= "^http" then
+		yank(path)
+		return
+	end
+	local cwd = mp.get_property_native("working-directory")
+	yank(require("mp.utils").join_path(cwd, path))
+end
+
+function yanktime()
+	local s = mp.get_property_native("playback-time")
+	local ts = string.format("%.2d:%.2d:%.2d.%.3d", s / (60 * 60), s / 60 % 60, s % 60, (s % 1) * 1000)
+	yank(ts)
+end
+
+function yanksubs()
+	local subText = mp.get_property("sub-text")
+	-- local secondarySubText = mp.get_property("secondary-sub-text")
+	yank(subText)
+end
+
+mp.add_key_binding("y-t", "yanktime", yanktime)
+mp.add_key_binding("y-p", "yankpath", yankpath)
+mp.add_key_binding("y-s", "yanksubs", yanksubs)
diff --git a/.config/mpv/update-uosc.sh b/.config/mpv/update-uosc.sh
new file mode 100755
index 0000000..7334b58
--- /dev/null
+++ b/.config/mpv/update-uosc.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -e
+
+URI="
+	https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip
+	https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf
+"
+
+target="$(readlink -f -- "$(dirname "$0")")"
+tmp=$(mktemp -d)
+trap '{ rm -r $tmp; }' EXIT INT
+cd "$tmp" || exit 1
+
+for src in $URI; do
+	curl -sLO "$src"
+	{ unzip -qd "$tmp"/ "$tmp"/*.zip 2>/dev/null && rm "$tmp"/*.zip; } || true
+	{ tar -xf "$tmp"/*.tar* -C "$tmp"/ 2>/dev/null && rm "$tmp"/*.tar*; } || true
+done
+
+rsync -ac "$tmp"/ "$target"/
diff --git a/.config/mpv/vapoursynth/interpolate.vpy b/.config/mpv/vapoursynth/interpolate.vpy
new file mode 100644
index 0000000..414aa4d
--- /dev/null
+++ b/.config/mpv/vapoursynth/interpolate.vpy
@@ -0,0 +1,84 @@
+# vim: set ft=python:
+
+# see the README at https://gist.github.com/phiresky/4bfcfbbd05b3c2ed8645
+# source: https://github.com/mpv-player/mpv/issues/2149
+# source: https://github.com/mpv-player/mpv/issues/566
+# source: https://github.com/haasn/gentoo-conf/blob/nanodesu/home/nand/.mpv/filters/mvtools.vpy
+
+from vapoursynth import core
+
+# ref: http://avisynth.org.ru/mvtools/mvtools2.html#functions
+# default is 400, less means interpolation will only happen when it will work well
+ignore_threshold = 140
+# if n% of blocks change more than threshold then don't interpolate at all (default is 51%)
+scene_change_percentage = 15
+# default to 60fps
+dst_fps = 60
+realtime = True
+
+if "video_in" in globals():
+    # realtime (mpv)
+    clip = video_in
+    # Needed because clip FPS is missing
+    src_fps_num = int(container_fps * 1e8)
+    src_fps_den = int(1e8)
+    clip = core.std.AssumeFPS(clip, fpsnum=src_fps_num, fpsden=src_fps_den)
+    dst_fps = display_fps
+    # Interpolating to fps higher than 60 is too CPU-expensive, smoothmotion can handle the rest.
+    while (dst_fps > 60):
+        dst_fps /= 2
+else:
+    realtime = False
+    core.std.LoadPlugin('/usr/lib64/libffms2.so')
+    # run with vspipe
+    clip = core.ffms2.Source(source=video_in_path)
+    if "output_fps" in globals():
+        dst_fps = float(output_fps)
+
+# resolution in megapixels. 1080p ≈ 2MP, 720p ≈ 1MP
+mpix = clip.width * clip.height / 1000000
+
+# Skip interpolation for >1080p or 60 Hz content due to performance
+if not (realtime and (mpix > 2.5 or clip.fps_num / clip.fps_den > 59)):
+    analParams = {
+        'overlap': 0,
+        'search': 3,
+        'truemotion': True,
+        #'chrome': True,
+        #'blksize':16,
+        #'searchparam':1
+    }
+    blockParams = {
+        'thscd1': ignore_threshold,
+        'thscd2': int(scene_change_percentage * 255 / 100),
+        'mode': 3,
+    }
+
+    if realtime and mpix > 1.5:
+        # can't handle these on Full HD with Intel i5-2500k
+        # see the description of these parameters in http://avisynth.org.ru/mvtools/mvtools2.html#functions
+        analParams['search'] = 0
+        blockParams['mode'] = 0
+        quality = 'low'
+    else:
+        quality = 'high'
+
+    dst_fps_num = int(dst_fps * 1e4)
+    dst_fps_den = int(1e4)
+    print("Reflowing from {} fps to {} fps (quality={})".format(
+        clip.fps_num / clip.fps_den, dst_fps_num / dst_fps_den, quality))
+
+    sup = core.mv.Super(clip, pel=2)
+    bvec = core.mv.Analyse(sup, isb=True, **analParams)
+    fvec = core.mv.Analyse(sup, isb=False, **analParams)
+    clip = core.mv.BlockFPS(clip,
+                            sup,
+                            bvec,
+                            fvec,
+                            num=dst_fps_num,
+                            den=dst_fps_den,
+                            **blockParams)
+else:
+    print("Skipping interpolation")
+
+clip.set_output()