Skip to content

v0.0.30

Latest

Choose a tag to compare

@boxlore-master-pusher boxlore-master-pusher released this 01 Nov 22:43
a784a6b

boxlore v0.0.30

Added

  • New folder flow on Subscriptions (NewFolderFlow) with Shows, Genre and Auto, and full-page Edit folder and Auto-organize (BoxloreDialog; framed panel on tablets). Folder pages and drafts are kept in saved state (FolderPagesState). (#1198)
  • AutoOrganizeFolderLook: genre folders made when a show is followed or its genre changes use the look saved on Auto-organize. (#1198)
  • New Episodes swipe actions (add to queue, mark played) and pull to refresh through SubscriptionForegroundSync.refreshNow(), which skips the cooldown. (#1198)
  • subscriptions_grid_view, auto_organize_folder_size and auto_organize_tile_covers preferences. (#1198)
  • Listening history Insights tab (time, listening type, top shows, listening clock, weekdays, month mosaic, records, genres, milestones, Where you listen, New or catching up) and a monthly listening story with Echo (StoryLogic, StoryPlayer, How you listened, records, share card, Echo lines per situation in StoryEchoLines). (#1207)
  • Daily listening summaries keep the longest session and time by output and source (Room v41, ListeningDayTotals); they sync (listeningTotals) and go into backup version 9. (#1207)
  • Feedback conversations: new :core:support module (SupportRepository, local cache, reply topic sync, photo prep), SupportApi, and Feedback list, conversation and sent screens in :feature:settings, with list and conversation side by side on wide windows. (#1228)
  • Signed support requests (RequestProofSigner, RequestProofInterceptor), AccountTokenInterceptor, and retry keys that survive process restarts. (#1228)
  • support_reply push handling with a "Feedback replies" notification channel, the feedback?thread= push route and a reply dot on the top-bar Feedback action (LocalFeedbackReplyWaiting). (#1228)
  • Episode links, pushes and Android Auto items resolve by show: stored row, then trusted alias, then /v2/episode (old Podcast Index IDs included; v1 only when the v2 route can't answer), then one read of the show's feed for needsFeed episodes (EpisodeLinkResolver). (#1181)
  • boxlore://episode/{id}?show= and &show= share deep-link patterns, mapped onto the route's podcastId; Play install referrers keep show. (#1181)
  • Episode share links carry the recipe ID plus &show= and register the episode first (EpisodeSharePreparer); EpisodeShareRegistrationWorker (unique work episode-share-registration-<hash>) retries in the background. (#1181)
  • Episode page "Finding this episode" and "Episode not found" states. (#1181)
  • One-time feed address repair after the update (UpgradeAddressRepair, UpgradeRepairWorker with work name feed-address-upgrade-repair): checks saved addresses through /v2/shows/feeds, swaps confirmed moves through FeedAddressUpdater, re-reads each followed show and re-registers notifications, then reruns the cloud sync identity catch-up once. (#1183)
  • namespace_mismatch_count on background feed_fetch_result events. (#1183)
  • 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. (#1184)
  • English can be switched off for recommendations, search and vibes when every picked language is French, Spanish, German, Portuguese, Russian or Dutch (ContentRegions.ENGLISH_OFF_LANGUAGES, content_english_off, EnglishOffInterceptor, ContentLanguagesSheet); the Worker adds English only where a result runs thin. (#1187)
  • Room v40 (ListeningDataMigration): listening dates on history rows (firstPlayedAt, finishedAt), timed-play details (wallMs, source, output, episodePublishedAt), daily summaries per listening origin, played_shows and listening_days_pushed. (#1201)
  • Cloud sync of listening dates, daily listening and played shows (ListeningSync), and JSON backup v8 with timed listening and played shows (LibraryBackupListening). (#1201)
  • Showcase pages (feature/info/branded/): The Vergecast, Conspiracy Theories and Waveform get show pages in their own style with a "boxlore pick" tag, a show-designed header with a © popup, a latest-episode spotlight and a subscribe moment. The chip row, action buttons and episode list keep their layout. Waveform's page is dark only (BrandedShowPage.darkOnly). (#1204)
  • Showcase pages for Crime Junkie, The Joe Rogan Experience, Rotten Mango and The Daily (branded/crimejunkie, branded/jre, branded/rottenmango, branded/daily), with Flashlight, Third eye, Bada bing and Off the press subscribe moments. (#1219)
  • Shared BrandedTag label tag ("Made for:" with the boxlore mark), BrandedEpisodeActions, MomentHaptic and rememberVariableFont for all Showcase pages. (#1219)
  • Tapping the Explore tab again on the Explore page opens search (selectBottomNavTab, ExploreSearchRequestKey); taps from pages opened in Explore and taps that close a covering tablet detail still return to Explore. (#1220)
  • engagement_prompt_shown, store_review_opened, feedback_sent and explore_surface_impression events. (#1226)
  • Per-control settings_interaction keys for Smart Downloads, automatic downloads and Player appearance. (#1226)
  • AnalyticsEmit.personSetOnce. (#1226)
  • FirstPlayClaim, which sends first_episode_played once per install from the player. (#1226)
  • EntryPointContext, the allowlist for playback event context. (#1226)
  • App-owned NPS survey sheet on Home (NpsSurveyController, NpsSurveySheet) reporting PostHog survey shown / survey sent / survey dismissed to an api survey; wording overridable from the nps-survey-copy flag payload. (#1227)
  • NpsSurveySchedule and the backup-excluded NpsSurveyStore: finish-moment, 3 episodes, 7 days, not the first run, quiet days, 14-day cooldown, 90 days between showings. (#1227)
  • Seven survey-* Echo sequences built from existing clips; boxloreSheetIsCentered() for custom sheets. (#1227)
  • EpisodeListenerStore keeps one listener state per episode across its stored IDs: progress lives on one owner ID, likes and played state are absolute writes across aliases and episode_group_state, and reads are projected, so lists count each episode once. (#1178)
  • FeedAddressDailyCheck checks followed shows' feed addresses once a day through /v2/shows/feeds. FeedReporter reports unreadable feeds through /v2/feed-report. An address is swapped only for a confirmed move and only after the new address reads. (#1182)
  • PodcastNotificationRegistrationWorker re-registers a show's notifications after its address changes and retries until Firebase acknowledges. (#1182)
  • JSON library backup version 7 carries episode identity (episodeIdentity: aliases, group settings, renumbered shows) and restores it without overwriting another episode; OPML exports use each show's publisher feed address only. (#1188)
  • UpgradeGateTest: end-to-end upgrade gate from schemas 36, 37 and 38 through the catalog's first reads. (#1190)
  • Library settings › Storage › Clear saved shows removes every unfollowed show's saved episode list after a confirmation (ShowPageFeedPort.clearUnfollowed); history, downloads, the queue and followed shows stay. (#1192)
  • EpisodeRecipeId in :core:rss and scripts/sync/lib/episode-recipe-id.js compute boxlore episode IDs from (show ID, catalog key), checked against a shared fixture. (#1139)
  • The catalog sync logs why each vector point is stored or deleted; scripts/sync/attribution-report.js summarizes the log. (#1139)
  • turso.text() in the sync's Turso client binds catalog keys as text. (#1141)
  • docs/EPISODE_IDS.md documents episode identity, recipe IDs, collision handling and aliases. (#1145)
  • The catalog sync can create and write the episode book: permanent episode identity rows with recipe and served IDs, written all-or-nothing through turso.atomic(). (#1146)
  • The catalog sync can give chart shows that Podcast Index lags behind their feed's newest episode as their latest episode (R5_MODE, off by default). (#1154)
  • The catalog sync can save each fetched episode list in the episode book and vectorize from it, including episodes only the feed has (WINDOWS_MODE, off by default). (#1156)
  • Catalog sync: bootstrap-book.js --retry-missing maps episode points still marked eid_missing again, and a mapped point drops the mark. (#1161)
  • Catalog sync: stage 3 reports what a per-show check schedule (by each show's pace, with Apple's latest-episode date as a safety net) would have checked, beside the current tiers (CADENCE_MODE=shadow). (#1162)
  • Catalog sync: every episode and show vector records the day its show was last on a chart (charted_at), kept current by the cleanup and set on new vectors; stamp-charted-at.js dates the existing ones. (#1164)
  • Catalog sync: shows that leave the charts can keep their newest 5 episode vectors for 90 days (RETENTION_MODE, count by default), with a size guard that shortens retention and alerts when the index grows past its ceiling. (#1166)
  • Catalog sync: stages 3 and 4 respect a per-run time budget from the runner and record their counts and timings for its usage log. (#1167)
  • Catalog sync: the check-cadence report shows how many new episodes landed on their show's daily, weekly or regular schedule, and how long after release they were found. (#1168)
  • EpisodeRecipeId.showIdOrNull parses stored show IDs with the same canonical rule as the scripts implementation, and EpisodeRecipeId.forNamespace mints recipe IDs with a show's episode namespace. The scripts recipe test now covers the same rejected show IDs as the Kotlin test. (#1169)
  • BoxLoreApiV2 in :core:network: a Retrofit client for every /v2 route (shows and feed addresses, feed documents and reports, episode lookup with a NeedsFeed reply, fallback episode lists, episode registration, search, discovery and briefings). Identity fields are range-checked (V2EpisodeIds, EpisodeItem.v2Identity()), and request bounds match the API's. Feed reads are typed (V2FeedReadTarget), so an http-only show is read only through the API client. PodcastRepository.apiV2 shares the existing cached HTTP client, and the release contract gate now checks both Retrofit interfaces. (#1170)
  • App: feed_first rollout switch (FeedFirstSwitch). It is off without a PostHog answer, keeps the last answer in noBackupFilesDir, stays fixed for the process and records an exposure only for the live answer it uses. Also adds the fail-open kill switches upgrade-repair-off, address-check-off and v2-discovery-off (SavedKillSwitch). They use the last saved answer while PostHog is starting. All four reach :core:catalog as RolloutSwitches on PodcastRepository. (#1172)
  • Analytics events feed_fetch_result (one per show-page feed read, plus one summary per background pass and source) and feed_url_corrected (one per saved feed address swap). Neither carries an address, and http_status is sent only with http_error and gone. Catalog code reports them through the new FeedTelemetryPort, which AppContainer wires to :core:analytics. (#1173)
  • Optional recipe_id declared on every analytics event that carries episode_id, and recipe_ids on home_surface_impression. Optional recipeId and catalogKey on Episode. (#1173)
  • Room v39: episode_aliases, episode_group_state and upgrade_repair_shows, new columns on podcasts, local_episode_feeds, local_episodes and queue_metadata, and MIGRATION_38_39, which records an alias and one owner for every stored episode ID of a Podcast Index show without changing any stored value. (#1175)
  • Episode analytics events carry recipe_id (and recipe_ids on episode impressions) beside the local episode_id, and own_ids_upgrade_completed reports the one-time upgrade repair once per upgraded install. (#1189)
  • refresh-update-feed operation: rewrites the update feed's text from master's README without a build or announcement. (#1213)
  • AnalyticsPolicy: event levels (always, sampled for a fixed 10% of installs, off) with the analytics-policy-v1 PostHog flag as a remote switch (per event, per app version, per person), remembered across launches. (#1223)
  • AnalyticsBeforeSend: PostHog beforeSend hook that applies the levels to every captured event, SDK lifecycle events included, and stamps this build's is_internal / app_environment. (#1223)
  • AnalyticsHelper.reloadFeatureFlags: explicit flag reloads also apply the analytics switch. (#1223)
  • level and monthly_per_install columns in event_glossary.csv, checked by AnalyticsBudgetTest against the code and a monthly per-install budget. (#1223)
  • playback_session_ended: one record per listen with minutes heard from the player position, time taken, end reason, download or stream, speed, controller, output, seeks, wait before sound and buffering; qualified from 60 s heard. (#1224)
  • playback_interrupted: a listen the last run left unfinished because Android ended the app, reported at the next launch. (#1224)
  • listen_id, previously_finished and controlled_by on playback_started; playback errors also go to PostHog error tracking. (#1224)
  • TrackScreenView: one page-view helper for every screen, tab, sheet and dialog, sending screen_viewed (with previous_screen) once per visit for those without a view event of their own; ScreenViewCoverageTest fails when a named destination has none. (#1225)
  • screen and previous_screen on plays started from the app's screens; settings_interaction for account sign-in, sign-out, Sync now and delete; share_content for the monthly story. (#1225)

Changed

  • Subscriptions chips use the Explore Top tab chip style (SubscriptionChip); the sort sheet is rebuilt as Material 3 sections; genre folders show an auto icon instead of "Fills from" text. (#1198)
  • Listening history Timeline tab with Jump back in, search, days or shows and Jump to a day; floating Insights | Timeline switcher; listening sessions kept whole for 400 days (RETENTION_DAYS, was 180). (#1207)
  • Feedback sends a conversation instead of a one-way email; the privacy page and docs/PRIVACY.md describe conversations and replies. (#1228)
  • Cloud sync carries each history and queue row's recipe ID and catalog key, records pulled rows as episode aliases (SyncEpisodeIdentity, cloud_sync provenance), merges and removes queue entries per episode with rich removals, and runs a one-time identity catch-up (sync metadata version 3) that adds identity to older builds' cloud rows without changing their values. (#1179)
  • New-episode pushes match by catalog key and (show, recipe ID) after GUID and enclosure, and open the recipe ID when no local row exists yet. (#1181)
  • Android Auto reads queue, history and downloads across an episode's trusted IDs. (#1181)
  • 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. (#1184)
  • Library settings › Discovery is split into Charts › Country and Recommendations › Languages rows, and changing the country keeps the listener's languages. (#1187)
  • Onboarding after search or a library import asks before suggesting shows: See similar shows or Go to Home (SimilarShowsChoice), with Skip to Home while suggestions load; the similar-shows request runs only when asked for. (#1194)
  • Show page loading states: (#1196)
  • an opening skeleton in the page's shape (ShowPageSkeleton); (#1196)
  • loading episode cards in place of spinners (EpisodeCardSkeleton); (#1196)
  • a plain status line with wavy progress in place of the status card (ShowListStatusLine). (#1196)
  • Full player tile tray uses outlined Material icons for speed, sleep, queue, chapters and transcript; Up next's View queue icon matches and the top bar reads "Now playing". (#1199)
  • artworkPlayerColorScheme in light mode deepens a bright cover's primary (same hue) until its light Play icon reaches 4.5:1 instead of switching the icon to black, and softens near-black text to a deep tone of the cover's hue where it keeps 7:1. (#1199)
  • After subscribing, the catalog refresh shows as a status line in the page body instead of the top-bar pill (PodcastInfoPostSubscribeLogic). (#1204)
  • The show description's open state is saved with the page. (#1204)
  • Every Showcase card uses the regular episode button row with progress; chips are centred; card pictures use the episode's own picture, else the show's cover. (#1219)
  • The More from header on the episode page uses secondaryContainer. (#1219)
  • IIIF image servers (CBC) are asked for the size in the path instead of going through the resizing proxy. (#1219)
  • The Daily's Showcase subscribe moment is Daybreak (DailyDaybreak, DailyDawn), replacing Off the press. (#1222)
  • Crime Junkie's Subscribe uses the show's accent, like Play; BrandedAccentTest checks every Showcase page. (#1222)
  • Notification permission is requested only from a show's bell and the import card. (#1226)
  • feedback_submitted is only the NPS survey trigger. (#1226)
  • The review prompt counts episodes finished since install, uses each milestone once, and waits two days after install or a library import. (#1226)
  • settings_interaction is sent only when a value changes. (#1226)
  • Echo chat analytics cap typed text at 500 characters and drop the reply text. (#1226)
  • PostHog Android SDK 3.45.0 → 3.75.0 (projectToken); PostHog surveys off. (#1227)
  • The milestone review prompt appears only at a finish moment; the NPS promoter review hand-off is removed in favour of the survey's Play Store button. (#1227)
  • New-episode notifications are checked hourly on our own server instead of GitHub Actions, whose schedule ran every few hours. (#1151)
  • Feed parsing records each item's position in the feed document (ParsedFeedItem, ParsedRssFeed.items). When items share a catalog key, the first in the document wins in the parser merge, LocalEpisodeCatalogPersist, StickyRssEpisodeRemap and the legacy RSS upgrade. (#1176)
  • Items without a GUID are keyed on their first playable <enclosure url>. Items that share a first enclosure keep their audio address as the key. Stored rows are also found by their own minted ID and by the audio address older builds used, so no stored episode ID changes. Minting is unchanged. (#1176)
  • Feed-only extras keep the ID of the supplement row already stored for them across refreshes. (#1176)
  • Switching a Podcast Index show to its RSS feed no longer re-keys history, downloads or queue rows. Feed episodes get rss_source_link aliases (trusted for a unique key or an exact audio address), relinked after a backup restore, a re-follow and each feed refresh, and per-show reads use Podcast.listenerShowIds. (#1178)
  • Download state, starting and removing a download, and queue indicators work across an episode's IDs. Queue removals cover every ID of an episode except the playing one and record the episode's identity. (#1178)
  • The orphan rematch after a feed read no longer moves trusted or ambiguous episode IDs. (#1178)
  • Discovery (recommendations, Home bootstrap, trending, semantic search, curated vibes, curiosity, because-you-like, similar episodes, briefing metadata and chapters) uses the /v2 routes. Recipe IDs in answers are translated to the phone's stored IDs (DiscoveryEpisodeIds, RoomDiscoveryEpisodeIds). Seeds and exclusions are sent as alias recipe IDs. Show search and typeahead stay on v1 /search. The v2-discovery-off switch restores the v1 routes. (#1180)
  • FeedAddressUpdater is now the only writer of a show's saved feed address. Every path, including restore, cloud pulls, auto-download and feed moves, resets feed validators, keeps the previous address and re-registers notifications. (#1182)
  • TurnstileStepUpSheet shows Echo (onboarding's asking pose, on its stage) beside a new heading, explains the check in one line and wraps Cloudflare's check in a surfaceContainerHigh card. TurnstileStepUpHostAdaptiveTest checks Echo beside its line and the check inside its card in every presentation. (#1205)
  • compileSdk and targetSdk 37 (Android 17) in every module; the release build and dry run install platforms;android-37.0. (#1216)
  • On Android 17 the full player's Cast button requests ACCESS_LOCAL_NETWORK before its device sheet scans, and opens the system output picker (SystemOutputSwitcherDialogController) when it's declined. (#1216)
  • ApiSessionManager: background App Check upgrades and refreshes wait up to 30 s (ApiSessionTiming.backgroundAppCheckTimeoutMs); launch, expiry and rejection exchanges keep the 6 s limit. (#1221)
  • StickyEpisodeIdentity.catalogKey trims with an explicit character set equal to Kotlin's trim() on Android, so catalog keys no longer depend on the runtime's Unicode tables. (#1139)
  • New chart shows get their newest 5 episodes embedded first and the rest of their window after 7 days on the charts (NEW_ENTRANT_EPISODES, NEW_ENTRANT_DEPTH_DAYS). (#1141)
  • The catalog sync records new episodes in the episode book, names their vectors by served ID, and no longer re-embeds an episode when Podcast Index changes its ID. (#1147)
  • Latest-episode tips in the catalog use each episode's stable ID. (#1152)
  • The weekly tracked-podcast repair fills missing feed addresses from the feed-address registry and replaces addresses the registry has proven dead, only when the saved value is unchanged since it was read. (#1153)
  • The catalog sync calls Podcast Index faster, cools off and slows down when it pushes back, and pauses Podcast Index for the run when trouble continues instead of failing. (#1157)
  • EpisodeRecipeId moved from :core:rss to :core:model without changes, so :core:database can use it. The shared fixture and its test stay in core/rss/src/test. (#1169)
  • The destructive Room fallback now covers only versions 1–11 and downgrades; a missing upgrade path fails to open instead of wiping the database. (#1175)
  • LocalEpisodeCatalogDao.upsertFeed and PodcastDao.upsert keep a stored row's v39 columns; JSON backups leave the new podcast fields out. (#1175)
  • New episodes of followed Podcast Index shows take their recipe ID (LocalEpisodeCatalogPersist.plan, NewEpisodeIds), minted in the show's stored namespace. Stored IDs never change. Evicted episodes reuse their IDs through trusted aliases, and a new item keeps an ID its show's history, queue or downloads already hold when its own mint or a unique listener match proves it is the same episode (ListenerIdAdoption). (#1177)
  • Catalog reads record an episode_aliases row for every ID they write, settle unresolved aliases they can attribute, and store key repairs as alias provenance (CatalogAliasRecords). The collision guard checks aliases, tries the recipe ID before reserving, and reserves IDs from the show ID with an alias. (#1177)
  • Recipe-range episode IDs resolve through /v2/episode and never through the Podcast Index episode route. (#1177)
  • Catalog reads no longer load a Podcast Index list (RefreshRequest.loadPiBaseline removed). The 1,000-item GET episodes/match list now serves only the feed-only extras of Podcast Index shows. (#1177)
  • PodcastInfoViewModel is split into same-package helpers (PodcastInfoShowLoader, PodcastInfoEpisodePaging, PodcastInfoPullRefresh, PodcastInfoPostSubscribeRefresh, PodcastInfoEpisodeActions, PodcastInfoSessionTracker, PodcastEpisodePlaybackStateLogic) sharing one PodcastInfoPageState, bringing it under the 1,000-line source limit with its API and behaviour unchanged. (#1208)
  • The unit suite runs as parallel jobs (static checks, three unit-test shards, merged Kover coverage from the shards, two lint shards) behind the same testDebugUnitTest check; only master saves the Gradle cache, docs-only PRs skip the Android jobs, Release copy applies the impact labels before checking them (Auto-Triage removed), and coderabbit-threads-resolved no longer runs for every finished check. (#1209)
  • Listener notes live in release-notes/upcoming.md, which PRs edit in place; README Upcoming, the update feed's Coming next, What's New and the release announcement are built from it, and release preparation keeps it as release-notes/vX.Y.Z.md. (#1212)
  • The update feed trims long notes by whole lines to the app's limits instead of failing. (#1212)
  • Publication and refresh-latest-artifacts share one job: publication builds the merged release commit (or master's tip with build_from: latest-master) at the same version, checks that commit's own README and CHANGELOG, records the build commit in build-commit.txt, and on a re-run reuses that commit's published files instead of building again. (#1213)
  • release-contracts.yml (Release dry run) runs on every release PR; android-setup installs SDK packages with retries for it and the release build. (#1213)
  • README What's New is rendered from release-notes/vX.Y.Z.md, and refresh_update_feed.py refreshes the update feed's What's new and Coming next. (#1213)
  • Listener notes about the same screen or feature are kept together in release-notes/upcoming.md; the list's rules, the PR template and AGENTS.md say so, and the current list is regrouped. (#1214)
  • The unit suite runs the feature tests in two shards (FEATURES_A and the rest) and lint in three (app, core, features), so the merged coverage job starts sooner; coverage is counted as before. (#1217)
  • Unit-test shards keep Robolectric's Android runtime jars in the Actions cache (per shard, keyed by the Robolectric version and the SDKs tests pin; saved on master only), so a Maven Central download can't fail the suite. (#1218)
  • playback_heartbeat, adaptive_ranking_status, android_auto_browse, session_restore_prompt and Application Backgrounded are off by default; playback_seeked is sampled. (#1223)
  • playback_paused is off by default; playback_buffering is sent only for stalls of 5 s or more; android_auto_connected counts only real car connections; playback_error is sent once per cause per episode until a listen starts. (#1224)
  • Leaving summaries of the show, episode, Explore, Lore and Library pages are screen_left (sampled) instead of their view events; view events fire once per visit. (#1225)

Fixed

  • Subscription sync re-checks a dead saved feed address against GET /podcast once a day per show and reads a new address at once; shows whose own feed failed get their latest episode from /sync. (#1160)
  • A feed that moved for good (301/308 redirects or itunes:new-feed-url, same show title) has its new HTTPS address saved; temporary redirects and rss: shows are never moved. (#1160)
  • Feed read errors are typed (FeedHttpException, NotAFeedException) and classified by FeedReadFailure; PostHog kill switches feed-fix-address-recheck-off, feed-fix-sync-fallback-off, feed-fix-moved-feed-off. (#1160)
  • Feed-episode baselines read up to 1,000 newest episodes from GET episodes/match (match fields only) instead of the oldest 100, with the old page as fallback and a PostHog kill switch (feed-fix-match-baseline-off); a matched newest tip fills empty fields from the feed. (#1159)
  • LocalEpisodeCollisionGuard keeps a local catalog row from replacing another show's episode: a clashing row gets a reserved negative ID, and the legacy RSS upgrade stays on RSS instead. (#1159)
  • PodcastDao.setFeedUrl marks the row dirty, so a corrected feed address syncs. (#1159)
  • A Podcast Index show that answers under a new show ID is recorded in boxlore_renumbered_shows and treated as one show: its follow moves to the new ID with merged settings (synced as a follow plus an unfollow, notifications through the pending registration journal), existing duplicate follows collapse on the next foreground pass, the old show's history, queue and download rows link to the new show's episodes without re-keying any ID, and PodcastInfoScreen counts either follow and takes the resolved show's artwork colours. (#1186)
  • Fixed a crash in the tablet Home hero carousel at the end of the row: ResponsiveHeroCarousel now keeps 1dp of end padding so material3's uncontained carousel never gets an empty end shift range. HeroCarouselEndTest covers tablet, foldable, right-to-left and live resize cases. (#1193)
  • RSS notification cleanup removes only the RTDB feedUrl child instead of the whole tracked_podcasts row, which the rules refuse. A refused delete for a row that does not exist still unsubscribes the FCM topic and clears the journal entry, so RssNotificationSyncWorker no longer retries indefinitely. (#1140)
  • New-episode notifications carry a working feed address for shows that moved, use stable episode IDs, and are never sent twice after a partly failed check. (#1150)
  • A pull from the start no longer restores history rows removed on the device, and push batches read at most 100 dirty rows without descriptions. (#1179)
  • English off now reaches every discovery request: request builders keep a list without English as it is (normalizeKeepingEnglishChoice), and Because you like sends the listener's languages; the languages sheet notes that a smaller pool narrows recommendations. (#1191)
  • Marquee's Artwork and App theme looks redraw covers in vivid ArtworkDuotone pairs (tone-10 shadow, tone-80 highlight) instead of Duotone's dark tones, drop the accent glow, and pick the top bar's words from the cover in every colour choice. Duotone cards are unchanged. (#1195)
  • Show pages with Hide completed episodes on keep loading until unfinished episodes appear (HideCompletedFill), and say "You’ve finished every episode" with Show completed episodes when none are left, instead of going blank. (#1196)
  • Library JSON export reads every stored listening-history row through ListeningHistoryBackupPort.exportStoredHistory (paged ListeningHistoryDao.getAllStoredHistory) instead of the screens' latest 300, so backups keep older listening. (#1200)
  • Listening insights count finished, in-progress and liked episodes across the whole history (ListeningHistoryDao.observeAllStoredHistory) instead of the latest 300. (#1200)
  • The new-episode checker skips a show whose feed hangs (withDeadline, checkEachShow), saves each show's state as it changes (writeStateFile) and requests feeds uncompressed, so a hung run can't make later runs resend the same pushes. (#1202)
  • Queue entries keep their own show: addToQueue, addToQueueNext and same-show continuation fill a missing show from the show the episode was queued from (QueueShowPolicy.withShow), and PlaybackMediaControllerBridge no longer reuses the previous show for an entry of another show. A new queue gives its show-less episodes the show it was started from, and an entry with no show at all plays under "Unknown Podcast" instead of the previous episode's show. (#1203)
  • Smart Queue refill items carry their show ID (boxlore_podcast_id media item extra), and entries rebuilt from a media item take the show title from the album title, then subtitle, then artist, so Android Auto durations and Smart Queue hosts are no longer read as show names. (#1203)
  • A media item transition that the player moved past during its show lookup no longer overwrites the newer transition's state. (#1203)
  • Downloaded episodes keep their show (DownloadedEpisodeEntity.toEpisode), and a queue started under a placeholder show (All Downloads, Home mixes) starts under its first episode's own show (QueueShowPolicy.startShow), so the first session saved for it never takes the placeholder's name. (#1203)
  • Episodes a feed tags as trailers but that run longer than 15 minutes count as full episodes (Episode.isTrailer, Episode.effectiveEpisodeType) in the episode list, type labels, spotlight, release-frequency chip and Smart Queue. (#1204)
  • Pull-to-refresh no longer starts a second feed read while the refresh after subscribing runs, and that refresh's status line stays until the last one ends. (#1204)
  • A finished latest episode hides the Showcase card when completed episodes are hidden; the © popup flips above and scrolls in short windows; subscribe moments follow the system animation speed. (#1219)
  • The Subscribe button no longer sticks at its pressed shape after subscribing. (#1219)
  • Stored episodes carry their saved show's name and cover, so More from shows the show's cover. (#1219)
  • The mini player's shadow moves with the bar when it is swiped to Dismiss. (#1219)
  • m3Shimmer follows a shared ShimmerScope clock that arrives or leaves after the block is attached. (#1219)
  • Timeline's Jump back in folds away instead of leaving the list, so switching Days and Shows no longer jumps the page. (#1220)
  • All in progress scrolls the Timeline to its filters (TimelineScrollTarget.InProgress). (#1220)
  • The Subscribe button shows its pressed shape only while a finger is on it (subscribePressTarget, trackFingerDown), so a press whose end never arrives can't leave it stuck. (#1222)
  • Phone plays were labelled android_auto_* because onAddMediaItems relabelled the app's own setMediaItems. (#1226)
  • Explore For You impressions were sent as explore_recommendation_tapped. (#1226)
  • install_channel was stored nested under a person property named $set_once. (#1226)
  • Media item extras reached playback events, and context could overwrite canonical fields. (#1226)
  • Every new episode or mix sent queue_modified clear. (#1226)
  • Play's (not set) referrer counted as utm. (#1226)
  • PodcastInfoScreen now saves open search and its query, the five dialog and sheet flags, the tag/genre draft, and selected episode IDs with their sort, so they survive activity recreation (fold density changes, process death) and back-stack returns. A recreated page waits for its first page, then looks its selected episodes up again in the saved sort; it drops IDs it cannot find and keeps them all when the lookup fails. Toolbar actions wait until the selection is complete. PodcastInfoViewModel.loadPodcast no longer reloads shows whose page ID differs from the route ID (rss:-linked feeds). Added PodcastInfoAdaptiveTransitionTest, which covers the 600dp width, 480dp height and 840dp landscape gates, orientation and fold transitions, and every dialog exit on compact and tablet hosts. (#1174)
  • An item without a GUID whose enclosure and media:content point at different files is no longer parsed as two episodes. (#1176)
  • A feed crossing 4 MB no longer changes or deletes the IDs of feed-only extras. (#1176)
  • Downloaded Smart Queue picks now play from the downloaded file. (#1178)
  • Home picks, trending and Explore's For you keep what they show when a discovery request fails. The last good /v2 picks are kept across restarts (boxlore_discovery_last_good). (#1180)
  • InstallReferrerManager opens an install link only on a fresh install, so clearing app data no longer reopens the shared page. (#1185)
  • A feed episode whose GUID starts with U+FEFF is no longer added again on every read: feed keys are compared with stored ones as SQLite stores them (EpisodeRecipeId.comparableKey). (#1190)
  • Play builds whose update check fails offer Open Google Play (openPlayListing) with Try again instead of a dead-end error. (#1197)
  • Listening history's New or catching up share (Insights and the monthly story) is taken of listening whose episode release date is known, instead of counting undated episodes as older. Daily summaries keep a tenth total, releaseKnownMs (Room v42, ListeningReleaseKnownMigration), carried by cloud sync (listeningReleaseKnown), backup version 10 and Undo; the split shows once half the period's listening, and at least 30 minutes of it, has a known date. (#1211)
  • Widget updates rejected by Android 17's bitmap memory cap leave the widget's last update in place instead of throwing in the player service (pushWidgetUpdate). (#1216)
  • Feedback panes give back the room the docked compact player frees above the Floating navigation bar (feedbackBottomSpace), so the privacy link and list buttons are no longer cut off or pushed up; FeedbackComposeScreen split for Sonar's complexity limit. (#1229)
  • The catalog sync no longer deletes vectors it later re-embeds: the nightly trim fails closed, shows keep the highest cap they earned, partial episode lists never cause pruning, and vectors from a failed upload are spooled and uploaded by the next run. (#1141)
  • The catalog sync refuses to store objects as [object Object] text: its Turso client rejects them, and the show import and latest-episode update scrub Podcast Index values first. (#1144)
  • EpisodeRecipeId rejects show IDs above 2^53 − 1, matching the scripts implementation. (#1145)
  • The episode book bootstrap no longer fails a show whose episodes a duplicate Podcast Index feed already holds; those episodes are marked with the show that holds them. (#1148)
  • Episodes that Podcast Index renumbers or lists twice keep their original ID for older app builds. (#1149)
  • The catalog import retries chart shows whose Podcast Index lookup failed instead of skipping them for 30 days, and re-checks dump misses after each new dump. (#1155)
  • Catalog sync: index episode_pi_alias by rid, so the episode book's served_show marker check no longer scans every alias inside each book write. (#1158)
  • Catalog sync: stage 3 re-reads its candidate list only when charts refresh, the list is 20 hours old or the core countries change, instead of every run; core-country shows without a relaxed tag no longer count as untagged. (#1163)
  • Catalog sync: the check-cadence report counts only new episodes as found later, not old episodes Podcast Index re-indexed under a new ID. (#1165)
  • LocalEpisodeCollisionGuard compares stored catalog keys after the recipe trim and keeps a stored row's ID when the show already holds the key. A legacy key ending in a newline, tab or no-break space no longer gets a second row, and a refresh never deletes a row through the unique (show, key) index. (#1171)
  • Legacy local catalog rows keyed id:<own ID> or by audio address take the feed's key when the item's own minted ID equals theirs, including on a first read that matched a Podcast Index episode. Rows that find no free ID are logged instead of dropped silently. (#1171)
  • The player's transcript_view_toggles counted a name the player never logged, so it was always 0. (#1225)

Removed

  • SubscriptionFolderDialog, FolderEditSheet and FolderEditComponents. (#1198)
  • BoxcastPostHogSurveysDelegate, NpsSurveyTriggers, NpsSurveyBranching and the app's survey UI; feedback_submitted and trackSurveyNps*. (#1227)
  • coderabbit-threads-resolved workflow and its required check; merging relies on the master ruleset's conversation resolution, which covers every review thread. (#1210)
  • Per-PR README release copy, PR-link requirements, the changelog reconcile and backfill steps, the release-notes source option and the offline release rehearsal. (#1212)
  • maestro-nightly.yml and new-episode-check.yml, both disabled: Maestro flows stay a local check, and the new-episode notifier runs on the VPS. (#1215)