My 1.17 MB player's catalog grid: the channelId trap, overlapping pages, and a cursor echo
Disclosure: I built RKMovies, a 1.17 MB Android 4K media player (plus a Windows build). This is a dev diary about the listing layer. Get the Android APK.
The player is a thin WebView over a catalog API. There is no backend of ours for listings, the app calls the upstream /api/filter through its proxy. Getting the grid right taught me three things about that API.
1. channelId is not optional. The routes map to channels: /movies is channelId: 1, /tv-series is 2, /animated-series is 1006 with genre=Anime pinned. Omit the param and the upstream silently defaults the channel, so you get movies and TV mixed together with no error anywhere. Tile links carry tabId=2 / tabId=5 instead of a channel, so the client derives it (tabId=2 becomes channelId=1, tabId=5 becomes 2). One more quirk: the Movies lane never sends year at all, because the upstream year feeds are junk-heavy.
2. Pages overlap, and the real cursor hides one level down. The proxy consumes several upstream pages per client page (30 cards each) because it filters out junk re-uploads server side. It hands back an upstream cursor, pager.upNext, which the client has to echo as up= on every request with page > 1. Skip that and infinite scroll re-serves the same raw pages. And upstream pages overlap by a card or two, so before appending anything we dedupe by detailPath:
const have = new Set(STATE.items.map(x => x.detailPath));
const fresh = items.filter(x => x.detailPath && !have.has(x.detailPath));
STATE.items = STATE.items.concat(fresh);
STATE.upCursor = d.data.pager?.upNext || null;
3. Infinite scroll is just an IntersectionObserver with a loading guard. A sentinel div, rootMargin: '300px' so pages load before you hit the bottom, and a _scrollLoading flag flipped while a fetch is in flight so the observer cannot fire twice.
The filter bar is a custom dropdown component: button plus menu plus a hidden native <select> for semantics, closed on outside click or Escape. The whole listing layer is about 280 lines in parts/app-listing.js with zero dependencies, which is a big part of why the APK stays at 1.17 MB.
If you are building against someone else's undocumented JSON API: send the params they imply but never document, expect overlapping pages, and echo back cursors you do not understand. The real pagination contract usually hides one level down.
Try the player: a sub-2 MB webview player for old phones, install it on Android, or run it on your laptop.
Top comments (0)