diff --git a/routes/profileRoute.ts b/routes/profileRoute.ts index 22e3b11..10f3097 100644 --- a/routes/profileRoute.ts +++ b/routes/profileRoute.ts @@ -23,8 +23,10 @@ profileRouter.get('/discover', async (req, res, next) => { let discoveryData; if (cursor) { discoveryData = await twitch.getDirectory(15, cursor) + .catch(next) } else { discoveryData = await twitch.getDirectory(15) + .catch(next) } res.send({ @@ -69,4 +71,20 @@ profileRouter.get('/badges', async (req, res, next) => { }) }) +profileRouter.get('/search', async (req, res, next) => { + const query = req.query.query + if(!query) + return res.status(400).send({ + status: 'error', + message: 'No query provided' + }) + + const result = await twitch.getSearchResult(query.toString()) + .catch(next) + res.send({ + status: 'ok', + data: result + }) +}) + export default profileRouter \ No newline at end of file diff --git a/types/scraping/Category.ts b/types/scraping/Category.ts index 769d5b0..dd3a36a 100644 --- a/types/scraping/Category.ts +++ b/types/scraping/Category.ts @@ -5,7 +5,7 @@ export interface Category { displayName: string viewers: number tags: Tag[] - createdAt: Date - cursor: string + createdAt?: Date + cursor?: string image: string } \ No newline at end of file diff --git a/types/scraping/Streamer.ts b/types/scraping/Streamer.ts index 94b0e6a..3ae0168 100644 --- a/types/scraping/Streamer.ts +++ b/types/scraping/Streamer.ts @@ -17,13 +17,12 @@ export interface StreamData { export interface StreamerData { username: string followers: number - followersAbbv: string isLive: boolean about: string socials?: Social[] pfp: string stream?: StreamData | null - isPartner: boolean + isPartner: boolean | null colorHex: string id: number } diff --git a/util/errorHandler.ts b/util/errorHandler.ts index 116b4a8..8c0bfb5 100644 --- a/util/errorHandler.ts +++ b/util/errorHandler.ts @@ -5,5 +5,5 @@ export const errorHandler = (err: Error, req: Request, res: Response, next: Next return next(err) } - res.status(500).send({ status: 'error', message: err.message}) + res.status(500).send({ status: 'error', message: err}) } \ No newline at end of file diff --git a/util/scraping/extractor/index.ts b/util/scraping/extractor/index.ts index df558c9..0954645 100644 --- a/util/scraping/extractor/index.ts +++ b/util/scraping/extractor/index.ts @@ -11,502 +11,577 @@ const base64 = (data: String) => { * Class that interacts with the Twitch api */ export class TwitchAPI { - public readonly twitchUrl = 'https://gql.twitch.tv/gql' - public headers = { - "Client-Id": "kimne78kx3ncx6brgo4mv6wki5h1ko" - } + public readonly twitchUrl = 'https://gql.twitch.tv/gql' + public headers = { + "Client-Id": "kimne78kx3ncx6brgo4mv6wki5h1ko" + } - constructor() {} - - /** - * Gets information about a streamer, like socials, about, and more. - * @see StreamerData - * @param streamerName The username of the streamer - * @returns Promise - */ - public getStreamerInfo = async (streamerName: string) => { - const payload = [ - { - "operationName": "ChannelRoot_AboutPanel", - "variables": { - "channelLogin": streamerName, - "skipSchedule": false - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "6089531acef6c09ece01b440c41978f4c8dc60cb4fa0124c9a9d3f896709b6c6" - } - } - }, - { - "operationName":"StreamMetadata", - "variables":{ - "channelLogin": streamerName - }, - "extensions":{ - "persistedQuery":{ - "version":1, - "sha256Hash":"a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962" - } - } - }, - { - "operationName": "StreamTagsTrackingChannel", - "variables": { - "channel": streamerName - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "6aa3851aaaf88c320d514eb173563d430b28ed70fdaaf7eeef6ed4b812f48608" - } - } - }, - { - "operationName": "VideoPreviewOverlay", - "variables": { - "login": streamerName - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "9515480dee68a77e667cb19de634739d33f243572b007e98e67184b1a5d8369f" - } - } - }, - { - "operationName": "UseViewCount", - "variables": { - "channelLogin": streamerName - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "00b11c9c428f79ae228f30080a06ffd8226a1f068d6f52fbc057cbde66e994c2" - } - } - }, - ] - - const res = await fetch(this.twitchUrl, { - method: 'POST', - body: JSON.stringify(payload), - headers: this.headers - }) - - const data = await res.json() - const rawStreamerData = data[0].data - - - // get socials - const socials: LooseObject[] = [] - if (rawStreamerData.user.channel && rawStreamerData.user.channel.socialMedias) { - for (let social of rawStreamerData.user.channel.socialMedias) { - socials.push({ - type: social.name, - name: social.title, - link: social.url - }) - } - } - - // check if is liver - const rawStreamData = data[1].data.user.stream - let parsedStream: StreamData | null; - if(!rawStreamData) { - parsedStream = null - } else { - const tags: string[] = [] - for (let tagData of data[2].data.user.stream.freeformTags) { - tags.push(tagData.name) - } - - const previewUrl = `${process.env.URL}/proxy/img/${base64(data[3].data.user.stream.previewImageURL)}` - - parsedStream = { - title: data[1].data.user.lastBroadcast.title, - topic: rawStreamData.game.name, - startedAt: new Date(rawStreamData.createdAt).valueOf(), - tags, - viewers: Number(data[4].data.user.stream.viewersCount), - preview: previewUrl - } - } - - const abbreviatedFollowers = Intl.NumberFormat('en-US', { - notation: "compact", - maximumFractionDigits: 1 - }).format(rawStreamerData.user.followers.totalCount) - - const streamerData: StreamerData = { - username: rawStreamerData.user.displayName, - about: rawStreamerData.user.description, - pfp: `${process.env.URL}/proxy/img/${base64(rawStreamerData.user.profileImageURL)}`, - followers: rawStreamerData.user.followers.totalCount, - socials: socials as Social[], - isLive: (!!parsedStream), - isPartner: rawStreamerData.user.isPartner, - followersAbbv: abbreviatedFollowers, - colorHex: '#' + rawStreamerData.user.primaryColorHex, - id: Number(rawStreamerData.user.id), - stream: parsedStream - } - - return Promise.resolve(streamerData) - } - - /** - * Gets the current viewers of a stream - * @param streamerName The username of the streamer - * @returns Promise - */ - public getViewers = async (streamerName: string) => { - const payload = [ - { - "operationName": "UseViewCount", - "variables": { - "channelLogin": streamerName - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "00b11c9c428f79ae228f30080a06ffd8226a1f068d6f52fbc057cbde66e994c2" - } - } - }, - ] - - const res = await fetch(this.twitchUrl, { - method: 'POST', - body: JSON.stringify(payload), - headers: this.headers - }) - - const rawData = await res.json() - - if(!rawData[0].data.user.stream) - return Promise.reject(new Error(`Streamer ${streamerName} is not live`)) - - return Promise.resolve(rawData[0].data.user.stream.viewersCount) - } - - public isLive = async (streamerName: string) => { - const payload = [ - { - "operationName": "UseViewCount", - "variables": { - "channelLogin": streamerName - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "00b11c9c428f79ae228f30080a06ffd8226a1f068d6f52fbc057cbde66e994c2" - } - } - }, - ] - - const res = await fetch(this.twitchUrl, { - method: 'POST', - body: JSON.stringify(payload), - headers: this.headers - }) - - const rawData = await res.json() - - if(!rawData[0].data.user.stream) - 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`)) - - // Get token - const payload = { - "operationName": "PlaybackAccessToken_Template", - "query": "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}", + constructor() { } + /** + * Gets information about a streamer, like socials, about, and more. + * @see StreamerData + * @param streamerName The username of the streamer + * @returns Promise + */ + public getStreamerInfo = async (streamerName: string) => { + const payload = [ + { + "operationName": "ChannelRoot_AboutPanel", "variables": { - "isLive": true, - "login": streamerName, - "isVod": false, - "vodID": "", - "playerType": "site" - } - } - - var res = await fetch(this.twitchUrl, { - headers: this.headers, - body: JSON.stringify(payload), - method: 'POST' - }) - - const data = await res.json() - - const token = data.data.streamPlaybackAccessToken.value - const signature = data.data.streamPlaybackAccessToken.signature - - const playlistUrl = `https://usher.ttvnw.net/api/channel/hls/${streamerName.toLowerCase()}.m3u8` - const params = `?sig=${signature}&token=${token}` - var res = await fetch(playlistUrl + params, { - headers: this.headers - }) - - 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?: string) => { - 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" - } + "channelLogin": streamerName, + "skipSchedule": false + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "6089531acef6c09ece01b440c41978f4c8dc60cb4fa0124c9a9d3f896709b6c6" } } - ] - - 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: Category[] = [] - - for (let category of categories) { - let tags: Tag[] = [] - for (let tag of category.node.tags) { - tags.push(tag.tagName) + }, + { + "operationName": "StreamMetadata", + "variables": { + "channelLogin": streamerName + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962" + } } + }, + { + "operationName": "StreamTagsTrackingChannel", + "variables": { + "channel": streamerName + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "6aa3851aaaf88c320d514eb173563d430b28ed70fdaaf7eeef6ed4b812f48608" + } + } + }, + { + "operationName": "VideoPreviewOverlay", + "variables": { + "login": streamerName + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "9515480dee68a77e667cb19de634739d33f243572b007e98e67184b1a5d8369f" + } + } + }, + { + "operationName": "UseViewCount", + "variables": { + "channelLogin": streamerName + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "00b11c9c428f79ae228f30080a06ffd8226a1f068d6f52fbc057cbde66e994c2" + } + } + }, + ] - formattedCategories.push({ - name: category.node.name, - displayName: category.node.displayName, - viewers: category.node.viewersCount, - tags: tags, - createdAt: category.node.originalReleaseDate, - cursor: category.cursor, - image: `${process.env.URL}/proxy/img/${base64(category.node.avatarURL)}` + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + + const data = await res.json() + const rawStreamerData = data[0].data + + + // get socials + const socials: LooseObject[] = [] + if (rawStreamerData.user.channel && rawStreamerData.user.channel.socialMedias) { + for (let social of rawStreamerData.user.channel.socialMedias) { + socials.push({ + type: social.name, + name: social.title, + link: social.url }) } - - return formattedCategories } - public getDirectoryGame = async (name: string, streamLimit: number, cursor?: string) => { - const payload: any[] = [ - { - "operationName": "DirectoryPage_Game", - "variables": { - "imageWidth": 50, - "name": name, - "options": { - "sort": "RELEVANCE", - "recommendationsContext": { - "platform": "web" - }, - "requestID": "JIRA-VXP-2397", - "freeformTags": null, - "tags": [] + // check if is liver + const rawStreamData = data[1].data.user.stream + let parsedStream: StreamData | null; + if (!rawStreamData) { + parsedStream = null + } else { + const tags: string[] = [] + for (let tagData of data[2].data.user.stream.freeformTags) { + tags.push(tagData.name) + } + + const previewUrl = `${process.env.URL}/proxy/img/${base64(data[3].data.user.stream.previewImageURL)}` + + parsedStream = { + title: data[1].data.user.lastBroadcast.title, + topic: rawStreamData.game.name, + startedAt: new Date(rawStreamData.createdAt).valueOf(), + tags, + viewers: Number(data[4].data.user.stream.viewersCount), + preview: previewUrl + } + } + + const abbreviatedFollowers = Intl.NumberFormat('en-US', { + notation: "compact", + maximumFractionDigits: 1 + }).format(rawStreamerData.user.followers.totalCount) + + const streamerData: StreamerData = { + username: rawStreamerData.user.displayName, + about: rawStreamerData.user.description, + pfp: `${process.env.URL}/proxy/img/${base64(rawStreamerData.user.profileImageURL)}`, + followers: rawStreamerData.user.followers.totalCount, + socials: socials as Social[], + isLive: (!!parsedStream), + isPartner: rawStreamerData.user.isPartner, + colorHex: '#' + rawStreamerData.user.primaryColorHex, + id: Number(rawStreamerData.user.id), + stream: parsedStream + } + + return Promise.resolve(streamerData) + } + + /** + * Gets the current viewers of a stream + * @param streamerName The username of the streamer + * @returns Promise + */ + public getViewers = async (streamerName: string) => { + const payload = [ + { + "operationName": "UseViewCount", + "variables": { + "channelLogin": streamerName + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "00b11c9c428f79ae228f30080a06ffd8226a1f068d6f52fbc057cbde66e994c2" + } + } + }, + ] + + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + + const rawData = await res.json() + + if (!rawData[0].data.user.stream) + return Promise.reject(new Error(`Streamer ${streamerName} is not live`)) + + return Promise.resolve(rawData[0].data.user.stream.viewersCount) + } + + public isLive = async (streamerName: string) => { + const payload = [ + { + "operationName": "UseViewCount", + "variables": { + "channelLogin": streamerName + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "00b11c9c428f79ae228f30080a06ffd8226a1f068d6f52fbc057cbde66e994c2" + } + } + }, + ] + + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + + const rawData = await res.json() + + if (!rawData[0].data.user.stream) + 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`)) + + // Get token + const payload = { + "operationName": "PlaybackAccessToken_Template", + "query": "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}", + "variables": { + "isLive": true, + "login": streamerName, + "isVod": false, + "vodID": "", + "playerType": "site" + } + } + + var res = await fetch(this.twitchUrl, { + headers: this.headers, + body: JSON.stringify(payload), + method: 'POST' + }) + + const data = await res.json() + + const token = data.data.streamPlaybackAccessToken.value + const signature = data.data.streamPlaybackAccessToken.signature + + const playlistUrl = `https://usher.ttvnw.net/api/channel/hls/${streamerName.toLowerCase()}.m3u8` + const params = `?sig=${signature}&token=${token}` + var res = await fetch(playlistUrl + params, { + headers: this.headers + }) + + 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?: string) => { + const payload: any[] = [ + { + "operationName": "BrowsePage_AllDirectories", + "variables": { + "limit": limit, + "options": { + "recommendationsContext": { + "platform": "web" }, - "sortTypeIsRecency": false, - "limit": streamLimit - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "df4bb6cc45055237bfaf3ead608bbafb79815c7100b6ee126719fac3762ddf8b" - } + "requestID": "JIRA-VXP-2397", + "sort": "RELEVANCE", + "tags": [] } }, - { - "operationName": "Directory_DirectoryBanner", - "variables": { - "name": name - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "2670fbecd8fbea0211c56528d6eff5752ef9d6c73cd5238d395784b46335ded4" - } + "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() - - if(!data[0].data.game) return null; - - - let streams = [] - if(data[0].data.game.streams) - streams = data[0].data.game.streams.edges - let formatedStreams: CategoryMinifiedStream[] = [] - - 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: `${process.env.URL}/proxy/img/${base64(stream.node.previewImageURL)}`, - tags, - cursor: stream.cursor, - streamer: { - name: stream.node.broadcaster.displayName, - pfp: `${process.env.URL}/proxy/img/${base64(stream.node.broadcaster.profileImageURL)}`, - colorHex: stream.node.broadcaster.primaryColorHex - } - }) } - - const rawGameData = data[1].data.game; + ] + + 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: Category[] = [] + + for (let category of categories) { let tags: Tag[] = [] - for(let tag of rawGameData.tags) { + for (let tag of category.node.tags) { tags.push(tag.tagName) } - const formatedGameData: CategoryData = { - name: rawGameData.name, - cover: `${process.env.URL}/proxy/img/${base64(rawGameData.avatarURL)}`, - description: rawGameData.description, - viewers: rawGameData.viewersCount, - followers: rawGameData.followersCount, - tags, - streams: formatedStreams - } - - - return formatedGameData + formattedCategories.push({ + name: category.node.name, + displayName: category.node.displayName, + viewers: category.node.viewersCount, + tags: tags, + createdAt: category.node.originalReleaseDate, + cursor: category.cursor, + image: `${process.env.URL}/proxy/img/${base64(category.node.avatarURL)}` + }) } - public getTwitchBadges = async () => { - const payload = [ - { - "operationName": "ChannelPointsPredictionBadges", - "variables": {}, - "extensions": { - "persistedQuery": { + return formattedCategories + } + + public getDirectoryGame = async (name: string, streamLimit: number, cursor?: string) => { + 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" + } + } + }, + ] + + 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() + + if (!data[0].data.game) return null; + + + let streams = [] + if (data[0].data.game.streams) + streams = data[0].data.game.streams.edges + let formatedStreams: CategoryMinifiedStream[] = [] + + 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: `${process.env.URL}/proxy/img/${base64(stream.node.previewImageURL)}`, + tags, + cursor: stream.cursor, + streamer: { + name: stream.node.broadcaster.displayName, + pfp: `${process.env.URL}/proxy/img/${base64(stream.node.broadcaster.profileImageURL)}`, + colorHex: stream.node.broadcaster.primaryColorHex + } + }) + } + + const rawGameData = data[1].data.game; + let tags: Tag[] = [] + for (let tag of rawGameData.tags) { + tags.push(tag.tagName) + } + + const formatedGameData: CategoryData = { + name: rawGameData.name, + cover: `${process.env.URL}/proxy/img/${base64(rawGameData.avatarURL)}`, + description: rawGameData.description, + viewers: rawGameData.viewersCount, + followers: rawGameData.followersCount, + tags, + streams: formatedStreams + } + + + return formatedGameData + } + + public getTwitchBadges = async () => { + const payload = [ + { + "operationName": "ChannelPointsPredictionBadges", + "variables": {}, + "extensions": { + "persistedQuery": { "sha256Hash": "36995b30b22c31d1cd0aa329987ac9b5368bb7e6e1ab1df42808bdaa80a6dbf9", "version": 1 - } - }, - } - ] - - const res = await fetch(this.twitchUrl, { - method: 'POST', - body: JSON.stringify(payload), - headers: this.headers - }) - const data = await res.json() - - if(!data[0].data.badges) return null; - let formatedBadges: Badge[] = [] - - for (let badge of data[0].data.badges) { - let formatedBadge: Badge = { - id: Buffer.from(badge.id, 'base64').toString(), - setId: badge.setID, - title: badge.title, - version: badge.version, - images: { - image1x: `${process.env.URL}/proxy/img/${base64(badge.image1x)}`, - image2x: `${process.env.URL}/proxy/img/${base64(badge.image2x)}`, - image4x: `${process.env.URL}/proxy/img/${base64(badge.image4x)}`, } - } + }, + } + ] - formatedBadges.push(formatedBadge) + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + const data = await res.json() + + if (!data[0].data.badges) return null; + let formatedBadges: Badge[] = [] + + for (let badge of data[0].data.badges) { + let formatedBadge: Badge = { + id: Buffer.from(badge.id, 'base64').toString(), + setId: badge.setID, + title: badge.title, + version: badge.version, + images: { + image1x: `${process.env.URL}/proxy/img/${base64(badge.image1x)}`, + image2x: `${process.env.URL}/proxy/img/${base64(badge.image2x)}`, + image4x: `${process.env.URL}/proxy/img/${base64(badge.image4x)}`, + } } - return formatedBadges + formatedBadges.push(formatedBadge) } - public getStreamerBadges = async (streamerName: string) => { - const payload = { - "extensions": { - "persistedQuery": { - "sha256Hash": "86f43113c04606e6476e39dcd432dee47c994d77a83e54b732e11d4935f0cd08", - "version": 1 - } - }, - "operationName": "ChatList_Badges", - "variables": { - "channelLogin": streamerName - } - } + return formatedBadges + } - const res = await fetch(this.twitchUrl, { - method: 'POST', - body: JSON.stringify(payload), - headers: this.headers - }) - const data = await res.json() - - if(!data.data.user.broadcastBadges) return null; - let formatedBadges: Badge[] = [] - - for (let badge of data.data.user.broadcastBadges) { - let formatedBadge: Badge = { - id: Buffer.from(badge.id, 'base64').toString(), - setId: badge.setID, - title: badge.title, - version: badge.version, - images: { - image1x: `${process.env.URL}/proxy/img/${base64(badge.image1x)}`, - image2x: `${process.env.URL}/proxy/img/${base64(badge.image2x)}`, - image4x: `${process.env.URL}/proxy/img/${base64(badge.image4x)}`, - } + public getStreamerBadges = async (streamerName: string) => { + const payload = { + "extensions": { + "persistedQuery": { + "sha256Hash": "86f43113c04606e6476e39dcd432dee47c994d77a83e54b732e11d4935f0cd08", + "version": 1 } + }, + "operationName": "ChatList_Badges", + "variables": { + "channelLogin": streamerName + } + } - formatedBadges.push(formatedBadge) + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + const data = await res.json() + + if (!data.data.user.broadcastBadges) return null; + let formatedBadges: Badge[] = [] + + for (let badge of data.data.user.broadcastBadges) { + let formatedBadge: Badge = { + id: Buffer.from(badge.id, 'base64').toString(), + setId: badge.setID, + title: badge.title, + version: badge.version, + images: { + image1x: `${process.env.URL}/proxy/img/${base64(badge.image1x)}`, + image2x: `${process.env.URL}/proxy/img/${base64(badge.image2x)}`, + image4x: `${process.env.URL}/proxy/img/${base64(badge.image4x)}`, + } } - return formatedBadges + formatedBadges.push(formatedBadge) } + + return formatedBadges + } + + public getSearchResult = async (query: string) => { + const payload = { + "operationName": "SearchResultsPage_SearchResults", + "variables": { + "query": query, + "options": null, + "requestID": "75948144-d051-4203-8511-57f3ee9b809a" + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "6ea6e6f66006485e41dbe3ebd69d5674c5b22896ce7b595d7fce6411a3790138" + } + } + } + + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + const data = await res.json() + const resultsData = data.data.searchFor + + const formattedStreamers: StreamerData[] = resultsData.channels.edges.map((data: any) => { + return { + username: data.item.login, + followers: data.item.followers.totalCount, + isLive: !(data.item.stream === null), + about: data.item.description, + pfp: `${process.env.URL}/proxy/img/${base64(data.item.profileImageURL)}`, + isPartner: null, + colorHex: '#fff', + id: Number(data.item.channel.id) + } + }) + + const foundCategories: Category[] = [] + for (let category of resultsData.games.edges) { + let tags = category.item.tags.map((data: { tagName: string }) => { + return data.tagName + }) + + foundCategories.push({ + name: category.item.name, + displayName: category.item.displayName, + viewers: category.item.viewersCount, + tags, + image: category.item.boxArtURL, + }) + } + + const foundRelatedLiveChannels: StreamData[] = await Promise.all(resultsData.relatedLiveChannels.edges.map(async (data: any) => { + return await this.getStreamerInfo(data.item.stream.broadcaster.login) + })) + + const foundChannelsWithTag: StreamData[] = resultsData.channelsWithTag.edges.map((data: any) => { + return { + username: data.item.login, + followers: data.item.followers.totalCount, + isLive: !(data.item.stream === null), + about: data.item.description, + pfp: `${process.env.URL}/proxy/img/${base64(data.item.profileImageURL)}`, + isPartner: null, + colorHex: '#fff', + id: Number(data.item.channel.id) + } + }) + + return { + channels: formattedStreamers, + categories: foundCategories, + relatedChannels: foundRelatedLiveChannels, + channelsWithTag: foundChannelsWithTag + } + } } \ No newline at end of file