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
182
183
184
185
186
187
188
189
190
191
192
193
194
|
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 is a rich document rather than a string of prose, in whichever format
// DeviantArt stored it: tiptap JSON on anything recent (Type is "tiptap"),
// Draft.js JSON on older bodies, or plain HTML on the oldest. The functions
// returning a Text generally flatten it to plain text in a neighbouring field,
// which is what most callers want; read Markup itself for the formatting,
// images, and links that flattening discards.
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 flatten a rich-text
// document themselves. Description is empty for the many deviations that have
// none.
//
// 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)
// The description lives in Extended.DescriptionText on the great majority of
// deviations; TextContent carries it on only a small minority. Prefer the
// former and fall back, since either may be the populated one.
desc := st.Deviation.Extended.DescriptionText.Html.Markup
if desc == "" {
desc = st.Deviation.TextContent.Html.Markup
}
st.Description = flattenMarkup(desc)
return
}
|