319 lines
6.5 KiB
Go
319 lines
6.5 KiB
Go
|
package wallhaven
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"path"
|
||
|
"path/filepath"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// Search Types
|
||
|
|
||
|
// Category is an enum used to represent wallpaper categories
|
||
|
type Category string
|
||
|
|
||
|
// Purity is an enum used to represent
|
||
|
type Purity string
|
||
|
|
||
|
// Sort enum specifies the various sort types accepted by WH api
|
||
|
type Sort int
|
||
|
|
||
|
// Sort Enum Values
|
||
|
const (
|
||
|
DateAdded Sort = iota + 1
|
||
|
Relevance
|
||
|
Random
|
||
|
Views
|
||
|
Favorites
|
||
|
Toplist
|
||
|
)
|
||
|
|
||
|
func (s Sort) String() string {
|
||
|
str := [...]string{"", "date_added", "relevance", "random", "views", "favorites", "toplist"}
|
||
|
return str[s]
|
||
|
}
|
||
|
|
||
|
// Order enum specifies the sort orders accepted by WH api
|
||
|
type Order int
|
||
|
|
||
|
// Sort Enum Values
|
||
|
const (
|
||
|
Desc Order = iota + 1
|
||
|
Asc
|
||
|
)
|
||
|
|
||
|
func (o Order) String() string {
|
||
|
str := [...]string{"", "desc", "asc"}
|
||
|
return str[o]
|
||
|
}
|
||
|
|
||
|
// Privacy enum specifies the collection privacy returned by WH api
|
||
|
type Privacy int
|
||
|
|
||
|
// Privacy Enum Values
|
||
|
const (
|
||
|
Private Privacy = iota
|
||
|
Public
|
||
|
)
|
||
|
|
||
|
func (p Privacy) String() string {
|
||
|
str := [...]string{"private", "public"}
|
||
|
return str[p]
|
||
|
}
|
||
|
|
||
|
// TopRange is used to specify the time window for 'top' result when topList is chosen as sort param
|
||
|
type TopRange int
|
||
|
|
||
|
// Enum for TopRange values
|
||
|
const (
|
||
|
Day TopRange = iota + 1
|
||
|
ThreeDay
|
||
|
Week
|
||
|
Month
|
||
|
ThreeMonth
|
||
|
SixMonth
|
||
|
Year
|
||
|
)
|
||
|
|
||
|
func (t TopRange) String() string {
|
||
|
str := [...]string{"1d", "3d", "1w", "1M", "3M", "6M", "1y"}
|
||
|
return str[t]
|
||
|
}
|
||
|
|
||
|
// Resolution specifies the image resolution to find
|
||
|
type Resolution struct {
|
||
|
Width int64
|
||
|
Height int64
|
||
|
}
|
||
|
|
||
|
func (r Resolution) String() string {
|
||
|
return fmt.Sprintf("%vx%v", r.Width, r.Height)
|
||
|
}
|
||
|
|
||
|
func (r Resolution) isValid() bool {
|
||
|
return r.Width > 0 && r.Height > 0
|
||
|
}
|
||
|
|
||
|
// Ratio may be used to specify the aspect ratio of the search
|
||
|
type Ratio struct {
|
||
|
Horizontal int
|
||
|
Vertical int
|
||
|
}
|
||
|
|
||
|
func (r Ratio) String() string {
|
||
|
return fmt.Sprintf("%vx%v", r.Horizontal, r.Vertical)
|
||
|
}
|
||
|
|
||
|
func (r Ratio) isValid() bool {
|
||
|
return r.Vertical > 0 && r.Horizontal > 0
|
||
|
}
|
||
|
|
||
|
// WallpaperID is a string representing a wallpaper
|
||
|
type WallpaperID string
|
||
|
|
||
|
// Q is used to hold the Q params for various fulltext options that the WH Search supports
|
||
|
type Q struct {
|
||
|
Tags []string
|
||
|
ExcudeTags []string
|
||
|
UserName string
|
||
|
TagID int
|
||
|
Type string // Type is one of png/jpg
|
||
|
Like WallpaperID
|
||
|
}
|
||
|
|
||
|
func (q Q) toQuery() url.Values {
|
||
|
var sb strings.Builder
|
||
|
for _, tag := range q.Tags {
|
||
|
sb.WriteString("+")
|
||
|
sb.WriteString(tag)
|
||
|
}
|
||
|
for _, etag := range q.ExcudeTags {
|
||
|
sb.WriteString("-")
|
||
|
sb.WriteString(etag)
|
||
|
}
|
||
|
if len(q.UserName) > 0 {
|
||
|
sb.WriteString("@")
|
||
|
sb.WriteString(q.UserName)
|
||
|
}
|
||
|
if len(q.Type) > 0 {
|
||
|
sb.WriteString("type:")
|
||
|
sb.WriteString(q.Type)
|
||
|
}
|
||
|
out := url.Values{}
|
||
|
val := sb.String()
|
||
|
if len(val) > 0 {
|
||
|
out.Set("q", val)
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// Search provides various parameters to search for on wallhaven
|
||
|
type Search struct {
|
||
|
Query Q
|
||
|
Categories string
|
||
|
Purities string
|
||
|
Sorting Sort
|
||
|
Order Order
|
||
|
TopRange string
|
||
|
AtLeast Resolution
|
||
|
Resolutions []Resolution
|
||
|
Ratios []Ratio
|
||
|
Colors []string // Colors is an array of hex colors represented as strings in #RRGGBB format
|
||
|
Page int
|
||
|
}
|
||
|
|
||
|
func (s Search) toQuery() url.Values {
|
||
|
v := s.Query.toQuery()
|
||
|
if s.Categories != "" {
|
||
|
v.Add("categories", s.Categories)
|
||
|
}
|
||
|
if s.Purities != "" {
|
||
|
v.Add("purity", s.Purities)
|
||
|
}
|
||
|
if s.Sorting > 0 {
|
||
|
v.Add("sorting", s.Sorting.String())
|
||
|
}
|
||
|
if s.Order > 0 {
|
||
|
v.Add("order", s.Order.String())
|
||
|
}
|
||
|
if s.TopRange != "" && s.Sorting == Toplist {
|
||
|
v.Add("topRange", s.TopRange)
|
||
|
}
|
||
|
if s.AtLeast.isValid() {
|
||
|
v.Add("atleast", s.AtLeast.String())
|
||
|
}
|
||
|
if len(s.Resolutions) > 0 {
|
||
|
outRes := []string{}
|
||
|
for _, res := range s.Resolutions {
|
||
|
if res.isValid() {
|
||
|
outRes = append(outRes, res.String())
|
||
|
}
|
||
|
}
|
||
|
if len(outRes) > 0 {
|
||
|
v.Add("resolutions", strings.Join(outRes, ","))
|
||
|
}
|
||
|
}
|
||
|
if len(s.Ratios) > 0 {
|
||
|
outRat := []string{}
|
||
|
for _, rat := range s.Ratios {
|
||
|
if rat.isValid() {
|
||
|
outRat = append(outRat, rat.String())
|
||
|
}
|
||
|
}
|
||
|
if len(outRat) > 0 {
|
||
|
v.Add("ratios", strings.Join(outRat, ","))
|
||
|
}
|
||
|
}
|
||
|
if len(s.Colors) > 0 {
|
||
|
v.Add("colors", strings.Join([]string(s.Colors), ","))
|
||
|
}
|
||
|
if s.Page > 0 {
|
||
|
v.Add("page", strconv.Itoa(s.Page))
|
||
|
}
|
||
|
return v
|
||
|
}
|
||
|
|
||
|
// SearchWallpapers performs a search on WH given a set of criteria.
|
||
|
// Note that this API behaves slightly differently than the various
|
||
|
// single item apis as it also includes the metadata for paging purposes
|
||
|
func SearchWallpapers(search *Search) (*SearchResults, error) {
|
||
|
resp, err := getWithValues("/search/", search.toQuery())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
out := &SearchResults{}
|
||
|
err = processResponse(resp, out)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return out, nil
|
||
|
}
|
||
|
|
||
|
func processResponse(resp *http.Response, out interface{}) error {
|
||
|
byt, err := io.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
resp.Body.Close()
|
||
|
return json.Unmarshal(byt, out)
|
||
|
}
|
||
|
|
||
|
// Result Structs -- server responses
|
||
|
|
||
|
// SearchResults a wrapper containing search results from wh
|
||
|
type SearchResults struct {
|
||
|
Data []Wallpaper `json:"data"`
|
||
|
}
|
||
|
|
||
|
// Wallpaper information about a given wallpaper
|
||
|
type Wallpaper struct {
|
||
|
Path string `json:"path"`
|
||
|
}
|
||
|
|
||
|
// Tag full data on a given wallpaper tag
|
||
|
type Tag struct {
|
||
|
ID int `json:"id"`
|
||
|
Name string `json:"name"`
|
||
|
Alias string `json:"alias"`
|
||
|
CategoryID int `json:"category_id"`
|
||
|
Category string `json:"category"`
|
||
|
Purity string `json:"purity"`
|
||
|
CreatedAt string `json:"created_at"`
|
||
|
}
|
||
|
|
||
|
const baseURL = "https://wallhaven.cc/api/v1"
|
||
|
|
||
|
func getWithBase(p string) string {
|
||
|
return baseURL + p
|
||
|
}
|
||
|
|
||
|
func getWithValues(p string, v url.Values) (*http.Response, error) {
|
||
|
u, err := url.Parse(getWithBase(p))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
u.RawQuery = v.Encode()
|
||
|
return getAuthedResponse(u.String())
|
||
|
}
|
||
|
|
||
|
func getAuthedResponse(url string) (*http.Response, error) {
|
||
|
req, err := http.NewRequest("GET", url, nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
req.Header.Set("X-API-Key", os.Getenv("WH_API_KEY"))
|
||
|
return client.Do(req)
|
||
|
}
|
||
|
|
||
|
var client = &http.Client{}
|
||
|
|
||
|
func download(filepath string, resp *http.Response) error {
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
out, err := os.Create(filepath)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer out.Close()
|
||
|
|
||
|
_, err = io.Copy(out, resp.Body)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Download downloads a wallpaper given the local filepath to save the wallpaper to
|
||
|
func (w *Wallpaper) Download(dir string) error {
|
||
|
resp, err := getAuthedResponse(w.Path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
path := filepath.Join(dir, path.Base(w.Path))
|
||
|
return download(path, resp)
|
||
|
}
|