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