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