misc improvements
authorIan Kelling <ian@iankelling.org>
Tue, 8 Apr 2025 07:56:33 +0000 (03:56 -0400)
committerIan Kelling <ian@iankelling.org>
Tue, 8 Apr 2025 07:56:33 +0000 (03:56 -0400)
12 files changed:
brc
brc2
distro-begin
filesystem/etc/i3/config [deleted file]
filesystem/usr/local/bin/ikclip
fsf-script-lib
i3-sway/gen
pkgs
subdir_files/.config/mpv/input.conf
subdir_files/.config/mpv/mpv.conf
subdir_files/.config/mpv/scripts/iank-like.lua [new file with mode: 0644]
subdir_files/.config/mpv/scripts/playlistmanager.lua [new file with mode: 0644]

diff --git a/brc b/brc
index 59175d654e14b7f2ef42cae42f109dfa0490700b..da736c4c7845115ff6bc60d0c8fe745155812476 100644 (file)
--- a/brc
+++ b/brc
@@ -2507,8 +2507,8 @@ grep ps and output in a nice format"
   fi
 }
 
-pubip() { curl -4s https://icanhazip.com; }
-pubip6() { curl -6s https://icanhazip.com; }
+pubip() { wget -4 -q -O- https://icanhazip.com; }
+pubip6() { wget -6 -q -O- https://icanhazip.com; }
 whatismyip() { pubip; }
 
 
diff --git a/brc2 b/brc2
index 43df9e503b3e038f1c41687879cf0db6be3a41f9..c7500167f8bc0d7eebc537bd5ba916e5524e8862 100644 (file)
--- a/brc2
+++ b/brc2
@@ -2832,6 +2832,9 @@ mpvd() {
 mpva() {
   mpv --profile=a "$@";
 }
+mpvl() {
+  mpv --profile=l "$@";
+}
 # mpv for testing video quality, dont scale.
 mpvt() {
   mpv --video-unscaled "$@";
@@ -4744,12 +4747,64 @@ linediff() {
   meld <(printf "%s\n" "$l1") <(printf "%s\n" "$l2") &
 }
 
-# list count of files in directories
-wc-l-dirs() {
+# List count of files in directories.
+#
+# Note: you may want to add on | sort -n.
+ls-wc-files() {
+  local dir
+  for dir in "$@"; do
+    e "$(find -L $dir -maxdepth 1 -type f -printf x | wc -c)" "$dir"
+  done
+}
+
+ls-wc-dirs() {
   local dir
   for dir in "$@"; do
-    e "$(find $dir -maxdepth 1 -type f -printf x | wc -c)" "$dir"
-  done | sort -n
+    e "$(find -L $dir -maxdepth 1 -type d -not -path . -printf x | wc -c)" "$dir"
+  done
+}
+
+
+# usage: $0 [--debug] WEBP_FILES...
+#
+# output: array webp_frames = count of animation frames in each of
+# WEBP_FILES. 0 means the webp has no animation and is just a still
+# image.
+#
+# Example use:
+#
+#webp-anim-array *webp; fs=(*webp); mv -t ../stil2 $(for (( i=0; i < ${#webp_frames[@]}; i++ )); do if [[ ${webp_frames[$i]} == 0 ]]; then echo ${fs[$i]}; fi; done)
+#
+webp-anim-array() {
+  local debug=false tmps
+  local -a args
+  local -i i
+  if [[ $1 == --debug ]]; then
+    debug=true
+    shift
+  fi
+  args=( "$@" )
+  if (( $# <= 2 )); then
+    echo "error: something went wrong, \$#=$#. Only 2+ is useful."
+    return 1
+  fi
+  # note, we could save the file name in the webinfo output if our first 2 seds were s/^.* /0 /
+  tmps=$(webpinfo -summary "${args[@]}" | sed -rn '1{s/.*/0/;x};/^File:/{s/.*/0/;x;p};${x;p}; /^[[:space:]]*Number of frames:[[:space:]]+([2-9]|[1-9][0-9]+)$/{s/^.*[[:space:]]([0-9]+)$/\1/;G;s/\n0//;h}')
+  unset webp_frames
+  mapfile -t webp_frames <<<"$tmps"
+  if (( ${#webp_frames[@]} != $# )); then
+    echo "error: \${#webp_frames[@]}:${#webp_frames[@]} != \$#:$#"
+    return 1
+  fi
+  if $debug; then
+    for (( i=0; i < ${#webp_frames[@]}; i++ )); do
+      echo "${webp_frames[$i]} ${args[$i]}"
+    done | sort -n
+    for (( i=0; i < ${#webp_frames[@]}; i++ )); do
+      echo "${webp_frames[$i]}"
+    done | sort -n | uniq -c
+
+  fi
 }
 
 export BASEFILE_DIR=/a/bin/fai-basefiles
index 08abcd0f2f0ff1a98b1903b5dcc065ec9ff05dea..9e7e01f798b61cdc0877a2447dbc63454cb47704 100755 (executable)
@@ -652,6 +652,11 @@ if has_monitor; then
 
   ###### install X
   pi i3 python3-i3ipc
+  f=/etc/i3/config
+  if [[ -e $f && ! -w $f ]]; then
+    sudo chown iank:iank $f
+  fi
+
 
   ##### install xinput
   case $(distro-name) in
diff --git a/filesystem/etc/i3/config b/filesystem/etc/i3/config
deleted file mode 100644 (file)
index 55ebe22..0000000
+++ /dev/null
@@ -1,303 +0,0 @@
-####### DO NOT EDIT LIVE CONFIG. generated from /a/bin/distro-setup/i3-sway/gen #######
-
-# random thoughts: what to do with a window I don't have room for?
-# * I could tabify it
-# * I could split an existing window with it
-# * I could send it away to another workspace,
-# * I could resize it to be very small.
-
-
-# todo: think whether this is useful: https://github.com/tmfink/i3-wk-switch
-# todo: see comment by Jakstern551 here for tip about jumping to windows
-# https://old.reddit.com/r/i3wm/comments/k8m4k4/share_your_i3_tips_and_tricks_that_you_have/
-
-# https://i3wm.org/docs/userguide.html#keybindings
-#To get the current mapping of your keys, use xmodmap -pke. To
-#interactively enter a key and see what keysym it is configured to, use
-#xev.
-set $mod Mod4
-
-# for non-gui apps, use this.
-set $ex exec --no-startup-id
-
-bindsym $mod+2 $ex "i3-split-maybe"; exec "pavucontrol"
-# calling without --new-instance makes this to be the instance that links
-# will open in from other applications.
-# unused. todo: consider binding this to some key on the right side of keyboard.
-#bindsym $mod+3 $ex "i3-split-maybe"; exec "abrowser"
-# calling just abrowser mysteriously stopped working,
-# so I figured out this is how to get output, but then
-# it suddenly started working again.
-#bindsym $mod+3 exec "abrowser 2>&1 >/tmp/l"
-#bindsym $mod+3 exec "abrowser --new-instance -P sfw"
-bindsym $mod+4 $ex "i3-abrowser --new-instance -P firefox-main-profile"
-bindsym $mod+5 $ex "/a/bin/ds/stream-interlude"
-bindsym $mod+6 $ex "i3-split-maybe"; exec "/usr/local/bin/start-tor-browser"
-bindsym $mod+7 $ex "/a/bin/ds/myx"
-#bindsym $mod+6 $ex "/a/bin/redshift.sh"
-# bindsym $mod+equal $ex "t s w; t in"
-# bindsym $mod+Home $ex "t out"
-# #bindsym $mod+End $ex "t s x; t in"
-# bindsym $mod+grave $ex "t s lunch; t in; t out -a '45 minutes from now'"
-
-
-bindsym $mod+1 focus parent
-bindsym $mod+shift+1 focus child
-
-# note, i used to have a key: "floating toggle; floating toggle" to
-# as undo split, as suggested here https://github.com/i3/i3/issues/3808
-# but something
-#
-bindsym $mod+grave floating toggle
-bindsym $mod+equal $ex "i3-set-layout splith"
-# move firefox to current workspace.
-# https://i3wm.org/docs/userguide.html#keybindings
-# get class with xprop, example output
-# WM_CLASS(STRING) = "irssi", "URxvt"
-# xprop |& grep WM_CLASS
-bindsym $mod+w $ex "i3-abrowser"
-bindsym $mod+shift+w fullscreen toggle
-
-bindsym $mod+e $ex "i3-emacs"
-# unused
-#bindsym $mod+shift+e
-bindsym $mod+r $ex "/a/bin/ds/xl"
-
-bindsym $mod+backslash $ex "scrot"
-
-bindsym $mod+t $ex "i3-set-layout splitv"
-
-bindsym $mod+g $ex "i3-set-layout tabbed"
-
-
-# Use Mouse+$mod to drag floating windows to their wanted position
-floating_modifier $mod
-
-bindsym $mod+u focus left; $ex "i3-mouse-warp"
-# i dont expect to use this much
-bindsym $mod+shift+u $ex "i3-auto-layout-toggle"
-bindsym $mod+i focus right; $ex "i3-mouse-warp"
-bindsym $mod+o focus up; $ex "i3-mouse-warp"
-bindsym $mod+p focus down; $ex "i3-mouse-warp"
-
-bindsym $mod+Left $ex "i3-split-push left"
-bindsym $mod+Right $ex "i3-split-push right"
-bindsym $mod+Up $ex "i3-split-push up"
-bindsym $mod+Down $ex "i3-split-push down"
-
-# for testing in case there is a problem with above.
-# these could be rebound to other things.
-bindsym $mod+shift+Left move left
-bindsym $mod+shift+Right move right
-bindsym $mod+shift+Up move up
-bindsym $mod+shift+Down move down
-
-bindsym $mod+Shift+a move container to workspace 1
-bindsym $mod+a workspace 1
-
-
-bindsym $mod+Shift+s move container to workspace 4
-bindsym $mod+s workspace 4
-
-bindsym $mod+Shift+d move container to workspace 3
-bindsym $mod+d workspace 3
-
-bindsym $mod+Shift+f move container to workspace 2
-bindsym $mod+f workspace 2
-
-bindsym $mod+Shift+z move container to workspace 5
-bindsym $mod+z workspace 5
-
-bindsym $mod+Shift+x move container to workspace 6
-bindsym $mod+x workspace 6
-
-bindsym $mod+v split vertical
-bindsym $mod+Shift+v move workspace to output MON-LEFT
-
-# 122 = XF86AudioLowerVolume, keyboardio function + t
-bindcode 122 $ex "toggle-mute unmute"; mode "ptt"
-mode "ptt" {
-# normally, if we hold down a button, it will start automatically
-# repeating itself, up and down events.  But this stops that from
-# happening. Based on testing, making mode be 1st or 2nd doesn't matter.
-bindcode --release 122 $ex "toggle-mute mute"; mode "default"
-}
-# 171 = XF86AudioNext, keyboardio function + g
-bindcode 171 $ex "toggle-mute unmute"
-
-## temp for testing, add antying here
-#bindsym $mod+shift+5 $ex "/a/a.sh"
-
-
-bindsym $mod+b $ex "i3-konsole"
-bindsym $mod+shift+b unmark term; mark term
-
-
-# for use to cleanup extra emacs windows
-# https://faq.i3wm.org/question/7662/reverse-perl-matches-in-criteria-in-i3-config.1.html
-# I found their regex slightly wrong. This is a hacky way to
-# ignore my irc emacs instances, their window titles
-# are irc room names. Another way would be to hack on the
-# window title, or xprop stuff, but I figure I'm switching
-# to wayland soon, lets wait and see how things work there.
-bindsym $mod+shift+6 [class="Emacs" title="^(?!#[a-zA-Z][a-zA-Z-]*$)"] move workspace current
-
-bindsym $mod+c kill
-
-bindsym $mod+Home split horizontal
-bindsym $mod+Shift+End move container to workspace 7
-bindsym $mod+End workspace 7
-bindsym $mod+Shift+q move container to workspace 8
-bindsym $mod+q workspace 8
-
-bindsym $mod+Shift+8 move container to workspace 9
-bindsym $mod+8 workspace 9
-bindsym $mod+Shift+9 move container to workspace 10
-bindsym $mod+9 workspace 10
-
-bindsym $mod+m $ex "dunstctl close-all"
-bindsym $mod+Shift+m border toggle
-
-# 65 = space.
-# toggle tiling / floating.
-#
-# The idea here is: when floating a window, make it sticky and 1080p,
-# because the only reason we want to do this is to keep it on screen
-# when doing an obs broacast. When unfloating a window, just act as a
-# normal unfloat. There is a quirk with this: in a layout with 3 windows,
-# 2 stacked, 1 tall, floating and ufloating the tall one will make it
-# another stacked one, but still 1920x1080, you need to move it to the
-# right to get it back into its tall spot. I could automate this,
-# but I'm not bothering right now
-bindcode $mod+65 $ex obs-auto-scene-switch-toggle; floating toggle; sticky enable; resize set 1920 1080; move position 100 ppt 0 ppt
-
-# change focus between tiling / floating windows
-bindcode $mod+shift+65 focus mode_toggle
-
-bindsym $mod+shift+h $ex /b/ds/stream-clip hc
-bindsym $mod+j $ex "i3-split-maybe"; exec emacsclient -c
-bindsym $mod+shift+j $ex /b/ds/stream-clip up
-bindsym $mod+k $ex "i3-split-maybe"; exec konsole
-bindsym $mod+shift+k $ex /b/ds/stream-clip intro
-bindsym $mod+l $ex dmenu_run
-bindsym $mod+shift+l $ex /b/ds/stream-clip steady
-bindsym $mod+shift+semicolon $ex /b/ds/stream-clip sad
-# note default is 27% on my system76. not sure if these
-# keybinds will screw up other laptop brightness keys.
-bindsym XF86MonBrightnessUp $ex brightnessctl s +5%
-bindsym XF86MonBrightnessDown $ex brightnessctl s 5%-
-for_window [class="copyq" instance="copyq" window_type="normal"] floating enable
-# eh, dont really like web page titles + a long browser name string.
-for_window [class="firefox" instance="Navigator" window_role="browser"] title_format "b"
-bindsym $mod+y $ex copyq-restart
-bindsym $mod+shift+y $ex "i3-chat"
-
-# unused
-#bindsym $mod+shift+F1
-
-
-# Font for window titles. Will also be used by the bar unless a different font
-# is used in the bar {} block below.
-font pango:monospace 7
-
-# not helpful when i have split screen from myx
-#hide_edge_borders vertical
-
-#exec --no-startup-id /usr/lib/x86_64-linux-gnu/libexec/kdeconnectd
-
-# Start clipster daemon
-#exec --no-startup-id /a/opt/clipster/clipster -d
-
-# title bars but no borders. i tried this out a bit
-#default_border normal 0
-
-# default border is like 2 pixels
-default_border pixel 2
-# for debugging
-#default_border normal 10
-
-# I dont see a way to make processing windows act like normal windows,
-# this does it.
-# https://unix.stackexchange.com/questions/450700/opening-a-programme-in-a-floating-window-in-i3
-#
-# This is the info for a processing window launched from the ide.
-# I'm not sure I want it like this, so commenting it out for now.
-#for_window [class="processing-core-PApplet" instance="processing-core-PApplet"] floating disable
-
-# this is the processing window for my app named focus.
-for_window [class="focus" instance="focus"] floating disable
-
-client.focused          #4c7899 #285577 #ffffff #2e9ef4   #ff4400
-client.focused_inactive #333333 #5f676a #ffffff #484e50   #B8C8CD
-client.unfocused        #333333 #222222 #888888 #292d2e   #B8C8CD
-# exit i3 (logs you out of your X session)
-bindsym $mod+Shift+o exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'"
-
-bindsym $mod+Shift+i $ex /b/ds/i3-sway/gen
-bindsym $mod+Shift+p restart
-
-
-$ex copyq
-$ex dunst
-# haven't been using it enough to justify automatically running it.]
-#$ex /usr/lib/x86_64-linux-gnu/libexec/kdeconnectd
-$ex /usr/local/bin/awatch
-# this dies when we restart i3.
-exec_always --no-startup-id i3-event-hook
-workspace 1 output
-workspace 2 output
-workspace 3 output
-workspace 4 output
-workspace 5 output
-workspace 6 output
-workspace 7 output
-workspace 8 output
-workspace 9 output
-workspace 10 output
-workspace 11 output
-# by default, new workspaces are created on whatever screen doesn't have
-# one active or else the current one. That is annoying, I have one
-# primary monitor, I don't want a new workspace created on secondary
-# monitor just because I happen be focused on it. This fixes that.
-workspace 1 output primary
-workspace 2 output MON-LEFT
-workspace 3 output MON-RIGHT
-workspace 4 output MON-RIGHT
-workspace 5 output MON-RIGHT
-workspace 6 output MON-RIGHT
-workspace 7 output MON-RIGHT
-workspace 8 output MON-RIGHT
-workspace 9 output MON-RIGHT
-workspace 10 output MON-RIGHT
-
-default_orientation vertical
-
-# bar is needed for kde connect
-bar {
-
-# keep it only on secondary monitor to save space and make for less
-# missing pixes in obs live stream. For docs on this, search "output
-# primary" in the i3 guide.
-output primary
-
-# the builtin prog
-#status_command i3status
-
-#for faster testing
-#status_command          /a/bin/ds/filesystem/usr/local/bin/myi3status
-status_command          /usr/local/bin/myi3status
-# note: old wip command: /p/c/myi3life
-
-
-font pango:monospace 18
-
-tray_output primary
-
-# I found I didn't need these.
-# workspace_buttons no
-
-# note /a/c/myx adds the last bit to this file conditionally.
-mode hide
-hidden_state hide
-}
-bindsym $mod+Shift+t move workspace to output
index 2868e6154b08c75f0f05c35c7712c64e51868373..50d428d4e877c0e514ab679998ea490c76ff1c89 100755 (executable)
@@ -20,6 +20,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+printf "%s" "$*" >> /tmp/ikclip.log
 # both!
 printf "%s" "$*" | xclip
 printf "%s" "$*" | xclip -selection clipboard
index b4767d4504be61f80add7c9537c24f971f908f46..7fa83184bd50fcd6875cde91347511c1e9628bc1 100644 (file)
@@ -123,6 +123,17 @@ slog() {
 # echo COMMAND then run it.
 m() { printf "+ %s\n" "$*" >&2;  "$@"; }
 
+# usage: mq COMMAND...
+#
+# echo COMMAND if verbose=true.
+# Then run it. Like m, but more quietly.
+mq() {
+  # shellcheck disable=SC2154
+  if [[ $verbose == true ]]; then
+    printf "+ %s\n" "$*" >&2
+  fi
+  "$@"
+}
 
 # mb, maybe. echo args if they fail.
 mb() {
index 67004fa59f34002f8a14f726e819b17f62c9c72c..a3e509a7fe2990f0b7ebb38fe499f39df12598ee 100755 (executable)
@@ -41,13 +41,12 @@ cat common.conf sway.conf > $dir/config
 # instead.
 rm -f ~/.config/i3/config
 
-dir=/a/bin/distro-setup/filesystem/etc/i3
-mkdir -p $dir
-cat common.conf i3.conf > $dir/config
-if [[ -s ~/i3-myx.conf ]]; then
-  cat ~/i3-myx.conf >>$dir/config
+f=/etc/i3/config
+if [[ -e /etc/i3 && ! -w /etc/i3 ]]; then
+  sudo chown -R iank:iank /etc/i3
 fi
-conflink -f
+cat common.conf i3.conf $(if [[ -s ~/i3-myx.conf ]]; then echo ~/i3-myx.conf; fi) >$f
+
 if [[ $I3SOCK ]]; then
   echo $0: i3-msg $i3_cmd
   i3-msg $i3_cmd
diff --git a/pkgs b/pkgs
index d088afef9d648aa78c19c78d0d708a91bd39841a..9e00cb443ea81853393de532361e602ebaa1a6d6 100644 (file)
--- a/pkgs
+++ b/pkgs
@@ -253,6 +253,7 @@ p3=(
   oathtool
   opendkim-tools
   p7zip-full
+  parallel
   parted
   parted-doc
   pass
@@ -319,6 +320,7 @@ p3=(
   # vlc stdout complains that it doesn't find a file from this package.
   libvdpau-va-gl1
   wamerican-huge
+  webp
   wireless-tools
   w3m
   whois
index 0bee5f0d70666ba809bf6821ae417a153b444dd8..55597599a2e7ff419f7ef418d34fd2175611d7ef 100644 (file)
@@ -1,3 +1,9 @@
 a cycle_values video-rotate "90" "180" "270" "0"
 Alt+h add video-zoom 0.25
 Alt+g add video-zoom -0.25
+
+# Skip to the next file
+b playlist-next
+
+# Skip to the previous file
+c playlist-prev
index c84a33255bca4d3005300ac466a0081464bc0a9c..f91c63d0323a2515133ca9ec72cfb80bec714462 100644 (file)
@@ -14,6 +14,19 @@ replaygain=track
 # the /etc one.
 hwdec=vdpau
 
+# loop. saves playlist position. good for looking at gifs
+[l]
+loop-file=inf
+save-position-on-quit
+#no-resume-playback
+#no-save-position-on-quit
+
+[m]
+loop-file=inf
+save-position-on-quit
+shuffle
+
+
 # use --profile d
 [d]
 loop-file=inf
diff --git a/subdir_files/.config/mpv/scripts/iank-like.lua b/subdir_files/.config/mpv/scripts/iank-like.lua
new file mode 100644 (file)
index 0000000..2796de8
--- /dev/null
@@ -0,0 +1,16 @@
+local log_file = "/t/mpvlike.log"
+
+mp.add_key_binding("v", "write_filename", function()
+    local filepath = mp.get_property("path")
+    -- Convert to absolute path if it's not already
+    if filepath and not filepath:match("^/") and not filepath:match("^%a:") then
+        local working_dir = mp.get_property("working-directory")
+        if working_dir then
+            filepath = working_dir .. "/" .. filepath
+        end
+
+        local file = io.open(log_file, "a") -- Open the file in append mode
+        file:write(filepath .. "\n") -- Write the filename with a newline
+        file:close()
+    end
+end)
diff --git a/subdir_files/.config/mpv/scripts/playlistmanager.lua b/subdir_files/.config/mpv/scripts/playlistmanager.lua
new file mode 100644 (file)
index 0000000..8908986
--- /dev/null
@@ -0,0 +1,1706 @@
+-- iank wget https://raw.githubusercontent.com/jonniek/mpv-playlistmanager/16e18949e3d604c2ffe43e95391f420227881139/playlistmanager.lua
+local settings = {
+  --navigation keybindings force override only while playlist is visible
+  --if "no" then you can display the playlist by any of the navigation keys
+  dynamic_binds = true,
+
+  -- to bind multiple keys separate them by a space
+
+  -- main key to show playlist
+  key_showplaylist = "SHIFT+ENTER",
+
+  -- display playlist while key is held down
+  key_peek_at_playlist = "",
+
+  -- dynamic keys
+  key_moveup = "UP",
+  key_movedown = "DOWN",
+  key_movepageup = "PGUP",
+  key_movepagedown = "PGDWN",
+  key_movebegin = "HOME",
+  key_moveend = "END",
+  key_selectfile = "RIGHT LEFT",
+  key_unselectfile = "",
+  key_playfile = "ENTER",
+  key_removefile = "BS",
+  key_closeplaylist = "ESC SHIFT+ENTER",
+
+  -- extra functionality keys
+  key_sortplaylist = "",
+  key_shuffleplaylist = "",
+  key_reverseplaylist = "",
+  key_loadfiles = "",
+  key_saveplaylist = "",
+
+  --replaces matches on filenames based on extension, put as empty string to not replace anything
+  --replace rules are executed in provided order
+  --replace rule key is the pattern and value is the replace value
+  --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial
+  --'all' will match any extension or protocol if it has one
+  --uses json and parses it into a lua table to be able to support .conf file
+
+  filename_replace = [[
+    [
+      {
+        "protocol": { "all": true },
+        "rules": [
+          { "%%(%x%x)": "hex_to_char" }
+        ]
+      }
+    ]
+  ]],
+
+--[=====[ START OF SAMPLE REPLACE - Remove this line to use it
+  --Sample replace: replaces underscore to space on all files
+  --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space
+  filename_replace = [[
+    [
+      {
+        "ext": { "all": true},
+        "rules": [
+          { "_" : " " }
+        ]
+      },{
+        "ext": { "mp4": true, "mkv": true },
+        "rules": [
+          { "^(.+)%..+$": "%1" },
+          { "%s*[%[%(].-[%]%)]%s*": "" },
+          { "(%w)%.(%w)": "%1 %2" }
+        ]
+      },{
+        "protocol": { "http": true, "https": true },
+        "rules": [
+          { "^%a+://w*%.?": "" }
+        ]
+      }
+    ]
+  ]],
+--END OF SAMPLE REPLACE ]=====]
+
+  --json array of filetypes to search from directory
+  loadfiles_filetypes = [[
+    [
+      "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp",
+      "mp3", "wav", "ogm", "flac", "m4a", "wma", "ogg", "opus",
+      "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp"
+    ]
+  ]],
+
+  --loadfiles at startup if 1 or more items in playlist
+  loadfiles_on_start = false,
+  -- loadfiles from working directory on idle startup
+  loadfiles_on_idle_start = false,
+  --always put loaded files after currently playing file
+  loadfiles_always_append = false,
+
+  --sort playlist when files are added to playlist
+  sortplaylist_on_file_add = false,
+
+  --default sorting method, must be one of: "name-asc", "name-desc", "date-asc", "date-desc", "size-asc", "size-desc".
+  default_sort = "name-asc",
+
+  --"linux | windows | auto"
+  system = "auto",
+
+  --Use ~ for home directory. Leave as empty to use mpv/playlists
+  playlist_savepath = "",
+
+  -- constant filename to save playlist as. Note that it will override existing playlist. Leave empty for generated name.
+  playlist_save_filename = "",
+
+  --save playlist automatically after current file was unloaded
+  save_playlist_on_file_end = false,
+
+
+  --show file title every time a new file is loaded
+  show_title_on_file_load = false,
+  --show playlist every time a new file is loaded
+  show_playlist_on_file_load = false,
+  --close playlist when selecting file to play
+  close_playlist_on_playfile = false,
+
+  --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.)
+  --has the sideeffect of moving cursor if file happens to change when navigating
+  --good side is cursor always following current file when going back and forth files with playlist-next/prev
+  sync_cursor_on_load = true,
+
+  --allow the playlist cursor to loop from end to start and vice versa
+  loop_cursor = true,
+
+
+  -- allow playlistmanager to write watch later config when navigating between files
+  allow_write_watch_later_config = true,
+
+  -- reset cursor navigation when closing or opening playlist
+  reset_cursor_on_close = true,
+  reset_cursor_on_open = true,
+
+  --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename.
+  prefer_titles = "url",
+
+  --youtube-dl executable for title resolving if enabled, probably "youtube-dl" or "yt-dlp", can be absolute path
+  youtube_dl_executable = "yt-dlp",
+
+  --call youtube-dl to resolve the titles of urls in the playlist
+  resolve_url_titles = false,
+
+  --call ffprobe to resolve the titles of local files in the playlist (if they exist in the metadata)
+  resolve_local_titles = false,
+
+  -- timeout in seconds for url title resolving
+  resolve_title_timeout = 15,
+
+  -- how many url titles can be resolved at a time. Higher number might lead to stutters.
+  concurrent_title_resolve_limit = 10,
+
+  --osd timeout on inactivity in seconds, use 0 for no timeout
+  playlist_display_timeout = 0,
+
+  -- when peeking at playlist, show playlist at the very least for display timeout
+  peek_respect_display_timeout = false,
+
+  -- the maximum amount of lines playlist will render. -1 will automatically calculate lines.
+  showamount = -1,
+
+  --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
+  --example {\\q2\\an7\\fnUbuntu\\fs10\\b0\\bord1} equals: line-wrap=no, align=top left, font=Ubuntu, size=10, bold=no, border=1
+  --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
+  --undeclared tags will use default osd settings
+  --these styles will be used for the whole playlist
+  --\\q2 style is recommended since filename wrapping may lead to unexpected rendering
+  --\\an7 style is recommended to align to top left otherwise, osd-align-x/y is respected
+  style_ass_tags = "{\\q2\\an7}",
+  --paddings for left right and top bottom
+  text_padding_x = 30,
+  text_padding_y = 60,
+
+  --screen dim when menu is open 0.0 - 1.0 (0 is no dim, 1 is black)
+  curtain_opacity=0.0,
+
+  --set title of window with stripped name
+  set_title_stripped = false,
+  title_prefix = "",
+  title_suffix = " - mpv",
+
+  --slice long filenames, and how many chars to show
+  slice_longfilenames = false,
+  slice_longfilenames_amount = 70,
+
+  --Playlist header template
+  --%mediatitle or %filename = title or name of playing file
+  --%pos = position of playing file
+  --%cursor = position of navigation
+  --%plen = playlist length
+  --%N = newline
+  playlist_header = "[%cursor/%plen]",
+
+  --Playlist file templates
+  --%pos = position of file with leading zeros
+  --%name = title or name of file
+  --%N = newline
+  --you can also use the ass tags mentioned above. For example:
+  --  selected_file="{\\c&HFF00FF&}➔ %name"   | to add a color for selected file. However, if you
+  --  use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20)
+  normal_file = "○ %name",
+  hovered_file = "● %name",
+  selected_file = "➔ %name",
+  playing_file = "▷ %name",
+  playing_hovered_file = "▶ %name",
+  playing_selected_file = "➤ %name",
+
+
+  -- what to show when playlist is truncated
+  playlist_sliced_prefix = "...",
+  playlist_sliced_suffix = "...",
+
+  --output visual feedback to OSD for tasks
+  display_osd_feedback = true,
+}
+local opts = require("mp.options")
+opts.read_options(settings, "playlistmanager", function(list) update_opts(list) end)
+
+local utils = require("mp.utils")
+local msg = require("mp.msg")
+local assdraw = require("mp.assdraw")
+
+local alignment_table = {
+    [1] = { ["x"] = "left",   ["y"] = "bottom" },
+    [2] = { ["x"] = "center", ["y"] = "bottom" },
+    [3] = { ["x"] = "right",  ["y"] = "bottom" },
+    [4] = { ["x"] = "left",   ["y"] = "center" },
+    [5] = { ["x"] = "center", ["y"] = "center" },
+    [6] = { ["x"] = "right",  ["y"] = "center" },
+    [7] = { ["x"] = "left",   ["y"] = "top" },
+    [8] = { ["x"] = "center", ["y"] = "top" },
+    [9] = { ["x"] = "right",  ["y"] = "top" },
+}
+
+--check os
+if settings.system=="auto" then
+  local o = {}
+  if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
+    settings.system = "windows"
+  else
+    settings.system = "linux"
+  end
+end
+
+-- auto calculate showamount
+if settings.showamount == -1 then
+  -- same as draw_playlist() height
+  local h = 720
+
+  local playlist_h = h
+  -- both top and bottom with same padding
+  playlist_h = playlist_h - settings.text_padding_y * 2
+
+  -- osd-font-size is based on 720p height
+  -- see https://mpv.io/manual/stable/#options-osd-font-size
+  -- details in https://mpv.io/manual/stable/#options-sub-font-size
+  -- draw_playlist() is based on 720p, need some conversion
+  local fs = mp.get_property_native('osd-font-size') * h / 720
+  -- get the ass font size
+  if settings.style_ass_tags ~= nil then
+    local ass_fs_tag = settings.style_ass_tags:match('\\fs%d+')
+    if ass_fs_tag ~= nil then
+      fs = tonumber(ass_fs_tag:match('%d+'))
+    end
+  end
+
+  settings.showamount = math.floor(playlist_h / fs)
+
+  -- exclude the header line
+  if settings.playlist_header ~= "" then
+    settings.showamount = settings.showamount - 1
+    -- probably some newlines (%N or \N) in the header
+    for _ in settings.playlist_header:gmatch('%%N') do
+      settings.showamount = settings.showamount - 1
+    end
+    for _ in settings.playlist_header:gmatch('\\N') do
+      settings.showamount = settings.showamount - 1
+    end
+  end
+
+  msg.info('auto showamount: ' .. settings.showamount)
+end
+
+--global variables
+local playlist_overlay = mp.create_osd_overlay("ass-events")
+local playlist_visible = false
+local strippedname = nil
+local path = nil
+local directory = nil
+local filename = nil
+local pos = 0
+local plen = 0
+local cursor = 0
+--table for saved media titles for later if we prefer them
+local title_table = {}
+-- table for urls and local file paths that we have requested to be resolved to titles
+local requested_titles = {}
+
+local filetype_lookup = {}
+
+function refresh_UI()
+  if not playlist_visible then return end
+  refresh_globals()
+  if plen == 0 then return end
+  draw_playlist()
+end
+
+function update_opts(changelog)
+  msg.verbose('updating options')
+
+  --parse filename json
+  if changelog.filename_replace then
+    if(settings.filename_replace~="") then
+      settings.filename_replace = utils.parse_json(settings.filename_replace)
+    else
+      settings.filename_replace = false
+    end
+  end
+
+  --parse loadfiles json
+  if changelog.loadfiles_filetypes then
+    settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes)
+
+    filetype_lookup = {}
+    --create loadfiles set
+    for _, ext in ipairs(settings.loadfiles_filetypes) do
+      filetype_lookup[ext] = true
+    end
+  end
+
+  if changelog.resolve_url_titles then
+    resolve_titles()
+  end
+
+  if changelog.resolve_local_titles then
+    resolve_titles()
+  end
+
+  if changelog.playlist_display_timeout then
+    keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+    keybindstimer:kill()
+  end
+
+  refresh_UI()
+end
+
+update_opts({filename_replace = true, loadfiles_filetypes = true})
+
+----- winapi start -----
+-- in windows system, we can use the sorting function provided by the win32 API
+-- see https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-strcmplogicalw
+local winapisort = nil
+if settings.system == "windows" then
+  -- ffiok is false usually means the mpv builds without luajit
+  local ffiok, ffi = pcall(require, "ffi")
+  if ffiok then
+    ffi.cdef[[
+      int MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
+      int StrCmpLogicalW(const wchar_t * psz1, const wchar_t * psz2);
+    ]]
+
+    local shlwapi = ffi.load("shlwapi.dll")
+
+    function MultiByteToWideChar(MultiByteStr)
+      local UTF8_CODEPAGE = 65001
+      if MultiByteStr then
+        local utf16_len = ffi.C.MultiByteToWideChar(UTF8_CODEPAGE, 0, MultiByteStr, -1, nil, 0)
+        if utf16_len > 0 then
+          local utf16_str = ffi.new("wchar_t[?]", utf16_len)
+          if ffi.C.MultiByteToWideChar(UTF8_CODEPAGE, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
+            return utf16_str
+          end
+        end
+      end
+      return ""
+    end
+
+    winapisort = function (a, b)
+      return shlwapi.StrCmpLogicalW(MultiByteToWideChar(a), MultiByteToWideChar(b)) < 0
+    end
+
+  end
+end
+----- winapi end -----
+
+local sort_modes = {
+  {
+    id="name-asc",
+    title="name ascending",
+    sort_fn=function (a, b, playlist)
+      if winapisort ~= nil then
+        return winapisort(playlist[a].string, playlist[b].string)
+      end
+      return alphanumsort(playlist[a].string, playlist[b].string)
+    end,
+  },
+  {
+    id="name-desc",
+    title="name descending",
+    sort_fn=function (a, b, playlist)
+      if winapisort ~= nil then
+        return winapisort(playlist[b].string, playlist[a].string)
+      end
+      return alphanumsort(playlist[b].string, playlist[a].string)
+    end,
+  },
+  {
+    id="date-asc",
+    title="date ascending",
+    sort_fn=function (a, b)
+      return (get_file_info(a).mtime or 0) < (get_file_info(b).mtime or 0)
+    end,
+  },
+  {
+    id="date-desc",
+    title="date descending",
+    sort_fn=function (a, b)
+      return (get_file_info(a).mtime or 0) > (get_file_info(b).mtime or 0)
+    end,
+  },
+  {
+    id="size-asc",
+    title="size ascending",
+    sort_fn=function (a, b)
+      return (get_file_info(a).size or 0) < (get_file_info(b).size or 0)
+    end,
+  },
+  {
+    id="size-desc",
+    title="size descending",
+    sort_fn=function (a, b)
+      return (get_file_info(a).size or 0) > (get_file_info(b).size or 0)
+    end,
+  },
+}
+
+local sort_mode = 1
+for mode, sort_data in pairs(sort_modes) do
+  if sort_data.id == settings.default_sort then
+    sort_mode = mode
+  end
+end
+
+function is_protocol(path)
+  return type(path) == 'string' and path:match('^%a[%a%d-_]+://') ~= nil
+end
+
+function on_file_loaded()
+  refresh_globals()
+  if settings.sync_cursor_on_load then cursor=pos end
+  refresh_UI() -- refresh only after moving cursor
+
+  filename = mp.get_property("filename")
+  path = mp.get_property('path')
+  local media_title = mp.get_property("media-title")
+  if is_protocol(path) and not title_table[path] and path ~= media_title then
+    title_table[path] = media_title
+  end
+
+  strippedname = stripfilename(mp.get_property('media-title'))
+  if settings.show_title_on_file_load then
+    mp.commandv('show-text', strippedname)
+  end
+  if settings.show_playlist_on_file_load then
+    showplaylist()
+  end
+  if settings.set_title_stripped then
+    mp.set_property("title", settings.title_prefix..strippedname..settings.title_suffix)
+  end
+end
+
+function on_start_file()
+  refresh_globals()
+  filename = mp.get_property("filename")
+  path = mp.get_property('path')
+  --if not a url then join path with working directory
+  if not is_protocol(path) then
+    path = utils.join_path(mp.get_property('working-directory'), path)
+    directory = utils.split_path(path)
+  else
+    directory = nil
+  end
+
+  if settings.loadfiles_on_start and plen == 1 then
+    local ext = filename:match("%.([^%.]+)$")
+    -- a directory or playlist has been loaded, let's not do anything as mpv will expand it into files
+    if ext and filetype_lookup[ext:lower()] then
+      msg.info("Loading files from playing files directory")
+      playlist()
+    end
+  end
+end
+
+function on_end_file()
+  if settings.save_playlist_on_file_end then save_playlist() end
+  strippedname = nil
+  path = nil
+  directory = nil
+  filename = nil
+end
+
+function refresh_globals()
+  pos = mp.get_property_number('playlist-pos', 0)
+  plen = mp.get_property_number('playlist-count', 0)
+end
+
+function escapepath(dir, escapechar)
+  return string.gsub(dir, escapechar, '\\'..escapechar)
+end
+
+function replace_table_has_value(value, valid_values)
+  if value == nil or valid_values == nil then
+    return false
+  end
+  return valid_values['all'] or valid_values[value]
+end
+
+local filename_replace_functions = {
+  --decode special characters in url
+  hex_to_char = function(x) return string.char(tonumber(x, 16)) end
+}
+
+-- from http://lua-users.org/wiki/LuaUnicode
+local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'
+
+-- return a substring based on utf8 characters
+-- like string.sub, but negative index is not supported
+local function utf8_sub(s, i, j)
+  if i > j then
+    return s
+  end
+
+  local t = {}
+  local idx = 1
+  for char in s:gmatch(UTF8_PATTERN) do
+    if i <= idx and idx <= j then
+      local width = #char > 2 and 2 or 1
+      idx = idx + width
+      t[#t + 1] = char
+    end
+  end
+  return table.concat(t)
+end
+
+--strip a filename based on its extension or protocol according to rules in settings
+function stripfilename(pathfile, media_title)
+  if pathfile == nil then return '' end
+  local ext = pathfile:match("%.([^%.]+)$")
+  local protocol = pathfile:match("^(%a%a+)://")
+  if not ext then ext = "" end
+  local tmp = pathfile
+  if settings.filename_replace and not media_title then
+    for k,v in ipairs(settings.filename_replace) do
+      if replace_table_has_value(ext, v['ext']) or replace_table_has_value(protocol, v['protocol']) then
+        for ruleindex, indexrules in ipairs(v['rules']) do
+          for rule, override in pairs(indexrules) do
+            override = filename_replace_functions[override] or override
+            tmp = tmp:gsub(rule, override)
+          end
+        end
+      end
+    end
+  end
+  local tmp_clip = utf8_sub(tmp, 1, settings.slice_longfilenames_amount)
+  if settings.slice_longfilenames and tmp ~= tmp_clip then
+    tmp = tmp_clip .. "..."
+  end
+  return tmp
+end
+
+--gets the file info of an item
+function get_file_info(item)
+  local path = mp.get_property('playlist/' .. item - 1 .. '/filename')
+  if is_protocol(path) then return {} end
+  local file_info = utils.file_info(path)
+  if not file_info then
+    msg.warn('failed to read file info for', path)
+    return {}
+  end
+
+  return file_info
+end
+
+--gets a nicename of playlist entry at 0-based position i
+function get_name_from_index(i, notitle)
+  refresh_globals()
+  if plen <= i then msg.error("no index in playlist", i, "length", plen); return nil end
+  local _, name = nil
+  local title = mp.get_property('playlist/'..i..'/title')
+  local name = mp.get_property('playlist/'..i..'/filename')
+
+  local should_use_title = settings.prefer_titles == 'all' or is_protocol(name) and settings.prefer_titles == 'url'
+
+  --check if file has a media title stored
+  if not title and should_use_title and title_table[name] then
+    title = title_table[name]
+  end
+
+  --if we have media title use a more conservative strip
+  if title and not notitle and should_use_title then
+    -- Escape a string for verbatim display on the OSD
+    -- Ref: https://github.com/mpv-player/mpv/blob/94677723624fb84756e65c8f1377956667244bc9/player/lua/stats.lua#L145
+    return stripfilename(title, true):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h")
+  end
+
+  --remove paths if they exist, keeping protocols for stripping
+  if string.sub(name, 1, 1) == '/' or name:match("^%a:[/\\]") then
+    _, name = utils.split_path(name)
+  end
+  return stripfilename(name):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h")
+end
+
+function parse_header(string)
+  local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%")
+  local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%")
+  return string:gsub("%%N", "\\N")
+               -- add a blank character at the end of each '\N'  to ensure that the height of the empty line is the same as the non empty line
+               :gsub("\\N", "\\N ")
+               :gsub("%%pos", mp.get_property_number("playlist-pos",0)+1)
+               :gsub("%%plen", mp.get_property("playlist-count"))
+               :gsub("%%cursor", cursor+1)
+               :gsub("%%mediatitle", esc_title)
+               :gsub("%%filename", esc_file)
+               -- undo name escape
+               :gsub("%%%%", "%%")
+end
+
+function parse_filename(string, name, index)
+  local base = tostring(plen):len()
+  local esc_name = stripfilename(name):gsub("%%", "%%%%")
+  return string:gsub("%%N", "\\N")
+               :gsub("%%pos", string.format("%0"..base.."d", index+1))
+               :gsub("%%name", esc_name)
+               -- undo name escape
+               :gsub("%%%%", "%%")
+end
+
+function parse_filename_by_index(index)
+  local template = settings.normal_file
+
+  local is_idle = mp.get_property_native('idle-active')
+  local position = is_idle and -1 or pos
+
+  if index == position then
+    if index == cursor then
+      if selection then
+        template = settings.playing_selected_file
+      else
+        template = settings.playing_hovered_file
+      end
+    else
+      template = settings.playing_file
+    end
+  elseif index == cursor then
+    if selection then
+      template = settings.selected_file
+    else
+      template = settings.hovered_file
+    end
+  end
+
+  return parse_filename(template, get_name_from_index(index), index)
+end
+
+function is_terminal_mode()
+  local width, height, aspect_ratio = mp.get_osd_size()
+  return width == 0 and height == 0 and aspect_ratio == 0
+end
+
+function draw_playlist()
+  refresh_globals()
+
+  -- if there is no playing file, then cursor can be -1. That would break rendering of playlist.
+  if cursor == -1 then
+    cursor = 0
+  end
+
+  local ass = assdraw.ass_new()
+  local terminaloutput = ""
+
+  local _, _, a = mp.get_osd_size()
+  local h = 720
+  local w = math.ceil(h * a)
+
+  if settings.curtain_opacity ~= nil and settings.curtain_opacity ~= 0 and settings.curtain_opacity <= 1.0 then
+  -- curtain dim from https://github.com/christoph-heinrich/mpv-quality-menu/blob/501794bfbef468ee6a61e54fc8821fe5cd72c4ed/quality-menu.lua#L699-L707
+    local alpha = 255 - math.ceil(255 * settings.curtain_opacity)
+    ass.text = string.format('{\\pos(0,0)\\rDefault\\an7\\1c&H000000&\\alpha&H%X&}', alpha)
+    ass:draw_start()
+    ass:rect_cw(0, 0, w, h)
+    ass:draw_stop()
+    ass:new_event()
+  end
+
+  ass:append(settings.style_ass_tags)
+
+  -- add \clip style
+  -- make both left and right follow text_padding_x
+  --      both top and bottom follow text_padding_y
+  local border_size = mp.get_property_number('osd-border-size')
+  if settings.style_ass_tags ~= nil then
+    local bord = tonumber(settings.style_ass_tags:match('\\bord(%d+%.?%d*)'))
+    if bord ~= nil then border_size = bord end
+  end
+  ass:append(string.format('{\\clip(%f,%f,%f,%f)}',
+    settings.text_padding_x - border_size,         settings.text_padding_y - border_size,
+    w - 1 - settings.text_padding_x + border_size, h - 1 - settings.text_padding_y + border_size))
+
+  -- align from mpv.conf
+  local align_x = mp.get_property("osd-align-x")
+  local align_y = mp.get_property("osd-align-y")
+  -- align from style_ass_tags
+  if settings.style_ass_tags ~= nil then
+    local an = tonumber(settings.style_ass_tags:match('\\an(%d)'))
+    if an ~= nil and alignment_table[an] ~= nil then
+      align_x = alignment_table[an]["x"]
+      align_y = alignment_table[an]["y"]
+    end
+  end
+  -- range of x [0, w-1]
+  local pos_x
+  if align_x == 'left' then
+    pos_x = settings.text_padding_x
+  elseif align_x == 'right' then
+    pos_x = w - 1 - settings.text_padding_x
+  else
+    pos_x = math.floor((w - 1) / 2)
+  end
+  -- range of y [0, h-1]
+  local pos_y
+  if align_y == 'top' then
+    pos_y = settings.text_padding_y
+  elseif align_y == 'bottom' then
+    pos_y = h - 1 - settings.text_padding_y
+  else
+    pos_y = math.floor((h - 1) / 2)
+  end
+  ass:pos(pos_x, pos_y)
+
+  if settings.playlist_header ~= "" then
+    local header = parse_header(settings.playlist_header)
+    ass:append(header.."\\N")
+    terminaloutput = terminaloutput..header.."\n"
+  end
+
+  -- (visible index, playlist index) pairs of playlist entries that should be rendered
+  local visible_indices = {}
+
+  local one_based_cursor = cursor + 1
+  table.insert(visible_indices, one_based_cursor)
+
+  local offset = 1;
+  local visible_indices_length = 1;
+  while visible_indices_length < settings.showamount and visible_indices_length < plen do
+    -- add entry for offset steps below the cursor
+    local below = one_based_cursor + offset
+    if below <= plen then
+      table.insert(visible_indices, below)
+      visible_indices_length = visible_indices_length + 1;
+    end
+
+    -- add entry for offset steps above the cursor
+    -- also need to double check that there is still space, this happens if we have even numbered limit
+    local above = one_based_cursor - offset
+    if above >= 1 and visible_indices_length < settings.showamount and visible_indices_length < plen then
+      table.insert(visible_indices, 1, above)
+      visible_indices_length = visible_indices_length + 1;
+    end
+
+    offset = offset + 1
+  end
+
+  -- both indices are 1 based
+  for display_index, playlist_index in pairs(visible_indices) do
+    if display_index == 1 and playlist_index ~= 1 then
+      ass:append(settings.playlist_sliced_prefix.."\\N")
+      terminaloutput = terminaloutput..settings.playlist_sliced_prefix.."\n"
+    elseif display_index == settings.showamount and playlist_index ~= plen then
+      ass:append(settings.playlist_sliced_suffix)
+      terminaloutput = terminaloutput..settings.playlist_sliced_suffix.."\n"
+    else
+      -- parse_filename_by_index expects 0 based index
+      local fname = parse_filename_by_index(playlist_index - 1)
+      ass:append(fname.."\\N")
+      terminaloutput = terminaloutput..fname.."\n"
+    end
+  end
+
+  if is_terminal_mode() then
+    local timeout_setting = settings.playlist_display_timeout
+    local timeout = timeout_setting == 0 and 2147483 or timeout_setting
+    -- TODO: probably have to strip ass tags from terminal output
+    -- would maybe be possible to use terminal color output instead
+    mp.osd_message(terminaloutput, timeout)
+  else
+    playlist_overlay.data = ass.text
+    playlist_overlay:update()
+  end
+end
+
+local peek_display_timer = nil
+local peek_button_pressed = false
+
+function peek_timeout()
+  peek_display_timer:kill()
+  if not peek_button_pressed and not playlist_visible then
+    remove_keybinds()
+  end
+end
+
+function handle_complex_playlist_toggle(table)
+  local event = table["event"]
+  if event == "press" then
+    msg.error("Complex key event not supported. Falling back to normal playlist display.")
+    showplaylist()
+  elseif event == "down" then
+    showplaylist(1000000)
+    if settings.peek_respect_display_timeout then
+      peek_button_pressed = true
+      peek_display_timer = mp.add_periodic_timer(settings.playlist_display_timeout, peek_timeout)
+    end
+  elseif event == "up" then
+    -- set playlist state to not visible, doesn't actually hide playlist yet
+    -- this will allow us to check if other functionality has rendered playlist before removing binds
+    playlist_visible = false
+
+    function remove_keybinds_after_timeout()
+      -- if playlist is still not visible then lets actually hide it
+      -- this lets other keys that interupt the peek to render playlist without peek up event closing it
+      if not playlist_visible then
+        remove_keybinds()
+      end
+    end
+
+    if settings.peek_respect_display_timeout then
+      peek_button_pressed = false
+      if not peek_display_timer:is_enabled() then
+        mp.add_timeout(0.01, remove_keybinds_after_timeout)
+      end
+    else
+      -- use small delay to let dynamic binds run before keys are potentially unbound
+      mp.add_timeout(0.01, remove_keybinds_after_timeout)
+    end
+  end
+end
+
+function toggle_playlist(show_function)
+  local show = show_function or showplaylist
+  if playlist_visible then
+    remove_keybinds()
+  else
+    -- toggle always shows without timeout
+    show(0)
+  end
+end
+
+function showplaylist(duration)
+  refresh_globals()
+  if plen == 0 then return end
+  if not playlist_visible and settings.reset_cursor_on_open then
+    resetcursor()
+  end
+
+  playlist_visible = true
+  add_keybinds()
+
+  draw_playlist()
+  keybindstimer:kill()
+
+  local dur = tonumber(duration) or settings.playlist_display_timeout
+  if dur > 0 then
+    keybindstimer = mp.add_periodic_timer(dur, remove_keybinds)
+  end
+end
+
+function showplaylist_non_interactive(duration)
+  refresh_globals()
+  if plen == 0 then return end
+  if not playlist_visible and settings.reset_cursor_on_open then
+    resetcursor()
+  end
+  playlist_visible = true
+  draw_playlist()
+  keybindstimer:kill()
+
+  local dur = tonumber(duration) or settings.playlist_display_timeout
+  if dur > 0 then
+    keybindstimer = mp.add_periodic_timer(dur, remove_keybinds)
+  end
+end
+
+selection=nil
+function selectfile()
+  refresh_globals()
+  if plen == 0 then return end
+  if not selection then
+    selection=cursor
+  else
+    selection=nil
+  end
+  showplaylist()
+end
+
+function unselectfile()
+  selection=nil
+  showplaylist()
+end
+
+function resetcursor()
+  selection = nil
+  cursor = mp.get_property_number('playlist-pos', 1)
+end
+
+function removefile()
+  refresh_globals()
+  if plen == 0 then return end
+  selection = nil
+  if cursor==pos then mp.command("script-message unseenplaylist mark true \"playlistmanager avoid conflict when removing file\"") end
+  mp.commandv("playlist-remove", cursor)
+  if cursor==plen-1 then cursor = cursor - 1 end
+  if plen == 1 then
+    remove_keybinds()
+  else
+    showplaylist()
+  end
+end
+
+function moveup()
+  refresh_globals()
+  if plen == 0 then return end
+  if cursor~=0 then
+    if selection then mp.commandv("playlist-move", cursor,cursor-1) end
+    cursor = cursor-1
+  elseif settings.loop_cursor then
+    if selection then mp.commandv("playlist-move", cursor,plen) end
+    cursor = plen-1
+  end
+  showplaylist()
+end
+
+function movedown()
+  refresh_globals()
+  if plen == 0 then return end
+  if cursor ~= plen-1 then
+    if selection then mp.commandv("playlist-move", cursor,cursor+2) end
+    cursor = cursor + 1
+  elseif settings.loop_cursor then
+    if selection then mp.commandv("playlist-move", cursor,0) end
+    cursor = 0
+  end
+  showplaylist()
+end
+
+
+function movepageup()
+  refresh_globals()
+  if plen == 0 or cursor == 0 then return end
+  local offset = settings.showamount % 2 == 0 and 1 or 0
+  local last_file_that_doesnt_scroll = math.ceil(settings.showamount / 2)
+  local reverse_cursor = plen - cursor
+  local files_to_jump = math.max(last_file_that_doesnt_scroll + offset - reverse_cursor, 0) + settings.showamount - 2
+  local prev_cursor = cursor
+  cursor = cursor - files_to_jump
+  if cursor < last_file_that_doesnt_scroll then
+    cursor = 0
+  end
+  if selection then
+    mp.commandv("playlist-move", prev_cursor, cursor)
+  end
+  showplaylist()
+end
+
+function movepagedown()
+  refresh_globals()
+  if plen == 0 or cursor == plen - 1 then return end
+  local last_file_that_doesnt_scroll = math.ceil(settings.showamount / 2) - 1
+  local files_to_jump = math.max(last_file_that_doesnt_scroll - cursor, 0) + settings.showamount - 2
+  local prev_cursor = cursor
+  cursor = cursor + files_to_jump
+
+  local cursor_on_last_page = plen - (settings.showamount - 3)
+  if cursor > cursor_on_last_page then
+    cursor = plen - 1
+  end
+  if selection then
+    mp.commandv("playlist-move", prev_cursor, cursor + 1)
+  end
+  showplaylist()
+end
+
+
+function movebegin()
+  refresh_globals()
+  if plen == 0 or cursor == 0 then return end
+  local prev_cursor = cursor
+  cursor = 0
+  if selection then mp.commandv("playlist-move", prev_cursor, cursor) end
+  showplaylist()
+end
+
+function moveend()
+  refresh_globals()
+  if plen == 0 or cursor == plen-1 then return end
+  local prev_cursor = cursor
+  cursor = plen-1
+  if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end
+  showplaylist()
+end
+
+function write_watch_later(force_write)
+  if settings.allow_write_watch_later_config then
+    if mp.get_property_bool("save-position-on-quit") or force_write then
+      mp.command("write-watch-later-config")
+    end
+  end
+end
+
+function playlist_next()
+  write_watch_later(true)
+  mp.commandv("playlist-next", "weak")
+  if settings.close_playlist_on_playfile then
+    remove_keybinds()
+  end
+  refresh_UI()
+end
+
+function playlist_prev()
+  write_watch_later(true)
+  mp.commandv("playlist-prev", "weak")
+  if settings.close_playlist_on_playfile then
+    remove_keybinds()
+  end
+  refresh_UI()
+end
+
+function playlist_random()
+  write_watch_later()
+  refresh_globals()
+  if plen < 2 then return end
+  math.randomseed(os.time())
+  local random = pos
+  while random == pos do
+    random = math.random(0, plen-1)
+  end
+  mp.set_property("playlist-pos", random)
+  if settings.close_playlist_on_playfile then
+    remove_keybinds()
+  end
+end
+
+function playfile()
+  refresh_globals()
+  if plen == 0 then return end
+  selection = nil
+  local is_idle = mp.get_property_native('idle-active')
+  if cursor ~= pos or is_idle then
+    write_watch_later()
+    mp.set_property("playlist-pos", cursor)
+  else
+    if cursor~=plen-1 then
+      cursor = cursor + 1
+    end
+    write_watch_later()
+    mp.commandv("playlist-next", "weak")
+  end
+  if settings.close_playlist_on_playfile then
+    remove_keybinds()
+  elseif playlist_visible then
+    showplaylist()
+  end
+end
+
+function file_filter(filenames)
+    local files = {}
+    for i = 1, #filenames do
+        local file = filenames[i]
+        local ext = file:match('%.([^%.]+)$')
+        if ext and filetype_lookup[ext:lower()] then
+            table.insert(files, file)
+        end
+    end
+    return files
+end
+
+function get_playlist_filenames_set()
+  local filenames = {}
+  for n=0,plen-1,1 do
+    local filename = mp.get_property('playlist/'..n..'/filename')
+    local _, file = utils.split_path(filename)
+    filenames[file] = true
+  end
+  return filenames
+end
+
+--Creates a playlist of all files in directory, will keep the order and position
+--For exaple, Folder has 12 files, you open the 5th file and run this, the remaining 7 are added behind the 5th file and prior 4 files before it
+function playlist(force_dir)
+  refresh_globals()
+  if not directory and plen > 0 then return end
+  local hasfile = true
+  if plen == 0 then
+    hasfile = false
+    dir = mp.get_property('working-directory')
+  else
+    dir = directory
+  end
+
+  if dir == "." then dir = "" end
+  if force_dir then dir = force_dir end
+
+  local files = file_filter(utils.readdir(dir, "files"))
+  if winapisort ~= nil then
+    table.sort(files, winapisort)
+  else
+    table.sort(files, alphanumsort)
+  end
+
+
+  if files == nil then
+    msg.verbose("no files in directory")
+    return
+  end
+
+  local filenames = get_playlist_filenames_set()
+  local c, c2 = 0,0
+  if files then
+    local cur = false
+    local filename = mp.get_property("filename")
+    for _, file in ipairs(files) do
+      if file == nil or file[1] == "." then
+          break
+      end
+      local appendstr = "append"
+      if not hasfile then
+        cur = true
+        appendstr = "append-play"
+        hasfile = true
+      end
+      if filename == file then
+        cur = true
+      elseif filenames[file] then
+        -- skip files already in playlist
+      elseif cur == true or settings.loadfiles_always_append then
+        mp.commandv("loadfile", utils.join_path(dir, file), appendstr)
+        msg.info("Appended to playlist: " .. file)
+        c2 = c2 + 1
+      else
+        mp.commandv("loadfile", utils.join_path(dir, file), appendstr)
+        msg.info("Prepended to playlist: " .. file)
+        mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1)-1,  c)
+        c = c + 1
+      end
+    end
+    if c2 > 0 or c>0 then
+      msg.info("Added "..c + c2.." files to playlist")
+    else
+      msg.info("No additional files found")
+    end
+    cursor = mp.get_property_number('playlist-pos', 1)
+  else
+    msg.error("Could not scan for files: "..(error or ""))
+  end
+  refresh_globals()
+  if playlist_visible then
+    showplaylist()
+  end
+  if settings.display_osd_feedback then
+    if c2 > 0 or c>0 then
+      mp.osd_message("Added "..c + c2.." files to playlist")
+    else
+      mp.osd_message("No additional files found")
+    end
+  end
+  return c + c2
+end
+
+function parse_home(path)
+  if not path:find("^~") then
+    return path
+  end
+  local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE")
+  if not home_dir then
+    local drive = os.getenv("HOMEDRIVE")
+    local path = os.getenv("HOMEPATH")
+    if drive and path then
+      home_dir = utils.join_path(drive, path)
+    else
+      msg.error("Couldn't find home dir.")
+      return nil
+    end
+  end
+  local result = path:gsub("^~", home_dir)
+  return result
+end
+
+local interactive_save = false
+function activate_playlist_save()
+  if interactive_save then
+    remove_keybinds()
+    mp.command("script-message playlistmanager-save-interactive \"start interactive filenaming process\"")
+  else
+    save_playlist()
+  end
+end
+
+--saves the current playlist into a m3u file
+function save_playlist(filename)
+  local length = mp.get_property_number('playlist-count', 0)
+  if length == 0 then return end
+
+  --get playlist save path
+  local savepath
+  if settings.playlist_savepath == nil or settings.playlist_savepath == "" then
+    savepath = mp.command_native({"expand-path", "~~home/"}).."/playlists"
+  else
+    savepath = parse_home(settings.playlist_savepath)
+    if savepath == nil then return end
+  end
+
+  --create savepath if it doesn't exist
+  if utils.readdir(savepath) == nil then
+    local windows_args = {'powershell', '-NoProfile', '-Command', 'mkdir', savepath}
+    local unix_args = { 'mkdir', savepath }
+    local args = settings.system == 'windows' and windows_args or unix_args
+    local res = utils.subprocess({ args = args, cancellable = false })
+    if res.status ~= 0 then
+      msg.error("Failed to create playlist save directory "..savepath..". Error: "..(res.error or "unknown"))
+      return
+    end
+  end
+
+  local name = filename
+  if name == nil then
+    if settings.playlist_save_filename == nil or settings.playlist_save_filename == "" then
+      local date = os.date("*t")
+      local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(date.year, date.month, date.day, date.hour, date.min, date.sec)
+
+      name = datestring.."_playlist-size_"..length..".m3u"
+    else
+      name = settings.playlist_save_filename
+    end
+  end
+
+  local savepath = utils.join_path(savepath, name)
+  local file, err = io.open(savepath, "w")
+  if not file then
+    msg.error("Error in creating playlist file, check permissions. Error: "..(err or "unknown"))
+  else
+    file:write("#EXTM3U\n")
+    local i=0
+    while i < length do
+      local pwd = mp.get_property("working-directory")
+      local filename = mp.get_property('playlist/'..i..'/filename')
+      local fullpath = filename
+      if not is_protocol(filename) then
+        fullpath = utils.join_path(pwd, filename)
+      end
+      local title = mp.get_property('playlist/'..i..'/title') or title_table[filename]
+      if title then
+        file:write("#EXTINF:,"..title.."\n")
+      end
+      file:write(fullpath, "\n")
+      i=i+1
+    end
+    local saved_msg = "Playlist written to: "..savepath
+    if settings.display_osd_feedback then mp.osd_message(saved_msg) end
+    msg.info(saved_msg)
+    file:close()
+  end
+end
+
+function alphanumsort(a, b)
+  local function padnum(d)
+    local dec, n = string.match(d, "(%.?)0*(.+)")
+    return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n)
+  end
+  return tostring(a):lower():gsub("%.?%d+",padnum)..("%3d"):format(#b)
+       < tostring(b):lower():gsub("%.?%d+",padnum)..("%3d"):format(#a)
+end
+
+-- fast sort algo from https://github.com/zsugabubus/dotfiles/blob/master/.config/mpv/scripts/playlist-filtersort.lua
+function sortplaylist(startover)
+  local playlist = mp.get_property_native('playlist')
+  if #playlist < 2 then return end
+
+  local order = {}
+  for i=1, #playlist do
+               order[i] = i
+    playlist[i].string = get_name_from_index(i - 1, true)
+       end
+
+  table.sort(order, function(a, b)
+    return sort_modes[sort_mode].sort_fn(a, b, playlist)
+  end)
+
+  for i=1, #playlist do
+    playlist[order[i]].new_pos = i
+  end
+
+  for i=1, #playlist do
+    while true do
+      local j = playlist[i].new_pos
+      if i == j then
+        break
+      end
+      mp.commandv('playlist-move', (i)     - 1, (j + 1) - 1)
+      mp.commandv('playlist-move', (j - 1) - 1, (i)     - 1)
+      playlist[j], playlist[i] = playlist[i], playlist[j]
+    end
+  end
+
+  for i = 1, #playlist do
+    local filename = mp.get_property('playlist/' .. i - 1 .. '/filename')
+    local ext = filename:match("%.([^%.]+)$")
+    if not ext or not filetype_lookup[ext:lower()] then
+      --move the directory to the end of the playlist
+      mp.commandv('playlist-move', i - 1, #playlist)
+    end
+  end
+
+  cursor = mp.get_property_number('playlist-pos', 0)
+  if startover then
+    mp.set_property('playlist-pos', 0)
+  end
+  if playlist_visible then
+    showplaylist()
+  end
+  if settings.display_osd_feedback then
+    mp.osd_message("Playlist sorted with "..sort_modes[sort_mode].title)
+  end
+end
+
+function reverseplaylist()
+  local length = mp.get_property_number('playlist-count', 0)
+  if length < 2 then return end
+  for outer=1, length-1, 1 do
+    mp.commandv('playlist-move', outer, 0)
+  end
+  if playlist_visible then
+    showplaylist()
+  end
+  if settings.display_osd_feedback then
+    mp.osd_message("Playlist reversed")
+  end
+end
+
+function shuffleplaylist()
+  refresh_globals()
+  if plen < 2 then return end
+  mp.command("playlist-shuffle")
+  math.randomseed(os.time())
+  mp.commandv("playlist-move", pos, math.random(0, plen-1))
+
+  local playlist = mp.get_property_native('playlist')
+  for i = 1, #playlist do
+    local filename = mp.get_property('playlist/' .. i - 1 .. '/filename')
+    local ext = filename:match("%.([^%.]+)$")
+    if not ext or not filetype_lookup[ext:lower()] then
+      --move the directory to the end of the playlist
+      mp.commandv('playlist-move', i - 1, #playlist)
+    end
+  end
+
+  mp.set_property('playlist-pos', 0)
+  refresh_globals()
+  if playlist_visible then
+    showplaylist()
+  end
+  if settings.display_osd_feedback then
+    mp.osd_message("Playlist shuffled")
+  end
+end
+
+function bind_keys(keys, name, func, opts)
+  if keys == nil or keys == "" then
+    mp.add_key_binding(keys, name, func, opts)
+    return
+  end
+  local i = 1
+  for key in keys:gmatch("[^%s]+") do
+    local prefix = i == 1 and '' or i
+    mp.add_key_binding(key, name..prefix, func, opts)
+    i = i + 1
+  end
+end
+
+function bind_keys_forced(keys, name, func, opts)
+  if keys == nil or keys == "" then
+    mp.add_forced_key_binding(keys, name, func, opts)
+    return
+  end
+  local i = 1
+  for key in keys:gmatch("[^%s]+") do
+    local prefix = i == 1 and '' or i
+    mp.add_forced_key_binding(key, name..prefix, func, opts)
+    i = i + 1
+  end
+end
+
+function unbind_keys(keys, name)
+  if keys == nil or keys == "" then
+    mp.remove_key_binding(name)
+    return
+  end
+  local i = 1
+  for key in keys:gmatch("[^%s]+") do
+    local prefix = i == 1 and '' or i
+    mp.remove_key_binding(name..prefix)
+    i = i + 1
+  end
+end
+
+function add_keybinds()
+  bind_keys_forced(settings.key_moveup, 'moveup', moveup, "repeatable")
+  bind_keys_forced(settings.key_movedown, 'movedown', movedown, "repeatable")
+  bind_keys_forced(settings.key_movepageup, 'movepageup', movepageup, "repeatable")
+  bind_keys_forced(settings.key_movepagedown, 'movepagedown', movepagedown, "repeatable")
+  bind_keys_forced(settings.key_movebegin, 'movebegin', movebegin, "repeatable")
+  bind_keys_forced(settings.key_moveend, 'moveend', moveend, "repeatable")
+  bind_keys_forced(settings.key_selectfile, 'selectfile', selectfile)
+  bind_keys_forced(settings.key_unselectfile, 'unselectfile', unselectfile)
+  bind_keys_forced(settings.key_playfile, 'playfile', playfile)
+  bind_keys_forced(settings.key_removefile, 'removefile', removefile, "repeatable")
+  bind_keys_forced(settings.key_closeplaylist, 'closeplaylist', remove_keybinds)
+end
+
+function remove_keybinds()
+  keybindstimer:kill()
+  keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+  keybindstimer:kill()
+  playlist_overlay.data = ""
+  playlist_overlay:remove()
+  if is_terminal_mode() then
+    mp.osd_message("")
+  end
+  playlist_visible = false
+  if settings.reset_cursor_on_close then
+    resetcursor()
+  end
+  if settings.dynamic_binds then
+    unbind_keys(settings.key_moveup, 'moveup')
+    unbind_keys(settings.key_movedown, 'movedown')
+    unbind_keys(settings.key_movepageup, 'movepageup')
+    unbind_keys(settings.key_movepagedown, 'movepagedown')
+    unbind_keys(settings.key_movebegin, 'movebegin')
+    unbind_keys(settings.key_moveend, 'moveend')
+    unbind_keys(settings.key_selectfile, 'selectfile')
+    unbind_keys(settings.key_unselectfile, 'unselectfile')
+    unbind_keys(settings.key_playfile, 'playfile')
+    unbind_keys(settings.key_removefile, 'removefile')
+    unbind_keys(settings.key_closeplaylist, 'closeplaylist')
+  end
+end
+
+keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+keybindstimer:kill()
+
+if not settings.dynamic_binds then
+  add_keybinds()
+end
+
+if settings.loadfiles_on_idle_start and mp.get_property_number('playlist-count', 0) == 0 then
+  playlist()
+end
+
+mp.observe_property('playlist-count', "number", function(_, plcount)
+  --if we promised to listen and sort on playlist size increase do it
+  if settings.sortplaylist_on_file_add and (plcount > plen) then
+    msg.info("Added files will be automatically sorted")
+    refresh_globals()
+    sortplaylist()
+  end
+  refresh_UI()
+  resolve_titles()
+end)
+mp.observe_property('osd-dimensions', 'native', refresh_UI)
+
+
+url_request_queue = {}
+function url_request_queue.push(item) table.insert(url_request_queue, item) end
+function url_request_queue.pop() return table.remove(url_request_queue, 1) end
+local url_titles_to_fetch = url_request_queue
+local ongoing_url_requests = {}
+
+function url_fetching_throttler()
+  if #url_titles_to_fetch == 0 then
+    url_title_fetch_timer:kill()
+  end
+
+  local ongoing_url_requests_count = 0
+  for _, ongoing in pairs(ongoing_url_requests) do
+    if ongoing then
+      ongoing_url_requests_count = ongoing_url_requests_count + 1
+    end
+  end
+
+  -- start resolving some url titles if there is available slots
+  local amount_to_fetch = math.max(0, settings.concurrent_title_resolve_limit - ongoing_url_requests_count)
+  for index=1,amount_to_fetch,1 do
+    local file = url_titles_to_fetch.pop()
+    if file then
+      ongoing_url_requests[file] = true
+      resolve_ytdl_title(file)
+    end
+  end
+end
+
+url_title_fetch_timer = mp.add_periodic_timer(0.1, url_fetching_throttler)
+url_title_fetch_timer:kill()
+
+local_request_queue = {}
+function local_request_queue.push(item) table.insert(local_request_queue, item) end
+function local_request_queue.pop() return table.remove(local_request_queue, 1) end
+local local_titles_to_fetch = local_request_queue
+local ongoing_local_request = false
+
+-- this will only allow 1 concurrent local title resolve process
+function local_fetching_throttler()
+  if not ongoing_local_request then
+    local file = local_titles_to_fetch.pop()
+    if file then
+      ongoing_local_request = true
+      resolve_ffprobe_title(file)
+    end
+  end
+end
+
+function resolve_titles()
+  if settings.prefer_titles == 'none' then return end
+  if not settings.resolve_url_titles and not settings.resolve_local_titles then return end
+
+  local length = mp.get_property_number('playlist-count', 0)
+  if length < 2 then return end
+  -- loop all items in playlist because we can't predict how it has changed
+  local added_urls = false
+  local added_local = false
+  for i=0,length - 1,1 do
+    local filename = mp.get_property('playlist/'..i..'/filename')
+    local title = mp.get_property('playlist/'..i..'/title')
+    if i ~= pos
+      and filename
+      and not title
+      and not title_table[filename]
+      and not requested_titles[filename]
+    then
+      requested_titles[filename] = true
+      if filename:match('^https?://') and settings.resolve_url_titles then
+        url_titles_to_fetch.push(filename)
+        added_urls = true
+      elseif settings.prefer_titles == "all" and settings.resolve_local_titles then
+        local_titles_to_fetch.push(filename)
+        added_local = true
+      end
+    end
+  end
+  if added_urls then
+    url_title_fetch_timer:resume()
+  end
+  if added_local then
+    local_fetching_throttler()
+  end
+end
+
+function resolve_ytdl_title(filename)
+  local args = {
+    settings.youtube_dl_executable,
+    '--no-playlist',
+    '--flat-playlist',
+    '-sJ',
+    '--no-config',
+    filename,
+  }
+  local req = mp.command_native_async(
+    {
+      name = "subprocess",
+      args = args,
+      playback_only = false,
+      capture_stdout = true
+    },
+    function (success, res)
+      ongoing_url_requests[filename] = false
+      if res.killed_by_us then
+        msg.verbose('Request to resolve url title ' .. filename .. ' timed out')
+        return
+      end
+      if res.status == 0 then
+        local json, err = utils.parse_json(res.stdout)
+        if not err then
+          local is_playlist = json['_type'] and json['_type'] == 'playlist'
+          local title = (is_playlist and '[playlist]: ' or '') .. json['title']
+          msg.verbose(filename .. " resolved to '" .. title .. "'")
+          title_table[filename] = title
+          mp.set_property_native('user-data/playlistmanager/titles', title_table)
+          refresh_UI()
+        else
+          msg.error("Failed parsing json, reason: "..(err or "unknown"))
+        end
+      else
+        msg.error("Failed to resolve url title "..filename.." Error: "..(res.error or "unknown"))
+      end
+    end
+  )
+
+  mp.add_timeout(
+    settings.resolve_title_timeout,
+    function()
+      mp.abort_async_command(req)
+      ongoing_url_requests[filename] = false
+    end
+  )
+end
+
+function resolve_ffprobe_title(filename)
+  local args = { "ffprobe", "-show_format", "-show_entries", "format=tags", "-loglevel", "quiet", filename }
+  local req = mp.command_native_async(
+    {
+      name = "subprocess",
+      args = args,
+      playback_only = false,
+      capture_stdout = true
+    },
+    function (success, res)
+      ongoing_local_request = false
+      local_fetching_throttler()
+      if res.killed_by_us then
+        msg.verbose('Request to resolve local title ' .. filename .. ' timed out')
+        return
+      end
+      if res.status == 0 then
+        local title = string.match(res.stdout, "title=([^\n\r]+)")
+        if title then
+          msg.verbose(filename .. " resolved to '" .. title .. "'")
+          title_table[filename] = title
+          mp.set_property_native('user-data/playlistmanager/titles', title_table)
+          refresh_UI()
+        end
+      else
+        msg.error("Failed to resolve local title "..filename.." Error: "..(res.error or "unknown"))
+      end
+    end
+  )
+end
+
+--script message handler
+function handlemessage(msg, value, value2)
+  if msg == "show" and value == "playlist" then
+    if value2 ~= "toggle" then
+      showplaylist(value2)
+      return
+    else
+      toggle_playlist(showplaylist)
+      return
+    end
+  end
+  if msg == "show" and value == "playlist-nokeys" then
+    if value2 ~= "toggle" then
+      showplaylist_non_interactive(value2)
+      return
+    else
+      toggle_playlist(showplaylist_non_interactive)
+      return
+    end
+  end
+  if msg == "show" and value == "filename" and strippedname and value2 then
+    mp.commandv('show-text', strippedname, tonumber(value2)*1000 ) ; return
+  end
+  if msg == "show" and value == "filename" and strippedname then
+    mp.commandv('show-text', strippedname ) ; return
+  end
+  if msg == "sort" then sortplaylist(value) ; return end
+  if msg == "shuffle" then shuffleplaylist() ; return end
+  if msg == "reverse" then reverseplaylist() ; return end
+  if msg == "loadfiles" then playlist(value) ; return end
+  if msg == "save" then save_playlist(value) ; return end
+  if msg == "playlist-next" then playlist_next() ; return end
+  if msg == "playlist-prev" then playlist_prev() ; return end
+  if msg == "playlist-next-random" then playlist_random() ; return end
+  if msg == "enable-interactive-save" then interactive_save = true end
+  if msg == "close" then remove_keybinds() end
+end
+
+mp.register_script_message("playlistmanager", handlemessage)
+
+bind_keys(settings.key_sortplaylist, "sortplaylist", function()
+  sortplaylist()
+  sort_mode = sort_mode + 1
+  if sort_mode > #sort_modes then sort_mode = 1 end
+end)
+bind_keys(settings.key_shuffleplaylist, "shuffleplaylist", shuffleplaylist)
+bind_keys(settings.key_reverseplaylist, "reverseplaylist", reverseplaylist)
+bind_keys(settings.key_loadfiles, "loadfiles", playlist)
+bind_keys(settings.key_saveplaylist, "saveplaylist", activate_playlist_save)
+bind_keys(settings.key_showplaylist, "showplaylist", showplaylist)
+bind_keys(
+  settings.key_peek_at_playlist,
+  "peek_at_playlist",
+  handle_complex_playlist_toggle,
+  { complex=true }
+)
+
+mp.register_event("start-file", on_start_file)
+mp.register_event("file-loaded", on_file_loaded)
+mp.register_event("end-file", on_end_file)