forked from kosa3/pexels-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit cde90af
Showing
19 changed files
with
780 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Pexels API Client for Go | ||
|
||
`kosa3/pexels-go` is [Pexels API](https://www.pexels.com/api/documentation/) Client for Go. | ||
[Pexels](https://www.pexels.com/) is the best free stock photos & videos shared by talented creators. | ||
|
||
|
||
## Install | ||
|
||
``` | ||
$ go get -u github.com/kosa3/pexels-go | ||
``` | ||
|
||
## Examples | ||
|
||
See [example](./example) directory. | ||
|
||
```go | ||
func main() { | ||
cli := pixels.NewClient(os.Args[1]) | ||
ctx := context.Background() | ||
ps, err := cli.PhotoService.Search(ctx, &pixels.PhotoParams{ | ||
Query: "people", | ||
Page: 2, | ||
}) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
fmt.Println("Source Medium URL:", ps.Photos[0].Src.Medium) | ||
fmt.Println("RateLimit:", cli.LastRateLimit.Limit) | ||
} | ||
``` | ||
|
||
## Supported API | ||
|
||
### Photo | ||
|
||
| Endpoint | HTTP Method | | ||
|-----------------------------------------|:-----------:| | ||
|/v1/search | GET | | ||
|/v1/curated | GET | | ||
|/v1/photos/{id} | GET | | ||
|
||
|
||
|
||
### Video | ||
|
||
| Endpoint | HTTP Method | | ||
|-----------------------------------------|:-----------:| | ||
|/videos/search | GET | | ||
|/videos/popular | GET | | ||
|/videos/videos/{id} | GET | | ||
|
||
### Collection | ||
|
||
| Endpoint | HTTP Method | | ||
|-----------------------------------------|:-----------:| | ||
|/v1/collections/featured | GET | | ||
|/v1/collections | GET | | ||
|/v1/collections/{id} | GET | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
package pixels | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"reflect" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const ( | ||
baseURL = "https://api.pexels.com/v1" | ||
videoBaseURL = "https://api.pexels.com" | ||
) | ||
|
||
type RateLimit struct { | ||
Limit int64 | ||
Remaining int64 | ||
Reset time.Time | ||
} | ||
|
||
type Client struct { | ||
PhotoService PhotoService | ||
VideoService VideoService | ||
CollectionService CollectionService | ||
|
||
HTTPClient *http.Client | ||
AccessToken string | ||
BaseURL string | ||
LastRateLimit *RateLimit | ||
} | ||
|
||
func NewClient(accessToken string) *Client { | ||
var cli Client | ||
cli.PhotoService = &photoService{cli: &cli} | ||
cli.VideoService = &videoService{cli: &cli} | ||
cli.CollectionService = &collectionService{cli: &cli} | ||
|
||
cli.AccessToken = accessToken | ||
cli.BaseURL = baseURL | ||
|
||
return &cli | ||
} | ||
|
||
func (cli *Client) httpClient() *http.Client { | ||
if cli.HTTPClient != nil { | ||
return cli.HTTPClient | ||
} | ||
|
||
return http.DefaultClient | ||
} | ||
|
||
func (cli *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) { | ||
req = req.WithContext(ctx) | ||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cli.AccessToken)) | ||
req.Header.Set("Content-Type", "application/json") | ||
//req.Header.Set("User-Agent", "Pexels/Go") | ||
return cli.httpClient().Do(req) | ||
} | ||
|
||
func (cli *Client) get(ctx context.Context, path string, params url.Values, v interface{}) error { | ||
r := regexp.MustCompile("^videos") | ||
if r.MatchString(path) { | ||
cli.BaseURL = videoBaseURL | ||
} | ||
|
||
reqURL := cli.BaseURL + "/" + path | ||
if len(params) > 0 { | ||
reqURL += "?" + params.Encode() | ||
} | ||
fmt.Println(reqURL) | ||
|
||
req, err := http.NewRequest(http.MethodGet, reqURL, nil) | ||
if err != nil { | ||
return fmt.Errorf("cannot create HTTP request: %w", err) | ||
} | ||
|
||
resp, err := cli.do(ctx, req) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) { | ||
return cli.error(resp.StatusCode, resp.Body) | ||
} | ||
|
||
rl, err := RateLimitFromHeader(resp.Header) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
cli.LastRateLimit = rl | ||
|
||
if err := json.NewDecoder(resp.Body).Decode(v); err != nil { | ||
return fmt.Errorf("cannot parse HTTP body: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func RateLimitFromHeader(h http.Header) (*RateLimit, error) { | ||
ls := h.Get("X-Ratelimit-Limit") | ||
if ls == "" { | ||
return nil, errors.New("cannot get X-Ratelimit-Limit from header") | ||
} | ||
|
||
l, err := strconv.ParseInt(ls, 10, 64) | ||
if err != nil { | ||
return nil, fmt.Errorf("X-Ratelimit-Limit is invalid value: %w", err) | ||
} | ||
|
||
rs := h.Get("X-Ratelimit-Remaining") | ||
if rs == "" { | ||
return nil, errors.New("cannot get X-Ratelimit-Remaining from header") | ||
} | ||
|
||
r, err := strconv.ParseInt(rs, 10, 64) | ||
if err != nil { | ||
return nil, fmt.Errorf("X-Rate-Limit-Remaining is invalid value: %w", err) | ||
} | ||
|
||
ts := h.Get("X-Ratelimit-Reset") | ||
if ts == "" { | ||
return nil, errors.New("cannot get X-Ratelimit-Reset from header") | ||
} | ||
|
||
t, err := strconv.ParseInt(ts, 10, 64) | ||
if err != nil { | ||
return nil, fmt.Errorf("X-Ratelimit-Reset is invalid value: %w", err) | ||
} | ||
|
||
return &RateLimit{ | ||
Limit: l, | ||
Remaining: r, | ||
Reset: time.Unix(t, 0), | ||
}, nil | ||
} | ||
|
||
func (cli *Client) error(statusCode int, body io.Reader) error { | ||
var aerr APIError | ||
if err := json.NewDecoder(body).Decode(&aerr); err != nil { | ||
return &APIError{HTTPStatus: statusCode} | ||
} | ||
aerr.HTTPStatus = statusCode | ||
return &aerr | ||
} | ||
|
||
func StructToMap(i interface{}) (values url.Values) { | ||
values = url.Values{} | ||
if reflect.ValueOf(i).IsNil() { | ||
return | ||
} | ||
iVal := reflect.ValueOf(i).Elem() | ||
typ := iVal.Type() | ||
for i := 0; i < iVal.NumField(); i++ { | ||
if !iVal.Field(i).IsZero() { | ||
values.Set(toSnakeCase(typ.Field(i).Name), fmt.Sprint(iVal.Field(i))) | ||
} | ||
} | ||
return | ||
} | ||
|
||
func toSnakeCase(str string) string { | ||
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") | ||
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") | ||
|
||
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") | ||
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") | ||
return strings.ToLower(snake) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package pixels | ||
|
||
type CollectionParams struct { | ||
Type string | ||
PageParams | ||
} | ||
|
||
type CollectionResponse struct { | ||
TotalResults int `json:"total_results"` | ||
Page int `json:"page"` | ||
PerPage int `json:"per_page"` | ||
Collections []*Collection `json:"collections"` | ||
NextPage string `json:"next_page"` | ||
PrevPage string `json:"prev_page"` | ||
} | ||
|
||
type CollectionMediaResponse struct { | ||
ID string `json:"id"` | ||
Media []Media `json:"media"` | ||
Page int `json:"page"` | ||
PerPage int `json:"per_page"` | ||
TotalResults int `json:"total_results"` | ||
NextPage string `json:"next_page"` | ||
PrevPage string `json:"prev_page"` | ||
} | ||
|
||
type Media struct { | ||
Type string `json:"type"` | ||
ID int `json:"id"` | ||
Width int `json:"width"` | ||
Height int `json:"height"` | ||
URL string `json:"url"` | ||
Photographer string `json:"photographer,omitempty"` | ||
PhotographerURL string `json:"photographer_url,omitempty"` | ||
PhotographerID int `json:"photographer_id,omitempty"` | ||
AvgColor string `json:"avg_color"` | ||
Src Source `json:"src,omitempty"` | ||
Liked bool `json:"liked,omitempty"` | ||
Duration int `json:"duration,omitempty"` | ||
FullRes interface{} `json:"full_res,omitempty"` | ||
Tags []interface{} `json:"tags,omitempty"` | ||
Image string `json:"image,omitempty"` | ||
User User `json:"user,omitempty"` | ||
VideoFiles []VideoFile `json:"video_files,omitempty"` | ||
VideoPictures []VideoPicture `json:"video_pictures,omitempty"` | ||
} | ||
|
||
type Collection struct { | ||
ID string `json:"id"` | ||
Title string `json:"title"` | ||
Description string `json:"description"` | ||
Private bool `json:"private"` | ||
MediaCount int `json:"media_count"` | ||
PhotosCount int `json:"photos_count"` | ||
VideosCount int `json:"videos_count"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package pixels | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
) | ||
|
||
type CollectionService interface { | ||
Featured(ctx context.Context, params *PageParams) (*CollectionResponse, error) | ||
Get(ctx context.Context, params *PageParams) (*CollectionResponse, error) | ||
Find(ctx context.Context, collectionId string, params *CollectionParams) (*CollectionMediaResponse, error) | ||
} | ||
|
||
type collectionService struct { | ||
cli *Client | ||
} | ||
|
||
func (s *collectionService) Featured(ctx context.Context, params *PageParams) (*CollectionResponse, error) { | ||
var sr CollectionResponse | ||
if err := s.cli.get(ctx, "collections/featured", StructToMap(params), &sr); err != nil { | ||
return nil, fmt.Errorf("GET featured failed: %w", err) | ||
} | ||
|
||
return &sr, nil | ||
} | ||
|
||
func (s *collectionService) Get(ctx context.Context, params *PageParams) (*CollectionResponse, error) { | ||
var cr CollectionResponse | ||
if err := s.cli.get(ctx, "collections", StructToMap(params), &cr); err != nil { | ||
return nil, fmt.Errorf("GET get failed: %w", err) | ||
} | ||
|
||
return &cr, nil | ||
} | ||
|
||
func (s *collectionService) Find(ctx context.Context, collectionId string, params *CollectionParams) (*CollectionMediaResponse, error) { | ||
var cmr CollectionMediaResponse | ||
if err := s.cli.get(ctx, "collections/"+collectionId, StructToMap(params), &cmr); err != nil { | ||
return nil, fmt.Errorf("GET find failed: %w", err) | ||
} | ||
|
||
return &cmr, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package pixels | ||
|
||
import ( | ||
"fmt" | ||
) | ||
|
||
type APIError struct { | ||
HTTPStatus int `json:"-"` | ||
Code int `json:"code"` | ||
Message string `json:"message"` | ||
} | ||
|
||
var _ error = (*APIError)(nil) | ||
|
||
func (err *APIError) Error() string { | ||
if err.Message == "" { | ||
return fmt.Sprintf("request failed with status code %d", err.HTTPStatus) | ||
} | ||
return fmt.Sprintf("StatusCode: %d, Code: %d, Message: %s", err.HTTPStatus, err.Code, err.Message) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
pixels "github.com/kosa3/pexels-go" | ||
"log" | ||
"os" | ||
) | ||
|
||
func main() { | ||
cli := pixels.NewClient(os.Args[1]) | ||
ctx := context.Background() | ||
cs, err := cli.CollectionService.Featured(ctx, &pixels.PageParams{ | ||
Page: 1, | ||
}) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
fmt.Println("Collection Title:", cs.Collections[0].Title) | ||
fmt.Println("RateLimit:", cli.LastRateLimit.Limit) | ||
} |
Oops, something went wrong.