artwork.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. package core
  2. import (
  3. "errors"
  4. "fmt"
  5. "html/template"
  6. "sort"
  7. "strings"
  8. "sync"
  9. "time"
  10. "codeberg.org/vnpower/pixivfe/v2/session"
  11. "github.com/goccy/go-json"
  12. "github.com/gofiber/fiber/v2"
  13. )
  14. // Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
  15. // Those values are saved in `xRestrict`
  16. // 0: Safe
  17. // 1: R18
  18. // 2: R18G
  19. type xRestrict int
  20. const (
  21. Safe xRestrict = 0
  22. R18 xRestrict = 1
  23. R18G xRestrict = 2
  24. )
  25. var xRestrictModel = map[xRestrict]string{
  26. Safe: "",
  27. R18: "R18",
  28. R18G: "R18G",
  29. }
  30. // Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
  31. // Those values are saved in `aiType`
  32. // 0: Not rated / Unknown
  33. // 1: Not AI-generated
  34. // 2: AI-generated
  35. type aiType int
  36. const (
  37. Unrated aiType = 0
  38. NotAI aiType = 1
  39. AI aiType = 2
  40. )
  41. var aiTypeModel = map[aiType]string{
  42. Unrated: "Unrated",
  43. NotAI: "Not AI",
  44. AI: "AI",
  45. }
  46. type ImageResponse struct {
  47. Width int `json:"width"`
  48. Height int `json:"height"`
  49. Urls map[string]string `json:"urls"`
  50. }
  51. type Image struct {
  52. Width int
  53. Height int
  54. Small string
  55. Medium string
  56. Large string
  57. Original string
  58. }
  59. type Tag struct {
  60. Name string `json:"tag"`
  61. TranslatedName string `json:"translation"`
  62. }
  63. type Comment struct {
  64. AuthorID string `json:"userId"`
  65. AuthorName string `json:"userName"`
  66. Avatar string `json:"img"`
  67. Context string `json:"comment"`
  68. Stamp string `json:"stampId"`
  69. Date string `json:"commentDate"`
  70. }
  71. type UserBrief struct {
  72. ID string `json:"userId"`
  73. Name string `json:"name"`
  74. Avatar string `json:"imageBig"`
  75. }
  76. type ArtworkBrief struct {
  77. ID string `json:"id"`
  78. Title string `json:"title"`
  79. ArtistID string `json:"userId"`
  80. ArtistName string `json:"userName"`
  81. ArtistAvatar string `json:"profileImageUrl"`
  82. Thumbnail string `json:"url"`
  83. Pages int `json:"pageCount"`
  84. XRestrict int `json:"xRestrict"`
  85. AiType int `json:"aiType"`
  86. Bookmarked any `json:"bookmarkData"`
  87. IllustType int `json:"illustType"`
  88. }
  89. type Illust struct {
  90. ID string `json:"id"`
  91. Title string `json:"title"`
  92. Description template.HTML `json:"description"`
  93. UserID string `json:"userId"`
  94. UserName string `json:"userName"`
  95. UserAccount string `json:"userAccount"`
  96. Date time.Time `json:"uploadDate"`
  97. Images []Image
  98. Tags []Tag `json:"tags"`
  99. Pages int `json:"pageCount"`
  100. Bookmarks int `json:"bookmarkCount"`
  101. Likes int `json:"likeCount"`
  102. Comments int `json:"commentCount"`
  103. Views int `json:"viewCount"`
  104. CommentDisabled int `json:"commentOff"`
  105. SanityLevel int `json:"sl"`
  106. XRestrict xRestrict `json:"xRestrict"`
  107. AiType aiType `json:"aiType"`
  108. BookmarkData any `json:"bookmarkData"`
  109. Liked bool `json:"likeData"`
  110. User UserBrief
  111. RecentWorks []ArtworkBrief
  112. RelatedWorks []ArtworkBrief
  113. CommentsList []Comment
  114. IsUgoira bool
  115. BookmarkID string
  116. }
  117. func GetUserBasicInformation(c *fiber.Ctx, id string) (UserBrief, error) {
  118. var user UserBrief
  119. URL := GetUserInformationURL(id)
  120. response, err := UnwrapWebAPIRequest(c.Context(), URL, "")
  121. if err != nil {
  122. return user, err
  123. }
  124. response = session.ProxyImageUrl(c, response)
  125. err = json.Unmarshal([]byte(response), &user)
  126. if err != nil {
  127. return user, err
  128. }
  129. return user, nil
  130. }
  131. func GetArtworkImages(c *fiber.Ctx, id string) ([]Image, error) {
  132. var resp []ImageResponse
  133. var images []Image
  134. URL := GetArtworkImagesURL(id)
  135. response, err := UnwrapWebAPIRequest(c.Context(), URL, "")
  136. if err != nil {
  137. return nil, err
  138. }
  139. response = session.ProxyImageUrl(c, response)
  140. err = json.Unmarshal([]byte(response), &resp)
  141. if err != nil {
  142. return images, err
  143. }
  144. // Extract and proxy every images
  145. for _, imageRaw := range resp {
  146. var image Image
  147. // this is the original art dimention, not the "regular" art dimension
  148. // the image ratio of "regular" is close to Width/Height
  149. // maybe not useful
  150. image.Width = imageRaw.Width
  151. image.Height = imageRaw.Height
  152. image.Small = imageRaw.Urls["thumb_mini"]
  153. image.Medium = imageRaw.Urls["small"]
  154. image.Large = imageRaw.Urls["regular"]
  155. image.Original = imageRaw.Urls["original"]
  156. images = append(images, image)
  157. }
  158. return images, nil
  159. }
  160. func GetArtworkComments(c *fiber.Ctx, id string) ([]Comment, error) {
  161. var body struct {
  162. Comments []Comment `json:"comments"`
  163. }
  164. URL := GetArtworkCommentsURL(id)
  165. response, err := UnwrapWebAPIRequest(c.Context(), URL, "")
  166. if err != nil {
  167. return nil, err
  168. }
  169. response = session.ProxyImageUrl(c, response)
  170. err = json.Unmarshal([]byte(response), &body)
  171. if err != nil {
  172. return nil, err
  173. }
  174. return body.Comments, nil
  175. }
  176. func GetRelatedArtworks(c *fiber.Ctx, id string) ([]ArtworkBrief, error) {
  177. var body struct {
  178. Illusts []ArtworkBrief `json:"illusts"`
  179. }
  180. // TODO: keep the hard-coded limit?
  181. URL := GetArtworkRelatedURL(id, 96)
  182. response, err := UnwrapWebAPIRequest(c.Context(), URL, "")
  183. if err != nil {
  184. return nil, err
  185. }
  186. response = session.ProxyImageUrl(c, response)
  187. err = json.Unmarshal([]byte(response), &body)
  188. if err != nil {
  189. return nil, err
  190. }
  191. return body.Illusts, nil
  192. }
  193. func GetArtworkByID(c *fiber.Ctx, id string, full bool) (*Illust, error) {
  194. URL := GetArtworkInformationURL(id)
  195. token := session.GetPixivToken(c)
  196. response, err := UnwrapWebAPIRequest(c.Context(), URL, token)
  197. if err != nil {
  198. return nil, err
  199. }
  200. var illust struct {
  201. *Illust
  202. // recent illustrations by same user
  203. Recent map[int]any `json:"userIllusts"`
  204. RawTags json.RawMessage `json:"tags"`
  205. }
  206. // Parse basic illust information
  207. err = json.Unmarshal([]byte(response), &illust)
  208. if err != nil {
  209. return nil, err
  210. }
  211. if illust.BookmarkData != nil {
  212. t := illust.BookmarkData.(map[string]any)
  213. illust.BookmarkID = t["id"].(string)
  214. }
  215. // Begin testing here
  216. wg := sync.WaitGroup{}
  217. cerr := make(chan error, 6)
  218. wg.Add(3)
  219. go func() {
  220. // Get illust images
  221. defer wg.Done()
  222. images, err := GetArtworkImages(c, id)
  223. if err != nil {
  224. cerr <- err
  225. return
  226. }
  227. illust.Images = images
  228. }()
  229. go func() {
  230. // Get basic user information (the URL above does not contain avatars)
  231. defer wg.Done()
  232. var err error
  233. userInfo, err := GetUserBasicInformation(c, illust.UserID)
  234. if err != nil {
  235. cerr <- err
  236. return
  237. }
  238. illust.User = userInfo
  239. }()
  240. go func() {
  241. defer wg.Done()
  242. var err error
  243. // Extract tags
  244. var tags struct {
  245. Tags []struct {
  246. Tag string `json:"tag"`
  247. Translation map[string]string `json:"translation"`
  248. } `json:"tags"`
  249. }
  250. err = json.Unmarshal(illust.RawTags, &tags)
  251. if err != nil {
  252. cerr <- err
  253. return
  254. }
  255. var tagsList []Tag
  256. for _, tag := range tags.Tags {
  257. var newTag Tag
  258. newTag.Name = tag.Tag
  259. newTag.TranslatedName = tag.Translation["en"]
  260. tagsList = append(tagsList, newTag)
  261. }
  262. illust.Tags = tagsList
  263. }()
  264. if full {
  265. wg.Add(3)
  266. go func() {
  267. defer wg.Done()
  268. var err error
  269. // Get recent artworks
  270. ids := make([]int, 0)
  271. for k := range illust.Recent {
  272. ids = append(ids, k)
  273. }
  274. sort.Sort(sort.Reverse(sort.IntSlice(ids)))
  275. idsString := ""
  276. count := min(len(ids), 20)
  277. for i := 0; i < count; i++ {
  278. idsString += fmt.Sprintf("&ids[]=%d", ids[i])
  279. }
  280. recent, err := GetUserArtworks(c, illust.UserID, idsString)
  281. if err != nil {
  282. cerr <- err
  283. return
  284. }
  285. sort.Slice(recent[:], func(i, j int) bool {
  286. left := recent[i].ID
  287. right := recent[j].ID
  288. return numberGreaterThan(left, right)
  289. })
  290. illust.RecentWorks = recent
  291. }()
  292. go func() {
  293. defer wg.Done()
  294. var err error
  295. related, err := GetRelatedArtworks(c, id)
  296. if err != nil {
  297. cerr <- err
  298. return
  299. }
  300. illust.RelatedWorks = related
  301. }()
  302. go func() {
  303. defer wg.Done()
  304. if illust.CommentDisabled == 1 {
  305. return
  306. }
  307. var err error
  308. comments, err := GetArtworkComments(c, id)
  309. if err != nil {
  310. cerr <- err
  311. return
  312. }
  313. illust.CommentsList = comments
  314. }()
  315. }
  316. wg.Wait()
  317. close(cerr)
  318. all_errors := []error{}
  319. for suberr := range cerr {
  320. all_errors = append(all_errors, suberr)
  321. }
  322. err_summary := errors.Join(all_errors...)
  323. if err_summary != nil {
  324. return nil, err_summary
  325. }
  326. // If this artwork is an ugoira
  327. illust.IsUgoira = strings.Contains(illust.Images[0].Original, "ugoira")
  328. return illust.Illust, nil
  329. }