Files
PixivFE/core/artwork.go
T
2024-10-20 01:11:45 +11:00

480 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"errors"
"fmt"
"log"
"net/http"
"sort"
"strings"
"sync"
"time"
"github.com/goccy/go-json"
"codeberg.org/vnpower/pixivfe/v2/i18n"
"codeberg.org/vnpower/pixivfe/v2/server/session"
)
// 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 {
switch x {
case Safe:
return i18n.Tr("Safe")
case R18:
return i18n.Tr("R18")
case R18G:
return i18n.Tr("R18G")
}
log.Panicf("invalid value: %#v", int(x))
return ""
}
// 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 {
switch x {
case Unrated:
return i18n.Tr("Unrated")
case NotAI:
return i18n.Tr("Not AI")
case AI:
return i18n.Tr("AI")
}
log.Panicf("invalid value: %#v", int(x))
return ""
}
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 Tag 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"`
Pages int `json:"pageCount"`
XRestrict int `json:"xRestrict"`
AiType int `json:"aiType"`
Bookmarked any `json:"bookmarkData"`
IllustType int `json:"illustType"`
}
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 []Tag `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(r *http.Request, id string) (UserBrief, error) {
var user UserBrief
URL := GetUserInformationURL(id)
response, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
if err != nil {
return user, err
}
response = session.ProxyImageUrl(r, response)
err = json.Unmarshal([]byte(response), &user)
if err != nil {
return user, err
}
return user, nil
}
func GetArtworkImages(r *http.Request, id string, illustType int) ([]Image, error) {
var resp []ImageResponse
var images []Image
URL := GetArtworkImagesURL(id)
response, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
if err != nil {
return nil, err
}
response = session.ProxyImageUrl(r, response)
err = json.Unmarshal([]byte(response), &resp)
if err != nil {
return images, err
}
// Extract and proxy every images
for _, imageRaw := range resp {
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 GetArtworkComments(r *http.Request, id string) ([]Comment, error) {
var body struct {
Comments []Comment `json:"comments"`
}
URL := GetArtworkCommentsURL(id)
response, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
if err != nil {
return nil, err
}
response = session.ProxyImageUrl(r, response)
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
return body.Comments, nil
}
func GetRelatedArtworks(r *http.Request, id string) ([]ArtworkBrief, error) {
var body struct {
Illusts []ArtworkBrief `json:"illusts"`
}
// TODO: keep the hard-coded limit?
URL := GetArtworkRelatedURL(id, 180)
response, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
if err != nil {
return nil, err
}
response = session.ProxyImageUrl(r, response)
err = json.Unmarshal([]byte(response), &body)
if err != nil {
return nil, err
}
return body.Illusts, nil
}
// GetArtworkByID retrieves information about a specific artwork, with the option to include additional related data.
// External API calls are made to gather metadata, images, related works, user info, and comments.
func GetArtworkByID(r *http.Request, id string, full bool) (*Illust, error) {
token := session.GetUserToken(r)
// Core artwork metadata fetched from Pixiv.
var illust struct {
Illust
UserIllusts map[int]any `json:"userIllusts"` // Allows fetching user's other artworks.
RawTags json.RawMessage `json:"tags"` // Retain untranslated tags for later processing.
}
// Auxiliary data stores that are fetched asynchronously.
var illustAuxilary struct {
Images []Image // To store the artwork's images in various sizes.
RelatedWorks []ArtworkBrief // Place to gather related artworks, if requested.
CommentsList []Comment // Where fetched comments will be stored, if requested.
}
// We use a WaitGroup to synchronize multiple concurrent API requests to different endpoints.
var wg sync.WaitGroup
cerr := make(chan error, 7) // Channel to gather errors from multiple API calls.
// Fetch core artwork metadata.
wg.Add(1)
go func() {
defer wg.Done()
// Fetch core artwork metadata as the starting point for assembling all related information.
urlArtInfo := GetArtworkInformationURL(id)
response, err := API_GET_UnwrapJson(r.Context(), urlArtInfo, token, r.Header)
if err != nil {
cerr <- err
return
}
// Decode basic artwork data. This forms the foundation for all subsequent requests.
err = json.Unmarshal([]byte(response), &illust)
if err != nil {
cerr <- err
return
}
// If the artwork is bookmarked, store its BookmarkID for user-specific interactions.
if illust.BookmarkData != nil {
t := illust.BookmarkData.(map[string]any)
illust.BookmarkID = t["id"].(string)
}
// Fetch user details like avatar and name (necessary because the user object isnt always fully populated in the artwork data).
wg.Add(1)
go func() {
defer wg.Done()
// This request ensures that we have the user's avatar and other basic details, which might not always be present in the artwork info.
userInfo, err := GetUserBasicInformation(r, illust.UserID)
if err != nil {
cerr <- err
return
}
illust.User = userInfo
}()
// Fetch available image sizes (e.g. small, medium, large, original).
// Art types vary (e.g. manga, illustrations), so this request happens after we obtain artwork metadata.
wg.Add(1)
go func() {
defer wg.Done()
// Fetch image variants using the artwork ID and its IllustType.
images, err := GetArtworkImages(r, id, illust.IllustType)
if err != nil {
cerr <- err
return
}
illustAuxilary.Images = images
}()
// If full details are requested, fetch related artworks.
if full {
wg.Add(1)
go func() {
defer wg.Done()
// Fetch related artworks to help users explore similar content.
related, err := GetRelatedArtworks(r, id)
if err != nil {
cerr <- err
return
}
illustAuxilary.RelatedWorks = related
}()
}
// The tags are stored untranslated; fetch translated tags in parallel with other metadata.
wg.Add(1)
go func() {
defer wg.Done()
// Unmarshal the raw tag data and translate them so users can visualize the tags in their preferred language.
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 {
cerr <- err
return
}
// Store translated tags and preserve the original language form for international users.
var tagsList []Tag
for _, tag := range tags.Tags {
var newTag Tag
newTag.Name = tag.Tag
newTag.TranslatedName = tag.Translation["en"]
tagsList = append(tagsList, newTag)
}
illust.Tags = tagsList
}()
// If full details are requested, fetch the user's recent works.
if full {
wg.Add(1)
go func() {
defer wg.Done()
// Fetch the list of the user's recent works by ID. Collect a limited number of artwork metadata (e.g., recent 20).
ids := make([]int, 0)
for k := range illust.UserIllusts {
ids = append(ids, k)
}
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
// Prepare the artwork ID string to request metadata for recent works.
idsString := ""
count := min(len(ids), 20)
for i := 0; i < count; i++ {
idsString += fmt.Sprintf("&ids[]=%d", ids[i])
}
// Fetch the user's recent artworks for display.
recent, err := fetchArtworkIDs(r, illust.UserID, idsString)
if err != nil {
cerr <- err
return
}
sort.Slice(recent[:], func(i, j int) bool {
left := recent[i].ID
right := recent[j].ID
return numberGreaterThan(left, right)
})
illust.RecentWorks = recent
}()
}
// We only fetch comments if full details are requested and comments are enabled (since disabled comments would lead to unnecessary API calls).
// This condition is evaluated *after* fetching basic artwork information since we first need the comment-off flag.
if full && illust.CommentDisabled != 1 {
wg.Add(1)
go func() {
defer wg.Done()
// Retrieve a list of comments for this artwork, if comments are not disabled.
comments, err := GetArtworkComments(r, id)
if err != nil {
cerr <- err
return
}
illustAuxilary.CommentsList = comments
}()
}
}()
// Wait until all the asynchronous requests complete their operations.
wg.Wait()
close(cerr)
// Integrate the auxiliary data (images, related works, comments) back into the core `illust` structure.
illust.Images = illustAuxilary.Images
illust.RelatedWorks = illustAuxilary.RelatedWorks
illust.CommentsList = illustAuxilary.CommentsList
// Handle all dispatched errors. If one or more requests failed, aggregate them into one summary error.
all_errors := []error{}
for suberr := range cerr {
all_errors = append(all_errors, suberr)
}
err_summary := errors.Join(all_errors...)
if err_summary != nil {
return nil, err_summary
}
// Pixiv has a specific art format called ugoira (animated illustrations); if detected, flag this artwork as such for further handling by the Jet template engine.
illust.IsUgoira = strings.Contains(illust.Images[0].Original, "ugoira")
// Return the fully assembled `illust` object, complete with metadata, images, related works, tags, and comments (if requested in full mode).
return &illust.Illust, nil
}