@hyrious/blivec
Advanced tools
Comparing version 0.3.8 to 0.3.9
188
dist/bin.js
@@ -9,20 +9,27 @@ #!/usr/bin/env node | ||
import readline from "readline"; | ||
import { Connection, getRoomPlayInfo, sendDanmaku, testUrl } from "./index.js"; | ||
import { Connection, getRoomPlayInfo, searchRoom, sendDanmaku, stripTags, testUrl } from "./index.js"; | ||
const help = ` | ||
Usage: bl <room_id> # listen danmaku | ||
--json # print all events in json | ||
Usage: | ||
bl <room_id> # listen danmaku | ||
--json # print all events in json | ||
bl <room_id> <message> # send danmaku | ||
bl <room_id> <message> # send danmaku | ||
bl get <room_id> # get stream url | ||
--json # print them in json | ||
bl get <room_id> # get stream url | ||
--json # print them in json | ||
bl d <room_id> [--interval=1] # dd mode | ||
--interval=<minutes> # set 0 to disable polling | ||
--mpv # open in mpv instead | ||
--on-close=<behavior> # do something on window close | ||
default # restart player | ||
ask # ask quality again | ||
quit # quit DD mode | ||
-- [...player_args] # pass args to ffplay or mpv | ||
bl d <room_id> [--interval=1] # dd mode | ||
--interval=<minutes> # set 0 to disable polling | ||
--mpv # open in mpv instead | ||
--on-close=<behavior> # do something on window close | ||
default # restart player | ||
ask # ask quality again | ||
quit # quit DD mode | ||
-- [...player_args] # pass args to ffplay or mpv | ||
Examples: | ||
bl 123456 | ||
bl 123456 "Hello, world!" | ||
bl get 123456 | ||
bl d 123456 --mpv --on-close=quit -- --volume=50 | ||
`.trim(); | ||
@@ -63,3 +70,3 @@ const has_colors = tty.WriteStream.prototype.hasColors(); | ||
} | ||
function listen(id, { json = false } = {}) { | ||
function listen(id2, { json = false } = {}) { | ||
let count = 0; | ||
@@ -86,3 +93,3 @@ const events = json ? { | ||
line = line.slice(2); | ||
send(id, line).catch(log.catch_error); | ||
send(id2, line).catch(log.catch_error); | ||
} else { | ||
@@ -109,5 +116,5 @@ log.info('message needs to start with "> " (space is required)'); | ||
}; | ||
return new Connection(id, events); | ||
return new Connection(id2, events); | ||
} | ||
async function send(id, message) { | ||
async function send(id2, message) { | ||
function example() { | ||
@@ -150,3 +157,3 @@ console.error("Example content:"); | ||
if (env.SESSDATA && env.bili_jct) { | ||
await sendDanmaku(id, message, env).catch(log.catch_error); | ||
await sendDanmaku(id2, message, env).catch(log.catch_error); | ||
} else { | ||
@@ -157,5 +164,5 @@ log.error("Invalid cookie.txt"); | ||
} | ||
async function get(id, { json = false } = {}) { | ||
async function get(id2, { json = false } = {}) { | ||
try { | ||
const info = await getRoomPlayInfo(id); | ||
const info = await getRoomPlayInfo(id2); | ||
if (!json) { | ||
@@ -179,4 +186,4 @@ console.log("Title:", info.title); | ||
} | ||
async function D(id, { interval = 1, mpv = false, on_close = "default", args = [] } = {}) { | ||
log.info(`DD ${id} ${interval > 0 ? `every ${interval} minutes` : "once"}`); | ||
async function D(id2, { interval = 1, mpv = false, on_close = "default", args = [] } = {}) { | ||
log.info(`DD ${id2} ${interval > 0 ? `every ${interval} minutes` : "once"}`); | ||
let con; | ||
@@ -191,3 +198,3 @@ let child; | ||
while (info === null) { | ||
info = await getRoomPlayInfo(id).catch(() => null); | ||
info = await getRoomPlayInfo(id2).catch(() => null); | ||
if (info && !await testUrl(first(info.streams).url, headers)) | ||
@@ -288,3 +295,3 @@ info = null; | ||
child = play(info.streams[selected].url, info.title, args); | ||
con ||= listen(id); | ||
con ||= listen(id2); | ||
con.resume(); | ||
@@ -327,57 +334,92 @@ child.on("exit", () => { | ||
const [arg1, arg2, ...rest] = process.argv.slice(2); | ||
if (arg1 === void 0 || arg1 === "--help" || arg2 === "--help" || rest.includes("--help")) { | ||
console.log(help); | ||
process.exit(0); | ||
} | ||
let action = "listen"; | ||
let id_or_keyword; | ||
let id; | ||
if (arg1 === "get" || arg1 === "d" || arg1 === "dd") { | ||
const id = Number.parseInt(arg2); | ||
if (Number.isSafeInteger(id) && id > 0) { | ||
if (arg1 === "get") { | ||
const json = rest.includes("--json"); | ||
await get(id, { json }); | ||
} else { | ||
let interval = 1; | ||
let mpv = false; | ||
let on_close = "default"; | ||
let args; | ||
for (const arg of rest) { | ||
if (arg.startsWith("--interval=")) { | ||
const value = Number.parseInt(arg.slice(11)); | ||
if (Number.isFinite(value)) { | ||
interval = Math.max(0, value); | ||
} else { | ||
log.error("Invalid interval, expect a number >= 0"); | ||
process.exit(1); | ||
} | ||
} else 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); | ||
} | ||
} else if (arg === "--mpv") { | ||
mpv = true; | ||
} else if (arg === "--") { | ||
args = []; | ||
} else if (args) { | ||
args.push(arg); | ||
} | ||
} | ||
const con = await D(id, { interval, mpv, on_close, args }); | ||
con && sigint(con); | ||
action = arg1; | ||
id_or_keyword = arg2; | ||
} else { | ||
id_or_keyword = arg1; | ||
} | ||
let maybe_id = Number.parseInt(id_or_keyword); | ||
if (Number.isSafeInteger(maybe_id) && maybe_id > 0) { | ||
id = maybe_id; | ||
} else { | ||
let rooms = await searchRoom(id_or_keyword); | ||
if (rooms.length === 0) { | ||
log.error("Not found room with keyword " + JSON.stringify(id_or_keyword)); | ||
process.exit(1); | ||
} else if (rooms.length === 1) { | ||
id = rooms[0].roomid; | ||
} else { | ||
log.info("Found multiple rooms:"); | ||
const choices = []; | ||
for (let i2 = 0; i2 < rooms.length; i2++) { | ||
const room = rooms[i2]; | ||
const title = stripTags(room.title); | ||
log.info(` ${String(i2 + 1).padStart(2)}: ${room.uname} - ${title}`); | ||
choices.push(i2 + 1); | ||
} | ||
choices.push("Y=1", "n"); | ||
const repl2 = setup_repl(); | ||
const answer = await new Promise((resolve) => { | ||
repl2.question(`Choose a room, or give up: (${choices.join("/")}) `, (a) => resolve(a || "Y")); | ||
}); | ||
let selected = rooms[0]; | ||
let i = Number.parseInt(answer); | ||
if (Number.isSafeInteger(i) && 1 <= i && i <= rooms.length) { | ||
selected = rooms[i - 1]; | ||
} else if (answer[0].toLowerCase() === "n") { | ||
process.exit(0); | ||
} | ||
id = selected.roomid; | ||
} | ||
} | ||
if (action === "listen") { | ||
const json = arg2 === "--json"; | ||
if (arg2 && !json) { | ||
await send(id, arg2); | ||
} else { | ||
console.log(help); | ||
const con = listen(id, { json }); | ||
sigint(con, { json }); | ||
} | ||
} else if (action === "get") { | ||
const json = rest.includes("--json"); | ||
await get(id, { json }); | ||
} 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 }); | ||
let interval = 1; | ||
let mpv = false; | ||
let on_close = "default"; | ||
let args; | ||
for (const arg of rest) { | ||
if (arg.startsWith("--interval=")) { | ||
const value = Number.parseInt(arg.slice(11)); | ||
if (Number.isFinite(value)) { | ||
interval = Math.max(0, value); | ||
} else { | ||
log.error("Invalid interval, expect a number >= 0"); | ||
process.exit(1); | ||
} | ||
} else 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); | ||
} | ||
} else if (arg === "--mpv") { | ||
mpv = true; | ||
} else if (arg === "--") { | ||
args = []; | ||
} else if (args) { | ||
args.push(arg); | ||
} | ||
} else { | ||
console.log(help); | ||
} | ||
const con = await D(id, { interval, mpv, on_close, args }); | ||
con && sigint(con); | ||
} |
@@ -81,4 +81,12 @@ import { Socket } from 'net'; | ||
}>; | ||
declare function searchRoom(keyword: string): Promise<{ | ||
roomid: number; | ||
uname: string; | ||
title: string; | ||
live_time: string; | ||
cover: string; | ||
}[]>; | ||
declare function testUrl(url: string, headers?: string[]): false | Promise<boolean>; | ||
declare function stripTags(html: string): string; | ||
export { Connection, ConnectionInfo, DanmuInfo, Env, Events, RoomInfo, getDanmuInfo, getRoomInfo, getRoomPlayInfo, sendDanmaku, testUrl }; | ||
export { Connection, ConnectionInfo, DanmuInfo, Env, Events, RoomInfo, getDanmuInfo, getRoomInfo, getRoomPlayInfo, searchRoom, sendDanmaku, stripTags, testUrl }; |
@@ -14,3 +14,3 @@ "use strict"; | ||
}; | ||
const get = (url) => new Promise((resolve, reject) => https.get(url, text(resolve)).on("error", reject)); | ||
const get = (url, options = {}) => new Promise((resolve, reject) => https.get(url, options, text(resolve)).on("error", reject)); | ||
const inflateAsync = /* @__PURE__ */ promisify(inflate); | ||
@@ -294,2 +294,17 @@ const brotliDecompressAsync = /* @__PURE__ */ promisify(brotliDecompress); | ||
} | ||
export async function searchRoom(keyword) { | ||
keyword = encodeURIComponent(keyword); | ||
const api = "https://api.bilibili.com/x/web-interface/search/type"; | ||
const params = "&order=online&coverType=user_cover&page=1"; | ||
const info = await get(`${api}?search_type=live_room&keyword=${keyword}${params}`, { | ||
headers: { | ||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.1) Gecko/20100101 Firefox/60.1", | ||
"Referer": `https://search.bilibili.com/live?keyword=${keyword}${params}&search_type=live` | ||
} | ||
}); | ||
const { code, message, data } = JSON.parse(info); | ||
if (code != 0) | ||
throw new Error(message); | ||
return data.result; | ||
} | ||
export function testUrl(url, headers = []) { | ||
@@ -307,1 +322,4 @@ if (!url) | ||
} | ||
export function stripTags(html) { | ||
return html.replace(/<[^>]+>/g, ""); | ||
} |
{ | ||
"name": "@hyrious/blivec", | ||
"version": "0.3.8", | ||
"version": "0.3.9", | ||
"description": "bilibili live cli", | ||
@@ -5,0 +5,0 @@ "type": "module", |
170
src/bin.ts
@@ -8,21 +8,28 @@ #!/usr/bin/env node | ||
import readline from "readline"; | ||
import { Connection, Events, getRoomPlayInfo, sendDanmaku, testUrl } from "./index.js"; | ||
import { Connection, Events, getRoomPlayInfo, searchRoom, sendDanmaku, stripTags, testUrl } from "./index.js"; | ||
const help = ` | ||
Usage: bl <room_id> # listen danmaku | ||
--json # print all events in json | ||
Usage: | ||
bl <room_id> # listen danmaku | ||
--json # print all events in json | ||
bl <room_id> <message> # send danmaku | ||
bl <room_id> <message> # send danmaku | ||
bl get <room_id> # get stream url | ||
--json # print them in json | ||
bl get <room_id> # get stream url | ||
--json # print them in json | ||
bl d <room_id> [--interval=1] # dd mode | ||
--interval=<minutes> # set 0 to disable polling | ||
--mpv # open in mpv instead | ||
--on-close=<behavior> # do something on window close | ||
default # restart player | ||
ask # ask quality again | ||
quit # quit DD mode | ||
-- [...player_args] # pass args to ffplay or mpv | ||
bl d <room_id> [--interval=1] # dd mode | ||
--interval=<minutes> # set 0 to disable polling | ||
--mpv # open in mpv instead | ||
--on-close=<behavior> # do something on window close | ||
default # restart player | ||
ask # ask quality again | ||
quit # quit DD mode | ||
-- [...player_args] # pass args to ffplay or mpv | ||
Examples: | ||
bl 123456 | ||
bl 123456 "Hello, world!" | ||
bl get 123456 | ||
bl d 123456 --mpv --on-close=quit -- --volume=50 | ||
`.trim(); | ||
@@ -349,58 +356,97 @@ | ||
const [arg1, arg2, ...rest] = process.argv.slice(2); | ||
if (arg1 === void 0 || arg1 === "--help" || arg2 === "--help" || rest.includes("--help")) { | ||
console.log(help); | ||
process.exit(0); | ||
} | ||
let action = "listen"; | ||
let id_or_keyword: string; | ||
let id: number; | ||
if (arg1 === "get" || arg1 === "d" || arg1 === "dd") { | ||
const id = Number.parseInt(arg2); | ||
if (Number.isSafeInteger(id) && id > 0) { | ||
if (arg1 === "get") { | ||
const json = rest.includes("--json"); | ||
await get(id, { json }); | ||
} else { | ||
let interval = 1; | ||
let mpv = false; | ||
let on_close = "default"; | ||
let args: string[] | undefined; | ||
for (const arg of rest) { | ||
if (arg.startsWith("--interval=")) { | ||
const value = Number.parseInt(arg.slice(11)); | ||
if (Number.isFinite(value)) { | ||
interval = Math.max(0, value); | ||
} else { | ||
log.error("Invalid interval, expect a number >= 0"); | ||
process.exit(1); | ||
} | ||
} else 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); | ||
} | ||
} else if (arg === "--mpv") { | ||
mpv = true; | ||
} else if (arg === "--") { | ||
args = []; | ||
} else if (args) { | ||
args.push(arg); | ||
} | ||
} | ||
const con = await D(id, { interval, mpv, on_close, args }); | ||
con && sigint(con); | ||
action = arg1; | ||
id_or_keyword = arg2; | ||
} else { | ||
id_or_keyword = arg1; | ||
} | ||
// resolve keyword to room id | ||
let maybe_id = Number.parseInt(id_or_keyword); | ||
if (Number.isSafeInteger(maybe_id) && maybe_id > 0) { | ||
id = maybe_id; | ||
} else { | ||
let rooms = await searchRoom(id_or_keyword); | ||
if (rooms.length === 0) { | ||
log.error("Not found room with keyword " + JSON.stringify(id_or_keyword)); | ||
process.exit(1); | ||
} else if (rooms.length === 1) { | ||
id = rooms[0].roomid; | ||
} else { | ||
log.info("Found multiple rooms:"); | ||
const choices: Array<number | string> = []; | ||
for (let i = 0; i < rooms.length; i++) { | ||
const room = rooms[i]; | ||
const title = stripTags(room.title); | ||
log.info(` ${String(i + 1).padStart(2)}: ${room.uname} - ${title}`); | ||
choices.push(i + 1); | ||
} | ||
choices.push("Y=1", "n"); | ||
const repl = setup_repl(); | ||
const answer = await new Promise<string>((resolve) => { | ||
repl.question(`Choose a room, or give up: (${choices.join("/")}) `, (a) => resolve(a || "Y")); | ||
}); | ||
let selected = rooms[0]; | ||
let i = Number.parseInt(answer); | ||
if (Number.isSafeInteger(i) && 1 <= i && i <= rooms.length) { | ||
selected = rooms[i - 1]; | ||
} else if (answer[0].toLowerCase() === "n") { | ||
process.exit(0); | ||
} | ||
id = selected.roomid; | ||
} | ||
} | ||
if (action === "listen") { | ||
const json = arg2 === "--json"; | ||
if (arg2 && !json) { | ||
await send(id, arg2); | ||
} else { | ||
console.log(help); | ||
const con = listen(id, { json }); | ||
sigint(con, { json }); | ||
} | ||
} else if (action === "get") { | ||
const json = rest.includes("--json"); | ||
await get(id, { json }); | ||
} 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 }); | ||
let interval = 1; | ||
let mpv = false; | ||
let on_close = "default"; | ||
let args: string[] | undefined; | ||
for (const arg of rest) { | ||
if (arg.startsWith("--interval=")) { | ||
const value = Number.parseInt(arg.slice(11)); | ||
if (Number.isFinite(value)) { | ||
interval = Math.max(0, value); | ||
} else { | ||
log.error("Invalid interval, expect a number >= 0"); | ||
process.exit(1); | ||
} | ||
} else 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); | ||
} | ||
} else if (arg === "--mpv") { | ||
mpv = true; | ||
} else if (arg === "--") { | ||
args = []; | ||
} else if (args) { | ||
args.push(arg); | ||
} | ||
} else { | ||
console.log(help); | ||
} | ||
const con = await D(id, { interval, mpv, on_close, args }); | ||
con && sigint(con); | ||
} |
@@ -15,4 +15,4 @@ import https from "https"; | ||
const get = (url: string) => | ||
new Promise<string>((resolve, reject) => https.get(url, text(resolve)).on("error", reject)); | ||
const get = (url: string, options: https.RequestOptions = {}) => | ||
new Promise<string>((resolve, reject) => https.get(url, options, text(resolve)).on("error", reject)); | ||
@@ -405,2 +405,31 @@ const inflateAsync = /* @__PURE__ */ promisify(inflate); | ||
interface SerachResult { | ||
result: { | ||
roomid: number; | ||
// player name | ||
uname: string; | ||
// may include html tags | ||
title: string; | ||
// start time | ||
live_time: string; | ||
// screen shot | ||
cover: string; | ||
}[]; | ||
} | ||
export async function searchRoom(keyword: string) { | ||
keyword = encodeURIComponent(keyword); | ||
const api = "https://api.bilibili.com/x/web-interface/search/type"; | ||
const params = "&order=online&coverType=user_cover&page=1"; | ||
const info = await get(`${api}?search_type=live_room&keyword=${keyword}${params}`, { | ||
headers: { | ||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.1) Gecko/20100101 Firefox/60.1", | ||
"Referer": `https://search.bilibili.com/live?keyword=${keyword}${params}&search_type=live`, | ||
}, | ||
}); | ||
const { code, message, data } = JSON.parse(info); | ||
if (code != 0) throw new Error(message); | ||
return (data as SerachResult).result; | ||
} | ||
export function testUrl(url: string, headers: string[] = []) { | ||
@@ -419,1 +448,5 @@ if (!url) return false; | ||
} | ||
export function stripTags(html: string) { | ||
return html.replace(/<[^>]+>/g, ""); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
56705
1619