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 } func ParseStream(data string) (*structs.Stream, error) { // check if live stream := gjson.Get(data, "1.data.user.stream") if !stream.IsObject() { return nil, errors.New("streamer is not live") } var tags []string tagArea := gjson.Get(data, "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{ Title: gjson.Get(data, "1.data.user.lastBroadcast.title").String(), Topic: stream.Get("game.name").String(), StartedAt: time, Tags: tags, Viewers: int(gjson.Get(data, "4.data.user.stream.viewersCount").Int()), Preview: extractor.ProxyUrl(gjson.Get(data, "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("id").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, } }