Skip to content

Fix members only stream error & update video metadata #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bbc8c9e
Disable assertPlayability to avoid error on members only stream
HitomaruKonpaku Sep 3, 2022
2de2a0a
Parse more video metadata from html
HitomaruKonpaku Sep 3, 2022
e5c8b6b
Format & update typing
HitomaruKonpaku Sep 8, 2022
e2e969c
Fix milestone message `durationText` parser
HitomaruKonpaku Dec 10, 2022
f663e17
Skip error actions
HitomaruKonpaku Dec 10, 2022
0849ac9
Add chat metadata isMembersOnly
HitomaruKonpaku Jul 9, 2023
e8d4f5e
Try to fix build issue
HitomaruKonpaku Apr 22, 2024
c4182c3
Fix for membersOnly stream
HitomaruKonpaku May 8, 2024
b902acd
Store video extra metadata
HitomaruKonpaku May 11, 2024
66eaa30
Allow to update metadata & extend masterchat
HitomaruKonpaku May 12, 2024
0132e74
Fix unreachable code
HitomaruKonpaku Sep 24, 2024
820cf71
Update YTLiveChatPollRenderer payload
HitomaruKonpaku Sep 24, 2024
297cb6b
Update function access modifiers
HitomaruKonpaku Jun 4, 2025
29dcb0a
Update metadata parser
HitomaruKonpaku Jun 17, 2025
df729d4
Update metadata interface
HitomaruKonpaku Jun 17, 2025
ad4e10c
Update metadata parser
HitomaruKonpaku Jun 17, 2025
712a012
Add isUpcoming
HitomaruKonpaku Jun 18, 2025
9727fdf
release: v1.2.0
HitomaruKonpaku Jun 18, 2025
5eb75b9
Ignore cheerio type
HitomaruKonpaku Jun 18, 2025
999b6a2
Update README
HitomaruKonpaku Jun 18, 2025
e873c9d
Add membership to addMembershipTickerAction
HitomaruKonpaku Jun 20, 2025
5e03fc6
Update addBannerToLiveChatCommand from stu43005/masterchat
HitomaruKonpaku Jun 20, 2025
b15aa48
Update assertPlayability
HitomaruKonpaku Jun 29, 2025
8666af4
Update assertPlayability
HitomaruKonpaku Jun 29, 2025
5477ae1
Throw some data on MembersOnlyError
HitomaruKonpaku Jul 2, 2025
1ef1a39
Fix error
HitomaruKonpaku Jul 3, 2025
d02f025
Ability to set timeoutLength in timeoutParams
FlashlightXi Jul 4, 2025
8650ff7
Fix flatMap Type Error
FlashlightXi Jul 4, 2025
d2998bd
Delete iterator-helpers-polyfill
FlashlightXi Jul 4, 2025
d7bbf83
Merge pull request #1 from FlashlightXi/master
HitomaruKonpaku Jul 4, 2025
d796dd5
Add bot error
HitomaruKonpaku Jul 5, 2025
1ab38d7
Refactor
HitomaruKonpaku Jul 6, 2025
baa1066
Update docs url
HitomaruKonpaku Jul 6, 2025
036f789
Update CHANGELOG
HitomaruKonpaku Jul 6, 2025
ae0d899
release: v1.3.0
HitomaruKonpaku Jul 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
# Changelog

## v1.3.0

### New

- Add `ErrorAction`, `UnknownAction`
- Add `BotError`

### Improvements

- Update from [stu43005/masterchat](https://github.com/stu43005/masterchat)
- Commit: [5e30cf71ee0c9b7739e59cdcf92402f7794b7ee5](https://github.com/stu43005/masterchat/commit/5e30cf71ee0c9b7739e59cdcf92402f7794b7ee5)
- `addBannerToLiveChatCommand`
- `addChatItemAction`
- #1 Updated version of timeoutParams() (@FlashlightXi)
- Update interfaces
- `YTAnyText`
- `YTPlayabilityStatus`
- `MembersOnlyError` should have additional data
- `parseMetadataFromWatch` should handle more edge cases
- Chat can be available even when `playabilityStatus` not exist
- `MembersOnly` chat should be available when stream ended

## v1.2.0

### New

- Add `isUpcoming`
- Add `isMembersOnly`
- Add `videoMetadata` following format [VideoObject](./src/interfaces/yt/metadata.ts)

### Improvements

- Add more field on `Credentials` for members-only live stream

- `__Secure-1PAPISID`
- `__Secure-1PSID`
- `__Secure-1PSIDTS`
- `__Secure-1PSIDCC`

- Error parsing action `parseAction` should not crash
- Change access modifier of `get`, `post`, `postWithRetry` to `protected`

### Fixes

- Milestone message duration parsing error
- Question text from `parseShowLiveChatActionPanelAction` realted to poll

## v1.1.0

### New
Expand Down
4 changes: 2 additions & 2 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ await mc
.iter()
.filter((action) => action.type === "addChatItemAction") // only chat events
.map((chat) => JSON.stringify(chat) + "\n") // convert to JSONL
.forEach((jsonl) => appendFile("./chats.jsonl", jsonl)) // append to the file
.forEach((jsonl) => appendFile("./chats.jsonl", jsonl)); // append to the file
```

### Chat moderation bot
Expand Down Expand Up @@ -248,7 +248,7 @@ const mc = await Masterchat.init("<videoId>", { axiosInstance });

## Reference

[API Documentation](https://holodata.github.io/masterchat)
[API Documentation](https://sigvt.github.io/masterchat)

### Action type

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# Masterchat

[![npm](https://badgen.net/npm/v/masterchat)](https://npmjs.org/package/masterchat)
[![npm: total downloads](https://badgen.net/npm/dt/masterchat)](https://npmjs.org/package/masterchat)
[![npm: publish size](https://badgen.net/packagephobia/publish/masterchat)](https://npmjs.org/package/masterchat)
[![typedoc](https://badgen.net/badge/docs/typedoc/purple)](https://holodata.github.io/masterchat/)
[![npm](https://badgen.net/npm/v/@hitomaru/masterchat)](https://npmjs.org/package/@hitomaru/masterchat)
[![npm: total downloads](https://badgen.net/npm/dt/@hitomaru/masterchat)](https://npmjs.org/package/@hitomaru/masterchat)
[![npm: publish size](https://badgen.net/packagephobia/publish/@hitomaru/masterchat)](https://npmjs.org/package/@hitomaru/masterchat)
[![typedoc](https://badgen.net/badge/docs/typedoc/purple)](https://sigvt.github.io/masterchat/)

Masterchat is the most powerful library for YouTube Live Chat, supporting parsing 20+ actions, video comments and transcripts, as well as sending messages and moderating chats.

## Install

```
npm install masterchat
npm install @hitomaru/masterchat
```

```js
import { Masterchat, stringify } from "masterchat";
import { Masterchat, stringify } from "@hitomaru/masterchat";

const mc = await Masterchat.init("oyxvhJW1Cf8");

Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "masterchat",
"name": "@hitomaru/masterchat",
"description": "JavaScript library for YouTube Live Chat",
"version": "1.1.0",
"version": "1.3.0",
"author": "Yasuaki Uechi <y@uechi.io> (https://uechi.io/)",
"scripts": {
"build": "cross-env NODE_ENV=production rollup -c && shx rm -rf lib/lib lib/*.map && patch lib/masterchat.d.ts patches/fix-dts.patch",
Expand Down Expand Up @@ -30,6 +30,7 @@
],
"dependencies": {
"axios": "^0.27.2",
"cheerio": "^1.0.0-rc.12",
"debug": "^4.3.2",
"iterator-helpers-polyfill": "^2.2.8",
"sha1": "^1.1.1"
Expand Down Expand Up @@ -64,13 +65,13 @@
"typedoc": "^0.22.16",
"typescript": "^4.7.2"
},
"homepage": "https://github.com/holodata/masterchat",
"homepage": "https://github.com/HitomaruKonpaku/masterchat",
"repository": {
"type": "git",
"url": "https://github.com/holodata/masterchat.git"
"url": "git+https://github.com/HitomaruKonpaku/masterchat.git"
},
"bugs": {
"url": "https://github.com/holodata/masterchat/issues"
"url": "https://github.com/HitomaruKonpaku/masterchat/issues"
},
"license": "Apache-2.0",
"keywords": [
Expand Down
159 changes: 130 additions & 29 deletions src/chat/actions/addBannerToLiveChatCommand.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { toUnknownAction } from "..";
import {
AddBannerAction,
AddRedirectBannerAction,
AddIncomingRaidBannerAction,
AddOutgoingRaidBannerAction,
AddProductBannerAction,
AddChatSummaryBannerAction,
AddCallForQuestionsBannerAction,
} from "../../interfaces/actions";
import { YTAddBannerToLiveChatCommand } from "../../interfaces/yt/chat";
import { debugLog, stringify, tsToDate } from "../../utils";
import { debugLog, endpointToUrl, stringify, tsToDate } from "../../utils";
import { parseBadges } from "../badge";
import { pickThumbUrl } from "../utils";

Expand All @@ -13,20 +18,12 @@ export function parseAddBannerToLiveChatCommand(
// add pinned item
const bannerRdr = payload["bannerRenderer"]["liveChatBannerRenderer"];

if (
bannerRdr.header &&
bannerRdr.header.liveChatBannerHeaderRenderer.icon.iconType !== "KEEP"
) {
debugLog(
"[action required] Unknown icon type (addBannerToLiveChatCommand)",
JSON.stringify(bannerRdr.header)
);
}

// banner
const actionId = bannerRdr.actionId;
const targetId = bannerRdr.targetId;
const viewerIsCreator = bannerRdr.viewerIsCreator;
const isStackable = bannerRdr.isStackable;
const bannerType = bannerRdr.bannerType;

// contents
const contents = bannerRdr.contents;
Expand All @@ -40,7 +37,7 @@ export function parseAddBannerToLiveChatCommand(
const authorName = stringify(rdr.authorName);
const authorPhoto = pickThumbUrl(rdr.authorPhoto);
const authorChannelId = rdr.authorExternalChannelId;
const { isVerified, isOwner, isModerator, membership } = parseBadges(rdr);
const badges = parseBadges(rdr);

// header
const header = bannerRdr.header!.liveChatBannerHeaderRenderer;
Expand All @@ -65,33 +62,137 @@ export function parseAddBannerToLiveChatCommand(
authorName,
authorPhoto,
authorChannelId,
isVerified,
isOwner,
isModerator,
membership,
...badges,
viewerIsCreator,
contextMenuEndpointParams:
rdr.contextMenuEndpoint?.liveChatItemContextMenuEndpoint.params,
};
return parsed;
} else if ("liveChatBannerRedirectRenderer" in contents) {
// TODO:
}

if ("liveChatBannerRedirectRenderer" in contents) {
const rdr = contents.liveChatBannerRedirectRenderer;
const authorName = rdr.bannerMessage.runs[0].text;
const targetVideoId =
"watchEndpoint" in rdr.inlineActionButton.buttonRenderer.command
? rdr.inlineActionButton.buttonRenderer.command.watchEndpoint.videoId
: undefined;

const photo = pickThumbUrl(rdr.authorPhoto);

if (targetVideoId) {
// Outgoing
const targetName = rdr.bannerMessage.runs[1].text;
const payload: AddOutgoingRaidBannerAction = {
type: "addOutgoingRaidBannerAction",
actionId,
targetId,
targetName,
targetPhoto: photo,
targetVideoId,
};
return payload;
} else {
// Incoming
const sourceName = rdr.bannerMessage.runs[0].text;
const payload: AddIncomingRaidBannerAction = {
type: "addIncomingRaidBannerAction",
actionId,
targetId,
sourceName,
sourcePhoto: photo,
};
return payload;
}
}

if ("liveChatProductItemRenderer" in contents) {
const rdr = contents.liveChatProductItemRenderer;
const title = rdr.title;
const description = rdr.accessibilityTitle;
const thumbnail = rdr.thumbnail.thumbnails[0].url;
const price = rdr.price;
const vendorName = rdr.vendorName;
const creatorMessage = rdr.creatorMessage;
const creatorName = rdr.creatorName;
const authorPhoto = pickThumbUrl(rdr.authorPhoto);
const payload: AddRedirectBannerAction = {
type: "addRedirectBannerAction",
const url = endpointToUrl(rdr.onClickCommand)!;

if (!url) {
debugLog(
`Empty url at liveChatProductItemRenderer: ${JSON.stringify(rdr)}`
);
}

const dialogMessage =
rdr.informationDialog.liveChatDialogRenderer.dialogMessages;
const isVerified = rdr.isVerified;

const payload: AddProductBannerAction = {
type: "addProductBannerAction",
actionId,
targetId,
authorName,
viewerIsCreator,
isStackable,
title,
description,
thumbnail,
price,
vendorName,
creatorMessage,
creatorName,
authorPhoto,
url,
dialogMessage,
isVerified,
};
return payload;
} else {
throw new Error(
`[action required] Unrecognized content type found in parseAddBannerToLiveChatCommand: ${JSON.stringify(
payload
)}`
);
}

if ("liveChatCallForQuestionsRenderer" in contents) {
const rdr = contents.liveChatCallForQuestionsRenderer;
const creatorAvatar = rdr.creatorAvatar.thumbnails[0].url;
const creatorAuthorName = stringify(rdr.creatorAuthorName);
const questionMessage = rdr.questionMessage.runs;

const parsed: AddCallForQuestionsBannerAction = {
type: "addCallForQuestionsBannerAction",
actionId,
targetId,
isStackable,
bannerType,
creatorAvatar,
creatorAuthorName,
questionMessage,
};
return parsed;
}

if ("liveChatBannerChatSummaryRenderer" in contents) {
const rdr = contents.liveChatBannerChatSummaryRenderer;
const id = rdr.liveChatSummaryId;
const timestampUsec = id.split("_").at(-1)!;
const timestamp = tsToDate(timestampUsec);
const chatSummary = rdr.chatSummary.runs;

const parsed: AddChatSummaryBannerAction = {
type: "addChatSummaryBannerAction",
id,
actionId,
targetId,
isStackable,
bannerType,
timestampUsec,
timestamp,
chatSummary,
};
return parsed;
}

debugLog(
`[action required] Unrecognized content type found in parseAddBannerToLiveChatCommand: ${JSON.stringify(
payload
)}`
);

return toUnknownAction(payload);
}
Loading