152 lines
4.3 KiB
JavaScript
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`)
|