@hyrious/blivec
Advanced tools
Comparing version 0.3.4 to 0.3.5
263
dist/bin.js
@@ -7,12 +7,6 @@ #!/usr/bin/env node | ||
import { join } from "path"; | ||
import rl from "readline"; | ||
import cp from "child_process"; | ||
import { setTimeout } from "timers/promises"; | ||
import { | ||
Connection, | ||
getRoomPlayInfo, | ||
sendDanmaku, | ||
testUrl | ||
} from "./index.js"; | ||
const help_text = ` | ||
import readline from "readline"; | ||
import { Connection, getRoomPlayInfo, sendDanmaku, testUrl } from "./index.js"; | ||
const help = ` | ||
Usage: bl <room_id> # listen danmaku | ||
@@ -29,10 +23,10 @@ --json # print all events in json | ||
--mpv # open in mpv instead | ||
--on-close=<behavior> # do something on window close | ||
default # restart player | ||
ask # ask quality again | ||
quit # quit DD mode | ||
`.trim(); | ||
const hasColors = tty.WriteStream.prototype.hasColors(); | ||
function help() { | ||
console.log(help_text); | ||
} | ||
function format(beg, end) { | ||
return hasColors ? (str) => "\x1B[" + beg + "m" + str + "\x1B[" + end + "m" : (str) => str; | ||
} | ||
const has_colors = tty.WriteStream.prototype.hasColors(); | ||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
const format = (s, e) => has_colors ? (m) => "\x1B[" + s + "m" + m + "\x1B[" + e + "m" : (m) => m; | ||
const red = format(31, 39); | ||
@@ -42,62 +36,60 @@ const cyan = format(36, 39); | ||
const bgRed = format(41, 49); | ||
function print_error(msg) { | ||
console.error(`${bgRed(black("\u2009ERROR\u2009"))} ${red(msg)}`); | ||
const bgCyan = format(46, 49); | ||
const log = { | ||
error: (msg) => console.error(`${bgRed(black(" ERROR "))} ${red(msg)}`), | ||
info: (msg) => console.error(`${bgCyan(black(" BLIVC "))} ${cyan(msg)}`), | ||
catch_error: (error) => log.error(error.message) | ||
}; | ||
let repl; | ||
function setup_repl() { | ||
if (!repl) { | ||
repl = readline.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
prompt: "" | ||
}); | ||
repl.on("SIGINT", () => { | ||
repl && repl.close(); | ||
process.exit(0); | ||
}); | ||
} | ||
return repl; | ||
} | ||
function print_info(msg) { | ||
console.error(`${cyan("\u2009INFO\u2009")} ${cyan(msg)}`); | ||
function quit_repl() { | ||
if (repl) { | ||
repl.close(); | ||
repl = void 0; | ||
} | ||
} | ||
function error_exit(error) { | ||
print_error(error.message); | ||
process.exit(1); | ||
} | ||
function listen(id, { json = false } = {}) { | ||
let repl; | ||
function setup_repl() { | ||
if (process.stdout.isTTY && !repl) { | ||
console.log('[blivec] type "> message" to send danmaku'); | ||
repl = rl.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
prompt: "" | ||
}); | ||
repl.on("line", (line) => { | ||
line = line.trim(); | ||
if (line.startsWith("> ") && line.length > 2) { | ||
rl.moveCursor(process.stdout, 0, -1); | ||
rl.clearLine(process.stdout, 0); | ||
line = line.slice(2); | ||
send(id, line).catch((error) => { | ||
print_error(error.message); | ||
}); | ||
} else { | ||
console.log( | ||
'[blivec] message needs to start with "> " (space is required)' | ||
); | ||
} | ||
}); | ||
repl.on("SIGINT", () => { | ||
repl && repl.close(); | ||
process.exit(0); | ||
}); | ||
} | ||
} | ||
let first = 0; | ||
let count = 0; | ||
const events = json ? { | ||
init: (data) => console.log(JSON.stringify({ cmd: "init", data })), | ||
message: (data) => console.log(JSON.stringify(data)), | ||
error: (err) => print_error(err.message) | ||
error: log.catch_error | ||
} : { | ||
init({ title, live_status, live_start_time }) { | ||
if (first === 0) { | ||
if (count === 0) { | ||
if (live_status === 1) { | ||
const time = new Date(live_start_time * 1e3).toLocaleString(); | ||
console.log(`[blivec] listening ${title} (start at ${time})`); | ||
log.info(`listening ${title} (start at ${time})`); | ||
} else { | ||
console.log(`[blivec] listening ${title} (offline)`); | ||
log.info(`listening ${title} (offline)`); | ||
} | ||
setup_repl(); | ||
const repl2 = setup_repl(); | ||
repl2.on("line", (line) => { | ||
line = line.trim(); | ||
if (line.startsWith("> ") && line.length > 2) { | ||
readline.moveCursor(process.stdout, 0, -1); | ||
readline.clearLine(process.stdout, 0); | ||
line = line.slice(2); | ||
send(id, line).catch(log.catch_error); | ||
} else { | ||
log.info('message needs to start with "> " (space is required)'); | ||
} | ||
}); | ||
} else { | ||
print_info(`reconnected (x${first})`); | ||
log.info(`reconnected (x${count})`); | ||
} | ||
first++; | ||
count++; | ||
}, | ||
@@ -111,4 +103,4 @@ message(a) { | ||
}, | ||
error: (err) => print_error(err.message), | ||
quit: () => repl && repl.close(), | ||
error: log.catch_error, | ||
quit: quit_repl, | ||
pause: () => repl && repl.pause(), | ||
@@ -142,3 +134,3 @@ resume: () => repl && repl.resume() | ||
if (!path) { | ||
console.error('Please create a file "cookie.txt" in current directory.'); | ||
log.error('Please create a file "cookie.txt" in current directory.'); | ||
example(); | ||
@@ -158,9 +150,6 @@ process.exit(1); | ||
if (env.SESSDATA && env.bili_jct) { | ||
await sendDanmaku(id, message, env).catch((err) => { | ||
print_error(err.message); | ||
}); | ||
await sendDanmaku(id, message, env).catch(log.catch_error); | ||
} else { | ||
print_error("Invalid cookie.txt"); | ||
log.error("Invalid cookie.txt"); | ||
example(); | ||
process.exit(1); | ||
} | ||
@@ -171,5 +160,4 @@ } | ||
const info = await getRoomPlayInfo(id); | ||
const title = info.title; | ||
if (!json) { | ||
console.log("Title:", title); | ||
console.log("Title:", info.title); | ||
console.log(); | ||
@@ -188,10 +176,7 @@ } | ||
} catch (err) { | ||
error_exit(err); | ||
log.catch_error(err); | ||
} | ||
} | ||
async function D(id, { interval = 1, mpv = false } = {}) { | ||
console.log( | ||
`[blivec] DD ${id}`, | ||
interval > 0 ? `every ${interval} minutes` : "once" | ||
); | ||
async function D(id, { interval = 1, mpv = false, on_close = "default" } = {}) { | ||
log.info(`DD ${id} ${interval > 0 ? `every ${interval} minutes` : "once"}`); | ||
let con; | ||
@@ -207,9 +192,9 @@ let child; | ||
info = await getRoomPlayInfo(id).catch(() => null); | ||
if (info && !await testUrl(fst(info.streams).url, headers)) | ||
if (info && !await testUrl(first(info.streams).url, headers)) | ||
info = null; | ||
if (info || interval === 0) | ||
break; | ||
await setTimeout(interval * 60 * 1e3); | ||
await delay(interval * 60 * 1e3); | ||
} | ||
function fst(obj) { | ||
function first(obj) { | ||
for (const key in obj) | ||
@@ -223,38 +208,30 @@ return obj[key]; | ||
const { title, streams } = info; | ||
console.log("[blivec] " + "=====".repeat(10)); | ||
console.log("[blivec] Title:", title); | ||
console.log("[blivec] " + "=====".repeat(10)); | ||
console.log("[blivec] Available streams:"); | ||
log.info("=====".repeat(12)); | ||
log.info("Title: " + title); | ||
log.info("=====".repeat(12)); | ||
log.info("Available streams:"); | ||
const names = Object.keys(streams); | ||
const width = names.length > 9 ? 2 : 1; | ||
names.forEach((name, index) => { | ||
console.log(` ${String(index + 1).padStart(width)}: ${name}`); | ||
const choices = []; | ||
for (let i2 = 0; i2 < names.length; i2++) { | ||
const name = names[i2]; | ||
log.info(` ${String(i2 + 1).padStart(width)}: ${name}`); | ||
choices.push(i2 + 1); | ||
} | ||
choices.push("Y=1", "max", "n"); | ||
const repl2 = setup_repl(); | ||
const answer = await new Promise((resolve) => { | ||
repl2.question(`Choose a stream, or give up: (${choices.join("/")}) `, (a) => resolve(a || "Y")); | ||
}); | ||
const input = rl.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout | ||
}); | ||
const choices = Array.from({ length: names.length }, (_, i2) => i2 + 1); | ||
const hint = [...choices, "Y=1", "max", "n"].join("/"); | ||
const choice = await new Promise((resolve) => { | ||
input.question( | ||
`[blivec] Choose a stream, or give up: (${hint}) `, | ||
(a) => { | ||
input.close(); | ||
resolve(a || "Y"); | ||
} | ||
); | ||
}); | ||
const i = Number.parseInt(choice); | ||
let selected2 = names[0]; | ||
let i = Number.parseInt(answer); | ||
if (Number.isSafeInteger(i) && 1 <= i && i <= names.length) { | ||
selected2 = names[i - 1]; | ||
} else { | ||
const a = choice[0].toLowerCase(); | ||
if (a === "n") { | ||
return; | ||
} else if (a === "m") { | ||
selected2 = names.reduce( | ||
(a2, b) => streams[a2].qn > streams[b].qn ? a2 : b | ||
); | ||
switch (answer[0].toLowerCase()) { | ||
case "n": | ||
return; | ||
case "m": | ||
selected2 = names.reduce((a, b) => streams[a].qn > streams[b].qn ? a : b); | ||
break; | ||
} | ||
@@ -265,3 +242,2 @@ } | ||
function play(url, title) { | ||
let child2; | ||
if (mpv) { | ||
@@ -273,4 +249,3 @@ const args = ["--quiet"]; | ||
args.push(url); | ||
child2 = cp.spawn("mpv", args, { stdio: "inherit", detached: true }); | ||
child2.unref(); | ||
return cp.spawn("mpv", args, { stdio: "ignore", detached: true }); | ||
} else { | ||
@@ -280,19 +255,25 @@ const args = ["-hide_banner", "-loglevel", "error"]; | ||
args.push("-window_title", title); | ||
args.push("-x", "1280", "-y", "720"); | ||
args.push("-x", "720", "-y", "405"); | ||
args.push(url); | ||
child2 = cp.spawn("ffplay", args, { stdio: "inherit" }); | ||
return cp.spawn("ffplay", args, { stdio: "ignore" }); | ||
} | ||
return child2; | ||
} | ||
let selected; | ||
async function replay() { | ||
async function replay(initial = true) { | ||
const info = await poll(); | ||
if (!info) | ||
return; | ||
const { title } = info; | ||
selected ||= await ask(info); | ||
process.exit(0); | ||
if (initial) { | ||
selected = await ask(info); | ||
} else if (on_close === "default") { | ||
selected ||= await ask(info); | ||
} else if (on_close === "ask") { | ||
selected = await ask(info); | ||
} else if (on_close === "quit" || on_close === "exit") { | ||
selected = void 0; | ||
} | ||
if (!selected) | ||
return; | ||
console.log("[blivec] Now playing:", `[${selected}] ${title}`); | ||
child = play(info.streams[selected].url, title); | ||
process.exit(0); | ||
log.info(`Now playing: [${selected}] ${info.title}`); | ||
child = play(info.streams[selected].url, info.title); | ||
con ||= listen(id); | ||
@@ -302,3 +283,4 @@ con.resume(); | ||
con.pause(); | ||
setTimeout(100).then(replay); | ||
log.info('to exit, press "Ctrl+C" in the console'); | ||
setTimeout(replay, 100, false); | ||
}); | ||
@@ -313,3 +295,3 @@ } | ||
} else { | ||
process.kill(-child.pid, "SIGTERM"); | ||
child.kill(); | ||
} | ||
@@ -327,3 +309,3 @@ }; | ||
else | ||
console.log("[blivec] closing..."); | ||
log.info("closing..."); | ||
con.close(); | ||
@@ -342,2 +324,3 @@ }); | ||
let mpv = false; | ||
let on_close = "default"; | ||
for (const arg of rest) { | ||
@@ -349,28 +332,22 @@ if (arg.startsWith("--interval=")) { | ||
} else { | ||
console.error("Invalid interval, expect a number >= 0"); | ||
log.error("Invalid interval, expect a number >= 0"); | ||
process.exit(1); | ||
} | ||
} | ||
if (arg.startsWith("--on-close=")) { | ||
const value = arg.slice(11); | ||
if (["default", "ask", "quit", "exit"].includes(value)) { | ||
on_close = value; | ||
} else { | ||
log.error("Invalid on-close option, expect 'default' 'ask' 'quit'"); | ||
process.exit(1); | ||
} | ||
} | ||
if (arg === "--mpv") | ||
mpv = true; | ||
} | ||
const con = await D(id, { interval, mpv }); | ||
const con = await D(id, { interval, mpv, on_close }); | ||
con && sigint(con); | ||
} | ||
} else { | ||
help(); | ||
} | ||
} else { | ||
const id = Number.parseInt(arg1); | ||
const json = arg2 === "--json"; | ||
if (Number.isSafeInteger(id) && id > 0) { | ||
if (arg2 && !json) { | ||
await send(id, arg2); | ||
} else { | ||
const con = listen(id, { json }); | ||
sigint(con, { json }); | ||
} | ||
} else { | ||
help(); | ||
} | ||
} |
@@ -14,5 +14,3 @@ "use strict"; | ||
}; | ||
const get = (url) => new Promise( | ||
(resolve, reject) => https.get(url, text(resolve)).on("error", reject) | ||
); | ||
const get = (url) => new Promise((resolve, reject) => https.get(url, text(resolve)).on("error", reject)); | ||
const inflateAsync = /* @__PURE__ */ promisify(inflate); | ||
@@ -167,5 +165,3 @@ const brotliDecompressAsync = /* @__PURE__ */ promisify(brotliDecompress); | ||
let rs = await Promise.all(tasks); | ||
return rs.flatMap( | ||
(r) => r.protocol === 2 || r.protocol === 3 ? r.data : r | ||
); | ||
return rs.flatMap((r) => r.protocol === 2 || r.protocol === 3 ? r.data : r); | ||
} | ||
@@ -172,0 +168,0 @@ async _decode2(buffer) { |
{ | ||
"name": "@hyrious/blivec", | ||
"version": "0.3.4", | ||
"version": "0.3.5", | ||
"description": "bilibili live cli", | ||
@@ -16,2 +16,7 @@ "type": "module", | ||
"types": "./src/index.ts", | ||
"scripts": { | ||
"test": "node dist/bin.js", | ||
"clean": "rimraf dist", | ||
"build": "esbuild src/index.ts src/bin.ts --target=node16.15 --outdir=dist" | ||
}, | ||
"engines": { | ||
@@ -32,8 +37,3 @@ "node": ">=16.15" | ||
"esbuild": "^0.17.8" | ||
}, | ||
"scripts": { | ||
"test": "node dist/bin.js", | ||
"clean": "rimraf dist", | ||
"build": "esbuild src/index.ts src/bin.ts --target=node16.15 --outdir=dist" | ||
} | ||
} | ||
} |
282
src/bin.ts
@@ -6,14 +6,7 @@ #!/usr/bin/env node | ||
import { join } from "path"; | ||
import rl from "readline"; | ||
import cp, { ChildProcess } from "child_process"; | ||
import { setTimeout } from "timers/promises"; | ||
import { | ||
Connection, | ||
Events, | ||
getRoomPlayInfo, | ||
sendDanmaku, | ||
testUrl, | ||
} from "./index.js"; | ||
import cp from "child_process"; | ||
import readline from "readline"; | ||
import { Connection, Events, getRoomPlayInfo, sendDanmaku, testUrl } from "./index.js"; | ||
const help_text = ` | ||
const help = ` | ||
Usage: bl <room_id> # listen danmaku | ||
@@ -30,16 +23,14 @@ --json # print all events in json | ||
--mpv # open in mpv instead | ||
--on-close=<behavior> # do something on window close | ||
default # restart player | ||
ask # ask quality again | ||
quit # quit DD mode | ||
`.trim(); | ||
const hasColors = tty.WriteStream.prototype.hasColors(); | ||
const has_colors = tty.WriteStream.prototype.hasColors(); | ||
function help() { | ||
console.log(help_text); | ||
} | ||
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)); | ||
function format(beg: number, end: number) { | ||
return hasColors | ||
? (str: any) => "\x1B[" + beg + "m" + str + "\x1B[" + end + "m" | ||
: (str: any) => str; | ||
} | ||
const format = (s: number, e: number) => | ||
has_colors ? (m: string) => "\x1B[" + s + "m" + m + "\x1B[" + e + "m" : (m: string) => m; | ||
const red = format(31, 39); | ||
@@ -49,49 +40,38 @@ const cyan = format(36, 39); | ||
const bgRed = format(41, 49); | ||
function print_error(msg: string) { | ||
console.error(`${bgRed(black("\u2009ERROR\u2009"))} ${red(msg)}`); | ||
} | ||
const bgCyan = format(46, 49); | ||
const log = { | ||
error: (msg: string) => console.error(`${bgRed(black(" ERROR "))} ${red(msg)}`), | ||
info: (msg: string) => console.error(`${bgCyan(black(" BLIVC "))} ${cyan(msg)}`), | ||
catch_error: (error: Error) => log.error(error.message), | ||
}; | ||
function print_info(msg: string) { | ||
console.error(`${cyan("\u2009INFO\u2009")} ${cyan(msg)}`); | ||
// Reuse this repl during the whole program | ||
// 1. Listen 'line' event in danmaku mode to send message | ||
// 2. Question about the stream quality in DD mode, this will temporarily eat the next 'line' event, | ||
// @see https://github.com/nodejs/node/blob/-/lib/internal/readline/interface.js#L408 | ||
let repl: readline.Interface | undefined; | ||
function setup_repl() { | ||
if (!repl) { | ||
repl = readline.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
prompt: "", | ||
}); | ||
repl.on("SIGINT", () => { | ||
repl && repl.close(); | ||
process.exit(0); // trigger process event 'exit' | ||
}); | ||
} | ||
return repl; | ||
} | ||
function error_exit(error: Error): never { | ||
print_error(error.message); | ||
process.exit(1); | ||
function quit_repl() { | ||
if (repl) { | ||
repl.close(); | ||
repl = void 0; | ||
} | ||
} | ||
function listen(id: number, { json = false } = {}) { | ||
let repl: rl.Interface | undefined; | ||
let count = 0; | ||
function setup_repl() { | ||
if (process.stdout.isTTY && !repl) { | ||
console.log('[blivec] type "> message" to send danmaku'); | ||
repl = rl.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
prompt: "", | ||
}); | ||
repl.on("line", (line) => { | ||
line = line.trim(); | ||
if (line.startsWith("> ") && line.length > 2) { | ||
rl.moveCursor(process.stdout, 0, -1); // move up | ||
rl.clearLine(process.stdout, 0); // clear the user input | ||
line = line.slice(2); | ||
send(id, line).catch((error: Error) => { | ||
print_error(error.message); | ||
}); | ||
} else { | ||
console.log( | ||
'[blivec] message needs to start with "> " (space is required)' | ||
); | ||
} | ||
}); | ||
repl.on("SIGINT", () => { | ||
repl && repl.close(); | ||
process.exit(0); | ||
}); | ||
} | ||
} | ||
let first = 0; | ||
const events: Events = json | ||
@@ -101,18 +81,29 @@ ? { | ||
message: (data) => console.log(JSON.stringify(data)), | ||
error: (err) => print_error(err.message), | ||
error: log.catch_error, | ||
} | ||
: { | ||
init({ title, live_status, live_start_time }) { | ||
if (first === 0) { | ||
if (count === 0) { | ||
if (live_status === 1) { | ||
const time = new Date(live_start_time * 1000).toLocaleString(); | ||
console.log(`[blivec] listening ${title} (start at ${time})`); | ||
log.info(`listening ${title} (start at ${time})`); | ||
} else { | ||
console.log(`[blivec] listening ${title} (offline)`); | ||
log.info(`listening ${title} (offline)`); | ||
} | ||
setup_repl(); | ||
const repl = setup_repl(); | ||
repl.on("line", (line) => { | ||
line = line.trim(); | ||
if (line.startsWith("> ") && line.length > 2) { | ||
readline.moveCursor(process.stdout, 0, -1); // move up | ||
readline.clearLine(process.stdout, 0); // clear the user input | ||
line = line.slice(2); | ||
send(id, line).catch(log.catch_error); | ||
} else { | ||
log.info('message needs to start with "> " (space is required)'); | ||
} | ||
}); | ||
} else { | ||
print_info(`reconnected (x${first})`); | ||
log.info(`reconnected (x${count})`); | ||
} | ||
first++; | ||
count++; | ||
}, | ||
@@ -126,4 +117,4 @@ message(a) { | ||
}, | ||
error: (err) => print_error(err.message), | ||
quit: () => repl && repl.close(), | ||
error: log.catch_error, | ||
quit: quit_repl, | ||
pause: () => repl && repl.pause(), | ||
@@ -157,3 +148,3 @@ resume: () => repl && repl.resume(), | ||
if (!path) { | ||
console.error('Please create a file "cookie.txt" in current directory.'); | ||
log.error('Please create a file "cookie.txt" in current directory.'); | ||
example(); | ||
@@ -175,9 +166,6 @@ process.exit(1); | ||
if (env.SESSDATA && env.bili_jct) { | ||
await sendDanmaku(id, message, env).catch((err) => { | ||
print_error(err.message); | ||
}); | ||
await sendDanmaku(id, message, env).catch(log.catch_error); | ||
} else { | ||
print_error("Invalid cookie.txt"); | ||
log.error("Invalid cookie.txt"); | ||
example(); | ||
process.exit(1); | ||
} | ||
@@ -189,5 +177,4 @@ } | ||
const info = await getRoomPlayInfo(id); | ||
const title = info.title; | ||
if (!json) { | ||
console.log("Title:", title); | ||
console.log("Title:", info.title); | ||
console.log(); | ||
@@ -205,12 +192,9 @@ } | ||
} | ||
} catch (err: any) { | ||
error_exit(err); | ||
} catch (err) { | ||
log.catch_error(err); | ||
} | ||
} | ||
async function D(id: number, { interval = 1, mpv = false } = {}) { | ||
console.log( | ||
`[blivec] DD ${id}`, | ||
interval > 0 ? `every ${interval} minutes` : "once" | ||
); | ||
async function D(id: number, { interval = 1, mpv = false, on_close = "default" } = {}) { | ||
log.info(`DD ${id} ${interval > 0 ? `every ${interval} minutes` : "once"}`); | ||
@@ -230,7 +214,7 @@ let con!: Connection; | ||
info = await getRoomPlayInfo(id).catch(() => null); | ||
if (info && !(await testUrl(fst(info.streams).url, headers))) info = null; | ||
if (info && !(await testUrl(first(info.streams).url, headers))) info = null; | ||
if (info || interval === 0) break; | ||
await setTimeout(interval * 60 * 1000); | ||
await delay(interval * 60 * 1000); | ||
} | ||
function fst<T extends {}>(obj: T): T[keyof T] { | ||
function first<T extends {}>(obj: T): T[keyof T] { | ||
for (const key in obj) return obj[key]; | ||
@@ -242,41 +226,33 @@ return {} as any; | ||
async function ask(info: RoomPlayInfo) { | ||
// returns undefined if user inputs 'n' | ||
async function ask(info: RoomPlayInfo): Promise<string | undefined> { | ||
const { title, streams } = info; | ||
console.log("[blivec] " + "=====".repeat(10)); | ||
console.log("[blivec] Title:", title); | ||
console.log("[blivec] " + "=====".repeat(10)); | ||
console.log("[blivec] Available streams:"); | ||
log.info("=====".repeat(12)); | ||
log.info("Title: " + title); | ||
log.info("=====".repeat(12)); | ||
log.info("Available streams:"); | ||
const names = Object.keys(streams); | ||
const width = names.length > 9 ? 2 : 1; | ||
names.forEach((name, index) => { | ||
console.log(` ${String(index + 1).padStart(width)}: ${name}`); | ||
const choices: Array<number | string> = []; | ||
for (let i = 0; i < names.length; i++) { | ||
const name = names[i]; | ||
log.info(` ${String(i + 1).padStart(width)}: ${name}`); | ||
choices.push(i + 1); | ||
} | ||
choices.push("Y=1", "max", "n"); | ||
const repl = setup_repl(); | ||
const answer = await new Promise<string>((resolve) => { | ||
repl.question(`Choose a stream, or give up: (${choices.join("/")}) `, (a) => resolve(a || "Y")); | ||
}); | ||
const input = rl.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
}); | ||
const choices = Array.from({ length: names.length }, (_, i) => i + 1); | ||
const hint = [...choices, "Y=1", "max", "n"].join("/"); | ||
const choice = await new Promise<string>((resolve) => { | ||
input.question( | ||
`[blivec] Choose a stream, or give up: (${hint}) `, | ||
(a) => { | ||
input.close(); | ||
resolve(a || "Y"); | ||
} | ||
); | ||
}); | ||
const i = Number.parseInt(choice); | ||
let selected = names[0]; | ||
let i = Number.parseInt(answer); | ||
if (Number.isSafeInteger(i) && 1 <= i && i <= names.length) { | ||
selected = names[i - 1]; | ||
} else { | ||
const a = choice[0].toLowerCase(); | ||
if (a === "n") { | ||
return; | ||
} else if (a === "m") { | ||
selected = names.reduce((a, b) => | ||
streams[a].qn > streams[b].qn ? a : b | ||
); | ||
switch (answer[0].toLowerCase()) { | ||
case "n": | ||
return; | ||
case "m": | ||
selected = names.reduce((a, b) => (streams[a].qn > streams[b].qn ? a : b)); | ||
break; | ||
} | ||
@@ -288,3 +264,2 @@ } | ||
function play(url: string, title: string) { | ||
let child: ChildProcess; | ||
if (mpv) { | ||
@@ -296,4 +271,3 @@ const args = ["--quiet"]; | ||
args.push(url); | ||
child = cp.spawn("mpv", args, { stdio: "inherit", detached: true }); | ||
child.unref(); | ||
return cp.spawn("mpv", args, { stdio: "ignore", detached: true }); | ||
} else { | ||
@@ -303,20 +277,26 @@ const args = ["-hide_banner", "-loglevel", "error"]; | ||
args.push("-window_title", title); | ||
args.push("-x", "1280", "-y", "720"); | ||
args.push("-x", "720", "-y", "405"); | ||
args.push(url); | ||
child = cp.spawn("ffplay", args, { stdio: "inherit" }); | ||
return cp.spawn("ffplay", args, { stdio: "ignore" }); | ||
} | ||
return child; | ||
} | ||
let selected: string | undefined; | ||
async function replay() { | ||
async function replay(initial = true) { | ||
const info = await poll(); | ||
if (!info) return; | ||
if (!info) process.exit(0); | ||
const { title } = info; | ||
selected ||= await ask(info); | ||
if (!selected) return; | ||
if (initial) { | ||
selected = await ask(info); | ||
} else if (on_close === "default") { | ||
selected ||= await ask(info); | ||
} else if (on_close === "ask") { | ||
selected = await ask(info); | ||
} else if (on_close === "quit" || on_close === "exit") { | ||
selected = void 0; | ||
} | ||
if (!selected) process.exit(0); | ||
console.log("[blivec] Now playing:", `[${selected}] ${title}`); | ||
child = play(info.streams[selected].url, title); | ||
log.info(`Now playing: [${selected}] ${info.title}`); | ||
child = play(info.streams[selected].url, info.title); | ||
con ||= listen(id); | ||
@@ -326,3 +306,4 @@ con.resume(); | ||
con.pause(); | ||
setTimeout(100).then(replay); | ||
log.info('to exit, press "Ctrl+C" in the console'); | ||
setTimeout(replay, 100, false); | ||
}); | ||
@@ -339,3 +320,3 @@ } | ||
} else { | ||
process.kill(-child.pid!, "SIGTERM"); | ||
child.kill(); | ||
} | ||
@@ -354,3 +335,3 @@ }; | ||
if (json) console.log(JSON.stringify({ cmd: "exit" })); | ||
else console.log("[blivec] closing..."); | ||
else log.info("closing..."); | ||
con.close(); | ||
@@ -371,2 +352,3 @@ }); | ||
let mpv = false; | ||
let on_close = "default"; | ||
for (const arg of rest) { | ||
@@ -378,27 +360,21 @@ if (arg.startsWith("--interval=")) { | ||
} else { | ||
console.error("Invalid interval, expect a number >= 0"); | ||
log.error("Invalid interval, expect a number >= 0"); | ||
process.exit(1); | ||
} | ||
} | ||
if (arg.startsWith("--on-close=")) { | ||
const value = arg.slice(11); | ||
if (["default", "ask", "quit", "exit"].includes(value)) { | ||
on_close = value; | ||
} else { | ||
log.error("Invalid on-close option, expect 'default' 'ask' 'quit'"); | ||
process.exit(1); | ||
} | ||
} | ||
if (arg === "--mpv") mpv = true; | ||
} | ||
const con = await D(id, { interval, mpv }); | ||
const con = await D(id, { interval, mpv, on_close }); | ||
con && sigint(con); | ||
} | ||
} else { | ||
help(); | ||
} | ||
} else { | ||
const id = Number.parseInt(arg1); | ||
const json = arg2 === "--json"; | ||
if (Number.isSafeInteger(id) && id > 0) { | ||
if (arg2 && !json) { | ||
await send(id, arg2); | ||
} else { | ||
const con = listen(id, { json }); | ||
sigint(con, { json }); | ||
} | ||
} else { | ||
help(); | ||
} | ||
} |
@@ -9,13 +9,10 @@ import https from "https"; | ||
const text = | ||
(resolve: (value: string) => void) => async (res: IncomingMessage) => { | ||
const chunks: Buffer[] = []; | ||
for await (const chunk of res) chunks.push(chunk); | ||
resolve(Buffer.concat(chunks).toString("utf8")); | ||
}; | ||
const text = (resolve: (value: string) => void) => async (res: IncomingMessage) => { | ||
const chunks: Buffer[] = []; | ||
for await (const chunk of res) chunks.push(chunk); | ||
resolve(Buffer.concat(chunks).toString("utf8")); | ||
}; | ||
const get = (url: string) => | ||
new Promise<string>((resolve, reject) => | ||
https.get(url, text(resolve)).on("error", reject) | ||
); | ||
new Promise<string>((resolve, reject) => https.get(url, text(resolve)).on("error", reject)); | ||
@@ -155,3 +152,3 @@ const inflateAsync = /* @__PURE__ */ promisify(inflate); | ||
type: 2, | ||
}) | ||
}), | ||
); | ||
@@ -218,5 +215,3 @@ } | ||
let rs = await Promise.all(tasks); | ||
return rs.flatMap((r) => | ||
r.protocol === 2 || r.protocol === 3 ? r.data : r | ||
); | ||
return rs.flatMap((r) => (r.protocol === 2 || r.protocol === 3 ? r.data : r)); | ||
} | ||
@@ -223,0 +218,0 @@ |
3
46580
7
1320