2026-04-18 18:12:17 -07:00
#!/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> Path to the extracted mpv-handler folder.
--mpv <path> Override the mpv executable path written to config.toml.
--ytdl <path> 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' ) {
2026-05-17 12:29:13 -07:00
const archHint = process . arch === 'x64'
? 'Download and extract the upstream archive, then pass --root /path/to/extracted/mpv-handler-linux-amd64.'
: ` The upstream project only publishes an official linux-amd64 archive. On ${ process . arch } , build or obtain a compatible mpv-handler binary and pass --root /path/to/extracted/mpv-handler. `
fail ( ` Could not find a Linux mpv-handler release. ${ archHint } ` )
2026-04-18 18:12:17 -07:00
}
fail ( 'Automatic setup is not available for this platform yet.' )
}
2026-05-17 12:29:13 -07:00
function getLinuxConfigDir ( ) {
const xdgConfigHome = process . env . XDG _CONFIG _HOME ? . trim ( )
return xdgConfigHome
? path . join ( xdgConfigHome , 'mpv-handler' )
: path . join ( os . homedir ( ) , '.config' , 'mpv-handler' )
}
function getConfigPath ( rootPath ) {
if ( process . platform === 'linux' ) {
return path . join ( getLinuxConfigDir ( ) , 'config.toml' )
}
return path . join ( rootPath , 'config.toml' )
}
2026-04-18 18:12:17 -07:00
function ensureConfig ( rootPath , options ) {
2026-05-17 12:29:13 -07:00
const configPath = getConfigPath ( rootPath )
2026-04-18 18:12:17 -07:00
if ( options . skipConfig ) {
return { configPath , changed : false , mpvPath : '' , ytdlPath : '' }
}
const configExists = existsSync ( configPath )
2026-05-17 12:29:13 -07:00
const bundledConfigPath = path . join ( rootPath , 'config.toml' )
2026-04-18 18:12:17 -07:00
const content = configExists
? readFileSync ( configPath , 'utf8' )
2026-05-17 12:29:13 -07:00
: existsSync ( bundledConfigPath )
? readFileSync ( bundledConfigPath , 'utf8' )
: readFileSync ( templateConfigPath , 'utf8' )
2026-04-18 18:12:17 -07:00
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 = ! configExists
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 ) {
2026-05-17 12:29:13 -07:00
mkdirSync ( path . dirname ( configPath ) , { recursive : true } )
2026-04-18 18:12:17 -07:00
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 ( )