diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 00000000..1b982e35 --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,151 @@ +# Manual + +## Usage + +[API Documentation](https://holodata.github.io/masterchat) + +### Iterate live chats + +```js +import { Masterchat, runsToString } from "masterchat"; + +async function main() { + try { + const client = await Masterchat.init(""); + + for await (const { actions } of client.iterate()) { + const chats = actions.filter( + (action) => action.type === "addChatItemAction" + ); + + for (const chat of chats) { + console.log(chat.authorName, runsToString(chat.rawMessage)); + } + } + } catch (err) { + console.log(err.code); + // "disabled" => Live chat is disabled + // "membersOnly" => No permission (members-only) + // "private" => No permission (private video) + // "unavailable" => Deleted OR wrong video id + // "unarchived" => Live stream recording is not available + // "denied" => Access denied + // "invalid" => Invalid request + // "unknown" => Unknown error + } +} + +main(); +``` + +### Download replay chat as JSONLines + +```js +import { Masterchat, convertRunsToString } from "masterchat"; +import { appendFile, writeFile, readFile } from "fs/promises"; + +async function main() { + const client = await Masterchat.init(""); + + const lastContinuation = await readFile("./checkpoint").catch( + () => undefined + ); + + for await (const { actions, continuation } of client.iterate({ + continuation: lastContinuation, + })) { + const chats = actions.filter( + (action) => action.type === "addChatItemAction" + ); + + const jsonl = chats.map((chat) => JSON.stringify(chat)).join("\n"); + await appendFile("./chats.jsonl", jsonl + "\n"); + + // save checkpoint + await writeFile("./checkpoint", continuation.token); + } +} + +main(); +``` + +### Auto-moderator + +```js +import { Masterchat, runsToString } from "masterchat"; +import { isSpam } from "spamreaper"; + +async function main() { + // `credentials` is an object containing YouTube session cookie or a base64-encoded JSON string of them + const credentials = { + SAPISID: "", + APISID: "", + HSID: "", + SID: "", + SSID: "", + }; + + const client = await Masterchat.init("", { credentials }); + + for await (const { actions } of client.iterate({ + ignoreFirstResponse: true, + })) { + for (const action of actions) { + if (action.type !== "addChatItemAction") continue; + + if (isSpam(runsToString(action.rawMessage))) { + await client.remove(action.id); + } + } + } +} + +main(); +``` + +## Advanced Usage + +### Faster instantiation + +To skip loading watch page, use `new Masterchat(videoId: string, channelId: string, { isLive?: boolean })`: + +```js +const live = new Masterchat(videoId, channelId, { isLive: true }); +``` + +instead of: + +```js +const live = await Masterchat.init(videoId); +``` + +The former won't populate metadata. If you need metadata, call: + +```js +await live.populateMetadata(); // will scrape metadata from watch page +console.log(live.title); +console.log(live.channelName); +``` + +### Fetch credentials + +```bash +cd extra/credentials-fetcher +npm i +npm start +``` + +## Stream type + +| type | isLive (auto) | auto | direct (isLive: true) | direct (isLive: false) | +| ----------------------------------------------- | ------------- | ------------------------ | ---------------------- | ---------------------- | +| live/pre stream | `true` | **OK** | **OK** | `DisabledChatError` | +| pre stream but chat disabled | `true` | `DisabledChatError` | `DisabledChatError` | `DisabledChatError` | +| archived stream | `false` | **OK** | `DisabledChatError` | **OK** | +| archived stream but replay chat being processed | `false` | `DisabledChatError` | `DisabledChatError` | `DisabledChatError` | +| members-only live stream | N/A | `MembersOnlyError` | `DisabledChatError` | `MembersOnlyError` | +| members-only archived stream | N/A | `MembersOnlyError` | `DisabledChatError` | **OK** | +| unarchived stream | N/A | `NoStreamRecordingError` | `DisabledChatError` | `DisabledChatError` | +| privated stream | N/A | `NoPermissionError` | `NoPermissionError` | `NoPermissionError` | +| deleted stream | N/A | `UnavailableError` | `UnavailableError` | `UnavailableError` | +| invalid video/channel id | N/A | `InvalidArgumentError` | `InvalidArgumentError` | `InvalidArgumentError` | diff --git a/MEMO.md b/MEMO.md deleted file mode 100644 index 2c486a92..00000000 --- a/MEMO.md +++ /dev/null @@ -1,16 +0,0 @@ -# Memo - -## Stream type - -| type | isLive (auto) | auto | direct (isLive: true) | direct (isLive: false) | -| ----------------------------------------------- | ------------- | ------------------------ | ---------------------- | ---------------------- | -| live/pre stream | `true` | **OK** | **OK** | `DisabledChatError` | -| pre stream but chat disabled | `true` | `DisabledChatError` | `DisabledChatError` | `DisabledChatError` | -| archived stream | `false` | **OK** | `DisabledChatError` | **OK** | -| archived stream but replay chat being processed | `false` | `DisabledChatError` | `DisabledChatError` | `DisabledChatError` | -| members-only live stream | N/A | `MembersOnlyError` | `DisabledChatError` | `MembersOnlyError` | -| members-only archived stream | N/A | `MembersOnlyError` | `DisabledChatError` | **OK** | -| unarchived stream | N/A | `NoStreamRecordingError` | `DisabledChatError` | `DisabledChatError` | -| privated stream | N/A | `NoPermissionError` | `NoPermissionError` | `NoPermissionError` | -| deleted stream | N/A | `UnavailableError` | `UnavailableError` | `UnavailableError` | -| invalid video/channel id | N/A | `InvalidArgumentError` | `InvalidArgumentError` | `InvalidArgumentError` | diff --git a/README.md b/README.md index 4e37119c..5ab8cffa 100644 --- a/README.md +++ b/README.md @@ -13,153 +13,18 @@ npm i masterchat ``` -## Usage - -### Iterate live chats - -```js -import { Masterchat, runsToString } from "masterchat"; - -async function main() { - try { - const client = await Masterchat.init(""); - - for await (const { actions } of client.iterate()) { - const chats = actions.filter( - (action) => action.type === "addChatItemAction" - ); - - for (const chat of chats) { - console.log(chat.authorName, runsToString(chat.rawMessage)); - } - } - } catch (err) { - console.log(err.code); - // "disabled" => Live chat is disabled - // "membersOnly" => No permission (members-only) - // "private" => No permission (private video) - // "unavailable" => Deleted OR wrong video id - // "unarchived" => Live stream recording is not available - // "denied" => Access denied - // "invalid" => Invalid request - // "unknown" => Unknown error - } -} - -main(); -``` - -### Download replay chat as JSONLines - -```js -import { Masterchat, convertRunsToString } from "masterchat"; -import { appendFile, writeFile, readFile } from "fs/promises"; - -async function main() { - const client = await Masterchat.init(""); - - const lastContinuation = await readFile("./checkpoint").catch( - () => undefined - ); - - for await (const { actions, continuation } of client.iterate({ - continuation: lastContinuation, - })) { - const chats = actions.filter( - (action) => action.type === "addChatItemAction" - ); - - const jsonl = chats.map((chat) => JSON.stringify(chat)).join("\n"); - await appendFile("./chats.jsonl", jsonl + "\n"); - - // save checkpoint - await writeFile("./checkpoint", continuation.token); - } -} - -main(); -``` - -### Auto-moderator - -```js -import { Masterchat, runsToString } from "masterchat"; -import { isSpam } from "spamreaper"; - -async function main() { - // `credentials` is an object containing YouTube session cookie or a base64-encoded JSON string of them - const credentials = { - SAPISID: "", - APISID: "", - HSID: "", - SID: "", - SSID: "", - }; - - const client = await Masterchat.init("", { credentials }); - - for await (const { actions } of client.iterate({ - ignoreFirstResponse: true, - })) { - for (const action of actions) { - if (action.type !== "addChatItemAction") continue; - - if (isSpam(runsToString(action.rawMessage))) { - await client.remove(action.id); - } - } - } -} - -main(); -``` - -## Advanced Usage - -### Faster instantiation - -To skip loading watch page, use `new Masterchat(videoId: string, channelId: string, { isLive?: boolean })`: - -```js -const live = new Masterchat(videoId, channelId, { isLive: true }); -``` - -instead of: - -```js -const live = await Masterchat.init(videoId); -``` - -The former won't populate metadata. If you need metadata, call: - -```js -await live.populateMetadata(); // will scrape metadata from watch page -console.log(live.title); -console.log(live.channelName); -``` - -### Fetch credentials - -```bash -cd extra/credentials-fetcher -npm i -npm start -``` +See [MANUAL](https://github.com/holodata/masterchat/tree/master/USAGE.md) for usage. ## CLI -[![npm](https://badgen.net/npm/v/masterchat-cli)](https://npmjs.org/package/masterchat-cli) -[![npm: total downloads](https://badgen.net/npm/dt/masterchat-cli)](https://npmjs.org/package/masterchat-cli) - See YouTube Live Chat through flexible filtering engine. -- [Documentation](https://github.com/holodata/masterchat-cli/blob/master/README.md) -- [Source](https://github.com/holodata/masterchat-cli) - ``` npm i -g masterchat-cli ``` +See [masterchat-cli](https://github.com/holodata/masterchat-cli) for usage. + ## Desktop For a desktop app, see [Komet](https://github.com/holodata/komet). @@ -172,11 +37,11 @@ For a desktop app, see [Komet](https://github.com/holodata/komet). - [x] Ability to send chat - [x] Moderation functionality -## masterchat in real world +## Show case -- [https://holodex.net](Holodex): for their TLDex backend -- [https://github.com/holodata/honeybee](Honeybee): large-scale chat collection cluster -- [https://github.com/holodata/Komet](Komet): Tweetdeck-like live chat client for macOS/Windows +- [Holodex](https://holodex.net) (TLDex backend) +- [Honeybee](https://github.com/holodata/honeybee) +- [Komet](https://github.com/holodata/Komet) ## Contribute