mirror of
https://codeberg.org/VnPower/PixivFE
synced 2024-12-06 19:16:23 +01:00
480 lines
13 KiB
Go
480 lines
13 KiB
Go
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 isn’t 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
|
||
}
|