From ec5f81e6cbe7626aad2dfb51f71cb873286d05fb Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:18:04 +0800 Subject: [PATCH] =?UTF-8?q?bug=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- internal/service/adult_image_crop.go | 1 + internal/service/adult_image_crop_test.go | 22 +++++++++++++++ internal/service/image_proxy_paths.go | 2 +- internal/service/image_proxy_test.go | 14 +++++++--- .../service/scraper_write_metadata_files.go | 24 ++++++++++++++--- .../scraper_write_metadata_files_test.go | 27 +++++++++++++++++++ 7 files changed, 83 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 88e3afc..0fe802c 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/ulikunitz/xz v0.5.12 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.49.0 + golang.org/x/image v0.37.0 golang.org/x/sys v0.42.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 @@ -86,7 +87,6 @@ require ( go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.25.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/image v0.37.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/internal/service/adult_image_crop.go b/internal/service/adult_image_crop.go index c92df90..825d550 100644 --- a/internal/service/adult_image_crop.go +++ b/internal/service/adult_image_crop.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/metatube-community/metatube-sdk-go/detector" + _ "golang.org/x/image/webp" // register WebP decoder for AVE covers ) const adultPosterRatio = 2.0 / 3.0 diff --git a/internal/service/adult_image_crop_test.go b/internal/service/adult_image_crop_test.go index b4458aa..9da1bf8 100644 --- a/internal/service/adult_image_crop_test.go +++ b/internal/service/adult_image_crop_test.go @@ -2,6 +2,7 @@ package service import ( "bytes" + "encoding/base64" "image" "image/color" "image/draw" @@ -58,6 +59,27 @@ func TestCropAdultCoverPosterWideLandscape(t *testing.T) { } } +func TestCropAdultCoverPosterDecodesWebP(t *testing.T) { + wideWebP, err := base64.StdEncoding.DecodeString("UklGRjwAAABXRUJQVlA4IDAAAADQAQCdASoJAAYAAUAmJaACdLoB+AADsAD+8ut//NgVzXPv9//S4P0uD9Lg/9KQAAA=") + if err != nil { + t.Fatal(err) + } + croppedBytes, ctype, err := CropAdultCoverPoster(wideWebP) + if err != nil { + t.Fatalf("crop WebP: %v", err) + } + if ctype != "image/jpeg" { + t.Fatalf("cropped WebP type = %q, want image/jpeg", ctype) + } + cropped, _, err := image.Decode(bytes.NewReader(croppedBytes)) + if err != nil { + t.Fatal(err) + } + if got := float64(cropped.Bounds().Dx()) / float64(cropped.Bounds().Dy()); got < 0.65 || got > 0.68 { + t.Fatalf("cropped WebP ratio = %.3f, want 2:3", got) + } +} + func TestCropAdultCoverPosterCentersDetectedFace(t *testing.T) { originalDetector := findPrimaryFaceAxisRatio findPrimaryFaceAxisRatio = func(image.Image, float64, bool) (float64, bool) { diff --git a/internal/service/image_proxy_paths.go b/internal/service/image_proxy_paths.go index a3d001a..de9c6ab 100644 --- a/internal/service/image_proxy_paths.go +++ b/internal/service/image_proxy_paths.go @@ -73,7 +73,7 @@ func isLocalImagePath(raw string) bool { } ext := strings.ToLower(filepath.Ext(raw)) switch ext { - case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn": + case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn", ".img": return true default: return false diff --git a/internal/service/image_proxy_test.go b/internal/service/image_proxy_test.go index 960f3cf..fe4ac85 100644 --- a/internal/service/image_proxy_test.go +++ b/internal/service/image_proxy_test.go @@ -13,6 +13,12 @@ import ( "github.com/truewhile/MeBox/internal/config" ) +func TestIsLocalImagePathAcceptsLegacyImgSidecar(t *testing.T) { + if !isLocalImagePath(filepath.Join(t.TempDir(), "movie-poster.img")) { + t.Fatal("legacy .img sidecar should remain displayable") + } +} + var testJPEG = []byte{ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F', 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, @@ -117,12 +123,12 @@ func TestIsPrivateHost(t *testing.T) { // Hostnames must NOT be blocked even though GFW DNS poisoning may resolve // them to private/loopback IPs — blocking them broke legitimate posters. allowed := []string{"image.tmdb.org", "lain.bgm.tv", "example.com", "8.8.8.8"} - for _, h := range allowed { - if isPrivateHost(h) { - t.Errorf("isPrivateHost(%q) = true, want false", h) - } + for _, h := range allowed { + if isPrivateHost(h) { + t.Errorf("isPrivateHost(%q) = true, want false", h) } } +} func TestImageProxyAllowedRemoteHostBypassesPrivateCheck(t *testing.T) { proxy := NewImageProxy(&config.Config{Cache: config.CacheConfig{CacheDir: filepath.Join(t.TempDir(), "cache")}}, zap.NewNop()) diff --git a/internal/service/scraper_write_metadata_files.go b/internal/service/scraper_write_metadata_files.go index 328d02a..ecf536a 100644 --- a/internal/service/scraper_write_metadata_files.go +++ b/internal/service/scraper_write_metadata_files.go @@ -91,10 +91,22 @@ func shouldCropAdultPoster(media *model.Media, lib *model.Library) bool { // downloadArtworkToPathWithOptions fetches an artwork URL via the image proxy cache and // writes it under dir/.. For adult posters, it crops the right half of the cover. func (s *ScraperService) downloadArtworkToPathWithOptions(ctx context.Context, dir, name, raw string, cropAdultPoster bool) string { - if !isHTTPish(raw) { + var ( + data []byte + ctype string + err error + ) + switch { + case isHTTPish(raw): + data, ctype, err = s.images.Fetch(ctx, raw) + case isLocalImagePath(raw): + data, err = os.ReadFile(sanitizeLocalPath(resolveMappedDestinationPath(raw))) + if err == nil { + ctype = detectContentType(data) + } + default: return "" } - data, ctype, err := s.images.Fetch(ctx, raw) if err != nil || len(data) == 0 { s.log.Warn("scrape artwork download failed", zap.String("name", name), @@ -105,6 +117,12 @@ func (s *ScraperService) downloadArtworkToPathWithOptions(ctx context.Context, d if !isImageContentType(ctype) || isTransparentPlaceholderData(data) { return "" } + // Upstreams sometimes report non-standard values such as image/jpg. + // Use the decoded bytes as the source of truth so sidecars receive a + // standard extension instead of the legacy .img fallback. + if detected := detectContentType(data); isImageContentType(detected) { + ctype = detected + } if cropAdultPoster { if cropped, croppedType, err := CropAdultCoverPoster(data); err == nil && len(cropped) > 0 { data = cropped @@ -166,7 +184,7 @@ func (s *ScraperService) writeArtworkDataToPath(dir, name, ctype string, data [] // extension that could confuse media players. func imageExtForContentType(ctype string) string { switch strings.ToLower(strings.TrimSpace(strings.Split(ctype, ";")[0])) { - case "image/jpeg", "image/pjpeg": + case "image/jpeg", "image/jpg", "image/pjpeg": return ".jpg" case "image/png", "image/x-png": return ".png" diff --git a/internal/service/scraper_write_metadata_files_test.go b/internal/service/scraper_write_metadata_files_test.go index 0e2612c..f80e8e7 100644 --- a/internal/service/scraper_write_metadata_files_test.go +++ b/internal/service/scraper_write_metadata_files_test.go @@ -88,6 +88,7 @@ func TestShouldCropAdultPosterUsesLibraryType(t *testing.T) { func TestImageExtForContentType(t *testing.T) { cases := map[string]string{ "image/jpeg": ".jpg", + "image/jpg": ".jpg", "image/pjpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", @@ -103,6 +104,32 @@ func TestImageExtForContentType(t *testing.T) { } } +func TestDownloadArtworkMigratesLocalImgToCanonicalCroppedJPEG(t *testing.T) { + scraper := &ScraperService{log: zap.NewNop()} + dir := t.TempDir() + legacy := filepath.Join(dir, "movie-poster.img") + wide := createTestImage(900, 600, color.RGBA{R: 255, A: 255}, color.RGBA{B: 255, A: 255}) + if err := os.WriteFile(legacy, wide, 0o644); err != nil { + t.Fatal(err) + } + + dst := scraper.downloadArtworkToPathWithOptions(t.Context(), dir, "movie-poster", legacy, true) + if filepath.Base(dst) != "movie-poster.jpg" { + t.Fatalf("destination = %q, want canonical JPEG sidecar", dst) + } + data, err := os.ReadFile(dst) + if err != nil { + t.Fatal(err) + } + cropped, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if got := float64(cropped.Bounds().Dx()) / float64(cropped.Bounds().Dy()); got < 0.65 || got > 0.68 { + t.Fatalf("migrated poster ratio = %.3f, want 2:3", got) + } +} + // TestWriteMediaArtworkFilesAfterScrapeSkipsCloud verifies that cloud media // never has artwork written anywhere. func TestWriteMediaArtworkFilesAfterScrapeSkipsCloud(t *testing.T) {