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}