artwork.go 9.0 KB

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