From 4e89ba8637875aa3669dff09a15e885a2b555c64 Mon Sep 17 00:00:00 2001 From: dragongoose Date: Tue, 4 Jul 2023 20:54:12 -0400 Subject: [PATCH] Add VOD preview support --- extractor/structs/parsed.go | 30 ++++++++++++ extractor/twitch/parser.go | 74 +++++++++++++++++++++++++++++ extractor/twitch/twitchExtractor.go | 49 +++++++++++++++++++ routes/api/vods/homeShelves.go | 17 +++++++ routes/routes.go | 2 + 5 files changed, 172 insertions(+) create mode 100644 routes/api/vods/homeShelves.go diff --git a/extractor/structs/parsed.go b/extractor/structs/parsed.go index 15f523d..5d03cd9 100644 --- a/extractor/structs/parsed.go +++ b/extractor/structs/parsed.go @@ -83,3 +83,33 @@ type SearchResult struct { RelatedLiveChannels []Streamer `json:"relatedChannels"` ChannelsWithTag []Streamer `json:"channelsWithTag"` } + +type MinifiedCategory struct { + Image string `json:"image"` + Id string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` +} + +type MinifiedStreamer struct { + Name string `json:"name"` + Login string `json:"login"` + Pfp string `json:"pfp"` + ColorHex string `json:"colorHex"` +} + +type Video struct { + Preview string `json:"preview"` + Game MinifiedCategory `json:"game"` + Duration int `json:"duration"` + Title string `json:"title"` + PublishedAt time.Time `json:"publishedAt"` + Views int `json:"views"` + Tag []string `json:"tags,omitempty"` + Streamer MinifiedStreamer `json:"streamer"` +} + +type Shelve struct { + Title string `json:"title"` + Videos []Video `json:"videos"` +} diff --git a/extractor/twitch/parser.go b/extractor/twitch/parser.go index 07ac5d8..8ef333a 100644 --- a/extractor/twitch/parser.go +++ b/extractor/twitch/parser.go @@ -211,3 +211,77 @@ func ProxyPlaylistFile(playlist string, isSubPlaylist bool) string { 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) structs.Video { + tags := []string{} + + for _, tag := range data.Get("contentTags").Array() { + tags = append(tags, tag.Get("localizedName").String()) + } + + return structs.Video{ + 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()), + Tag: tags, + Streamer: ParseMinifiedStreamer(data.Get("owner")), + } +} + +func ParseClip(data gjson.Result) structs.Video { + tags := []string{} + + for _, tag := range data.Get("contentTags").Array() { + tags = append(tags, tag.Get("localizedName").String()) + } + + return structs.Video{ + 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()), + Tag: tags, + Streamer: ParseMinifiedStreamer(data.Get("broadcaster")), + } +} +func ParseShelve(data gjson.Result) 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)) + } else { + parsedVideos = append(parsedVideos, ParseVideo(video)) + } + } + + return structs.Shelve{ + Title: data.Get("title").String(), + Videos: parsedVideos, + } +} diff --git a/extractor/twitch/twitchExtractor.go b/extractor/twitch/twitchExtractor.go index cb36ff5..3dd61d7 100644 --- a/extractor/twitch/twitchExtractor.go +++ b/extractor/twitch/twitchExtractor.go @@ -580,3 +580,52 @@ func GetStreamerId(channelName string) (string, error) { return "", errors.New("could not find user") } } + +/* +If you don't understand the meaning of shelve in +this context, go to an offline streamer on twitch +and look under the videos tag. Each row is considered +a shelve, and each can be expanded. This is to be used +on a streamers profile page +*/ +func GetStreamerVideoShelves(channelName string) ([]structs.Shelve, error) { + payload := []TwitchPayload{ + { + "operationName": "ChannelVideoShelvesQuery", + "variables": map[string]interface{}{ + "channelLogin": channelName, + "first": 5, + }, + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "8afefb1ed16c4d8e20fa55024a7ed1727f63b6eca47d8d33a28500770bad8479", + }, + }, + }, + } + + _, body, err := parseResponse(payload) + if err != nil { + return []structs.Shelve{}, err + } + + /* + Twitch seperates videos by shelves. + There are 4 shells: + - Recent broadcasts + - Recent highlights and uploads + - Popular clips + - All videos + + Each one of these shelves can be expanded to get more data + */ + + shelves := gjson.Get(string(body), "0.data.user.videoShelves.edges") + parsedShelves := []structs.Shelve{} + for _, shelve := range shelves.Array() { + parsedShelves = append(parsedShelves, ParseShelve(shelve.Get("node"))) + } + + return parsedShelves, nil +} diff --git a/routes/api/vods/homeShelves.go b/routes/api/vods/homeShelves.go new file mode 100644 index 0000000..f2e0ad6 --- /dev/null +++ b/routes/api/vods/homeShelves.go @@ -0,0 +1,17 @@ +package vods + +import ( + "safetwitch-backend/extractor" + "safetwitch-backend/extractor/twitch" + + "github.com/gin-gonic/gin" +) + +func Routes(route *gin.Engine) { + auth := route.Group("/api/vods") + + auth.GET("/shelve/:streamerName", func(context *gin.Context) { + data, _ := twitch.GetStreamerVideoShelves(context.Param("streamerName")) + context.JSON(200, extractor.FormatMessage(data, true)) + }) +} diff --git a/routes/routes.go b/routes/routes.go index a9239b5..4f4e5b2 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -5,6 +5,7 @@ import ( "safetwitch-backend/routes/api/discover" "safetwitch-backend/routes/api/search" "safetwitch-backend/routes/api/users" + "safetwitch-backend/routes/api/vods" "safetwitch-backend/routes/proxy" "safetwitch-backend/routes/root" @@ -16,6 +17,7 @@ func SetRoutes(router *gin.Engine) { discover.Routes(router) badges.Routes(router) search.Routes(router) + vods.Routes(router) proxy.Routes(router) root.Routes(router)