From d3ea797dd503a88e17025dc113a5c23a1c69ae81 Mon Sep 17 00:00:00 2001
From: A Farzat <a@farzat.xyz>
Date: Fri, 8 Nov 2024 10:44:46 +0900
Subject: Use mpv script directory instead of a symlink

---
 .config/mpv/scripts/reload.lua       |   1 -
 .config/mpv/scripts/reload/LICENSE   |  21 ++
 .config/mpv/scripts/reload/README.md |  75 ++++++
 .config/mpv/scripts/reload/main.lua  | 427 +++++++++++++++++++++++++++++++++++
 4 files changed, 523 insertions(+), 1 deletion(-)
 delete mode 120000 .config/mpv/scripts/reload.lua
 create mode 100644 .config/mpv/scripts/reload/LICENSE
 create mode 100644 .config/mpv/scripts/reload/README.md
 create mode 100644 .config/mpv/scripts/reload/main.lua

(limited to '.config/mpv/scripts')

diff --git a/.config/mpv/scripts/reload.lua b/.config/mpv/scripts/reload.lua
deleted file mode 120000
index 35d9cc0..0000000
--- a/.config/mpv/scripts/reload.lua
+++ /dev/null
@@ -1 +0,0 @@
-../script-repos/mpv-reload/reload.lua
\ No newline at end of file
diff --git a/.config/mpv/scripts/reload/LICENSE b/.config/mpv/scripts/reload/LICENSE
new file mode 100644
index 0000000..3716cf8
--- /dev/null
+++ b/.config/mpv/scripts/reload/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Dmitrii Bushev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/.config/mpv/scripts/reload/README.md b/.config/mpv/scripts/reload/README.md
new file mode 100644
index 0000000..3fd6350
--- /dev/null
+++ b/.config/mpv/scripts/reload/README.md
@@ -0,0 +1,75 @@
+# reload.lua
+
+When an online video is stuck during buffering or got slow CDN source,
+restarting often helps. This script provides automatic reloading of videos that
+didn't have buffering progress for some time, keeping the current time position
+while preserving entries in the playlist. It also adds `Ctrl+r` keybinding to
+reload video manually.
+
+## Install
+
+Mpv reads its configuration from `MPV_HOME` directory. On Unix it is
+`~/.config/mpv`, see [files](https://mpv.io/manual/stable/#files) section of
+the manual for the Windows configuration files.
+
+To install the script, you can either:
+
+* clone this repository to the `MPV_HOME/scripts` directory and name it
+  appropriately: `git clone <repo-url> -- <MPV_HOME>/scripts/reload`
+* or copy `main.lua` to `MPV_HOME/scripts` and rename it `reload.lua`.
+
+To override default settings, create `reload.conf` file in the script-opts
+directory `MPV_HOME/script-opts`.
+
+NOTE: config file name should match the name of the script.
+
+For configuration example you can also check
+[4e6/dotfiles](https://github.com/4e6/dotfiles/tree/master/.config/mpv) repo.
+
+## Settings
+
+Default `reload.conf` settings:
+
+```
+# enable automatic reload on timeout
+# when paused-for-cache event fired, we will wait
+# paused_for_cache_timer_timeout sedonds and then reload the video
+paused_for_cache_timer_enabled=yes
+
+# checking paused_for_cache property interval in seconds,
+# can not be less than 0.05 (50 ms)
+paused_for_cache_timer_interval=1
+
+# time in seconds to wait until reload
+paused_for_cache_timer_timeout=10
+
+# enable automatic reload based on demuxer cache
+# if demuxer-cache-time property didn't change in demuxer_cache_timer_timeout
+# time interval, the video will be reloaded as soon as demuxer cache depleated
+demuxer_cache_timer_enabled=yes
+
+# checking demuxer-cache-time property interval in seconds,
+# can not be less than 0.05 (50 ms)
+demuxer_cache_timer_interval=2
+
+# if demuxer cache didn't receive any data during demuxer_cache_timer_timeout
+# we decide that it has no progress and will reload the stream when
+# paused_for_cache event happens
+demuxer_cache_timer_timeout=20
+
+# when the end-of-file is reached, reload the stream to check
+# if there is more content available.
+reload_eof_enabled=no
+
+# keybinding to reload stream from current time position
+# you can disable keybinding by setting it to empty value
+# reload_key_binding=
+reload_key_binding=Ctrl+r
+```
+
+## Debugging
+
+Debug messages will be printed to stdout with mpv command line option
+`--msg-level='reload=debug'`. You may also need to add the `--no-msg-color`
+option to make the debug logs visible if you are using a dark colorscheme in
+terminal.
diff --git a/.config/mpv/scripts/reload/main.lua b/.config/mpv/scripts/reload/main.lua
new file mode 100644
index 0000000..36e2e5b
--- /dev/null
+++ b/.config/mpv/scripts/reload/main.lua
@@ -0,0 +1,427 @@
+-- reload.lua
+--
+-- When an online video is stuck buffering or got very slow CDN
+-- source, restarting often helps. This script provides automatic
+-- reloading of videos that doesn't have buffering progress for some
+-- time while keeping the current time position. It also adds `Ctrl+r`
+-- keybinding to reload video manually.
+--
+-- SETTINGS
+--
+-- To override default setting put the `lua-settings/reload.conf` file in
+-- mpv user folder, on linux it is `~/.config/mpv`.  NOTE: config file
+-- name should match the name of the script.
+--
+-- Default `reload.conf` settings:
+--
+-- ```
+-- # enable automatic reload on timeout
+-- # when paused-for-cache event fired, we will wait
+-- # paused_for_cache_timer_timeout sedonds and then reload the video
+-- paused_for_cache_timer_enabled=yes
+--
+-- # checking paused_for_cache property interval in seconds,
+-- # can not be less than 0.05 (50 ms)
+-- paused_for_cache_timer_interval=1
+--
+-- # time in seconds to wait until reload
+-- paused_for_cache_timer_timeout=10
+--
+-- # enable automatic reload based on demuxer cache
+-- # if demuxer-cache-time property didn't change in demuxer_cache_timer_timeout
+-- # time interval, the video will be reloaded as soon as demuxer cache depleated
+-- demuxer_cache_timer_enabled=yes
+--
+-- # checking demuxer-cache-time property interval in seconds,
+-- # can not be less than 0.05 (50 ms)
+-- demuxer_cache_timer_interval=2
+--
+-- # if demuxer cache didn't receive any data during demuxer_cache_timer_timeout
+-- # we decide that it has no progress and will reload the stream when
+-- # paused_for_cache event happens
+-- demuxer_cache_timer_timeout=20
+--
+-- # when the end-of-file is reached, reload the stream to check
+-- # if there is more content available.
+-- reload_eof_enabled=no
+--
+-- # keybinding to reload stream from current time position
+-- # you can disable keybinding by setting it to empty value
+-- # reload_key_binding=
+-- reload_key_binding=Ctrl+r
+-- ```
+--
+-- DEBUGGING
+--
+-- Debug messages will be printed to stdout with mpv command line option
+-- `--msg-level='reload=debug'`. You may also need to add the `--no-msg-color`
+-- option to make the debug logs visible if you are using a dark colorscheme
+-- in terminal.
+
+local msg = require 'mp.msg'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+
+local settings = {
+  paused_for_cache_timer_enabled = true,
+  paused_for_cache_timer_interval = 1,
+  paused_for_cache_timer_timeout = 10,
+  demuxer_cache_timer_enabled = true,
+  demuxer_cache_timer_interval = 2,
+  demuxer_cache_timer_timeout = 20,
+  reload_eof_enabled = false,
+  reload_key_binding = "Ctrl+r",
+}
+
+-- global state stores properties between reloads
+local property_path = nil
+local property_time_pos = 0
+local property_keep_open = nil
+
+-- FSM managing the demuxer cache.
+--
+-- States:
+--
+-- * fetch - fetching new data
+-- * stale - unable to fetch new data for time <  'demuxer_cache_timer_timeout'
+-- * stuck - unable to fetch new data for time >= 'demuxer_cache_timer_timeout'
+--
+-- State transitions:
+--
+--    +---------------------------+
+--    v                           |
+-- +-------+     +-------+     +-------+
+-- + fetch +<--->+ stale +---->+ stuck |
+-- +-------+     +-------+     +-------+
+--   |   ^         |   ^         |   ^
+--   +---+         +---+         +---+
+local demuxer_cache = {
+  timer = nil,
+
+  state = {
+    name = 'uninitialized',
+    demuxer_cache_time = 0,
+    in_state_time = 0,
+  },
+
+  events = {
+    continue_fetch = { name = 'continue_fetch', from = 'fetch', to = 'fetch' },
+    continue_stale = { name = 'continue_stale', from = 'stale', to = 'stale' },
+    continue_stuck = { name = 'continue_stuck', from = 'stuck', to = 'stuck' },
+    fetch_to_stale = { name = 'fetch_to_stale', from = 'fetch', to = 'stale' },
+    stale_to_fetch = { name = 'stale_to_fetch', from = 'stale', to = 'fetch' },
+    stale_to_stuck = { name = 'stale_to_stuck', from = 'stale', to = 'stuck' },
+    stuck_to_fetch = { name = 'stuck_to_fetch', from = 'stuck', to = 'fetch' },
+  },
+
+}
+
+-- Always start with 'fetch' state
+function demuxer_cache.reset_state()
+  demuxer_cache.state = {
+    name = demuxer_cache.events.continue_fetch.to,
+    demuxer_cache_time = 0,
+    in_state_time = 0,
+  }
+end
+
+-- Has 'demuxer_cache_time' changed
+function demuxer_cache.has_progress_since(t)
+  return demuxer_cache.state.demuxer_cache_time ~= t
+end
+
+function demuxer_cache.is_state_fetch()
+  return demuxer_cache.state.name == demuxer_cache.events.continue_fetch.to
+end
+
+function demuxer_cache.is_state_stale()
+  return demuxer_cache.state.name == demuxer_cache.events.continue_stale.to
+end
+
+function demuxer_cache.is_state_stuck()
+  return demuxer_cache.state.name == demuxer_cache.events.continue_stuck.to
+end
+
+function demuxer_cache.transition(event)
+  if demuxer_cache.state.name == event.from then
+
+    -- state setup
+    demuxer_cache.state.demuxer_cache_time = event.demuxer_cache_time
+
+    if event.name == 'continue_fetch' then
+      demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
+    elseif event.name == 'continue_stale' then
+      demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
+    elseif event.name == 'continue_stuck' then
+      demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
+    elseif event.name == 'fetch_to_stale' then
+      demuxer_cache.state.in_state_time = 0
+    elseif event.name == 'stale_to_fetch' then
+      demuxer_cache.state.in_state_time = 0
+    elseif event.name == 'stale_to_stuck' then
+      demuxer_cache.state.in_state_time = 0
+    elseif event.name == 'stuck_to_fetch' then
+      demuxer_cache.state.in_state_time = 0
+    end
+
+    -- state transition
+    demuxer_cache.state.name = event.to
+
+    msg.debug('demuxer_cache.transition', event.name, utils.to_string(demuxer_cache.state))
+  else
+    msg.error(
+      'demuxer_cache.transition',
+      'illegal transition', event.name,
+      'from state', demuxer_cache.state.name)
+  end
+end
+
+function demuxer_cache.initialize(demuxer_cache_timer_interval)
+  demuxer_cache.reset_state()
+  demuxer_cache.timer = mp.add_periodic_timer(
+    demuxer_cache_timer_interval,
+    function()
+      demuxer_cache.demuxer_cache_timer_tick(
+        mp.get_property_native('demuxer-cache-time'),
+        demuxer_cache_timer_interval)
+    end
+  )
+end
+
+-- If there is no progress of demuxer_cache_time in
+-- settings.demuxer_cache_timer_timeout time interval switch state to
+-- 'stuck' and switch back to 'fetch' as soon as any progress is made
+function demuxer_cache.demuxer_cache_timer_tick(demuxer_cache_time, demuxer_cache_timer_interval)
+  local event = nil
+  local cache_has_progress = demuxer_cache.has_progress_since(demuxer_cache_time)
+
+  -- I miss pattern matching so much
+  if demuxer_cache.is_state_fetch() then
+    if cache_has_progress then
+      event = demuxer_cache.events.continue_fetch
+    else
+      event = demuxer_cache.events.fetch_to_stale
+    end
+  elseif demuxer_cache.is_state_stale() then
+    if cache_has_progress then
+      event = demuxer_cache.events.stale_to_fetch
+    elseif demuxer_cache.state.in_state_time < settings.demuxer_cache_timer_timeout then
+      event = demuxer_cache.events.continue_stale
+    else
+      event = demuxer_cache.events.stale_to_stuck
+    end
+  elseif demuxer_cache.is_state_stuck() then
+    if cache_has_progress then
+      event = demuxer_cache.events.stuck_to_fetch
+    else
+      event = demuxer_cache.events.continue_stuck
+    end
+  end
+
+  event.demuxer_cache_time = demuxer_cache_time
+  event.interval = demuxer_cache_timer_interval
+  demuxer_cache.transition(event)
+end
+
+
+local paused_for_cache = {
+  timer = nil,
+  time = 0,
+}
+
+function paused_for_cache.reset_timer()
+  msg.debug('paused_for_cache.reset_timer', paused_for_cache.time)
+  if paused_for_cache.timer then
+    paused_for_cache.timer:kill()
+    paused_for_cache.timer = nil
+    paused_for_cache.time = 0
+  end
+end
+
+function paused_for_cache.start_timer(interval_seconds, timeout_seconds)
+  msg.debug('paused_for_cache.start_timer', paused_for_cache.time)
+  if not paused_for_cache.timer then
+    paused_for_cache.timer = mp.add_periodic_timer(
+      interval_seconds,
+      function()
+        paused_for_cache.time = paused_for_cache.time + interval_seconds
+        if paused_for_cache.time >= timeout_seconds then
+          paused_for_cache.reset_timer()
+          reload_resume()
+        end
+        msg.debug('paused_for_cache', 'tick', paused_for_cache.time)
+      end
+    )
+  end
+end
+
+function paused_for_cache.handler(property, is_paused)
+  if is_paused then
+
+    if demuxer_cache.is_state_stuck() then
+      msg.info("demuxer cache has no progress")
+      -- reset demuxer state to avoid immediate reload if
+      -- paused_for_cache event triggered right after reload
+      demuxer_cache.reset_state()
+      reload_resume()
+    end
+
+    paused_for_cache.start_timer(
+      settings.paused_for_cache_timer_interval,
+      settings.paused_for_cache_timer_timeout)
+  else
+    paused_for_cache.reset_timer()
+  end
+end
+
+function read_settings()
+  options.read_options(settings, mp.get_script_name())
+  msg.debug(utils.to_string(settings))
+end
+
+function reload(path, time_pos)
+  msg.debug("reload", path, time_pos)
+  if time_pos == nil then
+    mp.commandv("loadfile", path, "replace")
+  else
+    local success = mp.commandv("loadfile", path, "replace", -1, "start=+" .. time_pos)
+    -- fallback to old syntax of loadfile for compatibility
+    if success == nil then
+      msg.warn("old loadfile syntax detected. falling back to using old syntax. update mpv to remove this warning")
+      mp.commandv("loadfile", path, "replace", "start=+" .. time_pos)
+    end
+  end
+end
+
+function reload_resume()
+  local path = mp.get_property("path", property_path)
+  local time_pos = mp.get_property("time-pos")
+  local reload_duration = mp.get_property_native("duration")
+
+  local playlist_count = mp.get_property_number("playlist/count")
+  local playlist_pos = mp.get_property_number("playlist-pos")
+  local playlist = {}
+  for i = 0, playlist_count-1 do
+      playlist[i] = mp.get_property("playlist/" .. i .. "/filename")
+  end
+  -- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero
+  -- duration property. When reloading VOD, to keep the current time position
+  -- we should provide offset from the start. Stream doesn't have fixed start.
+  -- Decent choice would be to reload stream from it's current 'live' positon.
+  -- That's the reason we don't pass the offset when reloading streams.
+  if reload_duration and reload_duration > 0 then
+    msg.info("reloading video from", time_pos, "second")
+    reload(path, time_pos)
+  -- VODs get stuck when reload is called without a time_pos
+  -- this is most noticeable in youtube videos whenever download gets stuck in the first frames
+  -- video would stay paused without being actually paused
+  -- issue surfaced in mpv 0.33, afaik
+  elseif reload_duration and reload_duration == 0 then
+    msg.info("reloading video from", time_pos, "second")
+    reload(path, time_pos)
+  else
+    msg.info("reloading stream")
+    reload(path, nil)
+  end
+  msg.info("file", playlist_pos+1, "of", playlist_count, "in playlist")
+  for i = 0, playlist_pos-1 do
+    mp.commandv("loadfile", playlist[i], "append")
+  end
+  mp.commandv("playlist-move", 0, playlist_pos+1)
+  for i = playlist_pos+1, playlist_count-1 do
+    mp.commandv("loadfile", playlist[i], "append")
+  end
+end
+
+function reload_eof(property, eof_reached)
+  msg.debug("reload_eof", property, eof_reached)
+  local time_pos = mp.get_property_number("time-pos")
+  local duration = mp.get_property_number("duration")
+
+  if eof_reached and round(time_pos) == round(duration) then
+    msg.debug("property_time_pos", property_time_pos, "time_pos", time_pos)
+
+    -- Check that playback time_pos made progress after the last reload. When
+    -- eof is reached we try to reload the video, in case there is more content
+    -- available. If time_pos stayed the same after reload, it means that the
+    -- video length stayed the same, and we can end the playback.
+    if round(property_time_pos) == round(time_pos) then
+      msg.info("eof reached, playback ended")
+      mp.set_property("keep-open", property_keep_open)
+    else
+      msg.info("eof reached, checking if more content available")
+      reload_resume()
+      mp.set_property_bool("pause", false)
+      property_time_pos = time_pos
+    end
+  end
+end
+
+function on_file_loaded(event)
+  local debug_info = {
+    event = event,
+    time_pos = mp.get_property("time-pos"),
+    stream_pos = mp.get_property("stream-pos"),
+    stream_end = mp.get_property("stream-end"),
+    duration = mp.get_property("duration"),
+    seekable = mp.get_property("seekable"),
+    pause = mp.get_property("pause"),
+    paused_for_cache = mp.get_property("paused-for-cache"),
+    cache_buffering_state = mp.get_property("cache-buffering-state"),
+  }
+  msg.debug("debug_info", utils.to_string(debug_info))
+
+  -- When the video is reloaded after being paused for cache, it won't start
+  -- playing again while all properties looks fine:
+  -- `pause=no`, `paused-for-cache=no` and `cache-buffering-state=100`.
+  -- As a workaround, we cycle through the paused state by sending two SPACE
+  -- keypresses.
+  -- What didn't work:
+  -- - Cycling through the `pause` property.
+  -- - Run the `playlist-play-index current` command.
+  mp.commandv("keypress", 'SPACE')
+  mp.commandv("keypress", 'SPACE')
+end
+
+-- Round positive numbers.
+function round(num)
+  return math.floor(num + 0.5)
+end
+
+-- main
+
+read_settings()
+
+if settings.reload_key_binding ~= "" then
+  mp.add_key_binding(settings.reload_key_binding, "reload_resume", reload_resume)
+end
+
+if settings.paused_for_cache_timer_enabled then
+  mp.observe_property("paused-for-cache", "bool", paused_for_cache.handler)
+end
+
+if settings.demuxer_cache_timer_enabled then
+  demuxer_cache.initialize(settings.demuxer_cache_timer_interval)
+end
+
+if settings.reload_eof_enabled then
+  -- vo-configured == video output created && its configuration went ok
+  mp.observe_property(
+    "vo-configured",
+    "bool",
+    function(name, vo_configured)
+      msg.debug(name, vo_configured)
+      if vo_configured then
+        property_path = mp.get_property("path")
+        property_keep_open = mp.get_property("keep-open")
+        mp.set_property("keep-open", "yes")
+        mp.set_property("keep-open-pause", "no")
+      end
+    end
+  )
+
+  mp.observe_property("eof-reached", "bool", reload_eof)
+end
+
+mp.register_event("file-loaded", on_file_loaded)
-- 
cgit v1.2.3-70-g09d2