summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-09-29 06:26:00 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-09-29 06:26:00 +0000
commit16a47088a315786a940590794d7ce4003447cf7c (patch)
tree968816a3175164f8f01e69180bf4e18c2e8f9c8d /CLAUDE.md
parentc496ce8d8fb9d91d41cd544800a1f7fabb5d3ca1 (diff)
Start shared timestamps playing, and stop the feed clobbering real lengths
**The hand-off.** Sharing a timestamped YouTube link now opens Listen on the player, already playing at that point. Two things were missing. The timestamp was only honoured on first insert -- the insert ignores a clashing guid, so every share after the first silently did nothing and playback resumed wherever Listen had last left it, which is exactly the going-back- and-forth case. And nothing started playing; the video landed in the library to be found. Re-sharing now moves the position and unplays the episode, because sharing a moment in something you finished is how you go back to it. A share with no timestamp still just adds it quietly -- interrupting whatever is playing would be rude. **A regression in yesterday's duration fix, found by following Peter's own observation.** He noted the 22:05-vs-29:34 gap was probably content versus ads. It is -- and that has two consequences I had missed. The first is that `EpisodeMatcher.mergedWith` rewrites `durationMs` from the feed on every refresh, correctly, because it is feed metadata. Writing the measured length into that column meant the correction survived until the next refresh and then silently reverted. So there are two columns now: `durationMs` stays the feed's claim, `actualDurationMs` is what the player measured, and `playbackDurationMs` prefers the latter. The second is that the wrong number was never only cosmetic. It sizes the decode buffer a full-episode midroll scan allocates -- truncating the scan by minutes on precisely the shows that have midrolls -- places the outro offset, and drives the finished check, which would mark a 29-minute episode played at 21 and advance the queue. Those all read `playbackDurationMs` now. And the third thing, which is Peter's idea and free: the difference between the two numbers is the advertising in the episode, with no audio analysis at all. `insertedAdMs`, shown on the episode screen, and a check on the midroll detector -- whatever it eventually finds should add up to about that much. Measuring it turned up something worth knowing: the same Grass Apes came back 7:29 longer than declared on one fetch and 5:34 on another. Insertion happens per request, so this is a property of a file, not of an episode. Verified on the emulator. Hand-off: cold start into the player at 12:39 from a stored 12:30, and the warm onNewIntent path at 25:14 from 25:00. The YouTube resolver itself cannot be exercised here -- this host gets a 302 bot page for every watch URL, as CLAUDE.md already records -- so the new code was driven through the intent it receives, with the resolver's own behaviour unchanged. Durations: list showed 22m on a fresh subscribe, 27m after playing, with 1325000 and 1659454 stored separately, and the episode screen reading "Ads inserted ~5:34". 550 unit tests, 12 migration tests on device, detekt and lint clean. Coverage 92.96% -> 93.02%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md39
1 files changed, 32 insertions, 7 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 0ce7d44..41809a2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -211,13 +211,38 @@ by default and correct; it is simply waiting for a publisher who marks breaks.
## Domain notes worth keeping
- **A feed's declared duration is often wrong, and not by rounding.** Radiolab publishes Grass
- Apes as `00:22:05`; the file ExoPlayer opens is 29:34. The seven and a half minutes between
- them are dynamically inserted ads -- the feed states the ad-free content length and the CDN
- stitches in the rest -- so the error is large, systematic, and worst on exactly the shows the
- midroll detector cares about. `PositionWriter` replaces the stored value with the player's
- once per episode, on the position tick, via the narrow `PlaybackStateDao`. Once per episode,
- not once per tick: a lookup on every tick for the length of an episode is the per-episode
- work the feed-scale note below warns about.
+ Apes as `00:22:05`; the file served is 27-30 minutes. The gap is dynamically inserted ads --
+ the feed states the ad-free content length and the CDN stitches in the rest -- so the error
+ is large, systematic, and worst on exactly the shows the midroll detector cares about.
+
+ **Two columns, not one.** `durationMs` is the feed's claim and stays the feed's:
+ `EpisodeMatcher.mergedWith` rewrites it on every refresh, correctly, which is why writing the
+ measured length there lasted exactly until the next refresh. `actualDurationMs` is what the
+ player measured and nothing else touches. **Read `playbackDurationMs`**, which prefers the
+ measured one -- the feed's figure otherwise sizes the decode buffer a full-episode scan
+ allocates, places the outro offset, and drives the finished check that would mark a
+ 29-minute episode played at 21.
+
+ `PositionWriter` records it once per episode on the position tick, via the narrow
+ `PlaybackStateDao` -- not once per tick, which would be the per-episode work the feed-scale
+ note below warns about.
+
+- **The difference between the two durations is the advertising**, free and with no audio
+ analysis: `insertedAdMs`, shown on the episode screen. Peter's observation. It is also a
+ check on the midroll detector, which should eventually account for roughly that much.
+ **It is per *file*, not per episode** -- the same Grass Apes measured 7:29 of ads on one
+ fetch and 5:34 on another, because insertion happens per request. Do not treat it as a
+ stable property of an episode, and do not be surprised when a re-download changes the
+ length.
+
+- **A timestamped link shared in starts playing there.** `WatchLink` parses the `t`, the
+ repository stores it as the position, and the share activity hands the episode to
+ `MainActivity.playIntent` so the app opens on the player already playing -- the point being
+ to move between YouTube and Listen without hunting for your place. Re-sharing a video
+ already in the library updates its position and unplays it, which is the case that was
+ broken: the insert ignores a clashing guid, so every share after the first silently did
+ nothing. A share carrying no timestamp still just adds it quietly, since interrupting
+ whatever is playing would be rude.
- **The media notification opens the app**, via `setSessionActivity` on the session. Without it
the transport buttons work and tapping the body does nothing, which reads as breakage rather