From 3a6da13535ff9aefcea477d3a3166409dbdc5f28 Mon Sep 17 00:00:00 2001 From: perennial Date: Sat, 9 Nov 2024 01:52:07 +1100 Subject: [PATCH] core/user: use goroutines to avoid blocking on network requests --- core/user.go | 238 +++++++++++++++++++++++++++++---------------------- 1 file changed, 135 insertions(+), 103 deletions(-) diff --git a/core/user.go b/core/user.go index 9e93e9d..b6534d2 100644 --- a/core/user.go +++ b/core/user.go @@ -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.