mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 21:06:38 +08:00
bug处理
This commit is contained in:
@@ -19,6 +19,7 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.12
|
github.com/ulikunitz/xz v0.5.12
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.49.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/sys v0.42.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -86,7 +87,6 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.25.0 // indirect
|
golang.org/x/arch v0.25.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // 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/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/metatube-community/metatube-sdk-go/detector"
|
"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
|
const adultPosterRatio = 2.0 / 3.0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"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) {
|
func TestCropAdultCoverPosterCentersDetectedFace(t *testing.T) {
|
||||||
originalDetector := findPrimaryFaceAxisRatio
|
originalDetector := findPrimaryFaceAxisRatio
|
||||||
findPrimaryFaceAxisRatio = func(image.Image, float64, bool) (float64, bool) {
|
findPrimaryFaceAxisRatio = func(image.Image, float64, bool) (float64, bool) {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func isLocalImagePath(raw string) bool {
|
|||||||
}
|
}
|
||||||
ext := strings.ToLower(filepath.Ext(raw))
|
ext := strings.ToLower(filepath.Ext(raw))
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn":
|
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn", ".img":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import (
|
|||||||
"github.com/truewhile/MeBox/internal/config"
|
"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{
|
var testJPEG = []byte{
|
||||||
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F',
|
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F',
|
||||||
0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01,
|
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
|
// Hostnames must NOT be blocked even though GFW DNS poisoning may resolve
|
||||||
// them to private/loopback IPs — blocking them broke legitimate posters.
|
// them to private/loopback IPs — blocking them broke legitimate posters.
|
||||||
allowed := []string{"image.tmdb.org", "lain.bgm.tv", "example.com", "8.8.8.8"}
|
allowed := []string{"image.tmdb.org", "lain.bgm.tv", "example.com", "8.8.8.8"}
|
||||||
for _, h := range allowed {
|
for _, h := range allowed {
|
||||||
if isPrivateHost(h) {
|
if isPrivateHost(h) {
|
||||||
t.Errorf("isPrivateHost(%q) = true, want false", h)
|
t.Errorf("isPrivateHost(%q) = true, want false", h)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestImageProxyAllowedRemoteHostBypassesPrivateCheck(t *testing.T) {
|
func TestImageProxyAllowedRemoteHostBypassesPrivateCheck(t *testing.T) {
|
||||||
proxy := NewImageProxy(&config.Config{Cache: config.CacheConfig{CacheDir: filepath.Join(t.TempDir(), "cache")}}, zap.NewNop())
|
proxy := NewImageProxy(&config.Config{Cache: config.CacheConfig{CacheDir: filepath.Join(t.TempDir(), "cache")}}, zap.NewNop())
|
||||||
|
|||||||
@@ -91,10 +91,22 @@ func shouldCropAdultPoster(media *model.Media, lib *model.Library) bool {
|
|||||||
// downloadArtworkToPathWithOptions fetches an artwork URL via the image proxy cache and
|
// downloadArtworkToPathWithOptions fetches an artwork URL via the image proxy cache and
|
||||||
// writes it under dir/<name>.<ext>. For adult posters, it crops the right half of the cover.
|
// writes it under dir/<name>.<ext>. 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 {
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
data, ctype, err := s.images.Fetch(ctx, raw)
|
|
||||||
if err != nil || len(data) == 0 {
|
if err != nil || len(data) == 0 {
|
||||||
s.log.Warn("scrape artwork download failed",
|
s.log.Warn("scrape artwork download failed",
|
||||||
zap.String("name", name),
|
zap.String("name", name),
|
||||||
@@ -105,6 +117,12 @@ func (s *ScraperService) downloadArtworkToPathWithOptions(ctx context.Context, d
|
|||||||
if !isImageContentType(ctype) || isTransparentPlaceholderData(data) {
|
if !isImageContentType(ctype) || isTransparentPlaceholderData(data) {
|
||||||
return ""
|
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 cropAdultPoster {
|
||||||
if cropped, croppedType, err := CropAdultCoverPoster(data); err == nil && len(cropped) > 0 {
|
if cropped, croppedType, err := CropAdultCoverPoster(data); err == nil && len(cropped) > 0 {
|
||||||
data = cropped
|
data = cropped
|
||||||
@@ -166,7 +184,7 @@ func (s *ScraperService) writeArtworkDataToPath(dir, name, ctype string, data []
|
|||||||
// extension that could confuse media players.
|
// extension that could confuse media players.
|
||||||
func imageExtForContentType(ctype string) string {
|
func imageExtForContentType(ctype string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(strings.Split(ctype, ";")[0])) {
|
switch strings.ToLower(strings.TrimSpace(strings.Split(ctype, ";")[0])) {
|
||||||
case "image/jpeg", "image/pjpeg":
|
case "image/jpeg", "image/jpg", "image/pjpeg":
|
||||||
return ".jpg"
|
return ".jpg"
|
||||||
case "image/png", "image/x-png":
|
case "image/png", "image/x-png":
|
||||||
return ".png"
|
return ".png"
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func TestShouldCropAdultPosterUsesLibraryType(t *testing.T) {
|
|||||||
func TestImageExtForContentType(t *testing.T) {
|
func TestImageExtForContentType(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"image/jpeg": ".jpg",
|
"image/jpeg": ".jpg",
|
||||||
|
"image/jpg": ".jpg",
|
||||||
"image/pjpeg": ".jpg",
|
"image/pjpeg": ".jpg",
|
||||||
"image/png": ".png",
|
"image/png": ".png",
|
||||||
"image/webp": ".webp",
|
"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
|
// TestWriteMediaArtworkFilesAfterScrapeSkipsCloud verifies that cloud media
|
||||||
// never has artwork written anywhere.
|
// never has artwork written anywhere.
|
||||||
func TestWriteMediaArtworkFilesAfterScrapeSkipsCloud(t *testing.T) {
|
func TestWriteMediaArtworkFilesAfterScrapeSkipsCloud(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user