0
Fork 0
mirror of https://codeberg.org/SafeTwitch/safetwitch-backend.git synced 2024-12-22 05:02:58 -05:00
safetwitch-backend/extractor/twitch/parser.go
2024-02-06 12:02:28 -05:00

337 lines
10 KiB
Go

package twitch
import (
"encoding/base64"
"errors"
"os"
"safetwitch-backend/extractor"
"safetwitch-backend/extractor/structs"
"strings"
"time"
"github.com/tidwall/gjson"
)
func ParseStreamer(streamerData gjson.Result, isLive bool, socials []structs.Social, stream *structs.Stream, login string, streamerBanner string) (structs.Streamer, error) {
// Store streamerColorHex in memory to use as pointer
var streamerColorHex *string
rawStreamerColorHex := streamerData.Get("user.primaryColorHex").String()
if rawStreamerColorHex == "" {
streamerColorHex = nil
} else {
streamerColorHex = &rawStreamerColorHex
}
var bannerUrl *string = &streamerBanner
if *bannerUrl == "" {
bannerUrl = nil
} else {
proxied := extractor.ProxyUrl(*bannerUrl)
bannerUrl = &proxied
}
parsedStreamer := structs.Streamer{
Username: streamerData.Get("user.displayName").String(),
Login: login,
About: streamerData.Get("user.description").String(),
Pfp: extractor.ProxyUrl(streamerData.Get("user.profileImageURL").String()),
Banner: bannerUrl,
Followers: int(streamerData.Get("user.followers.totalCount").Int()),
Socials: socials,
IsLive: isLive,
IsPartner: streamerData.Get("user.isPartner").Bool(),
ColorHex: streamerColorHex,
Id: streamerData.Get("user.id").String(),
Stream: stream,
}
return parsedStreamer, nil
}
func ParseSocials(data string) ([]structs.Social, error) {
var parsedSocials []structs.Social
result := gjson.Get(data, "user.channel.socialMedias")
for _, social := range result.Array() {
parsedSocials = append(parsedSocials, structs.Social{
Name: social.Get("title").String(),
Type: social.Get("name").String(),
Url: social.Get("url").String(),
})
}
if !result.Exists() {
return parsedSocials, errors.New("error while parsing socials, path does not exist")
}
return parsedSocials, nil
}
// Takes in all of the data returned from twitch (see payload from Streamer.go)
// and parses it into a stream. Offset is used for bulk processing
// when the offsets are different. Use offset 0 for normal uses
func ParseStream(data string, offset int) (*structs.Stream, error) {
// check if live
// default "1.data.user.stream"
stream := gjson.Get(data, extractor.GenGjsonQuery(offset+1, ".data.user.stream"))
if !stream.IsObject() {
return nil, errors.New("streamer is not live")
}
var tags []string
// default "2.data.user.stream.freeformTags"
tagArea := gjson.Get(data, extractor.GenGjsonQuery(offset+2, ".data.user.stream.freeformTags")).Array()
for _, tag := range tagArea {
tags = append(tags, tag.Get("name").String())
}
time, err := time.Parse(time.RFC3339, stream.Get("createdAt").String())
if err != nil {
return &structs.Stream{}, err
}
parsedStream := structs.Stream{
// default "1.data.user.lastBroadcast.title"
Title: gjson.Get(data, extractor.GenGjsonQuery(offset+1, ".data.user.lastBroadcast.title")).String(),
Topic: stream.Get("game.name").String(),
StartedAt: time,
Tags: tags,
// default "4.data.user.stream.viewersCount"
Viewers: int(gjson.Get(data, extractor.GenGjsonQuery(offset+4, ".data.user.stream.viewersCount")).Int()),
// default "3.data.user.stream.previewImageURL"
Preview: extractor.ProxyUrl(gjson.Get(data, extractor.GenGjsonQuery(offset+3, ".data.user.stream.previewImageURL")).String()),
}
return &parsedStream, nil
}
// discover
func ParseCategory(data gjson.Result) (structs.CategoryPreview, error) {
tags := data.Get("node.tags").Array()
var parsedTags []string
for _, tag := range tags {
parsedTags = append(parsedTags, tag.Get("localizedName").String())
}
return structs.CategoryPreview{
Name: data.Get("node.name").String(),
DisplayName: data.Get("node.displayName").String(),
Viewers: int(data.Get("node.viewersCount").Int()),
CreatedAt: data.Get("node.originalReleaseDate").Time(),
Tags: parsedTags,
Cursor: data.Get("cursor").String(),
Image: extractor.ProxyUrl(data.Get("node.avatarURL").String()),
}, nil
}
func ParseMinifiedStream(data gjson.Result) (structs.CategoryMinifiedStream, error) {
var tags []string
tagArea := data.Get("node.freeformTacgs").Array()
for _, tag := range tagArea {
tags = append(tags, tag.Get("name").String())
}
// Store streamerColorHex in memory to use as pointer
var streamerColorHex *string
rawStreamerColorHex := data.Get("node.broadcaster.primaryColorHex").String()
if rawStreamerColorHex == "" {
streamerColorHex = nil
} else {
streamerColorHex = &rawStreamerColorHex
}
parsedStream := structs.CategoryMinifiedStream{
Title: data.Get("node.title").String(),
Viewers: int(data.Get("node.viewersCount").Int()),
Preview: extractor.ProxyUrl(data.Get("node.previewImageURL").String()),
Tags: tags,
Streamer: structs.CategoryMinifiedStreamer{
Name: data.Get("node.broadcaster.login").String(),
Pfp: extractor.ProxyUrl(data.Get("node.broadcaster.profileImageURL").String()),
ColorHex: streamerColorHex,
},
Cursor: data.Get("cursor").String(),
}
return parsedStream, nil
}
func ParseBadges(data gjson.Result) ([]structs.Badge, error) {
var formattedBadges = []structs.Badge{}
for _, badge := range data.Array() {
id := badge.Get("id").String()
decodedId, err := base64.StdEncoding.DecodeString(id)
if err != nil {
return []structs.Badge{}, nil
}
formattedBadges = append(formattedBadges, structs.Badge{
Id: string(decodedId),
SetId: badge.Get("setID").String(),
Title: badge.Get("title").String(),
Version: badge.Get("version").String(),
Images: map[string]string{
"image1x": extractor.ProxyUrl(badge.Get("image1x").String()),
"image2x": extractor.ProxyUrl(badge.Get("image2x").String()),
"image4x": extractor.ProxyUrl(badge.Get("image4x").String()),
},
})
}
return formattedBadges, nil
}
func StreamSubProxyUrl(url string, isVOD bool) string {
encodedUrl := base64.StdEncoding.EncodeToString([]byte(url))
backendUrl := os.Getenv("URL")
if isVOD {
return backendUrl + "/proxy/vod/sub/" + encodedUrl + "/video.m3u8"
} else {
return backendUrl + "/proxy/stream/sub/" + encodedUrl
}
}
func StreamSegmentProxyUrl(url string) string {
encodedUrl := base64.StdEncoding.EncodeToString([]byte(url))
backendUrl := os.Getenv("URL")
return backendUrl + "/proxy/stream/segment/" + encodedUrl
}
func ProxyPlaylistFile(playlist string, isSubPlaylist bool, isVod bool) string {
// Split the playlist into individual entries
entries := strings.Split(playlist, "\n")[1:] // Ignore the first line which contains the M3U header
// Loop through each entry and replace the URL
for i, entry := range entries {
if strings.HasPrefix(entry, "http") { // Only modify lines that contain URLs
var newURL string
if isSubPlaylist {
newURL = StreamSegmentProxyUrl(entry)
} else {
newURL = StreamSubProxyUrl(entry, isVod)
}
entries[i] = newURL
}
}
if isVod {
// Loop through each entry and replace the URL
for i, entry := range entries {
if strings.HasSuffix(entry, ".ts") {
entries[i] = entry
}
}
}
// Join the modified entries back into a single string separated by a newline
modifiedPlaylist := "#EXTM3U\n" + strings.Join(entries, "\n")
return modifiedPlaylist
}
func ParseMinifiedCategory(data gjson.Result) structs.MinifiedCategory {
return structs.MinifiedCategory{
Image: extractor.ProxyUrl(data.Get("boxArtURL").String()),
Name: data.Get("name").String(),
DisplayName: data.Get("displayName").String(),
Id: data.Get("id").String(),
}
}
func ParseMinifiedStreamer(data gjson.Result) structs.MinifiedStreamer {
return structs.MinifiedStreamer{
Name: data.Get("displayName").String(),
Login: data.Get("login").String(),
Pfp: extractor.ProxyUrl(data.Get("profileImageURL").String()),
ColorHex: data.Get("primaryColorHex").String(),
}
}
func ParseVideo(data gjson.Result, streamer structs.Streamer) structs.Video {
return structs.Video{
Type: "vod",
Preview: extractor.ProxyUrl(data.Get("previewThumbnailURL").String()),
Game: ParseMinifiedCategory(data.Get("game")),
Duration: int(data.Get("lengthSeconds").Int()),
Title: data.Get("title").String(),
PublishedAt: data.Get("createdAt").Time(),
Views: int(data.Get("viewCount").Int()),
Streamer: streamer,
Id: data.Get("id").String(),
}
}
func ParseClip(data gjson.Result, streamer structs.Streamer) structs.Video {
return structs.Video{
Type: "clip",
Preview: extractor.ProxyUrl(data.Get("thumbnailURL").String()),
Game: ParseMinifiedCategory(data.Get("clipGame")),
Duration: int(data.Get("lengthSeconds").Int()),
Title: data.Get("clipTitle").String(),
PublishedAt: data.Get("publishedAt").Time(),
Views: int(data.Get("clipViewCount").Int()),
Streamer: streamer,
Id: data.Get("slug").String(),
}
}
func ParseShelve(data gjson.Result, streamer structs.Streamer) structs.Shelve {
rawVideos := data.Get("items").Array()
parsedVideos := []structs.Video{}
isClip := data.Get("type").String() == "TOP_CLIPS"
for _, video := range rawVideos {
if isClip {
parsedVideos = append(parsedVideos, ParseClip(video, streamer))
} else {
parsedVideos = append(parsedVideos, ParseVideo(video, streamer))
}
}
return structs.Shelve{
Title: data.Get("title").String(),
Videos: parsedVideos,
}
}
func ParseVODMessage(data gjson.Result) structs.VodComment {
messager := structs.MinifiedStreamer{
Login: data.Get("node.commenter.login").String(),
Name: data.Get("node.commenter.displayName").String(),
ColorHex: data.Get("node.message.userColor").String(),
}
parsedBadges := []structs.VodCommentBadge{}
for _, badge := range data.Get("node.message.userBadges").Array() {
setID := badge.Get("setID").String()
version := badge.Get("version").String()
if version != "" && setID != "" {
b := structs.VodCommentBadge{
SetID: setID,
Version: version,
}
parsedBadges = append(parsedBadges, b)
}
}
return structs.VodComment{
Message: data.Get("node.message.fragments.0.text").String(),
Offset: int(data.Get("node.contentOffsetSeconds").Int()),
Cursor: data.Get("cursor").String(),
Messager: messager,
Badges: parsedBadges,
}
}
func ParseFollowingStreamer(data gjson.Result, login string) structs.FollowingStreamer {
return structs.FollowingStreamer{
Username: data.Get("user.displayName").String(),
Login: login,
Pfp: extractor.ProxyUrl(data.Get("user.profileImageURL").String()),
Followers: int(data.Get("user.followers.totalCount").Int()),
}
}