redgifs: new extractor

This commit is contained in:
stefanodvx 2025-04-19 12:52:07 +02:00
parent f8b1cb6d48
commit ebba6a8835
5 changed files with 244 additions and 5 deletions

View file

@ -55,11 +55,9 @@ func URLHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
}
func URLFilter(msg *gotgbot.Message) bool {
return message.Text(msg) && !message.Command(msg) && containsURL(msg)
}
func containsURL(msg *gotgbot.Message) bool {
return message.Entity("url")(msg)
return message.Text(msg) &&
!message.Command(msg) &&
message.Entity("url")(msg)
}
func getMessageURL(msg *gotgbot.Message) string {

View file

@ -5,6 +5,7 @@ import (
"govd/ext/ninegag"
"govd/ext/pinterest"
"govd/ext/reddit"
"govd/ext/redgifs"
"govd/ext/tiktok"
"govd/ext/twitter"
"govd/models"
@ -23,5 +24,6 @@ var List = []*models.Extractor{
reddit.Extractor,
reddit.ShortExtractor,
ninegag.Extractor,
redgifs.Extractor,
// todo: add every ext lol
}

150
ext/redgifs/main.go Normal file
View file

@ -0,0 +1,150 @@
package redgifs
import (
"fmt"
"govd/enums"
"govd/models"
"govd/util"
"net/http"
"regexp"
"github.com/bytedance/sonic"
)
const (
baseAPI = "https://api.redgifs.com/v2/"
tokenEndpoint = baseAPI + "auth/temporary"
videoEndpoint = baseAPI + "gifs/"
)
var (
session = util.GetHTTPSession()
baseApiHeaders = map[string]string{
"referer": "https://www.redgifs.com/",
"origin": "https://www.redgifs.com",
"content-type": "application/json",
}
)
var Extractor = &models.Extractor{
Name: "RedGifs",
CodeName: "redgifs",
Type: enums.ExtractorTypeSingle,
Category: enums.ExtractorCategorySocial,
URLPattern: regexp.MustCompile(`https?://(?:(?:www\.)?redgifs\.com/(?:watch|ifr)/|thumbs2\.redgifs\.com/)(?P<id>[^-/?#\.]+)`),
Host: []string{
"redgifs.com",
"thumbs2.redgifs.com",
},
Run: func(ctx *models.DownloadContext) (*models.ExtractorResponse, error) {
mediaList, err := MediaListFromAPI(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get media: %w", err)
}
return &models.ExtractorResponse{
MediaList: mediaList,
}, nil
},
}
func MediaListFromAPI(ctx *models.DownloadContext) ([]*models.Media, error) {
var mediaList []*models.Media
response, err := GetVideo(ctx.MatchedContentID)
if err != nil {
return nil, fmt.Errorf("failed to get from api: %w", err)
}
gif := response.Gif
media := ctx.Extractor.NewMedia(
ctx.MatchedContentID,
ctx.MatchedContentURL,
)
if gif.Description != "" {
media.SetCaption(gif.Description)
}
media.NSFW = true // always nsfw
if gif.Urls.Sd != "" {
media.AddFormat(&models.MediaFormat{
FormatID: "sd",
Type: enums.MediaTypeVideo,
URL: []string{gif.Urls.Sd},
VideoCodec: enums.MediaCodecAVC,
Width: int64(gif.Width / 2),
Height: int64(gif.Height / 2),
})
}
if gif.Urls.Hd != "" {
media.AddFormat(&models.MediaFormat{
FormatID: "hd",
Type: enums.MediaTypeVideo,
URL: []string{gif.Urls.Hd},
VideoCodec: enums.MediaCodecAVC,
Width: int64(gif.Width),
Height: int64(gif.Height),
})
}
if gif.Urls.Poster != "" {
thumbnails := []string{gif.Urls.Poster}
if gif.Urls.Thumbnail != "" {
thumbnails = append(thumbnails, gif.Urls.Thumbnail)
}
for _, format := range media.Formats {
format.Thumbnail = thumbnails
format.Duration = int64(gif.Duration)
}
}
if gif.HasAudio {
for _, format := range media.Formats {
format.AudioCodec = enums.MediaCodecAAC
}
}
if len(media.Formats) > 0 {
mediaList = append(mediaList, media)
}
return mediaList, nil
}
func GetVideo(videoID string) (*Response, error) {
url := videoEndpoint + videoID + "?views=true"
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
token, err := GetAccessToken()
if err != nil {
return nil, fmt.Errorf("failed to get access token: %w", err)
}
req.Header.Set("authorization", "Bearer "+token.AccessToken)
req.Header.Set("user-agent", token.Agent)
req.Header.Set("x-customheader", "https://www.redgifs.com/watch/"+videoID)
for k, v := range baseApiHeaders {
req.Header.Set(k, v)
}
res, err := session.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get video: %s", res.Status)
}
var response Response
err = sonic.ConfigFastest.NewDecoder(res.Body).Decode(&response)
if err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if response.Gif == nil {
return nil, fmt.Errorf("failed to get video: %s", res.Status)
}
return &response, nil
}

44
ext/redgifs/models.go Normal file
View file

@ -0,0 +1,44 @@
package redgifs
type Response struct {
Gif *Gif `json:"gif"`
}
type Token struct {
AccessToken string `json:"token"`
Agent string `json:"agent"`
ExpiresIn int64 `json:"expires_in"`
}
type Urls struct {
Silent string `json:"silent"`
Sd string `json:"sd"`
Hd string `json:"hd"`
Thumbnail string `json:"thumbnail"`
HTML string `json:"html"`
Poster string `json:"poster"`
}
type Gif struct {
AvgColor string `json:"avgColor"`
CreateDate int `json:"createDate"`
Description string `json:"description"`
Duration float64 `json:"duration"`
HasAudio bool `json:"hasAudio"`
Height int `json:"height"`
HideHome bool `json:"hideHome"`
HideTrending bool `json:"hideTrending"`
Hls bool `json:"hls"`
ID string `json:"id"`
Likes int `json:"likes"`
Niches []string `json:"niches"`
Published bool `json:"published"`
Type int `json:"type"`
Sexuality []string `json:"sexuality"`
Tags []string `json:"tags"`
Urls Urls `json:"urls"`
UserName string `json:"userName"`
Verified bool `json:"verified"`
Views int `json:"views"`
Width int `json:"width"`
}

45
ext/redgifs/util.go Normal file
View file

@ -0,0 +1,45 @@
package redgifs
import (
"fmt"
"govd/util"
"net/http"
"time"
"github.com/bytedance/sonic"
)
var accessToken *Token
func GetAccessToken() (*Token, error) {
if accessToken == nil || time.Now().Unix() >= accessToken.ExpiresIn {
if err := RefreshAccessToken(); err != nil {
return nil, err
}
}
return accessToken, nil
}
func RefreshAccessToken() error {
req, err := http.NewRequest(http.MethodGet, tokenEndpoint, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", util.ChromeUA)
res, err := session.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("failed to get access token: %s", res.Status)
}
var token Token
err = sonic.ConfigFastest.NewDecoder(res.Body).Decode(&token)
if err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
token.ExpiresIn = time.Now().Add(23 * time.Hour).Unix()
accessToken = &token
return nil
}