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
171
172
173
174
175
176
177
178
179
180
181
|
package devianter
import (
"strconv"
"strings"
"time"
)
// timeStamp is a time.Time that parses DeviantArt's publication timestamps,
// which are ISO 8601 with no colon in the zone offset and so are rejected by
// encoding/json's default time handling.
type timeStamp struct {
time.Time
}
func (t *timeStamp) UnmarshalJSON(b []byte) (err error) {
if b[0] == '"' && b[len(b)-1] == '"' {
b = b[1 : len(b)-1]
}
t.Time, err = time.Parse("2006-01-02T15:04:05-0700", string(b))
return
}
// Deviation is a single artwork and its metadata: the central type of this
// package. Most endpoints return these, either alone or in slices.
//
// How much of it is populated depends on the endpoint. Search results and
// gallery listings return a shallow Deviation — enough for a thumbnail and a
// title — while [GetDeviation] fills in Extended, with the tags, original file
// details, and description. A zero-valued field usually means the endpoint did
// not send it rather than that the artwork lacks it.
type Deviation struct {
Title, Url, License string
PublishedTime timeStamp
ID int `json:"deviationId"`
NSFW bool `json:"isMature"`
AI bool `json:"isAiGenerated"`
DD bool `json:"isDailyDeviation"`
Author struct {
Username string
}
Stats struct {
Favourites, Views, Downloads int
}
Media Media
Extended struct {
Tags []struct {
Name string
}
OriginalFile struct {
Type string
Width int
Height int
Filesize int
}
DescriptionText Text
RelatedContent []struct {
Deviations []Deviation
}
}
TextContent Text
}
// Media locates a deviation's image files. It is not a usable URL on its own:
// the pieces have to be assembled, and the result signed with a token. Pass it
// to [UrlFromMedia] rather than building the URL by hand.
type Media struct {
BaseUri string
Name string `json:"prettyName"`
Token []string
// Types are the renditions available (thumbnails, preview, "fullview"), each
// with its own dimensions.
Types []struct {
T string
H, W int
}
}
// Text is a block of user-written text — a description, a comment, a group's
// about page. Markup holds either HTML or a JSON-encoded Draft.js document,
// distinguished by Type; the functions that return a Text generally extract the
// plain text into a neighbouring field, which is easier to use.
type Text struct {
Excerpt string
Html struct {
Markup, Type string
}
}
// Post is a deviation together with its comment metadata, as returned by
// [GetDeviation]. IMG and Description are conveniences that GetDeviation derives
// from the Deviation, so callers need not assemble a URL or decode Draft.js
// markup themselves.
//
// Comments holds only a total and a cursor. To retrieve the comments, pass them
// to [GetComments] with type 1.
type Post struct {
Deviation Deviation
Comments struct {
Total int
Cursor string
}
ParsedComments []struct {
Author string
Posted timeStamp
Replies, Likes int
}
IMG, Description string
}
// UrlFromMedia assembles a usable, token-signed image URL from a [Media], along
// with the filename DeviantArt would serve it under. It selects the "fullview"
// rendition and returns empty strings if the media has none.
//
// An optional thumb argument scales the request down towards that many pixels
// per side, for fetching a smaller copy than the original. GIFs and very large
// images (beyond roughly 33 megapixels) are returned at their original URL
// without resizing, as DeviantArt's resizer refuses them.
func UrlFromMedia(m Media, thumb ...int) (urlParsed, wellFormattedFilename string) {
var url strings.Builder
subtractWidthHeight := func(to int, target ...*int) {
for i, l := 0, len(target); i < l; i++ {
for x := *target[i]; x > to; x -= to {
*target[i] = x
}
}
}
for _, t := range m.Types {
if t.T == "fullview" {
url.WriteString(m.BaseUri)
if l := len(m.BaseUri); l != 0 && (m.BaseUri[l-3:] != "gif" && t.W*t.H < 33177600) {
if len(thumb) != 0 {
subtractWidthHeight(thumb[0], &t.W, &t.H)
}
wellFormattedFilename = m.Name + m.BaseUri[l-4:]
url.WriteString("/v1/fit/w_")
url.WriteString(strconv.Itoa(t.W))
url.WriteString(",h_")
url.WriteString(strconv.Itoa(t.H))
url.WriteString("/")
url.WriteString(wellFormattedFilename)
}
if len(m.Token) > 0 {
url.WriteString("?token=")
url.WriteString(m.Token[0])
}
}
}
urlParsed = url.String()
return
}
// GetDeviation retrieves a single deviation by its numeric ID and its author's
// username. Both are required: the endpoint will not resolve an ID alone. They
// appear in a deviation's page URL, which ends in a slug of the form
// title-by-author-123456789.
//
// The returned Post has its IMG and Description already derived, and its
// Deviation is fully populated, including Extended.
func GetDeviation(id string, user string) (st Post, err Error) {
err = ujson(
"dadeviation/init?deviationid="+id+"&username="+user+"&type=art&include_session=false&expand=deviation.related&preload=true",
&st,
)
st.IMG, _ = UrlFromMedia(st.Deviation.Media)
st.Description = flattenComment(st.Deviation.TextContent.Html.Markup)
return
}
|