0
Fork 0
mirror of https://codeberg.org/SafeTwitch/safetwitch-backend.git synced 2025-01-31 16:48:52 -05:00
safetwitch-backend/util/scraping/extractor/index.ts
2023-03-24 07:41:33 -04:00

419 lines
No EOL
13 KiB
TypeScript

import { LooseObject } from "../../../types/looseTypes"
import { StreamerData, StreamData, Social } from "../../../types/scraping/Streamer"
const base64 = (data: String) => {
return Buffer.from(data).toString('base64url')
}
/**
* Class that interacts with the Twitch api
*/
export class TwitchAPI {
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<StreamerData>
*/
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: data[3].data.user.stream.previewImageURL
}
}
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: 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<number>
*/
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<boolean>
*/
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?: 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/${base64(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
}
}