diff --git a/core/user.go b/core/user.go index d16720f..ea25598 100644 --- a/core/user.go +++ b/core/user.go @@ -6,6 +6,7 @@ import ( "net/http" "sort" "strconv" + "strings" "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/session" @@ -24,6 +25,13 @@ const ( CategoryNovels UserWorkCategory = "novels" ) +// UserWorkIDs holds separate ID strings for illustrations, manga, and novels. +type UserWorkIDs struct { + IllustrationIDs string + MangaIDs string + NovelIDs string +} + func (s UserWorkCategory) Validate() error { if s != CategoryAny && s != CategoryAnyAlt && @@ -155,44 +163,134 @@ func fetchUserWorks(r *http.Request, user *User, id string, category UserWorkCat } user.Artworks = works + user.NovelSeries = nil + user.MangaSeries = nil user.CategoryItemCount = count - } else { - // Fetch artworks or novels - ids, countInfo, series, err := getUserArtworksIDAndSeries(r, id, category, page) + return nil + } + + // Fetch artworks or novels + userWorkIDs, countInfo, series, err := getUserArtworksIDAndSeries(r, id, category, page) + if err != nil { + return err + } + + // Initialize ID strings for different work types + var illustrationIDs, mangaIDs, novelIDs string + + switch category { + case CategoryIllustration: + illustrationIDs = userWorkIDs.IllustrationIDs + case CategoryManga: + mangaIDs = userWorkIDs.MangaIDs + case CategoryNovels: + novelIDs = userWorkIDs.NovelIDs + case CategoryAny, CategoryAnyAlt: + // Combine all relevant ID strings + illustrationIDs = userWorkIDs.IllustrationIDs + mangaIDs = userWorkIDs.MangaIDs + novelIDs = userWorkIDs.NovelIDs + default: + return fmt.Errorf("unsupported category: %v", category) + } + + // Flags to determine which work types to fetch + fetchArtworks := false + fetchNovels := false + + switch category { + case CategoryIllustration, CategoryManga, CategoryAny, CategoryAnyAlt: + fetchArtworks = true + case CategoryNovels: + fetchNovels = true + } + + // Early return if there are no works to fetch + if countInfo.All == 0 { + user.Artworks = nil + user.CategoryItemCount = 0 + user.NovelSeries = nil + user.MangaSeries = nil + return nil + } + + // Fetch and process novels if required + if fetchNovels && novelIDs != "" { + ids := novelIDs + novels, err := fetchAndProcessItems[NovelBrief](r, id, ids, getUserNovelIDs) + if err != nil { + return err + } + user.Novels = novels + } + + // Fetch and process artworks if required + if fetchArtworks && (illustrationIDs != "" || mangaIDs != "") { + // Combine Illustration and Manga IDs + ids := illustrationIDs + mangaIDs + artworks, err := fetchAndProcessItems[ArtworkBrief](r, id, ids, getUserArtworkIDs) if err != nil { return err } - if countInfo.All > 0 { - artworks, novels, err := fetchAndProcessArtworks(r, id, ids, category) - if err != nil { - return err - } - - if category == CategoryNovels { - user.Novels = novels - } else { - user.Artworks = artworks - user.Illustrations = filterArtworksByType(artworks, 0) - user.Manga = filterArtworksByType(artworks, 1) - } - - if getTags { - user.FrequentTags, err = getUserFrequentTags(r, ids, category) - if err != nil { - return err - } - } - } - - // Handle series data - user.NovelSeries, user.MangaSeries = handleSeriesData(series, category) - user.CategoryItemCount = countInfo.All + user.Artworks = artworks + user.Illustrations = filterArtworksByType(artworks, 0) + user.Manga = filterArtworksByType(artworks, 1) } + // Fetch frequent tags if requested + if getTags { + // Combine all IDs when fetching tags for 'Any' categories + tagsIDs := illustrationIDs + mangaIDs + novelIDs + + user.FrequentTags, err = getUserFrequentTags(r, tagsIDs, category) + if err != nil { + return err + } + } + + // Handle series data + user.NovelSeries, user.MangaSeries = handleSeriesData(series, category) + user.CategoryItemCount = countInfo.All + return nil } +// The HasID interface allows for generic handling of the ArtworkBrief +// and NovelBrief struct structs, which both contaisn an ID string. +type HasID interface { + GetID() string +} + +// GetID implements the Identifiable interface for the ArtworkBrief struct. +func (a ArtworkBrief) GetID() string { + return a.ID +} + +// GetID implements the Identifiable interface for the NovelBrief struct. +func (n NovelBrief) GetID() string { + return n.ID +} + +// fetchAndProcessItems is a generic function that fetches and processes items of type T, +// where T must implement the HasID interface. +func fetchAndProcessItems[T HasID]( + r *http.Request, + id, ids string, + fetchFunc func(*http.Request, string, string) ([]T, error), +) ([]T, error) { + items, err := fetchFunc(r, id, ids) + if err != nil { + return nil, err + } + + sort.Slice(items, func(i, j int) bool { + return numberGreaterThan(items[i].GetID(), items[j].GetID()) + }) + + return items, nil +} + // filterArtworksByType filters artworks based on the IllustType. func filterArtworksByType(artworks []ArtworkBrief, illustType int) []ArtworkBrief { filtered := make([]ArtworkBrief, 0) @@ -210,8 +308,8 @@ func filterArtworksByType(artworks []ArtworkBrief, illustType int) []ArtworkBrie // don't exist for a user. type IntStringMap map[int]string -// unmarshalJSON implements custom unmarshalling for IntStringMap. -func (m *IntStringMap) unmarshalJSON(data []byte) error { +// 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) @@ -242,19 +340,18 @@ func (m *IntStringMap) unmarshalJSON(data []byte) error { } // getUserArtworksIDAndSeries retrieves artwork IDs and series information for a user. -func getUserArtworksIDAndSeries(r *http.Request, id string, category UserWorkCategory, page int) (idsString string, countInfo CountInfo, series json.RawMessage, err error) { +// It now returns separate ID strings for illustrations, manga, and novels encapsulated in UserWorkIDs. +func getUserArtworksIDAndSeries(r *http.Request, id string, category UserWorkCategory, page int) (userWorkIDs UserWorkIDs, countInfo CountInfo, series json.RawMessage, err error) { URL := GetUserArtworksURL(id) resp, err := API_GET_UnwrapJson(r.Context(), URL, "", r.Header) if err != nil { - return "", CountInfo{}, nil, err + return UserWorkIDs{}, CountInfo{}, 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"` @@ -264,108 +361,108 @@ func getUserArtworksIDAndSeries(r *http.Request, id string, category UserWorkCat err = json.Unmarshal([]byte(resp), &body) if err != nil { - return "", CountInfo{}, nil, fmt.Errorf("failed to unmarshal response body: %w", err) + return UserWorkIDs{}, CountInfo{}, nil, fmt.Errorf("failed to unmarshal response body: %w", err) } - var ids []int countInfo = CountInfo{} var illusts IntStringMap var manga IntStringMap var novels IntStringMap - if category == CategoryIllustration || category == CategoryAny || category == CategoryAnyAlt { - 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) - } - countInfo.All += len(illusts) - countInfo.Illustrations = len(illusts) + var illustIDs []int + var mangaIDs []int + var novelIDs []int + + // Process Illustrations + 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 { + illustIDs = append(illustIDs, k) + } + countInfo.All += len(illusts) + countInfo.Illustrations = len(illusts) + } + + // Process Manga + 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 { + mangaIDs = append(mangaIDs, k) + } + countInfo.All += len(manga) + countInfo.Manga = len(manga) + if body.MangaSeries != nil { + series = *body.MangaSeries } } - if category == CategoryManga || category == CategoryAny { - 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) - } - countInfo.All += len(manga) - countInfo.Manga = len(manga) - if body.MangaSeries != nil { - series = *body.MangaSeries - } + // Process Novels + 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 { + novelIDs = append(novelIDs, k) + } + countInfo.All += len(novels) + countInfo.Novels = len(novels) + if body.NovelSeries != nil { + series = *body.NovelSeries } } - if category == CategoryNovels { - 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) - } - countInfo.All += len(novels) - countInfo.Novels = len(novels) - if body.NovelSeries != nil { - series = *body.NovelSeries - } - } - } - - sort.Sort(sort.Reverse(sort.IntSlice(ids))) + // Sort IDs in descending order + sort.Sort(sort.Reverse(sort.IntSlice(illustIDs))) + sort.Sort(sort.Reverse(sort.IntSlice(mangaIDs))) + sort.Sort(sort.Reverse(sort.IntSlice(novelIDs))) worksPerPage := 30.0 - start, end, err := computeSliceBounds(page, worksPerPage, len(ids)) + // Compute slice bounds for each category + startIllust, endIllust, err := computeSliceBounds(page, worksPerPage, len(illustIDs)) if err != nil { - return "", CountInfo{}, nil, err - } - - // Build the IDs string for the API request - for _, k := range ids[start:end] { - idsString += fmt.Sprintf("&ids[]=%d", k) - } - - return idsString, countInfo, series, nil -} - -func fetchAndProcessArtworks(r *http.Request, id, ids string, category UserWorkCategory) ([]ArtworkBrief, []NovelBrief, error) { - var artworks []ArtworkBrief - var novels []NovelBrief - var err error - - if category == CategoryNovels { - novels, err = getUserNovelIDs(r, id, ids) - } else { - artworks, err = getUserArtworkIDs(r, id, ids) + return UserWorkIDs{}, CountInfo{}, nil, err } + startManga, endManga, err := computeSliceBounds(page, worksPerPage, len(mangaIDs)) if err != nil { - return nil, nil, err + return UserWorkIDs{}, CountInfo{}, nil, err } - // Sort the works - if category == CategoryNovels { - 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) - }) + startNovel, endNovel, err := computeSliceBounds(page, worksPerPage, len(novelIDs)) + if err != nil { + return UserWorkIDs{}, CountInfo{}, nil, err } - return artworks, novels, nil + // Build the ID strings for each category + var idsBuilder strings.Builder + + for _, k := range illustIDs[startIllust:endIllust] { + idsBuilder.WriteString(fmt.Sprintf("&ids[]=%d", k)) + } + userWorkIDs.IllustrationIDs = idsBuilder.String() + + idsBuilder.Reset() + for _, k := range mangaIDs[startManga:endManga] { + idsBuilder.WriteString(fmt.Sprintf("&ids[]=%d", k)) + } + userWorkIDs.MangaIDs = idsBuilder.String() + + idsBuilder.Reset() + for _, k := range novelIDs[startNovel:endNovel] { + idsBuilder.WriteString(fmt.Sprintf("&ids[]=%d", k)) + } + userWorkIDs.NovelIDs = idsBuilder.String() + + return userWorkIDs, countInfo, series, nil } func handleSeriesData(series json.RawMessage, category UserWorkCategory) ([]NovelSeries, []MangaSeries) {