artwork.go 8.3 KB

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