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)


Comments

Subscribe to the comments RSS feed.

Leave Comment

Note to Verbose Commenters
If you can't fit everything you want to say in the comment below then you really should record a response show instead.

Note to Spammers
All comments are moderated. All links are checked by humans. We strip out all html. Feel free to record a show about yourself, or your industry, or any other topic we may find interesting. We also check shows for spam :).

Provide feedback
Your Name/Handle:
Title:
Comment:
Anti Spam Question: What does the letter P in HPR stand for?