wip: responsive resolution for artwork thumbnails

core/mangaseries: inline comments

core/types: comment out debug statements
This commit is contained in:
perennial
2024-10-29 12:56:36 +11:00
parent 022a5e531c
commit 4cffa928b8
12 changed files with 329 additions and 67 deletions
+26 -12
View File
@@ -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
})
+18
View File
@@ -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
+9
View File
@@ -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()
+30 -9
View File
@@ -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
}
+10
View File
@@ -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
}
+10
View File
@@ -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
}
+30 -20
View File
@@ -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
+35
View File
@@ -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
+111
View File
@@ -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()
}
+43 -24
View File
@@ -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
}