Skip to content

refactor(api): make media image requests single-item#845

Merged
wizzomafizzo merged 2 commits into
mainfrom
refactor/media-image-single
May 25, 2026
Merged

refactor(api): make media image requests single-item#845
wizzomafizzo merged 2 commits into
mainfrom
refactor/media-image-single

Conversation

@wizzomafizzo
Copy link
Copy Markdown
Member

@wizzomafizzo wizzomafizzo commented May 25, 2026

Summary

  • remove bulk media.image responses and reject image batch requests
  • stop returning inline binary data from media.meta properties
  • add metadata-only property queries and capped blob reads for single-image responses

Breaking changes

  • media.image no longer accepts items; clients must request one image per call
  • media.meta.properties.*.data is removed; image bytes must come from media.image

Summary by CodeRabbit

  • New Features

    • Added optional blobSize to media.meta property responses; per-request size limits enforced for image fetches.
  • Documentation

    • Clarified media.meta returns metadata-only (no binary bytes) and directs clients to media.image for image bytes.
    • Updated media.image docs to list supported image types and state that batch image requests are not supported (one image per call).

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

This PR makes media.meta metadata-only (omits binary bytes, adds BlobSize), removes media.image batch support (single-image per request), enforces per-platform image size caps via capped blob/file reads, and adds metadata-only DB read APIs plus corresponding tests and mock updates.

Changes

Media API Refactor: Metadata-Only media.meta and Single-Image Capped media.image

Layer / File(s) Summary
API contract, docs, and models
docs/api/methods.md, docs/scraper.md, pkg/api/models/responses.go
Add blobSize to media.meta property schema, remove media.image batch response types, and update docs to state media.meta returns metadata-only properties and media.image does not support batch requests.
Server logging & tests
pkg/api/server.go, pkg/api/server_logging_test.go
Remove special-case logging for media.image batch responses and simplify batch-redaction test fixtures.
Database blob loading with size caps
pkg/database/database.go, pkg/database/mediadb/sql_blobs.go, pkg/database/mediadb/sql_blobs_test.go
Add ErrMediaBlobTooLarge and GetMediaBlobDataCapped to interface and implement SQL that enforces maxBytes, with tests for in-cap, over-cap, and not-found cases.
Metadata-only property DB queries
pkg/database/mediadb/sql_scraper.go, pkg/database/mediadb/sql_scraper_test.go
Introduce metadata-only SELECT builders, queries, and scanners plus four new DB methods (single and grouped for title/media properties) that omit binary payloads while returning BlobDBID/BlobSize/ContentType; add tests asserting Binary is nil.
Mock DB updates
pkg/testing/helpers/db_mocks.go
Add mock methods for metadata fetches and GetMediaBlobDataCapped, plus hasExpectedCall fallback logic to reuse non-metadata mock expectations when appropriate.
media.meta handler and tests
pkg/api/methods/media_meta.go, pkg/api/methods/media_meta_test.go
Switch to metadata-only DB calls, remove base64 inline data encoding, populate BlobSize and update tests and error messages to reflect metadata-only behavior.
media.image handler and tests
pkg/api/methods/media_image.go, pkg/api/methods/media_image_test.go
Refactor to strict single-image request parsing, compute per-platform maxBytes, enforce capped blob/file reads via new helpers, remove batch-response construction, and add tests asserting requests with items are rejected.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Sequence Diagram

sequenceDiagram
  participant Client
  participant API_Server
  participant MediaDB
  participant Filesystem
  Client->>API_Server: POST media.image (single request)
  API_Server->>MediaDB: Resolve media refs / GetMediaPropertyMetadata
  alt blob-backed property
    API_Server->>MediaDB: GetMediaBlobDataCapped(blobDBID, maxBytes)
    MediaDB-->>API_Server: (data or ErrMediaBlobTooLarge)
  else file-backed property
    API_Server->>Filesystem: Read file (capped)
    Filesystem-->>API_Server: (bytes or not found)
  end
  API_Server-->>Client: media.image response (base64 image or error)
Loading

Possibly Related PRs

Suggested Reviewers

  • BossRighteous

Poem

🐰 I hopped through docs and DB, kept nimble and spry,
I clipped big blobs and said "one image, no try!"
Metadata only, BlobSize in sight,
Caps keep bytes tidy, the handlers sleep tight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(api): make media image requests single-item' accurately and concisely summarizes the primary change: converting media image handling from batch to single-item requests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/media-image-single

Comment @coderabbitai help to get the list of available commands and usage tips.

@sentry
Copy link
Copy Markdown

sentry Bot commented May 25, 2026

Codecov Report

❌ Patch coverage is 63.92857% with 101 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/api/methods/media_image.go 58.82% 39 Missing and 10 partials ⚠️
pkg/database/mediadb/sql_scraper.go 66.17% 23 Missing and 23 partials ⚠️
pkg/database/mediadb/sql_blobs.go 75.00% 2 Missing and 2 partials ⚠️
pkg/api/methods/media_meta.go 77.77% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/api/methods/media_image_test.go (1)

226-283: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add one test that hits the params.Batch rejection path explicitly.

These two tests validate the raw items guard, but not the later params.Batch branch. A dedicated case for that path would lock behavior for both rejection mechanisms.

As per coding guidelines, **/*.go: Write tests for all new code — see TESTING.md and pkg/testing/README.md”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/api/methods/media_image_test.go` around lines 226 - 283, Add a new unit
test that exercises the params.Batch rejection path by calling HandleMediaImage
with params that set "batch": true (e.g., env := makeMediaImageEnv(t, mockDB,
json.RawMessage(`{"batch": true}`)) or include "batch": true alongside minimal
fields), then assert an error is returned, that it is a *models.ClientError (use
require.ErrorAs) and that err.Error() contains "batch requests are no longer
supported"; follow the existing test patterns (create test helper mockDB via
testhelpers.NewMockMediaDBI(), call HandleMediaImage(env), and call
mockDB.AssertExpectations(t)) and name it like
TestHandleMediaImage_BatchParamReturnsClientError to mirror other tests.
🧹 Nitpick comments (1)
pkg/api/methods/media_meta_test.go (1)

135-166: ⚡ Quick win

Assert metadata DB method usage directly in these tests.

These cases still stub legacy GetMediaProperties* / GetMediaTitleProperties* calls, so they can pass via mock fallback without proving HandleMediaMeta is using the new metadata query methods. Please set expectations on GetMediaPropertyMetadata* / GetMediaTitlePropertyMetadata* in these paths.

Suggested expectation pattern update
- mockDB.On("GetMediaProperties", mock.Anything, int64(3)).
+ mockDB.On("GetMediaPropertyMetadata", mock.Anything, int64(3)).
    Return([]database.MediaProperty{
      {TypeTag: "property:image", ContentType: "image/png", BlobSize: int64(len(blobData))},
    }, nil)

- mockDB.On("GetMediaTitleProperties", mock.Anything, int64(30)).
+ mockDB.On("GetMediaTitlePropertyMetadata", mock.Anything, int64(30)).
    Return([]database.MediaProperty{}, nil)

As per coding guidelines, **/*.go: "Write tests for all new code — see TESTING.md and pkg/testing/README.md".

Also applies to: 425-425, 444-444

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/api/methods/media_meta_test.go` around lines 135 - 166, The tests
TestHandleMediaMeta_BinaryPropertyMetadataOnly (and the OversizedBinary variant)
currently only stub legacy GetMediaProperties/GetMediaTitleProperties, allowing
HandleMediaMeta to pass via fallback; update these tests to assert the new
metadata queries are used by adding mock expectations for
GetMediaPropertyMetadata (for media DBID 3) and GetMediaTitlePropertyMetadata
(for title DBID 30) returning the same MediaProperty metadata (or empty slice
where appropriate), and remove or tighten the legacy
GetMediaProperties*/GetMediaTitleProperties* stubs so the test fails if
HandleMediaMeta still calls the old methods; keep the existing checks on
resp.Media.Properties, BlobSize, ContentType, and Extension unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/api/methods/media_image.go`:
- Around line 324-360: Summary: Replace direct os filesystem calls in
loadMediaImageFile with an injected afero.Fs to make filesystem access testable.
Change the signature of loadMediaImageFile to accept an afero.Fs (e.g., fs
afero.Fs), replace os.Stat with fs.Stat, keep the errors.Is(err, os.ErrNotExist)
checks, and call a version of readMediaBinaryFile that accepts the same afero.Fs
(update readMediaBinaryFile signature and implementation to use
afero.ReadFile/Open). Ensure mediaImageReadError, deleteStaleMediaImageProperty,
and callers of loadMediaImageFile are updated to pass the fs through so all
filesystem access in this path uses the injected afero.Fs.
- Around line 299-305: GetMediaBlobDataCapped currently returns an empty byte
slice for both "too large" and "missing" blobs, so the handler wrongly maps
empty results to mediaImageBlobTooLargeError. Update the handler around the
GetMediaBlobDataCapped call to distinguish missing blobs from capped blobs: if
binary is empty, first check for the blob's existence (e.g., call a DB method
that can return ErrNotFound or a boolean for *prop.BlobDBID) or change
GetMediaBlobDataCapped to return a sentinel ErrBlobNotFound; when the blob is
missing, delete the stale media metadata (remove the row referencing
*prop.BlobDBID) and fall back to the alternate handling path instead of
returning mediaImageBlobTooLargeError, and only use mediaImageBlobTooLargeError
when the DB signals an explicit size cap condition.

In `@pkg/api/server_logging_test.go`:
- Around line 105-110: The test uses a sentinel string secretBlob but never
injects it into the fixture, so the NotContains(secretBlob) assertion is
vacuous; update the fixture so the sentinel is actually present (e.g., set
textValue to secretBlob or assign secretBlob to
models.MediaMetaPropertyItem.Text in the metaProp instance) so the redaction
check exercises per-item details (also apply the same change for the duplicate
fixture at lines referenced around the other occurrence).

In `@pkg/database/mediadb/sql_blobs_test.go`:
- Around line 166-195: Add a test that verifies GetMediaBlobDataCapped returns
(nil, "", nil) for an unknown blob DB id: create a new test function (e.g.,
TestGetMediaBlobDataCapped_NotFound) that uses setupScraperTestDB and
context.Background(), call mediaDB.GetMediaBlobDataCapped with a non-existent
dbid (e.g., 0 or a large unlikely id), and assert that err is nil, the returned
data is nil and the contentType is empty (use require.NoError(t, err),
assert.Nil(t, got), assert.Empty(t, contentType)); reference the existing test
file functions TestGetMediaBlobDataCapped_FoundWithinCap and
TestGetMediaBlobDataCapped_OverCapReturnsNil for structure and naming.

In `@pkg/database/mediadb/sql_scraper_test.go`:
- Around line 1053-1074: Add parallel table tests mirroring
TestGetMediaTitlePropertyMetadata_WithBlobOmitsBinary for the other query paths:
GetMediaPropertyMetadata, GetMediaPropertyMetadataByMediaDBIDs, and
GetMediaTitlePropertyMetadataByMediaTitleDBIDs. For each test use
setupScraperTestDB, UpsertMediaBlob to create a blob and
UpsertMediaTitleProperties/UpsertMediaProperties (as appropriate) to attach a
property with BlobDBID, then call the target getter and assert BlobDBID equals
the inserted id, BlobSize equals len(data), ContentType equals "image/png", and
Binary is nil; keep the same ctx/cleanup pattern and parallel t.Parallel()
setup. Ensure one test covers the grouped-query variants (ByMediaDBIDs and
ByMediaTitleDBIDs) by passing slices of IDs to those functions so grouped
scanning/composition is exercised.

---

Outside diff comments:
In `@pkg/api/methods/media_image_test.go`:
- Around line 226-283: Add a new unit test that exercises the params.Batch
rejection path by calling HandleMediaImage with params that set "batch": true
(e.g., env := makeMediaImageEnv(t, mockDB, json.RawMessage(`{"batch": true}`))
or include "batch": true alongside minimal fields), then assert an error is
returned, that it is a *models.ClientError (use require.ErrorAs) and that
err.Error() contains "batch requests are no longer supported"; follow the
existing test patterns (create test helper mockDB via
testhelpers.NewMockMediaDBI(), call HandleMediaImage(env), and call
mockDB.AssertExpectations(t)) and name it like
TestHandleMediaImage_BatchParamReturnsClientError to mirror other tests.

---

Nitpick comments:
In `@pkg/api/methods/media_meta_test.go`:
- Around line 135-166: The tests TestHandleMediaMeta_BinaryPropertyMetadataOnly
(and the OversizedBinary variant) currently only stub legacy
GetMediaProperties/GetMediaTitleProperties, allowing HandleMediaMeta to pass via
fallback; update these tests to assert the new metadata queries are used by
adding mock expectations for GetMediaPropertyMetadata (for media DBID 3) and
GetMediaTitlePropertyMetadata (for title DBID 30) returning the same
MediaProperty metadata (or empty slice where appropriate), and remove or tighten
the legacy GetMediaProperties*/GetMediaTitleProperties* stubs so the test fails
if HandleMediaMeta still calls the old methods; keep the existing checks on
resp.Media.Properties, BlobSize, ContentType, and Extension unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67e1147e-5e46-44ed-bd47-2b0d0a9de03c

📥 Commits

Reviewing files that changed from the base of the PR and between fec7496 and 2c3271b.

📒 Files selected for processing (15)
  • docs/api/methods.md
  • docs/scraper.md
  • pkg/api/methods/media_image.go
  • pkg/api/methods/media_image_test.go
  • pkg/api/methods/media_meta.go
  • pkg/api/methods/media_meta_test.go
  • pkg/api/models/responses.go
  • pkg/api/server.go
  • pkg/api/server_logging_test.go
  • pkg/database/database.go
  • pkg/database/mediadb/sql_blobs.go
  • pkg/database/mediadb/sql_blobs_test.go
  • pkg/database/mediadb/sql_scraper.go
  • pkg/database/mediadb/sql_scraper_test.go
  • pkg/testing/helpers/db_mocks.go
💤 Files with no reviewable changes (1)
  • pkg/api/server.go

Comment thread pkg/api/methods/media_image.go
Comment thread pkg/api/methods/media_image.go
Comment thread pkg/api/server_logging_test.go
Comment thread pkg/database/mediadb/sql_blobs_test.go
Comment thread pkg/database/mediadb/sql_scraper_test.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
pkg/api/methods/media_image.go (1)

300-303: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not treat all empty blobs as stale records.

len(binary) == 0 conflates “blob exists but empty” with “blob missing”, and can delete valid metadata. Gate stale deletion on an explicit not-found signal (e.g., empty contentType from DB contract), not byte length alone.

Suggested fix
-		if len(binary) == 0 {
+		// Not found returns (nil, "", nil); only then mark stale.
+		if contentType == "" {
 			deleteStaleMediaImageProperty(ctx, db, row, prop, src.isMedia, typeTag)
 			return nil, true, nil
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/api/methods/media_image.go` around lines 300 - 303, The code currently
treats len(binary) == 0 as a "missing blob" and calls
deleteStaleMediaImageProperty, which can delete valid metadata for an
existing-but-empty blob; instead gate stale-deletion on an explicit not-found
signal from the DB contract (e.g., check the contentType field), not byte
length. Modify the conditional around deleteStaleMediaImageProperty in the
function handling media image rows to use the DB-provided not-found indicator
(for example check row.ContentType == "" or whatever explicit field the DB uses)
and only call deleteStaleMediaImageProperty(row, prop, src.isMedia, typeTag)
when that explicit not-found flag is present; leave cases where binary is empty
but contentType indicates existence untouched and return as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@pkg/api/methods/media_image.go`:
- Around line 300-303: The code currently treats len(binary) == 0 as a "missing
blob" and calls deleteStaleMediaImageProperty, which can delete valid metadata
for an existing-but-empty blob; instead gate stale-deletion on an explicit
not-found signal from the DB contract (e.g., check the contentType field), not
byte length. Modify the conditional around deleteStaleMediaImageProperty in the
function handling media image rows to use the DB-provided not-found indicator
(for example check row.ContentType == "" or whatever explicit field the DB uses)
and only call deleteStaleMediaImageProperty(row, prop, src.isMedia, typeTag)
when that explicit not-found flag is present; leave cases where binary is empty
but contentType indicates existence untouched and return as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dfd024d8-fb54-4e01-8f1d-dbc1f6f718de

📥 Commits

Reviewing files that changed from the base of the PR and between 2c3271b and 5ca31c6.

📒 Files selected for processing (10)
  • docs/api/methods.md
  • docs/scraper.md
  • pkg/api/methods/media_image.go
  • pkg/api/methods/media_image_test.go
  • pkg/api/methods/media_meta_test.go
  • pkg/api/server_logging_test.go
  • pkg/database/database.go
  • pkg/database/mediadb/sql_blobs.go
  • pkg/database/mediadb/sql_blobs_test.go
  • pkg/database/mediadb/sql_scraper_test.go
💤 Files with no reviewable changes (1)
  • docs/api/methods.md
✅ Files skipped from review due to trivial changes (1)
  • docs/scraper.md

@wizzomafizzo wizzomafizzo merged commit 872bb1a into main May 25, 2026
12 checks passed
@wizzomafizzo wizzomafizzo deleted the refactor/media-image-single branch May 25, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant