Files
PixivFE/core/artwork.go
T
perennial 9934268f01 core/artwork: rm duplicate PopulateThumbnails code
already populated in the populateArtworkIDs call
2024-11-08 21:26:05 +11:00

417 lines
10 KiB
Go

package core
import (
"fmt"
"net/http"
"sort"
"strings"
"time"
"golang.org/x/sync/errgroup"
"github.com/goccy/go-json"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/i18n"
"codeberg.org/vnpower/pixivfe/v2/server/session"
)
var artworkCommentService = NewCommentService(GetArtworkCommentsURL)
// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `XRestrict`
// 0: Safe
// 1: R18
// 2: R18G
type XRestrict int
const (
Safe XRestrict = 0
R18 XRestrict = 1
R18G XRestrict = 2
)
func (x XRestrict) String() (string, error) {
switch x {
case Safe:
return i18n.Tr("Safe"), nil
case R18:
return i18n.Tr("R18"), nil
case R18G:
return i18n.Tr("R18G"), nil
}
return "", fmt.Errorf("invalid value: %#v", int(x))
}
// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `aiType`
// 0: Not rated / Unknown
// 1: Not AI-generated
// 2: AI-generated
type AiType int
const (
Unrated AiType = 0
NotAI AiType = 1
AI AiType = 2
)
func (x AiType) String() (string, error) {
switch x {
case Unrated:
return i18n.Tr("Unrated"), nil
case NotAI:
return i18n.Tr("Not AI"), nil
case AI:
return i18n.Tr("AI"), nil
}
return "", fmt.Errorf("invalid value: %#v", int(x))
}
type ImageResponse struct {
Width int `json:"width"`
Height int `json:"height"`
Urls map[string]string `json:"urls"`
}
type Image struct {
Width int
Height int
Small string
Medium string
Large string
Original string
IllustType int
}
type ArtworkTag struct {
Name string `json:"tag"`
TranslatedName string `json:"translation"`
}
type Comment struct {
AuthorID string `json:"userId"`
AuthorName string `json:"userName"`
Avatar string `json:"img"`
Context string `json:"comment"`
Stamp string `json:"stampId"`
Date string `json:"commentDate"`
}
type UserBrief struct {
ID string `json:"userId"`
Name string `json:"name"`
Avatar string `json:"imageBig"`
}
type ArtworkBrief struct {
ID string `json:"id"`
Title string `json:"title"`
ArtistID string `json:"userId"`
ArtistName string `json:"userName"`
ArtistAvatar string `json:"profileImageUrl"`
Thumbnail string `json:"url"`
Thumbnails Thumbnails
Pages int `json:"pageCount"`
XRestrict int `json:"xRestrict"`
AiType int `json:"aiType"`
Bookmarked any `json:"bookmarkData"`
IllustType int `json:"illustType"`
Tags []string `json:"tags"` // used by core/popular_search
}
type Illust struct {
ID string `json:"id"`
Title string `json:"title"`
Description HTML `json:"description"`
UserID string `json:"userId"`
UserName string `json:"userName"`
UserAccount string `json:"userAccount"`
Date time.Time `json:"uploadDate"`
Images []Image
Tags []ArtworkTag `json:"tags"`
Pages int `json:"pageCount"`
Bookmarks int `json:"bookmarkCount"`
Likes int `json:"likeCount"`
Comments int `json:"commentCount"`
Views int `json:"viewCount"`
CommentDisabled int `json:"commentOff"`
SanityLevel int `json:"sl"`
XRestrict XRestrict `json:"xRestrict"`
AiType AiType `json:"aiType"`
BookmarkData any `json:"bookmarkData"`
Liked bool `json:"likeData"`
SeriesNavData struct {
SeriesType string `json:"seriesType"`
SeriesID string `json:"seriesId"`
Title string `json:"title"`
IsWatched bool `json:"isWatched"`
IsNotifying bool `json:"isNotifying"`
Order int `json:"order"`
Next struct {
Title string `json:"title"`
Order int `json:"order"`
ID string `json:"id"`
} `json:"next"`
Prev struct {
Title string `json:"title"`
Order int `json:"order"`
ID string `json:"id"`
} `json:"prev"`
} `json:"seriesNavData"`
User UserBrief
RecentWorks []ArtworkBrief
RelatedWorks []ArtworkBrief
CommentsList []Comment
IsUgoira bool
BookmarkID string
IllustType int `json:"illustType"`
}
func GetUserBasicInformation(auditor *audit.Auditor, r *http.Request, id string) (UserBrief, error) {
var user UserBrief
URL := GetUserInformationURL(id)
resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
if err != nil {
return user, err
}
resp, err = session.ProxyImageUrl(r, resp)
if err != nil {
return user, err
}
err = json.Unmarshal([]byte(resp), &user)
if err != nil {
return user, err
}
return user, nil
}
func GetArtworkImages(auditor *audit.Auditor, r *http.Request, id string, illustType int) ([]Image, error) {
var imageResp []ImageResponse
var images []Image
URL := GetArtworkImagesURL(id)
resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
if err != nil {
return nil, err
}
resp, err = session.ProxyImageUrl(r, resp)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(resp), &imageResp)
if err != nil {
return images, err
}
// Extract and proxy every images
for _, imageRaw := range imageResp {
var image Image
// this is the original art dimention, not the "regular" art dimension
// the image ratio of "regular" is close to Width/Height
// maybe not useful
image.Width = imageRaw.Width
image.Height = imageRaw.Height
image.Small = imageRaw.Urls["thumb_mini"]
image.Medium = imageRaw.Urls["small"]
image.Large = imageRaw.Urls["regular"]
image.Original = imageRaw.Urls["original"]
// Required for logic to display manga differently
image.IllustType = illustType
// Debug statement
// log.Printf("Artwork ID: %s, IllustType set to %d", id, image.IllustType)
images = append(images, image)
}
return images, nil
}
func GetRelatedArtworks(auditor *audit.Auditor, r *http.Request, id string) ([]ArtworkBrief, error) {
var body struct {
Illusts []ArtworkBrief `json:"illusts"`
}
// TODO: keep the hard-coded limit?
URL := GetArtworkRelatedURL(id, 180)
resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
if err != nil {
return nil, err
}
resp, err = session.ProxyImageUrl(r, resp)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(resp), &body)
if err != nil {
return nil, err
}
// Populate thumbnails for each artwork
for id, artwork := range body.Illusts {
if err := artwork.PopulateThumbnails(auditor); err != nil {
auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err)
return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err)
}
body.Illusts[id] = artwork
}
return body.Illusts, nil
}
// GetArtworkByID retrieves information about a specific artwork, with the option to include additional related data.
func GetArtworkByID(auditor *audit.Auditor, r *http.Request, id string, full bool) (*Illust, error) {
token := session.GetUserToken(r)
// Core artwork metadata fetched synchronously.
var illust struct {
Illust
UserIllusts map[int]any `json:"userIllusts"` // User's other artworks.
RawTags json.RawMessage `json:"tags"` // Untranslated tags.
}
// Fetch core artwork metadata.
urlArtInfo := GetArtworkInformationURL(id)
response, err := API_GET_UnwrapJson(r.Context(), auditor, urlArtInfo, token, r.Header)
if err != nil {
return nil, err
}
// Decode basic artwork data.
err = json.Unmarshal([]byte(response), &illust)
if err != nil {
return nil, err
}
// If the artwork is bookmarked, store its BookmarkID.
if illust.BookmarkData != nil {
t, ok := illust.BookmarkData.(map[string]any)
if ok && t["id"] != nil {
illust.BookmarkID = t["id"].(string)
}
}
// Use an errgroup to manage goroutines and errors.
var g errgroup.Group
// Fetch user basic information.
g.Go(func() error {
userInfo, err := GetUserBasicInformation(auditor, r, illust.UserID)
if err != nil {
return err
}
illust.User = userInfo
return nil
})
// Fetch artwork images.
g.Go(func() error {
images, err := GetArtworkImages(auditor, r, id, illust.IllustType)
if err != nil {
return err
}
illust.Images = images
return nil
})
// Fetch translated tags.
g.Go(func() error {
var tags struct {
Tags []struct {
Tag string `json:"tag"`
Translation map[string]string `json:"translation"`
} `json:"tags"`
}
err := json.Unmarshal(illust.RawTags, &tags)
if err != nil {
return err
}
// Store translated tags.
var tagsList []ArtworkTag
for _, tag := range tags.Tags {
newTag := ArtworkTag{
Name: tag.Tag,
TranslatedName: tag.Translation["en"],
}
tagsList = append(tagsList, newTag)
}
illust.Tags = tagsList
return nil
})
// Conditionally fetch related artworks if 'full' is true.
if full {
// Fetch related artworks.
g.Go(func() error {
related, err := GetRelatedArtworks(auditor, r, id)
if err != nil {
return err
}
illust.RelatedWorks = related
return nil
})
// Fetch recent works.
g.Go(func() error {
ids := make([]int, 0, len(illust.UserIllusts))
for k := range illust.UserIllusts {
ids = append(ids, k)
}
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
idsString := ""
count := min(len(ids), 20)
for i := 0; i < count; i++ {
idsString += fmt.Sprintf("&ids[]=%d", ids[i])
}
recent, _, err := populateArtworkIDs(auditor, r, illust.UserID, idsString)
if err != nil {
return err
}
sort.Slice(recent[:], func(i, j int) bool {
return numberGreaterThan(recent[i].ID, recent[j].ID)
})
illust.RecentWorks = recent
return nil
})
// Fetch comments if comments are enabled.
if illust.CommentDisabled != 1 {
g.Go(func() error {
comments, err := artworkCommentService.GetComments(auditor, r, id)
if err != nil {
return err
}
illust.CommentsList = comments
return nil
})
}
}
// Wait for all goroutines to finish.
if err := g.Wait(); err != nil {
return nil, err
}
// Check if the artwork is a ugoira.
illust.IsUgoira = strings.Contains(illust.Images[0].Original, "ugoira")
// Return the fully assembled 'illust' object.
return &illust.Illust, nil
}