// ==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) })()