krz/devianter
A DeviantArt guest API library for Go.
clone: git clone https://gitbay.org/krz/devianter.git
v0.3.2: deviantion.go · raw
1package devianter
2
3import (
4 "strconv"
5 "strings"
6 "time"
7)
8
9// timeStamp is a time.Time that parses DeviantArt's publication timestamps,
10// which are ISO 8601 with no colon in the zone offset and so are rejected by
11// encoding/json's default time handling.
12type timeStamp struct {
13 time.Time
14}
15
16func (t *timeStamp) UnmarshalJSON(b []byte) (err error) {
17 if b[0] == '"' && b[len(b)-1] == '"' {
18 b = b[1 : len(b)-1]
19 }
20 t.Time, err = time.Parse("2006-01-02T15:04:05-0700", string(b))
21 return
22}
23
24// Deviation is a single artwork and its metadata: the central type of this
25// package. Most endpoints return these, either alone or in slices.
26//
27// How much of it is populated depends on the endpoint. Search results and
28// gallery listings return a shallow Deviation — enough for a thumbnail and a
29// title — while [GetDeviation] fills in Extended, with the tags, original file
30// details, and description. A zero-valued field usually means the endpoint did
31// not send it rather than that the artwork lacks it.
32type Deviation struct {
33 Title, Url, License string
34 PublishedTime timeStamp
35 ID int `json:"deviationId"`
36
37 NSFW bool `json:"isMature"`
38 AI bool `json:"isAiGenerated"`
39 DD bool `json:"isDailyDeviation"`
40
41 Author struct {
42 Username string
43 }
44 Stats struct {
45 Favourites, Views, Downloads int
46 }
47 Media Media
48 Extended struct {
49 Tags []struct {
50 Name string
51 }
52 OriginalFile struct {
53 Type string
54 Width int
55 Height int
56 Filesize int
57 }
58 DescriptionText Text
59 RelatedContent []struct {
60 Deviations []Deviation
61 }
62 }
63 TextContent Text
64}
65
66// Media locates a deviation's image files. It is not a usable URL on its own:
67// the pieces have to be assembled, and the result signed with a token. Pass it
68// to [UrlFromMedia] rather than building the URL by hand.
69type Media struct {
70 BaseUri string
71 Name string `json:"prettyName"`
72 Token []string
73 // Types are the renditions available (thumbnails, preview, "fullview"), each
74 // with its own dimensions.
75 Types []struct {
76 T string
77 H, W int
78 }
79}
80
81// Text is a block of user-written text — a description, a comment, a group's
82// about page. Markup holds either HTML or a JSON-encoded Draft.js document,
83// distinguished by Type; the functions that return a Text generally extract the
84// plain text into a neighbouring field, which is easier to use.
85type Text struct {
86 Excerpt string
87 Html struct {
88 Markup, Type string
89 }
90}
91
92// Post is a deviation together with its comment metadata, as returned by
93// [GetDeviation]. IMG and Description are conveniences that GetDeviation derives
94// from the Deviation, so callers need not assemble a URL or decode Draft.js
95// markup themselves.
96//
97// Comments holds only a total and a cursor. To retrieve the comments, pass them
98// to [GetComments] with type 1.
99type Post struct {
100 Deviation Deviation
101 Comments struct {
102 Total int
103 Cursor string
104 }
105
106 ParsedComments []struct {
107 Author string
108 Posted timeStamp
109 Replies, Likes int
110 }
111
112 IMG, Description string
113}
114
115// UrlFromMedia assembles a usable, token-signed image URL from a [Media], along
116// with the filename DeviantArt would serve it under. It selects the "fullview"
117// rendition and returns empty strings if the media has none.
118//
119// An optional thumb argument scales the request down towards that many pixels
120// per side, for fetching a smaller copy than the original. GIFs and very large
121// images (beyond roughly 33 megapixels) are returned at their original URL
122// without resizing, as DeviantArt's resizer refuses them.
123func UrlFromMedia(m Media, thumb ...int) (urlParsed, wellFormattedFilename string) {
124 var url strings.Builder
125
126 subtractWidthHeight := func(to int, target ...*int) {
127 for i, l := 0, len(target); i < l; i++ {
128 for x := *target[i]; x > to; x -= to {
129 *target[i] = x
130 }
131 }
132 }
133
134 for _, t := range m.Types {
135 if t.T == "fullview" {
136 url.WriteString(m.BaseUri)
137 if l := len(m.BaseUri); l != 0 && (m.BaseUri[l-3:] != "gif" && t.W*t.H < 33177600) {
138 if len(thumb) != 0 {
139 subtractWidthHeight(thumb[0], &t.W, &t.H)
140 }
141 wellFormattedFilename = m.Name + m.BaseUri[l-4:]
142
143 url.WriteString("/v1/fit/w_")
144 url.WriteString(strconv.Itoa(t.W))
145 url.WriteString(",h_")
146 url.WriteString(strconv.Itoa(t.H))
147 url.WriteString("/")
148 url.WriteString(wellFormattedFilename)
149
150 }
151 if len(m.Token) > 0 {
152 url.WriteString("?token=")
153 url.WriteString(m.Token[0])
154 }
155 }
156 }
157
158 urlParsed = url.String()
159
160 return
161}
162
163// GetDeviation retrieves a single deviation by its numeric ID and its author's
164// username. Both are required: the endpoint will not resolve an ID alone. They
165// appear in a deviation's page URL, which ends in a slug of the form
166// title-by-author-123456789.
167//
168// The returned Post has its IMG and Description already derived, and its
169// Deviation is fully populated, including Extended.
170func GetDeviation(id string, user string) (st Post, err Error) {
171 err = ujson(
172 "dadeviation/init?deviationid="+id+"&username="+user+"&type=art&include_session=false&expand=deviation.related&preload=true",
173 &st,
174 )
175
176 st.IMG, _ = UrlFromMedia(st.Deviation.Media)
177
178 st.Description = flattenComment(st.Deviation.TextContent.Html.Markup)
179
180 return
181}