Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 120 additions & 69 deletions docs/scraper.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions pkg/api/methods/media_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ func mediaContentExtension(contentType, text string) *string {
return nil
}

func mediaContentType(contentType, text string) string {
trimmedContentType := strings.TrimSpace(contentType)
if trimmedContentType != "" {
return trimmedContentType
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ext := extensionFromTextPath(text)
if ext == "" {
return ""
}
return mime.TypeByExtension("." + ext)
}

func extensionFromContentType(contentType string) string {
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
Expand Down
32 changes: 32 additions & 0 deletions pkg/api/methods/media_content_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Zaparoo Core
// Copyright (c) 2026 The Zaparoo Project Contributors.
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of Zaparoo Core.
//
// Zaparoo Core is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Zaparoo Core is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.

package methods

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestMediaContentType_TrimsProvidedContentType(t *testing.T) {
t.Parallel()

assert.Equal(t, "image/png", mediaContentType(" image/png ", "cover.jpg"))
}
77 changes: 55 additions & 22 deletions pkg/api/methods/media_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"errors"
"fmt"
"os"
"sort"

"github.com/ZaparooProject/zaparoo-core/v2/pkg/api/models"
"github.com/ZaparooProject/zaparoo-core/v2/pkg/api/models/requests"
Expand All @@ -45,12 +46,13 @@ const (

// defaultImageTypes is the preference order used when no imageTypes param is provided.
var defaultImageTypes = []string{
"image", "boxart", "boxart3d", "screenshot", "wheel", "titleshot", "map",
"image", "thumbnail", "boxart", "boxart3d", "screenshot", "wheel", "titleshot", "map",
"marquee", "fanart",
}

var imageTypeTags = map[string]string{
"image": tags.PropertyTypeTag(tags.TagPropertyImageImage),
"thumbnail": tags.PropertyTypeTag(tags.TagPropertyImageThumbnail),
"boxart": tags.PropertyTypeTag(tags.TagPropertyImageBoxart),
"boxart3d": tags.PropertyTypeTag(tags.TagPropertyImageBoxart3D),
"boxartside": tags.PropertyTypeTag(tags.TagPropertyImageBoxartSide),
Expand Down Expand Up @@ -93,6 +95,27 @@ func buildPropsMap(props []database.MediaProperty) map[string]database.MediaProp
return m
}

func imagePropertyTypeTags(props []database.MediaProperty) []string {
known := make(map[string]struct{}, len(imageTypeTags))
for _, typeTag := range imageTypeTags {
known[typeTag] = struct{}{}
}

seen := make(map[string]struct{})
for _, p := range props {
if _, ok := known[p.TypeTag]; ok {
seen[p.TypeTag] = struct{}{}
}
}

result := make([]string, 0, len(seen))
for typeTag := range seen {
result = append(result, typeTag)
}
sort.Strings(result)
return result
}

// HandleMediaImage returns a single best-match image for a media record as a
// base64-encoded blob.
func HandleMediaImage(env requests.RequestEnv) (any, error) { //nolint:gocritic // single-use parameter in API handler
Expand Down Expand Up @@ -258,8 +281,18 @@ func selectMediaImage(
}
}

log.Debug().
Str("system", row.System.SystemID).
Str("path", row.Path).
Int64("mediaDBID", row.DBID).
Int64("titleDBID", row.Title.DBID).
Strs("prefs", prefs).
Strs("mediaImageProps", imagePropertyTypeTags(mediaProps)).
Strs("titleImageProps", imagePropertyTypeTags(titleProps)).
Msg("media.image: no image found")

return models.MediaImageResponse{}, models.ClientErrf(
"no image found for media: %s/%s", row.System.SystemID, row.Path,
"no image found for media: system=%q path=%q", row.System.SystemID, row.Path,
)
}

Expand All @@ -274,7 +307,7 @@ func loadMediaImageProperty(
maxBytes int64,
) (*models.MediaImageResponse, bool, error) {
var binary []byte
contentType := prop.ContentType
contentType := mediaContentType(prop.ContentType, prop.Text)

switch {
case len(prop.Binary) > 0:
Expand All @@ -298,11 +331,11 @@ func loadMediaImageProperty(
return nil, false, fmt.Errorf("media.image: read image blob %d: %w", *prop.BlobDBID, err)
}
if len(binary) == 0 {
deleteStaleMediaImageProperty(ctx, db, row, prop, src.isMedia, typeTag)
logStaleMediaImageProperty(row, prop, src.isMedia, typeTag, "empty blob data")
return nil, true, nil
}
case prop.Text != "":
data, stale, err := loadMediaImageFile(ctx, fs, db, row, prop, src.isMedia, typeTag, maxBytes)
data, stale, err := loadMediaImageFile(fs, row, prop, src.isMedia, typeTag, maxBytes)
if stale || err != nil {
return nil, stale, err
}
Expand All @@ -320,9 +353,7 @@ func loadMediaImageProperty(
}

func loadMediaImageFile(
ctx context.Context,
fs afero.Fs,
db database.MediaDBI,
row *database.MediaFullRow,
prop *database.MediaProperty,
isMedia bool,
Expand All @@ -332,7 +363,7 @@ func loadMediaImageFile(
info, err := fs.Stat(prop.Text)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
deleteStaleMediaImageProperty(ctx, db, row, prop, isMedia, typeTag)
logStaleMediaImageProperty(row, prop, isMedia, typeTag, err.Error())
return nil, true, nil
}
return nil, false, mediaImageReadError(prop.Text, err)
Expand All @@ -351,33 +382,35 @@ func loadMediaImageFile(
data, readErr := readMediaBinaryFile(fs, prop.Text, maxBytes)
if readErr != nil {
if errors.Is(readErr, os.ErrNotExist) {
deleteStaleMediaImageProperty(ctx, db, row, prop, isMedia, typeTag)
logStaleMediaImageProperty(row, prop, isMedia, typeTag, readErr.Error())
return nil, true, nil
}
return nil, false, mediaImageReadError(prop.Text, readErr)
}
return data, false, nil
}

func deleteStaleMediaImageProperty(
ctx context.Context,
db database.MediaDBI,
func logStaleMediaImageProperty(
row *database.MediaFullRow,
prop *database.MediaProperty,
isMedia bool,
typeTag string,
reason string,
) {
if !isMedia {
if delErr := db.DeleteMediaTitleProperty(ctx, row.Title.DBID, prop.TypeTagDBID); delErr != nil {
log.Warn().Err(delErr).Int64("titleDBID", row.Title.DBID).Str("typeTag", typeTag).
Msg("media.image: failed to delete stale title property")
}
return
}
if delErr := db.DeleteMediaProperty(ctx, row.DBID, prop.TypeTagDBID); delErr != nil {
log.Warn().Err(delErr).Int64("mediaDBID", row.DBID).Str("typeTag", typeTag).
Msg("media.image: failed to delete stale media property")
level := "title"
if isMedia {
level = "media"
}
log.Debug().
Str("system", row.System.SystemID).
Str("path", row.Path).
Int64("mediaDBID", row.DBID).
Int64("titleDBID", row.Title.DBID).
Str("typeTag", typeTag).
Str("text", prop.Text).
Str("source", level).
Str("reason", reason).
Msg("media.image: stale image property ignored")
}

func mediaImageMaxBytes(pl platforms.Platform) int64 {
Expand Down
Loading
Loading