mpv-handler setup
This folder contains the desktop playback helper assets used by API Media Player.
What is in this folder
setup-mpv-handler.mjs: cross-platform helper used bynpm run setup:mpv-handlerinstall-mpv-handler.ps1: Windows protocol registration scriptuninstall-mpv-handler.ps1: Windows protocol cleanup scripthandler-install.batandhandler-uninstall.bat: thin Windows wrappers for the PowerShell scriptsconfig.toml: template used when the helper needs to create or refresh anmpv-handlerconfigmpv-handler.exeandmpv-handler-debug.exe: bundled Windows handler binaries
Recommended commands
Windows
Run from an elevated PowerShell or Windows Terminal:
npm run setup:mpv-handler
Remove the registration later with:
npm run uninstall:mpv-handler
Linux
- Download and extract the upstream Linux release:
https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-linux-amd64.zip
- Point the helper at that extracted folder:
npm run setup:mpv-handler -- --root /path/to/extracted/mpv-handler-linux-amd64
The helper copies files into ~/.local/bin and ~/.local/share/applications, updates config.toml, and runs xdg-mime for both protocol handlers.
Useful flags
npm run setup:mpv-handler -- --help
npm run setup:mpv-handler -- --root /path/to/mpv-handler
npm run setup:mpv-handler -- --mpv /path/to/mpv
npm run setup:mpv-handler -- --ytdl /path/to/yt-dlp
npm run setup:mpv-handler -- --skip-config
npm run setup:mpv-handler -- --dry-run
--root points at the extracted upstream mpv-handler folder. On Windows it is optional because this repo already ships the handler files under scripts/. On Linux it is normally required.
What the helper does
Windows
- finds the bundled handler files in this folder unless you override
--root - creates or updates
config.toml - tries to detect
mpvandyt-dlpfromPATH - registers
mpv-handler://andmpv-handler-debug://through the PowerShell installer
Linux
- validates the extracted upstream release layout
- creates or updates
config.toml - copies the handler binary to
~/.local/bin/mpv-handler - copies the desktop entries to
~/.local/share/applications - rewrites
Exec=entries to the installed absolute binary path - runs
xdg-mime default ...for both schemes
Manual fallback
If you would rather install without the helper:
Windows
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install-mpv-handler.ps1 -InstallRoot .\scripts
Linux
cp /path/to/mpv-handler/mpv-handler ~/.local/bin/mpv-handler
cp /path/to/mpv-handler/mpv-handler.desktop ~/.local/share/applications/
cp /path/to/mpv-handler/mpv-handler-debug.desktop ~/.local/share/applications/
chmod +x ~/.local/bin/mpv-handler
xdg-mime default mpv-handler.desktop x-scheme-handler/mpv-handler
xdg-mime default mpv-handler-debug.desktop x-scheme-handler/mpv-handler-debug
Upstream project
Upstream mpv-handler releases and source:
https://github.com/akiirui/mpv-handler
https://github.com/akiirui/mpv-handler/releases
This repo uses the upstream protocol scheme and binaries; the docs here only describe the setup flow for API Media Player. *** Add File: c:\Forgejo\API-MediaPlayer\scripts\uninstall-mpv-handler.ps1 #Requires -Version 5.1 #Requires -RunAsAdministrator
[CmdletBinding()] param()
Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop'
function Remove-ProtocolKey { param( [Parameter(Mandatory = $true)] [string]$SchemeName )
$classesRoot = [Microsoft.Win32.Registry]::ClassesRoot try { $classesRoot.DeleteSubKeyTree($SchemeName, $false) } catch { } }
if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) { throw 'This uninstaller is only for Windows.' }
Remove-ProtocolKey -SchemeName 'mpv' Remove-ProtocolKey -SchemeName 'mpv-debug' Remove-ProtocolKey -SchemeName 'mpv-handler' Remove-ProtocolKey -SchemeName 'mpv-handler-debug'
Write-Host 'Successfully removed mpv-handler protocol registration.' -ForegroundColor Green *** Add File: c:\Forgejo\API-MediaPlayer\scripts\setup-mpv-handler.mjs #!/usr/bin/env node
import { spawnSync } from 'node:child_process' import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, chmodSync, writeFileSync } from 'node:fs' import os from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url'
const scriptPath = fileURLToPath(import.meta.url) const scriptDir = path.dirname(scriptPath) const templateConfigPath = path.join(scriptDir, 'config.toml')
const usage = `Usage: npm run setup:mpv-handler npm run setup:mpv-handler -- --root /path/to/mpv-handler npm run setup:mpv-handler -- --mpv /path/to/mpv --ytdl /path/to/yt-dlp
Options: --root Path to the extracted mpv-handler folder. --mpv Override the mpv executable path written to config.toml. --ytdl Override the yt-dlp executable path written to config.toml. --skip-config Do not create or update config.toml. --keep-existing Windows only. Keep existing protocol keys instead of replacing them. --dry-run Print the actions without changing files or running installers. --help Show this help text. `
function fail(message) {
console.error(Error: ${message})
process.exit(1)
}
function parseArgs(argv) { const options = { root: '', mpv: '', ytdl: '', skipConfig: false, keepExisting: false, dryRun: false, help: false, }
for (let index = 0; index < argv.length; index += 1) { const value = argv[index]
if (value === '--help' || value === '-h') {
options.help = true
continue
}
if (value === '--skip-config') {
options.skipConfig = true
continue
}
if (value === '--keep-existing') {
options.keepExisting = true
continue
}
if (value === '--dry-run') {
options.dryRun = true
continue
}
if (value === '--root' || value === '--mpv' || value === '--ytdl') {
const nextValue = argv[index + 1]
if (!nextValue || nextValue.startsWith('--')) {
fail(`Missing value for ${value}`)
}
if (value === '--root') options.root = nextValue
if (value === '--mpv') options.mpv = nextValue
if (value === '--ytdl') options.ytdl = nextValue
index += 1
continue
}
fail(`Unknown argument: ${value}`)
}
return options }
function escapeTomlString(value) { return value.replace(/\/g, '\\').replace(/"/g, '\"') }
function upsertTomlValue(content, key, value) {
const line = ${key} = "${escapeTomlString(value)}"
const pattern = new RegExp(^\\s*#?\\s*${key}\\s*=.*$, 'm')
if (pattern.test(content)) {
return content.replace(pattern, line)
}
const trimmed = content.trimEnd()
return trimmed ? ${trimmed}\n${line}\n : ${line}\n
}
function resolveOnPath(commandNames) { const locator = process.platform === 'win32' ? 'where.exe' : 'which'
for (const commandName of commandNames) { const result = spawnSync(locator, [commandName], { encoding: 'utf8' }) if (result.status !== 0) continue
const match = result.stdout
.split(/\r?\n/)
.map((entry) => entry.trim())
.find((entry) => entry)
if (match && existsSync(match)) {
return match
}
}
return '' }
function isFile(candidatePath) { try { return statSync(candidatePath).isFile() } catch { return false } }
function looksLikeRoot(rootPath) { const requiredFiles = process.platform === 'win32' ? ['mpv-handler.exe', 'mpv-handler-debug.exe'] : process.platform === 'linux' ? ['mpv-handler', 'mpv-handler.desktop', 'mpv-handler-debug.desktop'] : ['mpv-handler']
return requiredFiles.every((fileName) => isFile(path.join(rootPath, fileName))) }
function resolveRoot(options) { const candidates = [] if (options.root) candidates.push(path.resolve(options.root)) candidates.push(scriptDir) candidates.push(process.cwd())
const seen = new Set() for (const candidate of candidates) { if (!candidate || seen.has(candidate)) continue seen.add(candidate) if (looksLikeRoot(candidate)) { return candidate } }
if (process.platform === 'win32') { fail('Could not find mpv-handler files. Re-run with --root pointing at the extracted mpv-handler folder.') }
if (process.platform === 'linux') { fail('Could not find a Linux mpv-handler release. Download and extract the upstream archive, then pass --root /path/to/extracted/mpv-handler-linux-amd64.') }
fail('Automatic setup is not available for this platform yet.') }
function ensureConfig(rootPath, options) { const configPath = path.join(rootPath, 'config.toml') if (options.skipConfig) { return { configPath, changed: false, mpvPath: '', ytdlPath: '' } }
let content = existsSync(configPath) ? readFileSync(configPath, 'utf8') : readFileSync(templateConfigPath, 'utf8')
const detectedMpv = options.mpv || resolveOnPath(process.platform === 'win32' ? ['mpv.com', 'mpv.exe', 'mpv'] : ['mpv']) const detectedYtdl = options.ytdl || resolveOnPath(process.platform === 'win32' ? ['yt-dlp.exe', 'yt-dlp'] : ['yt-dlp'])
let changed = !existsSync(configPath) let nextContent = content
if (detectedMpv) { const updated = upsertTomlValue(nextContent, 'mpv', detectedMpv) changed ||= updated !== nextContent nextContent = updated }
if (detectedYtdl) { const updated = upsertTomlValue(nextContent, 'ytdl', detectedYtdl) changed ||= updated !== nextContent nextContent = updated }
if (changed && !options.dryRun) { writeFileSync(configPath, nextContent, 'utf8') }
return { configPath, changed, mpvPath: detectedMpv, ytdlPath: detectedYtdl, } }
function runWindowsSetup(rootPath, options) { const powershell = resolveOnPath(['powershell.exe', 'pwsh.exe']) || 'powershell.exe' const installScript = path.join(scriptDir, 'install-mpv-handler.ps1') const args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', installScript, '-InstallRoot', rootPath]
if (options.keepExisting) { args.push('-KeepExistingProtocolKeys') }
if (options.dryRun) {
console.log('Dry run: would execute Windows installer')
console.log(${powershell} ${args.map((value) => JSON.stringify(value)).join(' ')})
return
}
const result = spawnSync(powershell, args, { stdio: 'inherit' }) if (result.status !== 0) { fail('Windows protocol registration failed. Re-run the command from an elevated PowerShell or Windows Terminal.') } }
function rewriteDesktopExec(content, targetBinary) {
return content.replace(/^Exec=.*$/m, (line) => {
const prefix = 'Exec='
const rest = line.slice(prefix.length).trim()
const firstSpaceIndex = rest.indexOf(' ')
const suffix = firstSpaceIndex === -1 ? '' : rest.slice(firstSpaceIndex)
return ${prefix}${targetBinary}${suffix}
})
}
function runLinuxSetup(rootPath, options) { const localBin = path.join(os.homedir(), '.local', 'bin') const applicationsDir = path.join(os.homedir(), '.local', 'share', 'applications') const targetBinary = path.join(localBin, 'mpv-handler') const copies = [ { source: path.join(rootPath, 'mpv-handler'), target: targetBinary, executable: true, }, { source: path.join(rootPath, 'mpv-handler.desktop'), target: path.join(applicationsDir, 'mpv-handler.desktop'), patchExec: true, }, { source: path.join(rootPath, 'mpv-handler-debug.desktop'), target: path.join(applicationsDir, 'mpv-handler-debug.desktop'), patchExec: true, }, ]
if (options.dryRun) {
console.log('Dry run: would install Linux desktop files to ~/.local')
for (const item of copies) {
console.log(copy ${item.source} -> ${item.target})
}
console.log('xdg-mime default mpv-handler.desktop x-scheme-handler/mpv-handler')
console.log('xdg-mime default mpv-handler-debug.desktop x-scheme-handler/mpv-handler-debug')
return
}
mkdirSync(localBin, { recursive: true }) mkdirSync(applicationsDir, { recursive: true })
for (const item of copies) { if (item.patchExec) { const content = readFileSync(item.source, 'utf8') writeFileSync(item.target, rewriteDesktopExec(content, targetBinary), 'utf8') continue }
copyFileSync(item.source, item.target)
if (item.executable) {
chmodSync(item.target, 0o755)
}
}
for (const args of [
['default', 'mpv-handler.desktop', 'x-scheme-handler/mpv-handler'],
['default', 'mpv-handler-debug.desktop', 'x-scheme-handler/mpv-handler-debug'],
]) {
const result = spawnSync('xdg-mime', args, { stdio: 'inherit' })
if (result.status !== 0) {
fail(xdg-mime failed for ${args[2]}. Run the command manually after fixing your desktop environment registration.)
}
}
}
function main() { const options = parseArgs(process.argv.slice(2))
if (options.help) { console.log(usage) return }
if (!['win32', 'linux', 'darwin'].includes(process.platform)) {
fail(Unsupported platform: ${process.platform})
}
if (process.platform === 'darwin') { console.log('Automatic macOS protocol registration is not available in this repo yet.') console.log('The app can still browse media on macOS, but desktop playback setup must be handled manually.') return }
const rootPath = resolveRoot(options) const configResult = ensureConfig(rootPath, options)
console.log(Platform: ${process.platform})
console.log(mpv-handler root: ${rootPath})
console.log(config.toml: ${configResult.configPath}${configResult.changed ? ' (updated)' : ' (unchanged)'})
console.log(mpv: ${configResult.mpvPath || 'not detected'})
console.log(yt-dlp: ${configResult.ytdlPath || 'not detected'})
if (process.platform === 'win32') { runWindowsSetup(rootPath, options) } else if (process.platform === 'linux') { runLinuxSetup(rootPath, options) }
console.log('mpv-handler setup complete.') }
main()