diff --git a/assets/views/fragments/thumbnail-dt.jet.html b/assets/views/fragments/thumbnail-dt.jet.html index c7e6b83..e9fe895 100644 --- a/assets/views/fragments/thumbnail-dt.jet.html +++ b/assets/views/fragments/thumbnail-dt.jet.html @@ -41,7 +41,12 @@ {{- else if AiType == 2 && hideAi }} {{ .Title }} {{- else }} - {{ .Title }} + {{ .Title }} {{- end }} diff --git a/assets/views/mangaSeries.jet.html b/assets/views/mangaSeries.jet.html index 3783855..92f91a4 100644 --- a/assets/views/mangaSeries.jet.html +++ b/assets/views/mangaSeries.jet.html @@ -17,7 +17,7 @@
- {{ .Title }} + {{ .Title }}
diff --git a/core/artwork.go b/core/artwork.go index 70d4196..bbbd7ee 100644 --- a/core/artwork.go +++ b/core/artwork.go @@ -113,18 +113,13 @@ type ArtworkBrief struct { ArtistName string `json:"userName"` ArtistAvatar string `json:"profileImageUrl"` Thumbnail string `json:"url"` - Thumbnails struct { - Small string `json:"250x250"` - Medium string `json:"360x360"` - Large string `json:"540x540"` - Master string `json:"1200x1200"` - } `json:"urls"` - Pages int `json:"pageCount"` - XRestrict int `json:"xRestrict"` - AiType int `json:"aiType"` - Bookmarked any `json:"bookmarkData"` - IllustType int `json:"illustType"` - Tags []string `json:"tags"` // used by core/popular_search + Thumbnails Thumbnails + Pages int `json:"pageCount"` + XRestrict int `json:"xRestrict"` + AiType int `json:"aiType"` + Bookmarked any `json:"bookmarkData"` + IllustType int `json:"illustType"` + Tags []string `json:"tags"` // used by core/popular_search } type Illust struct { @@ -267,6 +262,15 @@ func GetRelatedArtworks(auditor *audit.Auditor, r *http.Request, id string) ([]A return nil, err } + // Populate thumbnails for each artwork + for id, artwork := range body.Illusts { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + body.Illusts[id] = artwork + } + return body.Illusts, nil } @@ -381,6 +385,16 @@ func GetArtworkByID(auditor *audit.Auditor, r *http.Request, id string, full boo sort.Slice(recent[:], func(i, j int) bool { return numberGreaterThan(recent[i].ID, recent[j].ID) }) + + // Populate thumbnails for each artwork + for id, artwork := range recent { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + recent[id] = artwork + } + illust.RecentWorks = recent return nil }) diff --git a/core/discovery.go b/core/discovery.go index 9254419..62623c6 100644 --- a/core/discovery.go +++ b/core/discovery.go @@ -39,6 +39,15 @@ func GetDiscoveryArtwork(auditor *audit.Auditor, r *http.Request, mode string) ( return nil, err } + // Populate thumbnails for each artwork + for id, artwork := range artworks { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + artworks[id] = artwork + } + return artworks, nil } @@ -108,6 +117,15 @@ func GetDiscoveryUsers(auditor *audit.Auditor, r *http.Request) ([]User, error) return nil, fmt.Errorf("error unmarshalling artworks in GetDiscoveryUser: %w", err) } + // Populate thumbnails for each artwork + for id, artwork := range artworks { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + artworks[id] = artwork + } + // Extract and unmarshal novels novelData := gjson.Get(resp, "thumbnails.novel").Raw var novels []NovelBrief diff --git a/core/index.go b/core/index.go index f139604..83732be 100644 --- a/core/index.go +++ b/core/index.go @@ -99,6 +99,15 @@ func GetLanding(auditor *audit.Auditor, r *http.Request, mode string, isLoggedIn return nil, err } + // Populate thumbnails for each artwork + for id, artwork := range artworks { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + artworks[id] = artwork + } + var pages Pages pagesStr := gjson.Get(resp, "page").String() diff --git a/core/mangaseries.go b/core/mangaseries.go index b269560..5b0852b 100644 --- a/core/mangaseries.go +++ b/core/mangaseries.go @@ -1,6 +1,7 @@ package core import ( + "fmt" "net/http" "strconv" "time" @@ -10,6 +11,7 @@ import ( "github.com/goccy/go-json" ) +// MangaSeries represents the metadata of a manga series. type MangaSeries struct { ID string `json:"id"` UserID string `json:"userId"` @@ -18,7 +20,7 @@ type MangaSeries struct { Caption string `json:"caption"` Total int `json:"total"` ContentOrder any `json:"content_order"` - URL string `json:"url"` + Thumbnail string `json:"url"` CoverImageSl int `json:"coverImageSl"` FirstIllustID string `json:"firstIllustId"` LatestIllustID string `json:"latestIllustId"` @@ -29,6 +31,7 @@ type MangaSeries struct { IsNotifying bool `json:"isNotifying"` } +// MangaSeriesContent encapsulates the content details of a manga series. type MangaSeriesContent struct { Series []struct { WorkID string `json:"workId"` @@ -45,6 +48,7 @@ type MangaSeriesContent struct { Brief MangaSeries } +// GetMangaSeriesContentByID retrieves the content of a manga series by its ID and page number. func GetMangaSeriesContentByID(auditor *audit.Auditor, r *http.Request, id string, page int) (MangaSeriesContent, error) { var series_content MangaSeriesContent @@ -60,31 +64,48 @@ func GetMangaSeriesContentByID(auditor *audit.Auditor, r *http.Request, id strin return series_content, err } - var mangas struct { + // Define a temporary struct to match the expected JSON response from the API + var manga struct { Thumbnails struct { - List []ArtworkBrief `json:"illust"` + List []ArtworkBrief `json:"illust"` // List of thumbnail images for artworks } `json:"thumbnails"` - IllustSeries []MangaSeries `json:"illustSeries"` - Page MangaSeriesContent `json:"page"` + IllustSeries []MangaSeries `json:"illustSeries"` // List of illustration series metadata + Page MangaSeriesContent `json:"page"` // Page-specific content and metadata } - err = json.Unmarshal([]byte(resp), &mangas) + // Unmarshal the JSON response into the temporary "manga" struct + err = json.Unmarshal([]byte(resp), &manga) if err != nil { return series_content, err } - series_content = mangas.Page + // Assign the page-specific content to the main series_content variable + series_content = manga.Page + + // Iterate over each series entry to populate the Brief field with corresponding artwork details for idx, content := range series_content.Series { - for _, item := range mangas.Thumbnails.List { + for _, item := range manga.Thumbnails.List { if item.ID == content.WorkID { + // If a match is found, assign the ArtworkBrief to the current series entry series_content.Series[idx].Brief = item + + // Populate thumbnails + err := series_content.Series[idx].Brief.PopulateThumbnails(auditor) + if err != nil { + return series_content, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + + // Break the inner loop with the matching artwork having been found and processed break } } } - for _, brief := range mangas.IllustSeries { + // Iterate over IllustSeries to find and assign the Brief metadata for the series + for _, brief := range manga.IllustSeries { + // Compare the series ID from the API response with the series_content's SeriesID if brief.ID == strconv.Itoa(series_content.SeriesID) { + // Assign the matching MangaSeries metadata to the Brief field of series_content series_content.Brief = brief break } diff --git a/core/newest.go b/core/newest.go index 288b559..db94213 100644 --- a/core/newest.go +++ b/core/newest.go @@ -1,6 +1,7 @@ package core import ( + "fmt" "net/http" "codeberg.org/vnpower/pixivfe/v2/audit" @@ -31,5 +32,14 @@ func GetNewestArtworks(auditor *audit.Auditor, r *http.Request, worktype string, return nil, err } + // Populate thumbnails for each artwork + for id, artwork := range body.Artworks { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + body.Artworks[id] = artwork + } + return body.Artworks, nil } diff --git a/core/personal.go b/core/personal.go index 8ebf68e..de0a966 100644 --- a/core/personal.go +++ b/core/personal.go @@ -1,6 +1,7 @@ package core import ( + "fmt" "net/http" "codeberg.org/vnpower/pixivfe/v2/audit" @@ -40,5 +41,14 @@ func GetNewestFromFollowing(auditor *audit.Auditor, r *http.Request, mode, page return nil, err } + // Populate thumbnails for each artwork + for id, artwork := range artworks.Artworks { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + artworks.Artworks[id] = artwork + } + return artworks.Artworks, nil } diff --git a/core/ranking.go b/core/ranking.go index 4d6ef50..2081d8d 100644 --- a/core/ranking.go +++ b/core/ranking.go @@ -1,6 +1,7 @@ package core import ( + "fmt" "net/http" "strings" "time" @@ -29,30 +30,34 @@ const ( ) type Ranking struct { - Contents []struct { - Title string `json:"title"` - Thumbnail string `json:"url"` - Pages int `json:"illust_page_count,string"` - ArtistName string `json:"user_name"` - ArtistAvatar string `json:"profile_img"` - ID int `json:"illust_id"` - ArtistID int `json:"user_id"` - Rank int `json:"rank"` - IllustType int `json:"illust_type,string"` - XRestrict int // zero field for template - } `json:"contents"` - - Mode string `json:"mode"` - Content string `json:"content"` - Page int `json:"page"` - RankTotal int `json:"rank_total"` - CurrentDate string `json:"date"` - PrevDateRaw json.RawMessage `json:"prev_date"` - NextDateRaw json.RawMessage `json:"next_date"` + Contents []RankingContent `json:"contents"` + Mode string `json:"mode"` + Content string `json:"content"` + Page int `json:"page"` + RankTotal int `json:"rank_total"` + CurrentDate string `json:"date"` + PrevDateRaw json.RawMessage `json:"prev_date"` + NextDateRaw json.RawMessage `json:"next_date"` PrevDate string NextDate string } +type RankingContent struct { + Title string `json:"title"` + Thumbnail string `json:"url"` + Thumbnails Thumbnails + Pages int `json:"illust_page_count,string"` + ArtistName string `json:"user_name"` + ArtistAvatar string `json:"profile_img"` + ID int `json:"illust_id"` + ArtistID int `json:"user_id"` + Rank int `json:"rank"` + IllustType int `json:"illust_type,string"` + XRestrict int // zero field for template +} + +// see core/types.go for the Ranking struct + func GetRanking(auditor *audit.Auditor, r *http.Request, mode, content, date, page string) (Ranking, error) { // log.Printf("GetRanking called with mode: %s, content: %s, date: %s, page: %s", mode, content, date, page) @@ -101,6 +106,11 @@ func GetRanking(auditor *audit.Auditor, r *http.Request, mode, content, date, pa ranking.PrevDate = strings.ReplaceAll(string(ranking.PrevDateRaw[:]), "\"", "") ranking.NextDate = strings.ReplaceAll(string(ranking.NextDateRaw[:]), "\"", "") + // Populate thumbnails + if err := ranking.PopulateThumbnails(auditor); err != nil { + return ranking, fmt.Errorf("failed to populate thumbnails for ranking: %w", err) + } + // log.Printf("GetRanking completed successfully. PrevDate: %s, NextDate: %s", ranking.PrevDate, ranking.NextDate) return ranking, nil diff --git a/core/tag.go b/core/tag.go index 4b74423..be03595 100644 --- a/core/tag.go +++ b/core/tag.go @@ -1,6 +1,7 @@ package core import ( + "fmt" "net/http" "net/url" "regexp" @@ -167,6 +168,15 @@ func getPopularSearch(auditor *audit.Auditor, r *http.Request, settings SearchPa // TODO: populate Popular (the regular one) and RelatedTags } + // Populate thumbnails for each artwork + for id, artwork := range result.Artworks.Artworks { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + result.Artworks.Artworks[id] = artwork + } + return result, nil } @@ -217,6 +227,31 @@ func getStandardSearch(auditor *audit.Auditor, r *http.Request, settings SearchP return nil, err } + // Populate thumbnails for each artwork + for id, artwork := range artworks.Artworks { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + artworks.Artworks[id] = artwork + } + + for id, artwork := range resultRaw.Popular.Permanent { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + resultRaw.Popular.Permanent[id] = artwork + } + + for id, artwork := range resultRaw.Popular.Recent { + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } + resultRaw.Popular.Recent[id] = artwork + } + // Assign artworks and popular data to the result result.Artworks = artworks result.Popular = resultRaw.Popular diff --git a/core/types.go b/core/types.go index fcceb8b..c2c4984 100644 --- a/core/types.go +++ b/core/types.go @@ -1,8 +1,12 @@ package core import ( + "fmt" "html/template" "net/http" + "net/url" + "path" + "regexp" "codeberg.org/vnpower/pixivfe/v2/audit" ) @@ -14,3 +18,110 @@ type RequestContext struct { auditor *audit.Auditor request *http.Request } + +// Thumbnails encapsulates all thumbnail sizes. +type Thumbnails struct { + Tiny string + Small string + Medium string + Large string + Original string +} + +func (work *ArtworkBrief) PopulateThumbnails(auditor *audit.Auditor) error { + thumbnails, err := PopulateThumbnailsFor(auditor, work.Thumbnail) + if err != nil { + return err + } + work.Thumbnails = thumbnails + return nil +} + +func (r *Ranking) PopulateThumbnails(auditor *audit.Auditor) error { + for i := range r.Contents { + err := r.Contents[i].PopulateThumbnails(auditor) + if err != nil { + return err + } + } + return nil +} + +func (content *RankingContent) PopulateThumbnails(auditor *audit.Auditor) error { + thumbnails, err := PopulateThumbnailsFor(auditor, content.Thumbnail) + if err != nil { + return err + } + content.Thumbnails = thumbnails + return nil +} + +// PopulateThumbnailsFor is a helper function +func PopulateThumbnailsFor(auditor *audit.Auditor, thumbnailURL string) (Thumbnails, error) { + var thumbnails Thumbnails + + // auditor.SugaredLogger.Debugf("PopulateThumbnails called with Thumbnail URL: %s", thumbnailURL) + + // Parse the original Thumbnail URL to ensure it's valid + parsedURL, err := url.Parse(thumbnailURL) + if err != nil { + auditor.SugaredLogger.Errorf("Invalid Thumbnail URL '%s': %v", thumbnailURL, err) + return thumbnails, fmt.Errorf("invalid Thumbnail URL: %w", err) + } + + // Define a regex to match the "/c/{parameters}/" segment + re := regexp.MustCompile(`/c/[^/]+/`) + + // Verify that the Thumbnail URL contains the expected pattern + if !re.MatchString(parsedURL.Path) { + auditor.SugaredLogger.Errorf("Thumbnail URL does not match expected pattern: %s", thumbnailURL) + return thumbnails, fmt.Errorf("Thumbnail URL does not match expected pattern") + } + + // Define the desired sizes for the thumbnails along with corresponding fields + thumbSizes := []struct { + name string + size string + field *string + }{ + {"Tiny", "250x250", &thumbnails.Tiny}, + {"Small", "360x360", &thumbnails.Small}, + {"Medium", "540x540", &thumbnails.Medium}, + {"Large", "1200x1200", &thumbnails.Large}, + } + + // Iterate over each size and generate the corresponding URL + for _, thumb := range thumbSizes { + finalURL := generateThumbnailURL(parsedURL, re, thumb.size) + *thumb.field = finalURL + // auditor.SugaredLogger.Debugf("Set Thumbnails.%s to: %s", thumb.name, finalURL) + } + + // Construct the Original URL by removing the "/c/{size}_{quality}/" segment + finalOriginalURL := generateOriginalURL(parsedURL, re) + thumbnails.Original = finalOriginalURL + + // auditor.SugaredLogger.Debugf("Set Thumbnails.Original to: %s", finalOriginalURL) + + return thumbnails, nil +} + +// generateThumbnailURL constructs a thumbnail URL for a given size. +func generateThumbnailURL(parsedURL *url.URL, re *regexp.Regexp, size string) string { + // Replace the existing size and quality segment with the new size + newPath := re.ReplaceAllString(parsedURL.Path, fmt.Sprintf("/c/%s/", size)) + updatedURL := *parsedURL // Copy + updatedURL.Path = newPath + return updatedURL.String() +} + +// generateOriginalURL constructs the original image URL by removing the size and quality segment. +func generateOriginalURL(parsedURL *url.URL, re *regexp.Regexp) string { + // Remove the size and quality segment + originalPath := re.ReplaceAllString(parsedURL.Path, "/") + // Clean up the path to ensure it's correctly formatted + originalPath = path.Clean(originalPath) + originalURL := *parsedURL + originalURL.Path = originalPath + return originalURL.String() +} diff --git a/core/user.go b/core/user.go index 64e3fc9..a09ded0 100644 --- a/core/user.go +++ b/core/user.go @@ -199,7 +199,7 @@ func getPopulatedWorks(auditor *audit.Auditor, r *http.Request, user *User, id s switch cat.Value { case "illustrations", "manga": auditor.Logger.Debug("Fetching and populating ArtworkBriefs", zap.String("category", cat.Value)) - artworks, err := fetchPopulatedBriefs[ArtworkBrief](auditor, r, id, cat.WorkIDs, fetchArtworkIDs) + artworks, err := fetchPopulatedArtworkBriefs(auditor, r, id, cat.WorkIDs, fetchArtworkIDs) if err != nil { auditor.SugaredLogger.Error("Failed to fetch populated ArtworkBriefs", zap.Error(err)) return err @@ -208,7 +208,7 @@ func getPopulatedWorks(auditor *audit.Auditor, r *http.Request, user *User, id s case "novels": auditor.Logger.Debug("Fetching and populating NovelBriefs", zap.String("category", cat.Value)) - novels, err := fetchPopulatedBriefs[NovelBrief](auditor, r, id, cat.WorkIDs, fetchNovelIDs) + novels, err := fetchPopulatedNovelBriefs(auditor, r, id, cat.WorkIDs, fetchNovelIDs) if err != nil { auditor.SugaredLogger.Error("Failed to fetch populated NovelBriefs", zap.Error(err)) return err @@ -407,37 +407,50 @@ func extractIDs(rawMessage *json.RawMessage, categoryName string) ([]int, int, e return ids, len(dataMap), nil } -// The HasID interface allows for generic handling of the ArtworkBrief -// and NovelBrief structs, which both contain an ID string. -type HasID interface { - GetID() string -} - -// GetID implements the HasID interface for the ArtworkBrief struct. -func (a ArtworkBrief) GetID() string { - return a.ID -} - -// GetID implements the HasID interface for the NovelBrief struct. -func (n NovelBrief) GetID() string { - return n.ID -} - -// fetchPopulatedBriefs is a generic function that fetches and processes items of type T, -// where T must implement the HasID interface. -func fetchPopulatedBriefs[T HasID]( +// fetchPopulatedArtworkBriefs fetches and processes ArtworkBrief items. +func fetchPopulatedArtworkBriefs( auditor *audit.Auditor, r *http.Request, id, ids string, - fetchFunc func(*audit.Auditor, *http.Request, string, string) ([]T, error), -) ([]T, error) { + fetchFunc func(*audit.Auditor, *http.Request, string, string) ([]ArtworkBrief, error), +) ([]ArtworkBrief, error) { items, err := fetchFunc(auditor, r, id, ids) if err != nil { return nil, err } + // Sort the items based on ID in descending order sort.Slice(items, func(i, j int) bool { - return numberGreaterThan(items[i].GetID(), items[j].GetID()) + return numberGreaterThan(items[i].ID, items[j].ID) + }) + + // Populate thumbnails for each artwork + for idx := range items { + artwork := &items[idx] + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", artwork.ID, err) + return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", artwork.ID, err) + } + } + + return items, nil +} + +// fetchPopulatedNovelBriefs fetches and processes NovelBrief items. +func fetchPopulatedNovelBriefs( + auditor *audit.Auditor, + r *http.Request, + id, ids string, + fetchFunc func(*audit.Auditor, *http.Request, string, string) ([]NovelBrief, error), +) ([]NovelBrief, error) { + items, err := fetchFunc(auditor, r, id, ids) + if err != nil { + return nil, err + } + + // Sort the items based on ID in descending order + sort.Slice(items, func(i, j int) bool { + return numberGreaterThan(items[i].ID, items[j].ID) }) return items, nil @@ -585,6 +598,12 @@ func fetchBookmarks(auditor *audit.Auditor, r *http.Request, id, mode string, pa } continue } + + // Populate thumbnails + if err := artwork.PopulateThumbnails(auditor); err != nil { + auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err) + return nil, -1, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err) + } artworks[index] = artwork }