core/user: use goroutines to avoid blocking on network requests

This commit is contained in:
perennial
2024-11-09 01:52:07 +11:00
parent 91a5b0f28b
commit 3a6da13535
+135 -103
View File
@@ -13,6 +13,7 @@ import (
"codeberg.org/vnpower/pixivfe/v2/server/session"
"github.com/goccy/go-json"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
// UserWorkCategory represents a unified structure for handling different work categories.
@@ -143,8 +144,9 @@ type User struct {
}
// GetUserProfile retrieves the user profile, including counts, artworks/bookmarks, and social data.
//
// Goroutines are used to avoid blocking on network requests.
func GetUserProfile(auditor *audit.Auditor, r *http.Request, id string, currentCategory *UserWorkCategory, page int, getTags bool, mode string) (User, error) {
var user User
auditor.Logger.Debug("GetUserProfile called",
zap.String("userID", id),
zap.String("currentCategory", currentCategory.Value),
@@ -152,67 +154,82 @@ func GetUserProfile(auditor *audit.Auditor, r *http.Request, id string, currentC
zap.Bool("getTags", getTags),
)
// Fetch user information
userInfoURL := GetUserInformationURL(id)
auditor.Logger.Debug("Fetching user information", zap.String("URL", userInfoURL))
token := session.GetUserToken(r)
resp, err := API_GET_UnwrapJson(r.Context(), auditor, userInfoURL, token, r.Header)
if err != nil {
auditor.SugaredLogger.Error("Failed to fetch user information", zap.Error(err))
return user, err
}
var userInfo User
var categories map[string]*UserWorkCategory
var g errgroup.Group
resp, err = session.ProxyImageUrl(r, resp)
if err != nil {
auditor.SugaredLogger.Error("Failed to proxy image URL", zap.Error(err))
return user, err
}
// Goroutine to fetch user information
g.Go(func() error {
// Fetch user information
userInfoURL := GetUserInformationURL(id)
auditor.Logger.Debug("Fetching user information", zap.String("URL", userInfoURL))
token := session.GetUserToken(r)
resp, err := API_GET_UnwrapJson(r.Context(), auditor, userInfoURL, token, r.Header)
if err != nil {
auditor.SugaredLogger.Error("Failed to fetch user information", zap.Error(err))
return err
}
if err := json.Unmarshal([]byte(resp), &user); err != nil {
auditor.SugaredLogger.Error("Failed to unmarshal user information", zap.Error(err))
return user, err
}
resp, err = session.ProxyImageUrl(r, resp)
if err != nil {
auditor.SugaredLogger.Error("Failed to proxy image URL", zap.Error(err))
return err
}
// Initialize the user categories
user.Categories = map[string]*UserWorkCategory{
"illustrations": {Value: "illustrations"},
"manga": {Value: "manga"},
"novels": {Value: "novels"},
"bookmarks": {Value: "bookmarks"},
}
if err := json.Unmarshal([]byte(resp), &userInfo); err != nil {
auditor.SugaredLogger.Error("Failed to unmarshal user information", zap.Error(err))
return err
}
// Get populated works or bookmarks based on the currentCategory and mode
auditor.Logger.Debug("Getting populated works")
if err := getPopulatedWorks(auditor, r, &user, id, currentCategory, page, getTags, mode); err != nil {
auditor.SugaredLogger.Error("Failed to get populated works", zap.Error(err))
return user, err
}
// Parse social data
auditor.Logger.Debug("Parsing social data")
if err := userInfo.parseSocial(); err != nil {
auditor.SugaredLogger.Error("Failed to parse social data", zap.Error(err))
return err
}
auditor.Logger.Debug("Social data parsed", zap.Any("social", userInfo.Social))
// Parse social data
auditor.Logger.Debug("Parsing social data")
if err := user.parseSocial(); err != nil {
auditor.SugaredLogger.Error("Failed to parse social data", zap.Error(err))
// Set background image if available
if userInfo.Background != nil {
if url, ok := userInfo.Background["url"].(string); ok {
userInfo.BackgroundImage = url
auditor.Logger.Debug("Background image set", zap.String("BackgroundImage", url))
} else {
auditor.SugaredLogger.Warn("Background URL not found or invalid")
}
}
return nil
})
// Goroutine to fetch works and populate categories
g.Go(func() error {
var err error
auditor.Logger.Debug("Getting populated works")
categories, err = getPopulatedWorks(auditor, r, id, currentCategory, page, getTags, mode)
if err != nil {
auditor.SugaredLogger.Error("Failed to get populated works", zap.Error(err))
return err
}
return nil
})
// Wait for both goroutines to finish
if err := g.Wait(); err != nil {
return User{}, err
}
auditor.Logger.Debug("Social data parsed", zap.Any("social", user.Social))
// Set background image if available
if user.Background != nil {
if url, ok := user.Background["url"].(string); ok {
user.BackgroundImage = url
auditor.Logger.Debug("Background image set", zap.String("BackgroundImage", url))
} else {
auditor.SugaredLogger.Warn("Background URL not found or invalid")
}
}
// Merge userInfo and categories into the final user struct
user := userInfo
user.Categories = categories
return user, nil
}
// getPopulatedWorks fetches then populates information for a user's works or bookmarks, based on the currentCategory.
// getPopulatedWorks fetches then populates information for a user's works, as well as handling their
// frequently used tags and series data.
//
// It also handles a user's frequently used tags and series data.
func getPopulatedWorks(auditor *audit.Auditor, r *http.Request, user *User, id string, currentCategory *UserWorkCategory, page int, getTags bool, mode string) error {
// Goroutines are used to avoid blocking on network requests.
func getPopulatedWorks(auditor *audit.Auditor, r *http.Request, id string, currentCategory *UserWorkCategory, page int, getTags bool, mode string) (map[string]*UserWorkCategory, error) {
auditor.Logger.Debug("getPopulatedWorks called",
zap.String("userID", id),
zap.String("currentCategory", currentCategory.Value),
@@ -225,80 +242,95 @@ func getPopulatedWorks(auditor *audit.Auditor, r *http.Request, user *User, id s
categoriesData, err := fetchWorkIDsAndSeriesData(auditor, r, id, currentCategory, page)
if err != nil {
auditor.SugaredLogger.Error("Failed to fetch work IDs and series data", zap.Error(err))
return err
return nil, err
}
// Update user.Categories with the fetched IDs and series data
for name, catData := range categoriesData {
user.Categories[name] = catData
}
// Create an errgroup
var g errgroup.Group
// Fetch and populate works for each category
for _, cat := range user.Categories {
// Fetch and populate works for each category in parallel
for _, cat := range categoriesData {
// Capture cat in local variable
cat := cat
// Skip categories with no work IDs (except bookmarks)
if cat.WorkIDs == "" && cat.Value != "bookmarks" {
continue
}
switch cat.Value {
case "illustrations", "manga":
auditor.Logger.Debug("Fetching and populating ArtworkBriefs", zap.String("category", cat.Value))
artworks, safeArtworks, err := populateArtworkIDs(auditor, r, id, cat.WorkIDs)
if err != nil {
auditor.SugaredLogger.Error("Failed to populate artwork IDs", zap.Error(err))
return err
}
if mode == "all" {
cat.IllustWorks = artworks
} else if mode == "safe" {
cat.IllustWorks = safeArtworks
g.Go(func() error {
switch cat.Value {
case "illustrations", "manga":
auditor.Logger.Debug("Fetching and populating ArtworkBriefs", zap.String("category", cat.Value))
artworks, safeArtworks, err := populateArtworkIDs(auditor, r, id, cat.WorkIDs)
if err != nil {
auditor.SugaredLogger.Error("Failed to populate artwork IDs", zap.Error(err))
return err
}
if mode == "all" {
cat.IllustWorks = artworks
} else if mode == "safe" {
cat.IllustWorks = safeArtworks
}
case "novels":
auditor.Logger.Debug("Fetching and populating NovelBriefs", zap.String("category", cat.Value))
novels, safeNovels, err := populateNovelIDs(auditor, r, id, cat.WorkIDs)
if err != nil {
auditor.SugaredLogger.Error("Failed to populate novel IDs", zap.Error(err))
return err
}
if mode == "all" {
cat.NovelWorks = novels
} else if mode == "safe" {
cat.NovelWorks = safeNovels
}
case "bookmarks":
auditor.Logger.Debug("Fetching and populating Bookmarks", zap.String("category", cat.Value))
bookmarks, count, err := populateBookmarks(auditor, r, id, "show", page)
if err != nil {
auditor.SugaredLogger.Error("Failed to populate bookmarked work IDs", zap.Error(err))
return err
}
cat.IllustWorks = bookmarks
cat.WorkCount = count
worksPerPage := 48.0
cat.PageLimit = int(math.Ceil(float64(count) / worksPerPage))
}
case "novels":
auditor.Logger.Debug("Fetching and populating NovelBriefs", zap.String("category", cat.Value))
novels, safeNovels, err := populateNovelIDs(auditor, r, id, cat.WorkIDs)
if err != nil {
auditor.SugaredLogger.Error("Failed to populate novel IDs", zap.Error(err))
return err
}
if mode == "all" {
cat.NovelWorks = novels
} else if mode == "safe" {
cat.NovelWorks = safeNovels
return nil
})
g.Go(func() error {
// Fetch frequent tags if requested
if getTags && cat.Value != "bookmarks" {
auditor.Logger.Debug("Fetching frequent tags for category", zap.String("category", cat.Value))
tags, err := fetchFrequentTags(auditor, r, cat.WorkIDs, cat)
if err != nil {
auditor.SugaredLogger.Error("Failed to fetch frequent tags", zap.String("category", cat.Value), zap.Error(err))
return err
}
cat.FrequentTags = tags
}
case "bookmarks":
auditor.Logger.Debug("Fetching and populating Bookmarks", zap.String("category", cat.Value))
bookmarks, count, err := populateBookmarks(auditor, r, id, "show", page)
if err != nil {
auditor.SugaredLogger.Error("Failed to populate bookmarked work IDs", zap.Error(err))
return err
}
cat.IllustWorks = bookmarks
cat.WorkCount = count
worksPerPage := 48.0
cat.PageLimit = int(math.Ceil(float64(count) / worksPerPage))
}
return nil
})
}
// Fetch frequent tags if requested
if getTags && cat.Value != "bookmarks" {
auditor.Logger.Debug("Fetching frequent tags for category", zap.String("category", cat.Value))
tags, err := fetchFrequentTags(auditor, r, cat.WorkIDs, cat)
if err != nil {
auditor.SugaredLogger.Error("Failed to fetch frequent tags", zap.String("category", cat.Value), zap.Error(err))
return err
}
cat.FrequentTags = tags
}
// Wait for all goroutines to finish
if err := g.Wait(); err != nil {
return nil, err
}
// Set PageLimit for the current category
if currentCategory != nil {
if cat, ok := user.Categories[currentCategory.Value]; ok {
if cat, ok := categoriesData[currentCategory.Value]; ok {
currentCategory.PageLimit = cat.PageLimit
}
}
return nil
return categoriesData, nil
}
// fetchWorkIDsAndSeriesData retrieves artwork IDs and series information for a user.