hpr4585 :: mpv util scripts
a collection of hand-written scripts for mpv and explaining how the mpv api works
Hosted by candycanearter on Friday, 2026-02-27 is flagged as Clean and is released under a CC-BY-SA license.
mpv, lua, scripting.
(Be the first).
Listen in ogg,
opus,
or mp3 format. Play now:
Duration: 00:11:55
Download the transcription and
subtitles.
Bash Scripting.
This is an open series in which Hacker Public Radio Listeners can share their Bash scripting knowledge and experience with the community. General programming topics and Bash commands are explored along with some tutorials for the complete novice.
sorry about the computer fan i didnt realize how loud it was until after everything was recorded
all scripts are prefixed with a_ for personal organization
_a_props.lua
mp.observe_property("path", "native", function()
local domain = string.match(mp.get_property_native("path") or "", ".*://w*%.*(.-)[:/]")
if domain then mp.set_property("user-data/domain-path", domain)
else mp.del_property("user-data/domain-path") end
end)
mp.observe_property("playtime-remaining", "native", function (_, tr)
if tr then mp.set_property("user-data/playtime-remaining-seconds", math.floor(tr)) end
end)
a_aspectratio.lua
local targetw = 16
local targeth = 9
local marginerror = 0.1
local function resetgem()
local dim = mp.get_property_native("osd-dimensions")
if not dim or dim.w == 0 then return end
mp.set_property("geometry", dim.w .. "x" .. dim.h)
end
local function dimensionhop(_, dim)
if dim.w == 0 or dim.h == 0 then return end
local cd = dim.w / dim.h
local td = targetw / targeth
-- floating points my beloved
-- checking we're in a good range so it doesnt inf loop
-- also it updates the geometry field so profile restore can work
if cd > (td - marginerror) and cd < (td + marginerror) then resetgem(); return end
local setw = dim.h * td
local newdim = setw .. "x" .. dim.h
mp.set_property("geometry", newdim)
mp.osd_message("setting " .. newdim)
end
mp.observe_property("osd-dimensions", "native", dimensionhop)
mp.register_event("start-file", resetgem)
mp.register_event("end-file", resetgem)
a_cover-visualiser.lua
local function resolve_missing_cover(domain)
local extico = {
["hub.hackerpublicradio.org"] = "https://hackerpublicradio.org/images/hpr_logo.png",
["yellowtealpurple.net"] = "https://yellowtealpurple.net/forums/data/assets/logo/favicon-32x32.png",
-- yes using a product picture is silly but so is not featuring your icon ANYWHERE else
["anonradio.net"] = "https://sdf.org/store/thumbs/anon3.jpg",
["hashnix.club"] = "default",
["radio.kingposs.com"] = "https://kingposs.com/assets/buttons/PossBadge.gif"
}
if domain then
local force = extico[domain]
if force == "default" then return resolve_missing_cover() end
if force and mp.commandv("video-add", force, "auto", "domainhardcode.png") then return end
local favico = "https://" .. domain .. "/favicon.ico"
if mp.commandv("video-add", favico, "auto", "favico.png") then return end
end
mp.command("video-add ~~/cover.png auto default.png")
end
local function inject_needed()
local tracks = mp.get_property_native("track-list")
local needed = true
for _, v in ipairs(tracks) do
if v.type == 'video' then
if not v.image then return end
needed = false
end
end
if needed then resolve_missing_cover(mp.get_property_native("user-data/domain-path")) end
mp.set_property("file-local-options/lavfi-complex",
"[aid1] asplit=3 [a0][a1][ao] ; " ..
"[vid1] scale=sws_dither=none:flags=neighbor:w=max(iw\,256):h=max(iw\,256):force_original_aspect_ratio=increas
e:force_divisible_by=8, scale=h=-1:w=720, split=3 [vref0][vref1][vfin] ; " ..
"[a0] showfreqs=size=hd720, hue=h=220 [rawfreq] ; " ..
"[rawfreq][vref0] scale=flags=neighbor:w=rw:h=rh/2 [freq] ; " ..
"[a1] showvolume=f=0.5:h=14 [rawvol] ; [rawvol][vref1] scale=flags=neighbor:w=(3*rw)/4:h=-1, geq=p(X\,Y):a=255
[vol] ; " ..
"[vfin][freq] overlay=y=main_h-overlay_h [prevo] ; [prevo][vol] overlay [vo] ")
end
-- mp.register_event("start-file", inject_needed)
-- mp.observe_property("current-tracks/audio", "native", inject_needed)
mp.add_hook("on_preloaded", 50, inject_needed)
my cover.png (640x480)
example with hardcoded image
(notice theres only one volume bar because hpr is mixed to mono)
example with favicon detection
(youll probably see this one a lot since its the default icon for icecast servers)
example with default/no cover
(my art!!)
a_playlist.lua
mp.register_script_message("full-clear", function()
mp.set_property("playlist-pos", -1)
mp.command("playlist-clear")
end)
mp.register_script_message("playlist-next-to-last", function()
local target = mp.get_property_native("playlist-pos")
if target < 0 then return end
target = target + 1
mp.osd_message("moved " .. mp.get_property_native("playlist/" .. target .. "/filename"))
mp.commandv("playlist-move", target, 999)
end)
a_rcfill.lua
-- relative cache refill
-- sets cache-pause-wait based on how fast the playback and download speed is
local function set_pause(_, incache)
if not incache then return end
-- rate of bytes incoming
local ds = mp.get_property_native("cache-speed")
if not ds then return end
-- rate of bytes consumed * 2
local kbc = (mp.get_property_native("audio-bitrate") or 0) + (mp.get_property_native("video-bitrate") or 0)
kbc = (kbc/8) * (mp.get_property_native("speed") or 1) * 3
local secs = math.min(kbc/ds, 20)
if secs < 1 then secs = 2 end
mp.set_property("file-local-options/cache-pause-wait", secs)
mp.osd_message("buffering " .. math.floor(secs) .. " secs...")
end
local function jump_to_ecache(amt)
if not amt then return end
local endtime = mp.get_property_native("demuxer-cache-time")
if not endtime then return end
mp.commandv("seek", endtime - amt, "absolute")
mp.osd_message("jumped to realtime-" .. amt .. "s")
end
mp.observe_property("paused-for-cache", "native", set_pause)
mp.register_script_message("jump-to-ecache", jump_to_ecache)
a_titlebar.lua
mp.set_property("user-data/dynatitle-default", mp.get_property("title") or "mpv")
local function title_update()
if not mp.get_property_native("media-title") then
mp.set_property("title", mp.get_property_native("user-data/dynatitle-default"))
return
end
local pl = mp.get_property_native("playlist-pos")
if pl ~= -1 then pl = mp.get_property_native("playlist-count") - pl - 1 end
local tr = mp.get_property_native("playtime-remaining")
if not tr then
-- file currently loading
-- since this is a slow changing value, we can just set this literally
local disp = ""
if pl ~= -1 then
disp = "( " .. pl .. " files remaining )"
end
mp.set_property("title", "loading ${media-title} " .. disp)
return
end
local progress = "${percent-pos} "
if tr < 100 then
local emg = "-"
if pl < 1 then emg = "-!" end
progress = emg .. "${user-data/playtime-remaining-seconds} "
end
if mp.get_property_native("paused-for-cache") then
progress = "B${cache-buffering-state} "
end
local netspeed = ""
if mp.get_property_native("demuxer-via-network") then
netspeed = "${cache-speed} "
end
local domainlabel = ""
if mp.get_property_native("user-data/domain-path") then
domainlabel = "via ${user-data/domain-path} "
end
mp.set_property("title", "${?pause==yes:P}" .. progress .. netspeed .. "${media-title} " .. domainlabel)
end
mp.observe_property("percent-pos", "native", title_update)
mp.observe_property("cache-buffering-state", "native", title_update)
mp.register_event("start-file", title_update)
mp.register_event("end-file", title_update)
mp.register_event("playback-restart", title_update)
mp.add_periodic_timer(5, title_update)