Files
PixivFE/core/index.go
T
2024-10-17 17:16:54 +11:00

233 lines
6.6 KiB
Go

package core
import (
"fmt"
"net/http"
"github.com/goccy/go-json"
"github.com/tidwall/gjson"
"codeberg.org/vnpower/pixivfe/v2/i18n"
"codeberg.org/vnpower/pixivfe/v2/server/session"
)
// Pixivision represents a Pixivision article as returned by the landing page endpoint.
//
// Note that this type is less complete than the PixivisionArticle type.
type Pixivision struct {
ID string `json:"id"`
Title string `json:"title"`
Thumbnail string `json:"thumbnailUrl"`
URL string `json:"url"`
}
// RecommendedTags groups artworks under a specific tag recommendation.
type RecommendedTags struct {
Name string `json:"tag"`
Artworks []ArtworkBrief
}
// LandingArtworks aggregates various categories of artworks and other related data
// to be displayed on the landing page.
type LandingArtworks struct {
Commissions []ArtworkBrief
Following []ArtworkBrief
Recommended []ArtworkBrief
Newest []ArtworkBrief
Rankings Ranking
Users []ArtworkBrief
Pixivision []Pixivision
RecommendByTags []RecommendedTags
}
// landingToRankingMode maps landing page modes to their corresponding ranking modes.
var landingToRankingMode = map[string]string{
"all": "daily",
"r18": "daily_r18",
}
// GetLanding retrieves and organizes the landing page data based on the provided mode and user login status.
//
// It fetches raw data from the landing URL, parses the JSON response, and populates the LandingArtworks struct.
func GetLanding(r *http.Request, mode string, isLoggedIn bool) (*LandingArtworks, error) {
URL := GetLandingURL(mode)
resp, err := fetchLandingData(r, URL)
if err != nil {
return nil, err
}
artworks, err := parseArtworks(resp)
if err != nil {
return nil, err
}
pages, err := parsePages(resp)
if err != nil {
return nil, err
}
landing := LandingArtworks{
Pixivision: pages.Pixivision,
}
// If the user is logged in, populate personalized sections
if isLoggedIn {
landing.Following = populateFollowing(pages.Follow, artworks)
landing.RecommendByTags = populateRecommendedByTags(pages.RecommendedByTags, artworks)
landing.Recommended = populateRecommended(pages.Recommended.IDs, artworks)
}
landing.Rankings, err = fetchRankings(r, mode)
if err != nil {
return nil, err
}
return &landing, nil
}
// fetchLandingData retrieves the landing data from the specified URL and validates its JSON format.
//
// Returns the raw JSON response as a string if valid.
func fetchLandingData(r *http.Request, URL string) (string, error) {
resp, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header)
if err != nil {
return "", err
}
resp = session.ProxyImageUrl(r, resp)
if !gjson.Valid(resp) {
return "", i18n.Errorf("Invalid JSON: %v", resp)
}
return resp, nil
}
// parseArtworks extracts artwork information from the "thumbnails.illust" field
// of the JSON response and maps them by their ID.
//
// This allows for quick lookup of artworks when populating various sections.
func parseArtworks(resp string) (map[string]ArtworkBrief, error) {
artworks := make(map[string]ArtworkBrief)
stuff := gjson.Get(resp, "thumbnails.illust")
// Iterate over each thumbnail entry and unmarshal it into the ArtworkBrief struct
stuff.ForEach(func(key, value gjson.Result) bool {
var artwork ArtworkBrief
if err := json.Unmarshal([]byte(value.String()), &artwork); err != nil {
// Invalid artwork entries are simply skipped
return false
}
// Map the artwork by its ID for easy access later
if artwork.ID != "" {
artworks[artwork.ID] = artwork
}
return true // Continue iteration
})
return artworks, nil
}
// parsePages unmarshals the "page" field from the JSON response into a structured format.
func parsePages(resp string) (*struct {
Pixivision []Pixivision `json:"pixivision"`
Follow []int `json:"follow"`
Recommended struct {
IDs []string `json:"ids"`
} `json:"recommend"`
RecommendedByTags []struct {
Name string `json:"tag"`
IDs []string `json:"ids"`
} `json:"recommendByTag"`
}, error,
) {
var pages struct {
Pixivision []Pixivision `json:"pixivision"`
Follow []int `json:"follow"`
Recommended struct {
IDs []string `json:"ids"`
} `json:"recommend"`
// Commented out fields that aren't currently implemented in the frontend
// EditorRecommended []any `json:"editorRecommend"`
// UserRecommended []any `json:"recommendUser"`
// Commission []any `json:"completeRequestIds"`
RecommendedByTags []struct {
Name string `json:"tag"`
IDs []string `json:"ids"`
} `json:"recommendByTag"`
}
pagesStr := gjson.Get(resp, "page").String()
if err := json.Unmarshal([]byte(pagesStr), &pages); err != nil {
return nil, err
}
return &pages, nil
}
// populateArtworks is a generic helper function that maps a slice of string IDs
// to their corresponding ArtworkBrief objects.
func populateArtworks(ids []string, artworks map[string]ArtworkBrief) []ArtworkBrief {
populated := make([]ArtworkBrief, 0, len(ids))
for _, id := range ids {
if artwork, exists := artworks[id]; exists {
populated = append(populated, artwork)
}
}
return populated
}
// populateFollowing converts int IDs to strings and uses the generic helper.
func populateFollowing(followIDs []int, artworks map[string]ArtworkBrief) []ArtworkBrief {
stringIDs := make([]string, 0, len(followIDs))
for _, id := range followIDs {
stringIDs = append(stringIDs, fmt.Sprint(id))
}
return populateArtworks(stringIDs, artworks)
}
// populateRecommended uses the generic helper.
func populateRecommended(recommendedIDs []string, artworks map[string]ArtworkBrief) []ArtworkBrief {
return populateArtworks(recommendedIDs, artworks)
}
// populateRecommendedByTags uses the generic helper for each tag's IDs.
func populateRecommendedByTags(recommends []struct {
Name string `json:"tag"`
IDs []string `json:"ids"`
}, artworks map[string]ArtworkBrief,
) []RecommendedTags {
recommendByTags := make([]RecommendedTags, 0, len(recommends))
for _, r := range recommends {
artworksList := populateArtworks(r.IDs, artworks)
recommendByTags = append(recommendByTags, RecommendedTags{
Name: r.Name,
Artworks: artworksList,
})
}
return recommendByTags
}
// fetchRankings retrieves the current rankings based on the selected mode.
//
// It maps the landing page mode to the appropriate ranking mode and fetches the ranking data.
func fetchRankings(r *http.Request, mode string) (Ranking, error) {
rankingMode, exists := landingToRankingMode[mode]
if !exists {
rankingMode = "daily"
}
rankings, err := GetRanking(r, rankingMode, "illust", "", "1")
if err != nil {
return Ranking{}, err
}
return rankings, nil
}