mirror of
https://codeberg.org/SafeTwitch/safetwitch.git
synced 2025-01-03 11:20:07 -05:00
Create category discovery page
This commit is contained in:
parent
a5f40b4e02
commit
e920c7d2f0
8 changed files with 369 additions and 11 deletions
80
frontend/src/views/CategoryView.vue
Normal file
80
frontend/src/views/CategoryView.vue
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async setup() {
|
||||||
|
const route = useRoute()
|
||||||
|
const game = route.params.game
|
||||||
|
const res = await fetch(`http://localhost:7000/api/discover/${game}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: await res.json()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
abbreviate(text: number) {
|
||||||
|
return Intl.NumberFormat('en-US', {
|
||||||
|
//@ts-ignore
|
||||||
|
notation: "compact",
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
}).format(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col max-w-5xl mx-auto">
|
||||||
|
<div class="flex space-x-4 p-3">
|
||||||
|
<img :src="data.cover" class="self-start rounded-md">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="font-bold text-5xl text-white">{{ data.name }}</h1>
|
||||||
|
<div class="inline-flex my-1 space-x-3">
|
||||||
|
<p class="font-bold text-white text-lg">Followers: {{ abbreviate(data.followers) }}</p>
|
||||||
|
<p class="font-bold text-white text-lg">Viewers: {{ abbreviate(data.viewers) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="mb-5">
|
||||||
|
<li v-for="tag in data.tags" :key="tag" class="inline-flex">
|
||||||
|
<span class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm">{{ tag }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="text-md text-gray-400 overflow-y-auto">{{ data.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-[58rem] mx-auto">
|
||||||
|
<ul>
|
||||||
|
<li v-for="stream in data.streams" :key="stream" class="inline-flex m-2 hover:scale-105 transition-transform">
|
||||||
|
<div class="bg-ctp-crust rounded-lg">
|
||||||
|
<a :href="`http://localhost:5173/${stream.streamer.name}`">
|
||||||
|
<img :src="stream.preview" class="rounded-lg rounded-b-none">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="text-white p-2 inline-flex space-x-2 w-full">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="inline-flex w-full">
|
||||||
|
<div class="inline-flex">
|
||||||
|
<img :src="stream.streamer.pfp" class="rounded-full mr-2">
|
||||||
|
<div>
|
||||||
|
<p class="font-bold w-[22.9rem] truncate">{{ stream.title }}</p>
|
||||||
|
<div class="inline-flex w-full justify-between">
|
||||||
|
<p class="text-gray-300">{{ stream.streamer.name }}</p>
|
||||||
|
<p class="self-end float-right"> <v-icon name="io-person"></v-icon> {{ abbreviate(stream.viewers) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
72
frontend/src/views/HomepageView.vue
Normal file
72
frontend/src/views/HomepageView.vue
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async setup() {
|
||||||
|
const res = await fetch(`http://localhost:7000/api/discover`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: await res.json()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
abbreviate(text: number) {
|
||||||
|
return Intl.NumberFormat('en-US', {
|
||||||
|
//@ts-ignore
|
||||||
|
notation: "compact",
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
}).format(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
|
<div class="p-2">
|
||||||
|
<h1 class="font-bold text-5xl text-white"> Discover </h1>
|
||||||
|
<p class="text-xl text-white"> Sort through popular categories</p>
|
||||||
|
|
||||||
|
<div class="pt-5 inline-flex text-white">
|
||||||
|
<p class="mr-2 font-bold text-white">Filter by tag</p>
|
||||||
|
<form class="relative">
|
||||||
|
<label for="searchBar" class="hidden">Search</label>
|
||||||
|
<v-icon
|
||||||
|
name="io-search-outline"
|
||||||
|
class="absolute my-auto inset-y-0 left-2"
|
||||||
|
></v-icon>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="searchBar"
|
||||||
|
name="searchBar"
|
||||||
|
placeholder="Search"
|
||||||
|
class="rounded-md p-1 pl-8 text-black bg-neutral-500 placeholder:text-white"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="">
|
||||||
|
<li v-for="category in data" :key="category" class="inline-flex m-2 hover:scale-105 transition-transform">
|
||||||
|
<div class="bg-ctp-crust max-w-[13.5rem] rounded-lg">
|
||||||
|
<a :href="`http://localhost:5173/game/${category.name}`">
|
||||||
|
<img :src="category.image" class="rounded-lg rounded-b-none">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<div>
|
||||||
|
<p class="font-bold text-white text-xl"> {{ category.displayName }}</p>
|
||||||
|
<p class="text-sm text-white"> {{ abbreviate(category.viewers) }} viewers</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="h-8 overflow-hidden">
|
||||||
|
<li v-for="tag in category.tags" :key="tag" class="inline-flex">
|
||||||
|
<span class="p-2.5 py-1.5 bg-ctp-surface0 rounded-md m-0.5 text-xs font-bold text-white">{{ tag }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -101,8 +101,8 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="w-full inline-flex space-x-4 justify-center p-4">
|
<div v-else class="w-full justify-center inline-flex space-x-4 p-4">
|
||||||
<div class="flex flex-wrap bg-ctp-crust p-6 rounded-lg max-w-prose min-w-[65ch] text-white">
|
<div class="flex bg-ctp-crust flex-col p-6 rounded-lg max-w-prose min-w-[65ch] text-white">
|
||||||
<div v-if="data.isLive" class="w-full mx-auto rounded-lg mb-5">
|
<div v-if="data.isLive" class="w-full mx-auto rounded-lg mb-5">
|
||||||
<video-player :options="videoOptions"> </video-player>
|
<video-player :options="videoOptions"> </video-player>
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,9 +127,9 @@ export default {
|
||||||
<h1 v-if="!data.stream" class="font-bold text-md self-end">
|
<h1 v-if="!data.stream" class="font-bold text-md self-end">
|
||||||
{{ data.followersAbbv }} Followers
|
{{ data.followersAbbv }} Followers
|
||||||
</h1>
|
</h1>
|
||||||
<div v-else class="w-[12rem]">
|
<div v-else class="w-[17rem]">
|
||||||
<p class="text-sm font-bold text-gray-200 self-end">
|
<p class="text-sm font-bold text-gray-200 self-end">
|
||||||
{{ truncate(data.stream.title, 75) }}
|
{{ truncate(data.stream.title, 130) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,9 +155,18 @@ export default {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2 pl- inline-flex">
|
||||||
|
<button class="text-white text-sm font-bold p-2 py-1 rounded-md bg-purple-600">
|
||||||
|
<v-icon name="bi-heart-fill" scale="0.85"></v-icon>
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p class="align-baseline font-bold ml-3">{{ data.followersAbbv }} Followers</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-ctp-mantle m-5 p-5 pt-3 rounded-lg w-full space-y-3">
|
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||||
<div class="inline-flex w-full">
|
<div class="inline-flex w-full">
|
||||||
<span class="pr-3 font-bold text-3xl">About</span>
|
<span class="pr-3 font-bold text-3xl">About</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,4 +14,14 @@ profileRouter.get('/users/:username', async (req, res, next) => {
|
||||||
res.send(streamerData)
|
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
|
export default profileRouter
|
|
@ -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) => {
|
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)
|
let m3u8Data = await twitch.getStream(req.params.username)
|
||||||
const urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
|
const urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
|
||||||
const matches = m3u8Data.match(urlRegex)
|
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) => {
|
proxyRouter.get('/hls/:encodedUrl' , async (req: Request, res: Response, next: NextFunction) => {
|
||||||
console.log('hi')
|
|
||||||
const unencodedUrl = Buffer.from(req.params.encodedUrl, 'base64url').toString()
|
const unencodedUrl = Buffer.from(req.params.encodedUrl, 'base64url').toString()
|
||||||
const m3u8Fetch = await fetch(unencodedUrl)
|
const m3u8Fetch = await fetch(unencodedUrl)
|
||||||
var m3u8Data = await m3u8Fetch.text()
|
var m3u8Data = await m3u8Fetch.text()
|
||||||
|
|
|
@ -15,7 +15,15 @@ const logLevels = {
|
||||||
|
|
||||||
export const logger = createLogger({
|
export const logger = createLogger({
|
||||||
format: format.combine(format.timestamp(), format.json()),
|
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
|
levels: logLevels
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { EventEmitter } from 'stream';
|
||||||
import WebSocket from 'ws'
|
import WebSocket from 'ws'
|
||||||
import { TwitchChatOptions, Metadata, MessageType, MessageTypes } from '../../../types/scraping/Chat'
|
import { TwitchChatOptions, Metadata, MessageType, MessageTypes } from '../../../types/scraping/Chat'
|
||||||
import { parseUsername } from './utils';
|
import { parseUsername } from './utils';
|
||||||
|
import { logger } from '../../logger';
|
||||||
|
|
||||||
export declare interface TwitchChat {
|
export declare interface TwitchChat {
|
||||||
on(event: 'PRIVMSG', listener: (username: string, messageType: MessageType, channel: string, message: string) => void): this
|
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 url = 'wss://irc-ws.chat.twitch.tv:443'
|
||||||
private ws: WebSocket | null;
|
private ws: WebSocket | null;
|
||||||
private isConnected: boolean = false
|
private isConnected: boolean = false
|
||||||
|
private manualDisconnect: boolean = false
|
||||||
|
|
||||||
constructor(options: TwitchChatOptions) {
|
constructor(options: TwitchChatOptions) {
|
||||||
super()
|
super()
|
||||||
|
@ -21,6 +23,7 @@ export class TwitchChat extends EventEmitter{
|
||||||
|
|
||||||
private parser() {
|
private parser() {
|
||||||
this.ws?.on('message', (data) => {
|
this.ws?.on('message', (data) => {
|
||||||
|
console.log(this.channels)
|
||||||
let normalData = data.toString()
|
let normalData = data.toString()
|
||||||
let splitted = normalData.split(":")
|
let splitted = normalData.split(":")
|
||||||
|
|
||||||
|
@ -45,10 +48,26 @@ export class TwitchChat extends EventEmitter{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect() {
|
public async connect() {
|
||||||
|
console.log('ss')
|
||||||
this.ws = new WebSocket(this.url)
|
this.ws = new WebSocket(this.url)
|
||||||
this.isConnected = true
|
this.isConnected = true
|
||||||
|
|
||||||
this.ws.on('open', () => {
|
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.onopen = () => {
|
||||||
if(this.ws) {
|
if(this.ws) {
|
||||||
this.ws.send('PASS none')
|
this.ws.send('PASS none')
|
||||||
this.ws.send('NICK justinfan333333333333')
|
this.ws.send('NICK justinfan333333333333')
|
||||||
|
@ -60,12 +79,13 @@ export class TwitchChat extends EventEmitter{
|
||||||
this.parser()
|
this.parser()
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addStreamer(streamerName: string) {
|
public addStreamer(streamerName: string) {
|
||||||
if(!this.isConnected) return;
|
if(!this.isConnected) return;
|
||||||
|
this.channels.push(streamerName)
|
||||||
this.ws!.send(`JOIN #${streamerName}`)
|
this.ws!.send(`JOIN #${streamerName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,11 @@ export class TwitchAPI {
|
||||||
return Promise.resolve(true)
|
return Promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the stream playlist file from twitch
|
||||||
|
* @param streamerName The username of the streamer
|
||||||
|
* @returns Promise<boolean>
|
||||||
|
*/
|
||||||
public getStream = async (streamerName: string) => {
|
public getStream = async (streamerName: string) => {
|
||||||
const isLive = await this.isLive(streamerName)
|
const isLive = await this.isLive(streamerName)
|
||||||
if(!isLive) return Promise.reject(new Error(`Streamer ${streamerName} is not live`))
|
if(!isLive) return Promise.reject(new Error(`Streamer ${streamerName} is not live`))
|
||||||
|
@ -249,4 +254,160 @@ export class TwitchAPI {
|
||||||
|
|
||||||
return await res.text()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue