0
Fork 0
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:
dragongoose 2023-08-21 23:28:23 -04:00
parent a991899c7f
commit cc1f5ef77b
No known key found for this signature in database
GPG key ID: 01397EEC371CDAA5
7 changed files with 170 additions and 2 deletions

101
extractor/twitch/Clip.go Normal file
View 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
}

View file

@ -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")

View file

@ -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 {

View file

@ -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
View 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))
})
}

View file

@ -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
}
})
} }

View file

@ -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)