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}