package twitch import ( "errors" "fmt" "io" "net/http" "safetwitch-backend/extractor" "safetwitch-backend/extractor/structs" "github.com/tidwall/gjson" ) func GetStreamerFromVOD(vodID string) (string, error) { payload := []TwitchPayload{ { "operationName": "VodChannelLoginQuery", "variables": map[string]interface{}{ "videoID": vodID, }, "extensions": map[string]interface{}{ "persistedQuery": map[string]interface{}{ "version": 1, "sha256Hash": "0c5feea4dad2565508828f16e53fe62614edf015159df4b3bca33423496ce78e", }, }, }, } _, body, err := parseResponse(payload) if err != nil { return "", err } streamerName := gjson.Get(string(body), "0.data.video.owner.login").String() if streamerName == "" { return "", errors.New("streamernotfound") } return streamerName, nil } func GetVODPlaylist(vodID string) ([]byte, error) { // get streamer streamerName, err := GetStreamerFromVOD(vodID) if err != nil { return []byte{}, err } // get playback token tokens, err := getPlaybackAccessToken(streamerName, vodID) if err != nil { return []byte{}, err } params := fmt.Sprintf("?sig=%s&token=%s&acmb=e30=&allow_source=true&fast_bread=true&p=4189675&player_backend=mediaplayer&playlist_include_framerate=true&reassignments_supported=true&transcode_mode=cbr_v1&cdm=wv&player_version=1.20.0", tokens.Signature, tokens.Token) req, err := http.NewRequest("GET", fmt.Sprintf("https://usher.ttvnw.net/vod/%s.m3u8%s", fmt.Sprint(vodID), params), nil) if err != nil { return []byte{}, err } // headers req.Header.Add("Client-Id", "ue6666qo983tsx6so1t0vnawi233wa") client := &http.Client{} resp, err := client.Do(req) if err != nil { return []byte{}, err } if resp.StatusCode == 403 { return []byte{}, errors.New("Video is sub-only") } body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } proxiedPlaylist := ProxyPlaylistFile(string(body), false, true) return []byte(proxiedPlaylist), nil } func GetVodMetadata(vodID string) (structs.Video, error) { // get streamer streamerName, err := GetStreamerFromVOD(vodID) if err != nil { return structs.Video{}, err } payload := []TwitchPayload{ { "operationName": "VideoMetadata", "variables": map[string]interface{}{ "channelLogin": streamerName, "videoID": vodID, }, "extensions": map[string]interface{}{ "persistedQuery": map[string]interface{}{ "version": 1, "sha256Hash": "49b5b8f268cdeb259d75b58dcb0c1a748e3b575003448a2333dc5cdafd49adad", }, }, }, { "operationName": "ChannelRoot_AboutPanel", "variables": map[string]interface{}{ "channelLogin": streamerName, "skipSchedule": false, }, "extensions": map[string]interface{}{ "persistedQuery": map[string]interface{}{ "version": 1, "sha256Hash": "6089531acef6c09ece01b440c41978f4c8dc60cb4fa0124c9a9d3f896709b6c6", }, }, }, } _, body, err := parseResponse(payload) if err != nil { return structs.Video{}, err } streamerData := gjson.Get(string(body), "1.data") parsedSocials, err := ParseSocials(streamerData.String()) if err != nil { return structs.Video{}, err } parsedStreamer, err := ParseStreamer(streamerData, false, parsedSocials, nil, streamerName, "") if err != nil { return structs.Video{}, err } videoData := gjson.Get(string(body), "0.data.video") return structs.Video{ Title: videoData.Get("title").String(), Preview: extractor.ProxyUrl(videoData.Get("previewThumbnailURL").String()), Duration: int(videoData.Get("lengthSeconds").Int()), Views: int(videoData.Get("viewCount").Int()), PublishedAt: videoData.Get("publishedAt").Time(), Game: ParseMinifiedCategory(videoData.Get("game")), Streamer: parsedStreamer, Id: vodID, }, nil }