mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-15 20:34:23 +08:00
Merge changes from prometheus/mebox-40a7378b
This commit is contained in:
@@ -73,17 +73,17 @@ func CropAdultCoverPosterForCode(data []byte, code string) ([]byte, string, erro
|
||||
}
|
||||
src, format, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return data, format, err
|
||||
return data, imageMIMEForFormat(format), err
|
||||
}
|
||||
bounds := src.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
if width <= 0 || height <= 0 {
|
||||
return data, format, nil
|
||||
return data, imageMIMEForFormat(format), nil
|
||||
}
|
||||
// If the image is already portrait or square, keep it as-is.
|
||||
if float64(width) <= float64(height)*1.15 {
|
||||
return data, format, nil
|
||||
return data, imageMIMEForFormat(format), nil
|
||||
}
|
||||
|
||||
position := 1.0
|
||||
@@ -101,6 +101,26 @@ func CropAdultCoverPosterForCode(data []byte, code string) ([]byte, string, erro
|
||||
return buf.Bytes(), "image/jpeg", nil
|
||||
}
|
||||
|
||||
// imageMIMEForFormat maps the format name returned by image.Decode ("jpeg" / "png" / "webp" …) to a MIME
|
||||
// type. The sidecar extension is derived from the MIME type, so handing back the bare format name made
|
||||
// every already-portrait poster fall through to the legacy ".img" extension.
|
||||
func imageMIMEForFormat(format string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(format)) {
|
||||
case "jpeg", "jpg":
|
||||
return "image/jpeg"
|
||||
case "png":
|
||||
return "image/png"
|
||||
case "webp":
|
||||
return "image/webp"
|
||||
case "gif":
|
||||
return "image/gif"
|
||||
case "avif":
|
||||
return "image/avif"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// adultCoverNeedsFaceDetection reports whether the cover composition has to be probed with the face
|
||||
// detector, mirroring upstream MetaTube: only "special" numbers (FC2 / HEYZO / 素人 / 无码) qualify,
|
||||
// regular DVD numbers are always cropped at the right-most panel.
|
||||
|
||||
@@ -167,10 +167,14 @@ func TestCropAdultCoverPosterKeepsPortrait(t *testing.T) {
|
||||
// Create an already vertical portrait image (500x700)
|
||||
origBytes := createTestImage(500, 700, color.RGBA{R: 255, A: 255}, color.RGBA{B: 255, A: 255})
|
||||
|
||||
croppedBytes, _, err := CropAdultCoverPoster(origBytes)
|
||||
croppedBytes, ctype, err := CropAdultCoverPoster(origBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("CropAdultCoverPoster error: %v", err)
|
||||
}
|
||||
// 未裁剪时必须回传真正的 MIME(image/jpeg),否则侧车文件扩展名会退化成 .img。
|
||||
if ctype != "image/jpeg" {
|
||||
t.Fatalf("portrait ctype = %q, want image/jpeg", ctype)
|
||||
}
|
||||
|
||||
croppedImg, _, err := image.Decode(bytes.NewReader(croppedBytes))
|
||||
if err != nil {
|
||||
|
||||
@@ -96,7 +96,8 @@ func TestAdultProviderRouting(t *testing.T) {
|
||||
if len(candidates) != 1 {
|
||||
t.Fatalf("expected 1 candidate, got %d", len(candidates))
|
||||
}
|
||||
wantPoster := mtServer.URL + "/v1/images/primary/javdb/999?auto=true&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fposter.jpg"
|
||||
// SSIS-001 是常规番号:MetaTube 代理不得开启人脸定位,固定取最右侧封面面板。
|
||||
wantPoster := mtServer.URL + "/v1/images/primary/javdb/999?auto=false&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fposter.jpg"
|
||||
wantBackdrop := mtServer.URL + "/v1/images/backdrop/javdb/999?quality=90"
|
||||
if candidates[0].PosterURL != wantPoster || candidates[0].BackdropURL != wantBackdrop {
|
||||
t.Fatalf("candidate artwork was not enriched: %#v", candidates[0])
|
||||
@@ -170,8 +171,8 @@ func TestBuiltinAdultScrapeUsesMetaTubeFaceAwareArtwork(t *testing.T) {
|
||||
if match == nil {
|
||||
t.Fatal("expected built-in match")
|
||||
}
|
||||
wantPoster := metaTubeServer.URL + "/v1/images/primary/AVE/999?auto=true&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fwide-cover.jpg"
|
||||
wantPoster := metaTubeServer.URL + "/v1/images/primary/AVE/999?auto=false&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fwide-cover.jpg"
|
||||
if match.PosterURL != wantPoster {
|
||||
t.Fatalf("built-in poster = %q, want face-aware URL %q", match.PosterURL, wantPoster)
|
||||
t.Fatalf("built-in poster = %q, want MetaTube-processed URL %q", match.PosterURL, wantPoster)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestManualAdultMatchUsesSelectedMetaTubeDetailsAndRealBackdrop(t *testing.T
|
||||
if detailCalls.Load() != 1 {
|
||||
t.Fatalf("selected MetaTube detail calls = %d, want 1", detailCalls.Load())
|
||||
}
|
||||
wantPoster := upstream.URL + "/v1/images/primary/AVE/94600?auto=true&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fposter.jpg"
|
||||
wantPoster := upstream.URL + "/v1/images/primary/AVE/94600?auto=false&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fposter.jpg"
|
||||
if match.PosterURL != wantPoster {
|
||||
t.Fatalf("selected poster = %q, want %q", match.PosterURL, wantPoster)
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ func (p *MetaTubeProvider) convertSearchResultToMatch(cfg MetaTubeConfig, query
|
||||
year := parseYearFromDate(res.ReleaseDate)
|
||||
|
||||
posterSource := firstNonEmpty(res.BigCoverURL, res.CoverURL, res.BigThumbURL, res.ThumbURL)
|
||||
posterURL, backdropURL := metaTubeArtworkURLs(cfg, res.Provider, res.ID, posterSource)
|
||||
posterURL, backdropURL := metaTubeArtworkURLs(cfg, res.Provider, res.ID, posterSource, code)
|
||||
if posterURL == "" {
|
||||
posterURL = firstNonEmpty(res.BigThumbURL, res.ThumbURL, res.BigCoverURL, res.CoverURL)
|
||||
}
|
||||
@@ -396,7 +396,7 @@ func (p *MetaTubeProvider) convertMovieInfoToMatch(cfg MetaTubeConfig, movie *Me
|
||||
year := parseYearFromDate(movie.ReleaseDate)
|
||||
|
||||
posterSource := firstNonEmpty(movie.BigCoverURL, movie.CoverURL, movie.BigThumbURL, movie.ThumbURL)
|
||||
posterURL, backdropURL := metaTubeArtworkURLs(cfg, movie.Provider, movie.ID, posterSource)
|
||||
posterURL, backdropURL := metaTubeArtworkURLs(cfg, movie.Provider, movie.ID, posterSource, code)
|
||||
if posterURL == "" {
|
||||
posterURL = firstNonEmpty(movie.BigThumbURL, movie.ThumbURL, movie.BigCoverURL, movie.CoverURL)
|
||||
}
|
||||
@@ -440,7 +440,13 @@ func (p *MetaTubeProvider) convertMovieInfoToMatch(cfg MetaTubeConfig, movie *Me
|
||||
}
|
||||
}
|
||||
|
||||
func metaTubeArtworkURLs(cfg MetaTubeConfig, provider, id, posterSource string) (string, string) {
|
||||
// metaTubeArtworkURLs 构造 MetaTube 图片代理地址。
|
||||
//
|
||||
// auto(服务端人脸定位)只能对特殊番号(FC2 / HEYZO / 素人 / 无码)开启,这与 MetaTube
|
||||
// 官方插件的默认行为一致:常规 DVD 番号的封套是 [封底 | 书脊 | 封面] 横图,封底那半张是样本
|
||||
// 剧照拼贴、密布人脸,服务端的人脸主簇会落到拼贴/书脊上,裁出来就是「半张封底」。
|
||||
// 常规番号固定 pos=1(最右侧封面面板)才能拿到正确封面。
|
||||
func metaTubeArtworkURLs(cfg MetaTubeConfig, provider, id, posterSource, code string) (string, string) {
|
||||
serverURL := strings.TrimRight(strings.TrimSpace(cfg.ServerURL), "/")
|
||||
provider = strings.TrimSpace(provider)
|
||||
id = strings.TrimSpace(id)
|
||||
@@ -459,7 +465,7 @@ func metaTubeArtworkURLs(cfg MetaTubeConfig, provider, id, posterSource string)
|
||||
}
|
||||
if cfg.CropCover {
|
||||
values.Set("pos", "1")
|
||||
values.Set("auto", "true")
|
||||
values.Set("auto", strconv.FormatBool(adultCoverNeedsFaceDetection(code)))
|
||||
} else {
|
||||
values.Set("pos", "-1")
|
||||
values.Set("auto", "false")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -137,7 +138,8 @@ func TestMetaTubeProviderGetMovie(t *testing.T) {
|
||||
if match.Overview != "超绝美少女相沢みなみ出道作品!" {
|
||||
t.Errorf("unexpected overview: %s", match.Overview)
|
||||
}
|
||||
if want := server.URL + "/v1/images/primary/javdb/123456?auto=true&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fbig_cover.jpg"; match.PosterURL != want {
|
||||
// IPX-235 是常规 DVD 番号:服务端不得开启人脸定位(auto=false),否则主簇会落到封底的剧照拼贴。
|
||||
if want := server.URL + "/v1/images/primary/javdb/123456?auto=false&pos=1&quality=90&ratio=-1&url=https%3A%2F%2Fexample.com%2Fbig_cover.jpg"; match.PosterURL != want {
|
||||
t.Errorf("poster URL = %q, want %q", match.PosterURL, want)
|
||||
}
|
||||
if want := server.URL + "/v1/images/backdrop/javdb/123456?quality=90"; match.BackdropURL != want {
|
||||
@@ -187,3 +189,30 @@ func TestMetaTubeProviderTestConnection(t *testing.T) {
|
||||
t.Fatalf("expected failure for wrong token, got %v", resFail)
|
||||
}
|
||||
}
|
||||
|
||||
// MetaTube 代理的 auto 参数决定服务端是否用 Pigo 做人脸定位裁切。
|
||||
// 常规 DVD 番号的封套封底是剧照拼贴、密布人脸,开启 auto 会裁出「半张封底」;
|
||||
// 只有 FC2 / HEYZO / 素人 / 无码这类构图不固定的封面才应该开启。
|
||||
func TestMetaTubeArtworkURLsFaceDetectionPolicy(t *testing.T) {
|
||||
cfg := MetaTubeConfig{ServerURL: "https://mt.example", CropCover: true}
|
||||
cases := []struct {
|
||||
code string
|
||||
wantAuto string
|
||||
}{
|
||||
{"IPX-235", "false"},
|
||||
{"SSNI-677", "false"},
|
||||
{"NGOD-361", "false"},
|
||||
{"FC2-PPV-482896", "true"},
|
||||
{"HEYZO-1234", "true"},
|
||||
{"123456-789", "true"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
poster, _ := metaTubeArtworkURLs(cfg, "javdb", "123456", "https://example.com/pl.jpg", c.code)
|
||||
if !strings.Contains(poster, "auto="+c.wantAuto) {
|
||||
t.Errorf("%s: poster URL = %q, want auto=%s", c.code, poster, c.wantAuto)
|
||||
}
|
||||
if !strings.Contains(poster, "pos=1") {
|
||||
t.Errorf("%s: poster URL = %q, want pos=1", c.code, poster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,10 @@ func (s *ScraperService) downloadArtworkToPathWithOptions(ctx context.Context, d
|
||||
if cropAdultPoster {
|
||||
if cropped, croppedType, err := CropAdultCoverPosterForCode(data, adultCode); err == nil && len(cropped) > 0 {
|
||||
data = cropped
|
||||
ctype = croppedType
|
||||
// 裁剪函数在“无需裁剪”时会原样返回,类型可能为空:此时保留 sniff 出来的 MIME。
|
||||
if strings.TrimSpace(croppedType) != "" {
|
||||
ctype = croppedType
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.writeArtworkDataToPath(dir, name, ctype, data)
|
||||
@@ -197,9 +200,25 @@ func (s *ScraperService) writeArtworkDataToPath(dir, name, ctype string, data []
|
||||
return ""
|
||||
}
|
||||
s.log.Debug("scrape artwork written", zap.String("dst", dst))
|
||||
s.removeLegacyArtworkImg(dir, name, dst)
|
||||
return dst
|
||||
}
|
||||
|
||||
// removeLegacyArtworkImg 清理同名历史 "<name>.img" 文件。
|
||||
//
|
||||
// 早期版本把 image.Decode 的格式名("jpeg"/"webp")当作 MIME 写进扩展名映射,
|
||||
// 导致这些海报全部以 .img 落盘。现在写出正确扩展名后,旧 .img 只会是孤立垃圾,
|
||||
// 但 local_metadata_artwork 仍会扫到它,所以一并清掉。
|
||||
func (s *ScraperService) removeLegacyArtworkImg(dir, name, dst string) {
|
||||
legacy := filepath.Join(dir, name+".img")
|
||||
if strings.EqualFold(legacy, dst) {
|
||||
return
|
||||
}
|
||||
if err := os.Remove(legacy); err == nil {
|
||||
s.log.Debug("scrape artwork removed legacy .img", zap.String("dst", legacy))
|
||||
}
|
||||
}
|
||||
|
||||
// artworkFileUnchanged 报告 path 是否已经就是 data 这些字节。
|
||||
//
|
||||
// 刮削在刷新海报地址时会无条件重写 sidecar 图片,即使下载回来的字节一模一样。
|
||||
|
||||
@@ -172,6 +172,31 @@ func TestDownloadArtworkMigratesLocalImgToCanonicalCroppedJPEG(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 已经裁好的竖版海报(例如 MetaTube 服务端裁好的 2:3 图)再次写入时不能再退化成 .img:
|
||||
// 早期版本把 image.Decode 的格式名当 MIME 用,落盘扩展名就成了 .img。
|
||||
func TestDownloadArtworkKeepsCanonicalExtensionForPortraitSource(t *testing.T) {
|
||||
scraper := &ScraperService{log: zap.NewNop()}
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "movie-poster-src.jpg")
|
||||
portrait := createTestImage(400, 600, color.RGBA{R: 255, A: 255}, color.RGBA{B: 255, A: 255})
|
||||
if err := os.WriteFile(src, portrait, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 模拟历史遗留的 .img 退格式文件。
|
||||
legacy := filepath.Join(dir, "movie-poster.img")
|
||||
if err := os.WriteFile(legacy, portrait, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dst := scraper.downloadArtworkToPathWithOptions(t.Context(), dir, "movie-poster", src, true, "SSNI-677")
|
||||
if filepath.Base(dst) != "movie-poster.jpg" {
|
||||
t.Fatalf("destination = %q, want canonical .jpg (not .img)", dst)
|
||||
}
|
||||
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
|
||||
t.Fatalf("legacy .img should be cleaned up, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteMediaArtworkFilesAfterScrapeSkipsCloud verifies that cloud media
|
||||
// never has artwork written anywhere.
|
||||
func TestWriteMediaArtworkFilesAfterScrapeSkipsCloud(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user