Skip to content

feat(info): read show pages from the feed - #1184

Merged
ashwkun merged 15 commits into
masterfrom
feat/feed-first-show-page
Oct 27, 2026
Merged

ashwkun merged 15 commits into
masterfrom
feat/feed-first-show-page

Conversation

@ashwkun

@ashwkun ashwkun commented Oct 27, 2026

Copy link
Copy Markdown
Member

Title

feat(info): read show pages from the feed

Built on top of #1182, which merges first. This PR uses that change's FeedAddressUpdater, feed report and registry address unchanged.

Summary

  • Behind feed_first (off by default, fixed per process), the show page of a show you don't follow reads that show's own RSS feed into a copy stored on the phone. The copy shows straight away on the next visit, and the page reads the rest of the archive when search, oldest-first sorting or scrolling needs it.
  • When the feed can't be read, the page falls back to boxlore's episode list (/v2/episodes) and says so on a status line.
  • With feed_first off, shows you don't follow list /v2/episodes under the IDs stored on this phone. In-show search filters that list on the phone. v1 episodes/search is no longer called, so served IDs never mix with recipe IDs. v1 /episodes remains only as the last fallback.

Motivation

  • Show pages should list what the publisher's feed lists, in the feed's order, under boxlore's own episode IDs, instead of Podcast Index's copy.
  • The failure ladder, offline handling and storage limits keep the page usable when a feed is slow, broken or offline.

What changed

  • :core:catalog
    • PodcastRepository.showPageSource: RSS, FOLLOWED_CATALOG, FEED or SERVER_LIST.
    • getShowPageEpisodes serves the show page only. Smart Queue, Android Auto, Home, downloads and the episode page keep getEpisodesPaginated, which reads a followed show's catalog or /v2/episodes and never the page's stored copy.
    • FeedFirstShowPages runs the failure ladder: read, one retry (not on 404/410), /v2/feed-report (reading a different direct address, or a proxy address through /v2/feed/<id>), then /v2/episodes with a notice.
    • Transport reads are capped at 25 MB, send the copy's ETag, map a 304, and follow a 404 not_proxied answer to its HTTPS fetchUrl.
    • One FeedReportLimiter built in AppContainer is shared with feat(catalog): update feed addresses in one place and check them daily #1182's daily check. The show page leaves 5 of the 20 daily reports for followed shows and never reports a followed show.
    • Playback windows (getEpisodeWindow, getEpisodes) for shows whose catalog isn't ready use the same IDs as the page: the copy around an episode it holds, else /v2/episodes, with v1 last (LEGACY(own-ids)).
  • :core:rss
    • ShowPageFeedCatalog reads the newest 100 items with a streaming prefix reader (FeedPrefixReader, RssFeedClient.fetchNewest) or the whole archive.
    • Show-page writes keep the stored row's ready and unsubscribe TTL. A newest read that skips past stored items marks the archive incomplete again.
    • Daily cleanup of unfollowed copies (30 days, size-capped) runs in the background.
  • :core:database
    • New LocalEpisodeCatalogDao queries for the v39 feed-first columns: setArchiveComplete, setLastOpenedAt, setTtlAndFollowed, seedFollowed, searchLimited, listUnfollowedCopies, deleteCatalogIfUnfollowed. No schema change; Room stays at v39.
  • :core:domain: ShowPageFeedPort.
  • :feature:info
    • PodcastInfoFeedFirst runs reads, the status line and reloads that keep rows, scroll and selection.
    • ShowFeedLogic holds the decisions.
    • ShowFeedStatusLine is a solid surfaceContainerHigh card with a polite live region.
    • On feed-first pages, pull to refresh replaces the "Missing episodes?" chip.
  • Module READMEs updated: app, core/catalog, core/domain, core/rss, core/database, core/downloads, feature/info, and the v2 fixtures README.

Behavior & compatibility

  • feed_first off (default): show pages for shows you don't follow list boxlore's episode list under the IDs already stored on this phone. Followed shows and rss: shows are unchanged.
  • feed_first on, new page states:
    • Cached: no line.
    • Reading the newest: "Checking the feed for new episodes…"
    • Completing the archive: "Loading all episodes from the feed…"
    • Fallback: "Can’t reach this show’s feed. Showing boxlore’s episode list instead."
    • Offline with a saved copy: "You’re offline. Showing saved episodes."
    • Offline without a copy: "You’re offline. Connect to load this show’s episodes."
  • After a fallback, search, sorting and scrolling don't try the feed again until the page is reopened. Pull to refresh still tries it.
  • Rollback (flag on → read → flag off): stored IDs, aliases, owners and the show's saved podcasts row are unchanged, except epNs, which a read may fill once. This path never calls /v2/episodes/register or /v2/shows/feeds and never touches notification registrations. It reads and writes only local_episodes and local_episode_feeds.
  • No change to deep links, rss:/negative IDs, cache keys, worker names, notification topics or the Room schema.

Impact (required)

User impact — pick exactly one

  • user-impact-critical
  • user-impact-high
  • user-impact-medium
  • user-impact-low
  • no-user-impact

Listener impact

What changes in the user’s life:

  • As the feature rolls out, a show you don't follow lists the same episodes, in the same order, as its own feed. Episodes you've opened before appear straight away and work offline. A short line explains when boxlore is checking the feed, loading older episodes, or showing its own list because the feed can't be reached.

Backend

  • backend-change

Release copy (verbatim — highest priority)

CHANGELOG.md (required developer copy)

Added

  • Feed-first show pages behind feed_first: shows you don't follow read their own feed into a stored copy (FeedFirstShowPages, ShowPageFeedCatalog, PodcastInfoFeedFirst), with a failure ladder to /v2/episodes, a status line, and a daily cleanup of unfollowed copies after 30 days.

Changed

  • Show pages of shows you don't follow list /v2/episodes under locally stored IDs when feed_first is off; in-show search filters that list on the phone instead of calling v1 episodes/search. Broken-feed reports share one limiter between the show page and the daily address check.

README What's New / Upcoming

Improvements

  • Show pages can now list episodes straight from a show’s own feed, keep them for offline viewing, and tell you when boxlore is showing its own episode list instead. Rolling out gradually.

Adaptive UI (phone / tablet / foldable)

  • Affected states: the show page's episode list and the new status line, in compact, medium and expanded widths, short landscape, split-screen and resizable windows, foldable flat and tabletop, and activity recreation.
  • Source inspection: the status line is a full-width item above the episodes in the same lazy list. It adds no layout branch per window size, and its state lives in the ViewModel's PodcastInfoUiState, so it survives recreation.
  • JVM tests:
    • PodcastInfoAdaptiveTransitionTest (extended) drives 599↔600, 479↔480, 839↔840 landscape, portrait↔landscape and flat↔tabletop fold both ways, plus StateRestorationTester recreation. It covers the status line in every state, a list that grows (older episodes below, newer above, the archive read) and switches source (stored copy → server list → back) in the same frame as each window change. The scrolled-to row, the selection and the episodes it plays are kept.
    • ShowFeedLogicTest and PodcastInfoFeedFirstTest cover the reload anchor and the status states.
  • Device evidence: none yet; see the manual acceptance path below. Tests at separate static sizes don't prove state survives resizing on a device.

Test plan

  • ./gradlew :feature:info:testDebugUnitTest :core:catalog:testDebugUnitTest :core:rss:testDebugUnitTest :core:downloads:testDebugUnitTest :core:database:testDebugUnitTest :app:testDebugUnitTest :app:compileDebugKotlin detekt
  • New JVM coverage:
    • PodcastRepositoryShowPagesTest: recorded /v2/episodes, /v2/podcast direct and proxy, and /v2/feed not_proxied fixtures through the real repository; only the show page reads the copy; rollback; playback windows under the page's IDs.
    • FeedFirstShowPagesTest: ladder, report to proxy target, followed shows not reported, cleanup not blocking open.
    • FeedReportsTest: shared limiter, reserve.
    • ShowPageFeedCatalogTest: TTL kept, gap marks the archive incomplete, followed seeded, cleanup keeps history, downloads, queue and aliases.
    • Also ShowPageFeedRulesTest, FeedPrefixReaderTest, ShowFeedLogicTest, PodcastInfoFeedFirstTest, AutoDownloadCoordinatorTest.
  • Built / installed on a phone (installDebug)
  • Manual acceptance (feed-first show page): turn feed_first on for the device, restart the app, and open a show you don't follow.
    1. Phone: "Checking the feed for new episodes…" shows, then disappears as the list fills. Scroll past the first 100: "Loading all episodes from the feed…" shows while older episodes appear below, and the list doesn't jump.
    2. Long-press an episode, then rotate and resize split-screen while the archive loads. The selection count and scroll position stay.
    3. On a show opened for the first time, tap Search and type a word. The archive line shows, then the results.
    4. On a long show, sort oldest first. The oldest episode appears after the archive line.
    5. Reopen the show within an hour. The saved list shows with no line, and pull to refresh reads the feed.
    6. Turn on airplane mode and reopen. The saved list shows with "You’re offline. Showing saved episodes." A show never opened before says "You’re offline. Connect to load this show’s episodes."
    7. Open a show whose feed is down. After a moment the line says "Can’t reach this show’s feed. Showing boxlore’s episode list instead.", and episodes still play.
    8. Repeat steps 1 and 2 on a tablet (landscape split at 840dp) and on a foldable (flat ↔ tabletop).
    9. Turn feed_first off. The same show lists its episodes from boxlore's list, and search still finds them.
  • Required checks are green before merge

Notes

  • Not recorded: a /v2/feed-report answer. Recording one would queue a real re-check and use up report quota. Tests use the documented response shape instead.
  • Transport documents (http-only feeds read through boxlore) use one fixed namespace per show. boxlore has never minted IDs from an http address, so no listener IDs are affected.

# Conflicts:
#	app/README.md
#	core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/FeedReports.kt
#	core/catalog/src/test/resources/fixtures/v2/README.md
#	core/rss/src/main/java/cx/aswin/boxlore/core/rss/LocalEpisodeCatalogRefresh.kt
@ashwkun ashwkun added the user-impact-medium Noticeable but not headline — include in README when space allows label Oct 27, 2026
@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 17 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between c8ba3e4 and 119689f.

Files selected for processing: 54
  • app/README.md
  • app/src/main/java/cx/aswin/boxlore/AppContainer.kt
  • core/catalog/README.md
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/FeedFirstShowPages.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/FeedReports.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/PodcastRepository.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/PodcastRepositoryEpisodeLinks.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/PodcastRepositoryNetworkLookups.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/PodcastRepositoryShowPageTransport.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/PodcastRepositoryShowPages.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/RolloutSwitches.kt
  • core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/SubscriptionForegroundSync.kt
  • core/catalog/src/test/java/cx/aswin/boxlore/core/catalog/FeedFirstShowPagesTest.kt
  • core/catalog/src/test/java/cx/aswin/boxlore/core/catalog/FeedReportsTest.kt
  • core/catalog/src/test/java/cx/aswin/boxlore/core/catalog/PodcastRepositoryEpisodeSupplementTest.kt
  • core/catalog/src/test/java/cx/aswin/boxlore/core/catalog/PodcastRepositoryShowPagesTest.kt
  • core/catalog/src/test/resources/fixtures/v2/README.md
  • core/catalog/src/test/resources/fixtures/v2/episodes.json
  • core/catalog/src/test/resources/fixtures/v2/feed-not-proxied.json
  • core/catalog/src/test/resources/fixtures/v2/podcast-direct.json
  • core/catalog/src/test/resources/fixtures/v2/podcast-proxy.json
  • core/database/README.md
  • core/database/src/main/java/cx/aswin/boxlore/core/database/LocalEpisodeCatalogDao.kt
  • core/database/src/main/java/cx/aswin/boxlore/core/database/LocalEpisodeFeedEntity.kt
  • core/domain/README.md
  • core/domain/src/main/java/cx/aswin/boxlore/core/domain/ports/ShowPageFeedPort.kt
  • core/downloads/README.md
  • core/downloads/src/test/java/cx/aswin/boxlore/core/downloads/AutoDownloadCoordinatorTest.kt
  • core/rss/README.md
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/FeedPrefixReader.kt
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/FeedReadFailure.kt
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/LocalEpisodeCatalogReads.kt
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/LocalEpisodeCatalogRefresh.kt
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/LocalEpisodeCatalogRepository.kt
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/RssFeedClient.kt
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/ShowPageFeedCatalog.kt
  • core/rss/src/main/java/cx/aswin/boxlore/core/rss/ShowPageFeedRules.kt
  • core/rss/src/test/java/cx/aswin/boxlore/core/rss/FeedPrefixReaderTest.kt
  • core/rss/src/test/java/cx/aswin/boxlore/core/rss/LocalEpisodeCatalogRepositoryTest.kt
  • core/rss/src/test/java/cx/aswin/boxlore/core/rss/ShowPageFeedCatalogTest.kt
  • core/rss/src/test/java/cx/aswin/boxlore/core/rss/ShowPageFeedRulesTest.kt
  • feature/info/README.md
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/PodcastInfoEpisodeList.kt
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/PodcastInfoFeedFirst.kt
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/PodcastInfoSupplementSupport.kt
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/PodcastInfoUiModels.kt
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/PodcastInfoViewModel.kt
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/components/ShowFeedStatusLine.kt
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/logic/PodcastInfoPullRefreshLogic.kt
  • feature/info/src/main/java/cx/aswin/boxlore/feature/info/logic/ShowFeedLogic.kt
  • feature/info/src/main/res/values/strings.xml
  • feature/info/src/test/java/cx/aswin/boxlore/feature/info/PodcastInfoAdaptiveTransitionTest.kt
  • feature/info/src/test/java/cx/aswin/boxlore/feature/info/PodcastInfoFeedFirstTest.kt
  • feature/info/src/test/java/cx/aswin/boxlore/feature/info/logic/ShowFeedLogicTest.kt
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Base automatically changed from feat/feed-address-updates to master October 27, 2026 18:56
…w-page

# Conflicts:
#	app/README.md
#	app/src/main/java/cx/aswin/boxlore/AppContainer.kt
#	core/catalog/README.md
#	core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/FeedReports.kt
#	core/catalog/src/main/java/cx/aswin/boxlore/core/catalog/SubscriptionForegroundSync.kt
#	core/catalog/src/test/java/cx/aswin/boxlore/core/catalog/FeedReportsTest.kt
#	core/catalog/src/test/resources/fixtures/v2/README.md
#	core/rss/src/main/java/cx/aswin/boxlore/core/rss/LocalEpisodeCatalogRefresh.kt
@ashwkun
ashwkun enabled auto-merge (squash) October 27, 2026 18:58
@sonarqubecloud

Copy link
Copy Markdown

@ashwkun
ashwkun merged commit cdb4e56 into master Oct 27, 2026
10 checks passed
@ashwkun
ashwkun deleted the feat/feed-first-show-page branch October 27, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

user-impact-medium Noticeable but not headline — include in README when space allows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant