diff --git a/backend/internal/latest/sites.go b/backend/internal/latest/sites.go index 35e7293..a79d0af 100644 --- a/backend/internal/latest/sites.go +++ b/backend/internal/latest/sites.go @@ -165,16 +165,24 @@ var singleQuotedMetaAttrRe = regexp.MustCompile(`(?is)([a-z][a-z0-9:_-]*)\s*=\s* // the target detail entry avoids matching posters from recommended results. var comixInitialDataRe = regexp.MustCompile(`(?is)]*\bid\s*=\s*["']initial-data["'][^>]*>(.*?)`) -// kagane's browser-fetched series response carries this published cover URL. -// Decode the top-level field, not merely an image-shaped URL elsewhere in JSON. +// kagane's browser-fetched series response publishes cover image IDs under +// series_covers. The API's canonical compressed image route is the only URL +// form accepted by the store and browser fetcher; no rendition is guessed. func kaganeCoverURL(body string) string { var response struct { - Cover string `json:"cover"` + SeriesCovers []struct { + ImageID string `json:"image_id"` + } `json:"series_covers"` } if err := json.Unmarshal([]byte(body), &response); err != nil { return "" } - return publishedCoverURL(response.Cover) + for _, cover := range response.SeriesCovers { + if kaganeImageIDRe.MatchString(cover.ImageID) { + return "https://kagane.to/api/v2/image/" + cover.ImageID + "/compressed" + } + } + return "" } func comixCoverURL(seriesURL, body string) string { diff --git a/backend/internal/latest/sites_test.go b/backend/internal/latest/sites_test.go index a82eac9..16cbab0 100644 --- a/backend/internal/latest/sites_test.go +++ b/backend/internal/latest/sites_test.go @@ -92,8 +92,8 @@ const demonicCoverFixture = `{"queries":{"[\"manga\",\"recommended\",\"n8we\",1]":{"poster":{"medium":"https://static.comix.to/recommended@280.jpg","large":"https://static.comix.to/recommended.jpg"}},"[\"manga\",\"detail\",\"n8we\"]":{"chapters":[{"hid":"nested"}],"poster":{"medium":"https://static.comix.to/039d/i/1/34/6a6742bf15736@280.jpg","large":"https://static.comix.to/039d/i/1/34/6a6742bf15736.jpg"}}}}` -// Trimmed from GET https://kagane.to/api/v2/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b on 2026-08-09. -const kaganeCoverFixture = `{"books":[{"cover":"https://kagane.to/api/v2/image/00000000-0000-0000-0000-000000000000/compressed"}],"cover":"https://kagane.to/api/v2/image/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b/compressed"}` +// Trimmed from GET https://kagane.to/api/v2/series/019fe11a-8670-7cf3-8343-0b02057d3787 on 2026-08-10. +const kaganeCoverFixture = `{"series_covers":[{"cover_id":"019fe11a-84d1-714b-9cf4-2827f277f3c0","language":"en","volume_number":"1","chapter_number":null,"note":null,"image_id":"019fe11a-84c3-7fc3-a84b-88787374b617"}]}` // Trimmed from https://novelfull.com/reverend-insanity.html on 2026-08-10. const novelfullCoverFixture = `` @@ -127,9 +127,9 @@ func TestCoverFrom(t *testing.T) { wantCover: "https://static.comix.to/039d/i/1/34/6a6742bf15736@280.jpg", }, { - name: "kagane reads API cover field", + name: "kagane reads API cover image ID", site: "kagane", body: kaganeCoverFixture, wantOK: true, - wantCover: "https://kagane.to/api/v2/image/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b/compressed", + wantCover: "https://kagane.to/api/v2/image/019fe11a-84c3-7fc3-a84b-88787374b617/compressed", }, { name: "novelfull reads image metadata",