mirror of
https://codeberg.org/VnPower/PixivFE
synced 2024-12-06 19:16:23 +01:00
e76f049cff
grabbing everything in GetUserArtworksIDAndSeries in one go isn't that easy due to how idsString is constructed nightmare function
538 lines
13 KiB
Go
538 lines
13 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"codeberg.org/vnpower/pixivfe/v2/i18n"
|
|
"codeberg.org/vnpower/pixivfe/v2/server/session"
|
|
"github.com/goccy/go-json"
|
|
)
|
|
|
|
// pixivfe internal data type. not used by pixiv.
|
|
type UserArtCategory string
|
|
|
|
const (
|
|
UserArt_Any UserArtCategory = ""
|
|
UserArt_AnyAlt UserArtCategory = "artworks"
|
|
UserArt_Illustration UserArtCategory = "illustrations"
|
|
UserArt_Manga UserArtCategory = "manga"
|
|
UserArt_Bookmarks UserArtCategory = "bookmarks" // what this user has bookmarked; not art by this user
|
|
UserArt_Novel UserArtCategory = "novels"
|
|
)
|
|
|
|
func (s UserArtCategory) Validate() error {
|
|
if s != UserArt_Any &&
|
|
s != UserArt_AnyAlt &&
|
|
s != UserArt_Illustration &&
|
|
s != UserArt_Manga &&
|
|
s != UserArt_Bookmarks &&
|
|
s != UserArt_Novel {
|
|
return i18n.Errorf(`Invalid work category: %#v. Only "%s", "%s", "%s", "%s", "%s" and "%s" are available`, s, UserArt_Any, UserArt_AnyAlt, UserArt_Illustration, UserArt_Manga, UserArt_Bookmarks, UserArt_Novel)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type FrequentTag struct {
|
|
Name string `json:"tag"`
|
|
TranslatedName string `json:"tag_translation"`
|
|
}
|
|
|
|
type CountInfo struct {
|
|
All int
|
|
Illustrations int
|
|
Manga int
|
|
Novels int
|
|
Bookmarks int
|
|
}
|
|
|
|
type User struct {
|
|
ID string `json:"userId"`
|
|
Name string `json:"name"`
|
|
Avatar string `json:"imageBig"`
|
|
Following int `json:"following"`
|
|
MyPixiv int `json:"mypixivCount"`
|
|
Comment HTML `json:"commentHtml"`
|
|
Webpage string `json:"webpage"`
|
|
SocialRaw json.RawMessage `json:"social"`
|
|
Artworks []ArtworkBrief `json:"artworks"` // this slice includes both illustrations and manga, but not novels
|
|
Illustrations []ArtworkBrief
|
|
Manga []ArtworkBrief
|
|
Novels []NovelBrief `json:"novels"`
|
|
Background map[string]any `json:"background"`
|
|
CategoryItemCount int
|
|
FrequentTags []FrequentTag
|
|
Social map[string]map[string]string
|
|
BackgroundImage string
|
|
NovelSeries []NovelSeries
|
|
MangaSeries []MangaSeries
|
|
|
|
IsFollowed bool `json:"isFollowed"` // Denotes whether the logged in user currently following the given user
|
|
|
|
// The following fields are internal to PixivFE, used to display the number of works for a given category
|
|
CountInfo CountInfo
|
|
}
|
|
|
|
// Utility function to compute slice bounds safely
|
|
func computeSliceBounds(page int, worksPerPage float64, totalItems int) (start, end int, err error) {
|
|
if totalItems == 0 {
|
|
return 0, 0, nil
|
|
}
|
|
|
|
maxPages := int(math.Ceil(float64(totalItems) / worksPerPage))
|
|
if page < 1 || page > maxPages {
|
|
return 0, 0, i18n.Error("Invalid page number.")
|
|
}
|
|
|
|
start = (page - 1) * int(worksPerPage)
|
|
end = min(start+int(worksPerPage), totalItems)
|
|
|
|
return start, end, nil
|
|
}
|
|
|
|
func (s *User) ParseSocial() error {
|
|
if string(s.SocialRaw[:]) == "[]" {
|
|
// Fuck Pixiv
|
|
return nil
|
|
}
|
|
|
|
err := json.Unmarshal(s.SocialRaw, &s.Social)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetFrequentTags(r *http.Request, ids string, category UserArtCategory) ([]FrequentTag, error) {
|
|
var tags []FrequentTag
|
|
var URL string
|
|
|
|
if category != "novels" {
|
|
URL = GetFrequentArtworkTagsURL(ids)
|
|
} else {
|
|
URL = GetFrequentNovelTagsURL(ids)
|
|
}
|
|
|
|
response, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(response), &tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
func GetUserArtworkList(r *http.Request, id, ids string) ([]ArtworkBrief, error) {
|
|
var works []ArtworkBrief
|
|
var illustrations []ArtworkBrief
|
|
var manga []ArtworkBrief
|
|
|
|
URL := GetUserFullArtworkURL(id, ids)
|
|
|
|
resp, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp = session.ProxyImageUrl(r, resp)
|
|
|
|
var body struct {
|
|
Illusts map[int]json.RawMessage `json:"works"`
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(resp), &body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, v := range body.Illusts {
|
|
var illust ArtworkBrief
|
|
err = json.Unmarshal(v, &illust)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
works = append(works, illust)
|
|
|
|
if illust.IllustType == 0 {
|
|
illustrations = append(illustrations, illust)
|
|
} else if illust.IllustType == 1 {
|
|
manga = append(manga, illust)
|
|
}
|
|
}
|
|
|
|
return works, nil
|
|
}
|
|
|
|
func GetUserNovels(r *http.Request, id, ids string) ([]NovelBrief, error) {
|
|
// VnPower: we can merge this function into GetUserArtworks (now GetUserArtworkList), but I want to make things simple for now
|
|
var works []NovelBrief
|
|
|
|
URL := GetUserFullNovelURL(id, ids)
|
|
|
|
resp, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp = session.ProxyImageUrl(r, resp)
|
|
|
|
var body struct {
|
|
Novels map[int]json.RawMessage `json:"works"`
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(resp), &body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, v := range body.Novels {
|
|
var novel NovelBrief
|
|
err = json.Unmarshal(v, &novel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
works = append(works, novel)
|
|
}
|
|
|
|
return works, nil
|
|
}
|
|
|
|
// IntStringMap is a custom type that can handle both JSON objects and empty arrays.
|
|
//
|
|
// Required as the Pixiv API returns empty arrays for work types that
|
|
// don't exist for a user.
|
|
type IntStringMap map[int]string
|
|
|
|
// UnmarshalJSON implements custom unmarshalling for IntStringMap.
|
|
func (m *IntStringMap) UnmarshalJSON(data []byte) error {
|
|
// Check if the data is an empty array.
|
|
if string(data) == "[]" {
|
|
*m = make(map[int]string)
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, expect an object.
|
|
var temp map[string]*string
|
|
if err := json.Unmarshal(data, &temp); err != nil {
|
|
return err
|
|
}
|
|
|
|
result := make(map[int]string)
|
|
for keyStr, value := range temp {
|
|
key, err := strconv.Atoi(keyStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid key '%s': %v", keyStr, err)
|
|
}
|
|
if value != nil {
|
|
result[key] = *value
|
|
} else {
|
|
result[key] = ""
|
|
}
|
|
}
|
|
|
|
*m = result
|
|
return nil
|
|
}
|
|
|
|
// GetUserArtworksIDAndSeries retrieves artwork IDs and series information for a user.
|
|
func GetUserArtworksIDAndSeries(r *http.Request, id string, category UserArtCategory, page int) (idsString string, count int, illustCount int, mangaCount int, novelCount int, series json.RawMessage, err error) {
|
|
URL := GetUserArtworksURL(id)
|
|
|
|
resp, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
|
|
if err != nil {
|
|
return "", -1, -1, -1, -1, nil, err
|
|
}
|
|
|
|
resp = session.ProxyImageUrl(r, resp)
|
|
|
|
var body struct {
|
|
// *json.RawMessage allows us to easily check whether
|
|
// a field exists by verifying if pointer != nil
|
|
Illusts *json.RawMessage `json:"illusts"`
|
|
Manga *json.RawMessage `json:"manga"`
|
|
MangaSeries *json.RawMessage `json:"mangaSeries"`
|
|
Novels *json.RawMessage `json:"novels"`
|
|
NovelSeries *json.RawMessage `json:"novelSeries"`
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(resp), &body)
|
|
if err != nil {
|
|
return "", -1, -1, -1, -1, nil, fmt.Errorf("failed to unmarshal response body: %w", err)
|
|
}
|
|
|
|
var ids []int
|
|
|
|
var illusts IntStringMap
|
|
var manga IntStringMap
|
|
var novels IntStringMap
|
|
|
|
if category == UserArt_Illustration || category == UserArt_Any || category == UserArt_AnyAlt {
|
|
if body.Illusts != nil && len(*body.Illusts) > 0 {
|
|
if err = json.Unmarshal(*body.Illusts, &illusts); err != nil {
|
|
fmt.Printf("Error unmarshalling illusts: %v\n", err)
|
|
illusts = make(IntStringMap)
|
|
}
|
|
for k := range illusts {
|
|
ids = append(ids, k)
|
|
count++
|
|
}
|
|
illustCount = len(illusts)
|
|
}
|
|
}
|
|
|
|
if category == UserArt_Manga || category == UserArt_Any {
|
|
if body.Manga != nil && len(*body.Manga) > 0 {
|
|
if err = json.Unmarshal(*body.Manga, &manga); err != nil {
|
|
fmt.Printf("Error unmarshalling manga: %v\n", err)
|
|
manga = make(IntStringMap)
|
|
}
|
|
for k := range manga {
|
|
ids = append(ids, k)
|
|
count++
|
|
}
|
|
mangaCount = len(manga)
|
|
if body.MangaSeries != nil {
|
|
series = *body.MangaSeries
|
|
}
|
|
}
|
|
}
|
|
|
|
if category == UserArt_Novel {
|
|
if body.Novels != nil && len(*body.Novels) > 0 {
|
|
if err = json.Unmarshal(*body.Novels, &novels); err != nil {
|
|
fmt.Printf("Error unmarshalling novels: %v\n", err)
|
|
novels = make(IntStringMap)
|
|
}
|
|
for k := range novels {
|
|
ids = append(ids, k)
|
|
count++
|
|
}
|
|
novelCount = len(novels)
|
|
if body.NovelSeries != nil {
|
|
series = *body.NovelSeries
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
|
|
|
|
worksPerPage := 30.0
|
|
|
|
start, end, err := computeSliceBounds(page, worksPerPage, len(ids))
|
|
if err != nil {
|
|
return "", -1, -1, -1, -1, nil, err
|
|
}
|
|
|
|
// Build the IDs string for the API request
|
|
for _, k := range ids[start:end] {
|
|
idsString += fmt.Sprintf("&ids[]=%d", k)
|
|
}
|
|
|
|
return idsString, count, illustCount, mangaCount, novelCount, series, nil
|
|
}
|
|
|
|
func fetchAndProcessArtworks(r *http.Request, id, ids string, category UserArtCategory) ([]ArtworkBrief, []NovelBrief, error) {
|
|
var artworks []ArtworkBrief
|
|
var novels []NovelBrief
|
|
var err error
|
|
|
|
if category == UserArt_Novel {
|
|
novels, err = GetUserNovels(r, id, ids)
|
|
} else {
|
|
artworks, err = GetUserArtworkList(r, id, ids)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Sort the works
|
|
if category == UserArt_Novel {
|
|
sort.Slice(novels[:], func(i, j int) bool {
|
|
return numberGreaterThan(novels[i].ID, novels[j].ID)
|
|
})
|
|
} else {
|
|
sort.Slice(artworks[:], func(i, j int) bool {
|
|
return numberGreaterThan(artworks[i].ID, artworks[j].ID)
|
|
})
|
|
}
|
|
|
|
return artworks, novels, nil
|
|
}
|
|
|
|
func handleSeriesData(series json.RawMessage, category UserArtCategory) ([]NovelSeries, []MangaSeries) {
|
|
var novelSeries []NovelSeries
|
|
var mangaSeries []MangaSeries
|
|
|
|
if series != nil {
|
|
if category == UserArt_Novel {
|
|
_ = json.Unmarshal(series, &novelSeries)
|
|
} else {
|
|
_ = json.Unmarshal(series, &mangaSeries)
|
|
}
|
|
}
|
|
|
|
return novelSeries, mangaSeries
|
|
}
|
|
|
|
func GetUserProfile(r *http.Request, id string, category UserArtCategory, page int, getTags bool) (User, error) {
|
|
var user User
|
|
|
|
token := session.GetUserToken(r)
|
|
|
|
URL := GetUserInformationURL(id)
|
|
|
|
resp, err := API_GET_UnwrapJson(r.Context(), URL, token, r.Header)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
|
|
resp = session.ProxyImageUrl(r, resp)
|
|
|
|
err = json.Unmarshal([]byte(resp), &user)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
|
|
// Get counts for illustrations and manga
|
|
_, allCount, illustCount, mangaCount, _, _, err := GetUserArtworksIDAndSeries(r, id, UserArt_Any, 1)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
user.CountInfo.All = allCount
|
|
user.CountInfo.Illustrations = illustCount
|
|
user.CountInfo.Manga = mangaCount
|
|
|
|
// Get count for novels separately
|
|
_, _, _, _, novelCount, _, err := GetUserArtworksIDAndSeries(r, id, UserArt_Novel, 1)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
user.CountInfo.Novels = novelCount
|
|
|
|
// Get bookmarks count
|
|
_, bookmarksCount, err := GetUserBookmarks(r, id, "show", 1)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
user.CountInfo.Bookmarks = bookmarksCount
|
|
|
|
if category == UserArt_Bookmarks {
|
|
// Bookmarks
|
|
works, _, err := GetUserBookmarks(r, id, "show", page)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
|
|
user.Artworks = works
|
|
user.CategoryItemCount = bookmarksCount
|
|
} else {
|
|
ids, count, _, _, _, series, err := GetUserArtworksIDAndSeries(r, id, category, page)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
|
|
if count > 0 {
|
|
artworks, novels, err := fetchAndProcessArtworks(r, id, ids, category)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
|
|
if category == UserArt_Novel {
|
|
user.Novels = novels
|
|
} else {
|
|
user.Artworks = artworks
|
|
user.Illustrations = make([]ArtworkBrief, 0)
|
|
user.Manga = make([]ArtworkBrief, 0)
|
|
for _, artwork := range artworks {
|
|
if artwork.IllustType == 0 {
|
|
user.Illustrations = append(user.Illustrations, artwork)
|
|
} else if artwork.IllustType == 1 {
|
|
user.Manga = append(user.Manga, artwork)
|
|
}
|
|
}
|
|
}
|
|
|
|
if getTags {
|
|
user.FrequentTags, err = GetFrequentTags(r, ids, category)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
}
|
|
}
|
|
|
|
user.NovelSeries, user.MangaSeries = handleSeriesData(series, category)
|
|
user.CategoryItemCount = count
|
|
}
|
|
|
|
err = user.ParseSocial()
|
|
if err != nil {
|
|
return User{}, err
|
|
}
|
|
|
|
if user.Background != nil {
|
|
user.BackgroundImage = user.Background["url"].(string)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func GetUserBookmarks(r *http.Request, id, mode string, page int) ([]ArtworkBrief, int, error) {
|
|
page--
|
|
|
|
URL := GetUserBookmarksURL(id, mode, page)
|
|
|
|
resp, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
|
|
if err != nil {
|
|
return nil, -1, err
|
|
}
|
|
resp = session.ProxyImageUrl(r, resp)
|
|
|
|
var body struct {
|
|
Artworks []json.RawMessage `json:"works"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(resp), &body)
|
|
if err != nil {
|
|
return nil, -1, err
|
|
}
|
|
|
|
artworks := make([]ArtworkBrief, len(body.Artworks))
|
|
|
|
for index, value := range body.Artworks {
|
|
var artwork ArtworkBrief
|
|
|
|
err = json.Unmarshal([]byte(value), &artwork)
|
|
if err != nil {
|
|
artworks[index] = ArtworkBrief{
|
|
ID: "#",
|
|
Title: "Deleted or Private",
|
|
Thumbnail: "https://s.pximg.net/common/images/limit_unknown_360.png",
|
|
}
|
|
continue
|
|
}
|
|
artworks[index] = artwork
|
|
}
|
|
|
|
return artworks, body.Total, nil
|
|
}
|
|
|
|
func numberGreaterThan(l, r string) bool {
|
|
if len(l) > len(r) {
|
|
return true
|
|
}
|
|
if len(l) < len(r) {
|
|
return false
|
|
}
|
|
return l > r
|
|
}
|