Skip to content

fix(media): align browse container resolution with media.meta and bound its batch - #1383

Open
wizzomafizzo wants to merge 2 commits into
mainfrom
fix/container-cap-and-alias-batch
Open

fix(media): align browse container resolution with media.meta and bound its batch#1383
wizzomafizzo wants to merge 2 commits into
mainfrom
fix/container-cap-and-alias-batch

Conversation

@wizzomafizzo

@wizzomafizzo wizzomafizzo commented Sep 1, 2026

Copy link
Copy Markdown
Member

Summary

pkg/database/container exists so one rule decides when a directory of indexed media collapses to a single launch target. Browse did not use that rule as written: isSingletonDirectoryAliasCandidate dropped any directory whose recursive FileCount exceeded 64 before the resolver was asked. FindSingleContainerLaunchMedia, behind media.meta and media.image, and container.Index, behind the scrapers, apply no size limit, so the three disagreed about the same folder. docs/api/methods.md and docs/scraper.md already described the rule without a limit, so the code was the outlier and neither needed an edit.

Removing the cap makes every directory on a page a candidate, which is the input that blows up #1377, so the batch is bounded first.

  • ResolveSingletonContainerAliases runs its ParentDir IN scan in aliasCandidatesPerQuery-sized chunks. GetMediaWithTitleAndSystemByIDs and GetMediaTagsByMediaDBIDs chunk at source rather than at the call site: the first is also reached from the media.meta batch path, the second from four more callers, all with caller-sized ID lists. Each loop checks the context between chunks so a cancelled request stops instead of running the rest.
  • Candidates and IDs are deduplicated before chunking. One IN list returns a row once however many times its key appears, but a key landing in two chunks reads its rows twice, and the alias scan appends per ParentDir — a repeated directory would look like it held twice the files and stop collapsing.
  • isSingletonDirectoryAliasCandidate is gone. A directory qualifies on FileCount > 0, and the container rule alone decides what collapses.
  • The step-timing debug line gains inScanChunks.

The cap's own comment called it a batch-size guard. Its stated fear was misplaced twice over: the batch query reads only ParentDir = <dir>/ rows, so a tree of subdirectories returns nothing and costs one index seek, and the example it named — MiSTer's _Arcade/_alternatives — holds media for a dozen systems and is already excluded by the single-system check above it. The two tests that encoded the cap are retargeted rather than deleted: one now asserts the multi-system guard that genuinely protects that directory, the other that a large directory is offered to the resolver and stays plain because no alias comes back. Three unrelated browse tests held fixtures that now reach the resolver and gained alias stubs.

Closes #1377
Closes #1378

Verified

task test, task lint, task cross-lint:all.

Both fixes were reproduced and confirmed on the MiSTer test device, A/B against origin/main with the same corpus under both binaries.

media.browse of the parent against media.meta on the folder path:

folder files main browse branch browse media.meta
1 .cue + 3 .bin 4 promoted promoted resolves
1 .cue + 63 .bin 64 promoted promoted resolves
1 .cue + 64 .bin 65 plain dir promoted resolves
1 .cue + 70 .bin 71 plain dir promoted resolves
1 .m3u + 100 .chd 101 plain dir promoted resolves
2 .cue + 68 .bin 70 plain dir plain dir no resolve
media in a subdirectory 2 plain dir plain dir no resolve

64 promoting and 65 not is the cap exactly. The media-folder scraper attached media/boxart/Y_ManyBins.png to the 71-file folder's cue, and the promoted browse entry reports hasCover for it — only that folder.

media.browse of 1000 one-cue directories:

maxResults main branch
100 0.83s, 100 promoted 0.80s, 100
500 1.36s, 500 promoted 1.62s, 500
999 30.1s, 0 promoted 2.61s, 999
1000 30.1s, 0 promoted 2.44s, 1000

Cost

Larger directories are now candidates, at roughly 0.2ms per direct row, spent proving directories are not containers. On the device's stock library:

page main branch rows scanned
/media/fat/games/MegaDrive 82ms 225ms 745
/media/fat/games/NES 179ms 316ms 584

This is the price of one shared definition. A guard would buy back a tenth of a second on pages of large flat ROM folders and reintroduce the disagreement.

Summary by CodeRabbit

  • Bug Fixes
    • Large disc folders with many tracks can now be correctly recognized as media-backed directories.
    • Singleton directory aliases are resolved more reliably across systems and larger collections.
    • Repeated or extensive media lookups are handled more consistently, improving browse results and metadata display.
    • Empty directories remain excluded from alias resolution.

ResolveSingletonContainerAliases built one ParentDir IN list for every
candidate directory on a browse page, then passed every resolved media ID
to GetMediaWithTitleAndSystemByIDs in a single call. A browse page size is
client-supplied, so on a page of ~1000 container directories that call
exceeded the 30s request budget: the whole resolution was abandoned and the
page came back with every directory unpromoted after a 30 second wait.

The ParentDir scan now runs in aliasCandidatesPerQuery-sized chunks, and the
two shared by-ID lookups chunk at source rather than at the call site, since
GetMediaWithTitleAndSystemByIDs is also reached from the media.meta batch
path and GetMediaTagsByMediaDBIDs from four more callers, all with
caller-sized ID lists. Each loop checks the context between chunks so a
cancelled request stops rather than running the rest.

Candidates and IDs are deduplicated before chunking. One IN list returns a
row once however many times its key appears, but a key landing in two chunks
would read its rows twice, and the alias scan appends per ParentDir: a
repeated directory would look like it held twice the files and stop
collapsing.

Measured on MiSTer against 1000 one-cue directories:

  maxResults   before                after
  100          0.83s, 100 promoted   0.80s, 100
  500          1.36s, 500 promoted   1.62s, 500
  999          30.1s, 0 promoted     2.61s, 999
  1000         30.1s, 0 promoted     2.44s, 1000

Closes #1377
isSingletonDirectoryAliasCandidate dropped any directory whose recursive
FileCount exceeded 64 before browse asked whether it collapsed.
FindSingleContainerLaunchMedia, behind media.meta and media.image, and
container.Index, behind the scrapers, apply no such cap, so the three
disagreed about what counts as a container. A folder holding one .cue and 70
.bin tracks browsed as a plain, coverless, non-launchable directory while
media.meta resolved it to the cue, the media-folder scraper attached
folder-named artwork to that row, and a <folder> entry in gamelist.xml wrote
metadata to it. Real containers were excluded outright: a disc folder over 64
tracks, or an .m3u set spanning enough discs.

The cap was a batch-size guard, as its own comment said, sitting in front of
the semantics instead of in front of the batch. Its stated fear was misplaced
twice over: the batch query reads only ParentDir = <dir>/ rows, so a tree of
subdirectories returns nothing and costs one index seek, and the example it
named, MiSTer's _Arcade/_alternatives, holds media for a dozen systems and is
already excluded by the single-system check above it. Bounding the batch is
now that query's own job.

Every directory holding media for the single system in scope is a candidate,
and the container rule alone decides what collapses. docs/api/methods.md and
docs/scraper.md already described the rule without a size limit.

Verified on MiSTer against a purpose-built corpus, browse against media.meta
on the same paths. 64 files promoted and 65 did not before the change; both
promote after, along with the 71-file cue set and a 101-file m3u set, while an
ambiguous two-cue folder and one holding media in a subdirectory stay plain
directories. The promoted 71-file folder reports hasCover for the artwork the
media-folder scraper wrote under its name.

Removing the cap makes larger directories candidates, which costs about 0.2ms
per direct row on pages that hold big flat ROM folders. On MiSTer's stock
library that is browse of /media/fat/games/MegaDrive going from 82ms to 225ms
over 745 rows, and /media/fat/games/NES from 179ms to 316ms over 584 rows.

Closes #1378
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: bad4dfa4-9f61-4ab4-9917-b8f51804fca4

📥 Commits

Reviewing files that changed from the base of the PR and between 4023de5 and ac46fba.

📒 Files selected for processing (4)
  • pkg/api/methods/media_browse.go
  • pkg/api/methods/media_browse_test.go
  • pkg/database/mediadb/sql_scraper.go
  • pkg/database/mediadb/sql_scraper_test.go

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The change chunks media ID, tag, and singleton alias queries. It removes browse’s 64-file candidate limit while excluding empty directories. Tests cover chunk boundaries, duplicate candidates, cancellation, and promotion of a 71-file disc folder.

Changes

Media alias resolution

Layer / File(s) Summary
Chunked media lookups
pkg/database/mediadb/sql_scraper.go, pkg/database/mediadb/sql_scraper_test.go
Media and tag lookups deduplicate IDs and process them in chunks of up to 200 entries. Tests cover large ID lists and repeated IDs.
Chunked singleton alias scanning
pkg/database/mediadb/sql_scraper.go, pkg/database/mediadb/sql_scraper_test.go
Singleton alias resolution deduplicates directories, scans ParentDir values in chunks, checks cancellation, and reports scan chunk counts. Tests cover boundaries, duplicates, cancellation, and a many-track disc folder.
Browse eligibility and promotion
pkg/api/methods/media_browse.go, pkg/api/methods/media_browse_test.go
Browse considers every directory with media, removes the 64-file cap, and retains multi-system filtering. Tests verify large-directory candidates and 71-file directory promotion.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: 🔵 Low · up to ac46f

Large directories can now be promoted consistently, but a browse request may scan and retain all direct media rows for each candidate, consuming significant database and browse capacity on unusually large libraries. Existing chunking, cancellation, and concurrency limits reduce the exposure; the PR is mergeable with explicit owner awareness of adding an aggregate work limit.

Sequence Diagram(s)

sequenceDiagram
  participant BrowseAPI
  participant ResolveSingletonContainerAliases
  participant MediaDB
  BrowseAPI->>ResolveSingletonContainerAliases: submit candidate directories
  ResolveSingletonContainerAliases->>MediaDB: scan ParentDir values in chunks
  MediaDB-->>ResolveSingletonContainerAliases: return direct media rows
  ResolveSingletonContainerAliases->>MediaDB: fetch media metadata in ID chunks
  MediaDB-->>ResolveSingletonContainerAliases: return titles and systems
  ResolveSingletonContainerAliases-->>BrowseAPI: return resolved aliases
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 4 files. 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 clearly summarizes both primary changes: aligning browse container resolution with shared media behavior and bounding batch processing.
Linked Issues check ✅ Passed The changes satisfy both linked issues. [#1377] adds chunked and cancellable media, tag, and container scans with deduplicated IDs. [#1378] removes the 64-file eligibility cap and preserves the requir…
Out of Scope Changes check ✅ Passed The implementation and tests remain within the linked objectives. The added helpers, counters, test stubs, and regression coverage directly support bounded alias resolution and consistent large-contai…
Full details: Linked Issues check

Explanation

The changes satisfy both linked issues. [#1377] adds chunked and cancellable media, tag, and container scans with deduplicated IDs. [#1378] removes the 64-file eligibility cap and preserves the required exclusions for multi-system and nested-media directories. Tests cover chunk boundaries, large valid containers, deduplication, cancellation, and metadata promotion.

Full details: Out of Scope Changes check

Explanation

The implementation and tests remain within the linked objectives. The added helpers, counters, test stubs, and regression coverage directly support bounded alias resolution and consistent large-container handling.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/container-cap-and-alias-batch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.05556% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/database/mediadb/sql_scraper.go 69.01% 12 Missing and 10 partials ⚠️
pkg/api/methods/media_browse.go 0.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant