Files
gdown/tools/native-host/host.mjs

152 lines
4.3 KiB
JavaScript

import { appendFile, mkdir } from 'node:fs/promises'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
const textEncoder = new TextEncoder()
let readBuffer = Buffer.alloc(0)
let pendingRequests = 0
let stdinEnded = false
function sendMessage(payload) {
const body = Buffer.from(JSON.stringify(payload), 'utf8')
const len = Buffer.alloc(4)
len.writeUInt32LE(body.length, 0)
process.stdout.write(len)
process.stdout.write(body)
}
function parseHeaders(lines = []) {
const map = new Map()
for (const line of lines) {
const idx = line.indexOf(':')
if (idx <= 0) continue
const key = line.slice(0, idx).trim().toLowerCase()
const value = line.slice(idx + 1).trim()
if (!key || !value) continue
map.set(key, value)
}
return map
}
function parseCookieHeader(lines = []) {
const headers = parseHeaders(lines)
return headers.get('cookie') || ''
}
function queueFilePath() {
return join(homedir(), '.gdown', 'external_add_queue.jsonl')
}
async function enqueueExternalAdd(payload) {
const filePath = queueFilePath()
await mkdir(dirname(filePath), { recursive: true })
await appendFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8')
}
function focusGdownApp() {
return Promise.resolve({ ok: true, note: 'focus disabled by design' })
}
async function handleRequest(message) {
const action = String(message?.action || '').trim()
if (action === 'ping') {
return {
ok: true,
version: '0.1.0',
host: 'org.gdown.nativehost',
capabilities: ['ping', 'addUri', 'focus', 'extractor:yt-dlp'],
}
}
if (action === 'addUri') {
const url = String(message?.url || '').trim()
if (!url) return { ok: false, error: 'url is required' }
const referer = String(message?.referer || '').trim()
const userAgent = String(message?.userAgent || '').trim()
const out = String(message?.out || '').trim()
const dir = String(message?.dir || '').trim()
const cookie = String(message?.cookie || '').trim()
const authorization = String(message?.authorization || '').trim()
const proxy = String(message?.proxy || '').trim()
const split = Number(message?.split || 0)
const extractor = String(message?.extractor || '').trim()
const format = String(message?.format || '').trim()
const parsedCookie = parseCookieHeader(Array.isArray(message?.headers) ? message.headers : [])
const cookieValue = cookie || parsedCookie
await enqueueExternalAdd({
url,
referer: referer || undefined,
userAgent: userAgent || undefined,
out: out || undefined,
dir: dir || undefined,
cookie: cookieValue || undefined,
authorization: authorization || undefined,
proxy: proxy || undefined,
split: Number.isFinite(split) && split > 0 ? Math.round(split) : undefined,
extractor: extractor || undefined,
format: format || undefined,
})
return {
ok: true,
pending: true,
mode: 'prompt',
requestId: `pending-${Date.now()}`,
}
}
if (action === 'focus') {
return focusGdownApp()
}
return { ok: false, error: `unknown action: ${action || '(empty)'}` }
}
async function drainBuffer() {
while (readBuffer.length >= 4) {
const bodyLength = readBuffer.readUInt32LE(0)
if (readBuffer.length < 4 + bodyLength) return
const body = readBuffer.subarray(4, 4 + bodyLength)
readBuffer = readBuffer.subarray(4 + bodyLength)
let payload
try {
payload = JSON.parse(body.toString('utf8'))
} catch (error) {
sendMessage({ ok: false, error: `invalid JSON payload: ${String(error)}` })
continue
}
try {
pendingRequests += 1
const result = await handleRequest(payload)
sendMessage(result)
} catch (error) {
sendMessage({ ok: false, error: String(error) })
} finally {
pendingRequests -= 1
if (stdinEnded && pendingRequests === 0) {
process.exit(0)
}
}
}
}
process.stdin.on('data', async (chunk) => {
readBuffer = Buffer.concat([readBuffer, chunk])
await drainBuffer()
})
process.stdin.on('end', () => {
stdinEnded = true
if (pendingRequests === 0) {
process.exit(0)
}
})
// Emit one line for manual shell smoke visibility (not used by native messaging framing).
process.stderr.write(`[native-host] started pid=${process.pid}\n`)