Files
api-HydrusNetwork/public/userscripts/api-media-player-open-in-mpv.user.js
2026-03-26 03:26:37 -07:00

157 lines
4.6 KiB
JavaScript

// ==UserScript==
// @name API Media Player: Open in mpv
// @namespace https://local.api-media-player
// @version 0.1.0
// @description Redirect direct media playback from this app into mpv instead of playing inside the browser.
// @match *://*/*
// @run-at document-start
// @inject-into page
// @grant none
// ==/UserScript==
(() => {
const RECENT_OPEN_WINDOW_MS = 1500
const DIRECT_MEDIA_EXTENSION = /\.(mp4|m4v|mkv|webm|mov|mp3|m4a|flac|ogg|opus|wav|aac)(?:$|[?#])/i
const PRIVATE_IPV4_HOST = /^(?:10\.|127\.|192\.168\.|172\.(?:1[6-9]|2\d|3[0-1])\.)/
const LOCAL_HOST_SUFFIX = /(?:\.local|\.lan)$/i
let lastOpenedUrl = ''
let lastOpenedAt = 0
function isAllowedHost(hostname) {
return hostname === 'localhost'
|| hostname === '[::1]'
|| PRIVATE_IPV4_HOST.test(hostname)
|| LOCAL_HOST_SUFFIX.test(hostname)
}
function getMediaUrlFromElement(element) {
if (!element) return ''
return element.currentSrc
|| element.src
|| element.getAttribute('src')
|| element.querySelector('source[src]')?.getAttribute('src')
|| ''
}
function isDirectMediaUrl(rawUrl) {
if (!rawUrl) return false
try {
const url = new URL(rawUrl, location.href)
if (!/^https?:$/i.test(url.protocol)) return false
return DIRECT_MEDIA_EXTENSION.test(url.pathname)
|| /\/get_files\/file$/i.test(url.pathname)
|| url.searchParams.has('file_id')
} catch {
return false
}
}
function encodeUrlSafeBase64(value) {
const bytes = new TextEncoder().encode(value)
let binary = ''
for (const byte of bytes) {
binary += String.fromCharCode(byte)
}
return btoa(binary).replace(/\//g, '_').replace(/\+/g, '-').replace(/=+$/g, '')
}
function looksLikeVideo(url, element) {
if (element?.tagName === 'VIDEO') return true
try {
const pathname = new URL(url, location.href).pathname
return /\.(mp4|m4v|mkv|webm|mov)$/i.test(pathname)
} catch {
return false
}
}
function buildDesktopMpvUrl(url, title) {
const encodedUrl = encodeUrlSafeBase64(url)
const encodedTitle = title ? encodeUrlSafeBase64(title) : ''
const query = encodedTitle ? `?v_title=${encodedTitle}` : ''
return `mpv-handler://play/${encodedUrl}/${query}`
}
function buildAndroidMpvUrl(url, element) {
try {
const parsed = new URL(url, location.href)
const scheme = parsed.protocol.replace(':', '') || 'https'
const intentPath = `${parsed.host}${parsed.pathname}${parsed.search}`
const mimeType = looksLikeVideo(url, element) ? 'video/*' : 'audio/*'
return `intent://${intentPath}#Intent;scheme=${scheme};package=is.xyz.mpv;action=android.intent.action.VIEW;type=${mimeType};end`
} catch {
return url
}
}
function buildExternalPlayerUrl(url, title, element) {
if (/Android/i.test(navigator.userAgent || '')) {
return buildAndroidMpvUrl(url, element)
}
return buildDesktopMpvUrl(url, title)
}
function openInExternalPlayer(url, element) {
const now = Date.now()
if (url === lastOpenedUrl && now - lastOpenedAt < RECENT_OPEN_WINDOW_MS) {
return
}
lastOpenedUrl = url
lastOpenedAt = now
const title = document.title || ''
const targetUrl = buildExternalPlayerUrl(url, title, element)
location.assign(targetUrl)
}
function shouldInterceptCurrentPage() {
return isAllowedHost(location.hostname)
}
function rejectPlaybackRedirect() {
return Promise.reject(new DOMException('Playback redirected to external player', 'AbortError'))
}
function interceptElementPlayback(element) {
if (!shouldInterceptCurrentPage()) return false
const mediaUrl = getMediaUrlFromElement(element)
if (!isDirectMediaUrl(mediaUrl)) return false
try { element.pause() } catch {}
try { element.removeAttribute('src') } catch {}
try { element.load() } catch {}
openInExternalPlayer(mediaUrl, element)
return true
}
const originalPlay = HTMLMediaElement.prototype.play
HTMLMediaElement.prototype.play = function play(...args) {
if (interceptElementPlayback(this)) {
return rejectPlaybackRedirect()
}
return originalPlay.apply(this, args)
}
document.addEventListener('click', (event) => {
if (!shouldInterceptCurrentPage()) return
const target = event.target instanceof Element ? event.target.closest('a[href]') : null
if (!target) return
const href = target.getAttribute('href') || ''
if (!isDirectMediaUrl(href)) return
event.preventDefault()
event.stopPropagation()
openInExternalPlayer(href)
}, true)
})()