package ninegag import ( "fmt" "net/http" "regexp" "govd/enums" "govd/models" "govd/util" "github.com/bytedance/sonic" "github.com/pkg/errors" ) const ( apiEndpoint = "https://9gag.com/v1/post" postNotFound = "Post not found" ) // 9gag gives 403 unless you use // real browser TLS fingerprint var httpSession = util.NewChromeClient() var Extractor = &models.Extractor{ Name: "9GAG", CodeName: "ninegag", Type: enums.ExtractorTypeSingle, Category: enums.ExtractorCategorySocial, URLPattern: regexp.MustCompile(`https?://(?:www\.)?9gag\.com/gag/(?P[^/?&#]+)`), Host: []string{"9gag.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) { contentID := ctx.MatchedContentID contentURL := ctx.MatchedContentURL postData, err := GetPostData(contentID) if err != nil { return nil, fmt.Errorf("failed to get post data: %w", err) } media := ctx.Extractor.NewMedia(contentID, contentURL) media.SetCaption(postData.Title) if postData.Nsfw == 1 { media.NSFW = true } switch postData.Type { case "Photo": bestPhoto, err := FindBestPhoto(postData.Images) if err != nil { return nil, err } media.AddFormat(&models.MediaFormat{ FormatID: "photo", Type: enums.MediaTypePhoto, URL: []string{bestPhoto.URL}, Width: int64(bestPhoto.Width), Height: int64(bestPhoto.Height), }) case "Animated": videoFormats, err := ParseVideoFormats(postData.Images) if err != nil { return nil, err } for _, format := range videoFormats { media.AddFormat(format) } } if len(media.Formats) > 0 { return []*models.Media{media}, nil } // no media found return nil, nil } func GetPostData(postID string) (*Post, error) { url := apiEndpoint + "?id=" + postID req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("User-Agent", util.ChromeUA) resp, err := httpSession.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("invalid status code: %d", resp.StatusCode) } var response Response decoder := sonic.ConfigFastest.NewDecoder(resp.Body) err = decoder.Decode(&response) if err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } if response.Meta != nil && response.Meta.Status != "Success" { return nil, fmt.Errorf("API error: %s", response.Meta.Status) } if response.Meta != nil && response.Meta.ErrorMessage == postNotFound { return nil, util.ErrUnavailable } if response.Data == nil || response.Data.Post == nil { return nil, errors.New("no post data found") } return response.Data.Post, nil }