Skip to content

Commit

Permalink
Fixed error when watching streams when no drop active (#121). Fixed i…
Browse files Browse the repository at this point in the history
…ssue with progress bar not updating correctly.
  • Loading branch information
TychoTheTaco committed Jun 9, 2022
1 parent ad15ee1 commit ee9957a
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 42 deletions.
5 changes: 3 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,12 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) {
if (campaign) {
result += `${ansiEscape("36m")}${campaign.game.name ?? campaign.game.displayName}${ansiEscape("39m")} | ${ansiEscape("35m")}${campaign.name}${ansiEscape("39m")}\n`;
} else {
result += '\n'
result += ansiEscape("2K") + '\n'
}
result += `${getDropBenefitNames(drop)} ${BarFormat((drop.self.currentMinutesWatched ?? 0) / drop.requiredMinutesWatched, options)} ${drop.self.currentMinutesWatched ?? 0} / ${drop.requiredMinutesWatched} minutes` + ansiEscape('0K') + '\n';
} else {
result += `- No Drops Active -\n\n`;
result += ansiEscape("2K") + `- No Drops Active -\n`;
result += ansiEscape("2K") +" \n";
}

if (isFirstOutput) {
Expand Down
9 changes: 6 additions & 3 deletions src/twitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,10 @@ export class Client {
return stream !== null && stream !== undefined;
}

async getStream(broadcasterId: string): Promise<Stream | null> {
async getStream(broadcasterId: string): Promise<{
id: string,
viewersCount: number
} | null> {
const data = await this.#post({
"operationName": "ChannelShell",
"variables": {
Expand Down Expand Up @@ -453,8 +456,8 @@ export function getDropBenefitNames(drop: TimeBasedDrop): string {
return result;
}

export function getStreamUrl(stream: Stream) {
return "https://www.twitch.tv/" + stream.broadcaster.login;
export function getStreamUrl(username: string) {
return "https://www.twitch.tv/" + username;
}

/**
Expand Down
72 changes: 36 additions & 36 deletions src/twitch_drops_bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import CommunityPointsComponent from "./components/community_points.js";
import WebSocketListener from "./web_socket_listener.js";
import {TwitchDropsWatchdog} from "./watchdog.js";
import {StreamPage} from "./pages/stream.js";
import utils, {TimedSet} from './utils.js';
import utils, {TimedSet, waitForResponseWithOperationName} from './utils.js';
import logger from "./logger.js";
import {Client, TimeBasedDrop, DropCampaign, StreamTag, getInventoryDrop, Tag, Inventory, isDropCompleted, getStreamUrl, Stream} from "./twitch.js";
import {Client, TimeBasedDrop, DropCampaign, StreamTag, getInventoryDrop, Tag, Inventory, isDropCompleted, getStreamUrl} from "./twitch.js";
import {NoStreamsError, NoProgressError, HighPriorityError, StreamLoadFailedError, StreamDownError} from "./errors.js";

type Class<T> = { new(...args: any[]): T };
Expand Down Expand Up @@ -701,7 +701,7 @@ export class TwitchDropsBot extends EventEmitter {

// Check if the first active stream has Drops available. If it doesn't, then we most likely completed
// this campaign already, so we shouldn't start watching the stream.
if (!(await this.#hasDropsAvailable(getStreamUrl(streams[0]), dropCampaignId))) {
if (!(await this.#hasDropsAvailable(getStreamUrl(streams[0].broadcaster.login), dropCampaignId))) {
continue;
}

Expand Down Expand Up @@ -749,24 +749,24 @@ export class TwitchDropsBot extends EventEmitter {
return null;
}

async #getNextPreferredBroadcasterStream(): Promise<Stream | null> {
async #getNextPreferredBroadcasterStreamUrl(): Promise<string | null> {
// Check if any of the preferred broadcasters are online
for (const broadcasterId of this.#broadcasterIds) {
const stream = await this.#twitchClient.getStream(broadcasterId);
if (stream) {
const streamUrl = getStreamUrl(stream);
const streamUrl = getStreamUrl(broadcasterId);
if (this.#streamUrlTemporaryBlacklist.has(streamUrl)) {
continue;
}
return stream;
return streamUrl;
}
}
return null;
}

async #getNextIdleStream(): Promise<Stream | null> {
async #getNextIdleStreamUrl(): Promise<string | null> {
// Check if any of the preferred broadcasters are online
const streamUrl = await this.#getNextPreferredBroadcasterStream();
const streamUrl = await this.#getNextPreferredBroadcasterStreamUrl();
if (streamUrl !== null) {
return streamUrl;
}
Expand All @@ -788,11 +788,11 @@ export class TwitchDropsBot extends EventEmitter {
}
const streams = await this.#twitchClient.getActiveStreams(campaign.game.displayName);
if (streams.length > 0) {
const streamUrl = getStreamUrl(streams[0]);
const streamUrl = getStreamUrl(streams[0].broadcaster.login);
if (this.#streamUrlTemporaryBlacklist.has(streamUrl)) {
continue;
}
return streams[0];
return streamUrl;
}
}

Expand Down Expand Up @@ -839,19 +839,18 @@ export class TwitchDropsBot extends EventEmitter {
logger.info("No drop campaigns active, watching a stream instead.");

// Choose a stream to watch
let stream: Stream | null = null;
let streamUrl: string | null = null;
try {
stream = await this.#getNextIdleStream();
streamUrl = await this.#getNextIdleStreamUrl();
} catch (error) {
logger.error("Error getting next idle stream!");
logger.debug(error);
}
if (stream === null) {
if (streamUrl === null) {
logger.info("No idle streams available! sleeping for a bit...");
await this.waitForDropCampaignUpdateOrTimeout(this.sleepTimeMilliseconds);
continue;
}
const streamUrl = getStreamUrl(stream);
logger.info("stream: " + streamUrl)

const dropProgressComponent = new DropProgressComponent({requireProgress: false, exitOnClaim: false});
Expand All @@ -868,15 +867,15 @@ export class TwitchDropsBot extends EventEmitter {
this.#pendingHighPriority = true;
} else {
// Check if a more preferred broadcaster is online
let preferredBroadcasterStream: Stream | null = null;
let preferredBroadcasterStreamUrl: string | null = null;
try {
preferredBroadcasterStream = await this.#getNextPreferredBroadcasterStream();
preferredBroadcasterStreamUrl = await this.#getNextPreferredBroadcasterStreamUrl();
} catch (error) {
logger.error("Failed to get next preferred broadcaster stream url");
logger.debug(error);
}
if (preferredBroadcasterStream !== null) {
if (getStreamUrl(preferredBroadcasterStream) !== streamUrl) {
if (preferredBroadcasterStreamUrl !== null) {
if (preferredBroadcasterStreamUrl !== streamUrl) {
this.#pendingHighPriority = true;
}
}
Expand All @@ -888,7 +887,7 @@ export class TwitchDropsBot extends EventEmitter {

// Watch stream
try {
await this.#watchStreamWrapper(stream, components);
await this.#watchStreamWrapper(streamUrl, components);
} catch (error) {
await this.#page.goto("about:blank");
} finally {
Expand Down Expand Up @@ -1064,7 +1063,7 @@ export class TwitchDropsBot extends EventEmitter {
continue;
}

return stream;
return streamUrl;
}

// Get a list of active streams that have drops enabled
Expand All @@ -1073,26 +1072,25 @@ export class TwitchDropsBot extends EventEmitter {

// Filter out streams that failed too many times
streams = streams.filter(stream => {
return !this.#streamUrlTemporaryBlacklist.has(getStreamUrl(stream));
return !this.#streamUrlTemporaryBlacklist.has(getStreamUrl(stream.broadcaster.login));
});
logger.info('Found ' + streams.length + ' good streams');

if (streams.length === 0) {
return null;
}

return streams[0];
return getStreamUrl(streams[0].broadcaster.login);
}

const stream = await getStreamToWatch();
if (stream === null) {
const streamUrl = await getStreamToWatch();
logger.debug("want to watch: " + streamUrl);
if (streamUrl === null) {
throw new NoStreamsError();
}

const streamUrl = getStreamUrl(stream);

if (!(await this.#hasDropsAvailable(streamUrl, dropCampaignId))) {
logger.warn("This stream has no available Drops. This is likely because you already completed this campaign");
logger.debug("This stream has no available Drops. This is likely because you already completed this campaign");
return;
}

Expand All @@ -1106,7 +1104,7 @@ export class TwitchDropsBot extends EventEmitter {
// Watch first stream
logger.info('Watching stream: ' + streamUrl);
try {
await this.#watchStreamWrapper(stream, components);
await this.#watchStreamWrapper(streamUrl, components);
//todo: update campaign info when we claim a drop
} catch (error) {
if (error instanceof NoProgressError) {
Expand Down Expand Up @@ -1185,7 +1183,7 @@ export class TwitchDropsBot extends EventEmitter {
}
}

async #watchStreamWrapper(stream: Stream, components: Component[]) {
async #watchStreamWrapper(streamUrl: string, components: Component[]) {

const getComponent = <T extends Component>(c: Class<T>): T | null => {
for (const component of components) {
Expand All @@ -1207,12 +1205,10 @@ export class TwitchDropsBot extends EventEmitter {
});
}

const streamUrl = getStreamUrl(stream);

const startWatchTime = new Date().getTime();
try {
this.emit("start_watching_stream", streamUrl);
await this.#watchStream(stream, components);
await this.#watchStream(streamUrl, components);
} catch (error) {
if (error instanceof HighPriorityError) {
// Ignore
Expand Down Expand Up @@ -1253,20 +1249,22 @@ export class TwitchDropsBot extends EventEmitter {
}
}

async #watchStream(stream: Stream, components: Component[]) {

const streamUrl = getStreamUrl(stream);
async #watchStream(streamUrl: string, components: Component[]): Promise<void> {

// Create a "Chrome Devtools Protocol" session to listen to websocket events
const webSocketListener = new WebSocketListener();

const channelShellOperationResult = waitForResponseWithOperationName(this.#page, "ChannelShell");

let channelId: string | null = null;

// Set up web socket listener
webSocketListener.on('stream-down', message => {
this.#isStreamDown = true;
});
webSocketListener.on("points-earned", data => {
// Ignore these events if they are from a different stream than the one we are currently watching. This can happen if a user is watching multiple streams on one account.
if (stream.broadcaster.id !== data["channel_id"]) {
if (channelId === null || channelId !== data["channel_id"]) {
return;
}
this.emit("community_points_earned", data);
Expand Down Expand Up @@ -1295,6 +1293,8 @@ export class TwitchDropsBot extends EventEmitter {
// Go to the stream URL
await this.#page.goto(streamUrl);

channelId = (await channelShellOperationResult.data())["userOrError"]["channel"]["id"];

// Wait for the page to load completely (hopefully). This checks the video player container for any DOM changes and waits until there haven't been any changes for a few seconds.
logger.info('Waiting for page to load...');
const element = (await this.#page.$x('//div[@data-a-player-state]'))[0]
Expand Down
38 changes: 37 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "node:fs"
import path from "node:path";

import {Page} from "puppeteer";
import {HTTPResponse, Page} from "puppeteer";
import {parse} from "csv-parse/sync";
import {stringify} from "csv-stringify/sync";
import axios from "axios";
Expand Down Expand Up @@ -156,6 +156,42 @@ export function updateGames(campaigns: DropCampaign[], sourcePath: string = "./g
logger.info('Games list updated');
}

/**
* Create a promise to wait for a response with the given operation name. After calling Page.goto(), await the returned data() function to get the result.
* @param page
* @param operationName
*/
export function waitForResponseWithOperationName(page: Page, operationName: string) {
const result: {
operationIndex: number,
promise: Promise<HTTPResponse> | null,
data: any
} = {
operationIndex: -1,
promise: null,
data: async function () {
const response = await this.promise;
return (await response?.json())[this.operationIndex]["data"];
}
}
result.promise = page.waitForResponse((response: HTTPResponse) => {
if (response.url().startsWith("https://gql.twitch.tv/gql")) {
const postData = response.request().postData();
if (postData) {
const data = JSON.parse(postData);
for (let i = 0; i < data.length; ++i) {
if ( data[i]["operationName"] === operationName) {
result.operationIndex = i;
return true;
}
}
}
}
return false;
});
return result;
}

export default {
saveScreenshotAndHtml
};

0 comments on commit ee9957a

Please sign in to comment.