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}