From e920c7d2f0f9098de4557fdf6954793a11c7cf16 Mon Sep 17 00:00:00 2001 From: dragongoose <19649813+dragongoose@users.noreply.github.com> Date: Sun, 19 Mar 2023 19:51:55 -0400 Subject: [PATCH] Create category discovery page --- frontend/src/views/CategoryView.vue | 80 ++++++++++++ frontend/src/views/HomepageView.vue | 72 +++++++++++ frontend/src/views/UserView.vue | 19 ++- server/routes/profileRoute.ts | 10 ++ server/routes/proxyRoute.ts | 2 - server/util/logger.ts | 10 +- server/util/scraping/chat/chat.ts | 24 +++- server/util/scraping/extractor/index.ts | 163 +++++++++++++++++++++++- 8 files changed, 369 insertions(+), 11 deletions(-) create mode 100644 frontend/src/views/CategoryView.vue create mode 100644 frontend/src/views/HomepageView.vue diff --git a/frontend/src/views/CategoryView.vue b/frontend/src/views/CategoryView.vue new file mode 100644 index 0000000..ad34269 --- /dev/null +++ b/frontend/src/views/CategoryView.vue @@ -0,0 +1,80 @@ + + + diff --git a/frontend/src/views/HomepageView.vue b/frontend/src/views/HomepageView.vue new file mode 100644 index 0000000..9f96606 --- /dev/null +++ b/frontend/src/views/HomepageView.vue @@ -0,0 +1,72 @@ + + + diff --git a/frontend/src/views/UserView.vue b/frontend/src/views/UserView.vue index a0315c8..b8bb289 100644 --- a/frontend/src/views/UserView.vue +++ b/frontend/src/views/UserView.vue @@ -101,8 +101,8 @@ export default { -
-
+
+
@@ -127,9 +127,9 @@ export default {

{{ data.followersAbbv }} Followers

-
+

- {{ truncate(data.stream.title, 75) }} + {{ truncate(data.stream.title, 130) }}

@@ -155,9 +155,18 @@ export default {
+ +
+ + +

{{ data.followersAbbv }} Followers

+
-
+
About
diff --git a/server/routes/profileRoute.ts b/server/routes/profileRoute.ts index cfee2ee..90eecc9 100644 --- a/server/routes/profileRoute.ts +++ b/server/routes/profileRoute.ts @@ -14,4 +14,14 @@ profileRouter.get('/users/:username', async (req, res, next) => { res.send(streamerData) }) +profileRouter.get('/discover', async (req, res, next) => { + let discoveryData = await twitch.getDirectory(50) + res.send(discoveryData) +}) + +profileRouter.get('/discover/:game', async (req, res, next) => { + let discoveryData = await twitch.getDirectoryGame(req.params.game, 50) + res.send(discoveryData) +}) + export default profileRouter \ No newline at end of file diff --git a/server/routes/proxyRoute.ts b/server/routes/proxyRoute.ts index 8206137..cd0f997 100644 --- a/server/routes/proxyRoute.ts +++ b/server/routes/proxyRoute.ts @@ -30,7 +30,6 @@ proxyRouter.get('/img', async (req: Request, res: Response, next: NextFunction) proxyRouter.get('/stream/:username/hls.m3u8', async (req: Request, res: Response, next: NextFunction) => { - console.log(req.params.username) let m3u8Data = await twitch.getStream(req.params.username) const urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; const matches = m3u8Data.match(urlRegex) @@ -46,7 +45,6 @@ proxyRouter.get('/stream/:username/hls.m3u8', async (req: Request, res: Response }) proxyRouter.get('/hls/:encodedUrl' , async (req: Request, res: Response, next: NextFunction) => { - console.log('hi') const unencodedUrl = Buffer.from(req.params.encodedUrl, 'base64url').toString() const m3u8Fetch = await fetch(unencodedUrl) var m3u8Data = await m3u8Fetch.text() diff --git a/server/util/logger.ts b/server/util/logger.ts index 4b92380..2624f21 100644 --- a/server/util/logger.ts +++ b/server/util/logger.ts @@ -15,7 +15,15 @@ const logLevels = { export const logger = createLogger({ format: format.combine(format.timestamp(), format.json()), - transports: [new transports.Console({}), new transports.File({ filename: './serverLog.log' })], + transports: [ + new transports.Console({ + format: format.combine( + format.colorize(), + format.simple() + ) + }), + new transports.File({ filename: './serverLog.log' }) + ], levels: logLevels }); diff --git a/server/util/scraping/chat/chat.ts b/server/util/scraping/chat/chat.ts index 5e021ec..2f7bcf3 100644 --- a/server/util/scraping/chat/chat.ts +++ b/server/util/scraping/chat/chat.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'stream'; import WebSocket from 'ws' import { TwitchChatOptions, Metadata, MessageType, MessageTypes } from '../../../types/scraping/Chat' import { parseUsername } from './utils'; +import { logger } from '../../logger'; export declare interface TwitchChat { on(event: 'PRIVMSG', listener: (username: string, messageType: MessageType, channel: string, message: string) => void): this @@ -12,6 +13,7 @@ export class TwitchChat extends EventEmitter{ private url = 'wss://irc-ws.chat.twitch.tv:443' private ws: WebSocket | null; private isConnected: boolean = false + private manualDisconnect: boolean = false constructor(options: TwitchChatOptions) { super() @@ -21,6 +23,7 @@ export class TwitchChat extends EventEmitter{ private parser() { this.ws?.on('message', (data) => { + console.log(this.channels) let normalData = data.toString() let splitted = normalData.split(":") @@ -45,10 +48,26 @@ export class TwitchChat extends EventEmitter{ } public async connect() { + console.log('ss') this.ws = new WebSocket(this.url) this.isConnected = true + + this.ws.onclose = () => { + logger.info('Disconnected from twitch IRC'), + logger.info(`Subscribed to channels ${this.channels}`) + if(this.manualDisconnect) return + const toEmit = { + type: 'SERVERMSG', + message: 'Disconnected' + } + this.emit(JSON.stringify(toEmit)) + + this.ws = null + this.isConnected = false + this.connect() + } - this.ws.on('open', () => { + this.ws.onopen = () => { if(this.ws) { this.ws.send('PASS none') this.ws.send('NICK justinfan333333333333') @@ -60,12 +79,13 @@ export class TwitchChat extends EventEmitter{ this.parser() return Promise.resolve() } - }) + } } public addStreamer(streamerName: string) { if(!this.isConnected) return; + this.channels.push(streamerName) this.ws!.send(`JOIN #${streamerName}`) } diff --git a/server/util/scraping/extractor/index.ts b/server/util/scraping/extractor/index.ts index 34104ed..f82bc9a 100644 --- a/server/util/scraping/extractor/index.ts +++ b/server/util/scraping/extractor/index.ts @@ -211,8 +211,13 @@ export class TwitchAPI { return Promise.resolve(false) return Promise.resolve(true) - } + } + /** + * Gets the stream playlist file from twitch + * @param streamerName The username of the streamer + * @returns Promise + */ public getStream = async (streamerName: string) => { const isLive = await this.isLive(streamerName) if(!isLive) return Promise.reject(new Error(`Streamer ${streamerName} is not live`)) @@ -249,4 +254,160 @@ export class TwitchAPI { return await res.text() } + + + /** + * Gets the homepage discovery tab of twitch + * @param limit Maximum categories to get + * @param cursor The current page you're at (for pagination) + + */ + public getDirectory = async (limit: number, cursor?: number) => { + const payload: any[] = [ + { + "operationName": "BrowsePage_AllDirectories", + "variables": { + "limit": limit, + "options": { + "recommendationsContext": { + "platform": "web" + }, + "requestID": "JIRA-VXP-2397", + "sort": "RELEVANCE", + "tags": [] + } + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "1d1914ca3cbfaa607ecd5595b2e305e96acf987c8f25328f7713b25f604c4668" + } + } + } + ] + + if(cursor) + payload[0].variables.cursor = cursor + + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + const data = await res.json() + const categories = data[0].data.directoriesWithTags.edges + let formattedCategories = [] + + for (let category of categories) { + let tags = [] + for (let tag of category.node.tags) { + tags.push(tag.tagName) + } + + formattedCategories.push({ + name: category.node.name, + displayName: category.node.displayName, + viewers: category.node.viewersCount, + tags: tags, + createdAt: category.node.originalReleaseDate, + image: `${process.env.URL}/proxy/img?imageUrl=${category.node.avatarURL}` + }) + } + + return formattedCategories + } + + public getDirectoryGame = async (name: string, streamLimit: number) => { + const payload: any[] = [ + { + "operationName": "DirectoryPage_Game", + "variables": { + "imageWidth": 50, + "name": name, + "options": { + "sort": "RELEVANCE", + "recommendationsContext": { + "platform": "web" + }, + "requestID": "JIRA-VXP-2397", + "freeformTags": null, + "tags": [] + }, + "sortTypeIsRecency": false, + "limit": streamLimit + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "df4bb6cc45055237bfaf3ead608bbafb79815c7100b6ee126719fac3762ddf8b" + } + } + }, + { + "operationName": "Directory_DirectoryBanner", + "variables": { + "name": name + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "2670fbecd8fbea0211c56528d6eff5752ef9d6c73cd5238d395784b46335ded4" + } + } + }, + ] + + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + const data = await res.json() + + if(!data[0].data.game) return null; + + + let streams = [] + if(data[0].data.game.streams) + streams = data[0].data.game.streams.edges + let formatedStreams = [] + + for(let stream of streams) { + let tags = [] + for (let tag of stream.node.freeformTags) { + tags.push(tag.name) + } + + formatedStreams.push({ + title: stream.node.title, + viewers: stream.node.viewersCount, + preview: stream.node.previewImageURL, + tags, + streamer: { + name: stream.node.broadcaster.displayName, + pfp: stream.node.broadcaster.profileImageURL, + colorHex: stream.node.broadcaster.primaryColorHex + } + }) + } + + const rawGameData = data[1].data.game; + let tags = [] + for(let tag of rawGameData.tags) { + tags.push(tag.tagName) + } + + const formatedGameData = { + name: rawGameData.name, + cover: rawGameData.avatarURL, + description: rawGameData.description, + viewers: rawGameData.viewersCount, + followers: rawGameData.followersCount, + tags, + streams: formatedStreams + } + + + return formatedGameData + } } \ No newline at end of file