krz/devianter

A DeviantArt guest API library for Go.

clone: git clone https://gitbay.org/krz/devianter.git

v0.3.2: misc.go · raw

  1package devianter
  2
  3import (
  4	"errors"
  5	"math"
  6	"net/url"
  7	"strconv"
  8	"strings"
  9)
 10
 11/* AVATARS AND EMOJIS */
 12// AEmedia fetches a user's avatar or a site emoji by name. t selects which:
 13// 'a' for an avatar, 'e' for an emoji.
 14//
 15// It returns the image data itself, not a URL. DeviantArt does not say which
 16// format a given name is stored in, so this tries .jpg, .png, and .gif in turn
 17// and returns the first that exists — up to three requests per call, and three
 18// for a name that does not exist.
 19//
 20// Passing any other t returns an error without making a request.
 21func AEmedia(name string, t rune) (string, error) {
 22	if len(name) < 2 {
 23		return "", errors.New("name must be specified")
 24	}
 25	var extensions = [3]string{
 26		".jpg",
 27		".png",
 28		".gif",
 29	}
 30	name = strings.ToLower(name)
 31
 32	// Avatars and emoji are sharded into directories by the leading characters of
 33	// the name; avatars additionally normalise dashes to underscores first.
 34	var b strings.Builder
 35	switch t {
 36	case 'a':
 37		b.WriteString("https://a.deviantart.net/avatars-big/")
 38		name_without_dashes := strings.ReplaceAll(name, "-", "_")
 39		b.WriteString(name_without_dashes[:1])
 40		b.WriteString("/")
 41		b.WriteString(name_without_dashes[1:2])
 42		b.WriteString("/")
 43	case 'e':
 44		b.WriteString("https://e.deviantart.net/emoticons/")
 45		b.WriteString(name[:1])
 46		b.WriteString("/")
 47	default:
 48		return "", errors.New("invalid type: want 'a' (avatar) or 'e' (emoji)")
 49	}
 50	b.WriteString(name)
 51
 52	// Probe each extension; the first 200 is the real format.
 53	for x := 0; x < len(extensions); x++ {
 54		req := request(b.String() + extensions[x])
 55		if req.Status == 200 {
 56			return req.Body, nil
 57		}
 58	}
 59
 60	return "", errors.New("user not exists")
 61}
 62
 63/* DAILY DEVIATIONS */
 64// DailyDeviations is the staff-curated front page selection. The picks are
 65// grouped into Strips, each a titled row as the site presents it; Deviations is
 66// the ungrouped listing.
 67type DailyDeviations struct {
 68	HasMore bool
 69	Strips  []struct {
 70		Codename, Title string
 71		TitleType       string
 72		Deviations      []Deviation
 73	}
 74	Deviations []Deviation
 75}
 76
 77// GetDailyDeviations retrieves a page of the daily deviation selection. Pages
 78// are zero-based; check the returned HasMore before asking for the next.
 79func GetDailyDeviations(page int) (dd DailyDeviations, err Error) {
 80	err = ujson("dabrowse/networkbar/rfy/deviations?page="+strconv.Itoa(page), &dd)
 81	return
 82}
 83
 84/* SEARCH */
 85// Search is a page of search results. Read the matches from Results, which
 86// [PerformSearch] populates whichever field the endpoint used.
 87//
 88// Total is DeviantArt's own estimate and is approximate. Pages is derived from
 89// it and capped at 417, the depth a guest session can reach before the API stops
 90// paginating.
 91type Search struct {
 92	Total   int `json:"estTotal"`
 93	Pages   int // only for 'a' and 'g' scope.
 94	HasMore bool
 95	Results []Deviation `json:"deviations"`
 96	// ResultsGalleryTemp receives the results of gallery and collection searches,
 97	// which return them under a different key. PerformSearch copies it into
 98	// Results; callers should not need this field.
 99	ResultsGalleryTemp []Deviation `json:"results"`
100}
101
102// PerformSearch searches DeviantArt. scope selects what is being searched:
103//
104//	'a' — everything, by title and description
105//	't' — by tag
106//	'g' — within one user's or group's gallery
107//	'f' — within one user's or group's collections (favourites)
108//
109// Scopes 'g' and 'f' search a particular account, so they require the username
110// as the final argument and return an error without it. The other two ignore it.
111//
112// Pages are zero-based. A guest session cannot page beyond roughly 417 pages
113// deep regardless of how many results Total claims.
114//
115// Passing any other scope returns an error without making a request.
116func PerformSearch(query string, page int, scope rune, user ...string) (ss Search, daError Error, err error) {
117	var buildurl strings.Builder
118
119	switch scope {
120	case 'a':
121		buildurl.WriteString("dabrowse/search/all?q=")
122	case 't':
123		buildurl.WriteString("dabrowse/networkbar/tag/deviations?tag=")
124	case 'g', 'f':
125		if user == nil {
126			err = errors.New("missing username (last argument)")
127			return
128		}
129
130		buildurl.WriteString("dashared/gallection/search?username=")
131		buildurl.WriteString(user[0])
132		buildurl.WriteString("&type=")
133		if scope == 'g' {
134			buildurl.WriteString("gallery")
135		} else {
136			buildurl.WriteString("collection")
137		}
138		buildurl.WriteString("&order=most-recent&init=true&limit=50&q=")
139	default:
140		err = errors.New("invalid scope: want 'a' (all), 't' (tag), 'g' (gallery) or 'f' (favourites)")
141		return
142	}
143
144	buildurl.WriteString(url.QueryEscape(query))
145	// Gallery search paginates by item offset rather than page number.
146	if scope != 'g' {
147		buildurl.WriteString("&page=")
148	} else {
149		buildurl.WriteString("&offset=")
150		page = 50 * page
151	}
152	buildurl.WriteString(strconv.Itoa(page))
153
154	daError = ujson(buildurl.String(), &ss)
155
156	if ss.Results == nil {
157		ss.Results = ss.ResultsGalleryTemp
158	}
159
160	// Derive the page count from the result estimate, clamped to the 417 pages a
161	// guest session can actually reach.
162	totalfloat := int(math.Round(float64(ss.Total / 25)))
163	for x := 0; x < totalfloat; x++ {
164		if x <= 417 {
165			ss.Pages = x
166		}
167	}
168
169	return
170}