mirror of
https://codeberg.org/SafeTwitch/safetwitch-backend.git
synced 2024-12-22 13:13:00 -05:00
Clip support
This commit is contained in:
parent
a991899c7f
commit
cc1f5ef77b
7 changed files with 170 additions and 2 deletions
101
extractor/twitch/Clip.go
Normal file
101
extractor/twitch/Clip.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ func GetStream(streamerName string) (string, error) {
|
||||||
signature := tokenwsig.Signature
|
signature := tokenwsig.Signature
|
||||||
|
|
||||||
playlistUrl := "https://usher.ttvnw.net/api/channel/hls/" + strings.ToLower(streamerName) + ".m3u8"
|
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, err := http.NewRequest("GET", playlistUrl+params, nil)
|
||||||
req.Header.Add("Client-Id", "ue6666qo983tsx6so1t0vnawi233wa")
|
req.Header.Add("Client-Id", "ue6666qo983tsx6so1t0vnawi233wa")
|
||||||
|
|
|
@ -266,7 +266,7 @@ func ParseClip(data gjson.Result, streamer structs.Streamer) structs.Video {
|
||||||
PublishedAt: data.Get("publishedAt").Time(),
|
PublishedAt: data.Get("publishedAt").Time(),
|
||||||
Views: int(data.Get("clipViewCount").Int()),
|
Views: int(data.Get("clipViewCount").Int()),
|
||||||
Streamer: streamer,
|
Streamer: streamer,
|
||||||
Id: data.Get("id").String(),
|
Id: data.Get("slug").String(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func ParseShelve(data gjson.Result, streamer structs.Streamer) structs.Shelve {
|
func ParseShelve(data gjson.Result, streamer structs.Streamer) structs.Shelve {
|
||||||
|
|
|
@ -12,6 +12,13 @@ func ProxyUrl(url string) string {
|
||||||
return backendUrl + "/proxy/img/" + encodedUrl
|
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 {
|
type ServerFormat struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message interface{} `json:"data"`
|
Message interface{} `json:"data"`
|
||||||
|
|
36
routes/api/clips/clips.go
Normal file
36
routes/api/clips/clips.go
Normal file
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
|
@ -114,6 +114,7 @@ func Routes(route *gin.Engine) {
|
||||||
})
|
})
|
||||||
|
|
||||||
auth.GET("/vod/sub/:encodedUrl/:segment", func(context *gin.Context) {
|
auth.GET("/vod/sub/:encodedUrl/:segment", func(context *gin.Context) {
|
||||||
|
|
||||||
decodedUrl, err := b64.StdEncoding.DecodeString(context.Param("encodedUrl"))
|
decodedUrl, err := b64.StdEncoding.DecodeString(context.Param("encodedUrl"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
context.Error(err)
|
context.Error(err)
|
||||||
|
@ -138,4 +139,25 @@ func Routes(route *gin.Engine) {
|
||||||
|
|
||||||
context.Data(200, "application/text", segment)
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package routes
|
||||||
import (
|
import (
|
||||||
"safetwitch-backend/routes/api/badges"
|
"safetwitch-backend/routes/api/badges"
|
||||||
"safetwitch-backend/routes/api/chat"
|
"safetwitch-backend/routes/api/chat"
|
||||||
|
"safetwitch-backend/routes/api/clips"
|
||||||
"safetwitch-backend/routes/api/discover"
|
"safetwitch-backend/routes/api/discover"
|
||||||
"safetwitch-backend/routes/api/search"
|
"safetwitch-backend/routes/api/search"
|
||||||
"safetwitch-backend/routes/api/users"
|
"safetwitch-backend/routes/api/users"
|
||||||
|
@ -20,6 +21,7 @@ func SetRoutes(router *gin.Engine) {
|
||||||
search.Routes(router)
|
search.Routes(router)
|
||||||
vods.Routes(router)
|
vods.Routes(router)
|
||||||
chat.Routes(router)
|
chat.Routes(router)
|
||||||
|
clips.Routes(router)
|
||||||
|
|
||||||
proxy.Routes(router)
|
proxy.Routes(router)
|
||||||
root.Routes(router)
|
root.Routes(router)
|
||||||
|
|
Loading…
Reference in a new issue