diff --git a/extractor/twitch/Clip.go b/extractor/twitch/Clip.go new file mode 100644 index 0000000..cbc4a39 --- /dev/null +++ b/extractor/twitch/Clip.go @@ -0,0 +1,101 @@ +package twitch + +import ( + "log" + "safetwitch-backend/extractor" + "safetwitch-backend/extractor/structs" + + "github.com/tidwall/gjson" +) + +func GetClipMetadata(clipSlug string, streamerName string) (structs.Video, error) { + payload := []TwitchPayload{ + { + "operationName": "ClipMetadata", + "variables": map[string]interface{}{ + "channelLogin": streamerName, + "clipSlug": clipSlug, + }, + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "ab70572e66f164789c87936a8291fd15e29adc2cea0114b02e60f17d60d6d154", + }, + }, + }, + { + "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 + } + + log.Println(string(body)) + + 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.clip") + + 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("createdAt").Time(), + Game: ParseMinifiedCategory(videoData.Get("game")), + Streamer: parsedStreamer, + Id: clipSlug, + }, nil + +} + +func GetClipLink(slug string) (string, error) { + payload := []TwitchPayload{ + { + "operationName": "VideoAccessToken_Clip", + "variables": map[string]interface{}{ + "slug": "CovertConsideratePorcupineSwiftRage-5tq5qcrbtQ_BhHRC", + }, + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11", + }, + }, + }, + } + + _, body, err := parseResponse(payload) + if err != nil { + return "", err + } + + token := gjson.Get(string(body), "0.data.clip.playbackAccessToken.value").String() + signature := gjson.Get(string(body), "0.data.clip.playbackAccessToken.signature").String() + + baseUrl := gjson.Get(string(body), "0.data.clip.videoQualities.0.sourceURL").String() + formattedUrl := baseUrl + "?sig=" + signature + "&token=" + token + + return extractor.ProxyClip(formattedUrl), nil +} diff --git a/extractor/twitch/Stream.go b/extractor/twitch/Stream.go index 4bd3cc1..d08a104 100644 --- a/extractor/twitch/Stream.go +++ b/extractor/twitch/Stream.go @@ -14,7 +14,7 @@ func GetStream(streamerName string) (string, error) { signature := tokenwsig.Signature playlistUrl := "https://usher.ttvnw.net/api/channel/hls/" + strings.ToLower(streamerName) + ".m3u8" - 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", signature, token) + params := fmt.Sprintf("?sig=%s&token=%s&acmb=e30=&allow_source=true&allow_audio_only=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", signature, token) req, err := http.NewRequest("GET", playlistUrl+params, nil) req.Header.Add("Client-Id", "ue6666qo983tsx6so1t0vnawi233wa") diff --git a/extractor/twitch/parser.go b/extractor/twitch/parser.go index 6faea93..b14315c 100644 --- a/extractor/twitch/parser.go +++ b/extractor/twitch/parser.go @@ -266,7 +266,7 @@ func ParseClip(data gjson.Result, streamer structs.Streamer) structs.Video { PublishedAt: data.Get("publishedAt").Time(), Views: int(data.Get("clipViewCount").Int()), Streamer: streamer, - Id: data.Get("id").String(), + Id: data.Get("slug").String(), } } func ParseShelve(data gjson.Result, streamer structs.Streamer) structs.Shelve { diff --git a/extractor/utils.go b/extractor/utils.go index 1297270..0468aac 100644 --- a/extractor/utils.go +++ b/extractor/utils.go @@ -12,6 +12,13 @@ func ProxyUrl(url string) string { return backendUrl + "/proxy/img/" + encodedUrl } +func ProxyClip(url string) string { + encodedUrl := b64.StdEncoding.EncodeToString([]byte(url)) + backendUrl := os.Getenv("URL") + + return backendUrl + "/proxy/clip/" + encodedUrl +} + type ServerFormat struct { Status string `json:"status"` Message interface{} `json:"data"` diff --git a/routes/api/clips/clips.go b/routes/api/clips/clips.go new file mode 100644 index 0000000..1899d39 --- /dev/null +++ b/routes/api/clips/clips.go @@ -0,0 +1,36 @@ +package clips + +import ( + "safetwitch-backend/extractor" + "safetwitch-backend/extractor/twitch" + + "github.com/gin-gonic/gin" +) + +func Routes(route *gin.Engine) { + clips := route.Group("/api/clips") + + clips.GET("/getlink/:slug", func(context *gin.Context) { + slug := context.Param("slug") + data, err := twitch.GetClipLink(slug) + if err != nil { + context.Error(err) + return + } + + context.JSON(200, extractor.FormatMessage(data, true)) + }) + + clips.GET("/:streamerName/:slug", func(context *gin.Context) { + slug := context.Param("slug") + streamerName := context.Param("streamerName") + data, err := twitch.GetClipMetadata(slug, streamerName) + if err != nil { + context.Error(err) + return + } + + data.Type = "clip" + context.JSON(200, extractor.FormatMessage(data, true)) + }) +} diff --git a/routes/proxy/proxy.go b/routes/proxy/proxy.go index 241d44f..8ee9f65 100644 --- a/routes/proxy/proxy.go +++ b/routes/proxy/proxy.go @@ -114,6 +114,7 @@ func Routes(route *gin.Engine) { }) auth.GET("/vod/sub/:encodedUrl/:segment", func(context *gin.Context) { + decodedUrl, err := b64.StdEncoding.DecodeString(context.Param("encodedUrl")) if err != nil { context.Error(err) @@ -138,4 +139,25 @@ func Routes(route *gin.Engine) { context.Data(200, "application/text", segment) }) + + auth.GET("/clip/:clipUrl", func(context *gin.Context) { + decodedUrl, err := b64.StdEncoding.DecodeString(context.Param("clipUrl")) + if err != nil { + context.Error(err) + return + } + + resp, err := http.Get(string(decodedUrl)) + if err != nil { + context.Error(err) + return + } + defer resp.Body.Close() + + _, err = io.Copy(context.Writer, resp.Body) + if err != nil { + context.Error(err) + return + } + }) } diff --git a/routes/routes.go b/routes/routes.go index 7837342..8d88e15 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -3,6 +3,7 @@ package routes import ( "safetwitch-backend/routes/api/badges" "safetwitch-backend/routes/api/chat" + "safetwitch-backend/routes/api/clips" "safetwitch-backend/routes/api/discover" "safetwitch-backend/routes/api/search" "safetwitch-backend/routes/api/users" @@ -20,6 +21,7 @@ func SetRoutes(router *gin.Engine) { search.Routes(router) vods.Routes(router) chat.Routes(router) + clips.Routes(router) proxy.Routes(router) root.Routes(router)