krz/devianter
A DeviantArt guest API library for Go.
clone: git clone https://gitbay.org/krz/devianter.git
v0.3.4: 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.
83//
84// Markup is a rich document rather than a string of prose, in whichever format
85// DeviantArt stored it: tiptap JSON on anything recent (Type is "tiptap"),
86// Draft.js JSON on older bodies, or plain HTML on the oldest. The functions
87// returning a Text generally flatten it to plain text in a neighbouring field,
88// which is what most callers want; read Markup itself for the formatting,
89// images, and links that flattening discards.
90type Text struct {
91 Excerpt string
92 Html struct {
93 Markup, Type string
94 }
95}
96
97// Post is a deviation together with its comment metadata, as returned by
98// [GetDeviation]. IMG and Description are conveniences that GetDeviation derives
99// from the Deviation, so callers need not assemble a URL or flatten a rich-text
100// document themselves. Description is empty for the many deviations that have
101// none.
102//
103// Comments holds only a total and a cursor. To retrieve the comments, pass them
104// to [GetComments] with type 1.
105type Post struct {
106 Deviation Deviation
107 Comments struct {
108 Total int
109 Cursor string
110 }
111
112 ParsedComments []struct {
113 Author string
114 Posted timeStamp
115 Replies, Likes int
116 }
117
118 IMG, Description string
119}
120
121// UrlFromMedia assembles a usable, token-signed image URL from a [Media], along
122// with the filename DeviantArt would serve it under. It selects the "fullview"
123// rendition and returns empty strings if the media has none.
124//
125// An optional thumb argument scales the request down towards that many pixels
126// per side, for fetching a smaller copy than the original. GIFs and very large
127// images (beyond roughly 33 megapixels) are returned at their original URL
128// without resizing, as DeviantArt's resizer refuses them.
129func UrlFromMedia(m Media, thumb ...int) (urlParsed, wellFormattedFilename string) {
130 var url strings.Builder
131
132 subtractWidthHeight := func(to int, target ...*int) {
133 for i, l := 0, len(target); i < l; i++ {
134 for x := *target[i]; x > to; x -= to {
135 *target[i] = x
136 }
137 }
138 }
139
140 for _, t := range m.Types {
141 if t.T == "fullview" {
142 url.WriteString(m.BaseUri)
143 if l := len(m.BaseUri); l != 0 && (m.BaseUri[l-3:] != "gif" && t.W*t.H < 33177600) {
144 if len(thumb) != 0 {
145 subtractWidthHeight(thumb[0], &t.W, &t.H)
146 }
147 wellFormattedFilename = m.Name + m.BaseUri[l-4:]
148
149 url.WriteString("/v1/fit/w_")
150 url.WriteString(strconv.Itoa(t.W))
151 url.WriteString(",h_")
152 url.WriteString(strconv.Itoa(t.H))
153 url.WriteString("/")
154 url.WriteString(wellFormattedFilename)
155
156 }
157 if len(m.Token) > 0 {
158 url.WriteString("?token=")
159 url.WriteString(m.Token[0])
160 }
161 }
162 }
163
164 urlParsed = url.String()
165
166 return
167}
168
169// GetDeviation retrieves a single deviation by its numeric ID and its author's
170// username. Both are required: the endpoint will not resolve an ID alone. They
171// appear in a deviation's page URL, which ends in a slug of the form
172// title-by-author-123456789.
173//
174// The returned Post has its IMG and Description already derived, and its
175// Deviation is fully populated, including Extended.
176func GetDeviation(id string, user string) (st Post, err Error) {
177 err = ujson(
178 "dadeviation/init?deviationid="+id+"&username="+user+"&type=art&include_session=false&expand=deviation.related&preload=true",
179 &st,
180 )
181
182 st.IMG, _ = UrlFromMedia(st.Deviation.Media)
183
184 // The description lives in Extended.DescriptionText on the great majority of
185 // deviations; TextContent carries it on only a small minority. Prefer the
186 // former and fall back, since either may be the populated one.
187 desc := st.Deviation.Extended.DescriptionText.Html.Markup
188 if desc == "" {
189 desc = st.Deviation.TextContent.Html.Markup
190 }
191 st.Description = flattenMarkup(desc)
192
193 return
194}